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