#!/usr/bin/perl
# 
# authProg_txtdb.pl -- script that implements Courier authdaemond's pipe
# protocol. See: http://www.courier-mta.org/authlib for details. Documentation 
# script at the bottom of this file and is readable with 'perldoc'.

package authProg_txtdb_pl;
use strict;

# $SERIAL is current date; weird but it works and is easy to get
my $SERIAL = '2013dec23';

# Working area initialization
#
# Init vars, prototypes, open files, early parse data etc.

# -- Library header section: vars and subs used by the script

# vars used by lib functions
my ($true, $false, $failure) = (1,0,0);
# used to map upper case strings to lower, for convenience
my %acct_attrslookuplist = qw(ADDRESS address HOME home GID gid UID uid USERNAME name NAME name MAILDIR maildir QUOTA quota PASSWD passwd2 PASSWD2 passwd2);
# use for finding PRE attrs
my @acct_attrspre = qw(ADDRESS UID GID HOME);
# used for finding AUTH attrs. please note the "." at the end
my @acct_attrsauth = qw(ADDRESS UID GID HOME QUOTA PASSWD2 );
# used to tell that something is not implemented
my $acct_attrnotimplemented = '.';
# cmds in the authpipe protocol
my ($pre,$auth,$passwd,$enumerate) = qw(PRE AUTH PASSWD ENUMERATE);
# Protocol cmd numbers are: PRECMD = 100, AUTHCMD = 101, PASSWDCMD = 102, ENUMERATECMD = 103
my ($PRECMD,$AUTHCMD,$PASSWDCMD,$ENUMERATECMD) = (100,101,102,103);

# subs used by this script; functions defined near end of script
sub dbgprint; 
sub dblog;
sub loadtxtdb;
# sub parse_dataline;
sub prehandler;
sub authhandler;
# sub passwdhandler;
# sub enumhandler;

# Some misc vars for script main() part
my ($mainloopflag, $stop, $txtdbfilename) = (0,0,'authProg.txt.db');
# $dbhandle file is the hashref that holds the data parsed from the data file
my $dbhandle = {};
# Code protocol command numbers for easy access
my ($PRECMD,$AUTHCMD,$PASSWDCMD,$ENUMERATECMD) = qw(PRE AUTH PASSWD ENUMERATE);

# -- Main program processing
#
# Open file(s) and decide on what base on data recvd on <STDIN> etc

# Fetch data from authdaemond
my @recvdline = <STDIN>; 
chomp @recvdline;

# Choose command to serve. 1st element is authpipe command type (PRE AUTH etc)
my $protocolcmd = $recvdline[0];

# We don't yet support PASSWD and ENUMERATE commands (even though we have
# handlers for them). Short circuit here. Future versions will not have this.
if ( $protocolcmd =~ /^$PASSWDCMD/ ) 
{
# Fail since we don't yet support PASSWD; for now change passwords by hand.
    print "FAIL\n";
    exit 0;
}
elsif ( $protocolcmd =~ /^$ENUMERATECMD/ ) 
{
# Tell authdaemon that we do not support ENUMERATE yet
    print ".\n";
    exit 0;
}

# Call loadtxtdb() to load data into $dbhandle
$dbhandle = loadtxtdb($dbhandle,$txtdbfilename);
if ( $dbhandle == undef ) 
{ 
    system('logger', 'authProg: Courier authProg authProg could not open database txt file. This is serious as some mails may not be accepted by the smtpd service until this is fixed');

# Bail out: database is down. authpipe protocol says if DB down, exit without
# sending any data in STDOUT. And so we do that here, very early in script.
    exit(0);
}

# Decide which function gets called; it's like our 'switch'. 
if ($protocolcmd =~ /^$PRECMD/) 
{
# make var assignments and call handler 
    
    my ($servicename,$username) = ($recvdline[2],$recvdline[3]);

    prehandler($dbhandle,$servicename,$username); 
}
elsif ($protocolcmd =~ /^$AUTHCMD/) 
{ 
# make var assignments and call handler 
 
   my ($servicename,$username,$password) = ($recvdline[1],$recvdline[3],$recvdline[4]); 

   authhandler($dbhandle,$servicename,$username,$password); 

}
else 
{ 
# We got some weird data, we don't know or care what it is, so ...
    exit 1;
}

