File Coverage

blib/lib/Mail/CheckUser.pm
Criterion Covered Total %
statement 198 231 85.7
branch 63 112 56.2
condition 20 41 48.7
subroutine 25 25 100.0
pod 2 6 33.3
total 308 415 74.2


line stmt bran cond sub pod time code
1             # Copyright (c) 1999-2003 by Ilya Martynov. All rights
2             # reserved.
3             #
4             # This program is free software; you can redistribute it and/or modify
5             # it under the same terms as Perl itself.
6              
7             package Mail::CheckUser;
8              
9 6     6   87577 use strict;
  6         10  
  6         202  
10 6     6   19 use vars qw(@ISA @EXPORT_OK %EXPORT_TAGS $VERSION);
  6         8  
  6         903  
11              
12             require Exporter;
13              
14             @ISA = qw(Exporter);
15              
16             @EXPORT_OK = qw(check_email
17             last_check
18             check_hostname
19             check_username);
20             $EXPORT_TAGS{constants} = [qw(CU_OK
21             CU_BAD_SYNTAX
22             CU_UNKNOWN_DOMAIN
23             CU_DNS_TIMEOUT
24             CU_UNKNOWN_USER
25             CU_SMTP_TIMEOUT
26             CU_SMTP_UNREACHABLE
27             CU_MAILBOX_FULL
28             CU_TRY_AGAIN)];
29             push @EXPORT_OK, @{$EXPORT_TAGS{constants}};
30              
31             $VERSION = '1.23';
32              
33 6     6   33 use Carp;
  6         9  
  6         524  
34 6     6   3528 use Net::DNS;
  6         540922  
  6         771  
35 6     6   4648 use Net::SMTP;
  6         87928  
  6         360  
36 6     6   48 use IO::Handle;
  6         9  
  6         274  
37              
38 6         1953 use vars qw($Skip_Network_Checks $Skip_SMTP_Checks
39             $Skip_SYN $Net_DNS_Resolver
40             $NXDOMAIN
41             $Timeout $Treat_Timeout_As_Fail $Debug
42             $Treat_Full_As_Fail
43             $Treat_Grey_As_Fail
44 6     6   25 $Sender_Addr $Helo_Domain $Last_Check);
  6         10  
