Filename | /usr/local/lib/perl5/site_perl/Mail/SpamAssassin/Plugin/AskDNS.pm |
Statements | Executed 2892 statements in 28.9ms |
Calls | P | F | Exclusive Time |
Inclusive Time |
Subroutine |
---|---|---|---|---|---|
235 | 1 | 1 | 24.2ms | 67.0ms | extract_metadata | Mail::SpamAssassin::Plugin::AskDNS::
2 | 1 | 1 | 320µs | 739µs | __ANON__[:356] | Mail::SpamAssassin::Plugin::AskDNS::
1 | 1 | 1 | 96µs | 96µs | CORE:regcomp (opcode) | Mail::SpamAssassin::Plugin::AskDNS::
9 | 4 | 1 | 86µs | 86µs | CORE:match (opcode) | Mail::SpamAssassin::Plugin::AskDNS::
1 | 1 | 1 | 72µs | 184µs | parse_and_canonicalize_subtest | Mail::SpamAssassin::Plugin::AskDNS::
1 | 1 | 1 | 72µs | 262µs | new | Mail::SpamAssassin::Plugin::AskDNS::
1 | 1 | 1 | 58µs | 69µs | BEGIN@187 | Mail::SpamAssassin::Plugin::AskDNS::
1 | 1 | 1 | 42µs | 138µs | set_config | Mail::SpamAssassin::Plugin::AskDNS::
1 | 1 | 1 | 31µs | 239µs | BEGIN@193 | Mail::SpamAssassin::Plugin::AskDNS::
1 | 1 | 1 | 26µs | 90µs | BEGIN@189 | Mail::SpamAssassin::Plugin::AskDNS::
1 | 1 | 1 | 24µs | 216µs | BEGIN@195 | Mail::SpamAssassin::Plugin::AskDNS::
1 | 1 | 1 | 24µs | 143µs | BEGIN@192 | Mail::SpamAssassin::Plugin::AskDNS::
1 | 1 | 1 | 21µs | 55µs | BEGIN@188 | Mail::SpamAssassin::Plugin::AskDNS::
1 | 1 | 1 | 13µs | 13µs | BEGIN@191 | Mail::SpamAssassin::Plugin::AskDNS::
1 | 1 | 1 | 11µs | 11µs | CORE:qr (opcode) | Mail::SpamAssassin::Plugin::AskDNS::
0 | 0 | 0 | 0s | 0s | __ANON__[:397] | Mail::SpamAssassin::Plugin::AskDNS::
0 | 0 | 0 | 0s | 0s | __ANON__[:488] | Mail::SpamAssassin::Plugin::AskDNS::
0 | 0 | 0 | 0s | 0s | askdns_hit | Mail::SpamAssassin::Plugin::AskDNS::
0 | 0 | 0 | 0s | 0s | launch_queries | Mail::SpamAssassin::Plugin::AskDNS::
0 | 0 | 0 | 0s | 0s | process_response_packet | Mail::SpamAssassin::Plugin::AskDNS::
Line | State ments |
Time on line |
Calls | Time in subs |
Code |
---|---|---|---|---|---|
1 | # <@LICENSE> | ||||
2 | # Licensed to the Apache Software Foundation (ASF) under one or more | ||||
3 | # contributor license agreements. See the NOTICE file distributed with | ||||
4 | # this work for additional information regarding copyright ownership. | ||||
5 | # The ASF licenses this file to you under the Apache License, Version 2.0 | ||||
6 | # (the "License"); you may not use this file except in compliance with | ||||
7 | # the License. You may obtain a copy of the License at: | ||||
8 | # | ||||
9 | # http://www.apache.org/licenses/LICENSE-2.0 | ||||
10 | # | ||||
11 | # Unless required by applicable law or agreed to in writing, software | ||||
12 | # distributed under the License is distributed on an "AS IS" BASIS, | ||||
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
14 | # See the License for the specific language governing permissions and | ||||
15 | # limitations under the License. | ||||
16 | # </@LICENSE> | ||||
17 | |||||
18 | =head1 NAME | ||||
19 | |||||
20 | AskDNS - form a DNS query using tag values, and look up the DNSxL lists | ||||
21 | |||||
22 | =head1 SYNOPSIS | ||||
23 | |||||
24 | loadplugin Mail::SpamAssassin::Plugin::AskDNS | ||||
25 | askdns D_IN_DWL _DKIMDOMAIN_._vouch.dwl.spamhaus.org TXT /\b(transaction|list|all)\b/ | ||||
26 | |||||
27 | =head1 DESCRIPTION | ||||
28 | |||||
29 | Using a DNS query template as specified in a parameter of a askdns rule, | ||||
30 | the plugin replaces tag names as found in the template with their values | ||||
31 | and launches DNS queries as soon as tag values become available. When DNS | ||||
32 | responses trickle in, filters them according to the requested DNS resource | ||||
33 | record type and optional subrule filtering expression, yielding a rule hit | ||||
34 | if a response meets filtering conditions. | ||||
35 | |||||
36 | =head1 USER SETTINGS | ||||
37 | |||||
38 | =over 4 | ||||
39 | |||||
40 | =item rbl_timeout t [t_min] [zone] (default: 15 3) | ||||
41 | |||||
42 | The rbl_timeout setting is common to all DNS querying rules (as implemented | ||||
43 | by other plugins). It can specify a DNS query timeout globally, or individually | ||||
44 | for each zone. When the zone parameter is specified, the settings affects DNS | ||||
45 | queries when their query domain equals the specified zone, or is its subdomain. | ||||
46 | See the C<Mail::SpamAssassin::Conf> POD for details on C<rbl_timeout>. | ||||
47 | |||||
48 | =back | ||||
49 | |||||
50 | =head1 RULE DEFINITIONS | ||||
51 | |||||
52 | =over 4 | ||||
53 | |||||
54 | =item askdns NAME_OF_RULE query_template [rr_type [subqueryfilter]] | ||||
55 | |||||
56 | A query template is a string which will be expanded to produce a domain name | ||||
57 | to be used in a DNS query. The template may include SpamAssassin tag names, | ||||
58 | which will be replaced by their values to form a final query domain. | ||||
59 | The final query domain must adhere to rules governing DNS domains, i.e. | ||||
60 | must consist of fields each up to 63 characters long, delimited by dots. | ||||
61 | There may be a trailing dot at the end, but it is redundant / carries | ||||
62 | no semantics, because SpamAssassin uses a Net::DSN::Resolver::send method | ||||
63 | for querying DNS, which ignores any 'search' or 'domain' DNS resolver options. | ||||
64 | Domain names in DNS queries are case-insensitive. | ||||
65 | |||||
66 | A tag name is a string of capital letters, preceded and followed by an | ||||
67 | underscore character. This syntax mirrors the add_header setting, except that | ||||
68 | tags cannot have parameters in parenthesis when used in askdns templates. | ||||
69 | Tag names may appear anywhere in the template - each queried DNS zone | ||||
70 | prescribes how a query should be formed. | ||||
71 | |||||
72 | A query template may contain any number of tag names including none, | ||||
73 | although in the most common anticipated scenario exactly one tag name would | ||||
74 | appear in each askdns rule. Specified tag names are considered dependencies. | ||||
75 | Askdns rules with dependencies on the same set of tags are grouped, and all | ||||
76 | queries in a group are launched as soon as all their dependencies are met, | ||||
77 | i.e. when the last of the awaited tag values becomes available by a call | ||||
78 | to set_tag() from some other plugin or elsewhere in the SpamAssassin code. | ||||
79 | |||||
80 | Launched queries from all askdns rules are grouped too according to a pair | ||||
81 | of: query type and an expanded query domain name. Even if there are multiple | ||||
82 | rules producing the same type/domain pair, only one DNS query is launched, | ||||
83 | and a reply to such query contributes to all the constituent rules. | ||||
84 | |||||
85 | A tag may produce none, one or multiple values. Askdns rules awaiting for | ||||
86 | a tag which never receives its value never result in a DNS query. Tags which | ||||
87 | produce multiple values will result in multiple queries launched, each with | ||||
88 | an expanded template using one of the tag values. An example is a DKIMDOMAIN | ||||
89 | tag which yields a list of signing domains, one for each valid signature in | ||||
90 | a signed message. | ||||
91 | |||||
92 | When more than one distinct tag name appears in a template, each potentially | ||||
93 | resulting in multiple values, a Cartesian product is formed, and each tuple | ||||
94 | results in a launch of one DNS query (duplicates excluded). For example, | ||||
95 | a query template _A_._B_.example._A_.com where tag A is a list (11,22) | ||||
96 | and B is (xx,yy,zz), will result in queries: 11.xx.example.11.com, | ||||
97 | 22.xx.example.22.com, 11.yy.example.11.com, 22.yy.example.22.com, | ||||
98 | 11.zz.example.11.com, 22.zz.example.22.com . | ||||
99 | |||||
100 | A parameter rr_type following the query template is a comma-separated list | ||||
101 | of expected DNS resource record (RR) types. Missing rr_type parameter implies | ||||
102 | an 'A'. A DNS result may bring resource records of multiple types, but only | ||||
103 | resource records of a type found in the rr_type parameter list are considered, | ||||
104 | other resource records found in the answer section of a DNS reply are ignored | ||||
105 | for this rule. A value ANY in the rr_type parameter list matches any resource | ||||
106 | record type. An empty DNS answer section does not match ANY. | ||||
107 | |||||
108 | The rr_type parameter not only provides a filter for RR types found in | ||||
109 | the DNS answer, but also determines the DNS query type. If only a single | ||||
110 | RR type is specified in the parameter (e.g. TXT), than this is also the RR | ||||
111 | type of a query. When more than one RR type is specified (e.g. A, AAAA, TXT) | ||||
112 | or if ANY is specified, then the DNS query type will be ANY and the rr_type | ||||
113 | parameter will only act as a filter on a result. | ||||
114 | |||||
115 | Currently recognized RR types in the rr_type parameter are: ANY, A, AAAA, | ||||
116 | MX, TXT, PTR, NAPTR, NS, SOA, CERT, CNAME, DNAME, DHCID, HINFO, MINFO, | ||||
117 | RP, HIP, IPSECKEY, KX, LOC, SRV, SSHFP, SPF. | ||||
118 | |||||
119 | http://www.iana.org/assignments/dns-parameters/dns-parameters.xml | ||||
120 | |||||
121 | The last optional parameter of a rule is a filtering expression, a.k.a. a | ||||
122 | subrule. Its function is much like the subrule in URIDNSBL plugin rules, | ||||
123 | or in the check_rbl eval rules. The main difference is that with askdns | ||||
124 | rules there is no need to manually group rules according to their queried | ||||
125 | zone, as the grouping is automatic and duplicate queries are implicitly | ||||
126 | eliminated. | ||||
127 | |||||
128 | The subrule filtering parameter can be: a plain string, a regular expression, | ||||
129 | a single numerical value or a pair of numerical values, or a list of rcodes | ||||
130 | (DNS status codes of a response). Absence of the filtering parameter implies | ||||
131 | no filtering, i.e. any positive DNS response (rcode=NOERROR) of the requested | ||||
132 | RR type will result in a rule hit, regardless of the RR value returned with | ||||
133 | the response. | ||||
134 | |||||
135 | When a plain string is used as a filter, it must be enclosed in single or | ||||
136 | double quotes. For the rule to hit, the response must match the filtering | ||||
137 | string exactly, and a RR type of a response must match the query type. | ||||
138 | Typical use is an exact text string for TXT queries, or an exact quad-dotted | ||||
139 | IPv4 address. In case of a TXT or SPF resource record which can return | ||||
140 | multiple character-strings (as defined in Section 3.3 of [RFC1035]), these | ||||
141 | strings are concatenated with no delimiters before comparing the result | ||||
142 | to the filtering string. This follows requirements of several documents, | ||||
143 | such as RFC 5518, RFC 4408, RFC 4871, RFC 5617. Examples of a plain text | ||||
144 | filtering parameter: "127.0.0.1", "transaction", 'list' . | ||||
145 | |||||
146 | A regular expression follows a familiar perl syntax like /.../ or m{...} | ||||
147 | optionally followed by regexp flags (such as 'i' for case-insensitivity). | ||||
148 | If a DNS response matches the requested RR type and the regular expression, | ||||
149 | the rule hits. Examples: /^127\.0\.0\.\d+$/, m{\bdial up\b}i . | ||||
150 | |||||
151 | A single numerical value can be a decimal number, or a hexadecimal number | ||||
152 | prefixed by 0x. Such numeric filtering expression is typically used with | ||||
153 | RR type-A DNS queries. The returned value (an IPv4 address) is masked | ||||
154 | with a specified filtering value and tested to fall within a 127.0.0.0/8 | ||||
155 | network range - the rule hits if the result is nonzero: | ||||
156 | ((r & n) != 0) && ((r & 0xff000000) == 0x7f000000). An example: 0x10 . | ||||
157 | |||||
158 | A pair of numerical values (each a decimal, hexadecimal or quad-dotted) | ||||
159 | delimited by a '-' specifies an IPv4 address range, and a pair of values | ||||
160 | delimited by a '/' specifies an IPv4 address followed by a bitmask. Again, | ||||
161 | this type of filtering expression is primarily intended with RR type-A | ||||
162 | DNS queries. The rule hits if the RR type matches, and the returned IP | ||||
163 | address falls within the specified range: (r >= n1 && r <= n2), or | ||||
164 | masked with a bitmask matches the specified value: (r & m) == (n & m) . | ||||
165 | |||||
166 | As a shorthand notation, a single quad-dotted value is equivalent to | ||||
167 | a n-n form, i.e. it must match the returned value exactly with all its bits. | ||||
168 | |||||
169 | Some typical examples of a numeric filtering parameter are: 127.0.1.2, | ||||
170 | 127.0.1.20-127.0.1.39, 127.0.1.0/255.255.255.0, 0.0.0.16/0.0.0.16, | ||||
171 | 0x10/0x10, 16, 0x10 . | ||||
172 | |||||
173 | Lastly, the filtering parameter can be a comma-separated list of DNS status | ||||
174 | codes (rcode), enclosed in square brackets. Rcodes can be represented either | ||||
175 | by their numeric decimal values (0=NOERROR, 3=NXDOMAIN, ...), or their names. | ||||
176 | See http://www.iana.org/assignments/dns-parameters for the list of names. When | ||||
177 | testing for a rcode where rcode is nonzero, a RR type parameter is ignored | ||||
178 | as a filter, as there is typically no answer section in a DNS reply when | ||||
179 | rcode indicates an error. Example: [NXDOMAIN], or [FormErr,ServFail,4,5] . | ||||
180 | |||||
181 | =back | ||||
182 | |||||
183 | =cut | ||||
184 | |||||
185 | package Mail::SpamAssassin::Plugin::AskDNS; | ||||
186 | |||||
187 | 2 | 79µs | 2 | 81µs | # spent 69µs (58+11) within Mail::SpamAssassin::Plugin::AskDNS::BEGIN@187 which was called:
# once (58µs+11µs) by Mail::SpamAssassin::PluginHandler::load_plugin at line 187 # spent 69µs making 1 call to Mail::SpamAssassin::Plugin::AskDNS::BEGIN@187
# spent 11µs making 1 call to strict::import |
188 | 2 | 77µs | 2 | 89µs | # spent 55µs (21+34) within Mail::SpamAssassin::Plugin::AskDNS::BEGIN@188 which was called:
# once (21µs+34µs) by Mail::SpamAssassin::PluginHandler::load_plugin at line 188 # spent 55µs making 1 call to Mail::SpamAssassin::Plugin::AskDNS::BEGIN@188
# spent 34µs making 1 call to warnings::import |
189 | 2 | 59µs | 2 | 154µs | # spent 90µs (26+64) within Mail::SpamAssassin::Plugin::AskDNS::BEGIN@189 which was called:
# once (26µs+64µs) by Mail::SpamAssassin::PluginHandler::load_plugin at line 189 # spent 90µs making 1 call to Mail::SpamAssassin::Plugin::AskDNS::BEGIN@189
# spent 64µs making 1 call to re::import |
190 | |||||
191 | 2 | 60µs | 1 | 13µs | # spent 13µs within Mail::SpamAssassin::Plugin::AskDNS::BEGIN@191 which was called:
# once (13µs+0s) by Mail::SpamAssassin::PluginHandler::load_plugin at line 191 # spent 13µs making 1 call to Mail::SpamAssassin::Plugin::AskDNS::BEGIN@191 |
192 | 2 | 76µs | 2 | 263µs | # spent 143µs (24+120) within Mail::SpamAssassin::Plugin::AskDNS::BEGIN@192 which was called:
# once (24µs+120µs) by Mail::SpamAssassin::PluginHandler::load_plugin at line 192 # spent 143µs making 1 call to Mail::SpamAssassin::Plugin::AskDNS::BEGIN@192
# spent 120µs making 1 call to Exporter::import |
193 | 2 | 71µs | 2 | 447µs | # spent 239µs (31+208) within Mail::SpamAssassin::Plugin::AskDNS::BEGIN@193 which was called:
# once (31µs+208µs) by Mail::SpamAssassin::PluginHandler::load_plugin at line 193 # spent 239µs making 1 call to Mail::SpamAssassin::Plugin::AskDNS::BEGIN@193
# spent 208µs making 1 call to Exporter::import |
194 | |||||
195 | 2 | 5.98ms | 2 | 409µs | # spent 216µs (24+192) within Mail::SpamAssassin::Plugin::AskDNS::BEGIN@195 which was called:
# once (24µs+192µs) by Mail::SpamAssassin::PluginHandler::load_plugin at line 195 # spent 216µs making 1 call to Mail::SpamAssassin::Plugin::AskDNS::BEGIN@195
# spent 192µs making 1 call to vars::import |
196 | 1 | 18µs | @ISA = qw(Mail::SpamAssassin::Plugin); | ||
197 | |||||
198 | 1 | 16µs | %rcode_value = ( # http://www.iana.org/assignments/dns-parameters, RFC 6195 | ||
199 | NOERROR => 0, FORMERR => 1, SERVFAIL => 2, NXDOMAIN => 3, NOTIMP => 4, | ||||
200 | REFUSED => 5, YXDOMAIN => 6, YXRRSET => 7, NXRRSET => 8, NOTAUTH => 9, | ||||
201 | NOTZONE => 10, BADVERS => 16, BADSIG => 16, BADKEY => 17, BADTIME => 18, | ||||
202 | BADMODE => 19, BADNAME => 20, BADALG => 21, BADTRUNC => 22, | ||||
203 | ); | ||||
204 | |||||
205 | # spent 262µs (72+190) within Mail::SpamAssassin::Plugin::AskDNS::new which was called:
# once (72µs+190µs) by Mail::SpamAssassin::PluginHandler::load_plugin at line 1 of (eval 113)[Mail/SpamAssassin/PluginHandler.pm:129] | ||||
206 | 1 | 2µs | my($class,$sa_main) = @_; | ||
207 | |||||
208 | 1 | 2µs | $class = ref($class) || $class; | ||
209 | 1 | 11µs | 1 | 23µs | my $self = $class->SUPER::new($sa_main); # spent 23µs making 1 call to Mail::SpamAssassin::Plugin::new |
210 | 1 | 2µs | bless($self, $class); | ||
211 | |||||
212 | 1 | 8µs | 1 | 138µs | $self->set_config($sa_main->{conf}); # spent 138µs making 1 call to Mail::SpamAssassin::Plugin::AskDNS::set_config |
213 | |||||
214 | 1 | 56µs | 1 | 29µs | $txtdata_can_provide_a_list = Net::DNS->VERSION >= 0.69; # spent 29µs making 1 call to version::_VERSION |
215 | |||||
216 | 1 | 16µs | return $self; | ||
217 | } | ||||
218 | |||||
219 | # --------------------------------------------------------------------------- | ||||
220 | |||||
221 | # Accepts argument as a string in single or double quotes, or as a regular | ||||
222 | # expression in // or m{} notation, or as a numerical value or a pair of | ||||
223 | # numerical values, or as a bracketed and comma-separated list of DNS rcode | ||||
224 | # names or their numerical codes. Recognized numerical forms are: m, n1-n2, | ||||
225 | # or n/m, where n,n1,n2,m can be any of: decimal digits, 0x followed by | ||||
226 | # up to 8 hexadecimal digits, or an IPv4 address in quad-dotted notation. | ||||
227 | # The argument is checked for syntax, undef is returned on syntax errors. | ||||
228 | # A string that looks like a regular expression is converted to a compiled | ||||
229 | # Regexp object and returned as a result. Otherwise, numeric components of | ||||
230 | # the remaining three forms are converted as follows: hex or decimal numeric | ||||
231 | # strings are converted to a number and a quad-dot is converted to a number, | ||||
232 | # then components are reassembled into a string delimited by '-' or '/'. | ||||
233 | # As a special backward compatibility measure, a single quad-dot (with no | ||||
234 | # second number) is converted into n-n, to distinguish it from a traditional | ||||
235 | # mask-only form. A list or rcodes is returned as a hashref, where keys | ||||
236 | # represent specified numerical rcodes. | ||||
237 | # | ||||
238 | # Arguments like the following are anticipated: | ||||
239 | # "127.0.0.1", "some text", 'some "more" text', | ||||
240 | # /regexp/flags, m{regexp}flags, | ||||
241 | # 127.0.1.2 (same as 127.0.1.2-127.0.1.2 or 127.0.1.2/255.255.255.255) | ||||
242 | # 127.0.1.20-127.0.1.39 (= 0x7f000114-0x7f000127 or 2130706708-2130706727) | ||||
243 | # 0.0.0.16/0.0.0.16 (same as 0x10/0x10 or 16/0x10 or 16/16) | ||||
244 | # 16 (traditional style mask-only, same as 0x10) | ||||
245 | # [NXDOMAIN], [FormErr,ServFail,4,5] | ||||
246 | # | ||||
247 | # spent 184µs (72+112) within Mail::SpamAssassin::Plugin::AskDNS::parse_and_canonicalize_subtest which was called:
# once (72µs+112µs) by Mail::SpamAssassin::Plugin::AskDNS::__ANON__[/usr/local/lib/perl5/site_perl/Mail/SpamAssassin/Plugin/AskDNS.pm:356] at line 329 | ||||
248 | 1 | 3µs | my($subtest) = @_; | ||
249 | 1 | 2µs | my $result; | ||
250 | |||||
251 | 1 | 5µs | local($1,$2,$3); | ||
252 | # modifiers /a, /d, /l, /u in suffix form were added with perl 5.13.10 (5.14) | ||||
253 | # currently known modifiers are [msixoadlu], but let's not be too picky here | ||||
254 | 1 | 20µs | 1 | 6µs | if ( $subtest =~ m{^ / (.+) / ([a-z]*) \z}xs) { # spent 6µs making 1 call to Mail::SpamAssassin::Plugin::AskDNS::CORE:match |
255 | 1 | 145µs | 2 | 106µs | $result = $2 ne '' ? qr{(?$2)$1} : qr{$1}; # spent 96µs making 1 call to Mail::SpamAssassin::Plugin::AskDNS::CORE:regcomp
# spent 11µs making 1 call to Mail::SpamAssassin::Plugin::AskDNS::CORE:qr |
256 | } elsif ($subtest =~ m{^ m \s* \( (.+) \) ([a-z]*) \z}xs) { | ||||
257 | $result = $2 ne '' ? qr{(?$2)$1} : qr{$1}; | ||||
258 | } elsif ($subtest =~ m{^ m \s* \[ (.+) \] ([a-z]*) \z}xs) { | ||||
259 | $result = $2 ne '' ? qr{(?$2)$1} : qr{$1}; | ||||
260 | } elsif ($subtest =~ m{^ m \s* \{ (.+) \} ([a-z]*) \z}xs) { | ||||
261 | $result = $2 ne '' ? qr{(?$2)$1} : qr{$1}; | ||||
262 | } elsif ($subtest =~ m{^ m \s* < (.+) > ([a-z]*) \z}xs) { | ||||
263 | $result = $2 ne '' ? qr{(?$2)$1} : qr{$1}; | ||||
264 | } elsif ($subtest =~ m{^ m \s* (\S) (.+) \1 ([a-z]*) \z}xs) { | ||||
265 | $result = $2 ne '' ? qr{(?$2)$1} : qr{$1}; | ||||
266 | } elsif ($subtest =~ m{^ (["']) (.*) \1 \z}xs) { # quoted string | ||||
267 | $result = $2; | ||||
268 | } elsif ($subtest =~ m{^ \[ ( (?:[A-Z]+|\d+) | ||||
269 | (?: \s* , \s* (?:[A-Z]+|\d+) )* ) \] \z}xis) { | ||||
270 | # a comma-separated list of rcode names or their decimal values | ||||
271 | my @rcodes = split(/\s*,\s*/, uc $1); | ||||
272 | for (@rcodes) { $_ = $rcode_value{$_} if exists $rcode_value{$_} } | ||||
273 | return if grep(!/^\d+\z/, @rcodes); | ||||
274 | # a hashref indicates a list of DNS rcodes (stored as hash keys) | ||||
275 | $result = { map( ($_,1), @rcodes) }; | ||||
276 | } elsif ($subtest =~ m{^ ([^/-]+) (?: ([/-]) (.+) )? \z}xs) { | ||||
277 | my($n1,$delim,$n2) = ($1,$2,$3); | ||||
278 | my $any_quad_dot; | ||||
279 | for ($n1,$n2) { | ||||
280 | if (!defined $_) { | ||||
281 | # ok, $n2 may not exist | ||||
282 | } elsif (/^\d{1,10}\z/) { | ||||
283 | $_ = 0 + $_; # decimal string -> number | ||||
284 | } elsif (/^0x[0-9a-zA-Z]{1,8}\z/) { | ||||
285 | $_ = hex($_); # hex string -> number | ||||
286 | } elsif (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/) { | ||||
287 | $_ = Mail::SpamAssassin::Util::my_inet_aton($_); # quad-dot -> number | ||||
288 | $any_quad_dot = 1; | ||||
289 | } else { | ||||
290 | return; | ||||
291 | } | ||||
292 | } | ||||
293 | $result = defined $n2 ? $n1.$delim.$n2 | ||||
294 | : $any_quad_dot ? $n1.'-'.$n1 : "$n1"; | ||||
295 | } | ||||
296 | 1 | 16µs | return $result; | ||
297 | } | ||||
298 | |||||
299 | # spent 138µs (42+96) within Mail::SpamAssassin::Plugin::AskDNS::set_config which was called:
# once (42µs+96µs) by Mail::SpamAssassin::Plugin::AskDNS::new at line 212 | ||||
300 | 1 | 2µs | my($self, $conf) = @_; | ||
301 | 1 | 2µs | my @cmds; | ||
302 | |||||
303 | push(@cmds, { | ||||
304 | setting => 'askdns', | ||||
305 | is_admin => 1, | ||||
306 | type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE, | ||||
307 | # spent 739µs (320+419) within Mail::SpamAssassin::Plugin::AskDNS::__ANON__[/usr/local/lib/perl5/site_perl/Mail/SpamAssassin/Plugin/AskDNS.pm:356] which was called 2 times, avg 369µs/call:
# 2 times (320µs+419µs) by Mail::SpamAssassin::Conf::Parser::parse at line 438 of Mail/SpamAssassin/Conf/Parser.pm, avg 369µs/call | ||||
308 | 2 | 10µs | my($self, $key, $value, $line) = @_; | ||
309 | 2 | 11µs | local($1,$2,$3,$4); | ||
310 | 2 | 109µs | 4 | 51µs | if (!defined $value || $value =~ /^$/) { # spent 51µs making 4 calls to Mail::SpamAssassin::Plugin::AskDNS::CORE:match, avg 13µs/call |
311 | return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE; | ||||
312 | } elsif ($value !~ /^ (\S+) \s+ (\S+) | ||||
313 | (?: \s+ ([A-Za-z0-9,]+) | ||||
314 | (?: \s+ (.*?) )? )? \s* $/xs) { | ||||
315 | return $Mail::SpamAssassin::Conf::INVALID_VALUE; | ||||
316 | } else { | ||||
317 | 2 | 12µs | my($rulename,$query_template,$query_type,$subtest) = ($1,$2,$3,$4); | ||
318 | 2 | 4µs | $query_type = 'A' if !defined $query_type; | ||
319 | 2 | 7µs | $query_type = uc $query_type; | ||
320 | 2 | 19µs | my @answer_types = split(/,/, $query_type); | ||
321 | # http://www.iana.org/assignments/dns-parameters/dns-parameters.xml | ||||
322 | 2 | 39µs | 2 | 17µs | if (grep(!/^(?:ANY|A|AAAA|MX|TXT|PTR|NAPTR|NS|SOA|CERT|CNAME|DNAME| # spent 17µs making 2 calls to Mail::SpamAssassin::Plugin::AskDNS::CORE:match, avg 8µs/call |
323 | DHCID|HINFO|MINFO|RP|HIP|IPSECKEY|KX|LOC|SRV| | ||||
324 | SSHFP|SPF)\z/x, @answer_types)) { | ||||
325 | return $Mail::SpamAssassin::Conf::INVALID_VALUE; | ||||
326 | } | ||||
327 | 2 | 6µs | $query_type = 'ANY' if @answer_types > 1 || $answer_types[0] eq 'ANY'; | ||
328 | 2 | 5µs | if (defined $subtest) { | ||
329 | 1 | 13µs | 1 | 184µs | $subtest = parse_and_canonicalize_subtest($subtest); # spent 184µs making 1 call to Mail::SpamAssassin::Plugin::AskDNS::parse_and_canonicalize_subtest |
330 | 1 | 2µs | defined $subtest or return $Mail::SpamAssassin::Conf::INVALID_VALUE; | ||
331 | } | ||||
332 | # collect tag names as used in each query template | ||||
333 | 2 | 33µs | 2 | 13µs | my @tags = $query_template =~ /_([A-Z][A-Z0-9]*)_/g; # spent 13µs making 2 calls to Mail::SpamAssassin::Plugin::AskDNS::CORE:match, avg 6µs/call |
334 | 4 | 19µs | my %seen; @tags = grep(!$seen{$_}++, @tags); # filter out duplicates | ||
335 | |||||
336 | # group rules by tag names used in them (to be used as a hash key) | ||||
337 | 2 | 19µs | my $depends_on_tags = !@tags ? '' : join(',',@tags); | ||
338 | |||||
339 | # subgroup rules by a DNS RR type and a nonexpanded query template | ||||
340 | 2 | 8µs | my $query_template_key = $query_type . ':' . $query_template; | ||
341 | |||||
342 | 2 | 17µs | $self->{askdns}{$depends_on_tags}{$query_template_key} ||= | ||
343 | { query => $query_template, rules => {}, q_type => $query_type, | ||||
344 | a_types => # optimization: undef means "same as q_type" | ||||
345 | @answer_types == 1 && $answer_types[0] eq $query_type ? undef | ||||
346 | : \@answer_types }; | ||||
347 | 2 | 18µs | $self->{askdns}{$depends_on_tags}{$query_template_key}{rules}{$rulename} | ||
348 | = $subtest; | ||||
349 | # dbg("askdns: rule: %s, config dep: %s, domkey: %s, subtest: %s", | ||||
350 | # $rulename, $depends_on_tags, $query_template_key, $subtest); | ||||
351 | |||||
352 | # just define the test so that scores and lint works | ||||
353 | 2 | 28µs | 2 | 154µs | $self->{parser}->add_test($rulename, undef, # spent 154µs making 2 calls to Mail::SpamAssassin::Conf::Parser::add_test, avg 77µs/call |
354 | $Mail::SpamAssassin::Conf::TYPE_EMPTY_TESTS); | ||||
355 | } | ||||
356 | } | ||||
357 | 1 | 16µs | }); | ||
358 | |||||
359 | 1 | 17µs | 1 | 96µs | $conf->{parser}->register_commands(\@cmds); # spent 96µs making 1 call to Mail::SpamAssassin::Conf::Parser::register_commands |
360 | } | ||||
361 | |||||
362 | # run as early as possible, launching DNS queries as soon as their | ||||
363 | # dependencies are fulfilled | ||||
364 | # | ||||
365 | # spent 67.0ms (24.2+42.8) within Mail::SpamAssassin::Plugin::AskDNS::extract_metadata which was called 235 times, avg 285µs/call:
# 235 times (24.2ms+42.8ms) by Mail::SpamAssassin::PluginHandler::callback at line 204 of Mail/SpamAssassin/PluginHandler.pm, avg 285µs/call | ||||
366 | 235 | 508µs | my($self, $opts) = @_; | ||
367 | 235 | 747µs | my $pms = $opts->{permsgstatus}; | ||
368 | 235 | 665µs | my $conf = $pms->{conf}; | ||
369 | |||||
370 | 235 | 2.21ms | 235 | 20.2ms | return if !$pms->is_dns_available; # spent 20.2ms making 235 calls to Mail::SpamAssassin::PerMsgStatus::is_dns_available, avg 86µs/call |
371 | 235 | 878µs | $pms->{askdns_map_dnskey_to_rules} = {}; | ||
372 | |||||
373 | # walk through all collected askdns rules, obtain tag values whenever | ||||
374 | # they may become available, and launch DNS queries right after | ||||
375 | # | ||||
376 | 470 | 4.59ms | for my $depends_on_tags (keys %{$conf->{askdns}}) { | ||
377 | 235 | 435µs | my @tags; | ||
378 | 235 | 1.71ms | @tags = split(/,/, $depends_on_tags) if $depends_on_tags ne ''; | ||
379 | |||||
380 | 235 | 1.62ms | 235 | 3.21ms | if (would_log("dbg","askdns")) { # spent 3.21ms making 235 calls to Mail::SpamAssassin::Logger::would_log, avg 14µs/call |
381 | while ( my($query_template_key, $struct) = | ||||
382 | each %{$conf->{askdns}{$depends_on_tags}} ) { | ||||
383 | my($query_template, $query_type, $answer_types_ref, $rules) = | ||||
384 | @$struct{qw(query q_type a_types rules)}; | ||||
385 | dbg("askdns: depend on tags %s, rules: %s ", | ||||
386 | $depends_on_tags, join(', ', keys %$rules)); | ||||
387 | } | ||||
388 | } | ||||
389 | |||||
390 | 235 | 2.77ms | if (!@tags) { | ||
391 | # no dependencies on tags, just call directly | ||||
392 | $self->launch_queries($pms,$depends_on_tags); | ||||
393 | } else { | ||||
394 | # enqueue callback for tags needed | ||||
395 | $pms->action_depends_on_tags(@tags == 1 ? $tags[0] : \@tags, | ||||
396 | sub { my($pms,@args) = @_; | ||||
397 | $self->launch_queries($pms,$depends_on_tags) } | ||||
398 | 235 | 5.58ms | 235 | 19.4ms | ); # spent 19.4ms making 235 calls to Mail::SpamAssassin::PerMsgStatus::action_depends_on_tags, avg 82µs/call |
399 | } | ||||
400 | } | ||||
401 | } | ||||
402 | |||||
403 | # generate DNS queries - called for each set of rules | ||||
404 | # when their tag dependencies are met | ||||
405 | # | ||||
406 | sub launch_queries { | ||||
407 | my($self, $pms, $depends_on_tags) = @_; | ||||
408 | my $conf = $pms->{conf}; | ||||
409 | |||||
410 | my %tags; | ||||
411 | # obtain tag/value pairs of tags we depend upon in this set of rules | ||||
412 | if ($depends_on_tags ne '') { | ||||
413 | %tags = map( ($_,$pms->get_tag($_)), split(/,/,$depends_on_tags) ); | ||||
414 | } | ||||
415 | dbg("askdns: preparing queries which depend on tags: %s", | ||||
416 | join(', ', map($_.' => '.$tags{$_}, keys %tags))); | ||||
417 | |||||
418 | # replace tag names in a query template with actual tag values | ||||
419 | # and launch DNS queries | ||||
420 | while ( my($query_template_key, $struct) = | ||||
421 | each %{$conf->{askdns}{$depends_on_tags}} ) { | ||||
422 | my($query_template, $query_type, $answer_types_ref, $rules) = | ||||
423 | @$struct{qw(query q_type a_types rules)}; | ||||
424 | |||||
425 | my @rulenames = keys %$rules; | ||||
426 | if (grep($conf->{scores}->{$_}, @rulenames)) { | ||||
427 | dbg("askdns: query template %s, type %s, rules: %s", | ||||
428 | $query_template, | ||||
429 | !$answer_types_ref ? $query_type | ||||
430 | : $query_type.'/'.join(',',@$answer_types_ref), | ||||
431 | join(', ', @rulenames)); | ||||
432 | } else { | ||||
433 | dbg("askdns: query template %s, type %s, all rules disabled: %s", | ||||
434 | $query_template, $query_type, join(', ', @rulenames)); | ||||
435 | next; | ||||
436 | } | ||||
437 | |||||
438 | # collect all tag names from a template, each may occur more than once | ||||
439 | my @templ_tags = $query_template =~ /_([A-Z][A-Z0-9]*)_/gs; | ||||
440 | |||||
441 | # filter out duplicate tag names, and tags with undefined or empty value | ||||
442 | my %seen; | ||||
443 | @templ_tags = grep(!$seen{$_}++ && defined $tags{$_} && $tags{$_} ne '', | ||||
444 | @templ_tags); | ||||
445 | |||||
446 | my %templ_vals; # values that each tag takes | ||||
447 | for my $t (@templ_tags) { | ||||
448 | my %seen; | ||||
449 | # a tag value may be a space-separated list, | ||||
450 | # store it as an arrayref, removing duplicate values | ||||
451 | $templ_vals{$t} = [ grep(!$seen{$_}++, split(' ',$tags{$t})) ]; | ||||
452 | } | ||||
453 | |||||
454 | # count through all tag value tuples | ||||
455 | my @digit = (0) x @templ_tags; # counting accumulator | ||||
456 | OUTER: | ||||
457 | for (;;) { | ||||
458 | my %current_tag_val; # maps a tag name to its current iteration value | ||||
459 | for my $j (0 .. $#templ_tags) { | ||||
460 | my $t = $templ_tags[$j]; | ||||
461 | $current_tag_val{$t} = $templ_vals{$t}[$digit[$j]]; | ||||
462 | } | ||||
463 | local $1; | ||||
464 | my $query_domain = $query_template; | ||||
465 | $query_domain =~ s{_([A-Z][A-Z0-9]*)_} | ||||
466 | { defined $current_tag_val{$1} ? $current_tag_val{$1} | ||||
467 | : '' }ge; | ||||
468 | |||||
469 | # the $dnskey identifies this query in AsyncLoop's pending_lookups | ||||
470 | my $dnskey = join(':', 'askdns', $query_type, $query_domain); | ||||
471 | dbg("askdns: expanded query %s, dns key %s", $query_domain, $dnskey); | ||||
472 | |||||
473 | if ($query_domain eq '') { | ||||
474 | # ignore, just in case | ||||
475 | } else { | ||||
476 | if (!exists $pms->{askdns_map_dnskey_to_rules}{$dnskey}) { | ||||
477 | $pms->{askdns_map_dnskey_to_rules}{$dnskey} = | ||||
478 | [ [$query_type, $answer_types_ref, $rules] ]; | ||||
479 | } else { | ||||
480 | push(@{$pms->{askdns_map_dnskey_to_rules}{$dnskey}}, | ||||
481 | [$query_type, $answer_types_ref, $rules] ); | ||||
482 | } | ||||
483 | # lauch a new DNS query for $query_type and $query_domain | ||||
484 | my $ent = $pms->{async}->bgsend_and_start_lookup( | ||||
485 | $query_domain, $query_type, undef, | ||||
486 | { key => $dnskey, zone => $query_domain }, | ||||
487 | sub { my ($ent2,$pkt) = @_; | ||||
488 | $self->process_response_packet($pms, $ent2, $pkt, $dnskey) }, | ||||
489 | master_deadline => $pms->{master_deadline} ); | ||||
490 | # these rules are now underway; unless the rule hits, these will | ||||
491 | # not be considered "finished" until harvest_dnsbl_queries() completes | ||||
492 | $pms->register_async_rule_start($dnskey) if $ent; | ||||
493 | } | ||||
494 | |||||
495 | last if !@templ_tags; | ||||
496 | # increment accumulator, little-endian | ||||
497 | for (my $j = 0; ; $j++) { | ||||
498 | last if ++$digit[$j] <= $#{$templ_vals{$templ_tags[$j]}}; | ||||
499 | $digit[$j] = 0; # and carry | ||||
500 | last OUTER if $j >= $#templ_tags; | ||||
501 | } | ||||
502 | } | ||||
503 | } | ||||
504 | } | ||||
505 | |||||
506 | sub process_response_packet { | ||||
507 | my($self, $pms, $ent, $pkt, $dnskey) = @_; | ||||
508 | |||||
509 | my $conf = $pms->{conf}; | ||||
510 | my %rulenames_hit; | ||||
511 | |||||
512 | # map a dnskey back to info on queries which caused this DNS lookup | ||||
513 | my $queries_ref = $pms->{askdns_map_dnskey_to_rules}{$dnskey}; | ||||
514 | |||||
515 | my($header, @question, @answer, $qtype, $rcode); | ||||
516 | # NOTE: $pkt will be undef if the DNS query was aborted (e.g. timed out) | ||||
517 | if ($pkt) { | ||||
518 | @answer = $pkt->answer; | ||||
519 | $header = $pkt->header; | ||||
520 | @question = $pkt->question; | ||||
521 | $qtype = uc $question[0]->qtype if @question; | ||||
522 | $rcode = uc $header->rcode if $header; # 'NOERROR', 'NXDOMAIN', ... | ||||
523 | |||||
524 | # NOTE: qname is encoded in RFC 1035 zone format, decode it | ||||
525 | dbg("askdns: answer received, rcode %s, query %s, answer has %d records", | ||||
526 | $rcode, | ||||
527 | join(', ', map(join('/', decode_dns_question_entry($_)), @question)), | ||||
528 | scalar @answer); | ||||
529 | |||||
530 | if (defined $rcode && exists $rcode_value{$rcode}) { | ||||
531 | # Net::DNS return a rcode name for codes it knows about, | ||||
532 | # and returns a number for the rest; we deal with numbers from here on | ||||
533 | $rcode = $rcode_value{$rcode} if exists $rcode_value{$rcode}; | ||||
534 | } | ||||
535 | } | ||||
536 | if (!@answer) { | ||||
537 | # a trick to make the following loop run at least once, so that we can | ||||
538 | # evaluate also rules which only care for rcode status | ||||
539 | @answer = ( undef ); | ||||
540 | } | ||||
541 | |||||
542 | # NOTE: $rr->rdatastr returns the result encoded in a DNS zone file | ||||
543 | # format, i.e. enclosed in double quotes if a result contains whitespace | ||||
544 | # (or other funny characters), and may use \DDD encoding or \X quoting as | ||||
545 | # per RFC 1035. Using $rr->txtdata instead avoids this unnecessary encoding | ||||
546 | # step and a need for decoding by a caller, returning an unmodified string. | ||||
547 | # Caveat: in case of multiple RDATA <character-string> fields contained | ||||
548 | # in a resource record (TXT, SPF, HINFO), starting with Net::DNS 0.69 | ||||
549 | # the $rr->txtdata in a list context returns these strings as a list. | ||||
550 | # The $rr->txtdata in a scalar context always returns a single string | ||||
551 | # with <character-string> fields joined by a single space character as | ||||
552 | # a separator. The $rr->txtdata in Net::DNS 0.68 and older returned | ||||
553 | # such joined space-separated string even in a list context. | ||||
554 | |||||
555 | # RFC 5518: If the RDATA in a TXT record contains multiple | ||||
556 | # character-strings (as defined in Section 3.3 of [RFC1035]), | ||||
557 | # the code handling such reply from DNS MUST assemble all of these | ||||
558 | # marshaled text blocks into a single one before any syntactical | ||||
559 | # verification takes place. | ||||
560 | # The same goes for RFC 4408 (SPF), RFC 4871 (DKIM), RFC 5617 (ADSP), | ||||
561 | # draft-kucherawy-dmarc-base (DMARC), ... | ||||
562 | |||||
563 | for my $rr (@answer) { | ||||
564 | my($rr_rdatastr, $rdatanum, $rr_type); | ||||
565 | if (!$rr) { | ||||
566 | # special case, no answer records, only rcode can be tested | ||||
567 | } else { | ||||
568 | $rr_type = uc $rr->type; | ||||
569 | if ($rr->UNIVERSAL::can('txtdata')) { # TXT, SPF | ||||
570 | # join with no intervening spaces, as per RFC 5518 | ||||
571 | if ($txtdata_can_provide_a_list || $rr_type ne 'TXT') { | ||||
572 | $rr_rdatastr = join('', $rr->txtdata); # txtdata() in list context! | ||||
573 | } else { # char_str_list() is only available for TXT records | ||||
574 | $rr_rdatastr = join('', $rr->char_str_list); # historical | ||||
575 | } | ||||
576 | } else { | ||||
577 | $rr_rdatastr = $rr->rdatastr; | ||||
578 | if ($rr_type eq 'A' && | ||||
579 | $rr_rdatastr =~ m/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/) { | ||||
580 | $rdatanum = Mail::SpamAssassin::Util::my_inet_aton($rr_rdatastr); | ||||
581 | } | ||||
582 | } | ||||
583 | # dbg("askdns: received rr type %s, data: %s", $rr_type, $rr_rdatastr); | ||||
584 | } | ||||
585 | |||||
586 | my $j = 0; | ||||
587 | for my $q_tuple (!ref $queries_ref ? () : @$queries_ref) { | ||||
588 | next if !$q_tuple; | ||||
589 | my($query_type, $answer_types_ref, $rules) = @$q_tuple; | ||||
590 | |||||
591 | next if !defined $qtype || $query_type ne $qtype; | ||||
592 | $answer_types_ref = [$query_type] if !defined $answer_types_ref; | ||||
593 | |||||
594 | # mark rule as done | ||||
595 | $pms->{askdns_map_dnskey_to_rules}{$dnskey}[$j++] = undef; | ||||
596 | |||||
597 | while (my($rulename,$subtest) = each %$rules) { | ||||
598 | my $match; | ||||
599 | local($1,$2,$3); | ||||
600 | if (ref $subtest eq 'HASH') { # a list of DNS rcodes (as hash keys) | ||||
601 | $match = 1 if $subtest->{$rcode}; | ||||
602 | } elsif ($rcode != 0) { | ||||
603 | # skip remaining tests on DNS error | ||||
604 | } elsif (!defined($rr_type) || | ||||
605 | !grep($_ eq 'ANY' || $_ eq $rr_type, @$answer_types_ref) ) { | ||||
606 | # skip remaining tests on wrong RR type | ||||
607 | } elsif (!defined $subtest) { | ||||
608 | $match = 1; # any valid response of the requested RR type matches | ||||
609 | } elsif (ref $subtest eq 'Regexp') { # a regular expression | ||||
610 | $match = 1 if $rr_rdatastr =~ $subtest; | ||||
611 | } elsif ($rr_rdatastr eq $subtest) { # exact equality | ||||
612 | $match = 1; | ||||
613 | } elsif (defined $rdatanum && | ||||
614 | $subtest =~ m{^ (\d+) (?: ([/-]) (\d+) )? \z}x) { | ||||
615 | my($n1,$delim,$n2) = ($1,$2,$3); | ||||
616 | $match = | ||||
617 | !defined $n2 ? ($rdatanum & $n1) && # mask only | ||||
618 | (($rdatanum & 0xff000000) == 0x7f000000) # 127/8 | ||||
619 | : $delim eq '-' ? $rdatanum >= $n1 && $rdatanum <= $n2 # range | ||||
620 | : $delim eq '/' ? ($rdatanum & $n2) == ($n1 & $n2) # value/mask | ||||
621 | : 0; | ||||
622 | } | ||||
623 | if ($match) { | ||||
624 | $self->askdns_hit($pms, $ent->{query_domain}, $qtype, | ||||
625 | $rr_rdatastr, $rulename); | ||||
626 | $rulenames_hit{$rulename} = 1; | ||||
627 | } | ||||
628 | } | ||||
629 | } | ||||
630 | } | ||||
631 | # these rules have completed (since they got at least 1 hit) | ||||
632 | $pms->register_async_rule_finish($_) for keys %rulenames_hit; | ||||
633 | } | ||||
634 | |||||
635 | sub askdns_hit { | ||||
636 | my($self, $pms, $query_domain, $qtype, $rr_rdatastr, $rulename) = @_; | ||||
637 | |||||
638 | $rr_rdatastr = '' if !defined $rr_rdatastr; # e.g. with rules testing rcode | ||||
639 | dbg('askdns: domain "%s" listed (%s): %s', | ||||
640 | $query_domain, $rulename, $rr_rdatastr); | ||||
641 | |||||
642 | # only the first hit will show in the test log report, even if | ||||
643 | # an answer section matches more than once - got_hit() handles this | ||||
644 | $pms->clear_test_state; | ||||
645 | $pms->test_log(sprintf("%s %s:%s", $query_domain,$qtype,$rr_rdatastr)); | ||||
646 | $pms->got_hit($rulename, 'ASKDNS: ', ruletype => 'askdns'); # score=>$score | ||||
647 | } | ||||
648 | |||||
649 | 1 | 22µs | 1; | ||
# spent 86µs within Mail::SpamAssassin::Plugin::AskDNS::CORE:match which was called 9 times, avg 10µs/call:
# 4 times (51µs+0s) by Mail::SpamAssassin::Plugin::AskDNS::__ANON__[/usr/local/lib/perl5/site_perl/Mail/SpamAssassin/Plugin/AskDNS.pm:356] at line 310, avg 13µs/call
# 2 times (17µs+0s) by Mail::SpamAssassin::Plugin::AskDNS::__ANON__[/usr/local/lib/perl5/site_perl/Mail/SpamAssassin/Plugin/AskDNS.pm:356] at line 322, avg 8µs/call
# 2 times (13µs+0s) by Mail::SpamAssassin::Plugin::AskDNS::__ANON__[/usr/local/lib/perl5/site_perl/Mail/SpamAssassin/Plugin/AskDNS.pm:356] at line 333, avg 6µs/call
# once (6µs+0s) by Mail::SpamAssassin::Plugin::AskDNS::parse_and_canonicalize_subtest at line 254 | |||||
# spent 11µs within Mail::SpamAssassin::Plugin::AskDNS::CORE:qr which was called:
# once (11µs+0s) by Mail::SpamAssassin::Plugin::AskDNS::parse_and_canonicalize_subtest at line 255 | |||||
# spent 96µs within Mail::SpamAssassin::Plugin::AskDNS::CORE:regcomp which was called:
# once (96µs+0s) by Mail::SpamAssassin::Plugin::AskDNS::parse_and_canonicalize_subtest at line 255 |