Perl x Open Food Facts Hackathon: Paris, France - May 24-25 Learn more

# Copyrights 2001-2025 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.03.
# This code is part of distribution Mail-Box-IMAP4. 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.
our $VERSION = '3.009';
}
use strict;
use Digest::HMAC_MD5; # only availability check for CRAM_MD5
use List::Util qw/first/;
sub init($)
{ my ($self, $args) = @_;
my $imap = $args->{imap_client} || 'Mail::IMAPClient';
if(ref $imap)
{ $args->{port} = $imap->Port;
$args->{hostname} = $imap->Server;
$args->{username} = $imap->User;
$args->{password} = $imap->Password;
}
else
{ $args->{port} ||= $args->{ssl} ? 993 : 143;
}
$args->{via} ||= 'imap4';
$self->SUPER::init($args) or return;
$self->authentication($args->{authenticate} || 'AUTO');
$self->{MTI_domain} = $args->{domain};
unless(ref $imap)
{ # Create the IMAP transporter
my %opts;
$opts{ucfirst lc} = delete $args->{$_}
for grep /^[A-Z]/, keys %$args;
# backwards compatibility
$opts{Starttls} ||= $args->{starttls};
my $ssl = $opts{Ssl} ||= $args->{ssl};
$opts{Ssl} = [ %$ssl ] if ref $ssl eq 'HASH';
$imap = $self->createImapClient($imap, %opts)
or return undef;
}
$self->imapClient($imap) or return undef;
$self->login or return undef;
$self;
}
sub url()
{ my $self = shift;
my ($host, $port, $user, $pwd) = $self->remoteHost;
my $name = $self->folderName;
my $proto = $self->usesSSL ? 'imap4s' : 'imap4';
"$proto://$user:$pwd\@$host:$port$name";
}
#------------------------------------------
sub usesSSL() { shift->imapClient->Ssl }
sub authentication(@)
{ my ($self, @types) = @_;
# What the client wants to use to login
@types
or @types = exists $self->{MTI_auth} ? @{$self->{MTI_auth}} : 'AUTO';
@types = qw/CRAM-MD5 DIGEST-MD5 PLAIN NTLM LOGIN/
if @types == 1 && $types[0] eq 'AUTO';
$self->{MTI_auth} = \@types;
my @clientside;
foreach my $auth (@types)
{ push @clientside
, ref $auth eq 'ARRAY' ? $auth
: $auth eq 'NTLM' ? [ NTLM => \&Authen::NTLM::ntlm ]
: [ $auth => undef ];
}
my %clientside = map +($_->[0] => $_), @clientside;
# What does the server support? in its order of preference.
my $imap = $self->imapClient or return ();
my @serverside = map { m/^AUTH=(\S+)/ ? uc($1) : () }
$imap->capability;
my @auth;
if(@serverside) # server list auth capabilities
{ @auth = map { $clientside{$_} ? delete $clientside{$_} : () }
@serverside;
}
@auth = @clientside unless @auth; # fallback to client's preference
@auth;
}
sub domain(;$)
{ my $self = shift;
return $self->{MTI_domain} = shift if @_;
$self->{MTI_domain} || ($self->remoteHost)[0];
}
#------------------------------------------
sub imapClient(;$)
{ my $self = shift;
@_ ? ($self->{MTI_client} = shift) : $self->{MTI_client};
}
sub createImapClient($@)
{ my ($self, $class, @args) = @_;
my ($host, $port) = $self->remoteHost;
my $debug_level = $self->logPriority('DEBUG')+0;
if($self->log <= $debug_level || $self->trace <= $debug_level)
{ tie *dh, 'Mail::IMAPClient::Debug', $self;
push @args, Debug => 1, Debug_fh => \*dh;
}
my $client = $class->new
( Server => $host, Port => $port
, User => undef, Password => undef # disable auto-login
, Uid => 1 # Safer
, Peek => 1 # Don't set \Seen automaticly
, @args
);
$self->log(ERROR => $@), return undef if $@;
$client;
}
sub login(;$)
{ my $self = shift;
my $imap = $self->imapClient;
return $self if $imap->IsAuthenticated;
my ($interval, $retries, $timeout) = $self->retry;
my ($host, $port, $username, $password) = $self->remoteHost;
unless(defined $username)
{ $self->log(ERROR => "IMAP4 requires a username and password");
return;
}
unless(defined $password)
{ $self->log(ERROR => "IMAP4 username $username requires a password");
return;
}
my $warn_fail;
while(1)
{
foreach my $auth ($self->authentication)
{ my ($mechanism, $challenge) = @$auth;
$imap->User(undef);
$imap->Password(undef);
$imap->Authmechanism(undef); # disable auto-login
$imap->Authcallback(undef);
unless($imap->connect)
{ $self->log(ERROR => "IMAP cannot connect to $host: "
, $imap->LastError);
return undef;
}
$imap->User($username);
$imap->Password($password);
$imap->Authmechanism($mechanism);
$imap->Authcallback($challenge) if defined $challenge;
if($imap->login)
{
$self->log(NOTICE => "IMAP4 authenication $mechanism to "
. "$username\@$host:$port successful");
return $self;
}
}
$self->log(ERROR => "Couldn't contact to $username\@$host:$port")
, return undef if $retries > 0 && --$retries == 0;
$warn_fail++
or $self->log(WARNING => "Failed attempt to login $username\@$host"
. ", retrying ".($retries+1)." times");
sleep $interval if $interval;
}
undef;
}
sub currentFolder(;$)
{ my $self = shift;
return $self->{MTI_folder} unless @_;
my $name = shift;
if(defined $self->{MTI_folder} && $name eq $self->{MTI_folder})
{ $self->log(DEBUG => "Folder $name already selected.");
return $name;
}
# imap first deselects the old folder so if the next call
# fails the server will not have anything selected.
$self->{MTI_folder} = undef;
my $imap = $self->imapClient or return;
if($name eq '/' || $imap->select($name))
{ $self->{MTI_folder} = $name;
$self->log(NOTICE => "Selected folder $name");
return 1;
}
# Just because we couldn't select the folder that doesn't mean it doesn't
# exist. It just means that this particular imap client is warning us
# that it can't contain messages. So we'll verify that it does exist
# and, if so, we'll pretend like we could have selected it as if it were
# a regular folder.
# IMAPClient::exists() only works reliably for leaf folders so we need
# to grep for it ourselves.
if(first { $_ eq $name } $self->folders)
{ $self->{MTI_folder} = $name;
$self->log(NOTICE => "Couldn't select $name but it does exist.");
return 0;
}
$self->log(NOTICE => "Folder $name does not exist!");
undef;
}
sub folders(;$)
{ my $self = shift;
my $top = shift;
my $imap = $self->imapClient or return ();
$top = undef if defined $top && $top eq '/';
# We need to force the remote IMAP client to only return folders
# *underneath* the folder we specify. By default they want to return
# all folders.
# Alas IMAPClient always appends the separator so, despite what it says
# in its own docs, there's purpose to doing this. We just need
# to get whatever we get and postprocess it. ???Still true???
my @folders = $imap->folders($top);
# We need to post-process the list returned by IMAPClient.
# This selects out the level of directories we're interested in.
my $sep = $imap->separator;
my $level = 1 + (defined $top ? () = $top =~ m/\Q$sep\E/g : -1);
# There may be duplications, thanks to subdirs so we uniq it
my %uniq;
$uniq{(split /\Q$sep\E/, $_)[$level] || ''}++ for @folders;
delete $uniq{''};
keys %uniq;
}
sub ids($)
{ my $self = shift;
my $imap = $self->imapClient or return ();
$imap->messages;
}
# Explanation in Mail::Box::IMAP4::Message chapter DETAILS
my %flags2labels =
( # Standard IMAP4 labels
'\Seen' => [seen => 1]
, '\Answered' => [replied => 1]
, '\Flagged' => [flagged => 1]
, '\Deleted' => [deleted => 1]
, '\Draft' => [draft => 1]
, '\Recent' => [old => 0]
# For the Netzwert extension (Mail::Box::Netzwert), some labels were
# added. You'r free to support them as well.
, '\Spam' => [spam => 1]
);
my %labels2flags;
while(my ($k, $v) = each %flags2labels)
{ $labels2flags{$v->[0]} = [ $k => $v->[1] ];
}
# where IMAP4 supports requests for multiple flags at once, we here only
# request one set of flags a time (which will be slower)
sub getFlags($$)
{ my ($self, $id) = @_;
my $imap = $self->imapClient or return ();
my $labels = $self->flagsToLabels(SET => $imap->flags($id));
# Add default values for missing flags
foreach my $s (values %flags2labels)
{ $labels->{$s->[0]} = not $s->[1]
unless exists $labels->{$s->[0]};
}
$labels;
}
sub listFlags() { keys %flags2labels }
# Mail::IMAPClient can only set one value a time, however we do more...
sub setFlags($@)
{ my ($self, $id) = (shift, shift);
my $imap = $self->imapClient or return ();
my (@set, @unset, @nonstandard);
while(@_)
{ my ($label, $value) = (shift, shift);
if(my $r = $labels2flags{$label})
{ my $flag = $r->[0];
$value = $value ? $r->[1] : !$r->[1];
# exor can not be used, because value may be string
$value ? (push @set, $flag) : (push @unset, $flag);
}
else
{ push @nonstandard, ($label => $value);
}
}
$imap->set_flag($_, $id) foreach @set;
$imap->unset_flag($_, $id) foreach @unset;
@nonstandard;
}
sub labelsToFlags(@)
{ my $thing = shift;
my @set;
if(@_==1)
{ my $labels = shift;
while(my ($label, $value) = each %$labels)
{ if(my $r = $labels2flags{$label})
{ push @set, $r->[0] if ($value ? $r->[1] : !$r->[1]);
}
}
}
else
{ while(@_)
{ my ($label, $value) = (shift, shift);
if(my $r = $labels2flags{$label})
{ push @set, $r->[0] if ($value ? $r->[1] : !$r->[1]);
}
}
}
join " ", sort @set;
}
sub flagsToLabels($@)
{ my ($thing, $what) = (shift, shift);
my %labels;
my $clear = $what eq 'CLEAR';
foreach my $f (@_)
{ if(my $lab = $flags2labels{$f})
{ $labels{$lab->[0]} = $clear ? not($lab->[1]) : $lab->[1];
}
else
{ (my $lab = $f) =~ s,^\\,,;
$labels{$lab}++;
}
}
if($what eq 'REPLACE')
{ my %found = map { ($_ => 1) } @_;
foreach my $f (keys %flags2labels)
{ next if $found{$f};
my $lab = $flags2labels{$f};
$labels{$lab->[0]} = not $lab->[1];
}
}
wantarray ? %labels : \%labels;
}
sub getFields($@)
{ my ($self, $id) = (shift, shift);
my $imap = $self->imapClient or return ();
my $parsed = $imap->parse_headers($id, @_) or return ();
my @fields;
while(my($n,$c) = each %$parsed)
{ push @fields, map { Mail::Message::Field::Fast->new($n, $_) } @$c;
}
@fields;
}
sub getMessageAsString($)
{ my $imap = shift->imapClient or return;
my $uid = ref $_[0] ? shift->unique : shift;
$imap->message_string($uid);
}
sub fetch($@)
{ my ($self, $msgs, @info) = @_;
return () unless @$msgs;
my $imap = $self->imapClient or return ();
my %msgs = map { ($_->unique => {message => $_} ) } @$msgs;
my $lines = $imap->fetch( [keys %msgs], @info );
# It's a pity that Mail::IMAPClient::fetch_hash cannot be used for
# single messages... now I had to reimplement the decoding...
while(@$lines)
{ my $line = shift @$lines;
next unless $line =~ /\(.*?UID\s+(\d+)/i;
my $id = $+;
my $info = $msgs{$id} or next; # wrong uid
if($line =~ s/^[^(]* \( \s* //x )
{ while($line =~ s/(\S+) # field
\s+
(?: # value
\" ( (?:\\.|[^"])+ ) \"
| \( ( (?:\\.|[^)])+ ) \)
| (\w+)
)//xi)
{ $info->{uc $1} = $+;
}
if( $line =~ m/^\s* (\S+) [ ]*$/x )
{ # Text block expected
my ($key, $value) = (uc $1, '');
while(@$lines)
{ my $extra = shift @$lines;
$extra =~ s/\r\n$/\n/;
last if $extra eq ")\n";
$value .= $extra;
}
$info->{$key} = $value;
}
}
}
values %msgs;
}
sub appendMessage($$)
{ my ($self, $message, $foldername, $date) = @_;
my $imap = $self->imapClient or return ();
$date = $imap->Rfc_822($date)
if $date && $date !~ m/\D/;
$imap->append_string
( $foldername, $message->string
, $self->labelsToFlags($message->labels)
, $date
);
}
sub destroyDeleted($)
{ my ($self, $folder) = @_;
defined $folder or return;
my $imap = shift->imapClient or return;
$imap->expunge($folder);
}
sub createFolder($)
{ my $imap = shift->imapClient or return ();
$imap->create(shift);
}
sub deleteFolder($)
{ my $imap = shift->imapClient or return ();
$imap->delete(shift);
}
#------------------------------------------
sub DESTROY()
{ my $self = shift;
my $imap = $self->imapClient;
$self->SUPER::DESTROY;
$imap->logout if defined $imap;
}
#------------------------------------------
# Tied filehandle translates IMAP's debug system into Mail::Reporter
# calls.
sub Mail::IMAPClient::Debug::TIEHANDLE($)
{ my ($class, $logger) = @_;
bless \$logger, $class;
}
sub Mail::IMAPClient::Debug::PRINT(@)
{ my $logger = ${ (shift) };
$logger->log(DEBUG => @_);
}
1;