# vim: set cindent expandtab ts=4 sw=4:
#
# Copyright (c) 1998-2005 Chi-Keung Ho. All rights reserved.
#
# This programe is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# Extmail - a high-performance webmail to maildir
# $Id$
package Ext::POP3;
use strict;
use Mail::POP3Client;
use Ext::Storage::Maildir;
use MIME::Base64;
use Fcntl qw(:flock);
use Ext::Utils; # import untaint()
use constant PROC_TIMEOUT => 1*60; # default processing timeout per account
use constant CHECK_INTVAL => 15*60; # default pop3 checking interval
use constant SOCK_TIMEOUT => 1*15; # default socket operation timeout
use constant DEAD_TIMEOUT => 30*60; # when will we remove a dead lock file?
use constant MAX_FILES => 30; # max files per receive process
use vars qw(@ISA @EXPORT);
@ISA = qw(Exporter);
@EXPORT = qw(parse_pop3config);
sub new {
my $this = shift;
my $self = bless {@_}, ref $this || $this;
$self->{uidlcf} = './pop3uidl.cf';
$self; # return the obj Ext::POP3
}
sub error {
my $self = shift;
my $err = $_[0];
if ($err) {
$self->{err} .= "$err\n";
}
return $self->{err};
0;
}
#
# XXX opt defination
#
# user
# passwd
# host
# port
# timeout
# check_intval
# max_files
# delete
sub init {
my $self = shift;
my %opt = @_;
$opt{timeout} = $opt{timeout} || SOCK_TIMEOUT;
$opt{max_files} = $opt{max_files} || MAX_FILES;
# XXX save opt
$self->{opt} = \%opt;
if (!$opt{user} || !$opt{passwd} || !$opt{host}) {
$self->error('Options not completed');
return;
}
my $pop = new Mail::POP3Client(
USER => $opt{user},
PASSWORD => $opt{passwd},
HOST => untaint($opt{host}),
PORT => untaint($opt{port}) || '110',
DEBUG => 0,
TIMEOUT => $opt{timeout},
STRIPCR => 1, # must strip, our lib can't handle crlf
AUTH_MODE => 'PASS',
);
$self->{pop} = $pop;
}
# do we need to receive pop3 or not?
# rc value: 0 -> can not, 1 -> can
#
# Newly design, for multiple pop3 accounts, we need to build
# a new mechanism, it can identify different pop session
# the can_receive() will always return 1 we calling it within
# the same object
sub can_receive {
my $self = shift;
my $config = $self->{uidlcf};
my $lockfile = "$config.lock";
my $uncheck = 0;
my $timeout = $self->{opt}->{check_intval} || CHECK_INTVAL;
# reduce redundent checking
return 1 if ($self->{can_receive});
# check dead locking to pop3uidl.cf.lock
if (-r "$lockfile") {
open (my $fh, "<", $lockfile) or
die "Error open $lockfile, $!\n";
if ($self->haslock($fh)) {
# somebody else locking it, we abort ?
return 0;
} else {
unlink $lockfile;
$uncheck = 1;
}
} else {
if (-r $config) {
my $mtime = (stat $config)[9];
$uncheck = 1 if (time - $mtime >= $timeout);
} else {
open (FD, "> $config"); # ignore error?
flock (FD, LOCK_EX);
print FD "";
flock (FD, LOCK_UN);
close FD;
$uncheck = 1;
}
}
if ($uncheck) {
# save pid
open (my $fh, "> $lockfile") or die "$!\n";
select((select($fh), $| = 1)[0]); # unbuffer
flock ($fh, LOCK_EX);
print $fh "$$";
$self->lock($fh);
$self->{lockfh} = $fh;
$self->{can_receive} = 1;
}
$uncheck;
}
sub lock {
my $self = shift;
my $fh = $_[0] || $self->{lockfh};
flock ($fh, LOCK_EX|LOCK_NB);
}
sub unlock {
my $self = shift;
my $fh = $_[0] || $self->{lockfh};
flock ($fh, LOCK_UN);
1;
}
sub haslock {
my $self = shift;
my $fh = $_[0] || $self->{lockfh};
if ($self->lock($fh)) {
$self->{_flock} = '1';
$self->unlock($fh);
return 0; # means no lock
}
1;
}
sub listsize {
my $ref = shift->{pop}->ListArray;
my @mid = (undef); # XXX redundent member
for (split(/\n/, $ref)) {
my ($id, $size) = (/^(\d+)\s*(\d+)\s*/);
push @mid, $size;
}
\@mid;
}
sub listuidl {
my $ref = shift->{pop}->Uidl;
my @uidl = (undef); # redundent member
return $ref if ($ref && ref $ref eq 'ARRAY');
for (split(/\n/, $ref)) {
my ($id, $uidl) = (/^(\d+)\s*(.*)\s*/);
push @uidl, $uidl;
}
\@uidl;
}
sub _combine {
my ($uidl, $size) = @_;
my @arr = (undef); # redundent member
return unless ($uidl && $size);
for (my $i=1; $i< scalar @$uidl; $i++) {
push @arr, {
id => $i,
uidl => $uidl->[$i],
size => $size->[$i],
};
}
\@arr;
}
# receive - retrieve mails from remote pop3 server
sub receive {
my $self = shift;
my $timeout = PROC_TIMEOUT; # hard code here! XXX FIXME
my %opt = %{$self->{opt}};
eval {
# install ALRM signal handler
local $SIG{ALRM} = sub {
# the object timeout, let everybody know it!
$self->{timeout} =1;
die "Time out\n"
};
alarm ($timeout);
$self->pop2maildir;
alarm (0);
};
if ($@ =~/Time out/) {
$self->error('POP3 operation timeout!');
}
# error message handler, know err from Mail::POP3Client are:
# ERR= POP3 command LIST may be given only in the 'TRANSACTION'
# state (current state is 'AUTHORIZATION').
# ERR= could not connect xxxxxxx
$_ = $self->{pop}->Message;
return $self->error unless ($_);
if (/AUTHORIZATION/) {
$self->error("$opt{user} authentication fail");
}
if (/^could not connect [^\:]+: (.*)/) {
my $res = $1;
if ($res =~ /in progress/) {
$res = 'time out';
}
$self->error("$opt{host} connection fail: $res\n");
}
$self->error;
}
sub pop2maildir {
my $self = shift;
my $max = $self->{opt}->{max_files};
my $pop = $self->{pop};
my $user = lc $pop->User; # get username
# Stage 1 - combine uidl with size and id, parse uidlcf
my $info = _combine($self->listuidl, $self->listsize);
my $uidl = _parse($self->{uidlcf});
my $counter = 0;
open (UIDL, ">> $self->{uidlcf}") or die "$!\n";
flock (UIDL, LOCK_EX);
for (my $i=1; $i < scalar @$info; $i++) {
last if ($counter >= $max);
last if ($self->{timeout});
my $u = $info->[$i]->{uidl};
next if ($uidl->{"$user/$u"});
my $tmpdraft = _gen_maildir_filename();
open (my $FD, "> ./tmp/$tmpdraft");
my $ok = $pop->RetrieveToFile($FD, $i);
close ($FD);
if ($ok) {
my $newdraft = _gen_maildir_filename("./tmp/$tmpdraft", 1);
my $size = (stat "./tmp/$tmpdraft")[7];
my $distname=$newdraft.",S=$size"; # marked as new
# Not overquota and file is completed
if ($size > 0 && is_overquota($size, 1) < 2) {
rename "./tmp/$tmpdraft", "./new/$distname";
print UIDL "$user/$u\n"; # save uild to uidlcf
update_quota_s({a => "$size 1"});
if (!$self->{opt}->{backup}) {
$pop->Delete($i); # mark the message delete
}
$counter ++;
} else {
$self->error("The uidl='$u' message retrieve broekn, $!\n");
unlink "./tmp/$tmpdraft";
}
} else {
unlink "./tmp/$tmpdraft"; # cleanup
$self->error($pop->Message);
}
}
flock (UIDL, LOCK_UN);
# update the modification timestamp
my $time = time;
utime $time, $time, $self->{uidlcf};
1;
}
sub _parse {
my $config = $_[0]; # must feed a file name
my %hash;
open (FD, "< $config") or return undef; # ignore error
while (<FD>) {
chomp;
$hash{$_} = 1;
}
close FD;
\%hash;
}
# this function tell Ext::POP3 we hit the end
# of object, time to destroy anything
sub finish {
my $self = shift;
delete $self->{finish};
delete $self->{_flock};
if ($self->{can_receive}) {
$self->unlock($self->{lockfh});
unlink './pop3uidl.cf.lock';
}
delete $self->{lockfh};
delete $self->{can_receive};
}
sub close {
my $self = shift;
my $pop = $self->{pop};
if ($pop) {
$pop->Close;
$self->{timeout} = 0;
$self->{err} = undef;
}
}
sub _gen_maildir_filename {
# according to http://cr.yp.to/proto/maildir.html and compatible
# with sqwebmail or maildrop etc, include postfix
my ($oldname, $flag) = @_;
if($oldname && $flag) { # get the standard maildir name
return gen_std_maildir($oldname);
}elsif($oldname) { # only strip status information
$oldname=~ s#([^,]+),S=.*#$1#;
return $oldname;
}else { # return the initial filename
return sprintf "%s_P%s_%s", time, $$, 'extmail';
}
}
sub _gen_name_tpart {
eval {
require 'sys/syscall.ph';
};
if($@) { return time; }
return time unless (defined &SYS_gettimeofday);
my $start = pack('LL', ());
syscall(&SYS_gettimeofday, $start, 0) != -1
or die "gettimeofday: $!";
my @start = unpack('LL', $start);
$start[0].'.M'.$start[1];
}
# parse pop3config.cf, storage struct
# An entry per line
#
# uid passwd host port option \n(newline)
#
# option => backup=on|off, color=#abcdef, active=on|off
# passwd => must base64 encoded (some password is space?)
sub parse_pop3config {
my $config = './pop3config.cf';
my @accounts;
if (-r $config) {
open (FD, "< $config") or die "$!\n";
while (my $buf = <FD>) {
chomp;
my @arr = split(/\s+/, $buf);
my $hash = {
uid => $arr[0],
passwd => decode_base64($arr[1]),
host => $arr[2],
port => $arr[3],
};
for (split(/,/, $arr[4])) {
/^([^=]+)=(.*)/;
$hash->{$1} = $2;
}
push @accounts, $hash;
}
return \@accounts;
}
[];
}
sub DESTROY {
my $self = shift;
$self->finish;
}
1;
syntax highlighted by Code2HTML, v. 0.9.1