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