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