]> git.ipfire.org Git - thirdparty/git.git/blobdiff - git-send-email.perl
Merge branch 'ln/userdiff-elixir'
[thirdparty/git.git] / git-send-email.perl
index edcc6d34692b28575d9728c25a407782310a39fb..dc95656f75c1c4a066665b91d31b387936998961 100755 (executable)
@@ -26,10 +26,13 @@ use Text::ParseWords;
 use Term::ANSIColor;
 use File::Temp qw/ tempdir tempfile /;
 use File::Spec::Functions qw(catdir catfile);
-use Error qw(:try);
+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 /;
 
@@ -56,6 +59,7 @@ git send-email --dump-aliases
     --[no-]cc               <str>  * Email Cc:
     --[no-]bcc              <str>  * Email Bcc:
     --subject               <str>  * Email "Subject:"
+    --reply-to              <str>  * Email "Reply-To:"
     --in-reply-to           <str>  * Email "In-Reply-To:"
     --[no-]xmailer                 * Add "X-Mailer:" header (default).
     --[no-]annotate                * Review each patch that will be sent in an editor.
@@ -78,8 +82,11 @@ git send-email --dump-aliases
                                      Pass an empty string to disable certificate
                                      verification.
     --smtp-domain           <str>  * The domain name sent to HELO/EHLO handshake
-    --smtp-auth             <str>  * Space-separated list of allowed AUTH mechanisms.
+    --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
+                                     `--smtp-auth=none`
     --smtp-debug            <0|1>  * Disable, enable Net::SMTP debug.
 
     --batch-size            <int>  * send max <int> message per connection.
@@ -90,7 +97,7 @@ git send-email --dump-aliases
     --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.
+    --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.
     --[no-]signed-off-by-cc        * Send to Signed-off-by: addresses. Default on.
@@ -115,6 +122,11 @@ EOT
        exit(1);
 }
 
