diff options
Diffstat (limited to 'git-send-email.perl')
| -rwxr-xr-x | git-send-email.perl | 234 |
1 files changed, 209 insertions, 25 deletions
diff --git a/git-send-email.perl b/git-send-email.perl index f0be4b4560..cd4b316ddc 100755 --- a/git-send-email.perl +++ b/git-send-email.perl @@ -16,7 +16,7 @@ # and second line is the subject of the message. # -use 5.008001; +require v5.26; use strict; use warnings $ENV{GIT_PERL_FATAL_WARNINGS} ? qw(FATAL all) : (); use Getopt::Long; @@ -31,6 +31,7 @@ sub usage { git send-email [<options>] <file|directory> git send-email [<options>] <format-patch options> git send-email --dump-aliases +git send-email --translate-aliases Composing: --from <str> * Email From: @@ -40,12 +41,16 @@ git send-email --dump-aliases --subject <str> * Email "Subject:" --reply-to <str> * Email "Reply-To:" --in-reply-to <str> * Email "In-Reply-To:" + --[no-]outlook-id-fix * The SMTP host is an Outlook server that munges the + Message-ID. Retrieve it from the server. --[no-]xmailer * Add "X-Mailer:" header (default). --[no-]annotate * Review each patch that will be sent in an editor. --compose * Open an editor for introduction. --compose-encoding <str> * Encoding to assume for introduction. --8bit-encoding <str> * Encoding to assume 8bit mails if undeclared --transfer-encoding <str> * Transfer encoding to use (quoted-printable, 8bit, base64) + --[no-]mailmap * Use mailmap file to map all email addresses to canonical + real names and email addresses. Sending: --envelope-sender <str> * Email envelope sender. @@ -57,7 +62,7 @@ git send-email --dump-aliases --smtp-user <str> * Username for SMTP-AUTH. --smtp-pass <str> * Password for SMTP-AUTH; not necessary. --smtp-encryption <str> * tls or ssl; anything else disables. - --smtp-ssl * Deprecated. Use '--smtp-encryption ssl'. + --smtp-ssl * Deprecated. Use `--smtp-encryption ssl`. --smtp-ssl-cert-path <str> * Path to ca-certificates (either directory or file). Pass an empty string to disable certificate verification. @@ -65,9 +70,13 @@ git send-email --dump-aliases --smtp-auth <str> * Space-separated list of allowed AUTH mechanisms, or "none" to disable authentication. This setting forces to use one of the listed mechanisms. - --no-smtp-auth Disable SMTP authentication. Shorthand for + --no-smtp-auth * Disable SMTP authentication. Shorthand for `--smtp-auth=none` --smtp-debug <0|1> * Disable, enable Net::SMTP debug. + --imap-sent-folder <str> * IMAP folder where a copy of the emails should be sent. + Make sure `git imap-send` is set up to use this feature. + --[no-]use-imap-only * Only copy emails to the IMAP folder specified by + `--imap-sent-folder` instead of actually sending them. --batch-size <int> * send max <int> message per connection. --relogin-delay <int> * delay <int> seconds between two successive login. @@ -99,6 +108,10 @@ git send-email --dump-aliases Information: --dump-aliases * Dump configured aliases and exit. + --translate-aliases * Translate aliases read from standard + input according to the configured email + alias file(s), outputting the result to + standard output. EOT exit(1); @@ -191,7 +204,7 @@ my $re_encoded_word = qr/=\?($re_token)\?($re_token)\?($re_encoded_text)\?=/; # Variables we fill in automatically, or via prompting: my (@to,@cc,@xh,$envelope_sender, - $initial_in_reply_to,$reply_to,$initial_subject,@files, + $initial_in_reply_to,$reply_to,$initial_subject,@files,@imap_copy, $author,$sender,$smtp_authpass,$annotate,$compose,$time); # Things we either get from config, *or* are overridden on the # command-line. @@ -212,6 +225,7 @@ my $format_patch; my $compose_filename; my $force = 0; my $dump_aliases = 0; +my $translate_aliases = 0; # Variables to prevent short format-patch options from being captured # as abbreviated send-email options @@ -267,19 +281,24 @@ my ($smtp_server, $smtp_server_port, @smtp_server_options); my ($smtp_authuser, $smtp_encryption, $smtp_ssl_cert_path); my ($batch_size, $relogin_delay); my ($identity, $aliasfiletype, @alias_files, $smtp_domain, $smtp_auth); +my ($imap_sent_folder); my ($confirm); my (@suppress_cc); my ($auto_8bit_encoding); my ($compose_encoding); my ($sendmail_cmd); +my ($mailmap_file, $mailmap_blob); # Variables with corresponding config settings & hardcoded defaults my ($debug_net_smtp) = 0; # Net::SMTP, see send_message() my $thread = 1; my $chain_reply_to = 0; my $use_xmailer = 1; my $validate = 1; +my $mailmap = 0; my $target_xfer_encoding = 'auto'; my $forbid_sendmail_variables = 1; +my $outlook_id_fix = 'auto'; +my $use_imap_only = 0; my %config_bool_settings = ( "thread" => \$thread, @@ -294,6 +313,9 @@ my %config_bool_settings = ( "annotate" => \$annotate, "xmailer" => \$use_xmailer, "forbidsendmailvariables" => \$forbid_sendmail_variables, + "mailmap" => \$mailmap, + "outlookidfix" => \$outlook_id_fix, + "useimaponly" => \$use_imap_only, ); my %config_settings = ( @@ -307,6 +329,7 @@ my %config_settings = ( "smtpauth" => \$smtp_auth, "smtpbatchsize" => \$batch_size, "smtprelogindelay" => \$relogin_delay, + "imapsentfolder" => \$imap_sent_folder, "to" => \@config_to, "tocmd" => \$to_cmd, "cc" => \@config_cc, @@ -327,6 +350,8 @@ my %config_settings = ( my %config_path_settings = ( "aliasesfile" => \@alias_files, "smtpsslcertpath" => \$smtp_ssl_cert_path, + "mailmap.file" => \$mailmap_file, + "mailmap.blob" => \$mailmap_blob, ); # Handle Uncouth Termination @@ -476,11 +501,14 @@ my $git_completion_helper; my %dump_aliases_options = ( "h" => \$help, "dump-aliases" => \$dump_aliases, + "translate-aliases" => \$translate_aliases, ); $rc = GetOptions(%dump_aliases_options); usage() unless $rc; die __("--dump-aliases incompatible with other options\n") - if !$help and $dump_aliases and @ARGV; + if !$help and ($dump_aliases or $translate_aliases) and @ARGV; +die __("--dump-aliases and --translate-aliases are mutually exclusive\n") + if !$help and $dump_aliases and $translate_aliases; my %options = ( "sender|from=s" => \$sender, "in-reply-to=s" => \$initial_in_reply_to, @@ -507,6 +535,8 @@ my %options = ( "smtp-domain:s" => \$smtp_domain, "smtp-auth=s" => \$smtp_auth, "no-smtp-auth" => sub {$smtp_auth = 'none'}, + "imap-sent-folder=s" => \$imap_sent_folder, + "use-imap-only!" => \$use_imap_only, "annotate!" => \$annotate, "compose" => \$compose, "quiet" => \$quiet, @@ -524,6 +554,8 @@ my %options = ( "thread!" => \$thread, "validate!" => \$validate, "transfer-encoding=s" => \$target_xfer_encoding, + "mailmap!" => \$mailmap, + "use-mailmap!" => \$mailmap, "format-patch!" => \$format_patch, "8bit-encoding=s" => \$auto_8bit_encoding, "compose-encoding=s" => \$compose_encoding, @@ -533,6 +565,7 @@ my %options = ( "relogin-delay=i" => \$relogin_delay, "git-completion-helper" => \$git_completion_helper, "v=s" => \$reroll_count, + "outlook-id-fix!" => \$outlook_id_fix, ); $rc = GetOptions(%options); @@ -724,6 +757,16 @@ if ($dump_aliases) { exit(0); } +if ($translate_aliases) { + while (<STDIN>) { + my @addr_list = parse_address_line($_); + @addr_list = expand_aliases(@addr_list); + @addr_list = sanitize_address_list(@addr_list); + print "$_\n" for @addr_list; + } + exit(0); +} + # is_format_patch_arg($f) returns 0 if $f names a patch, or 1 if # $f is a revision list specification to be passed to format-patch. sub is_format_patch_arg { @@ -1085,6 +1128,16 @@ if ($compose && $compose > 0) { our ($message_id, %mail, $subject, $in_reply_to, $references, $message, $needs_confirm, $message_num, $ask_default); +sub mailmap_address_list { + return @_ unless @_ and $mailmap; + my @options = (); + push(@options, "--mailmap-file=$mailmap_file") if $mailmap_file; + push(@options, "--mailmap-blob=$mailmap_blob") if $mailmap_blob; + my @addr_list = Git::command('check-mailmap', @options, @_); + s/^<(.*)>$/$1/ for @addr_list; + return @addr_list; +} + sub extract_valid_address { my $address = shift; my $local_part_regexp = qr/[^<>"\s@]+/; @@ -1294,6 +1347,7 @@ sub process_address_list { @addr_list = expand_aliases(@addr_list); @addr_list = sanitize_address_list(@addr_list); @addr_list = validate_address_list(@addr_list); + @addr_list = mailmap_address_list(@addr_list); return @addr_list; } @@ -1315,7 +1369,9 @@ sub process_address_list { sub valid_fqdn { my $domain = shift; - return defined $domain && !($^O eq 'darwin' && $domain =~ /\.local$/) && $domain =~ /\./; + my $subdomain = '(?!-)[A-Za-z0-9-]{1,63}(?<!-)'; + return defined $domain && !($^O eq 'darwin' && $domain =~ /\.local$/) + && $domain =~ /^$subdomain(?:\.$subdomain)*$/; } sub maildomain_net { @@ -1347,8 +1403,22 @@ sub maildomain_mta { return $maildomain; } +sub maildomain_hostname_command { + my $maildomain; + + if ($^O eq 'linux' || $^O eq 'darwin') { + my $domain = `(hostname -f) 2>/dev/null`; + if (!$?) { + chomp($domain); + $maildomain = $domain if valid_fqdn($domain); + } + } + return $maildomain; +} + sub maildomain { - return maildomain_net() || maildomain_mta() || 'localhost.localdomain'; + return maildomain_net() || maildomain_mta() || + maildomain_hostname_command || 'localhost.localdomain'; } sub smtp_host_string { @@ -1380,7 +1450,7 @@ sub smtp_auth_maybe { die "invalid smtp auth: '${smtp_auth}'"; } - # TODO: Authentication may fail not because credentials were + # Authentication may fail not because credentials were # invalid but due to other reasons, in which we should not # reject credentials. $auth = Git::credential({ @@ -1392,24 +1462,61 @@ sub smtp_auth_maybe { 'password' => $smtp_authpass }, sub { my $cred = shift; + my $result; + my $error; + + # catch all SMTP auth error in a unified eval block + eval { + if ($smtp_auth) { + my $sasl = Authen::SASL->new( + mechanism => $smtp_auth, + callback => { + user => $cred->{'username'}, + pass => $cred->{'password'}, + authname => $cred->{'username'}, + } + ); + $result = $smtp->auth($sasl); + } else { + $result = $smtp->auth($cred->{'username'}, $cred->{'password'}); + } + 1; # ensure true value is returned if no exception is thrown + } or do { + $error = $@ || 'Unknown error'; + }; + + return ($error + ? handle_smtp_error($error) + : ($result ? 1 : 0)); + }); - if ($smtp_auth) { - my $sasl = Authen::SASL->new( - mechanism => $smtp_auth, - callback => { - user => $cred->{'username'}, - pass => $cred->{'password'}, - authname => $cred->{'username'}, - } - ); + return $auth; +} - return !!$smtp->auth($sasl); +sub handle_smtp_error { + my ($error) = @_; + + # Parse SMTP status code from error message in: + # https://www.rfc-editor.org/rfc/rfc5321.html + if ($error =~ /\b(\d{3})\b/) { + my $status_code = $1; + if ($status_code =~ /^4/) { + # 4yz: Transient Negative Completion reply + warn "SMTP transient error (status code $status_code): $error"; + return 1; + } elsif ($status_code =~ /^5/) { + # 5yz: Permanent Negative Completion reply + warn "SMTP permanent error (status code $status_code): $error"; + return 0; } + # If no recognized status code is found, treat as transient error + warn "SMTP unknown error: $error. Treating as transient failure."; + return 1; + } - return !!$smtp->auth($cred->{'username'}, $cred->{'password'}); - }); - - return $auth; + # If no status code is found, treat as transient error + warn "SMTP generic error: $error"; + return 1; } sub ssl_verify_params { @@ -1462,7 +1569,7 @@ sub gen_header { @recipients = unique_email_list(@recipients,@cc,@initial_bcc); @recipients = (map { extract_valid_address_or_die($_) } @recipients); my $date = format_2822_time($time++); - my $gitversion = '@@GIT_VERSION@@'; + my $gitversion = '@GIT_VERSION@'; if ($gitversion =~ m/..GIT_VERSION../) { $gitversion = Git::version(); } @@ -1498,6 +1605,16 @@ Message-ID: $message_id return ($recipients_ref, $to, $date, $gitversion, $cc, $ccline, $header); } +sub is_outlook { + my ($host) = @_; + if ($outlook_id_fix eq 'auto') { + $outlook_id_fix = + ($host eq 'smtp.office365.com' || + $host eq 'smtp-mail.outlook.com') ? 1 : 0; + } + return $outlook_id_fix; +} + # 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. @@ -1546,8 +1663,18 @@ EOF default => $ask_default); die __("Send this email reply required") unless defined $_; if (/^n/i) { + # If we are skipping a message, we should make sure that + # the next message is treated as the successor to the + # previously sent message, and not the skipped message. + $message_num--; return 0; } elsif (/^e/i) { + # Since the same message will be sent again, we need to + # decrement the message number to the previous message. + # Otherwise, the edited message will be treated as a + # different message sent after the original non-edited + # message. + $message_num--; return -1; } elsif (/^q/i) { cleanup_compose_files(); @@ -1561,6 +1688,8 @@ EOF if ($dry_run) { # We don't want to send the email. + } elsif ($use_imap_only) { + die __("The destination IMAP folder is not properly defined.") if !defined $imap_sent_folder; } elsif (defined $sendmail_cmd || file_name_is_absolute($smtp_server)) { my $pid = open my $sm, '|-'; defined $pid or die $!; @@ -1661,6 +1790,23 @@ EOF $smtp->datasend("$line") or die $smtp->message; } $smtp->dataend() or die $smtp->message; + + # Outlook discards the Message-ID header we set while sending the email + # and generates a new random Message-ID. So in order to avoid breaking + # threads, we simply retrieve the Message-ID from the server response + # and assign it to the $message_id variable, which will then be + # assigned to $in_reply_to by the caller when the next message is sent + # as a response to this message. + if (is_outlook($smtp_server)) { + if ($smtp->message =~ /<([^>]+)>/) { + $message_id = "<$1>"; + $header =~ s/^(Message-ID:\s*).*\n/${1}$message_id\n/m; + printf __("Outlook reassigned Message-ID to: %s\n"), $message_id if $smtp->debug; + } else { + warn __("Warning: Could not retrieve Message-ID from server response.\n"); + } + } + $smtp->code =~ /250|200/ or die sprintf(__("Failed to send %s\n"), $subject).$smtp->message; } if ($quiet) { @@ -1695,6 +1841,17 @@ EOF print "\n"; } + if ($imap_sent_folder && !$dry_run) { + my $imap_header = $header; + if (@initial_bcc) { + # Bcc is not a part of $header, so we add it here. + # This is only for the IMAP copy, not for the actual email + # sent to the recipients. + $imap_header .= "Bcc: " . join(", ", @initial_bcc) . "\n"; + } + push @imap_copy, "From git-send-email\n$imap_header\n$message"; + } + return 1; } @@ -1797,6 +1954,9 @@ sub pre_process_file { $in_reply_to = $1; } } + elsif (/^Reply-To: (.*)/i) { + $reply_to = $1; + } elsif (/^References: (.*)/i) { if (!$initial_in_reply_to || $thread) { $references = $1; @@ -1847,9 +2007,9 @@ sub pre_process_file { $what, $_) unless $quiet; next; } - push @cc, $c; + push @cc, $sc; printf(__("(body) Adding cc: %s from line '%s'\n"), - $c, $_) unless $quiet; + $sc, $_) unless $quiet; } } close $fh; @@ -1978,6 +2138,17 @@ if ($validate) { } } + # Validate the SMTP server port, if provided. + if (defined $smtp_server_port) { + my $port = Git::port_num($smtp_server_port); + if ($port) { + $smtp_server_port = $port; + } else { + die sprintf(__("error: invalid SMTP port '%s'\n"), + $smtp_server_port); + } + } + # Run the loop once again to avoid gaps in the counter due to FIFO # arguments provided by the user. my $num = 1; @@ -2078,6 +2249,19 @@ sub cleanup_compose_files { $smtp->quit if $smtp; +if ($imap_sent_folder && @imap_copy && !$dry_run) { + my $imap_input = join("\n", @imap_copy); + eval { + print "\nStarting git imap-send...\n"; + my ($fh, $ctx) = Git::command_input_pipe(['imap-send', '-f', $imap_sent_folder]); + print $fh $imap_input; + Git::command_close_pipe($fh, $ctx); + 1; + } or do { + warn "Warning: failed to send messages to IMAP folder $imap_sent_folder: $@"; + }; +} + sub apply_transfer_encoding { my $message = shift; my $from = shift; |
