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 Getopt::Long; 23use Data::Dumper; 24use Git; 25 26package FakeTerm; 27sub new { 28my($class,$reason) =@_; 29returnbless \$reason,shift; 30} 31subreadline{ 32my$self=shift; 33die"Cannot use readline on FakeTerm:$$self"; 34} 35package main; 36 37# most mail servers generate the Date: header, but not all... 38sub format_2822_time { 39my($time) =@_; 40my@localtm=localtime($time); 41my@gmttm=gmtime($time); 42my$localmin=$localtm[1] +$localtm[2] *60; 43my$gmtmin=$gmttm[1] +$gmttm[2] *60; 44if($localtm[0] !=$gmttm[0]) { 45die"local zone differs from GMT by a non-minute interval\n"; 46} 47if((($gmttm[6] +1) %7) ==$localtm[6]) { 48$localmin+=1440; 49}elsif((($gmttm[6] -1) %7) ==$localtm[6]) { 50$localmin-=1440; 51}elsif($gmttm[6] !=$localtm[6]) { 52die"local time offset greater than or equal to 24 hours\n"; 53} 54my$offset=$localmin-$gmtmin; 55my$offhour=$offset/60; 56my$offmin=abs($offset%60); 57if(abs($offhour) >=24) { 58die("local time offset greater than or equal to 24 hours\n"); 59} 60 61returnsprintf("%s,%2d%s%d%02d:%02d:%02d%s%02d%02d", 62qw(Sun Mon Tue Wed Thu Fri Sat)[$localtm[6]], 63$localtm[3], 64qw(Jan Feb Mar Apr May Jun 65 Jul Aug Sep Oct Nov Dec)[$localtm[4]], 66$localtm[5]+1900, 67$localtm[2], 68$localtm[1], 69$localtm[0], 70($offset>=0) ?'+':'-', 71abs($offhour), 72$offmin, 73); 74} 75 76my$have_email_valid=eval{require Email::Valid;1}; 77my$smtp; 78 79sub unique_email_list(@); 80sub cleanup_compose_files(); 81 82# Constants (essentially) 83my$compose_filename=".msg.$$"; 84 85# Variables we fill in automatically, or via prompting: 86my(@to,@cc,@initial_cc,@bcclist, 87$initial_reply_to,$initial_subject,@files,$from,$compose,$time); 88 89# Behavior modification variables 90my($chain_reply_to,$quiet,$suppress_from,$no_signed_off_cc) = (1,0,0,0); 91my$smtp_server; 92 93# Example reply to: 94#$initial_reply_to = ''; #<20050203173208.GA23964@foobar.com>'; 95 96my$repo= Git->repository(); 97my$term=eval{ 98 new Term::ReadLine 'git-send-email'; 99}; 100if($@) { 101$term= new FakeTerm "$@: going non-interactive"; 102} 103 104# Begin by accumulating all the variables (defined above), that we will end up 105# needing, first, from the command line: 106 107my$rc= GetOptions("from=s"=> \$from, 108"in-reply-to=s"=> \$initial_reply_to, 109"subject=s"=> \$initial_subject, 110"to=s"=> \@to, 111"cc=s"=> \@initial_cc, 112"bcc=s"=> \@bcclist, 113"chain-reply-to!"=> \$chain_reply_to, 114"smtp-server=s"=> \$smtp_server, 115"compose"=> \$compose, 116"quiet"=> \$quiet, 117"suppress-from"=> \$suppress_from, 118"no-signed-off-cc|no-signed-off-by-cc"=> \$no_signed_off_cc, 119); 120 121# Verify the user input 122 123foreachmy$entry(@to) { 124die"Comma in --to entry:$entry'\n"unless$entry!~m/,/; 125} 126 127foreachmy$entry(@initial_cc) { 128die"Comma in --cc entry:$entry'\n"unless$entry!~m/,/; 129} 130 131foreachmy$entry(@bcclist) { 132die"Comma in --bcclist entry:$entry'\n"unless$entry!~m/,/; 133} 134 135# Now, let's fill any that aren't set in with defaults: 136 137my($author) =$repo->ident_person('author'); 138my($committer) =$repo->ident_person('committer'); 139 140my%aliases; 141my@alias_files=$repo->config('sendemail.aliasesfile'); 142my$aliasfiletype=$repo->config('sendemail.aliasfiletype'); 143my%parse_alias= ( 144# multiline formats can be supported in the future 145 mutt =>sub{my$fh=shift;while(<$fh>) { 146if(/^alias\s+(\S+)\s+(.*)$/) { 147my($alias,$addr) = ($1,$2); 148$addr=~s/#.*$//;# mutt allows # comments 149# commas delimit multiple addresses 150$aliases{$alias} = [split(/\s*,\s*/,$addr) ]; 151}}}, 152 mailrc =>sub{my$fh=shift;while(<$fh>) { 153if(/^alias\s+(\S+)\s+(.*)$/) { 154# spaces delimit multiple addresses 155$aliases{$1} = [split(/\s+/,$2) ]; 156}}}, 157 pine =>sub{my$fh=shift;while(<$fh>) { 158if(/^(\S+)\s+(.*)$/) { 159$aliases{$1} = [split(/\s*,\s*/,$2) ]; 160}}}, 161 gnus =>sub{my$fh=shift;while(<$fh>) { 162if(/\(define-mail-alias\s+"(\S+?)"\s+"(\S+?)"\)/) { 163$aliases{$1} = [$2]; 164}}} 165); 166 167if(@alias_filesand$aliasfiletypeand defined$parse_alias{$aliasfiletype}) { 168foreachmy$file(@alias_files) { 169open my$fh,'<',$fileor die"opening$file:$!\n"; 170$parse_alias{$aliasfiletype}->($fh); 171close$fh; 172} 173} 174 175my$prompting=0; 176if(!defined$from) { 177$from=$author||$committer; 178do{ 179$_=$term->readline("Who should the emails appear to be from? ", 180$from); 181}while(!defined$_); 182 183$from=$_; 184print"Emails will be sent from: ",$from,"\n"; 185$prompting++; 186} 187 188if(!@to) { 189do{ 190$_=$term->readline("Who should the emails be sent to? ", 191""); 192}while(!defined$_); 193my$to=$_; 194push@to,split/,/,$to; 195$prompting++; 196} 197 198sub expand_aliases { 199my@cur=@_; 200my@last; 201do{ 202@last=@cur; 203@cur=map{$aliases{$_} ? @{$aliases{$_}} :$_}@last; 204}while(join(',',@cur)ne join(',',@last)); 205return@cur; 206} 207 208@to= expand_aliases(@to); 209@initial_cc= expand_aliases(@initial_cc); 210@bcclist= expand_aliases(@bcclist); 211 212if(!defined$initial_subject&&$compose) { 213do{ 214$_=$term->readline("What subject should the emails start with? ", 215$initial_subject); 216}while(!defined$_); 217$initial_subject=$_; 218$prompting++; 219} 220 221if(!defined$initial_reply_to&&$prompting) { 222do{ 223$_=$term->readline("Message-ID to be used as In-Reply-To for the first email? ", 224$initial_reply_to); 225}while(!defined$_); 226 227$initial_reply_to=$_; 228$initial_reply_to=~s/(^\s+|\s+$)//g; 229} 230 231if(!$smtp_server) { 232foreach(qw( /usr/sbin/sendmail /usr/lib/sendmail )) { 233if(-x $_) { 234$smtp_server=$_; 235last; 236} 237} 238$smtp_server||='localhost';# could be 127.0.0.1, too... *shrug* 239} 240 241if($compose) { 242# Note that this does not need to be secure, but we will make a small 243# effort to have it be unique 244open(C,">",$compose_filename) 245or die"Failed to open for writing$compose_filename:$!"; 246print C "From$from# This line is ignored.\n"; 247printf C "Subject:%s\n\n",$initial_subject; 248printf C <<EOT; 249GIT: Please enter your email below. 250GIT: Lines beginning in "GIT: " will be removed. 251GIT: Consider including an overall diffstat or table of contents 252GIT: for the patch you are writing. 253 254EOT 255close(C); 256 257my$editor=$ENV{EDITOR}; 258$editor='vi'unlessdefined$editor; 259system($editor,$compose_filename); 260 261open(C2,">",$compose_filename.".final") 262or die"Failed to open$compose_filename.final : ".$!; 263 264open(C,"<",$compose_filename) 265or die"Failed to open$compose_filename: ".$!; 266 267while(<C>) { 268next ifm/^GIT: /; 269print C2 $_; 270} 271close(C); 272close(C2); 273 274do{ 275$_=$term->readline("Send this email? (y|n) "); 276}while(!defined$_); 277 278if(uc substr($_,0,1)ne'Y') { 279 cleanup_compose_files(); 280exit(0); 281} 282 283@files= ($compose_filename.".final"); 284} 285 286 287# Now that all the defaults are set, process the rest of the command line 288# arguments and collect up the files that need to be processed. 289formy$f(@ARGV) { 290if(-d $f) { 291opendir(DH,$f) 292or die"Failed to opendir$f:$!"; 293 294push@files,grep{ -f $_}map{ +$f."/".$_} 295sort readdir(DH); 296 297}elsif(-f $f) { 298push@files,$f; 299 300}else{ 301print STDERR "Skipping$f- not found.\n"; 302} 303} 304 305if(@files) { 306unless($quiet) { 307print$_,"\n"for(@files); 308} 309}else{ 310print<<EOT; 311git-send-email [options] <file | directory> [... file | directory ] 312Options: 313 --from Specify the "From:" line of the email to be sent. 314 315 --to Specify the primary "To:" line of the email. 316 317 --cc Specify an initial "Cc:" list for the entire series 318 of emails. 319 320 --bcc Specify a list of email addresses that should be Bcc: 321 on all the emails. 322 323 --compose Use \$EDITORto edit an introductory message for the 324 patch series. 325 326 --subject Specify the initial "Subject:" line. 327 Only necessary if --compose is also set. If --compose 328 is not set, this will be prompted for. 329 330 --in-reply-to Specify the first "In-Reply-To:" header line. 331 Only used if --compose is also set. If --compose is not 332 set, this will be prompted for. 333 334 --chain-reply-to If set, the replies will all be to the previous 335 email sent, rather than to the first email sent. 336 Defaults to on. 337 338 --no-signed-off-cc Suppress the automatic addition of email addresses 339 that appear in a Signed-off-by: line, to the cc: list. 340 Note: Using this option is not recommended. 341 342 --smtp-server If set, specifies the outgoing SMTP server to use. 343 Defaults to localhost. 344 345 --suppress-from Suppress sending emails to yourself if your address 346 appears in a From: line. 347 348 --quiet Make git-send-email less verbose. One line per email should be 349 all that is output. 350 351Error: Please specify a file or a directory on the command line. 352EOT 353exit(1); 354} 355 356# Variables we set as part of the loop over files 357our($message_id,$cc,%mail,$subject,$reply_to,$references,$message); 358 359sub extract_valid_address { 360my$address=shift; 361my$local_part_regexp='[^<>"\s@]+'; 362my$domain_regexp='[^.<>"\s@]+(?:\.[^.<>"\s@]+)+'; 363 364# check for a local address: 365return$addressif($address=~/^($local_part_regexp)$/); 366 367if($have_email_valid) { 368returnscalar Email::Valid->address($address); 369}else{ 370# less robust/correct than the monster regexp in Email::Valid, 371# but still does a 99% job, and one less dependency 372$address=~/($local_part_regexp\@$domain_regexp)/; 373return$1; 374} 375} 376 377# Usually don't need to change anything below here. 378 379# we make a "fake" message id by taking the current number 380# of seconds since the beginning of Unix time and tacking on 381# a random number to the end, in case we are called quicker than 382# 1 second since the last time we were called. 383 384# We'll setup a template for the message id, using the "from" address: 385my$message_id_from= extract_valid_address($from); 386my$message_id_template="<%s-git-send-email-$message_id_from>"; 387 388sub make_message_id 389{ 390my$date=time; 391my$pseudo_rand=int(rand(4200)); 392$message_id=sprintf$message_id_template,"$date$pseudo_rand"; 393#print "new message id = $message_id\n"; # Was useful for debugging 394} 395 396 397 398$cc=""; 399$time=time-scalar$#files; 400 401sub send_message 402{ 403my@recipients= unique_email_list(@to); 404my$to=join(",\n\t",@recipients); 405@recipients= unique_email_list(@recipients,@cc,@bcclist); 406my$date= format_2822_time($time++); 407my$gitversion='@@GIT_VERSION@@'; 408if($gitversion=~m/..GIT_VERSION../) { 409$gitversion= Git::version(); 410} 411 412my$header="From:$from 413To:$to 414Cc:$cc 415Subject:$subject 416Date:$date 417Message-Id:$message_id 418X-Mailer: git-send-email$gitversion 419"; 420if($reply_to) { 421 422$header.="In-Reply-To:$reply_to\n"; 423$header.="References:$references\n"; 424} 425 426if($smtp_server=~ m#^/#) { 427my$pid=open my$sm,'|-'; 428defined$pidor die$!; 429if(!$pid) { 430exec($smtp_server,'-i', 431map{ extract_valid_address($_) } 432@recipients)or die$!; 433} 434print$sm"$header\n$message"; 435close$smor die$?; 436}else{ 437require Net::SMTP; 438$smtp||= Net::SMTP->new($smtp_server); 439$smtp->mail($from)or die$smtp->message; 440$smtp->to(@recipients)or die$smtp->message; 441$smtp->dataor die$smtp->message; 442$smtp->datasend("$header\n$message")or die$smtp->message; 443$smtp->dataend()or die$smtp->message; 444$smtp->okor die"Failed to send$subject\n".$smtp->message; 445} 446if($quiet) { 447printf"Sent%s\n",$subject; 448}else{ 449print"OK. Log says:\nDate:$date\n"; 450if($smtp) { 451print"Server:$smtp_server\n"; 452}else{ 453print"Sendmail:$smtp_server\n"; 454} 455print"From:$from\nSubject:$subject\nCc:$cc\nTo:$to\n\n"; 456if($smtp) { 457print"Result: ",$smtp->code,' ', 458($smtp->message=~/\n([^\n]+\n)$/s),"\n"; 459}else{ 460print"Result: OK\n"; 461} 462} 463} 464 465$reply_to=$initial_reply_to; 466$references=$initial_reply_to||''; 467make_message_id(); 468$subject=$initial_subject; 469 470foreachmy$t(@files) { 471open(F,"<",$t)or die"can't open file$t"; 472 473my$author_not_sender=undef; 474@cc=@initial_cc; 475my$found_mbox=0; 476my$header_done=0; 477$message=""; 478while(<F>) { 479if(!$header_done) { 480$found_mbox=1,next if(/^From /); 481chomp; 482 483if($found_mbox) { 484if(/^Subject:\s+(.*)$/) { 485$subject=$1; 486 487}elsif(/^(Cc|From):\s+(.*)$/) { 488if($2eq$from) { 489next if($suppress_from); 490} 491elsif($1eq'From') { 492$author_not_sender=$2; 493} 494printf("(mbox) Adding cc:%sfrom line '%s'\n", 495$2,$_)unless$quiet; 496push@cc,$2; 497} 498 499}else{ 500# In the traditional 501# "send lots of email" format, 502# line 1 = cc 503# line 2 = subject 504# So let's support that, too. 505if(@cc==0) { 506printf("(non-mbox) Adding cc:%sfrom line '%s'\n", 507$_,$_)unless$quiet; 508 509push@cc,$_; 510 511}elsif(!defined$subject) { 512$subject=$_; 513} 514} 515 516# A whitespace line will terminate the headers 517if(m/^\s*$/) { 518$header_done=1; 519} 520}else{ 521$message.=$_; 522if(/^Signed-off-by: (.*)$/i&& !$no_signed_off_cc) { 523my$c=$1; 524chomp$c; 525push@cc,$c; 526printf("(sob) Adding cc:%sfrom line '%s'\n", 527$c,$_)unless$quiet; 528} 529} 530} 531close F; 532if(defined$author_not_sender) { 533$message="From:$author_not_sender\n\n$message"; 534} 535 536$cc=join(", ", unique_email_list(@cc)); 537 538 send_message(); 539 540# set up for the next message 541if($chain_reply_to||length($reply_to) ==0) { 542$reply_to=$message_id; 543if(length$references>0) { 544$references.="$message_id"; 545}else{ 546$references="$message_id"; 547} 548} 549 make_message_id(); 550} 551 552if($compose) { 553 cleanup_compose_files(); 554} 555 556sub cleanup_compose_files() { 557unlink($compose_filename,$compose_filename.".final"); 558 559} 560 561$smtp->quitif$smtp; 562 563sub unique_email_list(@) { 564my%seen; 565my@emails; 566 567foreachmy$entry(@_) { 568if(my$clean= extract_valid_address($entry)) { 569$seen{$clean} ||=0; 570next if$seen{$clean}++; 571push@emails,$entry; 572}else{ 573print STDERR "W: unable to extract a valid address", 574" from:$entry\n"; 575} 576} 577return@emails; 578}