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++)); 294my$gitversion='@@GIT_VERSION@@'; 295if($gitversion=~m/..GIT_VERSION../) { 296$gitversion=`git --version`; 297chomp$gitversion; 298# keep only what's after the last space 299$gitversion=~s/^.* //; 300} 301 302my$header="From:$from 303To:$to 304Cc:$cc 305Subject:$subject 306Reply-To:$from 307Date:$date 308Message-Id:$message_id 309X-Mailer: git-send-email$gitversion 310"; 311$header.="In-Reply-To:$reply_to\n"if$reply_to; 312 313$smtp||= Net::SMTP->new($smtp_server); 314$smtp->mail($from)or die$smtp->message; 315$smtp->to(@recipients)or die$smtp->message; 316$smtp->dataor die$smtp->message; 317$smtp->datasend("$header\n$message")or die$smtp->message; 318$smtp->dataend()or die$smtp->message; 319$smtp->okor die"Failed to send$subject\n".$smtp->message; 320 321if($quiet) { 322printf"Sent%s\n",$subject; 323}else{ 324print"OK. Log says: 325Date:$date 326Server:$smtp_serverPort: 25 327From:$from 328Subject:$subject 329Cc:$cc 330To:$to 331 332Result: ",$smtp->code,' ', ($smtp->message=~/\n([^\n]+\n)$/s),"\n"; 333} 334} 335 336$reply_to=$initial_reply_to; 337make_message_id(); 338$subject=$initial_subject; 339 340foreachmy$t(@files) { 341open(F,"<",$t)or die"can't open file$t"; 342 343my$author_not_sender=undef; 344@cc=@initial_cc; 345my$found_mbox=0; 346my$header_done=0; 347$message=""; 348while(<F>) { 349if(!$header_done) { 350$found_mbox=1,next if(/^From /); 351chomp; 352 353if($found_mbox) { 354if(/^Subject:\s+(.*)$/) { 355$subject=$1; 356 357}elsif(/^(Cc|From):\s+(.*)$/) { 358if($2eq$from) { 359next if($suppress_from); 360} 361else{ 362$author_not_sender=$2; 363} 364printf("(mbox) Adding cc:%sfrom line '%s'\n", 365$2,$_)unless$quiet; 366push@cc,$2; 367} 368 369}else{ 370# In the traditional 371# "send lots of email" format, 372# line 1 = cc 373# line 2 = subject 374# So let's support that, too. 375if(@cc==0) { 376printf("(non-mbox) Adding cc:%sfrom line '%s'\n", 377$_,$_)unless$quiet; 378 379push@cc,$_; 380 381}elsif(!defined$subject) { 382$subject=$_; 383} 384} 385 386# A whitespace line will terminate the headers 387if(m/^\s*$/) { 388$header_done=1; 389} 390}else{ 391$message.=$_; 392if(/^Signed-off-by: (.*)$/i&& !$no_signed_off_cc) { 393my$c=$1; 394chomp$c; 395push@cc,$c; 396printf("(sob) Adding cc:%sfrom line '%s'\n", 397$c,$_)unless$quiet; 398} 399} 400} 401close F; 402if(defined$author_not_sender) { 403$message="From:$author_not_sender\n\n$message"; 404} 405 406$cc=join(", ", unique_email_list(@cc)); 407 408 send_message(); 409 410# set up for the next message 411if($chain_reply_to||length($reply_to) ==0) { 412$reply_to=$message_id; 413} 414 make_message_id(); 415} 416 417if($compose) { 418 cleanup_compose_files(); 419} 420 421sub cleanup_compose_files() { 422unlink($compose_filename,$compose_filename.".final"); 423 424} 425 426$smtp->quitif$smtp; 427 428sub unique_email_list(@) { 429my%seen; 430my@emails; 431 432foreachmy$entry(@_) { 433my$clean= extract_valid_address($entry); 434next if$seen{$clean}++; 435push@emails,$entry; 436} 437return@emails; 438}