diff options
Diffstat (limited to 'git-send-email.perl')
| -rwxr-xr-x | git-send-email.perl | 185 | 
1 files changed, 116 insertions, 69 deletions
| diff --git a/git-send-email.perl b/git-send-email.perl index 6dab3bf6a7..734356a6fb 100755 --- a/git-send-email.perl +++ b/git-send-email.perl @@ -1,4 +1,4 @@ -#!/usr/bin/perl -w +#!/usr/bin/perl  #  # Copyright 2002,2005 Greg Kroah-Hartman <greg@kroah.com>  # Copyright 2005 Ryan Anderson <ryan@michonline.com> @@ -16,6 +16,7 @@  #    and second line is the subject of the message.  # +use 5.008;  use strict;  use warnings;  use Term::ReadLine; @@ -24,6 +25,7 @@ use Text::ParseWords;  use Data::Dumper;  use Term::ANSIColor;  use File::Temp qw/ tempdir tempfile /; +use File::Spec::Functions qw(catfile);  use Error qw(:try);  use Git; @@ -60,6 +62,7 @@ git send-email [options] <file | directory | rev-list options >      --envelope-sender       <str>  * Email envelope sender.      --smtp-server       <str:int>  * Outgoing SMTP server to use. The port                                       is optional. Default 'localhost'. +    --smtp-server-option    <str>  * Outgoing SMTP server option to use.      --smtp-server-port      <int>  * Outgoing SMTP server port.      --smtp-user             <str>  * Username for SMTP-AUTH.      --smtp-pass             <str>  * Password for SMTP-AUTH; not necessary. @@ -70,6 +73,7 @@ git send-email [options] <file | directory | rev-list options >    Automating:      --identity              <str>  * Use the sendemail.<id> options. +    --to-cmd                <str>  * Email To: via `<str> \$patch_path`      --cc-cmd                <str>  * Email Cc: via `<str> \$patch_path`      --suppress-cc           <str>  * author, self, sob, cc, cccmd, body, bodycc, all.      --[no-]signed-off-by-cc        * Send to Signed-off-by: addresses. Default on. @@ -85,6 +89,7 @@ git send-email [options] <file | directory | rev-list options >      --[no-]validate                * Perform patch sanity checks. Default on.      --[no-]format-patch            * understand any non optional arguments as                                       `git format-patch` ones. +    --force                        * Send even if safety checks would prevent it.  EOT  	exit(1); @@ -134,11 +139,8 @@ my $have_mail_address = eval { require Mail::Address; 1 };  my $smtp;  my $auth; -sub unique_email_list(@); -sub cleanup_compose_files(); -  # Variables we fill in automatically, or via prompting: -my (@to,$no_to,@cc,$no_cc,@initial_cc,@bcclist,$no_bcc,@xh, +my (@to,$no_to,@initial_to,@cc,$no_cc,@initial_cc,@bcclist,$no_bcc,@xh,  	$initial_reply_to,$initial_subject,@files,  	$author,$sender,$smtp_authpass,$annotate,$compose,$time); @@ -162,6 +164,7 @@ if ($@) {  my ($quiet, $dry_run) = (0, 0);  my $format_patch;  my $compose_filename; +my $force = 0;  # Handle interactive edition of files.  my $multiedit; @@ -187,9 +190,11 @@ sub do_edit {  }  # Variables with corresponding config settings -my ($thread, $chain_reply_to, $suppress_from, $signed_off_by_cc, $cc_cmd); -my ($smtp_server, $smtp_server_port, $smtp_authuser, $smtp_encryption); -my ($identity, $aliasfiletype, @alias_files, @smtp_host_parts, $smtp_domain); +my ($thread, $chain_reply_to, $suppress_from, $signed_off_by_cc); +my ($to_cmd, $cc_cmd); +my ($smtp_server, $smtp_server_port, @smtp_server_options); +my ($smtp_authuser, $smtp_encryption); +my ($identity, $aliasfiletype, @alias_files, $smtp_domain);  my ($validate, $confirm);  my (@suppress_cc);  my ($auto_8bit_encoding); @@ -210,10 +215,12 @@ my %config_bool_settings = (  my %config_settings = (      "smtpserver" => \$smtp_server,      "smtpserverport" => \$smtp_server_port, +    "smtpserveroption" => \@smtp_server_options,      "smtpuser" => \$smtp_authuser,      "smtppass" => \$smtp_authpass, -	"smtpdomain" => \$smtp_domain, -    "to" => \@to, +    "smtpdomain" => \$smtp_domain, +    "to" => \@initial_to, +    "tocmd" => \$to_cmd,      "cc" => \@initial_cc,      "cccmd" => \$cc_cmd,      "aliasfiletype" => \$aliasfiletype, @@ -268,10 +275,13 @@ $SIG{INT}  = \&signal_handler;  # Begin by accumulating all the variables (defined above), that we will end up  # needing, first, from the command line: -my $rc = GetOptions("sender|from=s" => \$sender, +my $help; +my $rc = GetOptions("h" => \$help, +		    "sender|from=s" => \$sender,                      "in-reply-to=s" => \$initial_reply_to,  		    "subject=s" => \$initial_subject, -		    "to=s" => \@to, +		    "to=s" => \@initial_to, +		    "to-cmd=s" => \$to_cmd,  		    "no-to" => \$no_to,  		    "cc=s" => \@initial_cc,  		    "no-cc" => \$no_cc, @@ -279,6 +289,7 @@ my $rc = GetOptions("sender|from=s" => \$sender,  		    "no-bcc" => \$no_bcc,  		    "chain-reply-to!" => \$chain_reply_to,  		    "smtp-server=s" => \$smtp_server, +		    "smtp-server-option=s" => \@smtp_server_options,  		    "smtp-server-port=s" => \$smtp_server_port,  		    "smtp-user=s" => \$smtp_authuser,  		    "smtp-pass:s" => \$smtp_authpass, @@ -301,8 +312,10 @@ my $rc = GetOptions("sender|from=s" => \$sender,  		    "validate!" => \$validate,  		    "format-patch!" => \$format_patch,  		    "8bit-encoding=s" => \$auto_8bit_encoding, +		    "force" => \$force,  	 ); +usage() if $help;  unless ($rc) {      usage();  } @@ -364,7 +377,7 @@ my(%suppress_cc);  if (@suppress_cc) {  	foreach my $entry (@suppress_cc) {  		die "Unknown --suppress-cc field: '$entry'\n" -			unless $entry =~ /^(all|cccmd|cc|author|self|sob|body|bodycc)$/; +			unless $entry =~ /^(?:all|cccmd|cc|author|self|sob|body|bodycc)$/;  		$suppress_cc{$entry} = 1;  	}  } @@ -409,7 +422,7 @@ my ($repoauthor, $repocommitter);  # Verify the user input -foreach my $entry (@to) { +foreach my $entry (@initial_to) {  	die "Comma in --to entry: $entry'\n" unless $entry !~ m/,/;  } @@ -508,12 +521,12 @@ while (defined(my $f = shift @ARGV)) {  		push @rev_list_opts, "--", @ARGV;  		@ARGV = ();  	} elsif (-d $f and !check_file_rev_conflict($f)) { -		opendir(DH,$f) +		opendir my $dh, $f  			or die "Failed to opendir $f: $!"; -		push @files, grep { -f $_ } map { +$f . "/" . $_ } -				sort readdir(DH); -		closedir(DH); +		push @files, grep { -f $_ } map { catfile($f, $_) } +				sort readdir $dh; +		closedir $dh;  	} elsif ((-f $f or -p $f) and !check_file_rev_conflict($f)) {  		push @files, $f;  	} else { @@ -545,7 +558,7 @@ if (@files) {  	usage();  } -sub get_patch_subject($) { +sub get_patch_subject {  	my $fn = shift;  	open (my $fh, '<', $fn);  	while (my $line = <$fh>) { @@ -563,7 +576,7 @@ if ($compose) {  	$compose_filename = ($repo ?  		tempfile(".gitsendemail.msg.XXXXXX", DIR => $repo->repo_path()) :  		tempfile(".gitsendemail.msg.XXXXXX", DIR => "."))[1]; -	open(C,">",$compose_filename) +	open my $c, ">", $compose_filename  		or die "Failed to open for writing $compose_filename: $!"; @@ -571,7 +584,7 @@ if ($compose) {  	my $tpl_subject = $initial_subject || '';  	my $tpl_reply_to = $initial_reply_to || ''; -	print C <<EOT; +	print $c <<EOT;  From $tpl_sender # This line is ignored.  GIT: Lines beginning in "GIT:" will be removed.  GIT: Consider including an overall diffstat or table of contents @@ -584,9 +597,9 @@ In-Reply-To: $tpl_reply_to  EOT  	for my $f (@files) { -		print C get_patch_subject($f); +		print $c get_patch_subject($f);  	} -	close(C); +	close $c;  	if ($annotate) {  		do_edit($compose_filename, @files); @@ -594,23 +607,23 @@ EOT  		do_edit($compose_filename);  	} -	open(C2,">",$compose_filename . ".final") +	open my $c2, ">", $compose_filename . ".final"  		or die "Failed to open $compose_filename.final : " . $!; -	open(C,"<",$compose_filename) +	open $c, "<", $compose_filename  		or die "Failed to open $compose_filename : " . $!;  	my $need_8bit_cte = file_has_nonascii($compose_filename);  	my $in_body = 0;  	my $summary_empty = 1; -	while(<C>) { +	while(<$c>) {  		next if m/^GIT:/;  		if ($in_body) {  			$summary_empty = 0 unless (/^\n$/);  		} elsif (/^\n$/) {  			$in_body = 1;  			if ($need_8bit_cte) { -				print C2 "MIME-Version: 1.0\n", +				print $c2 "MIME-Version: 1.0\n",  					 "Content-Type: text/plain; ",  					   "charset=UTF-8\n",  					 "Content-Transfer-Encoding: 8bit\n"; @@ -635,10 +648,10 @@ EOT  			print "To/Cc/Bcc fields are not interpreted yet, they have been ignored\n";  			next;  		} -		print C2 $_; +		print $c2 $_;  	} -	close(C); -	close(C2); +	close $c; +	close $c2;  	if ($summary_empty) {  		print "Summary email is empty, skipping it\n"; @@ -675,7 +688,7 @@ sub ask {  my %broken_encoding; -sub file_declares_8bit_cte($) { +sub file_declares_8bit_cte {  	my $fn = shift;  	open (my $fh, '<', $fn);  	while (my $line = <$fh>) { @@ -702,6 +715,16 @@ if (!defined $auto_8bit_encoding && scalar %broken_encoding) {  				  default => "UTF-8");  } +if (!$force) { +	for my $f (@files) { +		if (get_patch_subject($f) =~ /\Q*** SUBJECT HERE ***\E/) { +			die "Refusing to send because the patch\n\t$f\n" +				. "has the template subject '*** SUBJECT HERE ***'. " +				. "Pass --force if you really want to send.\n"; +		} +	} +} +  my $prompting = 0;  if (!defined $sender) {  	$sender = $repoauthor || $repocommitter || ''; @@ -711,9 +734,9 @@ if (!defined $sender) {  	$prompting++;  } -if (!@to) { +if (!@initial_to && !defined $to_cmd) {  	my $to = ask("Who should the emails be sent to? "); -	push @to, parse_address_line($to) if defined $to; # sanitized/validated later +	push @initial_to, parse_address_line($to) if defined $to; # sanitized/validated later  	$prompting++;  } @@ -731,8 +754,8 @@ sub expand_one_alias {  	return $aliases{$alias} ? expand_aliases(@{$aliases{$alias}}) : $alias;  } -@to = expand_aliases(@to); -@to = (map { sanitize_address($_) } @to); +@initial_to = expand_aliases(@initial_to); +@initial_to = (map { sanitize_address($_) } @initial_to);  @initial_cc = expand_aliases(@initial_cc);  @bcclist = expand_aliases(@bcclist); @@ -766,8 +789,8 @@ our ($message_id, %mail, $subject, $reply_to, $references, $message,  sub extract_valid_address {  	my $address = shift; -	my $local_part_regexp = '[^<>"\s@]+'; -	my $domain_regexp = '[^.<>"\s@]+(?:\.[^.<>"\s@]+)+'; +	my $local_part_regexp = qr/[^<>"\s@]+/; +	my $domain_regexp = qr/[^.<>"\s@]+(?:\.[^.<>"\s@]+)+/;  	# check for a local address:  	return $address if ($address =~ /^($local_part_regexp)$/); @@ -808,7 +831,7 @@ sub make_message_id {  		last if (defined $du_part and $du_part ne '');  	}  	if (not defined $du_part or $du_part eq '') { -		use Sys::Hostname qw(); +		require Sys::Hostname;  		$du_part = 'user@' . Sys::Hostname::hostname();  	}  	my $message_id_template = "<%s-git-send-email-%s>"; @@ -841,8 +864,8 @@ sub quote_rfc2047 {  sub is_rfc2047_quoted {  	my $s = shift; -	my $token = '[^][()<>@,;:"\/?.= \000-\037\177-\377]+'; -	my $encoded_text = '[!->@-~]+'; +	my $token = qr/[^][()<>@,;:"\/?.= \000-\037\177-\377]+/; +	my $encoded_text = qr/[!->@-~]+/;  	length($s) <= 75 &&  	$s =~ m/^(?:"[[:ascii:]]*"|=\?$token\?$token\?$encoded_text\?=)$/o;  } @@ -853,7 +876,7 @@ sub sanitize_address {  	my ($recipient_name, $recipient_addr) = ($recipient =~ /^(.*?)\s*(<.*)/);  	if (not $recipient_name) { -		return "$recipient"; +		return $recipient;  	}  	# if recipient_name is already quoted, do nothing @@ -870,7 +893,7 @@ sub sanitize_address {  	# double quotes are needed if specials or CTLs are included  	elsif ($recipient_name =~ /[][()<>@,;:\\".\000-\037\177]/) {  		$recipient_name =~ s/(["\\\r])/\\$1/g; -		$recipient_name = "\"$recipient_name\""; +		$recipient_name = qq["$recipient_name"];  	}  	return "$recipient_name $recipient_addr"; @@ -895,7 +918,7 @@ sub sanitize_address {  sub valid_fqdn {  	my $domain = shift; -	return !($^O eq 'darwin' && $domain =~ /\.local$/) && $domain =~ /\./; +	return defined $domain && !($^O eq 'darwin' && $domain =~ /\.local$/) && $domain =~ /\./;  }  sub maildomain_net { @@ -940,7 +963,7 @@ sub maildomain {  sub send_message {  	my @recipients = unique_email_list(@to);  	@cc = (grep { my $cc = extract_valid_address($_); -		      not grep { $cc eq $_ } @recipients +		      not grep { $cc eq $_ || $_ =~ /<\Q${cc}\E>$/ } @recipients  		    }  	       map { sanitize_address($_) }  	       @cc); @@ -1015,6 +1038,8 @@ X-Mailer: git-send-email $gitversion  		}  	} +	unshift (@sendmail_parameters, @smtp_server_options); +  	if ($dry_run) {  		# We don't want to send the email.  	} elsif ($smtp_server =~ m#^/#) { @@ -1024,7 +1049,7 @@ X-Mailer: git-send-email $gitversion  			exec($smtp_server, @sendmail_parameters) or die $!;  		}  		print $sm "$header\n$message"; -		close $sm or die $?; +		close $sm or die $!;  	} else {  		if (!defined $smtp_server) { @@ -1069,7 +1094,7 @@ X-Mailer: git-send-email $gitversion  			    "VALUES: server=$smtp_server ",  			    "encryption=$smtp_encryption ",  			    "hello=$smtp_domain", -			    defined $smtp_server_port ? "port=$smtp_server_port" : ""; +			    defined $smtp_server_port ? " port=$smtp_server_port" : "";  		}  		if (defined $smtp_authuser) { @@ -1130,12 +1155,13 @@ $subject = $initial_subject;  $message_num = 0;  foreach my $t (@files) { -	open(F,"<",$t) or die "can't open file $t"; +	open my $fh, "<", $t or die "can't open file $t";  	my $author = undef;  	my $author_encoding;  	my $has_content_type;  	my $body_encoding; +	@to = ();  	@cc = ();  	@xh = ();  	my $input_format = undef; @@ -1143,7 +1169,7 @@ foreach my $t (@files) {  	$message = "";  	$message_num++;  	# First unfold multiline header fields -	while(<F>) { +	while(<$fh>) {  		last if /^\s*$/;  		if (/^\s+\S/ and @header) {  			chomp($header[$#header]); @@ -1176,6 +1202,13 @@ foreach my $t (@files) {  					$1, $_) unless $quiet;  				push @cc, $1;  			} +			elsif (/^To:\s+(.*)$/) { +				foreach my $addr (parse_address_line($1)) { +					printf("(mbox) Adding to: %s from line '%s'\n", +						$addr, $_) unless $quiet; +					push @to, sanitize_address($addr); +				} +			}  			elsif (/^Cc:\s+(.*)$/) {  				foreach my $addr (parse_address_line($1)) {  					if (unquote_rfc2047($addr) eq $sender) { @@ -1219,7 +1252,7 @@ foreach my $t (@files) {  		}  	}  	# Now parse the message body -	while(<F>) { +	while(<$fh>) {  		$message .=  $_;  		if (/^(Signed-off-by|Cc): (.*)$/i) {  			chomp; @@ -1236,23 +1269,12 @@ foreach my $t (@files) {  				$c, $_) unless $quiet;  		}  	} -	close F; - -	if (defined $cc_cmd && !$suppress_cc{'cccmd'}) { -		open(F, "$cc_cmd \Q$t\E |") -			or die "(cc-cmd) Could not execute '$cc_cmd'"; -		while(<F>) { -			my $c = $_; -			$c =~ s/^\s*//g; -			$c =~ s/\n$//g; -			next if ($c eq $sender and $suppress_from); -			push @cc, $c; -			printf("(cc-cmd) Adding cc: %s from: '%s'\n", -				$c, $cc_cmd) unless $quiet; -		} -		close F -			or die "(cc-cmd) failed to close pipe to '$cc_cmd'"; -	} +	close $fh; + +	push @to, recipients_cmd("to-cmd", "to", $to_cmd, $t) +		if defined $to_cmd; +	push @cc, recipients_cmd("cc-cmd", "cc", $cc_cmd, $t) +		if defined $cc_cmd && !$suppress_cc{'cccmd'};  	if ($broken_encoding{$t} && !$has_content_type) {  		$has_content_type = 1; @@ -1293,13 +1315,15 @@ foreach my $t (@files) {  		($confirm =~ /^(?:auto|compose)$/ && $compose && $message_num == 1));  	$needs_confirm = "inform" if ($needs_confirm && $confirm_unconfigured && @cc); +	@to = (@initial_to, @to);  	@cc = (@initial_cc, @cc);  	my $message_was_sent = send_message();  	# set up for the next message  	if ($thread && $message_was_sent && -		(chain_reply_to() || !defined $reply_to || length($reply_to) == 0)) { +		(chain_reply_to() || !defined $reply_to || length($reply_to) == 0 || +		$message_num == 1)) {  		$reply_to = $message_id;  		if (length $references > 0) {  			$references .= "\n $message_id"; @@ -1310,15 +1334,38 @@ foreach my $t (@files) {  	$message_id = undef;  } +# Execute a command (e.g. $to_cmd) to get a list of email addresses +# and return a results array +sub recipients_cmd { +	my ($prefix, $what, $cmd, $file) = @_; + +	my $sanitized_sender = sanitize_address($sender); +	my @addresses = (); +	open my $fh, "$cmd \Q$file\E |" +	    or die "($prefix) Could not execute '$cmd'"; +	while (my $address = <$fh>) { +		$address =~ s/^\s*//g; +		$address =~ s/\s*$//g; +		$address = sanitize_address($address); +		next if ($address eq $sanitized_sender and $suppress_from); +		push @addresses, $address; +		printf("($prefix) Adding %s: %s from: '%s'\n", +		       $what, $address, $cmd) unless $quiet; +		} +	close $fh +	    or die "($prefix) failed to close pipe to '$cmd'"; +	return @addresses; +} +  cleanup_compose_files(); -sub cleanup_compose_files() { +sub cleanup_compose_files {  	unlink($compose_filename, $compose_filename . ".final") if $compose;  }  $smtp->quit if $smtp; -sub unique_email_list(@) { +sub unique_email_list {  	my %seen;  	my @emails; | 
