#!/usr/bin/perl -w
use strict;
use warnings;

use Time::HiRes qw(usleep);

### Settings
# Only %u, %n & %d are supported in PASSWORD_FILE and USER_FORMAT!
# Location of the password file
my $PASSWORD_FILE = '/home/vmail/%d/db/passwd';
# Leave undefined if users are listed in the passwd file in the same
# way they are received; or specify the format they appear in the passwd
# file.
#my $USER_FORMAT   = undef;
my $USER_FORMAT   = '%n';
# Location of dovecotpw binary
my $DOVECOTPW     = '/usr/sbin/dovecotpw';
# Hash method to use
# Methods which require the username are not supported
my $METHOD        = 'CRAM-MD5';
# Place to store logs
my $LOGFILE = '/var/log/horde/passwd.log';


### Exit codes:
# 1: Not all parameters were specified
# 2: Could not open pipe to dovecotpw
# 3: Error reading hash from dovecotpw pipe
# 4: Could not open passwd file
# 5: User not found
# 6: Could not open temporary passwd file for writing
# 7: Error in final rename
# 100: Wrong password informed

sub execute {
    my ($inputuser, $oldpassword, $newpassword) = read_input();

    my ($user, $passwdlocation) = substitute_variables($inputuser);

    check_password($user, $oldpassword, $passwdlocation);

    write_new_password($user, $newpassword, $passwdlocation);
}

sub read_input {
    my $username    = <STDIN>;
    my $oldpassword = <STDIN>;
    my $newpassword = <STDIN>;
    if (!$username || !$oldpassword || !$newpassword) {
        print "Internal error\n";
        print $::fLog "ERROR: Did not receive all parameters\n";
        print $::fLog "--- Ended in error! ---\n";
        close($::fLog);
        exit 1;
    }

    chomp($username);
    chomp($oldpassword);
    chomp($newpassword);

    return ($username, $oldpassword, $newpassword);
}

sub generate_hash {
    my ($password, $method) = @_;

    my $pwpipe;
    my $result = open($pwpipe, "-|");
    if (!defined($result)) {
        print "Internal error\n";
        print $::fLog "ERROR: Could not open pipe to dovecotpw\n";
        print $::fLog "--- Ended in error! ---\n";
        close($::fLog);
        exit 2;
    }
    if ($result == 0) {
        if (!exec($DOVECOTPW, "-s", $method, "-p", $password)) {
            print "Internal error\n";
            print $::fLog "ERROR: Could not open pipe to dovecotpw\n";
            print $::fLog "--- Ended in error! ---\n";
            close($::fLog);
            exit 2;
        }
    }

    my $hash = <$pwpipe>;
    if (!$hash) {
        print "Internal error\n";
        print $::fLog "ERROR: Error reading hash from dovecotpw pipe\n";
        print $::fLog "--- Ended in error! ---\n";
        close($::fLog);
        exit 3;
    }

    chomp($hash);
    return $hash;
}

sub get_hash {
    my ($user, $passwdlocation) = @_;

    my $pwfile;
    if (!open($pwfile, '<', $passwdlocation)) {
        print "Internal error\n";
        print $::fLog "ERROR: Could not open passwd file\n";
        print $::fLog "--- Ended in error! ---\n";
        close($::fLog);
        exit 4;
    }

    my ($hash, $method);
    while (<$pwfile>) {
      chomp;
      my ($thisuser, $thishash) = split(/:/);
      if ($thisuser eq $user) {
        $hash = $thishash;
        last;
      }
    }
    close($pwfile);

    if (!$hash) {
        print "Internal error\n";
        print $::fLog "ERROR: User not found\n";
        print $::fLog "--- Ended in error! ---\n";
        close($::fLog);
        exit 5;
    }

    if ($hash =~ /^\{([-a-zA-Z0-9]+)\}/) {
        $method = $1;
    } else {
        $method = 'PLAIN';
    }

    return ($hash, $method);
}

sub substitute_variables {
    my ($inputuser) = @_;

    my ($localpart, $domain);
    if ($inputuser =~ /^(.*)@(.*)$/) {
        $localpart = $1;
        $domain    = $2;
    } else {
        $localpart = $inputuser;
        $domain    = '';
    }

    my $user;
    if (defined $USER_FORMAT) {
        $user = $USER_FORMAT;
        $user =~ s/%u/$inputuser/;
        $user =~ s/%n/$localpart/;
        $user =~ s/%d/$domain/;
    } else {
        $user = $inputuser;
    }

    my $passwdfile = $PASSWORD_FILE;
    $passwdfile =~ s/%u/$inputuser/;
    $passwdfile =~ s/%n/$localpart/;
    $passwdfile =~ s/%d/$domain/;

    return ($user, $passwdfile);
}

sub check_password {
    my ($user, $oldpassword, $passwdlocation) = @_;

    my ($oldhash, $oldmethod) = get_hash($user, $passwdlocation);

    if ($oldhash ne generate_hash($oldpassword, $oldmethod)) {
        print "Wrong password\n";
        print $::fLog "ERROR: User provided wrong password\n";
        print $::fLog "--- End successfully, no change ---\n";
        close($::fLog);
        exit 100;
    }
}

sub write_new_password {
    my ($user, $newpassword, $passwdlocation) = @_;

    my $tempfile = $passwdlocation . ".new";

    # Dirty locking mechanism. Will wait forever if some process
    # leaves the .new file hanging around.
    while (1) {
        last if (! -f $tempfile);
        usleep(int(rand(2000)));
    }

    my $pwfile_old;
    if (!open($pwfile_old, '<', $passwdlocation)) {
        print "Internal error\n";
        print $::fLog "ERROR: Could not open passwd file\n";
        print $::fLog "--- Ended in error! ---\n";
        close($::fLog);
        exit 4;
    }

    my $pwfile_new;
    if (!open($pwfile_new, '>', $tempfile)) {
        print "Internal error\n";
        print $::fLog "ERROR: Could not open temporary passwd file for writing\n";
        print $::fLog "--- Ended in error! ---\n";
        close($::fLog);
        exit 6;
    }

    while (<$pwfile_old>) {
      chomp;

      if (/^$user:/) {
          print $pwfile_new "$user:" . generate_hash($newpassword, $METHOD) . "\n";
      } else {
          print $pwfile_new "$_\n";
      }
    }
    close($pwfile_new);
    close($pwfile_old);

    if (!rename($tempfile, $passwdlocation)) {
        unlink($tempfile);
        print "Internal error\n";
        print $::fLog "ERROR: Error in final rename\n";
        print $::fLog "--- Ended in error! ---\n";
        close($::fLog);
        exit 7;
    }

    print $::fLog "Changed password for $user in $passwdlocation\n";
}


### Real work stars here
our($fLog);
open($fLog, '>>', $LOGFILE);
print $fLog "--- Started at " . localtime() . " ---\n";

execute();

print $fLog "--- Ended successfully, changes were made ---\n";
close($fLog);

exit 0;
