1#!/usr/bin/perl -w 2# 3# Copyright 2002,2005 Greg Kroah-Hartman <greg@kroah.com> 4# Copyright 2005 Ryan Anderson <ryan@michonline.com> 5# 6# GPL v2 (See COPYING) 7# 8# Ported to support git "mbox" format files by Ryan Anderson <ryan@michonline.com> 9# 10# Sends a collection of emails to the given email addresses, disturbingly fast. 11# 12# Supports two formats: 13# 1. mbox format files (ignoring most headers and MIME formatting - this is designed for sending patches) 14# 2. The original format support by Greg's script: 15# first line of the message is who to CC, 16# and second line is the subject of the message. 17# 18 19use strict; 20use warnings; 21use Term::ReadLine; 22use Mail::Sendmail qw(sendmail%mailcfg); 23use Getopt::Long; 24use Data::Dumper; 25use Email::Valid; 26 27sub unique_email_list(@); 28sub cleanup_compose_files(); 29 30# Constants (essentially) 31my$compose_filename=".msg.$$"; 32 33# Variables we fill in automatically, or via prompting: 34my(@to,@cc,$initial_reply_to,$initial_subject,@files,$from,$compose); 35 36# Behavior modification variables 37my($chain_reply_to,$smtp_server) = (1,"localhost"); 38 39# Example reply to: 40#$initial_reply_to = ''; #<20050203173208.GA23964@foobar.com>'; 41 42my$term= new Term::ReadLine 'git-send-email'; 43 44# Begin by accumulating all the variables (defined above), that we will end up 45# needing, first, from the command line: 46 47my$rc= GetOptions("from=s"=> \$from, 48"in-reply-to=s"=> \$initial_reply_to, 49"subject=s"=> \$initial_subject, 50"to=s"=> \@to, 51"chain-reply-to!"=> \$chain_reply_to, 52"smtp-server=s"=> \$smtp_server, 53"compose"=> \$compose, 54); 55 56# Now, let's fill any that aren't set in with defaults: 57 58open(GITVAR,"-|","git-var","-l") 59or die"Failed to open pipe from git-var:$!"; 60 61my($author,$committer); 62while(<GITVAR>) { 63chomp; 64my($var,$data) =split/=/,$_,2; 65my@fields=split/\s+/,$data; 66 67my$ident=join(" ",@fields[0...(@fields-3)]); 68 69if($vareq'GIT_AUTHOR_IDENT') { 70$author=$ident; 71}elsif($vareq'GIT_COMMITTER_IDENT') { 72$committer=$ident; 73} 74} 75close(GITVAR); 76 77my$prompting=0; 78if(!defined$from) { 79$from=$author||$committer; 80do{ 81$_=$term->readline("Who should the emails appear to be from? ", 82$from); 83}while(!defined$_); 84 85$from=$_; 86print"Emails will be sent from: ",$from,"\n"; 87$prompting++; 88} 89 90if(!@to) { 91do{ 92$_=$term->readline("Who should the emails be sent to? ", 93""); 94}while(!defined$_); 95my$to=$_; 96push@to,split/,/,$to; 97$prompting++; 98} 99 100if(!defined$initial_subject&&$compose) { 101do{ 102$_=$term->readline("What subject should the emails start with? ", 103$initial_subject); 104}while(!defined$_); 105$initial_subject=$_; 106$prompting++; 107} 108 109if(!defined$initial_reply_to&&$prompting) { 110do{ 111$_=$term->readline("Message-ID to be used as In-Reply-To for the first email? ", 112$initial_reply_to); 113}while(!defined$_); 114 115$initial_reply_to=$_; 116$initial_reply_to=~s/(^\s+|\s+$)//g; 117} 118 119if(!defined$smtp_server) { 120$smtp_server="localhost"; 121} 122 123if($compose) { 124# Note that this does not need to be secure, but we will make a small 125# effort to have it be unique 126open(C,">",$compose_filename) 127or die"Failed to open for writing$compose_filename:$!"; 128print C "From\n"; 129printf C "Subject:%s\n\n",$initial_subject; 130printf C <<EOT; 131GIT: Please enter your email below. 132GIT: Lines beginning in "GIT: " will be removed. 133GIT: Consider including an overall diffstat or table of contents 134GIT: for the patch you are writing. 135 136EOT 137close(C); 138 139my$editor=$ENV{EDITOR}; 140$editor='vi'unlessdefined$editor; 141system($editor,$compose_filename); 142 143open(C2,">",$compose_filename.".final") 144or die"Failed to open$compose_filename.final : ".$!; 145 146open(C,"<",$compose_filename) 147or die"Failed to open$compose_filename: ".$!; 148 149while(<C>) { 150next ifm/^GIT: /; 151print C2 $_; 152} 153close(C); 154close(C2); 155 156do{ 157$_=$term->readline("Send this email? (y|n) "); 158}while(!defined$_); 159 160if(uc substr($_,0,1)ne'Y') { 161 cleanup_compose_files(); 162exit(0); 163} 164 165@files= ($compose_filename.".final"); 166} 167 168 169# Now that all the defaults are set, process the rest of the command line 170# arguments and collect up the files that need to be processed. 171formy$f(@ARGV) { 172if(-d $f) { 173opendir(DH,$f) 174or die"Failed to opendir$f:$!"; 175 176push@files,grep{ -f $_}map{ +$f."/".$_} 177sort readdir(DH); 178 179}elsif(-f $f) { 180push@files,$f; 181 182}else{ 183print STDERR "Skipping$f- not found.\n"; 184} 185} 186 187if(@files) { 188print$_,"\n"for@files; 189}else{ 190print<<EOT; 191git-send-email [options] <file | directory> [... file | directory ] 192Options: 193 --from Specify the "From:" line of the email to be sent. 194 195 --to Specify the primary "To:" line of the email. 196 197 --compose Use \$EDITORto edit an introductory message for the 198 patch series. 199 200 --subject Specify the initial "Subject:" line. 201 Only necessary if --compose is also set. If --compose 202 is not set, this will be prompted for. 203 204 --in-reply-to Specify the first "In-Reply-To:" header line. 205 Only used if --compose is also set. If --compose is not 206 set, this will be prompted for. 207 208 --chain-reply-to If set, the replies will all be to the previous 209 email sent, rather than to the first email sent. 210 Defaults to on. 211 212 --smtp-server If set, specifies the outgoing SMTP server to use. 213 Defaults to localhost. 214 215Error: Please specify a file or a directory on the command line. 216EOT 217exit(1); 218} 219 220# Variables we set as part of the loop over files 221our($message_id,$cc,%mail,$subject,$reply_to,$message); 222 223 224# Usually don't need to change anything below here. 225 226# we make a "fake" message id by taking the current number 227# of seconds since the beginning of Unix time and tacking on 228# a random number to the end, in case we are called quicker than 229# 1 second since the last time we were called. 230 231# We'll setup a template for the message id, using the "from" address: 232my$message_id_from= Email::Valid->address($from); 233my$message_id_template="<%s-git-send-email-$message_id_from>"; 234 235sub make_message_id 236{ 237my$date=`date "+\%s"`; 238chomp($date); 239my$pseudo_rand=int(rand(4200)); 240$message_id=sprintf$message_id_template,"$date$pseudo_rand"; 241#print "new message id = $message_id\n"; # Was useful for debugging 242} 243 244 245 246$cc=""; 247 248sub send_message 249{ 250my$to=join(", ", unique_email_list(@to)); 251 252%mail= ( To =>$to, 253 From =>$from, 254 CC =>$cc, 255 Subject =>$subject, 256 Message =>$message, 257'Reply-to'=>$from, 258'In-Reply-To'=>$reply_to, 259'Message-ID'=>$message_id, 260'X-Mailer'=>"git-send-email", 261); 262 263$mail{smtp} =$smtp_server; 264$mailcfg{mime} =0; 265 266#print Data::Dumper->Dump([\%mail],[qw(*mail)]); 267 268 sendmail(%mail)or die$Mail::Sendmail::error; 269 270print"OK. Log says:\n",$Mail::Sendmail::log; 271print"\n\n" 272} 273 274 275$reply_to=$initial_reply_to; 276make_message_id(); 277$subject=$initial_subject; 278 279foreachmy$t(@files) { 280my$F=$t; 281open(F,"<",$t)or die"can't open file$t"; 282 283@cc= (); 284my$found_mbox=0; 285my$header_done=0; 286$message=""; 287while(<F>) { 288if(!$header_done) { 289$found_mbox=1,next if(/^From /); 290chomp; 291 292if($found_mbox) { 293if(/^Subject:\s+(.*)$/) { 294$subject=$1; 295 296}elsif(/^(Cc|From):\s+(.*)$/) { 297printf("(mbox) Adding cc:%sfrom line '%s'\n", 298$2,$_); 299push@cc,$2; 300} 301 302}else{ 303# In the traditional 304# "send lots of email" format, 305# line 1 = cc 306# line 2 = subject 307# So let's support that, too. 308if(@cc==0) { 309printf("(non-mbox) Adding cc:%sfrom line '%s'\n", 310$_,$_); 311 312push@cc,$_; 313 314}elsif(!defined$subject) { 315$subject=$_; 316} 317} 318 319# A whitespace line will terminate the headers 320if(m/^\s*$/) { 321$header_done=1; 322} 323}else{ 324$message.=$_; 325if(/^Signed-off-by: (.*)$/i) { 326my$c=$1; 327chomp$c; 328push@cc,$c; 329printf("(sob) Adding cc:%sfrom line '%s'\n", 330$c,$_); 331} 332} 333} 334close F; 335 336$cc=join(", ", unique_email_list(@cc)); 337 338 send_message(); 339 340# set up for the next message 341if($chain_reply_to||length($reply_to) ==0) { 342$reply_to=$message_id; 343} 344 make_message_id(); 345} 346 347if($compose) { 348 cleanup_compose_files(); 349} 350 351sub cleanup_compose_files() { 352unlink($compose_filename,$compose_filename.".final"); 353 354} 355 356 357 358sub unique_email_list(@) { 359my%seen; 360my@emails; 361 362foreachmy$entry(@_) { 363my$clean= Email::Valid->address($entry); 364next if$seen{$clean}++; 365push@emails,$entry; 366} 367return@emails; 368}