|  | # Copyrights 1995-2018 by [Mark Overmeer]. | 
|  | #  For other contributors see ChangeLog. | 
|  | # See the manual pages for details on the licensing terms. | 
|  | # Pod stripped from pm file by OODoc 2.02. | 
|  | # This code is part of the bundle MailTools.  Meta-POD processed with | 
|  | # OODoc into POD and HTML manual-pages.  See README.md for Copyright. | 
|  | # Licensed under the same terms as Perl itself. | 
|  |  | 
|  | package Mail::Address; | 
|  | use vars '$VERSION'; | 
|  | $VERSION = '2.20'; | 
|  |  | 
|  | use strict; | 
|  |  | 
|  | use Carp; | 
|  |  | 
|  | # use locale;   removed in version 1.78, because it causes taint problems | 
|  |  | 
|  | sub Version { our $VERSION } | 
|  |  | 
|  |  | 
|  |  | 
|  | # given a comment, attempt to extract a person's name | 
|  | sub _extract_name | 
|  | {   # This function can be called as method as well | 
|  | my $self = @_ && ref $_[0] ? shift : undef; | 
|  |  | 
|  | local $_ = shift | 
|  | or return ''; | 
|  |  | 
|  | # Using encodings, too hard. See Mail::Message::Field::Full. | 
|  | return '' if m/\=\?.*?\?\=/; | 
|  |  | 
|  | # trim whitespace | 
|  | s/^\s+//; | 
|  | s/\s+$//; | 
|  | s/\s+/ /; | 
|  |  | 
|  | # Disregard numeric names (e.g. 123456.1234@compuserve.com) | 
|  | return "" if /^[\d ]+$/; | 
|  |  | 
|  | s/^\((.*)\)$/$1/; # remove outermost parenthesis | 
|  | s/^"(.*)"$/$1/;   # remove outer quotation marks | 
|  | s/\(.*?\)//g;     # remove minimal embedded comments | 
|  | s/\\//g;          # remove all escapes | 
|  | s/^"(.*)"$/$1/;   # remove internal quotation marks | 
|  | s/^([^\s]+) ?, ?(.*)$/$2 $1/; # reverse "Last, First M." if applicable | 
|  | s/,.*//; | 
|  |  | 
|  | # Change casing only when the name contains only upper or only | 
|  | # lower cased characters. | 
|  | unless( m/[A-Z]/ && m/[a-z]/ ) | 
|  | {   # Set the case of the name to first char upper rest lower | 
|  | s/\b(\w+)/\L\u$1/igo;  # Upcase first letter on name | 
|  | s/\bMc(\w)/Mc\u$1/igo; # Scottish names such as 'McLeod' | 
|  | s/\bo'(\w)/O'\u$1/igo; # Irish names such as 'O'Malley, O'Reilly' | 
|  | s/\b(x*(ix)?v*(iv)?i*)\b/\U$1/igo; # Roman numerals, eg 'Level III Support' | 
|  | } | 
|  |  | 
|  | # some cleanup | 
|  | s/\[[^\]]*\]//g; | 
|  | s/(^[\s'"]+|[\s'"]+$)//g; | 
|  | s/\s{2,}/ /g; | 
|  |  | 
|  | $_; | 
|  | } | 
|  |  | 
|  | sub _tokenise | 
|  | {   local $_ = join ',', @_; | 
|  | my (@words,$snippet,$field); | 
|  |  | 
|  | s/\A\s+//; | 
|  | s/[\r\n]+/ /g; | 
|  |  | 
|  | while ($_ ne '') | 
|  | {   $field = ''; | 
|  | if(s/^\s*\(/(/ )    # (...) | 
|  | {   my $depth = 0; | 
|  |  | 
|  | PAREN: while(s/^(\(([^\(\)\\]|\\.)*)//) | 
|  | {   $field .= $1; | 
|  | $depth++; | 
|  | while(s/^(([^\(\)\\]|\\.)*\)\s*)//) | 
|  | {   $field .= $1; | 
|  | last PAREN unless --$depth; | 
|  | $field .= $1 if s/^(([^\(\)\\]|\\.)+)//; | 
|  | } | 
|  | } | 
|  |  | 
|  | carp "Unmatched () '$field' '$_'" | 
|  | if $depth; | 
|  |  | 
|  | $field =~ s/\s+\Z//; | 
|  | push @words, $field; | 
|  |  | 
|  | next; | 
|  | } | 
|  |  | 
|  | if( s/^("(?:[^"\\]+|\\.)*")\s*//       # "..." | 
|  | || s/^(\[(?:[^\]\\]+|\\.)*\])\s*//    # [...] | 
|  | || s/^([^\s()<>\@,;:\\".[\]]+)\s*// | 
|  | || s/^([()<>\@,;:\\".[\]])\s*// | 
|  | ) | 
|  | {   push @words, $1; | 
|  | next; | 
|  | } | 
|  |  | 
|  | croak "Unrecognised line: $_"; | 
|  | } | 
|  |  | 
|  | push @words, ","; | 
|  | \@words; | 
|  | } | 
|  |  | 
|  | sub _find_next | 
|  | {   my ($idx, $tokens, $len) = @_; | 
|  |  | 
|  | while($idx < $len) | 
|  | {   my $c = $tokens->[$idx]; | 
|  | return $c if $c eq ',' || $c eq ';' || $c eq '<'; | 
|  | $idx++; | 
|  | } | 
|  |  | 
|  | ""; | 
|  | } | 
|  |  | 
|  | sub _complete | 
|  | {   my ($class, $phrase, $address, $comment) = @_; | 
|  |  | 
|  | @$phrase || @$comment || @$address | 
|  | or return undef; | 
|  |  | 
|  | my $o = $class->new(join(" ",@$phrase), join("",@$address), join(" ",@$comment)); | 
|  | @$phrase = @$address = @$comment = (); | 
|  | $o; | 
|  | } | 
|  |  | 
|  | #------------ | 
|  |  | 
|  | sub new(@) | 
|  | {   my $class = shift; | 
|  | bless [@_], $class; | 
|  | } | 
|  |  | 
|  |  | 
|  | sub parse(@) | 
|  | {   my $class = shift; | 
|  | my @line  = grep {defined} @_; | 
|  | my $line  = join '', @line; | 
|  |  | 
|  | my (@phrase, @comment, @address, @objs); | 
|  | my ($depth, $idx) = (0, 0); | 
|  |  | 
|  | my $tokens  = _tokenise @line; | 
|  | my $len     = @$tokens; | 
|  | my $next    = _find_next $idx, $tokens, $len; | 
|  |  | 
|  | local $_; | 
|  | for(my $idx = 0; $idx < $len; $idx++) | 
|  | {   $_ = $tokens->[$idx]; | 
|  |  | 
|  | if(substr($_,0,1) eq '(') { push @comment, $_ } | 
|  | elsif($_ eq '<')    { $depth++ } | 
|  | elsif($_ eq '>')    { $depth-- if $depth } | 
|  | elsif($_ eq ',' || $_ eq ';') | 
|  | {   warn "Unmatched '<>' in $line" if $depth; | 
|  | my $o = $class->_complete(\@phrase, \@address, \@comment); | 
|  | push @objs, $o if defined $o; | 
|  | $depth = 0; | 
|  | $next = _find_next $idx+1, $tokens, $len; | 
|  | } | 
|  | elsif($depth)       { push @address, $_ } | 
|  | elsif($next eq '<') { push @phrase,  $_ } | 
|  | elsif( /^[.\@:;]$/ || !@address || $address[-1] =~ /^[.\@:;]$/ ) | 
|  | {   push @address, $_ } | 
|  | else | 
|  | {   warn "Unmatched '<>' in $line" if $depth; | 
|  | my $o = $class->_complete(\@phrase, \@address, \@comment); | 
|  | push @objs, $o if defined $o; | 
|  | $depth = 0; | 
|  | push @address, $_; | 
|  | } | 
|  | } | 
|  | @objs; | 
|  | } | 
|  |  | 
|  | #------------ | 
|  |  | 
|  | sub phrase  { shift->set_or_get(0, @_) } | 
|  | sub address { shift->set_or_get(1, @_) } | 
|  | sub comment { shift->set_or_get(2, @_) } | 
|  |  | 
|  | sub set_or_get($) | 
|  | {   my ($self, $i) = (shift, shift); | 
|  | @_ or return $self->[$i]; | 
|  |  | 
|  | my $val = $self->[$i]; | 
|  | $self->[$i] = shift if @_; | 
|  | $val; | 
|  | } | 
|  |  | 
|  |  | 
|  | my $atext = '[\-\w !#$%&\'*+/=?^`{|}~]'; | 
|  | sub format | 
|  | {   my @addrs; | 
|  |  | 
|  | foreach (@_) | 
|  | {   my ($phrase, $email, $comment) = @$_; | 
|  | my @addr; | 
|  |  | 
|  | if(defined $phrase && length $phrase) | 
|  | {   push @addr | 
|  | , $phrase =~ /^(?:\s*$atext\s*)+$/o ? $phrase | 
|  | : $phrase =~ /(?<!\\)"/             ? $phrase | 
|  | :                                    qq("$phrase"); | 
|  |  | 
|  | push @addr, "<$email>" | 
|  | if defined $email && length $email; | 
|  | } | 
|  | elsif(defined $email && length $email) | 
|  | {   push @addr, $email; | 
|  | } | 
|  |  | 
|  | if(defined $comment && $comment =~ /\S/) | 
|  | {   $comment =~ s/^\s*\(?/(/; | 
|  | $comment =~ s/\)?\s*$/)/; | 
|  | } | 
|  |  | 
|  | push @addr, $comment | 
|  | if defined $comment && length $comment; | 
|  |  | 
|  | push @addrs, join(" ", @addr) | 
|  | if @addr; | 
|  | } | 
|  |  | 
|  | join ", ", @addrs; | 
|  | } | 
|  |  | 
|  | #------------ | 
|  |  | 
|  | sub name | 
|  | {   my $self   = shift; | 
|  | my $phrase = $self->phrase; | 
|  | my $addr   = $self->address; | 
|  |  | 
|  | $phrase    = $self->comment | 
|  | unless defined $phrase && length $phrase; | 
|  |  | 
|  | my $name   = $self->_extract_name($phrase); | 
|  |  | 
|  | # first.last@domain address | 
|  | if($name eq '' && $addr =~ /([^\%\.\@_]+([\._][^\%\.\@_]+)+)[\@\%]/) | 
|  | {   ($name  = $1) =~ s/[\._]+/ /g; | 
|  | $name   = _extract_name $name; | 
|  | } | 
|  |  | 
|  | if($name eq '' && $addr =~ m#/g=#i)    # X400 style address | 
|  | {   my ($f) = $addr =~ m#g=([^/]*)#i; | 
|  | my ($l) = $addr =~ m#s=([^/]*)#i; | 
|  | $name   = _extract_name "$f $l"; | 
|  | } | 
|  |  | 
|  | length $name ? $name : undef; | 
|  | } | 
|  |  | 
|  |  | 
|  | sub host | 
|  | {   my $addr = shift->address || ''; | 
|  | my $i    = rindex $addr, '@'; | 
|  | $i >= 0 ? substr($addr, $i+1) : undef; | 
|  | } | 
|  |  | 
|  |  | 
|  | sub user | 
|  | {   my $addr = shift->address || ''; | 
|  | my $i    = rindex $addr, '@'; | 
|  | $i >= 0 ? substr($addr,0,$i) : $addr; | 
|  | } | 
|  |  | 
|  | 1; |