File: DKIM.pm

package info (click to toggle)
spamassassin 3.4.1-6~bpo8%2B1
  • links: PTS, VCS
  • area: main
  • in suites: jessie-backports
  • size: 12,620 kB
  • sloc: perl: 56,281; ansic: 3,388; sh: 596; sql: 201; makefile: 198; python: 17
file content (1303 lines) | stat: -rw-r--r-- 52,186 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
# <@LICENSE>
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to you under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at:
# 
#     http://www.apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# </@LICENSE>

=head1 NAME

Mail::SpamAssassin::Plugin::DKIM - perform DKIM verification tests

=head1 SYNOPSIS

 loadplugin Mail::SpamAssassin::Plugin::DKIM [/path/to/DKIM.pm]

Taking into account signatures from any signing domains:

 full   DKIM_SIGNED           eval:check_dkim_signed()
 full   DKIM_VALID            eval:check_dkim_valid()
 full   DKIM_VALID_AU         eval:check_dkim_valid_author_sig()

Taking into account signatures from specified signing domains only:
(quotes may be omitted on domain names consisting only of letters, digits,
dots, and minus characters)

 full   DKIM_SIGNED_MY1       eval:check_dkim_signed('dom1','dom2',...)
 full   DKIM_VALID_MY1        eval:check_dkim_valid('dom1','dom2',...)
 full   DKIM_VALID_AU_MY1     eval:check_dkim_valid_author_sig('d1','d2',...)

 full   __DKIM_DEPENDABLE     eval:check_dkim_dependable()

Author Domain Signing Practices (ADSP) from any author domains:

 header DKIM_ADSP_NXDOMAIN    eval:check_dkim_adsp('N')
 header DKIM_ADSP_ALL         eval:check_dkim_adsp('A')
 header DKIM_ADSP_DISCARD     eval:check_dkim_adsp('D')
 header DKIM_ADSP_CUSTOM_LOW  eval:check_dkim_adsp('1')
 header DKIM_ADSP_CUSTOM_MED  eval:check_dkim_adsp('2')
 header DKIM_ADSP_CUSTOM_HIGH eval:check_dkim_adsp('3')

Author Domain Signing Practices (ADSP) from specified author domains only:

 header DKIM_ADSP_MY1         eval:check_dkim_adsp('*','dom1','dom2',...)

 describe DKIM_SIGNED   Message has a DKIM or DK signature, not necessarily valid
 describe DKIM_VALID    Message has at least one valid DKIM or DK signature
 describe DKIM_VALID_AU Message has a valid DKIM or DK signature from author's domain
 describe __DKIM_DEPENDABLE     A validation failure not attributable to truncation

 describe DKIM_ADSP_NXDOMAIN    Domain not in DNS and no valid author domain signature
 describe DKIM_ADSP_ALL         Domain signs all mail, no valid author domain signature
 describe DKIM_ADSP_DISCARD     Domain signs all mail and suggests discarding mail with no valid author domain signature, no valid author domain signature
 describe DKIM_ADSP_CUSTOM_LOW  adsp_override is CUSTOM_LOW, no valid author domain signature
 describe DKIM_ADSP_CUSTOM_MED  adsp_override is CUSTOM_MED, no valid author domain signature
 describe DKIM_ADSP_CUSTOM_HIGH adsp_override is CUSTOM_HIGH, no valid author domain signature

For compatibility with pre-3.3.0 versions, the following are synonyms:

 OLD: eval:check_dkim_verified = NEW: eval:check_dkim_valid
 OLD: eval:check_dkim_signall  = NEW: eval:check_dkim_adsp('A')
 OLD: eval:check_dkim_signsome = NEW: redundant, semantically always true

The __DKIM_DEPENDABLE eval rule deserves an explanation. The rule yields true
when signatures are supplied by a caller, OR ELSE when signatures are obtained
by this plugin AND either there are no signatures OR a rule __TRUNCATED was
false. In other words: __DKIM_DEPENDABLE is true when failed signatures can
not be attributed to message truncation when feeding a message to SpamAssassin.
It can be consulted to prevent false positives on large but truncated messages
with poor man's implementation of ADSP by hand-crafted rules.

=head1 DESCRIPTION

This SpamAssassin plugin implements DKIM lookups as described by the RFC 4871,
as well as historical DomainKeys lookups, as described by RFC 4870, thanks
to the support for both types of signatures by newer versions of module
Mail::DKIM.

It requires the C<Mail::DKIM> CPAN module to operate. Many thanks to Jason Long
for that module.

=head1 TAGS

The following tags are added to the set, available for use in reports,
header fields, other plugins, etc.:

  _DKIMIDENTITY_
    Agent or User Identifier (AUID) (the 'i' tag) from valid signatures;

  _DKIMDOMAIN_
    Signing Domain Identifier (SDID) (the 'd' tag) from valid signatures;

Identities and domains from signatures which failed verification are not
included in these tags. Duplicates are eliminated (e.g. when there are two or
more valid signatures from the same signer, only one copy makes it into a tag).
Note that there may be more than one signature in a message - currently they
are provided as a space-separated list, although this behaviour may change.

=head1 SEE ALSO

C<Mail::DKIM>, C<Mail::SpamAssassin::Plugin>

  http://jason.long.name/dkimproxy/
  http://tools.ietf.org/rfc/rfc4871.txt
  http://tools.ietf.org/rfc/rfc4870.txt
  http://tools.ietf.org/rfc/rfc5617.txt
  http://ietf.org/html.charters/dkim-charter.html

=cut

package Mail::SpamAssassin::Plugin::DKIM;

use Mail::SpamAssassin::Plugin;
use Mail::SpamAssassin::Logger;
use Mail::SpamAssassin::Timeout;

use strict;
use warnings;
use bytes;
use re 'taint';

use vars qw(@ISA);
@ISA = qw(Mail::SpamAssassin::Plugin);

# constructor: register the eval rule
sub new {
  my $class = shift;
  my $mailsaobject = shift;

  $class = ref($class) || $class;
  my $self = $class->SUPER::new($mailsaobject);
  bless ($self, $class);

  # signatures
  $self->register_eval_rule("check_dkim_signed");
  $self->register_eval_rule("check_dkim_valid");
  $self->register_eval_rule("check_dkim_valid_author_sig");
  $self->register_eval_rule("check_dkim_testing");

  # author domain signing practices
  $self->register_eval_rule("check_dkim_adsp");
  $self->register_eval_rule("check_dkim_dependable");

  # whitelisting
  $self->register_eval_rule("check_for_dkim_whitelist_from");
  $self->register_eval_rule("check_for_def_dkim_whitelist_from");

  # old names (aliases) for compatibility
  $self->register_eval_rule("check_dkim_verified");  # = check_dkim_valid
  $self->register_eval_rule("check_dkim_signall");   # = check_dkim_adsp('A')
  $self->register_eval_rule("check_dkim_signsome");  # redundant, always false

  $self->set_config($mailsaobject->{conf});

  return $self;
}