# Code below disabled for now
# --
# elsif ($protocolcmd == $PASSWDCMD) 
# { 
# 	passwdhandler(); 
# }
# elsif ($protocolcmd == $ENUMERATECMD) 
# { 
# 	enumhandler(); 
# }


# -- START PROGRAM LIBRARY 

# Function defs start here 
# 
# Functions are grouped by functionality, or area

# Our little debug function
sub dbgprint { print "authprog_lib: debug: @_\n"; }

# Our little log function
sub dbglog { system ('logger', "authProg: debug: @_\n"); }

# loadtxtdb ( ("string" OR scalar var with string), $db_handle);
#
# Takes "string" for file name and $db_handle as params. Reads the txtdatabase file into memory, parses and loads data into hashref. Hashref is returned and contains every record found in the database; it may be a large hashref depending on the size of the DB. Returns undef for failure to open DB, or '-1' for DB file parse error.
sub loadtxtdb {     
    my ($txtdbref,$txtdbfilename) = @_;

    my ($currntline,@currntlinetokens, @twotokenitems) = ( '',(),() );
    
    my $openretval = open(my $dbfile, '<',"$txtdbfilename");

    if ( $openretval == undef ) { return undef; }

# TODO: might need to add code to handle the options entry. It is complex in comparison to the rest of the fields in DB.

# Start our little loop to surf through the file. Compile each line into hashref
    while (<$dbfile>) {
# Save the currnt line to a more friendly var name.
	$currntline = $_;
	chomp $currntline;

	@currntlinetokens = split /\|/, $currntline;

# Username is first token in the stream. Save username db record away from
# everything else. We delete it later, so we have to save it here.
	my $usernametoken = $currntlinetokens[0];

# remove the username from the list
	shift @currntlinetokens;

# next few lines compile information into data structure
	foreach my $recordtoken (@currntlinetokens) {
	    @twotokenitems = split /=/, $recordtoken;

# Okay, first level of hash (the first $currentlineashlist) is the username.
# It anchors the data entry, i.e, how we access everything else. The $twotokenitems[n] usage is the key, and the 2nd $twotokenitems[n] usage is the value for that key stored in the hash. @twotokenitems[n] are the key/value pairs for data in the database.
	    $txtdbref->{$usernametoken}->{'name'} = $usernametoken;
	    $txtdbref->{$usernametoken}->{$twotokenitems[0]} = $twotokenitems[1];


# Applcation that uses this function for accessing the K-V pairs, it must do:
# my $username = recordshash->{$requestedname}->{name};
# my $user_uid = recordshash->{$requestedname}->{uid}; # etc and so on
	} # end inner data processing loop: tokens
    } # end main data processing loop: each record in the db

# By here, all records in the file have been processed and we proceed to 
# cleanup. We return the hash so that the rest of the application can make use
# of the data that we stored in txtdbref
    close($dbfile);
    return($txtdbref);
} # end loadtxtdb();


# Our protocol handling functions. The pipe protocol has four cmds: PRE, AUTH, PASSWD, and ENUMERATE. Each cmd has a handler function for it.

# prehandler($dbhandle,$servicename,$loginname);
#
# handler for the PRE cmd
# 
# TODO: update documentation for prehandler()
# Takes three parameters: $dbhandle is hashref that is the DB (in RAM) we will read data from, $servicename is the service name being used, $username is name being authed. Returns array reference of @pre_outbuffer which contains all the data needed to respond to the PRE command -- returns all data that the PRE protocol cmd expects in hashref.
sub prehandler {
# TODO: add check for number or args later
    my ($prehandlerref,$servicename,$username);
        $prehandlerref = $_[0];
    $servicename = $_[1];
    $username = $_[2];
    
# stage data to be returned in this var -- for return to caller
    my @pre_outbuffer = (); 

# Fail user for authentication if they are not in the DB
    if ( $prehandlerref->{$username}->{name} != $username ) {
	print "FAIL\n";
    } 
    
# were here so that means the user is in the db, so we compile the data and stage in it @pre_outbuffer so that we can return a reference of it to caller
    foreach my $courierauthattr (@acct_attrspre) {
	print "$courierauthattr=$prehandlerref->{$username}->{$acct_attrslookuplist{$courierauthattr}}\n";
    }
} # ends prehandler();

