patches: more minor updates
[git-osx-installer.git] / patches / km / git-send-email-libcurl.txt
blob110fd2c1a4cb42370f658782a6cf4098c0f10d9a
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
15 rather undesirable.
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>
44 ---
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
54 new file mode 100644
55 index 00000000..aa48792f
56 --- /dev/null
57 +++ b/git-send-email--libcurl.pl
58 @@ -0,0 +1,404 @@
59 +=comment
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.
78 +=cut
80 +package WWW::Curl::Libcurl;
82 +use File::Basename;
83 +use Cwd 'realpath';
84 +use IPC::Open2;
85 +use Exporter 'import';
86 +use vars qw($VERSION @EXPORT);
87 +BEGIN {*VERSION = \1.0}
88 +@EXPORT = qw(
89 +       CURLE_OK
90 +       CURLE_UNSUPPORTED_PROTOCOL
91 +       CURLE_URL_MALFORMAT
92 +       CURLE_COULDNT_RESOLVE_HOST
93 +       CURLE_COULDNT_CONNECT
94 +       CURLE_HTTP_RETURNED_ERROR
95 +       CURLE_UPLOAD_FAILED
96 +       CURLE_OUT_OF_MEMORY
97 +       CURLE_OPERATION_TIMEDOUT
98 +       CURLE_UNKNOWN_OPTION
99 +       CURLE_GOT_NOTHING
100 +       CURLE_LOGIN_DENIED
101 +       CURLE_AGAIN
102 +       CURLE_SSL_PINNEDPUBKEYNOTMATCH
105 +sub CURLE_OK()                         { 0}
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;
123 +sub spawn {
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]);
128 +       my $self = {};
129 +       $self->{'pid'} = $pid;
130 +       $self->{'chldin'} = $chldin;
131 +       $self->{'chldout'} = $chldout;
132 +       return bless $self;
135 +sub DESTROY {
136 +       my $self = shift;
137 +       close($self->{'chldin'});
138 +       waitpid($self->{'pid'}, 0);
139 +       close($self->{'chldout'});
142 +sub _readall {
143 +       my $offset = 0;
144 +       my $remain = $_[2];
145 +       my $result;
146 +       {
147 +               $result = read($_[0], $_[1], $remain, $offset);
148 +               $offset += $result, $remain -= $result, redo
149 +                       if $result && $result < $remain;
150 +       }
151 +       return $result ? $offset + $result : $result;
154 +sub _get_val {
155 +       use bytes;
156 +       my $self = shift;
157 +       my $tag = shift;
158 +       my $val = shift;
159 +       die "Invalid tag: '$tag'\n" unless length($tag) == 4;
160 +       printf {$self->{'chldin'}} "%s", $tag.pack('N',$val);
161 +       my $data;
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);
167 +sub _get_string {
168 +       use bytes;
169 +       my $self = shift;
170 +       my $tag = shift;
171 +       my $val = shift;
172 +       my $strlen = $self->_get_val($tag, $val);
173 +       return "" unless $strlen;
174 +       my $data;
175 +       my $result = _readall($self->{'chldout'}, $data, $strlen);
176 +       die "Read result '$tag' from helper failed\n" unless $result && $result == $strlen;
177 +       return $data;
180 +sub _set_string {
181 +       use bytes;
182 +       my $self = shift;
183 +       my $tag = shift;
184 +       my $str = shift;
185 +       die "Invalid tag: '$tag'\n" unless length($tag) == 4;
186 +       printf {$self->{'chldin'}} "%s", $tag.pack('N',length($str)).$str;
187 +       my $data;
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);
193 +sub version {
194 +       my $self = shift;
195 +       my $ans = $self->_get_val('vers', 0);
196 +       return (($ans >> 16) & 0xff) . '.' . (($ans >> 8) & 0xff) . '.' . ($ans & 0xff);
199 +sub strerror {
200 +       my $self = shift;
201 +       my $code = shift;
202 +       return $self->_get_string('stre', $code);
205 +sub perform {
206 +       my $self = shift;
207 +       return $self->_get_val('prfm', 0);
210 +sub reset {
211 +       my $self = shift;
212 +       return $self->_get_val('rset', 0);
215 +sub errbuf {
216 +       my $self = shift;
217 +       return $self->_get_string('errb', 0);
220 +sub lastheader {
221 +       my $self = shift;
222 +       return $self->_get_string('lsth', 0);
225 +# Note that any gzip/deflate content encoding will be automatically decoded
226 +sub body {
227 +       my $self = shift;
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:
234 +#    HTTP/1.1 200 OK
235 +sub headers {
236 +       my $self = shift;
237 +       my $hdrblob = $self->_get_string('hdta', 0);
238 +       my @ans = ();
239 +       foreach my $hdr (split(/\x00+/, $hdrblob)) {
240 +               $hdr =~ s/\s+$//os;
241 +               push(@ans, $hdr) if $hdr ne '';
242 +       }
243 +       return @ans;
246 +sub getinfo_response_code {
247 +       my $self = shift;
248 +       return $self->_get_val('rspc', 0);
251 +sub getinfo_redirect_url {
252 +       my $self = shift;
253 +       return $self->_get_string('rurl', 0);
256 +sub getinfo_content_type {
257 +       my $self = shift;
258 +       return $self->_get_string('ctyp', 0);
261 +sub setopt_ssl_verifyhost {
262 +       my $self = shift;
263 +       my $bool = shift;
264 +       return $self->_get_val('vhst', $bool ? 1 : 0);
267 +sub setopt_ssl_verifypeer {
268 +       my $self = shift;
269 +       my $bool = shift;
270 +       return $self->_get_val('vper', $bool ? 1 : 0);
273 +sub setopt_verbose {
274 +       my $self = shift;
275 +       my $bool = shift;
276 +       return $self->_get_val('verb', $bool ? 1 : 0);
279 +sub setopt_noprogress {
280 +       my $self = shift;
281 +       my $bool = shift;
282 +       return $self->_get_val('nopg', $bool ? 1 : 0);
285 +sub setopt_port {
286 +       my $self = shift;
287 +       my $val = shift;
288 +       return $self->_set_val('port', $val);
291 +sub setopt_timeout {
292 +       my $self = shift;
293 +       my $val = shift;
294 +       return $self->_set_val('tmot', $val);
297 +sub setopt_httpget {
298 +       my $self = shift;
299 +       my $bool = shift;
300 +       return $self->_get_val('hget', $bool ? 1 : 0);
303 +sub setopt_nobody {
304 +       my $self = shift;
305 +       my $bool = shift;
306 +       return $self->_get_val('nbdy', $bool ? 1 : 0);
309 +sub setopt_post {
310 +       my $self = shift;
311 +       my $bool = shift;
312 +       return $self->_get_val('post', $bool ? 1 : 0);
315 +sub setopt_upload {
316 +       my $self = shift;
317 +       my $bool = shift;
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 {
324 +       my $self = shift;
325 +       my $val = shift;
326 +       return $self->_get_val('ussl', $val);
329 +# Whether to use a ~/.netrc file or not
330 +# 0 = never, 1 = required, -1 = optional fallback
331 +sub setopt_netrc {
332 +       my $self = shift;
333 +       my $val = shift;
334 +       return $self->_get_val('ntrc', $val);
337 +sub setopt_useragent {
338 +       my $self = shift;
339 +       my $val = shift;
340 +       return $self->_set_string('agnt', $val);
343 +sub setopt_proxy {
344 +       my $self = shift;
345 +       my $val = shift;
346 +       return $self->_set_string('prxy', $val);
349 +sub setopt_cainfo {
350 +       my $self = shift;
351 +       my $val = shift;
352 +       return $self->_set_string('cain', $val);
355 +sub setopt_capath {
356 +       my $self = shift;
357 +       my $val = shift;
358 +       return $self->_set_string('capa', $val);
361 +sub setopt_pinnedpublickey {
362 +       my $self = shift;
363 +       my $val = shift;
364 +       return $self->_set_string('pink', $val);
367 +sub setopt_sslcert {
368 +       my $self = shift;
369 +       my $val = shift;
370 +       return $self->_set_string('cert', $val);
373 +sub setopt_sslkey {
374 +       my $self = shift;
375 +       my $val = shift;
376 +       return $self->_set_string('key_', $val);
379 +sub setopt_mail_auth {
380 +       my $self = shift;
381 +       my $val = shift;
382 +       return $self->_set_string('auth', $val);
385 +sub setopt_mail_from {
386 +       my $self = shift;
387 +       my $val = shift;
388 +       return $self->_set_string('from', $val);
391 +sub setopt_username {
392 +       my $self = shift;
393 +       my $val = shift;
394 +       return $self->_set_string('user', $val);
397 +sub setopt_password {
398 +       my $self = shift;
399 +       my $val = shift;
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 {
409 +       my $self = shift;
410 +       my $val = shift;
411 +       return $self->_set_string('mech', $val);
414 +sub setopt_readdata {
415 +       my $self = shift;
416 +       my $val = shift;
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 {
423 +       my $self = shift;
424 +       my $val = shift;
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 {
431 +       my $self = shift;
432 +       my $cmd = shift;
433 +       if (defined($cmd)) {
434 +               $cmd = uc($cmd);
435 +               if ($cmd eq "ALL" || $cmd eq "SESS" || $cmd eq "FLUSH" || $cmd eq "RELOAD") {
436 +                       return $self->_set_string('ckls', $cmd);
437 +               }
438 +       }
439 +       return 0;
442 +sub setopt_url {
443 +       my $self = shift;
444 +       my $val = shift;
445 +       return $self->_set_string('url_', $val);
448 +sub setopt_mail_rcpt {
449 +       use bytes;
450 +       my $self = shift;
451 +       my $strings = join('', map($_.pack('C',0), @_));
452 +       return $self->_set_string('rcpt', $strings);
455 +sub setopt_httpheader {
456 +       use bytes;
457 +       my $self = shift;
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;
468  use Term::ANSIColor;
469  use File::Temp qw/ tempdir tempfile /;
470  use File::Spec::Functions qw(catfile);
471 +use Digest::MD5 qw(md5_hex);
472  use Error qw(:try);
473  use Git;
474 +use File::Basename;
475 +use Cwd 'realpath';
476 +BEGIN {
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]
498  );
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();
512         }
513 -       my $message_id_template = "<%s-%s>";
514 +       our $boot_time;
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 {
524         }
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) {
534 -               return 1;
535 +               return $auth;
536         }
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({
544 +       $auth = {
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
551 -       }, sub {
552 -               my $cred = shift;
554 -               if ($smtp_auth) {
555 -                       my $sasl = Authen::SASL->new(
556 -                               mechanism => $smtp_auth,
557 -                               callback => {
558 -                                       user => $cred->{'username'},
559 -                                       pass => $cred->{'password'},
560 -                                       authname => $cred->{'username'},
561 -                               }
562 -                       );
564 -                       return !!$smtp->auth($sasl);
565 -               }
567 -               return !!$smtp->auth($cred->{'username'}, $cred->{'password'});
568 -       });
570 +       };
571 +       Git::credential($auth, 'fill');
572         return $auth;
575  sub ssl_verify_params {
576 -       eval {
577 -               require IO::Socket::SSL;
578 -               IO::Socket::SSL->import(qw/SSL_VERIFY_PEER SSL_VERIFY_NONE/);
579 -       };
580 -       if ($@) {
581 -               print STDERR "Not using SSL_VERIFY_PEER due to out-of-date IO::Socket::SSL.\n";
582 -               return;
583 -       }
585 -       if (!defined $smtp_ssl_cert_path) {
586 -               # use the OpenSSL defaults
587 -               return (SSL_verify_mode => SSL_VERIFY_PEER());
588 -       }
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);
607         } else {
608                 die "CA path \"$smtp_ssl_cert_path\" does not exist";
609         }
610 @@ -1335,76 +1321,70 @@ Message-Id: $message_id
611                         die "The required SMTP server is not properly defined."
612                 }
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.
621 -                       {
622 -                               no warnings 'once';
623 -                               $IO::Socket::SSL::DEBUG = 1;
624 +               if (!$smtp) {
625 +                       $smtp = WWW::Curl::Libcurl->spawn;
626 +                       if (!$smtp) {
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" : "";
632                         }
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);
643 +                       } else {
644 +                               $smtp->setopt_noprogress(1);
645 +                       }
646 +                       $smtp->setopt_upload(1);
647 +               } else {
648 +                       # attempt to avoid throttling issues when sending multiple messages
649 +                       sleep(2);
650                 }
651 -               else {
652 -                       require Net::SMTP;
653 -                       $smtp_domain ||= maildomain();
654 +               my $scheme;
655 +               if ($smtp_encryption eq 'ssl') {
656 +                       $smtp_server_port ||= 465; # ssmtp
657 +                       $scheme = 'smtps';
658 +                       ssl_verify_params();
659 +               } else {
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');
668 -                               $smtp->response();
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);
677 -                               } else {
678 -                                       die "Server does not support STARTTLS! ".$smtp->message;
679 +                       $scheme = 'smtp';
680 +                       $smtp->setopt_use_ssl($smtp_encryption eq 'tls' ? 1 : -1);
681 +               }
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);
687 +               smtp_auth_maybe;
688 +               if ($auth && !$auth->{'finished_auth'}) {
689 +                       $smtp->setopt_username($auth->{'username'});
690 +                       $smtp->setopt_password($auth->{'password'});
691 +                       if ($smtp_auth) {
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}'";
695                                 }
696                         }
697                 }
699 -               if (!$smtp) {
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" : "";
705 -               }
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');
724 +                       }
725 +                       $auth->{'finished_auth'} = 1;
726                 }
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"
730 +                       if $retcode;
731         }
732         if ($quiet) {
733                 printf (($dry_run ? "Dry-" : "")."Sent %s\n", $subject);
734         } else {
735 +               print "\n";
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
740                 }
741                 print $header, "\n";
742                 if ($smtp) {
743 -                       print "Result: ", $smtp->code, ' ',
744 -                               ($smtp->message =~ /\n([^\n]+\n)$/s), "\n";
745 +                       print "Result: ", $smtp->lastheader, "\n";
746                 } else {
747                         print "Result: OK\n";
748                 }
749 @@ -1676,7 +1655,7 @@ sub cleanup_compose_files {
750         unlink($compose_filename, $compose_filename . ".final") if $compose;
753 -$smtp->quit if $smtp;
754 +$smtp = undef;
756  sub apply_transfer_encoding {
757         my $message = shift;
758 diff --git a/send-email--libcurl.c b/send-email--libcurl.c
759 new file mode 100644
760 index 00000000..dd8ebbb7
761 --- /dev/null
762 +++ b/send-email--libcurl.c
763 @@ -0,0 +1,637 @@
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.
785 +#include <stdint.h>
786 +#include <stdio.h>
787 +#include <stdlib.h>
788 +#include <string.h>
789 +#include <arpa/inet.h>
790 +#include <curl/curl.h>
792 +typedef struct {
793 +       char tag[4];
794 +       uint32_t val;
795 +} cmd_t;
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[])
807 +       int result;
808 +       CURL *curl;
809 +       FILE *inbinary = freopen(NULL, "rb", stdin);;
810 +       FILE *outbinary = freopen(NULL, "ab", stdout);
811 +       if (!inbinary || !outbinary || argc != 2 || strcmp(argv[1], "--spawn"))
812 +               return 2;
814 +       curl = curl_easy_init();
815 +       if (!curl)
816 +               return 3;
817 +       reset_curl(curl);
819 +       result = process_cmds(inbinary, outbinary, curl);
820 +       curl_easy_cleanup(curl);
821 +       return result;
824 +static int read_string(FILE *in, size_t len, char **out)
826 +       char *string = (char *)malloc(len + 1);
827 +       if (!string)
828 +               return 10;
829 +       if (len && !fread(string, len, 1, in)) {
830 +               free(string);
831 +               return 11;
832 +       }
833 +       string[len] = '\0';
834 +       *out = string;
835 +       return 0;
838 +static int handle_string_opt(CURL *curl, CURLoption o, size_t l, FILE *in, int *err)
840 +       int result;
841 +       char *string;
842 +       *err = read_string(in, l, &string);
843 +       if (*err)
844 +               return 1000000 + *err;
845 +       result = curl_easy_setopt(curl, o, string);
846 +       free(string);
847 +       return result;
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)
855 +               return 12;
856 +       if (!noflush)
857 +               fflush(out);
858 +       return 0;
861 +static int send_string(FILE *out, const void *p, size_t l)
863 +       int err = send_res_ex(out, (CURLcode)l, 1);
864 +       if (err)
865 +               return err;
866 +       if (l && fwrite(p, l, 1, out) != 1)
867 +               return 12;
868 +       fflush(out);
869 +       return 0;
872 +typedef struct {
873 +       const char *data;
874 +       size_t len;
875 +       size_t offset;
876 +} context_t;
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;
884 +       if (max) {
885 +               memcpy(ptr, cxt->data + cxt->offset, max);
886 +               cxt->offset += max;
887 +       }
888 +       return max;
891 +struct collect_s;
892 +static int append_collect(struct collect_s *clt, const void *p, size_t s);
894 +typedef struct {
895 +       char *line;
896 +       size_t space;
897 +       struct collect_s *clt;
898 +} hdrline_t;
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;
904 +       size_t len = max;
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'))
909 +               --len;
910 +       cxt->line[len] = '\0';
911 +       len = append_collect(cxt->clt, ptr, max);
912 +       if (!len || len != max || append_collect(cxt->clt, "", 1) != 1)
913 +               return 0;
914 +       return max;
917 +static int progressfunc(void *p, double t, double n, double u, double x)
919 +       (void)p;
920 +       (void)t;
921 +       (void)n;
922 +       (void)u;
923 +       (void)x;
924 +       return 0;
927 +static int xferfunc(void *p, curl_off_t t, curl_off_t n, curl_off_t u, curl_off_t x)
929 +       (void)p;
930 +       (void)t;
931 +       (void)n;
932 +       (void)u;
933 +       (void)x;
934 +       return 0;
937 +#define BLOCK_SIZE 32736
938 +typedef struct block_s {
939 +       struct block_s *next;
940 +       size_t offset;
941 +       char data[BLOCK_SIZE];
942 +} block_t;
944 +typedef struct collect_s {
945 +       block_t *first, *last;
946 +       size_t total;
947 +} collect_t;
949 +static void free_collect(collect_t *clt)
951 +       block_t *block = clt->first;
952 +       while (block) {
953 +               block_t *link = block->next;
954 +               free(block);
955 +               block = link;
956 +       }
957 +       clt->first = NULL;
958 +       clt->last = NULL;
959 +       clt->total = 0;
962 +static int append_collect(collect_t *clt, const void *p, size_t s)
964 +       size_t left;
965 +       const char *ptr;
967 +       if (!p || !s)
968 +               return 0;
970 +       ptr = (const char *)p;
971 +       left = s;
972 +       while (left) {
973 +               size_t cpyamt;
974 +               if (!clt->first) {
975 +                       clt->first = (block_t *)malloc(sizeof(block_t));
976 +                       if (!clt->first)
977 +                               return 0;
978 +                       clt->first->next = NULL;
979 +                       clt->first->offset = 0;
980 +                       clt->last = clt->first;
981 +               }
982 +               if (clt->last->offset >= BLOCK_SIZE) {
983 +                       block_t *next = (block_t *)malloc(sizeof(block_t));
984 +                       if (!next)
985 +                               return 0;
986 +                       next->next = NULL;
987 +                       next->offset = 0;
988 +                       clt->last->next = next;
989 +                       clt->last = next;
990 +               }
991 +               cpyamt = BLOCK_SIZE - clt->last->offset;
992 +               if (cpyamt > left)
993 +                       cpyamt = left;
994 +               memcpy(clt->last->data + clt->last->offset, ptr, cpyamt);
995 +               clt->last->offset += cpyamt;
996 +               clt->total += cpyamt;
997 +               ptr += cpyamt;
998 +               left -= cpyamt;
999 +       }
1001 +       return s;
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);
1013 +       if (err)
1014 +               return err;
1015 +       while (block) {
1016 +               if (block->offset) {
1017 +                       if (fwrite(block->data, block->offset, 1, out) != 1)
1018 +                               return 12;
1019 +               }
1020 +               block = block->next;
1021 +       }
1022 +       fflush(out);
1023 +       return 0;
1026 +enum {
1027 +       CURLCMD_PERFORM = -1000,
1028 +       CURLGET_ERRBUF,
1029 +       CURLGET_LASTHDR,
1030 +       CURLGET_STRERROR,
1031 +       CURLCMD_RESET,
1032 +       CURLGET_VERSION
1035 +typedef struct {
1036 +       char tag[4];
1037 +       int opt;
1038 +} cmd_lookup_t;
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)
1105 +       cmd_t cmd;
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));
1116 +       errbuf[0] = '\0';
1117 +       hdrbuf[0] = '\0';
1118 +       while (!ferror(in) && !ferror(out) && fread(&cmd, sizeof(cmd), 1, in)) {
1119 +               char *string;
1120 +               int err = 0;
1121 +               CURLcode res = 0;
1122 +               int opt = find_command(cmd.tag);
1123 +               const char *stringval = NULL;
1125 +               cmd.val = ntohl(cmd.val);
1126 +               switch (opt) {
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);
1137 +                       break;
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:
1145 +               case CURLOPT_URL:
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);
1155 +                       break;
1157 +               case CURLOPT_PORT:
1158 +               case CURLOPT_TIMEOUT:
1159 +                       res = curl_easy_setopt(curl, (CURLoption)opt, (long)cmd.val);
1160 +                       break;
1162 +               case CURLOPT_SSL_VERIFYHOST:
1163 +                       res = curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST,
1164 +                                               cmd.val ? 2L : 0L);
1165 +                       break;
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)));
1171 +                       break;
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)));
1177 +                       break;
1179 +               case CURLOPT_READDATA:
1180 +                       if (data)
1181 +                               free(data);
1182 +                       err = read_string(in, cmd.val, &data);
1183 +                       if (err)
1184 +                               break;
1185 +                       datalen = cmd.val;
1186 +                       break;
1188 +               case CURLOPT_COOKIEFILE:
1189 +               {
1190 +                       char *string;
1191 +                       err = read_string(in, cmd.val, &string);
1192 +                       if (err)
1193 +                               break;
1194 +                       res = curl_easy_setopt(curl, CURLOPT_COOKIEFILE, string);
1195 +                       if (!res && *string)
1196 +                               res = curl_easy_setopt(curl, CURLOPT_COOKIEJAR, string);
1197 +                       free(string);
1198 +                       break;
1199 +               }
1201 +               case CURLGET_VERSION:
1202 +               {
1203 +                       curl_version_info_data *data = curl_version_info(CURLVERSION_NOW);
1204 +                       res = data->version_num;
1205 +                       break;
1206 +               }
1208 +               case CURLGET_ERRBUF:
1209 +                       stringval = errbuf;
1210 +                       break;
1212 +               case CURLGET_LASTHDR:
1213 +                       stringval = hdrbuf;
1214 +                       break;
1216 +               case CURLGET_STRERROR:
1217 +                       stringval = curl_easy_strerror((CURLcode) cmd.val);
1218 +                       break;
1220 +               case CURLOPT_HEADERDATA:
1221 +                       err = send_collect(out, &hdata);
1222 +                       if (!err)
1223 +                               continue;
1224 +                       break;
1226 +               case CURLOPT_WRITEDATA:
1227 +                       err = send_collect(out, &wdata);
1228 +                       if (!err)
1229 +                               continue;
1230 +                       break;
1232 +               case CURLINFO_RESPONSE_CODE:
1233 +               {
1234 +                       long response;
1235 +                       res = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response);
1236 +                       if (res)
1237 +                         res += 1000000;
1238 +                       else
1239 +                         res = response;
1240 +                       break;
1241 +               }
1243 +               case CURLINFO_REDIRECT_URL:
1244 +               case CURLINFO_CONTENT_TYPE:
1245 +               {
1246 +                       char *string;
1247 +                       res = curl_easy_getinfo(curl, (CURLINFO)opt, &string);
1248 +                       stringval = res || !string ? "" : string;
1249 +                       break;
1250 +               }
1252 +               case CURLOPT_MAIL_RCPT:
1253 +               {
1254 +                       const char *ptr;
1255 +                       if (recipients) {
1256 +                               curl_slist_free_all(recipients);
1257 +                               recipients = NULL;
1258 +                       }
1259 +                       err = read_string(in, cmd.val, &string);
1260 +                       if (err)
1261 +                               break;
1262 +                       ptr = string;
1263 +                       while (cmd.val) {
1264 +                               const char *nul = memchr(ptr, 0, cmd.val);
1265 +                               if (!nul)
1266 +                                       return 13;
1267 +                               recipients = curl_slist_append(recipients, ptr);
1268 +                               cmd.val -= ++nul - ptr;
1269 +                               ptr = nul;
1270 +                       }
1271 +                       free(string);
1272 +                       res = curl_easy_setopt(curl, CURLOPT_MAIL_RCPT, recipients);
1273 +                       break;
1274 +               }
1276 +               case CURLOPT_HTTPHEADER:
1277 +               {
1278 +                       const char *ptr;
1279 +                       if (httpheaders) {
1280 +                               curl_slist_free_all(httpheaders);
1281 +                               httpheaders = NULL;
1282 +                       }
1283 +                       err = read_string(in, cmd.val, &string);
1284 +                       if (err)
1285 +                               break;
1286 +                       ptr = string;
1287 +                       while (cmd.val) {
1288 +                               const char *nul = memchr(ptr, 0, cmd.val);
1289 +                               if (!nul)
1290 +                                       return 13;
1291 +                               httpheaders = curl_slist_append(httpheaders, ptr);
1292 +                               cmd.val -= ++nul - ptr;
1293 +                               ptr = nul;
1294 +                       }
1295 +                       free(string);
1296 +                       res = curl_easy_setopt(curl, CURLOPT_HTTPHEADER, httpheaders);
1297 +                       break;
1298 +               }
1300 +               case CURLCMD_RESET:
1301 +               {
1302 +                       free_collect(&wdata);
1303 +                       free_collect(&hdata);
1304 +                       if (httpheaders) {
1305 +                               curl_slist_free_all(httpheaders);
1306 +                               httpheaders = NULL;
1307 +                       }
1308 +                       if (recipients) {
1309 +                               curl_slist_free_all(recipients);
1310 +                               recipients = NULL;
1311 +                       }
1312 +                       if (data) {
1313 +                               free(data);
1314 +                               data = NULL;
1315 +                               datalen = 0;
1316 +                       }
1318 +                       reset_curl(curl);
1319 +                       break;
1320 +               }
1322 +               case CURLCMD_PERFORM:
1323 +               {
1324 +                       context_t cxt;
1325 +                       hdrline_t lasthdr;
1327 +                       cxt.data = data;
1328 +                       cxt.len = datalen;
1329 +                       cxt.offset = 0;
1330 +                       errbuf[0] = '\0';
1331 +                       lasthdr.line = hdrbuf;
1332 +                       lasthdr.space = sizeof(hdrbuf);
1333 +                       lasthdr.clt = &hdata;
1334 +                       hdrbuf[0] = '\0';
1335 +                       free_collect(&wdata);
1336 +                       free_collect(&hdata);
1337 +                       res = curl_easy_setopt(curl,
1338 +                                       CURLOPT_ERRORBUFFER, errbuf);
1339 +                       if (!res)
1340 +                               res = curl_easy_setopt(curl,
1341 +                                       CURLOPT_INFILESIZE, (long)datalen);
1342 +                       if (!res)
1343 +                               res = curl_easy_setopt(curl,
1344 +                                       CURLOPT_POSTFIELDSIZE, (long)datalen);
1345 +                       if (!res)
1346 +                               res = curl_easy_setopt(curl,
1347 +                                       CURLOPT_READDATA, &cxt);
1348 +                       if (!res)
1349 +                               res = curl_easy_setopt(curl,
1350 +                                       CURLOPT_READFUNCTION, (void *)readfunc);
1351 +                       if (!res)
1352 +                               res = curl_easy_setopt(curl,
1353 +                                       CURLOPT_PROGRESSFUNCTION, (void *)progressfunc);
1354 +                       if (!res)
1355 +                               res = curl_easy_setopt(curl,
1356 +                                       CURLOPT_XFERINFOFUNCTION, (void *)xferfunc);
1357 +                       if (!res)
1358 +                               res = curl_easy_setopt(curl,
1359 +                                       CURLOPT_WRITEDATA, &wdata);
1360 +                       if (!res)
1361 +                               res = curl_easy_setopt(curl,
1362 +                                       CURLOPT_WRITEFUNCTION, (void *)writefunc);
1363 +                       if (!res)
1364 +                               res = curl_easy_setopt(curl,
1365 +                                       CURLOPT_HEADERDATA, &lasthdr);
1366 +                       if (!res)
1367 +                               res = curl_easy_setopt(curl,
1368 +                                       CURLOPT_HEADERFUNCTION, (void *)hdrfunc);
1369 +                       if (!res)
1370 +                               res = curl_easy_perform(curl);
1371 +                       break;
1372 +               }
1374 +               default:
1375 +                       err = 20;
1376 +                       break;
1378 +               }
1379 +               if (!err)
1380 +                       err = stringval ?
1381 +                               send_string(out, stringval, strlen(stringval)) :
1382 +                               send_res(out, res);
1383 +               if (err)
1384 +                       return err;
1385 +       }
1387 +       free_collect(&wdata);
1388 +       free_collect(&hdata);
1389 +       if (httpheaders)
1390 +               curl_slist_free_all(httpheaders);
1391 +       if (recipients)
1392 +               curl_slist_free_all(recipients);
1393 +       if (data)
1394 +               free(data);
1396 +       if (ferror(in) || !feof(in) || ferror(out))
1397 +               return 4;
1399 +       return 0;
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'
1410  Dry-OK. Log says:
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.
1415  Date: DATE-STRING
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'
1426  Dry-OK. Log says:
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.
1431  Date: DATE-STRING
1432  Message-Id: MESSAGE-ID-STRING
1433 -X-Mailer: X-MAILER-STRING
1435  Result: OK
1436  EOF
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'
1442  Dry-OK. Log says:
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.
1447  Date: DATE-STRING
1448  Message-Id: MESSAGE-ID-STRING
1449 -X-Mailer: X-MAILER-STRING
1451  Result: OK
1452  EOF
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>'
1458  Dry-OK. Log says:
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.
1463  Date: DATE-STRING
1464  Message-Id: MESSAGE-ID-STRING
1465 -X-Mailer: X-MAILER-STRING
1467  Result: OK
1468  EOF
1469 @@ -618,6 +618,7 @@ test_expect_success $PREREQ 'sendemail.cccmd' '
1470  test_expect_success $PREREQ 'setup expect' '
1471  cat >expected-suppress-all <<\EOF
1472  0001-Second.patch
1474  Dry-OK. Log says:
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.
1479  Date: DATE-STRING
1480  Message-Id: MESSAGE-ID-STRING
1481 -X-Mailer: X-MAILER-STRING
1483  Result: OK
1484  EOF
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'
1490  Dry-OK. Log says:
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.
1495  Date: DATE-STRING
1496  Message-Id: MESSAGE-ID-STRING
1497 -X-Mailer: X-MAILER-STRING
1499  Result: OK
1500  EOF
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'
1506  Dry-OK. Log says:
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.
1511  Date: DATE-STRING
1512  Message-Id: MESSAGE-ID-STRING
1513 -X-Mailer: X-MAILER-STRING
1515  Result: OK
1516  EOF
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'
1522  Dry-OK. Log says:
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.
1527  Date: DATE-STRING
1528  Message-Id: MESSAGE-ID-STRING
1529 -X-Mailer: X-MAILER-STRING
1531  Result: OK
1532  EOF
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>'
1538  Dry-OK. Log says:
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.
1543  Date: DATE-STRING
1544  Message-Id: MESSAGE-ID-STRING
1545 -X-Mailer: X-MAILER-STRING
1547  Result: OK
1548  EOF
1549 @@ -773,6 +773,7 @@ cat >expected-suppress-cc <<\EOF
1550  0001-Second.patch
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>'
1554  Dry-OK. Log says:
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.
1559  Date: DATE-STRING
1560  Message-Id: MESSAGE-ID-STRING
1561 -X-Mailer: X-MAILER-STRING
1563  Result: OK
1564  EOF