###########################################################################

sub set_config {
  my($self, $conf) = @_;
  my @cmds;

=head1 USER SETTINGS

=over 4

=item whitelist_from_dkim author@example.com [signing-domain]

Works similarly to whitelist_from, except that in addition to matching
an author address (From) to the pattern in the first parameter, the message
must also carry a valid Domain Keys Identified Mail (DKIM) signature made by
a signing domain (SDID, i.e. the d= tag) that is acceptable to us.

Only one whitelist entry is allowed per line, as in C<whitelist_from_rcvd>.
Multiple C<whitelist_from_dkim> lines are allowed. File-glob style characters
are allowed for the From address (the first parameter), just like with
C<whitelist_from_rcvd>.

The second parameter (the signing-domain) does not accept full file-glob style
wildcards, although a simple '*.' (or just a '.') prefix to a domain name
is recognized and implies any subdomain of the specified domain (but not
the domain itself).

If no signing-domain parameter is specified, the only acceptable signature
will be an Author Domain Signature (sometimes called first-party signature)
which is a signature where the signing domain (SDID) of a signature matches
the domain of the author's address (i.e. the address in a From header field).

Since this whitelist requires a DKIM check to be made, network tests must
be enabled.

Examples of whitelisting based on an author domain signature (first-party):

  whitelist_from_dkim joe@example.com
  whitelist_from_dkim *@corp.example.com
  whitelist_from_dkim *@*.example.com

Examples of whitelisting based on third-party signatures:

  whitelist_from_dkim jane@example.net      example.org
  whitelist_from_dkim rick@info.example.net example.net
  whitelist_from_dkim *@info.example.net    example.net
  whitelist_from_dkim *@*                   mail7.remailer.example.com
  whitelist_from_dkim *@*                   *.remailer.example.com

=item def_whitelist_from_dkim author@example.com [signing-domain]

Same as C<whitelist_from_dkim>, but used for the default whitelist entries
in the SpamAssassin distribution.  The whitelist score is lower, because
these are often targets for abuse of public mailers which sign their mail.

=item unwhitelist_from_dkim author@example.com [signing-domain]

Removes an email address with its corresponding signing-domain field
from def_whitelist_from_dkim and whitelist_from_dkim tables, if it exists.
Parameters to unwhitelist_from_dkim must exactly match the parameters of
a corresponding whitelist_from_dkim or def_whitelist_from_dkim config
option which created the entry, for it to be removed (a domain name is
matched case-insensitively);  i.e. if a signing-domain parameter was
specified in a whitelisting command, it must also be specified in the
unwhitelisting command.

Useful for removing undesired default entries from a distributed configuration
by a local or site-specific configuration or by C<user_prefs>.

=item adsp_override domain [signing-practices]

Currently few domains publish their signing practices (RFC 5617 - ADSP),
partly because the ADSP rfc is rather new, partly because they think
hardly any recipient bothers to check it, and partly for fear that some
recipients might lose mail due to problems in their signature validation
procedures or mail mangling by mailers beyond their control.

Nevertheless, recipients could benefit by knowing signing practices of a
sending (author's) domain, for example to recognize forged mail claiming
to be from certain domains which are popular targets for phishing, like
financial institutions. Unfortunately, as signing practices are seldom
published or are weak, it is hardly justifiable to look them up in DNS.

To overcome this chicken-or-the-egg problem, the C<adsp_override> mechanism
allows recipients using SpamAssassin to override published or defaulted
ADSP for certain domains. This makes it possible to manually specify a
stronger (or weaker) signing practices than a signing domain is willing
to publish (explicitly or by default), and also save on a DNS lookup.

Note that ADSP (published or overridden) is only consulted for messages
which do not contain a valid DKIM signature from the author's domain.

According to RFC 5617, signing practices can be one of the following:
C<unknown>, C<all> and C<discardable>.

C<unknown>: The domain might sign some or all email - messages from the
domain may or may not have an Author Domain Signature. This is a default
if a domain exists in DNS but no ADSP record is found.

C<all>: All mail from the domain is signed with an Author Domain Signature.

C<discardable>: All mail from the domain is signed with an Author Domain
Signature.  Furthermore, if a message arrives without a valid Author Domain
Signature, the domain encourages the recipient(s) to discard it.

ADSP lookup can also determine that a domain is "out of scope", i.e., the
domain does not exist (NXDOMAIN) in the DNS.

To override domain's signing practices in a SpamAssassin configuration file,
specify an C<adsp_override> directive for each sending domain to be overridden.

Its first argument is a domain name. Author's domain is matched against it,
matching is case insensitive. This is not a regular expression or a file-glob
style wildcard, but limited wildcarding is still available: if this argument
starts by a "*." (or is a sole "*"), author's domain matches if it is a
subdomain (to one or more levels) of the argument. Otherwise (with no leading
asterisk) the match must be exact (not a subdomain).

An optional second parameter is one of the following keywords
(case-insensitive): C<nxdomain>, C<unknown>, C<all>, C<discardable>,
C<custom_low>, C<custom_med>, C<custom_high>.

Absence of this second parameter implies C<discardable>. If a domain is not
listed by a C<adsp_override> directive nor does it explicitly publish any
ADSP record, then C<unknown> is implied for valid domains, and C<nxdomain>
for domains not existing in DNS. (Note: domain validity is only checked with
versions of Mail::DKIM 0.37 or later (actually since 0.36_5), the C<nxdomain>
would never turn up with older versions).

The strong setting C<discardable> is useful for domains which are known
to always sign their mail and to always send it directly to recipients
(not to mailing lists), and are frequent targets of fishing attempts,
such as financial institutions. The C<discardable> is also appropriate
for domains which are known never to send any mail.

When a message does not contain a valid signature by the author's domain
(the domain in a From header field), the signing practices pertaining
to author's domain determine which of the following rules fire and
contributes its score: DKIM_ADSP_NXDOMAIN, DKIM_ADSP_ALL, DKIM_ADSP_DISCARD,
DKIM_ADSP_CUSTOM_LOW, DKIM_ADSP_CUSTOM_MED, DKIM_ADSP_CUSTOM_HIGH. Not more
than one of these rules can fire for messages that have one author (but see
below). The last three can only result from a 'signing-practices' as given
in a C<adsp_override> directive (not from a DNS lookup), and can serve as
a convenient means of providing a different score if scores assigned to
DKIM_ADSP_ALL or DKIM_ADSP_DISCARD are not considered suitable for some
domains.

RFC 5322 permits a message to have more than one author - multiple addresses
may be listed in a single From header field.  RFC 5617 defines that a message
with multiple authors has multiple signing domain signing practices, but does
not prescribe how these should be combined. In presence of multiple signing
practices, more than one of the DKIM_ADSP_* rules may fire.

As a precaution against firing DKIM_ADSP_* rules when there is a known local
reason for a signature verification failure, the domain's ADSP is considered
'unknown' when DNS lookups are disabled or a DNS lookup encountered a temporary
problem on fetching a public key from the author's domain. Similarly, ADSP
is considered 'unknown' when this plugin did its own signature verification
(signatures were not passed to SA by a caller) and a metarule __TRUNCATED was
triggered, indicating the caller intentionally passed a truncated message to
SpamAssassin, which was a likely reason for a signature verification failure.

Example:

  adsp_override *.mydomain.example.com   discardable
  adsp_override *.neversends.example.com discardable

  adsp_override ebay.com
  adsp_override *.ebay.com
  adsp_override ebay.co.uk
  adsp_override *.ebay.co.uk
  adsp_override paypal.com
  adsp_override *.paypal.com
  adsp_override amazon.com
  adsp_override ealerts.bankofamerica.com
  adsp_override americangreetings.com
  adsp_override egreetings.com
  adsp_override bluemountain.com
  adsp_override hallmark.com   all
  adsp_override *.hallmark.com all
  adsp_override youtube.com    custom_high
  adsp_override google.com     custom_low
  adsp_override gmail.com      custom_low
  adsp_override googlemail.com custom_low
  adsp_override yahoo.com      custom_low
  adsp_override yahoo.com.au   custom_low
  adsp_override yahoo.se       custom_low

  adsp_override junkmailerkbw0rr.com nxdomain
  adsp_override junkmailerd2hlsg.com nxdomain

  # effectively disables ADSP network DNS lookups for all other domains:
  adsp_override *              unknown

  score DKIM_ADSP_ALL          2.5
  score DKIM_ADSP_DISCARD     25
  score DKIM_ADSP_NXDOMAIN     3

  score DKIM_ADSP_CUSTOM_LOW   1
  score DKIM_ADSP_CUSTOM_MED   3.5
  score DKIM_ADSP_CUSTOM_HIGH  8


=item dkim_minimum_key_bits n             (default: 1024)

The smallest size of a signing key (in bits) for a valid signature to be
considered for whitelisting. Additionally, the eval function check_dkim_valid()
will return false on short keys when called with explicitly listed domains,
and the eval function check_dkim_valid_author_sig() will return false on short
keys (regardless of its arguments). Setting the option to 0 disables a key
size check.

Note that the option has no effect when the eval function check_dkim_valid()
is called with no arguments (like in a rule DKIM_VALID). A mere presence of
some valid signature on a message has no reputational value (without being
associated with a particular domain), regardless of its key size - anyone can
prepend its own signature on a copy of some third party mail and re-send it,
which makes it no more trustworthy than without such signature. This is also
a reason for a rule DKIM_VALID to have a near-zero score, i.e. a rule hit
is only informational.

=cut

  push (@cmds, {
    setting => 'whitelist_from_dkim',
    type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST,
    code => sub {
      my ($self, $key, $value, $line) = @_;
      local ($1,$2);
      unless (defined $value && $value !~ /^$/) {
        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
      }
      unless ($value =~ /^(\S+)(?:\s+(\S+))?$/) {
        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
      }
      my $address = $1;
      my $sdid = defined $2 ? $2 : '';  # empty implies author domain signature
      $address =~ s/(\@[^@]*)\z/lc($1)/e;  # lowercase the email address domain
      $self->{parser}->add_to_addrlist_dkim('whitelist_from_dkim',
                                            $address, lc $sdid);
    }
  });

  push (@cmds, {
    setting => 'def_whitelist_from_dkim',
    type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST,
    code => sub {
      my ($self, $key, $value, $line) = @_;
      local ($1,$2);
      unless (defined $value && $value !~ /^$/) {
        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
      }
      unless ($value =~ /^(\S+)(?:\s+(\S+))?$/) {
        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
      }
      my $address = $1;
      my $sdid = defined $2 ? $2 : '';  # empty implies author domain signature
      $address =~ s/(\@[^@]*)\z/lc($1)/e;  # lowercase the email address domain
      $self->{parser}->add_to_addrlist_dkim('def_whitelist_from_dkim',
                                            $address, lc $sdid);
    }
  });

  push (@cmds, {
    setting => 'unwhitelist_from_dkim',
    type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST,
    code => sub {
      my ($self, $key, $value, $line) = @_;
      local ($1,$2);
      unless (defined $value && $value !~ /^$/) {
        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
      }
      unless ($value =~ /^(\S+)(?:\s+(\S+))?$/) {
        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
      }
      my $address = $1;
      my $sdid = defined $2 ? $2 : '';  # empty implies author domain signature
      $address =~ s/(\@[^@]*)\z/lc($1)/e;  # lowercase the email address domain
      $self->{parser}->remove_from_addrlist_dkim('whitelist_from_dkim',
                                                 $address, lc $sdid);
      $self->{parser}->remove_from_addrlist_dkim('def_whitelist_from_dkim',
                                                 $address, lc $sdid);
    }
  });

  push (@cmds, {
    setting => 'adsp_override',
    type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
    code => sub {
      my ($self, $key, $value, $line) = @_;
      local ($1,$2);
      unless (defined $value && $value !~ /^$/) {
        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
      }
      unless ($value =~ /^ \@? ( [*a-z0-9._-]+ )
                         (?: \s+ (nxdomain|unknown|all|discardable|
                                  custom_low|custom_med|custom_high) )?$/ix) {
        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
      }
      my $domain = lc $1;  # author's domain
      my $adsp = $2;       # author domain signing practices
      $adsp = 'discardable' if !defined $adsp;
      $adsp = lc $adsp;
      if    ($adsp eq 'custom_low' ) { $adsp = '1' }
      elsif ($adsp eq 'custom_med' ) { $adsp = '2' }
      elsif ($adsp eq 'custom_high') { $adsp = '3' }
      else { $adsp = uc substr($adsp,0,1) }  # N/U/A/D/1/2/3
      $self->{parser}->{conf}->{adsp_override}->{$domain} = $adsp;
    }
  });

  # minimal signing key size in bits that is acceptable for whitelisting
  push (@cmds, {
    setting => 'dkim_minimum_key_bits',
    default => 1024,
    type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
  });

=back

=head1 ADMINISTRATOR SETTINGS

=over 4

=item dkim_timeout n             (default: 5)

How many seconds to wait for a DKIM query to complete, before scanning
continues without the DKIM result. A numeric value is optionally suffixed
by a time unit (s, m, h, d, w, indicating seconds (default), minutes, hours,
days, weeks).

=back

=cut

  push (@cmds, {
    setting => 'dkim_timeout',
    is_admin => 1,
    default => 5,
    type => $Mail::SpamAssassin::Conf::CONF_TYPE_DURATION
  });

  $conf->{parser}->register_commands(\@cmds);
}

# ---------------------------------------------------------------------------

sub check_dkim_signed {
  my ($self, $pms, $full_ref, @acceptable_domains) = @_;
  $self->_check_dkim_signature($pms)  if !$pms->{dkim_checked_signature};
  my $result = 0;
  if (!$pms->{dkim_signed}) {
    # don't bother
  } elsif (!@acceptable_domains) {
    $result = 1;  # no additional constraints, any signing domain will do
  } else {
    $result = $self->_check_dkim_signed_by($pms,0,0,\@acceptable_domains);
  }
  return $result;
}

sub check_dkim_valid {
  my ($self, $pms, $full_ref, @acceptable_domains) = @_;
  $self->_check_dkim_signature($pms)  if !$pms->{dkim_checked_signature};
  my $result = 0;
  if (!$pms->{dkim_valid}) {
    # don't bother
  } elsif (!@acceptable_domains) {
    $result = 1;  # no additional constraints, any signing domain will do,
                  # also any signing key size will do
  } else {
    $result = $self->_check_dkim_signed_by($pms,1,0,\@acceptable_domains);
  }
  return $result;
}

sub check_dkim_valid_author_sig {
  my ($self, $pms, $full_ref, @acceptable_domains) = @_;
  $self->_check_dkim_signature($pms)  if !$pms->{dkim_checked_signature};
  my $result = 0;
  if (!%{$pms->{dkim_has_valid_author_sig}}) {
    # don't bother
  } else {
    $result = $self->_check_dkim_signed_by($pms,1,1,\@acceptable_domains);
  }
  return $result;
}

sub check_dkim_dependable {
  my ($self, $pms) = @_;
  $self->_check_dkim_signature($pms)  if !$pms->{dkim_checked_signature};
  return $pms->{dkim_signatures_dependable};
}

# mosnomer, old synonym for check_dkim_valid, kept for compatibility
sub check_dkim_verified {
  return check_dkim_valid(@_);
}

# no valid Author Domain Signature && ADSP matches the argument
sub check_dkim_adsp {
  my ($self, $pms, $adsp_char, @domains_list) = @_;
  $self->_check_dkim_signature($pms)  if !$pms->{dkim_checked_signature};
  my $result = 0;
  if (!$pms->{dkim_signatures_ready}) {
    # don't bother
  } else {
    $self->_check_dkim_adsp($pms)  if !$pms->{dkim_checked_adsp};

    # an asterisk indicates any ADSP type can match (as long as
    # there is no valid author domain signature present)
    $adsp_char = 'NAD123'  if $adsp_char eq '*';  # a shorthand for NAD123

    if ( !(grep { index($adsp_char,$_) >= 0 } values %{$pms->{dkim_adsp}}) ) {
      # not the right ADSP type
    } elsif (!@domains_list) {
      $result = 1;  # no additional constraints, any author domain will do
    } else {
      local $1;
      my %author_domains = %{$pms->{dkim_author_domains}};
      foreach my $dom (@domains_list) {
        if ($dom =~ /^\*?\.(.*)\z/s) {  # domain itself or its subdomain
          my $doms = lc $1;
          if ($author_domains{$doms} ||
              (grep { /\.\Q$doms\E\z/s } keys %author_domains) ) {
            $result = 1; last;
          }
        } else {  # match on domain (not a subdomain)
          if ($author_domains{lc $dom}) {
            $result = 1; last;
          }
        }
      }
    }
  }
  return $result;
}

# useless, semantically always true according to ADSP (RFC 5617)
sub check_dkim_signsome {
  my ($self, $pms) = @_;
  # the signsome is semantically always true, and thus redundant;
  # for compatibility just returns false to prevent
  # a legacy rule DKIM_POLICY_SIGNSOME from always firing
  return 0;
}

# synonym with check_dkim_adsp('A'), kept for compatibility
sub check_dkim_signall {
  my ($self, $pms) = @_;
  check_dkim_adsp($self, $pms, 'A');
}

# public key carries a testing flag
sub check_dkim_testing {
  my ($self, $pms) = @_;
  my $result = 0;
  $self->_check_dkim_signature($pms)  if !$pms->{dkim_checked_signature};
  $result = 1  if $pms->{dkim_key_testing};
  return $result;
}

sub check_for_dkim_whitelist_from {
  my ($self, $pms) = @_;
  $self->_check_dkim_whitelist($pms)  if !$pms->{whitelist_checked};
  return $pms->{dkim_match_in_whitelist_from_dkim} || 
         $pms->{dkim_match_in_whitelist_auth};
}

sub check_for_def_dkim_whitelist_from {
  my ($self, $pms) = @_;
  $self->_check_dkim_whitelist($pms)  if !$pms->{whitelist_checked};
  return $pms->{dkim_match_in_def_whitelist_from_dkim} || 
         $pms->{dkim_match_in_def_whitelist_auth};
}

# ---------------------------------------------------------------------------

sub _dkim_load_modules {
  my ($self) = @_;

  if (!$self->{tried_loading}) {
    $self->{service_available} = 0;
    my $timemethod = $self->{main}->UNIVERSAL::can("time_method") &&
                     $self->{main}->time_method("dkim_load_modules");
    my $eval_stat;
    eval {
      # Have to do this so that RPM doesn't find these as required perl modules.
      { require Mail::DKIM::Verifier }
    } or do {
      $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
    };
    $self->{tried_loading} = 1;

    if (defined $eval_stat) {
      dbg("dkim: cannot load Mail::DKIM module, DKIM checks disabled: %s",
          $eval_stat);
    } else {
      my $version = Mail::DKIM::Verifier->VERSION;
      if ($version >= 0.31) {
        dbg("dkim: using Mail::DKIM version $version");
      } else {
        info("dkim: Mail::DKIM $version is older than the required ".
             "minimal version 0.31, suggested upgrade to 0.37 or later!");
      }
      $self->{service_available} = 1;

      my $adsp_avail =
        eval { require Mail::DKIM::AuthorDomainPolicy };  # since 0.34
      if (!$adsp_avail) {  # fallback to pre-ADSP policy
        eval { require Mail::DKIM::DkimPolicy }  # ignoring status
      }
    }
  }
  return $self->{service_available};
}

# ---------------------------------------------------------------------------

sub _check_dkim_signed_by {
  my ($self, $pms, $must_be_valid, $must_be_author_domain_signature,
      $acceptable_domains_ref) = @_;
  my $result = 0;
  my $verifier = $pms->{dkim_verifier};
  my $minimum_key_bits = $pms->{conf}->{dkim_minimum_key_bits};
  foreach my $sig (@{$pms->{dkim_signatures}}) {
    next if !defined $sig;
    if ($must_be_valid) {
      next if ($sig->UNIVERSAL::can("result") ? $sig : $verifier)
                ->result ne 'pass';
      next if $sig->UNIVERSAL::can("check_expiration") &&
              !$sig->check_expiration;
      next if $minimum_key_bits && $sig->{_spamassassin_key_size} &&
              $sig->{_spamassassin_key_size} < $minimum_key_bits;
    }
    my $sdid = $sig->domain;
    next if !defined $sdid;  # a signature with a missing required tag 'd' ?
    $sdid = lc $sdid;
    if ($must_be_author_domain_signature) {
      next if !$pms->{dkim_author_domains}->{$sdid};
    }
    if (!@$acceptable_domains_ref) {
      $result = 1;
    } else {
      foreach my $ad (@$acceptable_domains_ref) {
        if ($ad =~ /^\*?\.(.*)\z/s) {  # domain itself or its subdomain
          my $d = lc $1;
          if ($sdid eq $d || $sdid =~ /\.\Q$d\E\z/s) { $result = 1; last }
        } else {  # match on domain (not a subdomain)
          if ($sdid eq lc $ad) { $result = 1; last }
        }
      }
    }
    last if $result;
  }
  return $result;
}

sub _get_authors {
  my ($self, $pms) = @_;

  # Note that RFC 5322 permits multiple addresses in the From header field,
  # and according to RFC 5617 such message has multiple authors and hence
  # multiple "Author Domain Signing Practices". For the time being the
  # SpamAssassin's get() can only provide a single author!

  my %author_domains;  local $1;
  my @authors = grep { defined $_ } ( $pms->get('from:addr',undef) );
  for (@authors) {
    # be tolerant, ignore trailing WSP after a domain name
    $author_domains{lc $1} = 1  if /\@([^\@]+?)[ \t]*\z/s;
  }
  $pms->{dkim_author_addresses} = \@authors;       # list of full addresses
  $pms->{dkim_author_domains} = \%author_domains;  # hash of their domains
}

sub _check_dkim_signature {
  my ($self, $pms) = @_;

  my $conf = $pms->{conf};
  my($verifier, @signatures, @valid_signatures);

  $pms->{dkim_checked_signature} = 1; # has this sub already been invoked?
  $pms->{dkim_signatures_ready} = 0;  # have we obtained & verified signatures?
  $pms->{dkim_signatures_dependable} = 0;
  # dkim_signatures_dependable =
  #   (signatures supplied by a caller) or
  #   ( (signatures obtained by this plugin) and
  #     (no signatures, or message was not truncated) )
  $pms->{dkim_signatures} = \@signatures;
  $pms->{dkim_valid_signatures} = \@valid_signatures;
  $pms->{dkim_signed} = 0;
  $pms->{dkim_valid} = 0;
  $pms->{dkim_key_testing} = 0;
  # the following hashes are keyed by a signing domain (SDID):
  $pms->{dkim_author_sig_tempfailed} = {}; # DNS timeout verifying author sign.
  $pms->{dkim_has_valid_author_sig} = {};  # a valid author domain signature
  $pms->{dkim_has_any_author_sig} = {};  # valid or invalid author domain sign.

  $self->_get_authors($pms)  if !$pms->{dkim_author_addresses};

  my $suppl_attrib = $pms->{msg}->{suppl_attrib};
  if (defined $suppl_attrib && exists $suppl_attrib->{dkim_signatures}) {
    # caller of SpamAssassin already supplied DKIM signature objects
    my $provided_signatures = $suppl_attrib->{dkim_signatures};
    @signatures = @$provided_signatures  if ref $provided_signatures;
    $pms->{dkim_signatures_ready} = 1;
    $pms->{dkim_signatures_dependable} = 1;
    dbg("dkim: signatures provided by the caller, %d signatures",
        scalar(@signatures));
  }

  if ($pms->{dkim_signatures_ready}) {
    # signatures already available and verified
  } elsif (!$pms->is_dns_available()) {
    dbg("dkim: signature verification disabled, DNS resolving not available");
  } elsif (!$self->_dkim_load_modules()) {
    # Mail::DKIM module not available
  } else {
    # signature objects not provided by the caller, must verify for ourselves
    my $timemethod = $self->{main}->UNIVERSAL::can("time_method") &&
                     $self->{main}->time_method("check_dkim_signature");
    if (Mail::DKIM::Verifier->VERSION >= 0.40) {
      my $edns = $conf->{dns_options}->{edns};
      if ($edns && $edns >= 1024) {
        # Let Mail::DKIM use our interface to Net::DNS::Resolver.
        # Only do so if EDNS0 provides a reasonably-sized UDP payload size,
        # as our interface does not provide a DNS fallback to TCP, unlike
        # the Net::DNS::Resolver::send which does provide it.
        my $res = $self->{main}->{resolver}->get_resolver;
        Mail::DKIM::DNS::resolver($res);
      }
    }
    $verifier = Mail::DKIM::Verifier->new;
    if (!$verifier) {
      dbg("dkim: cannot create Mail::DKIM::Verifier object");
      return;
    }
    $pms->{dkim_verifier} = $verifier;
    #
    # feed content of a message into verifier, using \r\n endings,
    # required by Mail::DKIM API (see bug 5300)
    # note: bug 5179 comment 28: perl does silly things on non-Unix platforms
    # unless we use \015\012 instead of \r\n
    eval {
      my $str = $pms->{msg}->get_pristine;
      $str =~ s/\r?\n/\015\012/sg;  # ensure \015\012 ending
      # feeding large chunks to Mail::DKIM is much faster than line-by-line
      $verifier->PRINT($str);
      1;
    } or do {  # intercept die() exceptions and render safe
      my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
      dbg("dkim: verification failed, intercepted error: $eval_stat");
      return 0;           # cannot verify message
    };

    my $timeout = $conf->{dkim_timeout};
    my $timer = Mail::SpamAssassin::Timeout->new(
                  { secs => $timeout, deadline => $pms->{master_deadline} });

    my $err = $timer->run_and_catch(sub {
      dbg("dkim: performing public key lookup and signature verification");
      $verifier->CLOSE();  # the action happens here

      # currently SpamAssassin's parsing is better than Mail::Address parsing,
      # don't bother fetching $verifier->message_originator->address
      # to replace what we already have in $pms->{dkim_author_addresses}

      # versions before 0.29 only provided a public interface to fetch one
      # signature, newer versions allow access to all signatures of a message
      @signatures = $verifier->UNIVERSAL::can("signatures") ?
                                 $verifier->signatures : $verifier->signature;
    });
    if ($timer->timed_out()) {
      dbg("dkim: public key lookup or verification timed out after %s s",
          $timeout );
#***
    # $pms->{dkim_author_sig_tempfailed}->{$_} = 1  for ...

    } elsif ($err) {
      chomp $err;
      dbg("dkim: public key lookup or verification failed: $err");
    }
    $pms->{dkim_signatures_ready} = 1;
    if (!@signatures || !$pms->{tests_already_hit}->{'__TRUNCATED'}) {
      $pms->{dkim_signatures_dependable} = 1;
    }
  }

  if ($pms->{dkim_signatures_ready}) {
    my $sig_result_supported;
    my $minimum_key_bits = $conf->{dkim_minimum_key_bits};
    foreach my $signature (@signatures) {
      # old versions of Mail::DKIM would give undef for an invalid signature
      next if !defined $signature;

      $sig_result_supported = $signature->UNIVERSAL::can("result_detail");
      my($info, $valid, $expired);
      $valid =
        ($sig_result_supported ? $signature : $verifier)->result eq 'pass';
      $info = $valid ? 'VALID' : 'FAILED';
      if ($valid && $signature->UNIVERSAL::can("check_expiration")) {
        $expired = !$signature->check_expiration;
        $info .= ' EXPIRED'  if $expired;
      }
      my $key_size;
      if ($valid && !$expired && $minimum_key_bits) {
        $key_size = eval { my $pk = $signature->get_public_key;
                           $pk && $pk->cork && $pk->cork->size * 8 };
        if ($key_size) {
          $signature->{_spamassassin_key_size} = $key_size; # stash it for later
          $info .= " WEAK($key_size)"  if $key_size < $minimum_key_bits;
        }
      }
      push(@valid_signatures, $signature)  if $valid && !$expired;

      # check if we have a potential Author Domain Signature, valid or not
      my $d = $signature->domain;
      if (!defined $d) {
        # can be undefined on a broken signature with missing required tags
      } else {
        $d = lc $d;
        if ($pms->{dkim_author_domains}->{$d}) {  # SDID matches author domain
          $pms->{dkim_has_any_author_sig}->{$d} = 1;
          if ($valid && !$expired &&
              $key_size && $key_size >= $minimum_key_bits) {
            $pms->{dkim_has_valid_author_sig}->{$d} = 1;
          } elsif ( ($sig_result_supported ? $signature
                                           : $verifier)->result_detail
                   =~ /\b(?:timed out|SERVFAIL)\b/i) {
            $pms->{dkim_author_sig_tempfailed}->{$d} = 1;
          }
        }
      }
      if (would_log("dbg","dkim")) {
        dbg("dkim: %s %s, i=%s, d=%s, s=%s, a=%s, c=%s, %s, %s",
          $info,
          $signature->isa('Mail::DKIM::DkSignature') ? 'DK' : 'DKIM',
          map(!defined $_ ? '(undef)' : $_,
            $signature->identity, $d, $signature->selector,
            $signature->algorithm, scalar($signature->canonicalization),
            $key_size ? "key_bits=$key_size" : (),
            ($sig_result_supported ? $signature : $verifier)->result ),
          defined $d && $pms->{dkim_author_domains}->{$d}
            ? 'matches author domain'
            : 'does not match author domain',
        );
      }
    }
    if (@valid_signatures) {
      $pms->{dkim_signed} = 1;
      $pms->{dkim_valid} = 1;
      # let the result stand out more clearly in the log, use uppercase
      my $sig = $valid_signatures[0];
      my $sig_res = ($sig_result_supported ? $sig : $verifier)->result_detail;
      dbg("dkim: signature verification result: %s", uc($sig_res));

      # supply values for both tags
      my(%seen1, %seen2, @identity_list, @domain_list);
      @identity_list = grep(defined $_ && $_ ne '' && !$seen1{$_}++,
                            map($_->identity, @valid_signatures));
      @domain_list =   grep(defined $_ && $_ ne '' && !$seen2{$_}++,
                            map($_->domain, @valid_signatures));
      $pms->set_tag('DKIMIDENTITY',
                    @identity_list == 1 ? $identity_list[0] : \@identity_list);
      $pms->set_tag('DKIMDOMAIN',
                    @domain_list == 1   ? $domain_list[0]   : \@domain_list);
    } elsif (@signatures) {
      $pms->{dkim_signed} = 1;
      my $sig = $signatures[0];
      my $sig_res =
        ($sig_result_supported && $sig ? $sig : $verifier)->result_detail;
      dbg("dkim: signature verification result: %s", uc($sig_res));
    } else {
      dbg("dkim: signature verification result: none");
    }
  }
}

sub _check_dkim_adsp {
  my ($self, $pms) = @_;

  $pms->{dkim_checked_adsp} = 1;

  # a message may have multiple authors (RFC 5322),
  # and hence multiple signing policies (RFC 5617)
  $pms->{dkim_adsp} = {};  # a hash: author_domain => adsp
  my $practices_as_string = '';

  $self->_get_authors($pms)  if !$pms->{dkim_author_addresses};

  # collect only fully qualified domain names, allow '-', think of IDN
  my @author_domains = grep { /.\.[a-z-]{2,}\z/si }
                            keys %{$pms->{dkim_author_domains}};

  my %label =
   ('D' => 'discardable', 'A' => 'all', 'U' => 'unknown', 'N' => 'nxdomain',
    '1' => 'custom_low', '2' => 'custom_med', '3' => 'custom_high');

  # must check the message first to obtain signer, domain, and verif. status
  $self->_check_dkim_signature($pms)  if !$pms->{dkim_checked_signature};

  if (!$pms->{dkim_signatures_ready}) {
    dbg("dkim: adsp not retrieved, signatures not obtained");

  } elsif (!@author_domains) {
    dbg("dkim: adsp not retrieved, no author f.q. domain name");
    $practices_as_string = 'no author domains, ignored';

  } else {

    foreach my $author_domain (@author_domains) {
      my $adsp;

      if ($pms->{dkim_has_valid_author_sig}->{$author_domain}) {
        # don't fetch adsp when valid
        # RFC 5617: If a message has an Author Domain Signature, ADSP provides
        # no benefit relative to that domain since the message is already known
        # to be compliant with any possible ADSP for that domain. [...]
        # implementations SHOULD avoid doing unnecessary DNS lookups
        #
        dbg("dkim: adsp not retrieved, author domain signature is valid");
        $practices_as_string = 'valid a. d. signature';

      } elsif ($pms->{dkim_author_sig_tempfailed}->{$author_domain}) {
        dbg("dkim: adsp ignored, tempfail varifying author domain signature");
        $practices_as_string = 'pub key tempfailed, ignored';

      } elsif ($pms->{dkim_has_any_author_sig}->{$author_domain} &&
               !$pms->{dkim_signatures_dependable}) {
        # the message did have an Author Domain Signature but it wasn't valid;
        # we also believe the message was truncated just before being passed
        # to SpamAssassin, which is a likely reason for verification failure,
        # so we shouldn't take it too harsh with ADSP rules - just pretend
        # the ADSP was 'unknown'
        #
        dbg("dkim: adsp ignored, message was truncated, ".
            "invalid author domain signature");
        $practices_as_string = 'truncated, ignored';

      } else {
        # search the adsp_override list

        # for a domain a.b.c.d it searches the hash in the following order:
        #   a.b.c.d
        #   *.b.c.d
        #     *.c.d
        #       *.d
        #         *
        my $matched_key;
        my $p = $pms->{conf}->{adsp_override};
        if ($p) {
          my @d = split(/\./, $author_domain);
          @d = map { shift @d; join('.', '*', @d) } (0..$#d);
          for my $key ($author_domain, @d) {
            $adsp = $p->{$key};
            if (defined $adsp) { $matched_key = $key; last }
          }
        }

        if (defined $adsp) {
          dbg("dkim: adsp override for domain %s", $author_domain);
          $practices_as_string = 'override';
          $practices_as_string .=
            " by $matched_key"  if $matched_key ne $author_domain;

        } elsif (!$pms->is_dns_available()) {
          dbg("dkim: adsp not retrieved, DNS resolving not available");

        } elsif (!$self->_dkim_load_modules()) {
          dbg("dkim: adsp not retrieved, module Mail::DKIM not available");

        } else {  # do the ADSP DNS lookup
          my $timemethod = $self->{main}->UNIVERSAL::can("time_method") &&
                           $self->{main}->time_method("check_dkim_adsp");

          my $practices;  # author domain signing practices object
          my $timeout = $pms->{conf}->{dkim_timeout};
          my $timer = Mail::SpamAssassin::Timeout->new(
                    { secs => $timeout, deadline => $pms->{master_deadline} });
          my $err = $timer->run_and_catch(sub {
            eval {
              if (Mail::DKIM::AuthorDomainPolicy->UNIVERSAL::can("fetch")) {
                dbg("dkim: adsp: performing lookup on _adsp._domainkey.%s",
                    $author_domain);
                # get our Net::DNS::Resolver object
                my $res = $self->{main}->{resolver}->get_resolver;
                $practices = Mail::DKIM::AuthorDomainPolicy->fetch(
                               Protocol => "dns", Domain => $author_domain,
                               DnsResolver => $res);
              }
              1;
            } or do {
              # fetching/parsing adsp record may throw error, ignore such s.p.
              my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
              dbg("dkim: adsp: fetch or parse on domain %s failed: %s",
                  $author_domain, $eval_stat);
              undef $practices;
            };
          });
          if ($timer->timed_out()) {
            dbg("dkim: adsp lookup on domain %s timed out after %s seconds",
                $author_domain, $timeout);
          } elsif ($err) {
            chomp $err;
            dbg("dkim: adsp lookup on domain %s failed: %s",
                $author_domain, $err);
          } else {
            my $sp;  # ADSP: unknown / all / discardable
            ($sp) = $practices->policy  if $practices;
            if (!defined $sp || $sp eq '') {  # SERVFAIL or a timeout
              dbg("dkim: signing practices on %s unavailable", $author_domain);
              $adsp = 'U';
              $practices_as_string = 'dns: no result';
            } else {
              $adsp = $sp eq "unknown"      ? 'U'  # most common
                    : $sp eq "all"          ? 'A'
                    : $sp eq "discardable"  ? 'D'  # ADSP
                    : $sp eq "strict"       ? 'D'  # old style SSP
                    : uc($sp) eq "NXDOMAIN" ? 'N'
                                            : 'U';
              $practices_as_string = 'dns: ' . $sp;
            }
          }
        }
      }

      # is signing practices available?
      $pms->{dkim_adsp}->{$author_domain} = $adsp  if defined $adsp;

      dbg("dkim: adsp result: %s (%s), author domain '%s'",
          !defined($adsp) ? '-' : $adsp.'/'.$label{$adsp},
          $practices_as_string, $author_domain);
    }
  }
}

sub _check_dkim_whitelist {
  my ($self, $pms) = @_;

  $pms->{whitelist_checked} = 1;

  $self->_get_authors($pms)  if !$pms->{dkim_author_addresses};

  my $authors_str = join(", ", @{$pms->{dkim_author_addresses}});
  if ($authors_str eq '') {
    dbg("dkim: check_dkim_whitelist: could not find author address");
    return;
  }

  # collect whitelist entries matching the author from all lists
  my @acceptable_sdid_tuples;
  $self->_wlcheck_acceptable_signature($pms, \@acceptable_sdid_tuples,
                                       'def_whitelist_from_dkim');
  $self->_wlcheck_author_signature($pms, \@acceptable_sdid_tuples,
                                       'def_whitelist_auth');
  $self->_wlcheck_acceptable_signature($pms, \@acceptable_sdid_tuples,
                                       'whitelist_from_dkim');
  $self->_wlcheck_author_signature($pms, \@acceptable_sdid_tuples,
                                       'whitelist_auth');
  if (!@acceptable_sdid_tuples) {
    dbg("dkim: no wl entries match author %s, no need to verify sigs",
        $authors_str);
    return;
  }

  # if the message doesn't pass DKIM validation, it can't pass DKIM whitelist

  # trigger a DKIM check;
  # continue if one or more signatures are valid or we want the debug info
  return unless $self->check_dkim_valid($pms) || would_log("dbg","dkim");
  return unless $pms->{dkim_signatures_ready};

  # now do all the matching in one go, against all signatures in a message
  my($any_match_at_all, $any_match_by_wl_ref) =
    _wlcheck_list($self, $pms, \@acceptable_sdid_tuples);

  my(@valid,@fail);
  foreach my $wl (keys %$any_match_by_wl_ref) {
    my $match = $any_match_by_wl_ref->{$wl};
    if (defined $match) {
      $pms->{"dkim_match_in_$wl"} = 1  if $match;
      push(@{$match ? \@valid : \@fail}, "$wl/$match");
    }
  }
  if (@valid) {
    dbg("dkim: author %s, WHITELISTED by %s",
        $authors_str, join(", ",@valid));
  } elsif (@fail) {
    dbg("dkim: author %s, found in %s BUT IGNORED",
        $authors_str, join(", ",@fail));
  } else {
    dbg("dkim: author %s, not in any dkim whitelist", $authors_str);
  }
}

# check for verifier-acceptable signatures; an empty (or undefined) signing
# domain in a whitelist implies checking for an Author Domain Signature
#
sub _wlcheck_acceptable_signature {
  my ($self, $pms, $acceptable_sdid_tuples_ref, $wl) = @_;
  my $wl_ref = $pms->{conf}->{$wl};
  foreach my $author (@{$pms->{dkim_author_addresses}}) {
    foreach my $white_addr (keys %$wl_ref) {
      my $wl_addr_ref = $wl_ref->{$white_addr};
      my $re = qr/$wl_addr_ref->{re}/i;
    # dbg("dkim: WL %s %s, d: %s", $wl, $white_addr,
    #     join(", ", map { $_ eq '' ? "''" : $_ } @{$wl_addr_ref->{domain}}));
      if ($author =~ $re) {
        foreach my $sdid (@{$wl_addr_ref->{domain}}) {
          push(@$acceptable_sdid_tuples_ref, [$author,$sdid,$wl,$re]);
        }
      }
    }
  }
}

# use a traditional whitelist_from -style addrlist, the only acceptable DKIM
# signature is an Author Domain Signature.  Note: don't pre-parse and store
# domains; that's inefficient memory-wise and only saves one m//
#
sub _wlcheck_author_signature {
  my ($self, $pms, $acceptable_sdid_tuples_ref, $wl) = @_;
  my $wl_ref = $pms->{conf}->{$wl};
  foreach my $author (@{$pms->{dkim_author_addresses}}) {
    foreach my $white_addr (keys %$wl_ref) {
      my $re = $wl_ref->{$white_addr};
    # dbg("dkim: WL %s %s", $wl, $white_addr);
      if ($author =~ $re) {
        push(@$acceptable_sdid_tuples_ref, [$author,undef,$wl,$re]);
      }
    }
  }
}

sub _wlcheck_list {
  my ($self, $pms, $acceptable_sdid_tuples_ref) = @_;

  my %any_match_by_wl;
  my $any_match_at_all = 0;
  my $verifier = $pms->{dkim_verifier};
  my $minimum_key_bits = $pms->{conf}->{dkim_minimum_key_bits};

  # walk through all signatures present in a message
  foreach my $signature (@{$pms->{dkim_signatures}}) {
    # old versions of Mail::DKIM would give undef for an invalid signature
    next if !defined $signature;

    my $sig_result_supported = $signature->UNIVERSAL::can("result_detail");
    my($info, $valid, $expired, $key_size_weak);
    $valid =
      ($sig_result_supported ? $signature : $verifier)->result eq 'pass';
    $info = $valid ? 'VALID' : 'FAILED';
    if ($valid && $signature->UNIVERSAL::can("check_expiration")) {
      $expired = !$signature->check_expiration;
      $info .= ' EXPIRED'  if $expired;
    }
    if ($valid && !$expired && $minimum_key_bits) {
      my $key_size = $signature->{_spamassassin_key_size};
      if ($key_size && $key_size < $minimum_key_bits) {
        $info .= " WEAK($key_size)"; $key_size_weak = 1;
      }
    }

    my $sdid = $signature->domain;
    $sdid = lc $sdid  if defined $sdid;

    my %tried_authors;
    foreach my $entry (@$acceptable_sdid_tuples_ref) {
      my($author, $acceptable_sdid, $wl, $re) = @$entry;
      # $re and $wl are here for logging purposes only, $re already checked.
      # The $acceptable_sdid is a verifier-acceptable signing domain
      # identifier (to be matched against a 'd' tag in signatures).
      # When $acceptable_sdid is undef or an empty string it implies
      # a check for Author Domain Signature.

      local $1;
      my $author_domain = $author !~ /\@([^\@]+)\z/s ? '' : lc $1;
      $tried_authors{$author} = 1;  # for logging purposes

      my $matches = 0;
      if (!defined $sdid) {
        # don't bother, invalid signature with a missing 'd' tag

      } elsif (!defined $acceptable_sdid || $acceptable_sdid eq '') {
        # An "Author Domain Signature" (sometimes called a first-party
        # signature) is a Valid Signature in which the domain name of the
        # DKIM signing entity, i.e., the d= tag in the DKIM-Signature header
        # field, is the same as the domain name in the Author Address.
        # Following [RFC5321], domain name comparisons are case insensitive.

        # checking for Author Domain Signature
        $matches = 1  if $sdid eq $author_domain;

      } else {  # checking for verifier-acceptable signature
        # The second argument to a 'whitelist_from_dkim' option is now (since
        # version 3.3.0) supposed to be a signing domain (SDID), no longer an
        # identity (AUID). Nevertheless, be prepared to accept the full e-mail
        # address there for compatibility, and just ignore its local-part.

        $acceptable_sdid = $1  if $acceptable_sdid =~ /\@([^\@]*)\z/s;
        if ($acceptable_sdid =~ s/^\*?\.//s) {
          $matches = 1  if $sdid =~ /\.\Q$acceptable_sdid\E\z/si;
        } else {
          $matches = 1  if $sdid eq lc $acceptable_sdid;
        }
      }
      if ($matches) {
        if (would_log("dbg","dkim")) {
          if ($sdid eq $author_domain) {
            dbg("dkim: %s author domain signature by %s, MATCHES %s %s",
                $info, $sdid, $wl, $re);
          } else {
            dbg("dkim: %s third-party signature by %s, author domain %s, ".
                "MATCHES %s %s", $info, $sdid, $author_domain, $wl, $re);
          }
        }
        # a defined value indicates at least a match, not necessarily valid
        # (this complication servers to preserve logging compatibility)
        $any_match_by_wl{$wl} = ''  if !exists $any_match_by_wl{$wl};
      }
      # only valid signature can cause whitelisting
      $matches = 0  if !$valid || $expired || $key_size_weak;

      if ($matches) {
        $any_match_at_all = 1;
        $any_match_by_wl{$wl} = $sdid;  # value used for debug logging
      }
    }
    dbg("dkim: %s signature by %s, author %s, no valid matches",
        $info,  defined $sdid ? $sdid : '(undef)',
        join(", ", keys %tried_authors))  if !$any_match_at_all;
  }
  return ($any_match_at_all, \%any_match_by_wl);
}

1;