45              
46             # if it is true Mail::CheckUser doesn't make network checks
47             $Skip_Network_Checks = 0;
48             # if it is true Mail::CheckUser doesn't try to connect to mail
49             # server to check if user is valid
50             $Skip_SMTP_Checks = 0;
51             # timeout in seconds for network checks
52             $Timeout = 60;
53             # if it is true the Net::Ping SYN/ACK check will be skipped
54             $Skip_SYN = 0;
55             # if it is true Mail::CheckUser treats timeouted checks as
56             # failed checks
57             $Treat_Timeout_As_Fail = 0;
58             # if it is true Mail::CheckUser treats mailbox full message
59             # as failed checks
60             $Treat_Full_As_Fail = 0;
61             # if it is true Mail::CheckUser treats temporary (400's)
62             # as failed checks
63             $Treat_Grey_As_Fail = 0;
64             # sender addr used in MAIL/RCPT check
65             $Sender_Addr = 'check@user.com';
66             # sender domain used in HELO SMTP command - if undef lets
67             # Net::SMTP use its default value
68             $Helo_Domain = undef;
69             # Default Net::DNS::Resolver override object
70             $Net_DNS_Resolver = undef;
71             # if true then enable debug mode
72             $Debug = 0;
73             # Wildcard gTLD always denote bogus domains
74             # (http://www.imperialviolet.org/dnsfix.html)
75             ## gTLD Wildcard IPs
76             $NXDOMAIN = {
77             # com/net
78             "64.94.110.11" => 1, # A
79              
80             # ac
81             "194.205.62.122" => 1, # A
82              
83             # cc
84             "206.253.214.102" => 1, # A
85             "snubby.enic.cc" => 1, # MX
86             "206.191.159.103" => 1, # MX
87              
88             # cx
89             "219.88.106.80" => 1, # A
90             "mail.nonregistered.nic.cx" => 1, # MX
91              
92             # mp
93             "202.128.12.163" => 1, # A
94              
95             # museum
96             "195.7.77.20" => 1, # A
97              
98             # nu
99             "64.55.105.9" => 1, # A
100             "212.181.91.6" => 1, # A
101              
102             # ph
103             "203.119.4.6" => 1, # A
104             "45.79.222.138" => 1, # A
105              
106             # pw
107             "216.98.141.250" => 1, # A
108             "65.125.231.178" => 1, # A
109             "wfb.dnsvr.com" => 1, # CNAME
110              
111             # sh
112             "194.205.62.62" => 1, # A
113              
114             # td
115             "146.101.245.154" => 1, # A
116             "www.nic.td" => 1, # CNAME
117              
118             # tk
119             "195.20.32.83" => 1, # A
120             "195.20.32.86" => 1, # A
121             "nukumatau.taloha.com" => 1, # MX
122             "195.20.32.99" => 1, # MX
123              
124             # tm
125             "194.205.62.42" => 1, # A
126              
127             # tw
128             "203.73.24.11" => 1, # A
129              
130             # ws
131             "216.35.187.246" => 1, # A
132             "mail.worldsite.ws" => 1, # MX
133             "mail.hope-mail.com" => 1, # MX
134             "216.35.187.251" => 1, # MX
135              
136             };
137              
138             # check_email EMAIL
139             sub check_email( $ );
140             # last_check
141             sub last_check( );
142             # check_hostname_syntax HOSTNAME
143             sub check_hostname_syntax( $ );
144             # check_username_syntax USERNAME
145             sub check_username_syntax( $ );
146             # check_network HOSTNAME, USERNAME
147             sub check_network( $$ );
148             # check_user_on_host MSERVER, USERNAME, HOSTNAME, TIMEOUT
149             sub check_user_on_host( $$$$ );
150             # _calc_timeout FULL_TIMEOUT START_TIME
151             sub _calc_timeout( $$ );
152             # _pm_log LOG_STR
153             sub _pm_log( $ );
154             # _result RESULT, REASON
155             sub _result( $$ );
156              
157             # check result codes
158 6     6   39 use constant CU_OK => 0;
  6         9  
  6         486  
159 6     6   42 use constant CU_BAD_SYNTAX => 1;
  6         11  
  6         262  
160 6     6   76 use constant CU_UNKNOWN_DOMAIN => 2;
  6         8  
  6         271  
161 6     6   24 use constant CU_DNS_TIMEOUT => 3;
  6         8  
  6         321  
162 6     6   41 use constant CU_UNKNOWN_USER => 4;
  6         6  
  6         298  
163 6     6   26 use constant CU_SMTP_TIMEOUT => 5;
  6         7  
  6         269  
164 6     6   38 use constant CU_SMTP_UNREACHABLE => 6;
  6         9  
  6         317  
165 6     6   22 use constant CU_MAILBOX_FULL => 7;
  6         8  
  6         304  
166 6     6   31 use constant CU_TRY_AGAIN => 8;
  6         8  
  6         16602  
