Merge branch 'rl/send-email-aliases'
authorJunio C Hamano <gitster@pobox.com>
Mon, 3 Aug 2015 18:01:15 +0000 (11:01 -0700)
committerJunio C Hamano <gitster@pobox.com>
Mon, 3 Aug 2015 18:01:15 +0000 (11:01 -0700)
"git send-email" now performs alias-expansion on names that are
given via --cccmd, etc.

This round comes with a lot more enhanced e-mail address parser,
which makes it a bit scary, but as long as it works as designed, it
makes it wonderful ;-).

* rl/send-email-aliases:
send-email: suppress meaningless whitespaces in from field
send-email: allow multiple emails using --cc, --to and --bcc
send-email: consider quote as delimiter instead of character
send-email: reduce dependencies impact on parse_address_line
send-email: minor code refactoring
send-email: allow use of aliases in the From field of --compose mode
send-email: refactor address list process
t9001-send-email: refactor header variable fields replacement
send-email: allow aliases in patch header and command script outputs
t9001-send-email: move script creation in a setup test

1  2 
Documentation/git-send-email.txt
git-send-email.perl
t/t9001-send-email.sh
index 7ae467ba415e5cb4413d0246883b8a620b8960e3,8eda8990a5b7579925e6a9d2895f2c66581e66f1..f14705ee04e491e698e63ab10d15d9ec427b56f4
@@@ -49,17 -49,17 +49,17 @@@ Composin
        of 'sendemail.annotate'. See the CONFIGURATION section for
        'sendemail.multiEdit'.
  
- --bcc=<address>::
+ --bcc=<address>,...::
        Specify a "Bcc:" value for each email. Default is the value of
        'sendemail.bcc'.
  +
- The --bcc option must be repeated for each user you want on the bcc list.
+ This option may be specified multiple times.
  
- --cc=<address>::
+ --cc=<address>,...::
        Specify a starting "Cc:" value for each email.
        Default is the value of 'sendemail.cc'.
  +
- The --cc option must be repeated for each user you want on the cc list.
+ This option may be specified multiple times.
  
  --compose::
        Invoke a text editor (see GIT_EDITOR in linkgit:git-var[1])
@@@ -110,13 -110,13 +110,13 @@@ is not set, this will be prompted for
        Only necessary if --compose is also set.  If --compose
        is not set, this will be prompted for.
  
- --to=<address>::
+ --to=<address>,...::
        Specify the primary recipient of the emails generated. Generally, this
        will be the upstream maintainer of the project involved. Default is the
        value of the 'sendemail.to' configuration value; if that is unspecified,
        and --to-cmd is not specified, this will be prompted for.
  +
- The --to option must be repeated for each user you want on the to list.
+ This option may be specified multiple times.
  
  --8bit-encoding=<encoding>::
        When encountering a non-ASCII message or subject that does not
@@@ -383,24 -383,7 +383,24 @@@ sendemail.aliasesFile:
  
  sendemail.aliasFileType::
        Format of the file(s) specified in sendemail.aliasesFile. Must be
 -      one of 'mutt', 'mailrc', 'pine', 'elm', or 'gnus'.
 +      one of 'mutt', 'mailrc', 'pine', 'elm', or 'gnus', or 'sendmail'.
 ++
 +What an alias file in each format looks like can be found in
 +the documentation of the email program of the same name. The
 +differences and limitations from the standard formats are
 +described below:
 ++
 +--
 +sendmail;;
 +*     Quoted aliases and quoted addresses are not supported: lines that
 +      contain a `"` symbol are ignored.
 +*     Redirection to a file (`/path/name`) or pipe (`|command`) is not
 +      supported.
 +*     File inclusion (`:include: /path/name`) is not supported.
 +*     Warnings are printed on the standard error output for any
 +      explicitly unsupported constructs, and any other lines that are not
 +      recognized by the parser.
 +--
  
  sendemail.multiEdit::
        If true (default), a single editor instance will be spawned to edit
diff --combined git-send-email.perl
index ae9f8698c5a4842c2c0d63db51dde067ddeeb1c9,62fc7d65e89097163c36453bc9122730cbdc71dc..b660cc238223b101762017c7effda67de6f70113
@@@ -460,25 -460,11 +460,11 @@@ my ($repoauthor, $repocommitter)
  ($repoauthor) = Git::ident_person(@repo, 'author');
  ($repocommitter) = Git::ident_person(@repo, 'committer');
  
