]> git.pld-linux.org Git - packages/swaks.git/blame - swaks.pl
Up to 20201014.0.
[packages/swaks.git] / swaks.pl
CommitLineData
a1e0888b 1#!/usr/bin/perl
2
3# use 'swaks --help' to view documentation for this program
4# if you want to be notified about future releases of this program,
5# please send an email to updates-swaks@jetmore.net
6
7use strict;
8
9my($p_name) = $0 =~ m|/?([^/]+)$|;
10my $p_version = "20061116.0";
11my $p_usage = "Usage: $p_name [--help|--version] (see --help for details)";
12my $p_cp = <<EOM;
13 Copyright (c) 2003-2006 John Jetmore <jj33\@pobox.com>
14
15 This program is free software; you can redistribute it and/or modify
16 it under the terms of the GNU General Public License as published by
17 the Free Software Foundation; either version 2 of the License, or
18 (at your option) any later version.
19
20 This program is distributed in the hope that it will be useful,
21 but WITHOUT ANY WARRANTY; without even the implied warranty of
22 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 GNU General Public License for more details.
24
25 You should have received a copy of the GNU General Public License
26 along with this program; if not, write to the Free Software
27 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
28EOM
29ext_usage(); # before we do anything else, check for --help
30
31my %O = ();
32$| = 1;
33
34# need to rewrite header-HEADER opts before std option parsing
35for (my $i = 0; $i < scalar(@ARGV); $i++) {
36 if ($ARGV[$i] =~ /^--h(?:eader)?-(.*)$/) {
37 $ARGV[$i] = "--header"; $ARGV[$i+1] = "$1: $ARGV[$i+1]";
38 }
39}
40if (!load("Getopt::Long")) {
41 ptrans(12, "Unable to load Getopt::Long for option processing, Exiting");
42 exit(1);
43}
44Getopt::Long::Configure("bundling_override");
45GetOptions(
46 'l|input-file=s' => \$O{option_file}, # (l)ocation of input data
47 'f|from:s' => \$O{mail_from}, # envelope-(f)rom address
48 't|to:s' => \$O{mail_to}, # envelope-(t)o address
49 'h|helo|ehlo|lhlo:s' => \$O{mail_helo}, # (h)elo string
50 's|server:s' => \$O{mail_server}, # (s)erver to use
51 'p|port:s' => \$O{mail_port}, # (p)ort to use
52 'protocol:s' => \$O{mail_protocol}, # protocol to use (smtp, esmtp, lmtp)
53 'd|data:s' => \$O{mail_data}, # (d)ata portion ('\n' for newlines)
54 'timeout:s' => \$O{timeout}, # timeout for each trans (def 30s)
55 'g' => \$O{data_on_stdin}, # (g)et data on stdin
56 'm' => \$O{emulate_mail}, # emulate (M)ail command
57 'q|quit|quit-after=s' => \$O{quit_after}, # (q)uit after
58 'n|suppress-data' => \$O{suppress_data}, # do (n)ot print data portion
59 'a|auth:s' => \$O{auth}, # force auth, exit if not supported
60 'au|auth-user:s' => \$O{auth_user}, # user for auth
61 'ap|auth-password:s' => \$O{auth_pass}, # pass for auth
62 'am|auth-map=s' => \$O{auth_map}, # auth type map
63 #'ahp|auth-hide-password' => \$O{auth_hidepw}, # hide passwords when possible
64 'apt|auth-plaintext' => \$O{auth_showpt}, # translate base64 strings
65 'ao|auth-optional:s' => \$O{auth_optional}, # auth optional (ignore failure)
66 'support' => \$O{get_support}, # report capabilties
67 'li|local-interface:s' => \$O{lint}, # local interface to use
68 'tls' => \$O{tls}, # use TLS
69 'tlso|tls-optional' => \$O{tls_optional}, # use tls if available
70 'tlsc|tls-on-connect' => \$O{tls_on_connect}, # use tls if available
71 'S|silent+' => \$O{silent}, # suppress output to varying degrees
72 'nsf|no-strip-from' => \$O{no_strip_from}, # Don't strip From_ line from DATA
73 'nth|no-hints' => \$O{no_hints}, # Don't show transaction hints
74 'hr|hide-receive' => \$O{hide_receive}, # Don't show reception lines
75 'hs|hide-send' => \$O{hide_send}, # Don't show sending lines
76 'stl|show-time-lapse:s' => \$O{show_time_lapse}, # print lapse for send/recv
77 'ndf|no-data-fixup' => \$O{no_data_fixup}, # don't touch the data
78 'pipe:s' => \$O{pipe_cmd}, # command to communicate with
79 'socket:s' => \$O{socket}, # unix domain socket to talk to
80 'body:s' => \$O{body_822}, # the content of the body of the DATA
81 'attach-type|attach:s' => \@{$O{attach_822}}, # A file to attach
82 'ah|add-header:s' => \@{$O{add_header}}, # replacement for %H DATA token
83 'header:s' => \@{$O{header}}, # replace header if exist, else add
84 'dump' => \$O{dump_args}, # build options and dump
85 'pipeline' => \$O{pipeline}, # attempt PIPELINING
86 'force-getpwuid' => \$O{force_getpwuid} # use getpwuid building -f
87) || exit(1);
88
89# lists of dependencies for features
90%G::dependencies = (
91 auth => { name => "Basic AUTH", opt => ['MIME::Base64'],
92 req => [] },
93 auth_cram_md5 => { name => "AUTH CRAM-MD5", req => ['Digest::MD5'] },
94 auth_cram_sha1 => { name => "AUTH CRAM-SHA1", req => ['Digest::SHA1'] },
95 auth_ntlm => { name => "AUTH NTLM", req => ['Authen::NTLM'] },
96 auth_digest_md5 => { name => "AUTH DIGEST-MD5",
97 req => ['Authen::DigestMD5'] },
98 dns => { name => "MX Routing", req => ['Net::DNS'] },
99 tls => { name => "TLS", req => ['Net::SSLeay'] },
100 pipe => { name => "Pipe Transport", req => ['IPC::Open2'] },
101 socket => { name => "Socket Transport", req => ['IO::Socket'] },
102 date_manip => { name => "Date Manipulation", req => ['Time::Local'] },
103 hostname => { name => "Local Hostname Detection",
104 req => ['Sys::Hostname'] },
105 hires_timing => { name => "High Resolution Timing",
106 req => ['Time::HiRes'] },
107);
108
109if ($O{get_support}) {
110 test_support();
111 exit(0);
112}
113
114# We need to fix things up a bit and set a couple of global options
115my $opts = process_args(\%O);
116
117if ($G::dump_args) {
118 test_support();
119 print "dump_args = ", $G::dump_args ? "TRUE" : "FALSE", "\n";
120 print "server_only = ", $G::server_only ? "TRUE" : "FALSE", "\n";
121 print "show_time_lapse = ", $G::show_time_lapse ? "TRUE" : "FALSE", "\n";
122 print "show_time_hires = ", $G::show_time_hires ? "TRUE" : "FALSE", "\n";
123 print "auth_showpt = ", $G::auth_showpt ? "TRUE" : "FALSE", "\n";
124 print "suppress_data = ", $G::suppress_data ? "TRUE" : "FALSE", "\n";
125 print "no_hints = ", $G::no_hints ? "TRUE" : "FALSE", "\n";
126 print "hide_send = ", $G::hide_send ? "TRUE" : "FALSE", "\n";
127 print "hide_receive = ", $G::hide_receive ? "TRUE" : "FALSE", "\n";
128 print "pipeline = ", $G::pipeline ? "TRUE" : "FALSE", "\n";
129 print "silent = $G::silent\n";
130 print "protocol = $G::protocol\n";
131 print "type = $G::link{type}\n";
132 print "server = $G::link{server}\n";
133 print "sockfile = $G::link{sockfile}\n";
134 print "process = $G::link{process}\n";
135 print "from = $opts->{from}\n";
136 print "to = $opts->{to}\n";
137 print "helo = $opts->{helo}\n";
138 print "port = $G::link{port}\n";
139 print "tls = ";
140 if ($G::tls) {
141 print "starttls (", $G::tls_optional ? 'optional' : 'required', ")\n";
142 } elsif ($G::tls_on_connect) {
143 print "on connect (required)\n";
144 } else { print "no\n"; }
145 print "auth = ";
146 if ($opts->{a_type}) {
147 print $G::auth_optional ? 'optional' : 'yes', " type='",
148 join(',', @{$opts->{a_type}}), "' ",
149 "user='$opts->{a_user}' pass='$opts->{a_pass}'\n";
150 } else { print "no\n"; }
151 print "auth map = ", join("\n".' 'x19,
152 map { "$_ = ".
153 join(', ', @{$G::auth_map_t{$_}})
154 } (keys %G::auth_map_t)
155 ), "\n";
156 print "quit after = $G::quit_after\n";
157 print "local int = $G::link{lint}\n";
158 print "timeout = $G::link{timeout}\n";
159 print "data = <<.\n$opts->{data}\n";
160 exit(0);
161}
162
163# we're going to abstract away the actual connection layer from the mail
164# process, so move the act of connecting into its own sub. The sub will
165# set info in global hash %G::link
166# XXX instead of passing raw data, have processs_opts create a link_data
167# XXX hash that we can pass verbatim here
168open_link();
169
170sendmail($opts->{from}, $opts->{to}, $opts->{helo}, $opts->{data},
171 $opts->{a_user}, $opts->{a_pass}, $opts->{a_type});
172
173teardown_link();
174
175exit(0);
176
177sub teardown_link {
178 if ($G::link{type} eq 'socket-inet' || $G::link{type} eq 'socket-unix') {
179 # XXX need anything special for tls teardown?
180 close($G::link{sock});
181 ptrans(11, "Connection closed with remote host.");
182 } elsif ($G::link{type} eq 'pipe') {
183 delete($SIG{PIPE});
184 $SIG{CHLD} = 'IGNORE';
185 close($G::link{sock}{wr});
186 close($G::link{sock}{re});
187 ptrans(11, "Connection closed with child process.");
188 }
189}
190
191sub open_link {
192 if ($G::link{type} eq 'socket-inet') {
193 ptrans(11, "Trying $G::link{server}:$G::link{port}...");
194 $@ = "";
195 $G::link{sock} = IO::Socket::INET->new(PeerAddr => $G::link{server},
196 PeerPort => $G::link{port}, Proto => 'tcp',
197 Timeout => $G::link{timeout},
198 LocalAddr => $G::link{lint});
199
200 if ($@) {
201 ptrans(12, "Error connecting $G::link{lint} " .
202 "to $G::link{server}:$G::link{port}:\n\t$@");
203 exit(2);
204 }
205 ptrans(11, "Connected to $G::link{server}.");
206 } elsif ($G::link{type} eq 'socket-unix') {
207 ptrans(11, "Trying $G::link{sockfile}...");
208 $SIG{PIPE} = 'IGNORE';
209 $@ = "";
210 $G::link{sock} = IO::Socket::UNIX->new(Peer => $G::link{sockfile},
211 Timeout => $G::link{timeout});
212
213 if ($@) {
214 ptrans(12, "Error connecting to $G::link{sockfile}:\n\t$@");
215 exit(2);
216 }
217 ptrans(11, "Connected to $G::link{sockfile}.");
218 } elsif ($G::link{type} eq 'pipe') {
219 $SIG{PIPE} = 'IGNORE';
220 $SIG{CHLD} = 'IGNORE';
221 ptrans(11, "Trying pipe to $G::link{process}...");
222 eval{
223 open2($G::link{sock}{re}, $G::link{sock}{wr}, $G::link{process});
224 };
225 if ($@) {
226 ptrans(12, "Error connecting to $G::link{process}:\n\t$@");
227 exit(2);
228 }
229 select((select($G::link{sock}{wr}), $| = 1)[0]);
230 select((select($G::link{sock}{re}), $| = 1)[0]);
231 ptrans(11, "Connected to $G::link{process}.");
232 } else {
233 ptrans(12, "Unknown or unimplemented connection type " .
234 "$G::link{type}");
235 exit(3);
236 }
237}
238
239sub sendmail {
240 my $from = shift; # envelope-from
241 my $to = shift; # envelope-to
242 my $helo = shift; # who am I?
243 my $data = shift; # body of message (content after DATA command)
244 my $a_user = shift; # what user to auth with?
245 my $a_pass = shift; # what pass to auth with
246 my $a_type = shift; # what kind of auth (this must be set to to attempt)
247 my $ehlo = {}; # If server is esmtp, save advertised features here
248
249 # start up tls if -tlsc specified
250 if ($G::tls_on_connect) {
251 if (start_tls()) {
252 ptrans(11, "TLS started w/ cipher $G::link{tls}{cipher}");
253 } else {
254 ptrans(12, "TLS startup failed ($G::link{tls}{res})");
255 exit(29);
256 }
257 }
258
259 # read the server's 220 banner
260 do_smtp_gen(undef, '220') || do_smtp_quit(1, 21);
261
262 # QUIT here if the user has asked us to do so
263 do_smtp_quit(1, 0) if ($G::quit_after eq 'connect');
264
265 # Send a HELO string
266 do_smtp_helo($helo, $ehlo, $G::protocol) || do_smtp_quit(1, 22);
267
268 # QUIT here if the user has asked us to do so
269 do_smtp_quit(1, 0) if ($G::quit_after eq 'first-helo');
270
271 # handle TLS here if user has requested it
272 if ($G::tls) {
273 do_smtp_quit(1, 29) if (!do_smtp_tls($ehlo) && !$G::tls_optional);
274 }
275
276 # QUIT here if the user has asked us to do so
277 do_smtp_quit(1, 0) if ($G::quit_after eq 'tls');
278
279 #if ($G::link{tls}{active} && $ehlo->{STARTTLS}) {
280 if ($G::link{tls}{active} && !$G::tls_on_connect) {
281 # According to RFC3207, we need to forget state info and re-EHLO here
282 $ehlo = {};
283 do_smtp_helo($helo, $ehlo, $G::protocol) || do_smtp_quit(1, 32);
284 }
285
286 # QUIT here if the user has asked us to do so
287 do_smtp_quit(1, 0) if ($G::quit_after eq 'helo');
288
289 # handle auth here if user has requested it
290 if ($a_type) {
291 do_smtp_quit(1, 28) if (!do_smtp_auth($ehlo, $a_type, $a_user, $a_pass)
292 && !$G::auth_optional);
293 }
294
295 # QUIT here if the user has asked us to do so
296 do_smtp_quit(1, 0) if ($G::quit_after eq 'auth');
297
298 # send MAIL
299 #do_smtp_gen("MAIL FROM:<$from>", '250') || do_smtp_quit(1, 23);
300 do_smtp_mail($from); # failures in this handled by smtp_mail_callback
301
302 # QUIT here if the user has asked us to do so
303 do_smtp_quit(1, 0) if ($G::quit_after eq 'mail');
304
305 # send RCPT (sub handles multiple, comma-delimited recips
306 #do_smtp_rcpt($to) || do_smtp_quit(1, 24);
307 do_smtp_rcpt($to); # failures in this handled by smtp_rcpt_callback
308 # note that smtp_rcpt_callback increments
309 # $G::smtp_rcpt_failures at every failure. This and
310 # $G::smtp_rcpt_total are used after DATA for LMTP
311
312 # QUIT here if the user has asked us to do so
313 do_smtp_quit(1, 0) if ($G::quit_after eq 'rcpt');
314
315 # send DATA
316 do_smtp_gen('DATA', '354') || do_smtp_quit(1, 25);
317
318 # send the actual data
319 #do_smtp_gen($data, '250', undef, $G::suppress_data) || do_smtp_quit(1, 26);
320 # this was moved to a custom sub because the server will have a custom
321 # behaviour when using LMTP
322 do_smtp_data($data, $G::suppress_data) || do_smtp_quit(1, 26);
323
324 # send QUIT
325 do_smtp_quit(0) || do_smtp_quit(1, 27);
326}
327
328sub start_tls {
329 my %t = (); # This is a convenience var to access $G::link{tls}{...}
330 $G::link{tls} = \%t;
331
332 Net::SSLeay::load_error_strings();
333 Net::SSLeay::SSLeay_add_ssl_algorithms();
334 Net::SSLeay::randomize();
335 $t{con} = Net::SSLeay::CTX_new() || return(0);
336 Net::SSLeay::CTX_set_options($t{con}, &Net::SSLeay::OP_ALL); # error check
337 $t{ssl} = Net::SSLeay::new($t{con}) || return(0);
338 if ($G::link{type} eq 'pipe') {
339 Net::SSLeay::set_wfd($t{ssl}, fileno($G::link{sock}{wr})); # error check?
340 Net::SSLeay::set_rfd($t{ssl}, fileno($G::link{sock}{re})); # error check?
341 } else {
342 Net::SSLeay::set_fd($t{ssl}, fileno($G::link{sock})); # error check?
343 }
344 $t{active} = Net::SSLeay::connect($t{ssl}) == 1 ? 1 : 0;
345 $t{res} = Net::SSLeay::ERR_error_string(Net::SSLeay::ERR_get_error())
346 if (!$t{active});
347 $t{cipher} = Net::SSLeay::get_cipher($t{ssl});
348
349 return($t{active});
350}
351
352sub ptrans {
353 my $c = shift; # transaction flag
354 my $m = shift; # message to print
355 my $b = shift; # be brief in what we print
356 my $o = \*STDOUT;
357 my $f;
358
359 return if (($G::hide_send && int($c/10) == 2) ||
360 ($G::hide_receive && int($c/10) == 3));
361
362 # global option silent controls what we echo to the terminal
363 # 0 - print everything
364 # 1 - don't show anything until you hit an error, then show everything
365 # received after that (done by setting option to 0 on first error)
366 # 2 - don't show anything but errors
367 # >=3 - don't print anything
368 if ($G::silent > 0) {
369 return if ($G::silent >= 3);
370 return if ($G::silent == 2 && $c%2 != 0);
371 if ($G::silent == 1) {
372 if ($c%2 != 0) {
373 return();
374 } else {
375 $G::silent = 0;
376 }
377 }
378 }
379
380 # 1x is program messages
381 # 2x is smtp send
382 # 3x is smtp recv
383 # x = 1 is info/normal
384 # x = 2 is error
385 # program info
386 if ($c == 11) { $f = '==='; }
387 # program error
388 elsif ($c == 12) { $f = '***'; $o = \*STDERR; }
389 # smtp send info
390 elsif ($c == 21) { $f = $G::link{tls}{active} ? ' ~>' : ' ->'; }
391 # smtp send error
392 elsif ($c == 22) { $f = $G::link{tls}{active} ? '*~>' : '**>'; }
393 # smtp recv info
394 elsif ($c == 31) { $f = $G::link{tls}{active} ? '<~ ' : '<- '; }
395 # smtp recv error
396 elsif ($c == 32) { $f = $G::link{tls}{active} ? '<~*' : '<**'; }
397 # something went unexpectedly
398 else { $c = '???'; }
399
400 $f .= ' ';
401 $f = '' if ($G::no_hints && int($c/10) != 1);
402
403 if ($b) {
404 # split to tmp list to prevent -w gripe
405 my @t = split(/\n/ms, $m); $m = scalar(@t) . " lines sent";
406 }
407 $m =~ s/\n/\n$f/msg;
408 print $o "$f$m\n";
409}
410
411sub do_smtp_quit {
412 my $exit = shift;
413 my $err = shift;
414
415 $G::link{allow_lost_cxn} = 1;
416 my $r = do_smtp_gen('QUIT', '221');
417 $G::link{allow_lost_cxn} = 0;
418
419 handle_disconnect($err) if ($G::link{lost_cxn});
420
421 if ($exit) {
422 teardown_link();
423 exit $err;
424 }
425
426 return($r);
427}
428
429sub do_smtp_tls {
430 my $e = shift; # ehlo config hash
431
432 if (!$e->{STARTTLS}) {
433 ptrans(12, "STARTTLS not supported");
434 return $G::tls_optional ? 1 : 0;
435 } elsif (!do_smtp_gen("STARTTLS", '220')) {
436 return $G::tls_optional ? 1 : 0;
437 } elsif (!start_tls()) {
438 ptrans(12, "TLS startup failed ($G::link{tls}{res})");
439 return $G::tls_optional ? 1 : 0;
440 }
441
442 ptrans(11, "TLS started w/ cipher $G::link{tls}{cipher}");
443 return(1);
444}
445
446sub do_smtp_auth {
447 my $e = shift; # ehlo config hash
448 my $at = shift; # auth type
449 my $au = shift; # auth user
450 my $ap = shift; # auth password
451
452 # the auth_optional stuff is handled higher up, so tell the truth about
453 # failing here
454
455 # note that we don't have to check whether the modules are loaded here,
456 # that's done in the option processing - trust that an auth type
457 # wouldn't be in $at if we didn't have the correct tools.
458
459 my $auth_attempted = 0; # set to true if we ever attempt auth
460
461 foreach my $btype (@$at) {
462 # if server doesn't support, skip type (may change in future)
463 next if (!$e->{AUTH}{$btype});
464
465 foreach my $type (@{$G::auth_map_t{'CRAM-MD5'}}) {
466 if ($btype eq $type) {
467 return(1) if (do_smtp_auth_cram($au, $ap, $type));
468 $auth_attempted = 1;
469 }
470 }
471 foreach my $type (@{$G::auth_map_t{'CRAM-SHA1'}}) {
472 if ($btype eq $type) {
473 return(1) if (do_smtp_auth_cram($au, $ap, $type));
474 $auth_attempted = 1;
475 }
476 }
477 foreach my $type (@{$G::auth_map_t{'DIGEST-MD5'}}) {
478 if ($btype eq $type) {
479 return(1) if (do_smtp_auth_digest($au, $ap, $type));
480 $auth_attempted = 1;
481 }
482 }
483 foreach my $type (@{$G::auth_map_t{'NTLM'}}) {
484 if ($btype eq $type) {
485 return(1) if (do_smtp_auth_ntlm($au, $ap, $type));
486 $auth_attempted = 1;
487 }
488 }
489 foreach my $type (@{$G::auth_map_t{'PLAIN'}}) {
490 if ($btype eq $type) {
491 return(1) if (do_smtp_auth_plain($au, $ap, $type));
492 $auth_attempted = 1;
493 }
494 }
495 foreach my $type (@{$G::auth_map_t{'LOGIN'}}) {
496 if ($btype eq $type) {
497 return(1) if (do_smtp_auth_login($au, $ap, $type));
498 $auth_attempted = 1;
499 }
500 }
501 }
502
503 if ($auth_attempted) {
504 ptrans(12, "No authentication type succeeded");
505 } else {
506 ptrans(12, "No acceptable authentication types available");
507 }
508 return(0);
509}
510
511sub do_smtp_auth_ntlm {
512 my $u = shift; # auth user
513 my $p = shift; # auth password
514 my $as = shift; # auth type (since NTLM might be SPA or MSN)
515 my $r = ''; # will store smtp response
516 my $domain;
517 ($u,$domain) = split(/%/, $u);
518
519 my $auth_string = "AUTH $as";
520 do_smtp_gen($auth_string, '334') || return(0);
521
522 my $d = db64(Authen::NTLM::ntlm());
523
524 $auth_string = eb64($d);
525 do_smtp_gen($auth_string, '334', \$r, '', $G::auth_showpt ? "$d" : '',
526 $G::auth_showpt ? \&unencode_smtp : '') || return(0);
527
528 $r =~ s/^....//; # maybe something a little better here?
529 Authen::NTLM::ntlm_domain($domain);
530 Authen::NTLM::ntlm_user($u);
531 Authen::NTLM::ntlm_password($p);
532 $d = db64(Authen::NTLM::ntlm($r));
533
534 $auth_string = eb64($d);
535 do_smtp_gen($auth_string, '235', \$r, '',
536 $G::auth_showpt ? "$d" : '') || return(0);
537
538 return(1);
539}
540
541sub do_smtp_auth_digest {
542 my $u = shift; # auth user
543 my $p = shift; # auth password
544 my $as = shift; # auth string
545 my $r = ''; # will store smtp response
546
547 my $auth_string = "AUTH $as";
548 do_smtp_gen($auth_string, '334', \$r, '', '',
549 $G::auth_showpt ? \&unencode_smtp : '')
550 || return(0);
551
552 $r =~ s/^....//; # maybe something a little better here?
553 $r = db64($r);
554 my $req = Authen::DigestMD5::Request->new($r);
555 my $res = Authen::DigestMD5::Response->new();
556 $res->got_request($req);
557 # XXX using link{server} here is probably a bug, but I don;t know what else
558 # XXX to use yet on a non-inet-socket connection
559 $res->set('username' => $u, 'realm' => '',
560 'digest-uri' => "smtp/$G::link{server}");
561 $res->add_digest(password => $p);
562 my $d = $res->output();
563 $auth_string = eb64($d);
564
565 do_smtp_gen($auth_string, '334', \$r, '', $G::auth_showpt ? "$d" : '',
566 $G::auth_showpt ? \&unencode_smtp : '')
567 || return(0);
568 $r =~ s/^....//; # maybe something a little better here?
569 $r = db64($r);
570 $req->input($r);
571 return(0) if (!$req->auth_ok);
572
573 do_smtp_gen("", '235', undef, '',
574 $G::auth_showpt ? "" : '') || return(0);
575 return(1);
576}
577
578# This can handle both CRAM-MD5 and CRAM-SHA1
579sub do_smtp_auth_cram {
580 my $u = shift; # auth user
581 my $p = shift; # auth password
582 my $as = shift; # auth string
583 my $r = ''; # will store smtp response
584
585 my $auth_string = "AUTH $as";
586 do_smtp_gen($auth_string, '334', \$r, '', '',
587 $G::auth_showpt ? \&unencode_smtp : '')
588 || return(0);
589
590 $r =~ s/^....//; # maybe something a little better here?
591 # specify which type of digest we need based on $as
592 my $d = get_digest($p, $r, ($as =~ /-SHA1$/ ? 'sha1' : 'md5'));
593 $auth_string = eb64("$u $d");
594
595 do_smtp_gen($auth_string, '235', undef, '',
596 $G::auth_showpt ? "$u $d" : '') || return(0);
597 return(1);
598}
599
600sub do_smtp_auth_login {
601 my $u = shift; # auth user
602 my $p = shift; # auth password
603 my $as = shift; # auth string
604 my $z = '';
605
606 my $auth_string = "AUTH $as";
607 do_smtp_gen($auth_string, '334', undef, '', '',
608 $G::auth_showpt ? \&unencode_smtp : '') || return(0);
609 $auth_string = eb64($u);
610 $z = $u if ($G::auth_showpt);
611 do_smtp_gen($auth_string, '334', undef, '', $z,
612 $G::auth_showpt ? \&unencode_smtp : '') || return(0);
613 $auth_string = eb64($p);
614 $z = $p if ($G::auth_showpt);
615 do_smtp_gen($auth_string, '235', undef, '', $z) || return(0);
616 return(1);
617}
618
619sub do_smtp_auth_plain {
620 my $u = shift; # auth user
621 my $p = shift; # auth password
622 my $as = shift; # auth string
623
624 my $auth_string = "AUTH $as " . eb64("\0$u\0$p");
625 my $z = '';
626 if ($G::auth_showpt) {
627 $z = "AUTH $as \\0$u\\0$p";
628 }
629 return(do_smtp_gen($auth_string, '235', undef, '', $z));
630}
631
632sub do_smtp_helo {
633 my $h = shift; # helo string to use
634 my $e = shift; # this is a hashref that will be populated w/ server options
635 my $p = shift; # protocol for the transaction
636 my $r = ''; # this'll be populated by do_smtp_gen
637
638 if ($p eq 'esmtp' || $p eq 'lmtp') {
639 my $l = $p eq 'lmtp' ? "LHLO" : "EHLO";
640 if (do_smtp_gen("$l $h", '250', \$r)) {
641 # $ehlo is designed to hold the advertised options, but I'm not sure how
642 # to store them all - for instance, SIZE is a simple key/value pair, but
643 # AUTH lends itself more towards a multilevel hash. What I'm going to do
644 # is come here and add each key in the way that makes most sense in each
645 # case. I only need auth for now.
646 foreach my $l (split(/\n/, $r)) {
647 $l =~ s/^....//;
648 if ($l =~ /^AUTH=?(.*)$/) {
649 map { $e->{AUTH}{uc($_)} = 1 } (split(' ', $1));
650 } elsif ($l =~ /^STARTTLS$/) {
651 $e->{STARTTLS} = 1;
652 } elsif ($l =~ /^PIPELINING$/) {
653 $e->{PIPELINING} = 1;
654 $G::pipeline_adv = 1;
655 }
656 }
657 return(1);
658 }
659 }
660 if ($p eq 'esmtp' || $p eq 'smtp') {
661 return(do_smtp_gen("HELO $h", '250'));
662 }
663
664 return(0);
665}
666
667sub do_smtp_mail {
668 my $m = shift; # from address
669
670 transact(cxn_string => "MAIL FROM:<$m>", expect => '250', defer => 1,
671 fail_callback => \&smtp_mail_callback);
672
673 return(1); # the callback handles failures, so just return here
674}
675
676# this only really needs to exist until I figure out a clever way of making
677# do_smtp_quit the callback while still preserving the exit codes
678sub smtp_mail_callback {
679 do_smtp_quit(1, 23);
680}
681
682sub do_smtp_rcpt {
683 my $m = shift; # string of comma separated recipients
684 my $f = 0; # The number of failures we've experienced
685
686 my @a = split(/,/, $m);
687 $G::smtp_rcpt_total = scalar(@a);
688 foreach my $addr (@a) {
689 #$f++ if (!do_smtp_gen("RCPT TO:<$addr>", '250'));
690 transact(cxn_string => "RCPT TO:<$addr>", expect => '250', defer => 1,
691 fail_callback => \&smtp_rcpt_callback);
692 }
693
694 return(1); # the callback handles failures, so just return here
695
696# # if at least one addr succeeded, we can proceed, else we stop here
697# return $f == scalar(@a) ? 0 : 1;
698}
699
700sub smtp_rcpt_callback {
701 # record that a failure occurred
702 $G::smtp_rcpt_failures++;
703
704 # if the number of failures is the same as the total rcpts (if every rcpt
705 # rejected), quit.
706 if ($G::smtp_rcpt_failures == $G::smtp_rcpt_total) {
707 do_smtp_quit(1, 24);
708 }
709}
710
711sub do_smtp_data {
712 my $m = shift; # string to send
713 my $b = shift; # be brief in the data we send
714 my $calls = $G::smtp_rcpt_total - $G::smtp_rcpt_failures;
715
716 my $ok = transact(cxn_string => $m, expect => '250', summarize_output => $b);
717
718 # now be a little messy - lmtp is not a lockstep after data - we need to
719 # listen for as many calls as we had accepted recipients
720 if ($G::protocol eq 'lmtp') {
721 foreach my $c (1..($calls-1)) { # -1 because we already got 1 above
722 $ok += transact(cxn_string => undef, expect => '250');
723 }
724 }
725 return($ok)
726}
727
728sub do_smtp_gen {
729 my $m = shift; # string to send
730 my $e = shift; # String we're expecting to get back
731 my $p = shift; # this is a scalar ref, assign the server return string to it
732 my $b = shift; # be brief in the data we send
733 my $x = shift; # if this is populated, print this instead of $m
734 my $c = shift; # if this is a code ref, call it on the return value b4 print
735 my $r = ''; # This'll be the return value from transact()
736 my $time;
737
738 return transact(cxn_string => $m, expect => $e, return_text => $p,
739 summarize_output => $b, show_string => $x,
740 print_callback => $c);
741}
742
743# If we detect that the other side has gone away when we were expecting
744# to still be reading, come in here to error and die. Abstracted because
745# the error message will vary depending on the type of connection
746sub handle_disconnect {
747 my $e = shift || 6; # this is the code we will exit with
748 if ($G::link{type} eq 'socket-inet') {
749 ptrans(12, "Remote host closed connection unexpectedly.");
750 } elsif ($G::link{type} eq 'socket-unix') {
751 ptrans(12, "Socket closed connection unexpectedly.");
752 } elsif ($G::link{type} eq 'pipe') {
753 ptrans(12, "Child process closed connection unexpectedly.");
754 }
755 exit($e);
756}
757
758sub flush_send_buffer {
759 my $s = $G::link{type} eq 'pipe' ? $G::link{sock}->{wr} : $G::link{sock};
760 return if (!$G::send_buffer);
761 if ($G::link{tls}{active}) {
762 my $res = Net::SSLeay::write($G::link{tls}{ssl}, $G::send_buffer);
763 } else {
764 print $s $G::send_buffer;
765 }
766 $G::send_buffer = '';
767}
768
769sub send_data {
770 my $d = shift; # data to write
771 $G::send_buffer .= "$d\r\n";
772}
773
774sub recv_line {
775 # Either an IO::Socket obj or a FH to my child - the thing to read from
776 my $s = $G::link{type} eq 'pipe' ? $G::link{sock}->{re} : $G::link{sock};
777 my $r = undef;
778
779 if ($G::link{tls}{active}) {
780 $r = Net::SSLeay::read($G::link{tls}{ssl});
781 } else {
782 $r = <$s>;
783 }
784 $r =~ s|\r||msg;
785#print "in recv_line, returning \$r = $r\n";
786 return($r);
787}
788
789# any request which has immediate set will be checking the return code.
790# any non-immediate request will handle results through fail_callback().
791# therefore, only return the state of the last transaction attempted,
792# which will always be immediate
793# We still need to reimplement timing
794sub transact {
795 my %h = @_; # this is an smtp transaction element
796 my $ret = 1; # this is our return value
797 my @handlers = (); # will hold and fail_handlers we need to run
798 my $time = ''; # used in time lapse calculations
799
800 push(@G::pending_send, \%h); # push onto send queue
801 if (!($G::pipeline && $G::pipeline_adv) || !$h{defer}) {
802
803 if ($G::show_time_lapse) {
804 if ($G::show_time_hires) { $time = [Time::HiRes::gettimeofday()]; }
805 else { $time = time(); }
806 }
807
808 while (my $i = shift(@G::pending_send)) {
809 if ($i->{cxn_string}) {
810 ptrans(21,$i->{show_string}||$i->{cxn_string},$i->{summarize_output});
811 send_data($i->{cxn_string});
812 }
813 push(@G::pending_recv, $i);
814 }
815 flush_send_buffer();
816 while (my $i = shift(@G::pending_recv)) {
817 my $buff = '';
818 eval {
819 local $SIG{'ALRM'} = sub {
820 $buff ="Timeout ($G::link{timeout} secs) waiting for server response";
821 die;
822 };
823 alarm($G::link{timeout});
824 while ($buff !~ /^\d\d\d /m) {
825 my $l = recv_line();
826 $buff .= $l;
827 if (!defined($l)) {
828 $G::link{lost_cxn} = 1;
829 last;
830 }
831 }
832 chomp($buff);
833 alarm(0);
834 };
835
836 if ($G::show_time_lapse) {
837 if ($G::show_time_hires) {
838 $time = sprintf("%0.03f", Time::HiRes::tv_interval($time,
839 [Time::HiRes::gettimeofday()]));
840 ptrans(11, "response in ${time}s");
841 $time = [Time::HiRes::gettimeofday()];
842 } else {
843 $time = time() - $time;
844 ptrans(11, "response in ${time}s");
845 $time = time();
846 }
847 }
848
849 ${$i->{return_text}} = $buff;
850 $buff = &{$i->{print_callback}}($buff)
851 if (ref($i->{print_callback}) eq 'CODE');
852 my $ptc;
853 ($ret,$ptc) = $buff !~ /^$i->{expect} /m ? (0,32) : (1,31);
854 ptrans($ptc, $buff) if ($buff);
855 if ($G::link{lost_cxn}) {
856 if ($G::link{allow_lost_cxn}) {
857 # this means the calling code wants to handle a lost cxn itself
858 return($ret);
859 } else {
860 # if caller didn't want to handle, we'll handle a lost cxn ourselves
861 handle_disconnect();
862 }
863 }
864 if (!$ret && ref($i->{fail_callback}) eq 'CODE') {
865 push(@handlers, $i->{fail_callback});
866 }
867 }
868 }
869 foreach my $h (@handlers) { &{$h}(); }
870 return($ret);
871}
872
873sub unencode_smtp {
874 my $t = shift;
875
876 my @t = split(' ', $t);
877 return("$t[0] " . db64($t[1]));
878}
879
880sub process_file {
881 my $f = shift;
882 my $h = shift;
883
884 if (! -e "$f") {
885 ptrans(12, "File $f does not exist, skipping");
886 return;
887 } elsif (! -f "$f") {
888 ptrans(12, "File $f is not a file, skipping");
889 return;
890 } elsif (!open(I, "<$f")) {
891 ptrans(12, "Couldn't open $f, skipping... ($!)");
892 return;
893 }
894
895 while (<I>) {
896 chomp;
897 next if (/^#?\s*$/); # skip blank lines and those that start w/ '#'
898 my($key,$value) = split(' ', $_, 2);
899 $h->{uc($key)} = $value;
900 }
901 return;
902}
903
904sub interact {
905 my($prompt) = shift;
906 my($regexp) = shift;
907 my($continue) = shift;
908 my($response) = '';
909
910 do {
911 print "$prompt";
912 chomp($response = <STDIN>);
913 } while ($regexp ne 'SKIP' && $response !~ /$regexp/);
914
915 return($response);
916}
917
918sub get_hostname {
919 # in some cases hostname returns value but gethostbyname doesn't.
920 return("") if (!avail("hostname"));
921 my $h = hostname();
922 return("") if (!$h);
923 my $l = (gethostbyname($h))[0];
924 return($l || $h);
925}
926
927sub get_server {
928 my $addr = shift;
929 my $pref = -1;
930 my $server = "localhost";
931
932 if ($addr =~ /\@\[(\d+\.\d+\.\d+\.\d+)\]$/) {
933 # handle automatic routing of domain literals (user@[1.2.3.4])
934 return($1);
935 } elsif ($addr =~ /\@\#(\d+)$/) {
936 # handle automatic routing of decimal domain literals (user@#16909060)
937 $addr = $1;
938 return(($addr/(2**24))%(2**8) . '.' . ($addr/(2**16))%(2**8) . '.'
939 .($addr/(2**8))%(2**8) . '.' . ($addr/(2**0))%(2**8));
940 }
941
942
943
944 if (!avail("dns")) {
945 ptrans(12, avail_str("dns").". Using $server as mail server");
946 return($server);
947 }
948 my $res = new Net::DNS::Resolver;
949
950 return($server) if ($addr !~ /\@/);
951
952 $addr =~ s/^.*\@([^\@]*)$/$1/;
953 return($server) if (!$addr);
954 $server = $addr;
955
956 my @mx = mx($res, $addr);
957 foreach my $rr (@mx) {
958 if ($rr->preference < $pref || $pref == -1) {
959 $pref = $rr->preference;
960 $server = $rr->exchange;
961 }
962 }
963 return($server);
964}
965
966sub load {
967 my $m = shift;
968
969 return $G::modules{$m} if (exists($G::modules{$m}));
970 eval("use $m");
971 return $G::modules{$m} = $@ ? 0 : 1;
972}
973
974# Currently this is just an informational string - it's set on both
975# success and failure. It currently has four output formats (supported,
976# supported but not optimal, unsupported, unsupported and missing optimal)
977sub avail_str { return $G::dependencies{$_[0]}{errstr}; }
978
979sub avail {
980 my $f = shift; # this is the feature we want to check support for (auth, tls)
981 my $s = \%G::dependencies;
982
983 # return immediately if we've already tested this.
984 return($s->{$f}{avail}) if (exists($s->{$f}{avail}));
985
986 $s->{$f}{req_failed} = [];
987 $s->{$f}{opt_failed} = [];
988 foreach my $m (@{$s->{$f}{req}}) {
989 push(@{$s->{$f}{req_failed}}, $m) if (!load($m));
990 }
991 foreach my $m (@{$s->{$f}{opt}}) {
992 push(@{$s->{$f}{opt_failed}}, $m) if (!load($m));
993 }
994
995 if (scalar(@{$s->{$f}{req_failed}})) {
996 $s->{$f}{errstr} = "$s->{$f}{name} not available: requires "
997 . join(', ', @{$s->{$f}{req_failed}});
998 if (scalar(@{$s->{$f}{opt_failed}})) {
999 $s->{$f}{errstr} .= ". Also missing optimizing "
1000 . join(', ', @{$s->{$f}{opt_failed}});
1001 }
1002 return $s->{$f}{avail} = 0;
1003 } else {
1004 if (scalar(@{$s->{$f}{opt_failed}})) {
1005 $s->{$f}{errstr} = "$s->{$f}{name} supported, but missing optimizing "
1006 . join(', ', @{$s->{$f}{opt_failed}});
1007 } else {
1008 $s->{$f}{errstr} = "$s->{$f}{name} supported";
1009 }
1010 return $s->{$f}{avail} = 1;
1011 }
1012}
1013
1014sub get_digest {
1015 my $secr = shift;
1016 my $chal = shift;
1017 my $type = shift || 'md5';
1018 my $ipad = chr(0x36) x 64;
1019 my $opad = chr(0x5c) x 64;
1020
1021 if ($chal !~ /^</) {
1022 chomp($chal = db64($chal));
1023 }
1024
1025 if (length($secr) > 64) {
1026 if ($type eq 'md5') {
1027 $secr = Digest::MD5::md5($secr);
1028 } elsif ($type eq 'sha1') {
1029 $secr = Digest::SHA1::sha1($secr);
1030 }
1031 } else {
1032 $secr .= chr(0) x (64 - length($secr));
1033 }
1034
1035 my $digest = $type eq 'md5'
1036 ? Digest::MD5::md5_hex(($secr ^ $opad),
1037 Digest::MD5::md5(($secr ^ $ipad), $chal))
1038 : Digest::SHA1::sha1_hex(($secr ^ $opad),
1039 Digest::SHA1::sha1(($secr ^ $ipad), $chal));
1040 return($digest);
1041}
1042
1043sub test_support {
1044 my $s = \%G::dependencies;
1045
1046 foreach my $act (sort { $s->{$a}{name} cmp $s->{$b}{name} } keys %$s) {
1047 ptrans(avail($act) ? 11 : 12, avail_str($act));
1048 #if (avail($act)) {
1049 # #ptrans(11, "$s->{$act}{name} supported");
1050 # ptrans(11, avail_err($act));
1051 #} else {
1052 # ptrans(12, avail_err($act));
1053 #}
1054 }
1055}
1056
1057sub time_to_seconds {
1058 my $t = shift || 30;
1059
1060 if ($t !~ /^(\d+)([hms])?/i) {
1061 return(30); # error condition - just use default value
1062 } else {
1063 my $r = $1;
1064 my $u = lc($2);
1065 if ($u eq 'h') {
1066 return($r * 3600);
1067 } elsif ($u eq 'm') {
1068 return($r * 60);
1069 } else {
1070 return($r);
1071 }
1072 }
1073}
1074
1075# A couple of global options are set in here, they will be in the G:: namespace
1076sub process_args {
1077 my $o = shift; # This is the args we got from command line
1078 my %n = (); # This is the hash we will return w/ the fixed-up args
1079 my $fconf = {}; # Hold config info from -l file if specified
1080
1081 # load the $fconf hash if user has specified a -l file
1082 process_file($o->{option_file}, $fconf) if ($o->{option_file});
1083
1084 $G::dump_args = 1 if ($o->{dump_args});
1085 $G::suppress_data = 1 if ($o->{suppress_data});
1086 $G::no_hints = 1 if ($o->{no_hints});
1087 $G::hide_send = 1 if ($o->{hide_send});
1088 $G::hide_receive = 1 if ($o->{hide_receive});
1089 $G::pipeline = 1 if ($o->{pipeline});
1090 $G::silent = $o->{silent} ? $o->{silent} : 0;
1091
1092 my %protos = (
1093 smtp => { proto => 'smtp', auth => 0, tls => '0' },
1094 ssmtp => { proto => 'esmtp', auth => 0, tls => 'c' },
1095 ssmtpa => { proto => 'esmtp', auth => 1, tls => 'c' },
1096 smtps => { proto => 'smtp', auth => 0, tls => 'c' },
1097 esmtp => { proto => 'esmtp', auth => 0, tls => '0' },
1098 esmtpa => { proto => 'esmtp', auth => 1, tls => '0' },
1099 esmtps => { proto => 'esmtp', auth => 0, tls => 's' },
1100 esmtpsa => { proto => 'esmtp', auth => 1, tls => 's' },
1101 lmtp => { proto => 'lmtp', auth => 0, tls => '0' },
1102 lmtpa => { proto => 'lmtp', auth => 1, tls => '0' },
1103 lmtps => { proto => 'lmtp', auth => 0, tls => 's' },
1104 lmtpsa => { proto => 'lmtp', auth => 1, tls => 's' },
1105 );
1106 $G::protocol = lc($o->{mail_protocol}) || 'esmtp';
1107 if (!$protos{$G::protocol}) {
1108 ptrans(12, "Unknown protocol $G::protocol specified, exiting");
1109 exit(1);
1110 }
1111 if ($protos{$G::protocol}{auth} && !$o->{auth_user} && !$o->{auth_pass} &&
1112 !$o->{auth_optional} && !$o->{auth})
1113 {
1114 $o->{auth} = ''; # cause auth to be processed below
1115 }
1116 if ($protos{$G::protocol}{tls} && !$o->{tls} && !$o->{tls_optional} &&
1117 !$o->{tls_on_connect})
1118 {
1119 if ($protos{$G::protocol}{tls} eq 's') {
1120 $o->{tls} = '';
1121 } elsif ($protos{$G::protocol}{tls} eq 'c') {
1122 $o->{tls_on_connect} = '';
1123 }
1124 }
1125 $G::protocol = $protos{$G::protocol}{proto};
1126
1127 # set global option of -q option
1128 if ($o->{quit_after}) {
1129 $G::quit_after = lc($o->{quit_after});
1130 if ($G::quit_after =~ /^[el]hlo$/) { $G::quit_after = 'helo'; }
1131 elsif ($G::quit_after =~ /first-[el]hlo/) { $G::quit_after = 'first-helo'; }
1132 elsif ($G::quit_after eq 'starttls') { $G::quit_after = 'tls'; }
1133 elsif ($G::quit_after eq 'from') { $G::quit_after = 'mail'; }
1134 elsif ($G::quit_after eq 'to') { $G::quit_after = 'rcpt'; }
1135 elsif ($G::quit_after ne 'connect' && $G::quit_after ne 'first-helo' &&
1136 $G::quit_after ne 'tls' && $G::quit_after ne 'helo' &&
1137 $G::quit_after ne 'auth' && $G::quit_after ne 'mail' &&
1138 $G::quit_after ne 'rcpt')
1139 {
1140 ptrans(12, "Unknown quit value $G::quit_after, exiting");
1141 exit(1);
1142 }
1143 # only rcpt _requires_ a to address
1144 $G::server_only = 1 if ($G::quit_after ne 'rcpt');
1145 } else {
1146 $G::quit_after = '';
1147 }
1148
1149 # set global flag for -stl flag
1150 $G::show_time_lapse = time() if (defined($o->{show_time_lapse}));
1151 $G::show_time_hires = 1 if ($G::show_time_lapse &&
1152 avail("hires_timing") &&
1153 $o->{show_time_lapse} !~ /^i/i);
1154
1155 if ($o->{emulate_mail}) { # set up for -m option
1156 $n{to} = shift if (!defined($o->{mail_to}));
1157 $o->{mail_data} = ''; # define it here so we get it on stdin later
1158 }
1159
1160 # pipe command, if one is specified
1161 $G::link{process} = $o->{pipe_cmd} || interact("Pipe: ", '^.+$')
1162 if (defined($o->{pipe_cmd}));
1163 $G::link{process} ||= $fconf->{PIPE} || "";
1164 if ($G::link{process}) { $G::link{type} = 'pipe'; }
1165 else { delete($G::link{process}); }
1166
1167 # socket file, if one is specified
1168 $G::link{sockfile} = $o->{socket} || interact("Socket File: ", '^.+$')
1169 if (defined($o->{socket}));
1170 $G::link{sockfile} ||= $fconf->{SOCKET} || "";
1171 if ($G::link{sockfile}) { $G::link{type} = 'socket-unix'; }
1172 else { delete($G::link{sockfile}); }
1173
1174 my $user = get_username($o->{force_getpwuid});
1175 my $hostname = get_hostname();
1176
1177 # SMTP mail from
1178 $n{from} = $o->{mail_from} || interact("From: ", '^.*$')
1179 if (defined($o->{mail_from}));
1180 $n{from} ||= $fconf->{FROM} || ($hostname || ($G::server_only &&
1181 $G::quit_after ne 'mail')
1182 ? "$user\@$hostname"
1183 : interact("From: ", '^.*$'));
1184 $n{from} = '' if ($n{from} eq '<>');
1185
1186 # SMTP helo/ehlo
1187 $n{helo} = $o->{mail_helo} || interact("Helo: ", '^.*$')
1188 if (defined($o->{mail_helo}));
1189 $n{helo} ||= $fconf->{HELO} || ($hostname || ($G::quit_after eq 'connect')
1190 ? $hostname
1191 : interact("Helo: ", '^.*$'));
1192
1193 # SMTP server and rcpt to are interdependant, so they are handled together
1194 $G::link{server} = $o->{mail_server} || interact("Server: ", '^.*$')
1195 if (defined($o->{mail_server}));
1196 $G::link{server} ||= $fconf->{SERVER};
1197 $n{to} = $o->{mail_to} || interact("To: ", '^.*$')
1198 if (defined($o->{mail_to}));
1199 $n{to} ||= $fconf->{TO};
1200 $n{to} = interact("To: ", '^.*$')
1201 if (!$n{to} && !($G::server_only && ($G::link{server} ||
1202 $G::link{type} eq 'socket-unix' ||
1203 $G::link{type} eq 'pipe')));
1204 if (!$G::link{type}) {
1205 $G::link{server} = get_server($n{to}) if (!$G::link{server});
1206 $G::link{type} = "socket-inet";
1207 }
1208
1209 # Verify we are able to handle the requested transport
1210 if ($G::link{type} eq 'pipe') {
1211 if (!avail("pipe")) {
1212 ptrans(12, avail_str("pipe").". Exiting");
1213 exit(2);
1214 }
1215 } else {
1216 if (!avail("socket")) {
1217 ptrans(12, avail_str("socket").". Exiting");
1218 exit(2);
1219 }
1220 }
1221
1222 # local interface to connect from
1223 $G::link{lint} = $o->{lint} || interact("Interface: ", '^.*$')
1224 if (defined($o->{lint}));
1225 $G::link{lint} ||= $fconf->{INTERFACE} || '0.0.0.0';
1226
1227 # SMTP timeout
1228 $o->{timeout} = '0s' if ($o->{timeout} eq '0'); # used 'eq' on purpose
1229 $G::link{timeout} = $o->{timeout} || interact("Timeout: ", '^\d+[hHmMsS]?$')
1230 if (defined($o->{timeout}));
1231 $G::link{timeout} ||= $fconf->{TIMEOUT} || '30s';
1232 $G::link{timeout} = time_to_seconds($G::link{timeout});
1233
1234 my $body = 'This is a test mailing'; # default message body
1235 my $bound = "";
1236 my $stdin = undef;
1237 if (defined($o->{body_822})) {
1238 # the --body option is the entire 822 body and trumps and other options
1239 # that mess with the body
1240 if (!$o->{body_822}) {
1241 $body = interact("Body: ", '.+');
1242 } elsif ($o->{body_822} eq '-') {
1243 $stdin = join('', <STDIN>);
1244 $body = $stdin;
1245 } else {
1246 $body = $o->{body_822};
1247 }
1248 if (open(I, "<$body")) {
1249 $body = join('', <I>);
1250 close(I);
1251 }
1252 }
1253 if (scalar(@{$o->{attach_822}})) {
1254 # this option is a list of files (or STDIN) to attach. In this case,
1255 # the message become a mime message and the "body" goes in the
1256 # first text/plain part
1257 my $mime_type = 'application/octet-stream';
1258 my @parts = ( { body => $body, type => 'text/plain' } );
1259 $bound = "----=_MIME_BOUNDARY_000_$$";
1260 while (defined(my $t = shift(@{$o->{attach_822}}))) {
1261 if ($t =~ m|^[^/]+/[^/]+$| && !stat($t)) {
1262 $mime_type = $t;
1263 } else {
1264 push(@parts, { body => "$t", type => $mime_type });
1265 }
1266 }
1267 $body = '';
1268 foreach my $p (@parts) {
1269 if ($p->{body} eq '-') {
1270 if ($stdin) {
1271 $p->{body} = $stdin;
1272 } else {
1273 $p->{body} = join('', <STDIN>);
1274 $stdin = $p->{body};
1275 }
1276 } elsif (open(I, "<$p->{body}")) {
1277 $p->{body} = join('', <I>);
1278 close(I);
1279 }
1280 $body .= "--$bound\n"
1281 . "Content-Type: $p->{type}\n";
1282 if ($p->{type} =~ m|^text/plain$|i) {
1283 $body .= "\n" . $p->{body} . "\n";
1284 } else {
1285 $body .= "Content-Transfer-Encoding: BASE64\n"
1286 . "Content-Disposition: attachment\n"
1287 . "\n"
1288 . eb64($p->{body}, "\n") . "\n";
1289 }
1290 }
1291 $body .= "--$bound--\n";
1292 }
1293
1294 # add-header option. In a strict technical sense all this is is a text
1295 # string that will replace %H in the DATA. Because of where %H is placed
1296 # in the default DATA, in practice this is used to add headers to the stock
1297 # DATA w/o having to craft a custom DATA portion
1298 #if (scalar(@{$o->{add_header}})) {
1299 # $n{add_header} = join("\n", @{$o->{add_header}}) . "\n";
1300 #}
1301 @{$o->{add_header}} = map { split(/\\n/) } @{$o->{add_header}};
1302
1303 # SMTP DATA
1304 # a '-' arg to -d is the same as setting -g
1305 if ($o->{mail_data} eq '-') {
1306 undef($o->{mail_data});
1307 $o->{data_on_stdin} = 1;
1308 }
1309 if (defined($o->{mail_data}) && !defined($o->{data_on_stdin})) {
1310 if (defined($o->{emulate_mail})) {
1311 $n{data} = "Subject: " . interact("Subject: ", 'SKIP') . "\n\n";
1312 do {
1313 $n{data} .= interact('', 'SKIP') . "\n";
1314 } while ($n{data} !~ /\n\.\n$/ms);
1315 $n{data} =~ s/\n\.\n$//ms;
1316 } else {
1317 $n{data} = $o->{mail_data} || interact("Data: ", '^.*$');
1318 }
1319 }
1320 $n{data} ||= $fconf->{DATA}
1321 || 'Date: %D\nTo: %T\nFrom: %F\nSubject: test %D\n'
1322 ."X-Mailer: swaks v$p_version jetmore.org/john/code/#swaks".'\n'
1323 . ($bound ? 'MIME-Version: 1.0\n'
1324 .'Content-Type: multipart/mixed; '
1325 .'boundary="'.$bound.'"\n'
1326 : '')
1327 .'%H' # newline will be added in replacement if it exists
1328 .'\n'
1329 .'%B\n';
1330 # The -g option trumps all other methods of getting the data
1331 $n{data} = join('', <STDIN>) if ($o->{data_on_stdin});
1332 if (!$o->{no_data_fixup}) {
1333 $n{data} =~ s/%B/$body/g;
1334 if (scalar(@{$o->{header}})) {
1335 my %matched = ();
1336 foreach my $l (map { split(/\\n/) } @{$o->{header}}) {
1337 if (my($h) = $l =~ /^([^:]+):/) {
1338 if (!$matched{$h} && $n{data} =~ s/(^|\\n)$h:.*?($|\\n)/$1$l$2/) {
1339 $matched{$h} = 1;
1340 } else { push(@{$o->{add_header}}, $l); }
1341 } else { push(@{$o->{add_header}}, $l); }
1342 }
1343 }
1344 $n{add_header} = join('\n', @{$o->{add_header}}) . "\n"
1345 if (@{$o->{add_header}});
1346 $n{data} =~ s/%H/$n{add_header}/g;
1347 $n{data} =~ s/\\n/\r\n/g;
1348 $n{data} =~ s/%F/$n{from}/g;
1349 $n{data} =~ s/%T/$n{to}/g;
1350 $n{data} =~ s/%D/get_date_string()/eg;
1351 $n{data} =~ s/^From [^\n]*\n// if (!$O{no_strip_from});
1352 $n{data} =~ s/\r?\n\.\r?\n?$//s; # If there was a trailing dot, remove it
1353 $n{data} =~ s/\n\./\n../g; # quote any other leading dots
1354 # translate line endings - run twice to get consecutive \n correctly
1355 $n{data} =~ s/([^\r])\n/$1\r\n/gs;
1356 $n{data} =~ s/([^\r])\n/$1\r\n/gs; # this identical call not a bug
1357 $n{data} .= "\r\n."; # add a trailing dot
1358 }
1359
1360 # Handle TLS options
1361 $G::tls_optional = 1 if (defined($o->{tls_optional}));
1362 $G::tls = 1 if (defined($o->{tls}) || $G::tls_optional);
1363 $G::tls_on_connect = 1 if (defined($o->{tls_on_connect}));
1364 $G::link{tls}{active} = 0;
1365 if ($G::tls || $G::tls_on_connect) {
1366 if (!avail("tls")) {
1367 if ($G::tls_optional) {
1368 $G::tls = undef; # so we won't try it later
1369 ptrans(12,avail_str("tls").". Skipping optional TLS");
1370 } else {
1371 ptrans(12,avail_str("tls").". Exiting");
1372 exit(10);
1373 }
1374 }
1375 }
1376
1377 # SMTP port
1378 $G::link{port} = $o->{mail_port} || interact("Port: ", '^\w+$')
1379 if (defined($o->{mail_port}));
1380 $G::link{port} ||= $fconf->{PORT};
1381 if ($G::link{port}) {
1382 # in here, the user has either specified a port, or that they _want_
1383 # to, so if it isn't a resolvable port, ,keep prompting for another one
1384 my $o_port = $G::link{port};
1385 if ($G::link{port} !~ /^\d+$/) {
1386 $G::link{port} = getservbyname($G::link{port}, 'tcp');
1387 while (!$G::link{port}) {
1388 $G::link{port} = $o_port =
1389 interact("Unable to resolve port $o_port\nPort: ", '^\w+$');
1390 $G::link{port} = getservbyname($G::link{port}, 'tcp')
1391 if ($G::link{port} !~ /^\d+$/);
1392 }
1393 }
1394 } else {
1395 # in here, user wants us to use default ports, so try look up services,
1396 # use default numbers is service names don't resolve. Never prompt user
1397 if ($G::protocol eq 'lmtp') {
1398 $G::link{port} = getservbyname('lmtp', 'tcp') || '24';
1399 } elsif ($G::tls_on_connect) {
1400 $G::link{port} = getservbyname('smtps', 'tcp') || '465';
1401 } else {
1402 $G::link{port} = getservbyname('smtp', 'tcp') || '25';
1403 }
1404 }
1405
1406
1407 # Handle AUTH options
1408 $G::auth_optional = 1 if (defined($o->{auth_optional}));
1409 $o->{auth_types} = [];
1410 if ($o->{auth}) {
1411 @{$o->{auth_types}} = map { uc($_) } (split(/,/, $o->{auth}));
1412 } elsif ($o->{auth_optional}) {
1413 @{$o->{auth_types}} = map { uc($_) } (split(/,/, $o->{auth_optional}));
1414 } elsif (defined($o->{auth_user}) || defined($o->{auth_pass}) ||
1415 $G::auth_optional || (defined($o->{auth}) && !$o->{auth}))
1416 {
1417 $o->{auth_types}[0] = 'ANY';
1418 $o->{auth} = 'ANY'; # this is checked below
1419 }
1420 # if after that processing we've defined some auth type, do some more
1421 # specific processing
1422 if (scalar(@{$o->{auth_types}})) {
1423 # there's a lot of option processing below. If any type looks like it
1424 # will succeed later, set this to true
1425 my $valid_auth_found = 0;
1426
1427 # handle the --auth-map options plus our default mappings
1428 foreach (split(/\s+,\s+/, $o->{auth_map}),"PLAIN=PLAIN","LOGIN=LOGIN",
1429 "CRAM-MD5=CRAM-MD5","DIGEST-MD5=DIGEST-MD5","CRAM-SHA1=CRAM-SHA1",
1430 "NTLM=NTLM","SPA=NTLM","MSN=NTLM")
1431 {
1432 my($alias,$type) = split(/=/, uc($_), 2);
1433 # this gives us a list of all aliases and what the alias
1434 $G::auth_map_f{$alias} = $type;
1435 # this gives a list of all base types and any aliases for it.
1436 $G::auth_map_t{$type} ||= [];
1437 push(@{$G::auth_map_t{$type}}, $alias);
1438 }
1439 if (!avail("auth")) { # check for general auth requirements
1440 if ($G::auth_optional) {
1441 ptrans(12, avail_str("auth"). ". Skipping optional AUTH");
1442 } else {
1443 ptrans(12, avail_str("auth"). ". Exiting");
1444 exit(10);
1445 }
1446 } else {
1447 # if the user doesn't specify an auth type, create a list from our
1448 # auth-map data. Simplifies processing later
1449 if ($o->{auth_types}[0] eq 'ANY') {
1450 $o->{auth_types} = [sort keys %G::auth_map_f];
1451 }
1452
1453 foreach my $type (@{$o->{auth_types}}) {
1454 # we need to evaluate whether we will be able to run the auth types
1455 # specified by the user
1456
1457 if (!$G::auth_map_f{$type}) {
1458 ptrans(12, "$type is not a recognized auth type, skipping");
1459 }
1460
1461 elsif ($G::auth_map_f{$type} eq 'CRAM-MD5' && !avail("auth_cram_md5"))
1462 {
1463 ptrans(12, avail_str("auth_cram_md5")) if ($o->{auth} ne 'ANY');
1464 }
1465
1466 elsif ($G::auth_map_f{$type} eq 'CRAM-SHA1' && !avail("auth_cram_sha1"))
1467 {
1468 ptrans(12, avail_str("auth_cram_sha1")) if ($o->{auth} ne 'ANY');
1469 }
1470
1471 elsif ($G::auth_map_f{$type} eq 'NTLM' && !avail("auth_ntlm"))
1472 {
1473 ptrans(12, avail_str("auth_ntlm")) if ($o->{auth} ne 'ANY');
1474 }
1475
1476 elsif ($G::auth_map_f{$type} eq 'DIGEST-MD5' &&
1477 !avail("auth_digest_md5"))
1478 {
1479 ptrans(12, avail_str("auth_digest_md5")) if ($o->{auth} ne 'ANY');
1480 }
1481
1482 else {
1483 $valid_auth_found = 1;
1484 push(@{$n{a_type}}, $type);
1485 }
1486
1487 } # foreach
1488
1489 if (!$valid_auth_found) {
1490 ptrans(12, "No auth types supported");
1491 if (!$G::auth_optional) {
1492 exit(10);
1493 }
1494 $n{a_user} = $n{a_pass} = $n{a_type} = undef;
1495 } else {
1496 $n{a_user} = $o->{auth_user} if (defined($o->{auth_user}));
1497 $n{a_user} ||= $fconf->{USER};
1498 $n{a_user} ||= interact("Username: ", 'SKIP');
1499 $n{a_user} = '' if ($n{a_user} eq '<>');
1500
1501 $n{a_pass} = $o->{auth_pass} if (defined($o->{auth_pass}));
1502 $n{a_pass} ||= $fconf->{PASS};
1503 $n{a_pass} ||= interact("Password: ", 'SKIP');
1504 $n{a_pass} = '' if ($n{a_pass} eq '<>');
1505
1506 $G::auth_showpt = 1 if (defined($o->{auth_showpt}));
1507 # This option is designed to hide passwords - turn echo off when
1508 # supplying at PW prompt, star out the PW strings in AUTH transactions.
1509 # Not implementing right now - the echo might be a portability issue,
1510 # and starring out is hard because the smtp transaction is abstracted
1511 # beyond where this is easy to do. Maybe sometime in the future
1512 #$G::auth_hidepw = 1 if (defined($o->{auth_hidepw}));
1513 }
1514 } # end avail("auth")
1515 } # end auth parsing
1516
1517 return(\%n);
1518}
1519
1520sub get_username {
1521 my $force_getpwuid = shift;
1522 if ($^O eq 'MSWin32') {
1523 require Win32;
1524 return Win32::LoginName();
1525 }
1526 if ($force_getpwuid) {
1527 return (getpwuid($<))[0];
1528 }
1529 return getlogin() || (getpwuid($<))[0];
1530}
1531
1532sub get_date_string {
1533 return($G::date_string) if (length($G::date_string) > 0);
1534
1535 my @l = localtime();
1536 my $o = 0;
1537
1538 if (!avail("date_manip")) {
1539 ptrans(12, avail_str("date_manip").". Date strings will be in GMT");
1540 @l = gmtime();
1541 } else {
1542 my @g = gmtime();
1543 $o = (timelocal(@l) - timelocal(@g))/36;
1544 }
1545 $G::date_string = sprintf("%s, %02d %s %d %02d:%02d:%02d %+05d",
1546 (qw(Sun Mon Tue Wed Thu Fri Sat))[$l[6]],
1547 $l[3],
1548 (qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec))[$l[4]],
1549 $l[5]+1900, $l[2], $l[1], $l[0],
1550 $o
1551 );
1552}
1553
1554# partially Cribbed from "Programming Perl" and MIME::Base64 v2.12
1555sub db64 {
1556 my $s = shift;
1557 if (load("MIME::Base64")) {
1558 return(decode_base64($s));
1559 } else {
1560 $s =~ tr#A-Za-z0-9+/##cd;
1561 $s =~ s|=+$||;
1562 $s =~ tr#A-Za-z0-9+/# -_#;
1563 my $r = '';
1564 while ($s =~ s/(.{1,60})//s) {
1565 $r .= unpack("u", chr(32 + int(length($1)*3/4)) . $1);
1566 }
1567 return($r);
1568 }
1569}
1570
1571# partially Cribbed from MIME::Base64 v2.12
1572sub eb64 {
1573 my $s = shift;
1574 my $e = shift || ''; # line ending to use "empty by default"
1575 if (load("MIME::Base64")) {
1576 return(encode_base64($s, $e));
1577 } else {
1578 my $l = length($s);
1579 chomp($s = pack("u", $s));
1580 $s =~ s|\n.||gms;
1581 $s =~ s|\A.||gms;
1582 $s =~ tr#` -_#AA-Za-z0-9+/#;
1583 my $p = (3 - $l%3) % 3;
1584 $s =~ s/.{$p}$/'=' x $p/e if ($p);
1585 $s =~ s/(.{1,76})/$1$e/g if (length($e));
1586 return($s);
1587 }
1588}
1589
1590sub ext_usage {
1591 if ($ARGV[0] =~ /^--help$/i) {
1592 require Config;
1593 $ENV{PATH} .= ":" unless $ENV{PATH} eq "";
1594 $ENV{PATH} = "$ENV{PATH}$Config::Config{'installscript'}";
1595 $< = $> = 1 if ($> == 0 || $< == 0);
1596 exec("perldoc", $0) || exit(1);
1597 # make parser happy
1598 %Config::Config = ();
1599 } elsif ($ARGV[0] =~ /^--version$/i) {
1600 print "$p_name version $p_version\n\n$p_cp\n";
1601 } else {
1602 return;
1603 }
1604
1605 exit(0);
1606}
1607
1608__END__
1609
1610=head1 NAME
1611
1612swaks - SMTP transaction tester
1613
1614=head1 USAGE
1615
1616swaks [--help|--version] | (see description of options below)
1617
1618=head1 OPTIONS
1619
1620=over 4
1621
1622=item --pipe
1623
1624This option takes as its argument a program and the program's arguments. If this option is present, swaks opens a pipe to the program and enters an SMTP transaction over that pipe rather than connecting to a remote server. Some MTAs have testing modes using stdin/stdout. This option allows you to tie into those options. For example, if you implemented DNSBL checking with exim and you wanted to make sure it was working, you could run 'swaks --pipe "exim -bh 127.0.0.2"'.
1625
1626In an ideal world the process you are talking to should behave exactly like an SMTP server on stdin and stdout. Any debugging should be sent to stderr, which will be directed to your terminal. In the real world swaks can generally handle some debug on the child's stdout, but there are no guarantees on how much it can handle.
1627
1628=item --socket
1629
1630This option takes as its argument a unix domain socket file. If this option is present, swaks enters an SMTP transaction over over the unix domains socket rather than over an internet domain socket. I think this option has uses when combined with a (yet unwritten) LMTP mode, but to be honest at this point I just implemented it because I could.
1631
1632=item -l, --input-file
1633
1634Argument to -l must be a path to a file containing TOKEN->VALUE pairs. The TOKEN and VALUE must be separated by whitespace. These tokens set values which would otherwise be set by command line arguments. See the description of the corresponding command line argument for details of each token. Valid tokens are FROM (-f), TO (-t), SERVER (-s), DATA (-d), HELO (-h), PORT (-p), INTERFACE (-li), and TIMEOUT (-to).
1635
1636=item -t, --to
1637
1638Use argument as "RCPT TO" address, or prompt user if no argument specified. Overridden by -l token TO. Multiple recipients can be specified by supplying as one comma-delimited argument.
1639
1640There is no default for this option. If no to addess is specified with -t or TO token, user will be prompted for To: address on STDIN.
1641
1642=item -f, --from
1643
1644Use argument as "MAIL FROM" address, or prompt user if no argument specified. Overridden by -l token FROM. If no from address is specified, default is user@host, where user is the best guess at user currently running program, and host is best guess at DNS hostname of local host. The string <> can be supplied to mean the null sender.
1645
1646=item -s, --server
1647
1648Use argument as mail server to which to connect, or prompt user if no argument specified. Overridden by -l token SERVER. If unspecified, swaks tries to determine primary MX of destination address. If Net::DNS module is not available, tries to connect to A record for recipient's domain.
1649
1650=item -p, --port
1651
1652Use argument as port to connect to on server, or prompt user if no argument is specified. This can be either a port number or a service name. Overridden by -l token PORT. If unspecified, swaks will use service lmtp if --protocol is LMTP, service smtps if --tls-on-connect is used, and smtp otherwise.
1653
1654=item -h, --helo, --ehlo
1655
1656Use argument as argument to SMTP EHLO/HELO command, or prompt use if no argument is specified. Overridden by -l token HELO. If unspecified, swaks uses best guess at DNS hostname of local host.
1657
1658=item -d, --data
1659
1660Use argument as DATA portion of SMTP transaction, or prompt user if no argument specified. Overridden by -l token DATA.
1661
1662This string should be on one single line, with a literal \n representing where line breaks should be placed. Leading dots will be quoted. Closing dot is not required but is allowed. Very basic token parsing is done. %F is replaced with the value that will be used for "MAIL FROM", %T is replaced with "RCPT TO" values, %D is replaced with a timestamp, %H is replaced with the contents of --add-header, and %B is replaced with the message body. See the --body option for the default body text.
1663
1664Default value for this option is "Date: %D\nTo: %T\nFrom: %F\nSubject: test %D\nX-Mailer: swaks v$p_version jetmore.org/john/code/#swaks\n%H\n\n%B\n".
1665
1666=item --body
1667
1668Specify the body of the email. The default is "This is a test mailing". If no argument to --body, you will be prompted to supply one. If '-' is supplied, body will be read from standard input. If any other text is provided and the text represents an openable file, the content of that file is used as the body. If it does not respresent an openable file, the text itself is used as the body.
1669
1670=item --attach
1671
1672When one or more --attach option is supplied, the message is changed into a multipart/mixed MIME message. The arguments to --attach are processed the same as --body with regard to stdin, file contents, etc. --attach can be supplie multiple times to create multiple attachments. By default each attachment is attached as a application/octet-stream file. See --attach-type for changing this behaviour.
1673
1674When the message changes to MIME format, the previous body (%B) is attached as a text/plain type as the first attachment. --body can still be used to specify the contents of this body attachment.
1675
1676It is legal for '-' (STDIN) to be specified as an argument multiple times (once for --body and multiple times for --attach). In this case, the same content will be attached each time it is specified. This is useful for attaching the same content with multiple MIME types.
1677
1678=item --attach-type
1679
1680By default, content that gets MIME attached to a message with the --attach option is encoded as application/octet-stream. --attach-type changes the mime type for every --attach option which follows it. It can be specified multiple times.
1681
1682=item -ah, --add-header
1683
1684In the strictest sense, all this does is provide a value that replaces the %H token in the data. Because of where %H is located in the default DATA, practically it is used to add custom headers without having to recraft the entire body.
1685
1686The option can either be specified multiple times or a single time with multiple headers seperated by a literal '\n' string. So, "--add-header 'Foo: bar' --add-header 'Baz: foo'" and "--add-header 'Foo: bar\nBaz: foo'" end up adding the same two headers.
1687
1688=item --header, --h-Header
1689
1690These options allow a way to change headers that already exist in the DATA. These two calls do the same thing:
1691
1692--header "Subject: foo"
1693--h-Subject foo
1694
1695Subject is the example used. If the header does not exist in the body already, these calls behave identically to --add-header. The purpose of this option it to provide a fast way to change the nature of the default DATA for specific tests. For instance if you wanted to test a subject filer in a mail system, you could use --h-Subject "SPAM STRING" to test rather than having to craft and entire new DATA string to pass to --data.
1696
1697=item --timeout
1698
1699Use argument as the SMTP transaction timeout, or prompt user if no argument given. Overridden by the -l token TIMEOUT. Argument can either be a pure digit, which will be interpretted as seconds, or can have a specifier s or m (5s = 5 seconds, 3m = 180 seconds). As a special case, 0 means don't timeout the transactions. Default value is 30s.
1700
1701=item --protocol
1702
1703Specify which protocol to use in the transaction. Valid options are shown in the table below. Currently the 'core' protocols are SMTP, ESMTP, and LMTP. By using variations of these protocol types one can specify default ports, whether authentication should be attempted, and the type of TLS connection that should be attempted. The default protocol is ESMTP. This table demonstrates the available arguments to --protocol and the options each sets as a side effect:
1704
1705 HELO AUTH TLS PORT
1706 --------------------------------------------------
1707 SMTP HELO smtp / 25
1708 SSMTP EHLO->HELO -tlsc smtps / 465
1709 SSMTPA EHLO->HELO -a -tlsc smtps / 465
1710 SMTPS HELO -tlsc smtps / 465
1711 ESMTP EHLO->HELO smtp / 25
1712 ESMTPA EHLO->HELO -a smtp / 25
1713 ESMTPS EHLO->HELO -tls smtp / 25
1714 ESMTPSA EHLO->HELO -a -tls smtp / 25
1715 LMTP LHLO lmtp / 24
1716 LMTPA LHLO -a lmtp / 24
1717 LMTPS LHLO -tls lmtp / 24
1718 LMTPSA LHLO -a -tls lmtp / 24
1719
1720=item -li, --local-interface
1721
1722Use argument as the local interface for the SMTP connection, or prompt user if no argument given. Overridden by the -l token INTERFACE. Argument can be an IP or a hostname. Default action is to let OS choose local interface.
1723
1724=item -g
1725
1726If specified, swaks will read the DATA value for the mail from STDIN. If there is a From_ line in the email, it will be removed (but see -nsf option). Useful for delivering real message (stored in files) instead of using example messages.
1727
1728=item -nsf, --no-strip-from
1729
1730Don't strip the From_ line from the DATA portion, if present.
1731
1732=item -n, --suppress-data
1733
1734If this option is specified, swaks summarizes the DATA portion of the SMTP transaction instead of printing every line.
1735
1736=item -q, --quit-after
1737
1738The argument to this option is used as an indicator of where to quit the SMTP transaction. It can be thought of as "quit after", with valid arguments CONNECT, FISRT-HELO, TLS, HELO, AUTH, MAIL, and RCPT. In a non-STARTTLS session, FIRST-HELO and HELO behave the same way. In a STARTTLS session, FIRST-HELO quits after the first HELO sent, while HELO quits after the second HELO is sent.
1739
1740For convenience, LHLO and EHLO are synonyms for HELO, FIRST-EHLO and FIRST-LHLO for FIRST-HELO, FROM for MAIL, and TO for RCPT.
1741
1742=item -m
1743
1744Emulate Mail command. Least used option in swaks.
1745
1746=item --support
1747
1748Cause swaks to print its capabilities and exit. Certain features require non-standard perl modules. This options evaluates whether these modules are present and lets you know which functionality is present.
1749
1750=item -S, --silent
1751
1752Cause swaks to be silent. "-S" causes swaks to print no output until an error occurs, after which all output is shown. "-S -S" causes swaks to only show error conditions. "-S -S -S" shows no output.
1753
1754=item --pipeline
1755
1756If the remote server supports it, attempt SMTP PIPELINING (RFC 2920). This is a younger option, if you experience problems with it please notify the author. Potential problem areas include servers accepting DATA even though there were no valid recipients (swaks should send empty body in that case, not QUIT) and deadlocks caused by sending packets outside the tcp window size.
1757
1758=item -tls
1759
1760Require connection to use STARTTLS. Exit if TLS not available for any reason (not advertised, negotiations failed, etc).
1761
1762=item -tlso, --tls-optional
1763
1764Attempt to use STARTTLS if possible, continue t/ normal transaction if TLS unavailable.
1765
1766=item -tlsc, --tls-on-connect
1767
1768Initiate a TLS connection immediately on connection. Use to test smtps/ssmtp servers. If this options is specified, the default port changes from 25 to 465, though this can still be overridden with the -p option.
1769
1770=item -a, --auth
1771
1772Require authentication. If Authentication fails or is unavailable, stop transaction. -a can take an argument specifying which type(s) of authentication to try. If multiple, comma-delimited arguments are given, each specified auth type is tried in order until one succeeds or they all fail. swaks currently supports PLAIN, LOGIN, and CRAM-MD5. If no argument is given any available authentication type is used. If neither password (-ap) or username (-au) is supplied on command line, swaks will prompt on STDIN.
1773
1774SPA (NTLM/MSN) authentication is now supported. Tested as a client against Exim and Stalker's CommuniGate, but implementation may be incomplete. Authen::NTLM is currently required. Note that CPAN hosts two different Authen::NTLM modules. Current implementation requires Mark Bush's implementation (Authen/NTLM-1.02.tar.gz). Plan to reimplement directly at some point to avoid confusion.
1775
1776DIGEST-MD5 is now supported. Tested as a client only against Stalker's Communigate, so implementation may be incomplete. Requires Authen::DigestMD5 module.
1777
1778CRAM-SHA1 is now supported. Only tested against a hacked server implementation in Exim, so may be incomplete or incorrect. Requires Digest::SHA1 module.
1779
1780=item -ao, --auth-optional
1781
1782Same as -a, but if authentication is unavailable or fails, attempts to continue transaction.
1783
1784=item -au, --auth-user
1785
1786Supply the username for authentication. The string <> can be supplied to mean an empty username.
1787
1788For SPA authentication, a "domain" can be specified after the regular username with a % seperator. For instance, if "-ap user@example.com%NTDOM" is passed, "user@example.com" is the username and "NTDOM" is the domain. NOTE: I don't actually have access to a mail server where the domain isn't ignored, so this may be implemented incorrectly.
1789
1790=item -ap, --auth-password
1791
1792Supply the password for authentication. The string <> can be supplied to mean an empty password.
1793
1794=item -am --auth-map
1795
1796Provides a way to map alternate names onto base authentication types. Useful for any sites that use alternate names for common types. This functionality is actually used internally to map types SPA and MSN onto the base type NTLM. The command line argument to simulate this would be "--auth-map SPA=NTLM,MSN=NTLM". The base types supported are LOGIN, PLAIN, CRAM-MD5, DIGEST-MD5, and NTLM. SPA and MSN are mapped on to NTLM automatically.
1797
1798=item -apt, --auth-plaintext
1799
1800Instead of showing AUTH strings literally (in base64), translate them to plaintext before printing on screen.
1801
1802=item -nth, --no-hints
1803
1804Don't show transaction hints. (Useful in conjunction with -hr to create copy/paste-able transactions
1805
1806=item -hr, --hide-receive
1807
1808Don't display reception lines
1809
1810=item -hs, --hide-send
1811
1812Don't display sending lines
1813
1814=item -stl, --show-time-lapse
1815
1816Display time lapse between send/receive pairs. If 'i' is provided as argument or the Time::HiRes module is unavailable the time lapse will be integer only, otherwise it will be to the thousandth of a second.
1817
1818=item --force-getpwuid
1819
1820In releases 20050709.1 and earlier of swaks the local_part of an automatically generated sender email address would be found using the getpwuid system call on the euid of the current process. Depending on the users' desires, this may be confusing. Following the 20050709.1 release the local_part is not looked up via the getlogin() funtion which attempts to look up the actual username of the logged in user, regardless of the euid of the process they are currently running.
1821
1822An example of where this might be an issue is running swaks under sudo for testing reasons when interacting with --pipe or --socket. It makes sense that you need to run the process as a specific username but you would prefer your email to be from your real username. You could always do this manually using the -s option, but this is an attempt to make it easier.
1823
1824--force-getpwuid forces the old behaviour for anyone who prefered that behaviour. Also, if there is no "real" user for getlogin() to look up, the old getpwuid method will be used. This would happen if the process was run from cron or some other headless daemon.
1825
1826=item --help
1827
1828This screen.
1829
1830=item --version
1831
1832Version info.
1833
1834=back
1835
1836=head1 EXAMPLES
1837
1838=over 4
1839
1840=item swaks
1841
1842prompt user for to address and send a default email.
1843
1844=item cat mailfile | swaks -g -n -t user@example.com -tlso -a -au user -ap password
1845
1846send the contents of "mailfile" to user@example.com, using TLS if available, requiring authentication, using user/password as authentication information.
1847
1848=back
1849
1850=head1 COMMENTS
1851
1852This program was written because I was testing a new MTA on an alternate port. I did so much testing that using interactive telnet grew tiresome. Over the next several years this program was fleshed out and every single option was added as a direct need of some testing I was doing as the mail admin of a medium sized ISP, with the exception of TLS support which was added on a whim. As such, all options are reasonably well thought out and fairly well tested (though TLS could use more testing).
1853
1854=head1 REQUIRES
1855
1856swaks does not have any single requirement except the standard module Getopt::Long. However, there may be modules that are required for a given invocation of swaks. The following list details the features reported by the --support option, what is actually being tested, and the consequences of the feature being reported as "not available"
1857
1858=over 4
1859
1860=item AUTH CRAM-MD5
1861
1862CRAM-MD5 authentication requires the Digest::MD5 perl module. If this is unavailable and authentication is required, swaks will error if CRAM-MD5 was the specific authentication type requested, or if no specific auth type was requested but CRAM-MD5 was the only type advertised by the server.
1863
1864=item AUTH CRAM-SHA1
1865
1866CRAM-SHA1 authentication requires the Digest::SHA1 perl module. If this is unavailable and authentication is required, swaks will error if CRAM-SHA1 was the specific authentication type requested, or if no specific auth type was requested but CRAM-SHA1 was the only type advertised by the server.
1867
1868=item AUTH DIGEST-MD5
1869
1870DIGEST-MD5 authentication requires the Authen::DigestMD5 perl module. If this is unavailable and authentication is required, swaks will error if DIGEST-MD5 was the specific authentication type requested, or if no specific auth type was requested but DIGEST-MD5 was the only type advertised by the server.
1871
1872=item AUTH NTLM
1873
1874NTLM/SPA/MSN authentication requires the Authen::NTLM perl module. If this is unavailable and authentication is required, swaks will error if NTLM was the specific authentication type requested, or if no specific auth type was requested but NTLM was the only type advertised by the server. Note that there are two modules using the Authen::NTLM namespace on CPAN. The Mark Bush implementation (Authen/NTLM-1.02.tar.gz) is the version required here.
1875
1876=item Basic AUTH
1877
1878All authentication types require base64 encoding and decoding. If possible, swaks uses the MIME::Base64 perl module to perform these actions. However, if MIME::Base64 is not available swaks will use its own onboard base64 routines. These are slower than the MIME::Base64 routines and less reviewed, though they have been tested thoroughly. When possible it is recommended that you install MIME::Base64.
1879
1880=item Date Manipulation
1881
1882swaks generates an RFC compliant date string when it interpolates the %D token in message bodies. In order to build the GMT offset in this string, it needs the Time::Local module. It would be very odd for this module not to be available because it has been included in the perl distribution for some time. However, if it is not loadable for some reason and swaks interpolates a %D token (as it would when using the default body), the date string is in GMT instead of your local timezone.
1883
1884=item High Resolution Timing
1885
1886When diagnosing SMTP delays using --show-time-lapse, by default high resolution timing is attempted using the Time::HiRes module. If this module is not available, swaks uses a much poorer timing source with one second granularity.
1887
1888=item Local Hostname Detection
1889
1890swaks uses your local machine's hostname to build the HELO string and sending email address when they are not specified on the command line. If the Sys::Hostname module (which is a part of the base distribution) is not available for some reason, the user is prompted interactively for the HELO and sender strings. Note that Sys::Hostname can sometimes fail to find the local hostname even when the module is available, which has the same behaviour.
1891
1892=item MX Routing
1893
1894If the destination mail server is not specified using the --server option, swaks attempts to use DNS to route the message based on the recipient email address. If the Net::DNS perl module is not available, swaks uses 'localhost' as the outbound mail server.
1895
1896=item Pipe Transport
1897
1898The IPC::Open2 module is required to deliver a message to a spawned subprocess using the --pipe option. If this module, which is included in the base perl distribution, in not available, attempting to call swaks with the --pipe option will result in an error.
1899
1900=item Socket Transport
1901
1902The IO::Socket module is required to deliver a message to an internet domain socket (the default behaviour of swaks) and to a unix domain socket (specified with the --socket option). If this module, which is included in the base perl distribution, is not available, attempting to call swaks with the --server or --socket options (or none of the --socket, --server, and --pipe options) will result in an error.
1903
1904=item TLS
1905
1906TLS functionality requires the Net::SSLeay perl module. If this module is not available and TLS was required (using the --tls-on-connect or --tls options), the session will error out. If TLS was requested but not required (using the --tls-optional option), swaks will continue but not attempt a TLS session.
1907
1908=back
1909
1910=head1 PORTABILITY
1911
1912=over 4
1913
1914=item Operating Systems
1915
1916This program was primarily intended for use on unix-like operating systems, and it should work on any reasonable version thereof. It has been developed and tested on Solaris, Linux, and Mac OS X and is feature complete on all of these.
1917
1918This program is known to demonstrate basic functionality on Windows using ActiveState's Perl. It has not been fully tested. Known to work are basic SMTP functionality and the LOGIN, PLAIN, and CRAM-MD5 auth types. Unknown is any TLS functionality and the NTLM/SPA and Digest-MD5 auth types.
1919
1920Because this program should work anywhere Perl works, I would appreciate knowing about any new operating systems you've thoroughly used swaks on as well as any problems encountered on a new OS.
1921
1922=item Mail Servers
1923
1924This program was almost exclusively developed against Exim mail servers. It was been used casually by the author, though not thoroughly tested, with sendmail, smail, and Communigate. Because all functionality in swaks is based off of known standards it should work with any fairly modern mail server. If a problem is found, please alert the author at the address below.
1925
1926=back
1927
1928=head1 EXIT CODES
1929
1930=over 4
1931
1932=item 0
1933
1934no errors occurred
1935
1936=item 1
1937
1938error parsing command line options
1939
1940=item 2
1941
1942error connecting to remote server
1943
1944=item 3
1945
1946unknown connection type
1947
1948=item 4
1949
1950while running with connection type of "pipe", fatal problem writing to or reading from the child process
1951
1952=item 5
1953
1954while running with connection type of "pipe", child process died unexpectedly. This can mean that the program specified with --pipe doesn't exist.
1955
1956=item 6
1957
1958Connection closed unexpectedly. If the close is detected in response to the 'QUIT' swaks sends following an unexpected response, the error code for that unexpected response is used instead.
1959
1960For instance, if a mail server returns a 550 response to a MAIL FROM: and then immediately closes the connection, swaks detects that the connection is closed, but uses the more specific exit code 23 to detail the nature of the failure.
1961
1962If instead the server return a 250 code and then immediately closes the connection, swaks will use the exit code 6 because there is not a more specific exit code.
1963
1964=item 10
1965
1966error in prerequisites (needed module not available)
1967
1968=item 21
1969
1970error reading initial banner from server
1971
1972=item 22
1973
1974error in HELO transaction
1975
1976=item 23
1977
1978error in MAIL transaction
1979
1980=item 24
1981
1982no RCPTs accepted
1983
1984=item 25
1985
1986server returned error to DATA request
1987
1988=item 26
1989
1990server did not accept mail following data
1991
1992=item 27
1993
1994server returned error after normal-session quit request
1995
1996=item 28
1997
1998error in AUTH transaction
1999
2000=item 29
2001
2002error in TLS transaction
2003
2004=item 32
2005
2006error in EHLO following TLS negotiation
2007
2008=back
2009
2010=head1 CONTACT
2011
2012=over 4
2013
2014=item proj-swaks@jetmore.net
2015
2016Please use this address for general contact, questions, patches, requests, etc.
2017
2018=item updates-swaks@jetmore.net
2019
2020If you would like to be put on a list to receive notifications when a new version of swaks is released, please send an email to this address.
2021
2022=item jetmore.org/john/code/#swaks
2023
2024Change logs, this help, and the latest version is found at this link.
2025
2026=back
2027
2028=cut
This page took 0.535145 seconds and 4 git commands to generate.