diff --git a/git-send-email.perl b/git-send-email.perl
index ec1428d961206f12e195274aea6ea3ae52374898..7b1cca70abcfcbf12c171c91d3f71ad4e43b0474 100755 (executable)
--- a/git-send-email.perl
+++ b/git-send-email.perl
use strict;
use warnings;
use Term::ReadLine;
use strict;
use warnings;
use Term::ReadLine;
-use Mail::Sendmail qw(sendmail %mailcfg);
use Getopt::Long;
use Data::Dumper;
use Getopt::Long;
use Data::Dumper;
-use Email::Valid;
+
+# most mail servers generate the Date: header, but not all...
+$ENV{LC_ALL} = 'C';
+use POSIX qw/strftime/;
+
+my $have_email_valid = eval { require Email::Valid; 1 };
+my $smtp;
sub unique_email_list(@);
sub cleanup_compose_files();
sub unique_email_list(@);
sub cleanup_compose_files();
my $compose_filename = ".msg.$$";
# Variables we fill in automatically, or via prompting:
my $compose_filename = ".msg.$$";
# Variables we fill in automatically, or via prompting:
-my (@to,@cc,$initial_reply_to,$initial_subject,@files,$from,$compose);
+my (@to,@cc,@initial_cc,@bcclist,
+ $initial_reply_to,$initial_subject,@files,$from,$compose,$time);
# Behavior modification variables
# Behavior modification variables
-my ($chain_reply_to, $smtp_server) = (1, "localhost");
+my ($chain_reply_to, $quiet, $suppress_from, $no_signed_off_cc) = (1, 0, 0, 0);
+my $smtp_server;
# Example reply to:
#$initial_reply_to = ''; #<20050203173208.GA23964@foobar.com>';
# Example reply to:
#$initial_reply_to = ''; #<20050203173208.GA23964@foobar.com>';
"in-reply-to=s" => \$initial_reply_to,
"subject=s" => \$initial_subject,
"to=s" => \@to,
"in-reply-to=s" => \$initial_reply_to,
"subject=s" => \$initial_subject,
"to=s" => \@to,
+ "cc=s" => \@initial_cc,
+ "bcc=s" => \@bcclist,
"chain-reply-to!" => \$chain_reply_to,
"smtp-server=s" => \$smtp_server,
"compose" => \$compose,
"chain-reply-to!" => \$chain_reply_to,
"smtp-server=s" => \$smtp_server,
"compose" => \$compose,
+ "quiet" => \$quiet,
+ "suppress-from" => \$suppress_from,
+ "no-signed-off-cc|no-signed-off-by-cc" => \$no_signed_off_cc,
);
# Now, let's fill any that aren't set in with defaults:
);
# Now, let's fill any that aren't set in with defaults:
-open(GITVAR,"-|","git-var","-l")
- or die "Failed to open pipe from git-var: $!";
-
-my ($author,$committer);
-while(<GITVAR>) {
- chomp;
- my ($var,$data) = split /=/,$_,2;
- my @fields = split /\s+/, $data;
+sub gitvar {
+ my ($var) = @_;
+ my $fh;
+ my $pid = open($fh, '-|');
+ die "$!" unless defined $pid;
+ if (!$pid) {
+ exec('git-var', $var) or die "$!";
+ }
+ my ($val) = <$fh>;
+ close $fh or die "$!";
+ chomp($val);
+ return $val;
+}
- my $ident = join(" ", @fields[0...(@fields-3)]);
+sub gitvar_ident {
+ my ($name) = @_;
+ my $val = gitvar($name);
+ my @field = split(/\s+/, $val);
+ return join(' ', @field[0...(@field-3)]);
+}
- if ($var eq 'GIT_AUTHOR_IDENT') {
- $author = $ident;
- } elsif ($var eq 'GIT_COMMITTER_IDENT') {
- $committer = $ident;
+my ($author) = gitvar_ident('GIT_AUTHOR_IDENT');
+my ($committer) = gitvar_ident('GIT_COMMITTER_IDENT');
+
+my %aliases;
+chomp(my @alias_files = `git-repo-config --get-all sendemail.aliasesfile`);
+chomp(my $aliasfiletype = `git-repo-config sendemail.aliasfiletype`);
+my %parse_alias = (
+ # multiline formats can be supported in the future
+ mutt => sub { my $fh = shift; while (<$fh>) {
+ if (/^alias\s+(\S+)\s+(.*)$/) {
+ my ($alias, $addr) = ($1, $2);
+ $addr =~ s/#.*$//; # mutt allows # comments
+ # commas delimit multiple addresses
+ $aliases{$alias} = [ split(/\s*,\s*/, $addr) ];
+ }}},
+ mailrc => sub { my $fh = shift; while (<$fh>) {
+ if (/^alias\s+(\S+)\s+(.*)$/) {
+ # spaces delimit multiple addresses
+ $aliases{$1} = [ split(/\s+/, $2) ];
+ }}},
+ pine => sub { my $fh = shift; while (<$fh>) {
+ if (/^(\S+)\s+(.*)$/) {
+ $aliases{$1} = [ split(/\s*,\s*/, $2) ];
+ }}},
+ gnus => sub { my $fh = shift; while (<$fh>) {
+ if (/\(define-mail-alias\s+"(\S+?)"\s+"(\S+?)"\)/) {
+ $aliases{$1} = [ $2 ];
+ }}}
+);
+
+if (@alias_files && defined $parse_alias{$aliasfiletype}) {
+ foreach my $file (@alias_files) {
+ open my $fh, '<', $file or die "opening $file: $!\n";
+ $parse_alias{$aliasfiletype}->($fh);
+ close $fh;
}
}
}
}
-close(GITVAR);
my $prompting = 0;
if (!defined $from) {
my $prompting = 0;
if (!defined $from) {
$prompting++;
}
$prompting++;
}
+sub expand_aliases {
+ my @cur = @_;
+ my @last;
+ do {
+ @last = @cur;
+ @cur = map { $aliases{$_} ? @{$aliases{$_}} : $_ } @last;
+ } while (join(',',@cur) ne join(',',@last));
+ return @cur;
+}
+
+@to = expand_aliases(@to);
+@initial_cc = expand_aliases(@initial_cc);
+@bcclist = expand_aliases(@bcclist);
+
if (!defined $initial_subject && $compose) {
do {
$_ = $term->readline("What subject should the emails start with? ",
if (!defined $initial_subject && $compose) {
do {
$_ = $term->readline("What subject should the emails start with? ",
$initial_reply_to =~ s/(^\s+|\s+$)//g;
}
$initial_reply_to =~ s/(^\s+|\s+$)//g;
}
-if (!defined $smtp_server) {
- $smtp_server = "localhost";
+if (!$smtp_server) {
+ foreach (qw( /usr/sbin/sendmail /usr/lib/sendmail )) {
+ if (-x $_) {
+ $smtp_server = $_;
+ last;
+ }
+ }
+ $smtp_server ||= 'localhost'; # could be 127.0.0.1, too... *shrug*
}
if ($compose) {
}
if ($compose) {
# effort to have it be unique
open(C,">",$compose_filename)
or die "Failed to open for writing $compose_filename: $!";
# effort to have it be unique
open(C,">",$compose_filename)
or die "Failed to open for writing $compose_filename: $!";
- print C "From \n";
+ print C "From $from # This line is ignored.\n";
printf C "Subject: %s\n\n", $initial_subject;
printf C <<EOT;
GIT: Please enter your email below.
printf C "Subject: %s\n\n", $initial_subject;
printf C <<EOT;
GIT: Please enter your email below.
}
if (@files) {
}
if (@files) {
- print $_,"\n" for @files;
+ unless ($quiet) {
+ print $_,"\n" for (@files);
+ }
} else {
print <<EOT;
git-send-email [options] <file | directory> [... file | directory ]
} else {
print <<EOT;
git-send-email [options] <file | directory> [... file | directory ]
--to Specify the primary "To:" line of the email.
--to Specify the primary "To:" line of the email.
+ --cc Specify an initial "Cc:" list for the entire series
+ of emails.
+
+ --bcc Specify a list of email addresses that should be Bcc:
+ on all the emails.
+
--compose Use \$EDITOR to edit an introductory message for the
patch series.
--compose Use \$EDITOR to edit an introductory message for the
patch series.
email sent, rather than to the first email sent.
Defaults to on.
email sent, rather than to the first email sent.
Defaults to on.
+ --no-signed-off-cc Suppress the automatic addition of email addresses
+ that appear in a Signed-off-by: line, to the cc: list.
+ Note: Using this option is not recommended.
+
--smtp-server If set, specifies the outgoing SMTP server to use.
Defaults to localhost.
--smtp-server If set, specifies the outgoing SMTP server to use.
Defaults to localhost.
+ --suppress-from Supress sending emails to yourself if your address
+ appears in a From: line.
+
+ --quiet Make git-send-email less verbose. One line per email should be
+ all that is output.
+
Error: Please specify a file or a directory on the command line.
EOT
exit(1);
}
# Variables we set as part of the loop over files
Error: Please specify a file or a directory on the command line.
EOT
exit(1);
}
# Variables we set as part of the loop over files
-our ($message_id, $cc, %mail, $subject, $reply_to, $message);
+our ($message_id, $cc, %mail, $subject, $reply_to, $references, $message);
+sub extract_valid_address {
+ my $address = shift;
+ my $local_part_regexp = '[^<>"\s@]+';
+ my $domain_regexp = '[^.<>"\s@]+(?:\.[^.<>"\s@]+)+';
+
+ # check for a local address:
+ return $address if ($address =~ /^($local_part_regexp)$/);
+
+ if ($have_email_valid) {
+ return scalar Email::Valid->address($address);
+ } else {
+ # less robust/correct than the monster regexp in Email::Valid,
+ # but still does a 99% job, and one less dependency
+ $address =~ /($local_part_regexp\@$domain_regexp)/;
+ return $1;
+ }
+}
# Usually don't need to change anything below here.
# Usually don't need to change anything below here.
# 1 second since the last time we were called.
# We'll setup a template for the message id, using the "from" address:
# 1 second since the last time we were called.
# We'll setup a template for the message id, using the "from" address:
-my $message_id_from = Email::Valid->address($from);
+my $message_id_from = extract_valid_address($from);
my $message_id_template = "<%s-git-send-email-$message_id_from>";
sub make_message_id
{
my $message_id_template = "<%s-git-send-email-$message_id_from>";
sub make_message_id
{
- my $date = `date "+\%s"`;
- chomp($date);
+ my $date = time;
my $pseudo_rand = int (rand(4200));
$message_id = sprintf $message_id_template, "$date$pseudo_rand";
#print "new message id = $message_id\n"; # Was useful for debugging
my $pseudo_rand = int (rand(4200));
$message_id = sprintf $message_id_template, "$date$pseudo_rand";
#print "new message id = $message_id\n"; # Was useful for debugging
$cc = "";
$cc = "";
+$time = time - scalar $#files;
sub send_message
{
sub send_message
{
- my $to = join (", ", unique_email_list(@to));
-
- %mail = ( To => $to,
- From => $from,
- CC => $cc,
- Subject => $subject,
- Message => $message,
- 'Reply-to' => $from,
- 'In-Reply-To' => $reply_to,
- 'Message-ID' => $message_id,
- 'X-Mailer' => "git-send-email",
- );
-
- $mail{smtp} = $smtp_server;
- $mailcfg{mime} = 0;
-
- #print Data::Dumper->Dump([\%mail],[qw(*mail)]);
+ my @recipients = unique_email_list(@to);
+ my $to = join (",\n\t", @recipients);
+ @recipients = unique_email_list(@recipients,@cc,@bcclist);
+ my $date = strftime('%a, %d %b %Y %H:%M:%S %z', localtime($time++));
+ my $gitversion = '@@GIT_VERSION@@';
+ if ($gitversion =~ m/..GIT_VERSION../) {
+ $gitversion = `git --version`;
+ chomp $gitversion;
+ # keep only what's after the last space
+ $gitversion =~ s/^.* //;
+ }
- sendmail(%mail) or die $Mail::Sendmail::error;
+ my $header = "From: $from
+To: $to
+Cc: $cc
+Subject: $subject
+Reply-To: $from
+Date: $date
+Message-Id: $message_id
+X-Mailer: git-send-email $gitversion
+";
+ if ($reply_to) {
+
+ $header .= "In-Reply-To: $reply_to\n";
+ $header .= "References: $references\n";
+ }
- print "OK. Log says:\n", $Mail::Sendmail::log;
- print "\n\n"
+ if ($smtp_server =~ m#^/#) {
+ my $pid = open my $sm, '|-';
+ defined $pid or die $!;
+ if (!$pid) {
+ exec($smtp_server,'-i',
+ map { extract_valid_address($_) }
+ @recipients) or die $!;
+ }
+ print $sm "$header\n$message";
+ close $sm or die $?;
+ } else {
+ require Net::SMTP;
+ $smtp ||= Net::SMTP->new( $smtp_server );
+ $smtp->mail( $from ) or die $smtp->message;
+ $smtp->to( @recipients ) or die $smtp->message;
+ $smtp->data or die $smtp->message;
+ $smtp->datasend("$header\n$message") or die $smtp->message;
+ $smtp->dataend() or die $smtp->message;
+ $smtp->ok or die "Failed to send $subject\n".$smtp->message;
+ }
+ if ($quiet) {
+ printf "Sent %s\n", $subject;
+ } else {
+ print "OK. Log says:\nDate: $date\n";
+ if ($smtp) {
+ print "Server: $smtp_server\n";
+ } else {
+ print "Sendmail: $smtp_server\n";
+ }
+ print "From: $from\nSubject: $subject\nCc: $cc\nTo: $to\n\n";
+ if ($smtp) {
+ print "Result: ", $smtp->code, ' ',
+ ($smtp->message =~ /\n([^\n]+\n)$/s), "\n";
+ } else {
+ print "Result: OK\n";
+ }
+ }
}
}
-
$reply_to = $initial_reply_to;
$reply_to = $initial_reply_to;
+$references = $initial_reply_to || '';
make_message_id();
$subject = $initial_subject;
foreach my $t (@files) {
make_message_id();
$subject = $initial_subject;
foreach my $t (@files) {
- my $F = $t;
open(F,"<",$t) or die "can't open file $t";
open(F,"<",$t) or die "can't open file $t";
- @cc = ();
+ my $author_not_sender = undef;
+ @cc = @initial_cc;
my $found_mbox = 0;
my $header_done = 0;
$message = "";
my $found_mbox = 0;
my $header_done = 0;
$message = "";
$subject = $1;
} elsif (/^(Cc|From):\s+(.*)$/) {
$subject = $1;
} elsif (/^(Cc|From):\s+(.*)$/) {
+ if ($2 eq $from) {
+ next if ($suppress_from);
+ }
+ else {
+ $author_not_sender = $2;
+ }
printf("(mbox) Adding cc: %s from line '%s'\n",
printf("(mbox) Adding cc: %s from line '%s'\n",
- $2, $_);
+ $2, $_) unless $quiet;
push @cc, $2;
}
push @cc, $2;
}
# So let's support that, too.
if (@cc == 0) {
printf("(non-mbox) Adding cc: %s from line '%s'\n",
# So let's support that, too.
if (@cc == 0) {
printf("(non-mbox) Adding cc: %s from line '%s'\n",
- $_, $_);
+ $_, $_) unless $quiet;
push @cc, $_;
push @cc, $_;
}
} else {
$message .= $_;
}
} else {
$message .= $_;
- if (/^Signed-off-by: (.*)$/i) {
+ if (/^Signed-off-by: (.*)$/i && !$no_signed_off_cc) {
my $c = $1;
chomp $c;
push @cc, $c;
printf("(sob) Adding cc: %s from line '%s'\n",
my $c = $1;
chomp $c;
push @cc, $c;
printf("(sob) Adding cc: %s from line '%s'\n",
- $c, $_);
+ $c, $_) unless $quiet;
}
}
}
close F;
}
}
}
close F;
+ if (defined $author_not_sender) {
+ $message = "From: $author_not_sender\n\n$message";
+ }
$cc = join(", ", unique_email_list(@cc));
$cc = join(", ", unique_email_list(@cc));
# set up for the next message
if ($chain_reply_to || length($reply_to) == 0) {
$reply_to = $message_id;
# set up for the next message
if ($chain_reply_to || length($reply_to) == 0) {
$reply_to = $message_id;
+ if (length $references > 0) {
+ $references .= " $message_id";
+ } else {
+ $references = "$message_id";
+ }
}
make_message_id();
}
}
make_message_id();
}
}
}
-
+$smtp->quit if $smtp;
sub unique_email_list(@) {
my %seen;
my @emails;
foreach my $entry (@_) {
sub unique_email_list(@) {
my %seen;
my @emails;
foreach my $entry (@_) {
- my $clean = Email::Valid->address($entry);
- next if $seen{$clean}++;
- push @emails, $entry;
+ if (my $clean = extract_valid_address($entry)) {
+ $seen{$clean} ||= 0;
+ next if $seen{$clean}++;
+ push @emails, $entry;
+ } else {
+ print STDERR "W: unable to extract a valid address",
+ " from: $entry\n";
+ }
}
return @emails;
}
}
return @emails;
}