File Coverage

lib/Sisimai/Address.pm
Criterion Covered Total %
statement 176 194 90.7
branch 114 144 79.1
condition 31 35 88.5
subroutine 13 13 100.0
pod 5 8 62.5
total 339 394 86.0


line stmt bran cond sub pod time code
1             package Sisimai::Address;
2 83     83   71252 use feature ':5.10';
  83         147  
  83         5957  
3 83     83   452 use strict;
  83         136  
  83         1466  
4 83     83   365 use warnings;
  83         167  
  83         1984  
5 83     83   7893 use Sisimai::RFC5322;
  83         155  
  83         3815  
6             use Class::Accessor::Lite (
7 83         689 'new' => 0,
8             'ro' => [
9             'address', # [String] Email address
10             'user', # [String] local part of the email address
11             'host', # [String] domain part of the email address
12             'verp', # [String] VERP
13             'alias', # [String] alias of the email address
14             ],
15             'rw' => [
16             'name', # [String] Display name
17             'comment', # [String] (Comment)
18             ]
19 83     83   5553 );
  83         13775  
20              
21             sub undisclosed {
22             # Return pseudo recipient or sender address
23             # @param [String] atype Address type: 'r' or 's'
24             # @return [String, Undef] Pseudo recipient address or sender address or
25             # Undef when the $argv1 is neither 'r' nor 's'
26 18     18 0 435 my $class = shift;
27 18   100     65 my $atype = shift || return undef;
28              
29 17 50       89 return undef unless $atype =~ /\A(?:r|s)\z/;
30 17 100       63 my $local = $atype eq 'r' ? 'recipient' : 'sender';
31 17         119 return sprintf("undisclosed-%s-in-headers%slibsisimai.org.invalid", $local, '@');
32             }
33              
34             sub new {
35             # Old constructor of Sisimai::Address, wrapper method of make()
36             # @param [String] email Email address
37             # @return [Sisimai::Address, Undef] Object or Undef when the email
38             # address was not valid.
39 50     50 1 38290 my $class = shift;
40 50   100     132 my $email = shift // return undef;
41 49         112 my $addrs = __PACKAGE__->find($email);
42              
43 49 100       101 return undef unless $addrs;
44 44 50       72 return undef unless scalar @$addrs;
45 44         111 return __PACKAGE__->make($addrs->[0]);
46             }
47              
48             sub make {
49             # New constructor of Sisimai::Address
50             # @param [Hash] argvs Email address, name, and other elements
51             # @return [Sisimai::Address] Object or Undef when the email address was
52             # not valid.
53             # @since v4.22.1
54 6014     6014 0 14801 my $class = shift;
55 6014   100     12015 my $argvs = shift // return undef;
56 6013         26855 my $thing = {
57             'address' => '', # Entire email address
58             'user' => '', # Local part
59             'host' => '', # Domain part
60             'verp' => '', # VERP
61             'alias' => '', # Alias
62             'comment' => '', # Comment
63             'name' => '', # Display name
64             };
65              
66 6013 50       13720 return undef unless ref $argvs eq 'HASH';
67 6013 50       10703 return undef unless exists $argvs->{'address'};
68 6013 100       9565 return undef unless $argvs->{'address'};
69              
70 6012         10125 my $heads = ['<'];
71 6012         11399 my $tails = ['>', ',', '.', ';'];
72              
73 6012 100 100     31253 if( $argvs->{'address'} =~ /\A([^\s]+)[@]([^@]+)\z/ ||
74             $argvs->{'address'} =~ /\A(["].+?["])[@]([^@]+)\z/ ) {
75             # Get the local part and the domain part from the email address
76 5998 100       13764 my $lpart = $1; for my $e ( @$heads ) { $lpart =~ s/\A$e//g if substr($lpart, 0, 1) eq $e }
  5998         9719  
  5998         14471  
77 5998 100       9624 my $dpart = $2; for my $e ( @$tails ) { $dpart =~ s/$e\z//g if substr($dpart, -1, 1) eq $e }
  5998         7477  
  23992         36541  
78 5998   100     13652 my $email = __PACKAGE__->expand_verp($argvs->{'address'}) || '';
79 5998         7262 my $alias = 0;
80              
81 5998 100       10172 unless( $email ) {
82             # Is not VERP address, try to expand the address as an alias
83 5997   100     12063 $email = __PACKAGE__->expand_alias($argvs->{'address'}) || '';
84 5997 100       11794 $alias = 1 if $email;
85             }
86              
87 5998 100       10529 if( $email =~ /\A.+[@].+?\z/ ) {
88             # The address is a VERP or an alias
89 7 100       23 if( $alias ) {
90             # The address is an alias: neko+nyaan@example.jp
91 6         17 $thing->{'alias'} = $argvs->{'address'};
92              
93             } else {
94             # The address is a VERP: b+neko=example.jp@example.org
95 1         3 $thing->{'verp'} = $argvs->{'address'};
96             }
97             }
98 5998         9428 $thing->{'user'} = $lpart;
99 5998         7945 $thing->{'host'} = $dpart;
100 5998         14575 $thing->{'address'} = $lpart.'@'.$dpart;
101              
102             } else {
103             # The argument does not include "@"
104 14 50       50 return undef unless Sisimai::RFC5322->is_mailerdaemon($argvs->{'address'});
105 14 50       74 return undef if rindex($argvs->{'address'}, ' ') > -1;
106              
107             # The argument does not include " "
108 14         34 $thing->{'user'} = $argvs->{'address'};
109 14         26 $thing->{'address'} = $argvs->{'address'};
110             }
111              
112 6012   100     15710 $thing->{'name'} = $argvs->{'name'} || '';
113 6012   100     16134 $thing->{'comment'} = $argvs->{'comment'} || '';
114 6012         17892 return bless($thing, __PACKAGE__);
115             }
116              
117             sub find {
118             # Email address parser with a name and a comment
119             # @param [String] argv1 String including email address
120             # @param [Boolean] addrs 0 = Returns list including all the elements
121             # 1 = Returns list including email addresses only
122             # @return [Array, Undef] Email address list or Undef when there is no
123             # email address in the argument
124             # @since v4.22.0
125 3943     3943 1 306703 my $class = shift;
126 3943   100     8304 my $argv1 = shift // return undef;
127 3942   100     11173 my $addrs = shift // undef;
128              
129 3942         5225 state $indicators = {
130             'email-address' => (1 << 0), #
131             'quoted-string' => (1 << 1), # "Neko, Nyaan"
132             'comment-block' => (1 << 2), # (neko)
133             };
134 3942         5733 state $delimiters = { '<' => 1, '>' => 1, '(' => 1, ')' => 1, '"' => 1, ',' => 1 };
135 3942         5218 state $validemail = qr{(?>
136             (?:([^\s]+|["].+?["])) # local part
137             [@]
138             (?:([^@\s]+|[0-9A-Za-z:\.]+)) # domain part
139             )
140             }x;
141              
142 3942         11344 my $emailtable = { 'address' => '', 'name' => '', 'comment' => '' };
143 3942         5450 my $addrtables = [];
144 3942         5800 my @readbuffer;
145 3942         4507 my $readcursor = 0;
146 3942         4538 my $v = $emailtable; # temporary buffer
147 3942         4475 my $p = ''; # current position
148              
149 3942 50       8698 $argv1 =~ y/\r//d if index($argv1, "\r") > -1; # Remove CR
150 3942 100       7486 $argv1 =~ y/\n//d if index($argv1, "\n") > -1; # Remove NL
151              
152 3942         29063 for my $e ( split('', $argv1) ) {
153             # Check each characters
154 123906 100       144780 if( $delimiters->{ $e } ) {
155             # The character is a delimiter character
156 6935 100       11650 if( $e eq ',' ) {
157             # Separator of email addresses or not
158 563 100 66     2497 if( index($v->{'address'}, '<') == 0 &&
      66        
159             rindex($v->{'address'}, '@') > -1 &&
160             substr($v->{'address'}, -1, 1) eq '>' ) {
161             # An email address has already been picked
162 2 50       8 if( $readcursor & $indicators->{'comment-block'} ) {
    50          
163             # The cursor is in the comment block (Neko, Nyaan)
164 0         0 $v->{'comment'} .= $e;
165              
166             } elsif( $readcursor & $indicators->{'quoted-string'} ) {
167             # "Neko, Nyaan"
168 0         0 $v->{'name'} .= $e;
169              
170             } else {
171             # The cursor is not in neither the quoted-string nor the comment block
172 2         4 $readcursor = 0; # reset cursor position
173 2         3 push @readbuffer, $v;
174 2         6 $v = { 'address' => '', 'name' => '', 'comment' => '' };
175 2         4 $p = '';
176             }
177             } else {
178             # "Neko, Nyaan" OR <"neko,nyaan"@example.org>
179 561 100       1191 $p ? ($v->{ $p } .= $e) : ($v->{'name'} .= $e);
180             }
181 563         976 next;
182             } # End of if(',')
183              
184 6372 100       9972 if( $e eq '<' ) {
185             # <: The beginning of an email address or not
186 2273 50       4461 if( $v->{'address'} ) {
187 0 0       0 $p ? ($v->{ $p } .= $e) : ($v->{'name'} .= $e);
188              
189             } else {
190             #
191 2273         3586 $readcursor |= $indicators->{'email-address'};
192 2273         3608 $v->{'address'} .= $e;
193 2273         4232 $p = 'address';
194             }
195 2273         2929 next;
196             } # End of if('<')
197              
198 4099 100       7060 if( $e eq '>' ) {
199             # >: The end of an email address or not
200 2277 100       4621 if( $readcursor & $indicators->{'email-address'} ) {
201             #
202 2273         3736 $readcursor &= ~$indicators->{'email-address'};
203 2273         2976 $v->{'address'} .= $e;
204 2273         3245 $p = '';
205              
206             } else {
207             # a comment block or a display name
208 4 50       12 $p ? ($v->{'comment'} .= $e) : ($v->{'name'} .= $e);
209             }
210 2277         3761 next;
211             } # End of if('>')
212              
213 1822 100       3301 if( $e eq '(' ) {
214             # The beginning of a comment block or not
215 82 100       362 if( $readcursor & $indicators->{'email-address'} ) {
    50          
    50          
216             # <"neko(nyaan)"@example.org> or
217 2 50       6 if( rindex($v->{'address'}, '"') > -1 ) {
218             # Quoted local part: <"neko(nyaan)"@example.org>
219 0         0 $v->{'address'} .= $e;
220              
221             } else {
222             # Comment:
223 2         4 $readcursor |= $indicators->{'comment-block'};
224 2 50       7 $v->{'comment'} .= ' ' if substr($v->{'comment'}, -1, 1) eq ')';
225 2         4 $v->{'comment'} .= $e;
226 2         3 $p = 'comment';
227             }
228             } elsif( $readcursor & $indicators->{'comment-block'} ) {
229             # Comment at the outside of an email address (...(...)
230 0 0       0 $v->{'comment'} .= ' ' if substr($v->{'comment'}, -1, 1) eq ')';
231 0         0 $v->{'comment'} .= $e;
232              
233             } elsif( $readcursor & $indicators->{'quoted-string'} ) {
234             # "Neko, Nyaan(cat)", Deal as a display name
235 0         0 $v->{'name'} .= $e;
236              
237             } else {
238             # The beginning of a comment block
239 80         136 $readcursor |= $indicators->{'comment-block'};
240 80 100       236 $v->{'comment'} .= ' ' if substr($v->{'comment'}, -1, 1) eq ')';
241 80         143 $v->{'comment'} .= $e;
242 80         119 $p = 'comment';
243             }
244 82         107 next;
245             } # End of if('(')
246              
247 1740 100       3346 if( $e eq ')' ) {
248             # The end of a comment block or not
249 82 100       252 if( $readcursor & $indicators->{'email-address'} ) {
    50          
250             # <"neko(nyaan)"@example.org> OR
251 2 50       5 if( rindex($v->{'address'}, '"') > -1 ) {
252             # Quoted string in the local part: <"neko(nyaan)"@example.org>
253 0         0 $v->{'address'} .= $e;
254              
255             } else {
256             # Comment:
257 2         3 $readcursor &= ~$indicators->{'comment-block'};
258 2         3 $v->{'comment'} .= $e;
259 2         3 $p = 'address';
260             }
261             } elsif( $readcursor & $indicators->{'comment-block'} ) {
262             # Comment at the outside of an email address (...(...)
263 80         128 $readcursor &= ~$indicators->{'comment-block'};
264 80         118 $v->{'comment'} .= $e;
265 80         96 $p = '';
266              
267             } else {
268             # Deal as a display name
269 0         0 $readcursor &= ~$indicators->{'comment-block'};
270 0         0 $v->{'name'} .= $e;
271 0         0 $p = '';
272             }
273 82         112 next;
274             } # End of if(')')
275              
276 1658 50       2794 if( $e eq '"' ) {
277             # The beginning or the end of a quoted-string
278 1658 50       2544 if( $p ) {
279             # email-address or comment-block
280 0         0 $v->{ $p } .= $e;
281              
282             } else {
283             # Display name like "Neko, Nyaan"
284 1658         2751 $v->{'name'} .= $e;
285 1658 50       4211 next unless $readcursor & $indicators->{'quoted-string'};
286 0 0       0 next if $v->{'name'} =~ /\x5c["]\z/; # "Neko, Nyaan \"...
287 0         0 $readcursor &= ~$indicators->{'quoted-string'};
288 0         0 $p = '';
289             }
290 0         0 next;
291             } # End of if('"')
292             } else {
293             # The character is not a delimiter
294 116971 100       142175 $p ? ($v->{ $p } .= $e) : ($v->{'name'} .= $e);
295 116971         119135 next;
296             }
297             }
298              
299 3942 100       13742 if( $v->{'address'} ) {
300             # Push the latest values
301 2271         3437 push @readbuffer, $v;
302              
303             } else {
304             # No email address like in the argument
305 1671 100       14494 if( $v->{'name'} =~ $validemail ) {
    100          
306             # String like an email address will be set to the value of "address"
307 1645         7021 $v->{'address'} = $1.'@'.$2;
308              
309             } elsif( Sisimai::RFC5322->is_mailerdaemon($v->{'name'}) ) {
310             # Allow if the argument is MAILER-DAEMON
311 5         11 $v->{'address'} = $v->{'name'};
312             }
313              
314 1671 100       3881 if( $v->{'address'} ) {
315             # Remove the comment from the address
316 1650 50       4357 if( $v->{'address'} =~ /(.*)([(].+[)])(.*)/ ) {
317             # (nyaan)nekochan@example.org, nekochan(nyaan)cat@example.org or
318             # nekochan(nyaan)@example.org
319 0         0 $v->{'address'} = $1.$3;
320 0         0 $v->{'comment'} = $2;
321             }
322 1650         2776 push @readbuffer, $v;
323             }
324             }
325              
326 3942         6285 for my $e ( @readbuffer ) {
327             # The element must not include any character except from 0x20 to 0x7e.
328 3923 50       12680 next if $e->{'address'} =~ /[^\x20-\x7e]/;
329 3923 100       17039 unless( $e->{'address'} =~ /\A.+[@].+\z/ ) {
330             # Allow if the argument is MAILER-DAEMON
331 20 50       87 next unless Sisimai::RFC5322->is_mailerdaemon($e->{'address'});
332             }
333              
334             # Remove angle brackets, other brackets, and quotations: []<>{}'`
335             # except a domain part is an IP address like neko@[192.0.2.222]
336 3923         10578 $e->{'address'} =~ s/\A[\[<{('`]//;
337 3923         11598 $e->{'address'} =~ s/[.'`>});]\z//;
338 3923 100       12372 $e->{'address'} =~ s/\]\z// unless $e->{'address'} =~ /[@]\[[0-9A-Za-z:\.]+\]\z/;
339              
340 3923 100       9186 unless( $e->{'address'} =~ /\A["].+["][@]/ ) {
341             # Remove double-quotations
342 3903 100       9552 substr($e->{'address'}, 0, 1, '') if substr($e->{'address'}, 0, 1) eq '"';
343 3903 100       8116 substr($e->{'address'}, -1, 1, '') if substr($e->{'address'}, -1, 1) eq '"';
344             }
345              
346 3923 100       7141 if( $addrs ) {
347             # Almost compatible with parse() method, returns email address only
348 833         1621 delete $e->{'name'};
349 833         1317 delete $e->{'comment'};
350              
351             } else {
352             # Remove double-quotations, trailing spaces.
353 3090         4843 for my $f ('name', 'comment') {
354             # Remove traliing spaces
355 6180 100       13025 $e->{ $f } =~ s/\A[ ]//g if index($e->{ $f }, ' ') == 0;
356 6180 100       17029 $e->{ $f } =~ s/[ ]\z//g if substr($e->{ $f }, -1, 1) eq ' ';
357             }
358 3090 100       7707 $e->{'comment'} = '' unless $e->{'comment'} =~ /\A[(].+[)]\z/;
359 3090 100       10121 $e->{'name'} =~ y/ //s unless $e->{'name'} =~ /\A["].+["]\z/;
360 3090 100       8921 $e->{'name'} =~ s/\A["]// unless $e->{'name'} =~ /\A["].+["][@]/;
361 3090 100       7218 substr($e->{'name'}, -1, 1, '') if substr($e->{'name'}, -1, 1) eq '"';
362             }
363 3923         8470 push @$addrtables, $e;
364             }
365              
366 3942 100       9006 return undef unless scalar @$addrtables;
367 3921         14036 return $addrtables;
368             }
369              
370             sub s3s4 {
371             # Runs like ruleset 3,4 of sendmail.cf
372             # @param [String] input Text including an email address
373             # @return [String] Email address without comment, brackets
374 805     805 1 208501 my $class = shift;
375 805   100     2745 my $input = shift // return undef;
376 804   100     2176 my $addrs = __PACKAGE__->find($input, 1) || [];
377 804 100       1878 return $input unless scalar @$addrs;
378 793         3224 return $addrs->[0]->{'address'};
379             }
380              
381             sub expand_verp {
382             # Expand VERP: Get the original recipient address from VERP
383             # @param [String] email VERP Address
384             # @return [String] Email address
385 5999     5999 1 8768 my $class = shift;
386 5999   50     10858 my $email = shift // return undef;
387 5999         15605 my $local = (split('@', $email, 2))[0];
388              
389             # bounce+neko=example.org@example.org => neko@example.org
390 5999 100       26304 return undef unless $local =~ /\A[-_\w]+?[+](\w[-._\w]+\w)[=](\w[-.\w]+\w)\z/;
391 2         8 my $verp0 = $1.'@'.$2;
392 2 50       7 return $verp0 if Sisimai::RFC5322->is_emailaddress($verp0);
393             }
394              
395             sub expand_alias {
396             # Expand alias: remove from '+' to '@'
397             # @param [String] email Email alias string
398             # @return [String] Expanded email address
399 5998     5998 1 7057 my $class = shift;
400 5998   50     10049 my $email = shift // return undef;
401 5998 100       14448 return undef unless Sisimai::RFC5322->is_emailaddress($email);
402              
403             # neko+straycat@example.org => neko@example.org
404 5967         16692 my @local = split('@', $email);
405 5967 100       28117 return undef unless $local[0] =~ /\A([-_\w]+?)[+].+\z/;
406 7         49 return $1.'@'.$local[1];
407             }
408              
409             sub TO_JSON {
410             # Instance method for JSON::encode()
411             # @return [String] The value of "address" accessor
412 1159     1159 0 31656 my $self = shift;
413 1159         2058 return $self->address;
414             }
415              
416             1;
417             __END__