plugins/email/classes/yf_email.class.php

Summary

Maintainability
D
2 days
Test Coverage
<?php

/**
 * High level html emails wrapper.
 */
class yf_email
{
    /** @const */
    const table_tpls = 'emails_templates';
    /** @const */
    const table_history = 'emails_history';
    /** @var string */
    public $ADMIN_EMAIL = '';
    /** @var string */
    public $ADMIN_NAME = '';
    /** @var string */
    public $EMAIL_FROM = '';
    /** @var string */
    public $NAME_FROM = '';
    /** @var string */
    public $SITE_NAME = '';
    /** @var string */
    public $SITE_URL = '';
    /** @var array List of emails addresses to send all copies to */
    public $SEND_ALL_COPY_TO = [
    ];
    /** @var array List of emails addresses to send user-addressing email copies to */
    public $SEND_ADMIN_COPY_TO = [
    ];
    /** @var array List of emails addresses to send admin-addressing email copies to */
    public $SEND_USER_COPY_TO = [
    ];
    /** @var */
    public $SMTP_CONFIG_DEFAULT = [
    ];
    /** @var */
    public $SMTP_CONFIG_ALTERNATE = [
    ];
    public $FORCE_SEND = false;
    public $MAIL_DEBUG = false;
    public $MAIL_DEBUG_ERROR = false;
    /** @var bool Use queues to send emails in asynchronous mode */
    public $ASYNC_SEND = false;
    public $QUEUE_NAME = 'emails_queue';
    public $DEQUEUE_USLEEP = 500000; // 0.5 sec

    /**
     * Catch missing method call.
     * @param mixed $name
     * @param mixed $args
     */
    public function __call($name, $args)
    {
        return main()->extend_call($this, $name, $args);
    }


    public function _init()
    {
        $this->ADMIN_EMAIL = defined('SITE_ADMIN_EMAIL') && strlen(SITE_ADMIN_EMAIL) ? SITE_ADMIN_EMAIL : 'support@' . $_SERVER['HTTP_HOST'];
        $this->ADMIN_NAME = defined('SITE_ADMIN_NAME') && strlen(SITE_ADMIN_NAME) ? SITE_ADMIN_NAME : SITE_ADVERT_TITLE . ' support';
        $this->EMAIL_FROM = SITE_ADMIN_EMAIL;
        $this->NAME_FROM = SITE_ADVERT_NAME;
        $this->SITE_NAME = SITE_ADVERT_NAME;
        $this->SITE_URL = SITE_ADVERT_URL;
    }

    /**
     * Should be wrapped under supervisor and best run from console
     * Example supervisor config entry for it:.
     *
     * [program:yf_emails_queue]
     * command = php index.php --object=admin_daemon --action=emails_dequeue
     * directory = /var/www/default/www/admin/
     * autorestart = true
     * numprocs = 1
     * process_name = %(program_name)s_%(process_num)s
     * user = www-data
     *
     * Example of php settings to ensure daemon long working
     *    if (is_console()) {
     *        error_reporting(E_ALL & ~E_NOTICE);
     *        ini_set('html_errors', 0);
     *        set_time_limit(0);
     *        ini_set('default_socket_timeout', -1);
     *    }
     * @param mixed $extra
     */
    public function dequeue($extra = [])
    {
        queue()->listen($this->QUEUE_NAME, function ($data) use ($extra) {
            $data = json_decode($data, true);
            if ($data) {
                if ($extra['verbose']) {
                    $msg = 'got item from queue; '
                        . ($data['history_id'] ? 'history_id: "' . $data['history_id'] . '"; ' : '')
                        . 'mail_to: "' . $data['to_mail'] . '"; '
                        . 'subj: "' . $data['subj'] . '"; '
                        . 'mail_from: "' . $data['from_mail'] . '"; ';
                    echo date('Y-m-d H:i:s') . ': ' . $msg . PHP_EOL;
                }
                $result = common()->send_mail($data);
                if ( ! $result) {
                    if ($extra['verbose']) {
                        echo date('Y-m-d H:i:s') . ': error: ' . _class('send_mail')->_last_error_message . PHP_EOL;
                        if (strlen($data['html']) > 500) {
                            $data['html'] = substr($data['html'], 0, 500) . PHP_EOL . '...TRUNCATED_FOR_DEBUG...' . PHP_EOL;
                        }
                        var_dump($data);
                    }
                } else {
                    if ($data['history_id']) {
                        db()->update_safe(self::table_history, ['status' => 1], 'id=' . (int) $data['history_id']);
                    }
                    if ($extra['verbose']) {
                        echo date('Y-m-d H:i:s') . ': sent ok' . PHP_EOL;
                    }
                }
            }
        }, ['usleep' => $this->DEQUEUE_USLEEP]);
    }

