<?php defined('MW_PATH') || exit('No direct script access allowed');

/**
 * DeliveryServer
 * 
 * @package MailWizz EMA
 * @author Serban George Cristian <cristian.serban@mailwizz.com> 
 * @link http://www.mailwizz.com/
 * @copyright 2013-2014 MailWizz EMA (http://www.mailwizz.com)
 * @license http://www.mailwizz.com/license/
 * @since 1.0
 */
 
/**
 * This is the model class for table "delivery_server".
 *
 * The followings are the available columns in table 'delivery_server':
 * @property integer $server_id
 * @property integer $bounce_server_id
 * @property string $type
 * @property string $hostname
 * @property string $username
 * @property string $password
 * @property integer $port
 * @property string $protocol
 * @property integer $timeout
 * @property integer $probability
 * @property integer $hourly_quota
 * @property integer $hourly_sent
 * @property integer $custom_from_header
 * @property string $last_sent
 * @property string $meta_data
 * @property string $confirmation_key
 * @property string $status
 * @property string $date_added
 * @property string $last_updated
 * 
 * The followings are the available model relations:
 * @property Campaign[] $campaigns
 * @property BounceServer $bounceServer
 *
 */
class DeliveryServer extends ActiveRecord
{
    const TRANSPORT_SMTP = 'smtp';
    
    const TRANSPORT_SMTP_AMAZON = 'smtp-amazon';
    
    const TRANSPORT_SENDMAIL = 'sendmail';
    
    const TRANSPORT_PHP_MAIL = 'php-mail';
    
    const TRANSPORT_PICKUP_DIRECTORY = 'pickup-directory';
    
    const DELIVERY_FOR_SYSTEM = 'system';
    
    const DELIVERY_FOR_CAMPAIGN_TEST = 'campaign-test';
    
    const DELIVERY_FOR_TEMPLATE_TEST = 'template-test';
    
    const DELIVERY_FOR_CAMPAIGN = 'campaign';
    
    const DELIVERY_FOR_LIST = 'list';
    
    protected $serverType = 'smtp';
    
    // flag to mark what kind of delivery we are making
    protected $_deliveryFor = 'system';
    
    // what do we deliver
    protected $_deliveryObject;
    
    // list of additional headers to send for this server
    public $additional_headers = array();
    
    // the email to test the custom from header against
    public $custom_from_header_test_email;
    
    /**
     * @return string the associated database table name
     */
    public function tableName()
    {
        return '{{delivery_server}}';
    }
    
    /**
     * @return array validation rules for model attributes.
     */
    public function rules()
    {
        $rules = array(
            array('hostname, username, probability, hourly_quota, custom_from_header', 'required'),
            
            array('hostname, username, password', 'length', 'min' => 3, 'max'=>150),
            array('port, probability, timeout', 'numerical', 'integerOnly'=>true),
            array('port', 'length', 'min'=> 2, 'max' => 5),
            array('probability', 'length', 'min'=> 1, 'max' => 3),
            array('probability', 'in', 'range' => array_keys($this->getProbabilityArray())),
            array('timeout', 'numerical', 'min' => 5, 'max' => 120),
            array('protocol', 'in', 'range' => array_keys($this->getProtocolsArray())),
            array('hourly_quota', 'numerical', 'integerOnly' => true, 'min' => 0),
            array('hourly_quota', 'length', 'max' => 11),
            array('bounce_server_id', 'exist', 'className' => 'BounceServer', 'attributeName' => 'server_id'),
            array('custom_from_header', 'in', 'range' => array_keys($this->getCustomFromHeaderOptions())),
            array('custom_from_header_test_email', 'email', 'allowEmpty' => true),
            array('custom_from_header_test_email', '_validateCustomFromHeaderTestEmail'),
            array('hostname, username, type, status', 'safe', 'on' => 'search'),
            array('additional_headers', '_validateAdditionalHeaders'),
        );
        
        return CMap::mergeArray($rules, parent::rules());
    }
    
    /**
     * @return array relational rules.
     */
    public function relations()
    {
        $relations = array(
            'campaigns' => array(self::MANY_MANY, 'Campaign', '{{campaign_to_delivery_server}}(server_id, campaign_id)'),
            'bounceServer' => array(self::BELONGS_TO, 'BounceServer', 'bounce_server_id'),
        );
        
        return CMap::mergeArray($relations, parent::relations());
    }

