use Term::ReadLine;
use Getopt::Long;
use Data::Dumper;
+use Term::ANSIColor;
use Git;
+$SIG{INT} = sub { print color("reset"), "\n"; exit };
+
package FakeTerm;
sub new {
my ($class, $reason) = @_;
--signed-off-cc Automatically add email addresses that appear in
Signed-off-by: or Cc: lines to the cc: list. Defaults to on.
+ --identity The configuration identity, a subsection to prioritise over
+ the default section.
+
--smtp-server If set, specifies the outgoing SMTP server to use.
- Defaults to localhost.
+ Defaults to localhost. Port number can be specified here with
+ hostname:port format or by using --smtp-server-port option.
+
+ --smtp-server-port Specify a port on the outgoing SMTP server to connect to.
+
+ --smtp-user The username for SMTP-AUTH.
- --suppress-from Suppress sending emails to yourself if your address
- appears in a From: line. Defaults to off.
+ --smtp-pass The password for SMTP-AUTH.
+
+ --smtp-ssl If set, connects to the SMTP server using SSL.
+
+ --suppress-from Suppress sending emails to yourself. Defaults to off.
--thread Specify that the "In-Reply-To:" header should be set on all
emails. Defaults to on.
--envelope-sender Specify the envelope sender used to send the emails.
+ --no-validate Don't perform any sanity checks on patches.
+
EOT
exit(1);
}
my $have_email_valid = eval { require Email::Valid; 1 };
my $smtp;
+my $auth;
sub unique_email_list(@);
sub cleanup_compose_files();
my (@to,@cc,@initial_cc,@bcclist,@xh,
$initial_reply_to,$initial_subject,@files,$author,$sender,$compose,$time);
-my $smtp_server;
my $envelope_sender;
# Example reply to:
# Variables with corresponding config settings
my ($thread, $chain_reply_to, $suppress_from, $signed_off_cc, $cc_cmd);
+my ($smtp_server, $smtp_server_port, $smtp_authuser, $smtp_authpass, $smtp_ssl);
+my ($identity, $aliasfiletype, @alias_files, @smtp_host_parts);
+my ($no_validate);
-my %config_settings = (
+my %config_bool_settings = (
"thread" => [\$thread, 1],
"chainreplyto" => [\$chain_reply_to, 1],
"suppressfrom" => [\$suppress_from, 0],
"signedoffcc" => [\$signed_off_cc, 1],
- "cccmd" => [\$cc_cmd, ""],
+ "smtpssl" => [\$smtp_ssl, 0],
);
-foreach my $setting (keys %config_settings) {
- my $config = $repo->config_bool("sendemail.$setting");
- ${$config_settings{$setting}->[0]} = (defined $config) ? $config : $config_settings{$setting}->[1];
-}
-
-@bcclist = $repo->config('sendemail.bcc');
-if (!@bcclist or !$bcclist[0]) {
- @bcclist = ();
-}
+my %config_settings = (
+ "smtpserver" => \$smtp_server,
+ "smtpserverport" => \$smtp_server_port,
+ "smtpuser" => \$smtp_authuser,
+ "smtppass" => \$smtp_authpass,
+ "to" => \@to,
+ "cccmd" => \$cc_cmd,
+ "aliasfiletype" => \$aliasfiletype,
+ "bcc" => \@bcclist,
+ "aliasesfile" => \@alias_files,
+);
# Begin by accumulating all the variables (defined above), that we will end up
# needing, first, from the command line:
"bcc=s" => \@bcclist,
"chain-reply-to!" => \$chain_reply_to,
"smtp-server=s" => \$smtp_server,
+ "smtp-server-port=s" => \$smtp_server_port,
+ "smtp-user=s" => \$smtp_authuser,
+ "smtp-pass=s" => \$smtp_authpass,
+ "smtp-ssl!" => \$smtp_ssl,
+ "identity=s" => \$identity,
"compose" => \$compose,
"quiet" => \$quiet,
"cc-cmd=s" => \$cc_cmd,
"dry-run" => \$dry_run,
"envelope-sender=s" => \$envelope_sender,
"thread!" => \$thread,
+ "no-validate" => \$no_validate,
);
unless ($rc) {
usage();
}
+# Now, let's fill any that aren't set in with defaults:
+
+sub read_config {
+ my ($prefix) = @_;
+
+ foreach my $setting (keys %config_bool_settings) {
+ my $target = $config_bool_settings{$setting}->[0];
+ $$target = $repo->config_bool("$prefix.$setting") unless (defined $$target);
+ }
+
+ foreach my $setting (keys %config_settings) {
+ my $target = $config_settings{$setting};
+ if (ref($target) eq "ARRAY") {
+ unless (@$target) {
+ my @values = $repo->config("$prefix.$setting");
+ @$target = @values if (@values && defined $values[0]);
+ }
+ }
+ else {
+ $$target = $repo->config("$prefix.$setting") unless (defined $$target);
+ }
+ }
+}
+
+# read configuration from [sendemail "$identity"], fall back on [sendemail]
+$identity = $repo->config("sendemail.identity") unless (defined $identity);
+read_config("sendemail.$identity") if (defined $identity);
+read_config("sendemail");
+
+# fall back on builtin bool defaults
+foreach my $setting (values %config_bool_settings) {
+ ${$setting->[0]} = $setting->[1] unless (defined (${$setting->[0]}));
+}
+
+my ($repoauthor) = $repo->ident_person('author');
+my ($repocommitter) = $repo->ident_person('committer');
+
# Verify the user input
foreach my $entry (@to) {
die "Comma in --bcclist entry: $entry'\n" unless $entry !~ m/,/;
}
-# Now, let's fill any that aren't set in with defaults:
-
-my ($repoauthor) = $repo->ident_person('author');
-my ($repocommitter) = $repo->ident_person('committer');
-
my %aliases;
-my @alias_files = $repo->config('sendemail.aliasesfile');
-my $aliasfiletype = $repo->config('sendemail.aliasfiletype');
my %parse_alias = (
# multiline formats can be supported in the future
mutt => sub { my $fh = shift; while (<$fh>) {
($sender) = expand_aliases($sender) if defined $sender;
+# Now that all the defaults are set, process the rest of the command line
+# arguments and collect up the files that need to be processed.
+for my $f (@ARGV) {
+ if (-d $f) {
+ opendir(DH,$f)
+ or die "Failed to opendir $f: $!";
+
+ push @files, grep { -f $_ } map { +$f . "/" . $_ }
+ sort readdir(DH);
+
+ } elsif (-f $f) {
+ push @files, $f;
+
+ } else {
+ print STDERR "Skipping $f - not found.\n";
+ }
+}
+
+if (!$no_validate) {
+ foreach my $f (@files) {
+ my $error = validate_patch($f);
+ $error and die "fatal: $f: $error\nwarning: no patches were sent\n";
+ }
+}
+
+if (@files) {
+ unless ($quiet) {
+ print $_,"\n" for (@files);
+ }
+} else {
+ print STDERR "\nNo patch files specified!\n\n";
+ usage();
+}
+
my $prompting = 0;
if (!defined $sender) {
$sender = $repoauthor || $repocommitter;
if (!defined $initial_subject && $compose) {
do {
- $_ = $term->readline("What subject should the emails start with? ",
+ $_ = $term->readline("What subject should the initial email start with? ",
$initial_subject);
} while (!defined $_);
$initial_subject = $_;
} while (!defined $_);
$initial_reply_to = $_;
- $initial_reply_to =~ s/(^\s+|\s+$)//g;
}
-
-if (!$smtp_server) {
- $smtp_server = $repo->config('sendemail.smtpserver');
+if (defined $initial_reply_to && $_ ne "") {
+ $initial_reply_to =~ s/^\s*<?/</;
+ $initial_reply_to =~ s/>?\s*$/>/;
}
-if (!$smtp_server) {
+
+if (!defined $smtp_server) {
foreach (qw( /usr/sbin/sendmail /usr/lib/sendmail )) {
if (-x $_) {
$smtp_server = $_;
close(C);
my $editor = $ENV{GIT_EDITOR} || $repo->config("core.editor") || $ENV{VISUAL} || $ENV{EDITOR} || "vi";
- system($editor, $compose_filename);
+ system('sh', '-c', '$0 $@', $editor, $compose_filename);
open(C2,">",$compose_filename . ".final")
or die "Failed to open $compose_filename.final : " . $!;
exit(0);
}
- @files = ($compose_filename . ".final");
-}
-
-
-# Now that all the defaults are set, process the rest of the command line
-# arguments and collect up the files that need to be processed.
-for my $f (@ARGV) {
- if (-d $f) {
- opendir(DH,$f)
- or die "Failed to opendir $f: $!";
-
- push @files, grep { -f $_ } map { +$f . "/" . $_ }
- sort readdir(DH);
-
- } elsif (-f $f) {
- push @files, $f;
-
- } else {
- print STDERR "Skipping $f - not found.\n";
- }
-}
-
-if (@files) {
- unless ($quiet) {
- print $_,"\n" for (@files);
- }
-} else {
- print STDERR "\nNo patch files specified!\n\n";
- usage();
+ @files = ($compose_filename . ".final", @files);
}
# Variables we set as part of the loop over files
# We'll setup a template for the message id, using the "from" address:
+my ($message_id_stamp, $message_id_serial);
sub make_message_id
{
- my $date = time;
- my $pseudo_rand = int (rand(4200));
+ my $uniq;
+ if (!defined $message_id_stamp) {
+ $message_id_stamp = sprintf("%s-%s", time, $$);
+ $message_id_serial = 0;
+ }
+ $message_id_serial++;
+ $uniq = "$message_id_stamp-$message_id_serial";
+
my $du_part;
for ($sender, $repocommitter, $repoauthor) {
$du_part = extract_valid_address(sanitize_address($_));
use Sys::Hostname qw();
$du_part = 'user@' . Sys::Hostname::hostname();
}
- my $message_id_template = "<%s-git-send-email-$du_part>";
- $message_id = sprintf $message_id_template, "$date$pseudo_rand";
+ my $message_id_template = "<%s-git-send-email-%s>";
+ $message_id = sprintf($message_id_template, $uniq, $du_part);
#print "new message id = $message_id\n"; # Was useful for debugging
}
sub unquote_rfc2047 {
local ($_) = @_;
- if (s/=\?utf-8\?q\?(.*)\?=/$1/g) {
+ my $encoding;
+ if (s/=\?([^?]+)\?q\?(.*)\?=/$2/g) {
+ $encoding = $1;
s/_/ /g;
s/=([0-9A-F]{2})/chr(hex($1))/eg;
}
- return "$_";
+ return wantarray ? ($_, $encoding) : $_;
}
# use the simplest quoting being able to handle the recipient
sub send_message
{
my @recipients = unique_email_list(@to);
- @cc = (map { sanitize_address($_) } @cc);
+ @cc = (grep { my $cc = extract_valid_address($_);
+ not grep { $cc eq $_ } @recipients
+ }
+ map { sanitize_address($_) }
+ @cc);
my $to = join (",\n\t", @recipients);
@recipients = unique_email_list(@recipients,@cc,@bcclist);
@recipients = (map { extract_valid_address($_) } @recipients);
$ccline = "\nCc: $cc";
}
my $sanitized_sender = sanitize_address($sender);
- make_message_id();
+ make_message_id() unless defined($message_id);
my $header = "From: $sanitized_sender
To: $to${ccline}
print $sm "$header\n$message";
close $sm or die $?;
} else {
- require Net::SMTP;
- $smtp ||= Net::SMTP->new( $smtp_server );
+
+ if (!defined $smtp_server) {
+ die "The required SMTP server is not properly defined."
+ }
+
+ if ($smtp_ssl) {
+ $smtp_server_port ||= 465; # ssmtp
+ require Net::SMTP::SSL;
+ $smtp ||= Net::SMTP::SSL->new($smtp_server, Port => $smtp_server_port);
+ }
+ else {
+ require Net::SMTP;
+ $smtp ||= Net::SMTP->new((defined $smtp_server_port)
+ ? "$smtp_server:$smtp_server_port"
+ : $smtp_server);
+ }
+
+ if (!$smtp) {
+ die "Unable to initialize SMTP properly. Is there something wrong with your config?";
+ }
+
+ if ((defined $smtp_authuser) && (defined $smtp_authpass)) {
+ $auth ||= $smtp->auth( $smtp_authuser, $smtp_authpass ) or die $smtp->message;
+ }
$smtp->mail( $raw_from ) or die $smtp->message;
$smtp->to( @recipients ) or die $smtp->message;
$smtp->data or die $smtp->message;
if ($quiet) {
printf (($dry_run ? "Dry-" : "")."Sent %s\n", $subject);
} else {
- print (($dry_run ? "Dry-" : "")."OK. Log says:\nDate: $date\n");
+ print (($dry_run ? "Dry-" : "")."OK. Log says:\n");
if ($smtp_server !~ m#^/#) {
print "Server: $smtp_server\n";
print "MAIL FROM:<$raw_from>\n";
} else {
print "Sendmail: $smtp_server ".join(' ',@sendmail_parameters)."\n";
}
- print "From: $sanitized_sender\nSubject: $subject\nCc: $cc\nTo: $to\n\n";
+ print $header, "\n";
if ($smtp) {
print "Result: ", $smtp->code, ' ',
($smtp->message =~ /\n([^\n]+\n)$/s), "\n";
open(F,"<",$t) or die "can't open file $t";
my $author = undef;
+ my $author_encoding;
+ my $has_content_type;
+ my $body_encoding;
@cc = @initial_cc;
@xh = ();
my $input_format = undef;
next if ($suppress_from);
}
elsif ($1 eq 'From') {
- $author = unquote_rfc2047($2);
+ ($author, $author_encoding)
+ = unquote_rfc2047($2);
}
printf("(mbox) Adding cc: %s from line '%s'\n",
$2, $_) unless $quiet;
push @cc, $2;
}
+ elsif (/^Content-type:/i) {
+ $has_content_type = 1;
+ if (/charset="?[^ "]+/) {
+ $body_encoding = $1;
+ }
+ push @xh, $_;
+ }
+ elsif (/^Message-Id: (.*)/i) {
+ $message_id = $1;
+ }
elsif (!/^Date:\s/ && /^[-A-Za-z]+:\s+\S/) {
push @xh, $_;
}
if (/^(Signed-off-by|Cc): (.*)$/i && $signed_off_cc) {
my $c = $2;
chomp $c;
+ next if ($c eq $sender and $suppress_from);
push @cc, $c;
printf("(sob) Adding cc: %s from line '%s'\n",
$c, $_) unless $quiet;
}
close F;
- if ($cc_cmd ne "") {
+ if (defined $cc_cmd) {
open(F, "$cc_cmd $t |")
or die "(cc-cmd) Could not execute '$cc_cmd'";
while(<F>) {
my $c = $_;
$c =~ s/^\s*//g;
$c =~ s/\n$//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;
if (defined $author) {
$message = "From: $author\n\n$message";
+ if (defined $author_encoding) {
+ if ($has_content_type) {
+ if ($body_encoding eq $author_encoding) {
+ # ok, we already have the right encoding
+ }
+ else {
+ # uh oh, we should re-encode
+ }
+ }
+ else {
+ push @xh,
+ 'MIME-Version: 1.0',
+ "Content-Type: text/plain; charset=$author_encoding",
+ 'Content-Transfer-Encoding: 8bit';
+ }
+ }
}
send_message();
$references = "$message_id";
}
}
+ $message_id = undef;
}
if ($compose) {
}
return @emails;
}
+
+sub validate_patch {
+ my $fn = shift;
+ open(my $fh, '<', $fn)
+ or die "unable to open $fn: $!\n";
+ while (my $line = <$fh>) {
+ if (length($line) > 998) {
+ return "$.: patch contains a line longer than 998 characters";
+ }
+ }
+ return undef;
+}