| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | package Net::DNS::SPF::Expander; | 
| 2 |  |  |  |  |  |  | $Net::DNS::SPF::Expander::VERSION = '0.019'; | 
| 3 | 4 |  |  | 4 |  | 6827 | use Moose; | 
|  | 4 |  |  |  |  | 1591806 |  | 
|  | 4 |  |  |  |  | 29 |  | 
| 4 | 4 |  |  | 4 |  | 27841 | use IO::All -utf8; | 
|  | 4 |  |  |  |  | 33921 |  | 
|  | 4 |  |  |  |  | 34 |  | 
| 5 | 4 |  |  | 4 |  | 2067 | use Net::DNS::ZoneFile; | 
|  | 4 |  |  |  |  | 123253 |  | 
|  | 4 |  |  |  |  | 206 |  | 
| 6 | 4 |  |  | 4 |  | 1756 | use Net::DNS::Resolver; | 
|  | 4 |  |  |  |  | 166388 |  | 
|  | 4 |  |  |  |  | 131 |  | 
| 7 | 4 |  |  | 4 |  | 1549 | use MooseX::Types::IO::All 'IO_All'; | 
|  | 4 |  |  |  |  | 290939 |  | 
|  | 4 |  |  |  |  | 22 |  | 
| 8 | 4 |  |  | 4 |  | 5401 | use List::AllUtils qw(sum any part first uniq); | 
|  | 4 |  |  |  |  | 31001 |  | 
|  | 4 |  |  |  |  | 342 |  | 
| 9 | 4 |  |  | 4 |  | 27 | use Scalar::Util (); | 
|  | 4 |  |  |  |  | 7 |  | 
|  | 4 |  |  |  |  | 12395 |  | 
| 10 |  |  |  |  |  |  |  | 
| 11 |  |  |  |  |  |  | with 'MooseX::Getopt'; | 
| 12 |  |  |  |  |  |  |  | 
| 13 |  |  |  |  |  |  | # ABSTRACT: Expands DNS SPF records, so you don't have to. | 
| 14 |  |  |  |  |  |  | # The problem is that you only get 10 per SPF records, | 
| 15 |  |  |  |  |  |  | # and recursions count against you. Your record won't | 
| 16 |  |  |  |  |  |  | # validate. | 
| 17 |  |  |  |  |  |  |  | 
| 18 |  |  |  |  |  |  | =head1 NAME | 
| 19 |  |  |  |  |  |  |  | 
| 20 |  |  |  |  |  |  | Net::DNS::SPF::Expander | 
| 21 |  |  |  |  |  |  |  | 
| 22 |  |  |  |  |  |  | =head1 DESCRIPTION | 
| 23 |  |  |  |  |  |  |  | 
| 24 |  |  |  |  |  |  | This module expands DNS SPF records, so you don't have to. | 
| 25 |  |  |  |  |  |  | The problem is that you only get 10 per SPF record, | 
| 26 |  |  |  |  |  |  | and recursions count against you. Your record won't | 
| 27 |  |  |  |  |  |  | validate. | 
| 28 |  |  |  |  |  |  |  | 
| 29 |  |  |  |  |  |  | Let's say you start with this as an SPF record: | 
| 30 |  |  |  |  |  |  |  | 
| 31 |  |  |  |  |  |  | @   TXT   "v=spf1 include:_spf.google.com include:sendgrid.net a:hq1.campusexplorer.com a:hq2.campusexplorer.com a:mail2.campusexplorer.com ~all" | 
| 32 |  |  |  |  |  |  |  | 
| 33 |  |  |  |  |  |  | You go to http://www.kitterman.com/spf/validate.html and check this record. | 
| 34 |  |  |  |  |  |  | It passes validation. But later you come back and add salesforce, so that you | 
| 35 |  |  |  |  |  |  | now have: | 
| 36 |  |  |  |  |  |  |  | 
| 37 |  |  |  |  |  |  | @   TXT   "v=spf1 include:_spf.google.com include:sendgrid.net include:salesforce.com a:hq1.campusexplorer.com a:hq2.campusexplorer.com a:mail2.campusexplorer.com ~all" | 
| 38 |  |  |  |  |  |  |  | 
| 39 |  |  |  |  |  |  | And now your record fails validation. | 
| 40 |  |  |  |  |  |  |  | 
| 41 |  |  |  |  |  |  | _spf.google.com takes 3 lookups. | 
| 42 |  |  |  |  |  |  | _spf1.google.com | 
| 43 |  |  |  |  |  |  | _spf2.google.com | 
| 44 |  |  |  |  |  |  | _spf3.google.com | 
| 45 |  |  |  |  |  |  | sendgrid.net takes 1 lookup. | 
| 46 |  |  |  |  |  |  | _sendgrid.biz | 
| 47 |  |  |  |  |  |  | hq1 takes 1 lookup. | 
| 48 |  |  |  |  |  |  | hq2 takes 1 lookup. | 
| 49 |  |  |  |  |  |  | mail2 takes 1 lookup. | 
| 50 |  |  |  |  |  |  |  | 
| 51 |  |  |  |  |  |  | Salesforce adds: | 
| 52 |  |  |  |  |  |  |  | 
| 53 |  |  |  |  |  |  | _spf.google.com (3 you already did) | 
| 54 |  |  |  |  |  |  | _spf1.google.com | 
| 55 |  |  |  |  |  |  | _spf2.google.com | 
| 56 |  |  |  |  |  |  | _spf3.google.com | 
| 57 |  |  |  |  |  |  | mx takes 4 lookups. | 
| 58 |  |  |  |  |  |  | salesforce.com.s8a1.psmtp.com. | 
| 59 |  |  |  |  |  |  | salesforce.com.s8a2.psmtp.com. | 
| 60 |  |  |  |  |  |  | salesforce.com.s8b1.psmtp.com. | 
| 61 |  |  |  |  |  |  | salesforce.com.s8b2.psmtp.com. | 
| 62 |  |  |  |  |  |  |  | 
| 63 |  |  |  |  |  |  | So now instead of 7 you have 14. The common advice is to | 
| 64 |  |  |  |  |  |  | expand them, and that is a tedious process. It's especially | 
| 65 |  |  |  |  |  |  | tedious when, say, salesforce changes their mx record. | 
| 66 |  |  |  |  |  |  |  | 
| 67 |  |  |  |  |  |  | So this module and the accompanying script attempt | 
| 68 |  |  |  |  |  |  | to automate this process for you. | 
| 69 |  |  |  |  |  |  |  | 
| 70 |  |  |  |  |  |  | =head1 SYNOPSIS | 
| 71 |  |  |  |  |  |  |  | 
| 72 |  |  |  |  |  |  | Using the script: | 
| 73 |  |  |  |  |  |  |  | 
| 74 |  |  |  |  |  |  | myhost:~/ $ dns-dpf-expander --input_file zone.db | 
| 75 |  |  |  |  |  |  | myhost:~/ $ ls | 
| 76 |  |  |  |  |  |  | zone.db   zone.db.new   zone.db.bak | 
| 77 |  |  |  |  |  |  |  | 
| 78 |  |  |  |  |  |  | Using the module: | 
| 79 |  |  |  |  |  |  |  | 
| 80 |  |  |  |  |  |  | { | 
| 81 |  |  |  |  |  |  | package MyDNSExpander; | 
| 82 |  |  |  |  |  |  |  | 
| 83 |  |  |  |  |  |  | use Net::DNS::SPF::Expander; | 
| 84 |  |  |  |  |  |  |  | 
| 85 |  |  |  |  |  |  | my $input_file = '/home/me/project/etc/zone.db'; | 
| 86 |  |  |  |  |  |  | my $expander = Net::DNS::SPF::Expander->new( | 
| 87 |  |  |  |  |  |  | input_file => $input_file | 
| 88 |  |  |  |  |  |  | ); | 
| 89 |  |  |  |  |  |  |  | 
| 90 |  |  |  |  |  |  | my $string = $expander->write; | 
| 91 |  |  |  |  |  |  |  | 
| 92 |  |  |  |  |  |  | 1; | 
| 93 |  |  |  |  |  |  | } | 
| 94 |  |  |  |  |  |  |  | 
| 95 |  |  |  |  |  |  | =head1 CONFIGURABLE ATTRIBUTES | 
| 96 |  |  |  |  |  |  |  | 
| 97 |  |  |  |  |  |  | =head2 input_file | 
| 98 |  |  |  |  |  |  |  | 
| 99 |  |  |  |  |  |  | This is the path and name of the zonefile whose SPF records you want | 
| 100 |  |  |  |  |  |  | to expand. It must be a valid L<Net::DNS::Zonefile> zonefile. | 
| 101 |  |  |  |  |  |  |  | 
| 102 |  |  |  |  |  |  | =cut | 
| 103 |  |  |  |  |  |  |  | 
| 104 |  |  |  |  |  |  | has 'input_file' => ( | 
| 105 |  |  |  |  |  |  | is       => 'ro', | 
| 106 |  |  |  |  |  |  | isa      => 'Str', | 
| 107 |  |  |  |  |  |  | required => 1, | 
| 108 |  |  |  |  |  |  | ); | 
| 109 |  |  |  |  |  |  |  | 
| 110 |  |  |  |  |  |  | =head2 output_file | 
| 111 |  |  |  |  |  |  |  | 
| 112 |  |  |  |  |  |  | The path and name of the output file. By default, we tack ".new" | 
| 113 |  |  |  |  |  |  | onto the end of the original filename. | 
| 114 |  |  |  |  |  |  |  | 
| 115 |  |  |  |  |  |  | =cut | 
| 116 |  |  |  |  |  |  |  | 
| 117 |  |  |  |  |  |  | has 'output_file' => ( | 
| 118 |  |  |  |  |  |  | is         => 'ro', | 
| 119 |  |  |  |  |  |  | isa        => 'Str', | 
| 120 |  |  |  |  |  |  | lazy       => 1, | 
| 121 |  |  |  |  |  |  | builder    => '_build_output_file', | 
| 122 |  |  |  |  |  |  | ); | 
| 123 |  |  |  |  |  |  |  | 
| 124 |  |  |  |  |  |  | =head2 backup_file | 
| 125 |  |  |  |  |  |  |  | 
| 126 |  |  |  |  |  |  | The path and name of the backup file. By default, we tack ".bak" | 
| 127 |  |  |  |  |  |  | onto the end of the original filename. | 
| 128 |  |  |  |  |  |  |  | 
| 129 |  |  |  |  |  |  | =cut | 
| 130 |  |  |  |  |  |  |  | 
| 131 |  |  |  |  |  |  | has 'backup_file' => ( | 
| 132 |  |  |  |  |  |  | is         => 'ro', | 
| 133 |  |  |  |  |  |  | isa        => IO_All, | 
| 134 |  |  |  |  |  |  | lazy       => 1, | 
| 135 |  |  |  |  |  |  | coerce     => 1, | 
| 136 |  |  |  |  |  |  | builder    => '_build_backup_file', | 
| 137 |  |  |  |  |  |  | ); | 
| 138 |  |  |  |  |  |  |  | 
| 139 |  |  |  |  |  |  | =head2 nameservers | 
| 140 |  |  |  |  |  |  |  | 
| 141 |  |  |  |  |  |  | A list of nameservers that will be passed to the resolver. | 
| 142 |  |  |  |  |  |  |  | 
| 143 |  |  |  |  |  |  | =cut | 
| 144 |  |  |  |  |  |  |  | 
| 145 |  |  |  |  |  |  | has 'nameservers' => ( | 
| 146 |  |  |  |  |  |  | is  => 'ro', | 
| 147 |  |  |  |  |  |  | isa => 'Maybe[ArrayRef]', | 
| 148 |  |  |  |  |  |  | ); | 
| 149 |  |  |  |  |  |  |  | 
| 150 |  |  |  |  |  |  | =head2 parsed_file | 
| 151 |  |  |  |  |  |  |  | 
| 152 |  |  |  |  |  |  | The L<Net::DNS::Zonefile> object created from the input_file. | 
| 153 |  |  |  |  |  |  |  | 
| 154 |  |  |  |  |  |  | =cut | 
| 155 |  |  |  |  |  |  |  | 
| 156 |  |  |  |  |  |  | has 'parsed_file' => ( | 
| 157 |  |  |  |  |  |  | is         => 'ro', | 
| 158 |  |  |  |  |  |  | isa        => 'Net::DNS::ZoneFile', | 
| 159 |  |  |  |  |  |  | lazy       => 1, | 
| 160 |  |  |  |  |  |  | builder    => '_build_parsed_file', | 
| 161 |  |  |  |  |  |  | ); | 
| 162 |  |  |  |  |  |  |  | 
| 163 |  |  |  |  |  |  | =head2 to_expand | 
| 164 |  |  |  |  |  |  |  | 
| 165 |  |  |  |  |  |  | An arrayref of regexes that we will expand. By default we expand | 
| 166 |  |  |  |  |  |  | a, mx, include, and redirect records. Configurable. | 
| 167 |  |  |  |  |  |  |  | 
| 168 |  |  |  |  |  |  | =cut | 
| 169 |  |  |  |  |  |  |  | 
| 170 |  |  |  |  |  |  | has 'to_expand' => ( | 
| 171 |  |  |  |  |  |  | is      => 'ro', | 
| 172 |  |  |  |  |  |  | isa     => 'ArrayRef[RegexpRef]', | 
| 173 |  |  |  |  |  |  | default => sub { | 
| 174 |  |  |  |  |  |  | [ qr/^a:/, qr/^mx/, qr/^include/, qr/^redirect/, ]; | 
| 175 |  |  |  |  |  |  | }, | 
| 176 |  |  |  |  |  |  | ); | 
| 177 |  |  |  |  |  |  |  | 
| 178 |  |  |  |  |  |  | =head2 to_copy | 
| 179 |  |  |  |  |  |  |  | 
| 180 |  |  |  |  |  |  | An arrayref of regexes that we will simply copy over. By default | 
| 181 |  |  |  |  |  |  | we will copy ip4, ip6, ptr, and exists records. Configurable. | 
| 182 |  |  |  |  |  |  |  | 
| 183 |  |  |  |  |  |  | =cut | 
| 184 |  |  |  |  |  |  |  | 
| 185 |  |  |  |  |  |  | has 'to_copy' => ( | 
| 186 |  |  |  |  |  |  | is      => 'rw', | 
| 187 |  |  |  |  |  |  | isa     => 'ArrayRef[RegexpRef]', | 
| 188 |  |  |  |  |  |  | default => sub { | 
| 189 |  |  |  |  |  |  | [ qr/v=spf1/, qr/^ip4/, qr/^ip6/, qr/^ptr/, qr/^exists/, ]; | 
| 190 |  |  |  |  |  |  | }, | 
| 191 |  |  |  |  |  |  | ); | 
| 192 |  |  |  |  |  |  |  | 
| 193 |  |  |  |  |  |  | =head2 to_ignore | 
| 194 |  |  |  |  |  |  |  | 
| 195 |  |  |  |  |  |  | An arrayref of regexes that we will ignore. By default we ignore ?all, | 
| 196 |  |  |  |  |  |  | exp, v=spf1, and ~all. | 
| 197 |  |  |  |  |  |  |  | 
| 198 |  |  |  |  |  |  | =cut | 
| 199 |  |  |  |  |  |  |  | 
| 200 |  |  |  |  |  |  | has 'to_ignore' => ( | 
| 201 |  |  |  |  |  |  | is      => 'ro', | 
| 202 |  |  |  |  |  |  | isa     => 'ArrayRef[RegexpRef]', | 
| 203 |  |  |  |  |  |  | default => sub { | 
| 204 |  |  |  |  |  |  | [ qr/^v=spf1/, qr/^(\??)all/, qr/^exp/, qr/^~all/ ]; | 
| 205 |  |  |  |  |  |  | }, | 
| 206 |  |  |  |  |  |  | ); | 
| 207 |  |  |  |  |  |  |  | 
| 208 |  |  |  |  |  |  | =head2 maximum_record_length | 
| 209 |  |  |  |  |  |  |  | 
| 210 |  |  |  |  |  |  | We leave out the protocol declaration and the trailing ~all | 
| 211 |  |  |  |  |  |  | while we are expanding records, so we need to subtract their length | 
| 212 |  |  |  |  |  |  | from our length calculation. | 
| 213 |  |  |  |  |  |  |  | 
| 214 |  |  |  |  |  |  | =cut | 
| 215 |  |  |  |  |  |  |  | 
| 216 |  |  |  |  |  |  | has 'maximum_record_length' => ( | 
| 217 |  |  |  |  |  |  | is      => 'ro', | 
| 218 |  |  |  |  |  |  | isa     => 'Int', | 
| 219 |  |  |  |  |  |  | default => sub { | 
| 220 |  |  |  |  |  |  | 255 - length('v=spf1 ') - length(' ~all') - length('"') - length('"'); | 
| 221 |  |  |  |  |  |  | }, | 
| 222 |  |  |  |  |  |  | ); | 
| 223 |  |  |  |  |  |  |  | 
| 224 |  |  |  |  |  |  | =head2 ttl | 
| 225 |  |  |  |  |  |  |  | 
| 226 |  |  |  |  |  |  | Default time to live is 10 minutes. Configurable. | 
| 227 |  |  |  |  |  |  |  | 
| 228 |  |  |  |  |  |  | =cut | 
| 229 |  |  |  |  |  |  |  | 
| 230 |  |  |  |  |  |  | has 'ttl' => ( | 
| 231 |  |  |  |  |  |  | is      => 'ro', | 
| 232 |  |  |  |  |  |  | isa     => 'Str', | 
| 233 |  |  |  |  |  |  | default => sub { | 
| 234 |  |  |  |  |  |  | '10M',; | 
| 235 |  |  |  |  |  |  | }, | 
| 236 |  |  |  |  |  |  | ); | 
| 237 |  |  |  |  |  |  |  | 
| 238 |  |  |  |  |  |  | =head2 origin | 
| 239 |  |  |  |  |  |  |  | 
| 240 |  |  |  |  |  |  | The origin of the zonefile. We take it from the zonefile, | 
| 241 |  |  |  |  |  |  | or you can set it if you like. | 
| 242 |  |  |  |  |  |  |  | 
| 243 |  |  |  |  |  |  | =cut | 
| 244 |  |  |  |  |  |  |  | 
| 245 |  |  |  |  |  |  | has 'origin' => ( | 
| 246 |  |  |  |  |  |  | is         => 'ro', | 
| 247 |  |  |  |  |  |  | isa        => 'Str', | 
| 248 |  |  |  |  |  |  | lazy       => 1, | 
| 249 |  |  |  |  |  |  | builder    => '_build_origin', | 
| 250 |  |  |  |  |  |  | ); | 
| 251 |  |  |  |  |  |  |  | 
| 252 |  |  |  |  |  |  | =head1 PRIVATE ATTRIBUTES | 
| 253 |  |  |  |  |  |  |  | 
| 254 |  |  |  |  |  |  | =head2 _input_file | 
| 255 |  |  |  |  |  |  |  | 
| 256 |  |  |  |  |  |  | The L<IO::All> object created from the input_file. | 
| 257 |  |  |  |  |  |  |  | 
| 258 |  |  |  |  |  |  | =cut | 
| 259 |  |  |  |  |  |  |  | 
| 260 |  |  |  |  |  |  | has '_input_file' => ( | 
| 261 |  |  |  |  |  |  | is         => 'ro', | 
| 262 |  |  |  |  |  |  | isa        => IO_All, | 
| 263 |  |  |  |  |  |  | coerce     => 1, | 
| 264 |  |  |  |  |  |  | lazy       => 1, | 
| 265 |  |  |  |  |  |  | builder    => '_build__input_file', | 
| 266 |  |  |  |  |  |  | ); | 
| 267 |  |  |  |  |  |  |  | 
| 268 |  |  |  |  |  |  | =head2 _resource_records | 
| 269 |  |  |  |  |  |  |  | 
| 270 |  |  |  |  |  |  | An arrayref of all the L<Net::DNS::RR> resource records | 
| 271 |  |  |  |  |  |  | found in the entire parsed_file. | 
| 272 |  |  |  |  |  |  |  | 
| 273 |  |  |  |  |  |  | =cut | 
| 274 |  |  |  |  |  |  |  | 
| 275 |  |  |  |  |  |  | has '_resource_records' => ( | 
| 276 |  |  |  |  |  |  | is         => 'ro', | 
| 277 |  |  |  |  |  |  | isa        => 'Maybe[ArrayRef[Net::DNS::RR]]', | 
| 278 |  |  |  |  |  |  | lazy       => 1, | 
| 279 |  |  |  |  |  |  | builder    => '_build__resource_records', | 
| 280 |  |  |  |  |  |  | ); | 
| 281 |  |  |  |  |  |  |  | 
| 282 |  |  |  |  |  |  | =head2 _spf_records | 
| 283 |  |  |  |  |  |  |  | 
| 284 |  |  |  |  |  |  | An arrayref of the L<Net::DNS::RR::TXT> or L<Net::DNS::RR::SPF> | 
| 285 |  |  |  |  |  |  | records found in the entire parsed_file. | 
| 286 |  |  |  |  |  |  |  | 
| 287 |  |  |  |  |  |  | =cut | 
| 288 |  |  |  |  |  |  |  | 
| 289 |  |  |  |  |  |  | has '_spf_records' => ( | 
| 290 |  |  |  |  |  |  | is         => 'ro', | 
| 291 |  |  |  |  |  |  | isa        => 'Maybe[ArrayRef[Net::DNS::RR]]', | 
| 292 |  |  |  |  |  |  | lazy       => 1, | 
| 293 |  |  |  |  |  |  | builder    => '_build__spf_records', | 
| 294 |  |  |  |  |  |  | ); | 
| 295 |  |  |  |  |  |  |  | 
| 296 |  |  |  |  |  |  | =head2 _resolver | 
| 297 |  |  |  |  |  |  |  | 
| 298 |  |  |  |  |  |  | What we use to do the DNS lookups and expand the records. A | 
| 299 |  |  |  |  |  |  | L<Net::DNS::Resolver> object. You can still set environment | 
| 300 |  |  |  |  |  |  | variables if you want to change the nameserver it uses. | 
| 301 |  |  |  |  |  |  |  | 
| 302 |  |  |  |  |  |  | =cut | 
| 303 |  |  |  |  |  |  |  | 
| 304 |  |  |  |  |  |  | has '_resolver' => ( | 
| 305 |  |  |  |  |  |  | is         => 'ro', | 
| 306 |  |  |  |  |  |  | isa        => 'Net::DNS::Resolver', | 
| 307 |  |  |  |  |  |  | lazy       => 1, | 
| 308 |  |  |  |  |  |  | builder    => '_build__resolver', | 
| 309 |  |  |  |  |  |  | ); | 
| 310 |  |  |  |  |  |  |  | 
| 311 |  |  |  |  |  |  | =head2 _expansions | 
| 312 |  |  |  |  |  |  |  | 
| 313 |  |  |  |  |  |  | This is a hashref representing the expanded SPF records. The keys | 
| 314 |  |  |  |  |  |  | are the names of the SPF records, and the values are hashrefs. | 
| 315 |  |  |  |  |  |  | Those are keyed on the include, and the values are arrayrefs of the | 
| 316 |  |  |  |  |  |  | expanded values. There is also a key called "elements" which gathers | 
| 317 |  |  |  |  |  |  | all the includes into one place, e.g., | 
| 318 |  |  |  |  |  |  |  | 
| 319 |  |  |  |  |  |  | "*.test_zone.com" => { | 
| 320 |  |  |  |  |  |  | "~all"   => undef, | 
| 321 |  |  |  |  |  |  | elements => [ | 
| 322 |  |  |  |  |  |  | "ip4:216.239.32.0/19", "ip4:64.233.160.0/19", | 
| 323 |  |  |  |  |  |  | "ip4:66.249.80.0/20",  "ip4:72.14.192.0/18", | 
| 324 |  |  |  |  |  |  | ... | 
| 325 |  |  |  |  |  |  | ], | 
| 326 |  |  |  |  |  |  | "include:_spf.google.com" => [ | 
| 327 |  |  |  |  |  |  | "ip4:216.239.32.0/19", | 
| 328 |  |  |  |  |  |  | "ip4:64.233.160.0/19", | 
| 329 |  |  |  |  |  |  | ... | 
| 330 |  |  |  |  |  |  | ], | 
| 331 |  |  |  |  |  |  | "ip4:96.43.144.0/20" => [ "ip4:96.43.144.0/20" ], | 
| 332 |  |  |  |  |  |  | "v=spf1"             => undef | 
| 333 |  |  |  |  |  |  | } | 
| 334 |  |  |  |  |  |  |  | 
| 335 |  |  |  |  |  |  | They are alpha sorted in the final results for predictability in tests. | 
| 336 |  |  |  |  |  |  |  | 
| 337 |  |  |  |  |  |  | =cut | 
| 338 |  |  |  |  |  |  |  | 
| 339 |  |  |  |  |  |  | has '_expansions' => ( | 
| 340 |  |  |  |  |  |  | is         => 'ro', | 
| 341 |  |  |  |  |  |  | isa        => 'HashRef', | 
| 342 |  |  |  |  |  |  | lazy       => 1, | 
| 343 |  |  |  |  |  |  | builder    => '_build__expansions', | 
| 344 |  |  |  |  |  |  | ); | 
| 345 |  |  |  |  |  |  |  | 
| 346 |  |  |  |  |  |  | =head2 _lengths_of_expansions | 
| 347 |  |  |  |  |  |  |  | 
| 348 |  |  |  |  |  |  | We need to know how long the expanded record would be, because | 
| 349 |  |  |  |  |  |  | SPF records should be less than 256 bytes. If the expanded | 
| 350 |  |  |  |  |  |  | record would be longer than that, we need to split it into | 
| 351 |  |  |  |  |  |  | pieces. | 
| 352 |  |  |  |  |  |  |  | 
| 353 |  |  |  |  |  |  | =cut | 
| 354 |  |  |  |  |  |  |  | 
| 355 |  |  |  |  |  |  | has '_lengths_of_expansions' => ( | 
| 356 |  |  |  |  |  |  | is         => 'ro', | 
| 357 |  |  |  |  |  |  | isa        => 'HashRef', | 
| 358 |  |  |  |  |  |  | lazy       => 1, | 
| 359 |  |  |  |  |  |  | builder    => '_build__lengths_of_expansions', | 
| 360 |  |  |  |  |  |  | ); | 
| 361 |  |  |  |  |  |  |  | 
| 362 |  |  |  |  |  |  | =head2 _record_class | 
| 363 |  |  |  |  |  |  |  | 
| 364 |  |  |  |  |  |  | What sort of records are SPF records? IN records. | 
| 365 |  |  |  |  |  |  |  | 
| 366 |  |  |  |  |  |  | =cut | 
| 367 |  |  |  |  |  |  |  | 
| 368 |  |  |  |  |  |  | has '_record_class' => ( | 
| 369 |  |  |  |  |  |  | is      => 'ro', | 
| 370 |  |  |  |  |  |  | isa     => 'Str', | 
| 371 |  |  |  |  |  |  | default => sub { | 
| 372 |  |  |  |  |  |  | 'IN',; | 
| 373 |  |  |  |  |  |  | }, | 
| 374 |  |  |  |  |  |  | ); | 
| 375 |  |  |  |  |  |  |  | 
| 376 |  |  |  |  |  |  | =head1 BUILDERS | 
| 377 |  |  |  |  |  |  |  | 
| 378 |  |  |  |  |  |  | =head2 _build_resolver | 
| 379 |  |  |  |  |  |  |  | 
| 380 |  |  |  |  |  |  | Return a L<Net::DNS::Resolver>. Any nameservers will be passed | 
| 381 |  |  |  |  |  |  | through to the resolver. | 
| 382 |  |  |  |  |  |  |  | 
| 383 |  |  |  |  |  |  | =cut | 
| 384 |  |  |  |  |  |  |  | 
| 385 |  |  |  |  |  |  | sub _build__resolver { | 
| 386 | 4 |  |  | 4 |  | 8 | my $self        = shift; | 
| 387 | 4 |  |  |  |  | 78 | my $nameservers = $self->nameservers; | 
| 388 | 4 | 50 |  |  |  | 61 | my $resolver    = Net::DNS::Resolver->new( | 
| 389 |  |  |  |  |  |  | recurse => 1, | 
| 390 |  |  |  |  |  |  | ( $nameservers ? ( nameservers => $nameservers ) : () ), | 
| 391 |  |  |  |  |  |  | ); | 
| 392 | 4 |  |  |  |  | 2010 | return $resolver; | 
| 393 |  |  |  |  |  |  | } | 
| 394 |  |  |  |  |  |  |  | 
| 395 |  |  |  |  |  |  | =head2 _build_origin | 
| 396 |  |  |  |  |  |  |  | 
| 397 |  |  |  |  |  |  | Extract the origin from parsed_file. | 
| 398 |  |  |  |  |  |  |  | 
| 399 |  |  |  |  |  |  | =cut | 
| 400 |  |  |  |  |  |  |  | 
| 401 |  |  |  |  |  |  | sub _build_origin { | 
| 402 | 3 |  |  | 3 |  | 8 | my $self = shift; | 
| 403 | 3 |  |  |  |  | 80 | return $self->parsed_file->origin; | 
| 404 |  |  |  |  |  |  | } | 
| 405 |  |  |  |  |  |  |  | 
| 406 |  |  |  |  |  |  | =head2 _build_expansions | 
| 407 |  |  |  |  |  |  |  | 
| 408 |  |  |  |  |  |  | =cut | 
| 409 |  |  |  |  |  |  |  | 
| 410 |  |  |  |  |  |  | sub _build__expansions { | 
| 411 | 4 |  |  | 4 |  | 9 | my $self = shift; | 
| 412 | 4 |  |  |  |  | 14 | return $self->_expand; | 
| 413 |  |  |  |  |  |  | } | 
| 414 |  |  |  |  |  |  |  | 
| 415 |  |  |  |  |  |  | =head2 _build_backup_file | 
| 416 |  |  |  |  |  |  |  | 
| 417 |  |  |  |  |  |  | Tack a ".bak" onto the end of the input_file. | 
| 418 |  |  |  |  |  |  |  | 
| 419 |  |  |  |  |  |  | =cut | 
| 420 |  |  |  |  |  |  |  | 
| 421 |  |  |  |  |  |  | sub _build_backup_file { | 
| 422 | 3 |  |  | 3 |  | 8 | my $self = shift; | 
| 423 | 3 |  |  |  |  | 54 | my $path = $self->_input_file->filepath; | 
| 424 | 3 |  |  |  |  | 134 | my $name = $self->_input_file->filename; | 
| 425 | 3 |  |  |  |  | 134 | return "${path}${name}.bak"; | 
| 426 |  |  |  |  |  |  | } | 
| 427 |  |  |  |  |  |  |  | 
| 428 |  |  |  |  |  |  | =head2 _build__input_file | 
| 429 |  |  |  |  |  |  |  | 
| 430 |  |  |  |  |  |  | Turn the string input_file into a filehandle with | 
| 431 |  |  |  |  |  |  | L<IO::All>. | 
| 432 |  |  |  |  |  |  |  | 
| 433 |  |  |  |  |  |  | =cut | 
| 434 |  |  |  |  |  |  |  | 
| 435 |  |  |  |  |  |  | sub _build__input_file { | 
| 436 | 4 |  |  | 4 |  | 8 | my $self = shift; | 
| 437 | 4 |  |  |  |  | 81 | return to_IO_All( $self->input_file ); | 
| 438 |  |  |  |  |  |  | } | 
| 439 |  |  |  |  |  |  |  | 
| 440 |  |  |  |  |  |  | =head2 _build_output_file | 
| 441 |  |  |  |  |  |  |  | 
| 442 |  |  |  |  |  |  | Tack a ".new" onto the end of the input_file. | 
| 443 |  |  |  |  |  |  |  | 
| 444 |  |  |  |  |  |  | =cut | 
| 445 |  |  |  |  |  |  |  | 
| 446 |  |  |  |  |  |  | sub _build_output_file { | 
| 447 | 3 |  |  | 3 |  | 7 | my $self = shift; | 
| 448 | 3 |  |  |  |  | 59 | my $path = $self->_input_file->filepath; | 
| 449 | 3 |  |  |  |  | 170 | my $name = $self->_input_file->filename; | 
| 450 | 3 |  |  |  |  | 128 | return "${path}${name}.new"; | 
| 451 |  |  |  |  |  |  | } | 
| 452 |  |  |  |  |  |  |  | 
| 453 |  |  |  |  |  |  | =head2 _build_parsed_file | 
| 454 |  |  |  |  |  |  |  | 
| 455 |  |  |  |  |  |  | Turn the L<IO::All> filehandle into a L<Net::DNS::Zonefile> | 
| 456 |  |  |  |  |  |  | object, so that we can extract the SPF records. | 
| 457 |  |  |  |  |  |  |  | 
| 458 |  |  |  |  |  |  | =cut | 
| 459 |  |  |  |  |  |  |  | 
| 460 |  |  |  |  |  |  | sub _build_parsed_file { | 
| 461 | 4 |  |  | 4 |  | 8 | my $self = shift; | 
| 462 | 4 |  |  |  |  | 78 | my $path = $self->_input_file->filepath; | 
| 463 | 4 |  |  |  |  | 344 | my $name = $self->_input_file->filename; | 
| 464 | 4 |  |  |  |  | 141 | return Net::DNS::ZoneFile->new("${path}${name}"); | 
| 465 |  |  |  |  |  |  | } | 
| 466 |  |  |  |  |  |  |  | 
| 467 |  |  |  |  |  |  | =head2 _build_resource_records | 
| 468 |  |  |  |  |  |  |  | 
| 469 |  |  |  |  |  |  | Extract all the resource records from the L<Net::DNS::Zonefile>. | 
| 470 |  |  |  |  |  |  |  | 
| 471 |  |  |  |  |  |  | =cut | 
| 472 |  |  |  |  |  |  |  | 
| 473 |  |  |  |  |  |  | sub _build__resource_records { | 
| 474 | 4 |  |  | 4 |  | 9 | my $self             = shift; | 
| 475 | 4 |  |  |  |  | 83 | my @resource_records = $self->parsed_file->read; | 
| 476 | 4 |  |  |  |  | 34623 | return \@resource_records; | 
| 477 |  |  |  |  |  |  | } | 
| 478 |  |  |  |  |  |  |  | 
| 479 |  |  |  |  |  |  | =head2 _build__spf_records | 
| 480 |  |  |  |  |  |  |  | 
| 481 |  |  |  |  |  |  | Grep through the _resource_records to find the SPF | 
| 482 |  |  |  |  |  |  | records. They can be both "TXT" and "SPF" records, | 
| 483 |  |  |  |  |  |  | so we search for the protocol string, v=spf1. | 
| 484 |  |  |  |  |  |  |  | 
| 485 |  |  |  |  |  |  | =cut | 
| 486 |  |  |  |  |  |  |  | 
| 487 |  |  |  |  |  |  | sub _build__spf_records { | 
| 488 | 4 |  |  | 4 |  | 8 | my $self = shift; | 
| 489 |  |  |  |  |  |  |  | 
| 490 |  |  |  |  |  |  | # This is crude but correct: SPF records can be both TXT and SPF. | 
| 491 |  |  |  |  |  |  | my @spf_records = | 
| 492 | 22 |  |  |  |  | 611 | grep { $_->txtdata =~ /v=spf1/ } | 
| 493 | 38 |  |  |  |  | 94 | grep { $_->can('txtdata') } | 
| 494 | 4 |  |  |  |  | 9 | @{ $self->_resource_records }; | 
|  | 4 |  |  |  |  | 83 |  | 
| 495 | 4 |  |  |  |  | 272 | return \@spf_records; | 
| 496 |  |  |  |  |  |  | } | 
| 497 |  |  |  |  |  |  |  | 
| 498 |  |  |  |  |  |  | =head2 _build__lengths_of_expansions | 
| 499 |  |  |  |  |  |  |  | 
| 500 |  |  |  |  |  |  | Calculate the length of each fully expanded SPF record, | 
| 501 |  |  |  |  |  |  | because they can't be longer than 256 bytes. We have to split them | 
| 502 |  |  |  |  |  |  | up into multiple records if they are. | 
| 503 |  |  |  |  |  |  |  | 
| 504 |  |  |  |  |  |  | =cut | 
| 505 |  |  |  |  |  |  |  | 
| 506 |  |  |  |  |  |  | sub _build__lengths_of_expansions { | 
| 507 | 4 |  |  | 4 |  | 7 | my $self              = shift; | 
| 508 | 4 |  |  |  |  | 123 | my $expansions        = $self->_expansions; | 
| 509 | 4 |  |  |  |  | 11 | my $length_per_domain = {}; | 
| 510 | 4 |  |  |  |  | 21 | for my $domain ( keys %$expansions ) { | 
| 511 |  |  |  |  |  |  | my $record_string = join( | 
| 512 |  |  |  |  |  |  | ' ', | 
| 513 | 9 |  |  |  |  | 17 | @{ $expansions->{$domain}{elements} } | 
|  | 9 |  |  |  |  | 74 |  | 
| 514 |  |  |  |  |  |  | ); | 
| 515 | 9 |  |  |  |  | 62 | $length_per_domain->{$domain} = length($record_string); | 
| 516 |  |  |  |  |  |  | } | 
| 517 | 4 |  |  |  |  | 91 | return $length_per_domain; | 
| 518 |  |  |  |  |  |  | } | 
| 519 |  |  |  |  |  |  |  | 
| 520 |  |  |  |  |  |  | =head1 PUBLIC METHODS | 
| 521 |  |  |  |  |  |  |  | 
| 522 |  |  |  |  |  |  | =head2 write | 
| 523 |  |  |  |  |  |  |  | 
| 524 |  |  |  |  |  |  | This is the only method you really need to call. This expands all your SPF | 
| 525 |  |  |  |  |  |  | records and writes out the new and the backup files. | 
| 526 |  |  |  |  |  |  |  | 
| 527 |  |  |  |  |  |  | Returns a scalar string of the data written to the file. | 
| 528 |  |  |  |  |  |  |  | 
| 529 |  |  |  |  |  |  | =cut | 
| 530 |  |  |  |  |  |  |  | 
| 531 |  |  |  |  |  |  | sub write { | 
| 532 | 3 |  |  | 3 | 1 | 1024 | my $self  = shift; | 
| 533 | 3 |  |  |  |  | 15 | my $lines = $self->_new_records_lines; | 
| 534 | 3 |  |  |  |  | 93 | my $path  = $self->_input_file->filepath; | 
| 535 | 3 |  |  |  |  | 228 | my $name  = $self->_input_file->filename; | 
| 536 | 3 |  |  |  |  | 174 | io( $self->backup_file )->print( $self->_input_file->all ); | 
| 537 | 3 |  |  |  |  | 6514 | io( $self->output_file )->print(@$lines); | 
| 538 | 3 |  |  |  |  | 2151 | return join( '', @$lines ); | 
| 539 |  |  |  |  |  |  | } | 
| 540 |  |  |  |  |  |  |  | 
| 541 |  |  |  |  |  |  | =head2 new_spf_records | 
| 542 |  |  |  |  |  |  |  | 
| 543 |  |  |  |  |  |  | In case you want to see how your records were expanded, this returns | 
| 544 |  |  |  |  |  |  | the hashref of L<Net::DNS::RR> objects used to create the new records. | 
| 545 |  |  |  |  |  |  |  | 
| 546 |  |  |  |  |  |  | =cut | 
| 547 |  |  |  |  |  |  |  | 
| 548 |  |  |  |  |  |  | sub new_spf_records { | 
| 549 | 4 |  |  | 4 | 1 | 747 | my $self       = shift; | 
| 550 | 4 |  |  |  |  | 121 | my $lengths    = $self->_lengths_of_expansions; | 
| 551 | 4 |  |  |  |  | 82 | my $expansions = $self->_expansions; | 
| 552 |  |  |  |  |  |  |  | 
| 553 | 4 |  |  |  |  | 11 | my %new_spf_records = (); | 
| 554 |  |  |  |  |  |  |  | 
| 555 | 4 |  |  |  |  | 17 | for my $domain ( keys %$lengths ) { | 
| 556 | 9 |  |  |  |  | 17 | my $new_records = []; | 
| 557 |  |  |  |  |  |  |  | 
| 558 |  |  |  |  |  |  | # We need to make sure the SPF record is less than 256 chars, | 
| 559 |  |  |  |  |  |  | # including the spf version and trailing ~all. | 
| 560 | 9 | 50 |  |  |  | 210 | if ( $lengths->{$domain} > $self->maximum_record_length ) { | 
| 561 |  |  |  |  |  |  | $new_records = $self->_new_records_from_partition( | 
| 562 |  |  |  |  |  |  | $domain, | 
| 563 |  |  |  |  |  |  | $expansions->{$domain}{elements}, | 
| 564 | 9 |  |  |  |  | 35 | ); | 
| 565 |  |  |  |  |  |  | } else { | 
| 566 |  |  |  |  |  |  | $new_records = $self->_new_records_from_arrayref( | 
| 567 |  |  |  |  |  |  | $domain, | 
| 568 |  |  |  |  |  |  | $expansions->{$domain}{elements}, | 
| 569 | 0 |  |  |  |  | 0 | ); | 
| 570 |  |  |  |  |  |  | } | 
| 571 | 9 |  |  |  |  | 29 | $new_spf_records{$domain} = $new_records; | 
| 572 |  |  |  |  |  |  | } | 
| 573 | 4 |  |  |  |  | 45 | return \%new_spf_records; | 
| 574 |  |  |  |  |  |  | } | 
| 575 |  |  |  |  |  |  |  | 
| 576 |  |  |  |  |  |  | =head1 PRIVATE METHODS | 
| 577 |  |  |  |  |  |  |  | 
| 578 |  |  |  |  |  |  | =head2 _normalize_component | 
| 579 |  |  |  |  |  |  |  | 
| 580 |  |  |  |  |  |  | Each component of an SPF record has a prefix, like include:, mx:, etc. | 
| 581 |  |  |  |  |  |  | Here we chop off the prefix before performing the lookup on the value. | 
| 582 |  |  |  |  |  |  |  | 
| 583 |  |  |  |  |  |  | =cut | 
| 584 |  |  |  |  |  |  |  | 
| 585 |  |  |  |  |  |  | sub _normalize_component { | 
| 586 | 245 |  |  | 245 |  | 476 | my ( $self, $component ) = @_; | 
| 587 | 245 |  |  |  |  | 478 | my $return = $component; | 
| 588 | 245 |  |  |  |  | 1263 | $return =~ s/^.+?://g; | 
| 589 | 245 |  |  |  |  | 599 | return $return; | 
| 590 |  |  |  |  |  |  | } | 
| 591 |  |  |  |  |  |  |  | 
| 592 |  |  |  |  |  |  | =head2 _perform_expansion | 
| 593 |  |  |  |  |  |  |  | 
| 594 |  |  |  |  |  |  | Expand a single SPF record component. This returns either undef or the | 
| 595 |  |  |  |  |  |  | full SPF record string from L<Net::DNS::RR::TXT>->txtdata. | 
| 596 |  |  |  |  |  |  |  | 
| 597 |  |  |  |  |  |  | =cut | 
| 598 |  |  |  |  |  |  |  | 
| 599 |  |  |  |  |  |  | sub _perform_expansion { | 
| 600 | 59 |  |  | 59 |  | 130 | my ( $self, $component ) = @_; | 
| 601 | 59 |  |  |  |  | 165 | $component = $self->_normalize_component($component); | 
| 602 | 59 |  |  |  |  | 1231 | my $packet = $self->_resolver->search( $component, 'TXT', 'IN' ); | 
| 603 | 59 | 100 | 66 |  |  | 700721 | return unless ($packet) && $packet->isa('Net::DNS::Packet'); | 
| 604 | 54 |  |  |  |  | 241 | my ($answer) = $packet->answer; | 
| 605 | 54 | 50 | 33 |  |  | 701 | return unless ($answer) && $answer->isa('Net::DNS::RR::TXT'); | 
| 606 | 54 |  |  |  |  | 213 | my $data = $answer->txtdata; | 
| 607 | 54 |  |  |  |  | 2702 | return $data; | 
| 608 |  |  |  |  |  |  | } | 
| 609 |  |  |  |  |  |  |  | 
| 610 |  |  |  |  |  |  | =head2 _expand_spf_component | 
| 611 |  |  |  |  |  |  |  | 
| 612 |  |  |  |  |  |  | Recursively call _perform_expansion for each component of the SPF record. | 
| 613 |  |  |  |  |  |  | This returns an array consisting of the component, e.g., include:salesforce.com, | 
| 614 |  |  |  |  |  |  | and an arrayref consisting of its full expansion, e.g., | 
| 615 |  |  |  |  |  |  |  | 
| 616 |  |  |  |  |  |  | [ | 
| 617 |  |  |  |  |  |  | "ip4:216.239.32.0/19", | 
| 618 |  |  |  |  |  |  | "ip4:64.233.160.0/19", | 
| 619 |  |  |  |  |  |  | ... | 
| 620 |  |  |  |  |  |  | "ip6:2c0f:fb50:4000::/36" | 
| 621 |  |  |  |  |  |  | ] | 
| 622 |  |  |  |  |  |  |  | 
| 623 |  |  |  |  |  |  | =cut | 
| 624 |  |  |  |  |  |  |  | 
| 625 |  |  |  |  |  |  | sub _expand_spf_component { | 
| 626 | 992 |  |  | 992 |  | 3815 | my ( $self, $component, $expansions ) = @_; | 
| 627 |  |  |  |  |  |  |  | 
| 628 | 992 |  | 100 |  |  | 2744 | $expansions ||= []; | 
| 629 |  |  |  |  |  |  |  | 
| 630 | 992 | 100 |  |  |  | 2265 | return unless $component; | 
| 631 |  |  |  |  |  |  |  | 
| 632 | 987 |  |  |  |  | 3012 | my @component_splits = split( ' ', $component ); | 
| 633 | 987 |  |  |  |  | 1584 | my $splits = @component_splits; | 
| 634 | 987 | 100 |  |  |  | 2185 | if ( $splits > 1 ) { | 
| 635 | 82 |  |  |  |  | 187 | for my $component (@component_splits) { | 
| 636 | 747 |  |  |  |  | 1871 | $self->_expand_spf_component( $component, $expansions ); | 
| 637 |  |  |  |  |  |  | } | 
| 638 |  |  |  |  |  |  | } else { | 
| 639 | 905 | 100 |  | 3308 |  | 3816 | if (( any { $component =~ $_ } @{ $self->to_ignore } )) { | 
|  | 3308 | 100 |  |  |  | 14267 |  | 
|  | 905 | 100 |  |  |  | 22206 |  | 
| 640 | 166 |  |  |  |  | 765 | return $component; | 
| 641 | 1769 |  |  | 1769 |  | 8448 | } elsif (( any { $component =~ $_ } @{ $self->to_copy } )) { | 
|  | 739 |  |  |  |  | 16979 |  | 
| 642 | 676 |  |  |  |  | 1227 | push @{$expansions}, $component; | 
|  | 676 |  |  |  |  | 1582 |  | 
| 643 | 186 |  |  | 186 |  | 722 | } elsif (( any { $component =~ $_ } @{ $self->to_expand } )) { | 
|  | 63 |  |  |  |  | 1354 |  | 
| 644 | 59 |  |  |  |  | 207 | my $new_component = $self->_perform_expansion($component); | 
| 645 | 59 |  |  |  |  | 255 | $self->_expand_spf_component( $new_component, $expansions ); | 
| 646 |  |  |  |  |  |  | } else { | 
| 647 | 4 |  |  |  |  | 16 | return $component; | 
| 648 |  |  |  |  |  |  | } | 
| 649 |  |  |  |  |  |  | } | 
| 650 | 817 |  |  |  |  | 3360 | return ( $component, $expansions ); | 
| 651 |  |  |  |  |  |  | } | 
| 652 |  |  |  |  |  |  |  | 
| 653 |  |  |  |  |  |  | =head2 _expand | 
| 654 |  |  |  |  |  |  |  | 
| 655 |  |  |  |  |  |  | Create the _expansions hashref from which we generate new SPF records. | 
| 656 |  |  |  |  |  |  |  | 
| 657 |  |  |  |  |  |  | =cut | 
| 658 |  |  |  |  |  |  |  | 
| 659 |  |  |  |  |  |  | sub _expand { | 
| 660 | 4 |  |  | 4 |  | 10 | my $self           = shift; | 
| 661 | 4 |  |  |  |  | 11 | my %spf_hash       = (); | 
| 662 | 4 |  |  |  |  | 8 | my %keys_to_delete = (); | 
| 663 | 4 |  |  |  |  | 6 | for my $spf_record ( @{ $self->_spf_records } ) { | 
|  | 4 |  |  |  |  | 98 |  | 
| 664 | 22 |  |  |  |  | 409 | my @spf_components = split( ' ', $spf_record->txtdata ); | 
| 665 | 22 |  |  |  |  | 1091 | for my $spf_component (@spf_components) { | 
| 666 | 186 |  |  |  |  | 2304 | my $component_name = $self->_normalize_component($spf_component); | 
| 667 |  |  |  |  |  |  | # We want to make sure that we do not look up spf records that are | 
| 668 |  |  |  |  |  |  | # defined in this zonefile. So that we could run this tool on a | 
| 669 |  |  |  |  |  |  | # previously expanded zonefile if we want to. That sort of defeats | 
| 670 |  |  |  |  |  |  | # the point of the tool, but you may edit the previously expanded zonefile, | 
| 671 |  |  |  |  |  |  | # adding a new include: or mx, appending it to the other _spfX includes. | 
| 672 |  |  |  |  |  |  | # We just take the component and its existing expansions and stick them | 
| 673 |  |  |  |  |  |  | # into the component's parent as a key and value, and then we remove that | 
| 674 |  |  |  |  |  |  | # component as a separate key from our hash. | 
| 675 | 186 | 100 |  | 2518 |  | 615 | if ( any { $component_name eq $_->name } @{ $self->_spf_records } ) { | 
|  | 2518 |  |  |  |  | 25829 |  | 
|  | 186 |  |  |  |  | 4105 |  | 
| 676 |  |  |  |  |  |  | my ($zonefile_record) | 
| 677 | 480 |  |  |  |  | 7118 | = grep { $component_name eq $_->name } | 
| 678 | 30 |  |  |  |  | 446 | @{ $self->_spf_records }; | 
|  | 30 |  |  |  |  | 915 |  | 
| 679 | 30 |  |  |  |  | 503 | my ( $comp, $expansions ) | 
| 680 |  |  |  |  |  |  | = $self->_expand_spf_component( | 
| 681 |  |  |  |  |  |  | $zonefile_record->txtdata ); | 
| 682 | 30 |  |  |  |  | 174 | $spf_hash{ $spf_record->name }{$spf_component} = $expansions; | 
| 683 | 30 |  |  |  |  | 887 | $keys_to_delete{$component_name} = 1; | 
| 684 |  |  |  |  |  |  | # If the include or what have you is not defined in the zonefile, | 
| 685 |  |  |  |  |  |  | # proceed as normal. | 
| 686 |  |  |  |  |  |  | } else { | 
| 687 | 156 |  |  |  |  | 1864 | my ( $comp, $expansions ) | 
| 688 |  |  |  |  |  |  | = $self->_expand_spf_component($spf_component); | 
| 689 | 156 |  |  |  |  | 527 | $spf_hash{ $spf_record->name }{$spf_component} = $expansions; | 
| 690 |  |  |  |  |  |  | } | 
| 691 |  |  |  |  |  |  | } | 
| 692 |  |  |  |  |  |  | my $expansion_elements = $self->_extract_expansion_elements( | 
| 693 | 22 |  |  |  |  | 374 | $spf_hash{ $spf_record->name } ); | 
| 694 | 22 |  |  |  |  | 108 | $spf_hash{ $spf_record->name }{elements} = $expansion_elements; | 
| 695 |  |  |  |  |  |  | } | 
| 696 | 4 |  |  |  |  | 115 | delete @spf_hash{ keys %keys_to_delete }; | 
| 697 | 4 |  |  |  |  | 105 | return \%spf_hash; | 
| 698 |  |  |  |  |  |  | } | 
| 699 |  |  |  |  |  |  |  | 
| 700 |  |  |  |  |  |  | =head2 _extract_expansion_elements | 
| 701 |  |  |  |  |  |  |  | 
| 702 |  |  |  |  |  |  | Filter ignored elements from component expansions. | 
| 703 |  |  |  |  |  |  |  | 
| 704 |  |  |  |  |  |  | =cut | 
| 705 |  |  |  |  |  |  |  | 
| 706 |  |  |  |  |  |  | sub _extract_expansion_elements { | 
| 707 | 22 |  |  | 22 |  | 283 | my ( $self, $expansions ) = @_; | 
| 708 | 22 |  |  |  |  | 57 | my @elements = (); | 
| 709 | 22 |  |  |  |  | 39 | my @leading  = (); | 
| 710 | 22 |  |  |  |  | 42 | my @trailing = (); | 
| 711 | 22 |  |  |  |  | 138 | KEY: for my $key ( keys %$expansions ) { | 
| 712 | 194 | 100 |  | 710 |  | 583 | if ( any { $key =~ $_ } @{ $self->to_ignore } ) { | 
|  | 710 |  |  |  |  | 2474 |  | 
|  | 194 |  |  |  |  | 4039 |  | 
| 713 | 34 |  |  |  |  | 129 | next KEY; | 
| 714 |  |  |  |  |  |  | } | 
| 715 | 160 | 50 |  |  |  | 654 | if ( ref( $expansions->{$key} ) eq 'ARRAY' ) { | 
| 716 | 160 |  |  |  |  | 254 | for my $expansion ( @{ $expansions->{$key} } ) { | 
|  | 160 |  |  |  |  | 357 |  | 
| 717 | 898 |  |  |  |  | 1550 | push @elements, $expansion; | 
| 718 |  |  |  |  |  |  | } | 
| 719 |  |  |  |  |  |  | } | 
| 720 |  |  |  |  |  |  | } | 
| 721 |  |  |  |  |  |  | # We sort these so we can be sure of the order in tests. | 
| 722 | 22 |  |  |  |  | 147 | my @return = uniq sort { $a cmp $b } ( @leading, @elements, @trailing ); | 
|  | 3582 |  |  |  |  | 5723 |  | 
| 723 | 22 |  |  |  |  | 160 | return \@return; | 
| 724 |  |  |  |  |  |  | } | 
| 725 |  |  |  |  |  |  |  | 
| 726 |  |  |  |  |  |  | =head2 _new_records_from_arrayref | 
| 727 |  |  |  |  |  |  |  | 
| 728 |  |  |  |  |  |  | The full expansion of a given SPF record is contained in an arrayref, | 
| 729 |  |  |  |  |  |  | and if the length of the resulting new SPF record would be less than the | 
| 730 |  |  |  |  |  |  | maximum_record_length, we can use this method to make new | 
| 731 |  |  |  |  |  |  | L<Net::DNS::RR> objects that will later be stringified for the new | 
| 732 |  |  |  |  |  |  | SPF record. | 
| 733 |  |  |  |  |  |  |  | 
| 734 |  |  |  |  |  |  | =cut | 
| 735 |  |  |  |  |  |  |  | 
| 736 |  |  |  |  |  |  | sub _new_records_from_arrayref { | 
| 737 | 37 |  |  | 37 |  | 62 | my ( $self, $domain, $expansions ) = @_; | 
| 738 |  |  |  |  |  |  |  | 
| 739 | 37 |  |  |  |  | 149 | my $txtdata = join(' ', @$expansions); | 
| 740 |  |  |  |  |  |  |  | 
| 741 | 37 |  |  |  |  | 54 | my @new_records = (); | 
| 742 | 37 |  |  |  |  | 837 | push @new_records, new Net::DNS::RR( | 
| 743 |  |  |  |  |  |  | type    => 'TXT', | 
| 744 |  |  |  |  |  |  | name    => $domain, | 
| 745 |  |  |  |  |  |  | class   => $self->_record_class, | 
| 746 |  |  |  |  |  |  | ttl     => $self->ttl, | 
| 747 |  |  |  |  |  |  | txtdata => $txtdata, | 
| 748 |  |  |  |  |  |  | ); | 
| 749 | 37 |  |  |  |  | 5509 | return \@new_records; | 
| 750 |  |  |  |  |  |  | } | 
| 751 |  |  |  |  |  |  |  | 
| 752 |  |  |  |  |  |  | =head2 _new_records_from_partition | 
| 753 |  |  |  |  |  |  |  | 
| 754 |  |  |  |  |  |  | The full expansion of a given SPF record is contained in an arrayref, | 
| 755 |  |  |  |  |  |  | and if the length of the resulting new SPF record would be greater than the | 
| 756 |  |  |  |  |  |  | maximum_record_length, we have to jump through some hoops to properly split | 
| 757 |  |  |  |  |  |  | it into new SPF records. Because there will be more than one, and each needs | 
| 758 |  |  |  |  |  |  | to be less than the maximum_record_length. We do our partitioning here, and | 
| 759 |  |  |  |  |  |  | then call _new_records_from_arrayref on each of the resulting partitions. | 
| 760 |  |  |  |  |  |  |  | 
| 761 |  |  |  |  |  |  | =cut | 
| 762 |  |  |  |  |  |  |  | 
| 763 |  |  |  |  |  |  | sub _new_records_from_partition { | 
| 764 | 9 |  |  | 9 |  | 21 | my ( $self, $domain, $elements, $partitions_only ) = @_; | 
| 765 | 9 |  |  |  |  | 59 | my $record_string = join( ' ', @$elements ); | 
| 766 | 9 |  |  |  |  | 49 | my $record_length = length($record_string); | 
| 767 | 9 |  |  |  |  | 167 | my $max_length    = $self->maximum_record_length; | 
| 768 | 9 |  |  |  |  | 14 | my $offset        = 0; | 
| 769 | 9 |  |  |  |  | 37 | my $result        = index( $record_string, ' ', $offset ); | 
| 770 | 9 |  |  |  |  | 21 | my @space_indices = (); | 
| 771 |  |  |  |  |  |  |  | 
| 772 | 9 |  |  |  |  | 26 | while ( $result != -1 ) { | 
| 773 | 336 | 50 |  |  |  | 592 | push @space_indices, $result if $result; | 
| 774 | 336 |  |  |  |  | 368 | $offset = $result + 1; | 
| 775 | 336 |  |  |  |  | 769 | $result = index( $record_string, ' ', $offset ); | 
| 776 |  |  |  |  |  |  | } | 
| 777 |  |  |  |  |  |  |  | 
| 778 | 9 | 50 |  |  |  | 42 | my $number_of_partitions = int($record_length / $max_length + 0.5) | 
| 779 |  |  |  |  |  |  | + ( ( $record_length % $max_length ) ? 1 : 0 ); | 
| 780 |  |  |  |  |  |  |  | 
| 781 | 9 |  |  |  |  | 18 | my @partitions       = (); | 
| 782 | 9 |  |  |  |  | 12 | my $partition_offset = 0; | 
| 783 |  |  |  |  |  |  |  | 
| 784 | 9 |  |  |  |  | 24 | for my $part ( 1 .. $number_of_partitions ) { | 
| 785 |  |  |  |  |  |  |  | 
| 786 |  |  |  |  |  |  | # We want the first space_index that is | 
| 787 |  |  |  |  |  |  | #   1. less than the max_length times the number of parts, and | 
| 788 |  |  |  |  |  |  | #   2. subtracting the partition_offset from it is less than | 
| 789 |  |  |  |  |  |  | #      max_length. | 
| 790 |  |  |  |  |  |  | my $split_point = first { | 
| 791 | 513 | 100 |  | 513 |  | 922 | ( $_ < ( $max_length * $part ) ) | 
| 792 |  |  |  |  |  |  | && ( ( $_ - $partition_offset ) < $max_length ) | 
| 793 | 37 |  |  |  |  | 149 | } reverse @space_indices; | 
| 794 |  |  |  |  |  |  |  | 
| 795 | 37 |  |  |  |  | 85 | my $partition_length = $split_point - $partition_offset; | 
| 796 |  |  |  |  |  |  |  | 
| 797 |  |  |  |  |  |  | # Go to the end of the string if we are dealing with | 
| 798 |  |  |  |  |  |  | # the last partition. Otherwise, the last element | 
| 799 |  |  |  |  |  |  | # gets chopped off, because it is after the last space_index! | 
| 800 | 37 | 100 |  |  |  | 70 | my $length | 
| 801 |  |  |  |  |  |  | = ( $part == $number_of_partitions ) ? undef : $partition_length; | 
| 802 | 37 |  |  |  |  | 55 | my $substring; | 
| 803 | 37 | 100 |  |  |  | 63 | if ( $part == $number_of_partitions ) { | 
| 804 |  |  |  |  |  |  | # Go to the end. | 
| 805 | 9 |  |  |  |  | 103 | $substring = substr( $record_string, $partition_offset ); | 
| 806 |  |  |  |  |  |  | } else { | 
| 807 |  |  |  |  |  |  | # Take a specific length. | 
| 808 | 28 |  |  |  |  | 50 | $substring = substr( $record_string, $partition_offset, | 
| 809 |  |  |  |  |  |  | $partition_length ); | 
| 810 |  |  |  |  |  |  | } | 
| 811 |  |  |  |  |  |  |  | 
| 812 | 37 |  |  |  |  | 159 | push @partitions, [ split( ' ', $substring ) ]; | 
| 813 | 37 |  |  |  |  | 78 | $partition_offset = $split_point; | 
| 814 |  |  |  |  |  |  | } | 
| 815 | 9 | 50 |  |  |  | 22 | return \@partitions if $partitions_only; | 
| 816 |  |  |  |  |  |  |  | 
| 817 | 9 |  |  |  |  | 18 | my @return = (); | 
| 818 |  |  |  |  |  |  |  | 
| 819 | 9 |  |  |  |  | 18 | for my $partition (@partitions) { | 
| 820 | 37 |  |  |  |  | 135 | my $result = $self->_new_records_from_arrayref( $domain, $partition ); | 
| 821 | 37 |  |  |  |  | 77 | push @return, $result; | 
| 822 |  |  |  |  |  |  | } | 
| 823 | 9 |  |  |  |  | 58 | return \@return; | 
| 824 |  |  |  |  |  |  | } | 
| 825 |  |  |  |  |  |  |  | 
| 826 |  |  |  |  |  |  | =head2 _get_single_record_string | 
| 827 |  |  |  |  |  |  |  | 
| 828 |  |  |  |  |  |  | Stringify the L<Net::DNS::RR::TXT> records when they will fit into | 
| 829 |  |  |  |  |  |  | a single SPF record. | 
| 830 |  |  |  |  |  |  |  | 
| 831 |  |  |  |  |  |  | =cut | 
| 832 |  |  |  |  |  |  |  | 
| 833 |  |  |  |  |  |  | sub _get_single_record_string { | 
| 834 | 0 |  |  | 0 |  | 0 | my ( $self, $domain, $record_set ) = @_; | 
| 835 | 0 |  |  |  |  | 0 | my $origin = $self->origin; | 
| 836 |  |  |  |  |  |  |  | 
| 837 | 0 |  |  |  |  | 0 | my @record_strings = (); | 
| 838 |  |  |  |  |  |  |  | 
| 839 | 0 |  |  |  |  | 0 | my @sorted_record_set = map { $_ } | 
| 840 | 0 |  |  |  |  | 0 | sort  { $a->string cmp $b->string } | 
|  | 0 |  |  |  |  | 0 |  | 
| 841 |  |  |  |  |  |  | @$record_set; | 
| 842 |  |  |  |  |  |  |  | 
| 843 | 0 |  |  |  |  | 0 | for my $record (@sorted_record_set) { | 
| 844 | 0 |  |  |  |  | 0 | $record->name($domain); | 
| 845 | 0 |  |  |  |  | 0 | $record->txtdata( 'v=spf1 ' . $record->txtdata . ' ~all' ); | 
| 846 |  |  |  |  |  |  |  | 
| 847 | 0 |  |  |  |  | 0 | my $string = $self->_normalize_record_name( $record->string ); | 
| 848 | 0 |  |  |  |  | 0 | push @record_strings, $string; | 
| 849 |  |  |  |  |  |  | } | 
| 850 | 0 |  |  |  |  | 0 | return \@record_strings; | 
| 851 |  |  |  |  |  |  | } | 
| 852 |  |  |  |  |  |  |  | 
| 853 |  |  |  |  |  |  | =head2 _normalize_record_name | 
| 854 |  |  |  |  |  |  |  | 
| 855 |  |  |  |  |  |  | L<Net::DNS> uses fully qualified record names, so that new SPF records | 
| 856 |  |  |  |  |  |  | will be named *.domain.com, and domain.com, instead of * and @. I prefer | 
| 857 |  |  |  |  |  |  | the symbols. This code replaces the fully qualified record names with symbols. | 
| 858 |  |  |  |  |  |  |  | 
| 859 |  |  |  |  |  |  | =cut | 
| 860 |  |  |  |  |  |  |  | 
| 861 |  |  |  |  |  |  | sub _normalize_record_name { | 
| 862 | 18 |  |  | 18 |  | 3935 | my ( $self, $record ) = @_; | 
| 863 |  |  |  |  |  |  |  | 
| 864 | 18 |  |  |  |  | 93 | $record =~ /(.+?)\s/; | 
| 865 | 18 |  |  |  |  | 44 | my $original_name = $1; | 
| 866 | 18 |  |  |  |  | 401 | my $origin        = $self->origin; | 
| 867 |  |  |  |  |  |  |  | 
| 868 | 18 |  |  |  |  | 26 | my $name; | 
| 869 |  |  |  |  |  |  |  | 
| 870 | 18 | 100 |  |  |  | 206 | if ( $original_name =~ /^$origin(.?)$/ ) { | 
|  |  | 50 |  |  |  |  |  | 
|  |  | 100 |  |  |  |  |  | 
| 871 | 3 |  |  |  |  | 7 | $name = '@'; | 
| 872 |  |  |  |  |  |  | } elsif ( $original_name =~ /^\.$/ ) { | 
| 873 | 0 |  |  |  |  | 0 | $name = '@'; | 
| 874 |  |  |  |  |  |  | } elsif ( $original_name =~ /^\*/ ) { | 
| 875 | 3 |  |  |  |  | 9 | $name = '*'; | 
| 876 |  |  |  |  |  |  | } else { | 
| 877 | 12 |  |  |  |  | 23 | $name = $original_name; | 
| 878 |  |  |  |  |  |  | } | 
| 879 | 18 |  |  |  |  | 321 | $record =~ s/\Q$original_name\E/$name/g; | 
| 880 | 18 |  |  |  |  | 105 | $record =~ s/\n//g; | 
| 881 | 18 |  |  |  |  | 149 | $record =~ s/(\(|\))//g; | 
| 882 | 18 |  |  |  |  | 98 | $record =~ s/\t\s/\t/g; | 
| 883 | 18 |  |  |  |  | 64 | $record =~ s/\s\t/\t/g; | 
| 884 | 18 |  |  |  |  | 49 | $record =~ s/\t\t/\t/g; | 
| 885 | 18 |  |  |  |  | 97 | $record =~ s/\t/    /g; | 
| 886 | 18 |  |  |  |  | 226 | $record =~ s/\s/ /g; | 
| 887 | 18 |  |  |  |  | 39 | $record = $record."\n"; | 
| 888 | 18 |  |  |  |  | 69 | return $record; | 
| 889 |  |  |  |  |  |  | } | 
| 890 |  |  |  |  |  |  |  | 
| 891 |  |  |  |  |  |  | =head2 _get_multiple_record_strings | 
| 892 |  |  |  |  |  |  |  | 
| 893 |  |  |  |  |  |  | Whereas a single new SPF record needs to be concatenated from | 
| 894 |  |  |  |  |  |  | the stringified L<Net::DNS::RR::TXT>s, and have the trailing | 
| 895 |  |  |  |  |  |  | ~all added, multiple new SPF records do not need that. They need to be given | 
| 896 |  |  |  |  |  |  | special _spf names that will then be included in "master" SPF records, and | 
| 897 |  |  |  |  |  |  | they don't need the trailing ~all. | 
| 898 |  |  |  |  |  |  |  | 
| 899 |  |  |  |  |  |  | =cut | 
| 900 |  |  |  |  |  |  |  | 
| 901 |  |  |  |  |  |  | sub _get_multiple_record_strings { | 
| 902 | 3 |  |  | 3 |  | 8 | my ( $self, $values, $start_index ) = @_; | 
| 903 | 3 |  |  |  |  | 63 | my $origin = $self->origin; | 
| 904 |  |  |  |  |  |  |  | 
| 905 | 3 |  |  |  |  | 7 | my @record_strings = (); | 
| 906 |  |  |  |  |  |  |  | 
| 907 | 3 |  |  |  |  | 7 | my @containing_records = (); | 
| 908 |  |  |  |  |  |  |  | 
| 909 | 3 |  | 50 |  |  | 24 | my $i = $start_index // 1; | 
| 910 | 3 |  |  |  |  | 20 | for my $value (@$values) { | 
| 911 | 11 |  |  |  |  | 286 | push @containing_records, | 
| 912 |  |  |  |  |  |  | new Net::DNS::RR( | 
| 913 |  |  |  |  |  |  | type    => 'TXT', | 
| 914 |  |  |  |  |  |  | name    => "_spf$i.$origin", | 
| 915 |  |  |  |  |  |  | class   => $self->_record_class, | 
| 916 |  |  |  |  |  |  | ttl     => $self->ttl, | 
| 917 |  |  |  |  |  |  | txtdata => 'v=spf1 ' . $value, | 
| 918 |  |  |  |  |  |  | ); | 
| 919 | 11 |  |  |  |  | 2085 | $i++; | 
| 920 |  |  |  |  |  |  | } | 
| 921 |  |  |  |  |  |  |  | 
| 922 |  |  |  |  |  |  | @record_strings = map { | 
| 923 | 11 |  |  |  |  | 1221 | $self->_normalize_record_name($_->string) | 
| 924 |  |  |  |  |  |  | } sort { | 
| 925 | 3 |  |  |  |  | 13 | $a->string cmp $b->string | 
|  | 14 |  |  |  |  | 6085 |  | 
| 926 |  |  |  |  |  |  | } @containing_records; | 
| 927 |  |  |  |  |  |  |  | 
| 928 | 3 |  |  |  |  | 29 | return \@record_strings; | 
| 929 |  |  |  |  |  |  | } | 
| 930 |  |  |  |  |  |  |  | 
| 931 |  |  |  |  |  |  | =head2 _get_master_record_strings | 
| 932 |  |  |  |  |  |  |  | 
| 933 |  |  |  |  |  |  | Create our "master" SPF records that include the split _spf records created | 
| 934 |  |  |  |  |  |  | in _get_multiple_record_strings, e.g., | 
| 935 |  |  |  |  |  |  |  | 
| 936 |  |  |  |  |  |  | *    600    IN    TXT    "v=spf1 include:_spf1.test_zone.com include:_spf2.test_zone.com ~all" | 
| 937 |  |  |  |  |  |  |  | 
| 938 |  |  |  |  |  |  | =cut | 
| 939 |  |  |  |  |  |  |  | 
| 940 |  |  |  |  |  |  | sub _get_master_record_strings { | 
| 941 | 3 |  |  | 3 |  | 11 | my ( $self, $values, $domains ) = @_; | 
| 942 |  |  |  |  |  |  |  | 
| 943 | 3 |  |  |  |  | 73 | (my $origin         = $self->origin) =~ s/\.$//g; | 
| 944 | 3 |  |  |  |  | 69 | my @record_strings = (); | 
| 945 |  |  |  |  |  |  |  | 
| 946 | 3 |  |  |  |  | 8 | my @containing_records = (); | 
| 947 |  |  |  |  |  |  |  | 
| 948 | 3 |  |  |  |  | 15 | my $master_records = [ map {"include:_spf$_.$origin"} ( 1 .. scalar(@$values)) ]; | 
|  | 11 |  |  |  |  | 35 |  | 
| 949 | 3 |  |  |  |  | 14 | my $master_record = join(' ', @$master_records); | 
| 950 |  |  |  |  |  |  |  | 
| 951 |  |  |  |  |  |  | # If our master record will be too long, split it into multiple strings | 
| 952 | 3 | 50 |  |  |  | 98 | if (length($master_record) > $self->maximum_record_length) { | 
| 953 |  |  |  |  |  |  |  | 
| 954 | 0 |  |  |  |  | 0 | my $new_master_record_partitions = $self->_new_records_from_partition( | 
| 955 |  |  |  |  |  |  | "master", | 
| 956 |  |  |  |  |  |  | $master_records, | 
| 957 |  |  |  |  |  |  | 1, # Just return raw partitions | 
| 958 |  |  |  |  |  |  | ); | 
| 959 |  |  |  |  |  |  |  | 
| 960 | 0 |  |  |  |  | 0 | my @master_record_strings = (); | 
| 961 | 0 |  |  |  |  | 0 | my $i = 0; | 
| 962 | 0 |  |  |  |  | 0 | for my $partition (@$new_master_record_partitions) { | 
| 963 | 0 |  |  |  |  | 0 | my @master_record_partition = @$master_records[$i .. ($i + $#{$partition})]; | 
|  | 0 |  |  |  |  | 0 |  | 
| 964 | 0 |  |  |  |  | 0 | push @master_record_strings, join(' ', @master_record_partition); | 
| 965 | 0 |  |  |  |  | 0 | $i += scalar(@$partition); | 
| 966 |  |  |  |  |  |  | } | 
| 967 | 0 |  |  |  |  | 0 | $master_record_strings[0] = 'v=spf1 '. $master_record_strings[0]; | 
| 968 | 0 |  |  |  |  | 0 | $master_record_strings[-1] = $master_record_strings[-1].' ~all'; | 
| 969 | 0 |  |  |  |  | 0 | my $master_record_string = ''; | 
| 970 | 0 |  |  |  |  | 0 | my $index = 0; | 
| 971 | 0 |  |  |  |  | 0 | for my $master_record (@master_record_strings) { | 
| 972 | 0 | 0 |  |  |  | 0 | $master_record = " ".$master_record unless $index == 0; | 
| 973 | 0 |  |  |  |  | 0 | $master_record_string .= qq|"$master_record"|; | 
| 974 | 0 |  |  |  |  | 0 | $index++; | 
| 975 |  |  |  |  |  |  | } | 
| 976 |  |  |  |  |  |  |  | 
| 977 | 0 |  |  |  |  | 0 | for my $domain (@$domains) { | 
| 978 |  |  |  |  |  |  |  | 
| 979 | 0 |  |  |  |  | 0 | push @containing_records, | 
| 980 |  |  |  |  |  |  | new Net::DNS::RR( | 
| 981 |  |  |  |  |  |  | type    => 'TXT', | 
| 982 |  |  |  |  |  |  | name    => $domain, | 
| 983 |  |  |  |  |  |  | class   => $self->_record_class, | 
| 984 |  |  |  |  |  |  | ttl     => $self->ttl, | 
| 985 |  |  |  |  |  |  | txtdata => \@master_record_strings, | 
| 986 |  |  |  |  |  |  | ); | 
| 987 |  |  |  |  |  |  | } | 
| 988 |  |  |  |  |  |  |  | 
| 989 |  |  |  |  |  |  | # Otherwise, proceed as normal | 
| 990 |  |  |  |  |  |  | } else { | 
| 991 |  |  |  |  |  |  |  | 
| 992 | 3 |  |  |  |  | 38 | for my $domain (@$domains) { | 
| 993 |  |  |  |  |  |  |  | 
| 994 |  |  |  |  |  |  | push @containing_records, | 
| 995 |  |  |  |  |  |  | new Net::DNS::RR( | 
| 996 |  |  |  |  |  |  | type    => 'TXT', | 
| 997 |  |  |  |  |  |  | name    => $domain, | 
| 998 |  |  |  |  |  |  | class   => $self->_record_class, | 
| 999 |  |  |  |  |  |  | ttl     => $self->ttl, | 
| 1000 |  |  |  |  |  |  | txtdata => 'v=spf1 ' . (join( | 
| 1001 |  |  |  |  |  |  | ' ', | 
| 1002 | 7 |  |  |  |  | 718 | ( map {"include:_spf$_.$origin"} ( 1 .. scalar(@$values) ) ) | 
|  | 27 |  |  |  |  | 87 |  | 
| 1003 |  |  |  |  |  |  | )) . ' ~all', | 
| 1004 |  |  |  |  |  |  | ); | 
| 1005 |  |  |  |  |  |  | } | 
| 1006 |  |  |  |  |  |  |  | 
| 1007 |  |  |  |  |  |  | } | 
| 1008 |  |  |  |  |  |  |  | 
| 1009 |  |  |  |  |  |  | @record_strings = map { | 
| 1010 | 7 |  |  |  |  | 1475 | $self->_normalize_record_name($_->string) | 
| 1011 |  |  |  |  |  |  | } sort { | 
| 1012 | 3 |  |  |  |  | 368 | $a->string cmp $b->string | 
|  | 5 |  |  |  |  | 1181 |  | 
| 1013 |  |  |  |  |  |  | } @containing_records; | 
| 1014 |  |  |  |  |  |  |  | 
| 1015 | 3 |  |  |  |  | 23 | return \@record_strings; | 
| 1016 |  |  |  |  |  |  | } | 
| 1017 |  |  |  |  |  |  |  | 
| 1018 |  |  |  |  |  |  | =head2 _new_records_lines | 
| 1019 |  |  |  |  |  |  |  | 
| 1020 |  |  |  |  |  |  | Assemble the new DNS zonefile from the lines of the original, | 
| 1021 |  |  |  |  |  |  | comment out the old SPF records, add in the new lines, and append the | 
| 1022 |  |  |  |  |  |  | end of the original. | 
| 1023 |  |  |  |  |  |  |  | 
| 1024 |  |  |  |  |  |  | =cut | 
| 1025 |  |  |  |  |  |  |  | 
| 1026 |  |  |  |  |  |  | sub _new_records_lines { | 
| 1027 | 3 |  |  | 3 |  | 8 | my $self           = shift; | 
| 1028 | 3 | 50 |  |  |  | 6 | my %new_records    = %{ $self->new_spf_records || {} }; | 
|  | 3 |  |  |  |  | 13 |  | 
| 1029 | 3 |  |  |  |  | 10 | my @record_strings = (); | 
| 1030 |  |  |  |  |  |  |  | 
| 1031 |  |  |  |  |  |  | # Make a list of the unique records in case we need it. | 
| 1032 | 3 |  |  |  |  | 18 | my @autosplit = (); | 
| 1033 | 3 |  |  |  |  | 9 | for my $domain ( keys %new_records ) { | 
| 1034 | 7 |  |  |  |  | 82 | for my $record_set ( @{ $new_records{$domain} } ) { | 
|  | 7 |  |  |  |  | 17 |  | 
| 1035 | 27 | 50 |  |  |  | 531 | if ( ref($record_set) eq 'ARRAY' ) { | 
| 1036 | 27 |  |  |  |  | 49 | for my $record (@$record_set) { | 
| 1037 | 27 |  |  |  |  | 54 | push @autosplit, $record->txtdata; | 
| 1038 |  |  |  |  |  |  | } | 
| 1039 |  |  |  |  |  |  | } else { | 
| 1040 | 0 |  |  |  |  | 0 | push @autosplit, $record_set->txtdata; | 
| 1041 |  |  |  |  |  |  | } | 
| 1042 |  |  |  |  |  |  | } | 
| 1043 |  |  |  |  |  |  | } | 
| 1044 | 3 |  |  |  |  | 102 | @autosplit = uniq @autosplit; | 
| 1045 |  |  |  |  |  |  |  | 
| 1046 |  |  |  |  |  |  | # If there are any autosplit SPF records, we just do that right away. | 
| 1047 |  |  |  |  |  |  | # This test is kind of nasty. | 
| 1048 |  |  |  |  |  |  | my $make_autosplit_records = grep { | 
| 1049 | 3 |  |  |  |  | 19 | defined( ${ $new_records{$_} }[0] ) | 
|  | 7 |  |  |  |  | 31 |  | 
| 1050 | 7 | 50 |  |  |  | 11 | && ref( ${ $new_records{$_} }[0] ) eq 'ARRAY' | 
|  | 7 |  |  |  |  | 31 |  | 
| 1051 |  |  |  |  |  |  | } sort keys %new_records; | 
| 1052 | 3 | 50 |  |  |  | 11 | if ($make_autosplit_records) { | 
| 1053 | 3 |  |  |  |  | 17 | my $master_record_strings | 
| 1054 |  |  |  |  |  |  | = $self->_get_master_record_strings( \@autosplit, | 
| 1055 |  |  |  |  |  |  | [ keys %new_records ] ); | 
| 1056 | 3 |  |  |  |  | 14 | my $record_strings | 
| 1057 |  |  |  |  |  |  | = $self->_get_multiple_record_strings( \@autosplit ); | 
| 1058 | 3 |  |  |  |  | 11 | push @record_strings, @$master_record_strings; | 
| 1059 | 3 |  |  |  |  | 14 | push @record_strings, @$record_strings; | 
| 1060 |  |  |  |  |  |  | } else { | 
| 1061 | 0 |  |  |  |  | 0 | for my $domain ( sort keys %new_records ) { | 
| 1062 |  |  |  |  |  |  | my $record_string = $self->_get_single_record_string( | 
| 1063 |  |  |  |  |  |  | $domain, | 
| 1064 | 0 |  |  |  |  | 0 | $new_records{$domain}, | 
| 1065 |  |  |  |  |  |  | ); | 
| 1066 | 0 |  |  |  |  | 0 | push @record_strings, @$record_string; | 
| 1067 |  |  |  |  |  |  | } | 
| 1068 |  |  |  |  |  |  | } | 
| 1069 | 3 |  |  |  |  | 77 | my @original_lines = $self->_input_file->slurp; | 
| 1070 | 3 |  |  |  |  | 2333 | my @new_lines      = (); | 
| 1071 | 3 |  |  |  |  | 9 | my @spf_indices; | 
| 1072 | 3 |  |  |  |  | 8 | my $i = 0; | 
| 1073 | 3 |  |  |  |  | 12 | LINE: for my $line (@original_lines) { | 
| 1074 | 44 | 100 |  |  |  | 159 | if ( $line =~ /^[^;].+?v=spf1/ ) { | 
| 1075 | 20 |  |  |  |  | 45 | push @spf_indices, $i; | 
| 1076 | 20 |  |  |  |  | 58 | $line = ";" . $line; | 
| 1077 |  |  |  |  |  |  | } | 
| 1078 | 44 |  |  |  |  | 92 | push @new_lines, $line; | 
| 1079 | 44 |  |  |  |  | 73 | $i++; | 
| 1080 |  |  |  |  |  |  | } | 
| 1081 | 3 |  |  |  |  | 19 | my @first_segment = @new_lines[ 0 .. $spf_indices[-1] ]; | 
| 1082 | 3 |  |  |  |  | 18 | my @last_segment  = @new_lines[ $spf_indices[-1] + 1 .. $#new_lines ]; | 
| 1083 | 3 |  |  |  |  | 18 | my @final_lines   = ( @first_segment, @record_strings, @last_segment ); | 
| 1084 |  |  |  |  |  |  |  | 
| 1085 | 3 |  |  |  |  | 10 | for my $line (@final_lines) { | 
| 1086 | 62 |  |  |  |  | 123 | $line =~ s/\t/    /g; | 
| 1087 | 62 |  |  |  |  | 102 | $line =~ s/\n\s+/\n/g; | 
| 1088 | 62 |  |  |  |  | 246 | $line =~ s/\s+\n/\n/g; | 
| 1089 | 62 |  |  |  |  | 204 | $line =~ s/\n+/\n/g; | 
| 1090 |  |  |  |  |  |  | } | 
| 1091 | 3 |  |  |  |  | 74 | return \@final_lines; | 
| 1092 |  |  |  |  |  |  | } | 
| 1093 |  |  |  |  |  |  |  | 
| 1094 |  |  |  |  |  |  | __PACKAGE__->meta->make_immutable; | 
| 1095 |  |  |  |  |  |  | __PACKAGE__->new_with_options->run unless caller; | 
| 1096 |  |  |  |  |  |  |  | 
| 1097 |  |  |  |  |  |  | 1; | 
| 1098 |  |  |  |  |  |  |  | 
| 1099 |  |  |  |  |  |  | __END__ | 
| 1100 |  |  |  |  |  |  |  | 
| 1101 |  |  |  |  |  |  | =head1 AUTHOR | 
| 1102 |  |  |  |  |  |  |  | 
| 1103 |  |  |  |  |  |  | Amiri Barksdale E<lt>amiri@campusexplorer.comE<gt> | 
| 1104 |  |  |  |  |  |  |  | 
| 1105 |  |  |  |  |  |  | =head2 CONTRIBUTORS | 
| 1106 |  |  |  |  |  |  |  | 
| 1107 |  |  |  |  |  |  | Neil Bowers E<lt>neil@bowers.comE<gt> | 
| 1108 |  |  |  |  |  |  |  | 
| 1109 |  |  |  |  |  |  | Marc Bradshaw E<lt>marc@marcbradshaw.netE<gt> | 
| 1110 |  |  |  |  |  |  |  | 
| 1111 |  |  |  |  |  |  | Karen Etheridge E<lt>ether@cpan.orgE<gt> | 
| 1112 |  |  |  |  |  |  |  | 
| 1113 |  |  |  |  |  |  | Chris Weyl E<lt>cweyl@campusexplorer.comE<gt> | 
| 1114 |  |  |  |  |  |  |  | 
| 1115 |  |  |  |  |  |  | =head1 COPYRIGHT | 
| 1116 |  |  |  |  |  |  |  | 
| 1117 |  |  |  |  |  |  | Copyright (c) 2015 Campus Explorer, Inc. | 
| 1118 |  |  |  |  |  |  |  | 
| 1119 |  |  |  |  |  |  | =head1 LICENSE | 
| 1120 |  |  |  |  |  |  |  | 
| 1121 |  |  |  |  |  |  | This library is free software; you can redistribute it and/or modify | 
| 1122 |  |  |  |  |  |  | it under the same terms as Perl itself. | 
| 1123 |  |  |  |  |  |  |  | 
| 1124 |  |  |  |  |  |  | =head1 SEE ALSO | 
| 1125 |  |  |  |  |  |  |  | 
| 1126 |  |  |  |  |  |  | L<Net::DNS> | 
| 1127 |  |  |  |  |  |  |  | 
| 1128 |  |  |  |  |  |  | L<Net::DNS::RR::TXT> | 
| 1129 |  |  |  |  |  |  |  | 
| 1130 |  |  |  |  |  |  | L<MooseX::Getopt> | 
| 1131 |  |  |  |  |  |  |  | 
| 1132 |  |  |  |  |  |  | =cut |