    /**
     * @return array customized attribute labels (name=>label)
     */
    public function attributeLabels()
    {
        $labels = array(
            'server_id'         => Yii::t('servers', 'Server'),
            'bounce_server_id'  => Yii::t('servers', 'Bounce server'),
            'type'              => Yii::t('servers', 'Type'),
            'hostname'          => Yii::t('servers', 'Hostname'),
            'username'          => Yii::t('servers', 'Username'),
            'password'          => Yii::t('servers', 'Password'),
            'port'              => Yii::t('servers', 'Port'),
            'protocol'          => Yii::t('servers', 'Protocol'),
            'timeout'           => Yii::t('servers', 'Timeout'),
            'probability'       => Yii::t('servers', 'Probability'),
            'hourly_quota'      => Yii::t('servers', 'Hourly quota'),
            'custom_from_header'=> Yii::t('servers', 'Custom "From" header'),
            'meta_data'         => Yii::t('servers', 'Meta data'),
            'additional_headers'=> Yii::t('servers', 'Additional headers'),
            'custom_from_header_test_email' => Yii::t('servers', 'Custom "From" header test email'),
        );
        
        return CMap::mergeArray($labels, parent::attributeLabels());
    }
    
    /**
    * Retrieves a list of models based on the current search/filter conditions.
    * @return CActiveDataProvider the data provider that can return the models based on the search/filter conditions.
    */
    public function search()
    {
        $criteria=new CDbCriteria;

        $criteria->compare('hostname', $this->hostname, true);
        $criteria->compare('username', $this->username, true);
        $criteria->compare('type', $this->type);
        $criteria->compare('status', $this->status);

        return new CActiveDataProvider(get_class($this), array(
            'criteria'      => $criteria,
            'pagination'    => array(
                'pageSize'  => (int)Yii::app()->request->getQuery('pageSize', 10),
                'pageVar'   => 'page',
            ),
            'sort'=>array(
                'defaultOrder'  => array(
                    'server_id' => CSort::SORT_DESC,
                ),
            ),
        ));
    }
    