# authhandler($authhandlerref,"username","password");
#
# handler for the AUTH cmd. It takes hashref $authhandlerref, 'username' and 'password' strings, and tries to auth the user. It will take a param and see if the user acct is in the db, and the password (or response) from the client is correct. Returns undef if either username or password is incorrect. If authentication okay, return array reference containing data that Courier authdaemon expects to give user access.
sub authhandler {
# Currently ony the LOGIN  auth-type is supported currently. So we respond to login and fail all the others (cram-md5, cram-sha1, cram-256).
    my ($authhandlerref,$servicename,$username,$password) = @_;

# stuff results here
    my @auth_outbuffer = ();
    
# check if username is in the database; if no return error
    if ( $authhandlerref->{$username}->{name} != $username) { 
	print "FAIL\n"; 
    }
    
# check if user supplied password matches what we have in the database
    if ( $authhandlerref->{$username}->{'passwd2'} != $password) { 
	print "FAIL\n"; 
    }

# The user is in db and password is good. Compile-stage data in @auth_outbuffer
    my $attr_line;
foreach my $courierauthattr (@acct_attrsauth) {    
    $attr_line = "$courierauthattr" . '=' . "$authhandlerref->{$username}->{$acct_attrslookuplist{$courierauthattr}}";
    push @auth_outbuffer, $attr_line;
}

# print AUTH data to STDOUT
    foreach my $authstdoutline (@auth_outbuffer) { print "$authstdoutline\n"; }
# print ".\n" since it is proper authpipe protocol
    print ".\n";

} # ends authhandler();

# passwdhandler();
#
# handler for the PASSWD
sub passwdhandler {
# check if datafile open, else return with error status

# TODO: add passwd change support. Dont support passwdhandler yet so we send protocol proper response. All passwd change attemps currently fail.
    print "FAIL\n";
}

# ***************
# enumdhandler();
#
# handler for the ENUMERATE cmd
sub enumhandler {
# check if datafile open, else return with error status

# dont support enumhandler yet so we send protocol proper response.    
    print "$acct_attrnotimplemented\n";    
}




__END__



=pod


=head1 authProg_txtdb.pl

Implements Courier authpipe protocol 

=head2 Synopsis

authProg_txtdb.pl takes no arguments. The requested data will be printed out on STDOUT, thus the requested data will be returned to the calling program. It uses a text file for the database. It's similar to userdb and userdb.dat The data file is expected to live in /usr/local/etc/authlib/authProg.txt.db or the directory where authProg lives in your installation.

authProg_txtdb.pl implements Courier authdaemond's pipe protocol. See: http://www.courier-mta.org/authlib/INSTALL.html for details. This is an implementation of the authProg for the pipe protocol. authProgs may be implemented in any language, as long as the language can read from STDIN and write to STDOUT.

=head2 Sample record in db (text file with fields separated by '|')

foouser|uid=1000|gid=1000|home=/home/foouser/|address=foouser@host.com|name=foo user|maildir=Maildir|quota=maxbytes10M|passwd=$45*45345345t36583^36$|passwd2=fooplainp4sswd

Note: Options are not yet supported by this script. In the future, yes, but not now.

=head2 To install

Copy authProg_txtdb.pl to filename authProg, and then copy authProg file in the directory that you CourierMTA installation expects to find authProg (read the Courier documentation). It must be executable of course; use commen sense as the script doesnt do security checks on how you install the file and its permissions. No support mailing list for this yet. Just don't expect Mr. Sam V to fix it for you.

=head2 How to run authProg_txtdb.pl

