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