637 lines
14 KiB
Perl
637 lines
14 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::Header;
|
|
use vars '$VERSION';
|
|
$VERSION = '2.20';
|
|
|
|
|
|
use strict;
|
|
use Carp;
|
|
|
|
my $MAIL_FROM = 'KEEP';
|
|
my %HDR_LENGTHS = ();
|
|
|
|
our $FIELD_NAME = '[^\x00-\x1f\x7f-\xff :]+:';
|
|
|
|
|
|
##
|
|
## Private functions
|
|
##
|
|
|
|
sub _error { warn @_; () }
|
|
|
|
# tidy up internal hash table and list
|
|
|
|
sub _tidy_header
|
|
{ my $self = shift;
|
|
my $deleted = 0;
|
|
|
|
for(my $i = 0 ; $i < @{$self->{mail_hdr_list}}; $i++)
|
|
{ next if defined $self->{mail_hdr_list}[$i];
|
|
|
|
splice @{$self->{mail_hdr_list}}, $i, 1;
|
|
$deleted++;
|
|
$i--;
|
|
}
|
|
|
|
if($deleted)
|
|
{ local $_;
|
|
my @del;
|
|
|
|
while(my ($key,$ref) = each %{$self->{mail_hdr_hash}} )
|
|
{ push @del, $key
|
|
unless @$ref = grep { ref $_ && defined $$_ } @$ref;
|
|
}
|
|
|
|
delete $self->{'mail_hdr_hash'}{$_} for @del;
|
|
}
|
|
}
|
|
|
|
# fold the line to the given length
|
|
|
|
my %STRUCTURE = map { (lc $_ => undef) }
|
|
qw{ To Cc Bcc From Date Reply-To Sender
|
|
Resent-Date Resent-From Resent-Sender Resent-To Return-Path
|
|
list-help list-post list-unsubscribe Mailing-List
|
|
Received References Message-ID In-Reply-To
|
|
Content-Length Content-Type Content-Disposition
|
|
Delivered-To
|
|
Lines
|
|
MIME-Version
|
|
Precedence
|
|
Status
|
|
};
|
|
|
|
sub _fold_line
|
|
{ my($ln,$maxlen) = @_;
|
|
|
|
$maxlen = 20
|
|
if $maxlen < 20;
|
|
|
|
my $max = int($maxlen - 5); # 4 for leading spcs + 1 for [\,\;]
|
|
my $min = int($maxlen * 4 / 5) - 4;
|
|
|
|
$_[0] =~ s/[\r\n]+//og; # Remove new-lines
|
|
$_[0] =~ s/\s*\Z/\n/so; # End line with an EOLN
|
|
|
|
return if $_[0] =~ /^From\s/io;
|
|
|
|
if(length($_[0]) > $maxlen)
|
|
{ if($_[0] =~ /^([-\w]+)/ && exists $STRUCTURE{ lc $1 } )
|
|
{ #Split the line up
|
|
# first bias towards splitting at a , or a ; >4/5 along the line
|
|
# next split a whitespace
|
|
# else we are looking at a single word and probably don't want to split
|
|
my $x = "";
|
|
$x .= "$1\n " while $_[0] =~
|
|
s/^\s*
|
|
( [^"]{$min,$max} [,;]
|
|
| [^"]{1,$max} [,;\s]
|
|
| [^\s"]*(?:"[^"]*"[ \t]?[^\s"]*)+\s
|
|
) //x;
|
|
|
|
$x .= $_[0];
|
|
$_[0] = $x;
|
|
$_[0] =~ s/(\A\s+|[\t ]+\Z)//sog;
|
|
$_[0] =~ s/\s+\n/\n/sog;
|
|
}
|
|
else
|
|
{ $_[0] =~ s/(.{$min,$max})(\s)/$1\n$2/g;
|
|
$_[0] =~ s/\s*$/\n/s;
|
|
}
|
|
}
|
|
|
|
$_[0] =~ s/\A(\S+)\n\s*(?=\S)/$1 /so;
|
|
}
|
|
|
|
# Tags are case-insensitive, but there is a (slightly) preferred construction
|
|
# being all characters are lowercase except the first of each word. Also
|
|
# if the word is an `acronym' then all characters are uppercase. We decide
|
|
# a word is an acronym if it does not contain a vowel.
|
|
# In general, this change of capitalization is a bad idea, but it is in
|
|
# the code for ages, and therefore probably crucial for existing
|
|
# applications.
|
|
|
|
sub _tag_case
|
|
{ my $tag = shift;
|
|
$tag =~ s/\:$//;
|
|
join '-'
|
|
, map { /^[b-df-hj-np-tv-z]+$|^(?:MIME|SWE|SOAP|LDAP|ID)$/i
|
|
? uc($_) : ucfirst(lc($_))
|
|
} split m/\-/, $tag, -1;
|
|
}
|
|
|
|
# format a complete line
|
|
# ensure line starts with the given tag
|
|
# ensure tag is correct case
|
|
# change the 'From ' tag as required
|
|
# fold the line
|
|
|
|
sub _fmt_line
|
|
{ my ($self, $tag, $line, $modify) = @_;
|
|
$modify ||= $self->{mail_hdr_modify};
|
|
my $ctag = undef;
|
|
|
|
($tag) = $line =~ /^($FIELD_NAME|From )/oi
|
|
unless defined $tag;
|
|
|
|
if(defined $tag && $tag =~ /^From /io && $self->{mail_hdr_mail_from} ne 'KEEP')
|
|
{ if($self->{mail_hdr_mail_from} eq 'COERCE')
|
|
{ $line =~ s/^From /Mail-From: /o;
|
|
$tag = "Mail-From:";
|
|
}
|
|
elsif($self->{mail_hdr_mail_from} eq 'IGNORE')
|
|
{ return ();
|
|
}
|
|
elsif($self->{mail_hdr_mail_from} eq 'ERROR')
|
|
{ return _error "unadorned 'From ' ignored: <$line>";
|
|
}
|
|
}
|
|
|
|
if(defined $tag)
|
|
{ $tag = _tag_case($ctag = $tag);
|
|
$ctag = $tag if $modify;
|
|
$ctag =~ s/([^ :])$/$1:/o if defined $ctag;
|
|
}
|
|
|
|
defined $ctag && $ctag =~ /^($FIELD_NAME|From )/oi
|
|
or croak "Bad RFC822 field name '$tag'\n";
|
|
|
|
# Ensure the line starts with tag
|
|
if(defined $ctag && ($modify || $line !~ /^\Q$ctag\E/i))
|
|
{ (my $xtag = $ctag) =~ s/\s*\Z//o;
|
|
$line =~ s/^(\Q$ctag\E)?\s*/$xtag /i;
|
|
}
|
|
|
|
my $maxlen = $self->{mail_hdr_lengths}{$tag}
|
|
|| $HDR_LENGTHS{$tag}
|
|
|| $self->fold_length;
|
|
|
|
if ($modify && defined $maxlen)
|
|
{ # folding will fix bad header continuations for us
|
|
_fold_line $line, $maxlen;
|
|
}
|
|
elsif($line =~ /\r?\n\S/)
|
|
{ return _error "Bad header continuation, skipping '$tag': ",
|
|
"no space after newline in '$line'\n";
|
|
}
|
|
|
|
|
|
$line =~ s/\n*$/\n/so;
|
|
($tag, $line);
|
|
}
|
|
|
|
sub _insert
|
|
{ my ($self, $tag, $line, $where) = @_;
|
|
|
|
if($where < 0)
|
|
{ $where = @{$self->{mail_hdr_list}} + $where + 1;
|
|
$where = 0 if $where < 0;
|
|
}
|
|
elsif($where >= @{$self->{mail_hdr_list}})
|
|
{ $where = @{$self->{mail_hdr_list}};
|
|
}
|
|
|
|
my $atend = $where == @{$self->{mail_hdr_list}};
|
|
splice @{$self->{mail_hdr_list}}, $where, 0, $line;
|
|
|
|
$self->{mail_hdr_hash}{$tag} ||= [];
|
|
my $ref = \${$self->{mail_hdr_list}}[$where];
|
|
|
|
my $def = $self->{mail_hdr_hash}{$tag};
|
|
if($def && $where)
|
|
{ if($atend) { push @$def, $ref }
|
|
else
|
|
{ my $i = 0;
|
|
foreach my $ln (@{$self->{mail_hdr_list}})
|
|
{ my $r = \$ln;
|
|
last if $r == $ref;
|
|
$i++ if $r == $def->[$i];
|
|
}
|
|
splice @$def, $i, 0, $ref;
|
|
}
|
|
}
|
|
else
|
|
{ unshift @$def, $ref;
|
|
}
|
|
}
|
|
|
|
#------------
|
|
|
|
sub new
|
|
{ my $call = shift;
|
|
my $class = ref($call) || $call;
|
|
my $arg = @_ % 2 ? shift : undef;
|
|
my %opt = @_;
|
|
|
|
$opt{Modify} = delete $opt{Reformat}
|
|
unless exists $opt{Modify};
|
|
|
|
my $self = bless
|
|
{ mail_hdr_list => []
|
|
, mail_hdr_hash => {}
|
|
, mail_hdr_modify => (delete $opt{Modify} || 0)
|
|
, mail_hdr_foldlen => 79
|
|
, mail_hdr_lengths => {}
|
|
}, $class;
|
|
|
|
$self->mail_from( uc($opt{MailFrom} || $MAIL_FROM) );
|
|
|
|
$self->fold_length($opt{FoldLength})
|
|
if exists $opt{FoldLength};
|
|
|
|
if(!ref $arg) {}
|
|
elsif(ref($arg) eq 'ARRAY') { $self->extract( [ @$arg ] ) }
|
|
elsif(defined fileno($arg)) { $self->read($arg) }
|
|
|
|
$self;
|
|
}
|
|
|
|
|
|
sub dup
|
|
{ my $self = shift;
|
|
my $dup = ref($self)->new;
|
|
|
|
%$dup = %$self;
|
|
$dup->empty; # rebuild tables
|
|
|
|
$dup->{mail_hdr_list} = [ @{$self->{mail_hdr_list}} ];
|
|
|
|
foreach my $ln ( @{$dup->{mail_hdr_list}} )
|
|
{ my $tag = _tag_case +($ln =~ /^($FIELD_NAME|From )/oi)[0];
|
|
push @{$dup->{mail_hdr_hash}{$tag}}, \$ln;
|
|
}
|
|
|
|
$dup;
|
|
}
|
|
|
|
#------------
|
|
|
|
sub extract
|
|
{ my ($self, $lines) = @_;
|
|
$self->empty;
|
|
|
|
while(@$lines)
|
|
{ my $line = shift @$lines;
|
|
last if $line =~ /^\r?$/;
|
|
|
|
$line =~ /^($FIELD_NAME|From )/o or next;
|
|
my $tag = $1;
|
|
|
|
$line .= shift @$lines
|
|
while @$lines && $lines->[0] =~ /^[ \t]+/;
|
|
|
|
($tag, $line) = _fmt_line $self, $tag, $line;
|
|
|
|
_insert $self, $tag, $line, -1
|
|
if defined $line;
|
|
}
|
|
|
|
$self;
|
|
}
|
|
|
|
|
|
sub read
|
|
{ my ($self, $fd) = @_;
|
|
$self->empty;
|
|
|
|
my ($ln, $tag, $line);
|
|
while(1)
|
|
{ $ln = <$fd>;
|
|
|
|
if(defined $ln && defined $line && $ln =~ /^[ \t]+/)
|
|
{ $line .= $ln; # folded line
|
|
next;
|
|
}
|
|
|
|
if(defined $line)
|
|
{ ($tag, $line) = _fmt_line $self, $tag, $line;
|
|
_insert $self, $tag, $line, -1
|
|
if defined $line;
|
|
($tag, $line) = ();
|
|
}
|
|
|
|
last if !defined $ln || $ln =~ m/^\r?$/;
|
|
|
|
$ln =~ /^($FIELD_NAME|From )/o or next;
|
|
($tag, $line) = ($1, $ln);
|
|
}
|
|
|
|
$self;
|
|
}
|
|
|
|
|
|
sub empty
|
|
{ my $self = shift;
|
|
$self->{mail_hdr_list} = [];
|
|
$self->{mail_hdr_hash} = {};
|
|
$self;
|
|
}
|
|
|
|
|
|
sub header
|
|
{ my $self = shift;
|
|
|
|
$self->extract(@_)
|
|
if @_;
|
|
|
|
$self->fold
|
|
if $self->{mail_hdr_modify};
|
|
|
|
[ @{$self->{mail_hdr_list}} ];
|
|
}
|
|
|
|
|
|
sub header_hashref
|
|
{ my ($self, $hashref) = @_;
|
|
|
|
while(my ($key, $value) = each %$hashref)
|
|
{ $self->add($key, $_) for ref $value ? @$value : $value;
|
|
}
|
|
|
|
$self->fold
|
|
if $self->{mail_hdr_modify};
|
|
|
|
defined wantarray # MO, added minimal optimization
|
|
or return;
|
|
|
|
+{ map { ($_ => [$self->get($_)] ) } # MO: Eh?
|
|
keys %{$self->{mail_hdr_hash}}
|
|
};
|
|
}
|
|
|
|
#------------
|
|
|
|
sub modify
|
|
{ my $self = shift;
|
|
my $old = $self->{mail_hdr_modify};
|
|
|
|
$self->{mail_hdr_modify} = 0 + shift
|
|
if @_;
|
|
|
|
$old;
|
|
}
|
|
|
|
|
|
sub mail_from
|
|
{ my $thing = shift;
|
|
my $choice = uc shift;
|
|
|
|
$choice =~ /^(IGNORE|ERROR|COERCE|KEEP)$/
|
|
or die "bad Mail-From choice: '$choice'";
|
|
|
|
if(ref $thing) { $thing->{mail_hdr_mail_from} = $choice }
|
|
else { $MAIL_FROM = $choice }
|
|
|
|
$thing;
|
|
}
|
|
|
|
|
|
sub fold_length
|
|
{ my $thing = shift;
|
|
my $old;
|
|
|
|
if(@_ == 2)
|
|
{ my $tag = _tag_case shift;
|
|
my $len = shift;
|
|
|
|
my $hash = ref $thing ? $thing->{mail_hdr_lengths} : \%HDR_LENGTHS;
|
|
$old = $hash->{$tag};
|
|
$hash->{$tag} = $len > 20 ? $len : 20;
|
|
}
|
|
else
|
|
{ my $self = $thing;
|
|
my $len = shift;
|
|
$old = $self->{mail_hdr_foldlen};
|
|
|
|
if(defined $len)
|
|
{ $self->{mail_hdr_foldlen} = $len > 20 ? $len : 20;
|
|
$self->fold if $self->{mail_hdr_modify};
|
|
}
|
|
}
|
|
|
|
$old;
|
|
}
|
|
|
|
#------------
|
|
|
|
sub fold
|
|
{ my ($self, $maxlen) = @_;
|
|
|
|
while(my ($tag, $list) = each %{$self->{mail_hdr_hash}})
|
|
{ my $len = $maxlen
|
|
|| $self->{mail_hdr_lengths}{$tag}
|
|
|| $HDR_LENGTHS{$tag}
|
|
|| $self->fold_length;
|
|
|
|
foreach my $ln (@$list)
|
|
{ _fold_line $$ln, $len
|
|
if defined $ln;
|
|
}
|
|
}
|
|
|
|
$self;
|
|
}
|
|
|
|
|
|
sub unfold
|
|
{ my $self = shift;
|
|
|
|
if(@_)
|
|
{ my $tag = _tag_case shift;
|
|
my $list = $self->{mail_hdr_hash}{$tag}
|
|
or return $self;
|
|
|
|
foreach my $ln (@$list)
|
|
{ $$ln =~ s/\r?\n\s+/ /sog
|
|
if defined $ln && defined $$ln;
|
|
}
|
|
|
|
return $self;
|
|
}
|
|
|
|
while( my ($tag, $list) = each %{$self->{mail_hdr_hash}})
|
|
{ foreach my $ln (@$list)
|
|
{ $$ln =~ s/\r?\n\s+/ /sog
|
|
if defined $ln && defined $$ln;
|
|
}
|
|
}
|
|
|
|
$self;
|
|
}
|
|
|
|
|
|
sub add
|
|
{ my ($self, $tag, $text, $where) = @_;
|
|
($tag, my $line) = _fmt_line $self, $tag, $text;
|
|
|
|
defined $tag && defined $line
|
|
or return undef;
|
|
|
|
defined $where
|
|
or $where = -1;
|
|
|
|
_insert $self, $tag, $line, $where;
|
|
|
|
$line =~ /^\S+\s(.*)/os;
|
|
$1;
|
|
}
|
|
|
|
|
|
sub replace
|
|
{ my $self = shift;
|
|
my $idx = @_ % 2 ? pop @_ : 0;
|
|
|
|
my ($tag, $line);
|
|
TAG:
|
|
while(@_)
|
|
{ ($tag,$line) = _fmt_line $self, splice(@_,0,2);
|
|
|
|
defined $tag && defined $line
|
|
or return undef;
|
|
|
|
my $field = $self->{mail_hdr_hash}{$tag};
|
|
if($field && defined $field->[$idx])
|
|
{ ${$field->[$idx]} = $line }
|
|
else { _insert $self, $tag, $line, -1 }
|
|
}
|
|
|
|
$line =~ /^\S+\s*(.*)/os;
|
|
$1;
|
|
}
|
|
|
|
|
|
sub combine
|
|
{ my $self = shift;
|
|
my $tag = _tag_case shift;
|
|
my $with = shift || ' ';
|
|
|
|
$tag =~ /^From /io && $self->{mail_hdr_mail_from} ne 'KEEP'
|
|
and return _error "unadorned 'From ' ignored";
|
|
|
|
my $def = $self->{mail_hdr_hash}{$tag}
|
|
or return undef;
|
|
|
|
return $def->[0]
|
|
if @$def <= 1;
|
|
|
|
my @lines = $self->get($tag);
|
|
chomp @lines;
|
|
|
|
my $line = (_fmt_line $self, $tag, join($with,@lines), 1)[1];
|
|
|
|
$self->{mail_hdr_hash}{$tag} = [ \$line ];
|
|
$line;
|
|
}
|
|
|
|
|
|
sub get
|
|
{ my $self = shift;
|
|
my $tag = _tag_case shift;
|
|
my $idx = shift;
|
|
|
|
my $def = $self->{mail_hdr_hash}{$tag}
|
|
or return ();
|
|
|
|
my $l = length $tag;
|
|
$l += 1 if $tag !~ / $/o;
|
|
|
|
if(defined $idx || !wantarray)
|
|
{ $idx ||= 0;
|
|
defined $def->[$idx] or return undef;
|
|
my $val = ${$def->[$idx]};
|
|
defined $val or return undef;
|
|
|
|
$val = substr $val, $l;
|
|
$val =~ s/^\s+//;
|
|
return $val;
|
|
}
|
|
|
|
map { my $tmp = substr $$_,$l; $tmp =~ s/^\s+//; $tmp } @$def;
|
|
}
|
|
|
|
|
|
|
|
sub count
|
|
{ my $self = shift;
|
|
my $tag = _tag_case shift;
|
|
my $def = $self->{mail_hdr_hash}{$tag};
|
|
defined $def ? scalar(@$def) : 0;
|
|
}
|
|
|
|
|
|
|
|
sub delete
|
|
{ my $self = shift;
|
|
my $tag = _tag_case shift;
|
|
my $idx = shift;
|
|
my @val;
|
|
|
|
if(my $def = $self->{mail_hdr_hash}{$tag})
|
|
{ my $l = length $tag;
|
|
$l += 2 if $tag !~ / $/;
|
|
|
|
if(defined $idx)
|
|
{ if(defined $def->[$idx])
|
|
{ push @val, substr ${$def->[$idx]}, $l;
|
|
undef ${$def->[$idx]};
|
|
}
|
|
}
|
|
else
|
|
{ @val = map {my $x = substr $$_,$l; undef $$_; $x } @$def;
|
|
}
|
|
|
|
_tidy_header($self);
|
|
}
|
|
|
|
@val;
|
|
}
|
|
|
|
|
|
|
|
sub print
|
|
{ my $self = shift;
|
|
my $fd = shift || \*STDOUT;
|
|
|
|
foreach my $ln (@{$self->{mail_hdr_list}})
|
|
{ defined $ln or next;
|
|
print $fd $ln or return 0;
|
|
}
|
|
|
|
1;
|
|
}
|
|
|
|
|
|
sub as_string { join '', grep {defined} @{shift->{mail_hdr_list}} }
|
|
|
|
|
|
sub tags { keys %{shift->{mail_hdr_hash}} }
|
|
|
|
|
|
sub cleanup
|
|
{ my $self = shift;
|
|
my $deleted = 0;
|
|
|
|
foreach my $key (@_ ? @_ : keys %{$self->{mail_hdr_hash}})
|
|
{ my $fields = $self->{mail_hdr_hash}{$key};
|
|
foreach my $field (@$fields)
|
|
{ next if $$field =~ /^\S+\s+\S/s;
|
|
undef $$field;
|
|
$deleted++;
|
|
}
|
|
}
|
|
|
|
_tidy_header $self
|
|
if $deleted;
|
|
|
|
$self;
|
|
}
|
|
|
|
1;
|