#!/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 Specifies the host to connect to. Must be specified and must be IP-resolvable. =item B<-m, --ehlo>=I Specified the machine name to use as the B value. Defaults to the fully-qualified name of the host running the command. =item B<-r, --rcptto>=I Specifies the recipient of message. This is used as the B value. =item B<-v, --vrfy> Uses the I parameter for the value in a B request. This disables the sending of an email. =item B<-e, --expn> Uses the I parameter for the value in an B request. This disables the sending of an email. =item B<-f, --mailfrom>=I Specified the value to use in the B 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 Specified the value to use in the message headers. Defaults to the B<-f> I value unless the B<-B> option is used. =item B<-B, --bounce> Replace the B<--mailfrom> I with B<\<\>> and the B<--from833> I with B where the host is the value of B<--ehlo> I =item B<-p, --port>=I 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. =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 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 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 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 Provides the path value for a B command. =item B<-W, --output>=I Provides the output file where the B data should be stored. =back =head1 DESCRIPTION B 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 only requires modules that should be in all normal distributions. =head1 AUTHOR Bernard Quatermass =head1 COPYRIGHT AND LICENSE This software is Copyright (c) 2008,2020,2024 by Bernard Quatermass. =cut ############################################################### # vi: sw=4 et # End of File ###############################################################