#!/usr/bin/php
<?php
/*
  vim: set expandtab tabstop=4 softtabstop=4 shiftwidth=4:
  Codificación: UTF-8
  +----------------------------------------------------------------------+
  | Elastix version 2.0                                                  |
  | http://www.elastix.org                                               |
  +----------------------------------------------------------------------+
  | Copyright (c) 2006 Palosanto Solutions S. A.                         |
  +----------------------------------------------------------------------+
  | Cdla. Nueva Kennedy Calle E 222 y 9na. Este                          |
  | Telfs. 2283-268, 2294-440, 2284-356                                  |
  | Guayaquil - Ecuador                                                  |
  | http://www.palosanto.com                                             |
  +----------------------------------------------------------------------+
  | The contents of this file are subject to the General Public License  |
  | (GPL) Version 2 (the "License"); you may not use this file except in |
  | compliance with the License. You may obtain a copy of the License at |
  | http://www.opensource.org/licenses/gpl-license.php                   |
  |                                                                      |
  | Software distributed under the License is distributed on an "AS IS"  |
  | basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See  |
  | the License for the specific language governing rights and           |
  | limitations under the License.                                       |
  +----------------------------------------------------------------------+
  | The Original Code is: Elastix Open Source.                           |
  | The Initial Developer of the Original Code is PaloSanto Solutions    |
  +----------------------------------------------------------------------+
  $Id: emailaccount.php,v 1.1 2012/03/21 11:32:00 rocio Exp $*/

require_once 'Console/Getopt.php';

$CYRUS = NULL;
define('SASL_DOMAIN', 'example.com');

// Ugly - should not depend on anything under /var/www/html
ini_set('include_path', '/var/www/html:'.ini_get('include_path'));

$opt = Console_Getopt::getopt($argv, '', array(
    'createdomain=',        // Create a new domain
    'deletedomain=',        // Remove a domain
    'createaccount',        // Create a new account on a domain
    'deleteaccount',        // Delete an account on a domain
    'setaccountpassword',   // Change password on existing account
    'username=',            // Username to create or delete
    'password=',            // Password for username to create or update
    'domain=',              // Domain for username to create or delete
    'quota=',               // Quota for createaccount or updatequota

    'reconstruct_mailbox',  // Repair mailbox damage, cyrus function
    'mailbox=',    // mailbox that must be reconstructed
));

if (PEAR::isError($opt)) error_exit($opt->getMessage()."\n");

foreach ($opt[0] as $option) switch ($option[0]) {
    case '--createdomain':
        exit(create_domain($opt) ? 0 : 1);
	case '--deletedomain':
		exit(delete_domain($opt) ? 0 : 1);
	case '--createaccount':
	    exit(create_account($opt) ? 0 : 1);
	case '--deleteaccount':
	    exit(delete_account($opt) ? 0 : 1);
	case '--setaccountpassword':
	    exit(set_account_password($opt) ? 0 : 1);
    case '--reconstruct_mailbox':
        exit(reconstruct_mailbox($opt) ? 0 : 1);
}


error_exit("No action specified (--createdomain,--deletedomain,--createaccount,--deleteaccount,--reconstruct_mailbox)\n");


function error_exit($sMsg, $errorcode = 1)
{
    fwrite(STDERR, $sMsg);
    exit($errorcode);
}

function loadCyrusPassword()
{
    global $CYRUS;
    
    if (is_null($CYRUS)) {
        $CYRUS = array(
            'HOST'  =>  'localhost',
            'PORT'  =>  143,
            'ADMIN' =>  'cyrus',
            'PASS'  =>  'palosanto',
        );
        foreach (file('/etc/elastix.conf') as $s) {
            $regs = NULL;
            if (preg_match('/^cyrususerpwd\s*=\s*(.+)/', trim($s), $regs)) {
                $CYRUS['PASS'] = $regs[1];
            }
        }
    }
}

