diff options
Diffstat (limited to 'git-send-email.perl')
| -rwxr-xr-x | git-send-email.perl | 579 |
1 files changed, 406 insertions, 173 deletions
diff --git a/git-send-email.perl b/git-send-email.perl index 1f425c0809..288ea1ae80 100755 --- a/git-send-email.perl +++ b/git-send-email.perl @@ -18,39 +18,18 @@ use 5.008; use strict; -use warnings; -use POSIX qw/strftime/; -use Term::ReadLine; +use warnings $ENV{GIT_PERL_FATAL_WARNINGS} ? qw(FATAL all) : (); use Getopt::Long; -use Text::ParseWords; -use Term::ANSIColor; -use File::Temp qw/ tempdir tempfile /; -use File::Spec::Functions qw(catdir catfile); use Git::LoadCPAN::Error qw(:try); -use Cwd qw(abs_path cwd); use Git; use Git::I18N; -use Net::Domain (); -use Net::SMTP (); -use Git::LoadCPAN::Mail::Address; Getopt::Long::Configure qw/ pass_through /; -package FakeTerm; -sub new { - my ($class, $reason) = @_; - return bless \$reason, shift; -} -sub readline { - my $self = shift; - die "Cannot use readline on FakeTerm: $$self"; -} -package main; - - sub usage { print <<EOT; -git send-email [options] <file | directory | rev-list options > +git send-email' [<options>] <file|directory> +git send-email' [<options>] <format-patch options> git send-email --dump-aliases Composing: @@ -70,6 +49,7 @@ git send-email --dump-aliases Sending: --envelope-sender <str> * Email envelope sender. + --sendmail-cmd <str> * Command to run to send email. --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. @@ -95,8 +75,10 @@ git send-email --dump-aliases 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` + --to-cmd <str> * Email To: via `<str> \$patch_path`. + --cc-cmd <str> * Email Cc: via `<str> \$patch_path`. + --header-cmd <str> * Add headers via `<str> \$patch_path`. + --no-header-cmd * Disable any header command in use. --suppress-cc <str> * author, self, sob, cc, cccmd, body, bodycc, misc-by, all. --[no-]cc-cover * Email Cc: addresses in the cover letter. --[no-]to-cover * Email To: addresses in the cover letter. @@ -122,9 +104,38 @@ EOT exit(1); } +sub uniq { + my %seen; + grep !$seen{$_}++, @_; +} + sub completion_helper { - print Git::command('format-patch', '--git-completion-helper'); - exit(0); + my ($original_opts) = @_; + my %not_for_completion = ( + "git-completion-helper" => undef, + "h" => undef, + ); + my @send_email_opts = (); + + foreach my $key (keys %$original_opts) { + unless (exists $not_for_completion{$key}) { + $key =~ s/!$//; + + if ($key =~ /[:=][si]$/) { + $key =~ s/[:=][si]$//; + push (@send_email_opts, "--$_=") foreach (split (/\|/, $key)); + } else { + push (@send_email_opts, "--$_") foreach (split (/\|/, $key)); + } + } + } + + my @format_patch_opts = split(/ /, Git::command('format-patch', '--git-completion-helper')); + my @opts = (@send_email_opts, @format_patch_opts); + @opts = uniq (grep !/^$/, @opts); + # There's an implicit '\n' here already, no need to add an explicit one. + print "@opts"; + exit(0); } # most mail servers generate the Date: header, but not all... @@ -166,7 +177,6 @@ sub format_2822_time { ); } -my $have_email_valid = eval { require Email::Valid; 1 }; my $smtp; my $auth; my $num_sent = 0; @@ -182,7 +192,7 @@ my (@to,@cc,@xh,$envelope_sender, $author,$sender,$smtp_authpass,$annotate,$compose,$time); # Things we either get from config, *or* are overridden on the # command-line. -my ($no_cc, $no_to, $no_bcc, $no_identity); +my ($no_cc, $no_to, $no_bcc, $no_identity, $no_header_cmd); my (@config_to, @getopt_to); my (@config_cc, @getopt_cc); my (@config_bcc, @getopt_bcc); @@ -192,14 +202,6 @@ my (@config_bcc, @getopt_bcc); my $repo = eval { Git->repository() }; my @repo = $repo ? ($repo) : (); -my $term = eval { - $ENV{"GIT_SEND_EMAIL_NOTTY"} - ? new Term::ReadLine 'git-send-email', \*STDIN, \*STDOUT - : new Term::ReadLine 'git-send-email'; -}; -if ($@) { - $term = new FakeTerm "$@: going non-interactive"; -} # Behavior modification variables my ($quiet, $dry_run) = (0, 0); @@ -208,33 +210,56 @@ my $compose_filename; my $force = 0; my $dump_aliases = 0; +# Variables to prevent short format-patch options from being captured +# as abbreviated send-email options +my $reroll_count; + # Handle interactive edition of files. my $multiedit; my $editor; +sub system_or_msg { + my ($args, $msg, $cmd_name) = @_; + system(@$args); + my $signalled = $? & 127; + my $exit_code = $? >> 8; + return unless $signalled or $exit_code; + + my @sprintf_args = ($cmd_name ? $cmd_name : $args->[0], $exit_code); + if (defined $msg) { + # Quiet the 'redundant' warning category, except we + # need to support down to Perl 5.8, so we can't do a + # "no warnings 'redundant'", since that category was + # introduced in perl 5.22, and asking for it will die + # on older perls. + no warnings; + return sprintf($msg, @sprintf_args); + } + return sprintf(__("fatal: command '%s' died with exit code %d"), + @sprintf_args); +} + +sub system_or_die { + my $msg = system_or_msg(@_); + die $msg if $msg; +} + sub do_edit { if (!defined($editor)) { $editor = Git::command_oneline('var', 'GIT_EDITOR'); } + my $die_msg = __("the editor exited uncleanly, aborting everything"); if (defined($multiedit) && !$multiedit) { - map { - system('sh', '-c', $editor.' "$@"', $editor, $_); - if (($? & 127) || ($? >> 8)) { - die(__("the editor exited uncleanly, aborting everything")); - } - } @_; + system_or_die(['sh', '-c', $editor.' "$@"', $editor, $_], $die_msg) for @_; } else { - system('sh', '-c', $editor.' "$@"', $editor, @_); - if (($? & 127) || ($? >> 8)) { - die(__("the editor exited uncleanly, aborting everything")); - } + system_or_die(['sh', '-c', $editor.' "$@"', $editor, @_], $die_msg); } } # Variables with corresponding config settings my ($suppress_from, $signed_off_by_cc); my ($cover_cc, $cover_to); -my ($to_cmd, $cc_cmd); +my ($to_cmd, $cc_cmd, $header_cmd); my ($smtp_server, $smtp_server_port, @smtp_server_options); my ($smtp_authuser, $smtp_encryption, $smtp_ssl_cert_path); my ($batch_size, $relogin_delay); @@ -243,6 +268,7 @@ my ($confirm); my (@suppress_cc); my ($auto_8bit_encoding); my ($compose_encoding); +my ($sendmail_cmd); # Variables with corresponding config settings & hardcoded defaults my ($debug_net_smtp) = 0; # Net::SMTP, see send_message() my $thread = 1; @@ -268,6 +294,7 @@ my %config_bool_settings = ( ); my %config_settings = ( + "smtpencryption" => \$smtp_encryption, "smtpserver" => \$smtp_server, "smtpserverport" => \$smtp_server_port, "smtpserveroption" => \@smtp_server_options, @@ -281,6 +308,7 @@ my %config_settings = ( "tocmd" => \$to_cmd, "cc" => \@config_cc, "cccmd" => \$cc_cmd, + "headercmd" => \$header_cmd, "aliasfiletype" => \$aliasfiletype, "bcc" => \@config_bcc, "suppresscc" => \@suppress_cc, @@ -290,6 +318,7 @@ my %config_settings = ( "assume8bitencoding" => \$auto_8bit_encoding, "composeencoding" => \$compose_encoding, "transferencoding" => \$target_xfer_encoding, + "sendmailcmd" => \$sendmail_cmd, ); my %config_path_settings = ( @@ -299,9 +328,9 @@ my %config_path_settings = ( # Handle Uncouth Termination sub signal_handler { - # Make text normal - print color("reset"), "\n"; + require Term::ANSIColor; + print Term::ANSIColor::color("reset"), "\n"; # SMTP password masked system "stty echo"; @@ -327,11 +356,17 @@ $SIG{INT} = \&signal_handler; # Read our sendemail.* config sub read_config { - my ($configured, $prefix) = @_; + my ($known_keys, $configured, $prefix) = @_; foreach my $setting (keys %config_bool_settings) { my $target = $config_bool_settings{$setting}; - my $v = Git::config_bool(@repo, "$prefix.$setting"); + my $key = "$prefix.$setting"; + next unless exists $known_keys->{$key}; + my $v = (@{$known_keys->{$key}} == 1 && + (defined $known_keys->{$key}->[0] && + $known_keys->{$key}->[0] =~ /^(?:true|false)$/s)) + ? $known_keys->{$key}->[0] eq 'true' + : Git::config_bool(@repo, $key); next unless defined $v; next if $configured->{$setting}++; $$target = $v; @@ -339,8 +374,10 @@ sub read_config { foreach my $setting (keys %config_path_settings) { my $target = $config_path_settings{$setting}; + my $key = "$prefix.$setting"; + next unless exists $known_keys->{$key}; if (ref($target) eq "ARRAY") { - my @values = Git::config_path(@repo, "$prefix.$setting"); + my @values = Git::config_path(@repo, $key); next unless @values; next if $configured->{$setting}++; @$target = @values; @@ -355,48 +392,77 @@ sub read_config { foreach my $setting (keys %config_settings) { my $target = $config_settings{$setting}; + my $key = "$prefix.$setting"; + next unless exists $known_keys->{$key}; if (ref($target) eq "ARRAY") { - my @values = Git::config(@repo, "$prefix.$setting"); - next unless @values; + my @values = @{$known_keys->{$key}}; + @values = grep { defined } @values; next if $configured->{$setting}++; @$target = @values; } else { - my $v = Git::config(@repo, "$prefix.$setting"); + my $v = $known_keys->{$key}->[-1]; next unless defined $v; next if $configured->{$setting}++; $$target = $v; } } +} - if (!defined $smtp_encryption) { - my $setting = "$prefix.smtpencryption"; - my $enc = Git::config(@repo, $setting); - return unless defined $enc; - return if $configured->{$setting}++; - if (defined $enc) { - $smtp_encryption = $enc; - } elsif (Git::config_bool(@repo, "$prefix.smtpssl")) { - $smtp_encryption = 'ssl'; - } +sub config_regexp { + my ($regex) = @_; + my @ret; + eval { + my $ret = Git::command( + 'config', + '--null', + '--get-regexp', + $regex, + ); + @ret = map { + # We must always return ($k, $v) here, since + # empty config values will be just "key\0", + # not "key\nvalue\0". + my ($k, $v) = split /\n/, $_, 2; + ($k, $v); + } split /\0/, $ret; + 1; + } or do { + # If we have no keys we're OK, otherwise re-throw + die $@ if $@->value != 1; + }; + return @ret; +} + +# Save ourselves a lot of work of shelling out to 'git config' (it +# parses 'bool' etc.) by only doing so for config keys that exist. +my %known_config_keys; +{ + my @kv = config_regexp("^sende?mail[.]"); + while (my ($k, $v) = splice @kv, 0, 2) { + push @{$known_config_keys{$k}} => $v; } } # sendemail.identity yields to --identity. We must parse this # special-case first before the rest of the config is read. -$identity = Git::config(@repo, "sendemail.identity"); -my $rc = GetOptions( +{ + my $key = "sendemail.identity"; + $identity = Git::config(@repo, $key) if exists $known_config_keys{$key}; +} +my %identity_options = ( "identity=s" => \$identity, "no-identity" => \$no_identity, ); +my $rc = GetOptions(%identity_options); usage() unless $rc; undef $identity if $no_identity; # Now we know enough to read the config { my %configured; - read_config(\%configured, "sendemail.$identity") if defined $identity; - read_config(\%configured, "sendemail"); + read_config(\%known_config_keys, \%configured, "sendemail.$identity") if defined $identity; + read_config(\%known_config_keys, \%configured, "sendemail"); } # Begin by accumulating all the variables (defined above), that we will end up @@ -404,14 +470,17 @@ undef $identity if $no_identity; my $help; my $git_completion_helper; -$rc = GetOptions("h" => \$help, - "dump-aliases" => \$dump_aliases); +my %dump_aliases_options = ( + "h" => \$help, + "dump-aliases" => \$dump_aliases, +); +$rc = GetOptions(%dump_aliases_options); usage() unless $rc; die __("--dump-aliases incompatible with other options\n") if !$help and $dump_aliases and @ARGV; -$rc = GetOptions( +my %options = ( "sender|from=s" => \$sender, - "in-reply-to=s" => \$initial_in_reply_to, + "in-reply-to=s" => \$initial_in_reply_to, "reply-to=s" => \$reply_to, "subject=s" => \$initial_subject, "to=s" => \@getopt_to, @@ -423,6 +492,7 @@ $rc = GetOptions( "no-bcc" => \$no_bcc, "chain-reply-to!" => \$chain_reply_to, "no-chain-reply-to" => sub {$chain_reply_to = 0}, + "sendmail-cmd=s" => \$sendmail_cmd, "smtp-server=s" => \$smtp_server, "smtp-server-option=s" => \@smtp_server_options, "smtp-server-port=s" => \$smtp_server_port, @@ -440,6 +510,8 @@ $rc = GetOptions( "compose" => \$compose, "quiet" => \$quiet, "cc-cmd=s" => \$cc_cmd, + "header-cmd=s" => \$header_cmd, + "no-header-cmd" => \$no_header_cmd, "suppress-from!" => \$suppress_from, "no-suppress-from" => sub {$suppress_from = 0}, "suppress-cc=s" => \@suppress_cc, @@ -467,7 +539,9 @@ $rc = GetOptions( "batch-size=i" => \$batch_size, "relogin-delay=i" => \$relogin_delay, "git-completion-helper" => \$git_completion_helper, - ); + "v=s" => \$reroll_count, +); +$rc = GetOptions(%options); # Munge any "either config or getopt, not both" variables my @initial_to = @getopt_to ? @getopt_to : ($no_to ? () : @config_to); @@ -475,12 +549,13 @@ my @initial_cc = @getopt_cc ? @getopt_cc : ($no_cc ? () : @config_cc); my @initial_bcc = @getopt_bcc ? @getopt_bcc : ($no_bcc ? () : @config_bcc); usage() if $help; -completion_helper() if $git_completion_helper; +my %all_options = (%options, %dump_aliases_options, %identity_options); +completion_helper(\%all_options) if $git_completion_helper; unless ($rc) { usage(); } -if ($forbid_sendmail_variables && (scalar Git::config_regexp("^sendmail[.]")) != 0) { +if ($forbid_sendmail_variables && grep { /^sendmail/s } keys %known_config_keys) { die __("fatal: found configuration options for 'sendmail'\n" . "git-send-email is configured with the sendemail.* options - note the 'e'.\n" . "Set sendemail.forbidSendmailVariables to false to disable this check.\n"); @@ -545,15 +620,27 @@ if (0) { } my ($repoauthor, $repocommitter); -($repoauthor) = Git::ident_person(@repo, 'author'); -($repocommitter) = Git::ident_person(@repo, 'committer'); +{ + my %cache; + my ($author, $committer); + my $common = sub { + my ($what) = @_; + return $cache{$what} if exists $cache{$what}; + ($cache{$what}) = Git::ident_person(@repo, $what); + return $cache{$what}; + }; + $repoauthor = sub { $common->('author') }; + $repocommitter = sub { $common->('committer') }; +} sub parse_address_line { + require Git::LoadCPAN::Mail::Address; return map { $_->format } Mail::Address->parse($_[0]); } sub split_addrs { - return quotewords('\s*,\s*', 1, @_); + require Text::ParseWords; + return Text::ParseWords::quotewords('\s*,\s*', 1, @_); } my %aliases; @@ -602,10 +689,11 @@ my %parse_alias = ( s/\\"/"/g foreach @addr; $aliases{$alias} = \@addr }}}, - mailrc => sub { my $fh = shift; while (<$fh>) { + mailrc => sub { my $fh = shift; while (<$fh>) { if (/^alias\s+(\S+)\s+(.*?)\s*$/) { + require Text::ParseWords; # spaces delimit multiple addresses - $aliases{$1} = [ quotewords('\s+', 0, $2) ]; + $aliases{$1} = [ Text::ParseWords::quotewords('\s+', 0, $2) ]; }}}, pine => sub { my $fh = shift; my $f='\t[^\t]*'; for (my $x = ''; defined($x); $x = $_) { @@ -653,7 +741,7 @@ sub is_format_patch_arg { if (defined($format_patch)) { return $format_patch; } - die sprintf(__ <<EOF, $f, $f); + die sprintf(__(<<EOF), $f, $f); File '%s' exists but it could also be the range of commits to produce patches for. Please disambiguate by... @@ -677,7 +765,8 @@ while (defined(my $f = shift @ARGV)) { opendir my $dh, $f or die sprintf(__("Failed to opendir %s: %s"), $f, $!); - push @files, grep { -f $_ } map { catfile($f, $_) } + require File::Spec; + push @files, grep { -f $_ } map { File::Spec->catfile($f, $_) } sort readdir $dh; closedir $dh; } elsif ((-f $f or -p $f) and !is_format_patch_arg($f)) { @@ -690,21 +779,52 @@ while (defined(my $f = shift @ARGV)) { if (@rev_list_opts) { die __("Cannot run git format-patch from outside a repository\n") unless $repo; - push @files, $repo->command('format-patch', '-o', tempdir(CLEANUP => 1), @rev_list_opts); + require File::Temp; + push @files, $repo->command('format-patch', '-o', File::Temp::tempdir(CLEANUP => 1), + defined $reroll_count ? ('-v', $reroll_count) : (), + @rev_list_opts); } -@files = handle_backup_files(@files); +if (defined $sender) { + $sender =~ s/^\s+|\s+$//g; + ($sender) = expand_aliases($sender); +} else { + $sender = $repoauthor->() || $repocommitter->() || ''; +} + +# $sender could be an already sanitized address +# (e.g. sendemail.from could be manually sanitized by user). +# But it's a no-op to run sanitize_address on an already sanitized address. +$sender = sanitize_address($sender); + +$time = time - scalar $#files; if ($validate) { + # FIFOs can only be read once, exclude them from validation. + my @real_files = (); foreach my $f (@files) { unless (-p $f) { - my $error = validate_patch($f, $target_xfer_encoding); - $error and die sprintf(__("fatal: %s: %s\nwarning: no patches were sent\n"), - $f, $error); + push(@real_files, $f); } } + + # Run the loop once again to avoid gaps in the counter due to FIFO + # arguments provided by the user. + my $num = 1; + my $num_files = scalar @real_files; + $ENV{GIT_SENDEMAIL_FILE_TOTAL} = "$num_files"; + foreach my $r (@real_files) { + $ENV{GIT_SENDEMAIL_FILE_COUNTER} = "$num"; + pre_process_file($r, 1); + validate_patch($r, $target_xfer_encoding); + $num += 1; + } + delete $ENV{GIT_SENDEMAIL_FILE_COUNTER}; + delete $ENV{GIT_SENDEMAIL_FILE_TOTAL}; } +@files = handle_backup_files(@files); + if (@files) { unless ($quiet) { print $_,"\n" for (@files); @@ -729,19 +849,20 @@ sub get_patch_subject { if ($compose) { # Note that this does not need to be secure, but we will make a small # effort to have it be unique + require File::Temp; $compose_filename = ($repo ? - tempfile(".gitsendemail.msg.XXXXXX", DIR => $repo->repo_path()) : - tempfile(".gitsendemail.msg.XXXXXX", DIR => "."))[1]; + File::Temp::tempfile(".gitsendemail.msg.XXXXXX", DIR => $repo->repo_path()) : + File::Temp::tempfile(".gitsendemail.msg.XXXXXX", DIR => "."))[1]; open my $c, ">", $compose_filename or die sprintf(__("Failed to open for writing %s: %s"), $compose_filename, $!); - my $tpl_sender = $sender || $repoauthor || $repocommitter || ''; + my $tpl_sender = $sender || $repoauthor->() || $repocommitter->() || ''; my $tpl_subject = $initial_subject || ''; my $tpl_in_reply_to = $initial_in_reply_to || ''; my $tpl_reply_to = $reply_to || ''; - print $c <<EOT1, Git::prefix_lines("GIT: ", __ <<EOT2), <<EOT3; + print $c <<EOT1, Git::prefix_lines("GIT: ", __(<<EOT2)), <<EOT3; From $tpl_sender # This line is ignored. EOT1 Lines beginning in "GIT:" will be removed. @@ -838,6 +959,21 @@ EOT3 do_edit(@files); } +{ + # Only instantiate one $term per program run, since some + # Term::ReadLine providers refuse to create a second instance. + my $term; + sub term { + require Term::ReadLine; + if (!defined $term) { + $term = $ENV{"GIT_SEND_EMAIL_NOTTY"} + ? Term::ReadLine->new('git-send-email', \*STDIN, \*STDOUT) + : Term::ReadLine->new('git-send-email'); + } + return $term; + } +} + sub ask { my ($prompt, %arg) = @_; my $valid_re = $arg{valid_re}; @@ -845,6 +981,7 @@ sub ask { my $confirm_only = $arg{confirm_only}; my $resp; my $i = 0; + my $term = term(); return defined $default ? $default : undef unless defined $term->IN and defined fileno($term->IN) and defined $term->OUT and defined fileno($term->OUT); @@ -938,18 +1075,6 @@ if (!$force) { } } -if (defined $sender) { - $sender =~ s/^\s+|\s+$//g; - ($sender) = expand_aliases($sender); -} else { - $sender = $repoauthor || $repocommitter || ''; -} - -# $sender could be an already sanitized address -# (e.g. sendemail.from could be manually sanitized by user). -# But it's a no-op to run sanitize_address on an already sanitized address. -$sender = sanitize_address($sender); - my $to_whom = __("To whom should the emails be sent (if anyone)?"); my $prompting = 0; if (!@initial_to && !defined $to_cmd) { @@ -996,16 +1121,19 @@ if (defined $reply_to) { $reply_to = sanitize_address($reply_to); } -if (!defined $smtp_server) { +if (!defined $sendmail_cmd && !defined $smtp_server) { my @sendmail_paths = qw( /usr/sbin/sendmail /usr/lib/sendmail ); push @sendmail_paths, map {"$_/sendmail"} split /:/, $ENV{PATH}; foreach (@sendmail_paths) { if (-x $_) { - $smtp_server = $_; + $sendmail_cmd = $_; last; } } - $smtp_server ||= 'localhost'; # could be 127.0.0.1, too... *shrug* + + if (!defined $sendmail_cmd) { + $smtp_server = 'localhost'; # could be 127.0.0.1, too... *shrug* + } } if ($compose && $compose > 0) { @@ -1025,6 +1153,7 @@ sub extract_valid_address { return $address if ($address =~ /^($local_part_regexp)$/); $address =~ s/^\s*<(.*)>\s*$/$1/; + my $have_email_valid = eval { require Email::Valid; 1 }; if ($have_email_valid) { return scalar Email::Valid->address($address); } @@ -1037,10 +1166,10 @@ sub extract_valid_address { sub extract_valid_address_or_die { my $address = shift; - $address = extract_valid_address($address); + my $valid_address = extract_valid_address($address); die sprintf(__("error: unable to extract a valid address from: %s\n"), $address) - if !$address; - return $address; + if !$valid_address; + return $valid_address; } sub validate_address { @@ -1084,14 +1213,15 @@ my ($message_id_stamp, $message_id_serial); sub make_message_id { my $uniq; if (!defined $message_id_stamp) { - $message_id_stamp = strftime("%Y%m%d%H%M%S.$$", gmtime(time)); + require POSIX; + $message_id_stamp = POSIX::strftime("%Y%m%d%H%M%S.$$", gmtime(time)); $message_id_serial = 0; } $message_id_serial++; $uniq = "$message_id_stamp-$message_id_serial"; my $du_part; - for ($sender, $repocommitter, $repoauthor) { + for ($sender, $repocommitter->(), $repoauthor->()) { $du_part = extract_valid_address(sanitize_address($_)); last if (defined $du_part and $du_part ne ''); } @@ -1104,10 +1234,6 @@ sub make_message_id { #print "new message id = $message_id\n"; # Was useful for debugging } - - -$time = time - scalar $#files; - sub unquote_rfc2047 { local ($_) = @_; my $charset; @@ -1254,6 +1380,7 @@ sub valid_fqdn { sub maildomain_net { my $maildomain; + require Net::Domain; my $domain = Net::Domain::domainname(); $maildomain = $domain if valid_fqdn($domain); @@ -1264,6 +1391,7 @@ sub maildomain_mta { my $maildomain; for my $host (qw(mailhost localhost)) { + require Net::SMTP; my $smtp = Net::SMTP->new($host); if (defined $smtp) { my $domain = $smtp->domain; @@ -1383,16 +1511,7 @@ sub file_name_is_absolute { return File::Spec::Functions::file_name_is_absolute($path); } -# Prepares the email, then asks the user what to do. -# -# If the user chooses to send the email, it's sent and 1 is returned. -# If the user chooses not to send the email, 0 is returned. -# If the user decides they want to make further edits, -1 is returned and the -# caller is expected to call send_message again after the edits are performed. -# -# If an error occurs sending the email, this just dies. - -sub send_message { +sub gen_header { my @recipients = unique_email_list(@to); @cc = (grep { my $cc = extract_valid_address_or_die($_); not grep { $cc eq $_ || $_ =~ /<\Q${cc}\E>$/ } @recipients @@ -1418,7 +1537,7 @@ sub send_message { To: $to${ccline} Subject: $subject Date: $date -Message-Id: $message_id +Message-ID: $message_id "; if ($use_xmailer) { $header .= "X-Mailer: git-send-email $gitversion\n"; @@ -1434,6 +1553,22 @@ Message-Id: $message_id if (@xh) { $header .= join("\n", @xh) . "\n"; } + my $recipients_ref = \@recipients; + return ($recipients_ref, $to, $date, $gitversion, $cc, $ccline, $header); +} + +# Prepares the email, then asks the user what to do. +# +# If the user chooses to send the email, it's sent and 1 is returned. +# If the user chooses not to send the email, 0 is returned. +# If the user decides they want to make further edits, -1 is returned and the +# caller is expected to call send_message again after the edits are performed. +# +# If an error occurs sending the email, this just dies. + +sub send_message { + my ($recipients_ref, $to, $date, $gitversion, $cc, $ccline, $header) = gen_header(); + my @recipients = @$recipients_ref; my @sendmail_parameters = ('-i', @recipients); my $raw_from = $sender; @@ -1485,11 +1620,17 @@ EOF if ($dry_run) { # We don't want to send the email. - } elsif (file_name_is_absolute($smtp_server)) { + } elsif (defined $sendmail_cmd || file_name_is_absolute($smtp_server)) { my $pid = open my $sm, '|-'; defined $pid or die $!; if (!$pid) { - exec($smtp_server, @sendmail_parameters) or die $!; + if (defined $sendmail_cmd) { + exec ("sh", "-c", "$sendmail_cmd \"\$@\"", "-", @sendmail_parameters) + or die $!; + } else { + exec ($smtp_server, @sendmail_parameters) + or die $!; + } } print $sm "$header\n$message"; close $sm or die $!; @@ -1585,14 +1726,21 @@ EOF printf($dry_run ? __("Dry-Sent %s\n") : __("Sent %s\n"), $subject); } else { print($dry_run ? __("Dry-OK. Log says:\n") : __("OK. Log says:\n")); - if (!file_name_is_absolute($smtp_server)) { + if (!defined $sendmail_cmd && !file_name_is_absolute($smtp_server)) { print "Server: $smtp_server\n"; print "MAIL FROM:<$raw_from>\n"; foreach my $entry (@recipients) { print "RCPT TO:<$entry>\n"; } } else { - print "Sendmail: $smtp_server ".join(' ',@sendmail_parameters)."\n"; + my $sm; + if (defined $sendmail_cmd) { + $sm = $sendmail_cmd; + } else { + $sm = $smtp_server; + } + + print "Sendmail: $sm ".join(' ',@sendmail_parameters)."\n"; } print $header, "\n"; if ($smtp) { @@ -1608,14 +1756,10 @@ EOF $in_reply_to = $initial_in_reply_to; $references = $initial_in_reply_to || ''; -$subject = $initial_subject; $message_num = 0; -# Prepares the email, prompts the user, sends it out -# Returns 0 if an edit was done and the function should be called again, or 1 -# otherwise. -sub process_file { - my ($t) = @_; +sub pre_process_file { + my ($t, $quiet) = @_; open my $fh, "<", $t or die sprintf(__("can't open file %s"), $t); @@ -1631,18 +1775,20 @@ sub process_file { @xh = (); my $input_format = undef; my @header = (); + $subject = $initial_subject; $message = ""; $message_num++; - # First unfold multiline header fields + undef $message_id; + # Retrieve and unfold header fields. + my @header_lines = (); while(<$fh>) { last if /^\s*$/; - if (/^\s+\S/ and @header) { - chomp($header[$#header]); - s/^\s+/ /; - $header[$#header] .= $_; - } else { - push(@header, $_); - } + push(@header_lines, $_); + } + @header = unfold_headers(@header_lines); + # Add computed headers, if applicable. + unless ($no_header_cmd || ! $header_cmd) { + push @header, invoke_header_cmd($header_cmd, $t); } # Now parse the header foreach(@header) { @@ -1700,7 +1846,7 @@ sub process_file { $has_mime_version = 1; push @xh, $_; } - elsif (/^Message-Id: (.*)/i) { + elsif (/^Message-ID: (.*)/i) { $message_id = $1; } elsif (/^Content-Transfer-Encoding: (.*)/i) { @@ -1768,9 +1914,9 @@ sub process_file { } close $fh; - push @to, recipients_cmd("to-cmd", "to", $to_cmd, $t) + push @to, recipients_cmd("to-cmd", "to", $to_cmd, $t, $quiet) if defined $to_cmd; - push @cc, recipients_cmd("cc-cmd", "cc", $cc_cmd, $t) + push @cc, recipients_cmd("cc-cmd", "cc", $cc_cmd, $t, $quiet) if defined $cc_cmd && !$suppress_cc{'cccmd'}; if ($broken_encoding{$t} && !$has_content_type) { @@ -1829,6 +1975,15 @@ sub process_file { @initial_to = @to; } } +} + +# Prepares the email, prompts the user, and sends it out +# Returns 0 if an edit was done and the function should be called again, or 1 +# on the email being successfully sent out. +sub process_file { + my ($t) = @_; + + pre_process_file($t, $quiet); my $message_was_sent = send_message(); if ($message_was_sent == -1) { @@ -1837,15 +1992,23 @@ sub process_file { } # set up for the next message - if ($thread && $message_was_sent && - ($chain_reply_to || !defined $in_reply_to || length($in_reply_to) == 0 || - $message_num == 1)) { - $in_reply_to = $message_id; - if (length $references > 0) { - $references .= "\n $message_id"; - } else { - $references = "$message_id"; + if ($thread) { + if ($message_was_sent && + ($chain_reply_to || !defined $in_reply_to || length($in_reply_to) == 0 || + $message_num == 1)) { + $in_reply_to = $message_id; + if (length $references > 0) { + $references .= "\n $message_id"; + } else { + $references = "$message_id"; + } } + } elsif (!defined $initial_in_reply_to) { + # --thread and --in-reply-to manage the "In-Reply-To" header and by + # extension the "References" header. If these commands are not used, reset + # the header values to their defaults. + $in_reply_to = undef; + $references = ''; } $message_id = undef; $num_sent++; @@ -1866,15 +2029,64 @@ foreach my $t (@files) { } } +# Execute a command and return its output lines as an array. Blank +# lines which do not appear at the end of the output are reported as +# errors. +sub execute_cmd { + my ($prefix, $cmd, $file) = @_; + my @lines = (); + my $seen_blank_line = 0; + open my $fh, "-|", "$cmd \Q$file\E" + or die sprintf(__("(%s) Could not execute '%s'"), $prefix, $cmd); + while (my $line = <$fh>) { + die sprintf(__("(%s) Malformed output from '%s'"), $prefix, $cmd) + if $seen_blank_line; + if ($line =~ /^$/) { + $seen_blank_line = $line =~ /^$/; + next; + } + push @lines, $line; + } + close $fh + or die sprintf(__("(%s) failed to close pipe to '%s'"), $prefix, $cmd); + return @lines; +} + +# Process headers lines, unfolding multiline headers as defined by RFC +# 2822. +sub unfold_headers { + my @headers; + foreach(@_) { + last if /^\s*$/; + if (/^\s+\S/ and @headers) { + chomp($headers[$#headers]); + s/^\s+/ /; + $headers[$#headers] .= $_; + } else { + push(@headers, $_); + } + } + return @headers; +} + +# Invoke the provided CMD with FILE as an argument, which should +# output RFC 2822 email headers. Fold multiline headers and return the +# headers as an array. +sub invoke_header_cmd { + my ($cmd, $file) = @_; + my @lines = execute_cmd("header-cmd", $header_cmd, $file); + return unfold_headers(@lines); +} + # 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 ($prefix, $what, $cmd, $file, $quiet) = @_; + my @lines = (); my @addresses = (); - open my $fh, "-|", "$cmd \Q$file\E" - or die sprintf(__("(%s) Could not execute '%s'"), $prefix, $cmd); - while (my $address = <$fh>) { + + @lines = execute_cmd($prefix, $cmd, $file); + for my $address (@lines) { $address =~ s/^\s*//g; $address =~ s/\s*$//g; $address = sanitize_address($address); @@ -1883,8 +2095,6 @@ sub recipients_cmd { printf(__("(%s) Adding %s: %s from: '%s'\n"), $prefix, $what, $address, $cmd) unless $quiet; } - close $fh - or die sprintf(__("(%s) failed to close pipe to '%s'"), $prefix, $cmd); return @addresses; } @@ -1942,21 +2152,43 @@ sub validate_patch { my ($fn, $xfer_encoding) = @_; if ($repo) { - my $validate_hook = catfile(catdir($repo->repo_path(), 'hooks'), - 'sendemail-validate'); + my $hook_name = 'sendemail-validate'; + my $hooks_path = $repo->command_oneline('rev-parse', '--git-path', 'hooks'); + require File::Spec; + my $validate_hook = File::Spec->catfile($hooks_path, $hook_name); my $hook_error; if (-x $validate_hook) { - my $target = abs_path($fn); + require Cwd; + my $target = Cwd::abs_path($fn); # The hook needs a correct cwd and GIT_DIR. - my $cwd_save = cwd(); + my $cwd_save = Cwd::getcwd(); chdir($repo->wc_path() or $repo->repo_path()) or die("chdir: $!"); local $ENV{"GIT_DIR"} = $repo->repo_path(); - $hook_error = "rejected by sendemail-validate hook" - if system($validate_hook, $target); + + my ($recipients_ref, $to, $date, $gitversion, $cc, $ccline, $header) = gen_header(); + + require File::Temp; + my ($header_filehandle, $header_filename) = File::Temp::tempfile( + TEMPLATE => ".gitsendemail.header.XXXXXX", + DIR => $repo->repo_path(), + UNLINK => 1, + ); + print $header_filehandle $header; + + my @cmd = ("git", "hook", "run", "--ignore-missing", + $hook_name, "--"); + my @cmd_msg = (@cmd, "<patch>", "<header>"); + my @cmd_run = (@cmd, $target, $header_filename); + $hook_error = system_or_msg(\@cmd_run, undef, "@cmd_msg"); chdir($cwd_save) or die("chdir: $!"); } - return $hook_error if $hook_error; + if ($hook_error) { + $hook_error = sprintf( + __("fatal: %s: rejected by %s hook\n%s\nwarning: no patches were sent\n"), + $fn, $hook_name, $hook_error); + die $hook_error; + } } # Any long lines will be automatically fixed if we use a suitable transfer @@ -1966,7 +2198,8 @@ sub validate_patch { or die sprintf(__("unable to open %s: %s\n"), $fn, $!); while (my $line = <$fh>) { if (length($line) > 998) { - return sprintf(__("%s: patch contains a line longer than 998 characters"), $.); + die sprintf(__("fatal: %s:%d is longer than 998 characters\n" . + "warning: no patches were sent\n"), $fn, $.); } } } |