At sh shell prompt:
    user@host:$ requesting_cmd | authProg_txtdb.pl

With authtest:

    user@host:$ authtest -s pop3 foouser foopassword

Or even:

    user@host:$ echo "PRE . courier foousername\n" | authProg_txtdb.pl

Output for successful authentication is, each item followed by newline:

    ADDRESS=full_email_address_of_authed_user
    HOME=/path_to_user_home
    GID=guid_of_authed_user       (or guid account data of the virtual user)
    UID=uid_of_authed_user        (or uid account data of the virtual user)

Output for failed authentication is (the data printed on STDOUT):
    FAIL <newline>        # just like in Perl, print "FAIL\n"

Normally, this script is called from a pipe within a program language such as C, Perl, LISP, or whatever else there is.

=head2 Features and notes of this authProg: authProg_txtdb.pl

The script on implements the basic requriements of the authpipe protocol. That is enough to authenticate users across the Internet for imap, pop3, and smtp mail access (the smtpd will accept mail for that email address).

=over 

=item Follows the pipe protocol

As noted above, it follows Courier authdaemond's authpipe protocol.
Protocol spec here: http://www.courier-mta.org/authlib/README_authlib.html#authpipeproto for details

=item Can be persistent -- not all authProgs are persistent

As the name authProg_txtdb.pl suggests, the program's main operation operates from that of a pipe. It reads data from STDIN and prints it out to STDOUT.

Note: this script will in the future have a persistent backend.

For Perl, this is important since all scripts must be parsed and compiled, so saving that script process can save some CPU time. authProg_txtdb.pl can take multiple authpipe protocol commands before exiting -- thus saving parse-compile CPU cycles.

=item Supports password changes

Note: planned for the future.

There is support for changing passwords in the databose in the future.

=item Does not support enumerate

The ENUMERATE protocol command is very expensive. It basically requests all of the accounts that are on the system. This is analog to requesting all the records in a database. Even if the records are only 128 bytes in length. The scary part is that you have... say 50,000 accounts on the email server. Luckily, the ENUMERATE command is optional.

Support for ENUMERATE is planned.

=back 

=head2 Script operation summary

Script operation summary (this command can operate as a daemon):
 initialize the working environment
 init need vars
 set things up and get ready to enter main processing loop
 enter loop
     read data
     run method for command requests
     look up data
     output data
     do loop again, unless time to exit
 de-init vars, close files etc, and miscellaneous cleanup
 give exit code, and QUIT

=head2 Options to pass the script

It takes the -f flag to specify a different datafile from the default /usr/local/etc/courier/authprogcsvbacker.txtdatabase

Note: -f flag not supported. db has file name as listed above, and lives in the same directory as authProg.

=head2 authProg_txtdb.pl txt database format

The database is a text file and is simple to parse. Each record (database entry) takes up one line terminated by a newline.

List of fields and descriptions in the database (these are required by authdaemond):
    USERNAME=username         -- system account which owns mailbox (name)
    UID=uid                   -- system account which owns mailbox (numeric uid)
    GID=gid                   -- numeric groupid
    HOME=homedir              -- home directory
    ADDRESS=addr              -- e-mail address
    NAME=name                 -- full name
    MAILDIR=maildir           -- Maildir relative to home directory
    QUOTA=quota               -- quota string: maxbytesS,maxfilesC
    PASSWD=cryptpasswd        -- encrypted password
    PASSWD2=plainpasswd       -- plain text password
    OPTIONS=acctoptions       -- option1=val1,option2=val2,...
    .                         -- token tells that protocol cmd not available

Here are some sample txt database entries:
    foouser|uid=1000|gid=1000|home=/home/foouser/|address=foouser@host.com|name=foo user|maildir=Maildir|quota=maxbytes10M|passwd=$45*45345345t36583^36$|passwd2=fooplainp4sswd

Note: Options field is not currently supported be the script. so leave it out of your text database file. Not all ATTRs are returned, just enough so that courier can receive ESMTP mail, and auth users over the Internet using courierpop3 and courierimapd with say Thunderbird or other popular e-mail clients.

=cut