function create_domain($opt)
{
    $sNewDomain = NULL;
    
    foreach ($opt[0] as $option) switch ($option[0]) {
        case '--createdomain':
            $sNewDomain = $option[1];
            break;
    }    
    if (is_null($sNewDomain)) {
        error_exit("ERR: --createdomain: no domain specified.\n");
    }
    if (!preg_match('/^[\w\.\-]+$/', $sNewDomain)) {
        error_exit("ERR: --createdomain: invalid domain.\n");
    }
    
    // Check for existing domain, and add new domain to database
    try {
        $conn = new PDO('sqlite:/var/www/db/email.db');
        $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        
        $sth = $conn->prepare('SELECT domain_name FROM domain WHERE domain_name = ?');
        $sth->execute(array($sNewDomain));
        $domainList = $sth->fetchAll(PDO::FETCH_COLUMN);
        if (count($domainList) > 0) {
            error_exit("ERR: Domain name already exists\n");
        }        
        $sth = $conn->prepare('INSERT INTO domain(domain_name) VALUES (?)');
        $sth->execute(array($sNewDomain));
    } catch (PDOException $e) {
        error_exit("ERR: failed to check or insert domain - ".$e->getMessage()."\n");
    }

    // Add the new domain to the required key in main.cf
    $bPostfixElastix2 = isPostfixToElastix2();
    add_remove_domain_postfix($sNewDomain, TRUE, $bPostfixElastix2);
    
    return TRUE;
}

function delete_domain($opt)
{
    $sOldDomain = NULL;
    
    foreach ($opt[0] as $option) switch ($option[0]) {
        case '--deletedomain':
            $sOldDomain = $option[1];
            break;
    }    
    if (is_null($sOldDomain)) {
        error_exit("ERR: --deletedomain: no domain specified.\n");
    }
    
    loadCyrusPassword();

	require_once('libs/cyradm.php');

    // Connect to IMAP before doing anything else
    $cyr_conn = new cyradm;
    if (!$cyr_conn->imap_login()) {
        error_exit("ERR: failed to login to IMAP\n");
    }
    
    $bPostfixElastix2 = isPostfixToElastix2();
    try {
        $conn = new PDO('sqlite:/var/www/db/email.db');
        $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        
        $sthListDomain = $conn->prepare(
            'SELECT id, domain_name FROM domain WHERE domain_name = ?');
        $sthListAccounts = $conn->prepare(
            'SELECT username, password, id_domain, quota FROM accountuser '.
            'WHERE id_domain = ? ORDER BY username');
        $sthDeleteAccountDomain = $conn->prepare('DELETE FROM accountuser WHERE id_domain = ?');
        $sthDeleteDomain = $conn->prepare('DELETE FROM domain WHERE id = ?');

        // Query and list selected domain
        $sthListDomain->execute(array($sOldDomain));
        $domainList = $sthListDomain->fetchAll(PDO::FETCH_ASSOC);
        if (count($domainList) <= 0) {
            error_exit("ERR: specified domain not found in database.\n");
        }
        $domain = $domainList[0];
        
        // Query and list all accounts in the domain for removal
        $sthListAccounts->execute(array($domain['id']));
        $accountList = $sthListAccounts->fetchAll(PDO::FETCH_ASSOC);
        $preparedSth = array();
        foreach ($accountList as $account) {
            _internal_delete_account($conn, $preparedSth, $cyr_conn, $account, $bPostfixElastix2);
        }

        // Remove remaining references to domain in database
        $sthDeleteAccountDomain->execute(array($domain['id']));
        $sthDeleteDomain->execute(array($domain['id']));
    } catch (PDOException $e) {
        error_exit("ERR: failed to remove old domains - ".$e->getMessage()."\n");
    }
    $cyr_conn->imap_logout();

    // Remove the old domain from the required key in main.cf
    add_remove_domain_postfix($sOldDomain, FALSE, $bPostfixElastix2);
    
    return TRUE;
}

