On Fri, 24 May 2013, Ben Morrow wrote:

At  4PM -0700 on 23/05/13 you (Dan Mahoney, System Admin) wrote:

I'm in the process of writing some scripts which I want to be able to take
actions on my local mailbox.  (For example, to move a subset of messages
to the trash over time, if unread for a week.  To act on messages in my
learn-spam folder and then delete them).

http://wiki2.dovecot.org/PreAuth

You can also use doveadm for quite a lot of this sort of administration;
this may be easier if you're scripting in shell rather than something
more sophisticated.

I'd definitely consider something like an SSH key with a forced
command (I do see questions in the FAQ about making dovecot work over a
socket connection), but that forgoes using standard imap clients.

Well, I'm not sure what you consider 'standard' here, but there are both
Perl and Python IMAP libraries which will connect to a command rather
than a socket. If you're using a client which insists on connecting to
an (INET) socket, it's a little harder; while you can obviously connect
preauthed imap to a listening socket with netcat, that's not remotely
secure.

I could also create a dovecot-only user with my UID and no other login
privileges, but I'd like this to "just work" for anyone.

I believe with the latest 2.2 you can also do this with Kerberos
principals, if you're running Kerberos; I haven't looked into this yet,
but I mean to (for much the same reason).

Ben



To access the mail storage on the imap server you can just speak the imap protocol and authenticate against the imap server just like any other mail client would do. If you are using Kerberos and have a ticket granting ticket (after e.g. kinit) then the authentication against a properly configured imap server is done without typing passwords. If the imap server does support pam (and dovecot does) then this is handled there.

As an example I do attach a script that logs a user into an imap server using Kerberos authentication and then displays the mail quota. Any other action on the users mailboxes could be done as well. The script makes use of SASL, therefore by changing the authenticate call and the callback routine any other SASL mechanism could be used as well.

If you intend to perform tasks centrally for more than one user then doveadm is certainly the right choice as was pointed out already

For accessing local mailboxes (i.e. not stored on an imap server) I'd recommend one of the perl modules that can parse and process mailboxes (typically in mbox format)

Wolfgang
#!/usr/bin/perl -w

use strict;
use vars qw ( %opt $imap $SERVICE $realm $host $gss_api_step $sasl $sasl_step 
$conn );
use Getopt::Std;
use Mail::IMAPClient;
use MIME::Base64;
use Term::ReadKey;
use Authen::Krb5;
# Authen::SASL::Cyrus needs to be installed as well !!!
# SASL2 needs to provide the gssapi auth library
use Authen::SASL;
use Authen::SASL::Cyrus;

(my $prog = $0) =~ s!.*/!!;

# on Solaris there is no system sasl2
if ( $^O eq 'solaris' ) {
$ENV{SASL_PATH} = "/usr/local/lib/sasl/2.1.15/lib/sasl2"
    if -d "/usr/local/lib/sasl/2.1.15/lib/sasl2";
}

getopts('vh:r:u:', \%opt) or usage();
my $user = getusername();

Authen::Krb5::init_context() or die "no context: $@\n";
Authen::Krb5::init_ets();
my $realm = Authen::Krb5::get_default_realm();
if ( $opt{r} and $realm ne $opt{r} ) {
    print "using realm $opt{r} instead of default realm $realm\n";
    $realm = $opt{r};
}
die "Kerberos realm unknown, please provide it with -r\n" if ! $realm;

# get the host name(s) of the imap server(s)
# the IMAP server is often called mail or imap, let's assume it is called imap
my @hosts;
if ( $opt{h} ) {
    @hosts = split(/[,\s]+/, $opt{h});
} else {
    my $server = "imap.\L$realm";
    my $rawip = gethostbyname($server);
    $server = "mail.\L$realm" if ! $rawip;
    $rawip = gethostbyname($server);
    @hosts = ( $server ) if $rawip;
}
die "No imap server name found, please specify a valid name with -h\n"
    if ! @hosts;

