File Coverage

blib/lib/Mail/Address/Tagged.pm
Criterion Covered Total %
statement 128 131 97.7
branch 42 68 61.7
condition 11 23 47.8
subroutine 32 32 100.0
pod 23 23 100.0
total 236 277 85.2


line stmt bran cond sub pod time code
1             package Mail::Address::Tagged;
2              
3             #------------------------------------------------------------------------------
4             #
5             # Copyright (c) Andrew Wilson 2001
6             #
7             #------------------------------------------------------------------------------
8             #
9             # Modification History
10             #
11             # Auth Date AECN Description
12             # ------ --------- ---------- ----------------------------------------------
13             # andrew 16 Sep 01 1.0.1001 Expanded the documentation
14             # andrew 12 Sep 01 1.0.1000 Wrote this.
15             #------------------------------------------------------------------------------
16              
17             $Mail::Address::Tagged::VERSION = '0.01';
18              
19             =head1 NAME
20              
21             Mail::Address::Tagged - construct and validate email addresses with HMAC verification
22              
23             =head1 SYNOPSIS
24              
25             #---------------------------------------------------------------------------
26             # methods to use when constructing an address
27             #---------------------------------------------------------------------------
28              
29             my $tag = Mail::Address::Tagged->new(key => $key,
30             email => 'foo@bar.com');
31              
32             my $seconds = $tag->set_valid_for($period);
33             my $keyword = $tag->set_keyword('wibble');
34              
35             my $tag = Mail::Address::Tagged->new(key => $key,
36             user => 'foo',
37             host => 'bar.com',
38             valid_for => '10d',
39             keyword => 'wibble');
40              
41             $my $address = $tag->make_confirm(time => $unix_time,
42             pid => $pid,
43             keyword => $keyword);
44              
45             $my $address = $tag->make_confirm({time => $unix_time,
46             pid => $pid,});
47              
48             $my $address = $tag->make_dated;
49             $my $address = $tag->make_sender($address_to_receive_from);
50              
51              
52             #---------------------------------------------------------------------------
53             # methods to use when validating an address
54             #---------------------------------------------------------------------------
55              
56             my $tag = Mail::Address::Tagged->for_received(key => $key,
57             received => $address,
58             sender => $sender,);
59              
60             if ($tag->valid) {
61             ...
62             }
63              
64             my $still_valid = ! $tag->expired;
65              
66              
67             #---------------------------------------------------------------------------
68             # Methods for accessing the objects internals (these will probably
69             # be mainly used internally)
70             #---------------------------------------------------------------------------
71              
72             my $email = $tag->key;
73             my $address = $tag->email;
74             my $user = $tag->user;
75             my $host = $tag->host;
76             my $seconds = $tag->valid_for;
77             my $keyword = $tag->keyword
78             my $address = $tag->wrap('text_to_wrap');
79              
80             my $hmac = $tag->conf_mac(time => $time,
81             pid => $pid);
82              
83             my $hmac = $tag->conf_mac(time => $time,
84             pid => $pid,
85             keyword => $value);
86              
87             my $hmac = $tag->single_value_mac($date);
88             my $hmac = $tag->single_value_mac($sender);
89              
90              
91             # only set by for_received
92              
93             my $received_time = $tag->candidate_time
94             my $received_pid = $tag->candidate_pid
95             my $received_HMAC = $tag->candidate_mac
96             my $address_type = $tag->type
97             my $correspondent = $tag->sender
98              
99             =head1 DESCRIPTION
100              
101             This module implements an object that can generate and validate tagged
102             email addresses. These are designed to be used primarily in anti-spam
103             applications.
104              
105             The addresses generated all carry extra information, such as the date
106             when they expire, who may use them to send you mail etc. A
107             cryptocraphic hash of this extra information is also included in in
108             the address. This Hashed Message Authenticaion Code (HMAC RFC 2104)
109             is your guarantee that the information contained in the address has
110             not been tampered with.
111              
112             This module can generate and validate three different types of tagged
113             address. They are all extensions of a users normal email address and
114             are constructed in a similar manner. Where the normal address is
115             user@host.com, the tagged address takes the form
116             user-tagtype-tag@host.com.
117              
118             The three supported address types are confirm, dated and sender.
119              
120             Confirm addresses must contain a time (in unixtime) and the process id
121             of the process that generated them, they may also optionally contain a
122             keyword. They include the time and process id so that the system can
123             deal with more than one message a second. Addresses of this type are
124             used to request verification that a message should be delivered. The
125             point being that automated mailers are unlikely to be able to respond
126             in this way so spam will not get through. If a persistant spammer
127             does reach your mailbox, then you can always black list the address.
128             The keyword when it is supplied is the type of confirmation being
129             asked for. All three bits of information are combined and a
130             cryptographic hash is taken of the result, these bits of info are then
131             combined like this.
132              
133             user-confirm-keyword.time.pid.HMAC@host.com
134              
135             When mail like this is received, the bits can be separated out and a
136             new HMAC generated, if it matches the one in the address, then this is
137             a valid address.
138              
139             Dated addresses have an expiry time and are used to accept mail up to
140             a given time. They end up in the format
141              
142             user-dated-time.HMAC@host.com
143              
144             and are validated in the same manner
145              
146             The third type of supported address is sender, this takes the form
147              
148             user-sender-HMAC@host.com
149              
150             the address that this will be accepted from is included in the HMAC
151             generation. When mail of this form comes in the sending address can
152             be checked against the HMAC if they don't match then appropriate
153             action can be taken (disposal or confirmation request etc.).
154              
155             =cut
156              
157 1     1   26697 use strict;
  1         3  
  1         34  