- # Verify the user input
- foreach my $entry (@initial_to) {
-       die "Comma in --to entry: $entry'\n" unless $entry !~ m/,/;
- }
- foreach my $entry (@initial_cc) {
-       die "Comma in --cc entry: $entry'\n" unless $entry !~ m/,/;
- }
- foreach my $entry (@bcclist) {
-       die "Comma in --bcclist entry: $entry'\n" unless $entry !~ m/,/;
- }
  sub parse_address_line {
        if ($have_mail_address) {
                return map { $_->format } Mail::Address->parse($_[0]);
        } else {
-               return split_addrs($_[0]);
+               return Git::parse_mailboxes($_[0]);
        }
  }
  
@@@ -487,37 -473,6 +473,37 @@@ sub split_addrs 
  }
  
  my %aliases;
 +
 +sub parse_sendmail_alias {
 +      local $_ = shift;
 +      if (/"/) {
 +              print STDERR "warning: sendmail alias with quotes is not supported: $_\n";
 +      } elsif (/:include:/) {
 +              print STDERR "warning: `:include:` not supported: $_\n";
 +      } elsif (/[\/|]/) {
 +              print STDERR "warning: `/file` or `|pipe` redirection not supported: $_\n";
 +      } elsif (/^(\S+?)\s*:\s*(.+)$/) {
 +              my ($alias, $addr) = ($1, $2);
 +              $aliases{$alias} = [ split_addrs($addr) ];
 +      } else {
 +              print STDERR "warning: sendmail line is not recognized: $_\n";
 +      }
 +}
 +
 +sub parse_sendmail_aliases {
 +      my $fh = shift;
 +      my $s = '';
 +      while (<$fh>) {
 +              chomp;
 +              next if /^\s*$/ || /^\s*#/;
 +              $s .= $_, next if $s =~ s/\\$// || s/^\s+//;
 +              parse_sendmail_alias($s) if $s;
 +              $s = $_;
 +      }
 +      $s =~ s/\\$//; # silently tolerate stray '\' on last line
 +      parse_sendmail_alias($s) if $s;
 +}
 +
  my %parse_alias = (
        # multiline formats can be supported in the future
        mutt => sub { my $fh = shift; while (<$fh>) {
                               $aliases{$alias} = [ split_addrs($addr) ];
                          }
                      } },
 -
 +      sendmail => \&parse_sendmail_aliases,
        gnus => sub { my $fh = shift; while (<$fh>) {
                if (/\(define-mail-alias\s+"(\S+?)"\s+"(\S+?)"\)/) {
                        $aliases{$1} = [ $2 ];
@@@ -561,8 -516,6 +547,6 @@@ if (@alias_files and $aliasfiletype an
        }
  }
  
- ($sender) = expand_aliases($sender) if defined $sender;
  # 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 {
@@@ -807,7 -760,10 +791,10 @@@ if (!$force) 
        }
  }
  
- if (!defined $sender) {
+ if (defined $sender) {
+       $sender =~ s/^\s+|\s+$//g;
+       ($sender) = expand_aliases($sender);
+ } else {
        $sender = $repoauthor || $repocommitter || '';
  }
  
@@@ -839,12 -795,9 +826,9 @@@ sub expand_one_alias 
        return $aliases{$alias} ? expand_aliases(@{$aliases{$alias}}) : $alias;
  }
  
