diff options
Diffstat (limited to 'git-send-email.perl')
-rwxr-xr-x | git-send-email.perl | 282 |
1 files changed, 210 insertions, 72 deletions
diff --git a/git-send-email.perl b/git-send-email.perl index 6a17ed6d61..d491db92c9 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; @@ -47,18 +49,20 @@ git send-email [options] <file | directory | rev-list options > Composing: --from <str> * Email From: - --to <str> * Email To: - --cc <str> * Email Cc: - --bcc <str> * Email Bcc: + --[no-]to <str> * Email To: + --[no-]cc <str> * Email Cc: + --[no-]bcc <str> * Email Bcc: --subject <str> * Email "Subject:" --in-reply-to <str> * Email "In-Reply-To:" --annotate * Review each patch that will be sent in an editor. --compose * Open an editor for introduction. + --8bit-encoding <str> * Encoding to assume 8bit mails if undeclared Sending: --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. @@ -69,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. @@ -84,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); @@ -133,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,@cc,@initial_cc,@bcclist,@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); @@ -161,12 +164,16 @@ if ($@) { my ($quiet, $dry_run) = (0, 0); my $format_patch; my $compose_filename; +my $force = 0; # Handle interactive edition of files. my $multiedit; -my $editor = Git::command_oneline('var', 'GIT_EDITOR'); +my $editor; sub do_edit { + if (!defined($editor)) { + $editor = Git::command_oneline('var', 'GIT_EDITOR'); + } if (defined($multiedit) && !$multiedit) { map { system('sh', '-c', $editor.' "$@"', $editor, $_); @@ -183,11 +190,14 @@ 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); my ($debug_net_smtp) = 0; # Net::SMTP, see send_message() @@ -205,20 +215,26 @@ 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, "bcc" => \@bcclist, - "aliasesfile" => \@alias_files, "suppresscc" => \@suppress_cc, "envelopesender" => \$envelope_sender, "multiedit" => \$multiedit, "confirm" => \$confirm, "from" => \$sender, + "assume8bitencoding" => \$auto_8bit_encoding, +); + +my %config_path_settings = ( + "aliasesfile" => \@alias_files, ); # Help users prepare for 1.7.0 @@ -262,14 +278,21 @@ $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, "bcc=s" => \@bcclist, + "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, @@ -291,8 +314,11 @@ my $rc = GetOptions("sender|from=s" => \$sender, "thread!" => \$thread, "validate!" => \$validate, "format-patch!" => \$format_patch, + "8bit-encoding=s" => \$auto_8bit_encoding, + "force" => \$force, ); +usage() if $help; unless ($rc) { usage(); } @@ -310,8 +336,24 @@ sub read_config { $$target = Git::config_bool(@repo, "$prefix.$setting") unless (defined $$target); } + foreach my $setting (keys %config_path_settings) { + my $target = $config_path_settings{$setting}; + if (ref($target) eq "ARRAY") { + unless (@$target) { + my @values = Git::config_path(@repo, "$prefix.$setting"); + @$target = @values if (@values && defined $values[0]); + } + } + else { + $$target = Git::config_path(@repo, "$prefix.$setting") unless (defined $$target); + } + } + foreach my $setting (keys %config_settings) { my $target = $config_settings{$setting}; + next if $setting eq "to" and defined $no_to; + next if $setting eq "cc" and defined $no_cc; + next if $setting eq "bcc" and defined $no_bcc; if (ref($target) eq "ARRAY") { unless (@$target) { my @values = Git::config(@repo, "$prefix.$setting"); @@ -351,7 +393,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; } } @@ -396,7 +438,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/,/; } @@ -495,12 +537,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 { @@ -532,7 +574,7 @@ if (@files) { usage(); } -sub get_patch_subject($) { +sub get_patch_subject { my $fn = shift; open (my $fh, '<', $fn); while (my $line = <$fh>) { @@ -550,7 +592,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: $!"; @@ -558,7 +600,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 @@ -571,9 +613,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); @@ -581,23 +623,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"; @@ -622,10 +664,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"; @@ -660,6 +702,45 @@ sub ask { return undef; } +my %broken_encoding; + +sub file_declares_8bit_cte { + my $fn = shift; + open (my $fh, '<', $fn); + while (my $line = <$fh>) { + last if ($line =~ /^$/); + return 1 if ($line =~ /^Content-Transfer-Encoding: .*8bit.*$/); + } + close $fh; + return 0; +} + +foreach my $f (@files) { + next unless (body_or_subject_has_nonascii($f) + && !file_declares_8bit_cte($f)); + $broken_encoding{$f} = 1; +} + +if (!defined $auto_8bit_encoding && scalar %broken_encoding) { + print "The following files are 8bit, but do not declare " . + "a Content-Transfer-Encoding.\n"; + foreach my $f (sort keys %broken_encoding) { + print " $f\n"; + } + $auto_8bit_encoding = ask("Which 8bit encoding should I declare [UTF-8]? ", + 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 || ''; @@ -669,9 +750,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++; } @@ -689,8 +770,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); @@ -724,8 +805,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)$/); @@ -766,7 +847,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>"; @@ -799,8 +880,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; } @@ -811,7 +892,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 @@ -828,7 +909,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"; @@ -853,7 +934,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 { @@ -898,7 +979,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); @@ -973,6 +1054,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#^/#) { @@ -982,7 +1065,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) { @@ -1027,10 +1110,16 @@ 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) { + # Workaround AUTH PLAIN/LOGIN interaction defect + # with Authen::SASL::Cyrus + eval { + require Authen::SASL; + Authen::SASL->import(qw(Perl)); + }; if (!defined $smtp_authpass) { @@ -1088,12 +1177,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; @@ -1101,7 +1191,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]); @@ -1134,6 +1224,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) { @@ -1177,7 +1274,7 @@ foreach my $t (@files) { } } # Now parse the message body - while(<F>) { + while(<$fh>) { $message .= $_; if (/^(Signed-off-by|Cc): (.*)$/i) { chomp; @@ -1194,22 +1291,23 @@ 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; + push @xh, "MIME-Version: 1.0", + "Content-Type: text/plain; charset=$auto_8bit_encoding", + "Content-Transfer-Encoding: 8bit"; + $body_encoding = $auto_8bit_encoding; + } + + if ($broken_encoding{$t} && !is_rfc2047_quoted($subject)) { + $subject = quote_rfc2047($subject, $auto_8bit_encoding); } if (defined $author and $author ne $sender) { @@ -1224,6 +1322,7 @@ foreach my $t (@files) { } } else { + $has_content_type = 1; push @xh, 'MIME-Version: 1.0', "Content-Type: text/plain; charset=$author_encoding", @@ -1238,13 +1337,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"; @@ -1255,15 +1356,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; @@ -1301,3 +1425,17 @@ sub file_has_nonascii { } return 0; } + +sub body_or_subject_has_nonascii { + my $fn = shift; + open(my $fh, '<', $fn) + or die "unable to open $fn: $!\n"; + while (my $line = <$fh>) { + last if $line =~ /^$/; + return 1 if $line =~ /^Subject.*[^[:ascii:]]/; + } + while (my $line = <$fh>) { + return 1 if $line =~ /[^[:ascii:]]/; + } + return 0; +} |