-#!/usr/bin/perl -w
+#! /usr/bin/perl -w
#
-# checkmail.pl
-##############
-
-# (c) 2002-2005 Thomas Hochstein <thh@inter.net>
+# checkmail Version 0.6.3 by Thomas Hochstein
+#
+# This script tries to verify the deliverability of (a) mail address(es).
+#
+# Copyright (c) 2002-2016 Thomas Hochstein <thh@thh.name>
#
-# 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.
-
-# Versionsnummer ######################
-$ver = '0.2 beta (20050803)';
-
-# Modules #############################
+# It can be redistributed and/or modified under the same terms under
+# which Perl itself is published.
+
+our $VERSION = "0.6.3 (unreleased)";
+
+################################# Configuration ################################
+# Please fill in a working configuration!
+my %config=(
+ # value used for HELO/EHLO - a valid hostname you own
+ helo => 'testhost.domain.example',
+ # value used for MAIL FROM: - a valid address under your control
+ from => 'mailtest@testhost.domain.example'
+ );
+
+################################### Modules ####################################
+use strict;
+use File::Basename;
use Getopt::Std;
+use Mail::Address;
use Net::DNS;
use Net::SMTP;
-# Konfiguration #######################
-# Hier passende Werte einsetzen! #
-#######################################
-%config=();
-# HELO-/EHLO-Parameter - a valid hostname you own
-$config{'helo'} = 'testhost.domain.example';
-# MAIL FROM:-Parameter - a valid address you control
-$config{'from'} = 'mailtest@testhost.domain.example';
-# Zufaelliger Localpart fuer -r - a valid random localpart
-$config{'rand'} = 'ZOq62fow1i';
-
-################################################################
-# Hauptprogramm #######################
-
-# Konfiguration einlesen
+################################# Main program #################################
+
+$Getopt::Std::STANDARD_HELP_VERSION = 1;
+my $myself = basename($0);
+
+# read commandline options
my %options;
-getopts('hqlrf:m:', \%options);
-
-if ($options{'h'} or (!$options{'f'} and !$ARGV[0])) {
- print "$0 v $ver\nUsage: $0 [-hqlr] [-m <host>] -f <file>|<address>\n";
- print "Options: -h display this notice\n";
- print " -q quiet (no output, just exit with 0/1/2/3)\n";
- print " -l extended logging\n";
- print " -r test random address to verify verification\n";
- print " -m <host> no DNS lookup, just test this host\n";
- print " -f <file> parse file (one address per line)\n";
- print " <address> mail address to check\n\n";
- exit(100);
+getopts('Vhqlrf:m:s:e:', \%options);
+
+# -V: display version
+if ($options{'V'}) {
+ print "$myself v $VERSION\nCopyright (c) 2002-2016 Thomas Hochstein <thh\@thh.name>\n";
+ print "This program is free software; you may redistribute it and/or modify it under the same terms as Perl itself.\n";
+ exit(100);
+};
+
+# -h: feed myself to perldoc
+if ($options{'h'}) {
+ exec('perldoc', $0);
+ exit(100);
};
+# display usage information if neither -f nor an address are present
+if (!$options{'f'} and !$ARGV[0]) {
+ print "Usage: $myself [-hqlr] [-m <host>] [-s <from>] [-e <EHLO>] <address>|-f <file>\n";
+ print "Options: -V display copyright and version\n";
+ print " -h show documentation\n";
+ print " -q quiet (no output, just exit with 0/1/2/3)\n";
+ print " -l extended logging\n";
+ print " -r test random address to verify verification\n";
+ print " -m <host> no DNS lookup, just test this host\n";
+ print " -s <from> override configured value for MAIL FROM\n";
+ print " -e <EHLO> override configured value for EHLO\n";
+ print " <address> mail address to check\n\n";
+ print " -f <file> parse file (one address per line)\n";
+ exit(100);
+};
+
+# -s / -e: override configuration
+$config{'from'} = $options{'s'} if defined($options{'s'});
+$config{'helo'} = $options{'e'} if $options{'e'};
+
+# -f: open file and read addresses to @adresses
+my @addresses;
if ($options{'f'}) {
if (-e $options{'f'}) {
- open FILE, "<$options{'f'}" or die("ERROR: Could not open file $options{'f'} for reading: $!");
+ open FILE, "<$options{'f'}" or die("$myself ERROR: Could not open file $options{'f'} for reading: $!");
} else {
- die("ERROR: File $options{'f'} does not exist!\n");
+ die("$myself ERROR: File $options{'f'} does not exist!\n");
};
- $log = '';
while(<FILE>) {
chomp;
- ($status,$log) = checkdns($_,$log);
+ push(@addresses,$_);
};
close FILE;
- # force exit(0)
- $status = 0;
-} else {
- ($status,$log) = checkdns($ARGV[0]);
+# fill @adresses with single address to check
+ } else {
+ push(@addresses,$ARGV[0]);
+};
+
+# loop over each address and test it
+my (%targets,$curstat,$status,$log,$message);
+foreach (@addresses) {
+ my $address = $_;
+ # regexp taken from http://www.regular-expressions.info/email.html
+ # with escaping of "/" added two times and "*" changed to "+"
+ # in localpart, second alternative
+ if ($address !~ /^(?:[a-z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+\/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f]+)")@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i) {
+ printf(" > Address <%s> is syntactically INVALID.\n",$address) if !($options{'q'});
+ $curstat = 2;
+ } else {
+ my $domain = Mail::Address->new('',$address)->host;
+ printf(" * Testing %s ...\n",$address) if !($options{'q'});
+ $log .= "\n===== BEGIN $address =====\n";
+ # get list of target hosts or take host forced via -m
+ if (!$options{'m'}) {
+ %targets = %{gettargets($domain,\$log)};
+ } else {
+ $message = sprintf("Connection to %s forced by -m.\n",$options{'m'});
+ $log .= $message;
+ print " $message" if !($options{'q'});
+ # just one target host with preference 0
+ $targets{$options{'m'}} = 0;
+ };
+ if (%targets) {
+ $curstat = checkaddress($address,\%targets,\$log);
+ } else {
+ $curstat = 2;
+ $message = 'DNS lookup failure';
+ printf(" > Address is INVALID (%s).\n",$message) if !($options{'q'});
+ $log .= $message . '.';
+ };
+ $log .= "====== END $address ======\n";
+ };
+ $status = $curstat if (!defined($status) or $curstat > $status);
};
print $log if ($options{'l'});
# status 0: valid / batch processing
-# 1: invalid
-# 2: cannot verify
-# 3: temporary (?) failure
+# 1: connection failed or temporary failure
+# 2: invalid
+# 3: cannot verify
+#D print "\n-> EXIT $status\n";
exit($status);
-################################################################
-# Subroutinen #########################
-
-sub checkdns {
- # - fester Host angegeben (-m)?
- # - sonst: MX-Record ermitteln
- # - bei Verbindungsproblemen naechsten MX versuchen
- # - falls kein MX vorhanden, Fallback auf A
- # -> jeweils Adresse testen via checksmtp()
- my ($address,$logging) = @_;
- my ($rr,$mailhost,$status,@mx);
- my $dnsresult = 'okay';
- # (my $lp = $address) =~ s/^([^@]+)@.*/$1/;
- (my $domain = $address) =~ s/[^@]+\@(\S*)$/$1/;
-
- $logging .= "\n----- BEGIN $address -----\n";
-
- # DNS-Lookup unterdrueckt?
- if ($options{'m'}) {
- print " Connection to $options{'m'} forced by -m.\n";
- $logging .= "Connection to $options{'m'} forced by -m.\n";
- ($status,$logging) = checksmtp($options{'m'},$address,$domain,$logging);
- $logging .= "----- END $address -----\n";
- return ($status,$logging);
- };
+################################## gettargets ##################################
+# get mail exchanger(s) or A record(s) for a domain
+# IN : $domain: domain to query the DNS for
+# OUT: \%targets: reference to a hash containing a list of target hosts
+sub gettargets {
+ my ($domain,$logr) = @_;
+ # resolver objekt
+ my $resolver = Net::DNS::Resolver->new(udp_timeout => 15, tcp_timeout => 15);
- # Resolver-Objekt
- $resolve = Net::DNS::Resolver -> new();
- $resolve->usevc(1);
- $resolve->tcp_timeout(15);
-
- # MX-Record feststellen
- @mx = mx($resolve,$domain) or $dnsresult = $resolve->errorstring;
- print " $domain (MX: $dnsresult)\n" if !($options{'q'});
-
- if (@mx) {
- WALKMX: foreach $rr (@mx) {
- $mailhost = $rr->exchange;
- print " MX: $mailhost / $address\n" if !($options{'q'});
- $logging .= "Try MX: $mailhost\n";
- ($status,$logging) = checksmtp($mailhost,$address,$domain,$logging);
- last WALKMX if ($status < 3);
+ my %targets;
+ # get MX record(s) as a list sorted by preference
+ if (my @mxrr = mx($resolver,$domain)) {
+ print_dns_result($domain,'MX',scalar(@mxrr),undef,$logr);
+ foreach my $rr (@mxrr) {
+ $targets{$rr->exchange} = $rr->preference;
+ $$logr .= sprintf("(%d) %s\n",$rr->preference,$rr->exchange);
+ };
+ # no MX record found; log and try A record(s)
+ } else {
+ print_dns_result($domain,'MX',undef,$resolver->errorstring,$logr);
+ print(" Falling back to A record(s) ...\n") if !($options{'q'});
+ # get A record(s)
+ # may get CNAMEs instead ...
+ if (my $query = $resolver->query($domain,'A','IN')) {
+ print_dns_result($domain,'A/CNAME',$query->header->ancount,undef,$logr);
+ foreach my $rr ($query->answer) {
+ if ($rr->type ne 'A') {
+ # report CNAMEs and don't add them to target list
+ if ($rr->type eq 'CNAME') {
+ printf (" ~ '%s' is a CNAME for '%s' and will be resolved accordingly. \n",$rr->name,$rr->cname) if !($options{'q'});
+ $$logr .= sprintf("- CNAME resolved: %s -> %s\n",$rr->name,$rr->cname);
+ }
+ next;
+ }
+ $targets{$rr->address} = 0;
+ $$logr .= sprintf("- %s\n",$rr->address);
+ };
+ # no A record found either; log and fail
+ } else {
+ print_dns_result($domain,'A',undef,$resolver->errorstring,$logr);
+ printf(" %s has neither MX nor A records - mail cannot be delivered.\n",$domain) if !($options{'q'});
+ };
};
- } elsif ($dnsresult eq 'NXDOMAIN' or $dnsresult eq 'NOERROR' or $dnsresult eq 'REFUSED') {
- # wenn kein MX-Record: A-Record feststellen
- $logging .= "MX error: $dnsresult\n";
- $dnsresult = 'okay';
- $query = $resolve->search($domain) or $dnsresult = $resolve->errorstring;
- print " $domain (A: $dnsresult)\n" if !($options{'q'});
- if ($query) {
- foreach $rr ($query->answer) {
- next unless $rr->type eq "A";
- $mailhost = $rr->address;
- print " A: $mailhost / $address\n" if !($options{'q'});
- $logging .= "Try A: $mailhost\n";
- ($status,$logging) = checksmtp($mailhost,$address,$domain,$logging);
- };
- } elsif ($dnsresult eq 'NXDOMAIN' or $dnsresult eq 'NOERROR' or $dnsresult eq 'REFUSED') {
- # wenn auch kein A-Record: what a pity ...
- print " > NO DNS-RECORD (MX/A) FOUND.\n" if !($options{'q'});
- $logging .= "A error: $dnsresult\n";
- $status = 1;
+ return \%targets;
+};
+
+################################# checkaddress #################################
+# test address for deliverability
+# IN : $address: adress to be tested
+# \%targets: reference to a hash containing a list of MX hosts
+# \$log : reference to the log (to be printed out via -l)
+# OUT: ---
+# \$log will be changed
+sub checkaddress {
+ my ($address,$targetsr,$logr) = @_;
+ my %targets = %{$targetsr};
+ my $status;
+ # walk %targets in order of preference
+ foreach my $host (sort { $targets{$a} <=> $targets{$b} } keys %targets) {
+ printf(" / Trying %s (%s) with %s\n",$host,$targets{$host} || 'A',$address) if !($options{'q'});
+ $$logr .= sprintf("%s:\n%s\n",$host,"-" x (length($host)+1));
+ $status = checksmtp($address,$host,$logr);
+ last if ($status != 1);
};
- };
- $logging .= "----- END $address -----\n";
- return ($status,$logging);
+ return $status;
};
+################################### checksmtp ##################################
+# connect to a remote machine on port 25 and test deliverability of a mail
+# address by doing the SMTP dialog until RCPT TO stage
+# IN : $address: address to test
+# $target : target host
+# \$log : reference to the log (to be printed out via -l)
+# OUT: .........: reference to a hash containing a list of target hosts
+# \$log will be changed
sub checksmtp {
- # - zu $mailhost verbinden, $adresse testen (SMTP-Dialog bis RCPT TO)
- # - ggf. (-r) testen, ob sicher ungueltige Adresse abgelehnt oder
- # alles angenommen wird
- my($mailhost,$address,$domain,$logging)=@_;
- my($smtp,$status,$valid);
- $logging .= "-------------------------\n";
- CONNECT: if ($smtp = Net::SMTP->new($mailhost,Hello => $config{'helo'},Timeout => 30)) {
- $logging .= $smtp->banner;
- $logging .= "EHLO $config{'helo'}\n";
- $logging .= parse_reply($smtp->code,$smtp->message);
- $smtp->mail($config{'from'});
- $logging .= "MAIL FROM:<$config{'from'}>\n";
- $logging .= parse_reply($smtp->code,$smtp->message);
- # wird RCPT TO akzeptiert?
- $valid = $smtp->to($address);
- $logging .= "RCPT TO:<$address>\n";
- if ($smtp->code > 0) {
- # es kam eine Antwort auf RCPT TO
- $logging .= parse_reply($smtp->code,$smtp->message);
- if ($valid) {
- # RCPT TO akzeptiert
- $status = 0;
- if ($options{'r'}) {
- # werden sicher ungueltige Adressen abgewiesen?
- $valid = $smtp->to($config{'rand'}.'@'.$domain);
- $logging .= 'RCPT TO:<'.$config{'rand'}.'@'.$domain.">\n";
- if ($smtp->code > 0) {
- # es kam eine Antwort auf RCPT TO (fuer $rand)
- $logging .= parse_reply($smtp->code,$smtp->message);
- if ($valid) {
- # ungueltiges RCPT TO akzeptiert
- print " > Sorry, cannot verify. You'll have to send a testmail ...\n" if !($options{'q'});
- $status = 2;
+ my ($address,$target,$logr) = @_;
+ my ($status);
+ # start SMTP connection
+ if (my $smtp = Net::SMTP->new($target,Hello => $config{'helo'},Timeout => 30)) {
+ $$logr .= $smtp->banner; # Net::SMTP doesn't seem to support multiline greetings.
+ $$logr .= "EHLO $config{'helo'}\n";
+ log_smtp_reply($logr,$smtp->code,$smtp->message);
+ $smtp->mail($config{'from'});
+ $$logr .= "MAIL FROM:<$config{'from'}>\n";
+ log_smtp_reply($logr,$smtp->code,$smtp->message);
+ # test address
+ my ($success,$code,@message) = try_rcpt_to(\$smtp,$address,$logr);
+ # connection failure?
+ if ($success < 0) {
+ $status = connection_failed(@message);
+ # delivery attempt was successful?
+ } elsif ($success) {
+ # -r: try random address (which should be guaranteed to be invalid)
+ if ($options{'r'}) {
+ my ($success,$code,@message) = try_rcpt_to(\$smtp,create_rand_addr(Mail::Address->new('',$address)->host),$logr);
+ # connection failure?
+ if ($success < 0) {
+ $status = connection_failed(@message);
+ # reset status - the address has been checked and _is_ valid!
+ $status = 3;
+ print " > Address verification currently impossible. You'll have to try again or send a test mail ...\n" if !($options{'q'});
+ # verification impossible?
+ } elsif ($success) {
+ $status = 3;
+ print " > Address verification impossible. You'll have to send a test mail ...\n" if !($options{'q'});
+ }
+ }
+ # if -r is not set or status was not set to 3: valid address
+ if (!defined($status)) {
+ $status = 0;
+ print " > Address is valid.\n" if !($options{'q'});
};
- } else {
- # Timeout nach RCPT TO (fuer $rand)
- print " > Temporary failure.\n" if !($options{'q'});
- $logging .= "---Timeout---\n";
- $smtp->quit;
- $status = 3;
- };
- };
- print " > Address is valid.\n" if (!$status and !$options{'q'});
- } else {
- # RCPT TO nicht akzeptiert
- print " > Address is INVALID.\n" if !($options{'q'});
- $status = 1;
- };
- # Verbindung beenden
- $smtp->quit;
- $logging .= "QUIT\n";
- $logging .= parse_reply($smtp->code,$smtp->message);
+ # delivery attempt failed?
+ } else {
+ $status = 2;
+ print " > Address is INVALID:\n" if !($options{'q'});
+ print ' ' . join(' ',@message) if !($options{'q'});
+ }
+ # terminate SMTP connection
+ $smtp->quit;
+ $$logr .= "QUIT\n";
+ log_smtp_reply($logr,$smtp->code,$smtp->message);
} else {
- # Timeout nach RCPT TO
- print " > Temporary failure.\n" if !($options{'q'});
- $logging .= "---Timeout---\n";
- $smtp->quit;
- $status = 3;
+ # SMTP connection failed / timeout
+ $status = connection_failed();
+ $$logr .= "---Connection failure---\n";
};
- } else {
- # Verbindung fehlgeschlagen
- print " > Temporary failure.\n" if !($options{'q'});
- $logging .= "---Timeout---\n";
- $status = 3;
- };
- return ($status,$logging);
+ return $status;
+}
+
+############################### create_rand_addr ###############################
+# create a random mail address
+# IN : $domain: the domain part
+# OUT: $address: the address
+sub create_rand_addr {
+ my($domain)=@_;
+ my $allowed = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789-+_=';
+ my $address = '';
+ while (length($address) < 15) {
+ $address .= substr($allowed, (int(rand(length($allowed)))),1);
+ };
+ return ($address.'@'.$domain);
+};
+
+################################ parse_dns_reply ###############################
+# parse DNS response codes and return code and description
+# IN : $response: a DNS response code
+# OUT: "$response ($desciption)"
+sub parse_dns_reply {
+ my($response)=@_;
+ my %dnsrespcodes = (NOERROR => 'empty response',
+ NXDOMAIN => 'non-existent domain',
+ SERVFAIL => 'DNS server failure',
+ REFUSED => 'DNS query refused',
+ FORMERR => 'format error',
+ NOTIMP => 'not implemented');
+ if(defined($dnsrespcodes{$response})) {
+ return sprintf('%s (%s)',$response,$dnsrespcodes{$response});
+ } else {
+ return $response;
+ };
+};
+
+############################### print_dns_result ###############################
+# print and log result of DNS query
+# IN : $domain: domain the DNS was queried for
+# $type : record type (MX, A, ...)
+# $count : number of records found
+# $error : DNS response code
+# \$log : reference to the log (to be printed out via -l)
+# OUT: ---
+# \$log will be changed
+sub print_dns_result {
+ my ($domain,$type,$count,$error,$logr) = @_;
+ if (defined($count)) {
+ printf(" %d %s record(s) found for %s\n",$count,$type,$domain) if !($options{'q'});
+ $$logr .= sprintf("%s DNS record(s):\n",$type);
+ } else {
+ printf(" No %s records found for %s: %s\n",$type,$domain,parse_dns_reply($error)) if !($options{'q'});
+ $$logr .= sprintf("No %s records found: %s\n",$type,parse_dns_reply($error));
+ };
+ return;
};
-sub parse_reply {
- my($code,$message)=@_;
- my($reply);
- $reply = $code . ' ' . $message;
- return $reply;
+################################## try_rcpt_to #################################
+# send RCPT TO and return replies
+# IN : \$smtp : a reference to an SMTP object
+# $recipient: a mail address
+# \$log : reference to the log (to be printed out via -l)
+# OUT: $success: exit code (0 for false, 1 for true, -1 for tempfail)
+# $code : SMTP status code
+# $message: SMTP status message
+# \$log will be changed
+sub try_rcpt_to {
+ my($smtpr,$recipient,$logr)=@_;
+ $$logr .= sprintf("RCPT TO:<%s>\n",$recipient);
+ my $success;
+ $$smtpr->to($recipient);
+ if ($$smtpr->code) {
+ log_smtp_reply($logr,$$smtpr->code,$$smtpr->message);
+ $success = analyze_smtp_reply($$smtpr->code,$$smtpr->message);
+ } else {
+ $success = -1;
+ $$logr .= "---Connection failure---\n";
+ };
+ return ($success,$$smtpr->code,$$smtpr->message);
+};
+
+################################ log_smtp_reply ################################
+# log result of SMTP command
+# IN : \$log : reference to the log (to be printed out via -l)
+# $code : SMTP status code
+# @message : SMTP status message
+# OUT: ---
+# \$log will be changed
+sub log_smtp_reply {
+ my($logr,$code,@message)=@_;
+ $$logr .= sprintf('%s %s',$code,join('- ',@message));
+ return;
}
+############################### analyze_smtp_reply ##############################
+# analyze SMTP response codes and messages
+# IN : $code : SMTP status code
+# @message : SMTP status message
+# OUT: exit code (0 for false, 1 for true, -1 for tempfail)
+sub analyze_smtp_reply {
+ my($code,@message)=@_;
+ my $type = substr($code, 0, 1);
+ if ($type == 2) {
+ return 1;
+ } elsif ($type == 5) {
+ return 0;
+ } elsif ($type == 4) {
+ return -1;
+ };
+ return -1;
+}
+
+############################## connection_failed ###############################
+# print failure message and return status 1
+# IN : @message : SMTP status message
+# OUT: 1
+sub connection_failed {
+ my(@message)=@_;
+ print " ! Connection failed or other temporary failure.\n" if !($options{'q'});
+ printf(" %s\n",join(' ',@message)) if @message;
+ return 1;
+}
+
+__END__
+
+################################ Documentation #################################
+
+=head1 NAME
+
+checkmail - check deliverability of a mail address
+
+=head1 SYNOPSIS
+
+B<checkmail> [B<-Vhqlr>] [B<-m> I<host>] [-s I<sender>] [-e I<EHLO>] I<address>|B<-f> I<file>
+
+=head1 REQUIREMENTS
+
+=over 2
+
+=item -
+
+Perl 5.8 or later
+
+=item -
+
+File::Basename
+
+=item -
+
+Getopt::Std
+
+=item -
+
+Mail::Address I<(CPAN)>
+
+=item -
+
+Net::DNS I<(CPAN)>
+
+=item -
+
+Net::SMTP
+
+=back
+
+Furthermore you'll need a working DNS installation.
+
+=head1 DESCRIPTION
+
+checkmail checks the vailidity / deliverability of a mail address.
+You may submit just one address as the last argument or a file
+containing one address on each line using the B<-f> option.
+
+=head2 Configuration
+
+For the time being, all configuration is done in the script. You have
+to set the following elements of the %config hash:
+
+=over 4
+
+=item B<$config{'helo'}>
+
+The hostname to be used for I<HELO> or I<EHLO> in the SMTP dialog.
+
+=item B<$config{'from'}>
+
+The sender address to be used for I<MAIL FROM> while testing.
+May be empty ('') to set '<>' as MAIL FROM.
+
+=back
+
+You may override that configuration by using the B<-e> and B<-s>
+command line options.
+
+=head2 Usage
+
+After configuring the script you may run your first test with
+
+ checkmail user@example.org
+
+B<checkmail> will check the address for syntactic validity. If the
+address is valid, it will try to determine the mail exchanger(s) (MX)
+responsible for I<example.org> by querying the DNS for the respective
+MX records and then try to connect via SMTP (on port 25) to each of
+them in order of precedence (if necessary). It will run through the
+SMTP dialog until just before the I<DATA> stage, i.e. doing I<EHLO>,
+I<MAIL FROM> and I<RCPT TO>. If no MX is defined, B<checkmail> will
+fall back to the I<example.org> host itself, provided there is at
+least one A record defined in the DNS. CNAMEs will be accepted and
+resolved here. If there are neither MX nor A records for
+I<example.org>, mail is not deliverable and B<checkmail> will fail
+accordingly. If no host can be reached, B<checkmail> will fail,
+too. Finally B<checkmail> will fail if mail to the given recipient
+is not accepted by the respective host.
+
+If B<checkmail> fails, you'll not be able to deliver mail to that
+address - at least not using the configured sender address and from
+the host you're testing from. However, the opposite is not true: a
+mail you send may still not be delivered even if a test via
+B<checkmail> succeeds. The receiving entity may reject your mail after
+the I<DATA> stage, due to content checking or without any special
+reason, or it may even drop, filter or bounce your mail after finally
+accepting it. There is no way to be sure a mail will be accepted short
+of sending a real mail to the address in question.
+
+You may, however, try to detect hosts that will happily accept any and
+all recipient in the SMTP dialog and just reject your mail later on,
+for example to defeat exactly the kind of check you try to do.
+B<checkmail> will do that by submitting a recipient address that is
+known to be invalid; if that address is accepted, too, you'll know
+that you can't reliably check the validity of any address on that
+host. You can force that check by using the B<-r> option.
+
+If you don't want to see just the results of your test, you can get a
+B<complete log> of the SMTP dialog by using the B<-l> option. That may be
+helpful to test for temporary failure conditions.
+
+On the other hand you may use the B<-q> option to suppress all output;
+B<checkmail> will then terminate with one of the following B<exit
+status>:
+
+=over 4
+
+=item B<0>
+
+address(es) seem/seems to be valid
+
+=item B<1>
+
+temporary error (connection failure or temporary failure)
+
+=item B<2>
+
+address is invalid
+
+=item B<3>
+
+address cannot reliably be checked (test using B<-r> failed)
+
+=back
+
+You can do B<batch processing> using B<-f> and submitting a file with
+one address on each line. In that case the exit status is set to the
+highest value generated by testing all addresses, i.e. it is set to
+B<0> if and only if no adress failed, but to B<2> if even one address
+failed and to B<3> if even one addresses couldn't reliably be checked.
+
+And finally you can B<suppress DNS lookups> for MX and A records and
+just force B<checkmail> to connect to a particular host using the
+B<-m> option.
+
+B<Please note:> You shouldn't try to validate addresses while working
+from a dial-up or blacklisted host. If in doubt, use the B<-l> option
+to have a closer look on the SMTP dialog yourself.
+
+B<Please note:> To avoid shell expansion on addresses you submit to
+B<checkmail>, use B<batch processing>.
+
+=head1 OPTIONS
+
+=over 3
+
+=item B<-V> (version)
+
+Print out version and copyright information on B<checkmail> and exit.
+
+=item B<-h> (help)
+
+Print this man page and exit.
+
+=item B<-q> (quit)
+
+Suppress output and just terminate with a specific exit status.
+
+=item B<-l> (log)
+
+Log and print out the whole SMTP dialog.
+
+=item B<-r> (random address)
+
+Also try a reliably invalid address to catch hosts that try undermine
+address verification.
+
+=item B<-m> I<host> (MX to use)
+
+Force a connection to I<host> to check deliverability to that
+particular host irrespective of DNS entries. For example:
+
+ checkmail -m test.host.example user@domain.example
+
+=item B<-s> I<sender> (value for MAIL FROM)
+
+Override configuration and use I<sender> for MAIL FROM.
+
+=item B<-e> I<EHLO> (value for EHLO)
+
+Override configuration and use I<EHLO> for EHLO.
+
+=item B<-f> I<file> (file)
+
+Process all addresses from I<file> (one on each line).
+
+=back
+
+=head1 INSTALLATION
+
+Just copy checkmail to some directory and get started.
+
+You can run your first test with
+
+ checkmail user@example.org
+
+=head1 ENVIRONMENT
+
+See documentation of I<Net::DNS::Resolver>.
+
+=head1 FILES
+
+=over 4
+
+=item F<checkmail.pl>
+
+The script itself.
+
+=back
+
+=head1 BUGS
+
+Please report any bugs or feature request to the author or use the
+bug tracker at L<http://bugs.th-h.de/>!
+
+=head1 SEE ALSO
+
+L<http://th-h.de/download/scripts.php> will have the current
+version of this program.
+
+This program is maintained using the Git version control system. You
+may clone L<git://code.th-h.de/mail/checkmail.git> to check out the
+current development tree or browse it on the web via
+L<http://code.th-h.de/?p=mail/checkmail.git>.
+
+=head1 AUTHOR
+
+Thomas Hochstein <thh@thh.name>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright (c) 2002-2016 Thomas Hochstein <thh@thh.name>
+
+This program is free software; you may redistribute it and/or modify it
+under the same terms as Perl itself.
+
+=cut