File Coverage

blib/lib/Mail/Address.pm
Criterion Covered Total %
statement 128 139 92.0
branch 59 80 73.7
condition 48 64 75.0
subroutine 14 17 82.3
pod 9 11 81.8
total 258 311 82.9


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