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 | 189 ++++++--------
47 send-email--libcurl.c | 637 +++++++++++++++++++++++++++++++++++++++++++++
48 t/t9001-send-email.sh | 20 +-
49 4 files changed, 1135 insertions(+), 115 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 da81be40..05bfd305 100755
465 --- a/git-send-email.perl
466 +++ b/git-send-email.perl
467 @@ -26,8 +26,15 @@ use Text::ParseWords;
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 @@ -55,7 +62,7 @@ git send-email --dump-aliases
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 @@ -235,7 +242,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 @@ -317,6 +324,7 @@ $rc = GetOptions(
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 @@ -967,8 +975,12 @@ sub make_message_id {
510 require Sys::Hostname;
511 $du_part = 'user@' . Sys::Hostname::hostname();
513 - my $message_id_template = "<%s-%s>";
515 + defined($boot_time) or chomp($boot_time = qx(/usr/sbin/sysctl -n kern.boottime) || '');
516 + my $message_id_template = "%s-%s" . $boot_time;
517 $message_id = sprintf($message_id_template, $uniq, $du_part);
518 + @_ = split /@/, $message_id;
519 + $message_id = '<'.substr(md5_hex($_[0]),0,31).'@'.substr(md5_hex($_[1]),1,31).'>';
520 #print "new message id = $message_id\n"; # Was useful for debugging
523 @@ -1142,12 +1154,12 @@ sub smtp_host_string {
527 -# Returns 1 if authentication succeeded or was not necessary
528 -# (smtp_user was not specified), and 0 otherwise.
529 +# Returns a filled hashref from Git::credential fill if authentication
530 +# has been requested, undef otherwise.
532 sub smtp_auth_maybe {
533 if (!defined $smtp_authuser || $auth) {
538 # Workaround AUTH PLAIN/LOGIN interaction defect
539 @@ -1166,58 +1178,32 @@ sub smtp_auth_maybe {
540 # TODO: Authentication may fail not because credentials were
541 # invalid but due to other reasons, in which we should not
542 # reject credentials.
543 - $auth = Git::credential({
545 'protocol' => 'smtp',
546 'host' => smtp_host_string(),
547 'username' => $smtp_authuser,
548 # if there's no password, "git credential fill" will
549 # give us one, otherwise it'll just pass this one.
550 'password' => $smtp_authpass
555 - my $sasl = Authen::SASL->new(
556 - mechanism => $smtp_auth,
558 - user => $cred->{'username'},
559 - pass => $cred->{'password'},
560 - authname => $cred->{'username'},
564 - return !!$smtp->auth($sasl);
567 - return !!$smtp->auth($cred->{'username'}, $cred->{'password'});
571 + Git::credential($auth, 'fill');
575 sub ssl_verify_params {
577 - require IO::Socket::SSL;
578 - IO::Socket::SSL->import(qw/SSL_VERIFY_PEER SSL_VERIFY_NONE/);
581 - print STDERR "Not using SSL_VERIFY_PEER due to out-of-date IO::Socket::SSL.\n";
585 - if (!defined $smtp_ssl_cert_path) {
586 - # use the OpenSSL defaults
587 - return (SSL_verify_mode => SSL_VERIFY_PEER());
589 + return unless defined $smtp_ssl_cert_path; # use defaults
591 if ($smtp_ssl_cert_path eq "") {
592 - return (SSL_verify_mode => SSL_VERIFY_NONE());
593 + $smtp->setopt_ssl_verifyhost(0);
594 + $smtp->setopt_ssl_verifypeer(0);
595 } elsif (-d $smtp_ssl_cert_path) {
596 - return (SSL_verify_mode => SSL_VERIFY_PEER(),
597 - SSL_ca_path => $smtp_ssl_cert_path);
598 + #$smtp->setopt_capath($smtp_ssl_cert_path);
599 + die "SecureTransport does not support a CA directory, use a CA file instead.\n";
600 } elsif (-f $smtp_ssl_cert_path) {
601 - return (SSL_verify_mode => SSL_VERIFY_PEER(),
602 - SSL_ca_file => $smtp_ssl_cert_path);
603 + # These are the default values
604 + #$smtp->setopt_ssl_verifyhost(1);
605 + #$smtp->setopt_ssl_verifypeer(1);
606 + $smtp->setopt_cainfo($smtp_ssl_cert_path);
608 die "CA path \"$smtp_ssl_cert_path\" does not exist";
610 @@ -1335,76 +1321,70 @@ Message-Id: $message_id
611 die "The required SMTP server is not properly defined."
614 - if ($smtp_encryption eq 'ssl') {
615 - $smtp_server_port ||= 465; # ssmtp
616 - require Net::SMTP::SSL;
617 - $smtp_domain ||= maildomain();
618 - require IO::Socket::SSL;
620 - # Suppress "variable accessed once" warning.
622 - no warnings 'once';
623 - $IO::Socket::SSL::DEBUG = 1;
625 + $smtp = WWW::Curl::Libcurl->spawn;
627 + die "Unable to initialize SMTP properly. Check config and use --smtp-debug. ",
628 + "VALUES: server=$smtp_server ",
629 + "encryption=$smtp_encryption ",
630 + "hello=$smtp_domain",
631 + defined $smtp_server_port ? " port=$smtp_server_port" : "";
634 - # Net::SMTP::SSL->new() does not forward any SSL options
635 - IO::Socket::SSL::set_client_defaults(
636 - ssl_verify_params());
637 - $smtp ||= Net::SMTP::SSL->new($smtp_server,
638 - Hello => $smtp_domain,
639 - Port => $smtp_server_port,
640 - Debug => $debug_net_smtp);
641 + if ($debug_net_smtp || $ENV{'GIT_CURL_VERBOSE'}) {
642 + $smtp->setopt_verbose(1);
644 + $smtp->setopt_noprogress(1);
646 + $smtp->setopt_upload(1);
648 + # attempt to avoid throttling issues when sending multiple messages
653 - $smtp_domain ||= maildomain();
655 + if ($smtp_encryption eq 'ssl') {
656 + $smtp_server_port ||= 465; # ssmtp
658 + ssl_verify_params();
660 $smtp_server_port ||= 25;
661 - $smtp ||= Net::SMTP->new($smtp_server,
662 - Hello => $smtp_domain,
663 - Debug => $debug_net_smtp,
664 - Port => $smtp_server_port);
665 - if ($smtp_encryption eq 'tls' && $smtp) {
666 - require Net::SMTP::SSL;
667 - $smtp->command('STARTTLS');
669 - if ($smtp->code == 220) {
670 - $smtp = Net::SMTP::SSL->start_SSL($smtp,
671 - ssl_verify_params())
672 - or die "STARTTLS failed! ".IO::Socket::SSL::errstr();
673 - $smtp_encryption = '';
674 - # Send EHLO again to receive fresh
675 - # supported commands
676 - $smtp->hello($smtp_domain);
678 - die "Server does not support STARTTLS! ".$smtp->message;
680 + $smtp->setopt_use_ssl($smtp_encryption eq 'tls' ? 1 : -1);
682 + $smtp_domain ||= maildomain();
683 + $smtp->setopt_url("$scheme://$smtp_server:$smtp_server_port/$smtp_domain");
684 + $smtp->setopt_mail_from($raw_from);
685 + $smtp->setopt_mail_rcpt(@recipients);
688 + if ($auth && !$auth->{'finished_auth'}) {
689 + $smtp->setopt_username($auth->{'username'});
690 + $smtp->setopt_password($auth->{'password'});
692 + my @auths = split(" ", $smtp_auth);
693 + if (@auths && $smtp->setopt_login_options(join(";", map("AUTH=$_", @auths))) != 0) {
694 + die "unrecognized smtp auth: '${smtp_auth}'";
700 - die "Unable to initialize SMTP properly. Check config and use --smtp-debug. ",
701 - "VALUES: server=$smtp_server ",
702 - "encryption=$smtp_encryption ",
703 - "hello=$smtp_domain",
704 - defined $smtp_server_port ? " port=$smtp_server_port" : "";
707 - smtp_auth_maybe or die $smtp->message;
709 - $smtp->mail( $raw_from ) or die $smtp->message;
710 - $smtp->to( @recipients ) or die $smtp->message;
711 - $smtp->data or die $smtp->message;
712 - $smtp->datasend("$header\n") or die $smtp->message;
713 - my @lines = split /^/, $message;
714 - foreach my $line (@lines) {
715 - $smtp->datasend("$line") or die $smtp->message;
716 + my $payload = join("\r\n", split(/\n/, "$header\n$message", -1));
717 + $smtp->setopt_readdata($payload);
718 + my $retcode = $smtp->perform;
719 + if ($auth && !$auth->{'finished_auth'}) {
720 + if ($retcode == CURLE_LOGIN_DENIED) {
721 + Git::credential($auth, 'reject');
722 + } elsif (!$retcode) {
723 + Git::credential($auth, 'approve');
725 + $auth->{'finished_auth'} = 1;
727 - $smtp->dataend() or die $smtp->message;
728 - $smtp->code =~ /250|200/ or die "Failed to send $subject\n".$smtp->message;
729 + die "Failed to send $subject\nError ($retcode) ".$smtp->strerror($retcode).": ".$smtp->errbuf, "\n"
733 printf (($dry_run ? "Dry-" : "")."Sent %s\n", $subject);
736 print (($dry_run ? "Dry-" : "")."OK. Log says:\n");
737 if (!file_name_is_absolute($smtp_server)) {
738 print "Server: $smtp_server\n";
739 @@ -1417,8 +1397,7 @@ Message-Id: $message_id
743 - print "Result: ", $smtp->code, ' ',
744 - ($smtp->message =~ /\n([^\n]+\n)$/s), "\n";
745 + print "Result: ", $smtp->lastheader, "\n";
747 print "Result: OK\n";
749 @@ -1676,7 +1655,7 @@ sub cleanup_compose_files {
750 unlink($compose_filename, $compose_filename . ".final") if $compose;
753 -$smtp->quit if $smtp;
756 sub apply_transfer_encoding {
758 diff --git a/send-email--libcurl.c b/send-email--libcurl.c
760 index 00000000..dd8ebbb7
762 +++ b/send-email--libcurl.c
766 +send-email--libcurl.c -- libcURL helper for git-send-email.perl
767 +Copyright (C) 2014,2015 Kyle J. McKay. All rights reserved.
769 +This program is free software; you can redistribute it and/or
770 +modify it under the terms of the GNU General Public License
771 +as published by the Free Software Foundation; either version 2
772 +of the License, or (at your option) any later version.
774 +This program is distributed in the hope that it will be useful,
775 +but WITHOUT ANY WARRANTY; without even the implied warranty of
776 +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
777 +GNU General Public License for more details.
779 +You should have received a copy of the GNU General Public License
780 +along with this program; if not, write to the Free Software
781 +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
789 +#include <arpa/inet.h>
790 +#include <curl/curl.h>
797 +static int process_cmds(FILE *in, FILE *out, CURL *curl);
799 +static void reset_curl(CURL *curl)
801 + curl_easy_reset(curl);
802 + curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");
805 +int main(int argc, char *const argv[])
809 + FILE *inbinary = freopen(NULL, "rb", stdin);;
810 + FILE *outbinary = freopen(NULL, "ab", stdout);
811 + if (!inbinary || !outbinary || argc != 2 || strcmp(argv[1], "--spawn"))
814 + curl = curl_easy_init();
819 + result = process_cmds(inbinary, outbinary, curl);
820 + curl_easy_cleanup(curl);
824 +static int read_string(FILE *in, size_t len, char **out)
826 + char *string = (char *)malloc(len + 1);
829 + if (len && !fread(string, len, 1, in)) {
833 + string[len] = '\0';
838 +static int handle_string_opt(CURL *curl, CURLoption o, size_t l, FILE *in, int *err)
842 + *err = read_string(in, l, &string);
844 + return 1000000 + *err;
845 + result = curl_easy_setopt(curl, o, string);
850 +#define send_res(f,r) send_res_ex((f),(r),(0))
851 +static int send_res_ex(FILE *out, CURLcode res, int noflush)
853 + uint32_t e = htonl((uint32_t)res);
854 + if (fwrite(&e, sizeof(e), 1, out) != 1)
861 +static int send_string(FILE *out, const void *p, size_t l)
863 + int err = send_res_ex(out, (CURLcode)l, 1);
866 + if (l && fwrite(p, l, 1, out) != 1)
878 +static size_t readfunc(void *ptr, size_t size, size_t nmemb, void *_cxt)
880 + context_t *cxt = (context_t *)_cxt;
881 + size_t max = size * nmemb;
882 + if (max > cxt->len - cxt->offset)
883 + max = cxt->len - cxt->offset;
885 + memcpy(ptr, cxt->data + cxt->offset, max);
886 + cxt->offset += max;
892 +static int append_collect(struct collect_s *clt, const void *p, size_t s);
897 + struct collect_s *clt;
900 +static size_t hdrfunc(char *ptr, size_t size, size_t nmemb, void *_cxt)
902 + hdrline_t *cxt = (hdrline_t *)_cxt;
903 + size_t max = size * nmemb;
905 + if (len >= cxt->space)
906 + len = cxt->space - 1;
907 + memcpy(cxt->line, ptr, len);
908 + while (len && (ptr[len - 1] == '\r' || ptr[len - 1] == '\n'))
910 + cxt->line[len] = '\0';
911 + len = append_collect(cxt->clt, ptr, max);
912 + if (!len || len != max || append_collect(cxt->clt, "", 1) != 1)
917 +static int progressfunc(void *p, double t, double n, double u, double x)
927 +static int xferfunc(void *p, curl_off_t t, curl_off_t n, curl_off_t u, curl_off_t x)
937 +#define BLOCK_SIZE 32736
938 +typedef struct block_s {
939 + struct block_s *next;
941 + char data[BLOCK_SIZE];
944 +typedef struct collect_s {
945 + block_t *first, *last;
949 +static void free_collect(collect_t *clt)
951 + block_t *block = clt->first;
953 + block_t *link = block->next;
962 +static int append_collect(collect_t *clt, const void *p, size_t s)
970 + ptr = (const char *)p;
975 + clt->first = (block_t *)malloc(sizeof(block_t));
978 + clt->first->next = NULL;
979 + clt->first->offset = 0;
980 + clt->last = clt->first;
982 + if (clt->last->offset >= BLOCK_SIZE) {
983 + block_t *next = (block_t *)malloc(sizeof(block_t));
988 + clt->last->next = next;
991 + cpyamt = BLOCK_SIZE - clt->last->offset;
994 + memcpy(clt->last->data + clt->last->offset, ptr, cpyamt);
995 + clt->last->offset += cpyamt;
996 + clt->total += cpyamt;
1004 +static size_t writefunc(char *ptr, size_t size, size_t nmemb, void *_clt)
1006 + return append_collect((collect_t *)_clt, ptr, size * nmemb);
1009 +static int send_collect(FILE *out, const collect_t *clt)
1011 + const block_t *block = clt->first;
1012 + int err = send_res_ex(out, (CURLcode)clt->total, 1);
1016 + if (block->offset) {
1017 + if (fwrite(block->data, block->offset, 1, out) != 1)
1020 + block = block->next;
1027 + CURLCMD_PERFORM = -1000,
1040 +static cmd_lookup_t commands[] = {
1041 + {{'a','g','n','t'}, CURLOPT_USERAGENT},
1042 + {{'a','u','t','h'}, CURLOPT_MAIL_AUTH},
1043 + {{'c','a','i','n'}, CURLOPT_CAINFO},
1044 + {{'c','a','p','a'}, CURLOPT_CAPATH},
1045 + {{'c','e','r','t'}, CURLOPT_SSLCERT},
1046 + {{'c','k','l','s'}, CURLOPT_COOKIELIST},
1047 + {{'c','o','o','k'}, CURLOPT_COOKIEFILE},
1048 + {{'c','t','y','p'}, CURLINFO_CONTENT_TYPE},
1049 + {{'d','a','t','a'}, CURLOPT_READDATA},
1050 + {{'e','r','r','b'}, CURLGET_ERRBUF},
1051 + {{'f','r','o','m'}, CURLOPT_MAIL_FROM},
1052 + {{'h','d','r','s'}, CURLOPT_HTTPHEADER},
1053 + {{'h','d','t','a'}, CURLOPT_HEADERDATA},
1054 + {{'h','g','e','t'}, CURLOPT_HTTPGET},
1055 + {{'k','e','y','_'}, CURLOPT_SSLKEY},
1056 + {{'l','s','t','h'}, CURLGET_LASTHDR},
1057 + {{'m','e','c','h'}, CURLOPT_LOGIN_OPTIONS},
1058 + {{'n','b','d','y'}, CURLOPT_NOBODY},
1059 + {{'n','o','p','g'}, CURLOPT_NOPROGRESS},
1060 + {{'n','t','r','c'}, CURLOPT_NETRC},
1061 + {{'p','i','n','k'}, CURLOPT_PINNEDPUBLICKEY},
1062 + {{'p','o','r','t'}, CURLOPT_PORT},
1063 + {{'p','o','s','t'}, CURLOPT_POST},
1064 + {{'p','r','f','m'}, CURLCMD_PERFORM},
1065 + {{'p','r','x','y'}, CURLOPT_PROXY},
1066 + {{'p','s','w','d'}, CURLOPT_PASSWORD},
1067 + {{'r','c','p','t'}, CURLOPT_MAIL_RCPT},
1068 + {{'r','s','e','t'}, CURLCMD_RESET},
1069 + {{'r','s','p','c'}, CURLINFO_RESPONSE_CODE},
1070 + {{'r','u','r','l'}, CURLINFO_REDIRECT_URL},
1071 + {{'s','t','r','e'}, CURLGET_STRERROR},
1072 + {{'t','m','o','t'}, CURLOPT_TIMEOUT},
1073 + {{'u','p','l','d'}, CURLOPT_UPLOAD},
1074 + {{'u','r','l','_'}, CURLOPT_URL},
1075 + {{'u','s','e','r'}, CURLOPT_USERNAME},
1076 + {{'u','s','s','l'}, CURLOPT_USE_SSL},
1077 + {{'v','e','r','b'}, CURLOPT_VERBOSE},
1078 + {{'v','e','r','s'}, CURLGET_VERSION},
1079 + {{'v','h','s','t'}, CURLOPT_SSL_VERIFYHOST},
1080 + {{'v','p','e','r'}, CURLOPT_SSL_VERIFYPEER},
1081 + {{'w','d','t','a'}, CURLOPT_WRITEDATA}
1084 +static int cmp_cmd(const void *_e1, const void *_e2)
1086 + const cmd_lookup_t *e1 = (cmd_lookup_t *)_e1;
1087 + const cmd_lookup_t *e2 = (cmd_lookup_t *)_e2;
1088 + return memcmp(e1->tag, e2->tag, 4);
1091 +static int find_command(const char tag[4])
1093 + cmd_lookup_t search;
1094 + const cmd_lookup_t *ans;
1096 + memcpy(search.tag, tag, 4);
1097 + ans = (cmd_lookup_t *)
1098 + bsearch(&search, commands, sizeof(commands)/sizeof(commands[0]),
1099 + sizeof(commands[0]), cmp_cmd);
1100 + return ans ? ans->opt : -1;
1103 +static int process_cmds(FILE *in, FILE *out, CURL *curl)
1106 + char errbuf[CURL_ERROR_SIZE+1];
1107 + char hdrbuf[1024];
1108 + char *data = NULL;
1109 + size_t datalen = 0;
1110 + struct curl_slist *recipients = NULL;
1111 + struct curl_slist *httpheaders = NULL;
1112 + collect_t hdata, wdata;
1114 + memset(&hdata, 0, sizeof(hdata));
1115 + memset(&wdata, 0, sizeof(wdata));
1118 + while (!ferror(in) && !ferror(out) && fread(&cmd, sizeof(cmd), 1, in)) {
1122 + int opt = find_command(cmd.tag);
1123 + const char *stringval = NULL;
1125 + cmd.val = ntohl(cmd.val);
1128 + case CURLOPT_NOPROGRESS:
1129 + case CURLOPT_SSL_VERIFYPEER:
1130 + case CURLOPT_HTTPGET:
1131 + case CURLOPT_NOBODY:
1132 + case CURLOPT_POST:
1133 + case CURLOPT_UPLOAD:
1134 + case CURLOPT_VERBOSE:
1135 + res = curl_easy_setopt(curl, (CURLoption)opt,
1136 + cmd.val ? 1L : 0L);
1139 + case CURLOPT_CAINFO:
1140 + case CURLOPT_CAPATH:
1141 + case CURLOPT_LOGIN_OPTIONS:
1142 + case CURLOPT_MAIL_AUTH:
1143 + case CURLOPT_MAIL_FROM:
1144 + case CURLOPT_PASSWORD:
1146 + case CURLOPT_USERNAME:
1147 + case CURLOPT_PROXY:
1148 + case CURLOPT_COOKIELIST:
1149 + case CURLOPT_USERAGENT:
1150 + case CURLOPT_SSLCERT:
1151 + case CURLOPT_SSLKEY:
1152 + case CURLOPT_PINNEDPUBLICKEY:
1153 + res = handle_string_opt(curl, (CURLoption)opt,
1154 + cmd.val, in, &err);
1157 + case CURLOPT_PORT:
1158 + case CURLOPT_TIMEOUT:
1159 + res = curl_easy_setopt(curl, (CURLoption)opt, (long)cmd.val);
1162 + case CURLOPT_SSL_VERIFYHOST:
1163 + res = curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST,
1164 + cmd.val ? 2L : 0L);
1167 + case CURLOPT_USE_SSL:
1168 + res = curl_easy_setopt(curl, CURLOPT_USE_SSL, (long)(
1169 + (cmd.val & 0x80000000) ? CURLUSESSL_TRY :
1170 + (cmd.val ? CURLUSESSL_ALL : CURLUSESSL_NONE)));
1173 + case CURLOPT_NETRC:
1174 + res = curl_easy_setopt(curl, CURLOPT_NETRC, (long)(
1175 + (cmd.val & 0x80000000) ? CURL_NETRC_OPTIONAL :
1176 + (cmd.val ? CURL_NETRC_REQUIRED : CURL_NETRC_IGNORED)));
1179 + case CURLOPT_READDATA:
1182 + err = read_string(in, cmd.val, &data);
1185 + datalen = cmd.val;
1188 + case CURLOPT_COOKIEFILE:
1191 + err = read_string(in, cmd.val, &string);
1194 + res = curl_easy_setopt(curl, CURLOPT_COOKIEFILE, string);
1195 + if (!res && *string)
1196 + res = curl_easy_setopt(curl, CURLOPT_COOKIEJAR, string);
1201 + case CURLGET_VERSION:
1203 + curl_version_info_data *data = curl_version_info(CURLVERSION_NOW);
1204 + res = data->version_num;
1208 + case CURLGET_ERRBUF:
1209 + stringval = errbuf;
1212 + case CURLGET_LASTHDR:
1213 + stringval = hdrbuf;
1216 + case CURLGET_STRERROR:
1217 + stringval = curl_easy_strerror((CURLcode) cmd.val);
1220 + case CURLOPT_HEADERDATA:
1221 + err = send_collect(out, &hdata);
1226 + case CURLOPT_WRITEDATA:
1227 + err = send_collect(out, &wdata);
1232 + case CURLINFO_RESPONSE_CODE:
1235 + res = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response);
1243 + case CURLINFO_REDIRECT_URL:
1244 + case CURLINFO_CONTENT_TYPE:
1247 + res = curl_easy_getinfo(curl, (CURLINFO)opt, &string);
1248 + stringval = res || !string ? "" : string;
1252 + case CURLOPT_MAIL_RCPT:
1256 + curl_slist_free_all(recipients);
1257 + recipients = NULL;
1259 + err = read_string(in, cmd.val, &string);
1264 + const char *nul = memchr(ptr, 0, cmd.val);
1267 + recipients = curl_slist_append(recipients, ptr);
1268 + cmd.val -= ++nul - ptr;
1272 + res = curl_easy_setopt(curl, CURLOPT_MAIL_RCPT, recipients);
1276 + case CURLOPT_HTTPHEADER:
1279 + if (httpheaders) {
1280 + curl_slist_free_all(httpheaders);
1281 + httpheaders = NULL;
1283 + err = read_string(in, cmd.val, &string);
1288 + const char *nul = memchr(ptr, 0, cmd.val);
1291 + httpheaders = curl_slist_append(httpheaders, ptr);
1292 + cmd.val -= ++nul - ptr;
1296 + res = curl_easy_setopt(curl, CURLOPT_HTTPHEADER, httpheaders);
1300 + case CURLCMD_RESET:
1302 + free_collect(&wdata);
1303 + free_collect(&hdata);
1304 + if (httpheaders) {
1305 + curl_slist_free_all(httpheaders);
1306 + httpheaders = NULL;
1309 + curl_slist_free_all(recipients);
1310 + recipients = NULL;
1322 + case CURLCMD_PERFORM:
1325 + hdrline_t lasthdr;
1328 + cxt.len = datalen;
1331 + lasthdr.line = hdrbuf;
1332 + lasthdr.space = sizeof(hdrbuf);
1333 + lasthdr.clt = &hdata;
1335 + free_collect(&wdata);
1336 + free_collect(&hdata);
1337 + res = curl_easy_setopt(curl,
1338 + CURLOPT_ERRORBUFFER, errbuf);
1340 + res = curl_easy_setopt(curl,
1341 + CURLOPT_INFILESIZE, (long)datalen);
1343 + res = curl_easy_setopt(curl,
1344 + CURLOPT_POSTFIELDSIZE, (long)datalen);
1346 + res = curl_easy_setopt(curl,
1347 + CURLOPT_READDATA, &cxt);
1349 + res = curl_easy_setopt(curl,
1350 + CURLOPT_READFUNCTION, (void *)readfunc);
1352 + res = curl_easy_setopt(curl,
1353 + CURLOPT_PROGRESSFUNCTION, (void *)progressfunc);
1355 + res = curl_easy_setopt(curl,
1356 + CURLOPT_XFERINFOFUNCTION, (void *)xferfunc);
1358 + res = curl_easy_setopt(curl,
1359 + CURLOPT_WRITEDATA, &wdata);
1361 + res = curl_easy_setopt(curl,
1362 + CURLOPT_WRITEFUNCTION, (void *)writefunc);
1364 + res = curl_easy_setopt(curl,
1365 + CURLOPT_HEADERDATA, &lasthdr);
1367 + res = curl_easy_setopt(curl,
1368 + CURLOPT_HEADERFUNCTION, (void *)hdrfunc);
1370 + res = curl_easy_perform(curl);
1381 + send_string(out, stringval, strlen(stringval)) :
1382 + send_res(out, res);
1387 + free_collect(&wdata);
1388 + free_collect(&hdata);
1390 + curl_slist_free_all(httpheaders);
1392 + curl_slist_free_all(recipients);
1396 + if (ferror(in) || !feof(in) || ferror(out))
1401 diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
1402 index b3355d2c..03c9ac03 100755
1403 --- a/t/t9001-send-email.sh
1404 +++ b/t/t9001-send-email.sh
1405 @@ -146,6 +146,7 @@ cat >expected-show-all-headers <<\EOF
1406 (mbox) Adding cc: A <author@example.com> from line 'From: A <author@example.com>'
1407 (mbox) Adding cc: One <one@example.com> from line 'Cc: One <one@example.com>, two@example.com'
1408 (mbox) Adding cc: two@example.com from line 'Cc: One <one@example.com>, two@example.com'
1411 Server: relay.example.com
1412 MAIL FROM:<from@example.com>
1413 @@ -164,7 +165,6 @@ Cc: cc@example.com,
1414 Subject: [PATCH 1/1] Second.
1416 Message-Id: MESSAGE-ID-STRING
1417 -X-Mailer: X-MAILER-STRING
1418 In-Reply-To: <unique-message-id@example.com>
1419 References: <unique-message-id@example.com>
1421 @@ -499,6 +499,7 @@ cat >expected-suppress-sob <<\EOF
1422 (mbox) Adding cc: A <author@example.com> from line 'From: A <author@example.com>'
1423 (mbox) Adding cc: One <one@example.com> from line 'Cc: One <one@example.com>, two@example.com'
1424 (mbox) Adding cc: two@example.com from line 'Cc: One <one@example.com>, two@example.com'
1427 Server: relay.example.com
1428 MAIL FROM:<from@example.com>
1429 @@ -516,7 +517,6 @@ Cc: cc@example.com,
1430 Subject: [PATCH 1/1] Second.
1432 Message-Id: MESSAGE-ID-STRING
1433 -X-Mailer: X-MAILER-STRING
1437 @@ -551,6 +551,7 @@ cat >expected-suppress-sob <<\EOF
1438 (mbox) Adding cc: A <author@example.com> from line 'From: A <author@example.com>'
1439 (mbox) Adding cc: One <one@example.com> from line 'Cc: One <one@example.com>, two@example.com'
1440 (mbox) Adding cc: two@example.com from line 'Cc: One <one@example.com>, two@example.com'
1443 Server: relay.example.com
1444 MAIL FROM:<from@example.com>
1445 @@ -566,7 +567,6 @@ Cc: A <author@example.com>,
1446 Subject: [PATCH 1/1] Second.
1448 Message-Id: MESSAGE-ID-STRING
1449 -X-Mailer: X-MAILER-STRING
1453 @@ -584,6 +584,7 @@ cat >expected-suppress-cccmd <<\EOF
1454 (mbox) Adding cc: One <one@example.com> from line 'Cc: One <one@example.com>, two@example.com'
1455 (mbox) Adding cc: two@example.com from line 'Cc: One <one@example.com>, two@example.com'
1456 (body) Adding cc: C O Mitter <committer@example.com> from line 'Signed-off-by: C O Mitter <committer@example.com>'
1459 Server: relay.example.com
1460 MAIL FROM:<from@example.com>
1461 @@ -601,7 +602,6 @@ Cc: A <author@example.com>,
1462 Subject: [PATCH 1/1] Second.
1464 Message-Id: MESSAGE-ID-STRING
1465 -X-Mailer: X-MAILER-STRING
1469 @@ -618,6 +618,7 @@ test_expect_success $PREREQ 'sendemail.cccmd' '
1470 test_expect_success $PREREQ 'setup expect' '
1471 cat >expected-suppress-all <<\EOF
1475 Server: relay.example.com
1476 MAIL FROM:<from@example.com>
1477 @@ -627,7 +628,6 @@ To: to@example.com
1478 Subject: [PATCH 1/1] Second.
1480 Message-Id: MESSAGE-ID-STRING
1481 -X-Mailer: X-MAILER-STRING
1485 @@ -644,6 +644,7 @@ cat >expected-suppress-body <<\EOF
1486 (mbox) Adding cc: One <one@example.com> from line 'Cc: One <one@example.com>, two@example.com'
1487 (mbox) Adding cc: two@example.com from line 'Cc: One <one@example.com>, two@example.com'
1488 (cc-cmd) Adding cc: cc-cmd@example.com from: './cccmd'
1491 Server: relay.example.com
1492 MAIL FROM:<from@example.com>
1493 @@ -661,7 +662,6 @@ Cc: A <author@example.com>,
1494 Subject: [PATCH 1/1] Second.
1496 Message-Id: MESSAGE-ID-STRING
1497 -X-Mailer: X-MAILER-STRING
1501 @@ -677,6 +677,7 @@ cat >expected-suppress-body-cccmd <<\EOF
1502 (mbox) Adding cc: A <author@example.com> from line 'From: A <author@example.com>'
1503 (mbox) Adding cc: One <one@example.com> from line 'Cc: One <one@example.com>, two@example.com'
1504 (mbox) Adding cc: two@example.com from line 'Cc: One <one@example.com>, two@example.com'
1507 Server: relay.example.com
1508 MAIL FROM:<from@example.com>
1509 @@ -692,7 +693,6 @@ Cc: A <author@example.com>,
1510 Subject: [PATCH 1/1] Second.
1512 Message-Id: MESSAGE-ID-STRING
1513 -X-Mailer: X-MAILER-STRING
1517 @@ -708,6 +708,7 @@ cat >expected-suppress-sob <<\EOF
1518 (mbox) Adding cc: A <author@example.com> from line 'From: A <author@example.com>'
1519 (mbox) Adding cc: One <one@example.com> from line 'Cc: One <one@example.com>, two@example.com'
1520 (mbox) Adding cc: two@example.com from line 'Cc: One <one@example.com>, two@example.com'
1523 Server: relay.example.com
1524 MAIL FROM:<from@example.com>
1525 @@ -723,7 +724,6 @@ Cc: A <author@example.com>,
1526 Subject: [PATCH 1/1] Second.
1528 Message-Id: MESSAGE-ID-STRING
1529 -X-Mailer: X-MAILER-STRING
1533 @@ -741,6 +741,7 @@ cat >expected-suppress-bodycc <<\EOF
1534 (mbox) Adding cc: One <one@example.com> from line 'Cc: One <one@example.com>, two@example.com'
1535 (mbox) Adding cc: two@example.com from line 'Cc: One <one@example.com>, two@example.com'
1536 (body) Adding cc: C O Mitter <committer@example.com> from line 'Signed-off-by: C O Mitter <committer@example.com>'
1539 Server: relay.example.com
1540 MAIL FROM:<from@example.com>
1541 @@ -758,7 +759,6 @@ Cc: A <author@example.com>,
1542 Subject: [PATCH 1/1] Second.
1544 Message-Id: MESSAGE-ID-STRING
1545 -X-Mailer: X-MAILER-STRING
1549 @@ -773,6 +773,7 @@ cat >expected-suppress-cc <<\EOF
1551 (mbox) Adding cc: A <author@example.com> from line 'From: A <author@example.com>'
1552 (body) Adding cc: C O Mitter <committer@example.com> from line 'Signed-off-by: C O Mitter <committer@example.com>'
1555 Server: relay.example.com
1556 MAIL FROM:<from@example.com>
1557 @@ -786,7 +787,6 @@ Cc: A <author@example.com>,
1558 Subject: [PATCH 1/1] Second.
1560 Message-Id: MESSAGE-ID-STRING
1561 -X-Mailer: X-MAILER-STRING