158 1     1   666 use Digest::HMAC;
  1         570  
  1         53  
159 1     1   921 use Digest::SHA1;
  1         2325  
  1         3807  
160              
161             =head1 FACTORY METHODS
162              
163             =head2 new
164              
165             my $tag = Mail::Address::Tagged->new(key => $key,
166             email => 'foo@bar.com');
167              
168             my $tag = Mail::Address::Tagged->new(key => $key,
169             user => 'foo',
170             host => 'bar.com',
171             valid_for => '10d',
172             keyword => 'wibble');
173              
174             Pass this your key and email address it will constrct an object for
175             making tagged addresses. The email address may be complete
176             e.g. foo@bar.com or supplied as user and host.
177              
178             There are also various optional parametes that may be supplied to new.
179              
180             You may pass the valid_for attribute to control how long dated address
181             will be active for, if not supplied it defaults to 5 days (see
182             set_valid_for documentation)
183              
184             The keyword parameter is used when generating dated addresses. It is
185             included in the string and allows for the generation of addresses like
186             this:
187              
188             name-confirm-keyword.12344556.123.ABCDEF@host.org
189              
190             If not supplied it defaults to the empty string.
191              
192             =cut
193              
194             sub new {
195 18     18 1 3179 my $class = shift;
196              
197 18 50       93 my %arg = ref($_[0]) eq 'HASH' ? %{ $_[0] } : @_;
  0         0  
198 18 50       54 exists $arg{key} or return undef;
199 18 50 33     211 exists $arg{email} or (exists $arg{user} and exists $arg{host}) or return undef;
      66        
200              
201 18         49 my $self = bless {}, $class;
202 18 50       60 $self->_init(%arg) or return undef;
203              
204 18         68 return $self;
205             }
206              
207             sub _init {
208 18     18   26 my $self = shift;
209              
210 18         22 %{$self} = (@_);
  18         69  
211              
212 18 100       43 if (exists $self->{email}) {
213 5         16 @{$self}{'user', 'host'} = split "@", $self->{email};
  5         16  
214             } else {
215 13         41 $self->{email} = $self->{user} . '@' . $self->{host};
216             }
217              
218 18   100     75 $self->{keyword} ||= "";
219 18         65 $self->set_valid_for($self->{valid_for});
220             }
221              
222             =head2 for_received
223              
224             my $tag = Mail::Address::Tagged->for_received(key => $key,
225             received => $address,
226             sender => $sender,);
227              
228             This constructs an object based on the received address. It will
229             break it down into it's component parts, these may then be queried and
230             checked for validity.
231              
232             =cut
233              
234             sub for_received {
235 8     8 1 28 my $class = shift;
236              
237 8 50       43 my %arg = ref($_[0]) eq 'HASH' ? %{ $_[0] } : @_;
  0         0  
238 8 50 33     64 exists $arg{key} and
      33        
239             exists $arg{address} and
240             exists $arg{sender} or return undef;
241              
242 8         25 my ($user_part, $host) = split "@", $arg{address};
243 8         22 my ($user, $type, $data) = split "-", $user_part;
244              
245             # if we don't know what type of address this is then there's no
246             # point continuing
247 8 50 33     59 defined $type and $type =~ /^(confirm|dated|sender)$/ or return undef;
248              
249             # need to have an HMAC
250 8 50       18 defined $data or return undef;
251              
252             # now set up the object
253 8         26 my $self = $class->new(key => $arg{key},
254             user => $user,
255             host => $host,);
256 8 50       18 defined $self or return undef;
257              
258 8         18 $self->_set_type($type);
259 8         21 $self->_set_sender($arg{sender});
260              
261 8 100       20 if ($type eq 'confirm') {
    100          
262              
263 2         15 my ($keyword, $date, $pid, $mac) = split m#\.#, $data;
264 2         6 $self->set_keyword($keyword);
265 2         5 $self->_set_candidate_time($date);
266 2         7 $self->_set_candidate_pid($pid);
267 2         5 $self->_set_candidate_mac($mac);
268              
269             } elsif ($type eq 'dated') {
270              
271 3         8 my ($date, $mac) = split m#\.#, $data;
272 3         8 $self->_set_candidate_time($date);
273 3         7 $self->_set_candidate_mac($mac);
274              
275             } else {
276 3         8 $self->_set_candidate_mac($data);
277             }
278              
279 8         27 return $self;
280             }
281              
282             =head1 INSTANCE METHODS - Construction
283              
284             =head2 set_valid_for
285              
286             my $seconds = $tag->set_valid_for($period);
287              
288             This allows one to set the time period that dated addresses will be
289             valid for. Times periods are specified as a string which consists of
290             a positive integer folowed by a period modifier. Valid modifiers are:
291              
292             =over 4
293              
294             =item Y year
295              
296             =item M month
297              
298             =item w week
299              
300             =item d day
301              
302             =item h hours
303              
304             =item s seconds
305              
306             =back
307              
308             =cut
309              
310             my %Conv = ('Y' => 60 * 60 * 24 * 365,
311             'M' => 60 * 60 * 24 * 30,
312             'w' => 60 * 60 * 24 * 7,
313             'd' => 60 * 60 * 24,
314             'h' => 60 * 60,
315             'm' => 60,
316             's' => 1,);
317              
318             sub set_valid_for {
319 30     30 1 60 my $self = shift;
320              
321 30         53 my $period = shift;
322 30 100 66     125 if (defined $period and $period =~ /^(\d+)([YMwdhms])/) {
323 10         25 my $num = $1;
324 10         16 my $unit = $2;
325              
326 10         32 $self->{valid_for} = $Conv{$unit} * $num;
327             } else {
328 20         53 $self->{valid_for} = $Conv{d} * 5;
329             }
330 30         101 return $self->{valid_for};
331             }
332              
333             =head2 set_keyword
334              
335             my $keyword = $tag->set_keyword('wibble');
336              
337             Set the keyword of this object. Returns the new keyword.
338              
339             =cut
340              
341             sub set_keyword {
342 12     12 1 42 my $self = shift;
343              
344 12         13 my $new = shift;
345 12 100       36 defined $new or return $self->keyword;
346 10         31 $self->{keyword} = $new;
347             }
348              
349             =head2 make_confirm
350              
351             $my $address = $tag->make_confirm(time => $unix_time,
352             pid => $pid,
353             keyword => $keyword);
354              
355             $my $address = $tag->make_confirm({time => $unix_time,
356             pid => $pid,});
357              
358             Return an address that will be used to confirm an email from an
359             untrusted source. You must pass time and processid, you may also
360             optionally pass a keyword.
361              
362             =cut
363              
364             sub make_confirm {
365 6     6 1 2975 my $self = shift;
366              
367 6 50       18 my %arg = ref($_[0]) eq 'HASH' ? %{ $_[0] } : @_;
  6         25  
368              
369 6 50       21 exists $arg{time} or carp('no time passed to make_confirm');
370 6 50       16 exists $arg{pid} or carp('no pid passed to make_confirm');
371              
372 6 50       24 $self->set_keyword($arg{keyword}) if exists $arg{keyword};
373 6         13 my $keyword = $self->keyword;
374              
375             # generate the HMAC
376 6         7 my $details;
377 6         12 @{$details}{'time', 'pid'} = @arg{'time', 'pid'};
  6         21  
378 6         16 my $mac = $self->conf_mac($details);
379              
380 6         182 return $self->wrap("-confirm-$keyword.$arg{time}.$arg{pid}." . $mac);
381             }
382              
383             =head2 make_dated
384              
385             $my $address = $tag->make_dated;
386              
387             Return an address that will be allowed to send us mail for the default
388             period of time from now.
389              
390             =cut
391              
392             sub make_dated {
393 5     5 1 1344 my $self = shift;
394              
395 5         7 my $date = shift;
396 5 100       14 defined $date or $date = time();
397 5         11 $date += $self->valid_for;
398              
399 5         11 my $mac = $self->single_value_mac($date);
400              
401 5         115 return $self->wrap("-dated-". $date . '.' . $mac);
402             }
403              
404             =head2 make_sender
405              
406             $my $address = $tag->make_sender($address_to_receive_from);
407              
408             Return an address that will only accept mail if it is sent from one
409             particular sender address.
410              
411             =cut
412              
413             sub make_sender {
414 4     4 1 850 my $self = shift;
415              
416 4         9 my $sender = lc(shift);
417 4         9 my $mac = $self->single_value_mac($sender);
418              
419 4         77 return $self->wrap("-sender-" . $mac);
420             }
421              
422             =head1 INSTANCE METHODS - querying
423              
424             These methods are only useful on objects constructed with the
425             for_received method. They will tell you whether the address is
426             genuine and whether it has expired (for dated addresses).
427              
428             =head2 valid
429              
430             if ($tag->valid) {
431             ...
432             }
433              
434             This will tell you whether the HMAC matches the details of the address.
435              
436             =cut
437              
438             sub valid {
439 10     10 1 1733 my $self = shift;
440              
441 10 50       210 return undef unless $self->type;
442              
443 10 100       201 if ($self->type eq "confirm") {
    100          
    50          
444              
445 2         6 my $mac = $self->conf_mac({time => $self->candidate_time,
446             pid => $self->candidate_pid});
447 2         41 return $mac eq $self->candidate_mac;
448              
449             } elsif ($self->type eq "dated") {
450              
451 5         12 my $mac = $self->single_value_mac($self->candidate_time);
452 5         95 return $mac eq $self->candidate_mac;
453              
454             } elsif ($self->type eq "sender") {
455              
456 3         7 my $mac = $self->single_value_mac($self->sender);
457 3         55 return $mac eq $self->candidate_mac;
458              
459             }
460              
461 0         0 return 0;
462             }
463              
464             =head2 expired
465              
466             my $still_valid = ! $tag->expired;
467              
468             This will tell you whether dated addresses have expired.
469              
470             =cut
471              
472             sub expired {
473 2     2 1 44 my $self = shift;
474              
475 2 50 33     6 return undef unless $self->candidate_time and $self->valid;
476 2         6 return $self->candidate_time > time();
477             }
478              
479             =head1 VALIDATION METHODS (mostly only for internal use)
480              
481             =head2 conf_mac
482              
483             my $hmac = $tag->conf_mac(time => $time,
484             pid => $pid);
485              
486             my $hmac = $tag->conf_mac(time => $time,
487             pid => $pid,
488             keyword => $value);
489              
490             Return the HMAC for the time and pid passed in. The method may also
491             take an optional keyword -> value pairing and if provided this will
492             also be included in the HMAC generation.
493              
494             =cut
495              
496             sub conf_mac {
497 8     8 1 13 my $self = shift;
498              
499 8 50       20 my %arg = ref($_[0]) eq 'HASH' ? %{ $_[0] } : @_;
  8         28  
500 8 50       25 exists $arg{time} or carp('no time passed to conf_mac');
501 8 50       18 exists $arg{pid} or carp('no pid passed to conf_mac');
502              
503 8         22 my $digest = Digest::HMAC->new($self->key, "Digest::SHA1");
504 8         196 $digest->add($arg{time});
505 8         55 $digest->add($arg{pid});
506 8 50       46 $digest->add($self->keyword) if $self->keyword;
507             # we only want the first 6 hex digits of the HMAC (there are 40)
508 8         53 return substr($digest->hexdigest, 0, 6);
509             }
510              
511             =head2 single_value_mac
512              
513             my $hmac = $tag->single_value_mac($date);
514              
515             my $hmac = $tag->single_value_mac($sender);
516              
517             Return the HMAC for the value passed in.
518              
519             =cut
520              
521             sub single_value_mac {
522 17     17 1 21 my $self = shift;
523              
524 17         21 my $value = shift;
525 17 50       34 defined $value or carp('no value passed to single_value_mac');
526              
527 17         34 my $digest = Digest::HMAC->new($self->key, "Digest::SHA1");
528 17         331 $digest->add($value);
529             # we only want the first 6 hex digits of the HMAC (there are 40)
530 17         106 return substr($digest->hexdigest, 0, 6);
531             }
532              
533              
534             =head2 key
535              
536             my $email = $tag->key;
537              
538             Return the cryptographic key that this address is using.
539              
540             =head2 email
541              
542             my $address = $tag->email;
543              
544             This returns the unaltered email address that this object is
545             manipulating.
546              
547             =head2 user
548              
549             my $user = $tag->user;
550              
551             This returns the user portion of the email address that this object is
552             manipulating.
553              
554             =head2 host
555              
556             my $host = $tag->host;
557              
558             This returns the host portion of the email address that this object is
559             manipulating.
560              
561             =cut
562              
563 3     3 1 19 sub email { $_[0]->{email} }
564 19     19 1 88 sub host { $_[0]->{host} }
565 27     27 1 140 sub key { $_[0]->{key} };
566 22     22 1 103 sub user { $_[0]->{user} }
567              
568             =head2 valid_for
569              
570             my $seconds = $tag->valid_for;
571              
572             The number of seconds that a dated email address will be valid for.
573              
574             =cut
575              
576 14     14 1 1438 sub valid_for { $_[0]->{valid_for} }
577              
578             =head2 keyword
579              
580             my $keyword = $tag->keyword
581              
582             If a keyword was supplied to the constructor, this method returns its
583             value.
584              
585             =cut
586              
587 32     32 1 137 sub keyword { $_[0]->{keyword} }
588              
589             =head2 wrap
590              
591             my $address = $hmac->wrap('text_to_wrap');
592              
593             When you call this method it constructs an email address of the form
594              
595             nametext_to_wrap@host
596              
597             that is it wraps its argument in the user and host
598              
599             =cut
600              
601             sub wrap {
602 17     17 1 23 my $self = shift;
603              
604 17         20 my $text = shift;
605 17         37 return $self->user . $text . '@' . $self->host;
606             }
607              
608             # these methods will not form part of the public interface of the module
609             sub _set_candidate_time {
610 5 50   5   17 $_[0]->{candidate_time} = $_[1] if (defined $_[1])
611             };
612              
613             sub _set_candidate_pid {
614 2 50   2   13 $_[0]->{candidate_pid} = $_[1] if (defined $_[1])
615             };
616              
617             sub _set_candidate_mac {
618 8 50   8   29 $_[0]->{candidate_mac} = $_[1] if (defined $_[1])
619             };
620              
621             sub _set_type {
622 8 50   8   28 $_[0]->{type} = $_[1] if (defined $_[1])
623             };
624              
625             sub _set_sender {
626 8 50   8   32 $_[0]->{sender} = $_[1] if (defined $_[1])
627             };
628              
629             =head2 candidate_time
630              
631             my $time = $tag->candidate_time
632              
633             The time in the address. This is only valid when an address of type
634             confirm or dated is being validated. In all other cases it will
635             return undef.
636              
637             =head2 candidate_pid
638              
639             my $received_pid = $tag->candidate_pid
640              
641             the pid in the address. this is only valid when a confirmation
642             address is bing validated. it will return undef at all other times.
643              
644             =head2 candidate_mac
645              
646             my $tag->candidate_mac
647              
648             The HMAC of the address used to construct this object, this will only
649             being valid for objects that have been instantiated to validate an
650             address, it returns undef at all other times.
651              
652             =head2 type
653              
654             $tag->type
655              
656             The type of mail address used to create this object, it is only valid
657             when this object is being used for validation, it will return undef at
658             all other times.
659              
660             =head2 sender
661              
662             $tag->sender
663              
664             The sender argument passed to for_address when constructing an object
665             to validate an address. If the object was not constructed to validate
666             an addrss it will return undef.
667              
668             =cut
669              
670 13     13 1 972 sub candidate_time { $_[0]->{candidate_time} };
671 3     3 1 16 sub candidate_pid { $_[0]->{candidate_pid} };
672 10     10 1 55 sub candidate_mac { $_[0]->{candidate_mac} };
673 35     35 1 1057 sub type { $_[0]->{type} };
674 7     7 1 28 sub sender { $_[0]->{sender} };
675              
676             =head1 BUGS
677              
678             Nothing Known
679              
680             =head1 TODO
681              
682             Nothing Known
683              
684             =cut
685              
686             1;