<?php
# $Id: AliasHandler.php 1587 2013-11-16 20:20:50Z christian_boltz $ 

/** 
 * Handlers User level alias actions - e.g. add alias, get aliases, update etc.
 * @property $username name of alias
 * @property $return return of methods
 */
class AliasHandler extends PFAHandler {

    protected $db_table = 'alias';
    protected $id_field = 'address';
    protected $domain_field = 'domain';

    /**
     *
     * @public
     */
    public $return = null;

    protected function initStruct() {
        # hide 'goto_mailbox' if $this->new
        # (for existing aliases, init() hides it for non-mailbox aliases)
        $mbgoto = 1 - $this->new;

        $this->struct=array(
            # field name                allow       display in...   type    $PALANG label                     $PALANG description                 default / ...
            #                           editing?    form    list
            'address'       => pacol(   $this->new, 1,      1,      'mail', 'alias'                         , 'pCreate_alias_catchall_text'     ),
            'localpart'     => pacol(   $this->new, 0,      0,      'text', 'alias'                         , 'pCreate_alias_catchall_text'     , '', 
                /*options*/ '', 
                /*not_in_db*/ 1                         ),
            'domain'        => pacol(   $this->new, 0,      0,      'enum', ''                              , ''                                , '', 
                /*options*/ $this->allowed_domains      ),
            'goto'          => pacol(   1,          1,      1,      'txtl', 'to'                            , 'pEdit_alias_help'                , array() ),
            'is_mailbox'    => pacol(   0,          0,      1,      'int', ''                             , ''                                , 0 ,
                # technically 'is_mailbox' is bool, but the automatic bool conversion breaks the query. Flagging it as int avoids this problem.
                # Maybe having a vbool type (without the automatic conversion) would be cleaner - we'll see if we need it.
                /*options*/ '',
                /*not_in_db*/ 0,
                /*dont_write_to_db*/ 1,
                /*select*/ 'coalesce(__is_mailbox,0) as is_mailbox',
                /*extrafrom*/ 'LEFT JOIN ( ' .
                    ' SELECT 1 as __is_mailbox, username as __mailbox_username ' .
                    ' FROM ' . table_by_key('mailbox') .
                    ' WHERE username IS NOT NULL ' .
                    ' ) AS __mailbox ON __mailbox_username = address' ),
            'goto_mailbox'  => pacol(   $mbgoto,    $mbgoto,$mbgoto,'bool', 'pEdit_alias_forward_and_store' , ''                                , 0,
                /*options*/ '',
                /*not_in_db*/ 1                         ), # read_from_db_postprocess() sets the value
            'on_vacation'   => pacol(   1,          0,      1,      'bool', 'pUsersMenu_vacation'           , ''                                , 0 ,
                /*options*/ '', 
                /*not_in_db*/ 1                         ), # read_from_db_postprocess() sets the value - TODO: read active flag from vacation table instead?
            'active'        => pacol(   1,          1,      1,      'bool', 'active'                        , ''                                , 1     ),
            'created'       => pacol(   0,          0,      1,      'ts',   'created'                       , ''                                ),
            'modified'      => pacol(   0,          0,      1,      'ts',   'last_modified'                 , ''                                ),
            'editable'      => pacol(   0,          0,      1,      'int', ''                             , ''                                , 0 ,
                # aliases listed in $CONF[default_aliases] are read-only for domain admins if $CONF[special_alias_control] is NO.
                # technically 'editable' is bool, but the automatic bool conversion breaks the query. Flagging it as int avoids this problem.
                # Maybe having a vbool type (without the automatic conversion) would be cleaner - we'll see if we need it.
                /*options*/ '',
                /*not_in_db*/ 0,
                /*dont_write_to_db*/ 1,
                /*select*/ '1 as editable'              ),
        );
    }

    protected function initMsg() {
        $this->msg['error_already_exists'] = 'email_address_already_exists';
        $this->msg['error_does_not_exist'] = 'alias_does_not_exist';
        if ($this->new) {
            $this->msg['logname'] = 'create_alias';
            $this->msg['store_error'] = 'pCreate_alias_result_error';
            $this->msg['successmessage'] = 'pCreate_alias_result_success';
        } else {
            $this->msg['logname'] = 'edit_alias';
            $this->msg['store_error'] = 'pEdit_alias_result_error';
            $this->msg['successmessage'] = 'alias_updated';
        }
    }


    public function webformConfig() {
        if ($this->new) { # the webform will display a localpart field + domain dropdown on $new
            $this->struct['address']['display_in_form'] = 0;
            $this->struct['localpart']['display_in_form'] = 1;
            $this->struct['domain']['display_in_form'] = 1;
        }

        return array(
            # $PALANG labels
            'formtitle_create'  => 'pMain_create_alias',
            'formtitle_edit'    => 'pEdit_alias_welcome',
            'create_button'     => 'add_alias',

            # various settings
            'required_role' => 'admin',
            'listview'      => 'list-virtual.php',
            'early_init'    => 0,
            'prefill'       => array('domain'),
        );
    }

