send-email: add --header-cmd, --no-header-cmd options

Sometimes, adding a header different than CC or TO is desirable; for example, when using Debbugs, it is best to use 'X-Debbugs-Cc' headers to keep people in CC; this is an example use case enabled by the new '--header-cmd' option. The header unfolding logic is extracted to a subroutine so that it can be reused; a test is added for coverage. Signed-off-by: Maxim Cournoyer <maxim.cournoyer@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Maxim Cournoyer committed May 1, 2023 at 10:38 UTC ba92106e93c66e41af5180ac1b6ad0f959f72bb4
4 files changed +107 -14
Documentation/config/sendemail.txt
+1
@@ -61,6 +61,7 @@ sendemail.ccCmd::
61 sendemail.chainReplyTo::
62 sendemail.envelopeSender::
63 sendemail.from::
64 +sendemail.headerCmd::
65 sendemail.signedoffbycc::
66 sendemail.smtpPass::
67 sendemail.suppresscc::
Documentation/git-send-email.txt
+11
@@ -320,6 +320,17 @@ Automating
320 Output of this command must be single email address per line.
321 Default is the value of `sendemail.ccCmd` configuration value.
322
323 +--header-cmd=<command>::
324 + Specify a command that is executed once per outgoing message
325 + and output RFC 2822 style header lines to be inserted into
326 + them. When the `sendemail.headerCmd` configuration variable is
327 + set, its value is always used. When --header-cmd is provided
328 + at the command line, its value takes precedence over the
329 + `sendemail.headerCmd` configuration variable.
330 +
331 +--no-header-cmd::
332 + Disable any header command in use.
333 +
334 --[no-]chain-reply-to::
335 If this is set, each email will be sent as a reply to the previous
336 email sent. If disabled with "--no-chain-reply-to", all emails after
git-send-email.perl
+43 -12
@@ -87,8 +87,10 @@ git send-email --dump-aliases
87
88 Automating:
89 --identity <str> * Use the sendemail.<id> options.
90 - --to-cmd <str> * Email To: via `<str> \$patch_path`
91 - --cc-cmd <str> * Email Cc: via `<str> \$patch_path`
90 + --to-cmd <str> * Email To: via `<str> \$patch_path`.
91 + --cc-cmd <str> * Email Cc: via `<str> \$patch_path`.
92 + --header-cmd <str> * Add headers via `<str> \$patch_path`.
93 + --no-header-cmd * Disable any header command in use.
94 --suppress-cc <str> * author, self, sob, cc, cccmd, body, bodycc, misc-by, all.
95 --[no-]cc-cover * Email Cc: addresses in the cover letter.
96 --[no-]to-cover * Email To: addresses in the cover letter.
@@ -202,7 +204,7 @@ my (@to,@cc,@xh,$envelope_sender,
204 $author,$sender,$smtp_authpass,$annotate,$compose,$time);
205 # Things we either get from config, *or* are overridden on the
206 # command-line.
205 -my ($no_cc, $no_to, $no_bcc, $no_identity);
207 +my ($no_cc, $no_to, $no_bcc, $no_identity, $no_header_cmd);
208 my (@config_to, @getopt_to);
209 my (@config_cc, @getopt_cc);
210 my (@config_bcc, @getopt_bcc);
@@ -269,7 +271,7 @@ sub do_edit {
271 # Variables with corresponding config settings
272 my ($suppress_from, $signed_off_by_cc);
273 my ($cover_cc, $cover_to);
272 -my ($to_cmd, $cc_cmd);
274 +my ($to_cmd, $cc_cmd, $header_cmd);
275 my ($smtp_server, $smtp_server_port, @smtp_server_options);
276 my ($smtp_authuser, $smtp_encryption, $smtp_ssl_cert_path);
277 my ($batch_size, $relogin_delay);
@@ -318,6 +320,7 @@ my %config_settings = (
320 "tocmd" => \$to_cmd,
321 "cc" => \@config_cc,
322 "cccmd" => \$cc_cmd,
323 + "headercmd" => \$header_cmd,
324 "aliasfiletype" => \$aliasfiletype,
325 "bcc" => \@config_bcc,
326 "suppresscc" => \@suppress_cc,
@@ -519,6 +522,8 @@ my %options = (
522 "compose" => \$compose,
523 "quiet" => \$quiet,
524 "cc-cmd=s" => \$cc_cmd,
525 + "header-cmd=s" => \$header_cmd,
526 + "no-header-cmd" => \$no_header_cmd,
527 "suppress-from!" => \$suppress_from,
528 "no-suppress-from" => sub {$suppress_from = 0},
529 "suppress-cc=s" => \@suppress_cc,
@@ -1780,16 +1785,16 @@ sub process_file {
1785 $subject = $initial_subject;
1786 $message = "";
1787 $message_num++;
1783 - # First unfold multiline header fields
1788 + # Retrieve and unfold header fields.
1789 + my @header_lines = ();
1790 while(<$fh>) {
1791 last if /^\s*$/;
1786 - if (/^\s+\S/ and @header) {
1787 - chomp($header[$#header]);
1788 - s/^\s+/ /;
1789 - $header[$#header] .= $_;
1790 - } else {
1791 - push(@header, $_);
1792 - }
1792 + push(@header_lines, $_);
1793 + }
1794 + @header = unfold_headers(@header_lines);
1795 + # Add computed headers, if applicable.
1796 + unless ($no_header_cmd || ! $header_cmd) {
1797 + push @header, invoke_header_cmd($header_cmd, $t);
1798 }
1799 # Now parse the header
1800 foreach(@header) {
@@ -2036,6 +2041,32 @@ sub execute_cmd {
2041 return @lines;
2042 }
2043
2044 +# Process headers lines, unfolding multiline headers as defined by RFC
2045 +# 2822.
2046 +sub unfold_headers {
2047 + my @headers;
2048 + foreach(@_) {
2049 + last if /^\s*$/;
2050 + if (/^\s+\S/ and @headers) {
2051 + chomp($headers[$#headers]);
2052 + s/^\s+/ /;
2053 + $headers[$#headers] .= $_;
2054 + } else {
2055 + push(@headers, $_);
2056 + }
2057 + }
2058 + return @headers;
2059 +}
2060 +
2061 +# Invoke the provided CMD with FILE as an argument, which should
2062 +# output RFC 2822 email headers. Fold multiline headers and return the
2063 +# headers as an array.
2064 +sub invoke_header_cmd {
2065 + my ($cmd, $file) = @_;
2066 + my @lines = execute_cmd("header-cmd", $header_cmd, $file);
2067 + return unfold_headers(@lines);
2068 +}
2069 +
2070 # Execute a command (e.g. $to_cmd) to get a list of email addresses
2071 # and return a results array
2072 sub recipients_cmd {
t/t9001-send-email.sh
+52 -2
@@ -374,13 +374,16 @@ test_expect_success $PREREQ,!AUTOIDENT 'broken implicit ident aborts send-email'
374 )
375 '
376
377 -test_expect_success $PREREQ 'setup tocmd and cccmd scripts' '
377 +test_expect_success $PREREQ 'setup cmd scripts' '
378 write_script tocmd-sed <<-\EOF &&
379 sed -n -e "s/^tocmd--//p" "$1"
380 EOF
381 - write_script cccmd-sed <<-\EOF
381 + write_script cccmd-sed <<-\EOF &&
382 sed -n -e "s/^cccmd--//p" "$1"
383 EOF
384 + write_script headercmd-sed <<-\EOF
385 + sed -n -e "s/^headercmd--//p" "$1"
386 + EOF
387 '
388
389 test_expect_success $PREREQ 'tocmd works' '
@@ -410,6 +413,53 @@ test_expect_success $PREREQ 'cccmd works' '
413 grep "^ cccmd@example.com" msgtxt1
414 '
415
416 +test_expect_success $PREREQ 'headercmd works' '
417 + clean_fake_sendmail &&
418 + cp $patches headercmd.patch &&
419 + echo "headercmd--X-Debbugs-CC: dummy@example.com" >>headercmd.patch &&
420 + git send-email \
421 + --from="Example <nobody@example.com>" \
422 + --to=nobody@example.com \
423 + --header-cmd=./headercmd-sed \
424 + --smtp-server="$(pwd)/fake.sendmail" \
425 + headercmd.patch \
426 + &&
427 + grep "^X-Debbugs-CC: dummy@example.com" msgtxt1
428 +'
429 +
430 +test_expect_success $PREREQ '--no-header-cmd works' '
431 + clean_fake_sendmail &&
432 + cp $patches headercmd.patch &&
433 + echo "headercmd--X-Debbugs-CC: dummy@example.com" >>headercmd.patch &&
434 + git send-email \
435 + --from="Example <nobody@example.com>" \
436 + --to=nobody@example.com \
437 + --header-cmd=./headercmd-sed \
438 + --no-header-cmd \
439 + --smtp-server="$(pwd)/fake.sendmail" \
440 + headercmd.patch \
441 + &&
442 + ! grep "^X-Debbugs-CC: dummy@example.com" msgtxt1
443 +'
444 +
445 +test_expect_success $PREREQ 'multiline fields are correctly unfolded' '
446 + clean_fake_sendmail &&
447 + cp $patches headercmd.patch &&
448 + write_script headercmd-multiline <<-\EOF &&
449 + echo "X-Debbugs-CC: someone@example.com
450 +FoldedField: This is a tale
451 + best told using
452 + multiple lines."
453 + EOF
454 + git send-email \
455 + --from="Example <nobody@example.com>" \
456 + --to=nobody@example.com \
457 + --header-cmd=./headercmd-multiline \
458 + --smtp-server="$(pwd)/fake.sendmail" \
459 + headercmd.patch &&
460 + grep "^FoldedField: This is a tale best told using multiple lines.$" msgtxt1
461 +'
462 +
463 test_expect_success $PREREQ 'reject long lines' '
464 z8=zzzzzzzz &&
465 z64=$z8$z8$z8$z8$z8$z8$z8$z8 &&