    /**
     * @param mixed $email
     */
    public function _is_mailru($email)
    {
        list(, $host) = explode('@', $email);
        if (in_array($host, ['mail.ru', 'bk.ru', 'inbox.ru', 'mail.ua', 'list.ru'])) {
            return true;
        }
        return false;
    }

    /**
     * @param mixed $user_id
     * @param mixed $template_name
     * @param mixed $data
     * @param mixed $instant_send
     * @param mixed $require_verified_email
     * @param mixed $is_wall_update
     * @param mixed $template_group
     */
    public function _send_email_to_user($user_id, $template_name, $data = [], $instant_send = true, $require_verified_email = false, $is_wall_update = true, $template_group = '')
    {
        $instant_send = false;
        $user_data = db()->from('user')->whereid($user_id)->get();
        if (empty($user_data)) {
            return false;
        }
        if (empty($data['name'])) {
            $data['name'] = $user_data['name'];
        }
        if (substr($user_data['email'], 0, 6) == 'oauth.') {
            // no support for oauth-based auths
            return false;
        }
        if ($require_verified_email && ($user_data['email'] != $user_data['email_validated'])) {
            return false;
        }
        if ($is_wall_update) {
            return false;
        }
        if ($is_wall_update && $template_group != '') {
            $a = [];
            if ($user_data['email_wall_updates_config'] != '') {
                $data_config = json_decode($user_data['email_wall_updates_config'], true);
                foreach ($data_config as $v) {
                    $a[$v] = $v;
                }
                if (empty($a[$template_group])) {
                    return false;
                }
            } else {
                return false;
            }
        }
        return $this->_send_email_safe($user_data['email'], $user_data['name'], $template_name, $data, $instant_send);
    }

    /**
     * send_email_from_admin.
     * @param mixed $email_to
     * @param mixed $name_to
     * @param mixed $template_name
     * @param mixed $data
     * @param null|mixed $old_param1
     * @param mixed $override
     */
    public function _send_email_safe($email_to, $name_to, $template_name, $data = [], $old_param1 = null, $override = [])
    {
        $is_test = (defined('TEST_MODE') && TEST_MODE)
            && empty($override['force_send'])
            && empty($this->FORCE_SEND);
        if ($is_test) {
            common()->message_error('Test mode enabled. Email real sending is disabled');
            return false;
        }
        if (empty($email_to)) {
            return false;
        }
        if (empty($name_to)) {
            $name_to = $email_to;
        }
        list($subject, $html) = $this->_get_email_text($data, ['tpl_name' => $template_name]);
        if ($override['subject']) {
            $subject = $override['subject'];
        }
        db()->insert_safe(self::table_history, [
            'email_to' => $email_to,
            'name_to' => $name_to,
            'subject' => $subject,
            'text' => $html,
            'date' => $_SERVER['REQUEST_TIME'],
        ]);
        if ( ! $html) {
            if ($this->MAIL_DEBUG_ERROR) {
                trigger_error('Email body is empty', E_USER_WARNING);
            }
            return null;
        }
        $email_id = db()->insert_id();
        $params = [
            'from_mail' => $this->EMAIL_FROM,
            'from_name' => $this->NAME_FROM,
            'to_mail' => $email_to,
            'to_name' => $name_to,
            'subj' => $subject,
            'text' => $this->_text_from_html($html),
            'html' => $this->_css_to_inline_styles($html),
            'smtp' => $this->_is_mailru($email_to) ? $this->SMTP_CONFIG_ALTERNATE : $this->SMTP_CONFIG_DEFAULT,
        ];
        if ($this->ASYNC_SEND) {
            $result = queue()->add($this->QUEUE_NAME, json_encode($params + ['history_id' => $email_id]));
            $this->_send_copies($params);
        } else {
            $result = common()->send_mail((array) $params);
            if ( ! $result) {
                if ($this->MAIL_DEBUG_ERROR) {
                    trigger_error('Email not sent', E_USER_WARNING);
                }
            } else {
                db()->update_safe(self::table_history, ['status' => 1], 'id=' . (int) $email_id);
                $this->_send_copies($params);
            }
        }
        return $result;
    }

