File Coverage

lib/Net/validMX.pm
Criterion Covered Total %
statement 237 338 70.1
branch 154 290 53.1
condition 35 72 48.6
subroutine 14 19 73.6
pod 5 14 35.7
total 445 733 60.7


line stmt bran cond sub pod time code
1             # The "Artistic License"
2             #
3             # Preamble
4             #
5             # The intent of this document is to state the conditions under which a
6             # Package may be copied, such that the Copyright Holder maintains some
7             # semblance of artistic control over the development of the package,
8             # while giving the users of the package the right to use and distribute
9             # the Package in a more-or-less customary fashion, plus the right to make
10             # reasonable modifications.
11             #
12             # Definitions:
13             #
14             # "Package" refers to the collection of files distributed by the
15             # Copyright Holder, and derivatives of that collection of files
16             # created through textual modification.
17             #
18             # "Standard Version" refers to such a Package if it has not been
19             # modified, or has been modified in accordance with the wishes
20             # of the Copyright Holder as specified below.
21             #
22             # "Copyright Holder" is whoever is named in the copyright or
23             # copyrights for the package.
24             #
25             # "You" is you, if you're thinking about copying or distributing
26             # this Package.
27             #
28             # "Reasonable copying fee" is whatever you can justify on the
29             # basis of media cost, duplication charges, time of people involved,
30             # and so on. (You will not be required to justify it to the
31             # Copyright Holder, but only to the computing community at large
32             # as a market that must bear the fee.)
33             #
34             # "Freely Available" means that no fee is charged for the item
35             # itself, though there may be fees involved in handling the item.
36             # It also means that recipients of the item may redistribute it
37             # under the same conditions they received it.
38             #
39             # 1. You may make and give away verbatim copies of the source form of the
40             # Standard Version of this Package without restriction, provided that you
41             # duplicate all of the original copyright notices and associated disclaimers.
42             #
43             # 2. You may apply bug fixes, portability fixes and other modifications
44             # derived from the Public Domain or from the Copyright Holder. A Package
45             # modified in such a way shall still be considered the Standard Version.
46             #
47             # 3. You may otherwise modify your copy of this Package in any way, provided
48             # that you insert a prominent notice in each changed file stating how and
49             # when you changed that file, and provided that you do at least ONE of the
50             # following:
51             #
52             # a) place your modifications in the Public Domain or otherwise make them
53             # Freely Available, such as by posting said modifications to Usenet or
54             # an equivalent medium, or placing the modifications on a major archive
55             # site such as uunet.uu.net, or by allowing the Copyright Holder to include
56             # your modifications in the Standard Version of the Package.
57             #
58             # b) use the modified Package only within your corporation or organization.
59             #
60             # c) rename any non-standard executables so the names do not conflict
61             # with standard executables, which must also be provided, and provide
62             # a separate manual page for each non-standard executable that clearly
63             # documents how it differs from the Standard Version.
64             #
65             # d) make other distribution arrangements with the Copyright Holder.
66             #
67             # 4. You may distribute the programs of this Package in object code or
68             # executable form, provided that you do at least ONE of the following:
69             #
70             # a) distribute a Standard Version of the executables and library files,
71             # together with instructions (in the manual page or equivalent) on where
72             # to get the Standard Version.
73             #
74             # b) accompany the distribution with the machine-readable source of
75             # the Package with your modifications.
76             #
77             # c) give non-standard executables non-standard names, and clearly
78             # document the differences in manual pages (or equivalent), together
79             # with instructions on where to get the Standard Version.
80             #
81             # d) make other distribution arrangements with the Copyright Holder.
82             #
83             # 5. You may charge a reasonable copying fee for any distribution of this
84             # Package. You may charge any fee you choose for support of this
85             # Package. You may not charge a fee for this Package itself. However,
86             # you may distribute this Package in aggregate with other (possibly
87             # commercial) programs as part of a larger (possibly commercial) software
88             # distribution provided that you do not advertise this Package as a
89             # product of your own. You may embed this Package's interpreter within
90             # an executable of yours (by linking); this shall be construed as a mere
91             # form of aggregation, provided that the complete Standard Version of the
92             # interpreter is so embedded.
93             #
94             # 6. The scripts and library files supplied as input to or produced as
95             # output from the programs of this Package do not automatically fall
96             # under the copyright of this Package, but belong to whoever generated
97             # them, and may be sold commercially, and may be aggregated with this
98             # Package. If such scripts or library files are aggregated with this
99             # Package via the so-called "undump" or "unexec" methods of producing a
100             # binary executable image, then distribution of such an image shall
101             # neither be construed as a distribution of this Package nor shall it
102             # fall under the restrictions of Paragraphs 3 and 4, provided that you do
103             # not represent such an executable image as a Standard Version of this
104             # Package.
105             #
106             # 7. C subroutines (or comparably compiled subroutines in other
107             # languages) supplied by you and linked into this Package in order to
108             # emulate subroutines and variables of the language defined by this
109             # Package shall not be considered part of this Package, but are the
110             # equivalent of input as in Paragraph 6, provided these subroutines do
111             # not change the language in any way that would cause it to fail the
112             # regression tests for the language.
113             #
114             # 8. Aggregation of this Package with a commercial distribution is always
115             # permitted provided that the use of this Package is embedded; that is,
116             # when no overt attempt is made to make this Package's interfaces visible
117             # to the end user of the commercial distribution. Such use shall not be
118             # construed as a distribution of this Package.
119             #
120             # 9. The name of the Copyright Holder may not be used to endorse or promote
121             # products derived from this software without specific prior written permission.
122             #
123             # 10. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
124             # IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
125             # WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
126              
127             package Net::validMX;
128              
129 7     7   481155 use strict;
  7         71  
  7         198  
