File Coverage

blib/lib/Net/validMX.pm
Criterion Covered Total %
statement 219 329 66.5
branch 129 284 45.4
condition 27 51 52.9
subroutine 12 16 75.0
pod 4 12 33.3
total 391 692 56.5


line stmt bran cond sub pod time code
1             package Net::validMX;
2              
3 6     6   380520 use strict;
  6         51  
  6         184  
4 6     6   3215 use Net::DNS;
  6         554829  
  6         1341  
5              
6 6         1388 use vars qw(
7             $VERSION
8             @ISA
9             @EXPORT_OK
10             $DEBUG
11             $ALLOW_IP_ADDRESS_AS_MX
12             $FLAG_INTRANETS
13             $RESOLUTION_PROBLEM_RETURN
14 6     6   129 $QUERY_TIMEOUT);
  6         38  
15              
16             BEGIN {
17 6     6   52 require DynaLoader;
18 6         48 require Exporter;
19              
20 6         148 @ISA = qw(Exporter DynaLoader);
21 6         39 $VERSION = '2.5.0';
22 6         28 $DEBUG = 0;
23 6         15 $ALLOW_IP_ADDRESS_AS_MX = 1;
24 6         20 $FLAG_INTRANETS = 1;
25 6         37 $RESOLUTION_PROBLEM_RETURN = 1;
26 6         24849 $QUERY_TIMEOUT = 4;
27             }
28              
29 0     0 0 0 sub version { $VERSION; }
30              
31             @EXPORT_OK = qw(check_valid_mx get_output_result check_email_and_mx check_email_validity get_domain_from_email);
32              
33             sub new {
34 0     0 0 0 my (%self) = @_;
35              
36 0         0 my ($self);
37              
38 0 0       0 $DEBUG = $self{'debug'} if ($self{'debug'} ne '');
39 0 0       0 $ALLOW_IP_ADDRESS_AS_MX = $self{'allow_ip_address_as_mx'} if ($self{'allow_ip_address_as_mx'} ne '');
40 0 0       0 $FLAG_INTRANETS = $self{'flag_intranets'} if ($self{'flag_intranets'} ne '');
41 0 0       0 $RESOLUTION_PROBLEM_RETURN = $self{'resolution_problem_return'} if ($self{'resolution_problem_return'} ne '');
42 0 0       0 $QUERY_TIMEOUT = $self{'query_timeout'} if ($self{'query_timeout'} ne '');
43              
44 0         0 $self = \%self;
45 0         0 bless $self;
46              
47 0         0 return $self;
48             }
49              
50             sub get_output_result {
51 34     34 0 737 my ($email, $rv, $reason) = @_;
52 34         81 my ($output);
53              
54 34         180 $output = "$email\n\tValid MX? ".&Net::validMX::int_to_truefalse($rv);
55 34 100       137 if ($reason ne '') {
56 13         52 $output .= " - $reason";
57             }
58 34         89 $output .= "\n\n";
59              
60 34         2035 return $output;
61             }
62              
63             sub check_valid_mx {
64             #Based on Idea from Les Miksell and much input from Jan Pieter Cornet
65             #KAM 9-12-05 updated 10-24-05 & 11-3-05.
66             #takes the email address, extracts the domain name and performs multiple MX tests to see if the domain has valid
67             #MX exchange records
68              
69 34     34 1 28353 my ($res, $packet, @answer, $domain, @answer2, @answer3, $rv, $reason, $i, @unsorted_answer);
70 34         0 my ($check_implicit_mx, %params, $self, $ref, $resolution_problem_status);
71              
72             #print "DEBUG: ref for \$_[0] ".ref($_[0]). "\n";
73             #IN OO INSTEAD OF PROCEDURAL MODE?
74 34 50       191 if (uc(ref($_[0])) eq 'NET::VALIDMX') {
75 0         0 $self = shift(@_);
76             #foreach $ref (keys %$self) {
77             # print "DEBUG: OO MODE - $ref: $self->{$ref} \n";
78             #}
79             }
80              
81             #DID WE RECEIVE A HASH INSTEAD OF A SINGLE EMAIL?
82 34 100       160 if ($#_ % 2 == 0) {
83 33         127 ($params{'email'}) = @_;
84             } else {
85 1         3 %params = @_;
86             }
87              
88 34 50 66     155 $params{'email'} || $params{'sender'} || return (0, 'A blank email address will not be tested.');
89              
90             #CONSTANTS / SETTABLE OPTIONS
91 33 50       163 $params{'debug'} = $DEBUG unless (defined $params{'debug'});
92 33 50       147 $params{'allow_ip_address_as_mx'} = $ALLOW_IP_ADDRESS_AS_MX unless (defined $params{'allow_ip_address_as_mx'});
93 33 50       138 $params{'resolution_problem_return'} = $RESOLUTION_PROBLEM_RETURN unless (defined $params{'resolution_problem_return'});
94 33 50       122 $params{'query_timeout'} = $QUERY_TIMEOUT unless (defined $params{'query_timeout'});
95              
96 33 50       108 if ($params{'resolution_problem_return'} > 0) {
97 33         74 $resolution_problem_status = 'Passed';
98             } else {
99 0         0 $resolution_problem_status = 'Failed';
100             }
101              
102 33 50       111 print "DEBUG: function debug setting is $params{'debug'}\n" if $params{'debug'};
103 33 50       100 print "DEBUG: function allow_ip_address_as_mx setting is $params{'allow_ip_address_as_mx'}\n" if $params{'debug'};
104 33 50       114 print "DEBUG: function resolution_problem_return setting is $params{'resolution_problem_return'}\n" if $params{'debug'};
105 33 50       92 print "DEBUG: function query_timeout setting is $params{'query_timeout'}\n" if $params{'debug'};
106              
107             #FLAGS - I THINK THIS HAS A LOGIC ISSUE - I LIKELY MEANT ALLOW_IMPLICIT_MX as an option FIX
108 33         69 $check_implicit_mx = 0;
109              
110             #Setup a DNS Resolver Resource
111 33         351 $res = Net::DNS::Resolver->new;
112              
113 33 50       3727 if (defined ($res)) {
114 33         70 $check_implicit_mx = 0;
115 33         186 $res->defnames(0); #Turn off appending the default domain for names that have no dots just in case
116 33         629 $res->searchlist(); #Set the search list to undefined just in case
117              
118             #We have also set the default timeout to only 4 seconds which means we might get network
119             #delays which we do not want to handle as an error.
120 33         425 $res->tcp_timeout($params{'query_timeout'}); #Number of Seconds before query will fail
121 33         455 $res->udp_timeout($params{'query_timeout'}); #Number of Seconds before query will fail
122              
123             #Strip domain name from an email address
124 33         416 $domain = &get_domain_from_email($params{'email'});
125              
126             #Deny Explicit IP Address Domains
127 33 100       165 if ($domain =~ /^\[.*\]$/) {
128 1         4 $reason = "Use of IP Address $domain instead of a hostname is not allowed";
129 1 50       13 print "DEBUG: Test Failed - $reason\n" if $params{'debug'};
130 1         15 return (0, $reason);
131             }
132              
133             #Perform the DNS Query - Changed to Send instead of Query method to utilize the ancount method
134 32         155 $packet = $res->send($domain,'MX');
135              
136             #Net::DNS::Resolver had an error
137 32 50       2495687 if (!defined $packet) {
138 0 0       0 print "DEBUG: There was an error retrieving the MX Records for $domain\n" if $params{'debug'};
139 0 0       0 print "DEBUG: Test Passed by Default\n" if $params{'debug'};
140 0         0 return($params{'resolution_problem_return'}, "Test $resolution_problem_status due to a Resolution Problem retrieving the MX Records");
141             }
142              
143 32 50       205 print "DEBUG: Number of Answers in the MX resolution packet is: ".$packet->header->ancount."\n" if $params{'debug'};
144             #Parse the Query
145 32 100       123 if ($packet->header->ancount > 0) {
146 30 50       527 if (defined ($packet->answer)) {
147 30         315 @answer = $packet->answer;
148              
149 30         263 for ($i = 0; $i < scalar(@answer); $i++) {
150 40 100       222 if ($answer[$i]->type ne 'MX') {
151             #DISCARD ANSWER IF THE RECORD IS NOT AN MX RECORD SUCH AS THE CNAME FOR londo.cysticercus.com
152 3 50       103 print "DEBUG: Discarding one non-MX answer of type: ".$answer[$i]->type."\n" if $params{'debug'};
153             } else {
154 37         615 push @unsorted_answer, $answer[$i];
155             }
156             }
157              
158 30         105 undef @answer;
159              
160 30 50       133 print "DEBUG: Number of Answers Left to Check after discarding all but MX: ".scalar(@unsorted_answer)."\n" if $params{'debug'};
161 30 100       112 if (scalar(@unsorted_answer) < 1) {
162 1         6 $check_implicit_mx++;
163             } else {
164             #Sort to put answers into ascending order by mail exchange preference
165 29         159 @answer = sort {$a->preference <=> $b->preference} @unsorted_answer;
  10         98  
166             }
167              
168             #LOOP THROUGH THE ANSWERS WE HAVE
169 30         209 for ($i = 0; $i < scalar(@answer); $i++) {
170 36         126 undef $packet;
171 36 50       122 print "DEBUG: $i - MX Answer - Type: ".$answer[$i]->type." - Exchange: ".$answer[$i]->exchange." - Length: ".length($answer[$i]->exchange)."\n" if $params{'debug'};
172              
173             #localhost isn't a valid MX so return false
174 36 50       216 if ($answer[$i]->exchange eq 'localhost') {
175 0         0 $reason = 'Invalid use of Localhost as an MX record';
176 0 0       0 print "DEBUG: Test Failed - $reason\n" if $params{'debug'};
177 0         0 return (0, $reason);
178             }
179              
180             #IF the exchange is blank and the priority is 0 and it's the last answer, let's fail
181 36 0 33     3037 if ($answer[$i]->exchange eq '' && int($answer[$i]->preference) == 0 && $i == $#answer) {
      33        
182             #Test if there is a Blank MX record in the first slot Per Jan-Pieter Cornet recommendation
183             #and based on http://ietfreport.isoc.org/all-ids/draft-delany-nullmx-00.txt
184 0         0 $reason = 'Domain is publishing a blank MX record at Priority 0';
185 0 0       0 print "DEBUG: Test Failed - $reason\n" if $params{'debug'};
186 0         0 return (0, $reason);
187             }
188              
189             #resolve the exchange record
190 36 100 66     654 if ($answer[$i]->exchange ne '' and $answer[$i]->exchange !~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) {
191 34         1073 $packet = $res->send($answer[$i]->exchange, 'A');
192            
193 34 50       1511983 if (!defined ($packet)) {
194             #THERE WAS AN ERROR TRYING TO RESOLVE THE MAIL EXCHANGE
195 0 0       0 print "DEBUG: Test Passed by Default\n" if $params{'debug'};
196 0         0 return ($params{'resolution_problem_return'}, 'Test '.$resolution_problem_status.' due to a Resolution Problem');
197             }
198 34 50       230 print "DEBUG: $i - Number of Answers in the MX->A resolution packet is: ".$packet->header->ancount."\n" if $params{'debug'};
199              
200             #TEST TO SEE IF IT'S AN AAAA IPv6 RECORD - Thanks to Subramanian MOONESAMY sm@megawatt.resistor.net for pointing this out!
201 34 100 66     202 if (defined $packet && $packet->header->ancount < 1) {
202 6         161 $packet = $res->send($answer[$i]->exchange, 'AAAA');
203              
204 6 50       243543 if (!defined ($packet)) {
205             #THERE WAS AN ERROR TRYING TO RESOLVE THE MAIL EXCHANGE
206 0 0       0 print "DEBUG: Test Passed by Default\n" if $params{'debug'};
207 0         0 return ($params{'resolution_problem_return'}, 'Test '.$resolution_problem_status.' due to a Resolution Problem');
208             }
209 6 50       56 print "DEBUG: $i - Number of Answers in the MX->AAAA resolution packet is: ".$packet->header->ancount."\n" if $params{'debug'};
210             }
211             }
212              
213 36 100 100     872 if (defined $packet && $packet->header->ancount > 0) {
214 29         454 @answer2 = $packet->answer;
215              
216 29 50       316 print "DEBUG: $i - Resolution type of ".$answer[$i]->exchange.": ".$answer2[0]->type."\n" if $params{'debug'};
217 29 100       160 if ($answer2[0]->type eq "A") {
    100          
218 26 50       445 print "DEBUG: $i - A Name Address for ".$answer[$i]->exchange.": ".$answer2[0]->address."\n" if $params{'debug'};
219 26         117 ($rv, $reason) = &invalid_mx($answer2[0]->address);
220 26 100 100     262 if ($rv == 1 or ($rv == 2 && $i == $#answer)) {
    100 66        
221 1 50       6 if ($rv == 2) {
222 1         4 $reason .= ' - All MX Records Failed';
223             }
224 1 50       5 print "DEBUG: Test Failed - $reason\n" if $params{'debug'};
225 1         28 return (0, $reason);
226             } elsif ($rv < 1) {
227 20 50       57 print "DEBUG: Test Passed ".$answer2[0]->address." looks good\n" if $params{'debug'};
228 20         342 return (1, '');
229             }
230             } elsif ($answer2[0]->type eq "CNAME") {
231 2         120 $packet = $res->send($answer2[0]->cname,'A');
232              
233 2 50       28328 if (!defined ($packet)) {
234             #THERE WAS AN ERROR TRYING TO RESOLVE THE CNAME FOR THE MAIL EXCHANGE
235 0 0       0 print "DEBUG: Test Passed by Default\n" if $params{'debug'};
236 0         0 return ($params{'resolution_problem_return'}, 'Test '.$resolution_problem_status.' due to a Resolution Problem');
237             }
238              
239 2 50       9 if ($packet->header->ancount > 0) {
240 2 50       34 if (defined ($packet->answer)) {
241 2         23 @answer3 = $packet->answer;
242 2 50       19 print "DEBUG: $i - CNAME Resolution of Type: ".$answer3[0]->type." - Address: ".$answer3[0]->address."\n" if $params{'debug'};
243 2 100       8 if ($answer3[0]->type eq "A") {
244 1         18 ($rv, $reason) = &invalid_mx($answer3[0]->address);
245 1 50 33     19 if ($rv == 1 or ($rv == 2 && $i == $#answer)) {
    50 33        
246 0 0       0 if ($rv == 2) {
247 0         0 $reason .= ' - All MX Records Failed';
248             }
249 0 0       0 print "DEBUG: Test Failed - $reason\n" if $params{'debug'};
250 0         0 return (0, $reason);
251             } elsif ($rv < 1) {
252 1 50       5 print "DEBUG: Test Passed ".$answer3[0]->address." looks good\n" if $params{'debug'};
253 1         19 return (1,'');
254             }
255             } else {
256             #CNAMEs aren't RFC valid for MX's so if they chained two together, I'm not recursively resolving anymore levels, I'm just failing it
257 1         19 $reason = 'Invalid use of CNAME for MX record';
258 1 50       4 print "DEBUG: Test Failed - $reason\n" if $params{'debug'};
259 1         24 return (0, $reason);
260             }
261             }
262             } else {
263 0 0 0     0 if ($params{'allow_ip_address_as_mx'} > 0 && $answer[$i]->exchange =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) {
264 0         0 ($rv, $reason) = &invalid_mx($answer[$i]->exchange);
265 0 0       0 if ($rv) {
266 0         0 return (0, $reason);
267             } else {
268 0 0       0 print "DEBUG: Test Passed - Allowing IP Address as Hostname\n" if $params{'debug'};
269 0         0 return (1, '');
270             }
271             }
272              
273             #MX RECORD IS A CNAME WHICH DOES NOT RESOLVE
274 0         0 $reason = "MX Record: ".$answer2[0]->cname." does not resolve";
275 0 0       0 print "DEBUG: Test Failed - $reason\n" if $params{'debug'};
276 0         0 return (0, $reason);
277             }
278             }
279             } else { # ! $packet->header->ancount > 0
280              
281             #IF THIS IS THE LAST MX RECORD AND THE EXCHANGE IS BLANK, WE FAIL IT
282 7 50       129 if ($answer[$i]->exchange eq '') {
283 0 0       0 if ($i == $#answer) {
284 0         0 $reason = 'Domain is publishing only invalid and/or blank MX records';
285 0 0       0 print "DEBUG: Test Failed - $reason\n" if $params{'debug'};
286 0         0 return (0, $reason);
287             }
288             } else {
289             #PERHAPS WE'LL ALLOW AN IP ADDRESS AS AN MX FOR CLOWNS WHO CONFIGURE DNS INCORRECTLY
290 7 100 66     168 if ($params{'allow_ip_address_as_mx'} > 0 && $answer[$i]->exchange =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) {
291 2         45 ($rv, $reason) = &invalid_mx($answer[$i]->exchange);
292 2 100       39 if ($rv) {
293 1         24 return (0, $reason);
294             } else {
295 1 50       6 print "DEBUG: Test Passed - Allowing IP Address as Hostname\n" if $params{'debug'};
296 1         22 return (1, '');
297             }
298             }
299             }
300              
301             # Keep looping, unless this was the last answer in the MX
302             # resolution packet.
303 5 100       101 if ($i == $#answer) {
304              
305             #MX RECORD RETURNED DOES NOT RESOLVE
306 3         16 $reason = "MX Record: ".$answer[$i]->exchange." does not resolve";
307 3 50       86 print "DEBUG: Test Failed - $reason\n" if $params{'debug'};
308 3         76 return (0, $reason);
309             }
310             }
311              
312             } # for
313             }
314             } else {
315 2         56 ($rv, $reason) = $check_implicit_mx++;
316             }
317              
318 4 50       97 print "DEBUG: Checking Implicit MX is set to $check_implicit_mx\n" if $params{'debug'};
319              
320 4 100       27 if ($check_implicit_mx > 0) {
321 3         28 ($rv, $reason) = &check_implicit_mx($domain, $res, $params{'debug'}, $params{'resolution_problem_return'});
322 3 50       79 if (defined $rv) {
323 3         167 return ($rv, $reason);
324             }
325             }
326             } else {
327 0 0       0 print "DEBUG: There was an error setting up a Net::DNS::Resolver resource\n" if $params{'debug'};
328 0 0       0 print "DEBUG: Test Passed by Default\n" if $params{'debug'};
329 0         0 return ($params{'resolution_problem_return'}, 'Test '.$resolution_problem_status.' due to a Resolution Problem');
330             }
331              
332 1 50       9 print "DEBUG: Test Passed\n" if $params{'debug'};
333 1         49 return (1,'');
334             }
335              
336             sub check_implicit_mx ($$) {
337 3     3 0 21 my ($SenderDomain, $res, $debug, $resolution_problem_return) = @_;
338            
339 3         15 my ($rv, $reason, $packet, @answer, @answer2, $resolution_problem_status);
340              
341             #CONSTANTS/SETTABLE OPTIONS
342 3   33     23 $resolution_problem_return ||= $RESOLUTION_PROBLEM_RETURN;
343              
344 3 50       19 if ($resolution_problem_return > 0) {
345 3         16 $resolution_problem_status = 'Passed';
346             } else {
347 0         0 $resolution_problem_status = 'Failed';
348             }
349              
350 3 50       16 print "DEBUG: Checking for Implicit MX Records\n" if $debug;
351             #NO MX RECORDS RETURNED - CHECK FOR IMPLICIT MX RECORD BY A RECORD per Jan-Pieter Cornet recommendation
352 3         22 $packet = $res->send($SenderDomain,'A');
353 3 50       211889 if (!defined ($packet)) {
354             #THERE WAS AN ERROR - NO IMPLICIT A RECORD COULD BE RESOLVED
355 0 0       0 print "DEBUG: Test Passed by Default\n" if $debug;
356 0         0 return ($resolution_problem_return, 'Test '.$resolution_problem_status.' due to a Resolution Problem');
357             }
358              
359 3 50       23 print "DEBUG: Number of Answers in the Implicit A record resolution packet is: ".$packet->header->ancount."\n" if $debug;
360 3 100       22 if ($packet->header->ancount > 0) {
361 2         96 @answer = $packet->answer;
362 2 100       38 if ($answer[0]->type eq "A") {
    50          
363 1 50       36 print "DEBUG: $SenderDomain has no MX Records - Using Implicit A Record: ".$answer[0]->address."\n" if $debug;
364 1         10 ($rv, $reason) = &invalid_mx($answer[0]->address);
365 1 50       9 if ($rv) {
366 0 0       0 print "DEBUG: Test Failed - $reason\n" if $debug;
367 0         0 return (0, $reason);
368             } else {
369 1 50       7 print "DEBUG: Test Passed ".$answer[0]->address." looks good\n" if $debug;
370 1         13 return (1, '');
371             }
372             } elsif ($answer[0]->type eq "CNAME") {
373             #IS THIS REALLY A NECESSARY TEST? SHOULD WE BE TESTING FOR IMPLICIT CNAME RECORDS?
374 1 50       37 print "DEBUG: $SenderDomain has no MX Records - Using CNAME to Check for Implicit A Record: ".$answer[0]->cname."\n" if $debug;
375 1         5 $packet = $res->send($answer[0]->cname,'A');
376              
377 1 50       14607 if (!defined ($packet)) {
378             #THERE WAS AN ERROR TRYING TO RESOLVE THE CNAME FOR THE MAIL EXCHANGE
379 0 0       0 print "DEBUG: Test Passed by Default\n" if $debug;
380 0         0 return (1, '');
381             }
382              
383 1 50       4 if ($packet->header->ancount > 0) {
384 1 50       19 if (defined ($packet->answer)) {
385 1         14 @answer2 = $packet->answer;
386 1 50       21 if ($answer2[0]->type eq "A") {
387 0 0       0 print "DEBUG: CNAME Resolution of Type: ".$answer2[0]->type." - Address: ".$answer2[0]->address."\n" if $debug;
388 0         0 ($rv, $reason) = &invalid_mx($answer2[0]->address);
389 0 0       0 if ($rv > 0) {
390 0 0       0 print "DEBUG: Test Failed - $reason\n" if $debug;
391 0         0 return (0, $reason);
392             } else {
393 0 0       0 print "DEBUG: Test Passed ".$answer2[0]->address." looks good\n" if $debug;
394 0         0 return (1, '');
395             }
396             } else {
397             #CNAMEs aren't RFC valid for MX's so if they chained two together, I'm not recursively resolving anymore levels, I'm just failing it
398 1         21 $reason = 'Invalid use of CNAME for Implicit MX record';
399 1 50       36 print "DEBUG: Test Failed - $reason\n" if $debug;
400 1         13 return (0, $reason);
401             }
402             }
403             }
404             }
405             } else {
406 1         31 $reason = "No MX or A Records Exist for $SenderDomain";
407 1 50       7 print "DEBUG: Test Failed - $reason\n" if $debug;
408 1         8 return (0, $reason);
409             }
410 0         0 return undef;
411             }
412              
413             sub invalid_mx {
414 30     30 0 579 my ($ip) = @_;
415 30         77 my ($flag_intranets);
416              
417             #UPDATED MORE ON 11-18-2011 based on RFC 5735
418              
419             #0/8, 255/8, 127/8 aren't a valid MX so return false - added per Matthew van Eerde recomendation
420 30 50       259 if ($ip =~ /^(255|127|0)\./) {
421 0         0 return (1, "Invalid use of 0/8, 255/8 or 127/8 ($ip) as an MX record");
422             }
423              
424 30         90 $flag_intranets = $FLAG_INTRANETS;
425              
426             #10/8
427 30 100 66     193 if ($flag_intranets && $ip =~ /^10\./) {
428 3         26 return (2, "Invalid use of private IP (e.g. $ip) range for MX");
429             }
430             #172.16/12 - Fixed per Matthen van Eerde
431 27 50 33     150 if ($flag_intranets && $ip =~ /^172\.(16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31)\./) {
432 0         0 return (2, "Invalid use of private IP (e.g. $ip) range for MX");
433             }
434             #192.168/16
435 27 100 66     171 if ($flag_intranets && $ip =~ /^192\.168\./) {
436 4         40 return (2, "Invalid use of private IP (e.g. $ip) range for MX");
437             }
438              
439             #DHCP auto-discover added per Matthew van Eerde recomendation 169.254/16
440 23 50       93 if ($ip =~ /^169\.254\./) {
441 0         0 return (1, "Invalid use of a DHCP auto-discover IP range ($ip) as an MX record");
442             }
443              
444             #Multicast 224/8 through 239/8 added per Matthew van Eerde recomendation
445 23 50       70 if ($ip =~ /^(224|225|226|227|228|229|230|231|232|233|234|235|236|237|238|239)\./) {
446 0         0 return (1, "Invalid use of a Multicast IP range ($ip) as an MX record");
447             }
448              
449             #Experimental block - Former Class E - 240.0.0.0/4 courtesy of Mark Damrose
450 23 50       66 if ($ip =~ /^2[45]\d\./) {
451 0         0 return (1, "Invalid use of an experimental IP ($ip) as an MX record");
452             }
453              
454             #Reserved for benchmark tests of interconnect devices 192.18.0.0/15 courtesy of Mark Damrose
455 23 50       67 if ($ip =~ /^192\.1[89]\./) {
456 0         0 return (1, "Invalid use of a reserved IP ($ip) as an MX record");
457             }
458              
459             #Reserved for documentation or published examples 192.0.2.0/24 courtesy of Mark Damrose
460 23 50       72 if ($ip =~ /^192\.0\.2\./) {
461 0         0 return (1, "Invalid use of a reserved IP ($ip) as an MX record");
462             }
463              
464            
465 23         88 return (0,'');
466             }
467              
468             sub int_to_truefalse {
469 34     34 0 99 my ($int) = @_;
470              
471 34 100       107 if ($int) {
472 24         86 return "True";
473             } else {
474 10         43 return "False";
475             }
476             }
477              
478             sub check_email_and_mx {
479 3     3 1 1859 my ($email) = @_;
480 3         7 my ($rv, $fail_reason, $status, $debug);
481              
482 3         7 $debug = 0;
483              
484 3 50       9 $email || return 0;
485            
486 3 50       9 print "DEBUG: e-mail address is: $email
\n" if $debug;
487            
488             # SANITIZE THE E-MAIL ADDRESS OF SPACES
489 3         13 $email =~ s/ //g;
490              
491             # CHECK FOR INCOMPLETE ADDRESSES AT LARGE ISPS
492 3         13 $email =~ s/\@aol\.?$/\@aol.com/i;
493 3         8 $email =~ s/\@hotmail\.?$/\@hotmail.com/i;
494 3         4 $email =~ s/\@gmail\.?$/\@gmail.com/i;
495              
496 3 50       8 print "DEBUG: e-mail address is now: $email
\n" if $debug;
497              
498             # CHECK FOR A VALIDLY CONSTRUCTED E-MAIL ADDRESS
499 3         8 ($rv) = &Net::validMX::check_email_validity($email);
500            
501 3 50       7 if ($rv < 1) {
502 0         0 return($rv, "Failed check_email_validity", $email);
503             }
504              
505             # CHECK FOR VALID MX RECORD
506 3         9 ($rv, $fail_reason) = &Net::validMX::check_valid_mx($email);
507              
508 3 50       70 if ($rv < 1) {
509 0         0 return($rv, $fail_reason, $email);
510             }
511              
512 3         13 return($rv, "Passed", $email);
513             }
514              
515             sub check_email_validity {
516 9     9 1 105 my ($email) = @_;
517 9         15 my ($local);
518              
519             #allows an email address that contains -()/!#$%&*+~. A through Z a through Z and 0 through 9 in a format of [valid]@([valid].[valid]...).[valid]. = will also be
520             #allowed in the username. Thanks to Paul Whittney for reporting the issue.
521              
522             #PER WIKIPEDIA
523             #Per Wikipedia:
524              
525             #The format of email addresses is local-part@domain where the local-part may be up to 64 characters long and the domain name may have a maximum of 253 characters - but the maximum 256 characters length of a forward or reverse path restricts the entire email address to be no more than 254 characters.[1] - formally defined in RFC 5322 (sections 3.2.3 and 3.4.1) and by RFC 5321.
526            
527             #Can't have two dots
528 9 50       35 if ($email =~ /\.\./) {
529 0         0 return 0;
530             }
531              
532             #Can't be longer than 254 chars
533 9 50       25 if (length($email) > 254) {
534 0         0 return 0;
535             }
536              
537             #Can't end in a period
538 9 50       23 if ($email =~ /\.$/) {
539 0         0 return 0;
540             }
541              
542 9 100       144 if ($email =~ /^(.*)@[-()\/!#$%&*+~_A-Za-z0-9\.]+\.[-()\/!#$%&*+~_A-Za-z0-9\.]+$/) {
543            
544 7         56 $local = $1;
545            
546             #check local length
547 7 100       19 if (length($local) > 64) {
548 2         9 return 0;
549             }
550             #no need to check if domain is over 253 chars, as it would not pass both overall length and regex if it was
551              
552             # per RFC 3696 section 3 the local part of an address cannot begin or end with a period
553 5 50 33     46 if ($local =~ /^\./ or $local =~ /\.$/g) {
554 0         0 return 0;
555             }
556            
557             # PURGE ANYTHING EXITED BY BACKSLASH
558 5         15 $local =~ s/\\.//g;
559            
560             # per RFC 3696 section 3 the local part of the email can be quoted, which allows any character to appear if inside quotes
561             # PURGE BEGINNING AND END QUOTE IF IT CONTAINS QUOTES
562 5 50       14 if ($local =~ /"/) {
563 0         0 $local =~ s/^"//g;
564 0         0 $local =~ s/"$//g;
565            
566             # IF IT STILL CONTAINS A QUOTE, IT IS INVALID, OTHERWISE THE LOCAL PART IS VALID
567 0 0       0 if ($local =~ /"/) {
568 0         0 return 0;
569             } else {
570 0         0 return 1;
571             }
572             }
573            
574             # check for allowed characters, per RFC 3696 section 3
575 5 50       28 if ($local =~ /^[\@'-`\/!\?=#\$\%&*+~_A-Za-z0-9\.{}|]+$/) {
576 5         23 return 1;
577             } else {
578 0         0 return 0;
579             }
580              
581             }
582 2         12 return 0;
583              
584             }
585              
586             #get domain name from an email address
587             sub get_domain_from_email {
588 35     35 1 186 my ($email, %params) = @_;
589              
590 35         94 my ($domain, $local);
591              
592 35         78 $domain = $email;
593              
594             #REMOVE ANY LEADING/TRAILING <>'s
595 35         335 $domain =~ s/(^<|>$)//g;
596             #REMOVE ANY LEADING/TRAILING SPACE'S
597 35         289 $domain =~ s/^ *//g;
598 35         379 $domain =~ s/ *$//g;
599             #REMOVE EVERYTHING UP TO THE @ SYMBOL
600 35         196 $domain =~ s/(.*)\@//g;
601              
602 35         122 $local = $1;
603              
604 35 50       117 print "\nDEBUG: Extracted Sender Domain: $domain / Local: $local from $params{'email'}\n" if $params{'debug'};
605              
606 35 100       162 return wantarray ? ($local,$domain) : $domain;
607             }
608              
609             sub dns_lookup {
610 0     0 0   my ($domain, $type) = @_;
611 0           my ($dns, $query);
612              
613 0           $dns = Net::DNS::Resolver->new;
614 0           $query = $dns->search($domain, $type);
615 0 0         if ($query) {
616 0           return $query->answer;
617             } else {
618 0           warn "Error performing $type query for $domain! ". $dns->errorstring;
619             }
620             }
621              
622             sub check_spf_for_domain {
623 0     0 0   my ($domain, %params) = @_;
624 0           my ($dns, $query, $result, $spf_line, @clauses, $clause, $found_spf);
625              
626 0           $dns = Net::DNS::Resolver->new;
627 0           $query = $dns->search($domain, 'TXT');
628 0 0         if (not $query) {
629 0           warn "Error performing TXT query for $domain! ". $dns->errorstring;
630 0           return ("suspect", "no TXT record found");
631             }
632              
633 0           foreach $result ($query->answer) {
634 0 0         next unless $result->type eq 'TXT';
635 0           $spf_line = $result->txtdata;
636              
637 0 0         if ($spf_line =~ /^v=spf[12]/i) {
638 0           $found_spf++;
639              
640             # split into clauses
641 0           @clauses = split / /, $spf_line;
642              
643 0           foreach $clause (@clauses) {
644             # ignore clauses that reject email - only false accepts are good spam indicators
645 0 0         next if $clause =~ /^[-~]/;
646 0 0         if ($clause =~ /^.?all/) {
647             # if accepting email from all, rule is clearly useless
648 0           return ("bad", "use of universal pass rule $clause");
649             }
650             }
651             }
652             }
653              
654 0 0         if ($found_spf == 0) {
    0          
655 0           return ("suspect", "no TXT record matching SPF format found");
656             } elsif ($found_spf > 1) {
657 0           return ("suspect", "multiple TXT records matching SPF format found");
658             }
659              
660 0           return ("valid", undef);
661             }
662              
663             1;
664              
665             __END__