summaryrefslogtreecommitdiff
path: root/git-send-email.perl
diff options
context:
space:
mode:
Diffstat (limited to 'git-send-email.perl')
-rwxr-xr-xgit-send-email.perl290
1 files changed, 204 insertions, 86 deletions
diff --git a/git-send-email.perl b/git-send-email.perl
index 25be2ebd2a..04087221aa 100755
--- a/git-send-email.perl
+++ b/git-send-email.perl
@@ -18,21 +18,11 @@
use 5.008;
use strict;
-use warnings;
-use POSIX qw/strftime/;
-use Term::ReadLine;
+use warnings $ENV{GIT_PERL_FATAL_WARNINGS} ? qw(FATAL all) : ();
use Getopt::Long;
-use Text::ParseWords;
-use Term::ANSIColor;
-use File::Temp qw/ tempdir tempfile /;
-use File::Spec::Functions qw(catdir catfile);
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 /;
@@ -50,7 +40,8 @@ package main;
sub usage {
print <<EOT;
-git send-email [options] <file | directory | rev-list options >
+git send-email' [<options>] <file|directory>
+git send-email' [<options>] <format-patch options>
git send-email --dump-aliases
Composing:
@@ -70,6 +61,7 @@ git send-email --dump-aliases
Sending:
--envelope-sender <str> * Email envelope sender.
+ --sendmail-cmd <str> * Command to run to send email.
--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.
@@ -122,9 +114,38 @@ EOT
exit(1);
}
+sub uniq {
+ my %seen;
+ grep !$seen{$_}++, @_;
+}
+
sub completion_helper {
- print Git::command('format-patch', '--git-completion-helper');
- exit(0);
+ my ($original_opts) = @_;
+ my %not_for_completion = (
+ "git-completion-helper" => undef,
+ "h" => undef,
+ );
+ my @send_email_opts = ();
+
+ foreach my $key (keys %$original_opts) {
+ unless (exists $not_for_completion{$key}) {
+ $key =~ s/!$//;
+
+ if ($key =~ /[:=][si]$/) {
+ $key =~ s/[:=][si]$//;
+ push (@send_email_opts, "--$_=") foreach (split (/\|/, $key));
+ } else {
+ push (@send_email_opts, "--$_") foreach (split (/\|/, $key));
+ }
+ }
+ }
+
+ my @format_patch_opts = split(/ /, Git::command('format-patch', '--git-completion-helper'));
+ my @opts = (@send_email_opts, @format_patch_opts);
+ @opts = uniq (grep !/^$/, @opts);
+ # There's an implicit '\n' here already, no need to add an explicit one.
+ print "@opts";
+ exit(0);
}
# most mail servers generate the Date: header, but not all...
@@ -166,7 +187,6 @@ sub format_2822_time {
);
}
-my $have_email_valid = eval { require Email::Valid; 1 };
my $smtp;
my $auth;
my $num_sent = 0;
@@ -192,14 +212,6 @@ my (@config_bcc, @getopt_bcc);
my $repo = eval { Git->repository() };
my @repo = $repo ? ($repo) : ();
-my $term = eval {
- $ENV{"GIT_SEND_EMAIL_NOTTY"}
- ? new Term::ReadLine 'git-send-email', \*STDIN, \*STDOUT
- : new Term::ReadLine 'git-send-email';
-};
-if ($@) {
- $term = new FakeTerm "$@: going non-interactive";
-}
# Behavior modification variables
my ($quiet, $dry_run) = (0, 0);
@@ -262,6 +274,7 @@ my ($confirm);
my (@suppress_cc);
my ($auto_8bit_encoding);
my ($compose_encoding);
+my ($sendmail_cmd);
# Variables with corresponding config settings & hardcoded defaults
my ($debug_net_smtp) = 0; # Net::SMTP, see send_message()
my $thread = 1;
@@ -287,6 +300,7 @@ my %config_bool_settings = (
);
my %config_settings = (
+ "smtpencryption" => \$smtp_encryption,
"smtpserver" => \$smtp_server,
"smtpserverport" => \$smtp_server_port,
"smtpserveroption" => \@smtp_server_options,
@@ -309,6 +323,7 @@ my %config_settings = (
"assume8bitencoding" => \$auto_8bit_encoding,
"composeencoding" => \$compose_encoding,
"transferencoding" => \$target_xfer_encoding,
+ "sendmailcmd" => \$sendmail_cmd,
);
my %config_path_settings = (
@@ -318,9 +333,9 @@ my %config_path_settings = (
# Handle Uncouth Termination
sub signal_handler {
-
# Make text normal
- print color("reset"), "\n";
+ require Term::ANSIColor;
+ print Term::ANSIColor::color("reset"), "\n";
# SMTP password masked
system "stty echo";
@@ -346,11 +361,17 @@ $SIG{INT} = \&signal_handler;
# Read our sendemail.* config
sub read_config {
- my ($configured, $prefix) = @_;
+ my ($known_keys, $configured, $prefix) = @_;
foreach my $setting (keys %config_bool_settings) {
my $target = $config_bool_settings{$setting};
- my $v = Git::config_bool(@repo, "$prefix.$setting");
+ my $key = "$prefix.$setting";
+ next unless exists $known_keys->{$key};
+ my $v = (@{$known_keys->{$key}} == 1 &&
+ (defined $known_keys->{$key}->[0] &&
+ $known_keys->{$key}->[0] =~ /^(?:true|false)$/s))
+ ? $known_keys->{$key}->[0] eq 'true'
+ : Git::config_bool(@repo, $key);
next unless defined $v;
next if $configured->{$setting}++;
$$target = $v;
@@ -358,8 +379,10 @@ sub read_config {
foreach my $setting (keys %config_path_settings) {
my $target = $config_path_settings{$setting};
+ my $key = "$prefix.$setting";
+ next unless exists $known_keys->{$key};
if (ref($target) eq "ARRAY") {
- my @values = Git::config_path(@repo, "$prefix.$setting");
+ my @values = Git::config_path(@repo, $key);
next unless @values;
next if $configured->{$setting}++;
@$target = @values;
@@ -374,48 +397,77 @@ sub read_config {
foreach my $setting (keys %config_settings) {
my $target = $config_settings{$setting};
+ my $key = "$prefix.$setting";
+ next unless exists $known_keys->{$key};
if (ref($target) eq "ARRAY") {
- my @values = Git::config(@repo, "$prefix.$setting");
- next unless @values;
+ my @values = @{$known_keys->{$key}};
+ @values = grep { defined } @values;
next if $configured->{$setting}++;
@$target = @values;
}
else {
- my $v = Git::config(@repo, "$prefix.$setting");
+ my $v = $known_keys->{$key}->[-1];
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';
- }
+sub config_regexp {
+ my ($regex) = @_;
+ my @ret;
+ eval {
+ my $ret = Git::command(
+ 'config',
+ '--null',
+ '--get-regexp',
+ $regex,
+ );
+ @ret = map {
+ # We must always return ($k, $v) here, since
+ # empty config values will be just "key\0",
+ # not "key\nvalue\0".
+ my ($k, $v) = split /\n/, $_, 2;
+ ($k, $v);
+ } split /\0/, $ret;
+ 1;
+ } or do {
+ # If we have no keys we're OK, otherwise re-throw
+ die $@ if $@->value != 1;
+ };
+ return @ret;
+}
+
+# Save ourselves a lot of work of shelling out to 'git config' (it
+# parses 'bool' etc.) by only doing so for config keys that exist.
+my %known_config_keys;
+{
+ my @kv = config_regexp("^sende?mail[.]");
+ while (my ($k, $v) = splice @kv, 0, 2) {
+ push @{$known_config_keys{$k}} => $v;
}
}
# 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(
+{
+ my $key = "sendemail.identity";
+ $identity = Git::config(@repo, $key) if exists $known_config_keys{$key};
+}
+my %identity_options = (
"identity=s" => \$identity,
"no-identity" => \$no_identity,
);
+my $rc = GetOptions(%identity_options);
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");
+ read_config(\%known_config_keys, \%configured, "sendemail.$identity") if defined $identity;
+ read_config(\%known_config_keys, \%configured, "sendemail");
}
# Begin by accumulating all the variables (defined above), that we will end up
@@ -423,14 +475,17 @@ undef $identity if $no_identity;
my $help;
my $git_completion_helper;
-$rc = GetOptions("h" => \$help,
- "dump-aliases" => \$dump_aliases);
+my %dump_aliases_options = (
+ "h" => \$help,
+ "dump-aliases" => \$dump_aliases,
+);
+$rc = GetOptions(%dump_aliases_options);
usage() unless $rc;
die __("--dump-aliases incompatible with other options\n")
if !$help and $dump_aliases and @ARGV;
-$rc = GetOptions(
+my %options = (
"sender|from=s" => \$sender,
- "in-reply-to=s" => \$initial_in_reply_to,
+ "in-reply-to=s" => \$initial_in_reply_to,
"reply-to=s" => \$reply_to,
"subject=s" => \$initial_subject,
"to=s" => \@getopt_to,
@@ -442,6 +497,7 @@ $rc = GetOptions(
"no-bcc" => \$no_bcc,
"chain-reply-to!" => \$chain_reply_to,
"no-chain-reply-to" => sub {$chain_reply_to = 0},
+ "sendmail-cmd=s" => \$sendmail_cmd,
"smtp-server=s" => \$smtp_server,
"smtp-server-option=s" => \@smtp_server_options,
"smtp-server-port=s" => \$smtp_server_port,
@@ -486,7 +542,8 @@ $rc = GetOptions(
"batch-size=i" => \$batch_size,
"relogin-delay=i" => \$relogin_delay,
"git-completion-helper" => \$git_completion_helper,
- );
+);
+$rc = GetOptions(%options);
# Munge any "either config or getopt, not both" variables
my @initial_to = @getopt_to ? @getopt_to : ($no_to ? () : @config_to);
@@ -494,12 +551,13 @@ 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;
+my %all_options = (%options, %dump_aliases_options, %identity_options);
+completion_helper(\%all_options) if $git_completion_helper;
unless ($rc) {
usage();
}
-if ($forbid_sendmail_variables && (scalar Git::config_regexp("^sendmail[.]")) != 0) {
+if ($forbid_sendmail_variables && grep { /^sendmail/s } keys %known_config_keys) {
die __("fatal: found configuration options for 'sendmail'\n" .
"git-send-email is configured with the sendemail.* options - note the 'e'.\n" .
"Set sendemail.forbidSendmailVariables to false to disable this check.\n");
@@ -564,15 +622,27 @@ if (0) {
}
my ($repoauthor, $repocommitter);
-($repoauthor) = Git::ident_person(@repo, 'author');
-($repocommitter) = Git::ident_person(@repo, 'committer');
+{
+ my %cache;
+ my ($author, $committer);
+ my $common = sub {
+ my ($what) = @_;
+ return $cache{$what} if exists $cache{$what};
+ ($cache{$what}) = Git::ident_person(@repo, $what);
+ return $cache{$what};
+ };
+ $repoauthor = sub { $common->('author') };
+ $repocommitter = sub { $common->('committer') };
+}
sub parse_address_line {
+ require Git::LoadCPAN::Mail::Address;
return map { $_->format } Mail::Address->parse($_[0]);
}
sub split_addrs {
- return quotewords('\s*,\s*', 1, @_);
+ require Text::ParseWords;
+ return Text::ParseWords::quotewords('\s*,\s*', 1, @_);
}
my %aliases;
@@ -621,10 +691,11 @@ my %parse_alias = (
s/\\"/"/g foreach @addr;
$aliases{$alias} = \@addr
}}},
- mailrc => sub { my $fh = shift; while (<$fh>) {
+ mailrc => sub { my $fh = shift; while (<$fh>) {
if (/^alias\s+(\S+)\s+(.*?)\s*$/) {
+ require Text::ParseWords;
# spaces delimit multiple addresses
- $aliases{$1} = [ quotewords('\s+', 0, $2) ];
+ $aliases{$1} = [ Text::ParseWords::quotewords('\s+', 0, $2) ];
}}},
pine => sub { my $fh = shift; my $f='\t[^\t]*';
for (my $x = ''; defined($x); $x = $_) {
@@ -672,7 +743,7 @@ sub is_format_patch_arg {
if (defined($format_patch)) {
return $format_patch;
}
- die sprintf(__ <<EOF, $f, $f);
+ die sprintf(__(<<EOF), $f, $f);
File '%s' exists but it could also be the range of commits
to produce patches for. Please disambiguate by...
@@ -696,7 +767,8 @@ while (defined(my $f = shift @ARGV)) {
opendir my $dh, $f
or die sprintf(__("Failed to opendir %s: %s"), $f, $!);
- push @files, grep { -f $_ } map { catfile($f, $_) }
+ require File::Spec;
+ push @files, grep { -f $_ } map { File::Spec->catfile($f, $_) }
sort readdir $dh;
closedir $dh;
} elsif ((-f $f or -p $f) and !is_format_patch_arg($f)) {
@@ -709,7 +781,8 @@ while (defined(my $f = shift @ARGV)) {
if (@rev_list_opts) {
die __("Cannot run git format-patch from outside a repository\n")
unless $repo;
- push @files, $repo->command('format-patch', '-o', tempdir(CLEANUP => 1), @rev_list_opts);
+ require File::Temp;
+ push @files, $repo->command('format-patch', '-o', File::Temp::tempdir(CLEANUP => 1), @rev_list_opts);
}
@files = handle_backup_files(@files);
@@ -746,19 +819,20 @@ sub get_patch_subject {
if ($compose) {
# Note that this does not need to be secure, but we will make a small
# effort to have it be unique
+ require File::Temp;
$compose_filename = ($repo ?
- tempfile(".gitsendemail.msg.XXXXXX", DIR => $repo->repo_path()) :
- tempfile(".gitsendemail.msg.XXXXXX", DIR => "."))[1];
+ File::Temp::tempfile(".gitsendemail.msg.XXXXXX", DIR => $repo->repo_path()) :
+ File::Temp::tempfile(".gitsendemail.msg.XXXXXX", DIR => "."))[1];
open my $c, ">", $compose_filename
or die sprintf(__("Failed to open for writing %s: %s"), $compose_filename, $!);
- my $tpl_sender = $sender || $repoauthor || $repocommitter || '';
+ my $tpl_sender = $sender || $repoauthor->() || $repocommitter->() || '';
my $tpl_subject = $initial_subject || '';
my $tpl_in_reply_to = $initial_in_reply_to || '';
my $tpl_reply_to = $reply_to || '';
- print $c <<EOT1, Git::prefix_lines("GIT: ", __ <<EOT2), <<EOT3;
+ print $c <<EOT1, Git::prefix_lines("GIT: ", __(<<EOT2)), <<EOT3;
From $tpl_sender # This line is ignored.
EOT1
Lines beginning in "GIT:" will be removed.
@@ -855,6 +929,19 @@ EOT3
do_edit(@files);
}
+sub term {
+ my $term = eval {
+ require Term::ReadLine;
+ $ENV{"GIT_SEND_EMAIL_NOTTY"}
+ ? Term::ReadLine->new('git-send-email', \*STDIN, \*STDOUT)
+ : Term::ReadLine->new('git-send-email');
+ };
+ if ($@) {
+ $term = FakeTerm->new("$@: going non-interactive");
+ }
+ return $term;
+}
+
sub ask {
my ($prompt, %arg) = @_;
my $valid_re = $arg{valid_re};
@@ -862,6 +949,7 @@ sub ask {
my $confirm_only = $arg{confirm_only};
my $resp;
my $i = 0;
+ my $term = term();
return defined $default ? $default : undef
unless defined $term->IN and defined fileno($term->IN) and
defined $term->OUT and defined fileno($term->OUT);
@@ -959,7 +1047,7 @@ if (defined $sender) {
$sender =~ s/^\s+|\s+$//g;
($sender) = expand_aliases($sender);
} else {
- $sender = $repoauthor || $repocommitter || '';
+ $sender = $repoauthor->() || $repocommitter->() || '';
}
# $sender could be an already sanitized address
@@ -1013,16 +1101,19 @@ if (defined $reply_to) {
$reply_to = sanitize_address($reply_to);
}
-if (!defined $smtp_server) {
+if (!defined $sendmail_cmd && !defined $smtp_server) {
my @sendmail_paths = qw( /usr/sbin/sendmail /usr/lib/sendmail );
push @sendmail_paths, map {"$_/sendmail"} split /:/, $ENV{PATH};
foreach (@sendmail_paths) {
if (-x $_) {
- $smtp_server = $_;
+ $sendmail_cmd = $_;
last;
}
}
- $smtp_server ||= 'localhost'; # could be 127.0.0.1, too... *shrug*
+
+ if (!defined $sendmail_cmd) {
+ $smtp_server = 'localhost'; # could be 127.0.0.1, too... *shrug*
+ }
}
if ($compose && $compose > 0) {
@@ -1042,6 +1133,7 @@ sub extract_valid_address {
return $address if ($address =~ /^($local_part_regexp)$/);
$address =~ s/^\s*<(.*)>\s*$/$1/;
+ my $have_email_valid = eval { require Email::Valid; 1 };
if ($have_email_valid) {
return scalar Email::Valid->address($address);
}
@@ -1101,14 +1193,15 @@ my ($message_id_stamp, $message_id_serial);
sub make_message_id {
my $uniq;
if (!defined $message_id_stamp) {
- $message_id_stamp = strftime("%Y%m%d%H%M%S.$$", gmtime(time));
+ require POSIX;
+ $message_id_stamp = POSIX::strftime("%Y%m%d%H%M%S.$$", gmtime(time));
$message_id_serial = 0;
}
$message_id_serial++;
$uniq = "$message_id_stamp-$message_id_serial";
my $du_part;
- for ($sender, $repocommitter, $repoauthor) {
+ for ($sender, $repocommitter->(), $repoauthor->()) {
$du_part = extract_valid_address(sanitize_address($_));
last if (defined $du_part and $du_part ne '');
}
@@ -1271,6 +1364,7 @@ sub valid_fqdn {
sub maildomain_net {
my $maildomain;
+ require Net::Domain;
my $domain = Net::Domain::domainname();
$maildomain = $domain if valid_fqdn($domain);
@@ -1281,6 +1375,7 @@ sub maildomain_mta {
my $maildomain;
for my $host (qw(mailhost localhost)) {
+ require Net::SMTP;
my $smtp = Net::SMTP->new($host);
if (defined $smtp) {
my $domain = $smtp->domain;
@@ -1502,11 +1597,17 @@ EOF
if ($dry_run) {
# We don't want to send the email.
- } elsif (file_name_is_absolute($smtp_server)) {
+ } elsif (defined $sendmail_cmd || file_name_is_absolute($smtp_server)) {
my $pid = open my $sm, '|-';
defined $pid or die $!;
if (!$pid) {
- exec($smtp_server, @sendmail_parameters) or die $!;
+ if (defined $sendmail_cmd) {
+ exec ("sh", "-c", "$sendmail_cmd \"\$@\"", "-", @sendmail_parameters)
+ or die $!;
+ } else {
+ exec ($smtp_server, @sendmail_parameters)
+ or die $!;
+ }
}
print $sm "$header\n$message";
close $sm or die $!;
@@ -1602,14 +1703,21 @@ EOF
printf($dry_run ? __("Dry-Sent %s\n") : __("Sent %s\n"), $subject);
} else {
print($dry_run ? __("Dry-OK. Log says:\n") : __("OK. Log says:\n"));
- if (!file_name_is_absolute($smtp_server)) {
+ if (!defined $sendmail_cmd && !file_name_is_absolute($smtp_server)) {
print "Server: $smtp_server\n";
print "MAIL FROM:<$raw_from>\n";
foreach my $entry (@recipients) {
print "RCPT TO:<$entry>\n";
}
} else {
- print "Sendmail: $smtp_server ".join(' ',@sendmail_parameters)."\n";
+ my $sm;
+ if (defined $sendmail_cmd) {
+ $sm = $sendmail_cmd;
+ } else {
+ $sm = $smtp_server;
+ }
+
+ print "Sendmail: $sm ".join(' ',@sendmail_parameters)."\n";
}
print $header, "\n";
if ($smtp) {
@@ -1625,7 +1733,6 @@ EOF
$in_reply_to = $initial_in_reply_to;
$references = $initial_in_reply_to || '';
-$subject = $initial_subject;
$message_num = 0;
# Prepares the email, prompts the user, sends it out
@@ -1648,6 +1755,7 @@ sub process_file {
@xh = ();
my $input_format = undef;
my @header = ();
+ $subject = $initial_subject;
$message = "";
$message_num++;
# First unfold multiline header fields
@@ -1854,15 +1962,23 @@ sub process_file {
}
# set up for the next message
- if ($thread && $message_was_sent &&
- ($chain_reply_to || !defined $in_reply_to || length($in_reply_to) == 0 ||
- $message_num == 1)) {
- $in_reply_to = $message_id;
- if (length $references > 0) {
- $references .= "\n $message_id";
- } else {
- $references = "$message_id";
+ if ($thread) {
+ if ($message_was_sent &&
+ ($chain_reply_to || !defined $in_reply_to || length($in_reply_to) == 0 ||
+ $message_num == 1)) {
+ $in_reply_to = $message_id;
+ if (length $references > 0) {
+ $references .= "\n $message_id";
+ } else {
+ $references = "$message_id";
+ }
}
+ } elsif (!defined $initial_in_reply_to) {
+ # --thread and --in-reply-to manage the "In-Reply-To" header and by
+ # extension the "References" header. If these commands are not used, reset
+ # the header values to their defaults.
+ $in_reply_to = undef;
+ $references = '';
}
$message_id = undef;
$num_sent++;
@@ -1960,13 +2076,15 @@ sub validate_patch {
if ($repo) {
my $hooks_path = $repo->command_oneline('rev-parse', '--git-path', 'hooks');
- my $validate_hook = catfile($hooks_path,
+ require File::Spec;
+ my $validate_hook = File::Spec->catfile($hooks_path,
'sendemail-validate');
my $hook_error;
if (-x $validate_hook) {
- my $target = abs_path($fn);
+ require Cwd;
+ my $target = Cwd::abs_path($fn);
# The hook needs a correct cwd and GIT_DIR.
- my $cwd_save = cwd();
+ my $cwd_save = Cwd::getcwd();
chdir($repo->wc_path() or $repo->repo_path())
or die("chdir: $!");
local $ENV{"GIT_DIR"} = $repo->repo_path();