# 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 =~ /(?" 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;