#!/usr/bin/perl
#
###############################################################
###############################################################

use strict;

use Net::SMTP;
#use IO::Socket::SSL qw( SSL_ERROR );
use Net::Domain qw(hostfqdn);
use Getopt::Long qw(:config default bundling no_ignore_case auto_version);
use Pod::Usage;
use Net::Cmd;
use Data::Dumper;

our @ISA = qw(Net::Cmd);

###############################################################
###############################################################

my ($smtp,$optsin,$opt,$mess,$rcpt,@headers,$finished_header,$ofh);
$main::VERSION = '1.2.2';

$optsin = {
    'body|b'                    => \&optset,
    'debug|d'                   => \&optset,
    'ehlo|helo|m=s'             => \&optset,
    'rcptto|recipient|r=s'      => \&optset,
    'host|h=s'                  => \&optset,
    'from822|u=s'               => \&optset,
    'vrfy|v'                    => \&optset,
    'expn|e'                    => \&optset,
    'mailfrom|from821|from|f=s' => \&optset,
    'port|p=i'                  => \&optset,
    'wellknown|w=s'             => \&optset,
    'output|W=s'                => \&optset,
    'include_options|O'         => \&optset,
    'include_headers|H'         => \&optset,
    'bounce|B'                  => \&optset,
    'tls|S'                     => \&optset,
    'nostarttls|s'              => \&optset,
    'stricttls|strict_tls'      => \&optset,
    'sslargs|tlsargs=s'         => \&optset,
    'verbose'                   => \&optset,
    'help'                      => \&optset,
    'man'                       => \&optset,
};
map { my $t = $_; $t =~ s/\|.*//; $opt->{$t} = undef; } keys %$optsin;
GetOptions( %$optsin ) or pod2usage(2);
pod2usage(1) if $opt->{'help'};
pod2usage(-exitval => 0, -verbose => 2) if $opt->{'man'};

print _Dumper($opt, 'Options')
    if $opt->{'debug'};

###############################################################
###############################################################
##
## parameter checking
##
###############################################################
###############################################################

bail( 1, "Host(--host) must be provided" )
    if !$opt->{'host'};

$opt->{'port'} = $opt->{'tls'} ? 465 : 25
    if ! $opt->{'port'};

if (!$opt->{'ehlo'})
{
    $opt->{'ehlo'} = hostfqdn();
    fret( "Machine set to $opt->{'ehlo'}" ) if $opt->{'debug'};
}
if (!$opt->{'mailfrom'} && !$opt->{'bounce'})
{
    $opt->{'mailfrom'} = $ENV{USER}. "@". $opt->{'ehlo'};
    fret( "MAIL FROM set to $opt->{'mailfrom'}" ) if $opt->{'debug'};
}
if (!$opt->{'from822'})
{
    $opt->{'from822'} = $opt->{'mailfrom'};
    fret( "From: set to $opt->{'from822'}" ) if $opt->{'debug'};
}
if ($opt->{'bounce'})
{
    $opt->{'mailfrom'} = "";
    $opt->{'from822'} = 'mailer-daemon@'. hostfqdn();
    fret( "MAIL FROM set to $opt->{'mailfrom'}", "From: set to $opt->{'from822'}" ) if $opt->{'debug'};
}

bail( 1, "EXPN or VRFY cannot be used without a recipient" )
    if ($opt->{'expn'} || $opt->{'vrfy'}) && ! $opt->{'rcptto'};

bail( 1, "Either a recipient or well-known resource must be specified" )
    if ! $opt->{'wellknown'} && ! $opt->{'rcptto'};

bail( 1, "Only one of recipient or well-known resource can be specified" )
    if $opt->{'wellknown'} && $opt->{'rcptto'};

if ( $opt->{'sslargs'} )
{
    my @p = split /[=,]/, $opt->{'sslargs'};
    $opt->{'sslparams'} = \@p;
}
else
{
    $opt->{'sslparams'} = [ 'SSL_verify_mode', $opt->{'stricttls'} ? 1 : 0 ];
}
fret( _Dumper( $opt->{'sslparams'}, 'sslparams' ) )
    if $opt->{'debug'} && ( $opt->{'tls'} || ! $opt->{'nostarttls'} );

###############################################################
###############################################################
##
## parameter checking complete. now onto operations
##
##
###############################################################
###############################################################



$smtp= Net::SMTP->new(  $opt->{'host'},
                        Hello   => $opt->{'ehlo'},
                        Debug   => $opt->{'debug'},
                        ( $opt->{'tls'} ? ( 'SSL' => $opt->{'sslargs'} || 1 ) : () ),
                        Port    => $opt->{'port'},
                        );
bail( 1, "Connection Failed: $@" )
    if !$smtp;

if (!$opt->{'nostarttls'})
{
    bail( $smtp, 1, "Failed to STARTTLS - $@" )
        if ! $smtp->starttls( @{$opt->{'sslparams'}} );

    fret( $smtp->message() )
        if $opt->{'verbose'};
}

if ($opt->{'wellknown'})
{
    bail( $smtp, 1, "Server does not support WELLKNOWN" )
        if ! $smtp->supports('WELLKNOWN');

    my $e_wk = encode_xtext( $opt->{'wellknown'} );

    bail( $smtp, 1, "Failed to WELLKNOWN - $e_wk", $smtp->message() )
        if ! ( $smtp->command( 'WELLKNOWN', $e_wk )->response() == CMD_OK );

    fret( "Protocol violation. Code was OK, but not 250",   $smtp->code. " - ". $smtp->message )
        if $smtp->code ne '250';

    $mess = $smtp->message;
    my ($info,$size);
    ($info,$mess) = split( /\n/, $mess, 2 );
    $info =~ /size=(\d+)/i;
    $size = $1 + 0;
    $mess = decode_xtext( $mess );
    fret( "Size mismatch on wellknown fetch", "Expected: ". $size, "Received: ". length($mess) )
        if length($mess) != $size;

    if ( $opt->{'output'} )
    {
        # Output to named file
        #
        bail( $smtp, 1, "Unable to open file $opt->{'output'} for WELLKNOWN output - $!" )
            if ! ( $ofh = IO::File->new("> $opt->{'output'}") );

        print $ofh $mess;
        $ofh->close;
    }
    else
    {
        # might be hazardous, output via pager
        print STDERR "$mess\n";
    }
}

if ($opt->{'vrfy'})
{
    $smtp->verify($opt->{'vrfy'});
    fret( $smtp->message() );
}

if ($opt->{'expn'})
{
    $smtp->expand($opt->{'expn'});
    fret( $smtp->message() );
}

if ($opt->{'rcptto'})
{
    bail( $smtp, 1, "MAIL FROM $opt->{'mailfrom'} failed", $@ )
        if ! $smtp->mail($opt->{'mailfrom'});

    bail( $smtp, 1, "RCPT TO $opt->{'rcptto'} failed", $@ )
        if ! $smtp->to($opt->{'rcptto'});

    # handle any recipients on command line
    while( $rcpt = shift @ARGV )
    {
        last if $rcpt eq '--';
        fret( "RCPT TO $rcpt failed", $@ )
            if ! $smtp->to($rcpt);
    }

    bail( $smtp, 1, "Unable to set data mode", @_ )
        if ! $smtp->data();

    if ($opt->{'body'})
    {
        push @headers, "Subject: Test Message\n";
        $smtp->datasend("From: $opt->{'from822'}\n");
        $smtp->datasend("To: $opt->{'rcptto'}\n");
        $smtp->datasend("Subject: Test Message\n");
        $smtp->datasend("\n");
        $smtp->datasend("This is a test message\n");
        $smtp->datasend("generated with mailtest\n");
    }else
    {
        while(<>)
        {
            if($finished_header==0)
            {
                if (length($_)<=1)
                {
                    $finished_header = 1;
                }else
                {
                    push @headers,"    ".$_;
                }
            }
            $smtp->datasend("$_");
        }
    }
    if($opt->{'include_headers'} && @headers)
    {
        $smtp->datasend("\n Copy of headers follow....\n");
        foreach(@headers)
        {
            $smtp->datasend("$_");
        }
        $smtp->datasend("\n");
    }
    if($opt->{'include_options'})
    {
        $smtp->datasend("\n Copy of options follow....\n");
        $smtp->datasend("    SMTP HOST $opt->{'host'}\n");
        $smtp->datasend("    HELO $opt->{'ehlo'}\n");
        $smtp->datasend("    MAIL FROM: $opt->{'mailfrom'}\n");
        $smtp->datasend("    RCPT TO: $opt->{'rcptto'}\n\n");
    }
    fret( "dataend failed", $@ )
        if ! $smtp->dataend();
}
$smtp->quit();

exit;

##############################################################
##############################################################

sub
optset
{
    my $n = shift;
    my $v = shift;
    #print STDERR "Setting $n to $v\n";
    $opt->{$n->{'name'}} = $v;
}

sub
decode_xtext
{
    my $mess = shift;
    $mess =~ s/[\n\r]//g;
    $mess =~ s/\+([0-9a-fA-F]{2})/chr(hex($1))/ge;
    return $mess;
}

sub
encode_xtext
{
    my $mess = shift;
    $mess =~ s/([^!-*,-<>-~])/'+'.uc(unpack('H*', $1))/eg;
    return $mess;
}

sub
_Dumper
{
    return Data::Dumper->Dump( [$_[0]], [$_[1] || 'VAR1'] );
}

sub
fret
{
    map { print STDERR $_,"\n"; } @_;
}

sub
bail
{
    shift->quit
        if ref($_[0]);
    my $rc = shift;
    fret( @_ );
    exit $rc;
}

##############################################################
##############################################################

__END__

=head1 NAME

mailtest - Simple SMTP sending for diagnostics

=head1 SYNOPSIS

mailtest --host host.example.com --rcptto recipient@example.com [ send_options ... ] [ additional recipients ...]


Options:
  --help
                brief help message
  --debug
                enable debugging

  --host host
                host to connect to
  --rcptto recipient
                recipient for message

  --helo machine
                machine name for EHLO

  --vrfy        request VRFY of recipient
  --expn        request EXPN of recipient

  --mailfrom from
                use as MAIL FROM value
  --from822 from
                content From:

  --port port
                port to connect to

  --body            generate body
  --include_options
                include Options in body
  --include_headers
                include generated headers in body

  --tls         perform TLS on connect
  --nostarttls  do no attempt STARTTLS
  --stricttls   Enable strict verification on TLS connection

  --tlsargs arg=value[,arg=value]
                Explicitly define TLS options.

  --bounce      sending as bounce (<>)

  --wellknown path
                well-known path
  --output file
                Output file to receive well-known data

=head1 OPTIONS

=over 8


=item B<--help>

Print a brief help message and exits.

=item B<-d, --debug>

Enables debugging, outpus additional information whilst processing requests.

=item B<-h, --host>=I<host>

Specifies the host to connect to. Must be specified and must be IP-resolvable.

=item B<-m, --ehlo>=I<machine>

Specified the machine name to use as the B<EHLO> value. Defaults to the fully-qualified name of the host running the command.

=item B<-r, --rcptto>=I<recipient>

Specifies the recipient of message. This is used as the B<RCPT TO> value.

=item B<-v, --vrfy>

Uses the I<recipient> parameter for the value in a B<VRFY> request. This disables the sending of an email.

=item B<-e, --expn>

Uses the I<recipient> parameter for the value in an B<EXPN> request. This disables the sending of an email.

=item B<-f, --mailfrom>=I<from_address>

Specified the value to use in the B<MAIL FROM> command. Defaults to the current username at the FQDN of the machine B<-m> unless the B<-B> option is used.

=item B<-u, --from822>=I<from_user>

Specified the value to use in the message headers. Defaults to the B<-f> I<from_address> value unless the B<-B> option is used.

=item B<-B, --bounce>

Replace the B<--mailfrom> I<from_address> with B<\<\>> and the B<--from833> I<from_user> with B<mailer-daemon@host> where the host is the value of B<--ehlo> I<machine>

=item B<-p, --port>=I<port>

Specified the port to connect to on the specified host. Defaults to port 25 unless B<-S> is given in which case it defaults to 465.

=item B<-S, --tls>

Specifies that TLS be used directly on the connection prior to any SMTP commands. Changes the connection port to 465 unless it has been explicitly provided. Disables any attempts at B<STARTTLS>.

=item B<-s, --nostarttls>

Disables attempting STARTTLS if offered. Disabled by use of B<-S>.

=item B<--stricttls>

Enables strict verification of the TLS connection. Sets the underlying SSL option B<SSL_verify_mode> to 1/SSL_VERIFY_PEER rather than 0/SSL_VERIFY_NONE. Since the aim of this tool is to test the SMTP protocol behaviour and not the TLS behaviour the decision was made to default the B<SSL_verify_mode> to 0/SSL_VERIFY_NONE.

=item B<--sslargs>=argname=argvalue[,argname=argvalue...]

Allow full control over underlying SSL options. Overrides B<--stricttls>. See the documentation for B<IO::Socket::SSL> for further details.

    --sslargs SSL_verifycn_name=certname.example.com

=item B<-b, --body>

Generate a body for the message being sent.

=item B<-O, --include-options>

Include details of options used in the message body.

=item B<-H, --include-headers>

Include a copy of the generated headers in the message body.

=item B<-w, --wellknown>=I<well-known-path>

Provides the path value for a B<WELLKNOWN> command.

=item B<-W, --output>=I<output_file>

Provides the output file where the B<WELLKNOWN> data should be stored.

=back

=head1 DESCRIPTION

B<mailtest> is a simple utility for testing SMTP connections.
It is designed to debug endpoints and not for full email generation.

It support a number of basic operations, SEND, VRFY, EXPN, WELLKNOWN.

=head1 COMPATIBILITY

C<mailtest> only requires modules that should be in all normal distributions.

=head1 AUTHOR

Bernard Quatermass <bernardq@exim.org>

=head1 COPYRIGHT AND LICENSE

This software is Copyright (c) 2008,2020,2024 by Bernard Quatermass.


=cut

###############################################################
# vi: sw=4 et
# End of File
###############################################################
