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-2019 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   884 use vars '$VERSION';
  3         5  
  3         166  
11             $VERSION = '2.21';
12              
13 3     3   17 use strict;
  3         6  
  3         58  
14              
15 3     3   13 use Carp;
  3         6  
  3         7384  
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   155 my $self = @_ && ref $_[0] ? shift : undef;
27              
28 49 100       118 local $_ = shift
29             or return '';
30              
31             # Using encodings, too hard. See Mail::Message::Field::Full.
32 38 50       87 return '' if m/\=\?.*?\?\=/;
33              
34             # trim whitespace
35 38         89 s/^\s+//;
36 38         94 s/\s+$//;
37 38         105 s/\s+/ /;
38              
39             # Disregard numeric names (e.g. 123456.1234@compuserve.com)
40 38 50       141 return "" if /^[\d ]+$/;
41              
42 38         91 s/^\((.*)\)$/$1/; # remove outermost parenthesis
43 38         80 s/^"(.*)"$/$1/; # remove outer quotation marks
44 38         60 s/\(.*?\)//g; # remove minimal embedded comments
45 38         102 s/\\//g; # remove all escapes
46 38         56 s/^"(.*)"$/$1/; # remove internal quotation marks
47 38         60 s/^([^\s]+) ?, ?(.*)$/$2 $1/; # reverse "Last, First M." if applicable
48 38         55 s/,.*//;
49              
50             # Change casing only when the name contains only upper or only
51             # lower cased characters.
52 38 100 100     146 unless( m/[A-Z]/ && m/[a-z]/ )
53             { # Set the case of the name to first char upper rest lower
54 9         79 s/\b(\w+)/\L\u$1/igo; # Upcase first letter on name
55 9         28 s/\bMc(\w)/Mc\u$1/igo; # Scottish names such as 'McLeod'
56 9         19 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         67 s/\[[^\]]*\]//g;
62 38         169 s/(^[\s'"]+|[\s'"]+$)//g;
63 38         70 s/\s{2,}/ /g;
64              
65 38         80 $_;
66             }
67              
68             sub _tokenise
69 44     44   109 { local $_ = join ',', @_;
70 44         63 my (@words,$snippet,$field);
71              
72 44         86 s/\A\s+//;
73 44         80 s/[\r\n]+/ /g;
74              
75 44         89 while ($_ ne '')
76 437         544 { $field = '';
77 437 100       898 if(s/^\s*\(/(/ ) # (...)
78 18         31 { my $depth = 0;
79              
80 18         130 PAREN: while(s/^(\(([^\(\)\\]|\\.)*)//)
81 23         59 { $field .= $1;
82 23         31 $depth++;
83 23         109 while(s/^(([^\(\)\\]|\\.)*\)\s*)//)
84 23         45 { $field .= $1;
85 23 100       52 last PAREN unless --$depth;
86 5 100       79 $field .= $1 if s/^(([^\(\)\\]|\\.)+)//;
87             }
88             }
89              
90 18 50       31 carp "Unmatched () '$field' '$_'"
91             if $depth;
92              
93 18         50 $field =~ s/\s+\Z//;
94 18         37 push @words, $field;
95              
96 18         39 next;
97             }
98              
99 419 50 66     2403 if( s/^("(?:[^"\\]+|\\.)*")\s*// # "..."
      100        
      66        
100             || s/^(\[(?:[^\]\\]+|\\.)*\])\s*// # [...]
101             || s/^([^\s()<>\@,;:\\".[\]]+)\s*//
102             || s/^([()<>\@,;:\\".[\]])\s*//
103             )
104 419         954 { push @words, $1;
105 419         803 next;
106             }
107              
108 0         0 croak "Unrecognised line: $_";
109             }
110              
111 44         73 push @words, ",";
112 44         96 \@words;
113             }
114              
115             sub _find_next
116 92     92   140 { my ($idx, $tokens, $len) = @_;
117              
118 92         189 while($idx < $len)
119 304         426 { my $c = $tokens->[$idx];
120 304 100 100     1037 return $c if $c eq ',' || $c eq ';' || $c eq '<';
      100        
121 256         393 $idx++;
122             }
123              
124 44         113 "";
125             }
126              
127             sub _complete
128 51     51   135 { my ($class, $phrase, $address, $comment) = @_;
129              
130 51 50 100     162 @$phrase || @$comment || @$address
      66        
131             or return undef;
132              
133 51         235 my $o = $class->new(join(" ",@$phrase), join("",@$address), join(" ",@$comment));
134 51         138 @$phrase = @$address = @$comment = ();
135 51         78 $o;
136             }
137              
138             #------------
139              
140             sub new(@)
141 51     51 1 119 { my $class = shift;
142 51         155 bless [@_], $class;
143             }
144              
145              
146             sub parse(@)
147 44     44 1 832 { my $class = shift;
148 44         85 my @line = grep {defined} @_;
  44         126  
149 44         90 my $line = join '', @line;
150              
151 44         61 my (@phrase, @comment, @address, @objs);
152 44         60 my ($depth, $idx) = (0, 0);
153              
154 44         78 my $tokens = _tokenise @line;
155 44         65 my $len = @$tokens;
156 44         79 my $next = _find_next $idx, $tokens, $len;
157              
158 44         69 local $_;
159 44         80 for(my $idx = 0; $idx < $len; $idx++)
160 481         656 { $_ = $tokens->[$idx];
161              
162 481 100 100     2199 if(substr($_,0,1) eq '(') { push @comment, $_ }
  18 100 100     35  
    100 100        
    100          
    100          
    100          
    100          
163 20         34 elsif($_ eq '<') { $depth++ }
164 20 50       48 elsif($_ eq '>') { $depth-- if $depth }
165             elsif($_ eq ',' || $_ eq ';')
166 48 50       101 { warn "Unmatched '<>' in $line" if $depth;
167 48         107 my $o = $class->_complete(\@phrase, \@address, \@comment);
168 48 50       101 push @objs, $o if defined $o;
169 48         57 $depth = 0;
170 48         107 $next = _find_next $idx+1, $tokens, $len;
171             }
172 137         263 elsif($depth) { push @address, $_ }
173 32         82 elsif($next eq '<') { push @phrase, $_ }
174             elsif( /^[.\@:;]$/ || !@address || $address[-1] =~ /^[.\@:;]$/ )
175 203         436 { push @address, $_ }
176             else
177 3 50       6 { warn "Unmatched '<>' in $line" if $depth;
178 3         7 my $o = $class->_complete(\@phrase, \@address, \@comment);
179 3 50       6 push @objs, $o if defined $o;
180 3         5 $depth = 0;
181 3         6 push @address, $_;
182             }
183             }
184 44         176 @objs;
185             }
186              
187             #------------
188              
189 44     44 1 73 sub phrase { shift->set_or_get(0, @_) }
190 44     44 1 89 sub address { shift->set_or_get(1, @_) }
191 26     26 1 44 sub comment { shift->set_or_get(2, @_) }
192              
193             sub set_or_get($)
194 114     114 0 168 { my ($self, $i) = (shift, shift);
195 114 50       266 @_ 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 150 { my @addrs;
206              
207 44         88 foreach (@_)
208 44         89 { my ($phrase, $email, $comment) = @$_;
209 44         57 my @addr;
210              
211 44 100 66     166 if(defined $phrase && length $phrase)
    50 33        
212 18 50       328 { push @addr
    100          
213             , $phrase =~ /^(?:\s*$atext\s*)+$/o ? $phrase
214             : $phrase =~ /(?
215             : qq("$phrase");
216              
217 18 50 33     87 push @addr, "<$email>"
218             if defined $email && length $email;
219             }
220             elsif(defined $email && length $email)
221 26         43 { push @addr, $email;
222             }
223              
224 44 100 66     138 if(defined $comment && $comment =~ /\S/)
225 16         51 { $comment =~ s/^\s*\(?/(/;
226 16         108 $comment =~ s/\)?\s*$/)/;
227             }
228              
229 44 100 66     133 push @addr, $comment
230             if defined $comment && length $comment;
231              
232 44 50       156 push @addrs, join(" ", @addr)
233             if @addr;
234             }
235              
236 44         110 join ", ", @addrs;
237             }
238              
239             #------------
240              
241             sub name
242 44     44 1 141 { my $self = shift;
243 44         73 my $phrase = $self->phrase;
244 44         86 my $addr = $self->address;
245              
246 44 100 66     163 $phrase = $self->comment
247             unless defined $phrase && length $phrase;
248              
249 44         89 my $name = $self->_extract_name($phrase);
250              
251             # first.last@domain address
252 44 100 100     181 if($name eq '' && $addr =~ /([^\%\.\@_]+([\._][^\%\.\@_]+)+)[\@\%]/)
253 4         20 { ($name = $1) =~ s/[\._]+/ /g;
254 4         9 $name = _extract_name $name;
255             }
256              
257 44 100 100     103 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         4 $name = _extract_name "$f $l";
261             }
262              
263 44 100       162 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;