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;

