| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | package Net::SMTP::Verify; | 
| 2 |  |  |  |  |  |  |  | 
| 3 | 3 |  |  | 3 |  | 12224 | use Moose; | 
|  | 3 |  |  |  |  | 950652 |  | 
|  | 3 |  |  |  |  | 65 |  | 
| 4 |  |  |  |  |  |  |  | 
| 5 |  |  |  |  |  |  | our $VERSION = '1.03'; # VERSION | 
| 6 |  |  |  |  |  |  | # ABSTRACT: verify SMTP recipient addresses | 
| 7 |  |  |  |  |  |  |  | 
| 8 | 3 |  |  | 3 |  | 27667 | use Net::SMTP::Verify::ResultSet; | 
|  | 3 |  |  |  |  | 11 |  | 
|  | 3 |  |  |  |  | 112 |  | 
| 9 |  |  |  |  |  |  |  | 
| 10 | 3 |  |  | 3 |  | 1103 | use Net::DNS::Resolver; | 
|  | 3 |  |  |  |  | 85086 |  | 
|  | 3 |  |  |  |  | 106 |  | 
| 11 | 3 |  |  | 3 |  | 2977 | use Net::SMTP; | 
|  | 3 |  |  |  |  | 257375 |  | 
|  | 3 |  |  |  |  | 200 |  | 
| 12 | 3 |  |  | 3 |  | 24 | use Net::Cmd qw( CMD_OK ); | 
|  | 3 |  |  |  |  | 7 |  | 
|  | 3 |  |  |  |  | 178 |  | 
| 13 | 3 |  |  | 3 |  | 1535 | use Sys::Hostname; | 
|  | 3 |  |  |  |  | 2413 |  | 
|  | 3 |  |  |  |  | 179 |  | 
| 14 | 3 |  |  | 3 |  | 2507 | use Digest::SHA qw(sha224_hex); | 
|  | 3 |  |  |  |  | 8767 |  | 
|  | 3 |  |  |  |  | 6048 |  | 
| 15 |  |  |  |  |  |  |  | 
| 16 |  |  |  |  |  |  |  | 
| 17 |  |  |  |  |  |  | has 'host' => ( is => 'rw', isa => 'Maybe[Str]' ); | 
| 18 |  |  |  |  |  |  | has 'port' => ( is => 'rw', isa => 'Int', default => 25 ); | 
| 19 |  |  |  |  |  |  |  | 
| 20 |  |  |  |  |  |  | has 'helo_name' => ( | 
| 21 |  |  |  |  |  |  | is => 'rw', isa => 'Str', lazy => 1, | 
| 22 |  |  |  |  |  |  | default => sub { Sys::Hostname::hostname }, | 
| 23 |  |  |  |  |  |  | ); | 
| 24 |  |  |  |  |  |  | has 'timeout' => ( is => 'rw', isa => 'Int', default => 30 ); | 
| 25 |  |  |  |  |  |  |  | 
| 26 |  |  |  |  |  |  | has 'resolver' => ( | 
| 27 |  |  |  |  |  |  | is => 'rw', isa => 'Net::DNS::Resolver', lazy => 1, | 
| 28 |  |  |  |  |  |  | default => sub { | 
| 29 |  |  |  |  |  |  | Net::DNS::Resolver->new( | 
| 30 |  |  |  |  |  |  | dnssec => 1, | 
| 31 |  |  |  |  |  |  | adflag => 1, | 
| 32 |  |  |  |  |  |  | ); | 
| 33 |  |  |  |  |  |  | }, | 
| 34 |  |  |  |  |  |  | ); | 
| 35 |  |  |  |  |  |  |  | 
| 36 |  |  |  |  |  |  | has 'tlsa' => ( is => 'rw', isa => 'Bool', default => 0 ); | 
| 37 |  |  |  |  |  |  | has 'openpgpkey' => ( is => 'rw', isa => 'Bool', default => 0 ); | 
| 38 |  |  |  |  |  |  |  | 
| 39 |  |  |  |  |  |  | has 'logging_callback' => ( | 
| 40 |  |  |  |  |  |  | is => 'rw', isa => 'CodeRef', lazy => 1, | 
| 41 |  |  |  |  |  |  | traits => [ 'Code' ], | 
| 42 |  |  |  |  |  |  | handles => { | 
| 43 |  |  |  |  |  |  | log => 'execute', | 
| 44 |  |  |  |  |  |  | }, | 
| 45 |  |  |  |  |  |  | default => sub { sub {} }, | 
| 46 |  |  |  |  |  |  | ); | 
| 47 |  |  |  |  |  |  |  | 
| 48 |  |  |  |  |  |  | has 'debug' => ( is => 'ro', isa => 'Bool', default => 0 ); | 
| 49 |  |  |  |  |  |  |  | 
| 50 |  |  |  |  |  |  | sub BUILD { | 
| 51 | 3 |  |  | 3 | 0 | 13913 | my $self = shift; | 
| 52 | 3 | 50 |  |  |  | 110 | if( $self->debug ) { | 
| 53 |  |  |  |  |  |  | $self->logging_callback( sub { | 
| 54 | 0 |  |  | 0 |  | 0 | print STDERR shift."\n"; | 
| 55 | 0 |  |  |  |  | 0 | } ); | 
| 56 |  |  |  |  |  |  | } | 
| 57 |  |  |  |  |  |  | } | 
| 58 |  |  |  |  |  |  |  | 
| 59 |  |  |  |  |  |  | has '_known_hosts' => ( | 
| 60 |  |  |  |  |  |  | is => 'ro', isa => 'ArrayRef', lazy => 1, | 
| 61 |  |  |  |  |  |  | default => sub { [] }, | 
| 62 |  |  |  |  |  |  | traits => [ 'Array' ], | 
| 63 |  |  |  |  |  |  | handles => { | 
| 64 |  |  |  |  |  |  | '_reset_known_hosts' => 'clear', | 
| 65 |  |  |  |  |  |  | '_add_known_host' => 'push', | 
| 66 |  |  |  |  |  |  | } | 
| 67 |  |  |  |  |  |  | ); | 
| 68 |  |  |  |  |  |  |  | 
| 69 |  |  |  |  |  |  | sub _is_known_host { | 
| 70 | 1 |  |  | 1 |  | 84 | my ( $self, $host ) = @_; | 
| 71 | 1 | 50 |  |  |  | 3 | if( grep { $_ eq $host } @{$self->_known_hosts} ) { | 
|  | 0 |  |  |  |  | 0 |  | 
|  | 1 |  |  |  |  | 44 |  | 
| 72 | 0 |  |  |  |  | 0 | return 1; | 
| 73 |  |  |  |  |  |  | } | 
| 74 | 1 |  |  |  |  | 3 | return 0; | 
| 75 |  |  |  |  |  |  | } | 
| 76 |  |  |  |  |  |  |  | 
| 77 |  |  |  |  |  |  |  | 
| 78 |  |  |  |  |  |  | sub resolve { | 
| 79 | 5 |  |  | 5 | 1 | 527 | my ( $self, $domain ) = @_; | 
| 80 |  |  |  |  |  |  |  | 
| 81 | 5 | 100 |  |  |  | 193 | if( defined $self->host ) { | 
| 82 | 4 |  |  |  |  | 128 | return $self->host; | 
| 83 |  |  |  |  |  |  | } else { | 
| 84 | 1 |  |  |  |  | 48 | $self->log('looking up MX for '.$domain.'...'); | 
| 85 | 1 |  |  |  |  | 36 | my $reply = $self->resolver->query( $domain, 'MX' ); | 
| 86 | 1 | 50 |  |  |  | 9574 | if( $reply->answer ) { | 
| 87 | 1 |  |  |  |  | 13 | my @mx = grep { $_->type eq 'MX' } $reply->answer; | 
|  | 1 |  |  |  |  | 11 |  | 
| 88 | 1 |  |  |  |  | 15 | @mx = sort { $a->preference <=> $b->preference } @mx; | 
|  | 0 |  |  |  |  | 0 |  | 
| 89 | 1 |  |  |  |  | 3 | my @known_hosts = grep { $self->_is_known_host($_->exchange) } @mx; | 
|  | 1 |  |  |  |  | 5 |  | 
| 90 |  |  |  |  |  |  |  | 
| 91 | 1 |  |  |  |  | 2 | my $ex; | 
| 92 | 1 | 50 |  |  |  | 5 | if( @known_hosts ) { | 
| 93 | 0 |  |  |  |  | 0 | $ex = $known_hosts[0]->exchange; | 
| 94 |  |  |  |  |  |  | } else { | 
| 95 | 1 |  |  |  |  | 4 | $ex = $mx[0]->exchange; | 
| 96 | 1 |  |  |  |  | 61 | $self->_add_known_host( $ex ); | 
| 97 |  |  |  |  |  |  | } | 
| 98 | 1 | 50 |  |  |  | 49 | $self->log('found '.scalar(@mx).' records. using: '.$ex. | 
| 99 |  |  |  |  |  |  | ( @known_hosts ? ' (reuse)' : '') ); | 
| 100 | 1 |  |  |  |  | 19 | return $ex; | 
| 101 |  |  |  |  |  |  | } | 
| 102 | 0 |  |  |  |  | 0 | $self->log('looking up AAAA,A for '.$domain.'...'); | 
| 103 | 0 |  |  |  |  | 0 | $reply = $self->resolver->query( $domain, 'AAAA', 'A' ); | 
| 104 | 0 | 0 |  |  |  | 0 | if( my @rr = $reply->answer ) { | 
| 105 | 0 |  |  |  |  | 0 | $self->log('found '.scalar(@rr).' address records'); | 
| 106 | 0 |  |  |  |  | 0 | return $domain; | 
| 107 |  |  |  |  |  |  | } | 
| 108 | 0 |  |  |  |  | 0 | $self->log('unable to resolve domain '.$domain); | 
| 109 | 0 |  |  |  |  | 0 | return; # lookup failed | 
| 110 |  |  |  |  |  |  | } | 
| 111 |  |  |  |  |  |  |  | 
| 112 | 0 |  |  |  |  | 0 | die('unknown mode: '.$self->mode); | 
| 113 | 0 |  |  |  |  | 0 | return; | 
| 114 |  |  |  |  |  |  | } | 
| 115 |  |  |  |  |  |  |  | 
| 116 |  |  |  |  |  |  |  | 
| 117 |  |  |  |  |  |  | sub check_tlsa { | 
| 118 | 1 |  |  | 1 | 1 | 537 | my ( $self, $host, $port ) = @_; | 
| 119 | 1 | 50 |  |  |  | 8 | if( ! defined $port ) { | 
| 120 | 0 |  |  |  |  | 0 | $port = 25; | 
| 121 |  |  |  |  |  |  | } | 
| 122 | 1 |  |  |  |  | 6 | my $tlsa_name = '_'.$port.'._tcp.'.$host; | 
| 123 | 1 |  |  |  |  | 50 | $self->log('looking up TLSA for '.$tlsa_name.'...'); | 
| 124 | 1 |  |  |  |  | 34 | my $reply = $self->resolver->send( $tlsa_name, 'TLSA' ); | 
| 125 |  |  |  |  |  |  |  | 
| 126 | 1 | 50 |  |  |  | 86040 | if( ! $reply->header->ad ) { | 
| 127 | 0 |  |  |  |  | 0 | $self->log('no adflag set in response'); | 
| 128 | 0 |  |  |  |  | 0 | return 0; | 
| 129 |  |  |  |  |  |  | } | 
| 130 |  |  |  |  |  |  |  | 
| 131 | 1 | 50 |  |  |  | 23 | if( ! $reply->answer ) { | 
| 132 | 0 |  |  |  |  | 0 | $self->log('no TLSA record published'); | 
| 133 | 0 |  |  |  |  | 0 | return 0; | 
| 134 |  |  |  |  |  |  | } | 
| 135 |  |  |  |  |  |  |  | 
| 136 | 1 |  |  |  |  | 48 | return 1; | 
| 137 |  |  |  |  |  |  | } | 
| 138 |  |  |  |  |  |  |  | 
| 139 |  |  |  |  |  |  | sub check_openpgpkey { | 
| 140 | 0 |  |  | 0 | 0 | 0 | my ( $self, $rs, @rcpts ) = @_; | 
| 141 |  |  |  |  |  |  |  | 
| 142 | 0 |  |  |  |  | 0 | foreach my $rcpt ( @rcpts ) { | 
| 143 | 0 |  |  |  |  | 0 | my ( $local, $domain ) = split('@', $rcpt, 2); | 
| 144 | 0 |  |  |  |  | 0 | my $name = join('.', sha224_hex($local), '_openpgpkey', $domain); | 
| 145 | 0 |  |  |  |  | 0 | $self->log('looking up OPENPGPKEY: '.$name.'...'); | 
| 146 | 0 |  |  |  |  | 0 | my $reply = $self->resolver->send( $name, 'TYPE61' ); | 
| 147 | 0 | 0 |  |  |  | 0 | if( ! $reply->header->ad ) { | 
|  |  | 0 |  |  |  |  |  | 
| 148 | 0 |  |  |  |  | 0 | $self->log('no adflag set in response'); | 
| 149 | 0 |  |  |  |  | 0 | $rs->set( $rcpt, 'has_openpgpkey', 0 ); | 
| 150 |  |  |  |  |  |  | } elsif( ! $reply->answer ) { | 
| 151 | 0 |  |  |  |  | 0 | $self->log('no OPENPGPKEY record found'); | 
| 152 | 0 |  |  |  |  | 0 | $rs->set( $rcpt, 'has_openpgpkey', 0 ); | 
| 153 |  |  |  |  |  |  | } else { | 
| 154 | 0 |  |  |  |  | 0 | $self->log('OPENPGPKEY record found'); | 
| 155 | 0 |  |  |  |  | 0 | $rs->set( $rcpt, 'has_openpgpkey', 1 ); | 
| 156 |  |  |  |  |  |  | } | 
| 157 |  |  |  |  |  |  | } | 
| 158 |  |  |  |  |  |  |  | 
| 159 | 0 |  |  |  |  | 0 | return; | 
| 160 |  |  |  |  |  |  | } | 
| 161 |  |  |  |  |  |  |  | 
| 162 |  |  |  |  |  |  | sub check_smtp { | 
| 163 | 4 |  |  | 4 | 0 | 11 | my ( $self, $rs, $host, $size, $sender, @rcpts ) = @_; | 
| 164 |  |  |  |  |  |  |  | 
| 165 | 4 |  |  |  |  | 190 | $self->log('connecting to '.$host.'...'); | 
| 166 | 4 |  |  |  |  | 1029 | my $smtp = Net::SMTP->new( $host, | 
| 167 |  |  |  |  |  |  | Port => $self->port, | 
| 168 |  |  |  |  |  |  | Hello => $self->helo_name, | 
| 169 |  |  |  |  |  |  | Timeout => $self->timeout, | 
| 170 |  |  |  |  |  |  | ); | 
| 171 | 4 | 50 |  |  |  | 257811 | if( ! defined $smtp ) { | 
| 172 | 0 |  |  |  |  | 0 | $self->log('connection failed: '.$@); | 
| 173 | 0 |  |  |  |  | 0 | $rs->set( \@rcpts, 'error', 'connection failed: '.$@ ); | 
| 174 | 0 |  |  |  |  | 0 | return; | 
| 175 |  |  |  |  |  |  | } | 
| 176 |  |  |  |  |  |  |  | 
| 177 | 4 | 50 |  |  |  | 32 | $rs->set( \@rcpts, 'has_starttls', | 
| 178 |  |  |  |  |  |  | defined $smtp->supports('STARTTLS') ? 1 : 0 ); | 
| 179 |  |  |  |  |  |  |  | 
| 180 | 4 | 100 |  |  |  | 30 | if( defined $smtp->supports('PIPELINING') ) { | 
| 181 | 3 |  |  |  |  | 81 | $self->check_smtp_addresses_pipelining( $rs, $smtp, $size, $sender, @rcpts ); | 
| 182 |  |  |  |  |  |  | } else { | 
| 183 | 1 |  |  |  |  | 26 | $self->check_smtp_addresses( $rs, $smtp, $size, $sender, @rcpts ); | 
| 184 |  |  |  |  |  |  | } | 
| 185 |  |  |  |  |  |  |  | 
| 186 | 4 |  |  |  |  | 229 | $self->log('sending QUIT...'); | 
| 187 | 4 |  |  |  |  | 677 | $smtp->quit; | 
| 188 | 4 |  |  |  |  | 5758 | return; | 
| 189 |  |  |  |  |  |  | } | 
| 190 |  |  |  |  |  |  |  | 
| 191 |  |  |  |  |  |  | sub check_smtp_addresses { | 
| 192 | 1 |  |  | 1 | 0 | 6 | my ( $self, $rs, $smtp, $size, $sender, @rcpts ) = @_; | 
| 193 | 1 |  |  |  |  | 77 | $self->log('sending MAIL '.$sender.'...'); | 
| 194 | 1 | 50 | 33 |  |  | 379 | my $mail_ok = $smtp->mail( $sender, | 
| 195 |  |  |  |  |  |  | defined $size && $smtp->supports('SIZE') ? ( Size => $size ):() | 
| 196 |  |  |  |  |  |  | ); | 
| 197 | 1 |  |  |  |  | 1250 | my $msg = $smtp->message; chomp($msg); | 
|  | 1 |  |  |  |  | 22 |  | 
| 198 | 1 |  |  |  |  | 129 | $self->log('server said: '.$msg); | 
| 199 | 1 | 50 |  |  |  | 274 | if( ! $mail_ok ) { | 
| 200 | 0 |  |  |  |  | 0 | $rs->set( \@rcpts, 'smtp_message', $msg ); | 
| 201 | 0 |  |  |  |  | 0 | $rs->set( \@rcpts, 'smtp_code', $smtp->code ); | 
| 202 | 0 |  |  |  |  | 0 | return; | 
| 203 |  |  |  |  |  |  | } | 
| 204 |  |  |  |  |  |  |  | 
| 205 | 1 |  |  |  |  | 6 | foreach my $rcpt ( @rcpts ) { | 
| 206 | 3 |  |  |  |  | 198 | $self->log('sending RCPT '.$rcpt.'...'); | 
| 207 | 3 |  |  |  |  | 543 | my $rcpt_ok = $smtp->recipient( $rcpt ); | 
| 208 | 3 |  |  |  |  | 2043 | my $msg = $smtp->message; chomp( $msg ); | 
|  | 3 |  |  |  |  | 35 |  | 
| 209 | 3 |  |  |  |  | 184 | $self->log( 'server said: '.$msg ); | 
| 210 | 3 |  |  |  |  | 508 | $rs->set( $rcpt, 'smtp_message', $msg ); | 
| 211 | 3 |  |  |  |  | 80 | $rs->set( $rcpt, 'smtp_code', $smtp->code ); | 
| 212 |  |  |  |  |  |  | } | 
| 213 | 1 |  |  |  |  | 4 | return; | 
| 214 |  |  |  |  |  |  | } | 
| 215 |  |  |  |  |  |  |  | 
| 216 |  |  |  |  |  |  | has 'rcpt_bulk_size' => ( is => 'ro', isa => 'Int', default => 10 ); | 
| 217 |  |  |  |  |  |  |  | 
| 218 |  |  |  |  |  |  | sub check_smtp_addresses_pipelining { | 
| 219 | 3 |  |  | 3 | 0 | 19 | my ( $self, $rs, $smtp, $size, $sender, @rcpts ) = @_; | 
| 220 | 3 |  |  |  |  | 10 | my $mail_sent = 0; | 
| 221 |  |  |  |  |  |  |  | 
| 222 | 3 |  |  |  |  | 172 | while( my @bulk_rcpts = splice(@rcpts, 0, $self->rcpt_bulk_size) ) { | 
| 223 | 3 |  |  |  |  | 202 | $self->log('sending pipelined bulk...'); | 
| 224 | 3 |  |  |  |  | 916 | my $bulk = ''; | 
| 225 | 3 | 50 |  |  |  | 15 | if( ! $mail_sent ) { | 
| 226 | 3 | 50 | 33 |  |  | 87 | $bulk .= 'MAIL FROM: <'.$sender.'>' | 
| 227 |  |  |  |  |  |  | .( defined $size && $smtp->supports('SIZE') ? ' SIZE='.$size : '' ) | 
| 228 |  |  |  |  |  |  | ."\n" | 
| 229 |  |  |  |  |  |  | } | 
| 230 |  |  |  |  |  |  | $bulk .= join("\n", | 
| 231 | 3 |  |  |  |  | 75 | map { 'RCPT TO: <'.$_.'>' } @bulk_rcpts, | 
|  | 9 |  |  |  |  | 52 |  | 
| 232 |  |  |  |  |  |  | )."\n"; | 
| 233 |  |  |  |  |  |  |  | 
| 234 | 3 |  |  |  |  | 99 | $smtp->datasend( $bulk ); | 
| 235 |  |  |  |  |  |  |  | 
| 236 | 3 | 50 |  |  |  | 3906 | if( ! $mail_sent ) { | 
| 237 | 3 |  |  |  |  | 23 | my $resp = $smtp->response; | 
| 238 | 3 |  |  |  |  | 546 | my $msg = $smtp->message; chomp( $msg ); | 
|  | 3 |  |  |  |  | 51 |  | 
| 239 | 3 |  |  |  |  | 253 | $self->log("server response to MAIL: ".$msg ); | 
| 240 | 3 | 100 |  |  |  | 642 | if( $resp != CMD_OK ) { | 
| 241 | 1 |  |  |  |  | 11 | $rs->set( [ @bulk_rcpts, @rcpts ], 'smtp_code', $smtp->code ); | 
| 242 | 1 |  |  |  |  | 6 | $rs->set( [ @bulk_rcpts, @rcpts ], 'smtp_message', $msg ); | 
| 243 | 1 |  |  |  |  | 4 | return; | 
| 244 |  |  |  |  |  |  | } | 
| 245 | 2 |  |  |  |  | 5 | $mail_sent = 1; | 
| 246 |  |  |  |  |  |  | } | 
| 247 |  |  |  |  |  |  |  | 
| 248 | 2 |  |  |  |  | 22 | foreach my $rcpt ( @bulk_rcpts ) { | 
| 249 | 6 |  |  |  |  | 33 | $smtp->response; | 
| 250 | 6 |  |  |  |  | 75130 | my $msg = $smtp->message; chomp( $msg ); | 
|  | 6 |  |  |  |  | 96 |  | 
| 251 | 6 |  |  |  |  | 486 | $self->log("server response to RCPT $rcpt: ".$msg ); | 
| 252 | 6 |  |  |  |  | 1432 | $rs->set( $rcpt, 'smtp_code', $smtp->code ); | 
| 253 | 6 |  |  |  |  | 39 | $rs->set( $rcpt, 'smtp_message', $msg ); | 
| 254 |  |  |  |  |  |  | } | 
| 255 |  |  |  |  |  |  | } | 
| 256 | 2 |  |  |  |  | 11 | return; | 
| 257 |  |  |  |  |  |  | } | 
| 258 |  |  |  |  |  |  |  | 
| 259 |  |  |  |  |  |  |  | 
| 260 |  |  |  |  |  |  | sub check { | 
| 261 | 4 |  |  | 4 | 1 | 852 | my ( $self, $size, $sender, @rcpts ) = @_; | 
| 262 | 4 |  |  |  |  | 68 | my $rs = Net::SMTP::Verify::ResultSet->new; | 
| 263 |  |  |  |  |  |  |  | 
| 264 | 4 |  |  |  |  | 1425 | my $by_domain = {}; | 
| 265 | 4 |  |  |  |  | 22 | foreach my $rcpt ( @rcpts ) { | 
| 266 | 12 |  |  |  |  | 61 | my ( $user, $domain ) = split('@', $rcpt, 2); | 
| 267 | 12 | 100 |  |  |  | 70 | if( ! defined $by_domain->{$domain} ) { | 
| 268 | 4 |  |  |  |  | 19 | $by_domain->{$domain} = []; | 
| 269 |  |  |  |  |  |  | } | 
| 270 | 12 |  |  |  |  | 16 | push( @{$by_domain->{$domain}}, $rcpt ); | 
|  | 12 |  |  |  |  | 45 |  | 
| 271 |  |  |  |  |  |  | } | 
| 272 |  |  |  |  |  |  |  | 
| 273 | 4 |  |  |  |  | 7 | my $by_host = {}; | 
| 274 | 4 |  |  |  |  | 206 | $self->_reset_known_hosts; | 
| 275 | 4 |  |  |  |  | 17 | foreach my $domain ( keys %$by_domain ) { | 
| 276 | 4 |  |  |  |  | 43 | my $host = $self->resolve( $domain ); | 
| 277 | 4 | 50 |  |  |  | 21 | if( ! defined $host ) { | 
| 278 | 0 |  |  |  |  | 0 | $rs->set( $by_domain->{$domain}, | 
| 279 |  |  |  |  |  |  | 'error', 'unable to lookup '.$domain ); | 
| 280 | 0 |  |  |  |  | 0 | return; | 
| 281 |  |  |  |  |  |  | } | 
| 282 | 4 | 50 |  |  |  | 16 | if( ! defined $by_host->{$host} ) { | 
| 283 | 4 |  |  |  |  | 11 | $by_host->{$host} = []; | 
| 284 |  |  |  |  |  |  | } | 
| 285 | 4 |  |  |  |  | 8 | push( @{$by_host->{$host}}, @{$by_domain->{$domain}} ); | 
|  | 4 |  |  |  |  | 11 |  | 
|  | 4 |  |  |  |  | 36 |  | 
| 286 |  |  |  |  |  |  | } | 
| 287 |  |  |  |  |  |  |  | 
| 288 | 4 |  |  |  |  | 13 | foreach my $host ( keys %$by_host ) { | 
| 289 | 4 | 50 |  |  |  | 161 | if( $self->tlsa ) { | 
| 290 | 0 |  |  |  |  | 0 | $rs->set( $by_host->{$host}, | 
| 291 |  |  |  |  |  |  | 'has_tlsa', $self->check_tlsa( $host ) ); | 
| 292 |  |  |  |  |  |  | } | 
| 293 | 4 |  |  |  |  | 10 | $self->check_smtp( $rs, $host, $size, $sender, @{$by_host->{$host}} ); | 
|  | 4 |  |  |  |  | 34 |  | 
| 294 |  |  |  |  |  |  | } | 
| 295 |  |  |  |  |  |  |  | 
| 296 | 4 | 50 |  |  |  | 255 | if( $self->openpgpkey ) { | 
| 297 | 0 |  |  |  |  | 0 | $self->check_openpgpkey( $rs, @rcpts ); | 
| 298 |  |  |  |  |  |  | } | 
| 299 |  |  |  |  |  |  |  | 
| 300 | 4 |  |  |  |  | 78 | return $rs; | 
| 301 |  |  |  |  |  |  | } | 
| 302 |  |  |  |  |  |  |  | 
| 303 |  |  |  |  |  |  | 1; | 
| 304 |  |  |  |  |  |  |  | 
| 305 |  |  |  |  |  |  | __END__ | 
| 306 |  |  |  |  |  |  |  | 
| 307 |  |  |  |  |  |  | =pod | 
| 308 |  |  |  |  |  |  |  | 
| 309 |  |  |  |  |  |  | =encoding UTF-8 | 
| 310 |  |  |  |  |  |  |  | 
| 311 |  |  |  |  |  |  | =head1 NAME | 
| 312 |  |  |  |  |  |  |  | 
| 313 |  |  |  |  |  |  | Net::SMTP::Verify - verify SMTP recipient addresses | 
| 314 |  |  |  |  |  |  |  | 
| 315 |  |  |  |  |  |  | =head1 VERSION | 
| 316 |  |  |  |  |  |  |  | 
| 317 |  |  |  |  |  |  | version 1.03 | 
| 318 |  |  |  |  |  |  |  | 
| 319 |  |  |  |  |  |  | =head1 SYNOPSIS | 
| 320 |  |  |  |  |  |  |  | 
| 321 |  |  |  |  |  |  | use Net::SMTP::Verify; | 
| 322 |  |  |  |  |  |  |  | 
| 323 |  |  |  |  |  |  | my $v = Net::SMTP::Verify->new; | 
| 324 |  |  |  |  |  |  | my $resultset = $v->check( | 
| 325 |  |  |  |  |  |  | 100000, # size | 
| 326 |  |  |  |  |  |  | 'karl@senderdomain.de', # sender | 
| 327 |  |  |  |  |  |  | 'rcpt1@rcptdomain.de', # 1 or more recipients... | 
| 328 |  |  |  |  |  |  | 'rcpt2@rcptdomain.de', | 
| 329 |  |  |  |  |  |  | 'rcpt3@rcptdomain.de', | 
| 330 |  |  |  |  |  |  | ); | 
| 331 |  |  |  |  |  |  |  | 
| 332 |  |  |  |  |  |  | # check overall status | 
| 333 |  |  |  |  |  |  | $resultset->is_all_success; | 
| 334 |  |  |  |  |  |  |  | 
| 335 |  |  |  |  |  |  | # check a single result | 
| 336 |  |  |  |  |  |  | $resultset->rcpt('rcpt1@rcptdomain.de')->is_success; | 
| 337 |  |  |  |  |  |  | $resultset->rcpt('rcpt1@rcptdomain.de')->smtp_code; | 
| 338 |  |  |  |  |  |  | $resultset->rcpt('rcpt1@rcptdomain.de')->smtp_message; | 
| 339 |  |  |  |  |  |  | $resultset->rcpt('rcpt1@rcptdomain.de')->has_starttls; | 
| 340 |  |  |  |  |  |  | $resultset->rcpt('rcpt1@rcptdomain.de')->has_tlsa; | 
| 341 |  |  |  |  |  |  |  | 
| 342 |  |  |  |  |  |  | # more ways to retrieve results by status... | 
| 343 |  |  |  |  |  |  | $resultset->successfull_rcpts; | 
| 344 |  |  |  |  |  |  | $resultset->error_rcpts; | 
| 345 |  |  |  |  |  |  | $resultset->temp_error_rcpts; | 
| 346 |  |  |  |  |  |  | $resultset->perm_error_rcpts; | 
| 347 |  |  |  |  |  |  |  | 
| 348 |  |  |  |  |  |  | =head1 DESCRIPTION | 
| 349 |  |  |  |  |  |  |  | 
| 350 |  |  |  |  |  |  | This class implements checks for verifying SMTP addresses. | 
| 351 |  |  |  |  |  |  |  | 
| 352 |  |  |  |  |  |  | It implements the following checks: | 
| 353 |  |  |  |  |  |  |  | 
| 354 |  |  |  |  |  |  | =over | 
| 355 |  |  |  |  |  |  |  | 
| 356 |  |  |  |  |  |  | =item check addresses with SMTP MAIL FROM and RCPT TO commands | 
| 357 |  |  |  |  |  |  |  | 
| 358 |  |  |  |  |  |  | Check if the MX would accept mail for test addresses. | 
| 359 |  |  |  |  |  |  |  | 
| 360 |  |  |  |  |  |  | =item check of message size | 
| 361 |  |  |  |  |  |  |  | 
| 362 |  |  |  |  |  |  | If the mail exchanger (MX) supports the SIZE extension and a size is given the | 
| 363 |  |  |  |  |  |  | module will pass the message size with the MAIL FROM command. | 
| 364 |  |  |  |  |  |  |  | 
| 365 |  |  |  |  |  |  | This will check if the message would exceed message size limits or recipients | 
| 366 |  |  |  |  |  |  | quotas on the target MX. | 
| 367 |  |  |  |  |  |  |  | 
| 368 |  |  |  |  |  |  | =item check if MX could handle TLS connections | 
| 369 |  |  |  |  |  |  |  | 
| 370 |  |  |  |  |  |  | It will check if the STARTTLS extension required to enstablish encrypted TLS | 
| 371 |  |  |  |  |  |  | connections is supported by the target MX. | 
| 372 |  |  |  |  |  |  |  | 
| 373 |  |  |  |  |  |  | =item check if TLSA record is available | 
| 374 |  |  |  |  |  |  |  | 
| 375 |  |  |  |  |  |  | The module could check if a TLSA record has been published for the target MX | 
| 376 |  |  |  |  |  |  | server. | 
| 377 |  |  |  |  |  |  |  | 
| 378 |  |  |  |  |  |  | If such a record has been published the target MX SSL certificate could be | 
| 379 |  |  |  |  |  |  | verified with DANE. | 
| 380 |  |  |  |  |  |  |  | 
| 381 |  |  |  |  |  |  | =back | 
| 382 |  |  |  |  |  |  |  | 
| 383 |  |  |  |  |  |  | =head1 ATTRIBUTES | 
| 384 |  |  |  |  |  |  |  | 
| 385 |  |  |  |  |  |  | =head2 host (default: undef) | 
| 386 |  |  |  |  |  |  |  | 
| 387 |  |  |  |  |  |  | Query this smtp server instead of the MX records. | 
| 388 |  |  |  |  |  |  |  | 
| 389 |  |  |  |  |  |  | =head2 port (default: 25) | 
| 390 |  |  |  |  |  |  |  | 
| 391 |  |  |  |  |  |  | Use a different port. | 
| 392 |  |  |  |  |  |  |  | 
| 393 |  |  |  |  |  |  | =head2 helo_name (default: hostname() ) | 
| 394 |  |  |  |  |  |  |  | 
| 395 |  |  |  |  |  |  | Use a helo_name other than the hostname of the system. | 
| 396 |  |  |  |  |  |  |  | 
| 397 |  |  |  |  |  |  | =head2 timeout (default: 30) | 
| 398 |  |  |  |  |  |  |  | 
| 399 |  |  |  |  |  |  | Use this timeout for the SMTP connection. | 
| 400 |  |  |  |  |  |  |  | 
| 401 |  |  |  |  |  |  | =head2 resolver (default: system resolver) | 
| 402 |  |  |  |  |  |  |  | 
| 403 |  |  |  |  |  |  | Use a custom Net::DNS::Resolver object. | 
| 404 |  |  |  |  |  |  |  | 
| 405 |  |  |  |  |  |  | The default is: | 
| 406 |  |  |  |  |  |  |  | 
| 407 |  |  |  |  |  |  | Net::DNS::Resolver->new( | 
| 408 |  |  |  |  |  |  | dnssec => 1, | 
| 409 |  |  |  |  |  |  | adflag => 1, | 
| 410 |  |  |  |  |  |  | ); | 
| 411 |  |  |  |  |  |  |  | 
| 412 |  |  |  |  |  |  | The dnssec and adflag is required for the TLSA check. | 
| 413 |  |  |  |  |  |  |  | 
| 414 |  |  |  |  |  |  | =head2 tlsa (default: 0) | 
| 415 |  |  |  |  |  |  |  | 
| 416 |  |  |  |  |  |  | Set to 1 to activate TLSA lookup. | 
| 417 |  |  |  |  |  |  |  | 
| 418 |  |  |  |  |  |  | =head2 openpgpkey (default: 0) | 
| 419 |  |  |  |  |  |  |  | 
| 420 |  |  |  |  |  |  | Set to 1 to activate OPENPGPKEY lookup. | 
| 421 |  |  |  |  |  |  |  | 
| 422 |  |  |  |  |  |  | =head2 logging_callback (default: sub {}) | 
| 423 |  |  |  |  |  |  |  | 
| 424 |  |  |  |  |  |  | Set a callback to retrieve log messages. | 
| 425 |  |  |  |  |  |  |  | 
| 426 |  |  |  |  |  |  | =head2 debug (default: 0) | 
| 427 |  |  |  |  |  |  |  | 
| 428 |  |  |  |  |  |  | If set to 1 it will set a logging_callback method to output | 
| 429 |  |  |  |  |  |  | logs to STDERR. | 
| 430 |  |  |  |  |  |  |  | 
| 431 |  |  |  |  |  |  | =head1 METHODS | 
| 432 |  |  |  |  |  |  |  | 
| 433 |  |  |  |  |  |  | =head2 resolve( $domain ) | 
| 434 |  |  |  |  |  |  |  | 
| 435 |  |  |  |  |  |  | Tries to resolve a MX to an hostname. | 
| 436 |  |  |  |  |  |  |  | 
| 437 |  |  |  |  |  |  | It will choose the first record with the highest priority listed as MX. | 
| 438 |  |  |  |  |  |  |  | 
| 439 |  |  |  |  |  |  | When a host is MX for multiple domains it will try to reuse the same | 
| 440 |  |  |  |  |  |  | host for checks. | 
| 441 |  |  |  |  |  |  |  | 
| 442 |  |  |  |  |  |  | =head2 check_tlsa( $host, $port ) | 
| 443 |  |  |  |  |  |  |  | 
| 444 |  |  |  |  |  |  | Check if a TLSA record is available. | 
| 445 |  |  |  |  |  |  |  | 
| 446 |  |  |  |  |  |  | =head2 check( $size, $sender, $rcpt1, $rcpts...) | 
| 447 |  |  |  |  |  |  |  | 
| 448 |  |  |  |  |  |  | Performs check and returns a Net::SMTP::Verify::ResultSet. | 
| 449 |  |  |  |  |  |  |  | 
| 450 |  |  |  |  |  |  | =head1 AUTHOR | 
| 451 |  |  |  |  |  |  |  | 
| 452 |  |  |  |  |  |  | Markus Benning <ich@markusbenning.de> | 
| 453 |  |  |  |  |  |  |  | 
| 454 |  |  |  |  |  |  | =head1 COPYRIGHT AND LICENSE | 
| 455 |  |  |  |  |  |  |  | 
| 456 |  |  |  |  |  |  | This software is Copyright (c) 2015 by Markus Benning <ich@markusbenning.de>. | 
| 457 |  |  |  |  |  |  |  | 
| 458 |  |  |  |  |  |  | This is free software, licensed under: | 
| 459 |  |  |  |  |  |  |  | 
| 460 |  |  |  |  |  |  | The GNU General Public License, Version 2, June 1991 | 
| 461 |  |  |  |  |  |  |  | 
| 462 |  |  |  |  |  |  | =cut |