File Coverage

lib/Mail/DMARC/Report/Aggregate.pm
Criterion Covered Total %
statement 111 123 90.2
branch 33 46 71.7
condition 12 15 80.0
subroutine 16 17 94.1
pod 0 10 0.0
total 172 211 81.5


line stmt bran cond sub pod time code
1             package Mail::DMARC::Report::Aggregate;
2 12     12   60975 use strict;
  12         33  
  12         368  
3 12     12   88 use warnings;
  12         26  
  12         507  
4              
5             our $VERSION = '1.20230215';
6              
7 12     12   82 use Carp;
  12         25  
  12         761  
8 12     12   689 use Data::Dumper;
  12         6166  
  12         882  
9 12     12   7804 use XML::LibXML;
  12         384879  
  12         82  
10              
11 12     12   2832 use parent 'Mail::DMARC::Base';
  12         600  
  12         79  
12 12     12   6316 use Mail::DMARC::Report::Aggregate::Metadata;
  12         131  
  12         17287  
13              
14             sub metadata {
15 174     174 0 1378 my $self = shift;
16 174 100       935 return $self->{metadata} if ref $self->{metadata};
17 19         165 return $self->{metadata} = Mail::DMARC::Report::Aggregate::Metadata->new;
18             }
19              
20             sub policy_published {
21 65     65 0 4426 my ( $self, $policy ) = @_;
22 65 100       331 return $self->{policy_published} if ! $policy;
23 16 50       66 croak "not a policy object!" if 'Mail::DMARC::Policy' ne ref $policy;
24 16         72 return $self->{policy_published} = $policy;
25             }
26              
27             sub record { ## no critic (Ambiguous)
28 35     35 0 1120 my ($self, $record, @extra) = @_;
29 35 100       99 if ( !$record) {
30 19   100     203 return $self->{record} || [];
31             }
32              
33 16 50       112 if (@extra) { croak "invalid args"; }
  0         0  
34              
35 16 50       71 if ('Mail::DMARC::Report::Aggregate::Record' ne ref $record) {
36 0         0 croak "not a record object";
37             }
38              
39 16   100     93 $self->{record} ||= [];
40              
41 16         26 push @{ $self->{record} }, $record;
  16         60  
42              
43 16         52 return $self->{record};
44             };
45              
46             sub dump_report {
47 0     0 0 0 my $self = shift;
48 0         0 carp Dumper( $self->{metadata}, $self->{policy_published}, $self->{record} );
49 0         0 return;
50             }
51              
52             sub as_xml {
53 5     5 0 14 my $self = shift;
54 5         15 my $meta = $self->metadata->as_xml;
55 5         22 my $pubp = $self->get_policy_published_as_xml;
56 5         18 my $reco = $self->get_record_as_xml;
57              
58             return <<"EO_XML"
59            
60            
61             \t1.0
62             $meta
63             $pubp
64             $reco
65             EO_XML
66 5         41 ;
67             }
68              
69             sub get_record_as_xml {
70 6     6 0 11 my $self = shift;
71              
72 6         15 my $rec_xml = '';
73 6         8 foreach my $rec ( @{ $self->{record} } ) {
  6         17  
74 8         13 $rec_xml .= "\t\n";
75 8 50       27 my $ip = $rec->{row}{source_ip} or croak "no source IP!?";
76 8 50       20 my $count = $rec->{row}{count} or croak "no count!?";
77 8 50       21 $rec->{row}{policy_evaluated}{disposition} or croak "no disposition?";
78 8         111 $ip = XML::LibXML::Text->new( $ip )->toString();
79 8         74 $count = XML::LibXML::Text->new( $count )->toString();
80 8         57 $rec_xml
81             .="\t\t\n"
82             . "\t\t\t$ip\n"
83             . "\t\t\t$count\n"
84             . $self->get_policy_evaluated_as_xml( $rec )
85             . "\t\t\n"
86             . $self->get_identifiers_as_xml($rec)
87             . $self->get_auth_results_as_xml($rec);
88 8         33 $rec_xml .= "\t\n";
89             }
90 6         20 return $rec_xml;
91             }
92              
93             sub get_identifiers_as_xml {
94 8     8 0 19 my ( $self, $rec ) = @_;
95 8         12 my $id = "\t\t\n";
96 8         16 foreach my $f (qw/ envelope_to envelope_from header_from /) {
97 24 100       70 if ( $f eq 'header_from' ) { # min occurs = 1
    100          
    50          
98 8 50       25 croak "missing header_from!" if ! $rec->{identifiers}{$f};
99             }
100             elsif ( $f eq 'envelope_from') { # min occurs = 1
101 8 50       23 $rec->{identifiers}{$f} = '' if ! $rec->{identifiers}{$f};
102             }
103             elsif ( $f eq 'envelope_to' ) { # min occurs = 0
104 8 100       24 next if ! $rec->{identifiers}{$f};
105             };
106              
107 20         149 my $val = XML::LibXML::Text->new( $rec->{identifiers}{$f} )->toString();
108 20         105 $id .= "\t\t\t<$f>$val\n";
109             }
110 8         19 $id .= "\t\t\n";
111 8         35 return $id;
112             }
113              
114             sub get_auth_results_as_xml {
115 8     8 0 27 my ( $self, $rec ) = @_;
116 8         16 my $ar = "\t\t\n";
117              
118 8         12 foreach my $dkim_sig ( @{ $rec->{auth_results}{dkim} } ) {
  8         22  
119 4         8 $ar .= "\t\t\t\n";
120 4         10 foreach my $g (qw/ domain selector result human_result /) {
121 16 50       37 next if !defined $dkim_sig->{$g};
122 16         192 my $val = XML::LibXML::Text->new( $dkim_sig->{$g} )->toString();
123 16         78 $ar .= "\t\t\t\t<$g>$val\n";
124             }
125 4         12 $ar .= "\t\t\t\n";
126             }
127              
128 8         14 foreach my $spf ( @{ $rec->{auth_results}{spf} } ) {
  8         21  
129 8         14 $ar .= "\t\t\t\n";
130 8         17 foreach my $g (qw/ domain scope result /) {
131 24 50       50 next if !defined $spf->{$g};
132 24         158 my $val = XML::LibXML::Text->new( $spf->{$g} )->toString();
133 24         140 $ar .= "\t\t\t\t<$g>$val\n";
134             }
135 8         19 $ar .= "\t\t\t\n";
136             }
137              
138 8         15 $ar .= "\t\t\n";
139 8         24 return $ar;
140             }
141              
142             sub get_policy_published_as_xml {
143 6     6 0 12 my $self = shift;
144 6 50       14 my $pp = $self->policy_published or return '';
145 6         20 my $xml = "\t\n\t\t$pp->{domain}\n";
146 6         16 foreach my $f (qw/ adkim aspf p sp pct fo /) {
147 36         59 my $v = $pp->{$f};
148             # Set some default values for missing optional fields if necessary
149 36 100 66     85 if ( $f eq 'sp' && !defined $v ) {
150 6         10 $v = $pp->{'p'};
151             }
152 36 100 66     87 if ( $f eq 'pct' && !defined $v ) {
153 6         13 $v = '100';
154             }
155 36 100 100     77 if ( $f eq 'fo' && !defined $v ) {
156 4         5 $v = '0';
157             }
158 36 100       61 next if !defined $v;
159 24         195 $v = XML::LibXML::Text->new( $v )->toString();
160 24         124 $xml .= "\t\t<$f>$v\n";
161             }
162 6         17 $xml .= "\t";
163 6         25 return $xml;
164             }
165              
166             sub get_policy_evaluated_as_xml {
167 8     8 0 19 my ( $self, $rec ) = @_;
168 8         14 my $pe = "\t\t\t\n";
169              
170 8         56 foreach my $f (qw/ disposition dkim spf /) {
171 24         202 my $val = XML::LibXML::Text->new( $rec->{row}{policy_evaluated}{$f} )->toString();
172 24         119 $pe .= "\t\t\t\t<$f>$val\n";
173             }
174              
175 8         34 my $reasons = $rec->{row}{policy_evaluated}{reason};
176 8 50 50     71 if ( $reasons && scalar @$reasons ) {
177 0         0 foreach my $reason ( @$reasons ) {
178 0         0 my $typeval = XML::LibXML::Text->new( $reason->{type} )->toString();
179 0         0 my $commentval = XML::LibXML::Text->new( $reason->{comment} )->toString();
180 0         0 $pe .= "\t\t\t\t\n";
181 0         0 $pe .= "\t\t\t\t\t$typeval\n";
182 0         0 $pe .= "\t\t\t\t\t$commentval\n";
183 0         0 $pe .= "\t\t\t\t\n";
184             }
185             };
186 8         23 $pe .= "\t\t\t\n";
187 8         39 return $pe;
188             }
189              
190             1;
191              
192             __END__