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