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 Term::ANSIColor; 25use Git; 26 27$SIG{INT} =sub{print color("reset"),"\n";exit}; 28 29package FakeTerm; 30sub new { 31my($class,$reason) =@_; 32returnbless \$reason,shift; 33} 34subreadline{ 35my$self=shift; 36die"Cannot use readline on FakeTerm:$$self"; 37} 38package main; 39 40 41sub usage { 42print<<EOT; 43git-send-email [options] <file | directory>... 44Options: 45 --from Specify the "From:" line of the email to be sent. 46 47 --to Specify the primary "To:" line of the email. 48 49 --cc Specify an initial "Cc:" list for the entire series 50 of emails. 51 52 --cc-cmd Specify a command to execute per file which adds 53 per file specific cc address entries 54 55 --bcc Specify a list of email addresses that should be Bcc: 56 on all the emails. 57 58 --compose Use \$GIT_EDITOR, core.editor, \$EDITOR, or \$VISUALto edit 59 an introductory message for the patch series. 60 61 --subject Specify the initial "Subject:" line. 62 Only necessary if --compose is also set. If --compose 63 is not set, this will be prompted for. 64 65 --in-reply-to Specify the first "In-Reply-To:" header line. 66 Only used if --compose is also set. If --compose is not 67 set, this will be prompted for. 68 69 --chain-reply-to If set, the replies will all be to the previous 70 email sent, rather than to the first email sent. 71 Defaults to on. 72 73 --signed-off-cc Automatically add email addresses that appear in 74 Signed-off-by: or Cc: lines to the cc: list. Defaults to on. 75 76 --identity The configuration identity, a subsection to prioritise over 77 the default section. 78 79 --smtp-server If set, specifies the outgoing SMTP server to use. 80 Defaults to localhost. Port number can be specified here with 81 hostname:port format or by using --smtp-server-port option. 82 83 --smtp-server-port Specify a port on the outgoing SMTP server to connect to. 84 85 --smtp-user The username for SMTP-AUTH. 86 87 --smtp-pass The password for SMTP-AUTH. 88 89 --smtp-ssl If set, connects to the SMTP server using SSL. 90 91 --suppress-from Suppress sending emails to yourself. Defaults to off. 92 93 --thread Specify that the "In-Reply-To:" header should be set on all 94 emails. Defaults to on. 95 96 --quiet Make git-send-email less verbose. One line per email 97 should be all that is output. 98 99 --dry-run Do everything except actually send the emails. 100 101 --envelope-sender Specify the envelope sender used to send the emails. 102 103EOT 104exit(1); 105} 106 107# most mail servers generate the Date: header, but not all... 108sub format_2822_time { 109my($time) =@_; 110my@localtm=localtime($time); 111my@gmttm=gmtime($time); 112my$localmin=$localtm[1] +$localtm[2] *60; 113my$gmtmin=$gmttm[1] +$gmttm[2] *60; 114if($localtm[0] !=$gmttm[0]) { 115die"local zone differs from GMT by a non-minute interval\n"; 116} 117if((($gmttm[6] +1) %7) ==$localtm[6]) { 118$localmin+=1440; 119}elsif((($gmttm[6] -1) %7) ==$localtm[6]) { 120$localmin-=1440; 121}elsif($gmttm[6] !=$localtm[6]) { 122die"local time offset greater than or equal to 24 hours\n"; 123} 124my$offset=$localmin-$gmtmin; 125my$offhour=$offset/60; 126my$offmin=abs($offset%60); 127if(abs($offhour) >=24) { 128die("local time offset greater than or equal to 24 hours\n"); 129} 130 131returnsprintf("%s,%2d%s%d%02d:%02d:%02d%s%02d%02d", 132qw(Sun Mon Tue Wed Thu Fri Sat)[$localtm[6]], 133$localtm[3], 134qw(Jan Feb Mar Apr May Jun 135 Jul Aug Sep Oct Nov Dec)[$localtm[4]], 136$localtm[5]+1900, 137$localtm[2], 138$localtm[1], 139$localtm[0], 140($offset>=0) ?'+':'-', 141abs($offhour), 142$offmin, 143); 144} 145 146my$have_email_valid=eval{require Email::Valid;1}; 147my$smtp; 148my$auth; 149 150sub unique_email_list(@); 151sub cleanup_compose_files(); 152 153# Constants (essentially) 154my$compose_filename=".msg.$$"; 155 156# Variables we fill in automatically, or via prompting: 157my(@to,@cc,@initial_cc,@bcclist,@xh, 158$initial_reply_to,$initial_subject,@files,$author,$sender,$compose,$time); 159 160my$envelope_sender; 161 162# Example reply to: 163#$initial_reply_to = ''; #<20050203173208.GA23964@foobar.com>'; 164 165my$repo= Git->repository(); 166my$term=eval{ 167 new Term::ReadLine 'git-send-email'; 168}; 169if($@) { 170$term= new FakeTerm "$@: going non-interactive"; 171} 172 173# Behavior modification variables 174my($quiet,$dry_run) = (0,0); 175 176# Variables with corresponding config settings 177my($thread,$chain_reply_to,$suppress_from,$signed_off_cc,$cc_cmd); 178my($smtp_server,$smtp_server_port,$smtp_authuser,$smtp_authpass,$smtp_ssl); 179my($identity,$aliasfiletype,@alias_files,@smtp_host_parts); 180 181my%config_bool_settings= ( 182"thread"=> [\$thread,1], 183"chainreplyto"=> [\$chain_reply_to,1], 184"suppressfrom"=> [\$suppress_from,0], 185"signedoffcc"=> [\$signed_off_cc,1], 186"smtpssl"=> [\$smtp_ssl,0], 187); 188 189my%config_settings= ( 190"smtpserver"=> \$smtp_server, 191"smtpserverport"=> \$smtp_server_port, 192"smtpuser"=> \$smtp_authuser, 193"smtppass"=> \$smtp_authpass, 194"to"=> \@to, 195"cccmd"=> \$cc_cmd, 196"aliasfiletype"=> \$aliasfiletype, 197"bcc"=> \@bcclist, 198"aliasesfile"=> \@alias_files, 199); 200 201# Begin by accumulating all the variables (defined above), that we will end up 202# needing, first, from the command line: 203 204my$rc= GetOptions("sender|from=s"=> \$sender, 205"in-reply-to=s"=> \$initial_reply_to, 206"subject=s"=> \$initial_subject, 207"to=s"=> \@to, 208"cc=s"=> \@initial_cc, 209"bcc=s"=> \@bcclist, 210"chain-reply-to!"=> \$chain_reply_to, 211"smtp-server=s"=> \$smtp_server, 212"smtp-server-port=s"=> \$smtp_server_port, 213"smtp-user=s"=> \$smtp_authuser, 214"smtp-pass=s"=> \$smtp_authpass, 215"smtp-ssl!"=> \$smtp_ssl, 216"identity=s"=> \$identity, 217"compose"=> \$compose, 218"quiet"=> \$quiet, 219"cc-cmd=s"=> \$cc_cmd, 220"suppress-from!"=> \$suppress_from, 221"signed-off-cc|signed-off-by-cc!"=> \$signed_off_cc, 222"dry-run"=> \$dry_run, 223"envelope-sender=s"=> \$envelope_sender, 224"thread!"=> \$thread, 225); 226 227unless($rc) { 228 usage(); 229} 230 231# Now, let's fill any that aren't set in with defaults: 232 233sub read_config { 234my($prefix) =@_; 235 236foreachmy$setting(keys%config_bool_settings) { 237my$target=$config_bool_settings{$setting}->[0]; 238$$target=$repo->config_bool("$prefix.$setting")unless(defined$$target); 239} 240 241foreachmy$setting(keys%config_settings) { 242my$target=$config_settings{$setting}; 243if(ref($target)eq"ARRAY") { 244unless(@$target) { 245my@values=$repo->config("$prefix.$setting"); 246@$target=@valuesif(@values&&defined$values[0]); 247} 248} 249else{ 250$$target=$repo->config("$prefix.$setting")unless(defined$$target); 251} 252} 253} 254 255# read configuration from [sendemail "$identity"], fall back on [sendemail] 256$identity=$repo->config("sendemail.identity")unless(defined$identity); 257read_config("sendemail.$identity")if(defined$identity); 258read_config("sendemail"); 259 260# fall back on builtin bool defaults 261foreachmy$setting(values%config_bool_settings) { 262${$setting->[0]} =$setting->[1]unless(defined(${$setting->[0]})); 263} 264 265my($repoauthor) =$repo->ident_person('author'); 266my($repocommitter) =$repo->ident_person('committer'); 267 268# Verify the user input 269 270foreachmy$entry(@to) { 271die"Comma in --to entry:$entry'\n"unless$entry!~m/,/; 272} 273 274foreachmy$entry(@initial_cc) { 275die"Comma in --cc entry:$entry'\n"unless$entry!~m/,/; 276} 277 278foreachmy$entry(@bcclist) { 279die"Comma in --bcclist entry:$entry'\n"unless$entry!~m/,/; 280} 281 282my%aliases; 283my%parse_alias= ( 284# multiline formats can be supported in the future 285 mutt =>sub{my$fh=shift;while(<$fh>) { 286if(/^\s*alias\s+(\S+)\s+(.*)$/) { 287my($alias,$addr) = ($1,$2); 288$addr=~s/#.*$//;# mutt allows # comments 289# commas delimit multiple addresses 290$aliases{$alias} = [split(/\s*,\s*/,$addr) ]; 291}}}, 292 mailrc =>sub{my$fh=shift;while(<$fh>) { 293if(/^alias\s+(\S+)\s+(.*)$/) { 294# spaces delimit multiple addresses 295$aliases{$1} = [split(/\s+/,$2) ]; 296}}}, 297 pine =>sub{my$fh=shift;while(<$fh>) { 298if(/^(\S+)\t.*\t(.*)$/) { 299$aliases{$1} = [split(/\s*,\s*/,$2) ]; 300}}}, 301 gnus =>sub{my$fh=shift;while(<$fh>) { 302if(/\(define-mail-alias\s+"(\S+?)"\s+"(\S+?)"\)/) { 303$aliases{$1} = [$2]; 304}}} 305); 306 307if(@alias_filesand$aliasfiletypeand defined$parse_alias{$aliasfiletype}) { 308foreachmy$file(@alias_files) { 309open my$fh,'<',$fileor die"opening$file:$!\n"; 310$parse_alias{$aliasfiletype}->($fh); 311close$fh; 312} 313} 314 315($sender) = expand_aliases($sender)ifdefined$sender; 316 317# Now that all the defaults are set, process the rest of the command line 318# arguments and collect up the files that need to be processed. 319formy$f(@ARGV) { 320if(-d $f) { 321opendir(DH,$f) 322or die"Failed to opendir$f:$!"; 323 324push@files,grep{ -f $_}map{ +$f."/".$_} 325sort readdir(DH); 326 327}elsif(-f $f) { 328push@files,$f; 329 330}else{ 331print STDERR "Skipping$f- not found.\n"; 332} 333} 334 335foreachmy$f(@files) { 336my$error= validate_patch($f); 337$errorand die"fatal:$f:$error\nwarning: no patches were sent\n"; 338} 339 340if(@files) { 341unless($quiet) { 342print$_,"\n"for(@files); 343} 344}else{ 345print STDERR "\nNo patch files specified!\n\n"; 346 usage(); 347} 348 349my$prompting=0; 350if(!defined$sender) { 351$sender=$repoauthor||$repocommitter; 352do{ 353$_=$term->readline("Who should the emails appear to be from? [$sender] "); 354}while(!defined$_); 355 356$sender=$_if($_); 357print"Emails will be sent from: ",$sender,"\n"; 358$prompting++; 359} 360 361if(!@to) { 362do{ 363$_=$term->readline("Who should the emails be sent to? ", 364""); 365}while(!defined$_); 366my$to=$_; 367push@to,split/,/,$to; 368$prompting++; 369} 370 371sub expand_aliases { 372my@cur=@_; 373my@last; 374do{ 375@last=@cur; 376@cur=map{$aliases{$_} ? @{$aliases{$_}} :$_}@last; 377}while(join(',',@cur)ne join(',',@last)); 378return@cur; 379} 380 381@to= expand_aliases(@to); 382@to= (map{ sanitize_address($_) }@to); 383@initial_cc= expand_aliases(@initial_cc); 384@bcclist= expand_aliases(@bcclist); 385 386if(!defined$initial_subject&&$compose) { 387do{ 388$_=$term->readline("What subject should the initial email start with? ", 389$initial_subject); 390}while(!defined$_); 391$initial_subject=$_; 392$prompting++; 393} 394 395if($thread&& !defined$initial_reply_to&&$prompting) { 396do{ 397$_=$term->readline("Message-ID to be used as In-Reply-To for the first email? ", 398$initial_reply_to); 399}while(!defined$_); 400 401$initial_reply_to=$_; 402} 403if(defined$initial_reply_to&&$_ne"") { 404$initial_reply_to=~s/^\s*<?/</; 405$initial_reply_to=~s/>?\s*$/>/; 406} 407 408if(!defined$smtp_server) { 409foreach(qw( /usr/sbin/sendmail /usr/lib/sendmail )) { 410if(-x $_) { 411$smtp_server=$_; 412last; 413} 414} 415$smtp_server||='localhost';# could be 127.0.0.1, too... *shrug* 416} 417 418if($compose) { 419# Note that this does not need to be secure, but we will make a small 420# effort to have it be unique 421open(C,">",$compose_filename) 422or die"Failed to open for writing$compose_filename:$!"; 423print C "From$sender# This line is ignored.\n"; 424printf C "Subject:%s\n\n",$initial_subject; 425printf C <<EOT; 426GIT: Please enter your email below. 427GIT: Lines beginning in "GIT: " will be removed. 428GIT: Consider including an overall diffstat or table of contents 429GIT: for the patch you are writing. 430 431EOT 432close(C); 433 434my$editor=$ENV{GIT_EDITOR} ||$repo->config("core.editor") ||$ENV{VISUAL} ||$ENV{EDITOR} ||"vi"; 435system('sh','-c','$0$@',$editor,$compose_filename); 436 437open(C2,">",$compose_filename.".final") 438or die"Failed to open$compose_filename.final : ".$!; 439 440open(C,"<",$compose_filename) 441or die"Failed to open$compose_filename: ".$!; 442 443while(<C>) { 444next ifm/^GIT: /; 445print C2 $_; 446} 447close(C); 448close(C2); 449 450do{ 451$_=$term->readline("Send this email? (y|n) "); 452}while(!defined$_); 453 454if(uc substr($_,0,1)ne'Y') { 455 cleanup_compose_files(); 456exit(0); 457} 458 459@files= ($compose_filename.".final"); 460} 461 462# Variables we set as part of the loop over files 463our($message_id,%mail,$subject,$reply_to,$references,$message); 464 465sub extract_valid_address { 466my$address=shift; 467my$local_part_regexp='[^<>"\s@]+'; 468my$domain_regexp='[^.<>"\s@]+(?:\.[^.<>"\s@]+)+'; 469 470# check for a local address: 471return$addressif($address=~/^($local_part_regexp)$/); 472 473$address=~s/^\s*<(.*)>\s*$/$1/; 474if($have_email_valid) { 475returnscalar Email::Valid->address($address); 476}else{ 477# less robust/correct than the monster regexp in Email::Valid, 478# but still does a 99% job, and one less dependency 479$address=~/($local_part_regexp\@$domain_regexp)/; 480return$1; 481} 482} 483 484# Usually don't need to change anything below here. 485 486# we make a "fake" message id by taking the current number 487# of seconds since the beginning of Unix time and tacking on 488# a random number to the end, in case we are called quicker than 489# 1 second since the last time we were called. 490 491# We'll setup a template for the message id, using the "from" address: 492 493my($message_id_stamp,$message_id_serial); 494sub make_message_id 495{ 496my$uniq; 497if(!defined$message_id_stamp) { 498$message_id_stamp=sprintf("%s-%s",time,$$); 499$message_id_serial=0; 500} 501$message_id_serial++; 502$uniq="$message_id_stamp-$message_id_serial"; 503 504my$du_part; 505for($sender,$repocommitter,$repoauthor) { 506$du_part= extract_valid_address(sanitize_address($_)); 507last if(defined$du_partand$du_partne''); 508} 509if(not defined$du_partor$du_parteq'') { 510use Sys::Hostname qw(); 511$du_part='user@'. Sys::Hostname::hostname(); 512} 513my$message_id_template="<%s-git-send-email-%s>"; 514$message_id=sprintf($message_id_template,$uniq,$du_part); 515#print "new message id = $message_id\n"; # Was useful for debugging 516} 517 518 519 520$time=time-scalar$#files; 521 522sub unquote_rfc2047 { 523local($_) =@_; 524my$encoding; 525if(s/=\?([^?]+)\?q\?(.*)\?=/$2/g) { 526$encoding=$1; 527s/_/ /g; 528s/=([0-9A-F]{2})/chr(hex($1))/eg; 529} 530returnwantarray? ($_,$encoding) :$_; 531} 532 533# use the simplest quoting being able to handle the recipient 534sub sanitize_address 535{ 536my($recipient) =@_; 537my($recipient_name,$recipient_addr) = ($recipient=~/^(.*?)\s*(<.*)/); 538 539if(not$recipient_name) { 540return"$recipient"; 541} 542 543# if recipient_name is already quoted, do nothing 544if($recipient_name=~/^(".*"|=\?utf-8\?q\?.*\?=)$/) { 545return$recipient; 546} 547 548# rfc2047 is needed if a non-ascii char is included 549if($recipient_name=~/[^[:ascii:]]/) { 550$recipient_name=~s/([^-a-zA-Z0-9!*+\/])/sprintf("=%02X",ord($1))/eg; 551$recipient_name=~s/(.*)/=\?utf-8\?q\?$1\?=/; 552} 553 554# double quotes are needed if specials or CTLs are included 555elsif($recipient_name=~/[][()<>@,;:\\".\000-\037\177]/) { 556$recipient_name=~s/(["\\\r])/\\$1/; 557$recipient_name="\"$recipient_name\""; 558} 559 560return"$recipient_name$recipient_addr"; 561 562} 563 564sub send_message 565{ 566my@recipients= unique_email_list(@to); 567@cc= (grep{my$cc= extract_valid_address($_); 568not grep{$cceq$_}@recipients 569} 570map{ sanitize_address($_) } 571@cc); 572my$to=join(",\n\t",@recipients); 573@recipients= unique_email_list(@recipients,@cc,@bcclist); 574@recipients= (map{ extract_valid_address($_) }@recipients); 575my$date= format_2822_time($time++); 576my$gitversion='@@GIT_VERSION@@'; 577if($gitversion=~m/..GIT_VERSION../) { 578$gitversion= Git::version(); 579} 580 581my$cc=join(", ", unique_email_list(@cc)); 582my$ccline=""; 583if($ccne'') { 584$ccline="\nCc:$cc"; 585} 586my$sanitized_sender= sanitize_address($sender); 587 make_message_id()unlessdefined($message_id); 588 589my$header="From:$sanitized_sender 590To:$to${ccline} 591Subject:$subject 592Date:$date 593Message-Id:$message_id 594X-Mailer: git-send-email$gitversion 595"; 596if($thread&&$reply_to) { 597 598$header.="In-Reply-To:$reply_to\n"; 599$header.="References:$references\n"; 600} 601if(@xh) { 602$header.=join("\n",@xh) ."\n"; 603} 604 605my@sendmail_parameters= ('-i',@recipients); 606my$raw_from=$sanitized_sender; 607$raw_from=$envelope_senderif(defined$envelope_sender); 608$raw_from= extract_valid_address($raw_from); 609unshift(@sendmail_parameters, 610'-f',$raw_from)if(defined$envelope_sender); 611 612if($dry_run) { 613# We don't want to send the email. 614}elsif($smtp_server=~ m#^/#) { 615my$pid=open my$sm,'|-'; 616defined$pidor die$!; 617if(!$pid) { 618exec($smtp_server,@sendmail_parameters)or die$!; 619} 620print$sm"$header\n$message"; 621close$smor die$?; 622}else{ 623 624if(!defined$smtp_server) { 625die"The required SMTP server is not properly defined." 626} 627 628if($smtp_ssl) { 629$smtp_server_port||=465;# ssmtp 630require Net::SMTP::SSL; 631$smtp||= Net::SMTP::SSL->new($smtp_server, Port =>$smtp_server_port); 632} 633else{ 634require Net::SMTP; 635$smtp||= Net::SMTP->new((defined$smtp_server_port) 636?"$smtp_server:$smtp_server_port" 637:$smtp_server); 638} 639 640if(!$smtp) { 641die"Unable to initialize SMTP properly. Is there something wrong with your config?"; 642} 643 644if((defined$smtp_authuser) && (defined$smtp_authpass)) { 645$auth||=$smtp->auth($smtp_authuser,$smtp_authpass)or die$smtp->message; 646} 647$smtp->mail($raw_from)or die$smtp->message; 648$smtp->to(@recipients)or die$smtp->message; 649$smtp->dataor die$smtp->message; 650$smtp->datasend("$header\n$message")or die$smtp->message; 651$smtp->dataend()or die$smtp->message; 652$smtp->okor die"Failed to send$subject\n".$smtp->message; 653} 654if($quiet) { 655printf(($dry_run?"Dry-":"")."Sent%s\n",$subject); 656}else{ 657print(($dry_run?"Dry-":"")."OK. Log says:\n"); 658if($smtp_server!~ m#^/#) { 659print"Server:$smtp_server\n"; 660print"MAIL FROM:<$raw_from>\n"; 661print"RCPT TO:".join(',',(map{"<$_>"}@recipients))."\n"; 662}else{ 663print"Sendmail:$smtp_server".join(' ',@sendmail_parameters)."\n"; 664} 665print$header,"\n"; 666if($smtp) { 667print"Result: ",$smtp->code,' ', 668($smtp->message=~/\n([^\n]+\n)$/s),"\n"; 669}else{ 670print"Result: OK\n"; 671} 672} 673} 674 675$reply_to=$initial_reply_to; 676$references=$initial_reply_to||''; 677$subject=$initial_subject; 678 679foreachmy$t(@files) { 680open(F,"<",$t)or die"can't open file$t"; 681 682my$author=undef; 683my$author_encoding; 684my$has_content_type; 685my$body_encoding; 686@cc=@initial_cc; 687@xh= (); 688my$input_format=undef; 689my$header_done=0; 690$message=""; 691while(<F>) { 692if(!$header_done) { 693if(/^From /) { 694$input_format='mbox'; 695next; 696} 697chomp; 698if(!defined$input_format&&/^[-A-Za-z]+:\s/) { 699$input_format='mbox'; 700} 701 702if(defined$input_format&&$input_formateq'mbox') { 703if(/^Subject:\s+(.*)$/) { 704$subject=$1; 705 706}elsif(/^(Cc|From):\s+(.*)$/) { 707if(unquote_rfc2047($2)eq$sender) { 708next if($suppress_from); 709} 710elsif($1eq'From') { 711($author,$author_encoding) 712= unquote_rfc2047($2); 713} 714printf("(mbox) Adding cc:%sfrom line '%s'\n", 715$2,$_)unless$quiet; 716push@cc,$2; 717} 718elsif(/^Content-type:/i) { 719$has_content_type=1; 720if(/charset="?[^ "]+/) { 721$body_encoding=$1; 722} 723push@xh,$_; 724} 725elsif(/^Message-Id: (.*)/i) { 726$message_id=$1; 727} 728elsif(!/^Date:\s/&&/^[-A-Za-z]+:\s+\S/) { 729push@xh,$_; 730} 731 732}else{ 733# In the traditional 734# "send lots of email" format, 735# line 1 = cc 736# line 2 = subject 737# So let's support that, too. 738$input_format='lots'; 739if(@cc==0) { 740printf("(non-mbox) Adding cc:%sfrom line '%s'\n", 741$_,$_)unless$quiet; 742 743push@cc,$_; 744 745}elsif(!defined$subject) { 746$subject=$_; 747} 748} 749 750# A whitespace line will terminate the headers 751if(m/^\s*$/) { 752$header_done=1; 753} 754}else{ 755$message.=$_; 756if(/^(Signed-off-by|Cc): (.*)$/i&&$signed_off_cc) { 757my$c=$2; 758chomp$c; 759next if($ceq$senderand$suppress_from); 760push@cc,$c; 761printf("(sob) Adding cc:%sfrom line '%s'\n", 762$c,$_)unless$quiet; 763} 764} 765} 766close F; 767 768if(defined$cc_cmd) { 769open(F,"$cc_cmd$t|") 770or die"(cc-cmd) Could not execute '$cc_cmd'"; 771while(<F>) { 772my$c=$_; 773$c=~s/^\s*//g; 774$c=~s/\n$//g; 775next if($ceq$senderand$suppress_from); 776push@cc,$c; 777printf("(cc-cmd) Adding cc:%sfrom: '%s'\n", 778$c,$cc_cmd)unless$quiet; 779} 780close F 781or die"(cc-cmd) failed to close pipe to '$cc_cmd'"; 782} 783 784if(defined$author) { 785$message="From:$author\n\n$message"; 786if(defined$author_encoding) { 787if($has_content_type) { 788if($body_encodingeq$author_encoding) { 789# ok, we already have the right encoding 790} 791else{ 792# uh oh, we should re-encode 793} 794} 795else{ 796push@xh, 797'MIME-Version: 1.0', 798"Content-Type: text/plain; charset=$author_encoding", 799'Content-Transfer-Encoding: 8bit'; 800} 801} 802} 803 804 send_message(); 805 806# set up for the next message 807if($chain_reply_to|| !defined$reply_to||length($reply_to) ==0) { 808$reply_to=$message_id; 809if(length$references>0) { 810$references.="\n$message_id"; 811}else{ 812$references="$message_id"; 813} 814} 815$message_id=undef; 816} 817 818if($compose) { 819 cleanup_compose_files(); 820} 821 822sub cleanup_compose_files() { 823unlink($compose_filename,$compose_filename.".final"); 824 825} 826 827$smtp->quitif$smtp; 828 829sub unique_email_list(@) { 830my%seen; 831my@emails; 832 833foreachmy$entry(@_) { 834if(my$clean= extract_valid_address($entry)) { 835$seen{$clean} ||=0; 836next if$seen{$clean}++; 837push@emails,$entry; 838}else{ 839print STDERR "W: unable to extract a valid address", 840" from:$entry\n"; 841} 842} 843return@emails; 844} 845 846sub validate_patch { 847my$fn=shift; 848open(my$fh,'<',$fn) 849or die"unable to open$fn:$!\n"; 850while(my$line= <$fh>) { 851if(length($line) >998) { 852return"$.: patch contains a line longer than 998 characters"; 853} 854} 855returnundef; 856}