File Coverage

blib/lib/Mail/Address.pm
Criterion Covered Total %
statement 131 142 92.2
branch 59 80 73.7
condition 48 64 75.0
subroutine 15 18 83.3
pod 9 11 81.8
total 262 315 83.1


line stmt bran cond sub pod time code
1             # Copyrights 1995-2017 by [Mark Overmeer ].
2             # For other contributors see ChangeLog.
3             # See the manual pages for details on the licensing terms.
4             # Pod stripped from pm file by OODoc 2.02.
5             package Mail::Address;
6 3     3   531 use vars '$VERSION';
  3         5  
  3         137  
7             $VERSION = '2.19';
8              
9 3     3   15 use strict;
  3         6  
  3         64  
10              
11 3     3   15 use Carp;
  3         6  
  3         5437  
12              
13             # use locale; removed in version 1.78, because it causes taint problems
14              
15 0     0 0 0 sub Version { our $VERSION }
16              
17              
18              
19             # given a comment, attempt to extract a person's name
20             sub _extract_name
21             { # This function can be called as method as well
22 49 100 66 49   158 my $self = @_ && ref $_[0] ? shift : undef;
23              
24 49 100       96 local $_ = shift
25             or return '';
26              
27             # Using encodings, too hard. See Mail::Message::Field::Full.
28 38 50       78 return '' if m/\=\?.*?\?\=/;
29              
30             # trim whitespace
31 38         72 s/^\s+//;
32 38         91 s/\s+$//;
33 38         91 s/\s+/ /;
34              
35             # Disregard numeric names (e.g. 123456.1234@compuserve.com)
36 38 50       85 return "" if /^[\d ]+$/;
37              
38 38         87 s/^\((.*)\)$/$1/; # remove outermost parenthesis
39 38         76 s/^"(.*)"$/$1/; # remove outer quotation marks
40 38         56 s/\(.*?\)//g; # remove minimal embedded comments
41 38         43 s/\\//g; # remove all escapes
42 38         40 s/^"(.*)"$/$1/; # remove internal quotation marks
43 38         53 s/^([^\s]+) ?, ?(.*)$/$2 $1/; # reverse "Last, First M." if applicable
44 38         47 s/,.*//;
45              
46             # Change casing only when the name contains only upper or only
47             # lower cased characters.
48 38 100 100     148 unless( m/[A-Z]/ && m/[a-z]/ )
49             { # Set the case of the name to first char upper rest lower
50 9         80 s/\b(\w+)/\L\u$1/igo; # Upcase first letter on name
51 9         20 s/\bMc(\w)/Mc\u$1/igo; # Scottish names such as 'McLeod'
52 9         18 s/\bo'(\w)/O'\u$1/igo; # Irish names such as 'O'Malley, O'Reilly'
53 9         113 s/\b(x*(ix)?v*(iv)?i*)\b/\U$1/igo; # Roman numerals, eg 'Level III Support'
54             }
55              
56             # some cleanup
57 38         53 s/\[[^\]]*\]//g;
58 38         137 s/(^[\s'"]+|[\s'"]+$)//g;
59 38         58 s/\s{2,}/ /g;
60              
61 38         73 $_;
62             }
63              
64             sub _tokenise
65 44     44   75 { local $_ = join ',', @_;
66 44         57 my (@words,$snippet,$field);
67              
68 44         69 s/\A\s+//;
69 44         66 s/[\r\n]+/ /g;
70              
71 44         77 while ($_ ne '')
72 437         471 { $field = '';
73 437 100       729 if(s/^\s*\(/(/ ) # (...)
74 18         26 { my $depth = 0;
75              
76 18         108 PAREN: while(s/^(\(([^\(\)\\]|\\.)*)//)
77 23         55 { $field .= $1;
78 23         26 $depth++;
79 23         93 while(s/^(([^\(\)\\]|\\.)*\)\s*)//)
80 23         43 { $field .= $1;
81 23 100       41 last PAREN unless --$depth;
82 5 100       29 $field .= $1 if s/^(([^\(\)\\]|\\.)+)//;
83             }
84             }
85              
86 18 50       27 carp "Unmatched () '$field' '$_'"
87             if $depth;
88              
89 18         47 $field =~ s/\s+\Z//;
90 18         28 push @words, $field;
91              
92 18         35 next;
93             }
94              
95 419 50 66     2020 if( s/^("(?:[^"\\]+|\\.)*")\s*// # "..."
      100        
      66        
96             || s/^(\[(?:[^\]\\]+|\\.)*\])\s*// # [...]
97             || s/^([^\s()<>\@,;:\\".[\]]+)\s*//
98             || s/^([()<>\@,;:\\".[\]])\s*//
99             )
100 419         770 { push @words, $1;
101 419         733 next;
102             }
103              
104 0         0 croak "Unrecognised line: $_";
105             }
106              
107 44         60 push @words, ",";
108 44         84 \@words;
109             }
110              
111             sub _find_next
112 92     92   127 { my ($idx, $tokens, $len) = @_;
113              
114 92         150 while($idx < $len)
115 304         344 { my $c = $tokens->[$idx];
116 304 100 100     902 return $c if $c eq ',' || $c eq ';' || $c eq '<';
      100        
117 256         346 $idx++;
118             }
119              
120 44         96 "";
121             }
122              
123             sub _complete
124 51     51   102 { my ($class, $phrase, $address, $comment) = @_;
125              
126 51 50 100     136 @$phrase || @$comment || @$address
      66        
127             or return undef;
128              
129 51         171 my $o = $class->new(join(" ",@$phrase), join("",@$address), join(" ",@$comment));
130 51         124 @$phrase = @$address = @$comment = ();
131 51         69 $o;
132             }
133              
134             #------------
135              
136             sub new(@)
137 51     51 1 66 { my $class = shift;
138 51         127 bless [@_], $class;
139             }
140              
141              
142             sub parse(@)
143 44     44 1 709 { my $class = shift;
144 44         75 my @line = grep {defined} @_;
  44         105  
145 44         82 my $line = join '', @line;
146              
147 44         55 my (@phrase, @comment, @address, @objs);
148 44         56 my ($depth, $idx) = (0, 0);
149              
150 44         75 my $tokens = _tokenise @line;
151 44         67 my $len = @$tokens;
152 44         70 my $next = _find_next $idx, $tokens, $len;
153              
154 44         59 local $_;
155 44         73 for(my $idx = 0; $idx < $len; $idx++)
156 481         578 { $_ = $tokens->[$idx];
157              
158 481 100 100     1817 if(substr($_,0,1) eq '(') { push @comment, $_ }
  18 100 100     29  
    100 100        
    100          
    100          
    100          
    100          
159 20         28 elsif($_ eq '<') { $depth++ }
160 20 50       38 elsif($_ eq '>') { $depth-- if $depth }
161             elsif($_ eq ',' || $_ eq ';')
162 48 50       128 { warn "Unmatched '<>' in $line" if $depth;
163 48         90 my $o = $class->_complete(\@phrase, \@address, \@comment);
164 48 50       89 push @objs, $o if defined $o;
165 48         56 $depth = 0;
166 48         73 $next = _find_next $idx+1, $tokens, $len;
167             }
168 137         242 elsif($depth) { push @address, $_ }
169 32         63 elsif($next eq '<') { push @phrase, $_ }
170             elsif( /^[.\@:;]$/ || !@address || $address[-1] =~ /^[.\@:;]$/ )
171 203         390 { push @address, $_ }
172             else
173 3 50       6 { warn "Unmatched '<>' in $line" if $depth;
174 3         6 my $o = $class->_complete(\@phrase, \@address, \@comment);
175 3 50       6 push @objs, $o if defined $o;
176 3         3 $depth = 0;
177 3         7 push @address, $_;
178             }
179             }
180 44         152 @objs;
181             }
182              
183             #------------
184              
185 44     44 1 76 sub phrase { shift->set_or_get(0, @_) }
186 44     44 1 66 sub address { shift->set_or_get(1, @_) }
187 26     26 1 40 sub comment { shift->set_or_get(2, @_) }
188              
189             sub set_or_get($)
190 114     114 0 142 { my ($self, $i) = (shift, shift);
191 114 50       229 @_ or return $self->[$i];
192              
193 0         0 my $val = $self->[$i];
194 0 0       0 $self->[$i] = shift if @_;
195 0         0 $val;
196             }
197              
198              
199             my $atext = '[\-\w !#$%&\'*+/=?^`{|}~]';
200             sub format
201 44     44 1 134 { my @addrs;
202              
203 44         68 foreach (@_)
204 44         70 { my ($phrase, $email, $comment) = @$_;
205 44         48 my @addr;
206              
207 44 100 66     150 if(defined $phrase && length $phrase)
    50 33        
208 18 50       204 { push @addr
    100          
209             , $phrase =~ /^(?:\s*$atext\s*)+$/o ? $phrase
210             : $phrase =~ /(?
211             : qq("$phrase");
212              
213 18 50 33     74 push @addr, "<$email>"
214             if defined $email && length $email;
215             }
216             elsif(defined $email && length $email)
217 26         34 { push @addr, $email;
218             }
219              
220 44 100 66     119 if(defined $comment && $comment =~ /\S/)
221 16         41 { $comment =~ s/^\s*\(?/(/;
222 16         90 $comment =~ s/\)?\s*$/)/;
223             }
224              
225 44 100 66     115 push @addr, $comment
226             if defined $comment && length $comment;
227              
228 44 50       140 push @addrs, join(" ", @addr)
229             if @addr;
230             }
231              
232 44         104 join ", ", @addrs;
233             }
234              
235             #------------
236              
237             sub name
238 44     44 1 143 { my $self = shift;
239 44         64 my $phrase = $self->phrase;
240 44         66 my $addr = $self->address;
241              
242 44 100 66     140 $phrase = $self->comment
243             unless defined $phrase && length $phrase;
244              
245 44         88 my $name = $self->_extract_name($phrase);
246              
247             # first.last@domain address
248 44 100 100     153 if($name eq '' && $addr =~ /([^\%\.\@_]+([\._][^\%\.\@_]+)+)[\@\%]/)
249 4         16 { ($name = $1) =~ s/[\._]+/ /g;
250 4         8 $name = _extract_name $name;
251             }
252              
253 44 100 100     89 if($name eq '' && $addr =~ m#/g=#i) # X400 style address
254 1         4 { my ($f) = $addr =~ m#g=([^/]*)#i;
255 1         3 my ($l) = $addr =~ m#s=([^/]*)#i;
256 1         11 $name = _extract_name "$f $l";
257             }
258              
259 44 100       95 length $name ? $name : undef;
260             }
261              
262              
263             sub host
264 0   0 0 1   { my $addr = shift->address || '';
265 0           my $i = rindex $addr, '@';
266 0 0         $i >= 0 ? substr($addr, $i+1) : undef;
267             }
268              
269              
270             sub user
271 0   0 0 1   { my $addr = shift->address || '';
272 0           my $i = rindex $addr, '@';
273 0 0         $i >= 0 ? substr($addr,0,$i) : $addr;
274             }
275              
276             1;