function _internal_delete_account($conn, &$preparedSth, $cyr_conn, $account, $bPostfixElastix2)
{
    if (!isset($preparedSth['listAliases'])) {
        $preparedSth['listAliases'] = $conn->prepare(
            'SELECT alias FROM virtual WHERE username = ? ORDER BY alias');
    }
    if (!isset($preparedSth['deleteAliases'])) {
        $preparedSth['deleteAliases'] = $conn->prepare(
            'DELETE FROM virtual WHERE username = ?');
    }
    if (!isset($preparedSth['deleteAccount'])) {
        $preparedSth['deleteAccount'] = $conn->prepare(
            'DELETE FROM accountuser WHERE username = ?');
    }
    $sthListAliases = $preparedSth['listAliases'];
    $sthDeleteAliases = $preparedSth['deleteAliases'];
    $sthDeleteAccount = $preparedSth['deleteAccount'];
    
    // Query any aliases for the account, and delete them
    $sthListAliases->execute(array($account['username']));
    $aliasList = $sthListAliases->fetchAll(PDO::FETCH_COLUMN);
    $sthDeleteAliases->execute(array($account['username']));
    
    // Delete account and associated mailbox
    $sthDeleteAccount->execute(array($account['username']));
    if (!$cyr_conn->deletemb('user/'.$account['username'])) {
        error_exit("ERR: failed to delete mailbox for username ".
            $account['username'].' - '.$cyr_conn->getMessage()."\n");
    }
    
    // Remove sasl passwords linked to aliases
    if (!$bPostfixElastix2 && count($aliasList) <= 0)
        $aliasList[] = $account['username'].'@'.SASL_DOMAIN;
    foreach ($aliasList as $sAlias) {
        $retval = NULL;
        system('/usr/sbin/saslpasswd2 -d '.escapeshellarg($sAlias), $retval);
        if ($retval != 0) {
            error_exit("ERR: $sLevelTag: failed to delete alias ".
                $sAlias.' for username '.$account['username']."\n");
        }
    }
}

function add_remove_domain_postfix($sDomain, $bAddRemove, $bPostfixElastix2)
{
    $sDomainKey = $bPostfixElastix2 ? 'virtual_mailbox_domains' : 'mydomain2';
    $postfixConf = file('/etc/postfix/main.cf');
    if ($postfixConf === FALSE) {
        error_exit("ERR: failed to load /etc/postfix/main.cf for domain update\n");
    }
    $bUpdated = FALSE;
    $bKeyExists = FALSE;
    foreach (array_keys($postfixConf) as $i) {
        $regs = NULL;
        if (preg_match('/^(\w+)\s*=\s*(.*)/', rtrim($postfixConf[$i]), $regs)) {
            if ($regs[1] == $sDomainKey) {
                $bKeyExists = TRUE;
                $oldDomainList = array_unique(preg_split('/,\s*/', $regs[2]));
                
                $newDomainList = $bAddRemove 
                    ? array_unique(array_merge($oldDomainList, array($sDomain)))
                    : array_diff($oldDomainList, array($sDomain));
                
                $postfixConf[$i] = $sDomainKey.' = '.implode(',', $newDomainList)."\n";
                $bUpdated = (count($oldDomainList) != count($newDomainList));
            }
        }
    }
    if (!$bKeyExists && $bAddRemove) {
        $postfixConf[] = "$sDomainKey = $sDomain\n";
        $bUpdated = TRUE;
    }
    if ($bUpdated) {
        if (FALSE === file_put_contents('/etc/postfix/main.cf', $postfixConf)) {
            error_exit("ERR: failed to write /etc/postfix/main.cf for domain update\n");
        }
        $retval = NULL;
        system('/usr/sbin/postfix reload', $retval);
        if ($retval != 0) {
            error_exit("ERR: failed to reload postfix after domain update ($retval)\n");
        }
    }
}

