Merge branch 'ab/send-email-perl'
authorJunio C Hamano <gitster@pobox.com>
Wed, 27 Oct 2010 05:02:52 +0000 (22:02 -0700)
committerJunio C Hamano <gitster@pobox.com>
Wed, 27 Oct 2010 05:02:52 +0000 (22:02 -0700)
* ab/send-email-perl:
send-email: extract_valid_address use qr// regexes
send-email: is_rfc2047_quoted use qr// regexes
send-email: use Perl idioms in while loop
send-email: make_message_id use "require" instead of "use"
send-email: send_message die on $!, not $?
send-email: use (?:) instead of () if no match variables are needed
send-email: sanitize_address use qq["foo"], not "\"foo\""
send-email: sanitize_address use $foo, not "$foo"
send-email: use \E***\Q instead of \*\*\*
send-email: cleanup_compose_files doesn't need a prototype
send-email: unique_email_list doesn't need a prototype
send-email: file_declares_8bit_cte doesn't need a prototype
send-email: get_patch_subject doesn't need a prototype
send-email: use lexical filehandles during sending
send-email: use lexical filehandles for $compose
send-email: use lexical filehandle for opendir

Conflicts:
git-send-email.perl

1  2 
git-send-email.perl
t/t9001-send-email.sh
diff --combined git-send-email.perl
index f304ef913ecde17db0ad7ad4c57ad52028abc9a3,db17aae0207ed501a479e21e36f6d6ab6aa67adb..f68ed5a5d3208eb0669d7dc1289f40c567e077c7
@@@ -1,4 -1,4 +1,4 @@@
 -#!/usr/bin/perl -w
 +#!/usr/bin/perl
  #
  # Copyright 2002,2005 Greg Kroah-Hartman <greg@kroah.com>
  # Copyright 2005 Ryan Anderson <ryan@michonline.com>
@@@ -16,7 -16,6 +16,7 @@@
  #    and second line is the subject of the message.
  #
  
 +use 5.008;
  use strict;
  use warnings;
  use Term::ReadLine;
@@@ -62,7 -61,6 +62,7 @@@ git send-email [options] <file | direct
      --envelope-sender       <str>  * Email envelope sender.
      --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.
      --smtp-server-port      <int>  * Outgoing SMTP server port.
      --smtp-user             <str>  * Username for SMTP-AUTH.
      --smtp-pass             <str>  * Password for SMTP-AUTH; not necessary.
