b8b8fe7ee35aaf7a0bb84ab59b619dc554ae4dc1
   1#!/usr/bin/perl -w
   2#
   3# Copyright 2002,2005 Greg Kroah-Hartman <greg@kroah.com>
   4# Copyright 2005 Ryan Anderson <ryan@michonline.com>
   5#
   6# GPL v2 (See COPYING)
   7#
   8# Ported to support git "mbox" format files by Ryan Anderson <ryan@michonline.com>
   9#
  10# Sends a collection of emails to the given email addresses, disturbingly fast.
  11#
  12# Supports two formats:
  13# 1. mbox format files (ignoring most headers and MIME formatting - this is designed for sending patches)
  14# 2. The original format support by Greg's script:
  15#    first line of the message is who to CC,
  16#    and second line is the subject of the message.
  17#
  18
  19use strict;
  20use warnings;
  21use Term::ReadLine;
  22use Getopt::Long;
  23use Data::Dumper;
  24use Git;
  25
  26package FakeTerm;
  27sub new {
  28        my ($class, $reason) = @_;
  29        return bless \$reason, shift;
  30}
  31sub readline {
  32        my $self = shift;
  33        die "Cannot use readline on FakeTerm: $$self";
  34}
  35package main;
  36
  37
  38sub usage {
  39        print <<EOT;
  40git-send-email [options] <file | directory>...
  41Options:
  42   --from         Specify the "From:" line of the email to be sent.
  43
  44   --to           Specify the primary "To:" line of the email.
  45
  46   --cc           Specify an initial "Cc:" list for the entire series
  47                  of emails.
  48
  49   --bcc          Specify a list of email addresses that should be Bcc:
  50                  on all the emails.
  51
  52   --compose      Use \$EDITOR to edit an introductory message for the
  53                  patch series.
  54
  55   --subject      Specify the initial "Subject:" line.
  56                  Only necessary if --compose is also set.  If --compose
  57                  is not set, this will be prompted for.
  58
  59   --in-reply-to  Specify the first "In-Reply-To:" header line.
  60                  Only used if --compose is also set.  If --compose is not
  61                  set, this will be prompted for.
  62
  63   --chain-reply-to If set, the replies will all be to the previous
  64                  email sent, rather than to the first email sent.
  65                  Defaults to on.
  66
  67   --no-signed-off-cc Suppress the automatic addition of email addresses
  68                 that appear in Signed-off-by: or Cc: lines to the cc:
  69                 list.  Note: Using this option is not recommended.
  70
  71   --smtp-server  If set, specifies the outgoing SMTP server to use.
  72                  Defaults to localhost.
  73
  74   --suppress-from Suppress sending emails to yourself if your address
  75                  appears in a From: line.
  76
  77    --threaded    Specify that the "In-Reply-To:" header should be set on all
  78                  emails. Defaults to on.
  79
  80   --quiet        Make git-send-email less verbose.  One line per email
  81                  should be all that is output.
  82
  83   --dry-run      Do everything except actually send the emails.
  84
  85   --envelope-sender    Specify the envelope sender used to send the emails.
  86
  87EOT
  88        exit(1);
  89}
  90
  91# most mail servers generate the Date: header, but not all...
  92sub format_2822_time {
  93        my ($time) = @_;
  94        my @localtm = localtime($time);
  95        my @gmttm = gmtime($time);
  96        my $localmin = $localtm[1] + $localtm[2] * 60;
  97        my $gmtmin = $gmttm[1] + $gmttm[2] * 60;
  98        if ($localtm[0] != $gmttm[0]) {
  99                die "local zone differs from GMT by a non-minute interval\n";
 100        }
 101        if ((($gmttm[6] + 1) % 7) == $localtm[6]) {
 102                $localmin += 1440;
 103        } elsif ((($gmttm[6] - 1) % 7) == $localtm[6]) {
 104                $localmin -= 1440;
 105        } elsif ($gmttm[6] != $localtm[6]) {
 106                die "local time offset greater than or equal to 24 hours\n";
 107        }
 108        my $offset = $localmin - $gmtmin;
 109        my $offhour = $offset / 60;
 110        my $offmin = abs($offset % 60);
 111        if (abs($offhour) >= 24) {
 112                die ("local time offset greater than or equal to 24 hours\n");
 113        }
 114
 115        return sprintf("%s, %2d %s %d %02d:%02d:%02d %s%02d%02d",
 116                       qw(Sun Mon Tue Wed Thu Fri Sat)[$localtm[6]],
 117                       $localtm[3],
 118                       qw(Jan Feb Mar Apr May Jun
 119                          Jul Aug Sep Oct Nov Dec)[$localtm[4]],
 120                       $localtm[5]+1900,
 121                       $localtm[2],
 122                       $localtm[1],
 123                       $localtm[0],
 124                       ($offset >= 0) ? '+' : '-',
 125                       abs($offhour),
 126                       $offmin,
 127                       );
 128}
 129
 130my $have_email_valid = eval { require Email::Valid; 1 };
 131my $smtp;
 132
 133sub unique_email_list(@);
 134sub cleanup_compose_files();
 135
 136# Constants (essentially)
 137my $compose_filename = ".msg.$$";
 138
 139# Variables we fill in automatically, or via prompting:
 140my (@to,@cc,@initial_cc,@bcclist,@xh,
 141        $initial_reply_to,$initial_subject,@files,$from,$compose,$time);
 142
 143# Behavior modification variables
 144my ($threaded, $chain_reply_to, $quiet, $suppress_from, $no_signed_off_cc,
 145        $dry_run) = (1, 1, 0, 0, 0, 0);
 146my $smtp_server;
 147my $envelope_sender;
 148
 149# Example reply to:
 150#$initial_reply_to = ''; #<20050203173208.GA23964@foobar.com>';
 151
 152my $repo = Git->repository();
 153my $term = eval {
 154        new Term::ReadLine 'git-send-email';
 155};
 156if ($@) {
 157        $term = new FakeTerm "$@: going non-interactive";
 158}
 159
 160my %config_settings = (
 161    "threaded" => \$threaded,
 162    "chainreplyto" => \$chain_reply_to,
 163);
 164
 165foreach my $setting (keys %config_settings) {
 166    my $default = $repo->config_bool("sendemail.$setting");
 167    if (defined $default) {
 168        $config_settings{$setting} = $default ? 1 : 0;
 169    }
 170}
 171
 172@bcclist = $repo->config('sendemail.bcc');
 173if (!@bcclist or !$bcclist[0]) {
 174    @bcclist = ();
 175}
 176
 177# Begin by accumulating all the variables (defined above), that we will end up
 178# needing, first, from the command line:
 179
 180my $rc = GetOptions("from=s" => \$from,
 181                    "in-reply-to=s" => \$initial_reply_to,
 182                    "subject=s" => \$initial_subject,
 183                    "to=s" => \@to,
 184                    "cc=s" => \@initial_cc,
 185                    "bcc=s" => \@bcclist,
 186                    "chain-reply-to!" => \$chain_reply_to,
 187                    "smtp-server=s" => \$smtp_server,
 188                    "compose" => \$compose,
 189                    "quiet" => \$quiet,
 190                    "suppress-from" => \$suppress_from,
 191                    "no-signed-off-cc|no-signed-off-by-cc" => \$no_signed_off_cc,
 192                    "dry-run" => \$dry_run,
 193                    "envelope-sender=s" => \$envelope_sender,
 194                    "threaded!" => \$threaded,
 195         );
 196
 197unless ($rc) {
 198    usage();
 199}
 200
 201# Verify the user input
 202
 203foreach my $entry (@to) {
 204        die "Comma in --to entry: $entry'\n" unless $entry !~ m/,/;
 205}
 206
 207foreach my $entry (@initial_cc) {
 208        die "Comma in --cc entry: $entry'\n" unless $entry !~ m/,/;
 209}
 210
 211foreach my $entry (@bcclist) {
 212        die "Comma in --bcclist entry: $entry'\n" unless $entry !~ m/,/;
 213}
 214
 215# Now, let's fill any that aren't set in with defaults:
 216
 217my ($author) = $repo->ident_person('author');
 218my ($committer) = $repo->ident_person('committer');
 219
 220my %aliases;
 221my @alias_files = $repo->config('sendemail.aliasesfile');
 222my $aliasfiletype = $repo->config('sendemail.aliasfiletype');
 223my %parse_alias = (
 224        # multiline formats can be supported in the future
 225        mutt => sub { my $fh = shift; while (<$fh>) {
 226                if (/^\s*alias\s+(\S+)\s+(.*)$/) {
 227                        my ($alias, $addr) = ($1, $2);
 228                        $addr =~ s/#.*$//; # mutt allows # comments
 229                         # commas delimit multiple addresses
 230                        $aliases{$alias} = [ split(/\s*,\s*/, $addr) ];
 231                }}},
 232        mailrc => sub { my $fh = shift; while (<$fh>) {
 233                if (/^alias\s+(\S+)\s+(.*)$/) {
 234                        # spaces delimit multiple addresses
 235                        $aliases{$1} = [ split(/\s+/, $2) ];
 236                }}},
 237        pine => sub { my $fh = shift; while (<$fh>) {
 238                if (/^(\S+)\s+(.*)$/) {
 239                        $aliases{$1} = [ split(/\s*,\s*/, $2) ];
 240                }}},
 241        gnus => sub { my $fh = shift; while (<$fh>) {
 242                if (/\(define-mail-alias\s+"(\S+?)"\s+"(\S+?)"\)/) {
 243                        $aliases{$1} = [ $2 ];
 244                }}}
 245);
 246
 247if (@alias_files and $aliasfiletype and defined $parse_alias{$aliasfiletype}) {
 248        foreach my $file (@alias_files) {
 249                open my $fh, '<', $file or die "opening $file: $!\n";
 250                $parse_alias{$aliasfiletype}->($fh);
 251                close $fh;
 252        }
 253}
 254
 255my $prompting = 0;
 256if (!defined $from) {
 257        $from = $author || $committer;
 258        do {
 259                $_ = $term->readline("Who should the emails appear to be from? [$from] ");
 260        } while (!defined $_);
 261
 262        $from = $_ if ($_);
 263        print "Emails will be sent from: ", $from, "\n";
 264        $prompting++;
 265}
 266
 267if (!@to) {
 268        do {
 269                $_ = $term->readline("Who should the emails be sent to? ",
 270                                "");
 271        } while (!defined $_);
 272        my $to = $_;
 273        push @to, split /,/, $to;
 274        $prompting++;
 275}
 276
 277sub expand_aliases {
 278        my @cur = @_;
 279        my @last;
 280        do {
 281                @last = @cur;
 282                @cur = map { $aliases{$_} ? @{$aliases{$_}} : $_ } @last;
 283        } while (join(',',@cur) ne join(',',@last));
 284        return @cur;
 285}
 286
 287@to = expand_aliases(@to);
 288@to = (map { sanitize_address_rfc822($_) } @to);
 289@initial_cc = expand_aliases(@initial_cc);
 290@bcclist = expand_aliases(@bcclist);
 291
 292if (!defined $initial_subject && $compose) {
 293        do {
 294                $_ = $term->readline("What subject should the emails start with? ",
 295                        $initial_subject);
 296        } while (!defined $_);
 297        $initial_subject = $_;
 298        $prompting++;
 299}
 300
 301if ($threaded && !defined $initial_reply_to && $prompting) {
 302        do {
 303                $_= $term->readline("Message-ID to be used as In-Reply-To for the first email? ",
 304                        $initial_reply_to);
 305        } while (!defined $_);
 306
 307        $initial_reply_to = $_;
 308        $initial_reply_to =~ s/(^\s+|\s+$)//g;
 309}
 310
 311if (!$smtp_server) {
 312        $smtp_server = $repo->config('sendemail.smtpserver');
 313}
 314if (!$smtp_server) {
 315        foreach (qw( /usr/sbin/sendmail /usr/lib/sendmail )) {
 316                if (-x $_) {
 317                        $smtp_server = $_;
 318                        last;
 319                }
 320        }
 321        $smtp_server ||= 'localhost'; # could be 127.0.0.1, too... *shrug*
 322}
 323
 324if ($compose) {
 325        # Note that this does not need to be secure, but we will make a small
 326        # effort to have it be unique
 327        open(C,">",$compose_filename)
 328                or die "Failed to open for writing $compose_filename: $!";
 329        print C "From $from # This line is ignored.\n";
 330        printf C "Subject: %s\n\n", $initial_subject;
 331        printf C <<EOT;
 332GIT: Please enter your email below.
 333GIT: Lines beginning in "GIT: " will be removed.
 334GIT: Consider including an overall diffstat or table of contents
 335GIT: for the patch you are writing.
 336
 337EOT
 338        close(C);
 339
 340        my $editor = $ENV{EDITOR};
 341        $editor = 'vi' unless defined $editor;
 342        system($editor, $compose_filename);
 343
 344        open(C2,">",$compose_filename . ".final")
 345                or die "Failed to open $compose_filename.final : " . $!;
 346
 347        open(C,"<",$compose_filename)
 348                or die "Failed to open $compose_filename : " . $!;
 349
 350        while(<C>) {
 351                next if m/^GIT: /;
 352                print C2 $_;
 353        }
 354        close(C);
 355        close(C2);
 356
 357        do {
 358                $_ = $term->readline("Send this email? (y|n) ");
 359        } while (!defined $_);
 360
 361        if (uc substr($_,0,1) ne 'Y') {
 362                cleanup_compose_files();
 363                exit(0);
 364        }
 365
 366        @files = ($compose_filename . ".final");
 367}
 368
 369
 370# Now that all the defaults are set, process the rest of the command line
 371# arguments and collect up the files that need to be processed.
 372for my $f (@ARGV) {
 373        if (-d $f) {
 374                opendir(DH,$f)
 375                        or die "Failed to opendir $f: $!";
 376
 377                push @files, grep { -f $_ } map { +$f . "/" . $_ }
 378                                sort readdir(DH);
 379
 380        } elsif (-f $f) {
 381                push @files, $f;
 382
 383        } else {
 384                print STDERR "Skipping $f - not found.\n";
 385        }
 386}
 387
 388if (@files) {
 389        unless ($quiet) {
 390                print $_,"\n" for (@files);
 391        }
 392} else {
 393        print STDERR "\nNo patch files specified!\n\n";
 394        usage();
 395}
 396
 397# Variables we set as part of the loop over files
 398our ($message_id, %mail, $subject, $reply_to, $references, $message);
 399
 400sub extract_valid_address {
 401        my $address = shift;
 402        my $local_part_regexp = '[^<>"\s@]+';
 403        my $domain_regexp = '[^.<>"\s@]+(?:\.[^.<>"\s@]+)+';
 404
 405        # check for a local address:
 406        return $address if ($address =~ /^($local_part_regexp)$/);
 407
 408        if ($have_email_valid) {
 409                return scalar Email::Valid->address($address);
 410        } else {
 411                # less robust/correct than the monster regexp in Email::Valid,
 412                # but still does a 99% job, and one less dependency
 413                $address =~ /($local_part_regexp\@$domain_regexp)/;
 414                return $1;
 415        }
 416}
 417
 418# Usually don't need to change anything below here.
 419
 420# we make a "fake" message id by taking the current number
 421# of seconds since the beginning of Unix time and tacking on
 422# a random number to the end, in case we are called quicker than
 423# 1 second since the last time we were called.
 424
 425# We'll setup a template for the message id, using the "from" address:
 426
 427sub make_message_id
 428{
 429        my $date = time;
 430        my $pseudo_rand = int (rand(4200));
 431        my $du_part;
 432        for ($from, $committer, $author) {
 433                $du_part = extract_valid_address($_);
 434                last if ($du_part ne '');
 435        }
 436        if ($du_part eq '') {
 437                use Sys::Hostname qw();
 438                $du_part = 'user@' . Sys::Hostname::hostname();
 439        }
 440        my $message_id_template = "<%s-git-send-email-$du_part>";
 441        $message_id = sprintf $message_id_template, "$date$pseudo_rand";
 442        #print "new message id = $message_id\n"; # Was useful for debugging
 443}
 444
 445
 446
 447$time = time - scalar $#files;
 448
 449sub unquote_rfc2047 {
 450        local ($_) = @_;
 451        if (s/=\?utf-8\?q\?(.*)\?=/$1/g) {
 452                s/_/ /g;
 453                s/=([0-9A-F]{2})/chr(hex($1))/eg;
 454        }
 455        return "$_";
 456}
 457
 458# If an address contains a . in the name portion, the name must be quoted.
 459sub sanitize_address_rfc822
 460{
 461        my ($recipient) = @_;
 462        my ($recipient_name) = ($recipient =~ /^(.*?)\s+</);
 463        if ($recipient_name && $recipient_name =~ /\./ && $recipient_name !~ /^".*"$/) {
 464                my ($name, $addr) = ($recipient =~ /^(.*?)(\s+<.*)/);
 465                $recipient = "\"$name\"$addr";
 466        }
 467        return $recipient;
 468}
 469
 470sub send_message
 471{
 472        my @recipients = unique_email_list(@to);
 473        @cc = (map { sanitize_address_rfc822($_) } @cc);
 474        my $to = join (",\n\t", @recipients);
 475        @recipients = unique_email_list(@recipients,@cc,@bcclist);
 476        @recipients = (map { extract_valid_address($_) } @recipients);
 477        my $date = format_2822_time($time++);
 478        my $gitversion = '@@GIT_VERSION@@';
 479        if ($gitversion =~ m/..GIT_VERSION../) {
 480            $gitversion = Git::version();
 481        }
 482
 483        my $cc = join(", ", unique_email_list(@cc));
 484        my $ccline = "";
 485        if ($cc ne '') {
 486                $ccline = "\nCc: $cc";
 487        }
 488        $from = sanitize_address_rfc822($from);
 489        make_message_id();
 490
 491        my $header = "From: $from
 492To: $to${ccline}
 493Subject: $subject
 494Date: $date
 495Message-Id: $message_id
 496X-Mailer: git-send-email $gitversion
 497";
 498        if ($threaded && $reply_to) {
 499
 500                $header .= "In-Reply-To: $reply_to\n";
 501                $header .= "References: $references\n";
 502        }
 503        if (@xh) {
 504                $header .= join("\n", @xh) . "\n";
 505        }
 506
 507        my @sendmail_parameters = ('-i', @recipients);
 508        my $raw_from = $from;
 509        $raw_from = $envelope_sender if (defined $envelope_sender);
 510        $raw_from = extract_valid_address($raw_from);
 511        unshift (@sendmail_parameters,
 512                        '-f', $raw_from) if(defined $envelope_sender);
 513
 514        if ($dry_run) {
 515                # We don't want to send the email.
 516        } elsif ($smtp_server =~ m#^/#) {
 517                my $pid = open my $sm, '|-';
 518                defined $pid or die $!;
 519                if (!$pid) {
 520                        exec($smtp_server, @sendmail_parameters) or die $!;
 521                }
 522                print $sm "$header\n$message";
 523                close $sm or die $?;
 524        } else {
 525                require Net::SMTP;
 526                $smtp ||= Net::SMTP->new( $smtp_server );
 527                $smtp->mail( $raw_from ) or die $smtp->message;
 528                $smtp->to( @recipients ) or die $smtp->message;
 529                $smtp->data or die $smtp->message;
 530                $smtp->datasend("$header\n$message") or die $smtp->message;
 531                $smtp->dataend() or die $smtp->message;
 532                $smtp->ok or die "Failed to send $subject\n".$smtp->message;
 533        }
 534        if ($quiet) {
 535                printf (($dry_run ? "Dry-" : "")."Sent %s\n", $subject);
 536        } else {
 537                print (($dry_run ? "Dry-" : "")."OK. Log says:\nDate: $date\n");
 538                if ($smtp_server !~ m#^/#) {
 539                        print "Server: $smtp_server\n";
 540                        print "MAIL FROM:<$raw_from>\n";
 541                        print "RCPT TO:".join(',',(map { "<$_>" } @recipients))."\n";
 542                } else {
 543                        print "Sendmail: $smtp_server ".join(' ',@sendmail_parameters)."\n";
 544                }
 545                print "From: $from\nSubject: $subject\nCc: $cc\nTo: $to\n\n";
 546                if ($smtp) {
 547                        print "Result: ", $smtp->code, ' ',
 548                                ($smtp->message =~ /\n([^\n]+\n)$/s), "\n";
 549                } else {
 550                        print "Result: OK\n";
 551                }
 552        }
 553}
 554
 555$reply_to = $initial_reply_to;
 556$references = $initial_reply_to || '';
 557$subject = $initial_subject;
 558
 559foreach my $t (@files) {
 560        open(F,"<",$t) or die "can't open file $t";
 561
 562        my $author_not_sender = undef;
 563        @cc = @initial_cc;
 564        @xh = ();
 565        my $input_format = undef;
 566        my $header_done = 0;
 567        $message = "";
 568        while(<F>) {
 569                if (!$header_done) {
 570                        if (/^From /) {
 571                                $input_format = 'mbox';
 572                                next;
 573                        }
 574                        chomp;
 575                        if (!defined $input_format && /^[-A-Za-z]+:\s/) {
 576                                $input_format = 'mbox';
 577                        }
 578
 579                        if (defined $input_format && $input_format eq 'mbox') {
 580                                if (/^Subject:\s+(.*)$/) {
 581                                        $subject = $1;
 582
 583                                } elsif (/^(Cc|From):\s+(.*)$/) {
 584                                        if (unquote_rfc2047($2) eq $from) {
 585                                                $from = $2;
 586                                                next if ($suppress_from);
 587                                        }
 588                                        elsif ($1 eq 'From') {
 589                                                $author_not_sender = $2;
 590                                        }
 591                                        printf("(mbox) Adding cc: %s from line '%s'\n",
 592                                                $2, $_) unless $quiet;
 593                                        push @cc, $2;
 594                                }
 595                                elsif (!/^Date:\s/ && /^[-A-Za-z]+:\s+\S/) {
 596                                        push @xh, $_;
 597                                }
 598
 599                        } else {
 600                                # In the traditional
 601                                # "send lots of email" format,
 602                                # line 1 = cc
 603                                # line 2 = subject
 604                                # So let's support that, too.
 605                                $input_format = 'lots';
 606                                if (@cc == 0) {
 607                                        printf("(non-mbox) Adding cc: %s from line '%s'\n",
 608                                                $_, $_) unless $quiet;
 609
 610                                        push @cc, $_;
 611
 612                                } elsif (!defined $subject) {
 613                                        $subject = $_;
 614                                }
 615                        }
 616
 617                        # A whitespace line will terminate the headers
 618                        if (m/^\s*$/) {
 619                                $header_done = 1;
 620                        }
 621                } else {
 622                        $message .=  $_;
 623                        if (/^(Signed-off-by|Cc): (.*)$/i && !$no_signed_off_cc) {
 624                                my $c = $2;
 625                                chomp $c;
 626                                push @cc, $c;
 627                                printf("(sob) Adding cc: %s from line '%s'\n",
 628                                        $c, $_) unless $quiet;
 629                        }
 630                }
 631        }
 632        close F;
 633        if (defined $author_not_sender) {
 634                $author_not_sender = unquote_rfc2047($author_not_sender);
 635                $message = "From: $author_not_sender\n\n$message";
 636        }
 637
 638
 639        send_message();
 640
 641        # set up for the next message
 642        if ($chain_reply_to || !defined $reply_to || length($reply_to) == 0) {
 643                $reply_to = $message_id;
 644                if (length $references > 0) {
 645                        $references .= "\n $message_id";
 646                } else {
 647                        $references = "$message_id";
 648                }
 649        }
 650}
 651
 652if ($compose) {
 653        cleanup_compose_files();
 654}
 655
 656sub cleanup_compose_files() {
 657        unlink($compose_filename, $compose_filename . ".final");
 658
 659}
 660
 661$smtp->quit if $smtp;
 662
 663sub unique_email_list(@) {
 664        my %seen;
 665        my @emails;
 666
 667        foreach my $entry (@_) {
 668                if (my $clean = extract_valid_address($entry)) {
 669                        $seen{$clean} ||= 0;
 670                        next if $seen{$clean}++;
 671                        push @emails, $entry;
 672                } else {
 673                        print STDERR "W: unable to extract a valid address",
 674                                        " from: $entry\n";
 675                }
 676        }
 677        return @emails;
 678}