line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Mail::SPF::Query; |
2
|
|
|
|
|
|
|
|
3
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
4
|
|
|
|
|
|
|
# Mail::SPF::Query |
5
|
|
|
|
|
|
|
# Test an IP / sender address pair for SPF authorization |
6
|
|
|
|
|
|
|
# |
7
|
|
|
|
|
|
|
# http://www.openspf.org |
8
|
|
|
|
|
|
|
# http://search.cpan.org/dist/Mail-SPF-Query |
9
|
|
|
|
|
|
|
# |
10
|
|
|
|
|
|
|
# Copyright (C) 2003-2005 Meng Weng Wong |
11
|
|
|
|
|
|
|
# Contributions by various members of the SPF project |
12
|
|
|
|
|
|
|
# License: like Perl, i.e. GPL-2 and Artistic License |
13
|
|
|
|
|
|
|
# |
14
|
|
|
|
|
|
|
# $Id: Query.pm 143 2006-02-26 17:41:10Z julian $ |
15
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
16
|
|
|
|
|
|
|
|
17
|
1
|
|
|
1
|
|
44823
|
use 5.006; |
|
1
|
|
|
|
|
4
|
|
|
1
|
|
|
|
|
43
|
|
18
|
|
|
|
|
|
|
|
19
|
1
|
|
|
1
|
|
5
|
use strict; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
30
|
|
20
|
1
|
|
|
1
|
|
5
|
use warnings; |
|
1
|
|
|
|
|
6
|
|
|
1
|
|
|
|
|
32
|
|
21
|
1
|
|
|
1
|
|
4
|
no warnings 'uninitialized'; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
57
|
|
22
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
our $VERSION = '1.999.1'; # fake version for EU::MM and CPAN |
24
|
|
|
|
|
|
|
$VERSION = '1.999001'; # real numerical version |
25
|
|
|
|
|
|
|
|
26
|
1
|
|
|
1
|
|
1194
|
use Sys::Hostname::Long; |
|
1
|
|
|
|
|
4001
|
|
|
1
|
|
|
|
|
74
|
|
27
|
1
|
|
|
1
|
|
1035
|
use Net::DNS qw(); # by default it exports mx, which we define. |
|
1
|
|
|
|
|
103006
|
|
|
1
|
|
|
|
|
30
|
|
28
|
1
|
|
|
1
|
|
991
|
use Net::CIDR::Lite; |
|
1
|
|
|
|
|
4738
|
|
|
1
|
|
|
|
|
33
|
|
29
|
1
|
|
|
1
|
|
1070
|
use URI::Escape; |
|
1
|
|
|
|
|
1595
|
|
|
1
|
|
|
|
|
6619
|
|
30
|
|
|
|
|
|
|
|
31
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
32
|
|
|
|
|
|
|
# initialization |
33
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
34
|
|
|
|
|
|
|
|
35
|
|
|
|
|
|
|
my $GUESS_MECHS = "a/24 mx/24 ptr"; |
36
|
|
|
|
|
|
|
my $TRUSTED_FORWARDER = "include:spf.trusted-forwarder.org"; |
37
|
|
|
|
|
|
|
|
38
|
|
|
|
|
|
|
my $DEFAULT_EXPLANATION = "Please see http://www.openspf.org/why.html?sender=%{S}&ip=%{I}&receiver=%{R}"; |
39
|
|
|
|
|
|
|
my @KNOWN_MECHANISMS = qw( a mx ptr include ip4 ip6 exists all ); |
40
|
|
|
|
|
|
|
my $MAX_LOOKUP_COUNT = 10; |
41
|
|
|
|
|
|
|
|
42
|
|
|
|
|
|
|
my $Domains_Queried = {}; |
43
|
|
|
|
|
|
|
|
44
|
|
|
|
|
|
|
our $CACHE_TIMEOUT = 120; |
45
|
|
|
|
|
|
|
our $DNS_RESOLVER_TIMEOUT = 15; |
46
|
|
|
|
|
|
|
|
47
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
48
|
|
|
|
|
|
|
# no user-serviceable parts below this line |
49
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
50
|
|
|
|
|
|
|
|
51
|
|
|
|
|
|
|
my $looks_like_ipv4 = qr/\d+\.\d+\.\d+\.\d+/; |
52
|
|
|
|
|
|
|
my $looks_like_email = qr/\S+\@\S+/; |
53
|
|
|
|
|
|
|
|
54
|
|
|
|
|
|
|
=head1 NAME |
55
|
|
|
|
|
|
|
|
56
|
|
|
|
|
|
|
Mail::SPF::Query - query Sender Policy Framework for an IP,email,helo |
57
|
|
|
|
|
|
|
|
58
|
|
|
|
|
|
|
=head1 VERSION |
59
|
|
|
|
|
|
|
|
60
|
|
|
|
|
|
|
1.999.1 |
61
|
|
|
|
|
|
|
|
62
|
|
|
|
|
|
|
=head1 SYNOPSIS |
63
|
|
|
|
|
|
|
|
64
|
|
|
|
|
|
|
my $query = new Mail::SPF::Query (ip => "127.0.0.1", sender=>'foo@example.com', helo=>"somehost.example.com", trusted=>0, guess=>0); |
65
|
|
|
|
|
|
|
my ($result, # pass | fail | softfail | neutral | none | error | unknown [mechanism] |
66
|
|
|
|
|
|
|
$smtp_comment, # "please see http://www.openspf.org/why.html?..." when rejecting, return this string to the SMTP client |
67
|
|
|
|
|
|
|
$header_comment, # prepend_header("Received-SPF" => "$result ($header_comment)") |
68
|
|
|
|
|
|
|
$spf_record, # "v=spf1 ..." original SPF record for the domain |
69
|
|
|
|
|
|
|
) = $query->result(); |
70
|
|
|
|
|
|
|
|
71
|
|
|
|
|
|
|
if ($result eq "pass") { "Domain is not forged. Apply RHSBL and content filters." } |
72
|
|
|
|
|
|
|
elsif ($result eq "fail") { "Domain is forged. Reject or save to spambox." } |
73
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
=head1 ABSTRACT |
75
|
|
|
|
|
|
|
|
76
|
|
|
|
|
|
|
The SPF protocol relies on sender domains to describe their designated outbound |
77
|
|
|
|
|
|
|
mailers in DNS. Given an email address, Mail::SPF::Query determines the |
78
|
|
|
|
|
|
|
legitimacy of an SMTP client IP address. |
79
|
|
|
|
|
|
|
|
80
|
|
|
|
|
|
|
=head1 DESCRIPTION |
81
|
|
|
|
|
|
|
|
82
|
|
|
|
|
|
|
There are two ways to use Mail::SPF::Query. Your choice depends on whether the |
83
|
|
|
|
|
|
|
domains your server is an MX for have secondary MXes which your server doesn't |
84
|
|
|
|
|
|
|
know about. |
85
|
|
|
|
|
|
|
|
86
|
|
|
|
|
|
|
The first and more common style, calling ->result(), is suitable when all mail |
87
|
|
|
|
|
|
|
is received directly from the originator's MTA. If the domains you receive do |
88
|
|
|
|
|
|
|
not have secondary MX entries, this is appropriate. This style of use is |
89
|
|
|
|
|
|
|
outlined in the SYNOPSIS above. This is the common case. |
90
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
The second style is more complex, but works when your server receives mail from |
92
|
|
|
|
|
|
|
secondary MXes. This performs checks as each recipient is handled. If the |
93
|
|
|
|
|
|
|
message is coming from a valid MX secondary for a recipient, then the SPF check |
94
|
|
|
|
|
|
|
is not performed, and a "pass" response is returned right away. To do this, |
95
|
|
|
|
|
|
|
call C and C instead of C. |
96
|
|
|
|
|
|
|
|
97
|
|
|
|
|
|
|
If you do not know what a secondary MX is, you probably don't have one. Use |
98
|
|
|
|
|
|
|
the first style. |
99
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
You can try out Mail::SPF::Query on the command line with the following |
101
|
|
|
|
|
|
|
command: |
102
|
|
|
|
|
|
|
|
103
|
|
|
|
|
|
|
perl -MMail::SPF::Query -le 'print for Mail::SPF::Query->new( |
104
|
|
|
|
|
|
|
helo => shift, ipv4 => shift, sender => shift)->result' \ |
105
|
|
|
|
|
|
|
helohost.example.com 1.2.3.4 user@example.com |
106
|
|
|
|
|
|
|
|
107
|
|
|
|
|
|
|
=head1 BUGS |
108
|
|
|
|
|
|
|
|
109
|
|
|
|
|
|
|
Mail::SPF::Query tries to implement the SPF specification (see L"SEE ALSO">) |
110
|
|
|
|
|
|
|
as close as reasonably possible given that M:S:Q has been the very first SPF |
111
|
|
|
|
|
|
|
implementation and has changed with the SPF specification over time. As a |
112
|
|
|
|
|
|
|
result, M:S:Q has various known deficiencies that cannot be corrected with |
113
|
|
|
|
|
|
|
reasonably little effort: |
114
|
|
|
|
|
|
|
|
115
|
|
|
|
|
|
|
=over |
116
|
|
|
|
|
|
|
|
117
|
|
|
|
|
|
|
=item * |
118
|
|
|
|
|
|
|
|
119
|
|
|
|
|
|
|
B M:S:Q is not designed to |
120
|
|
|
|
|
|
|
support the I querying of the HELO and MAIL FROM identities. Passing |
121
|
|
|
|
|
|
|
the HELO identity as the C argument for a stand-alone HELO check might |
122
|
|
|
|
|
|
|
generally work but could yield unexpected results. |
123
|
|
|
|
|
|
|
|
124
|
|
|
|
|
|
|
=item * |
125
|
|
|
|
|
|
|
|
126
|
|
|
|
|
|
|
B IPv6 is not supported. C mechanisms in SPF records |
127
|
|
|
|
|
|
|
and everywhere else are simply ignored. |
128
|
|
|
|
|
|
|
|
129
|
|
|
|
|
|
|
=item * |
130
|
|
|
|
|
|
|
|
131
|
|
|
|
|
|
|
B If a |
132
|
|
|
|
|
|
|
query result was caused by anything other than a real SPF record (i.e. local |
133
|
|
|
|
|
|
|
policy, overrides, fallbacks, etc.), and no custom C was |
134
|
|
|
|
|
|
|
specified, the domain's explanation or M:S:Q's hard-coded default explanation |
135
|
|
|
|
|
|
|
will still be returned. Be aware that in this case the explanation may not |
136
|
|
|
|
|
|
|
correctly explain the reason for such an artificial result. |
137
|
|
|
|
|
|
|
|
138
|
|
|
|
|
|
|
=for comment |
139
|
|
|
|
|
|
|
INTERNAL NOTE: If the spf_source is not 'original-spf-record' (but e.g. a |
140
|
|
|
|
|
|
|
local policy source), do not return the "why.html" default explanation, because |
141
|
|
|
|
|
|
|
"why.html" will not be able to reproduce the local policy. |
142
|
|
|
|
|
|
|
|
143
|
|
|
|
|
|
|
=back |
144
|
|
|
|
|
|
|
|
145
|
|
|
|
|
|
|
=head1 NON-STANDARD FEATURES |
146
|
|
|
|
|
|
|
|
147
|
|
|
|
|
|
|
Also due to its long history, M:S:Q does have some legacy features that are not |
148
|
|
|
|
|
|
|
parts of the official SPF specification, most notably I |
149
|
|
|
|
|
|
|
and I. Please be careful when using |
150
|
|
|
|
|
|
|
these I features or when reproducing them in your own SPF |
151
|
|
|
|
|
|
|
implementation, as they may cause unexpected results. |
152
|
|
|
|
|
|
|
|
153
|
|
|
|
|
|
|
=head1 METHODS |
154
|
|
|
|
|
|
|
|
155
|
|
|
|
|
|
|
=head2 C<< Mail::SPF::Query->new() >> |
156
|
|
|
|
|
|
|
|
157
|
|
|
|
|
|
|
my $query = eval { new Mail::SPF::Query ( |
158
|
|
|
|
|
|
|
ip => '127.0.0.1', |
159
|
|
|
|
|
|
|
sender => 'foo@example.com', |
160
|
|
|
|
|
|
|
helo => 'host.example.com', |
161
|
|
|
|
|
|
|
|
162
|
|
|
|
|
|
|
# Optional parameters: |
163
|
|
|
|
|
|
|
debug => 1, debuglog => sub { print STDERR "@_\n" }, |
164
|
|
|
|
|
|
|
local => 'extra mechanisms', |
165
|
|
|
|
|
|
|
trusted => 1, # do trusted forwarder processing |
166
|
|
|
|
|
|
|
guess => 1, # do best guess if no SPF record |
167
|
|
|
|
|
|
|
default_explanation => 'Please see http://spf.my.isp/spferror.html for details', |
168
|
|
|
|
|
|
|
max_lookup_count => 10, # total number of SPF includes/redirects |
169
|
|
|
|
|
|
|
sanitize => 0, # do not sanitize all returned strings |
170
|
|
|
|
|
|
|
myhostname => 'foo.example.com', # prepended to header_comment |
171
|
|
|
|
|
|
|
override => { 'example.net' => 'v=spf1 a mx -all', |
172
|
|
|
|
|
|
|
'*.example.net' => 'v=spf1 a mx -all' }, |
173
|
|
|
|
|
|
|
fallback => { 'example.org' => 'v=spf1 a mx -all', |
174
|
|
|
|
|
|
|
'*.example.org' => 'v=spf1 a mx -all' } |
175
|
|
|
|
|
|
|
) }; |
176
|
|
|
|
|
|
|
|
177
|
|
|
|
|
|
|
if ($@) { warn "bad input to Mail::SPF::Query: $@" } |
178
|
|
|
|
|
|
|
|
179
|
|
|
|
|
|
|
Set C1> to turned on C accreditation |
180
|
|
|
|
|
|
|
checking. The mechanism C is used just |
181
|
|
|
|
|
|
|
before a C<-all> or C. The precise circumstances are somewhat more |
182
|
|
|
|
|
|
|
complicated, but it does get the case of C right -- i.e. |
183
|
|
|
|
|
|
|
C is not checked. B |
184
|
|
|
|
|
|
|
|
185
|
|
|
|
|
|
|
Set C1> to turned on automatic best guess processing. This will |
186
|
|
|
|
|
|
|
use the best_guess SPF record when one cannot be found in the DNS. Note that |
187
|
|
|
|
|
|
|
this can only return C or C. The C and C flags |
188
|
|
|
|
|
|
|
also operate when the best_guess is being used. B
|
189
|
|
|
|
|
|
|
feature.> |
190
|
|
|
|
|
|
|
|
191
|
|
|
|
|
|
|
Set C'include:local.domain'> to include some extra processing just |
192
|
|
|
|
|
|
|
before a C<-all> or C. The local processing happens just before the |
193
|
|
|
|
|
|
|
trusted forwarder processing. B |
194
|
|
|
|
|
|
|
|
195
|
|
|
|
|
|
|
Set C to a string to be used if the SPF record does not |
196
|
|
|
|
|
|
|
provide a specific explanation. The default value will direct the user to a |
197
|
|
|
|
|
|
|
page at www.openspf.org with the following message: |
198
|
|
|
|
|
|
|
|
199
|
|
|
|
|
|
|
Please see http://www.openspf.org/why.html?sender=%{S}&ip=%{I}&receiver=%{R} |
200
|
|
|
|
|
|
|
|
201
|
|
|
|
|
|
|
Note that the string has macro substitution performed. |
202
|
|
|
|
|
|
|
|
203
|
|
|
|
|
|
|
Set C to 0 to get all the returned strings unsanitized. |
204
|
|
|
|
|
|
|
Alternatively, pass a function reference and this function will be used to |
205
|
|
|
|
|
|
|
sanitize the returned values. The function must take a single string argument |
206
|
|
|
|
|
|
|
and return a single string which contains the sanitized result. |
207
|
|
|
|
|
|
|
|
208
|
|
|
|
|
|
|
Set C1> to watch the queries happen. |
209
|
|
|
|
|
|
|
|
210
|
|
|
|
|
|
|
Set C to define SPF records for domains that do publish but which you |
211
|
|
|
|
|
|
|
want to override anyway. Wildcards are supported. B
|
212
|
|
|
|
|
|
|
feature.> |
213
|
|
|
|
|
|
|
|
214
|
|
|
|
|
|
|
Set C to define "pretend" SPF records for domains that don't publish |
215
|
|
|
|
|
|
|
them yet. Wildcards are supported. B |
216
|
|
|
|
|
|
|
|
217
|
|
|
|
|
|
|
Note: domain name arguments to override and fallback need to be in all |
218
|
|
|
|
|
|
|
lowercase. |
219
|
|
|
|
|
|
|
|
220
|
|
|
|
|
|
|
=cut |
221
|
|
|
|
|
|
|
|
222
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
223
|
|
|
|
|
|
|
# new |
224
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
225
|
|
|
|
|
|
|
|
226
|
|
|
|
|
|
|
sub new { |
227
|
165
|
|
|
165
|
1
|
212900
|
my $class = shift; |
228
|
165
|
|
|
|
|
12734
|
my $query = bless { @_ }, $class; |
229
|
|
|
|
|
|
|
|
230
|
165
|
|
|
|
|
896
|
$query->{lookup_count} = 0; |
231
|
|
|
|
|
|
|
|
232
|
165
|
50
|
33
|
|
|
1219
|
$query->{ipv4} = delete $query->{ip} |
233
|
|
|
|
|
|
|
if defined($query->{ip}) and $query->{ip} =~ $looks_like_ipv4; |
234
|
165
|
50
|
|
|
|
515
|
$query->{helo} = delete $query->{ehlo} |
235
|
|
|
|
|
|
|
if defined($query->{ehlo}); |
236
|
|
|
|
|
|
|
|
237
|
165
|
50
|
|
|
|
2576
|
$query->{local} .= ' ' . $TRUSTED_FORWARDER if ($query->{trusted}); |
238
|
|
|
|
|
|
|
|
239
|
165
|
|
|
|
|
906
|
$query->{trusted} = undef; |
240
|
|
|
|
|
|
|
|
241
|
165
|
|
50
|
|
|
2754
|
$query->{spf_error_explanation} ||= "SPF record error"; |
242
|
|
|
|
|
|
|
|
243
|
165
|
|
66
|
|
|
470
|
$query->{default_explanation} ||= $DEFAULT_EXPLANATION; |
244
|
|
|
|
|
|
|
|
245
|
165
|
50
|
|
|
|
479
|
$query->{default_record} = $GUESS_MECHS if ($query->{guess}); |
246
|
|
|
|
|
|
|
|
247
|
165
|
50
|
33
|
|
|
2962
|
if (($query->{sanitize} && !ref($query->{sanitize})) || !defined($query->{sanitize})) { |
|
|
|
33
|
|
|
|
|
248
|
|
|
|
|
|
|
# Apply default sanitizer |
249
|
165
|
|
|
|
|
467
|
$query->{sanitize} = \&strict_sanitize; |
250
|
|
|
|
|
|
|
} |
251
|
|
|
|
|
|
|
|
252
|
165
|
|
|
|
|
579
|
$query->{sender} =~ s/<(.*)>/$1/g; |
253
|
|
|
|
|
|
|
|
254
|
165
|
50
|
33
|
|
|
1362
|
if (not ($query->{ipv4} and length $query->{ipv4})) { |
255
|
0
|
|
|
|
|
0
|
die "no IP address given"; |
256
|
|
|
|
|
|
|
} |
257
|
|
|
|
|
|
|
|
258
|
165
|
|
|
|
|
602
|
for ($query->{sender}) { s/^\s+//; s/\s+$//; } |
|
165
|
|
|
|
|
506
|
|
|
165
|
|
|
|
|
1297
|
|
259
|
|
|
|
|
|
|
|
260
|
165
|
|
|
|
|
2673
|
$query->{spf_source} = "domain of $query->{sender}"; |
261
|
165
|
|
|
|
|
555
|
$query->{spf_source_type} = "original-spf-record"; |
262
|
|
|
|
|
|
|
|
263
|
165
|
|
|
|
|
1270
|
($query->{domain}) = $query->{sender} =~ /([^@]+)$/; # given foo@bar@baz.com, the domain is baz.com, not bar@baz.com. |
264
|
|
|
|
|
|
|
|
265
|
|
|
|
|
|
|
# the domain should not be an address literal --- [1.2.3.4] |
266
|
165
|
50
|
|
|
|
2269
|
if ($query->{domain} =~ /^\[\d+\.\d+\.\d+\.\d+\]$/) { |
267
|
0
|
|
|
|
|
0
|
die "sender domain should be an FQDN, not an address literal"; |
268
|
|
|
|
|
|
|
} |
269
|
|
|
|
|
|
|
|
270
|
165
|
50
|
|
|
|
668
|
if (not $query->{helo}) { require Carp; import Carp qw(cluck); cluck ("Mail::SPF::Query: ->new() requires a \"helo\" argument.\n"); |
|
0
|
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
271
|
0
|
|
|
|
|
0
|
$query->{helo} = $query->{domain}; |
272
|
|
|
|
|
|
|
} |
273
|
|
|
|
|
|
|
|
274
|
165
|
|
|
|
|
1055
|
$query->debuglog("new: ipv4=$query->{ipv4}, sender=$query->{sender}, helo=$query->{helo}"); |
275
|
|
|
|
|
|
|
|
276
|
165
|
|
|
|
|
1641
|
($query->{helo}) =~ s/.*\@//; # strip localpart from helo |
277
|
|
|
|
|
|
|
|
278
|
165
|
50
|
|
|
|
550
|
if (not $query->{domain}) { |
279
|
0
|
|
|
|
|
0
|
$query->debuglog("sender $query->{sender} has no domain, using HELO domain $query->{helo} instead."); |
280
|
0
|
|
|
|
|
0
|
$query->{domain} = $query->{helo}; |
281
|
0
|
|
|
|
|
0
|
$query->{sender} = $query->{helo}; |
282
|
|
|
|
|
|
|
} |
283
|
|
|
|
|
|
|
|
284
|
165
|
50
|
|
|
|
592
|
if (not length $query->{domain}) { die "unable to identify domain of sender $query->{sender}" } |
|
0
|
|
|
|
|
0
|
|
285
|
|
|
|
|
|
|
|
286
|
165
|
|
|
|
|
528
|
$query->{orig_domain} = $query->{domain}; |
287
|
|
|
|
|
|
|
|
288
|
165
|
|
|
|
|
1122
|
$query->{loop_report} = [$query->{domain}]; |
289
|
|
|
|
|
|
|
|
290
|
165
|
|
|
|
|
5328
|
($query->{localpart}) = $query->{sender} =~ /(.+)\@/; |
291
|
165
|
100
|
|
|
|
660
|
$query->{localpart} = "postmaster" if not length $query->{localpart}; |
292
|
|
|
|
|
|
|
|
293
|
165
|
|
|
|
|
1148
|
$query->debuglog("localpart is $query->{localpart}"); |
294
|
|
|
|
|
|
|
|
295
|
165
|
0
|
|
|
|
1409
|
$query->{Reversed_IP} = ($query->{ipv4} ? reverse_in_addr($query->{ipv4}) : |
|
|
50
|
|
|
|
|
|
296
|
|
|
|
|
|
|
$query->{ipv6} ? die "IPv6 not supported" : ""); |
297
|
|
|
|
|
|
|
|
298
|
165
|
50
|
|
|
|
658
|
if (not $query->{myhostname}) { |
299
|
165
|
|
|
|
|
1332
|
$query->{myhostname} = Sys::Hostname::Long::hostname_long(); |
300
|
|
|
|
|
|
|
} |
301
|
165
|
|
50
|
|
|
45012
|
$query->{myhostname} ||= "localhost"; |
302
|
|
|
|
|
|
|
|
303
|
|
|
|
|
|
|
# Unfold legacy { 'domain' => { record => '...' } } override and fallback |
304
|
|
|
|
|
|
|
# structures to just { 'domain' => '...' }: |
305
|
165
|
|
|
|
|
11016
|
foreach ('override', 'fallback') { |
306
|
330
|
50
|
|
|
|
1526
|
if (ref(my $domains_hash = $query->{$_}) eq 'HASH') { |
307
|
0
|
|
|
|
|
0
|
foreach my $domain (keys(%$domains_hash)) { |
308
|
0
|
0
|
|
|
|
0
|
$domains_hash->{$domain} = $domains_hash->{$domain}->{record} |
309
|
|
|
|
|
|
|
if ref($domains_hash->{$domain}) eq 'HASH'; |
310
|
|
|
|
|
|
|
} |
311
|
|
|
|
|
|
|
} |
312
|
|
|
|
|
|
|
} |
313
|
|
|
|
|
|
|
|
314
|
165
|
50
|
|
|
|
2395
|
$query->post_new(@_) if $class->can("post_new"); |
315
|
|
|
|
|
|
|
|
316
|
165
|
|
|
|
|
1370
|
return $query; |
317
|
|
|
|
|
|
|
} |
318
|
|
|
|
|
|
|
|
319
|
|
|
|
|
|
|
=head2 C<< $query->result() >> |
320
|
|
|
|
|
|
|
|
321
|
|
|
|
|
|
|
my ($result, $smtp_comment, $header_comment, $spf_record, $detail) = $query->result(); |
322
|
|
|
|
|
|
|
|
323
|
|
|
|
|
|
|
C<$result> will be one of C, C, C, C, C, |
324
|
|
|
|
|
|
|
C or C: |
325
|
|
|
|
|
|
|
|
326
|
|
|
|
|
|
|
=over |
327
|
|
|
|
|
|
|
|
328
|
|
|
|
|
|
|
=item C |
329
|
|
|
|
|
|
|
|
330
|
|
|
|
|
|
|
The client IP address is an authorized mailer for the sender. The mail should |
331
|
|
|
|
|
|
|
be accepted subject to local policy regarding the sender. |
332
|
|
|
|
|
|
|
|
333
|
|
|
|
|
|
|
=item C |
334
|
|
|
|
|
|
|
|
335
|
|
|
|
|
|
|
The client IP address is not an authorized mailer, and the sender wants you to |
336
|
|
|
|
|
|
|
reject the transaction for fear of forgery. |
337
|
|
|
|
|
|
|
|
338
|
|
|
|
|
|
|
=item C |
339
|
|
|
|
|
|
|
|
340
|
|
|
|
|
|
|
The client IP address is not an authorized mailer, but the sender prefers that |
341
|
|
|
|
|
|
|
you accept the transaction because it isn't absolutely sure all its users are |
342
|
|
|
|
|
|
|
mailing through approved servers. The C status is often used during |
343
|
|
|
|
|
|
|
initial deployment of SPF records by a domain. |
344
|
|
|
|
|
|
|
|
345
|
|
|
|
|
|
|
=item C |
346
|
|
|
|
|
|
|
|
347
|
|
|
|
|
|
|
The sender makes no assertion about the status of the client IP. |
348
|
|
|
|
|
|
|
|
349
|
|
|
|
|
|
|
=item C |
350
|
|
|
|
|
|
|
|
351
|
|
|
|
|
|
|
There is no SPF record for this domain. |
352
|
|
|
|
|
|
|
|
353
|
|
|
|
|
|
|
=item C |
354
|
|
|
|
|
|
|
|
355
|
|
|
|
|
|
|
The DNS lookup encountered a temporary error during processing. |
356
|
|
|
|
|
|
|
|
357
|
|
|
|
|
|
|
=item C |
358
|
|
|
|
|
|
|
|
359
|
|
|
|
|
|
|
The domain has a configuration error in the published data or defines a |
360
|
|
|
|
|
|
|
mechanism that this library does not understand. If the data contained an |
361
|
|
|
|
|
|
|
unrecognized mechanism, it will be presented following "unknown". You should |
362
|
|
|
|
|
|
|
test for unknown using a regexp C^unknown/> rather than C. |
363
|
|
|
|
|
|
|
|
364
|
|
|
|
|
|
|
=back |
365
|
|
|
|
|
|
|
|
366
|
|
|
|
|
|
|
Results are cached internally for a default of 120 seconds. You can call |
367
|
|
|
|
|
|
|
C<-Eresult()> repeatedly; subsequent lookups won't hit your DNS. |
368
|
|
|
|
|
|
|
|
369
|
|
|
|
|
|
|
C should be displayed to the SMTP client. |
370
|
|
|
|
|
|
|
|
371
|
|
|
|
|
|
|
C goes into a C header, like so: |
372
|
|
|
|
|
|
|
|
373
|
|
|
|
|
|
|
Received-SPF: $result ($header_comment) |
374
|
|
|
|
|
|
|
|
375
|
|
|
|
|
|
|
C shows the original SPF record fetched for the query. If there is |
376
|
|
|
|
|
|
|
no SPF record, it is blank. Otherwise, it will start with C and |
377
|
|
|
|
|
|
|
contain the SPF mechanisms and such that describe the domain. |
378
|
|
|
|
|
|
|
|
379
|
|
|
|
|
|
|
Note that the strings returned by this method (and most of the other methods) |
380
|
|
|
|
|
|
|
are (at least partially) under the control of the sender's domain. This means |
381
|
|
|
|
|
|
|
that, if the sender is an attacker, the contents can be assumed to be hostile. |
382
|
|
|
|
|
|
|
The various methods that return these strings make sure that (by default) the |
383
|
|
|
|
|
|
|
strings returned contain only characters in the range 32 - 126. This behavior |
384
|
|
|
|
|
|
|
can be changed by setting C to 0 to turn off sanitization entirely. |
385
|
|
|
|
|
|
|
You can also set C to a function reference to perform custom |
386
|
|
|
|
|
|
|
sanitization. In particular, assume that C might contain a |
387
|
|
|
|
|
|
|
newline character. |
388
|
|
|
|
|
|
|
|
389
|
|
|
|
|
|
|
C is a hash of all the foregoing result elements, plus extra data |
390
|
|
|
|
|
|
|
returned by the SPF result. |
391
|
|
|
|
|
|
|
|
392
|
|
|
|
|
|
|
I In the beginning, C returned only one |
393
|
|
|
|
|
|
|
value, the C<$result>. Then C<$smtp_comment> and C<$header_comment> came |
394
|
|
|
|
|
|
|
along. Then C<$spf_record>. Past a certain number of positional results, it |
395
|
|
|
|
|
|
|
makes more sense to have a hash. But we didn't want to break backwards |
396
|
|
|
|
|
|
|
compatibility, so we just declared that the fifth result would be a hash and |
397
|
|
|
|
|
|
|
future return value would go in there. |
398
|
|
|
|
|
|
|
|
399
|
|
|
|
|
|
|
The keys of the hash are: |
400
|
|
|
|
|
|
|
|
401
|
|
|
|
|
|
|
result |
402
|
|
|
|
|
|
|
smtp_comment |
403
|
|
|
|
|
|
|
header_comment |
404
|
|
|
|
|
|
|
header_pairs |
405
|
|
|
|
|
|
|
spf_record |
406
|
|
|
|
|
|
|
modifiers |
407
|
|
|
|
|
|
|
|
408
|
|
|
|
|
|
|
=cut |
409
|
|
|
|
|
|
|
|
410
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
411
|
|
|
|
|
|
|
# result |
412
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
413
|
|
|
|
|
|
|
|
414
|
|
|
|
|
|
|
sub result { |
415
|
165
|
|
|
165
|
1
|
350
|
my $query = shift; |
416
|
165
|
|
|
|
|
342
|
my %result_set; |
417
|
|
|
|
|
|
|
|
418
|
165
|
50
|
|
|
|
825
|
my ($result, $smtp_explanation, $smtp_why, $orig_txt) = $query->spfquery( |
419
|
|
|
|
|
|
|
$query->{best_guess} ? $query->{guess_mechs} : () |
420
|
|
|
|
|
|
|
); |
421
|
|
|
|
|
|
|
|
422
|
165
|
50
|
|
|
|
10522
|
$smtp_why = "" if $smtp_why eq "default"; |
423
|
|
|
|
|
|
|
|
424
|
165
|
100
|
66
|
|
|
1877
|
my $smtp_comment = ($smtp_explanation && $smtp_why) ? "$smtp_explanation: $smtp_why" : ($smtp_explanation || $smtp_why); |
|
|
|
33
|
|
|
|
|
425
|
|
|
|
|
|
|
|
426
|
165
|
|
|
|
|
535
|
$query->{smtp_comment} = $smtp_comment; |
427
|
|
|
|
|
|
|
|
428
|
165
|
|
|
|
|
921
|
my $header_comment = "$query->{myhostname}: ". $query->header_comment($result); |
429
|
|
|
|
|
|
|
|
430
|
|
|
|
|
|
|
# $result =~ s/\s.*$//; # this regex truncates "unknown some:mechanism" to just "unknown" |
431
|
|
|
|
|
|
|
|
432
|
165
|
|
|
|
|
585
|
$query->{result} = $result; |
433
|
|
|
|
|
|
|
|
434
|
165
|
|
|
|
|
1102
|
my $hash = { result => $query->sanitize(lc $result), |
435
|
|
|
|
|
|
|
smtp_comment => $query->sanitize($smtp_comment), |
436
|
|
|
|
|
|
|
header_comment => $query->sanitize($header_comment), |
437
|
|
|
|
|
|
|
spf_record => $query->sanitize($orig_txt), |
438
|
|
|
|
|
|
|
modifiers => $query->{modifiers}, |
439
|
|
|
|
|
|
|
header_pairs => $query->sanitize(scalar $query->header_pairs()), |
440
|
|
|
|
|
|
|
}; |
441
|
|
|
|
|
|
|
|
442
|
165
|
50
|
|
|
|
2211
|
return ($hash->{result}, |
443
|
|
|
|
|
|
|
$hash->{smtp_comment}, |
444
|
|
|
|
|
|
|
$hash->{header_comment}, |
445
|
|
|
|
|
|
|
$hash->{spf_record}, |
446
|
|
|
|
|
|
|
$hash, |
447
|
|
|
|
|
|
|
) if wantarray; |
448
|
|
|
|
|
|
|
|
449
|
0
|
|
|
|
|
0
|
return $query->sanitize(lc $result); |
450
|
|
|
|
|
|
|
} |
451
|
|
|
|
|
|
|
|
452
|
|
|
|
|
|
|
sub header_comment { |
453
|
165
|
|
|
165
|
0
|
401
|
my $query = shift; |
454
|
165
|
|
|
|
|
406
|
my $result = shift; |
455
|
165
|
|
|
|
|
902
|
my $ip = $query->ip; |
456
|
165
|
100
|
66
|
|
|
657
|
if ($result eq "pass" and $query->{smtp_comment} eq "localhost is always allowed.") { return $query->{smtp_comment} } |
|
1
|
|
|
|
|
12
|
|
457
|
|
|
|
|
|
|
|
458
|
164
|
|
|
|
|
1051
|
$query->debuglog("header_comment: spf_source = $query->{spf_source}"); |
459
|
164
|
|
|
|
|
1535
|
$query->debuglog("header_comment: spf_source_type = $query->{spf_source_type}"); |
460
|
|
|
|
|
|
|
|
461
|
164
|
50
|
|
|
|
1197
|
if ($query->{spf_source_type} eq "original-spf-record") { |
462
|
|
|
|
|
|
|
return |
463
|
164
|
0
|
|
|
|
13030
|
( $result eq "pass" ? "$query->{spf_source} designates $ip as permitted sender" |
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
464
|
|
|
|
|
|
|
: $result eq "fail" ? "$query->{spf_source} does not designate $ip as permitted sender" |
465
|
|
|
|
|
|
|
: $result eq "softfail" ? "transitioning $query->{spf_source} does not designate $ip as permitted sender" |
466
|
|
|
|
|
|
|
: $result =~ /^unknown / ? "encountered unrecognized mechanism during SPF processing of $query->{spf_source}" |
467
|
|
|
|
|
|
|
: $result eq "unknown" ? "error in processing during lookup of $query->{sender}" |
468
|
|
|
|
|
|
|
: $result eq "neutral" ? "$ip is neither permitted nor denied by domain of $query->{sender}" |
469
|
|
|
|
|
|
|
: $result eq "error" ? "encountered temporary error during SPF processing of $query->{spf_source}" |
470
|
|
|
|
|
|
|
: $result eq "none" ? "$query->{spf_source} does not designate permitted sender hosts" |
471
|
|
|
|
|
|
|
: "could not perform SPF query for $query->{spf_source}" ); |
472
|
|
|
|
|
|
|
} |
473
|
|
|
|
|
|
|
|
474
|
0
|
|
|
|
|
0
|
return $query->{spf_source}; |
475
|
|
|
|
|
|
|
|
476
|
|
|
|
|
|
|
} |
477
|
|
|
|
|
|
|
|
478
|
|
|
|
|
|
|
sub header_pairs { |
479
|
165
|
|
|
165
|
0
|
384
|
my $query = shift; |
480
|
|
|
|
|
|
|
# from spf-draft-200404.txt |
481
|
|
|
|
|
|
|
# SPF clients may append zero or more of the following key-value-pairs |
482
|
|
|
|
|
|
|
# at their discretion: |
483
|
|
|
|
|
|
|
# |
484
|
|
|
|
|
|
|
# receiver the hostname of the SPF client |
485
|
|
|
|
|
|
|
# client-ip the IP address of the SMTP client |
486
|
|
|
|
|
|
|
# envelope-from the envelope sender address |
487
|
|
|
|
|
|
|
# helo the hostname given in the HELO or EHLO command |
488
|
|
|
|
|
|
|
# mechanism the mechanism that matched (if no mechanisms |
489
|
|
|
|
|
|
|
# matched, substitute the word "default".) |
490
|
|
|
|
|
|
|
# problem if an error was returned, details about the error |
491
|
|
|
|
|
|
|
# |
492
|
|
|
|
|
|
|
# Other key-value pairs may be defined by SPF clients. Until a new key |
493
|
|
|
|
|
|
|
# name becomes widely accepted, new key names should start with "x-". |
494
|
|
|
|
|
|
|
|
495
|
165
|
50
|
50
|
|
|
2469
|
my @pairs = ( |
|
|
50
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
496
|
|
|
|
|
|
|
"receiver" => $query->{myhostname}, |
497
|
|
|
|
|
|
|
"client-ip" => ($query->{ipv4} || $query->{ipv6} || ""), |
498
|
|
|
|
|
|
|
"envelope-from" => $query->{sender}, |
499
|
|
|
|
|
|
|
"helo" => $query->{helo}, |
500
|
|
|
|
|
|
|
mechanism => ($query->{matched_mechanism} ? display_mechanism($query->{matched_mechanism}) : "default"), |
501
|
|
|
|
|
|
|
($query->{result} eq "error" |
502
|
|
|
|
|
|
|
? (problem => $query->{spf_error_explanation}) |
503
|
|
|
|
|
|
|
: ()), |
504
|
|
|
|
|
|
|
($query->{spf_source_type} ne "original-spf-record" ? ("x-spf-source" => $query->{spf_source}) : ()), |
505
|
|
|
|
|
|
|
); |
506
|
|
|
|
|
|
|
|
507
|
165
|
50
|
|
|
|
600
|
if (wantarray) { return @pairs; } |
|
0
|
|
|
|
|
0
|
|
508
|
165
|
|
|
|
|
451
|
my @pair_text; |
509
|
165
|
|
|
|
|
456
|
while (@pairs) { |
510
|
825
|
|
|
|
|
3184
|
my ($key, $val) = (shift(@pairs), shift (@pairs)); |
511
|
825
|
|
|
|
|
3900
|
push @pair_text, "$key=$val;"; |
512
|
|
|
|
|
|
|
} |
513
|
165
|
|
|
|
|
4977
|
return join " ", @pair_text; |
514
|
|
|
|
|
|
|
} |
515
|
|
|
|
|
|
|
|
516
|
|
|
|
|
|
|
=head2 C<< $query->result2() >> |
517
|
|
|
|
|
|
|
|
518
|
|
|
|
|
|
|
my ($result, $smtp_comment, $header_comment, $spf_record) = $query->result2('recipient@domain', 'recipient2@domain'); |
519
|
|
|
|
|
|
|
|
520
|
|
|
|
|
|
|
C does everything that C does, but it first checks to see if |
521
|
|
|
|
|
|
|
the sending system is a recognized MX secondary for the recipient(s). If so, |
522
|
|
|
|
|
|
|
then it returns C and does not perform the SPF query. Note that the |
523
|
|
|
|
|
|
|
sending system may be a MX secondary for some (but not all) of the recipients |
524
|
|
|
|
|
|
|
for a multi-recipient message, which is why result2 takes an argument list. |
525
|
|
|
|
|
|
|
See also C. |
526
|
|
|
|
|
|
|
|
527
|
|
|
|
|
|
|
B B
|
528
|
|
|
|
|
|
|
exemption of trusted relays, such as secondary MXes, should really be performed |
529
|
|
|
|
|
|
|
by the software that uses this library before doing an SPF check.> |
530
|
|
|
|
|
|
|
|
531
|
|
|
|
|
|
|
C<$result> will be one of C, C, C, or C. |
532
|
|
|
|
|
|
|
See C above for meanings. |
533
|
|
|
|
|
|
|
|
534
|
|
|
|
|
|
|
If you have secondary MXes and if you are unable to explicitly white-list them |
535
|
|
|
|
|
|
|
before SPF tests occur, you can use this method in place of C, |
536
|
|
|
|
|
|
|
calling it as many times as there are recipients, or just providing all the |
537
|
|
|
|
|
|
|
recipients at one time. |
538
|
|
|
|
|
|
|
|
539
|
|
|
|
|
|
|
C can be displayed to the SMTP client. |
540
|
|
|
|
|
|
|
|
541
|
|
|
|
|
|
|
For example: |
542
|
|
|
|
|
|
|
|
543
|
|
|
|
|
|
|
my $query = new Mail::SPF::Query (ip => "127.0.0.1", |
544
|
|
|
|
|
|
|
sender=>'foo@example.com', |
545
|
|
|
|
|
|
|
helo=>"somehost.example.com"); |
546
|
|
|
|
|
|
|
|
547
|
|
|
|
|
|
|
... |
548
|
|
|
|
|
|
|
|
549
|
|
|
|
|
|
|
my ($result, $smtp_comment, $header_comment); |
550
|
|
|
|
|
|
|
|
551
|
|
|
|
|
|
|
($result, $smtp_comment, $header_comment) = $query->result2('recip1@example.com'); |
552
|
|
|
|
|
|
|
# return suitable error code based on $result eq 'fail' or not |
553
|
|
|
|
|
|
|
|
554
|
|
|
|
|
|
|
($result, $smtp_comment, $header_comment) = $query->result2('recip2@example.org'); |
555
|
|
|
|
|
|
|
# return suitable error code based on $result eq 'fail' or not |
556
|
|
|
|
|
|
|
|
557
|
|
|
|
|
|
|
($result, $smtp_comment, $header_comment) = $query->message_result2(); |
558
|
|
|
|
|
|
|
# return suitable error if $result eq 'fail' |
559
|
|
|
|
|
|
|
# prefix message with "Received-SPF: $result ($header_comment)" |
560
|
|
|
|
|
|
|
|
561
|
|
|
|
|
|
|
=cut |
562
|
|
|
|
|
|
|
|
563
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
564
|
|
|
|
|
|
|
# result2 |
565
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
566
|
|
|
|
|
|
|
|
567
|
|
|
|
|
|
|
sub result2 { |
568
|
13
|
|
|
13
|
1
|
2511
|
my $query = shift; |
569
|
13
|
|
|
|
|
30
|
my @recipients = @_; |
570
|
|
|
|
|
|
|
|
571
|
13
|
100
|
|
|
|
47
|
if (!$query->{result2}) { |
572
|
9
|
|
|
|
|
21
|
my $all_mx_secondary = 'neutral'; |
573
|
|
|
|
|
|
|
|
574
|
9
|
|
|
|
|
31
|
foreach my $recip (@recipients) { |
575
|
9
|
|
|
|
|
66
|
my ($rhost) = $recip =~ /([^@]+)$/; |
576
|
|
|
|
|
|
|
|
577
|
9
|
|
|
|
|
60
|
$query->debuglog("result2: Checking status of recipient $recip (at host $rhost)"); |
578
|
|
|
|
|
|
|
|
579
|
9
|
|
|
|
|
68
|
my $cache_result = $query->{mx_cache}->{$rhost}; |
580
|
9
|
50
|
|
|
|
31
|
if (not defined($cache_result)) { |
581
|
9
|
50
|
|
|
|
36
|
$cache_result = $query->{mx_cache}->{$rhost} = is_secondary_for($rhost, $query->{ipv4}) ? 'yes' : 'no'; |
582
|
9
|
|
|
|
|
62
|
$query->debuglog("result2: $query->{ipv4} is a MX for $rhost: $cache_result"); |
583
|
|
|
|
|
|
|
} |
584
|
|
|
|
|
|
|
|
585
|
9
|
50
|
|
|
|
69
|
if ($cache_result eq 'yes') { |
586
|
0
|
|
|
|
|
0
|
$query->{is_mx_good} = [$query->sanitize('pass'), |
587
|
|
|
|
|
|
|
$query->sanitize('message from secondary MX'), |
588
|
|
|
|
|
|
|
$query->sanitize("$query->{myhostname}: message received from $query->{ipv4} which is an MX secondary for $recip"), |
589
|
|
|
|
|
|
|
undef]; |
590
|
0
|
|
|
|
|
0
|
$all_mx_secondary = 'yes'; |
591
|
|
|
|
|
|
|
} else { |
592
|
9
|
|
|
|
|
21
|
$all_mx_secondary = 'no'; |
593
|
9
|
|
|
|
|
29
|
last; |
594
|
|
|
|
|
|
|
} |
595
|
|
|
|
|
|
|
} |
596
|
|
|
|
|
|
|
|
597
|
9
|
50
|
|
|
|
37
|
if ($all_mx_secondary eq 'yes') { |
598
|
0
|
0
|
|
|
|
0
|
return @{$query->{is_mx_good}} if wantarray; |
|
0
|
|
|
|
|
0
|
|
599
|
0
|
|
|
|
|
0
|
return $query->{is_mx_good}->[0]; |
600
|
|
|
|
|
|
|
} |
601
|
|
|
|
|
|
|
|
602
|
9
|
|
|
|
|
48
|
my @result = $query->result(); |
603
|
|
|
|
|
|
|
|
604
|
9
|
|
|
|
|
86
|
$query->{result2} = \@result; |
605
|
|
|
|
|
|
|
} |
606
|
|
|
|
|
|
|
|
607
|
13
|
50
|
|
|
|
37
|
return @{$query->{result2}} if wantarray; |
|
13
|
|
|
|
|
108
|
|
608
|
0
|
|
|
|
|
0
|
return $query->{result2}->[0]; |
609
|
|
|
|
|
|
|
} |
610
|
|
|
|
|
|
|
|
611
|
|
|
|
|
|
|
sub is_secondary_for { |
612
|
9
|
|
|
9
|
0
|
25
|
my ($host, $addr) = @_; |
613
|
|
|
|
|
|
|
|
614
|
9
|
|
|
|
|
162
|
my $resolver = Net::DNS::Resolver->new( |
615
|
|
|
|
|
|
|
tcp_timeout => $DNS_RESOLVER_TIMEOUT, |
616
|
|
|
|
|
|
|
udp_timeout => $DNS_RESOLVER_TIMEOUT, |
617
|
|
|
|
|
|
|
) |
618
|
|
|
|
|
|
|
; |
619
|
9
|
50
|
|
|
|
744
|
if ($resolver) { |
620
|
9
|
|
|
|
|
40
|
my $mx = $resolver->send($host, 'MX'); |
621
|
9
|
50
|
|
|
|
248175
|
if ($mx) { |
622
|
9
|
|
|
|
|
55
|
my @mxlist = sort { $a->preference <=> $b->preference } (grep { $_->type eq 'MX' } $mx->answer); |
|
8
|
|
|
|
|
83
|
|
|
8
|
|
|
|
|
82
|
|
623
|
|
|
|
|
|
|
# discard the first entry (top priority) - we shouldn't get mail from them |
624
|
9
|
|
|
|
|
68
|
shift @mxlist; |
625
|
9
|
|
|
|
|
99
|
foreach my $rr (@mxlist) { |
626
|
6
|
|
|
|
|
260
|
my $a = $resolver->send($rr->exchange, 'A'); |
627
|
6
|
50
|
|
|
|
27043
|
if ($a) { |
628
|
6
|
|
|
|
|
29
|
foreach my $rra ($a->answer) { |
629
|
6
|
50
|
|
|
|
56
|
if ($rra->type eq 'A') { |
630
|
6
|
50
|
|
|
|
82
|
if ($rra->address eq $addr) { |
631
|
0
|
|
|
|
|
0
|
return 1; |
632
|
|
|
|
|
|
|
} |
633
|
|
|
|
|
|
|
} |
634
|
|
|
|
|
|
|
} |
635
|
|
|
|
|
|
|
} |
636
|
|
|
|
|
|
|
} |
637
|
|
|
|
|
|
|
} |
638
|
|
|
|
|
|
|
} |
639
|
|
|
|
|
|
|
|
640
|
9
|
|
|
|
|
291
|
return undef; |
641
|
|
|
|
|
|
|
} |
642
|
|
|
|
|
|
|
|
643
|
|
|
|
|
|
|
=head2 C<< $query->message_result2() >> |
644
|
|
|
|
|
|
|
|
645
|
|
|
|
|
|
|
my ($result, $smtp_comment, $header_comment, $spf_record) = $query->message_result2(); |
646
|
|
|
|
|
|
|
|
647
|
|
|
|
|
|
|
C returns an overall status for the message after zero or |
648
|
|
|
|
|
|
|
more calls to C. It will always be the last status returned by |
649
|
|
|
|
|
|
|
C, or the status returned by C if C was never |
650
|
|
|
|
|
|
|
called. |
651
|
|
|
|
|
|
|
|
652
|
|
|
|
|
|
|
C<$result> will be one of C, C, C, or C. See |
653
|
|
|
|
|
|
|
C above for meanings. |
654
|
|
|
|
|
|
|
|
655
|
|
|
|
|
|
|
=cut |
656
|
|
|
|
|
|
|
|
657
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
658
|
|
|
|
|
|
|
# message_result2 |
659
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
660
|
|
|
|
|
|
|
|
661
|
|
|
|
|
|
|
sub message_result2 { |
662
|
9
|
|
|
9
|
1
|
2842
|
my $query = shift; |
663
|
|
|
|
|
|
|
|
664
|
9
|
50
|
|
|
|
36
|
if (!$query->{result2}) { |
665
|
0
|
0
|
|
|
|
0
|
if ($query->{is_mx_good}) { |
666
|
0
|
0
|
|
|
|
0
|
return @{$query->{is_mx_good}} if wantarray; |
|
0
|
|
|
|
|
0
|
|
667
|
0
|
|
|
|
|
0
|
return $query->{is_mx_good}->[0]; |
668
|
|
|
|
|
|
|
} |
669
|
|
|
|
|
|
|
|
670
|
|
|
|
|
|
|
# we are very unlikely to get here -- unless result2 was not called. |
671
|
|
|
|
|
|
|
|
672
|
0
|
|
|
|
|
0
|
my @result = $query->result(); |
673
|
|
|
|
|
|
|
|
674
|
0
|
|
|
|
|
0
|
$query->{result2} = \@result; |
675
|
|
|
|
|
|
|
} |
676
|
|
|
|
|
|
|
|
677
|
9
|
50
|
|
|
|
27
|
return @{$query->{result2}} if wantarray; |
|
9
|
|
|
|
|
130
|
|
678
|
0
|
|
|
|
|
0
|
return $query->{result2}->[0]; |
679
|
|
|
|
|
|
|
} |
680
|
|
|
|
|
|
|
|
681
|
|
|
|
|
|
|
=head2 C<< $query->best_guess() >> |
682
|
|
|
|
|
|
|
|
683
|
|
|
|
|
|
|
my ($result, $smtp_comment, $header_comment) = $query->best_guess(); |
684
|
|
|
|
|
|
|
|
685
|
|
|
|
|
|
|
When a domain does not publish an SPF record, this library can produce an |
686
|
|
|
|
|
|
|
educated guess anyway. |
687
|
|
|
|
|
|
|
|
688
|
|
|
|
|
|
|
It pretends the domain defined A, MX, and PTR mechanisms, plus a few others. |
689
|
|
|
|
|
|
|
The default set of directives is |
690
|
|
|
|
|
|
|
|
691
|
|
|
|
|
|
|
a/24 mx/24 ptr |
692
|
|
|
|
|
|
|
|
693
|
|
|
|
|
|
|
That default set will return either "pass" or "neutral". |
694
|
|
|
|
|
|
|
|
695
|
|
|
|
|
|
|
If you want to experiment with a different default, you can pass it as an |
696
|
|
|
|
|
|
|
argument: C<< $query->best_guess("a mx ptr") >> |
697
|
|
|
|
|
|
|
|
698
|
|
|
|
|
|
|
B B You |
699
|
|
|
|
|
|
|
should set C1> on the C method instead. |
700
|
|
|
|
|
|
|
|
701
|
|
|
|
|
|
|
=head2 C<< $query->trusted_forwarder() >> |
702
|
|
|
|
|
|
|
|
703
|
|
|
|
|
|
|
my ($result, $smtp_comment, $header_comment) = $query->best_guess(); |
704
|
|
|
|
|
|
|
|
705
|
|
|
|
|
|
|
It is possible that the message is coming through a known-good relay like |
706
|
|
|
|
|
|
|
C or C. During the transitional period, many legitimate |
707
|
|
|
|
|
|
|
services may appear to forge a sender address: for example, a news website may |
708
|
|
|
|
|
|
|
have a "send me this article in email" link. |
709
|
|
|
|
|
|
|
|
710
|
|
|
|
|
|
|
The C domain is a white-list of known-good hosts that |
711
|
|
|
|
|
|
|
either forward mail or perform benign envelope sender forgery: |
712
|
|
|
|
|
|
|
|
713
|
|
|
|
|
|
|
include:spf.trusted-forwarder.org |
714
|
|
|
|
|
|
|
|
715
|
|
|
|
|
|
|
This will return either "pass" or "neutral". |
716
|
|
|
|
|
|
|
|
717
|
|
|
|
|
|
|
B B You |
718
|
|
|
|
|
|
|
should set C1> on the C method instead. |
719
|
|
|
|
|
|
|
|
720
|
|
|
|
|
|
|
=cut |
721
|
|
|
|
|
|
|
|
722
|
|
|
|
|
|
|
sub clone { |
723
|
0
|
|
|
0
|
0
|
0
|
my $query = shift; |
724
|
0
|
|
|
|
|
0
|
my $class = ref $query; |
725
|
|
|
|
|
|
|
|
726
|
0
|
|
|
|
|
0
|
my %guts = (%$query, @_, parent=>$query); |
727
|
|
|
|
|
|
|
|
728
|
0
|
|
|
|
|
0
|
my $clone = bless \%guts, $class; |
729
|
|
|
|
|
|
|
|
730
|
0
|
|
|
|
|
0
|
push @{$clone->{loop_report}}, delete $clone->{reason}; |
|
0
|
|
|
|
|
0
|
|
731
|
|
|
|
|
|
|
|
732
|
0
|
|
|
|
|
0
|
$query->debuglog(" clone: new object:"); |
733
|
0
|
|
|
|
|
0
|
for ($clone->show) { $clone->debuglog( "clone: $_" ) } |
|
0
|
|
|
|
|
0
|
|
734
|
|
|
|
|
|
|
|
735
|
0
|
|
|
|
|
0
|
return $clone; |
736
|
|
|
|
|
|
|
} |
737
|
|
|
|
|
|
|
|
738
|
|
|
|
|
|
|
sub top { |
739
|
2024
|
|
|
2024
|
0
|
3640
|
my $query = shift; |
740
|
2024
|
50
|
|
|
|
6789
|
if ($query->{parent}) { return $query->{parent}->top } |
|
0
|
|
|
|
|
0
|
|
741
|
2024
|
|
|
|
|
13354
|
return $query; |
742
|
|
|
|
|
|
|
} |
743
|
|
|
|
|
|
|
|
744
|
|
|
|
|
|
|
sub set_temperror { |
745
|
0
|
|
|
0
|
0
|
0
|
my $query = shift; |
746
|
0
|
|
|
|
|
0
|
$query->{error} = shift; |
747
|
|
|
|
|
|
|
} |
748
|
|
|
|
|
|
|
|
749
|
|
|
|
|
|
|
sub show { |
750
|
0
|
|
|
0
|
0
|
0
|
my $query = shift; |
751
|
|
|
|
|
|
|
|
752
|
0
|
|
|
|
|
0
|
return map { sprintf ("%20s = %s", $_, $query->{$_}) } keys %$query; |
|
0
|
|
|
|
|
0
|
|
753
|
|
|
|
|
|
|
} |
754
|
|
|
|
|
|
|
|
755
|
|
|
|
|
|
|
sub best_guess { |
756
|
0
|
|
|
0
|
1
|
0
|
my $query = shift; |
757
|
0
|
|
0
|
|
|
0
|
my $guess_mechs = shift || $GUESS_MECHS; |
758
|
|
|
|
|
|
|
|
759
|
|
|
|
|
|
|
# clone the query object with best_guess mode turned on. |
760
|
0
|
|
|
|
|
0
|
my $guess_query = $query->clone( best_guess => 1, |
761
|
|
|
|
|
|
|
guess_mechs => $guess_mechs, |
762
|
|
|
|
|
|
|
reason => "has no data. best guess", |
763
|
|
|
|
|
|
|
); |
764
|
|
|
|
|
|
|
|
765
|
0
|
|
|
|
|
0
|
$guess_query->top->{lookup_count} = 0; |
766
|
|
|
|
|
|
|
|
767
|
|
|
|
|
|
|
# if result is not defined, the domain has no SPF. |
768
|
|
|
|
|
|
|
# perform fallback lookups. |
769
|
|
|
|
|
|
|
# perform trusted-forwarder lookups. |
770
|
|
|
|
|
|
|
# perform guess lookups. |
771
|
|
|
|
|
|
|
# |
772
|
|
|
|
|
|
|
# if result is defined, return it. |
773
|
|
|
|
|
|
|
|
774
|
0
|
|
|
|
|
0
|
my ($result, $smtp_comment, $header_comment) = $guess_query->result(); |
775
|
0
|
0
|
0
|
|
|
0
|
if (defined $result and $result eq "pass") { |
776
|
0
|
|
|
|
|
0
|
my $ip = $query->ip; |
777
|
0
|
|
|
|
|
0
|
$header_comment = $query->sanitize("seems reasonable for $query->{sender} to mail through $ip"); |
778
|
0
|
0
|
|
|
|
0
|
return ($result, $smtp_comment, $header_comment) if wantarray; |
779
|
0
|
|
|
|
|
0
|
return $result; |
780
|
|
|
|
|
|
|
} |
781
|
|
|
|
|
|
|
|
782
|
0
|
|
|
|
|
0
|
return $query->sanitize("neutral"); |
783
|
|
|
|
|
|
|
} |
784
|
|
|
|
|
|
|
|
785
|
|
|
|
|
|
|
sub trusted_forwarder { |
786
|
0
|
|
|
0
|
1
|
0
|
my $query = shift; |
787
|
0
|
|
0
|
|
|
0
|
my $guess_mechs = shift || $TRUSTED_FORWARDER; |
788
|
0
|
|
|
|
|
0
|
return $query->best_guess($guess_mechs); |
789
|
|
|
|
|
|
|
} |
790
|
|
|
|
|
|
|
|
791
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
792
|
|
|
|
|
|
|
|
793
|
|
|
|
|
|
|
=head2 C<< $query->sanitize('string') >> |
794
|
|
|
|
|
|
|
|
795
|
|
|
|
|
|
|
This applies the sanitization rules for the particular query object. These |
796
|
|
|
|
|
|
|
rules are controlled by the C parameter to the c method. |
797
|
|
|
|
|
|
|
|
798
|
|
|
|
|
|
|
=cut |
799
|
|
|
|
|
|
|
|
800
|
|
|
|
|
|
|
sub sanitize { |
801
|
825
|
|
|
825
|
1
|
1312
|
my $query = shift; |
802
|
825
|
|
|
|
|
1206
|
my $txt = shift; |
803
|
|
|
|
|
|
|
|
804
|
825
|
50
|
|
|
|
2311
|
if (ref($query->{sanitize})) { |
805
|
825
|
|
|
|
|
1976
|
$txt = $query->{sanitize}->($txt); |
806
|
|
|
|
|
|
|
} |
807
|
|
|
|
|
|
|
|
808
|
825
|
|
|
|
|
3681
|
return $txt; |
809
|
|
|
|
|
|
|
} |
810
|
|
|
|
|
|
|
|
811
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
812
|
|
|
|
|
|
|
|
813
|
|
|
|
|
|
|
=head2 C<< strict_sanitize('string') >> |
814
|
|
|
|
|
|
|
|
815
|
|
|
|
|
|
|
This ensures that all the characters in the returned string are printable. All |
816
|
|
|
|
|
|
|
whitespace is converted into spaces, and all other non-printable characters are |
817
|
|
|
|
|
|
|
converted into question marks. This is probably over-aggressive for many |
818
|
|
|
|
|
|
|
applications. |
819
|
|
|
|
|
|
|
|
820
|
|
|
|
|
|
|
This function is used by default when the C option is passed to the |
821
|
|
|
|
|
|
|
C method. |
822
|
|
|
|
|
|
|
|
823
|
|
|
|
|
|
|
B |
824
|
|
|
|
|
|
|
|
825
|
|
|
|
|
|
|
=cut |
826
|
|
|
|
|
|
|
|
827
|
|
|
|
|
|
|
sub strict_sanitize { |
828
|
825
|
|
|
825
|
1
|
1178
|
my $txt = shift; |
829
|
|
|
|
|
|
|
|
830
|
825
|
|
|
|
|
8989
|
$txt =~ s/\s/ /g; |
831
|
825
|
|
|
|
|
8003
|
$txt =~ s/[^[:print:]]/?/g; |
832
|
|
|
|
|
|
|
|
833
|
825
|
|
|
|
|
1623
|
return $txt; |
834
|
|
|
|
|
|
|
} |
835
|
|
|
|
|
|
|
|
836
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
837
|
|
|
|
|
|
|
|
838
|
|
|
|
|
|
|
=head2 C<< $query->debuglog() >> |
839
|
|
|
|
|
|
|
|
840
|
|
|
|
|
|
|
Subclasses may override this with their own debug logger. C is |
841
|
|
|
|
|
|
|
recommended. |
842
|
|
|
|
|
|
|
|
843
|
|
|
|
|
|
|
Alternatively, pass the C constructor a C<< debuglog => sub { ... } >> |
844
|
|
|
|
|
|
|
callback, and we'll pass debugging lines to that. |
845
|
|
|
|
|
|
|
|
846
|
|
|
|
|
|
|
=cut |
847
|
|
|
|
|
|
|
|
848
|
|
|
|
|
|
|
sub debuglog { |
849
|
1696
|
|
|
1696
|
1
|
2752
|
my $query = shift; |
850
|
1696
|
50
|
33
|
|
|
11131
|
return if ref $query and not $query->{debug}; |
851
|
|
|
|
|
|
|
|
852
|
1696
|
|
|
|
|
5657
|
my $toprint = join (" ", @_); |
853
|
1696
|
|
|
|
|
4016
|
chomp $toprint; |
854
|
1696
|
|
|
|
|
4978
|
$toprint = sprintf ("%-8s %s %s %s", |
855
|
|
|
|
|
|
|
("|" x ($query->top->{lookup_count}+1)), |
856
|
|
|
|
|
|
|
$query->{localpart}, |
857
|
|
|
|
|
|
|
$query->{domain}, |
858
|
|
|
|
|
|
|
$toprint); |
859
|
|
|
|
|
|
|
|
860
|
1696
|
50
|
33
|
|
|
16843
|
if (exists $query->{debuglog} and ref $query->{debuglog} eq "CODE") { |
861
|
1696
|
|
|
|
|
2629
|
eval { $query->{debuglog}->($toprint) }; |
|
1696
|
|
|
|
|
8138
|
|
862
|
|
|
|
|
|
|
} |
863
|
|
|
|
|
|
|
else { |
864
|
0
|
|
|
|
|
0
|
printf STDERR "%s", "$toprint\n"; |
865
|
|
|
|
|
|
|
} |
866
|
|
|
|
|
|
|
} |
867
|
|
|
|
|
|
|
|
868
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
869
|
|
|
|
|
|
|
# spfquery |
870
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
871
|
|
|
|
|
|
|
|
872
|
|
|
|
|
|
|
sub spfquery { |
873
|
|
|
|
|
|
|
# |
874
|
|
|
|
|
|
|
# usage: my ($result, $explanation, $text, $time) = $query->spfquery( [ GUESS_MECHS ] ) |
875
|
|
|
|
|
|
|
# |
876
|
|
|
|
|
|
|
# performs a full SPF resolution using the data in $query. to use different data, clone the object. |
877
|
|
|
|
|
|
|
# |
878
|
|
|
|
|
|
|
# if GUESS_MECHS is present, we are operating in "guess" mode so we will not actually query the domain for TXT; we will use the guess_mechs instead. |
879
|
|
|
|
|
|
|
# |
880
|
165
|
|
|
165
|
0
|
250
|
my $query = shift; |
881
|
165
|
|
|
|
|
321
|
my $guess_mechs = shift; |
882
|
|
|
|
|
|
|
|
883
|
165
|
100
|
66
|
|
|
1416
|
if ($query->{ipv4} and |
884
|
1
|
|
|
|
|
4
|
$query->{ipv4}=~ /^127\./) { return "pass", "localhost is always allowed." } |
885
|
|
|
|
|
|
|
|
886
|
164
|
|
|
|
|
703
|
$query->top->{lookup_count}++; |
887
|
|
|
|
|
|
|
|
888
|
164
|
50
|
|
|
|
649
|
if ($query->is_looping) { return "unknown", $query->{spf_error_explanation}, $query->is_looping } |
|
0
|
|
|
|
|
0
|
|
889
|
164
|
50
|
|
|
|
581
|
if ($query->can_use_cached_result) { return $query->cached_result; } |
|
0
|
|
|
|
|
0
|
|
890
|
164
|
|
|
|
|
591
|
else { $query->tell_cache_that_lookup_is_underway; } |
891
|
|
|
|
|
|
|
|
892
|
164
|
|
|
|
|
1889
|
my $directive_set = DirectiveSet->new($query->{domain}, $query, $guess_mechs, $query->{local}, $query->{default_record}); |
893
|
|
|
|
|
|
|
|
894
|
164
|
50
|
|
|
|
1054
|
if (not defined $directive_set) { |
895
|
164
|
|
|
|
|
933
|
$query->debuglog("no SPF record found for $query->{domain}"); |
896
|
164
|
|
|
|
|
1664
|
$query->delete_cache_point; |
897
|
164
|
50
|
|
|
|
870
|
if ($query->{domain} ne $query->{orig_domain}) { |
898
|
0
|
0
|
|
|
|
0
|
if ($query->{error}) { |
899
|
0
|
|
|
|
|
0
|
return "error", $query->{spf_error_explanation}, $query->{error}; |
900
|
|
|
|
|
|
|
} |
901
|
0
|
|
|
|
|
0
|
return "unknown", $query->{spf_error_explanation}, "Missing SPF record at $query->{domain}"; |
902
|
|
|
|
|
|
|
} |
903
|
164
|
50
|
|
|
|
582
|
if ($query->{last_dns_error} eq 'NXDOMAIN') { |
904
|
164
|
|
|
|
|
717
|
my $explanation = $query->macro_substitute($query->{default_explanation}); |
905
|
164
|
|
|
|
|
1323
|
return "unknown", $explanation, "domain of sender $query->{sender} does not exist"; |
906
|
|
|
|
|
|
|
} |
907
|
0
|
|
|
|
|
0
|
return "none", "SPF", "domain of sender $query->{sender} does not designate mailers"; |
908
|
|
|
|
|
|
|
} |
909
|
|
|
|
|
|
|
|
910
|
0
|
0
|
|
|
|
0
|
if ($directive_set->{hard_syntax_error}) { |
911
|
0
|
|
|
|
|
0
|
$query->debuglog(" syntax error while parsing $directive_set->{txt}"); |
912
|
0
|
|
|
|
|
0
|
$query->delete_cache_point; |
913
|
0
|
|
|
|
|
0
|
return "unknown", $query->{spf_error_explanation}, $directive_set->{hard_syntax_error}; |
914
|
|
|
|
|
|
|
} |
915
|
|
|
|
|
|
|
|
916
|
0
|
|
|
|
|
0
|
$query->{directive_set} = $directive_set; |
917
|
|
|
|
|
|
|
|
918
|
0
|
|
|
|
|
0
|
foreach my $mechanism ($directive_set->mechanisms) { |
919
|
0
|
|
|
|
|
0
|
my ($result, $comment) = $query->evaluate_mechanism($mechanism); |
920
|
|
|
|
|
|
|
|
921
|
0
|
0
|
|
|
|
0
|
if ($query->{error}) { |
922
|
0
|
|
|
|
|
0
|
$query->debuglog(" returning temporary error: $query->{error}"); |
923
|
0
|
|
|
|
|
0
|
$query->delete_cache_point; |
924
|
0
|
|
|
|
|
0
|
return "error", $query->{spf_error_explanation}, $query->{error}; |
925
|
|
|
|
|
|
|
} |
926
|
|
|
|
|
|
|
|
927
|
0
|
0
|
|
|
|
0
|
if (defined $result) { |
928
|
0
|
|
|
|
|
0
|
$query->debuglog(" saving result $result to cache point and returning."); |
929
|
0
|
0
|
|
|
|
0
|
my $explanation = $query->interpolate_explanation( |
930
|
|
|
|
|
|
|
($result =~ /^unknown/) |
931
|
|
|
|
|
|
|
? $query->{spf_error_explanation} : $query->{default_explanation}); |
932
|
0
|
|
|
|
|
0
|
$query->save_result_to_cache($result, |
933
|
|
|
|
|
|
|
$explanation, |
934
|
|
|
|
|
|
|
$comment, |
935
|
|
|
|
|
|
|
$query->{directive_set}->{orig_txt}); |
936
|
0
|
|
|
|
|
0
|
$query->{matched_mechanism} = $mechanism; |
937
|
0
|
|
|
|
|
0
|
return $result, $explanation, $comment, $query->{directive_set}->{orig_txt}; |
938
|
|
|
|
|
|
|
} |
939
|
|
|
|
|
|
|
} |
940
|
|
|
|
|
|
|
|
941
|
|
|
|
|
|
|
# run the redirect modifier |
942
|
0
|
0
|
|
|
|
0
|
if ($query->{directive_set}->redirect) { |
943
|
0
|
|
|
|
|
0
|
my $new_domain = $query->macro_substitute($query->{directive_set}->redirect); |
944
|
|
|
|
|
|
|
|
945
|
0
|
|
|
|
|
0
|
$query->debuglog(" executing redirect=$new_domain"); |
946
|
|
|
|
|
|
|
|
947
|
0
|
|
|
|
|
0
|
my $inner_query = $query->clone(domain => $new_domain, |
948
|
|
|
|
|
|
|
reason => "redirects to $new_domain", |
949
|
|
|
|
|
|
|
); |
950
|
|
|
|
|
|
|
|
951
|
0
|
|
|
|
|
0
|
my @inner_result = $inner_query->spfquery(); |
952
|
|
|
|
|
|
|
|
953
|
0
|
|
|
|
|
0
|
$query->delete_cache_point; |
954
|
|
|
|
|
|
|
|
955
|
0
|
|
|
|
|
0
|
$query->debuglog(" executed redirect=$new_domain, got result @inner_result"); |
956
|
|
|
|
|
|
|
|
957
|
0
|
|
|
|
|
0
|
$query->{spf_source} = $inner_query->{spf_source}; |
958
|
0
|
|
|
|
|
0
|
$query->{spf_source_type} = $inner_query->{spf_source_type}; |
959
|
0
|
|
|
|
|
0
|
$query->{matched_mechanism} = $inner_query->{matched_mechanism}; |
960
|
|
|
|
|
|
|
|
961
|
0
|
|
|
|
|
0
|
return @inner_result; |
962
|
|
|
|
|
|
|
} |
963
|
|
|
|
|
|
|
|
964
|
0
|
|
|
|
|
0
|
$query->debuglog(" no mechanisms matched; deleting cache point and using neutral"); |
965
|
0
|
|
|
|
|
0
|
$query->delete_cache_point; |
966
|
0
|
|
|
|
|
0
|
return "neutral", $query->interpolate_explanation($query->{default_explanation}), $directive_set->{soft_syntax_error}; |
967
|
|
|
|
|
|
|
} |
968
|
|
|
|
|
|
|
|
969
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
970
|
|
|
|
|
|
|
# we cache into $Domains_Queried. |
971
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
972
|
|
|
|
|
|
|
|
973
|
|
|
|
|
|
|
sub cache_point { |
974
|
656
|
|
|
656
|
0
|
1122
|
my $query = shift; |
975
|
656
|
|
50
|
|
|
48002
|
return my $cache_point = join "/", ($query->{best_guess} || 0, |
|
|
|
50
|
|
|
|
|
976
|
|
|
|
|
|
|
$query->{guess_mechs} || "", |
977
|
|
|
|
|
|
|
$query->{ipv4}, |
978
|
|
|
|
|
|
|
$query->{localpart}, |
979
|
|
|
|
|
|
|
$query->{domain}, |
980
|
|
|
|
|
|
|
$query->{default_record}, |
981
|
|
|
|
|
|
|
$query->{local}); |
982
|
|
|
|
|
|
|
} |
983
|
|
|
|
|
|
|
|
984
|
|
|
|
|
|
|
sub is_looping { |
985
|
164
|
|
|
164
|
0
|
888
|
my $query = shift; |
986
|
164
|
|
|
|
|
603
|
my $cache_point = $query->cache_point; |
987
|
|
|
|
|
|
|
|
988
|
164
|
50
|
33
|
|
|
752
|
return join(" ", "loop encountered:", @{$query->{loop_report}}) |
|
0
|
|
|
|
|
0
|
|
989
|
|
|
|
|
|
|
if exists $Domains_Queried->{$cache_point} |
990
|
|
|
|
|
|
|
and not defined $Domains_Queried->{$cache_point}->[0]; |
991
|
|
|
|
|
|
|
|
992
|
164
|
50
|
33
|
|
|
842
|
return join(" ", "query caused more than" . $query->max_lookup_count . " lookups:", @{$query->{loop_report}}) |
|
0
|
|
|
|
|
0
|
|
993
|
|
|
|
|
|
|
if $query->max_lookup_count and $query->top->{lookup_count} > $query->max_lookup_count; |
994
|
|
|
|
|
|
|
|
995
|
164
|
|
|
|
|
757
|
return 0; |
996
|
|
|
|
|
|
|
} |
997
|
|
|
|
|
|
|
|
998
|
|
|
|
|
|
|
sub max_lookup_count { |
999
|
328
|
|
|
328
|
0
|
853
|
my $query = shift; |
1000
|
328
|
|
33
|
|
|
7627
|
return $query->{max_lookup_count} || $MAX_LOOKUP_COUNT; |
1001
|
|
|
|
|
|
|
} |
1002
|
|
|
|
|
|
|
|
1003
|
|
|
|
|
|
|
sub can_use_cached_result { |
1004
|
164
|
|
|
164
|
0
|
3795
|
my $query = shift; |
1005
|
164
|
|
|
|
|
369
|
my $cache_point = $query->cache_point; |
1006
|
|
|
|
|
|
|
|
1007
|
164
|
50
|
|
|
|
1717
|
if ($Domains_Queried->{$cache_point}) { |
1008
|
0
|
|
|
|
|
0
|
$query->debuglog(" lookup: we have already processed $query->{domain} before with $query->{ipv4}."); |
1009
|
0
|
|
|
|
|
0
|
my @cached = @{ $Domains_Queried->{$cache_point} }; |
|
0
|
|
|
|
|
0
|
|
1010
|
0
|
0
|
0
|
|
|
0
|
if (not defined $CACHE_TIMEOUT |
1011
|
|
|
|
|
|
|
or time - $cached[-1] > $CACHE_TIMEOUT) { |
1012
|
0
|
|
|
|
|
0
|
$query->debuglog(" lookup: but its cache entry is stale; deleting it."); |
1013
|
0
|
|
|
|
|
0
|
delete $Domains_Queried->{$cache_point}; |
1014
|
0
|
|
|
|
|
0
|
return 0; |
1015
|
|
|
|
|
|
|
} |
1016
|
|
|
|
|
|
|
|
1017
|
0
|
|
|
|
|
0
|
$query->debuglog(" lookup: the cache entry is fresh; returning it."); |
1018
|
0
|
|
|
|
|
0
|
return 1; |
1019
|
|
|
|
|
|
|
} |
1020
|
164
|
|
|
|
|
553
|
return 0; |
1021
|
|
|
|
|
|
|
} |
1022
|
|
|
|
|
|
|
|
1023
|
|
|
|
|
|
|
sub tell_cache_that_lookup_is_underway { |
1024
|
164
|
|
|
164
|
0
|
199
|
my $query = shift; |
1025
|
|
|
|
|
|
|
|
1026
|
|
|
|
|
|
|
# define an entry here so we don't loop endlessly in an Include loop. |
1027
|
164
|
|
|
|
|
4211
|
$Domains_Queried->{$query->cache_point} = [undef, undef, undef, undef, time]; |
1028
|
|
|
|
|
|
|
} |
1029
|
|
|
|
|
|
|
|
1030
|
|
|
|
|
|
|
sub save_result_to_cache { |
1031
|
0
|
|
|
0
|
0
|
0
|
my $query = shift; |
1032
|
0
|
|
|
|
|
0
|
my ($result, $explanation, $comment, $orig_txt) = (shift, shift, shift, shift); |
1033
|
|
|
|
|
|
|
|
1034
|
|
|
|
|
|
|
# define an entry here so we don't loop endlessly in an Include loop. |
1035
|
0
|
|
|
|
|
0
|
$Domains_Queried->{$query->cache_point} = [$result, $explanation, $comment, $orig_txt, time]; |
1036
|
|
|
|
|
|
|
} |
1037
|
|
|
|
|
|
|
|
1038
|
|
|
|
|
|
|
sub cached_result { |
1039
|
0
|
|
|
0
|
0
|
0
|
my $query = shift; |
1040
|
0
|
|
|
|
|
0
|
my $cache_point = $query->cache_point; |
1041
|
|
|
|
|
|
|
|
1042
|
0
|
0
|
|
|
|
0
|
if ($Domains_Queried->{$cache_point}) { |
1043
|
0
|
|
|
|
|
0
|
return @{ $Domains_Queried->{$cache_point} }; |
|
0
|
|
|
|
|
0
|
|
1044
|
|
|
|
|
|
|
} |
1045
|
0
|
|
|
|
|
0
|
return; |
1046
|
|
|
|
|
|
|
} |
1047
|
|
|
|
|
|
|
|
1048
|
|
|
|
|
|
|
sub delete_cache_point { |
1049
|
164
|
|
|
164
|
0
|
279
|
my $query = shift; |
1050
|
164
|
|
|
|
|
964
|
delete $Domains_Queried->{$query->cache_point}; |
1051
|
|
|
|
|
|
|
} |
1052
|
|
|
|
|
|
|
|
1053
|
|
|
|
|
|
|
sub clear_cache { |
1054
|
9
|
|
|
9
|
0
|
10894
|
$Domains_Queried = {}; |
1055
|
|
|
|
|
|
|
} |
1056
|
|
|
|
|
|
|
|
1057
|
|
|
|
|
|
|
sub get_ptr_domain { |
1058
|
0
|
|
|
0
|
0
|
0
|
my ($query) = shift; |
1059
|
|
|
|
|
|
|
|
1060
|
0
|
0
|
|
|
|
0
|
return $query->{ptr_domain} if ($query->{ptr_domain}); |
1061
|
|
|
|
|
|
|
|
1062
|
0
|
|
|
|
|
0
|
foreach my $ptrdname ($query->myquery(reverse_in_addr($query->{ipv4}) . ".in-addr.arpa", "PTR", "ptrdname")) { |
1063
|
0
|
|
|
|
|
0
|
$query->debuglog(" get_ptr_domain: $query->{ipv4} is $ptrdname"); |
1064
|
|
|
|
|
|
|
|
1065
|
0
|
|
|
|
|
0
|
$query->debuglog(" get_ptr_domain: checking hostname $ptrdname for legitimacy."); |
1066
|
|
|
|
|
|
|
|
1067
|
|
|
|
|
|
|
# check for legitimacy --- PTR -> hostname A -> PTR |
1068
|
0
|
|
|
|
|
0
|
foreach my $ptr_to_a ($query->myquery($ptrdname, "A", "address")) { |
1069
|
|
|
|
|
|
|
|
1070
|
0
|
|
|
|
|
0
|
$query->debuglog(" get_ptr_domain: hostname $ptrdname -> $ptr_to_a"); |
1071
|
|
|
|
|
|
|
|
1072
|
0
|
0
|
|
|
|
0
|
if ($ptr_to_a eq $query->{ipv4}) { |
1073
|
0
|
|
|
|
|
0
|
return $query->{ptr_domain} = $ptrdname; |
1074
|
|
|
|
|
|
|
} |
1075
|
|
|
|
|
|
|
} |
1076
|
|
|
|
|
|
|
} |
1077
|
|
|
|
|
|
|
|
1078
|
0
|
|
|
|
|
0
|
return undef; |
1079
|
|
|
|
|
|
|
} |
1080
|
|
|
|
|
|
|
|
1081
|
|
|
|
|
|
|
sub macro_substitute_item { |
1082
|
27
|
|
|
27
|
0
|
51
|
my $query = shift; |
1083
|
27
|
|
|
|
|
70
|
my $arg = shift; |
1084
|
|
|
|
|
|
|
|
1085
|
27
|
50
|
|
|
|
72
|
if ($arg eq "%") { return "%" } |
|
0
|
|
|
|
|
0
|
|
1086
|
27
|
50
|
|
|
|
52
|
if ($arg eq "_") { return " " } |
|
0
|
|
|
|
|
0
|
|
1087
|
27
|
50
|
|
|
|
74
|
if ($arg eq "-") { return "%20" } |
|
0
|
|
|
|
|
0
|
|
1088
|
|
|
|
|
|
|
|
1089
|
27
|
|
|
|
|
153
|
$arg =~ s/^{(.*)}$/$1/; |
1090
|
|
|
|
|
|
|
|
1091
|
27
|
|
|
|
|
592
|
my ($field, $num, $reverse, $delim) = $arg =~ /^(x?\w)(\d*)(r?)(.*)$/; |
1092
|
|
|
|
|
|
|
|
1093
|
27
|
50
|
|
|
|
91
|
$delim = '.' if not length $delim; |
1094
|
|
|
|
|
|
|
|
1095
|
27
|
|
|
|
|
181
|
my $newval = $arg; |
1096
|
27
|
|
|
|
|
36
|
my $timestamp = time; |
1097
|
|
|
|
|
|
|
|
1098
|
27
|
50
|
|
|
|
68
|
$newval = $query->{localpart} if (lc $field eq 'u'); |
1099
|
27
|
50
|
|
|
|
64
|
$newval = $query->{localpart} if (lc $field eq 'l'); |
1100
|
27
|
50
|
|
|
|
60
|
$newval = $query->{domain} if (lc $field eq 'd'); |
1101
|
27
|
100
|
|
|
|
394
|
$newval = $query->{sender} if (lc $field eq 's'); |
1102
|
27
|
50
|
|
|
|
53
|
$newval = $query->{orig_domain} if (lc $field eq 'o'); |
1103
|
27
|
100
|
|
|
|
93
|
$newval = $query->ip if (lc $field eq 'i'); |
1104
|
27
|
50
|
|
|
|
70
|
$newval = $timestamp if (lc $field eq 't'); |
1105
|
27
|
50
|
|
|
|
104
|
$newval = $query->{helo} if (lc $field eq 'h'); |
1106
|
27
|
50
|
|
|
|
59
|
$newval = $query->get_ptr_domain if (lc $field eq 'p'); |
1107
|
27
|
100
|
|
|
|
77
|
$newval = $query->{myhostname} if (lc $field eq 'r'); # only used in explanation |
1108
|
27
|
0
|
|
|
|
60
|
$newval = $query->{ipv4} ? 'in-addr' : 'ip6' |
|
|
50
|
|
|
|
|
|
1109
|
|
|
|
|
|
|
if (lc $field eq 'v'); |
1110
|
|
|
|
|
|
|
|
1111
|
|
|
|
|
|
|
# We need to escape a bunch of characters inside a character class |
1112
|
27
|
|
|
|
|
65
|
$delim =~ s/([\^\-\]\:\\])/\\$1/g; |
1113
|
|
|
|
|
|
|
|
1114
|
27
|
50
|
|
|
|
59
|
if (length $delim) { |
1115
|
27
|
|
|
|
|
306
|
my @parts = split /[$delim]/, $newval; |
1116
|
|
|
|
|
|
|
|
1117
|
27
|
50
|
|
|
|
60
|
@parts = reverse @parts if ($reverse); |
1118
|
|
|
|
|
|
|
|
1119
|
27
|
50
|
|
|
|
53
|
if ($num) { |
1120
|
0
|
|
|
|
|
0
|
while (@parts > $num) { shift @parts } |
|
0
|
|
|
|
|
0
|
|
1121
|
|
|
|
|
|
|
} |
1122
|
|
|
|
|
|
|
|
1123
|
27
|
|
|
|
|
302
|
$newval = join ".", @parts; |
1124
|
|
|
|
|
|
|
} |
1125
|
|
|
|
|
|
|
|
1126
|
27
|
50
|
|
|
|
139
|
$newval = uri_escape($newval) if ($field ne lc $field); |
1127
|
|
|
|
|
|
|
|
1128
|
27
|
|
|
|
|
729
|
$query->debuglog(" macro_substitute_item: $arg: field=$field, num=$num, reverse=$reverse, delim=$delim, newval=$newval"); |
1129
|
|
|
|
|
|
|
|
1130
|
27
|
|
|
|
|
362
|
return $newval; |
1131
|
|
|
|
|
|
|
} |
1132
|
|
|
|
|
|
|
|
1133
|
|
|
|
|
|
|
sub macro_substitute { |
1134
|
164
|
|
|
164
|
0
|
279
|
my $query = shift; |
1135
|
164
|
|
|
|
|
256
|
my $arg = shift; |
1136
|
164
|
|
|
|
|
279
|
my $maxlen = shift; |
1137
|
|
|
|
|
|
|
|
1138
|
164
|
|
|
|
|
327
|
my $original = $arg; |
1139
|
|
|
|
|
|
|
|
1140
|
|
|
|
|
|
|
# macro-char = ( '%{' alpha *digit [ 'r' ] *delim '}' ) |
1141
|
|
|
|
|
|
|
# / '%%' |
1142
|
|
|
|
|
|
|
# / '%_' |
1143
|
|
|
|
|
|
|
# / '%-' |
1144
|
|
|
|
|
|
|
|
1145
|
164
|
|
|
|
|
621
|
$arg =~ s/%([%_-]|{(\w[^}]*)})/$query->macro_substitute_item($1)/ge; |
|
27
|
|
|
|
|
78
|
|
1146
|
|
|
|
|
|
|
|
1147
|
164
|
50
|
33
|
|
|
781
|
if ($maxlen && length $arg > $maxlen) { |
1148
|
0
|
|
|
|
|
0
|
$arg = substr($arg, -$maxlen); # super.long.string -> er.long.string |
1149
|
0
|
|
|
|
|
0
|
$arg =~ s/[^.]*\.//; # er.long.string -> long.string |
1150
|
|
|
|
|
|
|
} |
1151
|
164
|
100
|
|
|
|
491
|
$query->debuglog(" macro_substitute: $original -> $arg") if ($original ne $arg); |
1152
|
164
|
|
|
|
|
546
|
return $arg; |
1153
|
|
|
|
|
|
|
} |
1154
|
|
|
|
|
|
|
|
1155
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
1156
|
|
|
|
|
|
|
# display_mechanism |
1157
|
|
|
|
|
|
|
# |
1158
|
|
|
|
|
|
|
# in human-readable form; used in header_pairs above. |
1159
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
1160
|
|
|
|
|
|
|
|
1161
|
|
|
|
|
|
|
sub display_mechanism { |
1162
|
0
|
|
|
0
|
0
|
0
|
my ($modifier, $mechanism, $argument, $source) = @{shift()}; |
|
0
|
|
|
|
|
0
|
|
1163
|
|
|
|
|
|
|
|
1164
|
0
|
0
|
|
|
|
0
|
return "$modifier$mechanism" . (length($argument) ? ":$argument" : ""); |
1165
|
|
|
|
|
|
|
} |
1166
|
|
|
|
|
|
|
|
1167
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
1168
|
|
|
|
|
|
|
# evaluate_mechanism |
1169
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
1170
|
|
|
|
|
|
|
|
1171
|
|
|
|
|
|
|
sub evaluate_mechanism { |
1172
|
0
|
|
|
0
|
0
|
0
|
my $query = shift; |
1173
|
0
|
|
|
|
|
0
|
my ($modifier, $mechanism, $argument, $source) = @{shift()}; |
|
0
|
|
|
|
|
0
|
|
1174
|
|
|
|
|
|
|
|
1175
|
0
|
0
|
|
|
|
0
|
$modifier = "+" if not length $modifier; |
1176
|
|
|
|
|
|
|
|
1177
|
0
|
|
|
|
|
0
|
$query->debuglog(" evaluate_mechanism: $modifier$mechanism($argument) for domain=$query->{domain}"); |
1178
|
|
|
|
|
|
|
|
1179
|
0
|
0
|
|
|
|
0
|
if ({ map { $_=>1 } @KNOWN_MECHANISMS }->{$mechanism}) { |
|
0
|
|
|
|
|
0
|
|
1180
|
0
|
|
|
|
|
0
|
my $mech_sub = "mech_$mechanism"; |
1181
|
0
|
|
|
|
|
0
|
my ($hit, $text) = $query->$mech_sub($query->macro_substitute($argument, 255)); |
1182
|
1
|
|
|
1
|
|
14
|
no warnings 'uninitialized'; |
|
1
|
|
|
|
|
3
|
|
|
1
|
|
|
|
|
6315
|
|
1183
|
0
|
|
|
|
|
0
|
$query->debuglog(" evaluate_mechanism: $modifier$mechanism($argument) returned $hit $text"); |
1184
|
|
|
|
|
|
|
|
1185
|
0
|
0
|
|
|
|
0
|
return if not $hit; |
1186
|
|
|
|
|
|
|
|
1187
|
0
|
0
|
|
|
|
0
|
return ($hit, $text) if ($hit ne "hit"); |
1188
|
|
|
|
|
|
|
|
1189
|
0
|
0
|
|
|
|
0
|
if ($source) { |
1190
|
0
|
|
|
|
|
0
|
$query->{spf_source} = $source; |
1191
|
0
|
|
|
|
|
0
|
$query->{spf_source_type} = "from mechanism $mechanism"; |
1192
|
|
|
|
|
|
|
} |
1193
|
|
|
|
|
|
|
|
1194
|
0
|
|
|
|
|
0
|
return $query->shorthand2value($modifier), $text; |
1195
|
|
|
|
|
|
|
} |
1196
|
|
|
|
|
|
|
else { |
1197
|
0
|
0
|
|
|
|
0
|
my $unrecognized_mechanism = join ("", |
|
|
0
|
|
|
|
|
|
1198
|
|
|
|
|
|
|
($modifier eq "+" ? "" : $modifier), |
1199
|
|
|
|
|
|
|
$mechanism, |
1200
|
|
|
|
|
|
|
($argument ? ":" : ""), |
1201
|
|
|
|
|
|
|
$argument); |
1202
|
0
|
|
|
|
|
0
|
my $error_string = "unknown $unrecognized_mechanism"; |
1203
|
0
|
|
|
|
|
0
|
$query->debuglog(" evaluate_mechanism: unrecognized mechanism $unrecognized_mechanism, returning $error_string"); |
1204
|
0
|
|
|
|
|
0
|
return $error_string => "unrecognized mechanism $unrecognized_mechanism"; |
1205
|
|
|
|
|
|
|
} |
1206
|
|
|
|
|
|
|
|
1207
|
0
|
|
|
|
|
0
|
return ("neutral", "evaluate-mechanism: neutral"); |
1208
|
|
|
|
|
|
|
} |
1209
|
|
|
|
|
|
|
|
1210
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
1211
|
|
|
|
|
|
|
# myquery wraps DNS resolver queries |
1212
|
|
|
|
|
|
|
# |
1213
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
1214
|
|
|
|
|
|
|
|
1215
|
|
|
|
|
|
|
sub myquery { |
1216
|
164
|
|
|
164
|
0
|
239
|
my $query = shift; |
1217
|
164
|
|
|
|
|
412
|
my $label = shift; |
1218
|
164
|
|
|
|
|
204
|
my $qtype = shift; |
1219
|
164
|
|
|
|
|
256
|
my $method = shift; |
1220
|
164
|
|
|
|
|
751
|
my $sortby = shift; |
1221
|
|
|
|
|
|
|
|
1222
|
164
|
|
|
|
|
584
|
$query->debuglog(" myquery: doing $qtype query on $label"); |
1223
|
|
|
|
|
|
|
|
1224
|
164
|
|
|
|
|
1244
|
for ($label) { |
1225
|
164
|
50
|
33
|
|
|
7420
|
if (/\.\./ or /^\./) { |
1226
|
|
|
|
|
|
|
# convert .foo..com to foo.com, etc. |
1227
|
0
|
|
|
|
|
0
|
$query->debuglog(" myquery: fixing up invalid syntax in $label"); |
1228
|
0
|
|
|
|
|
0
|
s/\.\.+/\./g; |
1229
|
0
|
|
|
|
|
0
|
s/^\.//; |
1230
|
0
|
|
|
|
|
0
|
$query->debuglog(" myquery: corrected label is $label"); |
1231
|
|
|
|
|
|
|
} |
1232
|
|
|
|
|
|
|
} |
1233
|
164
|
|
|
|
|
460
|
my $resquery = $query->resolver->query($label, $qtype); |
1234
|
|
|
|
|
|
|
|
1235
|
164
|
|
|
|
|
2370621
|
my $errorstring = $query->resolver->errorstring; |
1236
|
164
|
50
|
33
|
|
|
2334
|
if (not $resquery and $errorstring eq "NOERROR") { |
1237
|
0
|
|
|
|
|
0
|
return; |
1238
|
|
|
|
|
|
|
} |
1239
|
|
|
|
|
|
|
|
1240
|
164
|
|
|
|
|
718
|
$query->{last_dns_error} = $errorstring; |
1241
|
|
|
|
|
|
|
|
1242
|
164
|
50
|
|
|
|
565
|
if (not $resquery) { |
1243
|
164
|
50
|
|
|
|
1114
|
if ($errorstring eq "NXDOMAIN") { |
1244
|
164
|
|
|
|
|
1149
|
$query->debuglog(" myquery: $label $qtype failed: NXDOMAIN."); |
1245
|
164
|
|
|
|
|
2898
|
return; |
1246
|
|
|
|
|
|
|
} |
1247
|
|
|
|
|
|
|
|
1248
|
0
|
|
|
|
|
0
|
$query->debuglog(" myquery: $label $qtype lookup error: $errorstring"); |
1249
|
0
|
|
|
|
|
0
|
$query->debuglog(" myquery: will set error condition."); |
1250
|
0
|
|
|
|
|
0
|
$query->set_temperror("DNS error while looking up $label $qtype: $errorstring"); |
1251
|
0
|
|
|
|
|
0
|
return; |
1252
|
|
|
|
|
|
|
} |
1253
|
|
|
|
|
|
|
|
1254
|
0
|
|
|
|
|
0
|
my @answers = grep { lc $_->type eq lc $qtype } $resquery->answer; |
|
0
|
|
|
|
|
0
|
|
1255
|
|
|
|
|
|
|
|
1256
|
|
|
|
|
|
|
# $query->debuglog(" myquery: found $qtype response: @answers"); |
1257
|
|
|
|
|
|
|
|
1258
|
0
|
|
|
|
|
0
|
my @toreturn; |
1259
|
0
|
0
|
|
|
|
0
|
if ($sortby) { @toreturn = map { rr_method($_,$method) } sort { $a->$sortby() <=> $b->$sortby() } @answers; } |
|
0
|
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
1260
|
0
|
|
|
|
|
0
|
else { @toreturn = map { rr_method($_,$method) } @answers; } |
|
0
|
|
|
|
|
0
|
|
1261
|
|
|
|
|
|
|
|
1262
|
0
|
0
|
|
|
|
0
|
if (not @toreturn) { |
1263
|
0
|
|
|
|
|
0
|
$query->debuglog(" myquery: result had no data."); |
1264
|
0
|
|
|
|
|
0
|
return; |
1265
|
|
|
|
|
|
|
} |
1266
|
|
|
|
|
|
|
|
1267
|
0
|
|
|
|
|
0
|
return @toreturn; |
1268
|
|
|
|
|
|
|
} |
1269
|
|
|
|
|
|
|
|
1270
|
|
|
|
|
|
|
sub rr_method { |
1271
|
0
|
|
|
0
|
0
|
0
|
my ($answer, $method) = @_; |
1272
|
0
|
0
|
|
|
|
0
|
if ($method ne "char_str_list") { return $answer->$method() } |
|
0
|
|
|
|
|
0
|
|
1273
|
|
|
|
|
|
|
|
1274
|
|
|
|
|
|
|
# long TXT records can't be had with txtdata; they need to be pulled out with char_str_list which returns a list of strings |
1275
|
|
|
|
|
|
|
# that need to be joined. |
1276
|
|
|
|
|
|
|
|
1277
|
0
|
|
|
|
|
0
|
my @char_str_list = $answer->$method(); |
1278
|
|
|
|
|
|
|
# print "rr_method returning join of @char_str_list\n"; |
1279
|
|
|
|
|
|
|
|
1280
|
0
|
|
|
|
|
0
|
return join "", @char_str_list; |
1281
|
|
|
|
|
|
|
} |
1282
|
|
|
|
|
|
|
|
1283
|
|
|
|
|
|
|
# |
1284
|
|
|
|
|
|
|
# Mechanisms return one of the following: |
1285
|
|
|
|
|
|
|
# |
1286
|
|
|
|
|
|
|
# undef mechanism did not match |
1287
|
|
|
|
|
|
|
# "hit" mechanism matched |
1288
|
|
|
|
|
|
|
# "unknown" some error happened during processing |
1289
|
|
|
|
|
|
|
# "error" some temporary error |
1290
|
|
|
|
|
|
|
# |
1291
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
1292
|
|
|
|
|
|
|
# all |
1293
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
1294
|
|
|
|
|
|
|
|
1295
|
|
|
|
|
|
|
sub mech_all { |
1296
|
0
|
|
|
0
|
0
|
0
|
my $query = shift; |
1297
|
0
|
|
|
|
|
0
|
return "hit" => "default"; |
1298
|
|
|
|
|
|
|
} |
1299
|
|
|
|
|
|
|
|
1300
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
1301
|
|
|
|
|
|
|
# include |
1302
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
1303
|
|
|
|
|
|
|
|
1304
|
|
|
|
|
|
|
sub mech_include { |
1305
|
0
|
|
|
0
|
0
|
0
|
my $query = shift; |
1306
|
0
|
|
|
|
|
0
|
my $argument = shift; |
1307
|
|
|
|
|
|
|
|
1308
|
0
|
0
|
|
|
|
0
|
if (not $argument) { |
1309
|
0
|
|
|
|
|
0
|
$query->debuglog(" mechanism include: no argument given."); |
1310
|
0
|
|
|
|
|
0
|
return "unknown", "include mechanism not given an argument"; |
1311
|
|
|
|
|
|
|
} |
1312
|
|
|
|
|
|
|
|
1313
|
0
|
|
|
|
|
0
|
$query->debuglog(" mechanism include: recursing into $argument"); |
1314
|
|
|
|
|
|
|
|
1315
|
0
|
|
|
|
|
0
|
my $inner_query = $query->clone(domain => $argument, |
1316
|
|
|
|
|
|
|
reason => "includes $argument", |
1317
|
|
|
|
|
|
|
local => undef, |
1318
|
|
|
|
|
|
|
trusted => undef, |
1319
|
|
|
|
|
|
|
guess => undef, |
1320
|
|
|
|
|
|
|
default_record => undef, |
1321
|
|
|
|
|
|
|
); |
1322
|
|
|
|
|
|
|
|
1323
|
0
|
|
|
|
|
0
|
my ($result, $explanation, $text, $orig_txt, $time) = $inner_query->spfquery(); |
1324
|
|
|
|
|
|
|
|
1325
|
0
|
|
|
|
|
0
|
$query->debuglog(" mechanism include: got back result $result / $text / $time"); |
1326
|
|
|
|
|
|
|
|
1327
|
0
|
0
|
|
|
|
0
|
if ($result eq "pass") { return hit => $text, $time; } |
|
0
|
|
|
|
|
0
|
|
1328
|
0
|
0
|
|
|
|
0
|
if ($result eq "error") { return $result => $text, $time; } |
|
0
|
|
|
|
|
0
|
|
1329
|
0
|
0
|
|
|
|
0
|
if ($result eq "unknown") { return $result => $text, $time; } |
|
0
|
|
|
|
|
0
|
|
1330
|
0
|
0
|
|
|
|
0
|
if ($result eq "none") { return unknown => $text, $time; } # fail-safe mode. convert an included NONE into an UNKNOWN error. |
|
0
|
|
|
|
|
0
|
|
1331
|
0
|
0
|
0
|
|
|
0
|
if ($result eq "fail" || |
|
|
|
0
|
|
|
|
|
1332
|
|
|
|
|
|
|
$result eq "neutral" || |
1333
|
0
|
|
|
|
|
0
|
$result eq "softfail") { return undef, $text, $time; } |
1334
|
|
|
|
|
|
|
|
1335
|
0
|
|
|
|
|
0
|
$query->debuglog(" mechanism include: reducing result $result to unknown"); |
1336
|
0
|
|
|
|
|
0
|
return "unknown", $text, $time; |
1337
|
|
|
|
|
|
|
} |
1338
|
|
|
|
|
|
|
|
1339
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
1340
|
|
|
|
|
|
|
# a |
1341
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
1342
|
|
|
|
|
|
|
|
1343
|
|
|
|
|
|
|
sub mech_a { |
1344
|
0
|
|
|
0
|
0
|
0
|
my $query = shift; |
1345
|
0
|
|
|
|
|
0
|
my $argument = shift; |
1346
|
|
|
|
|
|
|
|
1347
|
0
|
0
|
|
|
|
0
|
my $ip4_cidr_length = ($argument =~ s/ \/(\d+)//x) ? $1 : 32; |
1348
|
0
|
0
|
|
|
|
0
|
my $ip6_cidr_length = ($argument =~ s/\/\/(\d+)//x) ? $1 : 128; |
1349
|
|
|
|
|
|
|
|
1350
|
0
|
|
0
|
|
|
0
|
my $domain_to_use = $argument || $query->{domain}; |
1351
|
|
|
|
|
|
|
|
1352
|
|
|
|
|
|
|
# see code below in ip4 for more validation |
1353
|
0
|
0
|
|
|
|
0
|
if ($domain_to_use !~ / \. [a-z] (?: [a-z0-9-]* [a-z0-9] ) $ /ix) { |
1354
|
0
|
|
|
|
|
0
|
return ("unknown" => "bad argument to a: $domain_to_use not a valid FQDN"); |
1355
|
|
|
|
|
|
|
} |
1356
|
|
|
|
|
|
|
|
1357
|
0
|
|
|
|
|
0
|
foreach my $a ($query->myquery($domain_to_use, "A", "address")) { |
1358
|
0
|
|
|
|
|
0
|
$query->debuglog(" mechanism a: $a"); |
1359
|
0
|
0
|
|
|
|
0
|
if ($a eq $query->{ipv4}) { |
|
|
0
|
|
|
|
|
|
1360
|
0
|
|
|
|
|
0
|
$query->debuglog(" mechanism a: match found: $domain_to_use A $a == $query->{ipv4}"); |
1361
|
0
|
|
|
|
|
0
|
return "hit", "$domain_to_use A $query->{ipv4}"; |
1362
|
|
|
|
|
|
|
} |
1363
|
|
|
|
|
|
|
elsif ($ip4_cidr_length < 32) { |
1364
|
0
|
|
|
|
|
0
|
my $cidr = Net::CIDR::Lite->new("$a/$ip4_cidr_length"); |
1365
|
|
|
|
|
|
|
|
1366
|
0
|
|
|
|
|
0
|
$query->debuglog(" mechanism a: looking for $query->{ipv4} in $a/$ip4_cidr_length"); |
1367
|
|
|
|
|
|
|
|
1368
|
0
|
0
|
|
|
|
0
|
return (hit => "$domain_to_use A $a /$ip4_cidr_length contains $query->{ipv4}") |
1369
|
|
|
|
|
|
|
if $cidr->find($query->{ipv4}); |
1370
|
|
|
|
|
|
|
} |
1371
|
|
|
|
|
|
|
} |
1372
|
0
|
|
|
|
|
0
|
return; |
1373
|
|
|
|
|
|
|
} |
1374
|
|
|
|
|
|
|
|
1375
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
1376
|
|
|
|
|
|
|
# mx |
1377
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
1378
|
|
|
|
|
|
|
|
1379
|
|
|
|
|
|
|
sub mech_mx { |
1380
|
0
|
|
|
0
|
0
|
0
|
my $query = shift; |
1381
|
0
|
|
|
|
|
0
|
my $argument = shift; |
1382
|
|
|
|
|
|
|
|
1383
|
0
|
0
|
|
|
|
0
|
my $ip4_cidr_length = ($argument =~ s/ \/(\d+)//x) ? $1 : 32; |
1384
|
0
|
0
|
|
|
|
0
|
my $ip6_cidr_length = ($argument =~ s/\/\/(\d+)//x) ? $1 : 128; |
1385
|
|
|
|
|
|
|
|
1386
|
0
|
|
0
|
|
|
0
|
my $domain_to_use = $argument || $query->{domain}; |
1387
|
|
|
|
|
|
|
|
1388
|
0
|
0
|
|
|
|
0
|
if ($domain_to_use !~ / \. [a-z] (?: [a-z0-9-]* [a-z0-9] ) $ /ix) { |
1389
|
0
|
|
|
|
|
0
|
return ("unknown" => "bad argument to mx: $domain_to_use not a valid FQDN"); |
1390
|
|
|
|
|
|
|
} |
1391
|
|
|
|
|
|
|
|
1392
|
0
|
|
|
|
|
0
|
my @mxes = $query->myquery($domain_to_use, "MX", "exchange", "preference"); |
1393
|
|
|
|
|
|
|
|
1394
|
0
|
|
|
|
|
0
|
foreach my $mx (@mxes) { |
1395
|
|
|
|
|
|
|
# $query->debuglog(" mechanism mx: $mx"); |
1396
|
|
|
|
|
|
|
|
1397
|
0
|
|
|
|
|
0
|
foreach my $a ($query->myquery($mx, "A", "address")) { |
1398
|
0
|
0
|
|
|
|
0
|
if ($a eq $query->{ipv4}) { |
|
|
0
|
|
|
|
|
|
1399
|
0
|
|
|
|
|
0
|
$query->debuglog(" mechanism mx: we have a match; $domain_to_use MX $mx A $a == $query->{ipv4}"); |
1400
|
0
|
|
|
|
|
0
|
return "hit", "$domain_to_use MX $mx A $a"; |
1401
|
|
|
|
|
|
|
} |
1402
|
|
|
|
|
|
|
elsif ($ip4_cidr_length < 32) { |
1403
|
0
|
|
|
|
|
0
|
my $cidr = Net::CIDR::Lite->new("$a/$ip4_cidr_length"); |
1404
|
|
|
|
|
|
|
|
1405
|
0
|
|
|
|
|
0
|
$query->debuglog(" mechanism mx: looking for $query->{ipv4} in $a/$ip4_cidr_length"); |
1406
|
|
|
|
|
|
|
|
1407
|
0
|
0
|
|
|
|
0
|
return (hit => "$domain_to_use MX $mx A $a /$ip4_cidr_length contains $query->{ipv4}") |
1408
|
|
|
|
|
|
|
if $cidr->find($query->{ipv4}); |
1409
|
|
|
|
|
|
|
|
1410
|
|
|
|
|
|
|
} |
1411
|
|
|
|
|
|
|
} |
1412
|
|
|
|
|
|
|
} |
1413
|
0
|
|
|
|
|
0
|
return; |
1414
|
|
|
|
|
|
|
} |
1415
|
|
|
|
|
|
|
|
1416
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
1417
|
|
|
|
|
|
|
# ptr |
1418
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
1419
|
|
|
|
|
|
|
|
1420
|
|
|
|
|
|
|
sub mech_ptr { |
1421
|
0
|
|
|
0
|
0
|
0
|
my $query = shift; |
1422
|
0
|
|
|
|
|
0
|
my $argument = shift; |
1423
|
|
|
|
|
|
|
|
1424
|
0
|
0
|
|
|
|
0
|
if ($query->{ipv6}) { return "neutral", "ipv6 not yet supported"; } |
|
0
|
|
|
|
|
0
|
|
1425
|
|
|
|
|
|
|
|
1426
|
0
|
|
0
|
|
|
0
|
my $domain_to_use = $argument || $query->{domain}; |
1427
|
|
|
|
|
|
|
|
1428
|
0
|
|
|
|
|
0
|
foreach my $ptrdname ($query->myquery(reverse_in_addr($query->{ipv4}) . ".in-addr.arpa", "PTR", "ptrdname")) { |
1429
|
0
|
|
|
|
|
0
|
$query->debuglog(" mechanism ptr: $query->{ipv4} is $ptrdname"); |
1430
|
|
|
|
|
|
|
|
1431
|
0
|
|
|
|
|
0
|
$query->debuglog(" mechanism ptr: checking hostname $ptrdname for legitimacy."); |
1432
|
|
|
|
|
|
|
|
1433
|
|
|
|
|
|
|
# check for legitimacy --- PTR -> hostname A -> PTR |
1434
|
0
|
|
|
|
|
0
|
foreach my $ptr_to_a ($query->myquery($ptrdname, "A", "address")) { |
1435
|
|
|
|
|
|
|
|
1436
|
0
|
|
|
|
|
0
|
$query->debuglog(" mechanism ptr: hostname $ptrdname -> $ptr_to_a"); |
1437
|
|
|
|
|
|
|
|
1438
|
0
|
0
|
|
|
|
0
|
if ($ptr_to_a eq $query->{ipv4}) { |
1439
|
0
|
|
|
|
|
0
|
$query->debuglog(" mechanism ptr: we have a valid PTR: $query->{ipv4} PTR $ptrdname A $ptr_to_a"); |
1440
|
0
|
|
|
|
|
0
|
$query->debuglog(" mechanism ptr: now we see if $ptrdname ends in $domain_to_use."); |
1441
|
|
|
|
|
|
|
|
1442
|
0
|
0
|
|
|
|
0
|
if ($ptrdname =~ /(^|\.)\Q$domain_to_use\E$/i) { |
1443
|
0
|
|
|
|
|
0
|
$query->debuglog(" mechanism ptr: $query->{ipv4} PTR $ptrdname does end in $domain_to_use."); |
1444
|
0
|
|
|
|
|
0
|
return hit => "$query->{ipv4} PTR $ptrdname matches $domain_to_use"; |
1445
|
|
|
|
|
|
|
} |
1446
|
|
|
|
|
|
|
else { |
1447
|
0
|
|
|
|
|
0
|
$query->debuglog(" mechanism ptr: $ptrdname does not end in $domain_to_use. no match."); |
1448
|
|
|
|
|
|
|
} |
1449
|
|
|
|
|
|
|
} |
1450
|
|
|
|
|
|
|
} |
1451
|
|
|
|
|
|
|
} |
1452
|
0
|
|
|
|
|
0
|
return; |
1453
|
|
|
|
|
|
|
} |
1454
|
|
|
|
|
|
|
|
1455
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
1456
|
|
|
|
|
|
|
# exists |
1457
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
1458
|
|
|
|
|
|
|
|
1459
|
|
|
|
|
|
|
sub mech_exists { |
1460
|
0
|
|
|
0
|
0
|
0
|
my $query = shift; |
1461
|
0
|
|
|
|
|
0
|
my $argument = shift; |
1462
|
|
|
|
|
|
|
|
1463
|
0
|
0
|
|
|
|
0
|
return if (!$argument); |
1464
|
|
|
|
|
|
|
|
1465
|
0
|
|
|
|
|
0
|
my $domain_to_use = $argument; |
1466
|
|
|
|
|
|
|
|
1467
|
0
|
|
|
|
|
0
|
$query->debuglog(" mechanism exists: looking up $domain_to_use"); |
1468
|
|
|
|
|
|
|
|
1469
|
0
|
|
|
|
|
0
|
foreach ($query->myquery($domain_to_use, "A", "address")) { |
1470
|
0
|
|
|
|
|
0
|
$query->debuglog(" mechanism exists: $_"); |
1471
|
0
|
|
|
|
|
0
|
$query->debuglog(" mechanism exists: we have a match."); |
1472
|
0
|
|
|
|
|
0
|
my @txt = map { s/^"//; s/"$//; $_ } $query->myquery($domain_to_use, "TXT", "char_str_list"); |
|
0
|
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
1473
|
0
|
0
|
|
|
|
0
|
if (@txt) { |
1474
|
0
|
|
|
|
|
0
|
return hit => join(" ", @txt); |
1475
|
|
|
|
|
|
|
} |
1476
|
0
|
|
|
|
|
0
|
return hit => "$domain_to_use found"; |
1477
|
|
|
|
|
|
|
} |
1478
|
0
|
|
|
|
|
0
|
return; |
1479
|
|
|
|
|
|
|
} |
1480
|
|
|
|
|
|
|
|
1481
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
1482
|
|
|
|
|
|
|
# ip4 |
1483
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
1484
|
|
|
|
|
|
|
|
1485
|
|
|
|
|
|
|
sub mech_ip4 { |
1486
|
0
|
|
|
0
|
0
|
0
|
my $query = shift; |
1487
|
0
|
|
|
|
|
0
|
my $cidr_spec = shift; |
1488
|
|
|
|
|
|
|
|
1489
|
0
|
0
|
|
|
|
0
|
if ($cidr_spec eq '') { |
1490
|
0
|
|
|
|
|
0
|
return ("unknown" => "no argument given to ip4"); |
1491
|
|
|
|
|
|
|
} |
1492
|
|
|
|
|
|
|
|
1493
|
0
|
|
|
|
|
0
|
my ($network, $cidr_length) = split (/\//, $cidr_spec, 2); |
1494
|
|
|
|
|
|
|
|
1495
|
0
|
0
|
0
|
|
|
0
|
if ( |
|
|
|
0
|
|
|
|
|
1496
|
|
|
|
|
|
|
$network !~ /^\d+\.\d+\.\d+\.\d+$/ || |
1497
|
|
|
|
|
|
|
(defined($cidr_length) && $cidr_length !~ /^\d+$/) |
1498
|
0
|
|
|
|
|
0
|
) { return ("unknown" => "bad argument to ip4: $cidr_spec"); } |
1499
|
|
|
|
|
|
|
|
1500
|
0
|
0
|
|
|
|
0
|
$cidr_length = "32" if not defined $cidr_length; |
1501
|
|
|
|
|
|
|
|
1502
|
0
|
|
|
|
|
0
|
local $@; |
1503
|
0
|
|
|
|
|
0
|
my $cidr = eval { Net::CIDR::Lite->new("$network/$cidr_length") }; |
|
0
|
|
|
|
|
0
|
|
1504
|
0
|
0
|
|
|
|
0
|
if ($@) { return ("unknown" => "unable to parse ip4:$cidr_spec"); } |
|
0
|
|
|
|
|
0
|
|
1505
|
|
|
|
|
|
|
|
1506
|
0
|
|
|
|
|
0
|
$query->debuglog(" mechanism ip4: looking for $query->{ipv4} in $cidr_spec"); |
1507
|
|
|
|
|
|
|
|
1508
|
0
|
0
|
|
|
|
0
|
return (hit => "$cidr_spec contains $query->{ipv4}") if $cidr->find($query->{ipv4}); |
1509
|
|
|
|
|
|
|
|
1510
|
0
|
|
|
|
|
0
|
return; |
1511
|
|
|
|
|
|
|
} |
1512
|
|
|
|
|
|
|
|
1513
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
1514
|
|
|
|
|
|
|
# ip6 |
1515
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
1516
|
|
|
|
|
|
|
|
1517
|
|
|
|
|
|
|
sub mech_ip6 { |
1518
|
0
|
|
|
0
|
0
|
0
|
my $query = shift; |
1519
|
|
|
|
|
|
|
|
1520
|
0
|
|
|
|
|
0
|
return; |
1521
|
|
|
|
|
|
|
} |
1522
|
|
|
|
|
|
|
|
1523
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
1524
|
|
|
|
|
|
|
# functions |
1525
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
1526
|
|
|
|
|
|
|
|
1527
|
|
|
|
|
|
|
sub ip { # accessor |
1528
|
174
|
|
|
174
|
0
|
360
|
my $query = shift; |
1529
|
174
|
|
33
|
|
|
782
|
return $query->{ipv4} || $query->{ipv6}; |
1530
|
|
|
|
|
|
|
} |
1531
|
|
|
|
|
|
|
|
1532
|
|
|
|
|
|
|
sub reverse_in_addr { |
1533
|
165
|
|
|
165
|
0
|
1447
|
return join (".", (reverse split /\./, shift)); |
1534
|
|
|
|
|
|
|
} |
1535
|
|
|
|
|
|
|
|
1536
|
|
|
|
|
|
|
sub resolver { |
1537
|
328
|
|
|
328
|
0
|
929
|
my $query = shift; |
1538
|
328
|
|
66
|
|
|
3817
|
return $query->{res} ||= Net::DNS::Resolver->new( |
1539
|
|
|
|
|
|
|
tcp_timeout => $DNS_RESOLVER_TIMEOUT, |
1540
|
|
|
|
|
|
|
udp_timeout => $DNS_RESOLVER_TIMEOUT, |
1541
|
|
|
|
|
|
|
); |
1542
|
|
|
|
|
|
|
} |
1543
|
|
|
|
|
|
|
|
1544
|
|
|
|
|
|
|
sub fallbacks { |
1545
|
0
|
|
|
0
|
0
|
0
|
my $query = shift; |
1546
|
0
|
|
|
|
|
0
|
return @{$query->{fallbacks}}; |
|
0
|
|
|
|
|
0
|
|
1547
|
|
|
|
|
|
|
} |
1548
|
|
|
|
|
|
|
|
1549
|
|
|
|
|
|
|
sub shorthand2value { |
1550
|
0
|
|
|
0
|
0
|
0
|
my $query = shift; |
1551
|
0
|
|
|
|
|
0
|
my $shorthand = shift; |
1552
|
0
|
|
0
|
|
|
0
|
return { "-" => "fail", |
1553
|
|
|
|
|
|
|
"+" => "pass", |
1554
|
|
|
|
|
|
|
"~" => "softfail", |
1555
|
|
|
|
|
|
|
"?" => "neutral" } -> {$shorthand} || $shorthand; |
1556
|
|
|
|
|
|
|
} |
1557
|
|
|
|
|
|
|
|
1558
|
|
|
|
|
|
|
sub value2shorthand { |
1559
|
0
|
|
|
0
|
0
|
0
|
my $query = shift; |
1560
|
0
|
|
|
|
|
0
|
my $value = lc shift; |
1561
|
0
|
|
0
|
|
|
0
|
return { "fail" => "-", |
1562
|
|
|
|
|
|
|
"pass" => "+", |
1563
|
|
|
|
|
|
|
"softfail" => "~", |
1564
|
|
|
|
|
|
|
"deny" => "-", |
1565
|
|
|
|
|
|
|
"allow" => "+", |
1566
|
|
|
|
|
|
|
"softdeny" => "~", |
1567
|
|
|
|
|
|
|
"unknown" => "?", |
1568
|
|
|
|
|
|
|
"neutral" => "?" } -> {$value} || $value; |
1569
|
|
|
|
|
|
|
} |
1570
|
|
|
|
|
|
|
|
1571
|
|
|
|
|
|
|
sub interpolate_explanation { |
1572
|
0
|
|
|
0
|
0
|
0
|
my $query = shift; |
1573
|
0
|
|
|
|
|
0
|
my $txt = shift; |
1574
|
|
|
|
|
|
|
|
1575
|
0
|
0
|
|
|
|
0
|
if ($query->{directive_set}->explanation) { |
1576
|
0
|
|
|
|
|
0
|
my @txt = map { s/^"//; s/"$//; $_ } $query->myquery($query->macro_substitute($query->{directive_set}->explanation), "TXT", "char_str_list"); |
|
0
|
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
1577
|
0
|
|
|
|
|
0
|
$txt = join " ", @txt; |
1578
|
|
|
|
|
|
|
} |
1579
|
|
|
|
|
|
|
|
1580
|
0
|
|
|
|
|
0
|
return $query->macro_substitute($txt); |
1581
|
|
|
|
|
|
|
} |
1582
|
|
|
|
|
|
|
|
1583
|
|
|
|
|
|
|
sub find_ancestor { |
1584
|
0
|
|
|
0
|
0
|
0
|
my $query = shift; |
1585
|
0
|
|
|
|
|
0
|
my $which_hash = shift; |
1586
|
0
|
|
|
|
|
0
|
my $current_domain = shift; |
1587
|
|
|
|
|
|
|
|
1588
|
0
|
0
|
|
|
|
0
|
return if not exists $query->{$which_hash}; |
1589
|
|
|
|
|
|
|
|
1590
|
0
|
|
|
|
|
0
|
$current_domain =~ s/\.$//g; |
1591
|
0
|
|
|
|
|
0
|
my @current_domain = split /\./, $current_domain; |
1592
|
|
|
|
|
|
|
|
1593
|
0
|
|
|
|
|
0
|
foreach my $ancestor_level (0 .. @current_domain) { |
1594
|
0
|
|
|
|
|
0
|
my @ancestor = @current_domain; |
1595
|
0
|
|
|
|
|
0
|
for (1 .. $ancestor_level) { shift @ancestor } |
|
0
|
|
|
|
|
0
|
|
1596
|
0
|
|
|
|
|
0
|
my $ancestor = join ".", @ancestor; |
1597
|
|
|
|
|
|
|
|
1598
|
0
|
0
|
|
|
|
0
|
for my $match ($ancestor_level > 0 ? "*.$ancestor" : $ancestor) { |
1599
|
0
|
|
|
|
|
0
|
$query->debuglog(" DirectiveSet $which_hash: is $match in the $which_hash hash?"); |
1600
|
0
|
0
|
|
|
|
0
|
if (my $record = $query->{$which_hash}->{lc $match}) { |
1601
|
0
|
|
|
|
|
0
|
$query->debuglog(" DirectiveSet $which_hash: yes, it is."); |
1602
|
0
|
0
|
|
|
|
0
|
return wantarray ? ($which_hash, $match, $record) : $record; |
1603
|
|
|
|
|
|
|
} |
1604
|
|
|
|
|
|
|
} |
1605
|
|
|
|
|
|
|
} |
1606
|
0
|
|
|
|
|
0
|
return; |
1607
|
|
|
|
|
|
|
} |
1608
|
|
|
|
|
|
|
|
1609
|
|
|
|
|
|
|
sub found_record_for { |
1610
|
0
|
|
|
0
|
0
|
0
|
my $query = shift; |
1611
|
0
|
|
|
|
|
0
|
my ($which_hash, $matched_domain_glob, $record) = $query->find_ancestor(@_); |
1612
|
0
|
0
|
|
|
|
0
|
return if not $record; |
1613
|
0
|
|
|
|
|
0
|
$query->{spf_source} = "explicit $which_hash found: $matched_domain_glob defines $record"; |
1614
|
0
|
|
|
|
|
0
|
$query->{spf_source_type} = "full-explanation"; |
1615
|
0
|
0
|
|
|
|
0
|
$record = "v=spf1 $record" if $record !~ /^v=spf1\b/i; |
1616
|
0
|
|
|
|
|
0
|
return $record; |
1617
|
|
|
|
|
|
|
} |
1618
|
|
|
|
|
|
|
|
1619
|
|
|
|
|
|
|
sub try_override { |
1620
|
0
|
|
|
0
|
0
|
0
|
my $query = shift; |
1621
|
0
|
|
|
|
|
0
|
return $query->found_record_for("override", @_); |
1622
|
|
|
|
|
|
|
} |
1623
|
|
|
|
|
|
|
|
1624
|
|
|
|
|
|
|
sub try_fallback { |
1625
|
0
|
|
|
0
|
0
|
0
|
my $query = shift; |
1626
|
0
|
|
|
|
|
0
|
return $query->found_record_for("fallback", @_); |
1627
|
|
|
|
|
|
|
} |
1628
|
|
|
|
|
|
|
|
1629
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
1630
|
|
|
|
|
|
|
# algo |
1631
|
|
|
|
|
|
|
# ---------------------------------------------------------- |
1632
|
|
|
|
|
|
|
|
1633
|
|
|
|
|
|
|
{ |
1634
|
|
|
|
|
|
|
package DirectiveSet; |
1635
|
|
|
|
|
|
|
|
1636
|
|
|
|
|
|
|
sub new { |
1637
|
164
|
|
|
164
|
|
303
|
my $class = shift; |
1638
|
164
|
|
|
|
|
501
|
my $current_domain = shift; |
1639
|
164
|
|
|
|
|
328
|
my $query = shift; |
1640
|
164
|
|
|
|
|
276
|
my $override_text = shift; |
1641
|
164
|
|
|
|
|
211
|
my $localpolicy = shift; |
1642
|
164
|
|
|
|
|
332
|
my $default_record = shift; |
1643
|
|
|
|
|
|
|
|
1644
|
164
|
|
|
|
|
253
|
my $txt; |
1645
|
|
|
|
|
|
|
|
1646
|
|
|
|
|
|
|
# Overrides can come from two places: |
1647
|
|
|
|
|
|
|
# - When operating in best_guess mode, spfquery may be called with a $guess_mechs argument, which comes in as $override_text. |
1648
|
|
|
|
|
|
|
# - When operating with ->new(..., override => { ... }) we need to load the override dynamically. |
1649
|
164
|
50
|
|
|
|
703
|
if ($override_text) { |
|
|
50
|
|
|
|
|
|
1650
|
0
|
|
|
|
|
0
|
$txt = "v=spf1 $override_text ?all"; |
1651
|
0
|
|
|
|
|
0
|
$query->{spf_source} = "local policy"; |
1652
|
0
|
|
|
|
|
0
|
$query->{spf_source_type} = "full-explanation"; |
1653
|
|
|
|
|
|
|
} |
1654
|
|
|
|
|
|
|
elsif (exists $query->{override}) { |
1655
|
0
|
|
|
|
|
0
|
$txt = $query->try_override($current_domain); |
1656
|
|
|
|
|
|
|
} |
1657
|
|
|
|
|
|
|
|
1658
|
|
|
|
|
|
|
# Retrieve a record from DNS: |
1659
|
164
|
50
|
|
|
|
598
|
if (!defined $txt) { |
1660
|
164
|
|
|
|
|
221
|
my @txt; |
1661
|
164
|
|
|
|
|
660
|
$query->debuglog(" DirectiveSet->new(): doing TXT query on $current_domain"); |
1662
|
164
|
|
|
|
|
2386
|
@txt = $query->myquery($current_domain, "TXT", "char_str_list"); |
1663
|
164
|
|
|
|
|
1683
|
$query->debuglog(" DirectiveSet->new(): TXT query on $current_domain returned error=$query->{error}, last_dns_error=$query->{last_dns_error}"); |
1664
|
|
|
|
|
|
|
|
1665
|
|
|
|
|
|
|
# Combine multiple TXT strings into a single string: |
1666
|
164
|
|
|
|
|
1808
|
foreach (@txt) { |
1667
|
0
|
0
|
|
|
|
0
|
$txt .= $1 if /^v=spf1\s*(.*)$/; |
1668
|
|
|
|
|
|
|
} |
1669
|
|
|
|
|
|
|
|
1670
|
|
|
|
|
|
|
$txt = undef |
1671
|
164
|
50
|
33
|
|
|
1292
|
if $query->{error} or $query->{last_dns_error} eq 'NXDOMAIN'; |
1672
|
|
|
|
|
|
|
} |
1673
|
|
|
|
|
|
|
|
1674
|
|
|
|
|
|
|
# Try the fallbacks: |
1675
|
164
|
50
|
33
|
|
|
1019
|
if (!defined $txt and exists $query->{fallback}) { |
1676
|
0
|
|
|
|
|
0
|
$query->debuglog(" DirectiveSet->new(): will try fallbacks."); |
1677
|
0
|
|
|
|
|
0
|
$txt = $query->try_fallback($current_domain, "fallback"); |
1678
|
0
|
0
|
|
|
|
0
|
defined($txt) |
1679
|
|
|
|
|
|
|
or $query->debuglog(" DirectiveSet->new(): fallback search failed."); |
1680
|
|
|
|
|
|
|
} |
1681
|
|
|
|
|
|
|
|
1682
|
164
|
50
|
33
|
|
|
1179
|
if (!defined $txt and defined $default_record) { |
1683
|
0
|
|
|
|
|
0
|
$txt = "v=spf1 $default_record ?all"; |
1684
|
0
|
|
|
|
|
0
|
$query->{spf_source} = "local policy"; |
1685
|
0
|
|
|
|
|
0
|
$query->{spf_source_type} = "full-explanation"; |
1686
|
|
|
|
|
|
|
} |
1687
|
|
|
|
|
|
|
|
1688
|
164
|
|
|
|
|
2132
|
$query->debuglog(" DirectiveSet->new(): SPF policy: $txt"); |
1689
|
|
|
|
|
|
|
|
1690
|
164
|
50
|
|
|
|
1584
|
return if not defined $txt; |
1691
|
|
|
|
|
|
|
|
1692
|
|
|
|
|
|
|
# TODO: the prepending of the v=spf1 is a massive hack; get it right by saving the actual raw orig_txt. |
1693
|
0
|
0
|
|
|
|
|
my $directive_set = bless { orig_txt => ($txt =~ /^v=spf1/ ? $txt : "v=spf1 $txt"), txt => $txt } , $class; |
1694
|
|
|
|
|
|
|
|
1695
|
|
|
|
|
|
|
TXT_RESPONSE: |
1696
|
0
|
|
|
|
|
|
for ($txt) { |
1697
|
0
|
|
|
|
|
|
$query->debuglog(" lookup: TXT $_"); |
1698
|
|
|
|
|
|
|
|
1699
|
|
|
|
|
|
|
# parse the policy record |
1700
|
|
|
|
|
|
|
|
1701
|
0
|
|
|
|
|
|
while (/\S/) { |
1702
|
0
|
|
|
|
|
|
s/^\s*(\S+)\s*//; |
1703
|
0
|
|
|
|
|
|
my $word = $1; |
1704
|
|
|
|
|
|
|
# $query->debuglog(" lookup: word parsing word $word"); |
1705
|
0
|
0
|
|
|
|
|
if ($word =~ /^v=(\S+)/i) { |
1706
|
0
|
|
|
|
|
|
my $version = $1; |
1707
|
0
|
|
|
|
|
|
$query->debuglog(" lookup: TXT version=$version"); |
1708
|
0
|
|
|
|
|
|
$directive_set->{version} = $version; |
1709
|
0
|
0
|
|
|
|
|
next TXT_RESPONSE if ($version ne "spf1"); |
1710
|
0
|
|
|
|
|
|
next; |
1711
|
|
|
|
|
|
|
} |
1712
|
|
|
|
|
|
|
|
1713
|
|
|
|
|
|
|
# modifiers always have an = sign. |
1714
|
0
|
0
|
|
|
|
|
if (my ($lhs, $rhs) = $word =~ /^([^:\/]+)=(\S*)$/) { |
1715
|
|
|
|
|
|
|
# $query->debuglog(" lookup: TXT modifier found: $lhs = $rhs"); |
1716
|
|
|
|
|
|
|
|
1717
|
|
|
|
|
|
|
# if we ever come to support multiple of the same modifier, we need to make this a list. |
1718
|
0
|
|
|
|
|
|
$directive_set->{modifiers}->{lc $lhs} = $rhs; |
1719
|
0
|
|
|
|
|
|
next; |
1720
|
|
|
|
|
|
|
} |
1721
|
|
|
|
|
|
|
|
1722
|
|
|
|
|
|
|
# RHS optional, defaults to domain. |
1723
|
|
|
|
|
|
|
# [:/] matches a:foo and a/24 |
1724
|
0
|
0
|
|
|
|
|
if (my ($prefix, $lhs, $rhs) = $word =~ /^([-~+?]?)([\w_-]+)([\/:]\S*)?$/i) { |
1725
|
0
|
|
|
|
|
|
$rhs =~ s/^://; |
1726
|
0
|
|
0
|
|
|
|
$prefix ||= "+"; |
1727
|
0
|
|
|
|
|
|
$query->debuglog(" lookup: TXT prefix=$prefix, lhs=$lhs, rhs=$rhs"); |
1728
|
0
|
|
|
|
|
|
push @{$directive_set->{mechanisms}}, [$prefix => lc $lhs => $rhs]; |
|
0
|
|
|
|
|
|
|
1729
|
0
|
|
|
|
|
|
next; |
1730
|
|
|
|
|
|
|
} |
1731
|
|
|
|
|
|
|
|
1732
|
|
|
|
|
|
|
} |
1733
|
|
|
|
|
|
|
} |
1734
|
|
|
|
|
|
|
|
1735
|
0
|
0
|
|
|
|
|
if (my $rhs = delete $directive_set->{modifiers}->{default}) { |
1736
|
0
|
|
|
|
|
|
push @{$directive_set->{mechanisms}}, [ $query->value2shorthand($rhs), all => undef ]; |
|
0
|
|
|
|
|
|
|
1737
|
|
|
|
|
|
|
} |
1738
|
|
|
|
|
|
|
|
1739
|
0
|
0
|
|
|
|
|
$directive_set->{mechanisms} = [] if not $directive_set->{mechanisms}; |
1740
|
0
|
0
|
|
|
|
|
if ($localpolicy) { |
1741
|
0
|
|
|
|
|
|
my $mechanisms = $directive_set->{mechanisms}; |
1742
|
0
|
|
|
|
|
|
my $lastmech = $mechanisms->[$#$mechanisms]; |
1743
|
0
|
0
|
0
|
|
|
|
if (($lastmech->[0] eq '-' || $lastmech->[0] eq '?') && |
|
|
|
0
|
|
|
|
|
1744
|
|
|
|
|
|
|
$lastmech->[1] eq 'all') { |
1745
|
0
|
|
|
|
|
|
my $index; |
1746
|
|
|
|
|
|
|
|
1747
|
0
|
|
|
|
|
|
for ($index = $#$mechanisms - 1; $index >= 0; $index--) { |
1748
|
0
|
0
|
|
|
|
|
last if ($lastmech->[0] ne $mechanisms->[$index]->[0]); |
1749
|
|
|
|
|
|
|
} |
1750
|
0
|
0
|
|
|
|
|
if ($index >= 0) { |
1751
|
|
|
|
|
|
|
# We want to insert the localpolicy just *after* $index |
1752
|
0
|
|
|
|
|
|
$query->debuglog(" inserting local policy mechanisms into @{[$directive_set->show_mechanisms]} after position $index"); |
|
0
|
|
|
|
|
|
|
1753
|
0
|
|
|
|
|
|
my $localset = DirectiveSet->new($current_domain, $query->clone, $localpolicy); |
1754
|
|
|
|
|
|
|
|
1755
|
0
|
0
|
|
|
|
|
if ($localset) { |
1756
|
0
|
|
|
|
|
|
my @locallist = $localset->mechanisms; |
1757
|
|
|
|
|
|
|
# Get rid of the ?all at the end of the list |
1758
|
0
|
|
|
|
|
|
pop @locallist; |
1759
|
|
|
|
|
|
|
# $_->[3] goes into $query->{spf_source}. |
1760
|
0
|
0
|
|
|
|
|
map { $_->[3] = ($_->[1] eq 'include' |
|
0
|
|
|
|
|
|
|
1761
|
|
|
|
|
|
|
? "local policy includes SPF record at " . $query->macro_substitute($_->[2]) |
1762
|
|
|
|
|
|
|
: "local policy") } |
1763
|
|
|
|
|
|
|
@locallist; |
1764
|
0
|
|
|
|
|
|
splice(@$mechanisms, $index + 1, 0, @locallist); |
1765
|
|
|
|
|
|
|
} |
1766
|
|
|
|
|
|
|
} |
1767
|
|
|
|
|
|
|
} |
1768
|
|
|
|
|
|
|
} |
1769
|
0
|
|
|
|
|
|
$query->debuglog(" lookup: mec mechanisms=@{[$directive_set->show_mechanisms]}"); |
|
0
|
|
|
|
|
|
|
1770
|
0
|
|
|
|
|
|
return $directive_set; |
1771
|
|
|
|
|
|
|
} |
1772
|
|
|
|
|
|
|
|
1773
|
0
|
|
|
0
|
|
|
sub version { shift->{version} } |
1774
|
0
|
|
|
0
|
|
|
sub mechanisms { @{shift->{mechanisms}} } |
|
0
|
|
|
|
|
|
|
1775
|
0
|
|
|
0
|
|
|
sub explanation { shift->{modifiers}->{exp} } |
1776
|
0
|
|
|
0
|
|
|
sub redirect { shift->{modifiers}->{redirect} } |
1777
|
0
|
|
|
0
|
|
|
sub get_modifier { shift->{modifiers}->{shift()} } |
1778
|
0
|
|
|
0
|
|
|
sub syntax_error { shift->{syntax_error} } |
1779
|
|
|
|
|
|
|
|
1780
|
|
|
|
|
|
|
sub show_mechanisms { |
1781
|
0
|
|
|
0
|
|
|
my $directive_set = shift; |
1782
|
0
|
|
0
|
|
|
|
my @toreturn = map { $_->[0] . $_->[1] . "(" . ($_->[2]||"") . ")" } $directive_set->mechanisms; |
|
0
|
|
|
|
|
|
|
1783
|
|
|
|
|
|
|
# print STDERR ("showing mechanisms @toreturn: " . Dumper($directive_set)); use Data::Dumper; |
1784
|
0
|
|
|
|
|
|
return @toreturn; |
1785
|
|
|
|
|
|
|
} |
1786
|
|
|
|
|
|
|
} |
1787
|
|
|
|
|
|
|
|
1788
|
|
|
|
|
|
|
1; |
1789
|
|
|
|
|
|
|
|
1790
|
|
|
|
|
|
|
=head1 WARNINGS |
1791
|
|
|
|
|
|
|
|
1792
|
|
|
|
|
|
|
Mail::Query::SPF should only be used at the point where messages are received |
1793
|
|
|
|
|
|
|
from the Internet. The underlying assumption is that the sender of the e-mail |
1794
|
|
|
|
|
|
|
is sending the message directly to you or one of your secondary MXes. If your |
1795
|
|
|
|
|
|
|
MTA does not have an exhaustive list of secondary MXes, then the C |
1796
|
|
|
|
|
|
|
and C methods can be used. These methods take care to |
1797
|
|
|
|
|
|
|
permit mail from secondary MXes. |
1798
|
|
|
|
|
|
|
|
1799
|
|
|
|
|
|
|
=head1 AUTHORS |
1800
|
|
|
|
|
|
|
|
1801
|
|
|
|
|
|
|
Meng Weng Wong , Philip Gladstone, Julian Mehnle |
1802
|
|
|
|
|
|
|
|
1803
|
|
|
|
|
|
|
|
1804
|
|
|
|
|
|
|
=head1 SEE ALSO |
1805
|
|
|
|
|
|
|
|
1806
|
|
|
|
|
|
|
About SPF: L |
1807
|
|
|
|
|
|
|
|
1808
|
|
|
|
|
|
|
Mail::SPF::Query: L |
1809
|
|
|
|
|
|
|
|
1810
|
|
|
|
|
|
|
The latest release of the SPF specification: L |
1811
|
|
|
|
|
|
|
|
1812
|
|
|
|
|
|
|
=cut |
1813
|
|
|
|
|
|
|
|
1814
|
|
|
|
|
|
|
# vim:et sts=4 sw=4 |