1 Subject: [PATCH] git-send-email: use libcurl for implementation
3 Use libcurl's API to send SMTP messages rather than
4 various Perl modules that may be unavailable.
6 Using libcurl to send SMTP messages from Perl is
7 challenging. No released version of WWW::Curl
8 properly supports the CURLOPT_MAIL_RCPT option.
10 Furthermore, although it's trivial to patch WWW::Curl
11 to properly handle CURLOPT_MAIL_RCPT, the resulting
12 module builds a shared object that is tied to a
13 particular version of Perl such that a module built
14 for Perl 5.8 will NOT load into Perl 5.10. This is
17 Instead, since we only use a small subset of the
18 extensive libcurl API to send SMTP messages, we build
19 our own helper as git-send-email--libcurl that allows
20 Perl to talk to libcurl. This helper is not sensitive
21 to the version of Perl and works with any version of
22 Perl provided the libcurl shared library is available.
24 However, the interface to the helper is not very
25 friendly as everything has to be marshalled and then
26 unmarshalled for each call (even though the protocol
27 is very simple). So we use a git-send-email--libcurl.pl
28 helper that exports a WWW::Curl::Libcurl interface that
29 behaves very similarly to WWW::Curl::Easy which
30 makes use of the libcurl helper very simple.
32 This version of git-send-email supports everything
33 the old version did and probably some additional
34 authentication mechanisms as well.
36 Furthermore, in an effort to avoid exposing information
37 about the sending system that could render it more
38 vulnerable to hackers, this version of git-send-email
39 omits the X-Mailer header and hashes the Message-Id
40 value to render it opaque.
42 Signed-off-by: Kyle J. McKay <mackyle@gmail.com>
45 git-send-email--libcurl.pl | 404 ++++++++++++++++++++++++++++
46 git-send-email.perl | 162 ++++++------
47 send-email--libcurl.c | 637 +++++++++++++++++++++++++++++++++++++++++++++
48 t/t9001-send-email.sh | 20 +-
49 4 files changed, 1129 insertions(+), 94 deletions(-)
50 create mode 100644 git-send-email--libcurl.pl
51 create mode 100644 send-email--libcurl.c
53 diff --git a/git-send-email--libcurl.pl b/git-send-email--libcurl.pl
55 index 00000000..aa48792f
57 +++ b/git-send-email--libcurl.pl
61 +send-email--libcurl.pl -- libcURL helper for git-send-email.perl
62 +Copyright (C) 2014,2015 Kyle J. McKay. All rights reserved.
64 +This program is free software; you can redistribute it and/or
65 +modify it under the terms of the GNU General Public License
66 +as published by the Free Software Foundation; either version 2
67 +of the License, or (at your option) any later version.
69 +This program is distributed in the hope that it will be useful,
70 +but WITHOUT ANY WARRANTY; without even the implied warranty of
71 +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
72 +GNU General Public License for more details.
74 +You should have received a copy of the GNU General Public License
75 +along with this program; if not, write to the Free Software
76 +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
80 +package WWW::Curl::Libcurl;
85 +use Exporter 'import';
86 +use vars qw($VERSION @EXPORT);
87 +BEGIN {*VERSION = \1.0}
90 + CURLE_UNSUPPORTED_PROTOCOL
92 + CURLE_COULDNT_RESOLVE_HOST
93 + CURLE_COULDNT_CONNECT
94 + CURLE_HTTP_RETURNED_ERROR
97 + CURLE_OPERATION_TIMEDOUT
98 + CURLE_UNKNOWN_OPTION
102 + CURLE_SSL_PINNEDPUBKEYNOTMATCH
106 +sub CURLE_UNSUPPORTED_PROTOCOL() { 1}
107 +sub CURLE_URL_MALFORMAT() { 3}
108 +sub CURLE_COULDNT_RESOLVE_HOST() { 6}
109 +sub CURLE_COULDNT_CONNECT() { 7}
110 +sub CURLE_HTTP_RETURNED_ERROR() {22}
111 +sub CURLE_UPLOAD_FAILED() {25}
112 +sub CURLE_OUT_OF_MEMORY() {27}
113 +sub CURLE_OPERATION_TIMEDOUT() {28}
114 +sub CURLE_UNKNOWN_OPTION() {48}
115 +sub CURLE_GOT_NOTHING() {52}
116 +sub CURLE_LOGIN_DENIED() {67}
117 +sub CURLE_AGAIN() {81}
118 +sub CURLE_SSL_PINNEDPUBKEYNOTMATCH() {90}
120 +our $helper = realpath(dirname(realpath(__FILE__)).'/git-send-email--libcurl');
121 +die "Missing $helper\n" unless -x $helper;
124 + my ($pid, $chldout, $chldin);
125 + $pid = open2($chldout, $chldin, $helper, "--spawn");
126 + die "Cannot spawn $helper\n" unless $pid;
127 + select((select($childin),$|=1)[0]);
129 + $self->{'pid'} = $pid;
130 + $self->{'chldin'} = $chldin;
131 + $self->{'chldout'} = $chldout;
132 + return bless $self;
137 + close($self->{'chldin'});
138 + waitpid($self->{'pid'}, 0);
139 + close($self->{'chldout'});
144 + my $remain = $_[2];
147 + $result = read($_[0], $_[1], $remain, $offset);
148 + $offset += $result, $remain -= $result, redo
149 + if $result && $result < $remain;
151 + return $result ? $offset + $result : $result;
159 + die "Invalid tag: '$tag'\n" unless length($tag) == 4;
160 + printf {$self->{'chldin'}} "%s", $tag.pack('N',$val);
162 + my $result = _readall($self->{'chldout'}, $data, 4);
163 + die "Read result '$tag' from helper failed\n" unless $result && $result == 4;
164 + return unpack('N', $data);
172 + my $strlen = $self->_get_val($tag, $val);
173 + return "" unless $strlen;
175 + my $result = _readall($self->{'chldout'}, $data, $strlen);
176 + die "Read result '$tag' from helper failed\n" unless $result && $result == $strlen;
185 + die "Invalid tag: '$tag'\n" unless length($tag) == 4;
186 + printf {$self->{'chldin'}} "%s", $tag.pack('N',length($str)).$str;
188 + my $result = _readall($self->{'chldout'}, $data, 4);
189 + die "Read result '$tag' from helper failed\n" unless $result && $result == 4;
190 + return unpack('N', $data);
195 + my $ans = $self->_get_val('vers', 0);
196 + return (($ans >> 16) & 0xff) . '.' . (($ans >> 8) & 0xff) . '.' . ($ans & 0xff);
202 + return $self->_get_string('stre', $code);
207 + return $self->_get_val('prfm', 0);
212 + return $self->_get_val('rset', 0);
217 + return $self->_get_string('errb', 0);
222 + return $self->_get_string('lsth', 0);
225 +# Note that any gzip/deflate content encoding will be automatically decoded
228 + return $self->_get_string('wdta', 0);
231 +# These are the headers returned from the server returned as an array of
232 +# header lines (any \s+$ removed) as sent to cURL's CURLOPT_HEADERFUNCTION
233 +# note the first "header" may actually be an HTTP respose such as:
237 + my $hdrblob = $self->_get_string('hdta', 0);
239 + foreach my $hdr (split(/\x00+/, $hdrblob)) {
240 + $hdr =~ s/\s+$//os;
241 + push(@ans, $hdr) if $hdr ne '';
246 +sub getinfo_response_code {
248 + return $self->_get_val('rspc', 0);
251 +sub getinfo_redirect_url {
253 + return $self->_get_string('rurl', 0);
256 +sub getinfo_content_type {
258 + return $self->_get_string('ctyp', 0);
261 +sub setopt_ssl_verifyhost {
264 + return $self->_get_val('vhst', $bool ? 1 : 0);
267 +sub setopt_ssl_verifypeer {
270 + return $self->_get_val('vper', $bool ? 1 : 0);
273 +sub setopt_verbose {
276 + return $self->_get_val('verb', $bool ? 1 : 0);
279 +sub setopt_noprogress {
282 + return $self->_get_val('nopg', $bool ? 1 : 0);
288 + return $self->_set_val('port', $val);
291 +sub setopt_timeout {
294 + return $self->_set_val('tmot', $val);
297 +sub setopt_httpget {
300 + return $self->_get_val('hget', $bool ? 1 : 0);
306 + return $self->_get_val('nbdy', $bool ? 1 : 0);
312 + return $self->_get_val('post', $bool ? 1 : 0);
318 + return $self->_get_val('upld', $bool ? 1 : 0);
321 +# For non smtps connections whether to use STARTTLS
322 +# 0 = never, 1 = required, -1 = try if offered
323 +sub setopt_use_ssl {
326 + return $self->_get_val('ussl', $val);
329 +# Whether to use a ~/.netrc file or not
330 +# 0 = never, 1 = required, -1 = optional fallback
334 + return $self->_get_val('ntrc', $val);
337 +sub setopt_useragent {
340 + return $self->_set_string('agnt', $val);
346 + return $self->_set_string('prxy', $val);
352 + return $self->_set_string('cain', $val);
358 + return $self->_set_string('capa', $val);
361 +sub setopt_pinnedpublickey {
364 + return $self->_set_string('pink', $val);
367 +sub setopt_sslcert {
370 + return $self->_set_string('cert', $val);
376 + return $self->_set_string('key_', $val);
379 +sub setopt_mail_auth {
382 + return $self->_set_string('auth', $val);
385 +sub setopt_mail_from {
388 + return $self->_set_string('from', $val);
391 +sub setopt_username {
394 + return $self->_set_string('user', $val);
397 +sub setopt_password {
400 + return $self->_set_string('pswd', $val);
403 +# Arg must be one or more AUTH=<value> separated by ';'
404 +# The default is "AUTH=*" (for any available) which is the same as
405 +# "AUTH=LOGIN;AUTH=PLAIN;AUTH=CRAM-MD5;AUTH=DIGEST-MD5;AUTH=GSSAPI" .
406 +# ";AUTH=EXTERNAL;AUTH=NTLM;AUTH=XOAUTH2"
407 +# A badly formated options string will cause CURLE_URL_MALFORMAT (3).
408 +sub setopt_login_options {
411 + return $self->_set_string('mech', $val);
414 +sub setopt_readdata {
417 + return $self->_set_string('data', $val);
420 +# May be set to the empty string to activate cookies without external files
421 +# If set to a non-empty string then both COOKIEFILE and COOKIEJAR will be set
422 +sub setopt_cookiefile {
425 + return $self->_set_string('cook', $val);
428 +# ignores anything other than 'ALL', 'SESS', 'FLUSH', or 'RELOAD' in
429 +# a case-insensitive way
430 +sub setopt_cookielist {
433 + if (defined($cmd)) {
435 + if ($cmd eq "ALL" || $cmd eq "SESS" || $cmd eq "FLUSH" || $cmd eq "RELOAD") {
436 + return $self->_set_string('ckls', $cmd);
445 + return $self->_set_string('url_', $val);
448 +sub setopt_mail_rcpt {
451 + my $strings = join('', map($_.pack('C',0), @_));
452 + return $self->_set_string('rcpt', $strings);
455 +sub setopt_httpheader {
458 + my $strings = join('', map($_.pack('C',0), @_));
459 + return $self->_set_string('hdrs', $strings);
463 diff --git a/git-send-email.perl b/git-send-email.perl
464 index e1e9b146..e6b4829b 100755
465 --- a/git-send-email.perl
466 +++ b/git-send-email.perl
467 @@ -26,8 +26,15 @@ use Data::Dumper;
469 use File::Temp qw/ tempdir tempfile /;
470 use File::Spec::Functions qw(catfile);
471 +use Digest::MD5 qw(md5_hex);
477 +require(realpath(dirname(realpath(__FILE__))).'/git-send-email--libcurl.pl');
478 +WWW::Curl::Libcurl->import;
481 Getopt::Long::Configure qw/ pass_through /;
483 @@ -54,7 +61,7 @@ git send-email [options] <file | directory | rev-list options >
484 --[no-]bcc <str> * Email Bcc:
485 --subject <str> * Email "Subject:"
486 --in-reply-to <str> * Email "In-Reply-To:"
487 - --[no-]xmailer * Add "X-Mailer:" header (default).
488 + --[no-]xmailer * Add "X-Mailer:" header (default is off).
489 --[no-]annotate * Review each patch that will be sent in an editor.
490 --compose * Open an editor for introduction.
491 --compose-encoding <str> * Encoding to assume for introduction.
492 @@ -228,7 +235,7 @@ my %config_bool_settings = (
493 "validate" => [\$validate, 1],
494 "multiedit" => [\$multiedit, undef],
495 "annotate" => [\$annotate, undef],
496 - "xmailer" => [\$use_xmailer, 1]
497 + "xmailer" => [\$use_xmailer, 0]
500 my %config_settings = (
501 @@ -304,6 +311,7 @@ my $rc = GetOptions("h" => \$help,
502 "smtp-server-option=s" => \@smtp_server_options,
503 "smtp-server-port=s" => \$smtp_server_port,
504 "smtp-user=s" => \$smtp_authuser,
505 + "no-smtp-user" => sub {delete $config_settings{'smtpuser'}},
506 "smtp-pass:s" => \$smtp_authpass,
507 "smtp-ssl" => sub { $smtp_encryption = 'ssl' },
508 "smtp-encryption=s" => \$smtp_encryption,
509 @@ -925,8 +933,10 @@ sub make_message_id {
510 require Sys::Hostname;
511 $du_part = 'user@' . Sys::Hostname::hostname();
513 - my $message_id_template = "<%s-git-send-email-%s>";
514 + my $message_id_template = "%s-git-send-email-%s";
515 $message_id = sprintf($message_id_template, $uniq, $du_part);
516 + @_ = split /@/, $message_id;
517 + $message_id = '<'.substr(md5_hex($_[0]),0,31).'@'.substr(md5_hex($_[1]),1,31).'>';
518 #print "new message id = $message_id\n"; # Was useful for debugging
521 @@ -1090,12 +1100,12 @@ sub smtp_host_string {
525 -# Returns 1 if authentication succeeded or was not necessary
526 -# (smtp_user was not specified), and 0 otherwise.
527 +# Returns a filled hashref from Git::credential fill if authentication
528 +# has been requested, undef otherwise.
530 sub smtp_auth_maybe {
531 if (!defined $smtp_authuser || $auth) {
536 # Workaround AUTH PLAIN/LOGIN interaction defect
537 @@ -1108,47 +1118,36 @@ sub smtp_auth_maybe {
538 # TODO: Authentication may fail not because credentials were
539 # invalid but due to other reasons, in which we should not
540 # reject credentials.
541 - $auth = Git::credential({
543 'protocol' => 'smtp',
544 'host' => smtp_host_string(),
545 'username' => $smtp_authuser,
546 # if there's no password, "git credential fill" will
547 # give us one, otherwise it'll just pass this one.
548 'password' => $smtp_authpass
551 - return !!$smtp->auth($cred->{'username'}, $cred->{'password'});
555 + Git::credential($auth, 'fill');
559 sub ssl_verify_params {
561 - require IO::Socket::SSL;
562 - IO::Socket::SSL->import(qw/SSL_VERIFY_PEER SSL_VERIFY_NONE/);
565 - print STDERR "Not using SSL_VERIFY_PEER due to out-of-date IO::Socket::SSL.\n";
569 - if (!defined $smtp_ssl_cert_path) {
570 - # use the OpenSSL defaults
571 - return (SSL_verify_mode => SSL_VERIFY_PEER());
573 + return unless defined $smtp_ssl_cert_path; # use defaults
575 if ($smtp_ssl_cert_path eq "") {
576 - return (SSL_verify_mode => SSL_VERIFY_NONE());
577 + $smtp->setopt_ssl_verifyhost(0);
578 + $smtp->setopt_ssl_verifypeer(0);
579 } elsif (-d $smtp_ssl_cert_path) {
580 - return (SSL_verify_mode => SSL_VERIFY_PEER(),
581 - SSL_ca_path => $smtp_ssl_cert_path);
582 + #$smtp->setopt_capath($smtp_ssl_cert_path);
583 + die "SecureTransport does not support a CA directory, use a CA file instead.\n";
584 } elsif (-f $smtp_ssl_cert_path) {
585 - return (SSL_verify_mode => SSL_VERIFY_PEER(),
586 - SSL_ca_file => $smtp_ssl_cert_path);
587 + # These are the default values
588 + #$smtp->setopt_ssl_verifyhost(1);
589 + #$smtp->setopt_ssl_verifypeer(1);
590 + $smtp->setopt_cainfo($smtp_ssl_cert_path);
592 - print STDERR "Not using SSL_VERIFY_PEER because the CA path does not exist.\n";
593 - return (SSL_verify_mode => SSL_VERIFY_NONE());
594 + print STDERR "Not using SSL_VERIFYPEER because the CA path does not exist.\n";
595 + $smtp->setopt_ssl_verifyhost(0);
596 + $smtp->setopt_ssl_verifypeer(0);
600 @@ -1264,65 +1263,61 @@ Message-Id: $message_id
601 die "The required SMTP server is not properly defined."
605 + $smtp = WWW::Curl::Libcurl->spawn;
607 + die "Unable to initialize SMTP properly. Check config and use --smtp-debug. ",
608 + "VALUES: server=$smtp_server ",
609 + "encryption=$smtp_encryption ",
610 + "hello=$smtp_domain",
611 + defined $smtp_server_port ? " port=$smtp_server_port" : "";
613 + if ($debug_net_smtp || $ENV{'GIT_CURL_VERBOSE'}) {
614 + $smtp->setopt_verbose(1);
616 + $smtp->setopt_noprogress(1);
618 + $smtp->setopt_upload(1);
621 if ($smtp_encryption eq 'ssl') {
622 $smtp_server_port ||= 465; # ssmtp
623 - require Net::SMTP::SSL;
624 - $smtp_domain ||= maildomain();
625 - require IO::Socket::SSL;
626 - # Net::SMTP::SSL->new() does not forward any SSL options
627 - IO::Socket::SSL::set_client_defaults(
628 - ssl_verify_params());
629 - $smtp ||= Net::SMTP::SSL->new($smtp_server,
630 - Hello => $smtp_domain,
631 - Port => $smtp_server_port,
632 - Debug => $debug_net_smtp);
636 - $smtp_domain ||= maildomain();
638 + ssl_verify_params();
640 $smtp_server_port ||= 25;
641 - $smtp ||= Net::SMTP->new($smtp_server,
642 - Hello => $smtp_domain,
643 - Debug => $debug_net_smtp,
644 - Port => $smtp_server_port);
645 - if ($smtp_encryption eq 'tls' && $smtp) {
646 - require Net::SMTP::SSL;
647 - $smtp->command('STARTTLS');
649 - if ($smtp->code == 220) {
650 - $smtp = Net::SMTP::SSL->start_SSL($smtp,
651 - ssl_verify_params())
652 - or die "STARTTLS failed! ".IO::Socket::SSL::errstr();
653 - $smtp_encryption = '';
654 - # Send EHLO again to receive fresh
655 - # supported commands
656 - $smtp->hello($smtp_domain);
658 - die "Server does not support STARTTLS! ".$smtp->message;
662 + $smtp->setopt_use_ssl($smtp_encryption eq 'tls' ? 1 : -1);
666 - die "Unable to initialize SMTP properly. Check config and use --smtp-debug. ",
667 - "VALUES: server=$smtp_server ",
668 - "encryption=$smtp_encryption ",
669 - "hello=$smtp_domain",
670 - defined $smtp_server_port ? " port=$smtp_server_port" : "";
671 + $smtp_domain ||= maildomain();
672 + $smtp->setopt_url("$scheme://$smtp_server:$smtp_server_port/$smtp_domain");
673 + $smtp->setopt_mail_from($raw_from);
674 + $smtp->setopt_mail_rcpt(@recipients);
677 + if ($auth && !$auth->{'finished_auth'}) {
678 + $smtp->setopt_username($auth->{'username'});
679 + $smtp->setopt_password($auth->{'password'});
682 - smtp_auth_maybe or die $smtp->message;
684 - $smtp->mail( $raw_from ) or die $smtp->message;
685 - $smtp->to( @recipients ) or die $smtp->message;
686 - $smtp->data or die $smtp->message;
687 - $smtp->datasend("$header\n$message") or die $smtp->message;
688 - $smtp->dataend() or die $smtp->message;
689 - $smtp->code =~ /250|200/ or die "Failed to send $subject\n".$smtp->message;
690 + my $payload = join("\r\n", split(/\n/, "$header\n$message", -1));
691 + $smtp->setopt_readdata($payload);
692 + my $retcode = $smtp->perform;
693 + if ($auth && !$auth->{'finished_auth'}) {
694 + if ($retcode == CURLE_LOGIN_DENIED) {
695 + Git::credential($auth, 'reject');
696 + } elsif (!$retcode) {
697 + Git::credential($auth, 'approve');
699 + $auth->{'finished_auth'} = 1;
701 + die "Failed to send $subject\nError ($retcode) ".$smtp->strerror($retcode).": ".$smtp->errbuf, "\n"
705 printf (($dry_run ? "Dry-" : "")."Sent %s\n", $subject);
708 print (($dry_run ? "Dry-" : "")."OK. Log says:\n");
709 if (!file_name_is_absolute($smtp_server)) {
710 print "Server: $smtp_server\n";
711 @@ -1335,8 +1330,7 @@ Message-Id: $message_id
715 - print "Result: ", $smtp->code, ' ',
716 - ($smtp->message =~ /\n([^\n]+\n)$/s), "\n";
717 + print "Result: ", $smtp->lastheader, "\n";
719 print "Result: OK\n";
721 @@ -1594,7 +1588,7 @@ sub cleanup_compose_files {
722 unlink($compose_filename, $compose_filename . ".final") if $compose;
725 -$smtp->quit if $smtp;
728 sub apply_transfer_encoding {
730 diff --git a/send-email--libcurl.c b/send-email--libcurl.c
732 index 00000000..dd8ebbb7
734 +++ b/send-email--libcurl.c
738 +send-email--libcurl.c -- libcURL helper for git-send-email.perl
739 +Copyright (C) 2014,2015 Kyle J. McKay. All rights reserved.
741 +This program is free software; you can redistribute it and/or
742 +modify it under the terms of the GNU General Public License
743 +as published by the Free Software Foundation; either version 2
744 +of the License, or (at your option) any later version.
746 +This program is distributed in the hope that it will be useful,
747 +but WITHOUT ANY WARRANTY; without even the implied warranty of
748 +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
749 +GNU General Public License for more details.
751 +You should have received a copy of the GNU General Public License
752 +along with this program; if not, write to the Free Software
753 +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
761 +#include <arpa/inet.h>
762 +#include <curl/curl.h>
769 +static int process_cmds(FILE *in, FILE *out, CURL *curl);
771 +static void reset_curl(CURL *curl)
773 + curl_easy_reset(curl);
774 + curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");
777 +int main(int argc, char *const argv[])
781 + FILE *inbinary = freopen(NULL, "rb", stdin);;
782 + FILE *outbinary = freopen(NULL, "ab", stdout);
783 + if (!inbinary || !outbinary || argc != 2 || strcmp(argv[1], "--spawn"))
786 + curl = curl_easy_init();
791 + result = process_cmds(inbinary, outbinary, curl);
792 + curl_easy_cleanup(curl);
796 +static int read_string(FILE *in, size_t len, char **out)
798 + char *string = (char *)malloc(len + 1);
801 + if (len && !fread(string, len, 1, in)) {
805 + string[len] = '\0';
810 +static int handle_string_opt(CURL *curl, CURLoption o, size_t l, FILE *in, int *err)
814 + *err = read_string(in, l, &string);
816 + return 1000000 + *err;
817 + result = curl_easy_setopt(curl, o, string);
822 +#define send_res(f,r) send_res_ex((f),(r),(0))
823 +static int send_res_ex(FILE *out, CURLcode res, int noflush)
825 + uint32_t e = htonl((uint32_t)res);
826 + if (fwrite(&e, sizeof(e), 1, out) != 1)
833 +static int send_string(FILE *out, const void *p, size_t l)
835 + int err = send_res_ex(out, (CURLcode)l, 1);
838 + if (l && fwrite(p, l, 1, out) != 1)
850 +static size_t readfunc(void *ptr, size_t size, size_t nmemb, void *_cxt)
852 + context_t *cxt = (context_t *)_cxt;
853 + size_t max = size * nmemb;
854 + if (max > cxt->len - cxt->offset)
855 + max = cxt->len - cxt->offset;
857 + memcpy(ptr, cxt->data + cxt->offset, max);
858 + cxt->offset += max;
864 +static int append_collect(struct collect_s *clt, const void *p, size_t s);
869 + struct collect_s *clt;
872 +static size_t hdrfunc(char *ptr, size_t size, size_t nmemb, void *_cxt)
874 + hdrline_t *cxt = (hdrline_t *)_cxt;
875 + size_t max = size * nmemb;
877 + if (len >= cxt->space)
878 + len = cxt->space - 1;
879 + memcpy(cxt->line, ptr, len);
880 + while (len && (ptr[len - 1] == '\r' || ptr[len - 1] == '\n'))
882 + cxt->line[len] = '\0';
883 + len = append_collect(cxt->clt, ptr, max);
884 + if (!len || len != max || append_collect(cxt->clt, "", 1) != 1)
889 +static int progressfunc(void *p, double t, double n, double u, double x)
899 +static int xferfunc(void *p, curl_off_t t, curl_off_t n, curl_off_t u, curl_off_t x)
909 +#define BLOCK_SIZE 32736
910 +typedef struct block_s {
911 + struct block_s *next;
913 + char data[BLOCK_SIZE];
916 +typedef struct collect_s {
917 + block_t *first, *last;
921 +static void free_collect(collect_t *clt)
923 + block_t *block = clt->first;
925 + block_t *link = block->next;
934 +static int append_collect(collect_t *clt, const void *p, size_t s)
942 + ptr = (const char *)p;
947 + clt->first = (block_t *)malloc(sizeof(block_t));
950 + clt->first->next = NULL;
951 + clt->first->offset = 0;
952 + clt->last = clt->first;
954 + if (clt->last->offset >= BLOCK_SIZE) {
955 + block_t *next = (block_t *)malloc(sizeof(block_t));
960 + clt->last->next = next;
963 + cpyamt = BLOCK_SIZE - clt->last->offset;
966 + memcpy(clt->last->data + clt->last->offset, ptr, cpyamt);
967 + clt->last->offset += cpyamt;
968 + clt->total += cpyamt;
976 +static size_t writefunc(char *ptr, size_t size, size_t nmemb, void *_clt)
978 + return append_collect((collect_t *)_clt, ptr, size * nmemb);
981 +static int send_collect(FILE *out, const collect_t *clt)
983 + const block_t *block = clt->first;
984 + int err = send_res_ex(out, (CURLcode)clt->total, 1);
988 + if (block->offset) {
989 + if (fwrite(block->data, block->offset, 1, out) != 1)
992 + block = block->next;
999 + CURLCMD_PERFORM = -1000,
1012 +static cmd_lookup_t commands[] = {
1013 + {{'a','g','n','t'}, CURLOPT_USERAGENT},
1014 + {{'a','u','t','h'}, CURLOPT_MAIL_AUTH},
1015 + {{'c','a','i','n'}, CURLOPT_CAINFO},
1016 + {{'c','a','p','a'}, CURLOPT_CAPATH},
1017 + {{'c','e','r','t'}, CURLOPT_SSLCERT},
1018 + {{'c','k','l','s'}, CURLOPT_COOKIELIST},
1019 + {{'c','o','o','k'}, CURLOPT_COOKIEFILE},
1020 + {{'c','t','y','p'}, CURLINFO_CONTENT_TYPE},
1021 + {{'d','a','t','a'}, CURLOPT_READDATA},
1022 + {{'e','r','r','b'}, CURLGET_ERRBUF},
1023 + {{'f','r','o','m'}, CURLOPT_MAIL_FROM},
1024 + {{'h','d','r','s'}, CURLOPT_HTTPHEADER},
1025 + {{'h','d','t','a'}, CURLOPT_HEADERDATA},
1026 + {{'h','g','e','t'}, CURLOPT_HTTPGET},
1027 + {{'k','e','y','_'}, CURLOPT_SSLKEY},
1028 + {{'l','s','t','h'}, CURLGET_LASTHDR},
1029 + {{'m','e','c','h'}, CURLOPT_LOGIN_OPTIONS},
1030 + {{'n','b','d','y'}, CURLOPT_NOBODY},
1031 + {{'n','o','p','g'}, CURLOPT_NOPROGRESS},
1032 + {{'n','t','r','c'}, CURLOPT_NETRC},
1033 + {{'p','i','n','k'}, CURLOPT_PINNEDPUBLICKEY},
1034 + {{'p','o','r','t'}, CURLOPT_PORT},
1035 + {{'p','o','s','t'}, CURLOPT_POST},
1036 + {{'p','r','f','m'}, CURLCMD_PERFORM},
1037 + {{'p','r','x','y'}, CURLOPT_PROXY},
1038 + {{'p','s','w','d'}, CURLOPT_PASSWORD},
1039 + {{'r','c','p','t'}, CURLOPT_MAIL_RCPT},
1040 + {{'r','s','e','t'}, CURLCMD_RESET},
1041 + {{'r','s','p','c'}, CURLINFO_RESPONSE_CODE},
1042 + {{'r','u','r','l'}, CURLINFO_REDIRECT_URL},
1043 + {{'s','t','r','e'}, CURLGET_STRERROR},
1044 + {{'t','m','o','t'}, CURLOPT_TIMEOUT},
1045 + {{'u','p','l','d'}, CURLOPT_UPLOAD},
1046 + {{'u','r','l','_'}, CURLOPT_URL},
1047 + {{'u','s','e','r'}, CURLOPT_USERNAME},
1048 + {{'u','s','s','l'}, CURLOPT_USE_SSL},
1049 + {{'v','e','r','b'}, CURLOPT_VERBOSE},
1050 + {{'v','e','r','s'}, CURLGET_VERSION},
1051 + {{'v','h','s','t'}, CURLOPT_SSL_VERIFYHOST},
1052 + {{'v','p','e','r'}, CURLOPT_SSL_VERIFYPEER},
1053 + {{'w','d','t','a'}, CURLOPT_WRITEDATA}
1056 +static int cmp_cmd(const void *_e1, const void *_e2)
1058 + const cmd_lookup_t *e1 = (cmd_lookup_t *)_e1;
1059 + const cmd_lookup_t *e2 = (cmd_lookup_t *)_e2;
1060 + return memcmp(e1->tag, e2->tag, 4);
1063 +static int find_command(const char tag[4])
1065 + cmd_lookup_t search;
1066 + const cmd_lookup_t *ans;
1068 + memcpy(search.tag, tag, 4);
1069 + ans = (cmd_lookup_t *)
1070 + bsearch(&search, commands, sizeof(commands)/sizeof(commands[0]),
1071 + sizeof(commands[0]), cmp_cmd);
1072 + return ans ? ans->opt : -1;
1075 +static int process_cmds(FILE *in, FILE *out, CURL *curl)
1078 + char errbuf[CURL_ERROR_SIZE+1];
1079 + char hdrbuf[1024];
1080 + char *data = NULL;
1081 + size_t datalen = 0;
1082 + struct curl_slist *recipients = NULL;
1083 + struct curl_slist *httpheaders = NULL;
1084 + collect_t hdata, wdata;
1086 + memset(&hdata, 0, sizeof(hdata));
1087 + memset(&wdata, 0, sizeof(wdata));
1090 + while (!ferror(in) && !ferror(out) && fread(&cmd, sizeof(cmd), 1, in)) {
1094 + int opt = find_command(cmd.tag);
1095 + const char *stringval = NULL;
1097 + cmd.val = ntohl(cmd.val);
1100 + case CURLOPT_NOPROGRESS:
1101 + case CURLOPT_SSL_VERIFYPEER:
1102 + case CURLOPT_HTTPGET:
1103 + case CURLOPT_NOBODY:
1104 + case CURLOPT_POST:
1105 + case CURLOPT_UPLOAD:
1106 + case CURLOPT_VERBOSE:
1107 + res = curl_easy_setopt(curl, (CURLoption)opt,
1108 + cmd.val ? 1L : 0L);
1111 + case CURLOPT_CAINFO:
1112 + case CURLOPT_CAPATH:
1113 + case CURLOPT_LOGIN_OPTIONS:
1114 + case CURLOPT_MAIL_AUTH:
1115 + case CURLOPT_MAIL_FROM:
1116 + case CURLOPT_PASSWORD:
1118 + case CURLOPT_USERNAME:
1119 + case CURLOPT_PROXY:
1120 + case CURLOPT_COOKIELIST:
1121 + case CURLOPT_USERAGENT:
1122 + case CURLOPT_SSLCERT:
1123 + case CURLOPT_SSLKEY:
1124 + case CURLOPT_PINNEDPUBLICKEY:
1125 + res = handle_string_opt(curl, (CURLoption)opt,
1126 + cmd.val, in, &err);
1129 + case CURLOPT_PORT:
1130 + case CURLOPT_TIMEOUT:
1131 + res = curl_easy_setopt(curl, (CURLoption)opt, (long)cmd.val);
1134 + case CURLOPT_SSL_VERIFYHOST:
1135 + res = curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST,
1136 + cmd.val ? 2L : 0L);
1139 + case CURLOPT_USE_SSL:
1140 + res = curl_easy_setopt(curl, CURLOPT_USE_SSL, (long)(
1141 + (cmd.val & 0x80000000) ? CURLUSESSL_TRY :
1142 + (cmd.val ? CURLUSESSL_ALL : CURLUSESSL_NONE)));
1145 + case CURLOPT_NETRC:
1146 + res = curl_easy_setopt(curl, CURLOPT_NETRC, (long)(
1147 + (cmd.val & 0x80000000) ? CURL_NETRC_OPTIONAL :
1148 + (cmd.val ? CURL_NETRC_REQUIRED : CURL_NETRC_IGNORED)));
1151 + case CURLOPT_READDATA:
1154 + err = read_string(in, cmd.val, &data);
1157 + datalen = cmd.val;
1160 + case CURLOPT_COOKIEFILE:
1163 + err = read_string(in, cmd.val, &string);
1166 + res = curl_easy_setopt(curl, CURLOPT_COOKIEFILE, string);
1167 + if (!res && *string)
1168 + res = curl_easy_setopt(curl, CURLOPT_COOKIEJAR, string);
1173 + case CURLGET_VERSION:
1175 + curl_version_info_data *data = curl_version_info(CURLVERSION_NOW);
1176 + res = data->version_num;
1180 + case CURLGET_ERRBUF:
1181 + stringval = errbuf;
1184 + case CURLGET_LASTHDR:
1185 + stringval = hdrbuf;
1188 + case CURLGET_STRERROR:
1189 + stringval = curl_easy_strerror((CURLcode) cmd.val);
1192 + case CURLOPT_HEADERDATA:
1193 + err = send_collect(out, &hdata);
1198 + case CURLOPT_WRITEDATA:
1199 + err = send_collect(out, &wdata);
1204 + case CURLINFO_RESPONSE_CODE:
1207 + res = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response);
1215 + case CURLINFO_REDIRECT_URL:
1216 + case CURLINFO_CONTENT_TYPE:
1219 + res = curl_easy_getinfo(curl, (CURLINFO)opt, &string);
1220 + stringval = res || !string ? "" : string;
1224 + case CURLOPT_MAIL_RCPT:
1228 + curl_slist_free_all(recipients);
1229 + recipients = NULL;
1231 + err = read_string(in, cmd.val, &string);
1236 + const char *nul = memchr(ptr, 0, cmd.val);
1239 + recipients = curl_slist_append(recipients, ptr);
1240 + cmd.val -= ++nul - ptr;
1244 + res = curl_easy_setopt(curl, CURLOPT_MAIL_RCPT, recipients);
1248 + case CURLOPT_HTTPHEADER:
1251 + if (httpheaders) {
1252 + curl_slist_free_all(httpheaders);
1253 + httpheaders = NULL;
1255 + err = read_string(in, cmd.val, &string);
1260 + const char *nul = memchr(ptr, 0, cmd.val);
1263 + httpheaders = curl_slist_append(httpheaders, ptr);
1264 + cmd.val -= ++nul - ptr;
1268 + res = curl_easy_setopt(curl, CURLOPT_HTTPHEADER, httpheaders);
1272 + case CURLCMD_RESET:
1274 + free_collect(&wdata);
1275 + free_collect(&hdata);
1276 + if (httpheaders) {
1277 + curl_slist_free_all(httpheaders);
1278 + httpheaders = NULL;
1281 + curl_slist_free_all(recipients);
1282 + recipients = NULL;
1294 + case CURLCMD_PERFORM:
1297 + hdrline_t lasthdr;
1300 + cxt.len = datalen;
1303 + lasthdr.line = hdrbuf;
1304 + lasthdr.space = sizeof(hdrbuf);
1305 + lasthdr.clt = &hdata;
1307 + free_collect(&wdata);
1308 + free_collect(&hdata);
1309 + res = curl_easy_setopt(curl,
1310 + CURLOPT_ERRORBUFFER, errbuf);
1312 + res = curl_easy_setopt(curl,
1313 + CURLOPT_INFILESIZE, (long)datalen);
1315 + res = curl_easy_setopt(curl,
1316 + CURLOPT_POSTFIELDSIZE, (long)datalen);
1318 + res = curl_easy_setopt(curl,
1319 + CURLOPT_READDATA, &cxt);
1321 + res = curl_easy_setopt(curl,
1322 + CURLOPT_READFUNCTION, (void *)readfunc);
1324 + res = curl_easy_setopt(curl,
1325 + CURLOPT_PROGRESSFUNCTION, (void *)progressfunc);
1327 + res = curl_easy_setopt(curl,
1328 + CURLOPT_XFERINFOFUNCTION, (void *)xferfunc);
1330 + res = curl_easy_setopt(curl,
1331 + CURLOPT_WRITEDATA, &wdata);
1333 + res = curl_easy_setopt(curl,
1334 + CURLOPT_WRITEFUNCTION, (void *)writefunc);
1336 + res = curl_easy_setopt(curl,
1337 + CURLOPT_HEADERDATA, &lasthdr);
1339 + res = curl_easy_setopt(curl,
1340 + CURLOPT_HEADERFUNCTION, (void *)hdrfunc);
1342 + res = curl_easy_perform(curl);
1353 + send_string(out, stringval, strlen(stringval)) :
1354 + send_res(out, res);
1359 + free_collect(&wdata);
1360 + free_collect(&hdata);
1362 + curl_slist_free_all(httpheaders);
1364 + curl_slist_free_all(recipients);
1368 + if (ferror(in) || !feof(in) || ferror(out))
1373 diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
1374 index 7be14a4e..09fd92b3 100755
1375 --- a/t/t9001-send-email.sh
1376 +++ b/t/t9001-send-email.sh
1377 @@ -146,6 +146,7 @@ cat >expected-show-all-headers <<\EOF
1378 (mbox) Adding cc: A <author@example.com> from line 'From: A <author@example.com>'
1379 (mbox) Adding cc: One <one@example.com> from line 'Cc: One <one@example.com>, two@example.com'
1380 (mbox) Adding cc: two@example.com from line 'Cc: One <one@example.com>, two@example.com'
1383 Server: relay.example.com
1384 MAIL FROM:<from@example.com>
1385 @@ -164,7 +165,6 @@ Cc: cc@example.com,
1386 Subject: [PATCH 1/1] Second.
1388 Message-Id: MESSAGE-ID-STRING
1389 -X-Mailer: X-MAILER-STRING
1390 In-Reply-To: <unique-message-id@example.com>
1391 References: <unique-message-id@example.com>
1393 @@ -496,6 +496,7 @@ cat >expected-suppress-sob <<\EOF
1394 (mbox) Adding cc: A <author@example.com> from line 'From: A <author@example.com>'
1395 (mbox) Adding cc: One <one@example.com> from line 'Cc: One <one@example.com>, two@example.com'
1396 (mbox) Adding cc: two@example.com from line 'Cc: One <one@example.com>, two@example.com'
1399 Server: relay.example.com
1400 MAIL FROM:<from@example.com>
1401 @@ -513,7 +514,6 @@ Cc: cc@example.com,
1402 Subject: [PATCH 1/1] Second.
1404 Message-Id: MESSAGE-ID-STRING
1405 -X-Mailer: X-MAILER-STRING
1409 @@ -545,6 +545,7 @@ cat >expected-suppress-sob <<\EOF
1410 (mbox) Adding cc: A <author@example.com> from line 'From: A <author@example.com>'
1411 (mbox) Adding cc: One <one@example.com> from line 'Cc: One <one@example.com>, two@example.com'
1412 (mbox) Adding cc: two@example.com from line 'Cc: One <one@example.com>, two@example.com'
1415 Server: relay.example.com
1416 MAIL FROM:<from@example.com>
1417 @@ -560,7 +561,6 @@ Cc: A <author@example.com>,
1418 Subject: [PATCH 1/1] Second.
1420 Message-Id: MESSAGE-ID-STRING
1421 -X-Mailer: X-MAILER-STRING
1425 @@ -578,6 +578,7 @@ cat >expected-suppress-cccmd <<\EOF
1426 (mbox) Adding cc: One <one@example.com> from line 'Cc: One <one@example.com>, two@example.com'
1427 (mbox) Adding cc: two@example.com from line 'Cc: One <one@example.com>, two@example.com'
1428 (body) Adding cc: C O Mitter <committer@example.com> from line 'Signed-off-by: C O Mitter <committer@example.com>'
1431 Server: relay.example.com
1432 MAIL FROM:<from@example.com>
1433 @@ -595,7 +596,6 @@ Cc: A <author@example.com>,
1434 Subject: [PATCH 1/1] Second.
1436 Message-Id: MESSAGE-ID-STRING
1437 -X-Mailer: X-MAILER-STRING
1441 @@ -612,6 +612,7 @@ test_expect_success $PREREQ 'sendemail.cccmd' '
1442 test_expect_success $PREREQ 'setup expect' '
1443 cat >expected-suppress-all <<\EOF
1447 Server: relay.example.com
1448 MAIL FROM:<from@example.com>
1449 @@ -621,7 +622,6 @@ To: to@example.com
1450 Subject: [PATCH 1/1] Second.
1452 Message-Id: MESSAGE-ID-STRING
1453 -X-Mailer: X-MAILER-STRING
1457 @@ -638,6 +638,7 @@ cat >expected-suppress-body <<\EOF
1458 (mbox) Adding cc: One <one@example.com> from line 'Cc: One <one@example.com>, two@example.com'
1459 (mbox) Adding cc: two@example.com from line 'Cc: One <one@example.com>, two@example.com'
1460 (cc-cmd) Adding cc: cc-cmd@example.com from: './cccmd'
1463 Server: relay.example.com
1464 MAIL FROM:<from@example.com>
1465 @@ -655,7 +656,6 @@ Cc: A <author@example.com>,
1466 Subject: [PATCH 1/1] Second.
1468 Message-Id: MESSAGE-ID-STRING
1469 -X-Mailer: X-MAILER-STRING
1473 @@ -671,6 +671,7 @@ cat >expected-suppress-body-cccmd <<\EOF
1474 (mbox) Adding cc: A <author@example.com> from line 'From: A <author@example.com>'
1475 (mbox) Adding cc: One <one@example.com> from line 'Cc: One <one@example.com>, two@example.com'
1476 (mbox) Adding cc: two@example.com from line 'Cc: One <one@example.com>, two@example.com'
1479 Server: relay.example.com
1480 MAIL FROM:<from@example.com>
1481 @@ -686,7 +687,6 @@ Cc: A <author@example.com>,
1482 Subject: [PATCH 1/1] Second.
1484 Message-Id: MESSAGE-ID-STRING
1485 -X-Mailer: X-MAILER-STRING
1489 @@ -702,6 +702,7 @@ cat >expected-suppress-sob <<\EOF
1490 (mbox) Adding cc: A <author@example.com> from line 'From: A <author@example.com>'
1491 (mbox) Adding cc: One <one@example.com> from line 'Cc: One <one@example.com>, two@example.com'
1492 (mbox) Adding cc: two@example.com from line 'Cc: One <one@example.com>, two@example.com'
1495 Server: relay.example.com
1496 MAIL FROM:<from@example.com>
1497 @@ -717,7 +718,6 @@ Cc: A <author@example.com>,
1498 Subject: [PATCH 1/1] Second.
1500 Message-Id: MESSAGE-ID-STRING
1501 -X-Mailer: X-MAILER-STRING
1505 @@ -735,6 +735,7 @@ cat >expected-suppress-bodycc <<\EOF
1506 (mbox) Adding cc: One <one@example.com> from line 'Cc: One <one@example.com>, two@example.com'
1507 (mbox) Adding cc: two@example.com from line 'Cc: One <one@example.com>, two@example.com'
1508 (body) Adding cc: C O Mitter <committer@example.com> from line 'Signed-off-by: C O Mitter <committer@example.com>'
1511 Server: relay.example.com
1512 MAIL FROM:<from@example.com>
1513 @@ -752,7 +753,6 @@ Cc: A <author@example.com>,
1514 Subject: [PATCH 1/1] Second.
1516 Message-Id: MESSAGE-ID-STRING
1517 -X-Mailer: X-MAILER-STRING
1521 @@ -767,6 +767,7 @@ cat >expected-suppress-cc <<\EOF
1523 (mbox) Adding cc: A <author@example.com> from line 'From: A <author@example.com>'
1524 (body) Adding cc: C O Mitter <committer@example.com> from line 'Signed-off-by: C O Mitter <committer@example.com>'
1527 Server: relay.example.com
1528 MAIL FROM:<from@example.com>
1529 @@ -780,7 +781,6 @@ Cc: A <author@example.com>,
1530 Subject: [PATCH 1/1] Second.
1532 Message-Id: MESSAGE-ID-STRING
1533 -X-Mailer: X-MAILER-STRING