patches: more updates
[git-osx-installer.git] / patches / km / git-send-email-libcurl.txt
blob28c92d1a938a8fe68c4df6636ac279f4eb3a4b3c
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        | 162 ++++++------
47  send-email--libcurl.c      | 637 +++++++++++++++++++++++++++++++++++++++++++++
48  t/t9001-send-email.sh      |  20 +-
49  4 files changed, 1129 insertions(+), 94 deletions(-)
50  create mode 100644 git-send-email--libcurl.pl
51  create mode 100644 send-email--libcurl.c
53 diff --git a/git-send-email--libcurl.pl b/git-send-email--libcurl.pl
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 e1e9b146..e6b4829b 100755
465 --- a/git-send-email.perl
466 +++ b/git-send-email.perl
467 @@ -26,8 +26,15 @@ use Data::Dumper;
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 @@ -54,7 +61,7 @@ git send-email [options] <file | directory | rev-list options >
484      --[no-]bcc              <str>  * Email Bcc:
485      --subject               <str>  * Email "Subject:"
486      --in-reply-to           <str>  * Email "In-Reply-To:"
487 -    --[no-]xmailer                 * Add "X-Mailer:" header (default).
488 +    --[no-]xmailer                 * Add "X-Mailer:" header (default is off).
489      --[no-]annotate                * Review each patch that will be sent in an editor.
490      --compose                      * Open an editor for introduction.
491      --compose-encoding      <str>  * Encoding to assume for introduction.
492 @@ -228,7 +235,7 @@ my %config_bool_settings = (
493      "validate" => [\$validate, 1],
494      "multiedit" => [\$multiedit, undef],
495      "annotate" => [\$annotate, undef],
496 -    "xmailer" => [\$use_xmailer, 1]
497 +    "xmailer" => [\$use_xmailer, 0]
498  );
500  my %config_settings = (
501 @@ -304,6 +311,7 @@ my $rc = GetOptions("h" => \$help,
502                     "smtp-server-option=s" => \@smtp_server_options,
503                     "smtp-server-port=s" => \$smtp_server_port,
504                     "smtp-user=s" => \$smtp_authuser,
505 +                   "no-smtp-user" => sub {delete $config_settings{'smtpuser'}},
506                     "smtp-pass:s" => \$smtp_authpass,
507                     "smtp-ssl" => sub { $smtp_encryption = 'ssl' },
508                     "smtp-encryption=s" => \$smtp_encryption,
509 @@ -925,8 +933,10 @@ sub make_message_id {
510                 require Sys::Hostname;
511                 $du_part = 'user@' . Sys::Hostname::hostname();
512         }
513 -       my $message_id_template = "<%s-git-send-email-%s>";
514 +       my $message_id_template = "%s-git-send-email-%s";
515         $message_id = sprintf($message_id_template, $uniq, $du_part);
516 +       @_ = split /@/, $message_id;
517 +       $message_id = '<'.substr(md5_hex($_[0]),0,31).'@'.substr(md5_hex($_[1]),1,31).'>';
518         #print "new message id = $message_id\n"; # Was useful for debugging
521 @@ -1090,12 +1100,12 @@ sub smtp_host_string {
522         }
525 -# Returns 1 if authentication succeeded or was not necessary
526 -# (smtp_user was not specified), and 0 otherwise.
527 +# Returns a filled hashref from Git::credential fill if authentication
528 +# has been requested, undef otherwise.
530  sub smtp_auth_maybe {
531         if (!defined $smtp_authuser || $auth) {
532 -               return 1;
533 +               return $auth;
534         }
536         # Workaround AUTH PLAIN/LOGIN interaction defect
537 @@ -1108,47 +1118,36 @@ sub smtp_auth_maybe {
538         # TODO: Authentication may fail not because credentials were
539         # invalid but due to other reasons, in which we should not
540         # reject credentials.
541 -       $auth = Git::credential({
542 +       $auth = {
543                 'protocol' => 'smtp',
544                 'host' => smtp_host_string(),
545                 'username' => $smtp_authuser,
546                 # if there's no password, "git credential fill" will
547                 # give us one, otherwise it'll just pass this one.
548                 'password' => $smtp_authpass
549 -       }, sub {
550 -               my $cred = shift;
551 -               return !!$smtp->auth($cred->{'username'}, $cred->{'password'});
552 -       });
554 +       };
555 +       Git::credential($auth, 'fill');
556         return $auth;
559  sub ssl_verify_params {
560 -       eval {
561 -               require IO::Socket::SSL;
562 -               IO::Socket::SSL->import(qw/SSL_VERIFY_PEER SSL_VERIFY_NONE/);
563 -       };
564 -       if ($@) {
565 -               print STDERR "Not using SSL_VERIFY_PEER due to out-of-date IO::Socket::SSL.\n";
566 -               return;
567 -       }
569 -       if (!defined $smtp_ssl_cert_path) {
570 -               # use the OpenSSL defaults
571 -               return (SSL_verify_mode => SSL_VERIFY_PEER());
572 -       }
573 +       return unless defined $smtp_ssl_cert_path; # use defaults
575         if ($smtp_ssl_cert_path eq "") {
576 -               return (SSL_verify_mode => SSL_VERIFY_NONE());
577 +               $smtp->setopt_ssl_verifyhost(0);
578 +               $smtp->setopt_ssl_verifypeer(0);
579         } elsif (-d $smtp_ssl_cert_path) {
580 -               return (SSL_verify_mode => SSL_VERIFY_PEER(),
581 -                       SSL_ca_path => $smtp_ssl_cert_path);
582 +               #$smtp->setopt_capath($smtp_ssl_cert_path);
583 +               die "SecureTransport does not support a CA directory, use a CA file instead.\n";
584         } elsif (-f $smtp_ssl_cert_path) {
585 -               return (SSL_verify_mode => SSL_VERIFY_PEER(),
586 -                       SSL_ca_file => $smtp_ssl_cert_path);
587 +               # These are the default values
588 +               #$smtp->setopt_ssl_verifyhost(1);
589 +               #$smtp->setopt_ssl_verifypeer(1);
590 +               $smtp->setopt_cainfo($smtp_ssl_cert_path);
591         } else {
592 -               print STDERR "Not using SSL_VERIFY_PEER because the CA path does not exist.\n";
593 -               return (SSL_verify_mode => SSL_VERIFY_NONE());
594 +               print STDERR "Not using SSL_VERIFYPEER because the CA path does not exist.\n";
595 +               $smtp->setopt_ssl_verifyhost(0);
596 +               $smtp->setopt_ssl_verifypeer(0);
597         }
600 @@ -1264,65 +1263,61 @@ Message-Id: $message_id
601                         die "The required SMTP server is not properly defined."
602                 }
604 +               if (!$smtp) {
605 +                       $smtp = WWW::Curl::Libcurl->spawn;
606 +                       if (!$smtp) {
607 +                               die "Unable to initialize SMTP properly. Check config and use --smtp-debug. ",
608 +                                   "VALUES: server=$smtp_server ",
609 +                                   "encryption=$smtp_encryption ",
610 +                                   "hello=$smtp_domain",
611 +                                   defined $smtp_server_port ? " port=$smtp_server_port" : "";
612 +                       }
613 +                       if ($debug_net_smtp || $ENV{'GIT_CURL_VERBOSE'}) {
614 +                               $smtp->setopt_verbose(1);
615 +                       } else {
616 +                               $smtp->setopt_noprogress(1);
617 +                       }
618 +                       $smtp->setopt_upload(1);
619 +               }
620 +               my $scheme;
621                 if ($smtp_encryption eq 'ssl') {
622                         $smtp_server_port ||= 465; # ssmtp
623 -                       require Net::SMTP::SSL;
624 -                       $smtp_domain ||= maildomain();
625 -                       require IO::Socket::SSL;
626 -                       # Net::SMTP::SSL->new() does not forward any SSL options
627 -                       IO::Socket::SSL::set_client_defaults(
628 -                               ssl_verify_params());
629 -                       $smtp ||= Net::SMTP::SSL->new($smtp_server,
630 -                                                     Hello => $smtp_domain,
631 -                                                     Port => $smtp_server_port,
632 -                                                     Debug => $debug_net_smtp);
633 -               }
634 -               else {
635 -                       require Net::SMTP;
636 -                       $smtp_domain ||= maildomain();
637 +                       $scheme = 'smtps';
638 +                       ssl_verify_params();
639 +               } else {
640                         $smtp_server_port ||= 25;
641 -                       $smtp ||= Net::SMTP->new($smtp_server,
642 -                                                Hello => $smtp_domain,
643 -                                                Debug => $debug_net_smtp,
644 -                                                Port => $smtp_server_port);
645 -                       if ($smtp_encryption eq 'tls' && $smtp) {
646 -                               require Net::SMTP::SSL;
647 -                               $smtp->command('STARTTLS');
648 -                               $smtp->response();
649 -                               if ($smtp->code == 220) {
650 -                                       $smtp = Net::SMTP::SSL->start_SSL($smtp,
651 -                                                                         ssl_verify_params())
652 -                                               or die "STARTTLS failed! ".IO::Socket::SSL::errstr();
653 -                                       $smtp_encryption = '';
654 -                                       # Send EHLO again to receive fresh
655 -                                       # supported commands
656 -                                       $smtp->hello($smtp_domain);
657 -                               } else {
658 -                                       die "Server does not support STARTTLS! ".$smtp->message;
659 -                               }
660 -                       }
661 +                       $scheme = 'smtp';
662 +                       $smtp->setopt_use_ssl($smtp_encryption eq 'tls' ? 1 : -1);
663                 }
665 -               if (!$smtp) {
666 -                       die "Unable to initialize SMTP properly. Check config and use --smtp-debug. ",
667 -                           "VALUES: server=$smtp_server ",
668 -                           "encryption=$smtp_encryption ",
669 -                           "hello=$smtp_domain",
670 -                           defined $smtp_server_port ? " port=$smtp_server_port" : "";
671 +               $smtp_domain ||= maildomain();
672 +               $smtp->setopt_url("$scheme://$smtp_server:$smtp_server_port/$smtp_domain");
673 +               $smtp->setopt_mail_from($raw_from);
674 +               $smtp->setopt_mail_rcpt(@recipients);
676 +               smtp_auth_maybe;
677 +               if ($auth && !$auth->{'finished_auth'}) {
678 +                       $smtp->setopt_username($auth->{'username'});
679 +                       $smtp->setopt_password($auth->{'password'});
680                 }
682 -               smtp_auth_maybe or die $smtp->message;
684 -               $smtp->mail( $raw_from ) or die $smtp->message;
685 -               $smtp->to( @recipients ) or die $smtp->message;
686 -               $smtp->data or die $smtp->message;
687 -               $smtp->datasend("$header\n$message") or die $smtp->message;
688 -               $smtp->dataend() or die $smtp->message;
689 -               $smtp->code =~ /250|200/ or die "Failed to send $subject\n".$smtp->message;
690 +               my $payload = join("\r\n", split(/\n/, "$header\n$message", -1));
691 +               $smtp->setopt_readdata($payload);
692 +               my $retcode = $smtp->perform;
693 +               if ($auth && !$auth->{'finished_auth'}) {
694 +                       if ($retcode == CURLE_LOGIN_DENIED) {
695 +                               Git::credential($auth, 'reject');
696 +                       } elsif (!$retcode) {
697 +                               Git::credential($auth, 'approve');
698 +                       }
699 +                       $auth->{'finished_auth'} = 1;
700 +               }
701 +               die "Failed to send $subject\nError ($retcode) ".$smtp->strerror($retcode).": ".$smtp->errbuf, "\n"
702 +                       if $retcode;
703         }
704         if ($quiet) {
705                 printf (($dry_run ? "Dry-" : "")."Sent %s\n", $subject);
706         } else {
707 +               print "\n";
708                 print (($dry_run ? "Dry-" : "")."OK. Log says:\n");
709                 if (!file_name_is_absolute($smtp_server)) {
710                         print "Server: $smtp_server\n";
711 @@ -1335,8 +1330,7 @@ Message-Id: $message_id
712                 }
713                 print $header, "\n";
714                 if ($smtp) {
715 -                       print "Result: ", $smtp->code, ' ',
716 -                               ($smtp->message =~ /\n([^\n]+\n)$/s), "\n";
717 +                       print "Result: ", $smtp->lastheader, "\n";
718                 } else {
719                         print "Result: OK\n";
720                 }
721 @@ -1594,7 +1588,7 @@ sub cleanup_compose_files {
722         unlink($compose_filename, $compose_filename . ".final") if $compose;
725 -$smtp->quit if $smtp;
726 +$smtp = undef;
728  sub apply_transfer_encoding {
729         my $message = shift;
730 diff --git a/send-email--libcurl.c b/send-email--libcurl.c
731 new file mode 100644
732 index 00000000..dd8ebbb7
733 --- /dev/null
734 +++ b/send-email--libcurl.c
735 @@ -0,0 +1,637 @@
738 +send-email--libcurl.c -- libcURL helper for git-send-email.perl
739 +Copyright (C) 2014,2015 Kyle J. McKay.  All rights reserved.
741 +This program is free software; you can redistribute it and/or
742 +modify it under the terms of the GNU General Public License
743 +as published by the Free Software Foundation; either version 2
744 +of the License, or (at your option) any later version.
746 +This program is distributed in the hope that it will be useful,
747 +but WITHOUT ANY WARRANTY; without even the implied warranty of
748 +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
749 +GNU General Public License for more details.
751 +You should have received a copy of the GNU General Public License
752 +along with this program; if not, write to the Free Software
753 +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
757 +#include <stdint.h>
758 +#include <stdio.h>
759 +#include <stdlib.h>
760 +#include <string.h>
761 +#include <arpa/inet.h>
762 +#include <curl/curl.h>
764 +typedef struct {
765 +       char tag[4];
766 +       uint32_t val;
767 +} cmd_t;
769 +static int process_cmds(FILE *in, FILE *out, CURL *curl);
771 +static void reset_curl(CURL *curl)
773 +       curl_easy_reset(curl);
774 +       curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");
777 +int main(int argc, char *const argv[])
779 +       int result;
780 +       CURL *curl;
781 +       FILE *inbinary = freopen(NULL, "rb", stdin);;
782 +       FILE *outbinary = freopen(NULL, "ab", stdout);
783 +       if (!inbinary || !outbinary || argc != 2 || strcmp(argv[1], "--spawn"))
784 +               return 2;
786 +       curl = curl_easy_init();
787 +       if (!curl)
788 +               return 3;
789 +       reset_curl(curl);
791 +       result = process_cmds(inbinary, outbinary, curl);
792 +       curl_easy_cleanup(curl);
793 +       return result;
796 +static int read_string(FILE *in, size_t len, char **out)
798 +       char *string = (char *)malloc(len + 1);
799 +       if (!string)
800 +               return 10;
801 +       if (len && !fread(string, len, 1, in)) {
802 +               free(string);
803 +               return 11;
804 +       }
805 +       string[len] = '\0';
806 +       *out = string;
807 +       return 0;
810 +static int handle_string_opt(CURL *curl, CURLoption o, size_t l, FILE *in, int *err)
812 +       int result;
813 +       char *string;
814 +       *err = read_string(in, l, &string);
815 +       if (*err)
816 +               return 1000000 + *err;
817 +       result = curl_easy_setopt(curl, o, string);
818 +       free(string);
819 +       return result;
822 +#define send_res(f,r) send_res_ex((f),(r),(0))
823 +static int send_res_ex(FILE *out, CURLcode res, int noflush)
825 +       uint32_t e = htonl((uint32_t)res);
826 +       if (fwrite(&e, sizeof(e), 1, out) != 1)
827 +               return 12;
828 +       if (!noflush)
829 +               fflush(out);
830 +       return 0;
833 +static int send_string(FILE *out, const void *p, size_t l)
835 +       int err = send_res_ex(out, (CURLcode)l, 1);
836 +       if (err)
837 +               return err;
838 +       if (l && fwrite(p, l, 1, out) != 1)
839 +               return 12;
840 +       fflush(out);
841 +       return 0;
844 +typedef struct {
845 +       const char *data;
846 +       size_t len;
847 +       size_t offset;
848 +} context_t;
850 +static size_t readfunc(void *ptr, size_t size, size_t nmemb, void *_cxt)
852 +       context_t *cxt = (context_t *)_cxt;
853 +       size_t max = size * nmemb;
854 +       if (max > cxt->len - cxt->offset)
855 +               max = cxt->len - cxt->offset;
856 +       if (max) {
857 +               memcpy(ptr, cxt->data + cxt->offset, max);
858 +               cxt->offset += max;
859 +       }
860 +       return max;
863 +struct collect_s;
864 +static int append_collect(struct collect_s *clt, const void *p, size_t s);
866 +typedef struct {
867 +       char *line;
868 +       size_t space;
869 +       struct collect_s *clt;
870 +} hdrline_t;
872 +static size_t hdrfunc(char *ptr, size_t size, size_t nmemb, void *_cxt)
874 +       hdrline_t *cxt = (hdrline_t *)_cxt;
875 +       size_t max = size * nmemb;
876 +       size_t len = max;
877 +       if (len >= cxt->space)
878 +               len = cxt->space - 1;
879 +       memcpy(cxt->line, ptr, len);
880 +       while (len && (ptr[len - 1] == '\r' || ptr[len - 1] == '\n'))
881 +               --len;
882 +       cxt->line[len] = '\0';
883 +       len = append_collect(cxt->clt, ptr, max);
884 +       if (!len || len != max || append_collect(cxt->clt, "", 1) != 1)
885 +               return 0;
886 +       return max;
889 +static int progressfunc(void *p, double t, double n, double u, double x)
891 +       (void)p;
892 +       (void)t;
893 +       (void)n;
894 +       (void)u;
895 +       (void)x;
896 +       return 0;
899 +static int xferfunc(void *p, curl_off_t t, curl_off_t n, curl_off_t u, curl_off_t x)
901 +       (void)p;
902 +       (void)t;
903 +       (void)n;
904 +       (void)u;
905 +       (void)x;
906 +       return 0;
909 +#define BLOCK_SIZE 32736
910 +typedef struct block_s {
911 +       struct block_s *next;
912 +       size_t offset;
913 +       char data[BLOCK_SIZE];
914 +} block_t;
916 +typedef struct collect_s {
917 +       block_t *first, *last;
918 +       size_t total;
919 +} collect_t;
921 +static void free_collect(collect_t *clt)
923 +       block_t *block = clt->first;
924 +       while (block) {
925 +               block_t *link = block->next;
926 +               free(block);
927 +               block = link;
928 +       }
929 +       clt->first = NULL;
930 +       clt->last = NULL;
931 +       clt->total = 0;
934 +static int append_collect(collect_t *clt, const void *p, size_t s)
936 +       size_t left;
937 +       const char *ptr;
939 +       if (!p || !s)
940 +               return 0;
942 +       ptr = (const char *)p;
943 +       left = s;
944 +       while (left) {
945 +               size_t cpyamt;
946 +               if (!clt->first) {
947 +                       clt->first = (block_t *)malloc(sizeof(block_t));
948 +                       if (!clt->first)
949 +                               return 0;
950 +                       clt->first->next = NULL;
951 +                       clt->first->offset = 0;
952 +                       clt->last = clt->first;
953 +               }
954 +               if (clt->last->offset >= BLOCK_SIZE) {
955 +                       block_t *next = (block_t *)malloc(sizeof(block_t));
956 +                       if (!next)
957 +                               return 0;
958 +                       next->next = NULL;
959 +                       next->offset = 0;
960 +                       clt->last->next = next;
961 +                       clt->last = next;
962 +               }
963 +               cpyamt = BLOCK_SIZE - clt->last->offset;
964 +               if (cpyamt > left)
965 +                       cpyamt = left;
966 +               memcpy(clt->last->data + clt->last->offset, ptr, cpyamt);
967 +               clt->last->offset += cpyamt;
968 +               clt->total += cpyamt;
969 +               ptr += cpyamt;
970 +               left -= cpyamt;
971 +       }
973 +       return s;
976 +static size_t writefunc(char *ptr, size_t size, size_t nmemb, void *_clt)
978 +       return append_collect((collect_t *)_clt, ptr, size * nmemb);
981 +static int send_collect(FILE *out, const collect_t *clt)
983 +       const block_t *block = clt->first;
984 +       int err = send_res_ex(out, (CURLcode)clt->total, 1);
985 +       if (err)
986 +               return err;
987 +       while (block) {
988 +               if (block->offset) {
989 +                       if (fwrite(block->data, block->offset, 1, out) != 1)
990 +                               return 12;
991 +               }
992 +               block = block->next;
993 +       }
994 +       fflush(out);
995 +       return 0;
998 +enum {
999 +       CURLCMD_PERFORM = -1000,
1000 +       CURLGET_ERRBUF,
1001 +       CURLGET_LASTHDR,
1002 +       CURLGET_STRERROR,
1003 +       CURLCMD_RESET,
1004 +       CURLGET_VERSION
1007 +typedef struct {
1008 +       char tag[4];
1009 +       int opt;
1010 +} cmd_lookup_t;
1012 +static cmd_lookup_t commands[] = {
1013 +       {{'a','g','n','t'}, CURLOPT_USERAGENT},
1014 +       {{'a','u','t','h'}, CURLOPT_MAIL_AUTH},
1015 +       {{'c','a','i','n'}, CURLOPT_CAINFO},
1016 +       {{'c','a','p','a'}, CURLOPT_CAPATH},
1017 +       {{'c','e','r','t'}, CURLOPT_SSLCERT},
1018 +       {{'c','k','l','s'}, CURLOPT_COOKIELIST},
1019 +       {{'c','o','o','k'}, CURLOPT_COOKIEFILE},
1020 +       {{'c','t','y','p'}, CURLINFO_CONTENT_TYPE},
1021 +       {{'d','a','t','a'}, CURLOPT_READDATA},
1022 +       {{'e','r','r','b'}, CURLGET_ERRBUF},
1023 +       {{'f','r','o','m'}, CURLOPT_MAIL_FROM},
1024 +       {{'h','d','r','s'}, CURLOPT_HTTPHEADER},
1025 +       {{'h','d','t','a'}, CURLOPT_HEADERDATA},
1026 +       {{'h','g','e','t'}, CURLOPT_HTTPGET},
1027 +       {{'k','e','y','_'}, CURLOPT_SSLKEY},
1028 +       {{'l','s','t','h'}, CURLGET_LASTHDR},
1029 +       {{'m','e','c','h'}, CURLOPT_LOGIN_OPTIONS},
1030 +       {{'n','b','d','y'}, CURLOPT_NOBODY},
1031 +       {{'n','o','p','g'}, CURLOPT_NOPROGRESS},
1032 +       {{'n','t','r','c'}, CURLOPT_NETRC},
1033 +       {{'p','i','n','k'}, CURLOPT_PINNEDPUBLICKEY},
1034 +       {{'p','o','r','t'}, CURLOPT_PORT},
1035 +       {{'p','o','s','t'}, CURLOPT_POST},
1036 +       {{'p','r','f','m'}, CURLCMD_PERFORM},
1037 +       {{'p','r','x','y'}, CURLOPT_PROXY},
1038 +       {{'p','s','w','d'}, CURLOPT_PASSWORD},
1039 +       {{'r','c','p','t'}, CURLOPT_MAIL_RCPT},
1040 +       {{'r','s','e','t'}, CURLCMD_RESET},
1041 +       {{'r','s','p','c'}, CURLINFO_RESPONSE_CODE},
1042 +       {{'r','u','r','l'}, CURLINFO_REDIRECT_URL},
1043 +       {{'s','t','r','e'}, CURLGET_STRERROR},
1044 +       {{'t','m','o','t'}, CURLOPT_TIMEOUT},
1045 +       {{'u','p','l','d'}, CURLOPT_UPLOAD},
1046 +       {{'u','r','l','_'}, CURLOPT_URL},
1047 +       {{'u','s','e','r'}, CURLOPT_USERNAME},
1048 +       {{'u','s','s','l'}, CURLOPT_USE_SSL},
1049 +       {{'v','e','r','b'}, CURLOPT_VERBOSE},
1050 +       {{'v','e','r','s'}, CURLGET_VERSION},
1051 +       {{'v','h','s','t'}, CURLOPT_SSL_VERIFYHOST},
1052 +       {{'v','p','e','r'}, CURLOPT_SSL_VERIFYPEER},
1053 +       {{'w','d','t','a'}, CURLOPT_WRITEDATA}
1056 +static int cmp_cmd(const void *_e1, const void *_e2)
1058 +       const cmd_lookup_t *e1 = (cmd_lookup_t *)_e1;
1059 +       const cmd_lookup_t *e2 = (cmd_lookup_t *)_e2;
1060 +       return memcmp(e1->tag, e2->tag, 4);
1063 +static int find_command(const char tag[4])
1065 +       cmd_lookup_t search;
1066 +       const cmd_lookup_t *ans;
1068 +       memcpy(search.tag, tag, 4);
1069 +       ans  = (cmd_lookup_t *)
1070 +               bsearch(&search, commands, sizeof(commands)/sizeof(commands[0]),
1071 +                       sizeof(commands[0]), cmp_cmd);
1072 +       return ans ? ans->opt : -1;
1075 +static int process_cmds(FILE *in, FILE *out, CURL *curl)
1077 +       cmd_t cmd;
1078 +       char errbuf[CURL_ERROR_SIZE+1];
1079 +       char hdrbuf[1024];
1080 +       char *data = NULL;
1081 +       size_t datalen = 0;
1082 +       struct curl_slist *recipients = NULL;
1083 +       struct curl_slist *httpheaders = NULL;
1084 +       collect_t hdata, wdata;
1086 +       memset(&hdata, 0, sizeof(hdata));
1087 +       memset(&wdata, 0, sizeof(wdata));
1088 +       errbuf[0] = '\0';
1089 +       hdrbuf[0] = '\0';
1090 +       while (!ferror(in) && !ferror(out) && fread(&cmd, sizeof(cmd), 1, in)) {
1091 +               char *string;
1092 +               int err = 0;
1093 +               CURLcode res = 0;
1094 +               int opt = find_command(cmd.tag);
1095 +               const char *stringval = NULL;
1097 +               cmd.val = ntohl(cmd.val);
1098 +               switch (opt) {
1100 +               case CURLOPT_NOPROGRESS:
1101 +               case CURLOPT_SSL_VERIFYPEER:
1102 +               case CURLOPT_HTTPGET:
1103 +               case CURLOPT_NOBODY:
1104 +               case CURLOPT_POST:
1105 +               case CURLOPT_UPLOAD:
1106 +               case CURLOPT_VERBOSE:
1107 +                       res = curl_easy_setopt(curl, (CURLoption)opt,
1108 +                                               cmd.val ? 1L : 0L);
1109 +                       break;
1111 +               case CURLOPT_CAINFO:
1112 +               case CURLOPT_CAPATH:
1113 +               case CURLOPT_LOGIN_OPTIONS:
1114 +               case CURLOPT_MAIL_AUTH:
1115 +               case CURLOPT_MAIL_FROM:
1116 +               case CURLOPT_PASSWORD:
1117 +               case CURLOPT_URL:
1118 +               case CURLOPT_USERNAME:
1119 +               case CURLOPT_PROXY:
1120 +               case CURLOPT_COOKIELIST:
1121 +               case CURLOPT_USERAGENT:
1122 +               case CURLOPT_SSLCERT:
1123 +               case CURLOPT_SSLKEY:
1124 +               case CURLOPT_PINNEDPUBLICKEY:
1125 +                       res = handle_string_opt(curl, (CURLoption)opt,
1126 +                                               cmd.val, in, &err);
1127 +                       break;
1129 +               case CURLOPT_PORT:
1130 +               case CURLOPT_TIMEOUT:
1131 +                       res = curl_easy_setopt(curl, (CURLoption)opt, (long)cmd.val);
1132 +                       break;
1134 +               case CURLOPT_SSL_VERIFYHOST:
1135 +                       res = curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST,
1136 +                                               cmd.val ? 2L : 0L);
1137 +                       break;
1139 +               case CURLOPT_USE_SSL:
1140 +                       res = curl_easy_setopt(curl, CURLOPT_USE_SSL, (long)(
1141 +                               (cmd.val & 0x80000000) ? CURLUSESSL_TRY :
1142 +                                       (cmd.val ? CURLUSESSL_ALL : CURLUSESSL_NONE)));
1143 +                       break;
1145 +               case CURLOPT_NETRC:
1146 +                       res = curl_easy_setopt(curl, CURLOPT_NETRC, (long)(
1147 +                               (cmd.val & 0x80000000) ? CURL_NETRC_OPTIONAL :
1148 +                                       (cmd.val ? CURL_NETRC_REQUIRED : CURL_NETRC_IGNORED)));
1149 +                       break;
1151 +               case CURLOPT_READDATA:
1152 +                       if (data)
1153 +                               free(data);
1154 +                       err = read_string(in, cmd.val, &data);
1155 +                       if (err)
1156 +                               break;
1157 +                       datalen = cmd.val;
1158 +                       break;
1160 +               case CURLOPT_COOKIEFILE:
1161 +               {
1162 +                       char *string;
1163 +                       err = read_string(in, cmd.val, &string);
1164 +                       if (err)
1165 +                               break;
1166 +                       res = curl_easy_setopt(curl, CURLOPT_COOKIEFILE, string);
1167 +                       if (!res && *string)
1168 +                               res = curl_easy_setopt(curl, CURLOPT_COOKIEJAR, string);
1169 +                       free(string);
1170 +                       break;
1171 +               }
1173 +               case CURLGET_VERSION:
1174 +               {
1175 +                       curl_version_info_data *data = curl_version_info(CURLVERSION_NOW);
1176 +                       res = data->version_num;
1177 +                       break;
1178 +               }
1180 +               case CURLGET_ERRBUF:
1181 +                       stringval = errbuf;
1182 +                       break;
1184 +               case CURLGET_LASTHDR:
1185 +                       stringval = hdrbuf;
1186 +                       break;
1188 +               case CURLGET_STRERROR:
1189 +                       stringval = curl_easy_strerror((CURLcode) cmd.val);
1190 +                       break;
1192 +               case CURLOPT_HEADERDATA:
1193 +                       err = send_collect(out, &hdata);
1194 +                       if (!err)
1195 +                               continue;
1196 +                       break;
1198 +               case CURLOPT_WRITEDATA:
1199 +                       err = send_collect(out, &wdata);
1200 +                       if (!err)
1201 +                               continue;
1202 +                       break;
1204 +               case CURLINFO_RESPONSE_CODE:
1205 +               {
1206 +                       long response;
1207 +                       res = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response);
1208 +                       if (res)
1209 +                         res += 1000000;
1210 +                       else
1211 +                         res = response;
1212 +                       break;
1213 +               }
1215 +               case CURLINFO_REDIRECT_URL:
1216 +               case CURLINFO_CONTENT_TYPE:
1217 +               {
1218 +                       char *string;
1219 +                       res = curl_easy_getinfo(curl, (CURLINFO)opt, &string);
1220 +                       stringval = res || !string ? "" : string;
1221 +                       break;
1222 +               }
1224 +               case CURLOPT_MAIL_RCPT:
1225 +               {
1226 +                       const char *ptr;
1227 +                       if (recipients) {
1228 +                               curl_slist_free_all(recipients);
1229 +                               recipients = NULL;
1230 +                       }
1231 +                       err = read_string(in, cmd.val, &string);
1232 +                       if (err)
1233 +                               break;
1234 +                       ptr = string;
1235 +                       while (cmd.val) {
1236 +                               const char *nul = memchr(ptr, 0, cmd.val);
1237 +                               if (!nul)
1238 +                                       return 13;
1239 +                               recipients = curl_slist_append(recipients, ptr);
1240 +                               cmd.val -= ++nul - ptr;
1241 +                               ptr = nul;
1242 +                       }
1243 +                       free(string);
1244 +                       res = curl_easy_setopt(curl, CURLOPT_MAIL_RCPT, recipients);
1245 +                       break;
1246 +               }
1248 +               case CURLOPT_HTTPHEADER:
1249 +               {
1250 +                       const char *ptr;
1251 +                       if (httpheaders) {
1252 +                               curl_slist_free_all(httpheaders);
1253 +                               httpheaders = NULL;
1254 +                       }
1255 +                       err = read_string(in, cmd.val, &string);
1256 +                       if (err)
1257 +                               break;
1258 +                       ptr = string;
1259 +                       while (cmd.val) {
1260 +                               const char *nul = memchr(ptr, 0, cmd.val);
1261 +                               if (!nul)
1262 +                                       return 13;
1263 +                               httpheaders = curl_slist_append(httpheaders, ptr);
1264 +                               cmd.val -= ++nul - ptr;
1265 +                               ptr = nul;
1266 +                       }
1267 +                       free(string);
1268 +                       res = curl_easy_setopt(curl, CURLOPT_HTTPHEADER, httpheaders);
1269 +                       break;
1270 +               }
1272 +               case CURLCMD_RESET:
1273 +               {
1274 +                       free_collect(&wdata);
1275 +                       free_collect(&hdata);
1276 +                       if (httpheaders) {
1277 +                               curl_slist_free_all(httpheaders);
1278 +                               httpheaders = NULL;
1279 +                       }
1280 +                       if (recipients) {
1281 +                               curl_slist_free_all(recipients);
1282 +                               recipients = NULL;
1283 +                       }
1284 +                       if (data) {
1285 +                               free(data);
1286 +                               data = NULL;
1287 +                               datalen = 0;
1288 +                       }
1290 +                       reset_curl(curl);
1291 +                       break;
1292 +               }
1294 +               case CURLCMD_PERFORM:
1295 +               {
1296 +                       context_t cxt;
1297 +                       hdrline_t lasthdr;
1299 +                       cxt.data = data;
1300 +                       cxt.len = datalen;
1301 +                       cxt.offset = 0;
1302 +                       errbuf[0] = '\0';
1303 +                       lasthdr.line = hdrbuf;
1304 +                       lasthdr.space = sizeof(hdrbuf);
1305 +                       lasthdr.clt = &hdata;
1306 +                       hdrbuf[0] = '\0';
1307 +                       free_collect(&wdata);
1308 +                       free_collect(&hdata);
1309 +                       res = curl_easy_setopt(curl,
1310 +                                       CURLOPT_ERRORBUFFER, errbuf);
1311 +                       if (!res)
1312 +                               res = curl_easy_setopt(curl,
1313 +                                       CURLOPT_INFILESIZE, (long)datalen);
1314 +                       if (!res)
1315 +                               res = curl_easy_setopt(curl,
1316 +                                       CURLOPT_POSTFIELDSIZE, (long)datalen);
1317 +                       if (!res)
1318 +                               res = curl_easy_setopt(curl,
1319 +                                       CURLOPT_READDATA, &cxt);
1320 +                       if (!res)
1321 +                               res = curl_easy_setopt(curl,
1322 +                                       CURLOPT_READFUNCTION, (void *)readfunc);
1323 +                       if (!res)
1324 +                               res = curl_easy_setopt(curl,
1325 +                                       CURLOPT_PROGRESSFUNCTION, (void *)progressfunc);
1326 +                       if (!res)
1327 +                               res = curl_easy_setopt(curl,
1328 +                                       CURLOPT_XFERINFOFUNCTION, (void *)xferfunc);
1329 +                       if (!res)
1330 +                               res = curl_easy_setopt(curl,
1331 +                                       CURLOPT_WRITEDATA, &wdata);
1332 +                       if (!res)
1333 +                               res = curl_easy_setopt(curl,
1334 +                                       CURLOPT_WRITEFUNCTION, (void *)writefunc);
1335 +                       if (!res)
1336 +                               res = curl_easy_setopt(curl,
1337 +                                       CURLOPT_HEADERDATA, &lasthdr);
1338 +                       if (!res)
1339 +                               res = curl_easy_setopt(curl,
1340 +                                       CURLOPT_HEADERFUNCTION, (void *)hdrfunc);
1341 +                       if (!res)
1342 +                               res = curl_easy_perform(curl);
1343 +                       break;
1344 +               }
1346 +               default:
1347 +                       err = 20;
1348 +                       break;
1350 +               }
1351 +               if (!err)
1352 +                       err = stringval ?
1353 +                               send_string(out, stringval, strlen(stringval)) :
1354 +                               send_res(out, res);
1355 +               if (err)
1356 +                       return err;
1357 +       }
1359 +       free_collect(&wdata);
1360 +       free_collect(&hdata);
1361 +       if (httpheaders)
1362 +               curl_slist_free_all(httpheaders);
1363 +       if (recipients)
1364 +               curl_slist_free_all(recipients);
1365 +       if (data)
1366 +               free(data);
1368 +       if (ferror(in) || !feof(in) || ferror(out))
1369 +               return 4;
1371 +       return 0;
1373 diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
1374 index 7be14a4e..09fd92b3 100755
1375 --- a/t/t9001-send-email.sh
1376 +++ b/t/t9001-send-email.sh
1377 @@ -146,6 +146,7 @@ cat >expected-show-all-headers <<\EOF
1378  (mbox) Adding cc: A <author@example.com> from line 'From: A <author@example.com>'
1379  (mbox) Adding cc: One <one@example.com> from line 'Cc: One <one@example.com>, two@example.com'
1380  (mbox) Adding cc: two@example.com from line 'Cc: One <one@example.com>, two@example.com'
1382  Dry-OK. Log says:
1383  Server: relay.example.com
1384  MAIL FROM:<from@example.com>
1385 @@ -164,7 +165,6 @@ Cc: cc@example.com,
1386  Subject: [PATCH 1/1] Second.
1387  Date: DATE-STRING
1388  Message-Id: MESSAGE-ID-STRING
1389 -X-Mailer: X-MAILER-STRING
1390  In-Reply-To: <unique-message-id@example.com>
1391  References: <unique-message-id@example.com>
1393 @@ -496,6 +496,7 @@ cat >expected-suppress-sob <<\EOF
1394  (mbox) Adding cc: A <author@example.com> from line 'From: A <author@example.com>'
1395  (mbox) Adding cc: One <one@example.com> from line 'Cc: One <one@example.com>, two@example.com'
1396  (mbox) Adding cc: two@example.com from line 'Cc: One <one@example.com>, two@example.com'
1398  Dry-OK. Log says:
1399  Server: relay.example.com
1400  MAIL FROM:<from@example.com>
1401 @@ -513,7 +514,6 @@ Cc: cc@example.com,
1402  Subject: [PATCH 1/1] Second.
1403  Date: DATE-STRING
1404  Message-Id: MESSAGE-ID-STRING
1405 -X-Mailer: X-MAILER-STRING
1407  Result: OK
1408  EOF
1409 @@ -545,6 +545,7 @@ cat >expected-suppress-sob <<\EOF
1410  (mbox) Adding cc: A <author@example.com> from line 'From: A <author@example.com>'
1411  (mbox) Adding cc: One <one@example.com> from line 'Cc: One <one@example.com>, two@example.com'
1412  (mbox) Adding cc: two@example.com from line 'Cc: One <one@example.com>, two@example.com'
1414  Dry-OK. Log says:
1415  Server: relay.example.com
1416  MAIL FROM:<from@example.com>
1417 @@ -560,7 +561,6 @@ Cc: A <author@example.com>,
1418  Subject: [PATCH 1/1] Second.
1419  Date: DATE-STRING
1420  Message-Id: MESSAGE-ID-STRING
1421 -X-Mailer: X-MAILER-STRING
1423  Result: OK
1424  EOF
1425 @@ -578,6 +578,7 @@ cat >expected-suppress-cccmd <<\EOF
1426  (mbox) Adding cc: One <one@example.com> from line 'Cc: One <one@example.com>, two@example.com'
1427  (mbox) Adding cc: two@example.com from line 'Cc: One <one@example.com>, two@example.com'
1428  (body) Adding cc: C O Mitter <committer@example.com> from line 'Signed-off-by: C O Mitter <committer@example.com>'
1430  Dry-OK. Log says:
1431  Server: relay.example.com
1432  MAIL FROM:<from@example.com>
1433 @@ -595,7 +596,6 @@ Cc: A <author@example.com>,
1434  Subject: [PATCH 1/1] Second.
1435  Date: DATE-STRING
1436  Message-Id: MESSAGE-ID-STRING
1437 -X-Mailer: X-MAILER-STRING
1439  Result: OK
1440  EOF
1441 @@ -612,6 +612,7 @@ test_expect_success $PREREQ 'sendemail.cccmd' '
1442  test_expect_success $PREREQ 'setup expect' '
1443  cat >expected-suppress-all <<\EOF
1444  0001-Second.patch
1446  Dry-OK. Log says:
1447  Server: relay.example.com
1448  MAIL FROM:<from@example.com>
1449 @@ -621,7 +622,6 @@ To: to@example.com
1450  Subject: [PATCH 1/1] Second.
1451  Date: DATE-STRING
1452  Message-Id: MESSAGE-ID-STRING
1453 -X-Mailer: X-MAILER-STRING
1455  Result: OK
1456  EOF
1457 @@ -638,6 +638,7 @@ cat >expected-suppress-body <<\EOF
1458  (mbox) Adding cc: One <one@example.com> from line 'Cc: One <one@example.com>, two@example.com'
1459  (mbox) Adding cc: two@example.com from line 'Cc: One <one@example.com>, two@example.com'
1460  (cc-cmd) Adding cc: cc-cmd@example.com from: './cccmd'
1462  Dry-OK. Log says:
1463  Server: relay.example.com
1464  MAIL FROM:<from@example.com>
1465 @@ -655,7 +656,6 @@ Cc: A <author@example.com>,
1466  Subject: [PATCH 1/1] Second.
1467  Date: DATE-STRING
1468  Message-Id: MESSAGE-ID-STRING
1469 -X-Mailer: X-MAILER-STRING
1471  Result: OK
1472  EOF
1473 @@ -671,6 +671,7 @@ cat >expected-suppress-body-cccmd <<\EOF
1474  (mbox) Adding cc: A <author@example.com> from line 'From: A <author@example.com>'
1475  (mbox) Adding cc: One <one@example.com> from line 'Cc: One <one@example.com>, two@example.com'
1476  (mbox) Adding cc: two@example.com from line 'Cc: One <one@example.com>, two@example.com'
1478  Dry-OK. Log says:
1479  Server: relay.example.com
1480  MAIL FROM:<from@example.com>
1481 @@ -686,7 +687,6 @@ Cc: A <author@example.com>,
1482  Subject: [PATCH 1/1] Second.
1483  Date: DATE-STRING
1484  Message-Id: MESSAGE-ID-STRING
1485 -X-Mailer: X-MAILER-STRING
1487  Result: OK
1488  EOF
1489 @@ -702,6 +702,7 @@ cat >expected-suppress-sob <<\EOF
1490  (mbox) Adding cc: A <author@example.com> from line 'From: A <author@example.com>'
1491  (mbox) Adding cc: One <one@example.com> from line 'Cc: One <one@example.com>, two@example.com'
1492  (mbox) Adding cc: two@example.com from line 'Cc: One <one@example.com>, two@example.com'
1494  Dry-OK. Log says:
1495  Server: relay.example.com
1496  MAIL FROM:<from@example.com>
1497 @@ -717,7 +718,6 @@ Cc: A <author@example.com>,
1498  Subject: [PATCH 1/1] Second.
1499  Date: DATE-STRING
1500  Message-Id: MESSAGE-ID-STRING
1501 -X-Mailer: X-MAILER-STRING
1503  Result: OK
1504  EOF
1505 @@ -735,6 +735,7 @@ cat >expected-suppress-bodycc <<\EOF
1506  (mbox) Adding cc: One <one@example.com> from line 'Cc: One <one@example.com>, two@example.com'
1507  (mbox) Adding cc: two@example.com from line 'Cc: One <one@example.com>, two@example.com'
1508  (body) Adding cc: C O Mitter <committer@example.com> from line 'Signed-off-by: C O Mitter <committer@example.com>'
1510  Dry-OK. Log says:
1511  Server: relay.example.com
1512  MAIL FROM:<from@example.com>
1513 @@ -752,7 +753,6 @@ Cc: A <author@example.com>,
1514  Subject: [PATCH 1/1] Second.
1515  Date: DATE-STRING
1516  Message-Id: MESSAGE-ID-STRING
1517 -X-Mailer: X-MAILER-STRING
1519  Result: OK
1520  EOF
1521 @@ -767,6 +767,7 @@ cat >expected-suppress-cc <<\EOF
1522  0001-Second.patch
1523  (mbox) Adding cc: A <author@example.com> from line 'From: A <author@example.com>'
1524  (body) Adding cc: C O Mitter <committer@example.com> from line 'Signed-off-by: C O Mitter <committer@example.com>'
1526  Dry-OK. Log says:
1527  Server: relay.example.com
1528  MAIL FROM:<from@example.com>
1529 @@ -780,7 +781,6 @@ Cc: A <author@example.com>,
1530  Subject: [PATCH 1/1] Second.
1531  Date: DATE-STRING
1532  Message-Id: MESSAGE-ID-STRING
1533 -X-Mailer: X-MAILER-STRING
1535  Result: OK
1536  EOF