discourse-legacysite-perl/site/slowtwitch.com/cgi-bin/articles/GT/Maildir.pm
2024-06-17 21:49:12 +10:00

283 lines
7.5 KiB
Perl

package GT::Maildir;
use vars qw/$error $ERRORS @EXPORT @EXPORT_OK %EXPORT_TAGS/;
use strict;
use warnings;
use base 'GT::Base';
sub ST_DEV() { 0 }
sub ST_INO() { 1 }
sub ST_MODE() { 2 }
sub ST_NLINK() { 3 }
sub ST_UID() { 4 }
sub ST_GID() { 5 }
sub ST_RDEV() { 6 }
sub ST_SIZE() { 7 }
sub ST_ATIME() { 8 }
sub ST_MTIME() { 9 }
sub ST_CTIME() { 10 }
sub ST_BLKSIZE() { 11 }
sub ST_BLOCKS() { 12 }
sub ST_NEW () { 1 }
sub ST_CUR () { 2 }
eval {
require Time::HiRes;
Time::HiRes->import;
};
use Cwd;
use Sys::Hostname;
use Carp qw/croak/;
use Exporter();
sub MAILDIR_DELIVERY_TIMEOUT() { 60 * 30 } # 30 minutes
$ERRORS = {
CHDIR => 'Could not chdir to %s: %s',
MKTMPFILE => 'Race condition creating tmp file for delivery to %s',
FILE_MISSING => "Wrote maildir tmp file but now it's gone; Possible file system troubles",
LINK => "Failed to link %s to %s: %s",
OVERQUOTA => "User is over thier maildir quota",
TIMEOUT => "Timed out on maildir delivery"
};
*import = \&Exporter::import;
$error = '';
@EXPORT = ();
@EXPORT_OK = qw(
ST_NEW
ST_CUR
st_to_string
ST_DEV
ST_INO
ST_MODE
ST_NLINK
ST_UID
ST_GID
ST_RDEV
ST_SIZE
ST_ATIME
ST_MTIME
ST_CTIME
ST_BLKSIZE
ST_BLOCKS
);
%EXPORT_TAGS = (
all => [@EXPORT_OK, @EXPORT],
stat => [qw/
ST_DEV
ST_INO
ST_MODE
ST_NLINK
ST_UID
ST_GID
ST_RDEV
ST_SIZE
ST_ATIME
ST_MTIME
ST_CTIME
ST_BLKSIZE
ST_BLOCKS
/],
status => [qw(ST_NEW ST_CUR st_to_string)]
);
sub init {
my $self = shift;
my $class = ref $self;
croak "Invalid arguments to $class->new. Arguments must be key/value pairs" if @_ & 1;
my %opts = @_;
$opts{ lc $_ } = delete $opts{$_} for keys %opts;
croak "No Path specified to $class->new" unless exists $opts{path};
my $path = delete $opts{path};
croak "Invalid maildir path specified to $class->new" unless defined $path and length $path;
my $locker = delete $opts{locker};
unless ($locker) {
require GT::Maildir::Lock::NFSLock;
$locker = GT::Maildir::Lock::NFSLock->new;
}
my $subdir = delete $opts{subdir};
my $maildir_subdir = delete $opts{maildirsubdir};
$self->{_debug} = exists $opts{debug} ? delete $opts{debug} : $GT::Maildir::DEBUG;
croak "Unknown arguments to $class->new: ", join(", ", keys %opts) if keys %opts;
$self->{path} = $path;
$self->{maildir_subdir} = $maildir_subdir || 'Maildir';
$self->{subdir} = $subdir || 'gt';
$self->{locker} = $locker;
}
sub st_to_string {
my $st = shift;
return $st == ST_NEW ? "new" : "cur";
}
sub make_maildir_root {
my $self = shift;
croak "Unknown arguments to $self->make_maildir: ", join(", ", @_) if @_;
my $path = $self->get_maildir_path;
my $config_path = $self->get_config_path;
$self->get_locker->ex_lock($path, 60*5, 60*20);
unless (-d $path) {
unlink $path;
mkdir $path, 0700 or return $self->error("MKDIR", "WARN", $path, "$!");
}
for (qw(cur new tmp)) {
unless (-d "$path/$_") {
unlink "$path/$_";
mkdir "$path/$_", 0700 or return $self->error("MKDIR", "WARN", "$path/$_", "$!");
}
}
unless (-d $config_path) {
unlink $config_path;
mkdir $config_path, 0700 or return $self->error("MKDIR", "WARN", $path, "$!");
}
$self->get_locker->unlock($path);
return 1;
}
my $Maildir_Message_Number = 0;
sub deliver_message {
my $self = shift;
my $folder = shift;
my $folder_name = UNIVERSAL::isa($folder, "GT::Maildir::Folder")
? $folder->get_name
: $folder;
croak "Invalid folder $folder_name"
unless !ref($folder_name)
and defined $folder_name
and length $folder_name;
my $mail_thingy = shift;
my $mail_writer = UNIVERSAL::isa($mail_thingy, "GT::Mail")
? sub { $mail_thingy->write(shift) or die "$GT::Mail::error" }
: (!ref($mail_thingy) and -e $mail_thingy)
? sub { require GT::File::Tools; GT::File::Tools::copy($mail_thingy, shift) or die "$GT::File::Tools::error" }
: undef;
croak "Unknown email input $mail_thingy" unless defined $mail_writer;
my $quotastr = shift;
my $flags = join '', map { uc substr($_, 0, 1) } grep { defined and /^[DFRST]/i } @_;
my $path = $self->get_maildir_path;
my $folder_path = "$path/$folder_name";
my $cwd = getcwd || cwd || die "Could not get cwd";
unless (ref $mail_thingy) {
if ($mail_thingy !~ m{^/}) {
$mail_thingy = "$cwd/$mail_thingy";
}
}
chdir $folder_path or return $self->error("CHDIR", "WARN", $folder_path, "$!");
local $@;
eval {
local $SIG{__DIE__};
alarm 0;
};
my $can_alarm = $@ ? 0 : 1;
local $SIG{ALRM} = sub { die "TIMEOUT\n" };
my $pid = $$;
my $host = hostname;
$Maildir_Message_Number++;
my $tmpfile;
for (my $i = 0; ; $i++) {
my $t = time;
$tmpfile = "tmp/$t.$pid.$Maildir_Message_Number.$host";
if (!stat($tmpfile) and $! == 2) { # ENOENT
last;
}
if ($i == 2) {
return $self->error("MKTMPFILE", "WARN", "$folder_path/$tmpfile");
}
sleep 2;
}
if ($can_alarm) {
alarm(MAILDIR_DELIVERY_TIMEOUT);
}
my $newfile;
eval {
$mail_writer->($tmpfile);
undef $mail_thingy;
undef $mail_writer;
my @st = stat $tmpfile;
die "FILE_MISSING\n" unless @st;
if ($st[ST_SIZE] != 0 and $quotastr and $quotastr ne "NOQUOTA") {
require GT::Maildir::Quota;
my $q = GT::Maildir::Quota->open(".", $quotastr) or die "$GT::Maildir::Quota::error\n";
if (!$q->test($st[ST_SIZE], 1)) {
die "$GT::Maildir::Quota::error\n" if $GT::Maildir::Quota::error;
die "OVERQUOTA\n";
}
$q->add($st[ST_SIZE], 1);
$q->close();
}
my $new_tmp = "$tmpfile,S=$st[ST_SIZE]:2,$flags";
if (!rename($tmpfile, $new_tmp)) {
$self->error("RENAME", "FATAL", $tmpfile, $new_tmp, "$!");
}
$newfile = $new_tmp;
$newfile =~ s/tmp/new/;
if (!link($new_tmp, $newfile)) {
$self->error("LINK", "FATAL", $new_tmp, $newfile, "$!");
}
unlink $new_tmp;
};
if ($can_alarm) {
alarm 0;
}
if ($@) {
my $err = $@;
$err =~ s/\n//g;
chdir $cwd;
return $self->error($err, "WARN");
}
chdir $cwd;
return $newfile;
}
sub get_locker {
my $self = shift;
croak "Unknown arguments to $self->get_locker: ", join(", ", @_) if @_;
return $self->{locker};
}
sub get_path {
my $self = shift;
croak "Unknown arguments to $self->get_path: ", join(", ", @_) if @_;
return $self->{path};
}
sub get_subdir {
my $self = shift;
croak "Unknown arguments to $self->get_subdir: ", join(", ", @_) if @_;
return $self->{subdir};
}
sub get_maildir_subdir {
my $self = shift;
croak "Unknown arguments to $self->get_maildir_subdir: ", join(", ", @_) if @_;
return $self->{maildir_subdir};
}
sub get_maildir_path {
my $self = shift;
croak "Unknown arguments to $self->get_subdir: ", join(", ", @_) if @_;
return $self->{path} . "/" . $self->{maildir_subdir};
}
sub get_config_path {
my $self = shift;
croak "Unknown arguments to $self->get_subdir: ", join(", ", @_) if @_;
return $self->{path} . "/" . $self->{subdir};
}
1;