shell bypass 403
# Copyrights 2001-2019 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 distribution Mail-Box. Meta-POD processed with
# OODoc into POD and HTML manual-pages. See README.md
# Copyright Mark Overmeer. Licensed under the same terms as Perl itself.
package Mail::Box;
use vars '$VERSION';
$VERSION = '3.008';
use base 'Mail::Reporter';
use strict;
use warnings;
use Mail::Box::Message;
use Mail::Box::Locker;
use File::Spec;
use Carp;
use Scalar::Util 'weaken';
use List::Util qw/sum first/;
use Devel::GlobalDestruction 'in_global_destruction';
#-------------------------------------------
use overload '@{}' => sub { shift->{MB_messages} }
, '""' => 'name'
, 'cmp' => sub {$_[0]->name cmp "${_[1]}"};
#-------------------------------------------
sub new(@)
{ my $class = shift;
if($class eq __PACKAGE__)
{ my $package = __PACKAGE__;
croak <<USAGE;
You should not instantiate $package directly, but rather one of the
sub-classes, such as Mail::Box::Mbox. If you need automatic folder
type detection then use Mail::Box::Manager.
USAGE
}
my %args = @_;
weaken $args{manager}; # otherwise, the manager object may live too long
my $self = $class->SUPER::new
( @_
, init_options => \%args # for clone
) or return;
$self->read or return
if $self->{MB_access} =~ /r|a/;
$self;
}
sub init($)
{ my ($self, $args) = @_;
return unless defined $self->SUPER::init($args);
my $class = ref $self;
my $foldername = $args->{folder} || $ENV{MAIL};
unless($foldername)
{ $self->log(ERROR => "No folder name specified.");
return;
}
$self->{MB_foldername} = $foldername;
$self->{MB_init_options} = $args->{init_options};
$self->{MB_coerce_opts} = $args->{coerce_options} || [];
$self->{MB_access} = $args->{access} || 'r';
$self->{MB_remove_empty}
= defined $args->{remove_when_empty} ? $args->{remove_when_empty} : 1;
$self->{MB_save_on_exit}
= defined $args->{save_on_exit} ? $args->{save_on_exit} : 1;
$self->{MB_messages} = [];
$self->{MB_msgid} = {};
$self->{MB_organization} = $args->{organization} || 'FILE';
$self->{MB_linesep} = "\n";
$self->{MB_keep_dups} = !$self->writable || $args->{keep_dups};
$self->{MB_fix_headers} = $args->{fix_headers};
my $folderdir = $self->folderdir($args->{folderdir});
$self->{MB_trusted} = exists $args->{trusted} ? $args->{trusted}
: substr($foldername, 0, 1) eq '=' ? 1
: !defined $folderdir ? 0
: substr($foldername, 0, length $folderdir) eq $folderdir;
if(exists $args->{manager})
{ $self->{MB_manager} = $args->{manager};
weaken($self->{MB_manager});
}
my $message_type = $self->{MB_message_type}
= $args->{message_type} || $class . '::Message';
$self->{MB_body_type}
= $args->{body_type} || 'Mail::Message::Body::Lines';
$self->{MB_body_delayed_type}
= $args->{body_delayed_type}|| 'Mail::Message::Body::Delayed';
$self->{MB_head_delayed_type}
= $args->{head_delayed_type}|| 'Mail::Message::Head::Delayed';
$self->{MB_multipart_type}
= $args->{multipart_type} || 'Mail::Message::Body::Multipart';
$self->{MB_field_type} = $args->{field_type};
my $headtype = $self->{MB_head_type}
= $args->{head_type} || 'Mail::Message::Head::Complete';
my $extract = $args->{extract} || 'extractDefault';
$self->{MB_extract}
= ref $extract eq 'CODE' ? $extract
: $extract eq 'ALWAYS' ? sub {1}
: $extract eq 'LAZY' ? sub {0}
: $extract eq 'NEVER' ? sub {1} # compatibility
: $extract =~ m/\D/ ? sub {no strict 'refs';shift->$extract(@_)}
: sub { my $size = $_[1]->guessBodySize;
defined $size && $size < $extract;
};
#
# Create a locker.
#
$self->{MB_locker}
= $args->{locker}
|| Mail::Box::Locker->new
( folder => $self
, method => $args->{lock_type}
, timeout => $args->{lock_timeout}
, expires => $args->{lock_wait}
, file => ($args->{lockfile} || $args->{lock_file})
, $self->logSettings
);
$self;
}
#-------------------------------------------
sub folderdir(;$)
{ my $self = shift;
$self->{MB_folderdir} = shift if @_;
$self->{MB_folderdir};
}
sub foundIn($@) { shift->notImplemented }
sub name() {shift->{MB_foldername}}
sub type() {shift->notImplemented}
sub url()
{ my $self = shift;
$self->type . ':' . $self->name;
}
sub size() { sum map { $_->size } shift->messages('ACTIVE') }
sub update(@)
{ my $self = shift;
$self->updateMessages
( trusted => $self->{MB_trusted}
, head_type => $self->{MB_head_type}
, field_type => $self->{MB_field_type}
, message_type => $self->{MB_message_type}
, body_delayed_type => $self->{MB_body_delayed_type}
, head_delayed_type => $self->{MB_head_delayed_type}
, @_
);
$self;
}
sub organization() { shift->notImplemented }
sub addMessage($@)
{ my $self = shift;
my $message = shift or return $self;
my %args = @_;
confess <<ERROR if $message->can('folder') && defined $message->folder;
You cannot add a message which is already part of a folder to a new
one. Please use moveTo or copyTo.
ERROR
# Force the message into the right folder-type.
my $coerced = $self->coerce($message);
$coerced->folder($self);
unless($coerced->head->isDelayed)
{ # Do not add the same message twice, unless keep_dups.
my $msgid = $coerced->messageId;
unless($self->{MB_keep_dups})
{ if(my $found = $self->messageId($msgid))
{ $coerced->label(deleted => 1);
return $found;
}
}
$self->messageId($msgid, $coerced);
$self->toBeThreaded($coerced);
}
$self->storeMessage($coerced);
$coerced;
}
sub addMessages(@)
{ my $self = shift;
map $self->addMessage($_), @_;
}
sub copyTo($@)
{ my ($self, $to, %args) = @_;
my $select = $args{select} || 'ACTIVE';
my $subfolders = exists $args{subfolders} ? $args{subfolders} : 1;
my $can_recurse = not $self->isa('Mail::Box::POP3');
my ($flatten, $recurse)
= $subfolders eq 'FLATTEN' ? (1, 0)
: $subfolders eq 'RECURSE' ? (0, 1)
: !$subfolders ? (0, 0)
: $can_recurse ? (0, 1)
: (1, 0);
my $delete = $args{delete_copied} || 0;
my $share = $args{share} || 0;
$self->_copy_to($to, $select, $flatten, $recurse, $delete, $share);
}
# Interface may change without warning.
sub _copy_to($@)
{ my ($self, $to, @options) = @_;
my ($select, $flatten, $recurse, $delete, $share) = @options;
$self->log(ERROR => "Destination folder $to is not writable."),
return unless $to->writable;
# Take messages from this folder.
my @select = $self->messages($select);
$self->log(PROGRESS =>
"Copying ".@select." messages from $self to $to.");
foreach my $msg (@select)
{ if($msg->copyTo($to, share => $share))
{ $msg->label(deleted => 1) if $delete }
else { $self->log(ERROR => "Copying failed for one message.") }
}
return $self unless $flatten || $recurse;
# Take subfolders
SUBFOLDER:
foreach ($self->listSubFolders(check => 1))
{ my $subfolder = $self->openSubFolder($_, access => 'r');
$self->log(ERROR => "Unable to open subfolder $_"), next
unless defined $subfolder;
if($flatten) # flatten
{ unless($subfolder->_copy_to($to, @options))
{ $subfolder->close;
return;
}
}
else # recurse
{ my $subto = $to->openSubFolder($_, create => 1, access => 'rw');
unless($subto)
{ $self->log(ERROR => "Unable to create subfolder $_ of $to");
next SUBFOLDER;
}
unless($subfolder->_copy_to($subto, @options))
{ $subfolder->close;
$subto->close;
return;
}
$subto->close;
}
$subfolder->close;
}
$self;
}
sub close(@)
{ my ($self, %args) = @_;
my $force = $args{force} || 0;
return 1 if $self->{MB_is_closed};
$self->{MB_is_closed}++;
# Inform manager that the folder is closed.
my $manager = delete $self->{MB_manager};
$manager->close($self, close_by_self =>1)
if defined $manager && !$args{close_by_manager};
my $write;
for($args{write} || 'MODIFIED')
{ $write = $_ eq 'MODIFIED' ? $self->isModified
: $_ eq 'ALWAYS' ? 1
: $_ eq 'NEVER' ? 0
: croak "Unknown value to folder->close(write => $_).";
}
my $locker = $self->locker;
if($write && !$force && !$self->writable)
{ $self->log(WARNING => "Changes not written to read-only folder $self.
Suggestion: \$folder->close(write => 'NEVER')");
$locker->unlock if $locker;
$self->{MB_messages} = []; # Boom!
return 0;
}
my $rc = !$write
|| $self->write
( force => $force
, save_deleted => $args{save_deleted} || 0
);
$locker->unlock if $locker;
$self->{MB_messages} = []; # Boom!
$rc;
}
sub delete(@)
{ my ($self, %args) = @_;
my $recurse = exists $args{recursive} ? $args{recursive} : 1;
# Extra protection: do not remove read-only folders.
unless($self->writable)
{ $self->log(ERROR => "Folder $self not deleted: not writable.");
$self->close(write => 'NEVER');
return;
}
# Sub-directories need to be removed first.
if($recurse)
{ foreach ($self->listSubFolders)
{ my $sub = $self->openRelatedFolder
(folder => "$self/$_", access => 'd', create => 0);
defined $sub && $sub->delete(%args);
}
}
$self->close(write => 'NEVER');
$self;
}
#-------------------------------------------
sub appendMessages(@) {shift->notImplemented}
#-------------------------------------------
sub writable() {shift->{MB_access} =~ /w|a|d/ }
sub writeable() {shift->writable} # compatibility [typo]
sub readable() {1} # compatibility
sub access() {shift->{MB_access}}
sub modified(;$)
{ my $self = shift;
return $self->isModified unless @_; # compat 2.036
return
if $self->{MB_modified} = shift; # force modified flag
# unmodify all messages
$_->modified(0) foreach $self->messages;
0;
}
sub isModified()
{ my $self = shift;
return 1 if $self->{MB_modified};
foreach (@{$self->{MB_messages}})
{ return $self->{MB_modified} = 1
if $_->isDeleted || $_->isModified;
}
0;
}
#-------------------------------------------
sub message(;$$)
{ my ($self, $index) = (shift, shift);
@_ ? $self->{MB_messages}[$index] = shift : $self->{MB_messages}[$index];
}
sub messageId($;$)
{ my ($self, $msgid) = (shift, shift);
if($msgid =~ m/\<([^>]+)\>/s )
{ $msgid = $1;
$msgid =~ s/\s//gs;
$self->log(WARNING => "Message-id '$msgid' does not contain a domain.")
unless index($msgid, '@') >= 0;
}
return $self->{MB_msgid}{$msgid} unless @_;
my $message = shift;
# Undefine message?
unless($message)
{ delete $self->{MB_msgid}{$msgid};
return;
}
my $double = $self->{MB_msgid}{$msgid};
if(defined $double && !$self->{MB_keep_dups})
{ my $head1 = $message->head;
my $head2 = $double->head;
my $subj1 = $head1->get('subject') || '';
my $subj2 = $head2->get('subject') || '';
my $to1 = $head1->get('to') || '';
my $to2 = $head2->get('to') || '';
# Auto-delete doubles.
return $message->label(deleted => 1)
if $subj1 eq $subj2 && $to1 eq $to2;
$self->log(WARNING => "Different messages with id $msgid");
$msgid = $message->takeMessageId(undef);
}
$self->{MB_msgid}{$msgid} = $message;
weaken($self->{MB_msgid}{$msgid});
$message;
}
sub messageID(@) {shift->messageId(@_)} # compatibility
sub find($)
{ my ($self, $msgid) = (shift, shift);
my $msgids = $self->{MB_msgid};
if($msgid =~ m/\<([^>]*)\>/s)
{ $msgid = $1;
$msgid =~ s/\s//gs;
}
else
{ # Illegal message-id
$msgid =~ s/\s/+/gs;
}
$self->scanForMessages(undef, $msgid, 'EVER', 'ALL')
unless exists $msgids->{$msgid};
$msgids->{$msgid};
}
sub messages($;$)
{ my $self = shift;
return @{$self->{MB_messages}} unless @_;
my $nr = @{$self->{MB_messages}};
if(@_==2) # range
{ my ($begin, $end) = @_;
$begin += $nr if $begin < 0;
$begin = 0 if $begin < 0;
$end += $nr if $end < 0;
$end = $nr-1 if $end >= $nr;
return () if $begin > $end;
my @range = @{$self->{MB_messages}}[$begin..$end];
return @range;
}
my $what = shift;
my $action
= ref $what eq 'CODE'? $what
: $what eq 'DELETED' ? sub {$_[0]->isDeleted}
: $what eq 'ACTIVE' ? sub {not $_[0]->isDeleted}
: $what eq 'ALL' ? sub {1}
: $what =~ s/^\!// ? sub {not $_[0]->label($what)}
: sub {$_[0]->label($what)};
grep {$action->($_)} @{$self->{MB_messages}};
}
sub nrMessages(@) { scalar shift->messages(@_) }
sub messageIds() { map {$_->messageId} shift->messages }
sub allMessageIds() {shift->messageIds} # compatibility
sub allMessageIDs() {shift->messageIds} # compatibility
sub current(;$)
{ my $self = shift;
unless(@_)
{ return $self->{MB_current}
if exists $self->{MB_current};
# Which one becomes current?
my $current
= $self->findFirstLabeled(current => 1)
|| $self->findFirstLabeled(seen => 0)
|| $self->message(-1)
|| return undef;
$current->label(current => 1);
$self->{MB_current} = $current;
return $current;
}
my $next = shift;
if(my $previous = $self->{MB_current})
{ $previous->label(current => 0);
}
($self->{MB_current} = $next)->label(current => 1);
$next;
}
sub scanForMessages($$$$)
{ my ($self, $startid, $msgids, $moment, $window) = @_;
# Set-up msgid-list
my %search = map +($_ => 1), ref $msgids ? @$msgids : $msgids;
return () unless keys %search;
# do not run on empty folder
my $nr_messages = $self->messages
or return keys %search;
my $startmsg = defined $startid ? $self->messageId($startid) : undef;
# Set-up window-bound.
my $bound = 0;
if($window ne 'ALL' && defined $startmsg)
{ $bound = $startmsg->seqnr - $window;
$bound = 0 if $bound < 0;
}
my $last = ($self->{MBM_last} || $nr_messages) -1;
return keys %search if defined $bound && $bound > $last;
# Set-up time-bound
my $after = $moment eq 'EVER' ? 0
: $moment =~ m/^\d+$/ ? $moment
: !$startmsg ? 0
: $startmsg->timestamp - $self->timespan2seconds($moment);
while($last >= $bound)
{ my $message = $self->message($last);
my $msgid = $message->messageId; # triggers load
if(delete $search{$msgid}) # where we looking for this one?
{ last unless keys %search;
}
last if $message->timestamp < $after;
$last--;
}
$self->{MBM_last} = $last;
keys %search;
}
sub findFirstLabeled($;$$)
{ my ($self, $label, $set, $msgs) = @_;
if(!defined $set || $set)
{ my $f = first { $_->label($label) }
(defined $msgs ? @$msgs : $self->messages);
}
else
{ return first { not $_->label($label) }
(defined $msgs ? @$msgs : $self->messages);
}
}
#-------------------------------------------
sub listSubFolders(@) { () } # by default no sub-folders
sub openRelatedFolder(@)
{ my $self = shift;
my @options = (%{$self->{MB_init_options}}, @_);
$self->{MB_manager}
? $self->{MB_manager}->open(type => ref($self), @options)
: (ref $self)->new(@options);
}
sub openSubFolder($@)
{ my $self = shift;
my $name = $self->nameOfSubFolder(shift);
$self->openRelatedFolder(@_, folder => $name);
}
sub nameOfSubFolder($;$)
{ my ($thing, $name) = (shift, shift);
my $parent = @_ ? shift : ref $thing ? $thing->name : undef;
defined $parent ? "$parent/$name" : $name;
}
sub topFolderWithMessages() { 1 }
#-------------------------------------------
sub read(@)
{ my $self = shift;
$self->{MB_open_time} = time;
local $self->{MB_lazy_permitted} = 1;
# Read from existing folder.
return unless $self->readMessages
( trusted => $self->{MB_trusted}
, head_type => $self->{MB_head_type}
, field_type => $self->{MB_field_type}
, message_type => $self->{MB_message_type}
, body_delayed_type => $self->{MB_body_delayed_type}
, head_delayed_type => $self->{MB_head_delayed_type}
, @_
);
if($self->{MB_modified})
{ $self->log(INTERNAL => "Modified $self->{MB_modified}");
$self->{MB_modified} = 0; #after reading, no changes found yet.
}
$self;
}
#-------------------------------------------
sub write(@)
{ my ($self, %args) = @_;
unless($args{force} || $self->writable)
{ $self->log(ERROR => "Folder $self is opened read-only.");
return;
}
my (@keep, @destroy);
if($args{save_deleted})
{ @keep = $self->messages;
}
else
{ foreach ($self->messages)
{ if($_->isDeleted)
{ push @destroy, $_;
$_->diskDelete;
}
else {push @keep, $_}
}
}
unless(@destroy || $self->isModified)
{ $self->log(PROGRESS => "Folder $self not changed, so not updated.");
return $self;
}
$args{messages} = \@keep;
unless($self->writeMessages(\%args))
{ $self->log(WARNING => "Writing folder $self failed.");
return undef;
}
$self->modified(0);
$self->{MB_messages} = \@keep;
$self;
}
sub determineBodyType($$)
{ my ($self, $message, $head) = @_;
return $self->{MB_body_delayed_type}
if $self->{MB_lazy_permitted}
&& ! $message->isPart
&& ! $self->{MB_extract}->($self, $head);
my $bodytype = $self->{MB_body_type};
ref $bodytype ? $bodytype->($head) : $bodytype;
}
sub extractDefault($)
{ my ($self, $head) = @_;
my $size = $head->guessBodySize;
defined $size ? $size < 10000 : 0 # immediately extract < 10kb
}
sub lazyPermitted($)
{ my $self = shift;
$self->{MB_lazy_permitted} = shift;
}
sub storeMessage($)
{ my ($self, $message) = @_;
push @{$self->{MB_messages}}, $message;
$message->seqnr( @{$self->{MB_messages}} -1);
$message;
}
my %seps = (CR => "\015", LF => "\012", CRLF => "\015\012");
sub lineSeparator(;$)
{ my $self = shift;
return $self->{MB_linesep} unless @_;
my $sep = shift;
$sep = $seps{$sep} if exists $seps{$sep};
$self->{MB_linesep} = $sep;
$_->lineSeparator($sep) foreach $self->messages;
$sep;
}
sub create($@) {shift->notImplemented}
sub coerce($@)
{ my ($self, $message) = (shift, shift);
my $mmtype = $self->{MB_message_type};
$message->isa($mmtype) ? $message : $mmtype->coerce($message, @_);
}
sub readMessages(@) {shift->notImplemented}
sub updateMessages(@) { shift }
sub writeMessages(@) {shift->notImplemented}
sub locker() { shift->{MB_locker} }
sub toBeThreaded(@)
{ my $self = shift;
my $manager = $self->{MB_manager}
or return $self;
$manager->toBeThreaded($self, @_);
$self;
}
sub toBeUnthreaded(@)
{ my $self = shift;
my $manager = $self->{MB_manager}
or return $self;
$manager->toBeThreaded($self, @_);
$self;
}
#-------------------------------------------
sub timespan2seconds($)
{
if( $_[1] =~ /^\s*(\d+\.?\d*|\.\d+)\s*(hour|day|week)s?\s*$/ )
{ $2 eq 'hour' ? $1 * 3600
: $2 eq 'day' ? $1 * 86400
: $1 * 604800; # week
}
else
{ $_[0]->log(ERROR => "Invalid timespan '$_' specified.");
undef;
}
}
#-------------------------------------------
sub DESTROY
{ my $self = shift;
$self->close unless in_global_destruction || $self->{MB_is_closed};
}
#-------------------------------------------
1;