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   112051 use strict;
  6         14  
  6         264  
10 6     6   32 use vars qw(@ISA @EXPORT_OK %EXPORT_TAGS $VERSION);
  6         12  
  6         1104  
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.24';
32              
33 6     6   37 use Carp;
  6         14  
  6         521  
34 6     6   5080 use Net::DNS;
  6         652149  
  6         980  
35 6     6   5396 use Net::SMTP;
  6         111077  
  6         408  
36 6     6   63 use IO::Handle;
  6         13  
  6         339  
37              
38 6         1975 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   35 $Sender_Addr $Helo_Domain $Last_Check);
  6         11  
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   37 use constant CU_OK => 0;
  6         11  
  6         511  
159 6     6   33 use constant CU_BAD_SYNTAX => 1;
  6         8  
  6         330  
160 6     6   85 use constant CU_UNKNOWN_DOMAIN => 2;
  6         9  
  6         329  
161 6     6   29 use constant CU_DNS_TIMEOUT => 3;
  6         7  
  6         324  
162 6     6   27 use constant CU_UNKNOWN_USER => 4;
  6         9  
  6         291  
163 6     6   29 use constant CU_SMTP_TIMEOUT => 5;
  6         9  
  6         302  
164 6     6   41 use constant CU_SMTP_UNREACHABLE => 6;
  6         9  
  6         314  
165 6     6   28 use constant CU_MAILBOX_FULL => 7;
  6         9  
  6         308  
166 6     6   30 use constant CU_TRY_AGAIN => 8;
  6         8  
  6         17342  