    /**
     * @param mixed $extra
     */
    public function _send_to_default_admin($extra = [])
    {
        $params = [
            'from_mail' => $this->EMAIL_FROM,
            'from_name' => $this->NAME_FROM,
            'to_mail' => $this->ADMIN_EMAIL,
            'to_name' => $this->ADMIN_NAME,
            'subj' => $extra['subject'],
            'text' => $this->_text_from_html($extra['html']),
            'html' => $this->_css_to_inline_styles($extra['html']),
            'smtp' => $this->SMTP_CONFIG_DEFAULT,
        ];
        if ($this->ASYNC_SEND) {
            $result = queue()->add($this->QUEUE_NAME, json_encode($params));
        } else {
            $result = common()->send_mail((array) $params);
        }
        $this->_send_copies($params);
        return $result;
    }

    /**
     * @param mixed $replace
     * @param mixed $extra
     */
    public function _get_email_text($replace = [], $extra = [])
    {
        if ($extra['tpl_name']) {
            $lang = $extra['locale'] ?: conf('language');
            $a = db()->from(self::table_tpls)->where('name', $extra['tpl_name'])->where('locale', $lang)->get();
            if ( ! $a) {
                $a = db()->from(self::table_tpls)->where('name', $extra['tpl_name'])->get();
            }
        }
        if ($extra['subject']) {
            $a['subject'] = $extra['subject'];
        }
        $body = $a['text'] ?: $extra['body'];
        if ($a['parent_id']) {
            $parent = db()->from(self::table_tpls)->whereid($a['parent_id'])->where('locale', $a['locale'])->get();
            if ( ! $parent) {
                $parent = db()->from(self::table_tpls)->whereid($a['parent_id'])->get();
            }
            if ($parent) {
                $body = tpl()->parse_string($parent['text'], [
                    'main_content' => $body,
                ]);
            }
        }
        $subject = strip_tags($a['subject']);
        if (empty($subject) && empty($body)) {
            return false;
        }
        $replace = (array) $replace + [
            'site_name' => $this->SITE_NAME,
            'site_url' => $this->SITE_URL,
            'unsubscribe_url' => url_user('/unsubscribe/show/' . $extra['tpl_name'] . '-' . time()),
        ];
        return [
            tpl()->parse_string($subject, $replace),
            tpl()->parse_string($body, $replace),
        ];
    }

    /**
     * Send copies, mostly for debug and more control on what is going on.
     * @param mixed $params
     */
    public function _send_copies($params = [])
    {
        if ( ! $params) {
            return false;
        }
        $copy_to = [];
        foreach ((array) $this->SEND_ALL_COPY_TO as $mail_to) {
            $copy_to[$mail_to] = $mail_to;
        }
        if ($email_to === $this->ADMIN_EMAIL) {
            foreach ((array) $this->SEND_ADMIN_COPY_TO as $mail_to) {
                $copy_to[$mail_to] = $mail_to;
            }
        } else {
            foreach ((array) $this->SEND_USER_COPY_TO as $mail_to) {
                $copy_to[$mail_to] = $mail_to;
            }
        }
        $orig_to_mail = strtolower(trim($params['to_mail']));
        $orig_subj = $params['subj'];
        $params['subj'] = '[AUTO-COPY] ' . $params['subj'];
        foreach ((array) $copy_to as $mail_to) {
            $mail_to = trim($mail_to);
            if ( ! $mail_to || strtolower($mail_to) == $orig_to_mail) {
                continue;
            }
            $params['to_mail'] = $mail_to;
            if ($this->ASYNC_SEND) {
                queue()->add($this->QUEUE_NAME, json_encode($params));
            } else {
                common()->send_mail((array) $params);
            }
        }
        return true;
    }