+sub completion_helper {
+    print Git::command('format-patch', '--git-completion-helper');
+    exit(0);
+}
+
 # most mail servers generate the Date: header, but not all...
 sub format_2822_time {
        my ($time) = @_;
@@ -165,14 +177,18 @@ my $re_encoded_text = qr/[^? \000-\037\177-\377]+/;
 my $re_encoded_word = qr/=\?($re_token)\?($re_token)\?($re_encoded_text)\?=/;
 
 # Variables we fill in automatically, or via prompting:
-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,$use_xmailer,$compose,$time);
-
-my $envelope_sender;
+my (@to,@cc,@xh,$envelope_sender,
+       $initial_in_reply_to,$reply_to,$initial_subject,@files,
+       $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 (@config_to, @getopt_to);
+my (@config_cc, @getopt_cc);
+my (@config_bcc, @getopt_bcc);
 
 # Example reply to:
-#$initial_reply_to = ''; #<20050203173208.GA23964@foobar.com>';
+#$initial_in_reply_to = ''; #<20050203173208.GA23964@foobar.com>';
 
 my $repo = eval { Git->repository() };
 my @repo = $repo ? ($repo) : ();
@@ -216,33 +232,37 @@ sub do_edit {
 }
 
 # Variables with corresponding config settings
-my ($thread, $chain_reply_to, $suppress_from, $signed_off_by_cc);
+my ($suppress_from, $signed_off_by_cc);
 my ($cover_cc, $cover_to);
 my ($to_cmd, $cc_cmd);
 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 ($validate, $confirm);
+my ($confirm);
 my (@suppress_cc);
 my ($auto_8bit_encoding);
 my ($compose_encoding);
-my ($target_xfer_encoding);
-
+# 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 $target_xfer_encoding = 'auto';
 
 my %config_bool_settings = (
-    "thread" => [\$thread, 1],
-    "chainreplyto" => [\$chain_reply_to, 0],
-    "suppressfrom" => [\$suppress_from, undef],
-    "signedoffbycc" => [\$signed_off_by_cc, undef],
-    "cccover" => [\$cover_cc, undef],
-    "tocover" => [\$cover_to, undef],
-    "signedoffcc" => [\$signed_off_by_cc, undef],      # Deprecated
-    "validate" => [\$validate, 1],
-    "multiedit" => [\$multiedit, undef],
-    "annotate" => [\$annotate, undef],
-    "xmailer" => [\$use_xmailer, 1]
+    "thread" => \$thread,
+    "chainreplyto" => \$chain_reply_to,
+    "suppressfrom" => \$suppress_from,
+    "signedoffbycc" => \$signed_off_by_cc,
+    "cccover" => \$cover_cc,
+    "tocover" => \$cover_to,
+    "signedoffcc" => \$signed_off_by_cc,
+    "validate" => \$validate,
+    "multiedit" => \$multiedit,
+    "annotate" => \$annotate,
+    "xmailer" => \$use_xmailer,
 );
 
 my %config_settings = (
@@ -255,12 +275,12 @@ my %config_settings = (
     "smtpauth" => \$smtp_auth,
     "smtpbatchsize" => \$batch_size,
     "smtprelogindelay" => \$relogin_delay,
-    "to" => \@initial_to,
+    "to" => \@config_to,
     "tocmd" => \$to_cmd,
-    "cc" => \@initial_cc,
+    "cc" => \@config_cc,
     "cccmd" => \$cc_cmd,
     "aliasfiletype" => \$aliasfiletype,
-    "bcc" => \@bcclist,
+    "bcc" => \@config_bcc,
     "suppresscc" => \@suppress_cc,
     "envelopesender" => \$envelope_sender,
     "confirm"   => \$confirm,
@@ -303,25 +323,101 @@ sub signal_handler {
 $SIG{TERM} = \&signal_handler;
 $SIG{INT}  = \&signal_handler;
 
+# Read our sendemail.* config
+sub read_config {
+       my ($configured, $prefix) = @_;
+
+       foreach my $setting (keys %config_bool_settings) {
+               my $target = $config_bool_settings{$setting};
+               my $v = Git::config_bool(@repo, "$prefix.$setting");
+               next unless defined $v;
+               next if $configured->{$setting}++;
+               $$target = $v;
+       }
+
+       foreach my $setting (keys %config_path_settings) {
+               my $target = $config_path_settings{$setting};
+               if (ref($target) eq "ARRAY") {
+                       my @values = Git::config_path(@repo, "$prefix.$setting");
+                       next unless @values;
+                       next if $configured->{$setting}++;
+                       @$target = @values;
+               }
+               else {
+                       my $v = Git::config_path(@repo, "$prefix.$setting");
+                       next unless defined $v;
+                       next if $configured->{$setting}++;
+                       $$target = $v;
+               }
+       }
+
+       foreach my $setting (keys %config_settings) {
+               my $target = $config_settings{$setting};
+               if (ref($target) eq "ARRAY") {
+                       my @values = Git::config(@repo, "$prefix.$setting");
+                       next unless @values;
+                       next if $configured->{$setting}++;
+                       @$target = @values;
+               }
+               else {
+                       my $v = Git::config(@repo, "$prefix.$setting");
+                       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';
+               }
+       }
+}
+
+# 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(
+       "identity=s" => \$identity,
+       "no-identity" => \$no_identity,
+);
+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");
+}
+
 # Begin by accumulating all the variables (defined above), that we will end up
 # needing, first, from the command line:
 
 my $help;
-my $rc = GetOptions("h" => \$help,
-                    "dump-aliases" => \$dump_aliases);
+my $git_completion_helper;
+$rc = GetOptions("h" => \$help,
+                 "dump-aliases" => \$dump_aliases);
 usage() unless $rc;
 die __("--dump-aliases incompatible with other options\n")
     if !$help and $dump_aliases and @ARGV;
 $rc = GetOptions(
                    "sender|from=s" => \$sender,
-                    "in-reply-to=s" => \$initial_reply_to,
+                    "in-reply-to=s" => \$initial_in_reply_to,
+                   "reply-to=s" => \$reply_to,
                    "subject=s" => \$initial_subject,
-                   "to=s" => \@initial_to,
+                   "to=s" => \@getopt_to,
                    "to-cmd=s" => \$to_cmd,
                    "no-to" => \$no_to,
-                   "cc=s" => \@initial_cc,
+                   "cc=s" => \@getopt_cc,
                    "no-cc" => \$no_cc,
-                   "bcc=s" => \@bcclist,
+                   "bcc=s" => \@getopt_bcc,
                    "no-bcc" => \$no_bcc,
                    "chain-reply-to!" => \$chain_reply_to,
                    "no-chain-reply-to" => sub {$chain_reply_to = 0},
@@ -336,7 +432,7 @@ $rc = GetOptions(
                    "smtp-debug:i" => \$debug_net_smtp,
                    "smtp-domain:s" => \$smtp_domain,
                    "smtp-auth=s" => \$smtp_auth,
-                   "identity=s" => \$identity,
+                   "no-smtp-auth" => sub {$smtp_auth = 'none'},
                    "annotate!" => \$annotate,
                    "no-annotate" => sub {$annotate = 0},
                    "compose" => \$compose,
@@ -368,9 +464,16 @@ $rc = GetOptions(
                    "no-xmailer" => sub {$use_xmailer = 0},
                    "batch-size=i" => \$batch_size,
                    "relogin-delay=i" => \$relogin_delay,
+                   "git-completion-helper" => \$git_completion_helper,
         );
 
+# Munge any "either config or getopt, not both" variables
+my @initial_to = @getopt_to ? @getopt_to : ($no_to ? () : @config_to);
+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;
 unless ($rc) {
     usage();
 }
@@ -378,64 +481,9 @@ unless ($rc) {
 die __("Cannot run git format-patch from outside a repository\n")
        if $format_patch and not $repo;
 
-# Now, let's fill any that aren't set in with defaults:
-
-sub read_config {
-       my ($prefix) = @_;
-
-       foreach my $setting (keys %config_bool_settings) {
-               my $target = $config_bool_settings{$setting}->[0];
-               $$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");
-                               @$target = @values if (@values && defined $values[0]);
-                       }
-               }
-               else {
-                       $$target = Git::config(@repo, "$prefix.$setting") unless (defined $$target);
-               }
-       }
-
-       if (!defined $smtp_encryption) {
-               my $enc = Git::config(@repo, "$prefix.smtpencryption");
-               if (defined $enc) {
-                       $smtp_encryption = $enc;
-               } elsif (Git::config_bool(@repo, "$prefix.smtpssl")) {
-                       $smtp_encryption = 'ssl';
-               }
-       }
-}
-
-# read configuration from [sendemail "$identity"], fall back on [sendemail]
-$identity = Git::config(@repo, "sendemail.identity") unless (defined $identity);
-read_config("sendemail.$identity") if (defined $identity);
-read_config("sendemail");
-
-# fall back on builtin bool defaults
-foreach my $setting (values %config_bool_settings) {
-       ${$setting->[0]} = $setting->[1] unless (defined (${$setting->[0]}));
-}
+die __("`batch-size` and `relogin` must be specified together " .
+       "(via command-line or configuration option)\n")
+       if defined $relogin_delay and not defined $batch_size;
 
 # 'default' encryption is none -- this only prevents a warning
 $smtp_encryption = '' unless (defined $smtp_encryption);
@@ -444,14 +492,16 @@ $smtp_encryption = '' unless (defined $smtp_encryption);
 my(%suppress_cc);
 if (@suppress_cc) {
        foreach my $entry (@suppress_cc) {
+               # Please update $__git_send_email_suppresscc_options
+               # in git-completion.bash when you add new options.
                die sprintf(__("Unknown --suppress-cc field: '%s'\n"), $entry)
-                       unless $entry =~ /^(?:all|cccmd|cc|author|self|sob|body|bodycc)$/;
+                       unless $entry =~ /^(?:all|cccmd|cc|author|self|sob|body|bodycc|misc-by)$/;
                $suppress_cc{$entry} = 1;
        }
 }
 
 if ($suppress_cc{'all'}) {
-       foreach my $entry (qw (cccmd cc author self sob body bodycc)) {
+       foreach my $entry (qw (cccmd cc author self sob body bodycc misc-by)) {
                $suppress_cc{$entry} = 1;
        }
        delete $suppress_cc{'all'};
@@ -462,7 +512,7 @@ $suppress_cc{'self'} = $suppress_from if defined $suppress_from;
 $suppress_cc{'sob'} = !$signed_off_by_cc if defined $signed_off_by_cc;
 
 if ($suppress_cc{'body'}) {
-       foreach my $entry (qw (sob bodycc)) {
+       foreach my $entry (qw (sob bodycc misc-by)) {
                $suppress_cc{$entry} = 1;
        }
        delete $suppress_cc{'body'};
@@ -473,6 +523,8 @@ my $confirm_unconfigured = !defined $confirm;
 if ($confirm_unconfigured) {
        $confirm = scalar %suppress_cc ? 'compose' : 'auto';
 };
+# Please update $__git_send_email_confirm_options in
+# git-completion.bash when you add new options.
 die sprintf(__("Unknown --confirm setting: '%s'\n"), $confirm)
        unless $confirm =~ /^(?:auto|cc|compose|always|never)/;
 
@@ -489,7 +541,7 @@ my ($repoauthor, $repocommitter);
 ($repocommitter) = Git::ident_person(@repo, 'committer');
 
 sub parse_address_line {
-       return Git::parse_mailboxes($_[0]);
+       return map { $_->format } Mail::Address->parse($_[0]);
 }
 
 sub split_addrs {
@@ -566,6 +618,8 @@ my %parse_alias = (
                if (/\(define-mail-alias\s+"(\S+?)"\s+"(\S+?)"\)/) {
                        $aliases{$1} = [ $2 ];
                }}}
+       # Please update _git_config() in git-completion.bash when you
+       # add new MUAs.
 );
 
 if (@alias_files and $aliasfiletype and defined $parse_alias{$aliasfiletype}) {
@@ -636,7 +690,7 @@ if (@rev_list_opts) {
 if ($validate) {
        foreach my $f (@files) {
                unless (-p $f) {
-                       my $error = validate_patch($f);
+                       my $error = validate_patch($f, $target_xfer_encoding);
                        $error and die sprintf(__("fatal: %s: %s\nwarning: no patches were sent\n"),
                                                  $f, $error);
                }
@@ -676,7 +730,8 @@ if ($compose) {
 
        my $tpl_sender = $sender || $repoauthor || $repocommitter || '';
        my $tpl_subject = $initial_subject || '';
-       my $tpl_reply_to = $initial_reply_to || '';
+       my $tpl_in_reply_to = $initial_in_reply_to || '';
+       my $tpl_reply_to = $reply_to || '';
 
        print $c <<EOT1, Git::prefix_lines("GIT: ", __ <<EOT2), <<EOT3;
 From $tpl_sender # This line is ignored.
@@ -688,8 +743,9 @@ for the patch you are writing.
 Clear the body content if you don't wish to send a summary.
 EOT2
 From: $tpl_sender
+Reply-To: $tpl_reply_to
 Subject: $tpl_subject
-In-Reply-To: $tpl_reply_to
+In-Reply-To: $tpl_in_reply_to
 
 EOT3
        for my $f (@files) {
@@ -703,57 +759,73 @@ EOT3
                do_edit($compose_filename);
        }
 
-       open my $c2, ">", $compose_filename . ".final"
-               or die sprintf(__("Failed to open %s.final: %s"), $compose_filename, $!);
-
        open $c, "<", $compose_filename
                or die sprintf(__("Failed to open %s: %s"), $compose_filename, $!);
 
-       my $need_8bit_cte = file_has_nonascii($compose_filename);
-       my $in_body = 0;
-       my $summary_empty = 1;
        if (!defined $compose_encoding) {
                $compose_encoding = "UTF-8";
        }
-       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",
-                                        "Content-Type: text/plain; ",
-                                          "charset=$compose_encoding\n",
-                                        "Content-Transfer-Encoding: 8bit\n";
-                       }
-               } elsif (/^MIME-Version:/i) {
-                       $need_8bit_cte = 0;
-               } elsif (/^Subject:\s*(.+)\s*$/i) {
-                       $initial_subject = $1;
-                       my $subject = $initial_subject;
-                       $_ = "Subject: " .
-                               quote_subject($subject, $compose_encoding) .
-                               "\n";
-               } elsif (/^In-Reply-To:\s*(.+)\s*$/i) {
-                       $initial_reply_to = $1;
-                       next;
-               } elsif (/^From:\s*(.+)\s*$/i) {
-                       $sender = $1;
-                       next;
-               } elsif (/^(?:To|Cc|Bcc):/i) {
-                       print __("To/Cc/Bcc fields are not interpreted yet, they have been ignored\n");
-                       next;
+
+       my %parsed_email;
+       while (my $line = <$c>) {
+               next if $line =~ m/^GIT:/;
+               parse_header_line($line, \%parsed_email);
+               if ($line =~ /^$/) {
+                       $parsed_email{'body'} = filter_body($c);
                }
-               print $c2 $_;
        }
        close $c;
-       close $c2;
 
-       if ($summary_empty) {
+       open my $c2, ">", $compose_filename . ".final"
+       or die sprintf(__("Failed to open %s.final: %s"), $compose_filename, $!);
+
+
+       if ($parsed_email{'From'}) {
+               $sender = delete($parsed_email{'From'});
+       }
+       if ($parsed_email{'In-Reply-To'}) {
+               $initial_in_reply_to = delete($parsed_email{'In-Reply-To'});
+       }
+       if ($parsed_email{'Reply-To'}) {
+               $reply_to = delete($parsed_email{'Reply-To'});
+       }
+       if ($parsed_email{'Subject'}) {
+               $initial_subject = delete($parsed_email{'Subject'});
+               print $c2 "Subject: " .
+                       quote_subject($initial_subject, $compose_encoding) .
+                       "\n";
+       }
+
+       if ($parsed_email{'MIME-Version'}) {
+               print $c2 "MIME-Version: $parsed_email{'MIME-Version'}\n",
+                               "Content-Type: $parsed_email{'Content-Type'};\n",
+                               "Content-Transfer-Encoding: $parsed_email{'Content-Transfer-Encoding'}\n";
+               delete($parsed_email{'MIME-Version'});
+               delete($parsed_email{'Content-Type'});
+               delete($parsed_email{'Content-Transfer-Encoding'});
+       } elsif (file_has_nonascii($compose_filename)) {
+               my $content_type = (delete($parsed_email{'Content-Type'}) or
+                       "text/plain; charset=$compose_encoding");
+               print $c2 "MIME-Version: 1.0\n",
+                       "Content-Type: $content_type\n",
+                       "Content-Transfer-Encoding: 8bit\n";
+       }
+       # Preserve unknown headers
+       foreach my $key (keys %parsed_email) {
+               next if $key eq 'body';
+               print $c2 "$key: $parsed_email{$key}";
+       }
+
+       if ($parsed_email{'body'}) {
+               print $c2 "\n$parsed_email{'body'}\n";
+               delete($parsed_email{'body'});
+       } else {
                print __("Summary email is empty, skipping it\n");
                $compose = -1;
        }
+
+       close $c2;
+
 } elsif ($annotate) {
        do_edit(@files);
 }
@@ -792,6 +864,32 @@ sub ask {
        return;
 }
 
+sub parse_header_line {
+       my $lines = shift;
+       my $parsed_line = shift;
+       my $addr_pat = join "|", qw(To Cc Bcc);
+
+       foreach (split(/\n/, $lines)) {
+               if (/^($addr_pat):\s*(.+)$/i) {
+                       $parsed_line->{$1} = [ parse_address_line($2) ];
+               } elsif (/^([^:]*):\s*(.+)\s*$/i) {
+                       $parsed_line->{$1} = $2;
+               }
+       }
+}
+
+sub filter_body {
+       my $c = shift;
+       my $body = "";
+       while (my $body_line = <$c>) {
+               if ($body_line !~ m/^GIT:/) {
+                       $body .= $body_line;
+               }
+       }
+       return $body;
+}
+
+
 my %broken_encoding;
 
 sub file_declares_8bit_cte {
@@ -870,18 +968,24 @@ sub expand_one_alias {
 
 @initial_to = process_address_list(@initial_to);
 @initial_cc = process_address_list(@initial_cc);
-@bcclist = process_address_list(@bcclist);
+@initial_bcc = process_address_list(@initial_bcc);
 
-if ($thread && !defined $initial_reply_to && $prompting) {
-       $initial_reply_to = ask(
+if ($thread && !defined $initial_in_reply_to && $prompting) {
+       $initial_in_reply_to = ask(
                __("Message-ID to be used as In-Reply-To for the first email (if any)? "),
                default => "",
                valid_re => qr/\@.*\./, confirm_only => 1);
 }
-if (defined $initial_reply_to) {
-       $initial_reply_to =~ s/^\s*<?//;
-       $initial_reply_to =~ s/>?\s*$//;
-       $initial_reply_to = "<$initial_reply_to>" if $initial_reply_to ne '';
+if (defined $initial_in_reply_to) {
+       $initial_in_reply_to =~ s/^\s*<?//;
+       $initial_in_reply_to =~ s/>?\s*$//;
+       $initial_in_reply_to = "<$initial_in_reply_to>" if $initial_in_reply_to ne '';
+}
+
+if (defined $reply_to) {
+       $reply_to =~ s/^\s+|\s+$//g;
+       ($reply_to) = expand_aliases($reply_to);
+       $reply_to = sanitize_address($reply_to);
 }
 
 if (!defined $smtp_server) {
@@ -901,7 +1005,7 @@ if ($compose && $compose > 0) {
 }
 
 # Variables we set as part of the loop over files
-our ($message_id, %mail, $subject, $reply_to, $references, $message,
+our ($message_id, %mail, $subject, $in_reply_to, $references, $message,
        $needs_confirm, $message_num, $ask_default);
 
 sub extract_valid_address {
@@ -1124,7 +1228,7 @@ sub process_address_list {
 # domain name that corresponds the IP address in the HELO/EHLO
 # handshake. This is used to verify the connection and prevent
 # spammers from trying to hide their identity. If the DNS and IP don't
-# match, the receiveing MTA may deny the connection.
+# match, the receiving MTA may deny the connection.
 #
 # Here is a deny example of Net::SMTP with the default "localhost.localdomain"
 #
@@ -1142,10 +1246,8 @@ sub valid_fqdn {
 sub maildomain_net {
        my $maildomain;
 
-       if (eval { require Net::Domain; 1 }) {
-               my $domain = Net::Domain::domainname();
-               $maildomain = $domain if valid_fqdn($domain);
-       }
+       my $domain = Net::Domain::domainname();
+       $maildomain = $domain if valid_fqdn($domain);
 
        return $maildomain;
 }
@@ -1153,17 +1255,15 @@ sub maildomain_net {
 sub maildomain_mta {
        my $maildomain;
 
-       if (eval { require Net::SMTP; 1 }) {
-               for my $host (qw(mailhost localhost)) {
-                       my $smtp = Net::SMTP->new($host);
-                       if (defined $smtp) {
-                               my $domain = $smtp->domain;
-                               $smtp->quit;
+       for my $host (qw(mailhost localhost)) {
+               my $smtp = Net::SMTP->new($host);
+               if (defined $smtp) {
+                       my $domain = $smtp->domain;
+                       $smtp->quit;
 
-                               $maildomain = $domain if valid_fqdn($domain);
+                       $maildomain = $domain if valid_fqdn($domain);
 
-                               last if $maildomain;
-                       }
+                       last if $maildomain;
                }
        }
 
@@ -1186,7 +1286,7 @@ sub smtp_host_string {
 # (smtp_user was not specified), and 0 otherwise.
 
 sub smtp_auth_maybe {
-       if (!defined $smtp_authuser || $auth) {
+       if (!defined $smtp_authuser || $auth || (defined $smtp_auth && $smtp_auth eq "none")) {
                return 1;
        }
 
@@ -1275,9 +1375,14 @@ sub file_name_is_absolute {
        return File::Spec::Functions::file_name_is_absolute($path);
 }
 
-# Returns 1 if the message was sent, and 0 otherwise.
-# In actuality, the whole program dies when there
-# is an error sending a message.
+# 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 = unique_email_list(@to);
@@ -1286,7 +1391,7 @@ sub send_message {
                    }
               @cc);
        my $to = join (",\n\t", @recipients);
-       @recipients = unique_email_list(@recipients,@cc,@bcclist);
+       @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@@';
@@ -1310,11 +1415,14 @@ Message-Id: $message_id
        if ($use_xmailer) {
                $header .= "X-Mailer: git-send-email $gitversion\n";
        }
-       if ($reply_to) {
+       if ($in_reply_to) {
 
-               $header .= "In-Reply-To: $reply_to\n";
+               $header .= "In-Reply-To: $in_reply_to\n";
                $header .= "References: $references\n";
        }
+       if ($reply_to) {
+               $header .= "Reply-To: $reply_to\n";
+       }
        if (@xh) {
                $header .= join("\n", @xh) . "\n";
        }
@@ -1346,15 +1454,17 @@ Message-Id: $message_id
 
 EOF
                }
-               # TRANSLATORS: Make sure to include [y] [n] [q] [a] in your
+               # TRANSLATORS: Make sure to include [y] [n] [e] [q] [a] in your
                # translation. The program will only accept English input
                # at this point.
-               $_ = ask(__("Send this email? ([y]es|[n]o|[q]uit|[a]ll): "),
-                        valid_re => qr/^(?:yes|y|no|n|quit|q|all|a)/i,
+               $_ = ask(__("Send this email? ([y]es|[n]o|[e]dit|[q]uit|[a]ll): "),
+                        valid_re => qr/^(?:yes|y|no|n|edit|e|quit|q|all|a)/i,
                         default => $ask_default);
                die __("Send this email reply required") unless defined $_;
                if (/^n/i) {
                        return 0;
+               } elsif (/^e/i) {
+                       return -1;
                } elsif (/^q/i) {
                        cleanup_compose_files();
                        exit(0);
@@ -1414,7 +1524,7 @@ EOF
                                                         SSL => 1);
                        }
                }
-               else {
+               elsif (!$smtp) {
                        $smtp_server_port ||= 25;
                        $smtp ||= Net::SMTP->new($smtp_server,
                                                 Hello => $smtp_domain,
@@ -1436,7 +1546,6 @@ EOF
                                        $smtp->starttls(ssl_verify_params())
                                                or die sprintf(__("STARTTLS failed! %s"), IO::Socket::SSL::errstr());
                                }
-                               $smtp_encryption = '';
                                # Send EHLO again to receive fresh
                                # supported commands
                                $smtp->hello($smtp_domain);
@@ -1489,12 +1598,17 @@ EOF
        return 1;
 }
 
-$reply_to = $initial_reply_to;
-$references = $initial_reply_to || '';
+$in_reply_to = $initial_in_reply_to;
+$references = $initial_in_reply_to || '';
 $subject = $initial_subject;
 $message_num = 0;
 
-foreach my $t (@files) {
+# 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) = @_;
+
        open my $fh, "<", $t or die sprintf(__("can't open file %s"), $t);
 
        my $author = undef;
@@ -1584,10 +1698,15 @@ foreach my $t (@files) {
                        elsif (/^Content-Transfer-Encoding: (.*)/i) {
                                $xfer_encoding = $1 if not defined $xfer_encoding;
                        }
+                       elsif (/^In-Reply-To: (.*)/i) {
+                               $in_reply_to = $1;
+                       }
+                       elsif (/^References: (.*)/i) {
+                               $references = $1;
+                       }
                        elsif (!/^Date:\s/i && /^[-A-Za-z]+:\s+\S/) {
                                push @xh, $_;
                        }
-
                } else {
                        # In the traditional
                        # "send lots of email" format,
@@ -1607,7 +1726,7 @@ foreach my $t (@files) {
        # Now parse the message body
        while(<$fh>) {
                $message .=  $_;
-               if (/^(Signed-off-by|Cc): (.*)/i) {
+               if (/^([a-z][a-z-]*-by|Cc): (.*)/i) {
                        chomp;
                        my ($what, $c) = ($1, $2);
                        # strip garbage for the address we'll use:
@@ -1617,8 +1736,18 @@ foreach my $t (@files) {
                        if ($sc eq $sender) {
                                next if ($suppress_cc{'self'});
                        } else {
-                               next if $suppress_cc{'sob'} and $what =~ /Signed-off-by/i;
-                               next if $suppress_cc{'bodycc'} and $what =~ /Cc/i;
+                               if ($what =~ /^Signed-off-by$/i) {
+                                       next if $suppress_cc{'sob'};
+                               } elsif ($what =~ /-by$/i) {
+                                       next if $suppress_cc{'misc-by'};
+                               } elsif ($what =~ /Cc/i) {
+                                       next if $suppress_cc{'bodycc'};
+                               }
+                       }
+                       if ($c !~ /.+@.+|<.+>/) {
+                               printf("(body) Ignoring %s from line '%s'\n",
+                                       $what, $_) unless $quiet;
+                               next;
                        }
                        push @cc, $c;
                        printf(__("(body) Adding cc: %s from line '%s'\n"),
@@ -1662,18 +1791,11 @@ foreach my $t (@files) {
                        }
                }
        }
-       if (defined $target_xfer_encoding) {
-               $xfer_encoding = '8bit' if not defined $xfer_encoding;
-               $message = apply_transfer_encoding(
-                       $message, $xfer_encoding, $target_xfer_encoding);
-               $xfer_encoding = $target_xfer_encoding;
-       }
-       if (defined $xfer_encoding) {
-               push @xh, "Content-Transfer-Encoding: $xfer_encoding";
-       }
-       if (defined $xfer_encoding or $has_content_type) {
-               unshift @xh, 'MIME-Version: 1.0' unless $has_mime_version;
-       }
+       $xfer_encoding = '8bit' if not defined $xfer_encoding;
+       ($message, $xfer_encoding) = apply_transfer_encoding(
+               $message, $xfer_encoding, $target_xfer_encoding);
+       push @xh, "Content-Transfer-Encoding: $xfer_encoding";
+       unshift @xh, 'MIME-Version: 1.0' unless $has_mime_version;
 
        $needs_confirm = (
                $confirm eq "always" or
@@ -1697,12 +1819,16 @@ foreach my $t (@files) {
        }
 
        my $message_was_sent = send_message();
+       if ($message_was_sent == -1) {
+               do_edit($t);
+               return 0;
+       }
 
        # 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 $in_reply_to || length($in_reply_to) == 0 ||
                $message_num == 1)) {
-               $reply_to = $message_id;
+               $in_reply_to = $message_id;
                if (length $references > 0) {
                        $references .= "\n $message_id";
                } else {
@@ -1718,6 +1844,14 @@ foreach my $t (@files) {
                undef $auth;
                sleep($relogin_delay) if defined $relogin_delay;
        }
+
+       return 1;
+}
+
+foreach my $t (@files) {
+       while (!process_file($t)) {
+               # user edited the file
+       }
 }
 
 # Execute a command (e.g. $to_cmd) to get a list of email addresses
@@ -1755,7 +1889,7 @@ sub apply_transfer_encoding {
        my $from = shift;
        my $to = shift;
 
-       return $message if ($from eq $to and $from ne '7bit');
+       return ($message, $to) if ($from eq $to and $from ne '7bit');
 
        require MIME::QuotedPrint;
        require MIME::Base64;
@@ -1765,13 +1899,16 @@ sub apply_transfer_encoding {
        $message = MIME::Base64::decode($message)
                if ($from eq 'base64');
 
+       $to = ($message =~ /(?:.{999,}|\r)/) ? 'quoted-printable' : '8bit'
+               if $to eq 'auto';
+
        die __("cannot send message as 7bit")
                if ($to eq '7bit' and $message =~ /[^[:ascii:]]/);
-       return $message
+       return ($message, $to)
                if ($to eq '7bit' or $to eq '8bit');
-       return MIME::QuotedPrint::encode($message, "\n", 0)
+       return (MIME::QuotedPrint::encode($message, "\n", 0), $to)
                if ($to eq 'quoted-printable');
-       return MIME::Base64::encode($message, "\n")
+       return (MIME::Base64::encode($message, "\n"), $to)
                if ($to eq 'base64');
        die __("invalid transfer encoding");
 }
@@ -1790,7 +1927,7 @@ sub unique_email_list {
 }
 
 sub validate_patch {
-       my $fn = shift;
+       my ($fn, $xfer_encoding) = @_;
 
        if ($repo) {
                my $validate_hook = catfile(catdir($repo->repo_path(), 'hooks'),
@@ -1810,11 +1947,15 @@ sub validate_patch {
                return $hook_error if $hook_error;
        }
 
-       open(my $fh, '<', $fn)
-               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"), $.);
+       # Any long lines will be automatically fixed if we use a suitable transfer
+       # encoding.
+       unless ($xfer_encoding =~ /^(?:auto|quoted-printable|base64)$/) {
+               open(my $fh, '<', $fn)
+                       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"), $.);
+                       }
                }
        }
        return;