167              
168             sub check_email($) {
169 57     57 1 79099 my($email) = @_;
170              
171 57 50       241 unless(defined $email) {
172 0         0 croak __PACKAGE__ . "::check_email: \$email is undefined";
173             }
174              
175 57         260 _pm_log '=' x 40;
176 57         261 _pm_log "check_email: checking \"$email\"";
177              
178             # split email address on username and hostname
179 57         439 my($username, $hostname) = $email =~ /^(.*)@(.*)$/;
180             # return false if it impossible
181 57 100       336 unless(defined $hostname) {
182 2         8 return _result(CU_BAD_SYNTAX, 'bad address format: missing @');
183             }
184              
185 55         103 my $ok = 1;
186 55   66     426 $ok &&= check_hostname_syntax $hostname;
187 55   100     252 $ok &&= check_username_syntax $username;
188 55 100       186 if($Skip_Network_Checks) {
    50          
189 28         40 _pm_log "check_email: skipping network checks";
190             } elsif ($ok) {
191 27   66     139 $ok &&= check_network $hostname, $username;
192             }
193              
194 55         2525 return $ok;
195             }
196              
197             sub last_check() {
198 6     6 1 112 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 91 my($hostname) = @_;
209              
210 55         212 _pm_log "check_hostname_syntax: checking \"$hostname\"";
211              
212             # check if hostname syntax is correct
213 55 100       725 if($hostname =~ $HOSTNAME_RE) {
214 47         163 return _result(CU_OK, 'correct hostname syntax');
215             } else {
216 8         15 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 90 my($username) = @_;
230              
231 47         154 _pm_log "check_username_syntax: checking \"$username\"";
232              
233             # check if username syntax is correct
234 47 100       385 if($username =~ $USERNAME_RE) {
235 39         121 return _result(CU_OK, 'correct username syntax');
236             } else {
237 8         14 return _result(CU_BAD_SYNTAX, 'bad username syntax');
238             }
239             }
240              
241             sub check_network($$) {
242 27     27 0 63 my($hostname, $username) = @_;
243              
244 27         168 _pm_log "check_network: checking \"$username\" on \"$hostname\"";
245              
246             # list of mail servers for hostname
247 27         69 my @mservers = ();
248              
249 27         64 my $timeout = $Timeout;
250 27         62 my $start_time = time;
251              
252 27   33     457 my $resolver = $Mail::CheckUser::Net_DNS_Resolver || new Net::DNS::Resolver;
253 27         4251 my $tout = _calc_timeout($timeout, $start_time);
254 27 50       101 return _result(CU_DNS_TIMEOUT, 'DNS timeout') if $tout == 0;
255 27         209 $resolver->udp_timeout($tout);
256              
257 27         634 my @mx = mx($resolver, "$hostname.");
258 27         2547427 $tout = _calc_timeout($timeout, $start_time);
259 27 50       150 return _result(CU_DNS_TIMEOUT, 'DNS timeout') if $tout == 0;
260              
261             # check result of query
262 27 100       116 if(@mx) {
263             # if MX record exists,
264             # then it's already sorted by preference
265 13         51 @mservers = map {$_->exchange} @mx;
  43         2539  
266             } else {
267             # if there is no MX record try hostname as mail server
268 14         70 my $tout = _calc_timeout($timeout, $start_time);
269 14 50       80 return _result(CU_DNS_TIMEOUT, 'DNS timeout') if $tout == 0;
270 14         101 $resolver->udp_timeout($tout);
271              
272 14         375 my $res = $resolver->search("$hostname.", 'A');
273             # check if timeout has happen
274 14         835108 $tout = _calc_timeout($timeout, $start_time);
275 14 50       87 return _result(CU_DNS_TIMEOUT, 'DNS timeout') if $tout == 0;
276              
277             # check result of query
278 14 100       82 if($res) {
279 1         3 @mservers = ($hostname);
280 1         2 my $ip;
281 1         5 foreach my $rr ($res->answer) {
282 1 50       14 if ($rr->type eq "A") {
    0          
283 1         14 $ip = $rr->address;
284 1         119 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       10 return _result(CU_UNKNOWN_DOMAIN, 'Wildcard gTLD') if $NXDOMAIN->{lc $ip};
294             } else {
295 13         88 return _result(CU_UNKNOWN_DOMAIN, 'DNS failure: ' . $resolver->errorstring);
296             }
297             }
298              
299 13         649 foreach my $mserver (@mservers) {
300 43         200 _pm_log "check_network: \"$mserver\" Wildcard gTLD check";
301 43 100       340 return _result(CU_UNKNOWN_DOMAIN, 'Wildcard gTLD') if $NXDOMAIN->{lc $mserver};
302             }
303              
304 12 100       77 if($Skip_SMTP_Checks) {
305 3         12 return _result(CU_OK, 'skipping SMTP checks');
306             } else {
307 9 50       46 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         29 my $resolve = {};
336 9         31 my $tout = _calc_timeout($timeout, $start_time);
337 9         30 foreach my $mserver (@mservers) {
338             # All mservers need to be resolved to IPs before the SYN check
339 32 50       397 if ($mserver =~ /^\d+\.\d+\.\d+\.\d+$/) {
340 0         0 $resolve->{$mserver} = 1;
341             } else {
342 32         229 _pm_log "check_network: \"$mserver\" resolving";
343 32 50       140 return _result(CU_DNS_TIMEOUT, 'DNS timeout') if $tout == 0;
344 32         197 $resolver->udp_timeout($tout);
345 32 100       606 if (my $ans = $resolver->query($mserver)) {
346 30         392908 foreach my $rr_a ($ans->answer) {
347 30 50       441 if ($rr_a->type eq "A") {
348 30         710 $mserver = $rr_a->address;
349 30         897 $resolve->{$mserver} = 1;
350 30         217 _pm_log "check_network: resolved to IP \"$mserver\"";
351 30         100 last;
352             }
353             }
354             } else {
355 2         29085 _pm_log "check_network: \"$mserver\" host not found!";
356             }
357 32         272 $tout = _calc_timeout($timeout, $start_time);
358             }
359             }
360              
361 9         3476 require Net::Ping;
362 9         54204 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         48 my $ping = Net::Ping->new("syn", _calc_timeout($timeout, $start_time) * 3 / 4 + 1);
366 9         4196 $ping->{port_num} = getservbyname("smtp", "tcp");
367 9         106 $ping->tcp_service_check(1);
368 9         143 foreach my $mserver (@mservers) {
369 32         162 _pm_log "check_network: \"$mserver\" sending SYN...";
370             # untaint before passing to Net::Ping
371 32         320 my ($tainted) = $mserver =~ /(\d+\.\d+\.\d+\.\d+)/;
372 32 100 66     488 if ($tainted and $tainted eq $mserver and
      66        
      33        
373             $resolve->{$tainted} and $ping->ping($tainted)) {
374 30         9165 _pm_log "check_network: \"$tainted\" SYN packet sent.";
375             } else {
376 2         22 _pm_log "check_network: \"$mserver\" host not found!";
377             }
378             }
379 9         39 foreach my $mserver (@mservers) {
380 9         30 my $tout = _calc_timeout($timeout, $start_time);
381 9 50       47 return _result(CU_SMTP_TIMEOUT, 'SMTP timeout') if $tout == 0;
382              
383 9         54 _pm_log "check_network: \"$mserver\" waiting for ACK";
384 9 100       43 if ($resolve->{$mserver}) {
385             # untaint before passing to Net::Ping
386 7         76 my($mserver) = $mserver =~ /(\d+\.\d+\.\d+\.\d+)/;
387 7 50       43 if ($ping->ack($mserver)) {
388 7         215825 _pm_log "check_network: \"$mserver\" ACK received.";
389             # check user on this mail server
390 7         47 my $res = check_user_on_host $mserver, $username, $hostname, $tout;
391              
392 7 100       261 return 1 if $res == 1;
393 4 50       67 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         21 _pm_log "check_network: skipping check_user_on_host \"$mserver\" since it did not resolve";
400             }
401             }
402             }
403              
404 2         53 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 36 my($mserver, $username, $hostname, $timeout) = @_;
415              
416 7         61 _pm_log "check_user_on_host: checking user \"$username\" on \"$mserver\"";
417              
418 7         22 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         54 my $tout = _calc_timeout($timeout, $start_time);
426 7 50       56 return _result(CU_SMTP_TIMEOUT, 'SMTP timeout') if $tout == 0;
427              
428 7 50       58 my @hello_params = defined $Helo_Domain ? (Hello => $Helo_Domain) : ();
429 7         196 my $smtp = Net::SMTP->new($mserver, Timeout => $tout, @hello_params);
430 7 50       2452010 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         46 $tout = _calc_timeout($timeout, $start_time);
437 7 100       43 return _result(CU_SMTP_TIMEOUT, 'SMTP timeout') if $tout == 0;
438 6         74 $smtp->timeout($tout);
439              
440             # send MAIL FROM command
441 6 50       210 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       396773 if($smtp->to("$username\@$hostname")) {
453             # give server opportunity to exist gracefully by telling it QUIT
454 2         289271 my $tout = _calc_timeout($timeout, $start_time);
455 2 50       18 if($tout) {
456 2         29 $smtp->timeout($tout);
457 2         82 $smtp->quit;
458             }
459              
460 2         25760 return _result(CU_OK, 'SMTP server accepts username');
461             } else {
462             # check if verify returned error because of timeout
463 4         644172 my $tout = _calc_timeout($timeout, $start_time);
464 4 50       26 return _result(CU_SMTP_TIMEOUT, 'SMTP timeout') if $tout == 0;
465              
466 4         106 my $code = $smtp->code;
467              
468             # give server opportunity to exist gracefully by telling it QUIT
469 4         129 $smtp->timeout($tout);
470 4         97 $smtp->quit;
471              
472 4 50 33     140550 if($code == 550 or $code == 551 or $code == 553) {
    0 33        
    0          
473 4         30 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   553 my($full_timeout, $start_time) = @_;
491              
492 161         405 my $now_time = time;
493 161         505 my $passed_time = $now_time - $start_time;
494 161         1282 _pm_log "_calc_timeout: start - $start_time, now - $now_time";
495 161         1173 _pm_log "_calc_timeout: timeout - $full_timeout, passed - $passed_time";
496              
497 161         368 my $timeout = $full_timeout - $passed_time;
498              
499 161 50       1528 if($timeout < 0) {
500 0         0 return 0;
501             } else {
502 161         1324 return $timeout;
503             }
504             }
505              
506             sub _pm_log($) {
507 921     921   1602 my($log_str) = @_;
508              
509 921 50       8377 if($Debug) {
510 0         0 print STDERR "$log_str\n";
511             }
512             }
513              
514             sub _result($$) {
515 131     131   384 my($code, $reason) = @_;
516              
517 131         195 my $ok = 0;
518              
519 131 100       380 $ok = 1 if $code == CU_OK;
520 131 100       328 $ok = 1 if $code == CU_SMTP_UNREACHABLE;
521 131 50 33     415 $ok = 1 if $code == CU_MAILBOX_FULL and not $Treat_Full_As_Fail;
522 131 50 33     388 $ok = 1 if $code == CU_DNS_TIMEOUT and not $Treat_Timeout_As_Fail;
523 131 100 66     364 $ok = 1 if $code == CU_SMTP_TIMEOUT and not $Treat_Timeout_As_Fail;
524 131 50 33     353 $ok = 1 if $code == CU_TRY_AGAIN and not $Treat_Grey_As_Fail;
525              
526 131         629 $Last_Check = { ok => $ok,
527             code => $code,
528             reason => $reason };
529              
530 131         1716 my($sub) = (caller(1))[3] =~ /^.*::(.*)$/;
531              
532 131 100       917 _pm_log "$sub: check result is " .
533             ($ok ? 'ok' : 'not ok') .
534             ": [$code] $reason";
535              
536 131         1230 return $ok;
537             }
538              
539             1;
540             __END__