    /**
     * AliasHandler needs some special handling in init() and therefore overloads the function.
     * It also calls parent::init()
     */
    public function init($id) {
        @list($local_part,$domain) = explode ('@', $id); # supress error message if $id doesn't contain '@'

        if ($local_part == '*') { # catchall - postfix expects '@domain', not '*@domain'
            $id = '@' . $domain;
        }

        $retval = parent::init($id);

        if (!$retval) return false; # parent::init() failed, no need to continue
        
        # hide 'goto_mailbox' for non-mailbox aliases
        # parent::init called view() before, so we can rely on having $this->result filled
        # (only validate_new_id() is called from parent::init and could in theory change $this->result)
        if ($this->new || $this->result['is_mailbox'] == 0) {
            $this->struct['goto_mailbox']['editable']        = 0;
            $this->struct['goto_mailbox']['display_in_form'] = 0;
            $this->struct['goto_mailbox']['display_in_list'] = 0;
        }

        if ( !$this->new && $this->result['is_mailbox'] && $this->admin_username != ''&& !authentication_has_role('global-admin') ) {
            # domain admins are not allowed to change mailbox alias $CONF['alias_control_admin'] = NO
            # TODO: apply the same restriction to superadmins?
            if (!Config::bool('alias_control_admin')) {
                # TODO: make translateable
                $this->errormsg[] = "Domain administrators do not have the ability to edit user's aliases (check config.inc.php - alias_control_admin)";
                return false;
            }
        }

        return $retval;
    }

    protected function validate_new_id() {
        if ($this->id == '') {
            $this->errormsg[$this->id_field] = Config::lang('pCreate_alias_address_text_error1');
            return false;
        }

        list($local_part,$domain) = explode ('@', $this->id);

        if(!$this->create_allowed($domain)) {
            $this->errormsg[$this->id_field] = Config::lang('pCreate_alias_address_text_error3');
            return false;
        }
 
        # TODO: already checked in set() - does it make sense to check it here also? Only advantage: it's an early check
#        if (!in_array($domain, $this->allowed_domains)) { 
#            $this->errormsg[] = Config::lang('pCreate_alias_address_text_error1');
#            return false;
#        }

        if ($local_part == '') { # catchall
            $valid = true;
        } else {
            $email_check = check_email($this->id);
            if ($email_check == '') {
                $valid = true;
            } else {
                $this->errormsg[$this->id_field] = $email_check;
                $valid = false;
            }
        }

        return $valid;
    }

    /**
     * check number of existing aliases for this domain - is one more allowed?
     */
    private function create_allowed($domain) {
        if ($this->called_by == 'MailboxHandler') return true; # always allow creating an alias for a mailbox

        $limit = get_domain_properties ($domain);

        if ($limit['aliases'] == 0) return true; # unlimited
        if ($limit['aliases'] < 0) return false; # disabled
        if ($limit['alias_count'] >= $limit['aliases']) return false;
        return true;
    }


   /**
    * merge localpart and domain to address
    * called by edit.php (if id_field is editable and hidden in editform) _before_ ->init
    */
    public function mergeId($values) {
        if ($this->struct['localpart']['display_in_form'] == 1 && $this->struct['domain']['display_in_form']) { # webform mode - combine to 'address' field
            if (empty($values['localpart']) || empty($values['domain']) ) { # localpart or domain not set
                return "";
            }
            if ($values['localpart'] == '*') $values['localpart'] = ''; # catchall
            return $values['localpart'] . '@' . $values['domain'];
        } else {
            return $values[$this->id_field];
        }
    }

    protected function setmore($values) {
        if ($this->new) {
            if ($this->struct['address']['display_in_form'] == 1) { # default mode - split off 'domain' field from 'address' # TODO: do this unconditional?
                list(/*NULL*/,$domain) = explode('@', $values['address']);
                $this->values['domain'] = $domain;
            }
        }

        if (! $this->new) { # edit mode - preserve vacation and mailbox alias if they were included before
            $old_ah = new AliasHandler();

            if (!$old_ah->init($this->id)) {
                $this->errormsg[] = $old_ah->errormsg[0];
            } elseif (!$old_ah->view()) {
                $this->errormsg[] = $old_ah->errormsg[0];
            } else {
                $oldvalues = $old_ah->result();

                if (!isset($values['on_vacation'])) { # no new value given?
                    $values['on_vacation'] = $oldvalues['on_vacation'];
                }

                if ($values['on_vacation']) { 
                    $values['goto'][] = $this->getVacationAlias();
                }

                if ($oldvalues['is_mailbox']) { # alias belongs to a mailbox - add/keep mailbox to/in goto
                    if (!isset($values['goto_mailbox'])) { # no new value given?
                        $values['goto_mailbox'] = $oldvalues['goto_mailbox'];
                    }
                    if ($values['goto_mailbox']) {
                        $values['goto'][] = $this->id;

                        # if the alias points to the mailbox, don't display the "empty goto" error message
                        if (isset($this->errormsg['goto']) && $this->errormsg['goto'] == Config::lang('pEdit_alias_goto_text_error1') ) {
                            unset($this->errormsg['goto']);
                        }
                    }
                }
            }
        }

        $this->values['goto'] = join(',', $values['goto']);
    }