function create_account($opt)
{
    $bSuccess = FALSE;

    $sDomain = NULL;
    $sUsername = NULL;
    $sPassword = NULL;
    $sQuota = NULL;
    
    // Collect and validate options
    foreach ($opt[0] as $option) switch ($option[0]) {
        case '--domain':
            $sDomain = $option[1];
            break;
        case '--username':
            $sUsername = $option[1];
            break;
        case '--password':
            $sPassword = $option[1];
            break;
        case '--quota':
            $sQuota = $option[1];
            break;
    }    
    if (is_null($sDomain)) {
        error_exit("ERR: no domain specified.\n");
    }
    if (is_null($sUsername)) {
        error_exit("ERR: no username specified.\n");
    }
    if (is_null($sPassword)) {
        error_exit("ERR: no password specified.\n");
    }
    if (is_null($sQuota)) {
        error_exit("ERR: no quota specified.\n");
    }
    if (!preg_match('/^[a-z0-9]+([\._\-]?[a-z0-9]+[_\-]?)*$/', $sUsername)) {
        error_exit("ERR: Wrong format for username.\n");
    }
    if (!ctype_digit($sQuota)) {
        error_exit("ERR: invalid quota, expected integer.\n");
    }
    
    loadCyrusPassword();

	require_once('libs/cyradm.php');

    // Connect to IMAP before doing anything else
    $cyr_conn = new cyradm;
    if (!$cyr_conn->imap_login()) {
        error_exit("ERR: failed to login to IMAP\n");
    }
    
    $bPostfixElastix2 = isPostfixToElastix2();
    try {
        $conn = new PDO('sqlite:/var/www/db/email.db');
        $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $conn->beginTransaction();

        // Check whether username already exists
        $sth = $conn->prepare('SELECT username FROM accountuser WHERE username = ?');
        $sFullUsername = $bPostfixElastix2
            ? "{$sUsername}@{$sDomain}"
            : "{$sUsername}.{$sDomain}";
        $sth->execute(array($sFullUsername));
        $accountList = $sth->fetchAll(PDO::FETCH_COLUMN);
        if (count($accountList) > 0) {
            error_exit("ERR: The e-mail address already exists\n");
        }        

        // username, password, id_domain, quota
        $account = array(
            'username'      =>  $sUsername,
            'password'      =>  $sPassword,
            'domain'        =>  $sDomain,
            'quota'         =>  $sQuota,
        );
        $preparedSth = array();        
        $bSuccess = _internal_create_account($conn, $preparedSth, $cyr_conn,
            $account, $bPostfixElastix2);
        if ($bSuccess)
            $conn->commit();
        else $conn->rollBack();
    } catch (PDOException $e) {
        $conn->rollBack();
        error_exit("ERR: failed to add new account to database - ".$e->getMessage()."\n");
    }
    $cyr_conn->imap_logout();

    return $bSuccess;
}

/* This routine assumes it is running inside a transaction, and that the described
 * account has been already verified to not exist. This routine must perform all
 * of the database updates *BEFORE* performing any of the system-wide updates. */
