| # 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; |