File Coverage

blib/lib/Mail/STS/Policy.pm
Criterion Covered Total %
statement 3 60 5.0
branch 0 32 0.0
condition 0 3 0.0
subroutine 1 7 14.2
pod 5 5 100.0
total 9 107 8.4


line stmt bran cond sub pod time code
1             package Mail::STS::Policy;
2              
3 1     1   8 use Moose;
  1         2  
  1         8  
4              
5             our $VERSION = '0.04'; # VERSION
6             # ABSTRACT: class to parse and generate RFC8461 policies
7              
8              
9             has 'version' => (
10             is => 'rw',
11             isa => 'Str',
12             default => 'STSv1',
13             );
14              
15             has 'mode' => (
16             is => 'rw',
17             isa => 'Str',
18             default => 'none',
19             );
20              
21             has 'max_age' => (
22             is => 'rw',
23             isa => 'Maybe[Int]',
24             );
25              
26             has 'mx' => (
27             is => 'ro',
28             isa => 'ArrayRef[Str]',
29             default => sub { [] },
30             traits => ['Array'],
31             handles => {
32             'add_mx' => 'push',
33             'clear_mx' => 'clear',
34             },
35             );
36              
37              
38             sub new_from_string {
39 0     0 1   my ($class, $string) = @_;
40 0           my $self = $class->new;
41 0           $self->parse($string);
42 0           return $self;
43             }
44              
45              
46             sub parse {
47 0     0 1   my ($self, $string) = @_;
48 0           my @lines = split(/[\r\n]+/, $string);
49 0           my $ln = 0;
50 0           $self->clear_mx;
51              
52 0           while( my $line = shift(@lines) ) {
53 0           $ln++;
54 0           $line =~ s/[\r\n]*$//;
55 0           my ($key, $value) = split(/\s*:\s+/, $line, 2);
56 0 0 0       unless(defined $key && defined $value) {
57 0           die("invalid syntax on line ${ln}");
58             }
59 0 0         if($key eq 'version') {
    0          
    0          
    0          
60 0 0         unless($value eq 'STSv1') {
61 0           die('only STSv1 version of policy is supported');
62             }
63 0           $self->version($value);
64 0           next;
65             } elsif($key eq 'mode') {
66 0 0         unless($value =~ /^(testing|enforce|none)$/) {
67 0           die("unsupported mode on line ${ln}");
68             }
69 0           $self->mode($value);
70 0           next;
71             } elsif($key eq 'max_age') {
72 0 0         unless($value =~ /^(\d+)$/) {
73 0           die("max_age must be an integer on line ${ln}");
74             }
75 0           $self->max_age(int $value);
76 0           next;
77             } elsif($key eq 'mx') {
78 0 0         unless($value =~ /^(\*\.)?[0-9a-zA-Z\-]+(\.[0-9a-zA-Z\-]+)*$/) {
79 0           die("invalid mx entry on line ${ln}");
80             }
81 0           $self->add_mx($value);
82 0           next;
83             }
84 0           die("unknown key ${key} in policy on line ${line}");
85             }
86             }
87              
88              
89             sub as_hash {
90 0     0 1   my $self = shift;
91             return {
92 0           'version' => $self->version,
93             'mode' => $self->mode,
94             'max_age' => $self->max_age,
95             'mx' => $self->mx,
96             };
97             }
98              
99              
100             sub as_string {
101 0     0 1   my $self = shift;
102 0           my $hash = $self->as_hash;
103             return join('', map {
104 0           _sprint_key_value($_, $hash->{$_});
  0            
105             } 'version', 'mode', 'max_age', 'mx');
106             }
107              
108             sub _sprint_key_value {
109 0     0     my ($key, $value) = @_;
110 0 0         return '' unless defined $value;
111 0 0         unless(ref $value) {
112 0           return("${key}: ${value}\n");
113             }
114 0 0         if(ref($value) eq 'ARRAY') {
115 0           return join('', map { "${key}: $_\n" } @$value);
  0            
116             }
117 0           die('invalid data type for policy');
118             }
119              
120              
121             sub match_mx {
122 0     0 1   my ($self, $host) = @_;
123 0           foreach my $mx (@{$self->mx}) {
  0            
124 0 0         if($host eq $mx) {
125 0           return 1;
126             }
127 0 0         if(my ($domain) = $mx =~ /^\*\.(.+)$/) {
128 0 0         return 1 if $host eq $domain;
129 0           my $suffix = ".${domain}";
130 0           my $suffix_len = length($suffix);
131 0 0         return 1 if substr($host, -$suffix_len) eq $suffix;
132             }
133             }
134 0           return 0;
135             }
136              
137             1;
138              
139             __END__
140              
141             =pod
142              
143             =encoding UTF-8
144              
145             =head1 NAME
146              
147             Mail::STS::Policy - class to parse and generate RFC8461 policies
148              
149             =head1 VERSION
150              
151             version 0.04
152              
153             =head1 SYNOPSIS
154              
155             # generate a policy
156             my $policy = Mail::STS::Policy->new(
157             mode => 'enforce',
158             max_age => 604800,
159             mx => [ 'mail.example.com' ],
160             );
161             # setters
162             $policy->mode('testing');
163             $policy->add_mx('mail.example.com');
164             print $policy->as_string;
165              
166             # parse existing policy
167             my $policy = Mail::STS::Policy->new_from_string($string);
168             # access values
169             $policy->mode;
170             # 'enforce'
171             $policy->mx;
172             # [ 'mail.example.com' ]
173              
174             # check if a host is in there
175             $policy->match_mx('mail.blablub.de') or die;
176              
177             =head1 ATTRIBUTES
178              
179             =head2 version (default: 'STSv1')
180              
181             Currently always version 'STSv1'.
182              
183             =head2 mode (default: 'none')
184              
185             Get/set mode of policy.
186              
187             =head2 max_age (default: undef)
188              
189             Get/set max_age for policy caching.
190              
191             =head2 mx (default: [])
192              
193             Array reference to array of mx hosts.
194              
195             =head1 METHODS
196              
197             =head2 new_from_string($string)
198              
199             Constructor for creating a new policy object from a policy string.
200              
201             Internally creates objects by calling new() and execute parse() on it.
202              
203             =head2 parse($string)
204              
205             Parses values from $string to values in the object overwriting
206             and clearing all existing values.
207              
208             Will die() on parsing error.
209              
210             =head2 as_hash
211              
212             Returns a hash reference containing policy data.
213              
214             $policy->as_hash
215             # {
216             # 'version' => 'STSv1',
217             # 'mode' => 'enforce',
218             # 'max_age' => 3600,
219             # 'mx' => [ 'mx.example.com', ... ],
220             # }
221              
222             =head2 as_string
223              
224             Outputs the object as a RFC8461 policy document.
225              
226             =head2 match_mx($host)
227              
228             Returns if the policy matches $host.
229              
230             $policy->match_mx('mail.example.com') or die;
231              
232             =head1 AUTHOR
233              
234             Markus Benning <ich@markusbenning.de>
235              
236             =head1 COPYRIGHT AND LICENSE
237              
238             This software is copyright (c) 2018 by Markus Benning <ich@markusbenning.de>.
239              
240             This is free software; you can redistribute it and/or modify it under
241             the same terms as the Perl 5 programming language system itself.
242              
243             =cut