File Coverage

blib/lib/Mail/CheckUser.pm
Criterion Covered Total %
statement 196 231 84.8
branch 63 112 56.2
condition 20 41 48.7
subroutine 25 25 100.0
pod 2 6 33.3
total 306 415 73.7


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