for $host ( @hosts ) {
    $gss_api_step = $sasl_step = 0;
    print "Connecting to $host:143 User $user\n" if $opt{v};
    $imap = Mail::IMAPClient->new(      
                            Server      => $host,
                            User        => $user,
    ) or die "couldn't connect to $host port 143: $!\n";
    $SERVICE = 'imap';
    $imap->authenticate('GSSAPI', \&gssapi_auth)
                       or die "Could not authenticate:$@\n";
    # handle change in Mail::IMAPClient API since version 3
    my ($quota, $maxquota);
    my $major_version = substr($Mail::IMAPClient::VERSION, 0, 1);
    if ( $major_version >= 3 ) {
        $quota = ($imap->tag_and_run('GETQUOTAROOT "INBOX" '))[2];
    } else {
        $quota = ($imap->GETQUOTAROOT("INBOX"))[1];
    }
    if ( ! $@ ) {
        ($quota, $maxquota) = $quota =~ /STORAGE (\d+) (\d+)/;
        if ( $maxquota ) {
            printf "MAILQUOTA on %s: %d of %d kB used (%.1f percent)\n",
                $host, $quota, $maxquota, 100*$quota/$maxquota;
        }
    } else {
        print $imap->LastError, "\n";
    }
    $imap->logout; # or die "Logout error: ", $imap->LastError, "\n";
}

exit;

sub usage {
    print <<EOF;
Usage: $prog [-h host] [-r realm] [-u user] [-v]
       check the mail quota for a user. Authenticate using KerberosV.
       -h host[s]  check host[s] instead of default mail server
       -r realm    use realm instead of default realm
       -u user     use user as username, different from logged in user
       -v          show authentication details and folders processed
EOF
    exit;
}

# required for the gssapi callback
sub getusername {
    die "Invalid username '$opt{u}', must be alphanumeric\n"
        if $opt{u} and $opt{u} !~ /^\w+$/;
    return $opt{u} if $opt{u};
    return getpwuid($<);
}

# gssapi_auth uses $SERVICE, $realm and $host from the calling environment
sub gssapi_auth {
    $gss_api_step++;
    if ( $gss_api_step == 1 ) {
        print "Using Kerberos5 (gssapi) for authentication\n" if $opt{v};
        $sasl = Authen::SASL->new(mechanism => 'GSSAPI',
                                  callback  => { user => \&getusername,
                                  realm => $realm }
        );
        my $ac = Authen::Krb5::AuthContext->new() or die "no context: $@\n";
        my $cc = Authen::Krb5::cc_default();
        my $ticket = Authen::Krb5::mk_req($ac, 0, $SERVICE, $host, 0, $cc);
        if ($user and ! $ticket) {
#           system "kinit", $user;
            my $psw = read_password($user);
            my $client = Authen::Krb5::parse_name($user);
            $realm = Authen::Krb5::get_default_realm();
            my $server = Authen::Krb5::parse_name("krbtgt/$realm");
            $cc->initialize($client);
            my $i = Authen::Krb5::get_in_tkt_with_password($client, $server, 
$psw, $cc);
            die "could not get ticket:$@\n", Authen::Krb5::error($i), "\n"
                unless $i;
            $ticket = Authen::Krb5::mk_req($ac, 0, $SERVICE, $host, 0, $cc);
            $ticket or die "mk_req failed";
        }
        $conn = $sasl->client_new($SERVICE, $host);
        my $err = $conn->error;
        die "gssapi_auth error in client_new: $err\n" if $err !~ /successful/;
        if ( ! grep {$_ eq 'GSSAPI' } $conn->global_listmech() ) {
            die "SASL mechanism GSSAPI not available, known methods are\n",
            join(', ', $conn->global_listmech()), "\n";
        }
        my $mesg = $conn->client_start;
        $err = $conn->error;
        die "gssapi_auth error in step $gss_api_step: $err\n" if $err !~ 
/successful/;
        return encode_base64($mesg, '');
    } else {
        my $mesg=$conn->client_step(decode_base64($_[0]));
        my $err = $conn->error;
        #print "gssapi_auth error in step $gss_api_step: $err\n" if $err;
        return encode_base64($mesg || '', '');
    }
}

sub read_password {
    local $|=1;
    my $user = $_[0] || getusername;
    print "Please enter (UNIX) password for user $user:";
    ReadMode('noecho');
    my $psw = ReadLine(0);
    chomp $psw;
    ReadMode('restore');
    print "\n";
    die "Empty password for $user, exiting.\n" unless $psw;
    return $psw;
}

Reply via email to