167              
168             sub check_email($) {
169 57     57 1 43720 my($email) = @_;
170              
171 57 50       204 unless(defined $email) {
172 0         0 croak __PACKAGE__ . "::check_email: \$email is undefined";
173             }
174              
175 57         180 _pm_log '=' x 40;
176 57         207 _pm_log "check_email: checking \"$email\"";
177              
178             # split email address on username and hostname
179 57         370 my($username, $hostname) = $email =~ /^(.*)@(.*)$/;
180             # return false if it impossible
181 57 100       178 unless(defined $hostname) {
182 2         4 return _result(CU_BAD_SYNTAX, 'bad address format: missing @');
183             }
184              
185 55         84 my $ok = 1;
186 55   66     248 $ok &&= check_hostname_syntax $hostname;
187 55   100     239 $ok &&= check_username_syntax $username;
188 55 100       165 if($Skip_Network_Checks) {
    50          
189 28         32 _pm_log "check_email: skipping network checks";
190             } elsif ($ok) {
191 27   66     124 $ok &&= check_network $hostname, $username;
192             }
193              
194 55         2742 return $ok;
195             }
196              
197             sub last_check() {
198 6     6 1 42 return $Mail::CheckUser::Last_Check;
199             }
200              
201             # build hostname regexp
202             # NOTE: it doesn't strictly follow RFC822
203             # because of what registrars now allow.
204             my $DOMAIN_RE = qr/(?:[\da-zA-Z]+ -+)* [\da-zA-Z]+/x;
205             my $HOSTNAME_RE = qr/^ (?:$DOMAIN_RE \.)+ [a-zA-Z]+ $/xo;
206              
207             sub check_hostname_syntax($) {
208 55     55 0 102 my($hostname) = @_;
209              
210 55         197 _pm_log "check_hostname_syntax: checking \"$hostname\"";
211              
212             # check if hostname syntax is correct
213 55 100       627 if($hostname =~ $HOSTNAME_RE) {
214 47         115 return _result(CU_OK, 'correct hostname syntax');
215             } else {
216 8         11 return _result(CU_BAD_SYNTAX, 'bad hostname syntax');
217             }
218             }
219              
220             # build username regexp
221             # NOTE: it doesn't strictly follow RFC821
222             my $STRING_RE = ('[' . quotemeta(join '',
223             grep(!/[<>()\[\]\\\.,;:\@"]/, # ["], UnBug Emacs
224             map chr, 33 .. 126)) . ']');
225             my $USERNAME_RE = qr/^ (?:$STRING_RE+ \.)* $STRING_RE+ $/xo;
226              
227              
228             sub check_username_syntax($) {
229 47     47 0 110 my($username) = @_;
230              
231 47         156 _pm_log "check_username_syntax: checking \"$username\"";
232              
233             # check if username syntax is correct
234 47 100       337 if($username =~ $USERNAME_RE) {
235 39         103 return _result(CU_OK, 'correct username syntax');
236             } else {
237 8         12 return _result(CU_BAD_SYNTAX, 'bad username syntax');
238             }
239             }
240              
241             sub check_network($$) {
242 27     27 0 61 my($hostname, $username) = @_;
243              
244 27         131 _pm_log "check_network: checking \"$username\" on \"$hostname\"";
245              
246             # list of mail servers for hostname
247 27         100 my @mservers = ();
248              
249 27         64 my $timeout = $Timeout;
250 27         49 my $start_time = time;
251              
252 27   33     412 my $resolver = $Mail::CheckUser::Net_DNS_Resolver || new Net::DNS::Resolver;
253 27         3448 my $tout = _calc_timeout($timeout, $start_time);
254 27 50       95 return _result(CU_DNS_TIMEOUT, 'DNS timeout') if $tout == 0;
255 27         183 $resolver->udp_timeout($tout);
256              
257 27         525 my @mx = mx($resolver, "$hostname.");
258 27         1847423 $tout = _calc_timeout($timeout, $start_time);
259 27 50       119 return _result(CU_DNS_TIMEOUT, 'DNS timeout') if $tout == 0;
260              
261             # check result of query
262 27 100       104 if(@mx) {
263             # if MX record exists,
264             # then it's already sorted by preference
265 13         39 @mservers = map {$_->exchange} @mx;
  43         1673  
266             } else {
267             # if there is no MX record try hostname as mail server
268 14         54 my $tout = _calc_timeout($timeout, $start_time);
269 14 50       78 return _result(CU_DNS_TIMEOUT, 'DNS timeout') if $tout == 0;
270 14         123 $resolver->udp_timeout($tout);
271              
272 14         289 my $res = $resolver->search("$hostname.", 'A');
273             # check if timeout has happen
274 14         1345384 $tout = _calc_timeout($timeout, $start_time);
275 14 50       70 return _result(CU_DNS_TIMEOUT, 'DNS timeout') if $tout == 0;
276              
277             # check result of query
278 14 100       67 if($res) {
279 1         3 @mservers = ($hostname);
280 1         2 my $ip;
281 1         5 foreach my $rr ($res->answer) {
282 1 50       15 if ($rr->type eq "A") {
    0          
283 1         13 $ip = $rr->address;
284 1         10 last;
285             } elsif ($rr->type eq "CNAME") {
286 0         0 $ip = $rr->cname;
287             } else {
288             # Should never happen!
289 0         0 $ip = "";
290             }
291             }
292 1         6 _pm_log "check_network: \"$ip\" Wildcard gTLD check";
293 1 50       8 return _result(CU_UNKNOWN_DOMAIN, 'Wildcard gTLD') if $NXDOMAIN->{lc $ip};
294             } else {
295 13         73 return _result(CU_UNKNOWN_DOMAIN, 'DNS failure: ' . $resolver->errorstring);
296             }
297             }
298              
299 13         535 foreach my $mserver (@mservers) {
300 43         159 _pm_log "check_network: \"$mserver\" Wildcard gTLD check";
301 43 100       229 return _result(CU_UNKNOWN_DOMAIN, 'Wildcard gTLD') if $NXDOMAIN->{lc $mserver};
302             }
303              
304 12 100       56 if($Skip_SMTP_Checks) {
305 3         12 return _result(CU_OK, 'skipping SMTP checks');
306             } else {
307 9 50       32 if ($Skip_SYN) {
308             # Skip SYN/ACK check.
309             # Just check user on each mail server one at a time.
310 0         0 foreach my $mserver (@mservers) {
311 0         0 my $tout = _calc_timeout($timeout, $start_time);
312 0 0       0 if ($mserver !~ /^\d+\.\d+\.\d+\.\d+$/) {
313             # Resolve it to an IP
314 0 0       0 return _result(CU_DNS_TIMEOUT, 'DNS timeout') if $tout == 0;
315 0         0 $resolver->udp_timeout($tout);
316 0 0       0 if (my $ans = $resolver->query($mserver)) {
317 0         0 foreach my $rr_a ($ans->answer) {
318 0 0       0 if ($rr_a->type eq "A") {
319 0         0 $mserver = $rr_a->address;
320 0         0 last;
321             }
322             }
323             }
324 0         0 $tout = _calc_timeout($timeout, $start_time);
325             }
326 0 0       0 return _result(CU_SMTP_TIMEOUT, 'SMTP timeout') if $tout == 0;
327              
328 0         0 my $res = check_user_on_host $mserver, $username, $hostname, $tout;
329              
330 0 0       0 return 1 if $res == 1;
331 0 0       0 return 0 if $res == 0;
332             }
333             } else {
334             # Determine which mail servers are on
335 9         25 my $resolve = {};
336 9         34 my $tout = _calc_timeout($timeout, $start_time);
337 9         28 foreach my $mserver (@mservers) {
338             # All mservers need to be resolved to IPs before the SYN check
339 32 50       275 if ($mserver =~ /^\d+\.\d+\.\d+\.\d+$/) {
340 0         0 $resolve->{$mserver} = 1;
341             } else {
342 32         141 _pm_log "check_network: \"$mserver\" resolving";
343 32 50       94 return _result(CU_DNS_TIMEOUT, 'DNS timeout') if $tout == 0;
344 32         110 $resolver->udp_timeout($tout);
345 32 100       292 if (my $ans = $resolver->query($mserver)) {
346 30         282080 foreach my $rr_a ($ans->answer) {
347 30 50       274 if ($rr_a->type eq "A") {
348 30         395 $mserver = $rr_a->address;
349 30         399 $resolve->{$mserver} = 1;
350 30         137 _pm_log "check_network: resolved to IP \"$mserver\"";
351 30         75 last;
352             }
353             }
354             } else {
355 2         18985 _pm_log "check_network: \"$mserver\" host not found!";
356             }
357 32         126 $tout = _calc_timeout($timeout, $start_time);
358             }
359             }
360              
361 9         4355 require Net::Ping;
362 9         67631 import Net::Ping 2.24;
363             # Use only three-fourths of the full timeout for lookups
364             # in order to leave time to actually speak to the server.
365 9         56 my $ping = Net::Ping->new("syn", _calc_timeout($timeout, $start_time) * 3 / 4 + 1);
366 9         3093 $ping->{port_num} = getservbyname("smtp", "tcp");
367 9         77 $ping->tcp_service_check(1);
368 9         124 foreach my $mserver (@mservers) {
369 32         143 _pm_log "check_network: \"$mserver\" sending SYN...";
370             # untaint before passing to Net::Ping
371 32         268 my ($tainted) = $mserver =~ /(\d+\.\d+\.\d+\.\d+)/;
372 32 100 66     506 if ($tainted and $tainted eq $mserver and
      66        
      33        
373             $resolve->{$tainted} and $ping->ping($tainted)) {
374 30         7370 _pm_log "check_network: \"$tainted\" SYN packet sent.";
375             } else {
376 2         17 _pm_log "check_network: \"$mserver\" host not found!";
377             }
378             }
379 9         35 foreach my $mserver (@mservers) {
380 9         28 my $tout = _calc_timeout($timeout, $start_time);
381 9 50       46 return _result(CU_SMTP_TIMEOUT, 'SMTP timeout') if $tout == 0;
382              
383 9         44 _pm_log "check_network: \"$mserver\" waiting for ACK";
384 9 100       37 if ($resolve->{$mserver}) {
385             # untaint before passing to Net::Ping
386 7         105 my($mserver) = $mserver =~ /(\d+\.\d+\.\d+\.\d+)/;
387 7 50       47 if ($ping->ack($mserver)) {
388 7         215278 _pm_log "check_network: \"$mserver\" ACK received.";
389             # check user on this mail server
390 7         46 my $res = check_user_on_host $mserver, $username, $hostname, $tout;
391              
392 7 100       314 return 1 if $res == 1;
393 4 50       81 return 0 if $res == 0;
394             } else {
395 0   0     0 _pm_log "check_network: \"$mserver\" no ACK received: [".
396             ($ping->nack($mserver) || "no SYN sent")."]";
397             }
398             } else {
399 2         11 _pm_log "check_network: skipping check_user_on_host \"$mserver\" since it did not resolve";
400             }
401             }
402             }
403              
404 2         37 return _result(CU_SMTP_UNREACHABLE,
405             'Cannot connect SMTP servers: ' .
406             join(', ', @mservers));
407             }
408              
409             # it should be impossible to reach this statement
410 0         0 die "Internal error";
411             }
412              
413             sub check_user_on_host($$$$) {
414 7     7 0 28 my($mserver, $username, $hostname, $timeout) = @_;
415              
416 7         66 _pm_log "check_user_on_host: checking user \"$username\" on \"$mserver\"";
417              
418 7         26 my $start_time = time;
419              
420             # disable warnings because Net::SMTP can generate some on timeout
421             # conditions
422 7         83 local $^W = 0;
423              
424             # try to connect to mail server
425 7         45 my $tout = _calc_timeout($timeout, $start_time);
426 7 50       35 return _result(CU_SMTP_TIMEOUT, 'SMTP timeout') if $tout == 0;
427              
428 7 50       45 my @hello_params = defined $Helo_Domain ? (Hello => $Helo_Domain) : ();
429 7         159 my $smtp = Net::SMTP->new($mserver, Timeout => $tout, @hello_params);
430 7 50       2038119 unless(defined $smtp) {
431 0         0 _pm_log "check_user_on_host: unable to connect to \"$mserver\"";
432 0         0 return -1;
433             }
434              
435             # try to check if user is valid with MAIL/RCPT commands
436 7         51 $tout = _calc_timeout($timeout, $start_time);
437 7 100       40 return _result(CU_SMTP_TIMEOUT, 'SMTP timeout') if $tout == 0;
438 6         47 $smtp->timeout($tout);
439              
440             # send MAIL FROM command
441 6 50       176 unless($smtp->mail($Sender_Addr)) {
442             # something wrong?
443              
444             # check for timeout
445 0 0       0 return _result(CU_SMTP_TIMEOUT, 'SMTP timeout') if $tout == 0;
446              
447 0         0 _pm_log "check_user_on_host: can't say MAIL - " . $smtp->message;
448 0         0 return -1;
449             }
450              
451             # send RCPT TO command
452 6 100       373816 if($smtp->to("$username\@$hostname")) {
453             # give server opportunity to exist gracefully by telling it QUIT
454 2         424134 my $tout = _calc_timeout($timeout, $start_time);
455 2 50       11 if($tout) {
456 2         22 $smtp->timeout($tout);
457 2         42 $smtp->quit;
458             }
459              
460 2         28559 return _result(CU_OK, 'SMTP server accepts username');
461             } else {
462             # check if verify returned error because of timeout
463 4         568502 my $tout = _calc_timeout($timeout, $start_time);
464 4 50       70 return _result(CU_SMTP_TIMEOUT, 'SMTP timeout') if $tout == 0;
465              
466 4         163 my $code = $smtp->code;
467              
468             # give server opportunity to exist gracefully by telling it QUIT
469 4         195 $smtp->timeout($tout);
470 4         146 $smtp->quit;
471              
472 4 50 33     154791 if($code == 550 or $code == 551 or $code == 553) {
    0 33        
    0          
473 4         67 return _result(CU_UNKNOWN_USER, 'no such user');
474             } elsif($code == 552) {
475 0         0 return _result(CU_MAILBOX_FULL, 'mailbox full');
476             } elsif($code =~ /^4/) {
477 0         0 return _result(CU_TRY_AGAIN, 'temporary delivery failure');
478             } else {
479 0         0 _pm_log "check_user_on_host: unknown error in response";
480 0         0 return _result(CU_OK, 'unknown error in response');
481             }
482             }
483              
484              
485             # it should be impossible to reach this statement
486 0         0 die "Internal error";
487             }
488              
489             sub _calc_timeout($$) {
490 161     161   448 my($full_timeout, $start_time) = @_;
491              
492 161         307 my $now_time = time;
493 161         347 my $passed_time = $now_time - $start_time;
494 161         789 _pm_log "_calc_timeout: start - $start_time, now - $now_time";
495 161         573 _pm_log "_calc_timeout: timeout - $full_timeout, passed - $passed_time";
496              
497 161         352 my $timeout = $full_timeout - $passed_time;
498              
499 161 50       514 if($timeout < 0) {
500 0         0 return 0;
501             } else {
502 161         800 return $timeout;
503             }
504             }
505              
506             sub _pm_log($) {
507 921     921   1343 my($log_str) = @_;
508              
509 921 50       2577 if($Debug) {
510 0         0 print STDERR "$log_str\n";
511             }
512             }
513              
514             sub _result($$) {
515 131     131   354 my($code, $reason) = @_;
516              
517 131         164 my $ok = 0;
518              
519 131 100       333 $ok = 1 if $code == CU_OK;
520 131 100       258 $ok = 1 if $code == CU_SMTP_UNREACHABLE;
521 131 50 33     400 $ok = 1 if $code == CU_MAILBOX_FULL and not $Treat_Full_As_Fail;
522 131 50 33     400 $ok = 1 if $code == CU_DNS_TIMEOUT and not $Treat_Timeout_As_Fail;
523 131 100 66     306 $ok = 1 if $code == CU_SMTP_TIMEOUT and not $Treat_Timeout_As_Fail;
524 131 50 33     311 $ok = 1 if $code == CU_TRY_AGAIN and not $Treat_Grey_As_Fail;
525              
526 131         634 $Last_Check = { ok => $ok,
527             code => $code,
528             reason => $reason };
529              
530 131         1523 my($sub) = (caller(1))[3] =~ /^.*::(.*)$/;
531              
532 131 100       765 _pm_log "$sub: check result is " .
533             ($ok ? 'ok' : 'not ok') .
534             ": [$code] $reason";
535              
536 131         1197 return $ok;
537             }
538              
539             1;
540             __END__