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