scripts/swaks

3827 lines
143 KiB
Perl
Executable File

#!/usr/bin/perl
# use 'swaks --help' to view documentation for this program
#
# Homepage: http://jetmore.org/john/code/swaks/
# Online Docs: http://jetmore.org/john/code/swaks/latest/doc/ref.txt
# http://jetmore.org/john/code/swaks/faq.html
# Announce List: send mail to updates-swaks@jetmore.net
# Project RSS: http://jetmore.org/john/blog/c/swaks/feed/
# Twitter: http://www.twitter.com/SwaksSMTP
use strict;
$| = 1;
my($p_name) = $0 =~ m|/?([^/]+)$|;
my $p_version = build_version("DEVRELEASE", '$Id$');
my $p_usage = "Usage: $p_name [--help|--version] (see --help for details)";
my $p_cp = <<'EOM';
Copyright (c) 2003-2008,2010-2019 John Jetmore <jj33@pobox.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
EOM
# Get all input provided to our program, via file, env, command line, etc
my %O = %{ load_args() };
# before we do anything else, check for --help and --version
if (get_arg('help', \%O)) {
ext_usage();
exit;
}
if (get_arg('version', \%O)) {
print "$p_name version $p_version\n\n$p_cp\n";
exit;
}
# Get our functional dependencies and then print and exit early if requested
load_dependencies();
if (get_arg('get_support', \%O)) {
test_support();
exit(0);
}
# This 'synthetic' command line used for debug and reference
$G::cmdline = reconstruct_options(\%O);
# We need to fix things up a bit and set a couple of global options
my $opts = process_args(\%O);
if (scalar(keys(%G::dump_args))) {
if (my $running_state = get_running_state($opts, \%G::dump_args)) {
# --dump is intended as a debug tool for swaks internally. Always,
# unconditionally, show the user's auth password if one is given
$running_state =~ s/'%RAW_PASSWORD_STRING%'/shquote($opts->{a_pass})/ge;
print $G::trans_fh_oh $running_state;
}
exit(0);
}
elsif ($G::dump_mail) {
# if the user just wanted to generate an email body, dump it now and exit
$opts->{data} =~ s/\n\.\Z//;
print $G::trans_fh_oh $opts->{data};
exit(0);
}
# we're going to abstract away the actual connection layer from the mail
# process, so move the act of connecting into its own sub. The sub will
# set info in global hash %G::link
# XXX instead of passing raw data, have processs_opts create a link_data
# XXX hash that we can pass verbatim here
open_link();
sendmail($opts->{from}, $opts->{to}, $opts->{helo}, $opts->{data},
$opts->{a_user}, $opts->{a_pass}, $opts->{a_type});
teardown_link();
exit(0);
sub teardown_link {
if ($G::link{type} eq 'socket-inet' || $G::link{type} eq 'socket-unix') {
# XXX need anything special for tls teardown?
close($G::link{sock});
ptrans(11, "Connection closed with remote host.");
} elsif ($G::link{type} eq 'pipe') {
delete($SIG{PIPE});
$SIG{CHLD} = 'IGNORE';
close($G::link{sock}{wr});
close($G::link{sock}{re});
ptrans(11, "Connection closed with child process.");
}
}
sub open_link {
if ($G::link{type} eq 'socket-inet') {
ptrans(11, 'Trying ' . $G::link{server} . ':' . $G::link{port} . '...');
$@ = "";
my @extra_options = ();
push(@extra_options, "LocalAddr", $G::link{lint}) if ($G::link{lint});
push(@extra_options, "LocalPort", $G::link{lport}) if ($G::link{lport});
# INET6 also supports v4, so use it for everything if it's available. That
# allows the module to handle A vs AAAA records on domain lookups where the
# user hasn't set a specific ip version to be used. If INET6 isn't available
# and it's a domain that only has AAAA records, INET will just handle it like
# a bogus record and we just won't be able to connect
if (avail("ipv6")) {
if ($G::link{force_ipv6}) {
push(@extra_options, "Domain", Socket::AF_INET6() );
} elsif ($G::link{force_ipv4}) {
push(@extra_options, "Domain", Socket::AF_INET() );
}
$G::link{sock} = IO::Socket::INET6->new(
PeerAddr => $G::link{server},
PeerPort => $G::link{port},
Proto => 'tcp',
Timeout => $G::link{timeout},
@extra_options
);
} else {
$G::link{sock} = IO::Socket::INET->new(
PeerAddr => $G::link{server},
PeerPort => $G::link{port},
Proto => 'tcp',
Timeout => $G::link{timeout},
@extra_options
);
}
if ($@) {
ptrans(12, "Error connecting" . ($G::link{lint} ? " $G::link{lint}" : '') .
" to $G::link{server}:$G::link{port}:\n\t$@");
exit(2);
}
ptrans(11, "Connected to $G::link{server}.");
} elsif ($G::link{type} eq 'socket-unix') {
ptrans(11, 'Trying ' . $G::link{sockfile} . '...');
$SIG{PIPE} = 'IGNORE';
$@ = "";
$G::link{sock} = IO::Socket::UNIX->new(Peer => $G::link{sockfile}, Timeout => $G::link{timeout});
if ($@) {
ptrans(12, 'Error connecting to ' . $G::link{sockfile} . ":\n\t$@");
exit(2);
}
ptrans(11, 'Connected to ' . $G::link{sockfile} . '.');
} elsif ($G::link{type} eq 'pipe') {
$SIG{PIPE} = 'IGNORE';
$SIG{CHLD} = 'IGNORE';
ptrans(11, "Trying pipe to $G::link{process}...");
eval{ open2($G::link{sock}{re}, $G::link{sock}{wr}, $G::link{process}); };
if ($@) {
ptrans(12, 'Error connecting to ' . $G::link{process} . ":\n\t$@");
exit(2);
}
select((select($G::link{sock}{wr}), $| = 1)[0]);
select((select($G::link{sock}{re}), $| = 1)[0]);
ptrans(11, 'Connected to ' . $G::link{process} . '.');
} else {
ptrans(12, 'Unknown or unimplemented connection type ' . $G::link{type});
exit(3);
}
}
sub sendmail {
my $from = shift; # envelope-from
my $to = shift; # envelope-to
my $helo = shift; # who am I?
my $data = shift; # body of message (content after DATA command)
my $a_user = shift; # what user to auth with?
my $a_pass = shift; # what pass to auth with
my $a_type = shift; # what kind of auth (this must be set to to attempt)
my $ehlo = {}; # If server is esmtp, save advertised features here
do_smtp_proxy() if ($G::proxy{try});
# start up tls if -tlsc specified
if ($G::tls_on_connect) {
if (start_tls()) {
tls_post_start();
do_smtp_drop() if ($G::drop_after eq 'tls');
do_smtp_quit(1, 0) if ($G::quit_after eq 'tls');
} else {
ptrans(12, "TLS startup failed ($G::link{tls}{res})");
exit(29);
}
}
# read the server's 220 banner.
do_smtp_gen(undef, '220') || do_smtp_quit(1, 21);
do_smtp_drop() if ($G::drop_after eq 'connect');
do_smtp_quit(1, 0) if ($G::quit_after eq 'connect');
# Send a HELO string
$G::drop_before_read = 1 if ($G::drop_after_send eq 'first-helo');
do_smtp_helo($helo, $ehlo, $G::protocol) || do_smtp_quit(1, 22);
do_smtp_drop() if ($G::drop_after eq 'first-helo');
do_smtp_quit(1, 0) if ($G::quit_after eq 'first-helo');
if ($G::xclient{before_tls}) {
xclient_try($helo, $ehlo);
}
# handle TLS here if user has requested it
if ($G::tls) {
# 0 = tls succeeded
# 1 = tls not advertised
# 2 = tls advertised and attempted negotiations failed
# note there's some duplicate logic here (with process_args) but I think
# it's best to do as thorough a job covering the options in both places
# so as to minimize chance of options falling through the cracks
$G::drop_before_read = 1 if ($G::drop_after_send eq 'tls');
my $result = do_smtp_tls($ehlo);
if ($result == 1) {
ptrans(12, "Host did not advertise STARTTLS");
do_smtp_quit(1, 29) if (!$G::tls_optional);
} elsif ($result == 2) {
ptrans(12, "STARTTLS attempted but failed");
exit(29) if ($G::tls_optional != 1);
}
} elsif ($G::tls_optional == 2 && $ehlo->{STARTTLS}) {
ptrans(12, "TLS requested, advertised, and locally unavailable. Exiting");
do_smtp_quit(1, 29);
}
do_smtp_drop() if ($G::drop_after eq 'tls');
do_smtp_quit(1, 0) if ($G::quit_after eq 'tls');
if (!$G::xclient{before_tls}) {
xclient_try($helo, $ehlo);
}
#if ($G::link{tls}{active} && $ehlo->{STARTTLS}) {
if ($G::link{tls}{active} && !$G::tls_on_connect) {
# According to RFC3207, we need to forget state info and re-EHLO here
$ehlo = {};
$G::drop_before_read = 1 if ($G::drop_after_send eq 'helo');
do_smtp_helo($helo, $ehlo, $G::protocol) || do_smtp_quit(1, 32);
}
do_smtp_drop() if ($G::drop_after_send eq 'helo'); # haaaack. Need to use first-helo for this. Just quit here to prevent the mail from being delivered
do_smtp_drop() if ($G::drop_after eq 'helo');
do_smtp_quit(1, 0) if ($G::quit_after eq 'helo');
# handle auth here if user has requested it
if ($a_type) {
# 0 = auth succeeded
# 1 = auth not advertised
# 2 = auth advertised but not attempted, no matching auth types
# 3 = auth advertised but not attempted, auth not supported
# 4 = auth advertised and attempted but no type succeeded
# note there's some duplicate logic here (with process_args) but I think
# it's best to do as thorough a job covering the options in both places
# so as to minimize chance of options falling through the cracks
$G::drop_before_read = 1 if ($G::drop_after_send eq 'auth');
my $result = do_smtp_auth($ehlo, $a_type, $a_user, $a_pass);
if ($result == 1) {
ptrans(12, "Host did not advertise authentication");
do_smtp_quit(1, 28) if (!$G::auth_optional);
} elsif ($result == 2) {
if ($G::auth_type eq 'ANY') {
ptrans(12, "Auth not attempted, no advertised types available");
do_smtp_quit(1, 28) if ($G::auth_optional != 1);
} else {
ptrans(12, "Auth not attempted, requested type not available");
do_smtp_quit(1, 28) if (!$G::auth_optional);
}
} elsif ($result == 3) {
ptrans(12, "Auth advertised but not supported locally");
do_smtp_quit(1, 28) if ($G::auth_optional != 1);
} elsif ($result == 4) {
ptrans(12, "No authentication type succeeded");
do_smtp_quit(1, 28) if ($G::auth_optional != 1);
}
} elsif ($G::auth_optional == 2 && $ehlo->{AUTH}) {
ptrans(12, "Auth requested, advertised, and locally unavailable. Exiting");
do_smtp_quit(1, 28);
}
do_smtp_drop() if ($G::drop_after eq 'auth');
do_smtp_quit(1, 0) if ($G::quit_after eq 'auth');
# send MAIL
# 0 = mail succeeded
# 1 = prdr required but not advertised
$G::drop_before_read = 1 if ($G::drop_after_send eq 'mail');
my $result = do_smtp_mail($ehlo, $from); # failures in this handled by smtp_mail_callback
if ($result == 1) {
ptrans(12, "Host did not advertise PRDR support");
do_smtp_quit(1, 30);
}
do_smtp_drop() if ($G::drop_after eq 'mail');
do_smtp_quit(1, 0) if ($G::quit_after eq 'mail');
# send RCPT (sub handles multiple, comma-delimited recips)
$G::drop_before_read = 1 if ($G::drop_after_send eq 'rcpt');
do_smtp_rcpt($to); # failures in this handled by smtp_rcpt_callback
# note that smtp_rcpt_callback increments
# $G::smtp_rcpt_failures at every failure. This and
# $G::smtp_rcpt_total are used after DATA for LMTP
do_smtp_drop() if ($G::drop_after eq 'rcpt');
do_smtp_quit(1, 0) if ($G::quit_after eq 'rcpt');
# send DATA
$G::drop_before_read = 1 if ($G::drop_after_send eq 'data');
do_smtp_gen('DATA', '354') || do_smtp_quit(1, 25);
do_smtp_drop() if ($G::drop_after eq 'data');
# send the actual data
$G::drop_before_read = 1 if ($G::drop_after_send eq 'dot');
do_smtp_data($data, $G::suppress_data) || do_smtp_quit(1, 26);
do_smtp_drop() if ($G::drop_after eq 'dot');
# send QUIT
do_smtp_quit(0) || do_smtp_quit(1, 27);
}
sub xclient_try {
my $helo = shift;
my $ehlo = shift;
if ($G::xclient{try}) {
# 0 - xclient succeeded normally
# 1 - xclient not advertised
# 2 - xclient advertised but not attempted, mismatch in requested attrs
# 3 - xclient attempted but did not succeed
$G::drop_before_read = 1 if ($G::drop_after_send eq 'xclient');
my $result = do_smtp_xclient($ehlo);
if ($result == 1) {
ptrans(12, "Host did not advertise XCLIENT");
do_smtp_quit(1, 33) if (!$G::xclient{optional});
} elsif ($result == 2) {
ptrans(12, "Host did not advertise requested XCLIENT attributes");
do_smtp_quit(1, 33) if (!$G::xclient{optional});
} elsif ($result == 3) {
ptrans(12, "XCLIENT attempted but failed. Exiting");
do_smtp_quit(1, 33) if ($G::xclient{optional} != 1);
} else {
do_smtp_drop() if ($G::drop_after eq 'xclient');
do_smtp_quit(1, 0) if ($G::quit_after eq 'xclient');
# re-helo if the XCLIENT command succeeded
$G::drop_before_read = 1 if ($G::drop_after_send eq 'helo');
do_smtp_helo($helo, $ehlo, $G::protocol) || do_smtp_quit(1, 34);
do_smtp_drop() if ($G::drop_after eq 'helo');
do_smtp_quit(1, 0) if ($G::quit_after eq 'helo');
}
}
}
sub tls_post_start {
ptrans(11, "TLS started with cipher $G::link{tls}{cipher_string}");
if ($G::link{tls}{local_cert_subject}) {
ptrans(11, "TLS local DN=\"$G::link{tls}{local_cert_subject}\"");
} else {
ptrans(11, "TLS no local certificate set");
}
ptrans(11, "TLS peer DN=\"$G::link{tls}{cert_subject}\"");
if ($G::tls_get_peer_cert eq 'STDOUT') {
ptrans(11, $G::link{tls}{cert_x509});
} elsif ($G::tls_get_peer_cert) {
open(CERT, ">$G::tls_get_peer_cert") ||
ptrans(12, "Couldn't open $G::tls_get_peer_cert for writing: $!");
print CERT $G::link{tls}{cert_x509}, "\n";
close(CERT);
}
}
sub start_tls {
my %t = (); # This is a convenience var to access $G::link{tls}{...}
$G::link{tls} = \%t;
Net::SSLeay::load_error_strings();
Net::SSLeay::SSLeay_add_ssl_algorithms();
Net::SSLeay::randomize();
if (!($t{con} = Net::SSLeay::CTX_new())) {
$t{res} = "CTX_new(): " . Net::SSLeay::ERR_error_string(Net::SSLeay::ERR_get_error());
return(0);
}
my $ctx_options = &Net::SSLeay::OP_ALL;
if (scalar(@G::tls_protocols)) {
if ($G::tls_protocols[0] =~ /^no_/i) {
foreach my $p (@G::tls_supported_protocols) {
if (grep /^no_$p$/i, @G::tls_protocols) {
no strict "refs";
$ctx_options |= &{"Net::SSLeay::OP_NO_$p"}();
}
}
} else {
foreach my $p (@G::tls_supported_protocols) {
if (!grep /^$p$/i, @G::tls_protocols) {
no strict "refs";
$ctx_options |= &{"Net::SSLeay::OP_NO_$p"}();
}
}
}
}
Net::SSLeay::CTX_set_options($t{con}, $ctx_options);
Net::SSLeay::CTX_set_verify($t{con}, 0x01, 0) if ($G::tls_verify);
if ($G::tls_ca_path) {
my @args = ('', $G::tls_ca_path);
@args = ($G::tls_ca_path, '') if (-f $G::tls_ca_path);
if (!Net::SSLeay::CTX_load_verify_locations($t{con}, @args)) {
$t{res} = "Unable to set set CA path to (" . join(',', @args) . "): "
. Net::SSLeay::ERR_error_string(Net::SSLeay::ERR_get_error());
return(0);
}
} else {
Net::SSLeay::CTX_set_default_verify_paths($t{con});
}
if ($G::tls_cipher) {
if (!Net::SSLeay::CTX_set_cipher_list($t{con}, $G::tls_cipher)) {
$t{res} = "Unable to set cipher list to $G::tls_cipher: "
. Net::SSLeay::ERR_error_string(Net::SSLeay::ERR_get_error());
return(0);
}
}
if ($G::tls_cert && $G::tls_key) {
if (!Net::SSLeay::CTX_use_certificate_file($t{con}, $G::tls_cert, &Net::SSLeay::FILETYPE_PEM)) {
$t{res} = "Unable to add cert file $G::tls_cert to SSL CTX: "
. Net::SSLeay::ERR_error_string(Net::SSLeay::ERR_get_error());
return(0);
}
if (!Net::SSLeay::CTX_use_PrivateKey_file($t{con}, $G::tls_key, &Net::SSLeay::FILETYPE_PEM)) {
$t{res} = "Unable to add key file $G::tls_key to SSL CTX: "
. Net::SSLeay::ERR_error_string(Net::SSLeay::ERR_get_error());
return(0);
}
}
if (!($t{ssl} = Net::SSLeay::new($t{con}))) {
$t{res} = "new(): " . Net::SSLeay::ERR_error_string(Net::SSLeay::ERR_get_error());
return(0);
}
if ($G::tls_sni_hostname) {
if (!Net::SSLeay::set_tlsext_host_name($t{ssl}, $G::tls_sni_hostname)) {
$t{res} = "Unable to set SNI hostname to $G::tls_sni_hostname: "
. Net::SSLeay::ERR_error_string(Net::SSLeay::ERR_get_error());
return(0);
}
}
if ($G::link{type} eq 'pipe') {
Net::SSLeay::set_wfd($t{ssl}, fileno($G::link{sock}{wr})); # error check?
Net::SSLeay::set_rfd($t{ssl}, fileno($G::link{sock}{re})); # error check?
} else {
Net::SSLeay::set_fd($t{ssl}, fileno($G::link{sock})); # error check?
}
$t{active} = Net::SSLeay::connect($t{ssl}) == 1 ? 1 : 0;
if (!$t{active}) {
$t{res} = "connect(): " . Net::SSLeay::ERR_error_string(Net::SSLeay::ERR_get_error());
return(0);
}
# egrep 'define.*VERSION\b' *.h
# when adding new types here, see also the code that pushes supported values onto tls_supported_protocols
$t{version} = Net::SSLeay::version($t{ssl});
if ($t{version} == 0x0002) {
$t{version} = "SSLv2"; # openssl/ssl2.h
} elsif ($t{version} == 0x0300) {
$t{version} = "SSLv3"; # openssl/ssl3.h
} elsif ($t{version} == 0x0301) {
$t{version} = "TLSv1"; # openssl/tls1.h
} elsif ($t{version} == 0x0302) {
$t{version} = "TLSv1.1"; # openssl/tls1.h
} elsif ($t{version} == 0x0303) {
$t{version} = "TLSv1.2"; # openssl/tls1.h
} elsif ($t{version} == 0x0304) {
$t{version} = "TLSv1.3"; # openssl/tls1.h
} elsif ($t{version} == 0xFEFF) {
$t{version} = "DTLSv1"; # openssl/dtls1.h
} elsif ($t{version} == 0xFEFD) {
$t{version} = "DTLSv1.2"; # openssl/dtls1.h
} else {
$t{version} = sprintf("UNKNOWN(0x%04X)", $t{version});
}
$t{cipher} = Net::SSLeay::get_cipher($t{ssl});
if (!$t{cipher}) {
$t{res} = "empty response from get_cipher()";
return(0);
}
$t{cipher_bits} = Net::SSLeay::get_cipher_bits($t{ssl}, undef);
if (!$t{cipher_bits}) {
$t{res} = "empty response from get_cipher_bits()";
return(0);
}
$t{cipher_string} = sprintf("%s:%s:%s", $t{version}, $t{cipher}, $t{cipher_bits});
$t{cert} = Net::SSLeay::get_peer_certificate($t{ssl});
if (!$t{cert}) {
$t{res} = "error response from get_peer_certificate()";
return(0);
}
chomp($t{cert_x509} = Net::SSLeay::PEM_get_string_X509($t{cert}));
$t{cert_subject} = Net::SSLeay::X509_NAME_oneline(Net::SSLeay::X509_get_subject_name($t{cert}));
if ($G::tls_cert && $G::tls_key) {
$t{local_cert} = Net::SSLeay::get_certificate($t{ssl});
chomp($t{local_cert_x509} = Net::SSLeay::PEM_get_string_X509($t{local_cert}));
$t{local_cert_subject} = Net::SSLeay::X509_NAME_oneline(Net::SSLeay::X509_get_subject_name($t{local_cert}));
}
return($t{active});
}
sub deprecate {
my $message = shift;
ptrans(12, "DEPRECATION WARNING: $message");
}
sub ptrans {
my $c = shift; # transaction flag
my $m = shift; # message to print
my $b = shift; # be brief in what we print
my $a = shift; # return the message in an array ref instead of printing
my $o = $G::trans_fh_oh || \*STDOUT;
my $f = '';
return if (($G::hide_send && int($c/10) == 2) ||
($G::hide_receive && int($c/10) == 3) ||
($G::hide_informational && $c == 11) ||
($G::hide_all));
# global option silent controls what we echo to the terminal
# 0 - print everything
# 1 - don't show anything until you hit an error, then show everything
# received after that (done by setting option to 0 on first error)
# 2 - don't show anything but errors
# >=3 - don't print anything
if ($G::silent > 0) {
return if ($G::silent >= 3);
return if ($G::silent == 2 && $c%2 != 0);
if ($G::silent == 1 && !$G::ptrans_seen_error) {
if ($c%2 != 0) {
return();
} else {
$G::ptrans_seen_error = 1;
}
}
}
# 1x is program messages
# 2x is smtp send
# 3x is smtp recv
# x = 1 is info/normal
# x = 2 is error
# x = 3 dump output
# program info
if ($c == 11) { $f = $G::no_hints_info ? '' : '==='; }
# program error
elsif ($c == 12) { $f = $G::no_hints_info ? '' : '***'; $o = $G::trans_fh_eh || \*STDERR; }
# smtp send info
elsif ($c == 21) { $f = $G::no_hints_send ? '' : ($G::link{tls}{active} ? ' ~>' : ' ->'); }
# smtp send error
elsif ($c == 22) { $f = $G::no_hints_send ? '' : ($G::link{tls}{active} ? '*~>' : '**>'); }
# smtp send dump output
elsif ($c == 23) { $f = $G::no_hints_send ? '' : ' >'; }
# smtp recv info
elsif ($c == 31) { $f = $G::no_hints_recv ? '' : ($G::link{tls}{active} ? '<~ ' : '<- '); }
# smtp recv error
elsif ($c == 32) { $f = $G::no_hints_recv ? '' : ($G::link{tls}{active} ? '<~*' : '<**'); }
# smtp recv dump output
elsif ($c == 33) { $f = $G::no_hints_recv ? '' : '< '; }
# something went unexpectedly
else { $f = '???'; }
$f .= ' ' if ($f);
if ($b) {
# split to tmp list to prevent -w gripe
my @t = split(/\n/ms, $m); $m = scalar(@t) . " lines sent";
}
$m =~ s/\n/\n$f/msg;
if ($a) {
$m = "$f$m";
return([ split(/\n/, $m) ]);
}
else {
print $o "$f$m\n";
}
}
sub do_smtp_quit {
my $exit = shift;
my $err = shift;
# Ugh. Because PIPELINING allows mail's and rcpt's send to be disconnected,
# and possibly with a QUIT between them, we need to set a global "we have
# told the server we quit already" flag to prevent double-quits
return(1) if ($G::link{quit_sent});
$G::link{quit_sent} = 1;
$G::link{allow_lost_cxn} = 1;
my $r = do_smtp_gen('QUIT', '221');
$G::link{allow_lost_cxn} = 0;
handle_disconnect($err) if ($G::link{lost_cxn});
if ($exit) {
teardown_link();
exit $err;
}
return($r);
}
sub do_smtp_drop {
ptrans(11, "Dropping connection");
exit(0);
}
sub do_smtp_tls {
my $e = shift; # ehlo config hash
# 0 = tls succeeded
# 1 = tls not advertised
# 2 = tls advertised and attempted negotiations failed
if (!$e->{STARTTLS}) {
return(1);
} elsif (!do_smtp_gen("STARTTLS", '220')) {
return(2);
} elsif (!start_tls()) {
ptrans(12, "TLS startup failed ($G::link{tls}{res})");
return(2);
}
tls_post_start();
return(0);
}
sub do_smtp_xclient {
my $e = shift;
# 0 - xclient succeeded normally
# 1 - xclient not advertised
# 2 - xclient advertised but not attempted, mismatch in requested attrs
# 3 - xclient attempted but did not succeed
if (!$e->{XCLIENT}) {
return(1);
}
my @parts = ();
foreach my $attr (keys %{$G::xclient{attr}}) {
if (!$e->{XCLIENT}{$attr}) {
return(2) if (!$G::xclient{no_verify});
}
}
foreach my $string (@{$G::xclient{strings}}) {
my $str = "XCLIENT " . $string;
do_smtp_gen($str, '220') || return(3);
}
return(0);
}
# see xtext encoding in http://tools.ietf.org/html/rfc1891
sub to_xtext {
my $string = shift;
return join('', map { ($_ == 0x2b || $_ == 0x3d || $_ <= 0x20 || $_ >= 0xff)
? sprintf("+%02X", $_)
: chr($_)
} (unpack("C*", $string)));
}
sub do_smtp_auth {
my $e = shift; # ehlo config hash
my $at = shift; # auth type
my $au = shift; # auth user
my $ap = shift; # auth password
return(1) if (!$e->{AUTH});
return(3) if ($G::auth_unavailable);
my $auth_attempted = 0; # set to true if we ever attempt auth
foreach my $btype (@$at) {
# if server doesn't support, skip type (may change in future)
next if (!$e->{AUTH}{$btype});
foreach my $type (@{$G::auth_map_t{'CRAM-MD5'}}) {
if ($btype eq $type) {
return(0) if (do_smtp_auth_cram($au, $ap, $type));
$auth_attempted = 1;
}
}
foreach my $type (@{$G::auth_map_t{'CRAM-SHA1'}}) {
if ($btype eq $type) {
return(0) if (do_smtp_auth_cram($au, $ap, $type));
$auth_attempted = 1;
}
}
foreach my $type (@{$G::auth_map_t{'DIGEST-MD5'}}) {
if ($btype eq $type) {
return(0) if (do_smtp_auth_digest($au, $ap, $type));
$auth_attempted = 1;
}
}
foreach my $type (@{$G::auth_map_t{'NTLM'}}) {
if ($btype eq $type) {
return(0) if (do_smtp_auth_ntlm($au, $ap, $type));
$auth_attempted = 1;
}
}
foreach my $type (@{$G::auth_map_t{'PLAIN'}}) {
if ($btype eq $type) {
return(0) if (do_smtp_auth_plain($au, $ap, $type));
$auth_attempted = 1;
}
}
foreach my $type (@{$G::auth_map_t{'LOGIN'}}) {
if ($btype eq $type) {
return(0) if (do_smtp_auth_login($au, $ap, $type));
$auth_attempted = 1;
}
}
}
return $auth_attempted ? 4 : 2;
}
sub do_smtp_auth_ntlm {
my $u = shift; # auth user
my $p = shift; # auth password
my $as = shift; # auth type (since NTLM might be SPA or MSN)
my $r = ''; # will store smtp response
my $auth_string = "AUTH $as";
do_smtp_gen($auth_string, '334') || return(0);
my $d = db64(Authen::NTLM::ntlm());
$auth_string = eb64($d);
do_smtp_gen($auth_string, '334', \$r, '',
$G::auth_showpt ? "$d" : '',
$G::auth_showpt ? \&unencode_smtp : '') || return(0);
$r =~ s/^....//; # maybe something a little better here?
Authen::NTLM::ntlm_domain($G::auth_extras{DOMAIN});
Authen::NTLM::ntlm_user($u);
Authen::NTLM::ntlm_password($p);
$d = db64(Authen::NTLM::ntlm($r));
$auth_string = eb64($d);
do_smtp_gen($auth_string, '235', \$r, '', $G::auth_showpt ? "$d" : '') || return(0);
return(1);
}
sub do_smtp_auth_digest {
my $u = shift; # auth user
my $p = shift; # auth password
my $as = shift; # auth string
my $r = ''; # will store smtp response
my $e = ''; # will store Authen::SASL errors
my @digest_uri = ();
if (exists($G::auth_extras{"DMD5-SERV-TYPE"})) {
$digest_uri[0] = $G::auth_extras{"DMD5-SERV-TYPE"};
} else {
$digest_uri[0] = 'smtp';
}
if (exists($G::auth_extras{"DMD5-HOST"})) {
$digest_uri[1] = $G::auth_extras{"DMD5-HOST"};
} else {
if ($G::link{type} eq 'socket-unix') {
$digest_uri[1] = $G::link{sockfile};
$digest_uri[1] =~ s|[^a-zA-Z0-9\.\-]|-|g;
} elsif ($G::link{type} eq 'pipe') {
$digest_uri[1] = $G::link{process};
$digest_uri[1] =~ s|[^a-zA-Z0-9\.\-]|-|g;
} else {
$digest_uri[1] = $G::link{server};
}
}
if (exists($G::auth_extras{"DMD5-SERV-NAME"})) {
# There seems to be a hole in the Authen::SASL interface where there's
# no option to directory provide the digest-uri serv-name. But we can
# trick it into using the value we want by tacking it onto the end of host
$digest_uri[1] .= '/' . $G::auth_extras{"DMD5-SERV-NAME"};
}
my $auth_string = "AUTH $as";
do_smtp_gen($auth_string, '334', \$r, '', '', $G::auth_showpt ? \&unencode_smtp : '')
|| return(0);
$r =~ s/^....//; # maybe something a little better here?
$r = db64($r);
my $callbacks = { user => $u, pass => $p };
if (exists($G::auth_extras{REALM})) {
$callbacks->{realm} = $G::auth_extras{REALM};
}
my $sasl = Authen::SASL->new(
debug => 1,
mechanism => 'DIGEST-MD5',
callback => $callbacks,
);
my $sasl_client = $sasl->client_new(@digest_uri);
# Force the DIGEST-MD5 session to use qop=auth. I'm open to exposing this setting
# via some swaks options, but I don't know enough about the protocol to just guess
# here. I do know that letting it auto-negotiate didn't work in my reference
# environment. sendmail advertised auth,auth-int,auth-conf, but when Authen::SASL
# chose auth-int the session would fail (server would say auth succeeded, but then
# immediately terminate my session when I sent MAIL). My reference client
# (Mulberry) always sent auth, and indeed forcing swaks to auth also seems to work.
# If anyone out there knows more about this please let me know.
$sasl_client->property('maxssf' => 0);
$auth_string = $sasl_client->client_step($r);
if ($e = $sasl_client->error()) {
ptrans('12', "Error received from Authen::SASL sub-system: $e");
return(0);
}
do_smtp_gen(eb64($auth_string), '334', \$r, '',
$G::auth_showpt ? "$auth_string" : '',
$G::auth_showpt ? \&unencode_smtp : '')
|| return(0);
$r =~ s/^....//; # maybe something a little better here?
$r = db64($r);
$auth_string = $sasl_client->client_step($r);
if ($e = $sasl_client->error()) {
ptrans('12', "Canceling SASL exchange, error received from Authen::SASL sub-system: $e");
$auth_string = '*';
}
#do_smtp_gen(eb64($auth_string), '235', \$r, '', $G::auth_showpt ? "$auth_string" : '')
do_smtp_gen($auth_string, '235', \$r, '', $auth_string)
|| return(0);
if ($e = $sasl_client->error()) {
ptrans('12', "Error received from Authen::SASL sub-system: $e");
return(0);
}
return(0) if (!$sasl_client->is_success());
return(1);
}
# This can handle both CRAM-MD5 and CRAM-SHA1
sub do_smtp_auth_cram {
my $u = shift; # auth user
my $p = shift; # auth password
my $as = shift; # auth string
my $r = ''; # will store smtp response
my $auth_string = "AUTH $as";
do_smtp_gen($auth_string, '334', \$r, '', '', $G::auth_showpt ? \&unencode_smtp : '')
|| return(0);
$r =~ s/^....//; # maybe something a little better here?
# specify which type of digest we need based on $as
my $d = get_digest($p, $r, ($as =~ /-SHA1$/ ? 'sha1' : 'md5'));
$auth_string = eb64("$u $d");
do_smtp_gen($auth_string, '235', undef, '', $G::auth_showpt ? "$u $d" : '') || return(0);
return(1);
}
sub do_smtp_auth_login {
my $u = shift; # auth user
my $p = shift; # auth password
my $as = shift; # auth string
do_smtp_gen("AUTH $as", '334', undef, '', '', $G::auth_showpt ? \&unencode_smtp : '')
|| return(0);
do_smtp_gen(eb64($u), '334', undef, '', $G::auth_showpt ? $u : '', $G::auth_showpt ? \&unencode_smtp : '')
|| return(0);
do_smtp_gen(eb64($p), '235', undef, '',
$G::auth_showpt ? ($G::auth_hidepw || $p) : eb64($G::auth_hidepw || $p))
|| return(0);
return(1);
}
sub do_smtp_auth_plain {
my $u = shift; # auth user
my $p = shift; # auth password
my $as = shift; # auth string
return(do_smtp_gen("AUTH $as " . eb64("\0$u\0$p"), '235', undef, '',
$G::auth_showpt ? "AUTH $as \\0$u\\0" . ($G::auth_hidepw || $p)
: "AUTH $as " . eb64("\0$u\0" . ($G::auth_hidepw || $p))));
}
sub do_smtp_helo {
my $h = shift; # helo string to use
my $e = shift; # this is a hashref that will be populated w/ server options
my $p = shift; # protocol for the transaction
my $r = ''; # this'll be populated by do_smtp_gen
if ($p eq 'esmtp' || $p eq 'lmtp') {
my $l = $p eq 'lmtp' ? "LHLO" : "EHLO";
if (do_smtp_gen("$l $h", '250', \$r)) {
# There's not a standard structure for the $e hashref, each
# key is stored in the manner that makes the most sense
foreach my $l (split(/\n/, $r)) {
$l =~ s/^....//;
if ($l =~ /^AUTH=?(.*)$/) {
map { $e->{AUTH}{uc($_)} = 1 } (split(' ', $1));
} elsif ($l =~ /^XCLIENT\s*(.*?)$/) {
$e->{XCLIENT} = {}; # prime the pump in case no attributes were advertised
map { $e->{XCLIENT}{uc($_)} = 1 } (split(' ', $1));
} elsif ($l =~ /^STARTTLS$/) {
$e->{STARTTLS} = 1;
} elsif ($l =~ /^PIPELINING$/) {
$e->{PIPELINING} = 1;
$G::pipeline_adv = 1;
} elsif ($l =~ /^PRDR$/) {
$e->{PRDR} = 1;
}
}
return(1);
}
}
if ($p eq 'esmtp' || $p eq 'smtp') {
return(do_smtp_gen("HELO $h", '250'));
}
return(0);
}
sub do_smtp_mail {
my $e = shift; # ehlo response
my $a = shift; # from address
my $m = "MAIL FROM:<$a>";
if ($G::prdr) {
if (!$e->{PRDR}) {
return(1); # PRDR was required but was not advertised. Return error and let caller handle it
} else {
$m .= " PRDR";
}
}
transact(cxn_string => $m, expect => '250', defer => 1, fail_callback => \&smtp_mail_callback);
return(0); # the callback handles failures, so just return here
}
# this only really needs to exist until I figure out a clever way of making
# do_smtp_quit the callback while still preserving the exit codes
sub smtp_mail_callback {
do_smtp_quit(1, 23);
}
sub do_smtp_rcpt {
my $m = shift; # string of comma separated recipients
my $f = 0; # The number of failures we've experienced
my @a = split(/,/, $m);
$G::smtp_rcpt_total = scalar(@a);
foreach my $addr (@a) {
transact(cxn_string => 'RCPT TO:<' . $addr . '>', expect => '250', defer => 1,
fail_callback => \&smtp_rcpt_callback);
}
return(1); # the callback handles failures, so just return here
}
sub smtp_rcpt_callback {
# record that a failure occurred
$G::smtp_rcpt_failures++;
# if the number of failures is the same as the total rcpts (if every rcpt rejected), quit.
if ($G::smtp_rcpt_failures == $G::smtp_rcpt_total) {
do_smtp_quit(1, 24);
}
}
sub do_smtp_data {
my $m = shift; # string to send
my $b = shift; # be brief in the data we send
my $r = ''; # will store smtp response
my $e = $G::prdr ? '(250|353)' : '250';
my $calls = $G::smtp_rcpt_total - $G::smtp_rcpt_failures;
my $ok = transact(cxn_string => $m, expect => $e, summarize_output => $b, return_text => \$r);
# now be a little messy - lmtp is not a lockstep after data - we need to
# listen for as many calls as we had accepted recipients
if ($G::protocol eq 'lmtp') {
foreach my $c (1..($calls-1)) { # -1 because we already got 1 above
$ok += transact(cxn_string => undef, expect => '250');
}
} elsif ($G::protocol eq 'esmtp' && $G::prdr && $r =~ /^353 /) {
foreach my $c (1..$calls) {
transact(cxn_string => undef, expect => '250'); # read the status of each recipient off the wire
}
$ok = transact(cxn_string => undef, expect => '250'); # PRDR has an overall acceptance string, read it here and use it as th success indicator
}
return($ok)
}
sub do_smtp_gen {
my $m = shift; # string to send (if empty, we won't send anything, only read)
my $e = shift; # String we're expecting to get back
my $p = shift; # if this is a scalar ref, assign the server return string to it
my $b = shift; # be brief in the data we print
my $x = shift; # if this is populated, print this instead of $m
my $c = shift; # if this is a code ref, call it on the return value before printing it
my $n = shift; # if true, when the data is sent over the wire, it will not have \r\n appended to it
my $r = shift; # if true, we won't try to ready a response from the server
return transact(cxn_string => $m, expect => $e, return_text => $p,
summarize_output => $b, show_string => $x, print_callback => $c,
no_newline => $n, no_read_response => $r,
);
}
sub do_smtp_proxy {
my $send = undef;
my $print = undef;
my $no_newline = 0;
if ($G::proxy{version} == 2) {
$send = pack("W[12]", 0x0D, 0x0A,0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A);
if ($G::proxy{raw}) {
$send .= $G::proxy{raw};
} else {
# byte 13
# 4 bits = version (required to be 0x2)
# 4 bits = command (0x2 = LOCAL, 0x1 = PROXY)
$send .= pack("W", 0x20 + ($G::proxy{attr}{command} eq 'LOCAL' ? 0x02 : 0x01));
if ($G::proxy{attr}{command} eq 'LOCAL') {
# the protocol byte (14, including family and protocol) are ignored with local. Set to zeros
$send .= pack("W", 0x00);
# and, additionally, if we're local, there isn't going to be any address size (bytes 15 and 16)
$send .= pack("W", 0x00);
} else {
# byte 14
# 4 bits = address family (0x0 = AF_UNSPEC, 0x1 = AF_INET, 0x2 = AF_INET6, 0x3 = AF_UNIX)
# 4 bits = transport protocol (0x0 = UNSPEC, 0x1 = STREAM, 0x2 = DGRAM)
my $byte = 0;
if ($G::proxy{attr}{family} eq 'AF_UNSPEC') {
$byte = 0x00;
} elsif ($G::proxy{attr}{family} eq 'AF_INET') {
$byte = 0x10;
} elsif ($G::proxy{attr}{family} eq 'AF_INET6') {
$byte = 0x20;
} elsif ($G::proxy{attr}{family} eq 'AF_UNIX') {
$byte = 0x30;
}
if ($G::proxy{attr}{protocol} eq 'UNSPEC') {
$byte += 0x0;
} elsif ($G::proxy{attr}{protocol} eq 'STREAM') {
$byte += 0x1;
} elsif ($G::proxy{attr}{protocol} eq 'DGRAM') {
$byte += 0x2;
}
$send .= pack("W", $byte);
# network portion (bytes 17+)
my $net = pack_ip($G::proxy{attr}{source})
. pack_ip($G::proxy{attr}{dest})
. pack("n", $G::proxy{attr}{source_port})
. pack("n", $G::proxy{attr}{dest_port});
$send .= pack("n", length($net)) . $net; # add bytes 15+16 (length of network portion) plus the network portion
}
}
# version 2 is binary, so uuencode it before printing. Also, version 2 REQUIREs that you not send \r\n after it down the wire
$print = eb64($send);
$no_newline = 1;
} else {
if ($G::proxy{raw}) {
$send = "PROXY $G::proxy{raw}";
} else {
$send = join(' ', 'PROXY', $G::proxy{attr}{family}, $G::proxy{attr}{source}, $G::proxy{attr}{dest}, $G::proxy{attr}{source_port}, $G::proxy{attr}{dest_port});
}
}
do_smtp_gen($send, # to be send over the wire
'220', # response code indicating success
undef, # the return string from the server (don't need it)
0, # do not be brief when printing
$print, # if populated, print this instead of $send
undef, # don't want a post-processing callback
$no_newline, # if true, don't add \r\n to the end of $send when sent over the wire
1, # don't read a response - we only want to send the value
);
}
# no special attempt made at verifying, on purpose
sub pack_ip {
my $ip = shift;
if ($ip =~ /:/) {
# this is the stupidest piece of code ever. Please tell me all the fun ways it breaks
my @pieces = split(/:/, $ip);
my $p;
shift(@pieces) if ($pieces[0] eq '' && $pieces[1] eq ''); #
foreach my $word (@pieces) {
if ($word eq '') {
foreach my $i (0..(8-scalar(@pieces))) {
$p .= pack("n", 0);
}
} else {
$p .= pack("n", hex($word));
}
}
return($p);
} else {
return(pack("W*", split(/\./, $ip)));
}
}
# If we detect that the other side has gone away when we were expecting
# to still be reading, come in here to error and die. Abstracted because
# the error message will vary depending on the type of connection
sub handle_disconnect {
my $e = shift || 6; # this is the code we will exit with
if ($G::link{type} eq 'socket-inet') {
ptrans(12, "Remote host closed connection unexpectedly.");
} elsif ($G::link{type} eq 'socket-unix') {
ptrans(12, "Socket closed connection unexpectedly.");
} elsif ($G::link{type} eq 'pipe') {
ptrans(12, "Child process closed connection unexpectedly.");
}
exit($e);
}
sub flush_send_buffer {
my $s = $G::link{type} eq 'pipe' ? $G::link{sock}->{wr} : $G::link{sock};
return if (!$G::send_buffer);
if ($G::link{tls}{active}) {
my $res = Net::SSLeay::write($G::link{tls}{ssl}, $G::send_buffer);
} else {
print $s $G::send_buffer;
}
ptrans(23, hdump($G::send_buffer)) if ($G::show_raw_text);
$G::send_buffer = '';
}
sub send_data {
my $d = shift; # data to write
my $nnl = shift || 0; # if true, don't add a newline (needed for PROXY v2 support)
$G::send_buffer .= $d . ($nnl ? '' : "\r\n");
}
sub recv_line {
# Either an IO::Socket obj or a FH to my child - the thing to read from
my $s = $G::link{type} eq 'pipe' ? $G::link{sock}->{re} : $G::link{sock};
my $r = undef;
my $t = undef;
my $c = 0;
while ($G::recv_buffer !~ m|\n|si) {
last if (++$c > 1000); # Maybe I'll remove this once I trust this code more
if ($G::link{tls}{active}) {
$t = Net::SSLeay::read($G::link{tls}{ssl});
return($t) if (!defined($t));
# THIS CODE COPIED FROM THE ELSE BELOW. Found I could trip this condition
# by having the server sever the connection but not have swaks realize the
# connection was gone. For instance, send a PIPELINE mail that includes a
# "-q rcpt". There was a bug in swaks that made it try to send another quit
# later, thus tripping this "1000 reads" error (but only in TLS).
# Short term: add line below to prevent these reads
# Short Term: fix the "double-quit" bug
# Longer term: test to see if remote side closed connection
# the above line should be good enough but it isn't returning
# undef for some reason. I think heuristically it will be sufficient
# to just look for an empty packet (I hope. gulp). Comment out the
# following line if your swaks seems to be saying that it lost connection
# for no good reason. Then email me about it.
return(undef()) if (!length($t));
} elsif ($G::link{type} eq 'pipe') {
# XXX in a future release see if I can get read() or equiv to work on a pipe
$t = <$s>;
return($t) if (!defined($t));
# THIS CODE COPIED FROM THE ELSE BELOW.
# the above line should be good enough but it isn't returning
# undef for some reason. I think heuristically it will be sufficient
# to just look for an empty packet (I hope. gulp). Comment out the
# following line if your swaks seems to be saying that it lost connection
# for no good reason. Then email me about it.
return(undef()) if (!length($t));
} else {
# if you're having problems with reads, swap the comments on the
# the following two lines
my $recv_r = recv($s, $t, 8192, 0);
#$t = <$s>;
return($t) if (!defined($t));
# the above line should be good enough but it isn't returning
# undef for some reason. I think heuristically it will be sufficient
# to just look for an empty packet (I hope. gulp). Comment out the
# following line if your swaks seems to be saying that it lost connection
# for no good reason. Then email me about it.
return(undef()) if (!length($t));
#print "\$t = $t (defined = ", defined($t) ? "yes" : "no",
# "), \$recv_r = $recv_r (", defined($recv_r) ? "yes" : "no", ")\n";
}
$G::recv_buffer .= $t;
ptrans(33, hdump($t)) if ($G::show_raw_text);
}
if ($c >= 1000) {
# If you saw this in the wild, I'd love to hear more about it
# at proj-swaks@jetmore.net
ptrans(12, "In recv_line, hit loop counter. Continuing in unknown state");
}
# using only bare newlines is bound to cause me problems in the future
# but it matches the expectation we've already been using. All we can
# do is hone in on the proper behavior iteratively.
if ($G::recv_buffer =~ s|^(.*?\n)||si) {
$r = $1;
} else {
ptrans(12, "I'm in an impossible state");
}
$r =~ s|\r||msg;
return($r);
}
# any request which has immediate set will be checking the return code.
# any non-immediate request will handle results through fail_callback().
# therefore, only return the state of the last transaction attempted,
# which will always be immediate
# defer - if true, does not require immediate flush when pipelining
# cxn_string - What we will be sending the server. If undefined, we won't send, only read
# no_read_response - if true, we won't read a response from the server, we'll just send
# summarize_output - if true, don't print to terminal everything we send to server
# no_newline - if true, do not append \r\n to the data we send to server
# return_text - should be scalar ref. will be assigned reference to what was returned from server
# print_callback - if present and a code reference, will be called with server return data for printing to terminal
# fail_callback - if present and a code reference, will be called on failure
sub transact {
my %h = @_; # this is an smtp transaction element
my $ret = 1; # this is our return value
my @handlers = (); # will hold any fail_handlers we need to run
my $time = ''; # used in time lapse calculations
push(@G::pending_send, \%h); # push onto send queue
if (!($G::pipeline && $G::pipeline_adv) || !$h{defer}) {
if ($G::show_time_lapse eq 'hires') {
$time = [Time::HiRes::gettimeofday()];
}
elsif ($G::show_time_lapse eq 'integer') {
$time = time();
}
while (my $i = shift(@G::pending_send)) {
if (defined($i->{cxn_string})) {
ptrans(21, $i->{show_string} || $i->{cxn_string}, $i->{summarize_output});
send_data($i->{cxn_string}, $i->{no_newline});
}
push(@G::pending_recv, $i) if (!$i->{no_read_response});
}
flush_send_buffer();
do_smtp_drop() if ($G::drop_before_read);
while (my $i = shift(@G::pending_recv)) {
my $buff = '';
eval {
local $SIG{'ALRM'} = sub {
$buff = "Timeout ($G::link{timeout} secs) waiting for server response";
die;
};
alarm($G::link{timeout});
while ($buff !~ /^\d\d\d /m) {
my $l = recv_line();
$buff .= $l;
if (!defined($l)) {
$G::link{lost_cxn} = 1;
last;
}
}
chomp($buff);
alarm(0);
};
if ($G::show_time_lapse eq 'hires') {
$time = sprintf("%0.03f", Time::HiRes::tv_interval($time, [Time::HiRes::gettimeofday()]));
ptrans(11, "response in ${time}s");
$time = [Time::HiRes::gettimeofday()];
} elsif ($G::show_time_lapse eq 'integer') {
$time = time() - $time;
ptrans(11, "response in ${time}s");
$time = time();
}
${$i->{return_text}} = $buff;
$buff = &{$i->{print_callback}}($buff) if (ref($i->{print_callback}) eq 'CODE');
my $ptc;
($ret,$ptc) = $buff !~ /^$i->{expect} /m ? (0,32) : (1,31);
ptrans($ptc, $buff) if ($buff);
if ($G::link{lost_cxn}) {
if ($G::link{allow_lost_cxn}) {
# this means the calling code wants to handle a lost cxn itself
return($ret);
} else {
# if caller didn't want to handle, we'll handle a lost cxn ourselves
handle_disconnect();
}
}
if (!$ret && ref($i->{fail_callback}) eq 'CODE') {
push(@handlers, $i->{fail_callback});
}
}
}
foreach my $h (@handlers) { &{$h}(); }
return($ret);
}
# a quick-and-dirty hex dumper. Currently used by --show-raw-text
sub hdump {
my $r = shift;
my $c = 0; # counter
my $i = 16; # increment value
my $b; # buffer
while (length($r) && ($r =~ s|^(.{1,$i})||smi)) {
my $s = $1; # $s will be the ascii string we manipulate for display
my @c = map { ord($_); } (split('', $s));
$s =~ s|[^\x21-\x7E]|.|g;
my $hfs = ''; # This is the hex format string for printf
for (my $hc = 0; $hc < $i; $hc++) {
$hfs .= ' ' if (!($hc%4));
if ($hc < scalar(@c)) { $hfs .= '%02X '; } else { $hfs .= ' '; }
}
$b .= sprintf("%04d:$hfs %-16s\n", $c, @c, $s);
$c += $i;
}
chomp($b); # inelegant remnant of hdump's previous life
return($b)
}
sub unencode_smtp {
my $t = shift;
my @t = split(' ', $t, 2);
if ($t[1] =~ /\s/) {
# very occasionally we can have a situation where a successful response will
# be b64 encoded, while an error will not be. Try to tell the difference.
return($t);
} else {
return("$t[0] " . db64($t[1]));
}
}
sub obtain_from_netrc {
my $field = shift;
my $login = shift;
return if !avail('netrc');
if (my $netrc = Net::Netrc->lookup($G::link{server}, defined($login) ? $login : ())) {
return($netrc->$field);
}
return;
}
sub interact {
my $prompt = shift;
my $regexp = shift;
my $hide_input = shift;
my $response = '';
do {
print $prompt;
if (!$hide_input || !$G::protect_prompt || $G::interact_method eq 'default') {
chomp($response = <STDIN>);
} else {
if ($^O eq 'MSWin32') {
#if ($G::interact_method eq "win32-console" ||
# (!$G::interact_method && load("Win32::Console")))
#{
# Couldn't get this working in the time I wanted to devote to it
#}
if ($G::interact_method eq "win32-readkey" ||
(!$G::interact_method && load("Term::ReadKey")))
{
$G::interact_method ||= "win32-readkey";
# the trick to replace input w/ '*' doesn't work on Win32
# Term::ReadKey, so just use it as an stty replacement
ReadMode('noecho');
# need to think about this on windows some more
#local $SIG{INT} = sub { ReadMode('restore'); };
chomp($response = <STDIN>);
ReadMode('restore');
} else {
$G::interact_method ||= "default";
chomp($response = <STDIN>);
}
} else {
if ($G::interact_method eq "unix-readkey" || (!$G::interact_method && load("Term::ReadKey"))) {
$G::interact_method ||= "unix-readkey";
my @resp = ();
ReadMode('raw');
#local $SIG{INT} =
# reevaluate this code - what happens if del is first char we press?
while ((my $kp = ReadKey(0)) ne "\n") {
my $kp_num = ord($kp);
if($kp_num == 127 || $kp_num == 8) {
next if (!scalar(@resp));
pop(@resp);
print "\b \b";
} elsif($kp_num >= 32) {
push(@resp, $kp);
print "*";
}
}
ReadMode('restore');
$response = join('', @resp);
} elsif ($G::interact_method eq "unix-stty" || (!$G::interact_method && open(STTY, "stty -a |"))) {
$G::interact_method ||= "unix-stty";
{ my $foo = join('', <STTY>); }
system('stty', '-echo');
chomp($response = <STDIN>);
system('stty', 'echo');
} else {
$G::interact_method ||= "default";
chomp($response = <STDIN>);
}
}
}
} while ($regexp ne 'SKIP' && $response !~ /$regexp/);
return($response);
}
sub get_messageid {
if (!$G::message_id) {
my @time = localtime();
$G::message_id = sprintf("%04d%02d%02d%02d%02d%02d.%06d\@%s",
$time[5]+1900, $time[4]+1, $time[3], $time[2], $time[1], $time[0],
$$, get_hostname());
}
return($G::message_id);
}
sub get_hostname {
# in some cases hostname returns value but gethostbyname doesn't.
return("") if (!avail("hostname"));
my $h = hostname();
return("") if (!$h);
my $l = (gethostbyname($h))[0];
return($l || $h);
}
sub get_server {
my $addr = shift;
my $pref = -1;
my $server = "localhost";
if ($addr =~ /\@?\[(\d+\.\d+\.\d+\.\d+)\]$/) {
# handle automatic routing of domain literals (user@[1.2.3.4])
return($1);
} elsif ($addr =~ /\@?\#(\d+)$/) {
# handle automatic routing of decimal domain literals (user@#16909060)
$addr = $1;
return(($addr/(2**24))%(2**8) . '.' . ($addr/(2**16))%(2**8) . '.' .
($addr/(2**8))%(2**8) . '.' . ($addr/(2**0))%(2**8));
}
if (!avail("dns")) {
ptrans(12, avail_str("dns"). ". Using $server as mail server");
return($server);
}
my $res = Net::DNS::Resolver->new();
$addr =~ s/^.*\@([^\@]*)$/$1/;
return($server) if (!$addr);
$server = $addr;
my @mx = mx($res, $addr);
foreach my $rr (sort { $a->preference <=> $b->preference } @mx) {
if ($G::link{force_ipv4}) {
if ($res->query($rr->exchange, 'A')) {
$server = $rr->exchange;
last;
}
} elsif ($G::link{force_ipv6}) {
if ($res->query($rr->exchange, 'AAAA') || $res->query($rr->exchange, 'A6')) {
$server = $rr->exchange;
last;
}
} else {
# this is the old default behavior. Take the best priority MX, no matter what.
$server = $rr->exchange;
last;
}
}
return($server);
}
sub load {
my $m = shift;
return $G::modules{$m} if (exists($G::modules{$m}));
eval("use $m");
return $G::modules{$m} = $@ ? 0 : 1;
}
# Currently this is just an informational string - it's set on both
# success and failure. It currently has four output formats (supported,
# supported but not optimal, unsupported, unsupported and missing optimal)
sub avail_str { return $G::dependencies{$_[0]}{errstr}; }
sub avail {
my $f = shift; # this is the feature we want to check support for (auth, tls)
my $s = \%G::dependencies;
# return immediately if we've already tested this.
return($s->{$f}{avail}) if (exists($s->{$f}{avail}));
$s->{$f}{req_failed} = [];
$s->{$f}{opt_failed} = [];
foreach my $m (@{$s->{$f}{req}}) {
push(@{$s->{$f}{req_failed}}, $m) if (!load($m));
}
foreach my $m (@{$s->{$f}{opt}}) {
push(@{$s->{$f}{opt_failed}}, $m) if (!load($m));
}
if (scalar(@{$s->{$f}{req_failed}})) {
$s->{$f}{errstr} = "$s->{$f}{name} not available: requires " . join(', ', @{$s->{$f}{req_failed}});
if (scalar(@{$s->{$f}{opt_failed}})) {
$s->{$f}{errstr} .= ". Also missing optimizing " . join(', ', @{$s->{$f}{opt_failed}});
}
return $s->{$f}{avail} = 0;
} else {
if (scalar(@{$s->{$f}{opt_failed}})) {
$s->{$f}{errstr} = "$s->{$f}{name} supported, but missing optimizing " .
join(', ', @{$s->{$f}{opt_failed}});
} else {
$s->{$f}{errstr} = "$s->{$f}{name} supported";
}
return $s->{$f}{avail} = 1;
}
}
sub get_digest {
my $secr = shift;
my $chal = shift;
my $type = shift || 'md5';
my $ipad = chr(0x36) x 64;
my $opad = chr(0x5c) x 64;
if ($chal !~ /^</) {
chomp($chal = db64($chal));
}
if (length($secr) > 64) {
if ($type eq 'md5') {
$secr = Digest::MD5::md5($secr);
} elsif ($type eq 'sha1') {
$secr = Digest::SHA::sha1($secr);
}
} else {
$secr .= chr(0) x (64 - length($secr));
}
my $digest = $type eq 'md5' ? Digest::MD5::md5_hex(($secr ^ $opad), Digest::MD5::md5(($secr ^ $ipad), $chal))
: Digest::SHA::sha1_hex(($secr ^ $opad), Digest::SHA::sha1(($secr ^ $ipad), $chal));
return($digest);
}
sub test_support {
my $return = shift;
my $lines = [];
my $s = \%G::dependencies;
foreach my $act (sort { $s->{$a}{name} cmp $s->{$b}{name} } keys %$s) {
if ($return) {
push(@$lines, @{ptrans(avail($act) ? 11 : 12, avail_str($act), undef, 1)});
}
else {
ptrans(avail($act) ? 11 : 12, avail_str($act));
}
}
if ($return) {
return($lines);
}
}
sub time_to_seconds {
my $t = shift;
if ($t !~ /^(\d+)([hms])?$/i) {
ptrans(12, 'Unknown timeout format \'' . $t . '\'');
exit(1);
} else {
my $r = $1;
my $u = lc($2);
if ($u eq 'h') {
return($r * 3600);
} elsif ($u eq 'm') {
return($r * 60);
} else {
return($r);
}
}
}
sub load_dependencies {
%G::dependencies = (
auth => { name => "Basic AUTH", opt => ['MIME::Base64'],
req => [] },
auth_cram_md5 => { name => "AUTH CRAM-MD5", req => ['Digest::MD5'] },
auth_cram_sha1 => { name => "AUTH CRAM-SHA1", req => ['Digest::SHA'] },
auth_ntlm => { name => "AUTH NTLM", req => ['Authen::NTLM'] },
auth_digest_md5 => { name => "AUTH DIGEST-MD5", req => ['Authen::SASL'] },
dns => { name => "MX Routing", req => ['Net::DNS'] },
netrc => { name => 'Netrc Credentials', req => ['Net::Netrc'] },
tls => { name => "TLS", req => ['Net::SSLeay'] },
pipe => { name => "Pipe Transport", req => ['IPC::Open2'] },
socket => { name => "Socket Transport", req => ['IO::Socket'] },
ipv6 => { name => "IPv6", req => ['IO::Socket::INET6'] },
date_manip => { name => "Date Manipulation", req => ['POSIX'] },
hostname => { name => "Local Hostname Detection", req => ['Sys::Hostname'] },
hires_timing => { name => "High Resolution Timing", req => ['Time::HiRes'] },
);
}
sub process_opt_silent {
my $opt = shift;
my $arg = shift;
if ($arg =~ /^[123]$/) {
return($arg);
}
else {
return(1);
}
}
sub get_option_struct {
use constant {
OP_ARG_OPT => 0x01, # option takes an optional argument
OP_ARG_REQ => 0x02, # option takes a required argument
OP_ARG_NONE => 0x04, # option does not take any argument (will return boolean)
OP_FROM_PROMPT => 0x08, # option prompts for an argument if none provided
OP_FROM_FILE => 0x10, # option treats arg of '-' to mean 'read from stdin' (no prompt)
OP_DEPRECATED => 0x20, # This option is deprecated
};
@G::raw_option_data = (
# location of config file. Note that the "config" option is processed differently
# than any other option because it needs to be processed before standard option processing
# can happen. We still define it here to make Getopt::Long and fetch_args() happy.
{ opts => ['config'], suffix => ':s',
cfgs => OP_ARG_OPT,
okey => 'config_file', type => 'scalar', },
# envelope-(f)rom address
{ opts => ['from', 'f'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'From: ', match => '^.*$',
okey => 'mail_from', type => 'scalar', },
# envelope-(t)o address
{ opts => ['to', 't'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'To: ', match => '^.+$',
okey => 'mail_to', type => 'scalar', },
# (h)elo string
{ opts => ['helo', 'ehlo', 'lhlo', 'h'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'Helo: ', match => '^.*$',
okey => 'mail_helo', type => 'scalar', },
# (s)erver to use
{ opts => ['server', 's'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'Server: ', match => '^.*$',
okey => 'mail_server', type => 'scalar', },
# force ipv4 only
{ opts => ['4'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'force_ipv4', type => 'scalar', },
# force ipv6 only
{ opts => ['6'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'force_ipv6', type => 'scalar', },
# copy MX/routing from another domain
{ opts => ['copy-routing'], suffix => ':s',
cfgs => OP_ARG_REQ,
okey => 'copy_routing', type => 'scalar', },
# (p)ort to use
{ opts => ['port', 'p'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'Port: ', match => '^\w+$',
okey => 'mail_port', type => 'scalar', },
# protocol to use (smtp, esmtp, lmtp)
{ opts => ['protocol'], suffix => '=s',
cfgs => OP_ARG_REQ,
okey => 'mail_protocol', type => 'scalar', },
# (d)ata portion ('\n' for newlines)
{ opts => ['data', 'd'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT|OP_FROM_FILE,
prompt => 'Data: ', match => '^.*$',
okey => 'mail_data', type => 'scalar', },
# use the --dump text as default body
{ opts => ['dump-as-body', 'dab'], suffix => ':s',
cfgs => OP_ARG_OPT,
okey => 'dump_as_body', type => 'scalar', },
# implies --dump-as-body; forces raw passwords to be used
{ opts => ['dump-as-body-shows-password', 'dabsp'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'dab_sp', type => 'scalar', },
# timeout for each trans (def 30s)
{ opts => ['timeout'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'Timeout: ', match => '^\d+[hHmMsS]?$',
okey => 'timeout', type => 'scalar', },
# (q)uit after
{ opts => ['quit-after', 'quit', 'q'], suffix => '=s',
cfgs => OP_ARG_REQ,
okey => 'quit_after', type => 'scalar', },
# drop after (don't quit, just drop)
{ opts => ['drop-after', 'drop', 'da'], suffix => '=s',
cfgs => OP_ARG_REQ,
okey => 'drop_after', type => 'scalar', },
# drop after send (between send and read)
{ opts => ['drop-after-send', 'das'], suffix => '=s',
cfgs => OP_ARG_REQ,
okey => 'drop_after_send', type => 'scalar', },
# do (n)ot print data portion
{ opts => ['suppress-data', 'n'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'suppress_data', type => 'scalar', },
# force auth, exit if not supported
{ opts => ['auth', 'a'], suffix => ':s',
cfgs => OP_ARG_OPT,
okey => 'auth', type => 'scalar', },
# user for auth
{ opts => ['auth-user', 'au'], suffix => ':s',
cfgs => OP_ARG_OPT, # we dynamically change this later
okey => 'auth_user', type => 'scalar', },
# pass for auth
{ opts => ['auth-password', 'ap'], suffix => ':s',
cfgs => OP_ARG_OPT, # we dynamically change this later
okey => 'auth_pass', type => 'scalar', },
# auth type map
{ opts => ['auth-map', 'am'], suffix => '=s',
cfgs => OP_ARG_REQ,
okey => 'auth_map', type => 'scalar', },
# extra, authenticator-specific options
{ opts => ['auth-extra', 'ae'], suffix => '=s',
cfgs => OP_ARG_REQ,
okey => 'auth_extra', type => 'scalar', },
# hide passwords when possible
{ opts => ['auth-hide-password', 'ahp'], suffix => ':s',
cfgs => OP_ARG_OPT,
okey => 'auth_hidepw', type => 'scalar', },
# translate base64 strings
{ opts => ['auth-plaintext', 'apt'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'auth_showpt', type => 'scalar', },
# auth optional (ignore failure)
{ opts => ['auth-optional', 'ao'], suffix => ':s',
cfgs => OP_ARG_OPT,
okey => 'auth_optional', type => 'scalar', },
# req auth if avail
{ opts => ['auth-optional-strict', 'aos'], suffix => ':s',
cfgs => OP_ARG_OPT,
okey => 'auth_optional_strict', type => 'scalar', },
# report capabilties
{ opts => ['support'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'get_support', type => 'scalar', },
# local interface to use
{ opts => ['local-interface', 'li'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'Interface: ', match => '^.*$',
okey => 'lint', type => 'scalar', },
# local port
{ opts => ['local-port', 'lport', 'lp'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'Local Port: ', match => '^\w+$',
okey => 'lport', type => 'scalar', },
# use TLS
{ opts => ['tls'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'tls', type => 'scalar', },
# use tls if available
{ opts => ['tls-optional', 'tlso'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'tls_optional', type => 'scalar', },
# req tls if avail
{ opts => ['tls-optional-strict', 'tlsos'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'tls_optional_strict', type => 'scalar', },
# use tls if available
{ opts => ['tls-on-connect', 'tlsc'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'tls_on_connect', type => 'scalar', },
# local cert to present to server
{ opts => ['tls-cert'], suffix => '=s',
cfgs => OP_ARG_REQ,
okey => 'tls_cert', type => 'scalar', },
# local key to present to server
{ opts => ['tls-key'], suffix => '=s',
cfgs => OP_ARG_REQ,
okey => 'tls_key', type => 'scalar', },
# tls protocol to use
{ opts => ['tls-protocol', 'tlsp'], suffix => '=s',
cfgs => OP_ARG_REQ,
okey => 'tls_protocol', type => 'scalar', },
# tls cipher to use
{ opts => ['tls-cipher'], suffix => '=s',
cfgs => OP_ARG_REQ,
okey => 'tls_cipher', type => 'scalar', },
# save tls peer certificate
{ opts => ['tls-get-peer-cert'], suffix => ':s',
cfgs => OP_ARG_OPT,
okey => 'tls_get_peer_cert', type => 'scalar', },
# hostname to request in TLS SNI header
{ opts => ['tls-sni'], suffix => '=s',
cfgs => OP_ARG_REQ,
okey => 'tls_sni_hostname', type => 'scalar', },
# require verification of server certificate
{ opts => ['tls-verify'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'tls_verify', type => 'scalar', },
# local key to present to server
{ opts => ['tls-ca-path'], suffix => '=s',
cfgs => OP_ARG_REQ,
okey => 'tls_ca_path', type => 'scalar', },
# suppress output to varying degrees
{ opts => ['silent', 'S'], suffix => ':i',
cfgs => OP_ARG_OPT,
callout => \&process_opt_silent,
okey => 'silent', type => 'scalar', },
# Don't strip From_ line from DATA
{ opts => ['no-strip-from', 'nsf'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'no_strip_from', type => 'scalar', },
# Don't show send/receive hints (legacy)
{ opts => ['no-hints', 'nth'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'no_hints', type => 'scalar', },
# Don't show transaction hints
{ opts => ['no-send-hints', 'nsh'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'no_hints_send', type => 'scalar', },
# Don't show transaction hints
{ opts => ['no-receive-hints', 'nrh'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'no_hints_recv', type => 'scalar', },
# Don't show transaction hints
{ opts => ['no-info-hints', 'nih'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'no_hints_info', type => 'scalar', },
# Don't show reception lines
{ opts => ['hide-receive', 'hr'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'hide_receive', type => 'scalar', },
# Don't show sending lines
{ opts => ['hide-send', 'hs'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'hide_send', type => 'scalar', },
# Don't echo input on potentially sensitive prompts
{ opts => ['protect-prompt', 'pp'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'protect_prompt', type => 'scalar', },
# Don't show any swaks-generated, non-error informational lines
{ opts => ['hide-informational', 'hi'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'hide_informational', type => 'scalar', },
# Don't send any output to the terminal
{ opts => ['hide-all', 'ha'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'hide_all', type => 'scalar', },
# print lapse for send/recv
{ opts => ['show-time-lapse', 'stl'], suffix => ':s',
cfgs => OP_ARG_OPT,
okey => 'show_time_lapse', type => 'scalar', },
# print version and exit
{ opts => ['version'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'version', type => 'scalar', },
# print help and exit
{ opts => ['help'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'help', type => 'scalar', },
# don't touch the data
{ opts => ['no-data-fixup', 'ndf'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'no_data_fixup', type => 'scalar', },
# show dumps of the raw read/written text
{ opts => ['show-raw-text', 'raw'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'show_raw_text', type => 'scalar', },
# specify file to write to
{ opts => ['output', 'output-file'], suffix => '=s',
cfgs => OP_ARG_REQ,
okey => 'output_file', type => 'scalar', },
# specify file to write to
{ opts => ['output-file-stdout'], suffix => '=s',
cfgs => OP_ARG_REQ,
okey => 'output_file_stdout', type => 'scalar', },
# specify file to write to
{ opts => ['output-file-stderr'], suffix => '=s',
cfgs => OP_ARG_REQ,
okey => 'output_file_stderr', type => 'scalar', },
# command to communicate with
{ opts => ['pipe'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'Pipe: ', match => '^.+$',
okey => 'pipe_cmd', type => 'scalar', },
# unix domain socket to talk to
{ opts => ['socket'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'Socket File: ', match => '^.+$',
okey => 'socket', type => 'scalar', },
# the content of the body of the DATA
{ opts => ['body'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT|OP_FROM_FILE,
prompt => 'Body: ', match => '.+',
okey => 'body_822', type => 'scalar', },
# A file to attach
{ opts => ['attach-name'], suffix => ':s',
cfgs => OP_ARG_OPT,
okey => 'attach_name', akey => 'attach_accum', type => 'list', },
# A file to attach
{ opts => ['attach-type'], suffix => ':s',
cfgs => OP_ARG_REQ,
okey => 'attach_type', akey => 'attach_accum', type => 'list', },
# A file to attach
{ opts => ['attach'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_FILE,
okey => 'attach_attach', akey => 'attach_accum', type => 'list', },
# A file to attach
{ opts => ['attach-body'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_FILE,
okey => 'attach_body', akey => 'attach_accum', type => 'list', },
# replacement for %NEW_HEADERS% DATA token
{ opts => ['add-header', 'ah'], suffix => ':s',
cfgs => OP_ARG_REQ,
okey => 'add_header', type => 'list', },
# replace header if exist, else add
{ opts => ['header'], suffix => ':s',
cfgs => OP_ARG_REQ,
okey => 'header', type => 'list', },
# build options and dump
{ opts => ['dump'], suffix => ':s',
cfgs => OP_ARG_OPT,
okey => 'dump_args', type => 'scalar', },
# build options and dump the generate message body (EML)
{ opts => ['dump-mail'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'dump_mail', type => 'scalar', },
# attempt PIPELINING
{ opts => ['pipeline'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'pipeline', type => 'scalar', },
# attempt PRDR
{ opts => ['prdr'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'prdr', type => 'scalar', },
# use getpwuid building -f
{ opts => ['force-getpwuid'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'force_getpwuid', type => 'scalar', },
# XCLIENT
# These xclient_attrs options all get pushed onto an array so that we can determine their order later
# argument is a raw XCLIENT string
{ opts => ['xclient'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'XCLIENT string: ', match => '^.+$',
okey => 'xclient_raw', akey => 'xclient_accum', type => 'list', },
# XCLIENT NAME
{ opts => ['xclient-name'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'XCLIENT name: ', match => '^.+$',
okey => 'xclient_name', akey => 'xclient_accum', type => 'scalar', },
# XCLIENT ADDR
{ opts => ['xclient-addr'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'XCLIENT addr: ', match => '^.+$',
okey => 'xclient_addr', akey => 'xclient_accum', type => 'scalar', },
# XCLIENT PORT
{ opts => ['xclient-port'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'XCLIENT port: ', match => '^.+$',
okey => 'xclient_port', akey => 'xclient_accum', type => 'scalar', },
# XCLIENT PROTO
{ opts => ['xclient-proto'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'XCLIENT proto: ', match => '^.+$',
okey => 'xclient_proto', akey => 'xclient_accum', type => 'scalar', },
# XCLIENT DESTADDR
{ opts => ['xclient-destaddr'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'XCLIENT destaddr: ', match => '^.+$',
okey => 'xclient_destaddr', akey => 'xclient_accum', type => 'scalar', },
# XCLIENT DESTPORT
{ opts => ['xclient-destport'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'XCLIENT destport: ', match => '^.+$',
okey => 'xclient_destport', akey => 'xclient_accum', type => 'scalar', },
# XCLIENT HELO
{ opts => ['xclient-helo'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'XCLIENT helo: ', match => '^.+$',
okey => 'xclient_helo', akey => 'xclient_accum', type => 'scalar', },
# XCLIENT LOGIN
{ opts => ['xclient-login'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'XCLIENT login: ', match => '^.+$',
okey => 'xclient_login', akey => 'xclient_accum', type => 'scalar', },
# XCLIENT REVERSE_NAME
{ opts => ['xclient-reverse-name'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'XCLIENT reverse_name: ', match => '^.+$',
okey => 'xclient_reverse_name', akey => 'xclient_accum', type => 'scalar', },
# XCLIENT delimiter. Used to indicate that user wants to start a new xclient attr grouping
{ opts => ['xclient-delim'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'xclient_delim', akey => 'xclient_accum', type => 'list', },
# if set, XCLIENT will proceed even if XCLIENT not advertised
{ opts => ['xclient-optional'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'xclient_optional', type => 'scalar', },
# proceed if xclient not offered, but fail if offered and not accepted
{ opts => ['xclient-optional-strict'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'xclient_optional_strict', type => 'scalar', },
# we send xclient after starttls by default. if --xclient-before-starttls will send before tls
{ opts => ['xclient-before-starttls'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'xclient_before_starttls', type => 'scalar', },
# Don't require that the --xclient-ATTR attributes be advertised by server
{ opts => ['xclient-no-verify'], suffix => '',
cfgs => OP_ARG_NONE,
okey => 'xclient_no_verify', type => 'scalar', },
## xclient send by default after first helo, but can be sent almost anywhere (cf quit-after)
# { opts => ['xclient-after'], suffix => ':s',
# okey => 'xclient_after', type => 'scalar', },
# PROXY
# argument is the raw PROXY string
{ opts => ['proxy'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'PROXY string: ', match => '^.+$',
okey => 'proxy_raw', type => 'scalar', },
# PROXY version (1 or 2)
{ opts => ['proxy-version'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'PROXY version: ', match => '^[12]$',
okey => 'proxy_version', type => 'scalar', },
# PROXY protocol family (TCP4 or TCP6)
{ opts => ['proxy-family'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'PROXY family: ', match => '^.+$',
okey => 'proxy_family', type => 'scalar', },
# PROXY protocol command (LOCAL or PROXY)
{ opts => ['proxy-command'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'PROXY command: ', match => '^.+$',
okey => 'proxy_command', type => 'scalar', },
# PROXY transport protocol
{ opts => ['proxy-protocol'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'PROXY protocol: ', match => '^.+$',
okey => 'proxy_protocol', type => 'scalar', },
# PROXY source address (IPv4 or IPv6)
{ opts => ['proxy-source'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'PROXY source: ', match => '^.+$',
okey => 'proxy_source', type => 'scalar', },
# PROXY source port
{ opts => ['proxy-source-port'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'PROXY source_port: ', match => '^.+$',
okey => 'proxy_source_port', type => 'scalar', },
# PROXY destination address (IPv4 or IPv6)
{ opts => ['proxy-dest'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'PROXY dest: ', match => '^.+$',
okey => 'proxy_dest', type => 'scalar', },
# PROXY destination port
{ opts => ['proxy-dest-port'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_FROM_PROMPT,
prompt => 'PROXY dest_port: ', match => '^.+$',
okey => 'proxy_dest_port', type => 'scalar', },
# this option serve no purpose other than testing the deprecation system
{ opts => ['trigger-deprecation'], suffix => ':s',
cfgs => OP_ARG_REQ|OP_DEPRECATED,
okey => 'trigger_deprecation', type => 'scalar', },
);
return(\@G::raw_option_data);
}
# returns %O, the large raw option hash
# This sub is a jumping point. We will construct an argv based on the different ways that options can be specified
# and call GetOptions multiple times. We are essentially "layering" options. First we load from a config file (if
# exists/specified), then from any environment variables, then the actual command line.
sub load_args {
my %ARGS = (); # this is the structure that gets returned
my @fakeARGV = ();
# we load our options processing hash here. We abstract it back from the
# native getopt-format because we need to be able to intercept "no-" options
my $option_list = get_option_struct();
# do a loop through the options and make sure they are structured the way we expect
foreach my $e (@$option_list) {
if (!exists($e->{okey}) || !$e->{okey}) {
ptrans(12, 'Option configuration missing an okey (this is a swaks bug)');
exit(1);
}
elsif (!exists($e->{opts}) || ref($e->{opts}) ne 'ARRAY') {
ptrans(12, 'Option ' . $e->{okey} . ' missing or corrupt opts key (this is a swaks bug)');
exit(1);
}
elsif (!exists($e->{suffix})) {
ptrans(12, 'Option ' . $e->{okey} . ' missing suffix key (this is a swaks bug)');
exit(1);
}
elsif (!exists($e->{type}) || $e->{type} !~ /^(scalar|list)$/) {
ptrans(12, 'Option ' . $e->{okey} . ' missing or invalid type key (this is a swaks bug)');
exit(1);
}
elsif (!exists($e->{cfgs})) {
ptrans(12, 'Option ' . $e->{okey} . ' missing cfgs key (this is a swaks bug)');
exit(1);
}
$e->{akey} = $e->{okey} if (!exists($e->{akey}));
# 'cfgs' stores the okey config for easier access later
$ARGS{cfgs}{$e->{okey}} = $e;
}
# we want to process config files first. There's a default config file in
# ~/.swaksrc, but it is possible for the user to override this with the
# --config options. So, find the one and only file we will use here.
# If we encounter --config in later processing it is a noop.
# first find the default file
my $config_file = '';
my $skip_config = 0;
my $config_is_default = 1;
foreach my $v (qw(SWAKS_HOME HOME LOGDIR)) {
if (exists($ENV{$v}) && length($ENV{$v}) && -f $ENV{$v} . '/.swaksrc') {
$config_file = $ENV{$v} . '/.swaksrc';
last;
}
}
# then look through the ENV args to see if another file set there
if (exists($ENV{SWAKS_OPT_config})) {
if (!$ENV{SWAKS_OPT_config}) {
# if exist but not set, it just means "don't use default file"
$skip_config = 1;
} else {
$config_file = $ENV{SWAKS_OPT_config};
$config_is_default = 0;
}
}
# lastly go (backwards) through original command line looking for config file,
# choosing the first one found (meaning last one specified)
for (my $i = scalar(@ARGV) - 1; $i >= 0; $i--) {
if ($ARGV[$i] =~ /^-?-config$/) {
if ($i == scalar(@ARGV) - 1 || $ARGV[$i+1] =~ /^-/) {
$skip_config = 1;
} else {
$config_file = $ARGV[$i+1];
$config_is_default = 0;
$skip_config = 0;
}
last;
}
}
# All of the above will result in $config_file either being empty or
# containing the one and only config file we will use (though merged with DATA)
if (!$skip_config) {
my @configs = ('&DATA');
push(@configs, $config_file) if ($config_file);
foreach my $configf (@configs) {
my @fakeARGV = ();
if (open(C, '<' . $configf)) {
# "#" in col 0 is a comment
while (defined(my $m = <C>)) {
next if ($m =~ m|^#|);
chomp($m);
$m = '--' . $m if ($m !~ /^-/);
push(@fakeARGV, split(/\s/, $m, 2));
}
close(C);
} elsif (!$config_is_default && $configf eq $config_file) {
# we only print an error if the config was specified explicitly
ptrans(12, 'Config file ' . $configf . ' could not be opened ($!). Exiting');
exit(1);
}
# OK, all that work to load @fakeARGV with values from the config file. Now
# we just need to process it. (don't call if nothing set in @fakeARGV)
fetch_args(\%ARGS, $option_list, \@fakeARGV) if (scalar(@fakeARGV));
check_opt_processing(\@fakeARGV, 'Config file ' . $configf);
}
}
# OK, %ARGS contains all the settings from the config file. Now do it again
# with SWAKS_OPT_* environment variables
@fakeARGV = ();
foreach my $v (sort keys %ENV) {
if ($v =~ m|^SWAKS_OPT_(.*)$|) {
my $tv = $1; $tv =~ s|_|-|g;
push(@fakeARGV, '--' . $tv);
push(@fakeARGV, $ENV{$v}) if (length($ENV{$v}));
}
}
fetch_args(\%ARGS, $option_list, \@fakeARGV) if (scalar(@fakeARGV));
check_opt_processing(\@fakeARGV, 'environment');
# and now, after all of that, process the actual cmdline args
fetch_args(\%ARGS, $option_list, \@ARGV) if (scalar(@ARGV));
check_opt_processing(\@ARGV, 'command line');
return(\%ARGS);
}
# if there's anything left in the fake argv after Getopts processed it, it's an error. There's nothing
# that can be passed in to swaks that isn't an option or an argument to an option, all of which Getopt
# should consume. So if there's anything left, the user did something weird. Just let them know and
# error instead of letting them think their ignored stuff is working.
sub check_opt_processing {
my $argv_local = shift;
my $option_type = shift;
if (scalar(@$argv_local)) {
ptrans(12, 'Data left in option list when processing ' . $option_type . ' (' .
join(', ', map { "'$_'" } (@$argv_local)) .
'). Exiting');
exit(1);
}
}
sub fetch_args {
my $r = shift;
my $l = shift;
my $argv_local = shift;
my %to_delete = ();
# need to rewrite header-HEADER opts before std option parsing
# also see if there are any --no- options that need to be processed
RUNOPTS:
for (my $i = 0; $i < scalar(@$argv_local); $i++) {
# before doing any option processing, massage from the optional '--option=arg' format into '--option arg' format.
if ($argv_local->[$i] =~ /^(-[^=]+)=(.*)$/) {
$argv_local->[$i] = $1;
splice(@$argv_local, $i+1, 0, $2);
}
# -g is not really necessary. It is now deprecated. During the deprecation window, make
# it a straight-up alias to `--data -`. If has already appeared, just ignore -g. If
# --data has not appeared, change -g into `--data -`.
if ($argv_local->[$i] =~ /^-?-g$/) {
deprecate('The -g option is deprecated and will be removed. Please use \'--data -\' instead.');
if (scalar(grep(/^-?-data$/, @$argv_local)) || check_arg('mail_data', $r)) {
# if --data appears in the current stream or has already appeared in a previous stream, ignore -g
splice(@$argv_local, $i, 1); # remove the current index from @$argv_local
redo(RUNOPTS); # since there's now a new value at $i, redo this iteration of the loop
}
else {
# if we haven't seen --data yet, change -g into `--data -`
splice(@$argv_local, $i, 1, '--data', '-');
}
}
if ($argv_local->[$i] =~ /^-?-h(?:eader)?-([^:]+):?$/) {
# rewrite '--header-Foo bar' into '--header "Foo: bar"'
$argv_local->[$i] = "--header";
$argv_local->[$i+1] = $1 . ': ' . $argv_local->[$i+1];
}
elsif ($argv_local->[$i] =~ /^-?-no-h(?:eader)?-/) {
# rewrite '--no-header-Foo' into '--no-header'
$argv_local->[$i] = "--no-header";
}
}
# build the actual hash we will pass to GetOptions from our config list ($l):
# In the end I decided to build this at each call of this sub so that $r
# is defined. It's not much of a performance issue.
my %options = ();
foreach my $e (@$l) {
my $k = join('|', @{$e->{opts}}) . $e->{suffix};
my $nk = join('|', map { "no-$_" } (@{$e->{opts}}));
my $eval;
if ($e->{type} eq 'scalar' || $e->{type} eq 'list') {
$eval = "\$options{\$k} = sub { store_option(\$e, \$r, 0, \@_); };"
. "\$options{\$nk} = sub { store_option(\$e, \$r, 1, \@_); };";
}
else {
ptrans(12, "Unknown option type '$e->{type}' (this is a swaks bug)");
exit(1);
}
eval($eval);
if ($@) {
chomp($@);
ptrans(12, "Unable to load callback for $k option processing: $@");
exit(1);
}
}
if (!load("Getopt::Long")) {
ptrans(12, "Unable to load Getopt::Long for option processing, Exiting");
exit(1);
}
Getopt::Long::Configure("no_ignore_case");
Getopt::Long::GetOptionsFromArray($argv_local, %options) || exit(1);
}
sub store_option {
my $cfg_struct = shift; # this is the option definition structure
my $opt_struct = shift; # this is where we will be saving the option for later retrieval
my $remove = shift; # if true, we received a "no-" version of the option, remove all previous instances
my $opt_name = shift; # --xclient-name || --dump-mail || -f
my $opt_value = shift; # NAME || undef || foo@example.com
my $accum_key = $cfg_struct->{akey}; # xclient_attrs || dump_mail || mail_from
my $opt_key = $cfg_struct->{okey}; # xclient_name || dump_mail || mail_from
my $type = $cfg_struct->{type}; # scalar or list
# print "store_options called -> $cfg_struct, $opt_struct, $opt_name, $opt_value, $accum_key, $opt_key, $type\n";
if ($cfg_struct->{cfgs} & OP_DEPRECATED) {
deprecate("Option --$opt_name will be removed in the future. Please see documentation for more information.");
}
# 'accum' stores lists of the order they were received in
$opt_struct->{accums}{$accum_key} ||= [];
# 'values' stores the actual values and the name of the option that was used to pass it
$opt_struct->{values}{$opt_key} ||= [];
# if we're recording a scalar or were asked to remove, reset the values list to throw away any previous values
# and remove any previous recordings of this okey from the accumulator list
if ($type eq 'scalar' || $remove) {
$opt_struct->{values}{$opt_key} = [];
$opt_struct->{accums}{$accum_key} = [ grep { $_ ne $opt_key } (@{$opt_struct->{accums}{$accum_key}}) ];
}
# if we were asked to remove (which means called with a "--no-" prefix), get out now, there's nothing to record
return if ($remove);
push(@{$opt_struct->{accums}{$accum_key}}, $opt_key);
my $arg = $opt_value;
if ($cfg_struct->{callout}) {
$arg = $cfg_struct->{callout}("$opt_name", $arg);
}
push(@{$opt_struct->{values}{$opt_key}}, {
okey => $opt_key,
akey => $accum_key,
opt => "$opt_name",
arg => $arg,
});
}
# take a string and quote it such that it could be used in the shell
# O'Reilley -> 'O'\''Reilley'
sub shquote { my $s = shift; $s =~ s%'%'\\''%g; return "'$s'"; }
sub reconstruct_options {
my $o = shift; # ref to raw option hash (as returned by load_args)
my @c = (); # array to hold our reconstructed command line
my %already_seen = (); # for okeys like xclient_attrs, they only need to be processed once
my %indexer = ();
foreach my $opt (@G::raw_option_data) {
next if ($already_seen{$opt->{akey}});
next if (!exists($o->{accums}{$opt->{akey}}));
foreach my $okey (@{$o->{accums}{$opt->{akey}}}) {
$indexer{$okey} ||= 0;
my $optStruct = $o->{values}{$okey}[$indexer{$okey}];
my $lopt = $o->{cfgs}{$okey}{opts}[0];
push(@c, '--'.$lopt);
if (length($optStruct->{arg}) && !($o->{cfgs}{$okey}{cfgs} & OP_ARG_NONE)) {
if ($okey eq 'auth_pass') {
push(@c, shquote('%RAW_PASSWORD_STRING%'));
}
else {
push(@c, shquote($optStruct->{arg}));
}
}
}
$already_seen{$opt->{akey}} = 1;
}
#print join(', ', @c), "\n";
return join(' ', @c);
}
sub get_accum {
my $accum_key = shift;
my $userOpts = shift;
if (!exists($userOpts->{accums}{$accum_key})) {
return([]);
}
return($userOpts->{accums}{$accum_key});
}
# I might change this interface later, but I want a way to check whether the user provided the option
# without actually processing it.
sub check_arg {
my $opt = shift;
my $userOpts = shift;
if (exists($userOpts->{values}{$opt}) && scalar(@{$userOpts->{values}{$opt}})) {
return(1);
}
return(0);
}
# get the next value for $opt without doing any processing or popping it off of the list.
sub peek_arg {
my $opt = shift; # this should correspond to an okey from the @G::raw_option_data array
my $userOpts = shift; # all options we got from the command line
if (!exists($userOpts->{values}{$opt})) {
return(undef());
}
if (!scalar(@{$userOpts->{values}{$opt}})) {
return(undef());
}
return($userOpts->{values}{$opt}[0]{arg});
}
# there was a ton of repeated, boiler plate code in process_args. Attempt to abstract it out to get_arg
sub get_arg {
my $opt = shift; # this should correspond to an okey from the @G::raw_option_data array
my $userOpts = shift; # all options we got from the command line
my $optConfig = shift;
my $force = shift;
my $arg;
my $argExt;
my $return;
# print "in get_arg, opt = $opt\n";
# If the user didn't pass in a specific option config, look it up from the global option config
if (!$optConfig) {
if (!exists($userOpts->{cfgs}{$opt})) {
ptrans(12, "Internal option processing error: asked to evaluate non-existent option $opt");
exit(1);
}
$optConfig = $userOpts->{cfgs}{$opt};
}
# $arg will be the value actually provided on the command line
# !defined = not provided
# defined && !length = option provided but no argument provided
# defined && length = option provided and argument provided
if (!exists($userOpts->{values}{$opt})) {
# if the caller passed in $force, we act as if the option is present with an empty arg
# this is used when we need to use get_arg features like interact() even when the user
# didn't specify the option (specifically, --auth forces --auth-password to need to be
# processed, even if the user didn't pass it in)
$arg = $force ? '' : undef();
}
else {
$argExt = shift(@{$userOpts->{values}{$opt}});
$arg = $argExt->{arg};
}
# this option takes no arguments - it's a straight boolean
if ($optConfig->{cfgs} & OP_ARG_NONE) {
if ($arg) {
$return = 1;
}
else {
$return = 0;
}
}
# if the option is present, it must have an argument.
# theoretically I should have code here actually requiring the argument,
# but at the moment that's being handled by Getopt::Long
elsif ($optConfig->{cfgs} & OP_ARG_REQ) {
if (!defined($arg)) {
# the opt wasn't specified at all. Perfectly legal, return undef
$return = undef;
}
else {
# if there was an arg provided, just return it
if (length($arg)) {
$return = $arg;
}
# No arg, but we were requested to prompt the user - do so
elsif ($optConfig->{cfgs} & OP_FROM_PROMPT) {
if (!exists($optConfig->{prompt})) {
ptrans(12, "Internal option processing error: option $argExt->{opt} missing required prompt key (this is a swaks bug)");
exit(1);
}
if (!exists($optConfig->{match})) {
ptrans(12, "Internal option processing error: option $argExt->{opt} missing required match key (this is a swaks bug)");
exit(1);
}
$return = interact($optConfig->{prompt}, $optConfig->{match});
}
# No arg, no request to prompt - this is an error since we're requiring an arg
else {
ptrans(12, "Option processing error: option $argExt->{opt} specified with no argument");
exit(1);
}
# OP_FROM_FILE means that the above options might have resolved into '-' or @filename. If so, return the actual
# data contained in STDIN/@filename
if ($optConfig->{cfgs} & OP_FROM_FILE) {
if ($return eq '-') {
if (defined($G::stdin)) {
# multiple options can specify stdin, but we can only read it once. If anyone has
# already read stdin, provide the saved value here
$return = $G::stdin;
}
else {
$return = join('', <STDIN>);
$G::stdin = $return;
}
}
elsif ($return =~ /^\@\@/) {
# if the argument starts with \@\@, we take that to mean that the user wants a literal value that starts
# with an @. The first @ is just an indicator, so strip it off before continuing
$return =~ s/^\@//;
}
elsif ($return =~ /^\@/) {
# a single @ means it's a filename. Open it and use the contents as the return value
$return =~ s/^\@//;
if (!open(F, "<$return")) {
ptrans(12, "Option processing error: file $return not openable for option $argExt->{opt} ($!)");
exit(1);
}
$return = join('', <F>);
close(F);
}
{
# All of the functionality in this bare block is deprecated, remove the whole thing later.
# if --data and single line, try to open it, error otherwise
# if !--data and is openable file, try to open and read, otherwise just use it as literal data
if ($argExt->{opt} eq 'data') {
# the old rule for --data was that anything that didn't have a newline in it would be treated
# as a file, and we would error if we couldn't open it. That would prevent us from sending
# typoed filenames as the data of messages
if ($return !~ m/\\n|\n|%NEWLINE%/) {
deprecate('Inferring a filename from the argument to --' . $argExt->{opt} .
' will be removed in the future. Prefix filenames with \'@\' instead.');
if (!open(F, "<$return")) {
ptrans(12, "$argExt->{opt} option appears to be a file but is not openable: $return ($!)");
exit(1);
}
$return = join('', <F>);
close(F);
}
}
elsif (open(F, "<$return")) {
# the old rule for any other file option (--attach, --attach-body, --body) was that
# if it was openable, we would use the contents of the file, otherwise we would
# use the string itself
deprecate('Inferring a filename from the argument to --' . $argExt->{opt} .
' will be removed in the future. Prefix filenames with \'@\' instead.');
$return = join('', <F>);
close(F);
}
}
}
}
}
# The option can be present with or without an argument
# any "true" return value will be an actual provided option
# false and defined = option given but no argument given
# false and undefined = option not specified
elsif ($optConfig->{cfgs} & OP_ARG_OPT) {
if (!defined($arg)) {
# the opt wasn't specified at all. Perfectly legal, return undef
$return = undef;
}
else {
# we have an opt and an arg, return the arg
$return = $arg;
}
}
# if we read the last arg off an array, put it back on the array for future reads. I can't
# decide if this is the right behavior or not, but this makes it more like scalars, which
# can (and in a couple of cases, must) be read multiple times.
if (defined($arg) && ref($userOpts->{values}{$opt}) && !scalar(@{$userOpts->{values}{$opt}})) {
push(@{$userOpts->{values}{$opt}}, $argExt);
}
# print "returning ";
# if (defined($return)) {
# print length($return) ? "$return\n" : "defined but empty\n";
# }
# else {
# print "undefined\n";
# }
return($return);
}
# A couple of global options are set in here, they will be in the G:: namespace
sub process_args {
my $o = shift; # This is the args we got from command line
my %n = (); # This is the hash we will return w/ the fixed-up args
my $a = get_option_struct(); # defining information for all options
# handle the output file handles early so they can be used for errors
# we don't need to keep track of the actual files but it will make debugging
# easier later
$G::trans_fh_oh = \*STDOUT;
$G::trans_fh_of = "STDOUT";
$G::trans_fh_eh = \*STDERR;
$G::trans_fh_ef = "STDERR";
my $output_file = get_arg('output_file', $o);
my $output_file_stderr = get_arg('output_file_stderr', $o) || $output_file;
my $output_file_stdout = get_arg('output_file_stdout', $o) || $output_file;
if ($output_file_stderr) {
if (!open(OUTEFH, '>>'.$output_file_stderr)) {
ptrans(12, 'Unable to open ' . $output_file_stderr . ' for writing');
exit(1);
}
$G::trans_fh_eh = \*OUTEFH;
$G::trans_fh_ef = $output_file_stderr;
}
if ($output_file_stdout && $output_file_stdout eq $output_file_stderr) {
$G::trans_fh_oh = $G::trans_fh_eh;
$G::trans_fh_of = $G::trans_fh_ef;
}
elsif ($output_file_stdout) {
if (!open(OUTOFH, '>>'.$output_file_stdout)) {
ptrans(12, 'Unable to open ' . $output_file_stdout . ' for writing');
exit(1);
}
$G::trans_fh_oh = \*OUTOFH;
$G::trans_fh_of = $output_file_stdout;
}
if (get_arg('no_hints', $o)) {
$G::no_hints_send = 1;
$G::no_hints_recv = 1;
}
else {
$G::no_hints_send = get_arg('no_hints_send', $o);
$G::no_hints_recv = get_arg('no_hints_recv', $o);
}
$G::dump_mail = get_arg('dump_mail', $o);
$G::suppress_data = get_arg('suppress_data', $o);
$G::no_hints_info = get_arg('no_hints_info', $o);
$G::hide_send = get_arg('hide_send', $o);
$G::hide_receive = get_arg('hide_receive', $o);
$G::hide_informational = get_arg('hide_informational', $o);
$G::hide_all = get_arg('hide_all', $o);
$G::show_raw_text = get_arg('show_raw_text', $o);
$G::protect_prompt = get_arg('protect_prompt', $o);
$G::pipeline = get_arg('pipeline', $o);
$G::prdr = get_arg('prdr', $o);
$G::silent = get_arg('silent', $o) || 0;
if (defined(my $dump_args = get_arg('dump_args', $o))) {
map { $G::dump_args{uc($_)} = 1; } (split('\s*,\s*', $dump_args)); # map comma-delim options into a hash
$G::dump_args{'ALL'} = 1 if (!scalar(keys(%G::dump_args))); # if no options were given, just set ALL
}
my $mail_server_t = get_arg('mail_server', $o);
my $socket_t = get_arg('socket', $o);
my $pipe_cmd_t = get_arg('pipe_cmd', $o);
# it is an error if >1 of --server, --socket, or --pipe is specified
if ((defined($mail_server_t) && defined($socket_t)) ||
(defined($mail_server_t) && defined($pipe_cmd_t)) ||
(defined($pipe_cmd_t) && defined($socket_t)))
{
ptrans(12, "Multiple transport types specified, exiting");
exit(1);
}
my %protos = (
smtp => { proto => 'smtp', auth => 0, tls => '0' },
ssmtp => { proto => 'esmtp', auth => 0, tls => 'c' },
ssmtpa => { proto => 'esmtp', auth => 1, tls => 'c' },
smtps => { proto => 'smtp', auth => 0, tls => 'c' },
esmtp => { proto => 'esmtp', auth => 0, tls => '0' },
esmtpa => { proto => 'esmtp', auth => 1, tls => '0' },
esmtps => { proto => 'esmtp', auth => 0, tls => 's' },
esmtpsa => { proto => 'esmtp', auth => 1, tls => 's' },
lmtp => { proto => 'lmtp', auth => 0, tls => '0' },
lmtpa => { proto => 'lmtp', auth => 1, tls => '0' },
lmtps => { proto => 'lmtp', auth => 0, tls => 's' },
lmtpsa => { proto => 'lmtp', auth => 1, tls => 's' },
);
$G::protocol = lc(get_arg('mail_protocol', $o)) || 'esmtp';
my $tls = get_arg('tls', $o);
my $tls_optional = get_arg('tls_optional', $o);
my $tls_optional_strict = get_arg('tls_optional_strict', $o);
my $tls_on_connect = get_arg('tls_on_connect', $o);
if (!$protos{$G::protocol}) {
ptrans(12, "Unknown protocol $G::protocol specified, exiting");
exit(1);
}
my $auth_user_t = get_arg('auth_user', $o);
my $auth_pass_t = get_arg('auth_pass', $o);
my $auth_optional_t = get_arg('auth_optional', $o);
my $auth_optional_strict_t = get_arg('auth_optional_strict', $o);
my $auth_t = get_arg('auth', $o);
if ($protos{$G::protocol}{auth} && !$auth_user_t && !$auth_pass_t && !$auth_optional_t && !$auth_optional_strict_t && !$auth_t) {
$auth_t = ''; # cause auth to be processed below
}
if ($protos{$G::protocol}{tls} && !$tls && !$tls_optional && !$tls_optional_strict && !$tls_on_connect){
# 'touch' the variable so we process it below
if ($protos{$G::protocol}{tls} eq 's') {
$tls = 1;
} elsif ($protos{$G::protocol}{tls} eq 'c') {
$tls_on_connect = 1;
}
}
$G::protocol = $protos{$G::protocol}{proto};
# set global options for --quit-after, --drop-after, and --drop-after-send
foreach my $opt ('quit_after', 'drop_after', 'drop_after_send') {
no strict "refs";
if (my $value = get_arg($opt, $o)) {
${"G::$opt"} = lc($value);
if (${"G::$opt"} =~ /^[el]hlo$/) { ${"G::$opt"} = 'helo'; }
elsif (${"G::$opt"} =~ /first-[el]hlo/) { ${"G::$opt"} = 'first-helo'; }
elsif (${"G::$opt"} eq 'starttls') { ${"G::$opt"} = 'tls'; }
elsif (${"G::$opt"} eq 'banner') { ${"G::$opt"} = 'connect'; }
elsif (${"G::$opt"} eq 'from') { ${"G::$opt"} = 'mail'; }
elsif (${"G::$opt"} eq 'to') { ${"G::$opt"} = 'rcpt'; }
elsif (${"G::$opt"} ne 'connect' && ${"G::$opt"} ne 'first-helo' &&
${"G::$opt"} ne 'tls' && ${"G::$opt"} ne 'helo' &&
${"G::$opt"} ne 'auth' && ${"G::$opt"} ne 'mail' &&
${"G::$opt"} ne 'rcpt' && ${"G::$opt"} ne 'xclient' &&
${"G::$opt"} ne 'data' && ${"G::$opt"} ne 'dot')
{
ptrans(12, "Unknown $opt value " . ${"G::$opt"} . ", exiting");
exit(1);
}
# only rcpt, data, and dot _require_ a to address
$G::server_only = 1 if (${"G::$opt"} !~ /^(rcpt|data|dot)$/);
# data and dot aren't legal for quit_after
if ($opt eq 'quit_after' && (${"G::$opt"} eq 'data' || ${"G::$opt"} eq 'dot')) {
ptrans(12, "Unknown $opt value " . ${"G::$opt"} . ", exiting");
exit(1);
}
} else {
${"G::$opt"} = '';
}
}
# set global flag for -stl flag
$G::show_time_lapse = get_arg('show_time_lapse', $o);
if (defined($G::show_time_lapse)) {
if (length($G::show_time_lapse) && $G::show_time_lapse !~ /^i/i) {
ptrans(12, "Unknown argument '$G::show_time_lapse' to option show-time-lapse, exiting");
exit(1);
}
if (avail("hires_timing") && $G::show_time_lapse !~ /^i/i) {
$G::show_time_lapse = 'hires';
}
else {
$G::show_time_lapse = 'integer';
}
}
# pipe command, if one is specified
if ($pipe_cmd_t) {
$G::link{process} = $pipe_cmd_t;
$G::link{type} = 'pipe';
}
# socket file, if one is specified
if ($socket_t) {
$G::link{sockfile} = $socket_t;
$G::link{type} = 'socket-unix';
}
$n{force_getpwuid} = get_arg('force_getpwuid', $o); # make available for --dump
my $user = get_username($n{force_getpwuid});
my $hostname = get_hostname();
# SMTP mail from
if (!($n{from} = get_arg('mail_from', $o))) {
if ($hostname || ($G::server_only && $G::quit_after ne 'mail' && $G::drop_after ne 'mail' && $G::drop_after_send ne 'mail')) {
# if we have a hostname, or it doesn't matter anyway because we won't actually need it, use our manufactured from
$n{from} = "$user\@$hostname";
}
else {
ptrans(12, "From string required but couldn't be determined automatically. Please use --from");
exit(1);
}
}
$n{from} = '' if ($n{from} eq '<>');
# local interface and port
($G::link{lint},$G::link{lport}) = parse_server(get_arg('lint', $o), get_arg('lport', $o));
if ($G::link{lport} && $G::link{lport} !~ /^\d+$/) {
if (my $port = getservbyname($G::link{lport}, 'tcp')) {
$G::link{lport} = $port;
}
else {
ptrans(12, "unable to resolve service name $G::link{lport} into a port, exiting");
exit(1);
}
}
# SMTP helo/ehlo
if (!($n{helo} = get_arg('mail_helo', $o))) {
if ($hostname || ($G::quit_after eq 'connect' || $G::drop_after eq 'connect' || $G::drop_after_send eq 'connect')) {
# if we have a hostname, or it doesn't matter anyway because we won't actually need it, use our manufactured from
$n{helo} = $hostname;
}
else {
ptrans(12, "Helo string required but couldn't be determined automatically. Please use --helo");
exit(1);
}
}
# SMTP server, port and rcpt-to are interdependant, so they are handled together
$G::link{type} ||= 'socket-inet';
($G::link{server},$G::link{port}) = parse_server($mail_server_t, get_arg('mail_port', $o));
$n{to} = get_arg('mail_to', $o);
# we absolutely must have a recipient. If we don't have one yet, prompt for one
if (!$n{to} && !($G::server_only && ($G::link{server} || $G::link{type} eq 'socket-unix' || $G::link{type} eq 'pipe'))) {
$n{to} = interact("To: ", '^.+$'); # WCSXXXFIXME I wish we could look up the prompt and re from $a
}
# try to catch obvious -s/-li/-4/-6 errors as soon as possible. We don't actually do any DNS
# lookups ourselves, so errors like -s being a domain with only A RRs and -li being a domain
# with only AAAA RRs, or -s being an ipv6 and -li being a domain with only A RRs, will
# get passed into the IO::Socket module to deal with and will just registed as a connection
# failure.
if ($G::link{type} eq 'socket-inet') {
my $forceIPv4 = get_arg('force_ipv4', $o);
my $forceIPv6 = get_arg('force_ipv6', $o);
if ($forceIPv4 && $forceIPv6) {
ptrans(12, "Options -4 and -6 are mutually exclusive, cannot proceed");
exit 1;
} elsif ($forceIPv6) {
$G::link{force_ipv6} = 1;
} elsif ($forceIPv4) {
$G::link{force_ipv4} = 1;
}
if ($n{copy_routing} = get_arg('copy_routing', $o)) {
$G::link{server} ||= get_server($n{copy_routing});
}
else {
$G::link{server} ||= get_server($n{to});
}
if ($forceIPv4 && $G::link{server} =~ m|:|) {
ptrans(12, "Option -4 is set but server appears to be ipv6, cannot proceed");
exit 1;
} elsif ($forceIPv4 && $G::link{lint} =~ m|:|) {
ptrans(12, "Option -4 is set but local-interface appears to be ipv6, cannot proceed");
exit 1;
} elsif ($forceIPv6 && $G::link{server} =~ m|^\d+\.\d+\.\d+\.\d+$|) {
ptrans(12, "Option -6 is set but server appears to be ipv4, cannot proceed");
exit 1;
} elsif ($forceIPv6 && $G::link{lint} =~ m|^\d+\.\d+\.\d+\.\d+$|) {
ptrans(12, "Option -6 is set but local-interface appears to be ipv4, cannot proceed");
exit 1;
} elsif ($G::link{server} =~ m|:| && $G::link{lint} =~ m|^\d+\.\d+\.\d+\.\d+$|) {
ptrans(12, "server is ipv6 but local-interface is ipv4, cannot proceed");
exit 1;
} elsif ($G::link{server} =~ m|^\d+\.\d+\.\d+\.\d+$| && $G::link{lint} =~ m|:|) {
ptrans(12, "server is ipv4 but local-interface is ipv6, cannot proceed");
exit 1;
}
}
# Verify we are able to handle the requested transport
if ($G::link{type} eq 'pipe') {
if (!avail("pipe")) {
ptrans(12, avail_str("pipe").". Exiting");
exit(10);
}
} else {
if (!avail("socket")) {
ptrans(12, avail_str("socket").". Exiting");
exit(10);
} elsif (($G::link{force_ipv6} || $G::link{server} =~ m|:| || $G::link{lint} =~ m|:|) && !avail("ipv6")) {
ptrans(12, avail_str("ipv6").". Exiting");
exit(10);
}
}
# SMTP timeout
$G::link{timeout} = time_to_seconds(get_arg('timeout', $o) // '30s');
my $dab_sp = get_arg('dab_sp', $o);
my $dump_as_body = get_arg('dump_as_body', $o);
$dump_as_body = '' if ($dab_sp && !defined($dump_as_body));
my $body = 'This is a test mailing'; # default message body
$body = 'DUMP_AS_BODY_HAS_BEEN_SET' if (defined($dump_as_body));
my $bound = '';
my $main_content_type = 'multipart/mixed';
my $stdin = undef;
if (defined(my $body_822_t = get_arg('body_822', $o))) {
# the --body option is the entire 822 body and trumps any other options
# that mess with the body
$body = $body_822_t;
}
my $attach_accum = get_accum('attach_accum', $o);
if (scalar(@$attach_accum)) {
# this option is a list of files (or STDIN) to attach. In this case,
# the message become a mime message and the "body" goes in the
# first text/plain part
my $mime_type = '%SWAKS_DEFAULT_MIMETYTPE%';
my $next_name = undef();
my %parts = ( body => [], rest => [] );
$bound = "----=_MIME_BOUNDARY_000_$$";
foreach my $part (@$attach_accum) {
if ($part eq 'attach_type') {
$mime_type = get_arg($part, $o);
}
elsif ($part eq 'attach_name') {
$next_name = get_arg($part, $o);
}
elsif ($part eq 'attach_body') {
if ($mime_type eq '%SWAKS_DEFAULT_MIMETYTPE%') {
$mime_type = 'text/plain';
}
push(@{$parts{body}}, { body => get_arg($part, $o), type => $mime_type });
$next_name = undef(); # can't set filename for body, unset next_name so random attachment doesn't get it
$mime_type = '%SWAKS_DEFAULT_MIMETYTPE%'; # after each body, reset the default mime type
}
elsif ($part eq 'attach_attach') {
my $name = peek_arg($part, $o);
my $tpart = { body => get_arg($part, $o), type => $mime_type };
if (defined($next_name)) {
$tpart->{name} = $next_name;
$next_name = undef();
}
else {
my $filename = $name;
$filename =~ s/^\@//;
if ($name ne '-' && $filename !~ /^\@/ && $name ne $tpart->{body} && -f $filename) {
# name will have the unprocessed arg. If we think it came from a file, use the filename for
# the attachment name. Not super happy with this logic, try to improve in the future
($tpart->{name}) = $name =~ m|/?([^/]+)$|;
}
}
push(@{$parts{rest}}, $tpart);
} else {
ptrans(12, "Error processing attach args, unknown type $part when processing attachment options");
exit(1);
}
}
# if no body parts were set via --attach-body, set a text/plain body to $body
if (!scalar(@{$parts{body}})) {
push(@{$parts{body}}, { body => $body, type => 'text/plain' });
}
$body = '';
if (!scalar(@{$parts{rest}})) {
# if there are no non-body parts
if (scalar(@{$parts{body}}) > 1) {
$main_content_type = 'multipart/alternative';
}
else {
$main_content_type = 'multipart/mixed';
}
foreach my $part (@{$parts{body}}) {
$body .= encode_mime_part($part, $bound, 1);
}
}
else {
# otherwise, there's a mixture of both body and other. multipart/mixed
$main_content_type = 'multipart/mixed';
if (scalar(@{$parts{body}}) > 1) {
# we have multiple body parts, plus other attachments. Need to create a mp/mixes mime object for the bodies
my $mp_bound = "----=_MIME_BOUNDARY_004_$$";
$body .= "--$bound\n"
. 'Content-Type: multipart/alternative; boundary="' . $mp_bound . '"' . "\n\n";
foreach my $part (@{$parts{body}}) {
$body .= encode_mime_part($part, $mp_bound, 1);
}
$body .= "--$mp_bound--\n";
}
else {
$body .= encode_mime_part($parts{body}[0], $bound, 1);
}
# now handle the non-body attachments
foreach my $part (@{$parts{rest}}) {
$body .= encode_mime_part($part, $bound);
}
}
$body .= "--$bound--\n";
}
$body =~ s|%SWAKS_DEFAULT_MIMETYTPE%|application/octet-stream|g;
# SMTP DATA
$n{data} = get_arg('mail_data', $o) ||
'Date: %DATE%\nTo: %TO_ADDRESS%\nFrom: %FROM_ADDRESS%\nSubject: test %DATE%\n' .
"Message-Id: <%MESSAGEID%>\n" .
"X-Mailer: swaks v%SWAKS_VERSION% jetmore.org/john/code/swaks/".'\n' .
($bound ? 'MIME-Version: 1.0\nContent-Type: ' . $main_content_type . '; boundary="' . $bound. '"\n' : '') .
'%NEW_HEADERS%' . # newline will be added in replacement if it exists
'\n' .
'%BODY%\n';
if (!get_arg('no_data_fixup', $o)) {
$n{data} =~ s/%BODY%/$body/g;
$n{data} =~ s/\\n/\r\n/g;
my $addHeader_accum = get_accum('add_header', $o);
my $addHeaderOpt = [];
foreach my $okey (@$addHeader_accum) {
push(@$addHeaderOpt, get_arg($okey, $o));
}
# split the headers off into their own struct temporarily to make it much easier to manipulate them
my $header;
my @headers = ();
my %headers = ();
# cut the headers off of the data
if ($n{data} =~ s/\A(.*?)\r?\n\r?\n//s) {
$header = $1;
}
else {
$header = $n{data};
$n{data} = '';
}
# build the header string into an object. Each header is an array, each index is a line (to handle header continuation lines)
foreach my $headerLine (split(/\r?\n/, $header)) {
if ($headerLine =~ /^\s/) {
# continuation line
if (scalar(@headers)) {
push(@{$headers[-1]}, $headerLine);
}
else {
# it's illegal to have a continuation line w/o a previous header, but we're a test tool
push(@headers, [ $headerLine ]);
}
}
elsif ($headerLine =~ /^(\S[^:]+):/) {
# properly formed header
push(@headers, [ $headerLine ]);
$headers{$1} = $headers[-1];
}
else {
# malformed header - no colon. Allow it anyway, we're a test tool
push(@headers, [ $headerLine ]);
$headers{$headerLine} = $headers[-1];
}
}
# If the user specified headers and the header exists, replace it. If not, push it onto add_header to be added as new
my $header_accum = get_accum('header', $o);
my $headerOpt = [];
foreach my $okey (@$header_accum) {
push(@$headerOpt, get_arg($okey, $o));
}
foreach my $headerLine (map { split(/\\n/) } @{$headerOpt}) {
if (my($headerName) = $headerLine =~ /^([^:]+):/) {
if ($headers{$headerName}) {
$headers{$headerName}[0] = $headerLine;
splice(@{$headers{$headerName}}, 1); # remove from index 1 onward, if they existed (possible continuations)
}
else {
push(@{$addHeaderOpt}, $headerLine);
}
}
else {
push(@{$addHeaderOpt}, $headerLine);
}
}
# rebuild the header using our (possibly replaced) headers
my $newHeader = '';
foreach my $headerObj (@headers) {
foreach my $line (@$headerObj) {
$newHeader .= $line . "\r\n";
}
}
# if there are new headers, add them as appropriate
if ($newHeader =~ /%NEW_HEADERS%/) {
$n{add_header} = join("\r\n", @{$addHeaderOpt}) . "\r\n" if (@{$addHeaderOpt});
$newHeader =~ s/%NEW_HEADERS%/$n{add_header}/g;
} elsif (scalar(@{$addHeaderOpt})) {
foreach my $line (@{$addHeaderOpt}) {
$newHeader .= $line . "\r\n";
}
}
# Now re-assemble our data by adding the headers back on to the front
$n{data} = $newHeader . "\r\n" . $n{data};
$n{data} =~ s/\\n|%NEWLINE%/\r\n/g;
$n{data} =~ s/%FROM_ADDRESS%/$n{from}/g;
$n{data} =~ s/%TO_ADDRESS%/$n{to}/g;
$n{data} =~ s/%MESSAGEID%/get_messageid()/ge;
$n{data} =~ s/%SWAKS_VERSION%/$p_version/g;
$n{data} =~ s/%DATE%/get_date_string()/ge;
$n{data} =~ s/^From [^\n]*\n// if (!get_arg('no_strip_from', $o));
$n{data} =~ s/\r?\n\.\r?\n?$//s; # If there was a trailing dot, remove it
$n{data} =~ s/\n\./\n../g; # quote any other leading dots
$n{data} =~ s/([^\r])\n/$1\r\n/gs;
$n{data} =~ s/([^\r])\n/$1\r\n/gs; # this identical call is not a bug, called twice to get consecutive \n correctly
$n{data} .= "\r\n."; # add a trailing dot
}
# Handle TLS options
# tls => 0 - no. STARTTLS must be advertised and must succeed, else error.
# 1 - yes. Success if not advertised, advertised and fails _or_ succeeds.
# 2 - strict. Satisfied if not advertised, or advertised and succeeded.
# However, if it's advertised and fails, it's an error.
$G::tls_optional = 1 if ($tls_optional);
$G::tls_optional = 2 if ($tls_optional_strict);
$G::tls = 1 if ($tls || $G::tls_optional);
$G::tls_on_connect = 1 if ($tls_on_connect);
$G::link{tls}{active} = 0;
if ($G::tls || $G::tls_on_connect) {
if (!avail("tls")) {
if ($G::tls_optional) {
$G::tls = undef; # so we won't try it later
ptrans(12,avail_str("tls"));
} else {
ptrans(12,avail_str("tls").". Exiting");
exit(10);
}
}
$G::tls_verify = get_arg('tls_verify', $o);
$G::tls_sni_hostname = get_arg('tls_sni_hostname', $o);
$G::tls_cipher = get_arg('tls_cipher', $o);
$G::tls_cert = get_arg('tls_cert', $o);
$G::tls_key = get_arg('tls_key', $o);
if (($G::tls_cert || $G::tls_key) && !($G::tls_cert && $G::tls_key)) {
ptrans(12, "--tls-cert and --tls-key require each other. Exiting");
exit(1);
}
if (($G::tls_ca_path = get_arg('tls_ca_path', $o)) && !-f $G::tls_ca_path && !-d $G::tls_ca_path) {
ptrans(12, "--tls-ca-path: $G::tls_ca_path is not a valid file or directory. Exiting.");
exit(1);
}
# this is kind of a kludge. There doesn't appear to be a specific openssl call to find supported
# protocols, but the OP_NO_protocol functions exist for supported protocols. Loop through
# "known" protocols (which will unfortunately need to be added-to by hand when new protocols
# become available) to find out which of them are available (when adding new types here, see
# also the code that calls Net::SSLeay::version() and translates to a readable value
@G::tls_supported_protocols = ();
foreach my $p (qw(SSLv2 SSLv3 TLSv1 TLSv1_1 TLSv1_2 TLSv1_3)) {
eval { no strict "refs"; &{"Net::SSLeay::OP_NO_$p"}(); };
push(@G::tls_supported_protocols, $p) if (!$@);
}
if (my $tls_protocols = get_arg('tls_protocol', $o)) {
@G::tls_protocols = ();
my @requested = split(/,\s*/, $tls_protocols);
if (my $c = scalar(grep(/^no_/i, @requested))) {
if ($c != scalar(@requested)) {
ptrans(12, "cannot mix X and no_X forms in --tls-protocol option");
exit(1);
}
}
foreach my $p (@requested) {
my $t = $p;
$t =~ s/^no_//i;
if (grep /^$t$/i, @G::tls_supported_protocols) {
push(@G::tls_protocols, $p);
} else {
ptrans(12, "$p in --tls-protocol is not a known/supported protocol");
}
}
if (!scalar(@G::tls_protocols)) {
ptrans(12, "no valid arguments provided to --tls-protocol, exiting");
exit(1);
}
}
$G::tls_get_peer_cert = get_arg('tls_get_peer_cert', $o);
$G::tls_get_peer_cert = 'STDOUT' if (defined($G::tls_get_peer_cert) && !length($G::tls_get_peer_cert));
}
# SMTP port
if ($G::link{port}) {
if ($G::link{port} !~ /^\d+$/) {
if (my $port = getservbyname($G::link{port}, 'tcp')) {
$G::link{port} = $port;
}
else {
ptrans(12, "unable to resolve service name $G::link{port} into a port, exiting");
exit(1);
}
}
} else {
# in here, user wants us to use default ports, so try look up services,
# use default numbers is service names don't resolve. Never prompt user
if ($G::protocol eq 'lmtp') {
$G::link{port} = getservbyname('lmtp', 'tcp') || '24';
} elsif ($G::tls_on_connect) {
$G::link{port} = getservbyname('smtps', 'tcp') || '465';
} else {
$G::link{port} = getservbyname('smtp', 'tcp') || '25';
}
}
# XCLIENT
{ # Create a block for local variables
$G::xclient{try} = 0;
$G::xclient{attr} = {};
$G::xclient{strings} = [];
my @pieces = ();
my $xclient_accum = get_accum('xclient_accum', $o);
foreach my $attr (@$xclient_accum) {
if ($attr eq 'xclient_delim' || $attr eq 'xclient_raw') {
if (scalar(@pieces)) {
push(@{$G::xclient{strings}}, join(' ', @pieces));
@pieces = ();
}
if ($attr eq 'xclient_raw') {
push(@{$G::xclient{strings}}, get_arg('xclient_raw', $o));
}
} else {
if (my $value = get_arg($attr, $o)) {
$attr =~ /^xclient_(.*)$/;
my $name = uc($1);
$G::xclient{attr}{$name} = 1; # used later to verify that we haven't asked for an un-advertised attr
push(@pieces, $name . '=' . to_xtext($value));
}
}
}
push(@{$G::xclient{strings}}, join(' ', @pieces)) if (scalar(@pieces));
$G::xclient{no_verify} = get_arg('xclient_no_verify', $o);
$G::xclient{optional} = get_arg('xclient_optional', $o);
$G::xclient{optional} = 2 if (get_arg('xclient_optional_strict', $o));
#$G::xclient{after} = $o->{"xclient_after"} || interact("XCLIENT quit after: ", '^.+$')
# if (defined($o->{"xclient_after"}));
$G::xclient{try} = 1 if (scalar(@{$G::xclient{strings}}));
$G::xclient{before_tls} = get_arg('xclient_before_starttls', $o);
}
# PROXY
$G::proxy{try} = 0;
$G::proxy{attr} = {};
$G::proxy{version} = get_arg('proxy_version', $o);
$G::proxy{raw} = get_arg('proxy_raw', $o);
foreach my $attr ('family', 'source', 'source_port', 'dest', 'dest_port', 'protocol', 'command') {
if (my $val = get_arg('proxy_' . $attr, $o)) {
if ($G::proxy{raw}) {
ptrans(12, "Can't mix --proxy option with other --proxy-* options");
exit(35);
}
$G::proxy{attr}{$attr} = $val;
}
}
if ($G::proxy{version}) {
if ($G::proxy{version} != 1 && $G::proxy{version} != 2) {
ptrans(12, "Invalid argument to --proxy: $G::proxy{version} is not a legal proxy version");
exit(35);
}
}
else {
$G::proxy{version} = 1;
}
$G::proxy{try} = 1 if ($G::proxy{raw} || scalar(keys(%{$G::proxy{attr}})));
if ($G::proxy{try} && !$G::proxy{raw}) {
$G::proxy{attr}{protocol} ||= 'STREAM';
$G::proxy{attr}{command} ||= 'PROXY';
foreach my $attr ('family', 'source', 'source_port', 'dest', 'dest_port', 'protocol', 'command') {
if (!$G::proxy{attr}{$attr}) {
ptrans(12, "Incomplete set of --proxy-* options (missing $attr)");
exit(35);
}
$G::proxy{attr}{$attr} = uc($G::proxy{attr}{$attr});
}
if ($G::proxy{attr}{protocol} !~ /^(UNSPEC|STREAM|DGRAM)$/) {
ptrans(12, 'unknown --proxy-protocol argument ' . $G::proxy{attr}{protocol});
exit(35);
}
if ($G::proxy{attr}{command} !~ /^(LOCAL|PROXY)$/) {
ptrans(12, 'unknown --proxy-command argument ' . $G::proxy{attr}{command});
exit(35);
}
if ($G::proxy{version} == 2 && $G::proxy{attr}{family} !~ /^(AF_UNSPEC|AF_INET|AF_INET6|AF_UNIX)$/) {
ptrans(12, 'unknown --proxy-family argument ' . $G::proxy{attr}{family} . ' for version 2');
exit(35);
}
if ($G::proxy{version} == 1 && $G::proxy{attr}{family} !~ /^(TCP4|TCP6)$/) {
ptrans(12, 'unknown --proxy-family argument ' . $G::proxy{attr}{family} . ' for version 1');
exit(35);
}
}
# Handle AUTH options
# auth_optional => 0 - no. Auth must be advertised and must succeed, else error.
# 1 - yes. Success if not advertised, advertised and fails _or_ succeeds.
# 2 - strict. Satisfied if not advertised, or advertised and succeeded.
# However, if it's advertised and fails, it's an error.
$G::auth_optional = 1 if (defined($auth_optional_t));
$G::auth_optional = 2 if (defined($auth_optional_strict_t));
my $auth_types_t = [];
if ($auth_t) {
@{$auth_types_t} = map { uc($_) } (split(/,/, $auth_t));
} elsif ($auth_optional_strict_t) {
@{$auth_types_t} = map { uc($_) } (split(/,/, $auth_optional_strict_t));
} elsif ($auth_optional_t) {
@{$auth_types_t} = map { uc($_) } (split(/,/, $auth_optional_t));
} elsif (defined($auth_user_t) || defined($auth_pass_t) || $G::auth_optional || (defined($auth_t) && !$auth_t)) {
$auth_types_t->[0] = 'ANY';
$auth_t = 'ANY'; # this is checked below
$G::auth_type = 'ANY';
}
# if after that processing we've defined some auth type, do some more
# specific processing
if (scalar(@{$auth_types_t})) {
# there's a lot of option processing below. If any type looks like it
# will succeed later, set this to true
my $valid_auth_found = 0;
# handle the --auth-map options plus our default mappings
foreach (split(/\s*,\s*/, get_arg('auth_map', $o)),"PLAIN=PLAIN","LOGIN=LOGIN",
"CRAM-MD5=CRAM-MD5","DIGEST-MD5=DIGEST-MD5",
"CRAM-SHA1=CRAM-SHA1","NTLM=NTLM","SPA=NTLM","MSN=NTLM")
{
if (/^([^=]+)=(.+)$/) {
my($alias,$type) = ($1,$2);
$G::auth_map_f{$alias} = $type; # this gives us a list of all aliases pointing to types
$G::auth_map_t{$type} ||= []; # this gives a list of all base types and any aliases for it.
push(@{$G::auth_map_t{$type}}, $alias);
} else {
ptrans(12, "Unknown auth-map format '$_'");
exit(1);
}
}
# Now handle the --auth-extra options
foreach (split(/\s*,\s*/, get_arg('auth_extra', $o))) {
if (/^([^=]+)=(.+)$/) {
$G::auth_extras{uc($1)} = $2;
} else {
ptrans(12, "Unknown auth-extra format '$_'");
exit(1);
}
}
# handle the realm/domain synonyms
if ($G::auth_extras{DOMAIN}) {
$G::auth_extras{REALM} = $G::auth_extras{DOMAIN};
} elsif ($G::auth_extras{DOMAIN}) {
$G::auth_extras{DOMAIN} = $G::auth_extras{REALM};
}
if (!avail("auth")) { # check for general auth requirements
if ($G::auth_optional == 2) {
# we don't know yet if this is really an error. If the server
# doesn't advertise auth, then it's not really an error. So just
# save it in case we need it later
$G::auth_unavailable = avail_str("auth");
ptrans(12, avail_str("auth"));
} elsif ($G::auth_optional == 1) {
ptrans(12, avail_str("auth"). ". Skipping optional AUTH");
} else {
ptrans(12, avail_str("auth"). ". Exiting");
exit(10);
}
} else {
# if the user doesn't specify an auth type, create a list from our
# auth-map data. Simplifies processing later
if ($auth_types_t->[0] eq 'ANY') {
$auth_types_t = [sort keys %G::auth_map_f];
}
foreach my $type (@{$auth_types_t}) {
# we need to evaluate whether we will be able to run the auth types
# specified by the user
if (!$G::auth_map_f{$type}) {
ptrans(12, "$type is not a recognized auth type, skipping");
} elsif ($G::auth_map_f{$type} eq 'CRAM-MD5' && !avail("auth_cram_md5")) {
ptrans(12, avail_str("auth_cram_md5")) if ($auth_t ne 'ANY');
} elsif ($G::auth_map_f{$type} eq 'CRAM-SHA1' && !avail("auth_cram_sha1")) {
ptrans(12, avail_str("auth_cram_sha1")) if ($auth_t ne 'ANY');
} elsif ($G::auth_map_f{$type} eq 'NTLM' && !avail("auth_ntlm")) {
ptrans(12, avail_str("auth_ntlm")) if ($auth_t ne 'ANY');
} elsif ($G::auth_map_f{$type} eq 'DIGEST-MD5' && !avail("auth_digest_md5")) {
ptrans(12, avail_str("auth_digest_md5")) if ($auth_t ne 'ANY');
} else {
$valid_auth_found = 1;
push(@{$n{a_type}}, $type);
}
}
if (!$valid_auth_found) {
ptrans(12, "No auth types supported");
if ($G::auth_optional == 2) {
$G::auth_unavailable .= "No auth types supported";
} elsif ($G::auth_optional == 1) {
$n{a_user} = $n{a_pass} = $n{a_type} = undef;
} else {
exit(10);
}
} else {
$auth_user_t ||= obtain_from_netrc('login');
if (!$auth_user_t) {
my $cfg = { cfgs => OP_ARG_REQ|OP_FROM_PROMPT, prompt => 'Username: ', match => 'SKIP', okey => 'auth_user', akey => 'auth_user' };
$auth_user_t = get_arg('auth_user', $o, $cfg, 1);
}
$n{a_user} = $auth_user_t eq '<>' ? '' : $auth_user_t;
$auth_pass_t ||= obtain_from_netrc('password', $n{a_user});
if (!$auth_pass_t) {
my $cfg = { cfgs => OP_ARG_REQ|OP_FROM_PROMPT, prompt => 'Password: ', match => 'SKIP', okey => 'auth_pass', akey => 'auth_pass' };
$auth_pass_t = get_arg('auth_pass', $o, $cfg, 1);
}
$n{a_pass} = $auth_pass_t eq '<>' ? '' : $auth_pass_t;
$G::auth_showpt = get_arg('auth_showpt', $o);
$G::auth_hidepw = get_arg('auth_hidepw', $o);
if (defined($G::auth_hidepw) && !$G::auth_hidepw) {
$G::auth_hidepw = 'PROVIDED_BUT_REMOVED';
}
}
} # end avail("auth")
} # end auth parsing
# the very last thing we do is swap out the body if --dump-as-body used
if (defined($dump_as_body)) {
if ($dump_as_body) {
$dump_as_body = uc($dump_as_body);
$dump_as_body =~ s/\s//g;
map { $G::dump_as_body{$_} = 1; } (split(',', $dump_as_body));
}
else {
$G::dump_as_body{'ALL'} = 1;
}
$n{data} =~ s|DUMP_AS_BODY_HAS_BEEN_SET|get_running_state(\%n, \%G::dump_as_body, {SUPPORT => 1, DATA => 1})|e;
if ($dab_sp) {
$n{data} =~ s|'%RAW_PASSWORD_STRING%'|shquote($n{a_pass})|eg;
} elsif ($G::auth_hidepw) {
$n{data} =~ s|'%RAW_PASSWORD_STRING%'|shquote($G::auth_hidepw)|eg;
} else {
$n{data} =~ s|'%RAW_PASSWORD_STRING%'|shquote('PROVIDED_BUT_REMOVED')|eg;
}
}
return(\%n);
}
sub encode_mime_part {
my $part = shift;
my $boundary = shift;
my $no_attach_text = shift; # if this is true and there's no name, Don't set disposition to attachment
my $text = '';
$text .= "--$boundary\n";
if ($part->{type} =~ m|^text/plain$|i && !$part->{name}) {
$text .= "Content-Type: $part->{type}\n\n" . $part->{body} . "\n";
}
else {
if ($part->{name}) {
$text .= "Content-Type: $part->{type}; name=\"$part->{name}\"\n"
. "Content-Description: $part->{name}\n"
. "Content-Disposition: attachment; filename=\"$part->{name}\"\n";
}
else {
$text .= "Content-Type: $part->{type}\n";
if (!($part->{type} =~ m|^text/|i && $no_attach_text)) {
$text .= "Content-Disposition: attachment\n";
}
}
$text .= "Content-Transfer-Encoding: BASE64\n"
. "\n" . eb64($part->{body}, "\n") . "\n";
}
return($text);
}
sub parse_server {
my $server = shift;
my $port = shift;
if ($server =~ m|^\[([^\]]+)\]:(.*)$|) {
# [1.2.3.4]:25
# [hostname]:25
# [1:2::3]:25
return($1, $2);
} elsif ($server =~ m|^([^:]+):([^:]+)$|) {
# 1.2.3.4:25
# hostname:25
return($1, $2);
} elsif ($server =~ m|^\[?([^/\]]*)\]?/(\w+)$|) {
# 1.2.3.4/25 [1.2.3.4]/25
# hostname/25 [hostname]/25
# 1:2::3/25 [1:2::3]/25
return($1, $2);
} elsif ($server =~ m|^\[([^\]]+)\]$|) {
# [1.2.3.4]
# [hostname]
# [1:2::3]
return($1, $port);
}
return($server, $port);
}
sub get_running_state {
my $opts = shift;
my $dump_args = shift;
my $skip = shift;
my @parts = ();
if (($dump_args->{'SUPPORT'} || $dump_args->{'ALL'}) && !$skip->{'SUPPORT'}) {
push(@parts, test_support(1));
}
if ($dump_args->{'APP'} || $dump_args->{'ALL'}) {
push(@parts, [
'App Info:',
" X-Mailer = $p_name v$p_version jetmore.org/john/code/swaks/",
' Cmd Line = ' . $0 . ' ' . $G::cmdline,
]);
}
if ($dump_args->{'OUTPUT'} || $dump_args->{'ALL'}) {
push(@parts, [
'Output Info:',
' show_time_lapse = ' . ($G::show_time_lapse ? "TRUE ($G::show_time_lapse)" : 'FALSE'),
' show_raw_text = ' . ($G::show_raw_text ? 'TRUE' : 'FALSE'),
' suppress_data = ' . ($G::suppress_data ? 'TRUE' : 'FALSE'),
' protect_prompt = ' . ($G::protect_prompt ? 'TRUE' : 'FALSE'),
' no_hints_send = ' . ($G::no_hints_send ? 'TRUE' : 'FALSE'),
' no_hints_recv = ' . ($G::no_hints_recv ? 'TRUE' : 'FALSE'),
' no_hints_info = ' . ($G::no_hints_info ? 'TRUE' : 'FALSE'),
" silent = $G::silent",
' dump_mail = ' . ($G::dump_mail ? 'TRUE' : 'FALSE'),
' hide_send = ' . ($G::hide_send ? 'TRUE' : 'FALSE'),
' hide_receive = ' . ($G::hide_receive ? 'TRUE' : 'FALSE'),
' hide_informational = ' . ($G::hide_informational ? 'TRUE' : 'FALSE'),
' hide_all = ' . ($G::hide_all ? 'TRUE' : 'FALSE'),
" trans_fh_of = $G::trans_fh_of ($G::trans_fh_oh," . \*STDOUT . ')',
" trans_fh_ef = $G::trans_fh_ef ($G::trans_fh_eh," . \*STDERR . ')',
]);
}
if ($dump_args->{'TRANSPORT'} || $dump_args->{'ALL'}) {
push(@parts, [
'Transport Info:',
" type = $G::link{type}"
]);
if ($G::link{type} eq 'socket-inet') {
push(@{$parts[-1]},
' inet protocol = ' . ($G::link{force_ipv4} ? '4' : ($G::link{force_ipv6} ? '6' : 'any')),
" server = $G::link{server}",
" port = $G::link{port}",
" local interface = $G::link{lint}",
" local port = $G::link{lport}",
' copy routing = ' . ($opts->{copy_routing} ? $opts->{copy_routing} : 'FALSE'),
);
}
elsif ($G::link{type} eq 'socket-unix') {
push(@{$parts[-1]}, " sockfile = $G::link{sockfile}");
}
elsif ($G::link{type} eq 'pipe') {
push(@{$parts[-1]}, " process = $G::link{process}");
}
else {
push(@{$parts[-1]}, " UNKNOWN TRANSPORT TYPE");
}
}
if ($dump_args->{'PROTOCOL'} || $dump_args->{'ALL'}) {
push(@parts, [
'Protocol Info:',
" protocol = $G::protocol",
" helo = $opts->{helo}",
" from = $opts->{from}",
" to = $opts->{to}",
' force getpwuid = ' . ($opts->{force_getpwuid} ? 'TRUE' : 'FALSE'),
" quit after = $G::quit_after",
" drop after = $G::drop_after",
" drop after send = $G::drop_after_send",
' server_only = ' . ($G::server_only ? 'TRUE' : 'FALSE'),
" timeout = $G::link{timeout}",
' pipeline = ' . ($G::pipeline ? 'TRUE' : 'FALSE'),
' prdr = ' . ($G::prdr ? 'TRUE' : 'FALSE'),
]);
}
if ($dump_args->{'XCLIENT'} || $dump_args->{'ALL'}) {
push(@parts, ['XCLIENT Info:']);
if ($G::xclient{try}) {
if ($G::xclient{optional} == 2) { push(@{$parts[-1]}, ' xclient = optional-strict'); }
elsif ($G::xclient{optional} == 1) { push(@{$parts[-1]}, ' xclient = optional'); }
else { push(@{$parts[-1]}, ' xclient = required'); }
push(@{$parts[-1]},
' no_verify = ' . ($G::xclient{no_verify} ? 'TRUE' : 'FALSE'),
' before starttls = ' . ($G::xclient{before_tls} ? 'TRUE' : 'FALSE'),
);
for (my $i = 0; $i < scalar(@{$G::xclient{strings}}); $i++) {
my $prefix = $i ? ' ' : ' strings =';
push(@{$parts[-1]}, "$prefix XCLIENT $G::xclient{strings}[$i]");
}
} else {
push(@{$parts[-1]}, ' xclient = no');
}
}
if ($dump_args->{'PROXY'} || $dump_args->{'ALL'}) {
push(@parts, ['PROXY Info:']);
if ($G::proxy{try}) {
push(@{$parts[-1]}, ' proxy = yes');
push(@{$parts[-1]}, " version = $G::proxy{version}");
if ($G::proxy{raw}) {
push(@{$parts[-1]}, " raw string = $G::proxy{raw}");
} else {
push(@{$parts[-1]},
' family = ' . $G::proxy{attr}{family},
' source = ' . $G::proxy{attr}{source},
' source port = ' . $G::proxy{attr}{source_port},
' dest = ' . $G::proxy{attr}{dest},
' dest port = ' . $G::proxy{attr}{dest_port},
' protocol = ' . $G::proxy{attr}{protocol},
' command = ' . $G::proxy{attr}{command},
);
}
} else {
push(@{$parts[-1]}, ' proxy = no');
}
}
if ($dump_args->{'TLS'} || $dump_args->{'ALL'}) {
push(@parts, ['TLS / Encryption Info:']);
if ($G::tls || $G::tls_on_connect) {
if ($G::tls) {
if ($G::tls_optional == 2) { push(@{$parts[-1]}, ' tls = starttls (optional-strict)'); }
elsif ($G::tls_optional == 1) { push(@{$parts[-1]}, ' tls = starttls (optional)'); }
else { push(@{$parts[-1]}, ' tls = starttls (required)'); }
}
elsif ($G::tls_on_connect) { push(@{$parts[-1]}, ' tls = starttls on connect (required)'); }
push(@{$parts[-1]},
" peer cert = $G::tls_get_peer_cert",
" local cert = $G::tls_cert",
" local key = $G::tls_key",
" local cipher list = $G::tls_cipher",
" ca path = $G::tls_ca_path",
" sni string = $G::tls_sni_hostname",
' verify server cert = ' . ($G::tls_verify ? 'TRUE' : 'FALSE'),
' available protocols = ' . join(', ', @G::tls_supported_protocols),
' requested protocols = ' . join(', ', @G::tls_protocols),
);
}
else {
push(@{$parts[-1]}, ' tls = no');
}
}
if ($dump_args->{'AUTH'} || $dump_args->{'ALL'}) {
push(@parts, ['Authentication Info:']);
if ($opts->{a_type}) {
if ($G::auth_optional == 2) { push(@{$parts[-1]}, ' auth = optional-strict'); }
elsif ($G::auth_optional == 1) { push(@{$parts[-1]}, ' auth = optional'); }
else { push(@{$parts[-1]}, ' auth = required'); }
push(@{$parts[-1]},
" username = '$opts->{a_user}'",
" password = '%RAW_PASSWORD_STRING%'",
' show plaintext = ' . ($G::auth_showpt ? 'TRUE' : 'FALSE'),
' hide password = ' . ($G::auth_hidepw ? $G::auth_hidepw : 'FALSE'),
' allowed types = ' . join(', ', @{$opts->{a_type}}),
' extras = ' . join(', ', map { "$_=$G::auth_extras{$_}" } (sort(keys((%G::auth_extras))))),
' type map = ' . join("\n".' 'x19, map { "$_ = ". join(', ', @{$G::auth_map_t{$_}}) } (sort(keys(%G::auth_map_t)))),
);
}
else {
push(@{$parts[-1]}, " auth = no");
}
}
if (($dump_args->{'DATA'} || $dump_args->{'ALL'}) && !$skip->{'DATA'}) {
push(@parts, [
'DATA Info:',
' data = <<.',
$opts->{data}
]);
}
# rejoin the parts into a string now
# this whole exercise was to avoid extra newlines when only dumping certain parts
foreach my $part (@parts) {
$part = join("\n", @$part) . "\n";
}
return(join("\n", @parts));
}
sub get_username {
my $force_getpwuid = shift;
if ($^O eq 'MSWin32') {
require Win32;
return Win32::LoginName();
}
if ($force_getpwuid) {
return (getpwuid($<))[0];
}
return $ENV{LOGNAME} || (getpwuid($<))[0];
}
sub get_date_string {
return($G::date_string) if (length($G::date_string) > 0);
my $et = time();
if (!avail("date_manip")) {
ptrans(12, avail_str("date_manip").". Date strings will be in GMT");
my @l = gmtime($et);
$G::date_string = sprintf("%s, %02d %s %d %02d:%02d:%02d %+05d",
(qw(Sun Mon Tue Wed Thu Fri Sat))[$l[6]],
$l[3],
(qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec))[$l[4]],
$l[5]+1900, $l[2], $l[1], $l[0],
0);
} else {
$G::date_string = POSIX::strftime("%a, %d %b %Y %H:%M:%S %z", localtime($et));
}
return($G::date_string);
}
# partially Cribbed from "Programming Perl" and MIME::Base64 v2.12
sub db64 {
my $s = shift;
if (load("MIME::Base64")) {
return(decode_base64($s));
} else {
$s =~ tr#A-Za-z0-9+/##cd;
$s =~ s|=+$||;
$s =~ tr#A-Za-z0-9+/# -_#;
my $r = '';
while ($s =~ s/(.{1,60})//s) {
$r .= unpack("u", chr(32 + int(length($1)*3/4)) . $1);
}
return($r);
}
}
# partially Cribbed from MIME::Base64 v2.12
sub eb64 {
my $s = shift;
my $e = shift || ''; # line ending to use "empty by default"
if (load("MIME::Base64")) {
return(encode_base64($s, $e));
} else {
my $l = length($s);
chomp($s = pack("u", $s));
$s =~ s|\n.||gms;
$s =~ s|\A.||gms;
$s =~ tr#` -_#AA-Za-z0-9+/#;
my $p = (3 - $l%3) % 3;
$s =~ s/.{$p}$/'=' x $p/e if ($p);
$s =~ s/(.{1,76})/$1$e/g if (length($e));
return($s);
}
}
sub build_version {
my $static = shift;
my $svn = shift;
if ($static ne 'DEVRELEASE') {
# if gen-util passed in a static version, use it unconditionally
return $static;
} elsif ($svn =~ /\$Id:\s+\S+\s+(\d+)\s+(\d+)-(\d+)-(\d+)\s+/) {
# otherwise, this is a dev copy, dynamically build a version string for it
return("$2$3$4.$1-dev");
} else {
# we wanted a dynamic version, but the SVN Id tag wasn't in the format
# we expected, punt
return("DEVRELEASE");
}
}
sub ext_usage {
require Config;
$ENV{PATH} .= ":" unless $ENV{PATH} eq "";
$ENV{PATH} = $ENV{PATH} . $Config::Config{'installscript'};
$< = $> = 1 if ($> == 0 || $< == 0);
exec("perldoc", $0) || exit(1);
# make parser happy
%Config::Config = ();
exit(0);
}