    /**
     * Returns the static model of the specified AR class.
     * Please note that you should have this exact method in all your CActiveRecord descendants!
     * @param string $className active record class name.
     * @return DeliveryServer the static model class
     */
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }
    
    protected function afterConstruct()
    {
        $this->additional_headers = (array)$this->getModelMetaData()->itemAt('additional_headers');
        $this->custom_from_header_test_email = (string)$this->getModelMetaData()->itemAt('custom_from_header_test_email');
        $this->_deliveryFor = self::DELIVERY_FOR_SYSTEM;
        parent::afterConstruct();
    }
    
    protected function afterFind()
    {
        $this->additional_headers = (array)$this->getModelMetaData()->itemAt('additional_headers');
        $this->custom_from_header_test_email = (string)$this->getModelMetaData()->itemAt('custom_from_header_test_email');
        $this->_deliveryFor = self::DELIVERY_FOR_SYSTEM;
        parent::afterFind();
    }
    
    // send method
    public function sendEmail(array $params = array())
    {
        return false;
    }
    
    public function getMailer()
    {
        return Yii::app()->mailer;
    }
    
    protected function beforeValidate()
    {
        if ($this->isNewRecord && in_array($this->serverType, array('smtp', 'sendmail')) && !function_exists('proc_open')) {
            $this->addError('username', Yii::t('servers', 'The function {func} must be enabled in order to use this server type', array(
                '{func}' => 'proc_open',
            )));
        }
        
        return parent::beforeValidate();
    }
    
    protected function beforeSave()
    {
        if (!$this->isNewRecord) {
            $model = self::model()->findByPk((int)$this->server_id);
            $keys = array('hostname', 'username', 'password', 'port', 'protocol', 'bounce_server_id');
            foreach ($keys as $key) {
                if ($model->$key !== $this->$key) {
                    $this->status = self::STATUS_INACTIVE;
                    break;
                }
            }
            
            if ($this->custom_from_header == self::TEXT_YES && ($model->custom_from_header == self::TEXT_NO || $model->custom_from_header_test_email != $this->custom_from_header_test_email)) {
                $this->status = self::STATUS_INACTIVE;
            }
        }
        
        $this->getModelMetaData()->add('additional_headers', (array)$this->additional_headers);
        $this->getModelMetaData()->add('custom_from_header_test_email', (string)$this->custom_from_header_test_email);
        $this->type = $this->serverType;
        
        return parent::beforeSave();
    }
    
    public function attributeHelpTexts()
    {
        $texts = array(
            'bounce_server_id'  => Yii::t('servers', 'The server that will handle bounce emails for this SMTP server.'),
            'hostname'          => Yii::t('servers', 'The hostname of your SMTP server, usually something like smtp.domain.com.'),
            'username'          => Yii::t('servers', 'The username of your SMTP server, usually something like you@domain.com.'),
            'password'          => Yii::t('servers', 'The password of your SMTP server, used in combination with your username to authenticate your request.'),
            'port'              => Yii::t('servers', 'The port of your SMTP server, usually this is 25, but 465 and 587 are also valid choices for some of the servers depending on the security protocol they are using. If unsure leave it to 25.'),
            'protocol'          => Yii::t('servers', 'The security protocol used to access this server. If unsure, leave it blank or select TLS if blank does not work for you.'),
            'timeout'           => Yii::t('servers', 'The maximum number of seconds we should wait for the server to respond to our request. 30 seconds is a proper value.'),
            'probability'       => Yii::t('servers', 'When having multiple servers from where you send, the probability helps to choose one server more than another. This is useful if you are using servers with various quota limits. A lower probability means a lower sending rate using this server.'),
            'hourly_quota'      => Yii::t('servers', 'In case there are limits that apply for sending with this server, you can set a hourly quota for it and it will only send in one hour as many emails as you set here. Set it to 0 in order to not apply any hourly limit.'),
            'custom_from_header'=> Yii::t('servers', 'Allow emails to contain a custom From header as set by customers. Please note that the server must support this feature.'),
        
            'custom_from_header_test_email' => Yii::t('servers', 'A valid email address to be used as a custom "From" header, only for validation purposes.'),
        );
        
        return CMap::mergeArray($texts, parent::attributeHelpTexts());
    }
    
    public function getBounceServersArray()
    {
        static $_options = array();
        if (!empty($_options)) {
            return $_options;
        }
        
        $criteria = new CDbCriteria();
        $criteria->select = 'server_id, hostname, username, service';
        $criteria->order = 'server_id DESC';
        $models = BounceServer::model()->findAll($criteria);
        
        $_options[''] = Yii::t('app', 'Choose');
        foreach ($models as $model) {
            $_options[$model->server_id] = sprintf('%s - %s(%s)', strtoupper($model->service), $model->hostname, $model->username);
        }
        
        return $_options;
    }
    
    public function getDisplayBounceServer()
    {
        if (empty($this->bounceServer)) {
            return;
        }
        
        $model = $this->bounceServer;
        
        return sprintf('%s - %s(%s)', strtoupper($model->service), $model->hostname, $model->username);
    }
    
    public function getProtocolsArray()
    {
        return array(
            ''          => Yii::t('app', 'Choose'),
            'tls'       => 'TLS',
            'ssl'       => 'SSL',
            'starttls'  => 'STARTTLS',
        );
    }
    
    public function getProtocolName()
    {
        $protocols = $this->getProtocolsArray();
        return !empty($this->protocol) && !empty($protocols[$this->protocol]) ? $protocols[$this->protocol] : Yii::t('app', 'Default');
    }
    
    public function getProbabilityArray()
    {
        $options = array('' => Yii::t('app', 'Choose'));
        for ($i = 5; $i <= 100; ++$i) {
            if ($i % 5 == 0) {
                $options[$i] = $i . ' %';
            }
        }
        return $options;
    }
    
    public function getDefaultParamsArray()
    {
        return array(
            'transport'     => self::TRANSPORT_SMTP,
            'hostname'      => null,
            'username'      => null,
            'password'      => null,
            'port'          => 25,
            'timeout'       => 30,
            'protocol'      => null,
            'probability'   => 100,
            'to'            => null,
            'subject'       => null,
            'body'          => null,
        );
    }
    
    public function getParamsArray(array $params = array())
    {
        $params = array_merge($this->getDefaultParamsArray(), $this->attributes, $params);
        
        // if custom from is supported
        if ($from = $this->getCustomFromHeader()) {
            $params['from'] = $from;    
        }
        
        // inject the return path header if needed.
        if (empty($params['returnPath']) && !empty($this->bounceServer)) {
            $params['returnPath'] = $this->bounceServer->username;
        }

        // inject additional headers
        if (!empty($this->additional_headers) && is_array($this->additional_headers) && count($this->additional_headers) > 0) {
            if (!isset($params['headers']) || !is_array($params['headers'])) {
                $params['headers'] = array();
            }
            
            foreach ($this->additional_headers as $name => $value) {
                $params['headers'][$name] = $value;
            }
        }

        return $params;
    }
    
    public function getFromEmail()
    {
        return $this->username;
    }
    
    final public static function getTypesMapping()
    {
        $mapping = array(
            self::TRANSPORT_SMTP              => 'DeliveryServerSmtp',
            self::TRANSPORT_SMTP_AMAZON       => 'DeliveryServerSmtpAmazon',
            self::TRANSPORT_SENDMAIL          => 'DeliveryServerSendmail',
            self::TRANSPORT_PHP_MAIL          => 'DeliveryServerPhpMail',
            self::TRANSPORT_PICKUP_DIRECTORY  => 'DeliveryServerPickupDirectory',
        );
        
        return $mapping;
    }
    
    public function getStatusesList()
    {
        return array(
            self::STATUS_ACTIVE     => ucfirst(Yii::t('app', self::STATUS_ACTIVE)),
            self::STATUS_INACTIVE   => ucfirst(Yii::t('app', self::STATUS_INACTIVE)),
        );
    }
    
    public function getTypesList()
    {
        $list = array();
        foreach ($this->getTypesMapping() as $key => $value) {
            $list[$key] = ucwords(str_replace(array('-'), ' ', Yii::t('servers', $key)));
        }
        return $list;
    }
    
    public function getCustomFromHeaderOptions()
    {
        return array(
            self::TEXT_NO    => ucfirst(Yii::t('app', self::TEXT_NO)),
            self::TEXT_YES   => ucfirst(Yii::t('app', self::TEXT_YES)),
        );
    }
    
    public function setDeliveryObject($object)
    {
        $this->_deliveryObject = $object;
        return $this;   
    }
    
    public function getDeliveryObject()
    {
        return $this->_deliveryObject;
    }
    
    public function setDeliveryFor($deliveryFor)
    {
        $this->_deliveryFor = $deliveryFor;
        return $this;
    }
    
    public function getDeliveryFor()
    {
        return $this->_deliveryFor;
    }
    
    public function getCustomFromHeader() 
    {
        if ($this->custom_from_header != self::TEXT_YES) {
            return null;
        }
        
        if ($object = $this->getDeliveryObject()) {
            if (is_object($object) && $object instanceof Campaign) {
                return array($object->from_email => $object->from_name);
            }
            if (is_object($object) && $object instanceof Lists && !empty($object->default)) {
                return array($object->default->from_email => $object->default->from_name);
            }
        }

        if (empty($this->custom_from_header_test_email)) {
            $this->custom_from_header_test_email = $this->getFromEmail();
        }
        
        return array($this->custom_from_header_test_email => Yii::app()->options->get('system.common.site_name'));
    }
    
    // cannot be cached since paralel queries happen in console.
    final public static function pickServer($currentServerId = 0, $campaign = null)
    {
        // update server counters before choosing one, for better availability.
        self::model()->updateAll(array('hourly_sent' => 0), 
            'status = :st AND hourly_quota > 0 AND hourly_sent >= hourly_quota 
            AND (
                last_sent < DATE_SUB(NOW(), INTERVAL 1 HOUR) 
                OR 
                (DATE(last_sent) = CURDATE() AND HOUR(last_sent) < HOUR(NOW()))
            )', array(':st' => self::STATUS_ACTIVE)
        );
        
        // force choosen servers for this campaign.
        $campaignServers = array();
        if (!empty($campaign) && $campaign instanceof Campaign) {
            $_campaignServers = CampaignToDeliveryServer::model()->findAllByAttributes(array(
                'campaign_id' => $campaign->campaign_id,
            ));
            foreach ($_campaignServers as $mdl) {
                $server = self::model()->find(array(
                    'select'    => 'server_id',
                    'condition' => 'server_id = :sid AND status = :st',
                    'params'    => array(':sid' => (int)$mdl->server_id , ':st' => self::STATUS_ACTIVE),
                ));
                if (!empty($server)) {
                    $campaignServers[] = $server->server_id;
                }
            }
            unset($_campaignServers);
        }
        
        $criteria = new CDbCriteria();
        $criteria->select = 'server_id, type';
        
        if (!empty($campaignServers)) {
            $criteria->addInCondition('server_id', $campaignServers);
        } else {
            $criteria->compare('status', self::STATUS_ACTIVE);
        }

        $criteria->addCondition('(hourly_quota = 0 OR (hourly_quota > 0 AND hourly_sent < hourly_quota))');
        $criteria->order = 'probability ASC';
        
        $_servers = self::model()->findAll($criteria);
        
        if (empty($_servers)) {
            return false;
        }
        
        $mapping = self::getTypesMapping();
        foreach ($_servers as $index => $srv) {
            if (!isset($mapping[$srv->type])) {
                unset($_servers[$index]);
                continue;
            }
            $_servers[$index] = self::model($mapping[$srv->type])->findByPk($srv->server_id);
        }

        if (empty($_servers)) {
            return false;
        }
        
        $probability = rand(1, 100);
        $probabilities = array();
        foreach ($_servers as $srv) {
            if (!isset($probabilities[$srv->probability])) {
                $probabilities[$srv->probability] = array();
            }
            $probabilities[$srv->probability][] = $srv;
        }
        
        $server = null;
        
        foreach ($probabilities as $serverProbability => $probabilityServers) {
            if ($probability <= $serverProbability) {
                $rand = array_rand(array_keys($probabilityServers), 1);
                $server = $probabilityServers[$rand];
                break;
            }
        }

        if (empty($server)) {
            $rand = array_rand(array_keys($_servers), 1);
            $server = $_servers[$rand];
        }
        
        if (count($_servers) > 1 && $currentServerId > 0 && $server->server_id == $currentServerId) {
            return self::pickServer($server->server_id);
        }
        
        $server->getMailer()->resetTransport()->resetMailer()->resetMessage();
        $server->setDeliveryFor(self::DELIVERY_FOR_SYSTEM);
        
        return $server;
    }
    
    final public function markHourlyUsage()
    {
        if (!$this->isNewRecord && $this->hourly_quota > 0) {
            $this->hourly_sent++;
            $this->last_sent = new CDbExpression('NOW()');
            $this->save(false);
        }
        
        return $this;
    }
    
    public function getIsOverQuota()
    {
        if ($this->isNewRecord) {
            return false;
        }
        return $this->hourly_quota > 0 && $this->hourly_sent >= $this->hourly_quota;
    }
    
    public function getNeverAllowedHeaders()
    {
        $neverAllowed = array(
            'From', 'To', 'Subject', 'Date', 'Return-Path', 'Sender', 
            'Reply-To', 'Message-Id', 'List-Unsubscribe', 
            'Content-Type', 'Content-Transfer-Encoding', 'Content-Length', 'MIME-Version',
            'X-Sender', 'X-Receiver',
        );
        
        $neverAllowed = (array)Yii::app()->hooks->applyFilters('delivery_server_never_allowed_headers', $neverAllowed);
        return $neverAllowed;
    }
    
    public function _validateAdditionalHeaders($attribute, $params)
    {
        $headers = $this->$attribute;
        if (empty($headers) || !is_array($headers)) {
            $headers = array();
        }

        $this->$attribute   = array();
        $_headers           = array();
        
        $notAllowedHeaders  = (array)$this->getNeverAllowedHeaders();
        $notAllowedHeaders  = array_map('strtolower', $notAllowedHeaders);
        
        // try to be a bit restrictive
        $namePattern        = '/([a-z0-9\-\_])*/i';
        $valuePattern       = '/.*/i';
        
        foreach ($headers as $index => $header) {
            
            if (!is_array($header) || !isset($header['name'], $header['value'])) {
                unset($headers[$index]);
                continue;
            }
            
            $name = preg_replace('/:\s/', '', trim($header['name']));
            $value = preg_replace('/:/', '', trim($header['value']));

            if (empty($name) || in_array(strtolower($name), $notAllowedHeaders) || stripos($name, 'MW-') === 0 || !preg_match($namePattern, $name)) {
                unset($headers[$index]);
                continue;
            }
            
            if (empty($value) || !preg_match($valuePattern, $value)) {
                unset($headers[$index]);
                continue;
            }
            
            $_headers[$name] = $value;
        }
        
        $this->$attribute = $_headers;
    }
    
    public function _validateCustomFromHeaderTestEmail($attribute, $params)
    {
        if ($this->hasErrors()) {
            return;
        }
        
        if ($this->custom_from_header == self::TEXT_YES && empty($this->$attribute)) {
            $this->addError($attribute, Yii::t('servers', 'The {attr} cannot be blank!', array(
                '{attr}' => $this->getAttributeLabel($attribute),
            )));
        }
    }
}
