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