function _internal_create_account($conn, &$preparedSth, $cyr_conn, $account, $bPostfixElastix2)
{
    global $CYRUS;

    if (!isset($preparedSth['listDomain'])) {
        $preparedSth['listDomain'] = $conn->prepare(
            'SELECT id FROM domain WHERE domain_name = ?');
    }
    if (!isset($preparedSth['createAccount'])) {
        $preparedSth['createAccount'] = $conn->prepare(
            'INSERT INTO accountuser (id_domain, username, password, quota) '.
            'VALUES (?, ?, ?, ?)');
    }
    if (!isset($preparedSth['createAlias'])) {
        $preparedSth['createAlias'] = $conn->prepare(
            'INSERT INTO virtual (username, alias) VALUES (?, ?)');
    }
    
    // Query domain ID
    $preparedSth['listDomain']->execute(array($account['domain']));
    $domainList = $preparedSth['listDomain']->fetchAll(PDO::FETCH_COLUMN);
    if (count($domainList) <= 0) {
        fwrite(STDERR, "ERR: domain {$account['domain']} not found.\n");
        return FALSE;
    }
    $idDomain = $domainList[0];

    // Insert into database
    $sFullUsername = $bPostfixElastix2
        ? "{$account['username']}@{$account['domain']}"
        : "{$account['username']}.{$account['domain']}";
    $preparedSth['createAccount']->execute(array($idDomain, $sFullUsername, $account['password'], $account['quota']));
    $preparedSth['createAlias']->execute(array($sFullUsername, "{$account['username']}@{$account['domain']}"));

    // No exceptions so far, create actual email account in system
    if (!_internal_create_account_password($sFullUsername, $account['password'], $bPostfixElastix2))
        return FALSE;
    
    // Attempt to create the mailbox for the user
    $bUndoEmailCreation = FALSE;
    $userpath = 'user/'.$sFullUsername;
    if (!$cyr_conn->createmb($userpath)) {
        fwrite(STDERR, "ERR: failed to create user {$sFullUsername}: ".$cyr_conn->getMessage()."\n");
        $bUndoEmailCreation = TRUE;
    } elseif (!$cyr_conn->setacl($userpath, $CYRUS['ADMIN'], 'lrswipcda')) {
        fwrite(STDERR, "ERR: failed to set ACL on user {$sFullUsername}: ".$cyr_conn->getMessage()."\n");
        $bUndoEmailCreation = TRUE;
    } elseif (!$cyr_conn->setmbquota($userpath, $account['quota'])) {
        fwrite(STDERR, "ERR: failed to set quota on user {$sFullUsername}: ".$cyr_conn->getMessage()."\n");
        $bUndoEmailCreation = TRUE;
    }
    
    // Attempt to rollback failed mailbox/user creation
    if ($bUndoEmailCreation) {
        if (!$cyr_conn->deletemb($userpath)) {
            fwrite(STDERR, "ERR: failed to remove user {$sFullUsername}: ".$cyr_conn->getMessage()."\n");
        }
        $sAlias = $sFullUsername; if (!$bPostfixElastix2) $sAlias .= '@'.SASL_DOMAIN;
        $retval = NULL;
        system('/usr/sbin/saslpasswd2 -d '.escapeshellarg($sAlias), $retval);
        if ($retval != 0) {
            fwrite(STDERR, "ERR: failed to delete alias {$sAlias} for username {$account['username']}\n");
        }
        return FALSE;
    }
    return TRUE;
}

function _internal_create_account_password($sFullUsername, $sPassword, $bPostfixElastix2)
{
    $sAccountCommand = '/usr/sbin/saslpasswd2 -c '.escapeshellarg($sFullUsername);
    if (!$bPostfixElastix2) $sAccountCommand .= ' -u '.SASL_DOMAIN;
    $procHandle = popen($sAccountCommand, 'w');
    fwrite($procHandle, $sPassword);
    $retval = pclose($procHandle);
    if ($retval != 0) {
        fwrite(STDERR, "ERR: failed to create account $sFullUsername ($retval)\n");
        return FALSE;
    }
    return TRUE;
}

function delete_account($opt)
{
    $sUsername = NULL;
    
    // Collect and validate options
    foreach ($opt[0] as $option) switch ($option[0]) {
        case '--username':
            $sUsername = $option[1];
            break;
    }    
    if (is_null($sUsername)) {
        error_exit("ERR: no username specified.\n");
    }

    loadCyrusPassword();

	require_once('libs/cyradm.php');

    // Connect to IMAP before doing anything else
    $cyr_conn = new cyradm;
    if (!$cyr_conn->imap_login()) {
        error_exit("ERR: failed to login to IMAP\n");
    }
    
    $bPostfixElastix2 = isPostfixToElastix2();
    try {
        $conn = new PDO('sqlite:/var/www/db/email.db');
        $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        
        $sth = $conn->prepare(
            'SELECT username FROM accountuser WHERE username = ?');
        $sth->execute(array($sUsername));
        $accountList = $sth->fetchAll(PDO::FETCH_COLUMN);
        if (count($accountList) <= 0) {
            fwrite(STDERR, "ERR: account {$sUsername} not found.\n");
            $cyr_conn->imap_logout();
            return FALSE;
        }
        $account = array(
            'username'  =>  $sUsername,
        );
        _internal_delete_account($conn, $preparedSth, $cyr_conn, $account, $bPostfixElastix2);
    } catch (PDOException $e) {
        error_exit("ERR: failed to remove old account - ".$e->getMessage()."\n");
    }
    $cyr_conn->imap_logout();

    return TRUE;
}

