280 lines
		
	
	
	
		
			6.8 KiB
		
	
	
	
		
			Perl
		
	
	
	
	
	
			
		
		
	
	
			280 lines
		
	
	
	
		
			6.8 KiB
		
	
	
	
		
			Perl
		
	
	
	
	
	
# 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;
 |