    /**
     * @param mixed $html
     * @param mixed $extra
     */
    public function _css_to_inline_styles($html = '', $extra = [])
    {
        if ( ! strlen($html) || false === strpos($html, '<')/* || false === strpos($html, 'style=')*/) {
            return $html;
        }
        if (false === strpos($html, '<html') && false === strpos($html, '<body')) {
            $need_raw = true;
            $html = '<!DOCTYPE html><html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"><meta charset="utf-8"></head><body>' . $html . '</body></html>';
        }
        require_php_lib('css_to_inline_styles');
        $cti = new \TijsVerkoyen\CssToInlineStyles\CssToInlineStyles($html);
        $cti->setEncoding('UTF-8');
        $cti->setUseInlineStylesBlock();
        // $cti->setHTML($html);
        // $cti->setCSS($css);
        $result = $cti->convert();

        $result = preg_replace('~<style[^>]*?>.+?</style>~ims', '', $result);
        $result = preg_replace('~<script[^>]*?>.+?</script>~ims', '', $result);

        if ($need_raw) {
            preg_match('|<body.*>(.*)</body>|isU', $result, $matches);
            $result = $matches[1] ?: $result;
        }
        return $result;
    }

    /**
     * @param mixed $html
     */
    public function _text_from_html($html = '')
    {
        if ( ! strlen($html)) {
            return $html;
        }

        $text = trim($html);
        //        $text = services()->phpmailer()->html2text($text, $advanced = true);
        $text = trim($this->strip_html_tags($text));
        $text = str_replace("\t", '  ', $text);
        $text = preg_replace("~[\r\n][ ]+~m", PHP_EOL, $text);
        $text = preg_replace("~[\r\n]{2,}~m", PHP_EOL, $text);
        return $text;
    }

    /**
     * Remove HTML tags, including invisible text such as style and
     * script code, and embedded objects.  Add line breaks around
     * block-level tags to prevent word joining after tag removal.
     *
     * source article: http://nadeausoftware.com/articles/2007/09/php_tip_how_strip_html_tags_web_page
     * @param mixed $text
     */
    public function strip_html_tags($text)
    {
        $r = [
            // Remove invisible content
            '@<head[^>]*?>.*?</head>@siu' => ' ',
            '@<style[^>]*?>.*?</style>@siu' => ' ',
            '@<script[^>]*?.*?</script>@siu' => ' ',
            '@<object[^>]*?.*?</object>@siu' => ' ',
            '@<embed[^>]*?.*?</embed>@siu' => ' ',
            '@<applet[^>]*?.*?</applet>@siu' => ' ',
            '@<noframes[^>]*?.*?</noframes>@siu' => ' ',
            '@<noscript[^>]*?.*?</noscript>@siu' => ' ',
            '@<noembed[^>]*?.*?</noembed>@siu' => ' ',
            // Add line breaks before and after blocks
            '@</?((address)|(blockquote)|(center)|(del))@iu' => "\n\$0",
            '@</?((div)|(h[1-9])|(ins)|(isindex)|(p)|(pre))@iu' => "\n\$0",
            '@</?((dir)|(dl)|(dt)|(dd)|(li)|(menu)|(ol)|(ul))@iu' => "\n\$0",
            '@</?((table)|(th)|(td)|(caption))@iu' => "\n\$0",
            '@</?((form)|(button)|(fieldset)|(legend)|(input))@iu' => "\n\$0",
            '@</?((label)|(select)|(optgroup)|(option)|(textarea))@iu' => "\n\$0",
            '@</?((frameset)|(frame)|(iframe))@iu' => "\n\$0",
        ];
        $text = preg_replace(array_keys($r), array_values($r), $text);
        return strip_tags($text);
    }
}