function set_account_password($opt)
{
    $bSuccess = TRUE;

    $sUsername = NULL;
    $sPassword = NULL;
    
    // Collect and validate options
    foreach ($opt[0] as $option) switch ($option[0]) {
        case '--username':
            $sUsername = $option[1];
            break;
        case '--password':
            $sPassword = $option[1];
            break;
    }    
    if (is_null($sUsername)) {
        error_exit("ERR: no username specified.\n");
    }
    if (is_null($sPassword)) {
        error_exit("ERR: no password specified.\n");
    }

    $bPostfixElastix2 = isPostfixToElastix2();
    try {
        $conn = new PDO('sqlite:/var/www/db/email.db');
        $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $conn->beginTransaction();

        // Check whether username already exists
        $sth = $conn->prepare('SELECT username FROM accountuser WHERE username = ?');
        $sth->execute(array($sUsername));
        $accountList = $sth->fetchAll(PDO::FETCH_COLUMN);
        if (count($accountList) <= 0) {
            $conn->rollBack();
            error_exit("ERR: Specified username not found\n");
        }        

        $sth = $conn->prepare('UPDATE accountuser SET password = ? WHERE username = ?');
        $sth->execute(array($sPassword, $sUsername));

        $bSuccess = _internal_create_account_password($sUsername, $sPassword, $bPostfixElastix2);
        if ($bSuccess)
            $conn->commit();
        else $conn->rollBack();
    } catch (PDOException $e) {
        $conn->rollBack();
        error_exit("ERR: failed to add new account to database - ".$e->getMessage()."\n");
    }

    return $bSuccess;
}

function reconstruct_mailbox($opt)
{
	$bExito = TRUE;
	$output = $ret = null;
	$username =  NULL;
    foreach ($opt[0] as $option) switch ($option[0]) {
		case '--mailbox':
			$username = $option[1];
			break;
	}

    $regexpUsuario = isPostfixToElastix2()
        ? '/^[a-z0-9]+([\._\-]?[a-z0-9]+[_\-]?)*@[a-z0-9]+([\._\-]?[a-z0-9]+)*(\.[a-z0-9]{2,6})+$/'
        : '/^([a-z0-9]+([\._\-]?[a-z0-9]+[_\-]?)*)$/';

	if (is_null($username)) {
		$bExito = FALSE;
        fprintf(STDERR, "ERR: mailbox must be specified\n");
	} elseif (!preg_match($regexpUsuario, $username)) {
        error_exit("ERR: invalid username\n");
	} else {
		$sMailboxPath = escapeshellarg("user/$username");
		$sCmdReconstruct = escapeshellarg("/usr/lib/cyrus-imapd/reconstruct -r -f $sMailboxPath");
		$sCmdSu = "su - cyrus -c $sCmdReconstruct";
		exec($sCmdSu, $output, $ret);
		
		if ($ret == 0) {
			exec("/etc/init.d/cyrus-imapd restart", $output, $ret);
		}
		if($ret == 0){
			sleep(8);
		}
	}

	return ($ret == 0);
}

function isPostfixToElastix2()
{
    foreach (file('/etc/imapd.conf') as $s) {
        if (strpos(trim($s), 'virtdomains: yes') === 0) return TRUE;
    }
    return FALSE;
}
?>
