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