File Coverage

lib/Mail/DMARC/Policy.pm
Criterion Covered Total %
statement 115 115 100.0
branch 87 100 87.0
condition 12 18 66.6
subroutine 23 23 100.0
pod 14 19 73.6
total 251 275 91.2


line stmt bran cond sub pod time code
1             package Mail::DMARC::Policy;
2 12     12   273165 use strict;
  12         63  
  12         388  
3 12     12   64 use warnings;
  12         36  
  12         520  
4              
5             our $VERSION = '1.20230215';
6              
7 12     12   74 use Carp;
  12         36  
  12         747  
8              
9 12     12   4887 use Mail::DMARC::Report::URI;
  12         30  
  12         22400  
10              
11             sub new {
12 82     82 1 19155 my ( $class, @args ) = @_;
13 82 50       234 my $package = ref $class ? ref $class : $class;
14 82         167 my $self = bless {}, $package;
15              
16 82 100       300 return $self if 0 == scalar @args; # no args, empty pol
17 71 100       177 if (1 == @args) { # a string
18 55         167 my $policy = $self->parse( $args[0] );
19 55         191 $self->is_valid($policy);
20 53         319 return $policy;
21             }
22              
23 16 50       57 croak "invalid arguments" if @args % 2 != 0;
24 16         81 my $policy = {@args};
25 16         31 bless $policy, $package;
26 16 50       48 croak "invalid policy" if !$self->is_valid($policy);
27 14         99 return bless $policy, $package;
28             }
29              
30             sub parse {
31 75     75 1 2746 my ( $self, $str, @junk ) = @_;
32 75 50       186 croak "invalid parse request" if 0 != scalar @junk;
33 75         137 my $cleaned = $str;
34 75         398 $cleaned =~ s/\s//g; # remove whitespace
35 75         185 $cleaned =~ s/\\;/;/g; # replace \; with ;
36 75         139 $cleaned =~ s/;;/;/g; # replace ;; with ;
37 75         162 $cleaned =~ s/;0;/;/g; # replace ;0; with ;
38 75 100       229 chop $cleaned if ';' eq substr $cleaned, -1, 1; # remove a trailing ;
39 75         300 my @tag_vals = split /;/, $cleaned;
40 75         112 my %policy;
41 75         106 my $warned = 0;
42 75         148 foreach my $tv (@tag_vals) {
43 482         1619 my ($tag, $value) = split /=|:|-/, $tv, 2;
44 482 100 33     1940 if ( !defined $tag || !defined $value || $value eq '') {
      66        
45 2 50       4 if (!$warned) {
46             #warn "tv: $tv\n";
47 2         98 warn "invalid DMARC record, please post this message to\n" .
48             "\thttps://github.com/msimerson/mail-dmarc/issues/39\n" .
49             "\t$str\n";
50             }
51 2         7 $warned++;
52 2         5 next;
53             }
54 480         1057 $policy{lc $tag} = $value;
55             }
56 75         346 return bless \%policy, ref $self; # inherited defaults + overrides
57             }
58              
59             sub apply_defaults {
60 7     7 1 439 my $self = shift;
61              
62 7 100       46 $self->adkim('r') if !defined $self->adkim;
63 7 100       24 $self->aspf('r') if !defined $self->aspf;
64 7 50       29 $self->fo(0) if !defined $self->fo;
65 7 50       31 $self->ri(86400) if !defined $self->ri;
66 7 50       25 $self->rf('afrf') if !defined $self->rf;
67              
68             # pct # default is 100%, but 100% -vs- not defined is different
69 7         31 return 1;
70             }
71              
72             sub v {
73 6 100   6 1 1405 return $_[0]->{v} if 1 == scalar @_;
74 4 100       85 croak "unsupported DMARC version" if 'DMARC1' ne uc $_[1];
75 3         12 return $_[0]->{v} = $_[1];
76             }
77              
78             sub p {
79 37 100   37 1 3393 return $_[0]->{p} if 1 == scalar @_;
80 9 100       19 croak "invalid p" if !$_[0]->is_valid_p( $_[1] );
81 6         25 return $_[0]->{p} = $_[1];
82             }
83              
84             sub sp {
85 24 100   24 1 2955 return $_[0]->{sp} if 1 == scalar @_;
86 9 100       23 croak "invalid sp ($_[1])" if !$_[0]->is_valid_p( $_[1] );
87 6         26 return $_[0]->{sp} = $_[1];
88             }
89              
90             sub adkim {
91 42 100   42 1 2082 return $_[0]->{adkim} if 1 == scalar @_;
92 12 100       22 croak "invalid adkim" if 0 == grep {/^\Q$_[1]\E$/ix} qw/ r s /;
  24         332  
93 10         35 return $_[0]->{adkim} = $_[1];
94             }
95              
96             sub aspf {
97 46 100   46 1 2171 return $_[0]->{aspf} if 1 == scalar @_;
98 13 100       24 croak "invalid aspf" if 0 == grep {/^\Q$_[1]\E$/ix} qw/ r s /;
  26         473  
99 11         50 return $_[0]->{aspf} = $_[1];
100             }
101              
102             sub fo {
103 26 100   26 1 3829 return $_[0]->{fo} if 1 == scalar @_;
104 19 100       411 croak "invalid fo: $_[1]" if $_[1] !~ /^[01ds](:[01ds])*$/ix;
105 15         51 return $_[0]->{fo} = $_[1];
106             }
107              
108             sub rua {
109 36 100   36 1 1472 return $_[0]->{rua} if 1 == scalar @_;
110 17 100       67 croak "invalid rua" if !$_[0]->is_valid_uri_list( $_[1] );
111 15         99 return $_[0]->{rua} = $_[1];
112             }
113              
114             sub ruf {
115 2 50   2 1 352 return $_[0]->{ruf} if 1 == scalar @_;
116 2 100       5 croak "invalid rua" if !$_[0]->is_valid_uri_list( $_[1] );
117 1         6 return $_[0]->{ruf} = $_[1];
118             }
119              
120             sub rf {
121 21 100   21 1 2400 return $_[0]->{rf} if 1 == scalar @_;
122 14         73 foreach my $f ( split /,/, $_[1] ) {
123 14 100       51 croak "invalid format: $f" if !$_[0]->is_valid_rf($f);
124             }
125 11         45 return $_[0]->{rf} = $_[1];
126             }
127              
128             sub ri {
129 26 100   26 1 2347 return $_[0]->{ri} if 1 == scalar @_;
130 14 100       219 croak "not numeric ($_[1])!" if $_[1] =~ /\D/;
131 12 50       34 croak "not an integer!" if $_[1] != int $_[1];
132 12 100 66     146 croak "out of range" if ( $_[1] < 0 || $_[1] > 4294967295 );
133 11         41 return $_[0]->{ri} = $_[1];
134             }
135              
136             sub pct {
137 20 100   20 1 3568 return $_[0]->{pct} if 1 == scalar @_;
138 11 100       406 croak "not numeric ($_[1])!" if $_[1] =~ /\D/;
139 6 50       12 croak "not an integer!" if $_[1] != int $_[1];
140 6 100 66     95 croak "out of range" if $_[1] < 0 || $_[1] > 100;
141 5         20 return $_[0]->{pct} = $_[1];
142             }
143              
144             sub domain {
145 62 100   62 0 442 return $_[0]->{domain} if 1 == scalar @_;
146 7         72 return $_[0]->{domain} = $_[1];
147             }
148              
149             sub is_valid_rf {
150 19     19 0 1241 my ( $self, $f ) = @_;
151 19 100       36 return ( grep {/^\Q$f\E$/i} qw/ iodef afrf / ) ? 1 : 0;
  38         569  
152             }
153              
154             sub is_valid_p {
155 140     140 0 6561 my ( $self, $p ) = @_;
156 140 50       253 croak "unspecified p" if !defined $p;
157 140 100       221 return ( grep {/^\Q$p\E$/i} qw/ none reject quarantine / ) ? 1 : 0;
  420         2700  
158             }
159              
160             sub is_valid_uri_list {
161 21     21 0 59 my ( $self, $str ) = @_;
162 21   66     164 $self->{uri} ||= Mail::DMARC::Report::URI->new;
163 21         78 my $uris = $self->{uri}->parse($str);
164 21         341 return scalar @$uris;
165             }
166              
167             sub is_valid {
168 109     109 0 284 my ( $self, $obj ) = @_;
169 109 100       246 $obj = $self if !$obj;
170 109 100       327 croak "missing version specifier" if !$obj->{v};
171 108 50       270 croak "invalid version" if 'DMARC1' ne uc $obj->{v};
172 108 100       213 if ( !$obj->{p} ) {
173 4 100 100     14 if ( $obj->{rua} && $self->is_valid_uri_list( $obj->{rua} ) ) {
174 1         3 $obj->{p} = 'none';
175             }
176             else {
177 3         298 croak "missing policy action (p=)";
178             }
179             }
180 105 100       238 croak "invalid policy action" if !$self->is_valid_p( $obj->{p} );
181              
182             # everything else is optional
183 104         273 return 1;
184             }
185              
186             1;
187              
188             __END__