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