| 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 |