130 7     7   37 use warnings;
  7         11  
  7         240  
131              
132 7     7   3575 use Net::DNS;
  7         676635  
  7         917  
133              
134 7         984 use vars qw(
135             $VERSION
136             @ISA
137             @EXPORT_OK
138             $DEBUG
139             $ALLOW_IP_ADDRESS_AS_MX
140             $FLAG_INTRANETS
141             $RESOLUTION_PROBLEM_RETURN
142 7     7   67 $QUERY_TIMEOUT);
  7         12  
143              
144             BEGIN {
145 7     7   48 require DynaLoader;
146 7         26 require Exporter;
147              
148 7         200 @ISA = qw(Exporter DynaLoader);
149 7         36 $VERSION = '2.5.1';
150 7         22 $DEBUG = 0;
151 7         12 $ALLOW_IP_ADDRESS_AS_MX = 1;
152 7         12 $FLAG_INTRANETS = 1;
153 7         15 $RESOLUTION_PROBLEM_RETURN = 1;
154 7         30820 $QUERY_TIMEOUT = 4;
155             }
156              
157 0     0 0 0 sub version { $VERSION; }
158              
159             @EXPORT_OK = qw(check_valid_mx get_output_result check_email_and_mx check_email_validity get_domain_from_email);
160              
161             sub new {
162 1     1 0 876 my $self = bless {}, shift;
163              
164 1 50 33     12 $DEBUG = $self->{'debug'} if (defined $self->{'debug'} and $self->{'debug'} ne '');
165 1 50 33     6 $ALLOW_IP_ADDRESS_AS_MX = $self->{'allow_ip_address_as_mx'} if (defined $self->{'allow_ip_address_as_mx'} and $self->{'allow_ip_address_as_mx'} ne '');
166 1 50 33     3 $FLAG_INTRANETS = $self->{'flag_intranets'} if (defined $self->{'flag_intranets'} and $self->{'flag_intranets'} ne '');
167 1 50 33     5 $RESOLUTION_PROBLEM_RETURN = $self->{'resolution_problem_return'} if (defined $self->{'resolution_problem_return'} and $self->{'resolution_problem_return'} ne '');
168 1 50 33     4 $QUERY_TIMEOUT = $self->{'query_timeout'} if (defined $self->{'query_timeout'} and $self->{'query_timeout'} ne '');
169              
170 1         3 return $self;
171             }
172              
173             sub get_debug {
174 0     0 0 0 return $DEBUG;
175             }
176              
177             sub set_debug {
178 0     0 0 0 my $debug = shift;
179 0         0 $DEBUG = $debug;
180             }
181              
182             sub get_output_result {
183 36     36 0 648 my ($email, $rv, $reason) = @_;
184 36         60 my ($output);
185              
186 36         145 $output = "$email\n\tValid MX? ".Net::validMX::int_to_truefalse($rv);
187 36 100       112 if ($reason ne '') {
188 14         50 $output .= " - $reason";
189             }
190 36         69 $output .= "\n\n";
191              
192 36         1722 return $output;
193             }
194              
195             sub check_valid_mx {
196             #Based on Idea from Les Miksell and much input from Jan Pieter Cornet
197             #KAM 9-12-05 updated 10-24-05 & 11-3-05.
198             #takes the email address, extracts the domain name and performs multiple MX tests to see if the domain has valid
199             #MX exchange records
200              
201 37     37 1 26346 my ($res, $packet, @answer, $domain, @answer2, @answer3, $rv, $reason, $i, @unsorted_answer);
202 37         0 my ($check_implicit_mx, %params, $self, $ref, $resolution_problem_status);
203              
204             #print "DEBUG: ref for \$_[0] ".ref($_[0]). "\n";
205             #IN OO INSTEAD OF PROCEDURAL MODE?
206 37 100       196 if (uc(ref($_[0])) eq 'NET::VALIDMX') {
207 1         3 $self = shift(@_);
208             #foreach $ref (keys %$self) {
209             # print "DEBUG: OO MODE - $ref: $self->{$ref} \n";
210             #}
211             }
212              
213             #DID WE RECEIVE A HASH INSTEAD OF A SINGLE EMAIL?
214 37 100       146 if ($#_ % 2 == 0) {
215 35         119 ($params{'email'}) = @_;
216             } else {
217 2         8 %params = @_;
218             }
219              
220 37 50 66     123 $params{'email'} || $params{'sender'} || return (0, 'A blank email address will not be tested.');
221              
222             #CONSTANTS / SETTABLE OPTIONS
223 36 100       142 $params{'debug'} = $DEBUG unless (defined $params{'debug'});
224 36 100       135 $params{'allow_ip_address_as_mx'} = $ALLOW_IP_ADDRESS_AS_MX unless (defined $params{'allow_ip_address_as_mx'});
225 36 50       124 $params{'resolution_problem_return'} = $RESOLUTION_PROBLEM_RETURN unless (defined $params{'resolution_problem_return'});
226 36 50       122 $params{'query_timeout'} = $QUERY_TIMEOUT unless (defined $params{'query_timeout'});
227              
228 36 50       92 if ($params{'resolution_problem_return'} > 0) {
229 36         79 $resolution_problem_status = 'Passed';
230             } else {
231 0         0 $resolution_problem_status = 'Failed';
232             }
233              
234 36 100       116 print "DEBUG: function debug setting is $params{'debug'}\n" if $params{'debug'};
235 36 100       94 print "DEBUG: function allow_ip_address_as_mx setting is $params{'allow_ip_address_as_mx'}\n" if $params{'debug'};
236 36 100       120 print "DEBUG: function resolution_problem_return setting is $params{'resolution_problem_return'}\n" if $params{'debug'};
237 36 100       91 print "DEBUG: function query_timeout setting is $params{'query_timeout'}\n" if $params{'debug'};
238              
239             #FLAGS - I THINK THIS HAS A LOGIC ISSUE - I LIKELY MEANT ALLOW_IMPLICIT_MX as an option FIX
240 36         54 $check_implicit_mx = 0;
241              
242             #Setup a DNS Resolver Resource
243 36         279 $res = Net::DNS::Resolver->new;
244              
245 36 50       5495 if (defined ($res)) {
246 36         61 $check_implicit_mx = 0;
247 36         190 $res->defnames(0); #Turn off appending the default domain for names that have no dots just in case
248 36         599 $res->searchlist(); #Set the search list to undefined just in case
249              
250             #We have also set the default timeout to only 4 seconds which means we might get network
251             #delays which we do not want to handle as an error.
252 36         659 $res->tcp_timeout($params{'query_timeout'}); #Number of Seconds before query will fail
253 36         477 $res->udp_timeout($params{'query_timeout'}); #Number of Seconds before query will fail
254              
255             #Strip domain name from an email address
256 36         430 $domain = get_domain_from_email($params{'email'});
257              
258             #Deny Explicit IP Address Domains
259 36 100       132 if ($domain =~ /^\[.*\]$/) {
260 1         4 $reason = "Use of IP Address $domain instead of a hostname is not allowed";
261 1 50       4 print "DEBUG: Test Failed - $reason\n" if $params{'debug'};
262 1         11 return (0, $reason);
263             }
264              
265             #Perform the DNS Query - Changed to Send instead of Query method to utilize the ancount method
266 35         133 $packet = $res->send($domain,'MX');
267              
268             #Net::DNS::Resolver had an error
269 35 50       1680830 if (!defined $packet) {
270 0 0       0 print "DEBUG: There was an error retrieving the MX Records for $domain\n" if $params{'debug'};
271 0 0       0 print "DEBUG: Test Passed by Default\n" if $params{'debug'};
272 0         0 return($params{'resolution_problem_return'}, "Test $resolution_problem_status due to a Resolution Problem retrieving the MX Records");
273             }
274              
275 35 100       167 print "DEBUG: Number of Answers in the MX resolution packet is: ".$packet->header->ancount."\n" if $params{'debug'};
276             #Parse the Query
277 35 100       238 if ($packet->header->ancount > 0) {
278 33 50       729 if (defined ($packet->answer)) {
279 33         349 @answer = $packet->answer;
280              
281 33         337 for ($i = 0; $i < scalar(@answer); $i++) {
282 43 100       204 if ($answer[$i]->type ne 'MX') {
283             #DISCARD ANSWER IF THE RECORD IS NOT AN MX RECORD SUCH AS THE CNAME FOR londo.cysticercus.com
284 4 50       67 print "DEBUG: Discarding one non-MX answer of type: ".$answer[$i]->type."\n" if $params{'debug'};
285             } else {
286 39         705 push @unsorted_answer, $answer[$i];
287             }
288             }
289              
290 33         96 undef @answer;
291              
292 33 100       131 print "DEBUG: Number of Answers Left to Check after discarding all but MX: ".scalar(@unsorted_answer)."\n" if $params{'debug'};
293 33 100       111 if (scalar(@unsorted_answer) < 1) {
294 2         9 $check_implicit_mx++;
295             } else {
296             #Sort to put answers into ascending order by mail exchange preference
297 31         137 @answer = sort {$a->preference <=> $b->preference} @unsorted_answer;
  10         101  
298             }
299              
300             #LOOP THROUGH THE ANSWERS WE HAVE
301 33         222 for ($i = 0; $i < scalar(@answer); $i++) {
302 38         97 undef $packet;
303 38 100       112 print "DEBUG: $i - MX Answer - Type: ".$answer[$i]->type." - Exchange: ".$answer[$i]->exchange." - Length: ".length($answer[$i]->exchange)."\n" if $params{'debug'};
304              
305             #localhost isn't a valid MX so return false
306 38 50       266 if ($answer[$i]->exchange eq 'localhost') {
307 0         0 $reason = 'Invalid use of Localhost as an MX record';
308 0 0       0 print "DEBUG: Test Failed - $reason\n" if $params{'debug'};
309 0         0 return (0, $reason);
310             }
311              
312             #IF the exchange is blank and the priority is 0 and it's the last answer, let's fail
313 38 0 33     2619 if ($answer[$i]->exchange eq '' && int($answer[$i]->preference) == 0 && $i == $#answer) {
      33        
314             #Test if there is a Blank MX record in the first slot Per Jan-Pieter Cornet recommendation
315             #and based on http://ietfreport.isoc.org/all-ids/draft-delany-nullmx-00.txt
316 0         0 $reason = 'Domain is publishing a blank MX record at Priority 0';
317 0 0       0 print "DEBUG: Test Failed - $reason\n" if $params{'debug'};
318 0         0 return (0, $reason);
319             }
320              
321             #resolve the exchange record
322 38 100 66     595 if ($answer[$i]->exchange ne '' and $answer[$i]->exchange !~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) {
323 36         1091 $packet = $res->send($answer[$i]->exchange, 'A');
324            
325 36 50       723996 if (!defined ($packet)) {
326             #THERE WAS AN ERROR TRYING TO RESOLVE THE MAIL EXCHANGE
327 0 0       0 print "DEBUG: Test Passed by Default\n" if $params{'debug'};
328 0         0 return ($params{'resolution_problem_return'}, 'Test '.$resolution_problem_status.' due to a Resolution Problem');
329             }
330 36 100       163 print "DEBUG: $i - Number of Answers in the MX->A resolution packet is: ".$packet->header->ancount."\n" if $params{'debug'};
331              
332             #TEST TO SEE IF IT'S AN AAAA IPv6 RECORD - Thanks to Subramanian MOONESAMY sm@megawatt.resistor.net for pointing this out!
333 36 100 66     239 if (defined $packet && $packet->header->ancount < 1) {
334 7         171 $packet = $res->send($answer[$i]->exchange, 'AAAA');
335              
336 7 50       259659 if (!defined ($packet)) {
337             #THERE WAS AN ERROR TRYING TO RESOLVE THE MAIL EXCHANGE
338 0 0       0 print "DEBUG: Test Passed by Default\n" if $params{'debug'};
339 0         0 return ($params{'resolution_problem_return'}, 'Test '.$resolution_problem_status.' due to a Resolution Problem');
340             }
341 7 50       41 print "DEBUG: $i - Number of Answers in the MX->AAAA resolution packet is: ".$packet->header->ancount."\n" if $params{'debug'};
342             }
343             }
344              
345 38 100 100     819 if (defined $packet && $packet->header->ancount > 0) {
346 31         465 @answer2 = $packet->answer;
347              
348 31 100       312 print "DEBUG: $i - Resolution type of ".$answer[$i]->exchange.": ".$answer2[0]->type."\n" if $params{'debug'};
349 31 100       198 if ($answer2[0]->type =~ /^A{1,4}/) {
    50          
350 29 100       633 print "DEBUG: $i - A Name Address for ".$answer[$i]->exchange.": ".$answer2[0]->address."\n" if $params{'debug'};
351 29         224 ($rv, $reason) = invalid_mx($answer2[0]->address);
352 29 100 100     251 if ($rv == 1 or ($rv == 2 && $i == $#answer)) {
    100 100        
353 2 100       7 if ($rv == 2) {
354 1         4 $reason .= ' - All MX Records Failed';
355             }
356 2 50       10 print "DEBUG: Test Failed - $reason\n" if $params{'debug'};
357 2         51 return (0, $reason);
358             } elsif ($rv < 1) {
359 22 100       60 print "DEBUG: Test Passed ".$answer2[0]->address." looks good\n" if $params{'debug'};
360 22         430 return (1, '');
361             }
362             } elsif ($answer2[0]->type eq "CNAME") {
363 2         93 $packet = $res->send($answer2[0]->cname,'A');
364              
365 2 50       7323 if (!defined ($packet)) {
366             #THERE WAS AN ERROR TRYING TO RESOLVE THE CNAME FOR THE MAIL EXCHANGE
367 0 0       0 print "DEBUG: Test Passed by Default\n" if $params{'debug'};
368 0         0 return ($params{'resolution_problem_return'}, 'Test '.$resolution_problem_status.' due to a Resolution Problem');
369             }
370              
371 2 50       14 if ($packet->header->ancount > 0) {
372 2 50       37 if (defined ($packet->answer)) {
373 2         43 @answer3 = $packet->answer;
374 2 50       23 print "DEBUG: $i - CNAME Resolution of Type: ".$answer3[0]->type." - Address: ".$answer3[0]->address."\n" if $params{'debug'};
375 2 100       34 if ($answer3[0]->type eq "A") {
376 1         25 ($rv, $reason) = invalid_mx($answer3[0]->address);
377 1 50 33     14 if ($rv == 1 or ($rv == 2 && $i == $#answer)) {
    50 33        
378 0 0       0 if ($rv == 2) {
379 0         0 $reason .= ' - All MX Records Failed';
380             }
381 0 0       0 print "DEBUG: Test Failed - $reason\n" if $params{'debug'};
382 0         0 return (0, $reason);
383             } elsif ($rv < 1) {
384 1 50       6 print "DEBUG: Test Passed ".$answer3[0]->address." looks good\n" if $params{'debug'};
385 1         23 return (1,'');
386             }
387             } else {
388             #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
389 1         21 $reason = 'Invalid use of CNAME for MX record';
390 1 50       3 print "DEBUG: Test Failed - $reason\n" if $params{'debug'};
391 1         21 return (0, $reason);
392             }
393             }
394             } else {
395 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}$/) {
396 0         0 ($rv, $reason) = invalid_mx($answer[$i]->exchange);
397 0 0       0 if ($rv) {
398 0         0 return (0, $reason);
399             } else {
400 0 0       0 print "DEBUG: Test Passed - Allowing IP Address as Hostname\n" if $params{'debug'};
401 0         0 return (1, '');
402             }
403             }
404              
405             #MX RECORD IS A CNAME WHICH DOES NOT RESOLVE
406 0         0 $reason = "MX Record: ".$answer2[0]->cname." does not resolve";
407 0 0       0 print "DEBUG: Test Failed - $reason\n" if $params{'debug'};
408 0         0 return (0, $reason);
409             }
410             }
411             } else { # ! $packet->header->ancount > 0
412              
413             #IF THIS IS THE LAST MX RECORD AND THE EXCHANGE IS BLANK, WE FAIL IT
414 7 50       127 if ($answer[$i]->exchange eq '') {
415 0 0       0 if ($i == $#answer) {
416 0         0 $reason = 'Domain is publishing only invalid and/or blank MX records';
417 0 0       0 print "DEBUG: Test Failed - $reason\n" if $params{'debug'};
418 0         0 return (0, $reason);
419             }
420             } else {
421             #PERHAPS WE'LL ALLOW AN IP ADDRESS AS AN MX FOR CLOWNS WHO CONFIGURE DNS INCORRECTLY
422 7 100 66     162 if ($params{'allow_ip_address_as_mx'} > 0 && $answer[$i]->exchange =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) {
423 2         69 ($rv, $reason) = invalid_mx($answer[$i]->exchange);
424 2 100       13 if ($rv) {
425 1         24 return (0, $reason);
426             } else {
427 1 50       4 print "DEBUG: Test Passed - Allowing IP Address as Hostname\n" if $params{'debug'};
428 1         22 return (1, '');
429             }
430             }
431             }
432              
433             # Keep looping, unless this was the last answer in the MX
434             # resolution packet.
435             # XXX $packet->header->ancount, in the case of corrupt packets,
436             # may differ from the actual number of records and may return unwanted failures
437 5 100       89 if ($i == $#answer) {
438              
439             #MX RECORD RETURNED DOES NOT RESOLVE
440 3         13 $reason = "MX Record: ".$answer[$i]->exchange." does not resolve";
441 3 50       48 print "DEBUG: Test Failed - $reason\n" if $params{'debug'};
442 3         83 return (0, $reason);
443             }
444             }
445              
446             } # for
447             }
448             } else {
449 2         41 ($rv, $reason) = $check_implicit_mx++;
450             }
451              
452 4 50       25 print "DEBUG: Checking Implicit MX is set to $check_implicit_mx\n" if $params{'debug'};
453              
454 4 50       19 if ($check_implicit_mx > 0) {
455 4         23 ($rv, $reason) = check_implicit_mx($domain, $res, $params{'debug'}, $params{'resolution_problem_return'});
456 4 50       71 if (defined $rv) {
457 4         118 return ($rv, $reason);
458             }
459             }
460             } else {
461 0 0       0 print "DEBUG: There was an error setting up a Net::DNS::Resolver resource\n" if $params{'debug'};
462 0 0       0 print "DEBUG: Test Passed by Default\n" if $params{'debug'};
463 0         0 return ($params{'resolution_problem_return'}, 'Test '.$resolution_problem_status.' due to a Resolution Problem');
464             }
465              
466 0 0       0 print "DEBUG: Test Passed\n" if $params{'debug'};
467 0         0 return (1,'');
468             }
469              
470             sub check_implicit_mx {
471 4     4 0 15 my ($SenderDomain, $res, $debug, $resolution_problem_return) = @_;
472            
473 4         10 my ($rv, $reason, $packet, @answer, @answer2, $resolution_problem_status);
474              
475             #CONSTANTS/SETTABLE OPTIONS
476 4   33     14 $resolution_problem_return ||= $RESOLUTION_PROBLEM_RETURN;
477              
478 4 50       14 if ($resolution_problem_return > 0) {
479 4         16 $resolution_problem_status = 'Passed';
480             } else {
481 0         0 $resolution_problem_status = 'Failed';
482             }
483              
484 4 50       14 print "DEBUG: Checking for Implicit MX Records\n" if $debug;
485             #NO MX RECORDS RETURNED - CHECK FOR IMPLICIT MX RECORD BY A RECORD per Jan-Pieter Cornet recommendation
486 4         20 $packet = $res->send($SenderDomain,'A');
487 4 50       113621 if (!defined ($packet)) {
488             #THERE WAS AN ERROR - NO IMPLICIT A RECORD COULD BE RESOLVED
489 0 0       0 print "DEBUG: Test Passed by Default\n" if $debug;
490 0         0 return ($resolution_problem_return, 'Test '.$resolution_problem_status.' due to a Resolution Problem');
491             }
492              
493 4 50       24 print "DEBUG: Number of Answers in the Implicit A record resolution packet is: ".$packet->header->ancount."\n" if $debug;
494 4 100       24 if ($packet->header->ancount > 0) {
495 3         56 @answer = $packet->answer;
496 3 100       39 if ($answer[0]->type eq "A") {
    50          
497 1 50       22 print "DEBUG: $SenderDomain has no MX Records - Using Implicit A Record: ".$answer[0]->address."\n" if $debug;
498 1         8 ($rv, $reason) = invalid_mx($answer[0]->address);
499 1 50       6 if ($rv) {
500 0 0       0 print "DEBUG: Test Failed - $reason\n" if $debug;
501 0         0 return (0, $reason);
502             } else {
503 1 50       4 print "DEBUG: Test Passed ".$answer[0]->address." looks good\n" if $debug;
504 1         35 return (1, '');
505             }
506             } elsif ($answer[0]->type eq "CNAME") {
507             #IS THIS REALLY A NECESSARY TEST? SHOULD WE BE TESTING FOR IMPLICIT CNAME RECORDS?
508 2 50       61 print "DEBUG: $SenderDomain has no MX Records - Using CNAME to Check for Implicit A Record: ".$answer[0]->cname."\n" if $debug;
509 2         10 $packet = $res->send($answer[0]->cname,'A');
510              
511 2 50       6464 if (!defined ($packet)) {
512             #THERE WAS AN ERROR TRYING TO RESOLVE THE CNAME FOR THE MAIL EXCHANGE
513 0 0       0 print "DEBUG: Test Passed by Default\n" if $debug;
514 0         0 return (1, '');
515             }
516              
517 2 50       10 if ($packet->header->ancount > 0) {
518 2 50       39 if (defined ($packet->answer)) {
519 2         60 @answer2 = $packet->answer;
520 2 100       24 if ($answer2[0]->type eq "A") {
521 1 50       20 print "DEBUG: CNAME Resolution of Type: ".$answer2[0]->type." - Address: ".$answer2[0]->address."\n" if $debug;
522 1         5 ($rv, $reason) = invalid_mx($answer2[0]->address);
523 1 50       7 if ($rv > 0) {
524 0 0       0 print "DEBUG: Test Failed - $reason\n" if $debug;
525 0         0 return (0, $reason);
526             } else {
527 1 50       5 print "DEBUG: Test Passed ".$answer2[0]->address." looks good\n" if $debug;
528 1         9 return (1, '');
529             }
530             } else {
531             #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
532 1         20 $reason = 'Invalid use of CNAME for Implicit MX record';
533 1 50       26 print "DEBUG: Test Failed - $reason\n" if $debug;
534 1         9 return (0, $reason);
535             }
536             }
537             }
538             }
539             } else {
540 1         26 $reason = "No MX or A Records Exist for $SenderDomain";
541 1 50       4 print "DEBUG: Test Failed - $reason\n" if $debug;
542 1         10 return (0, $reason);
543             }
544 0         0 return;
545             }
546              
547             sub invalid_mx {
548 34     34 0 589 my ($ip) = @_;
549 34         68 my ($flag_intranets);
550              
551             #UPDATED MORE ON 11-18-2011 based on RFC 5735
552              
553             #0/8, 255/8, 127/8 aren't a valid MX so return false - added per Matthew van Eerde recomendation
554 34 50       202 if ($ip =~ /^(255|127|0)\./) {
555 0         0 return (1, "Invalid use of 0/8, 255/8 or 127/8 ($ip) as an MX record");
556             }
557              
558 34         78 $flag_intranets = $FLAG_INTRANETS;
559              
560             #10/8
561 34 100 66     185 if ($flag_intranets && $ip =~ /^10\./) {
562 3         26 return (2, "Invalid use of private IP (e.g. $ip) range for MX");
563             }
564             #172.16/12 - Fixed per Matthen van Eerde
565 31 50 33     196 if ($flag_intranets && $ip =~ /^172\.(16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31)\./) {
566 0         0 return (2, "Invalid use of private IP (e.g. $ip) range for MX");
567             }
568             #192.168/16
569 31 100 66     167 if ($flag_intranets && $ip =~ /^192\.168\./) {
570 4         35 return (2, "Invalid use of private IP (e.g. $ip) range for MX");
571             }
572              
573             #fc00::/7
574 27 50 33     179 if ($flag_intranets && $ip =~ /^fc00\:0\:/i) {
575 0         0 return (2, "Invalid use of unique local address (e.g. $ip) range for MX");
576             }
577              
578             #fd00::/8
579 27 50 33     142 if ($flag_intranets && $ip =~ /^fd00\:0\:/i) {
580 0         0 return (2, "Invalid use of private IP (e.g. $ip) range for MX");
581             }
582              
583             #DHCP auto-discover added per Matthew van Eerde recomendation 169.254/16
584 27 50       81 if ($ip =~ /^169\.254\./) {
585 0         0 return (1, "Invalid use of a DHCP auto-discover IP range ($ip) as an MX record");
586             }
587              
588             #IPv6 link-local addresses fe80::/10
589 27 100       88 if ($ip =~ /^fe80\:0\:/i) {
590 1         8 return (1, "Invalid use of a link-local IP range ($ip) as an MX record");
591             }
592              
593             #Multicast 224/8 through 239/8 added per Matthew van Eerde recomendation
594 26 50       76 if ($ip =~ /^(224|225|226|227|228|229|230|231|232|233|234|235|236|237|238|239)\./) {
595 0         0 return (1, "Invalid use of a Multicast IP range ($ip) as an MX record");
596             }
597              
598             #Experimental block - Former Class E - 240.0.0.0/4 courtesy of Mark Damrose
599 26 50       73 if ($ip =~ /^2[45]\d\./) {
600 0         0 return (1, "Invalid use of an experimental IP ($ip) as an MX record");
601             }
602              
603             #Reserved for benchmark tests of interconnect devices 192.18.0.0/15 courtesy of Mark Damrose
604 26 50       59 if ($ip =~ /^192\.1[89]\./) {
605 0         0 return (1, "Invalid use of a reserved IP ($ip) as an MX record");
606             }
607              
608             #Reserved for documentation or published examples 192.0.2.0/24 courtesy of Mark Damrose
609 26 50       79 if ($ip =~ /^192\.0\.2\./) {
610 0         0 return (1, "Invalid use of a reserved IP ($ip) as an MX record");
611             }
612              
613            
614 26         95 return (0,'');
615             }
616              
617             sub int_to_truefalse {
618 36     36 0 83 my ($int) = @_;
619              
620 36 100       85 if ($int) {
621 25         82 return "True";
622             } else {
623 11         35 return "False";
624             }
625             }
626              
627             sub check_email_and_mx {
628 3     3 1 2522 my ($email) = @_;
629 3         6 my ($rv, $fail_reason, $status, $debug);
630              
631 3         7 $debug = 0;
632              
633 3 50       10 $email || return 0;
634            
635 3 50       9 print "DEBUG: e-mail address is: $email
\n" if $debug;
636            
637             # SANITIZE THE E-MAIL ADDRESS OF SPACES
638 3         9 $email =~ s/ //g;
639              
640             # CHECK FOR INCOMPLETE ADDRESSES AT LARGE ISPS
641 3         15 $email =~ s/\@aol\.?$/\@aol.com/i;
642 3         7 $email =~ s/\@hotmail\.?$/\@hotmail.com/i;
643 3         7 $email =~ s/\@gmail\.?$/\@gmail.com/i;
644              
645 3 50       7 print "DEBUG: e-mail address is now: $email
\n" if $debug;
646              
647             # CHECK FOR A VALIDLY CONSTRUCTED E-MAIL ADDRESS
648 3         10 ($rv) = Net::validMX::check_email_validity($email);
649            
650 3 50       12 if ($rv < 1) {
651 0         0 return($rv, "Failed check_email_validity", $email);
652             }
653              
654             # CHECK FOR VALID MX RECORD
655 3         9 ($rv, $fail_reason) = Net::validMX::check_valid_mx($email);
656              
657 3 50       60 if ($rv < 1) {
658 0         0 return($rv, $fail_reason, $email);
659             }
660              
661 3         14 return($rv, "Passed", $email);
662             }
663              
664             sub check_email_validity {
665 9     9 1 921 my ($email) = @_;
666 9         12 my ($local);
667              
668             #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
669             #allowed in the username. Thanks to Paul Whittney for reporting the issue.
670              
671             #PER WIKIPEDIA
672             #Per Wikipedia:
673              
674             #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.
675            
676             #Can't have two dots
677 9 50       38 if ($email =~ /\.\./) {
678 0         0 return 0;
679             }
680              
681             #Can't be longer than 254 chars
682 9 50       59 if (length($email) > 254) {
683 0         0 return 0;
684             }
685              
686             #Can't end in a period
687 9 50       27 if ($email =~ /\.$/) {
688 0         0 return 0;
689             }
690              
691 9 100       146 if ($email =~ /^(.*)@[-()\/!#$%&*+~_A-Za-z0-9\.]+\.[-()\/!#$%&*+~_A-Za-z0-9\.]+$/) {
692            
693 7         22 $local = $1;
694            
695             #check local length
696 7 100       21 if (length($local) > 64) {
697 2         12 return 0;
698             }
699             #no need to check if domain is over 253 chars, as it would not pass both overall length and regex if it was
700              
701             # per RFC 3696 section 3 the local part of an address cannot begin or end with a period
702 5 50 33     32 if ($local =~ /^\./ or $local =~ /\.$/g) {
703 0         0 return 0;
704             }
705            
706             # PURGE ANYTHING EXITED BY BACKSLASH
707 5         13 $local =~ s/\\.//g;
708            
709             # per RFC 3696 section 3 the local part of the email can be quoted, which allows any character to appear if inside quotes
710             # PURGE BEGINNING AND END QUOTE IF IT CONTAINS QUOTES
711 5 50       15 if ($local =~ /"/) {
712 0         0 $local =~ s/^"//g;
713 0         0 $local =~ s/"$//g;
714            
715             # IF IT STILL CONTAINS A QUOTE, IT IS INVALID, OTHERWISE THE LOCAL PART IS VALID
716 0 0       0 if ($local =~ /"/) {
717 0         0 return 0;
718             } else {
719 0         0 return 1;
720             }
721             }
722            
723             # check for allowed characters, per RFC 3696 section 3
724 5 50       58 if ($local =~ /^[\@'-`\/!\?=#\$\%&*+~_A-Za-z0-9\.{}|]+$/) {
725 5         32 return 1;
726             } else {
727 0         0 return 0;
728             }
729              
730             }
731 2         15 return 0;
732              
733             }
734              
735             #get domain name from an email address
736             sub get_domain_from_email {
737 38     38 1 1017 my ($email, %params) = @_;
738              
739 38         78 my ($domain, $local);
740              
741 38         61 $domain = $email;
742              
743             #REMOVE ANY LEADING/TRAILING <>'s
744 38         305 $domain =~ s/(^<|>$)//g;
745             #REMOVE ANY LEADING/TRAILING SPACE'S
746 38         214 $domain =~ s/^ *//g;
747 38         353 $domain =~ s/ *$//g;
748             #REMOVE EVERYTHING UP TO THE @ SYMBOL
749 38         218 $domain =~ s/(.*)\@//g;
750              
751 38         111 $local = $1;
752              
753 38 50       117 print "\nDEBUG: Extracted Sender Domain: $domain / Local: $local from $params{'email'}\n" if $params{'debug'};
754              
755 38 100       145 return wantarray ? ($local,$domain) : $domain;
756             }
757              
758             sub dns_lookup {
759 0     0 0   my ($domain, $type) = @_;
760 0           my ($dns, $query);
761              
762 0           $dns = Net::DNS::Resolver->new;
763 0           $query = $dns->search($domain, $type);
764 0 0         if ($query) {
765 0           return $query->answer;
766             } else {
767 0           warn "Error performing $type query for $domain! ". $dns->errorstring;
768             }
769             }
770              
771             sub check_spf_for_domain {
772 0     0 1   my ($domain, %params) = @_;
773 0           my ($dns, $query, $spf_line, @clauses, $found_spf);
774              
775 0           $dns = Net::DNS::Resolver->new;
776 0           $query = $dns->search($domain, 'TXT');
777 0 0         if (not $query) {
778 0           warn "Error performing TXT query for $domain! ". $dns->errorstring;
779 0           return ("suspect", "no TXT record found");
780             }
781              
782 0           foreach my $result ($query->answer) {
783 0 0         next unless $result->type eq 'TXT';
784 0           $spf_line = $result->txtdata;
785              
786 0 0         if ($spf_line =~ /^v=spf[12]/i) {
787 0           $found_spf++;
788              
789             # split into clauses
790 0           @clauses = split / /, $spf_line;
791              
792 0           foreach my $clause (@clauses) {
793             # ignore clauses that reject email - only false accepts are good spam indicators
794 0 0         next if $clause =~ /^[-~]/;
795 0 0         if ($clause =~ /^.?all/) {
796             # if accepting email from all, rule is clearly useless
797 0           return ("bad", "use of universal pass rule $clause");
798             }
799             }
800             }
801             }
802              
803 0 0         if ($found_spf == 0) {
    0          
804 0           return ("suspect", "no TXT record matching SPF format found");
805             } elsif ($found_spf > 1) {
806 0           return ("suspect", "multiple TXT records matching SPF format found");
807             }
808              
809 0           return ("valid", undef);
810             }
811              
812             1;
813              
814             __END__