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