- @initial_to = expand_aliases(@initial_to);
- @initial_to = validate_address_list(sanitize_address_list(@initial_to));
- @initial_cc = expand_aliases(@initial_cc);
- @initial_cc = validate_address_list(sanitize_address_list(@initial_cc));
- @bcclist = expand_aliases(@bcclist);
- @bcclist = validate_address_list(sanitize_address_list(@bcclist));
+ @initial_to = process_address_list(@initial_to);
+ @initial_cc = process_address_list(@initial_cc);
+ @bcclist = process_address_list(@bcclist);
  
  if ($thread && !defined $initial_reply_to && $prompting) {
        $initial_reply_to = ask(
@@@ -1037,15 -990,17 +1021,17 @@@ sub sanitize_address 
                return $recipient;
        }
  
+       # remove non-escaped quotes
+       $recipient_name =~ s/(^|[^\\])"/$1/g;
        # rfc2047 is needed if a non-ascii char is included
        if ($recipient_name =~ /[^[:ascii:]]/) {
-               $recipient_name =~ s/^"(.*)"$/$1/;
                $recipient_name = quote_rfc2047($recipient_name);
        }
  
        # double quotes are needed if specials or CTLs are included
        elsif ($recipient_name =~ /[][()<>@,;:\\".\000-\037\177]/) {
-               $recipient_name =~ s/(["\\\r])/\\$1/g;
+               $recipient_name =~ s/([\\\r])/\\$1/g;
                $recipient_name = qq["$recipient_name"];
        }
  
@@@ -1057,6 -1012,14 +1043,14 @@@ sub sanitize_address_list 
        return (map { sanitize_address($_) } @_);
  }
  
+ sub process_address_list {
+       my @addr_list = map { parse_address_line($_) } @_;
+       @addr_list = expand_aliases(@addr_list);
+       @addr_list = sanitize_address_list(@addr_list);
+       @addr_list = validate_address_list(@addr_list);
+       return @addr_list;
+ }
  # Returns the local Fully Qualified Domain Name (FQDN) if available.
  #
  # Tightly configured MTAa require that a caller sends a real DNS
@@@ -1566,8 -1529,8 +1560,8 @@@ foreach my $t (@files) 
                ($confirm =~ /^(?:auto|compose)$/ && $compose && $message_num == 1));
        $needs_confirm = "inform" if ($needs_confirm && $confirm_unconfigured && @cc);
  
-       @to = validate_address_list(sanitize_address_list(@to));
-       @cc = validate_address_list(sanitize_address_list(@cc));
+       @to = process_address_list(@to);
+       @cc = process_address_list(@cc);
  
        @to = (@initial_to, @to);
        @cc = (@initial_cc, @cc);
diff --combined t/t9001-send-email.sh
index db2f45e83b14fb5ae5b5f32b4178d7cd1ce97bf8,bbfed5650d90ea98be50bd21c8dabc7ddb813e79..5b4a5ce06b94355725fafc84c31f9ed67b8b15ed
@@@ -312,13 -312,19 +312,19 @@@ test_expect_success $PREREQ,!AUTOIDENT 
        )
  '
  
+ test_expect_success $PREREQ 'setup tocmd and cccmd scripts' '
+       write_script tocmd-sed <<-\EOF &&
+       sed -n -e "s/^tocmd--//p" "$1"
+       EOF
+       write_script cccmd-sed <<-\EOF
+       sed -n -e "s/^cccmd--//p" "$1"
+       EOF
+ '
  test_expect_success $PREREQ 'tocmd works' '
        clean_fake_sendmail &&
        cp $patches tocmd.patch &&
        echo tocmd--tocmd@example.com >>tocmd.patch &&
-       write_script tocmd-sed <<-\EOF &&
-       sed -n -e "s/^tocmd--//p" "$1"
-       EOF
        git send-email \
                --from="Example <nobody@example.com>" \
                --to-cmd=./tocmd-sed \
@@@ -332,9 -338,6 +338,6 @@@ test_expect_success $PREREQ 'cccmd work
        clean_fake_sendmail &&
        cp $patches cccmd.patch &&
        echo "cccmd--  cccmd@example.com" >>cccmd.patch &&
-       write_script cccmd-sed <<-\EOF &&
-       sed -n -e "s/^cccmd--//p" "$1"
-       EOF
        git send-email \
                --from="Example <nobody@example.com>" \
                --to=nobody@example.com \
@@@ -519,6 -522,12 +522,12 @@@ Result: O
  EOF
  "
  
+ replace_variable_fields () {
+       sed     -e "s/^\(Date:\).*/\1 DATE-STRING/" \
+               -e "s/^\(Message-Id:\).*/\1 MESSAGE-ID-STRING/" \
+               -e "s/^\(X-Mailer:\).*/\1 X-MAILER-STRING/"
+ }
  test_suppression () {
        git send-email \
                --dry-run \
                --from="Example <from@example.com>" \
                --to=to@example.com \
                --smtp-server relay.example.com \
-               $patches |
-       sed     -e "s/^\(Date:\).*/\1 DATE-STRING/" \
-               -e "s/^\(Message-Id:\).*/\1 MESSAGE-ID-STRING/" \
-               -e "s/^\(X-Mailer:\).*/\1 X-MAILER-STRING/" \
+               $patches | replace_variable_fields \
                >actual-suppress-$1${2+"-$2"} &&
        test_cmp expected-suppress-$1${2+"-$2"} actual-suppress-$1${2+"-$2"}
  }
@@@ -1537,7 -1543,7 +1543,7 @@@ test_expect_success $PREREQ 'sendemail.
  
  test_expect_success $PREREQ 'sendemail.aliasfile=~/.mailrc' '
        clean_fake_sendmail &&
 -      echo "alias sbd  someone@example.org" >~/.mailrc &&
 +      echo "alias sbd  someone@example.org" >"$HOME/.mailrc" &&
        git config --replace-all sendemail.aliasesfile "~/.mailrc" &&
        git config sendemail.aliasfiletype mailrc &&
        git send-email \
        grep "^!someone@example\.org!$" commandline1
  '
  
 +test_sendmail_aliases () {
 +      msg="$1" && shift &&
 +      expect="$@" &&
 +      cat >.tmp-email-aliases &&
 +
 +      test_expect_success $PREREQ "$msg" '
 +              clean_fake_sendmail && rm -fr outdir &&
 +              git format-patch -1 -o outdir &&
 +              git config --replace-all sendemail.aliasesfile \
 +                      "$(pwd)/.tmp-email-aliases" &&
 +              git config sendemail.aliasfiletype sendmail &&
 +              git send-email \
 +                      --from="Example <nobody@example.com>" \
 +                      --to=alice --to=bcgrp \
 +                      --smtp-server="$(pwd)/fake.sendmail" \
 +                      outdir/0001-*.patch \
 +                      2>errors >out &&
 +              for i in $expect
 +              do
 +                      grep "^!$i!$" commandline1 || return 1
 +              done
 +      '
 +}
 +
 +test_sendmail_aliases 'sendemail.aliasfiletype=sendmail' \
 +      'awol@example\.com' \
 +      'bob@example\.com' \
 +      'chloe@example\.com' \
 +      'o@example\.com' <<-\EOF
 +      alice: Alice W Land <awol@example.com>
 +      bob: Robert Bobbyton <bob@example.com>
 +      # this is a comment
 +         # this is also a comment
 +      chloe: chloe@example.com
 +      abgroup: alice, bob
 +      bcgrp: bob, chloe, Other <o@example.com>
 +      EOF
 +
 +test_sendmail_aliases 'sendmail aliases line folding' \
 +      alice1 \
 +      bob1 bob2 \
 +      chuck1 chuck2 \
 +      darla1 darla2 darla3 \
 +      elton1 elton2 elton3 \
 +      fred1 fred2 \
 +      greg1 <<-\EOF
 +      alice: alice1
 +      bob: bob1,\
 +      bob2
 +      chuck: chuck1,
 +          chuck2
 +      darla: darla1,\
 +      darla2,
 +          darla3
 +      elton: elton1,
 +          elton2,\
 +      elton3
 +      fred: fred1,\
 +          fred2
 +      greg: greg1
 +      bcgrp: bob, chuck, darla, elton, fred, greg
 +      EOF
 +
 +test_sendmail_aliases 'sendmail aliases tolerate bogus line folding' \
 +      alice1 bob1 <<-\EOF
 +          alice: alice1
 +      bcgrp: bob1\
 +      EOF
 +
 +test_sendmail_aliases 'sendmail aliases empty' alice bcgrp <<-\EOF
 +      EOF
 +
+ test_expect_success $PREREQ 'alias support in To header' '
+       clean_fake_sendmail &&
+       echo "alias sbd  someone@example.org" >.mailrc &&
+       test_config sendemail.aliasesfile ".mailrc" &&
+       test_config sendemail.aliasfiletype mailrc &&
+       git format-patch --stdout -1 --to=sbd >aliased.patch &&
+       git send-email \
+               --from="Example <nobody@example.com>" \
+               --smtp-server="$(pwd)/fake.sendmail" \
+               aliased.patch \
+               2>errors >out &&
+       grep "^!someone@example\.org!$" commandline1
+ '
+ test_expect_success $PREREQ 'alias support in Cc header' '
+       clean_fake_sendmail &&
+       echo "alias sbd  someone@example.org" >.mailrc &&
+       test_config sendemail.aliasesfile ".mailrc" &&
+       test_config sendemail.aliasfiletype mailrc &&
+       git format-patch --stdout -1 --cc=sbd >aliased.patch &&
+       git send-email \
+               --from="Example <nobody@example.com>" \
+               --smtp-server="$(pwd)/fake.sendmail" \
+               aliased.patch \
+               2>errors >out &&
+       grep "^!someone@example\.org!$" commandline1
+ '
+ test_expect_success $PREREQ 'tocmd works with aliases' '
+       clean_fake_sendmail &&
+       echo "alias sbd  someone@example.org" >.mailrc &&
+       test_config sendemail.aliasesfile ".mailrc" &&
+       test_config sendemail.aliasfiletype mailrc &&
+       git format-patch --stdout -1 >tocmd.patch &&
+       echo tocmd--sbd >>tocmd.patch &&
+       git send-email \
+               --from="Example <nobody@example.com>" \
+               --to-cmd=./tocmd-sed \
+               --smtp-server="$(pwd)/fake.sendmail" \
+               tocmd.patch \
+               2>errors >out &&
+       grep "^!someone@example\.org!$" commandline1
+ '
+ test_expect_success $PREREQ 'cccmd works with aliases' '
+       clean_fake_sendmail &&
+       echo "alias sbd  someone@example.org" >.mailrc &&
+       test_config sendemail.aliasesfile ".mailrc" &&
+       test_config sendemail.aliasfiletype mailrc &&
+       git format-patch --stdout -1 >cccmd.patch &&
+       echo cccmd--sbd >>cccmd.patch &&
+       git send-email \
+               --from="Example <nobody@example.com>" \
+               --cc-cmd=./cccmd-sed \
+               --smtp-server="$(pwd)/fake.sendmail" \
+               cccmd.patch \
+               2>errors >out &&
+       grep "^!someone@example\.org!$" commandline1
+ '
  do_xmailer_test () {
        expected=$1 params=$2 &&
        git format-patch -1 &&
@@@ -1654,4 -1648,72 +1720,72 @@@ test_expect_success $PREREQ '--[no-]xma
        do_xmailer_test 1 "--xmailer"
  '
  
+ test_expect_success $PREREQ 'setup expected-list' '
+       git send-email \
+       --dry-run \
+       --from="Example <from@example.com>" \
+       --to="To 1 <to1@example.com>" \
+       --to="to2@example.com" \
+       --to="to3@example.com" \
+       --cc="Cc 1 <cc1@example.com>" \
+       --cc="Cc2 <cc2@example.com>" \
+       --bcc="bcc1@example.com" \
+       --bcc="bcc2@example.com" \
+       0001-add-master.patch | replace_variable_fields \
+       >expected-list
+ '
+ test_expect_success $PREREQ 'use email list in --cc --to and --bcc' '
+       git send-email \
+       --dry-run \
+       --from="Example <from@example.com>" \
+       --to="To 1 <to1@example.com>, to2@example.com" \
+       --to="to3@example.com" \
+       --cc="Cc 1 <cc1@example.com>, Cc2 <cc2@example.com>" \
+       --bcc="bcc1@example.com, bcc2@example.com" \
+       0001-add-master.patch | replace_variable_fields \
+       >actual-list &&
+       test_cmp expected-list actual-list
+ '
+ test_expect_success $PREREQ 'aliases work with email list' '
+       echo "alias to2 to2@example.com" >.mutt &&
+       echo "alias cc1 Cc 1 <cc1@example.com>" >>.mutt &&
+       test_config sendemail.aliasesfile ".mutt" &&
+       test_config sendemail.aliasfiletype mutt &&
+       git send-email \
+       --dry-run \
+       --from="Example <from@example.com>" \
+       --to="To 1 <to1@example.com>, to2, to3@example.com" \
+       --cc="cc1, Cc2 <cc2@example.com>" \
+       --bcc="bcc1@example.com, bcc2@example.com" \
+       0001-add-master.patch | replace_variable_fields \
+       >actual-list &&
+       test_cmp expected-list actual-list
+ '
+ test_expect_success $PREREQ 'leading and trailing whitespaces are removed' '
+       echo "alias to2 to2@example.com" >.mutt &&
+       echo "alias cc1 Cc 1 <cc1@example.com>" >>.mutt &&
+       test_config sendemail.aliasesfile ".mutt" &&
+       test_config sendemail.aliasfiletype mutt &&
+       TO1=$(echo "QTo 1 <to1@example.com>" | q_to_tab) &&
+       TO2=$(echo "QZto2" | qz_to_tab_space) &&
+       CC1=$(echo "cc1" | append_cr) &&
+       BCC1=$(echo "Q bcc1@example.com Q" | q_to_nul) &&
+       git send-email \
+       --dry-run \
+       --from="        Example <from@example.com>" \
+       --to="$TO1" \
+       --to="$TO2" \
+       --to="  to3@example.com   " \
+       --cc="$CC1" \
+       --cc="Cc2 <cc2@example.com>" \
+       --bcc="$BCC1" \
+       --bcc="bcc2@example.com" \
+       0001-add-master.patch | replace_variable_fields \
+       >actual-list &&
+       test_cmp expected-list actual-list
+ '
  test_done