    protected function storemore() {
        # TODO: if alias belongs to a mailbox, update mailbox active status
        return true;
    }

    protected function read_from_db_postprocess($db_result) {
        foreach ($db_result as $key => $value) {
            # split comma-separated 'goto' into an array
            $db_result[$key]['goto'] = explode(',', $db_result[$key]['goto']);

            # Vacation enabled?
            list($db_result[$key]['on_vacation'], $db_result[$key]['goto']) = remove_from_array($db_result[$key]['goto'], $this->getVacationAlias() );

            # if it is a mailbox, does the alias point to the mailbox?
            if ($db_result[$key]['is_mailbox']) {
                # this intentionally does not match mailbox targets with recipient delimiter.
                # if it would, we would have to make goto_mailbox a text instead of a bool (which would annoy 99% of the users)
                list($db_result[$key]['goto_mailbox'], $db_result[$key]['goto']) = remove_from_array($db_result[$key]['goto'], $key);
            } else { # not a mailbox
                $db_result[$key]['goto_mailbox'] = 0;
            }

            # TODO: set 'editable' to 0 if not superadmin, $CONF[special_alias_control] == NO and alias is in $CONF[default_aliases]
            # TODO: see check_alias_owner() in functions.inc.php
        }

        return $db_result;
    }

    public function getList($condition, $limit=-1, $offset=-1) {
        # only list aliases that do not belong to mailboxes
        return parent::getList( "__is_mailbox IS NULL AND ( $condition )", $limit, $offset);
    }

    protected function _validate_goto($field, $val) {
        if (count($val) == 0) {
            # empty is ok for mailboxes - this is checked in setmore() which can clear the error message
            $this->errormsg[$field] = Config::lang('pEdit_alias_goto_text_error1');
            return false;
        }

        $errors = array();

        foreach ($val as $singlegoto) {
            if (substr($this->id, 0, 1) == '@' && substr($singlegoto, 0, 1) == '@') { # domain-wide forward - check only the domain part
                # only allowed if $this->id is a catchall
                # Note: alias domains are better, but we should keep this way supported for backward compatibility
                #       and because alias domains can't forward to external domains
                list (/*NULL*/, $domain) = explode('@', $singlegoto);
                $domain_check = check_domain($domain);
                if ($domain_check != '') {
                     $errors[] = "$singlegoto: $domain_check";
                }
            } else {
                $email_check = check_email($singlegoto);
                if ($email_check != '') {
                    $errors[] = "$singlegoto: $email_check";
                }
            }
        }

        if (count($errors)) {
            $this->errormsg[$field] = join("   ", $errors); # TODO: find a way to display multiple error messages per field
            return false;
        } else {
            return true;
        }
    }

    /**
     * on $this->new, set localpart based on address
     */
    protected function _missing_localpart  ($field) {
        if (isset($this->RAWvalues['address'])) {
            $parts = explode('@', $this->RAWvalues['address']);
            if (count($parts) == 2) $this->RAWvalues['localpart'] = $parts[0];
        }
    }

    /**
     * on $this->new, set domain based on address
     */
    protected function _missing_domain     ($field) {
        if (isset($this->RAWvalues['address'])) {
            $parts = explode('@', $this->RAWvalues['address']);
            if (count($parts) == 2) $this->RAWvalues['domain'] = $parts[1];
        }
    }


     /**
     * Returns the vacation alias for this user. 
     * i.e. if this user's username was roger@example.com, and the autoreply domain was set to
     * autoreply.fish.net in config.inc.php we'd return roger#example.com@autoreply.fish.net
     * @return string an email alias.
     */
    protected function getVacationAlias() {
        $vacation_goto = str_replace('@', '#', $this->id); 
        return $vacation_goto . '@' . Config::read('vacation_domain');
    }
 
    /**
     *  @return true on success false on failure
     */
    public function delete() {
        if( ! $this->view() ) {
            $this->errormsg[] = Config::Lang('alias_does_not_exist');
            return false;
        }

        if ($this->result['is_mailbox']) {
            $this->errormsg[] = Config::Lang('mailbox_alias_cant_be_deleted');
            return false;
        }

        db_delete('alias', 'address', $this->id);

        list(/*NULL*/,$domain) = explode('@', $this->id);
        db_log ($domain, 'delete_alias', $this->id);
        $this->infomsg[] = Config::Lang_f('pDelete_delete_success', $this->id);
        return true;
    }

}

/* vim: set expandtab softtabstop=4 tabstop=4 shiftwidth=4: */