@@@ -73,7 -71,6 +73,7 @@@
  
    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`
      --suppress-cc           <str>  * author, self, sob, cc, cccmd, body, bodycc, all.
      --[no-]signed-off-by-cc        * Send to Signed-off-by: addresses. Default on.
@@@ -139,11 -136,8 +139,8 @@@ my $have_mail_address = eval { require 
  my $smtp;
  my $auth;
  
- sub unique_email_list(@);
- sub cleanup_compose_files();
  # Variables we fill in automatically, or via prompting:
 -my (@to,$no_to,@cc,$no_cc,@initial_cc,@bcclist,$no_bcc,@xh,
 +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,$compose,$time);
  
@@@ -193,11 -187,9 +190,11 @@@ sub do_edit 
  }
  
  # Variables with corresponding config settings
 -my ($thread, $chain_reply_to, $suppress_from, $signed_off_by_cc, $cc_cmd);
 -my ($smtp_server, $smtp_server_port, $smtp_authuser, $smtp_encryption);
 -my ($identity, $aliasfiletype, @alias_files, @smtp_host_parts, $smtp_domain);
 +my ($thread, $chain_reply_to, $suppress_from, $signed_off_by_cc);
 +my ($to_cmd, $cc_cmd);
 +my ($smtp_server, $smtp_server_port, @smtp_server_options);
 +my ($smtp_authuser, $smtp_encryption);
 +my ($identity, $aliasfiletype, @alias_files, $smtp_domain);
  my ($validate, $confirm);
  my (@suppress_cc);
  my ($auto_8bit_encoding);
@@@ -218,12 -210,10 +215,12 @@@ my %config_bool_settings = 
  my %config_settings = (
      "smtpserver" => \$smtp_server,
      "smtpserverport" => \$smtp_server_port,
 +    "smtpserveroption" => \@smtp_server_options,
      "smtpuser" => \$smtp_authuser,
      "smtppass" => \$smtp_authpass,
 -      "smtpdomain" => \$smtp_domain,
 -    "to" => \@to,
 +    "smtpdomain" => \$smtp_domain,
 +    "to" => \@initial_to,
 +    "tocmd" => \$to_cmd,
      "cc" => \@initial_cc,
      "cccmd" => \$cc_cmd,
      "aliasfiletype" => \$aliasfiletype,
@@@ -281,8 -271,7 +278,8 @@@ $SIG{INT}  = \&signal_handler
  my $rc = GetOptions("sender|from=s" => \$sender,
                      "in-reply-to=s" => \$initial_reply_to,
                    "subject=s" => \$initial_subject,
 -                  "to=s" => \@to,
 +                  "to=s" => \@initial_to,
 +                  "to-cmd=s" => \$to_cmd,
                    "no-to" => \$no_to,
                    "cc=s" => \@initial_cc,
                    "no-cc" => \$no_cc,
                    "no-bcc" => \$no_bcc,
                    "chain-reply-to!" => \$chain_reply_to,
                    "smtp-server=s" => \$smtp_server,
 +                  "smtp-server-option=s" => \@smtp_server_options,
                    "smtp-server-port=s" => \$smtp_server_port,
                    "smtp-user=s" => \$smtp_authuser,
                    "smtp-pass:s" => \$smtp_authpass,
@@@ -377,7 -365,7 +374,7 @@@ my(%suppress_cc)
  if (@suppress_cc) {
        foreach my $entry (@suppress_cc) {
                die "Unknown --suppress-cc field: '$entry'\n"
-                       unless $entry =~ /^(all|cccmd|cc|author|self|sob|body|bodycc)$/;
+                       unless $entry =~ /^(?:all|cccmd|cc|author|self|sob|body|bodycc)$/;
                $suppress_cc{$entry} = 1;
        }
  }
@@@ -422,7 -410,7 +419,7 @@@ my ($repoauthor, $repocommitter)
  
  # Verify the user input
  
 -foreach my $entry (@to) {
 +foreach my $entry (@initial_to) {
        die "Comma in --to entry: $entry'\n" unless $entry !~ m/,/;
  }
  
@@@ -521,12 -509,12 +518,12 @@@ while (defined(my $f = shift @ARGV)) 
                push @rev_list_opts, "--", @ARGV;
                @ARGV = ();
        } elsif (-d $f and !check_file_rev_conflict($f)) {
-               opendir(DH,$f)
+               opendir my $dh, $f
                        or die "Failed to opendir $f: $!";
  
                push @files, grep { -f $_ } map { catfile($f, $_) }
-                               sort readdir(DH);
-               closedir(DH);
+                               sort readdir $dh;
+               closedir $dh;
        } elsif ((-f $f or -p $f) and !check_file_rev_conflict($f)) {
                push @files, $f;
        } else {
@@@ -558,7 -546,7 +555,7 @@@ if (@files) 
        usage();
  }
  
- sub get_patch_subject($) {
+ sub get_patch_subject {
        my $fn = shift;
        open (my $fh, '<', $fn);
        while (my $line = <$fh>) {
@@@ -576,7 -564,7 +573,7 @@@ if ($compose) 
        $compose_filename = ($repo ?
                tempfile(".gitsendemail.msg.XXXXXX", DIR => $repo->repo_path()) :
                tempfile(".gitsendemail.msg.XXXXXX", DIR => "."))[1];
-       open(C,">",$compose_filename)
+       open my $c, ">", $compose_filename
                or die "Failed to open for writing $compose_filename: $!";
  
  
        my $tpl_subject = $initial_subject || '';
        my $tpl_reply_to = $initial_reply_to || '';
  
-       print C <<EOT;
+       print $c <<EOT;
  From $tpl_sender # This line is ignored.
  GIT: Lines beginning in "GIT:" will be removed.
  GIT: Consider including an overall diffstat or table of contents
@@@ -597,9 -585,9 +594,9 @@@ In-Reply-To: $tpl_reply_t
  
  EOT
        for my $f (@files) {
-               print C get_patch_subject($f);
+               print $c get_patch_subject($f);
        }
-       close(C);
+       close $c;
  
        if ($annotate) {
                do_edit($compose_filename, @files);
                do_edit($compose_filename);
        }
  
-       open(C2,">",$compose_filename . ".final")
+       open my $c2, ">", $compose_filename . ".final"
                or die "Failed to open $compose_filename.final : " . $!;
  
-       open(C,"<",$compose_filename)
+       open $c, "<", $compose_filename
                or die "Failed to open $compose_filename : " . $!;
  
        my $need_8bit_cte = file_has_nonascii($compose_filename);
        my $in_body = 0;
        my $summary_empty = 1;
-       while(<C>) {
+       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",
+                               print $c2 "MIME-Version: 1.0\n",
                                         "Content-Type: text/plain; ",
                                           "charset=UTF-8\n",
                                         "Content-Transfer-Encoding: 8bit\n";
                        print "To/Cc/Bcc fields are not interpreted yet, they have been ignored\n";
                        next;
                }
-               print C2 $_;
+               print $c2 $_;
        }
-       close(C);
-       close(C2);
+       close $c;
+       close $c2;
  
        if ($summary_empty) {
                print "Summary email is empty, skipping it\n";
@@@ -688,7 -676,7 +685,7 @@@ sub ask 
  
  my %broken_encoding;
  
- sub file_declares_8bit_cte($) {
+ sub file_declares_8bit_cte {
        my $fn = shift;
        open (my $fh, '<', $fn);
        while (my $line = <$fh>) {
@@@ -717,7 -705,7 +714,7 @@@ if (!defined $auto_8bit_encoding && sca
  
  if (!$force) {
        for my $f (@files) {
-               if (get_patch_subject($f) =~ /\*\*\* SUBJECT HERE \*\*\*/) {
+               if (get_patch_subject($f) =~ /\Q*** SUBJECT HERE ***\E/) {
                        die "Refusing to send because the patch\n\t$f\n"
                                . "has the template subject '*** SUBJECT HERE ***'. "
                                . "Pass --force if you really want to send.\n";
@@@ -734,9 -722,9 +731,9 @@@ if (!defined $sender) 
        $prompting++;
  }
  
 -if (!@to) {
 +if (!@initial_to && !defined $to_cmd) {
        my $to = ask("Who should the emails be sent to? ");
 -      push @to, parse_address_line($to) if defined $to; # sanitized/validated later
 +      push @initial_to, parse_address_line($to) if defined $to; # sanitized/validated later
        $prompting++;
  }
  
@@@ -754,8 -742,8 +751,8 @@@ sub expand_one_alias 
        return $aliases{$alias} ? expand_aliases(@{$aliases{$alias}}) : $alias;
  }
  
 -@to = expand_aliases(@to);
 -@to = (map { sanitize_address($_) } @to);
 +@initial_to = expand_aliases(@initial_to);
 +@initial_to = (map { sanitize_address($_) } @initial_to);
  @initial_cc = expand_aliases(@initial_cc);
  @bcclist = expand_aliases(@bcclist);
  
@@@ -789,8 -777,8 +786,8 @@@ our ($message_id, %mail, $subject, $rep
  
  sub extract_valid_address {
        my $address = shift;
-       my $local_part_regexp = '[^<>"\s@]+';
-       my $domain_regexp = '[^.<>"\s@]+(?:\.[^.<>"\s@]+)+';
+       my $local_part_regexp = qr/[^<>"\s@]+/;
+       my $domain_regexp = qr/[^.<>"\s@]+(?:\.[^.<>"\s@]+)+/;
  
        # check for a local address:
        return $address if ($address =~ /^($local_part_regexp)$/);
@@@ -831,7 -819,7 +828,7 @@@ sub make_message_id 
                last if (defined $du_part and $du_part ne '');
        }
        if (not defined $du_part or $du_part eq '') {
-               use Sys::Hostname qw();
+               require Sys::Hostname;
                $du_part = 'user@' . Sys::Hostname::hostname();
        }
        my $message_id_template = "<%s-git-send-email-%s>";
@@@ -864,8 -852,8 +861,8 @@@ sub quote_rfc2047 
  
  sub is_rfc2047_quoted {
        my $s = shift;
-       my $token = '[^][()<>@,;:"\/?.= \000-\037\177-\377]+';
-       my $encoded_text = '[!->@-~]+';
+       my $token = qr/[^][()<>@,;:"\/?.= \000-\037\177-\377]+/;
+       my $encoded_text = qr/[!->@-~]+/;
        length($s) <= 75 &&
        $s =~ m/^(?:"[[:ascii:]]*"|=\?$token\?$token\?$encoded_text\?=)$/o;
  }
@@@ -876,7 -864,7 +873,7 @@@ sub sanitize_address 
        my ($recipient_name, $recipient_addr) = ($recipient =~ /^(.*?)\s*(<.*)/);
  
        if (not $recipient_name) {
-               return "$recipient";
+               return $recipient;
        }
  
        # if recipient_name is already quoted, do nothing
        # double quotes are needed if specials or CTLs are included
        elsif ($recipient_name =~ /[][()<>@,;:\\".\000-\037\177]/) {
                $recipient_name =~ s/(["\\\r])/\\$1/g;
-               $recipient_name = "\"$recipient_name\"";
+               $recipient_name = qq["$recipient_name"];
        }
  
        return "$recipient_name $recipient_addr";
@@@ -1038,8 -1026,6 +1035,8 @@@ X-Mailer: git-send-email $gitversio
                }
        }
  
 +      unshift (@sendmail_parameters, @smtp_server_options);
 +
        if ($dry_run) {
                # We don't want to send the email.
        } elsif ($smtp_server =~ m#^/#) {
                        exec($smtp_server, @sendmail_parameters) or die $!;
                }
                print $sm "$header\n$message";
-               close $sm or die $?;
+               close $sm or die $!;
        } else {
  
                if (!defined $smtp_server) {
@@@ -1155,13 -1141,12 +1152,13 @@@ $subject = $initial_subject
  $message_num = 0;
  
  foreach my $t (@files) {
-       open(F,"<",$t) or die "can't open file $t";
+       open my $fh, "<", $t or die "can't open file $t";
  
        my $author = undef;
        my $author_encoding;
        my $has_content_type;
        my $body_encoding;
 +      @to = ();
        @cc = ();
        @xh = ();
        my $input_format = undef;
        $message = "";
        $message_num++;
        # First unfold multiline header fields
-       while(<F>) {
+       while(<$fh>) {
                last if /^\s*$/;
                if (/^\s+\S/ and @header) {
                        chomp($header[$#header]);
                                        $1, $_) unless $quiet;
                                push @cc, $1;
                        }
 +                      elsif (/^To:\s+(.*)$/) {
 +                              foreach my $addr (parse_address_line($1)) {
 +                                      printf("(mbox) Adding to: %s from line '%s'\n",
 +                                              $addr, $_) unless $quiet;
 +                                      push @to, sanitize_address($addr);
 +                              }
 +                      }
                        elsif (/^Cc:\s+(.*)$/) {
                                foreach my $addr (parse_address_line($1)) {
                                        if (unquote_rfc2047($addr) eq $sender) {
                }
        }
        # Now parse the message body
-       while(<F>) {
+       while(<$fh>) {
                $message .=  $_;
                if (/^(Signed-off-by|Cc): (.*)$/i) {
                        chomp;
                                $c, $_) unless $quiet;
                }
        }
-       close F;
+       close $fh;
  
 -      if (defined $cc_cmd && !$suppress_cc{'cccmd'}) {
 -              open my $fh, "$cc_cmd \Q$t\E |"
 -                      or die "(cc-cmd) Could not execute '$cc_cmd'";
 -              while(my $c = <$fh>) {
 -                      chomp $c;
 -                      $c =~ s/^\s*//g;
 -                      next if ($c eq $sender and $suppress_from);
 -                      push @cc, $c;
 -                      printf("(cc-cmd) Adding cc: %s from: '%s'\n",
 -                              $c, $cc_cmd) unless $quiet;
 -              }
 -              close $fh
 -                      or die "(cc-cmd) failed to close pipe to '$cc_cmd'";
 -      }
 +      push @to, recipients_cmd("to-cmd", "to", $to_cmd, $t)
 +              if defined $to_cmd;
 +      push @cc, recipients_cmd("cc-cmd", "cc", $cc_cmd, $t)
 +              if defined $cc_cmd && !$suppress_cc{'cccmd'};
  
        if ($broken_encoding{$t} && !$has_content_type) {
                $has_content_type = 1;
                ($confirm =~ /^(?:auto|compose)$/ && $compose && $message_num == 1));
        $needs_confirm = "inform" if ($needs_confirm && $confirm_unconfigured && @cc);
  
 +      @to = (@initial_to, @to);
        @cc = (@initial_cc, @cc);
  
        my $message_was_sent = send_message();
        $message_id = undef;
  }
  
-       open(F, "$cmd \Q$file\E |")
 +# 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 $sanitized_sender = sanitize_address($sender);
 +      my @addresses = ();
-       while(<F>) {
-               my $address = $_;
++      open my $fh, "$cmd \Q$file\E |"
 +          or die "($prefix) Could not execute '$cmd'";
-       close F
++      while (my $address = <$fh>) {
 +              $address =~ s/^\s*//g;
 +              $address =~ s/\s*$//g;
 +              $address = sanitize_address($address);
 +              next if ($address eq $sanitized_sender and $suppress_from);
 +              push @addresses, $address;
 +              printf("($prefix) Adding %s: %s from: '%s'\n",
 +                     $what, $address, $cmd) unless $quiet;
 +              }
++      close $fh
 +          or die "($prefix) failed to close pipe to '$cmd'";
 +      return @addresses;
 +}
 +
  cleanup_compose_files();
  
- sub cleanup_compose_files() {
+ sub cleanup_compose_files {
        unlink($compose_filename, $compose_filename . ".final") if $compose;
  }
  
  $smtp->quit if $smtp;
  
- sub unique_email_list(@) {
+ sub unique_email_list {
        my %seen;
        my @emails;
  
diff --combined t/t9001-send-email.sh
index 028758c6522f4b9668261bce5b7769da29c7812c,99a16d57502a7f880cae34863cf344a318f3b176..d1ba25205b86c3a4aeca16ec988e2ed2ecca1da2
@@@ -201,28 -201,10 +201,28 @@@ test_expect_success $PREREQ 'Prompting 
                grep "^To: to@example.com\$" msgtxt1
  '
  
 +test_expect_success $PREREQ 'tocmd works' '
 +      clean_fake_sendmail &&
 +      cp $patches tocmd.patch &&
 +      echo tocmd--tocmd@example.com >>tocmd.patch &&
 +      {
 +        echo "#!$SHELL_PATH"
 +        echo sed -n -e s/^tocmd--//p \"\$1\"
 +      } > tocmd-sed &&
 +      chmod +x tocmd-sed &&
 +      git send-email \
 +              --from="Example <nobody@example.com>" \
 +              --to-cmd=./tocmd-sed \
 +              --smtp-server="$(pwd)/fake.sendmail" \
 +              tocmd.patch \
 +              &&
 +      grep "^To: tocmd@example.com" msgtxt1
 +'
 +
  test_expect_success $PREREQ 'cccmd works' '
        clean_fake_sendmail &&
        cp $patches cccmd.patch &&
-       echo cccmd--cccmd@example.com >>cccmd.patch &&
+       echo "cccmd--  cccmd@example.com" >>cccmd.patch &&
        {
          echo "#!$SHELL_PATH"
          echo sed -n -e s/^cccmd--//p \"\$1\"
@@@ -297,7 -279,7 +297,7 @@@ test_expect_success $PREREQ 'Invalid In
                --to=nobody@example.com \
                --in-reply-to=" " \
                --smtp-server="$(pwd)/fake.sendmail" \
 -              $patches
 +              $patches \
                2>errors
        ! grep "^In-Reply-To: < *>" msgtxt1
  '
@@@ -965,45 -947,6 +965,45 @@@ test_expect_success $PREREQ '--no-bcc o
        ! grep "RCPT TO:<other@ex.com>" stdout
  '
  
 +test_expect_success $PREREQ 'patches To headers are used by default' '
 +      patch=`git format-patch -1 --to="bodies@example.com"` &&
 +      test_when_finished "rm $patch" &&
 +      git send-email \
 +              --dry-run \
 +              --from="Example <nobody@example.com>" \
 +              --smtp-server relay.example.com \
 +              $patch >stdout &&
 +      grep "RCPT TO:<bodies@example.com>" stdout
 +'
 +
 +test_expect_success $PREREQ 'patches To headers are appended to' '
 +      patch=`git format-patch -1 --to="bodies@example.com"` &&
 +      test_when_finished "rm $patch" &&
 +      git send-email \
 +              --dry-run \
 +              --from="Example <nobody@example.com>" \
 +              --to=nobody@example.com \
 +              --smtp-server relay.example.com \
 +              $patch >stdout &&
 +      grep "RCPT TO:<bodies@example.com>" stdout &&
 +      grep "RCPT TO:<nobody@example.com>" stdout
 +'
 +
 +test_expect_success $PREREQ 'To headers from files reset each patch' '
 +      patch1=`git format-patch -1 --to="bodies@example.com"` &&
 +      patch2=`git format-patch -1 --to="other@example.com" HEAD~` &&
 +      test_when_finished "rm $patch1 && rm $patch2" &&
 +      git send-email \
 +              --dry-run \
 +              --from="Example <nobody@example.com>" \
 +              --to="nobody@example.com" \
 +              --smtp-server relay.example.com \
 +              $patch1 $patch2 >stdout &&
 +      test $(grep -c "RCPT TO:<bodies@example.com>" stdout) = 1 &&
 +      test $(grep -c "RCPT TO:<nobody@example.com>" stdout) = 2 &&
 +      test $(grep -c "RCPT TO:<other@example.com>" stdout) = 1
 +'
 +
  test_expect_success $PREREQ 'setup expect' '
  cat >email-using-8bit <<EOF
  From fe6ecc66ece37198fe5db91fa2fc41d9f4fe5cc4 Mon Sep 17 00:00:00 2001