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