<?php /*

 Composr
 Copyright (c) ocProducts, 2004-2016

 See text/EN/licence.txt for full licencing information.


 NOTE TO PROGRAMMERS:
   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****

*/

/**
 * @license    http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
 * @copyright  ocProducts Ltd
 * @package    core
 */

/*EXTRA FUNCTIONS: DKIMSignature*/

/**
 * Standard code module initialisation function.
 *
 * @ignore
 */
function init__mail()
{
    require_lang('mail');

    global $SENDING_MAIL, $EMAIL_ATTACHMENTS;
    $SENDING_MAIL = false;
    $EMAIL_ATTACHMENTS = array();
}

/**
 * Replace an HTML img tag such that it is cid'd. Callback for preg_replace_callback.
 *
 * @param  array $matches Matches
 * @return string Replacement
 *
 * @ignore
 */
function _mail_img_rep_callback($matches)
{
    if ((!url_is_local($matches[0])) && (substr($matches[2], 0, strlen(get_custom_base_url())) != get_custom_base_url()) && (substr($matches[2], 0, strlen(get_base_url())) != get_base_url())) {
        return $matches[0];
    }

    global $CID_IMG_ATTACHMENT;
    $cid = uniqid('', true) . '@' . str_replace(' ', '_', get_domain()); // str_replace is in case someone has put in the domain wrong
    $CID_IMG_ATTACHMENT[$cid] = $matches[2];
    return '<img ' . $matches[1] . 'src="cid:' . $cid . '"';
}

/**
 * Replace CSS image references such that it is cid'd. Callback for preg_replace_callback.
 *
 * @param  array $matches Matches
 * @return string Replacement
 *
 * @ignore
 */
function _mail_css_rep_callback($matches)
{
    $filename = basename($matches[1]);
    if (($filename != 'block_background.png') && ($filename != 'gradient.png') && ($filename != 'keyboard.png') && ($filename != 'email_link.png') && ($filename != 'external_link.png')) {
        /*global $CID_IMG_ATTACHMENT;   CSS CIDs do not work with Thunderbird, but data does
        $cid = uniqid('', true) . '@' . get_domain();
        $CID_IMG_ATTACHMENT[$cid] = $matches[1];
        return 'url(\'cid:' . $cid . '\')';*/

        $total_filesize = 0;
        $test = _get_image_for_cid($matches[1], $GLOBALS['FORUM_DRIVER']->get_guest_id(), $total_filesize);
        if (is_null($test) || $total_filesize > 1024 * 50/*Let's be reasonable here*/) {
            return 'none';
        }
        list($mime_type, $filename, $file_contents) = $test;

        $value = 'data:' . get_mime_type(get_file_extension($filename), false) . ';base64,' . base64_encode($file_contents);
        return 'url(\'data:' . $value . '\')';
    }
    return 'none';
}

/**
 * Indent text lines. Callback for preg_replace_callback.
 *
 * @param  array $matches Matches
 * @return string Replacement
 *
 * @ignore
 */
function _indent_callback($matches)
{
    return '      ' . str_replace("\n", "\n" . '      ', $matches[1]);
}

/**
 * Make titles readable. Callback for preg_replace_callback.
 *
 * @param  array $matches Matches
 * @return string Replacement
 *
 * @ignore
 */
function _title_callback($matches)
{
    $symbol = '-';
    if (strpos($matches[2], '1') !== false || $matches[2] == '') {
        $symbol = '=';
    }

    $ret = $matches[1];
    if (substr_count($matches[1], "\n") == 0) {
        $ret .= "\n";
    }
    $ret .= $matches[3] . "\n" . str_repeat($symbol, strlen($matches[3]));
    return $ret;
}

/**
 * Make boxes readable. Callback for preg_replace_callback.
 *
 * @param  array $matches Matches
 * @return string Replacement
 *
 * @ignore
 */
function _box_callback($matches)
{
    return $matches[1] . "\n" . str_repeat('-', strlen($matches[1])) . "\n" . $matches[2];
}

/**
 * Make page tags into url tags. Callback for preg_replace_callback.
 *
 * @param  array $matches Matches
 * @return string Replacement
 *
 * @ignore
 */
function _page_callback($matches)
{
    list($zone, $attributes, $hash) = page_link_decode($matches[1]);
    $url = static_evaluate_tempcode(build_url($attributes, $zone, null, false, false, true, $hash));
    return '[url="' . addslashes($url) . '"]' . $matches[2] . '[/url]';
}

/**
 * Extract some random. Callback for preg_replace_callback.
 *
 * @param  array $matches Matches
 * @return string Replacement
 *
 * @ignore
 */
function _random_callback($matches)
{
    $parts = array();
    $num_parts = preg_match_all('# [^=]*="([^"]*)"#', $matches[1], $parts);
    return $parts[1][mt_rand(0, $num_parts - 1)];
}

/**
 * Extract all shocker/jumping text. Callback for preg_replace_callback.
 *
 * @param  array $matches Matches
 * @return string Replacement
 *
 * @ignore
 */
function _shocker_callback($matches)
{
    $parts = array();
    $num_parts = preg_match_all('# [^=]*="([^"]*)"#', $matches[1], $parts);
    $out = '';
    for ($i = 0; $i < $num_parts; $i++) {
        if ($out != '') {
            $out .= ', ';
        }
        $out .= $parts[1][$i];
    }
    return $out;
}

/**
 * Pass tag through Comcode parser. Callback for preg_replace_callback.
 *
 * @param  array $matches Matches
 * @return string Replacement
 *
 * @ignore
 */
function _comcode_callback($matches)
{
    return str_replace('xxx', $matches[2], static_evaluate_tempcode(comcode_to_tempcode($matches[1] . 'xxx' . $matches[3])));
}

/**
 * Pass tag through semihtml_to_comcode. Callback for preg_replace_callback.
 *
 * @param  array $matches Matches
 * @return string Replacement
 *
 * @ignore
 */
function _semihtml_to_comcode_callback($matches)
{
    return semihtml_to_comcode($matches[1], true, true);
}

/**
 * Make some Comcode more readable.
 *
 * @param  string $message_plain Comcode text to change
 * @param  boolean $for_extract Whether this is for generating an extract that does not need to be fully comprehended (i.e. favour brevity)
 * @param  ?array $tags_to_preserve List of tags to preserve (null: none)
 * @return string Clean text
 */
function comcode_to_clean_text($message_plain, $for_extract = false, $tags_to_preserve = null)
{
    if (is_null($tags_to_preserve)) {
        $tags_to_preserve = array();
    }

    //$message_plain = str_replace("\n", '', $message_plain);

    // Very simple case
    if ((strpos($message_plain, '[') === false) && (strpos($message_plain, '{') === false)) {
        return trim($message_plain);
    }

    // Strip resource loader
    if (stripos($message_plain, '[require') !== false) {
        $message_plain = preg_replace('#\[require_css(\s[^"\[\]]*)?\][^\[\]]*\[/require_css\]#', '', $message_plain);
        $message_plain = preg_replace('#\[require_javascript(\s[^"\[\]]*)?\][^\[\]]*\[/require_javascript\]#', '', $message_plain);
    }

    // If it is just HTML encapsulated in Comcode, force our best HTML to text conversion first
    if (stripos($message_plain, 'html]') !== false) {
        $match = array();
        if ((substr($message_plain, 0, 10) == '[semihtml]') && (substr(trim($message_plain), -11) == '[/semihtml]')) {
            require_code('comcode_from_html');
            $message_plain = comcode_preg_replace('semihtml', '#^\[semihtml\](.*)\[\/semihtml\]$#si', array('_semihtml_to_comcode_callback'), $message_plain);
        }
        if ((substr($message_plain, 0, 6) == '[html]') && (substr(trim($message_plain), -7) == '[/html]')) {
            require_code('comcode_from_html');
            $message_plain = comcode_preg_replace('html', '#^\[html\](.*)\[\/html\]$#si', array('_semihtml_to_comcode_callback'), $message_plain);
        }
    }

    // Now is a very simple case (after we converted HTML to Comcode)
    if ((strpos($message_plain, '[') === false) && (strpos($message_plain, '{') === false)) {
        return trim($message_plain);
    }

    require_code('tempcode_compiler');
    if ((strpos($message_plain, '[code') === false) && (strpos($message_plain, '[no_parse') === false) && (strpos($message_plain, '[tt') === false)) {
        // Change username links to plain username namings
        if (stripos($message_plain, '{{') !== false) {
            $message_plain = preg_replace('#\{\{([^|\}\{]*)\}\}#', '\1', $message_plain);
        }

        $message_plain = str_replace('{$SITE_NAME}', get_site_name(), $message_plain);
        $message_plain = str_replace('{$SITE_NAME*}', get_site_name(), $message_plain);

        if (stripos($message_plain, '{') !== false) {
            // Remove directives etc
            do {
                $before = $message_plain;
                $message_plain = preg_replace('#\{([^|\}\{]*)\}#', '', $message_plain);
            } while ($message_plain != $before);
        }

        if (strpos($message_plain, '{') !== false) {
            $message_plain = static_evaluate_tempcode(template_to_tempcode($message_plain, 0, false, '', null, null, true));
        }
    }

    $match = array();

    if (stripos($message_plain, 'html]') !== false) {
        if (!in_array('semihtml', $tags_to_preserve)) {
            require_code('comcode_from_html');
            $message_plain = comcode_preg_replace('semihtml', '#^\[semihtml\](.*)\[\/semihtml\]$#si', array('_semihtml_to_comcode_callback'), $message_plain);
        }
        if (!in_array('html', $tags_to_preserve)) {
            require_code('comcode_from_html');
            $message_plain = comcode_preg_replace('html', '#^\[html\](.*)\[\/html\]$#si', array('_semihtml_to_comcode_callback'), $message_plain);
        }
    }

    // Convert certain tags to 'url' tags. These may then be converted to text entirely, depending on if 'url' is being preserved
    if (stripos($message_plain, '[page') !== false) {
        if (!in_array('page', $tags_to_preserve)) {
            $message_plain = preg_replace_callback("#\[page=\"([^\"]*)\"[^\[\]]*\](.*)\[/page\]#Usi", '_page_callback', $message_plain);
        }
    }
    if (stripos($message_plain, '[flash') !== false) {
        if (!in_array('flash', $tags_to_preserve)) {
            $message_plain = preg_replace("#\[flash=\"([^\"]*)\"[^\[\]]*\](.*)\[/flash\]#Usi", '[url="\2"]\1[/url]', $message_plain);
            $message_plain = preg_replace("#\[flash[^\[\]]*\](.*)\[/flash\]#Usi", '[url="\1"]' . do_lang('VIEW') . '[/url]', $message_plain);
        }
    }
    if (stripos($message_plain, '[attachment') !== false) {
        if (!in_array('attachment', $tags_to_preserve)) {
            $message_plain = preg_replace("#\[attachment[^\[\]]* description=\"([^\"]*)\"[^\[\]]*\](\d*)\[/attachment[^\[\]]*\]#Usi", '[url="' . find_script('attachment') . '?id=\2"]\1[/url]', $message_plain);
            $message_plain = preg_replace("#\[attachment[^\[\]]*\](\d*)\[/attachment[^\[\]]*\]#Usi", '[url="' . find_script('attachment') . '?id=\1"]' . do_lang('VIEW') . '[/url]', $message_plain);
        }
    }
    if (stripos($message_plain, '[media') !== false) {
        if (!in_array('media', $tags_to_preserve)) {
            $message_plain = preg_replace("#\[media=\"([^\"]*)\"[^\[\]]*\](.*)\[/media\]#Usi", '[url="\2"]\1[/url]', $message_plain);
            $message_plain = preg_replace("#\[media[^\[\]]*\](.*)\[/media\]#Usi", '[url="\1"]' . do_lang('VIEW') . '[/url]', $message_plain);
        }
    }
    $message_plain = str_replace('{$BASE_URL*}', escape_html(get_base_url()), $message_plain);
    $message_plain = str_replace('{$BASE_URL}', get_base_url(), $message_plain);
    if (!in_array('thumb', $tags_to_preserve)) {
        $message_plain = str_replace('[/thumb', '[/img', str_replace('[thumb', '[img', $message_plain));
    }
    if (stripos($message_plain, '[img') !== false) {
        if (!in_array('img', $tags_to_preserve)) {
            $message_plain = preg_replace("#\[img[^\[\]]*\]\s*d\s*a\s*t\s*a\s*:[^\[\]]*\[/img\]#Usi", '', $message_plain);

            $message_plain = preg_replace("#\[img( param)?=\"([^\"]*)\"[^\[\]]*\](.*)\[/img\]#Usi", '[url="\3"]\2[/url] ', $message_plain);
            $message_plain = preg_replace("#\[img[^\[\]]*\](.*)\[/img\]#Usi", '[url="\1"]' . do_lang('VIEW') . '[/url] ', $message_plain);
        }
    }
    if (stripos($message_plain, '[email') !== false) {
        if (!in_array('email', $tags_to_preserve)) {
            $message_plain = preg_replace("#\[email[^\[\]]*\](.*)\[/email\]#Usi", '[url="mailto:\1"]\1[/url]', $message_plain);
        }
    }

    if (stripos($message_plain, '[url') !== false) {
        if (!in_array('url', $tags_to_preserve)) {
            $message_plain = preg_replace("#\[url( param)?=\"([^\"]*)\"[^\[\]]*\]\\2\[/url\]#", '\2', $message_plain);
            $message_plain = preg_replace("#\(\[url( param)?=\"(https?://[^\"]*)\"([^\]]*)\]([^\[\]]*)\[/url\]\)#", '\2', $message_plain);
            $message_plain = preg_replace("#\[url( param)?=\"(https?://[^\"]*)\"([^\]]*)\]([^\[\]]*)\[/url\]#", $for_extract ? '\4' : '\4 (\2)', $message_plain);
            $message_plain = preg_replace("#\[url( param)?=\"([^\"]*)\"[^\[\]]*\]([^\[\]]*)\[/url\]#", '\2 (\3)', $message_plain);
            $message_plain = preg_replace("#\[url( param)?=\"([^\"]*)\"([^\]]*)\]([^\[\]]*)\[/url\]#", $for_extract ? '\2' : '\2 (\4)', $message_plain);
        }
    }

    if (!in_array('html', $tags_to_preserve)) {
        $message_plain = strip_html($message_plain);
    }

    if (stripos($message_plain, '[random') !== false) {
        if (!in_array('random', $tags_to_preserve)) {
            $message_plain = preg_replace_callback('#\[random(( [^=]*="([^"]*)")*)\].*\[/random\]#Usi', '_random_callback', $message_plain);
        }
    }

    if (stripos($message_plain, '[shocker') !== false) {
        if (!in_array('shocker', $tags_to_preserve)) {
            $message_plain = preg_replace_callback('#\[shocker(( [^=]*="([^"]*)")*)\].*\[/shocker\]#Usi', '_shocker_callback', $message_plain);
        }
    }

    if (stripos($message_plain, '[jumping') !== false) {
        if (!in_array('jumping', $tags_to_preserve)) {
            $message_plain = preg_replace_callback('#\[jumping(( [^=]*="([^"]*)")*)\].*\[/jumping\]#Usi', '_shocker_callback', $message_plain);
        }
    }

    if (stripos($message_plain, '[abbr') !== false) {
        if (!in_array('abbr', $tags_to_preserve)) {
            $message_plain = preg_replace('#\[abbr="([^"]*)"[^\]]*\](.*)\[/abbr\]#Usi', '\2 (\1)', $message_plain);
        }
    }
    if (stripos($message_plain, '[acronym') !== false) {
        if (!in_array('acronym', $tags_to_preserve)) {
            $message_plain = preg_replace('#\[acronym="([^"]*)"[^\]]*\](.*)\[/acronym\]#Usi', '\2 (\1)', $message_plain);
        }
    }
    if (stripos($message_plain, '[tooltip') !== false) {
        if (!in_array('tooltip', $tags_to_preserve)) {
            $message_plain = preg_replace('#\[tooltip="([^"]*)"[^\]]*\](.*)\[/tooltip\]#Usi', '\2 (\1)', $message_plain);
        }
    }

    if (addon_installed('ecommerce')) {
        if (stripos($message_plain, '[currency') !== false) {
            if (!in_array('currency', $tags_to_preserve)) {
                $message_plain = preg_replace('#\[currency\](.*)\[/currency\]#Usi', get_option('currency') . ' \1', $message_plain);
                $message_plain = preg_replace('#\[currency="([^"]*)"[^\]]*\](.*)\[/currency\]#Usi', '\1 \2', $message_plain);
            }
        }
    }

    if (stripos($message_plain, '[hide') !== false) {
        if (!in_array('hide', $tags_to_preserve)) {
            $message_plain = preg_replace('#\[hide\](.*)\[/hide\]#Usi', do_lang('comcode:SPOILER_WARNING') . ':' . "\n" . '\1', $message_plain);
            $message_plain = preg_replace('#\[hide="([^"]*)"[^\]]*\](.*)\[/hide\]#Usi', '\1:' . "\n" . '\2', $message_plain);
        }
    }

    if (stripos($message_plain, '[indent') !== false) {
        if (!in_array('indent', $tags_to_preserve)) {
            $message_plain = preg_replace_callback('#\[indent[^\]]*\](.*)\[/indent\]#Usi', '_indent_callback', $message_plain);
        }
    }

    if (stripos($message_plain, '[title') !== false) {
        if (!in_array('title', $tags_to_preserve)) {
            $message_plain = preg_replace_callback('#(\s*)\[title([^\]]*)\](.*)\[/title\]#Usi', '_title_callback', $message_plain);
        }
    }

    if (stripos($message_plain, '[box') !== false) {
        if (!in_array('box', $tags_to_preserve)) {
            $message_plain = preg_replace_callback('#\[box="([^"]*)"[^\]]*\](.*)\[/box\]#Usi', '_box_callback', $message_plain);
        }
    }

    $tags_to_strip_entirely = array_diff(array(
        'snapback',
        'post',
        'thread',
        'topic',
        'include',
        'staff_note',
        'contents',
        'block',
        'section_controller',
        'big_tab_controller',
        'concepts',
        'menu',

        // These are handled earlier for normal attachments, this strips what may be left
        'attachment',
        'attachment_safe',
    ), $tags_to_preserve);
    foreach ($tags_to_strip_entirely as $s) {
        if (stripos($message_plain, '[' . $s) !== false) {
            $message_plain = preg_replace('#\[' . $s . '[^\]]*\].*\[/' . $s . '\]#Usi', '', $message_plain);
        }
    }

    if (stripos($message_plain, '[surround') !== false) {
        if (!in_array('surround', $tags_to_preserve)) {
            $message_plain = preg_replace('#\[surround="[\w ]+"\](.*)\[/surround\]#Usi', '$1', $message_plain);
        }
    }

    if (stripos($message_plain, '[if_in_group') !== false) {
        if (!in_array('if_in_group', $tags_to_preserve)) {
            $message_plain = preg_replace_callback('#(\[if_in_group="[^"]*"\])(.*)(\[/if_in_group\])#Usi', '_comcode_callback', $message_plain);
        }
    }

    $tags_to_strip_just_tags = array_diff(array(
        'surround',
        'ticker',
        'right',
        'center',
        'left',
        'align',
        'list',
        'html',
        'semihtml',
        'concept',
        'size',
        'color',
        'font',
        'tt',
        'address',
        'sup',
        'sub',
        'box',
        'samp',
        'q',
        'var',
        'overlay',
        'section',
        'big_tab',
        'tabs',
        'tab',
        'carousel',
        'pulse',
        'php',
        'codebox',
        'sql',
        'no_parse',
        'code',

        // Intentional metadata in these actually, so just leave them in
        //'reference',
        //'cite',
        //'quote',
        //'ins',
        //'s',
        //'del',
        //'dfn',
    ), $tags_to_preserve);
    foreach ($tags_to_strip_just_tags as $s) {
        if (stripos($message_plain, '[' . $s) !== false) {
            $message_plain = preg_replace('#\[' . $s . '[^\]]*\](.*)\[/' . $s . '\]#U', '\1', $message_plain);
        }
    }

    $reps = array();
    if (!in_array('list', $tags_to_preserve)) {
        $reps += array(
            '[/*]' => '',
            '[*]' => ' - ',
            "[list]\n" => '',
            "\n[/list]" => '',
            '[list]' => '',
            '[/list]' => '',
        );
    }
    if (!in_array('b', $tags_to_preserve)) {
        $reps += array(
            '[b]' => '**',
            '[/b]' => '**',
        );
    }
    if (!in_array('i', $tags_to_preserve)) {
        $reps += array(
            '[i]' => '*',
            '[/i]' => '*',
        );
    }
    if (!in_array('u', $tags_to_preserve)) {
        $reps += array(
            '[u]' => '__',
            '[/u]' => '__',
        );
    }
    if (!in_array('highlight', $tags_to_preserve)) {
        $reps += array(
            '[highlight]' => '***',
            '[/highlight]' => '***',
        );
    }
    $message_plain = str_replace(array_keys($reps), array_values($reps), $message_plain);
    if (!in_array('list', $tags_to_preserve)) {
        $message_plain = preg_replace('#\[list[^\[\]]*\]#', '', $message_plain);
    }

    if (stripos($message_plain, '{$') !== false) {
        $message_plain = preg_replace('#\{\$,[^\{\}]*\}#', '', $message_plain);
    }

    if (stripos($message_plain, "\n\n") !== false) {
        $message_plain = preg_replace('#\n\n+#', "\n\n", $message_plain);
    }

    return trim($message_plain);
}

/*
What headers to use can easily confuse. Here is a guide...

return-path    (aka envelope-from aka reverse-path)      SET BY SMTP SERVER, NOT HEADER
from           Who actually sent, SMTP-wise (should be accurate, as may be checked by SPF)
reply-to       Who replies go to
sender         Not needed, not often used
x-sender       As per sender, but might not be an email address

Full details:
http://people.dsv.su.se/~jpalme/ietf/ietf-mail-attributes.html
*/

/**
 * Attempt to send an e-mail to the specified recipient. The mail will be forwarding to the CC address specified in the options (if there is one, and if not specified not to cc).
 * The mail will be sent in dual HTML/text format, where the text is the unconverted Comcode source: if a member does not read HTML mail, they may wish to fallback to reading that.
 *
 * @param  string $subject_line The subject of the mail in plain text
 * @param  LONG_TEXT $message_raw The message, as Comcode
 * @param  ?array $to_email The destination (recipient) e-mail addresses [array of strings] (null: site staff address)
 * @param  ?mixed $to_name The recipient name. Array or string. (null: site name)
 * @param  EMAIL $from_email The from address (blank: site staff address)
 * @param  string $from_name The from name (blank: site name)
 * @param  integer $priority The message priority (1=urgent, 3=normal, 5=low)
 * @range  1 5
 * @param  ?array $attachments A list of attachments (each attachment being a map, absolute path=>filename) (null: none)
 * @param  boolean $no_cc Whether to NOT CC to the CC address
 * @param  ?MEMBER $as Convert Comcode->tempcode as this member (a privilege thing: we don't want people being able to use admin rights by default!) (null: guest)
 * @param  boolean $as_admin Replace above with arbitrary admin
 * @param  boolean $in_html HTML-only
 * @param  boolean $coming_out_of_queue Whether to bypass queueing, because this code is running as a part of the queue management tools
 * @param  ID_TEXT $mail_template The template used to show the email
 * @param  ?boolean $bypass_queue Whether to bypass queueing (null: auto-decide)
 * @param  ?array $extra_cc_addresses Extra CC addresses to use (null: none)
 * @param  ?array $extra_bcc_addresses Extra BCC addresses to use (null: none)
 * @param  ?TIME $require_recipient_valid_since Implement the Require-Recipient-Valid-Since header (null: no restriction)
 * @return boolean Success status
 */
function mail_wrap($subject_line, $message_raw, $to_email = null, $to_name = null, $from_email = '', $from_name = '', $priority = 3, $attachments = null, $no_cc = false, $as = null, $as_admin = false, $in_html = false, $coming_out_of_queue = false, $mail_template = 'MAIL', $bypass_queue = null, $extra_cc_addresses = null, $extra_bcc_addresses = null, $require_recipient_valid_since = null)
{
    if (running_script('stress_test_loader')) {
        require_code('files2');
        clean_temporary_mail_attachments($attachments);

        return false;
    }

    global $SITE_INFO;

    if (!empty($SITE_INFO['redirect_email_output'])) {
        $to_email = array($SITE_INFO['redirect_email_output']);
    }

    if (@$SITE_INFO['no_email_output'] === '1') {
        require_code('files2');
        clean_temporary_mail_attachments($attachments);

        return false;
    }

    if (method_exists($GLOBALS['FORUM_DRIVER'], 'get_member_from_email_address')) {
        if ($priority != 1 && $to_email !== null) {
            foreach ($to_email as $key => $email) {
                if ($GLOBALS['FORUM_DRIVER']->is_banned($GLOBALS['FORUM_DRIVER']->get_member_from_email_address($email))) {
                    unset($to_email[$key]);
                }

                if (count($to_email) == 0) {
                    require_code('files2');
                    clean_temporary_mail_attachments($attachments);

                    return true;
                }
            }
        }
    }

    if (is_null($bypass_queue)) {
        $bypass_queue = (($priority < 3) || (strpos(serialize($attachments), 'tmpfile') !== false));
    }

    global $EMAIL_ATTACHMENTS;
    $EMAIL_ATTACHMENTS = array();

    require_code('site');
    require_code('mime_types');
    require_code('type_sanitisation');

    if (is_null($as)) {
        $as = $GLOBALS['FORUM_DRIVER']->get_guest_id();
    }

    if (empty($attachments)) {
        $attachments = null;
    }
    if (is_null($extra_cc_addresses)) {
        $extra_cc_addresses = array();
    }
    if (is_null($extra_bcc_addresses)) {
        $extra_bcc_addresses = array();
    }

    if (!$coming_out_of_queue) {
        if ((mt_rand(0, 100) == 1) && (!$GLOBALS['SITE_DB']->table_is_locked('logged_mail_messages'))) {
            $GLOBALS['SITE_DB']->query('DELETE FROM ' . get_table_prefix() . 'logged_mail_messages WHERE m_date_and_time<' . strval(time() - 60 * 60 * 24 * intval(get_option('email_log_days'))) . ' AND m_queued=0', 500/*to reduce lock times*/); // Log it all for 2 weeks, then delete
        }

        $through_queue = (!$bypass_queue) && (((cron_installed()) && (get_option('mail_queue') === '1'))) || (get_option('mail_queue_debug') === '1');
        if ((!empty($attachments)) && (get_option('mail_queue_debug') === '0')) {
            foreach (array_keys($attachments) as $path) {
                if ((substr($path, 0, strlen(get_custom_file_base() . '/')) != get_custom_file_base() . '/') && (substr($path, 0, strlen(get_file_base() . '/')) != get_file_base() . '/')) {
                    $through_queue = false;
                }
            }
        }

        if (!$in_html) {
            inject_web_resources_context_to_comcode($message_raw);
        }

        $GLOBALS['SITE_DB']->query_insert('logged_mail_messages', array(
            'm_subject' => cms_mb_substr($subject_line, 0, 255),
            'm_message' => $message_raw,
            'm_to_email' => serialize($to_email),
            'm_to_name' => serialize($to_name),
            'm_extra_cc_addresses' => serialize($extra_cc_addresses),
            'm_extra_bcc_addresses' => serialize($extra_bcc_addresses),
            'm_join_time' => $require_recipient_valid_since,
            'm_from_email' => $from_email,
            'm_from_name' => $from_name,
            'm_priority' => $priority,
            'm_attachments' => serialize($attachments),
            'm_no_cc' => $no_cc ? 1 : 0,
            'm_as' => $as,
            'm_as_admin' => $as_admin ? 1 : 0,
            'm_in_html' => $in_html ? 1 : 0,
            'm_date_and_time' => time(),
            'm_member_id' => get_member(),
            'm_url' => get_self_url(true),
            'm_queued' => $through_queue ? 1 : 0,
            'm_template' => $mail_template,
        ), false, !$through_queue); // No errors if we don't NEED this to work

        if ($through_queue) {
            return true;
        }
    }

    global $SENDING_MAIL;
    if ($SENDING_MAIL) {
        require_code('files2');
        clean_temporary_mail_attachments($attachments);

        return false;
    }
    $SENDING_MAIL = true;

    // To and from, and language
    $staff_address = get_option('staff_address');
    if (!is_email_address($staff_address)) { // Required for security
        $staff_address = '';
    }
    if (is_null($to_email)) {
        $to_email = array($staff_address);
    }
    $to_email_new = array();
    foreach ($to_email as $test_address) {
        if ($test_address != '') {
            $to_email_new[] = $test_address;
        }
    }
    $to_email = $to_email_new;
    if ($to_email == array()) {
        $SENDING_MAIL = false;

        require_code('files2');
        clean_temporary_mail_attachments($attachments);

        return true;
    }

    $member_id = null;
    if ((method_exists($GLOBALS['FORUM_DRIVER'], 'get_member_from_email_address')) && (count($to_email) == 1)) {
        $member_id = $GLOBALS['FORUM_DRIVER']->get_member_from_email_address($to_email[0]);
    }
    if (($member_id === null) && (count($to_email) > 1)) {
        $member_id = $GLOBALS['FORUM_DRIVER']->get_guest_id();
    }

    if ($member_id === null) {
        $lang = user_lang();
    } else {
        $lang = get_lang($member_id);
    }

    if (is_null($to_name)) {
        if ($to_email[0] == $staff_address) {
            $to_name = get_site_name();
        } else {
            $to_name = '';
        }
    }
    if ($from_email == '') {
        $from_email = get_option('staff_address');
    }
    if (!is_email_address($from_email)) { // Required for security
        $from_email = '';
    }
    if ($from_name == '') {
        $from_name = get_site_name();
    }
    $from_email = str_replace(array("\r", "\n"), array('', ''), $from_email);
    $from_name = str_replace(array("\r", "\n"), array('', ''), $from_name);

    if (!$coming_out_of_queue) {
        cms_profile_start_for('mail_wrap');
    }

    $_theme = get_value('mail_theme');
    if ($_theme !== null) {
        $theme = $_theme;
    } else {
        if ($member_id === null) {
            $member_id = $GLOBALS['FORUM_DRIVER']->get_guest_id(); // As there's no browser-settings involved (as for user_lang()) we just want to set for what a guest would see
        }

        $theme = method_exists($GLOBALS['FORUM_DRIVER'], 'get_theme') ? $GLOBALS['FORUM_DRIVER']->get_theme(null, $member_id) : 'default';
        if ($theme == 'default' || $theme == 'admin') { // Sucks, probably due to sending from Admin Zone...
            $theme = $GLOBALS['FORUM_DRIVER']->get_theme('', $member_id); // ... So get theme of welcome zone
        }
    }

    // Line termination is fiddly. It is safer to rely on sendmail supporting \n than undetectable-qmail not supporting the correct \r\n
    /*
    $sendmail_path = ini_get('sendmail_path');
    if (strpos($sendmail_path, 'qmail') !== false) {
        $line_term = "\n";
    } else {
        $line_term = "\r\n";
    }
    */
    if ((strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') || (get_option('smtp_sockets_use') == '1')) {
        $line_term = "\r\n";
    /*} elseif (strtoupper(substr(PHP_OS, 0, 3)) == 'MAC')
    {
        $line_term = "\r";*/
    } else {
        $line_term = "\n";
    }

    // DKIM prep
    $dkim_private_key = get_option('dkim_private_key');
    $signed_headers = ''; // Will be filled later, potentially
    if (trim($dkim_private_key) != '') {
        require_code('mail_dkim');
    }

    // We use the boundary to separate message parts
    $_boundary = uniqid('Composr', true);
    $boundary = $_boundary . '_1';
    $boundary2 = $_boundary . '_2';
    $boundary3 = $_boundary . '_3';

    // Our subject
    $subject = do_template('MAIL_SUBJECT', array('_GUID' => '44a57c666bb00f96723256e26aade9e5', 'SUBJECT_LINE' => $subject_line), $lang, false, null, '.txt', 'text', $theme);
    $tightened_subject = $subject->evaluate($lang); // Note that this is slightly against spec, because characters aren't forced to be printable us-ascii. But it's better we allow this (which works in practice) than risk incompatibility via charset-base64 encoding.
    $tightened_subject = str_replace(array("\r", "\n"), array('', ''), $tightened_subject);

    $regexp = '#^[\x' . dechex(32) . '-\x' . dechex(126) . ']*$#';
    if (preg_match($regexp, $tightened_subject) == 0) {
        $tightened_subject = '=?' . do_lang('charset', null, null, null, $lang) . '?B?' . base64_encode($tightened_subject) . "?=";
    }
    if (preg_match($regexp, $from_name) == 0) {
        $from_name = '=?' . do_lang('charset', null, null, null, $lang) . '?B?' . base64_encode($from_name) . "?=";
    }
    if (is_array($to_name)) {
        foreach ($to_name as $i => $_to_name) {
            if (preg_match($regexp, $_to_name) == 0) {
                $to_name[$i] = '=?' . do_lang('charset', null, null, null, $lang) . '?B?' . base64_encode($_to_name) . "?=";
            }
        }
    } else {
        if (preg_match($regexp, $to_name) == 0) {
            $to_name = '=?' . do_lang('charset', null, null, null, $lang) . '?B?' . base64_encode($to_name) . "?=";
        }
    }

    $simplify_when_can = true; // Used for testing. Not actually needed

    global $CID_IMG_ATTACHMENT;
    $CID_IMG_ATTACHMENT = array();

    // Evaluate message. Needs doing early so we know if we have any headers
    if (!$in_html) {
        $cache_sig = serialize(array(
            $lang,
            $mail_template,
            $subject,
            $theme,
            crc32($message_raw),
        ));

        static $html_content_cache = array();
        if (isset($html_content_cache[$cache_sig])) {
            list($html_evaluated, $message_plain, $EMAIL_ATTACHMENTS) = $html_content_cache[$cache_sig];
        } else {
            require_code('media_renderer');
            push_media_mode(peek_media_mode() | MEDIA_LOWFI);

            $GLOBALS['NO_LINK_TITLES'] = true;
            global $LAX_COMCODE;
            $temp = $LAX_COMCODE;
            $LAX_COMCODE = true;
            $html_content = comcode_to_tempcode($message_raw, $as, $as_admin);
            $LAX_COMCODE = $temp;
            $GLOBALS['NO_LINK_TITLES'] = false;

            $_html_content = $html_content->evaluate($lang);
            $_html_content = preg_replace('#(keep|for)_session=\w*#', 'filtered=1', $_html_content);
            if (strpos($_html_content, '<html') !== false) {
                $message_html = make_string_tempcode($_html_content);
            } else {
                $message_html = do_template($mail_template, array(
                    '_GUID' => 'b23069c20202aa59b7450ebf8d49cde1',
                    'CSS' => '{CSS}',
                    'LOGOURL' => get_logo_url(''),
                    'LANG' => $lang,
                    'TITLE' => $subject,
                    'CONTENT' => $_html_content,
                ), $lang, false, 'MAIL', '.tpl', 'templates', $theme);
            }
            require_css('email');
            $css = css_tempcode(true, false, $message_html->evaluate($lang), $theme);
            $_css = $css->evaluate($lang);
            if (!GOOGLE_APPENGINE) {
                if (get_option('allow_ext_images') != '1') {
                    $_css = preg_replace_callback('#url\(["\']?(https?://[^"]*)["\']?\)#U', '_mail_css_rep_callback', $_css);
                }
            }
            $html_evaluated = $message_html->evaluate($lang);
            $html_evaluated = str_replace('{CSS}', $_css, $html_evaluated);

            // Cleanup the Comcode a bit
            $message_plain = comcode_to_clean_text($message_raw);
            $message_plain = static_evaluate_tempcode(do_template($mail_template, array(
                '_GUID' => 'a23069c20202aa59b7450ebf8d49cde1',
                'CSS' => '{CSS}',
                'LOGOURL' => get_logo_url(''),
                'LANG' => $lang,
                'TITLE' => $subject,
                'CONTENT' => $message_plain,
            ), $lang, false, 'MAIL', '.txt', 'text', $theme));

            $html_content_cache[$cache_sig] = array($html_evaluated, $message_plain, $EMAIL_ATTACHMENTS);

            pop_media_mode();
        }
        $attachments = array_merge(is_null($attachments) ? array() : $attachments, $EMAIL_ATTACHMENTS);
    } else {
        $html_evaluated = $message_raw;
    }

    // Headers
    $website_email = get_option('website_email');
    if ($website_email == '') {
        $website_email = $from_email;
    }
    if (
        (get_option('use_true_from') == '1') ||
        ((get_option('use_true_from') == '0') && (preg_replace('#^.*@#', '', $from_email) == preg_replace('#^.*@#', '', get_option('website_email')))) ||
        ((get_option('use_true_from') == '0') && (preg_replace('#^.*@#', '', $from_email) == preg_replace('#^.*@#', '', get_option('staff_address')))) ||
        ((addon_installed('tickets')) && (get_option('use_true_from') == '0') && (preg_replace('#^.*@#', '', $from_email) == preg_replace('#^.*@#', '', get_option('ticket_email_from')))) ||
        ((addon_installed('tickets')) && (get_option('use_true_from') == '0') && (preg_replace('#^.*@#', '', $from_email) == get_domain()))
    ) {
        $headers = 'From: "' . $from_name . '" <' . $from_email . '>' . $line_term;
    } else {
        $headers = 'From: "' . $from_name . '" <' . $website_email . '>' . $line_term;
    }
    $headers .= 'Reply-To: <' . $from_email . '>' . $line_term;
    $headers .= 'Return-Path: <' . $website_email . '>' . $line_term;
    $headers .= 'X-Sender: <' . $website_email . '>' . $line_term;
    $cc_address = $no_cc ? '' : get_option('cc_address');
    if ($cc_address != '') {
        if (get_option('bcc') == '0') {
            $extra_cc_addresses[] = $cc_address;
        } else {
            $extra_bcc_addresses[] = $cc_address;
        }
    }
    if ($extra_cc_addresses !== array()) {
        $headers .= 'Cc: ';
        foreach ($extra_cc_addresses as $i => $extra_cc_address) {
            if ($i != 0) {
                $headers .= ', ';
            }
            $headers .= '<' . $extra_cc_address . '>';
        }
        $headers .= $line_term;
    }
    if ($extra_bcc_addresses !== array()) {
        $headers .= 'Bcc: ';
        foreach ($extra_bcc_addresses as $i => $extra_bcc_address) {
            if ($i != 0) {
                $headers .= ', ';
            }
            $headers .= '<' . $extra_bcc_address . '>';
        }
        $headers .= $line_term;
    }
    $headers .= 'Date: ' . date('r', time()) . $line_term;
    $headers .= 'Message-ID: <' . $_boundary . '@' . get_domain() . '>' . $line_term;
    $headers .= 'X-Priority: ' . strval($priority) . $line_term;
    $brand_name = get_value('rebrand_name');
    if (is_null($brand_name)) {
        $brand_name = 'Composr';
    }
    $headers .= 'X-Mailer: ' . $brand_name . $line_term;
    $list_unsubscribe_target = get_value('list_unsubscribe_target');
    if (!empty($list_unsubscribe_target)) {
        $headers .= 'List-Unsubscribe: <' . $list_unsubscribe_target . '>' . $line_term;
    }
    if ((count($to_email) == 1) && (!is_null($require_recipient_valid_since))) {
        $_require_recipient_valid_since = date('r', $require_recipient_valid_since);
        $headers .= 'Require-Recipient-Valid-Since: ' . $to_email[0] . '; ' . $_require_recipient_valid_since . $line_term;
    }
    $headers .= 'MIME-Version: 1.0' . $line_term;
    if ((!empty($attachments)) || (!$simplify_when_can)) {
        $headers .= 'Content-Type: multipart/mixed; boundary="' . $boundary . '"';
    } else {
        $headers .= 'Content-Type: multipart/alternative; boundary="' . $boundary2 . '"';
    }
    $sending_message = '';
    $sending_message .= 'This is a multi-part message in MIME format.' . $line_term . $line_term;
    if ((!empty($attachments)) || (!$simplify_when_can)) {
        $sending_message .= '--' . $boundary . $line_term;
        $sending_message .= 'Content-Type: multipart/alternative; boundary="' . $boundary2 . '"' . $line_term . $line_term . $line_term;
    }

    if (GOOGLE_APPENGINE) {
        require_once('google/appengine/api/mail/Message.php');
        $message_class = 'google\appengine\api\mail\Message';

        $reply_to = $from_email;//$from_name.' <'.$from_email.'>'; GAE doesn't support nice format yet

        $mail_options = array(
            'sender' => $website_email,
            'subject' => $subject->evaluate($lang),
            'textBody' => $message_plain,
            'htmlBody' => $html_evaluated,
        );

        try {
            $message = new $message_class($mail_options);
            $message->addCc($extra_cc_addresses);
            $message->addBcc($extra_bcc_addresses);
            $message->setReplyTo($reply_to);
            foreach ($to_email as $_to_email) {
                $message->addTo($_to_email); // $to_name.' <'.$_to_email.'>' GAE doesn't support nice format yet
            }
            foreach ($attachments as $path => $filename) {
                if ((strpos($path, '://') === false) && (substr($path, 0, 5) != 'gs://')) {
                    if (!is_file($path)) {
                        continue;
                    }
                    $contents = file_get_contents($path);
                } else {
                    $contents = http_download_file($path, null, false);
                    if (is_null($contents)) {
                        continue;
                    }
                }
                $message->addAttachment($filename, $contents);
            }
            $message->send();
        } catch (InvalidArgumentException $e) {
            $error = $e->getMessage();

            require_code('site');
            attach_message(!is_null($error) ? make_string_tempcode($error) : do_lang_tempcode('MAIL_FAIL', escape_html(get_option('staff_address'))), 'warn');

            require_code('files2');
            clean_temporary_mail_attachments($attachments);

            if (!$coming_out_of_queue) {
                cms_profile_end_for('mail_wrap', $subject_line);
            }

            return false;
        }

        $SENDING_MAIL = false;
        require_code('files2');
        clean_temporary_mail_attachments($attachments);

        if (!$coming_out_of_queue) {
            cms_profile_end_for('mail_wrap', $subject_line);
        }

        return true;
    }

    $base64_encode = (get_value('base64_emails') === '1'); // More robust, but more likely to be spam-blocked, and some servers can scramble it.

    // Plain version
    if (!$in_html) {
        $sending_message .= '--' . $boundary2 . $line_term;
        $sending_message .= 'Content-Type: text/plain; charset=' . ((preg_match($regexp, $message_plain) == 0) ? do_lang('charset', null, null, null, $lang) : 'us-ascii') . $line_term; // '; name="message.txt"'.  Outlook doesn't like: makes it think it's an attachment
        if ($base64_encode) {
            $sending_message .= 'Content-Transfer-Encoding: base64' . $line_term . $line_term;
            $sending_message .= chunk_split(base64_encode(unixify_line_format($message_plain)) . $line_term, 76, $line_term);
        } else {
            $sending_message .= 'Content-Transfer-Encoding: 8bit' . $line_term . $line_term;
            $sending_message .= wordwrap(str_replace("\n", $line_term, unixify_line_format($message_plain)) . $line_term, 988, $line_term, true);
        }
    }

    // HTML version
    $sending_message .= '--' . $boundary2 . $line_term;
    $sending_message .= 'Content-Type: multipart/related; boundary="' . $boundary3 . '"' . $line_term . $line_term . $line_term;
    $sending_message .= '--' . $boundary3 . $line_term;
    $sending_message .= 'Content-Type: text/html; charset=' . ((preg_match($regexp, $html_evaluated) == 0) ? do_lang('charset', null, null, null, $lang) : 'us-ascii') . $line_term; // .'; name="message.html"'. Outlook doesn't like: makes it think it's an attachment
    if (get_option('allow_ext_images') != '1') {
        $cid_before = array_keys($CID_IMG_ATTACHMENT);
        $html_evaluated = preg_replace_callback('#<img\s([^>]*)src="(https?://[^"]*)"#U', '_mail_img_rep_callback', $html_evaluated);
        $cid_just_html = array_diff(array_keys($CID_IMG_ATTACHMENT), $cid_before);
        $matches = array();
        foreach (array('#<([^"<>]*\s)style="([^"]*)"#', '#<style( [^<>]*)?' . '>(.*)</style>#Us') as $over) {
            $num_matches = preg_match_all($over, $html_evaluated, $matches);
            for ($i = 0; $i < $num_matches; $i++) {
                $altered_inner = preg_replace_callback('#url\(["\']?(https?://[^"]*)["\']?\)#U', '_mail_css_rep_callback', $matches[2][$i]);
                if ($matches[2][$i] != $altered_inner) {
                    $altered_outer = str_replace($matches[2][$i], $altered_inner, $matches[0][$i]);
                    $html_evaluated = str_replace($matches[0][$i], $altered_outer, $html_evaluated);
                }
            }
        }

        // This is a hack to stop the images used by CSS showing as attachments in some mail clients
        $cid_just_css = array_diff(array_keys($CID_IMG_ATTACHMENT), $cid_just_html);
        foreach ($cid_just_css as $cid) {
            $html_evaluated .= '<img width="0" height="0" src="cid:' . $cid . '" />';
        }
    }
    foreach ($CID_IMG_ATTACHMENT as $id => $img) { // Make sure all inline images are referenced with img tags, otherwise some e-mail software may show it as an attachment
        $html_evaluated .= '<!-- <img src="cid:' . $id . '" /> -->';
    }

    if ($base64_encode) {
        $sending_message .= 'Content-Transfer-Encoding: base64' . $line_term . $line_term;
        $sending_message .= chunk_split(base64_encode(unixify_line_format($html_evaluated)) . $line_term, 76, $line_term);
    } else {
        $sending_message .= 'Content-Transfer-Encoding: 8bit' . $line_term . $line_term; // Requires RFC 1652
        $sending_message .= wordwrap(str_replace("\n", $line_term, unixify_line_format($html_evaluated)) . $line_term, 988, $line_term, true);
    }
    $total_filesize = 0;
    foreach ($CID_IMG_ATTACHMENT as $id => $img) {
        $test = _get_image_for_cid($img, $as, $total_filesize);
        if (is_null($test)) {
            continue;
        }
        list($mime_type, $filename, $file_contents) = $test;

        $sending_message .= '--' . $boundary3 . $line_term;
        $sending_message .= 'Content-Type: ' . escape_header($mime_type) . '; name="' . escape_header($filename) . '"' . $line_term;
        $sending_message .= 'Content-Transfer-Encoding: base64' . $line_term;
        $sending_message .= 'Content-ID: <' . $id . '>' . $line_term;
        $sending_message .= 'Content-Disposition: inline; filename="' . escape_header($filename) . '"' . $line_term . $line_term;
        if (is_string($file_contents)) {
            $sending_message .= chunk_split(base64_encode($file_contents), 76, $line_term);
        }
    }
    $sending_message .= $line_term . '--' . $boundary3 . '--' . $line_term;

    $sending_message .= $line_term . '--' . $boundary2 . '--' . $line_term;

    // Attachments
    if (!empty($attachments)) {
        foreach ($attachments as $path => $filename) {
            $mime_type = get_mime_type(get_file_extension($filename), has_privilege($as, 'comcode_dangerous'));

            if ((strpos($path, '://') === false) && (substr($path, 0, 5) != 'gs://')) {
                if (!is_file($path)) {
                    continue;
                }
                $contents = file_get_contents($path);
            } else {
                $contents = http_download_file($path, null, false);
                if (is_null($contents)) {
                    continue;
                }
                if (!is_null($GLOBALS['HTTP_DOWNLOAD_MIME_TYPE'])) {
                    $mime_type = $GLOBALS['HTTP_DOWNLOAD_MIME_TYPE'];
                }
            }

            $sending_message .= '--' . $boundary . $line_term;
            $sending_message .= 'Content-Type: ' . $mime_type . $line_term; // . '; name="' . escape_header($filename).'"'   http://www.imc.org/ietf-822/old-archive2/msg02121.html
            $sending_message .= 'Content-Transfer-Encoding: base64' . $line_term;
            $sending_message .= 'Content-Disposition: attachment; filename="' . escape_header($filename) . '"' . $line_term . $line_term;

            $sending_message .= chunk_split(base64_encode($contents), 76, $line_term);
        }

        $sending_message .= $line_term . '--' . $boundary . '--' . $line_term;
    }

    // Support for SMTP sockets rather than PHP mail()
    $error = null;
    if ((get_option('smtp_sockets_use') == '1') && (php_function_allowed('fsockopen'))) {
        $worked = false;

        $host = get_option('smtp_sockets_host');
        $port = intval(get_option('smtp_sockets_port'));

        $errno = 0;
        $errstr = '';
        foreach ($to_email as $i => $to) {
            $socket = @fsockopen($host, $port, $errno, $errstr, 30.0);
            if ($socket !== false) {
                $rcv = fread($socket, 1024);
                $base_url = parse_url(get_base_url());
                $domain = $base_url['host'];

                // Log in if necessary
                $username = get_option('smtp_sockets_username');
                $password = get_option('smtp_sockets_password');
                if ($username != '') {
                    fwrite($socket, 'EHLO ' . $domain . "\r\n");
                    $rcv = fread($socket, 1024);

                    fwrite($socket, "AUTH LOGIN\r\n");
                    $rcv = fread($socket, 1024);
                    if (strtolower(substr($rcv, 0, 3)) == '334') {
                        fwrite($socket, base64_encode($username) . "\r\n");
                        $rcv = fread($socket, 1024);
                        if ((strtolower(substr($rcv, 0, 3)) == '235') || (strtolower(substr($rcv, 0, 3)) == '334')) {
                            fwrite($socket, base64_encode($password) . "\r\n");
                            $rcv = fread($socket, 1024);
                            if (strtolower(substr($rcv, 0, 3)) == '235') {
                            } else {
                                $error = do_lang('MAIL_ERROR_CONNECT_PASSWORD') . ' (' . str_replace($password, '*', $rcv) . ')';
                            }
                        } else {
                            $error = do_lang('MAIL_ERROR_CONNECT_USERNAME') . ' (' . $rcv . ')';
                        }
                    } else {
                        $error = do_lang('MAIL_ERROR_CONNECT_AUTH') . ' (' . $rcv . ')';
                    }
                } else {
                    fwrite($socket, 'HELO ' . $domain . "\r\n");
                    $rcv = fread($socket, 1024);
                }

                if (is_null($error)) {
                    $smtp_from_address = get_option('smtp_from_address');
                    if ($smtp_from_address == '') {
                        $smtp_from_address = $website_email;
                    }
                    fwrite($socket, 'MAIL FROM:<' . $smtp_from_address . ">\r\n");
                    $rcv = fread($socket, 1024);
                    if ((strtolower(substr($rcv, 0, 3)) == '250') || (strtolower(substr($rcv, 0, 3)) == '251')) {
                        $sent_one = false;
                        fwrite($socket, "RCPT TO:<" . $to_email[$i] . ">\r\n");
                        $rcv = fread($socket, 1024);
                        if ((strtolower(substr($rcv, 0, 3)) != '250') && (strtolower(substr($rcv, 0, 3)) != '251')) {
                            $error = do_lang('MAIL_ERROR_TO') . ' (' . $rcv . ')' . ' ' . $to_email[$i];
                        } else {
                            $sent_one = true;
                        }
                        if ($sent_one) {
                            fwrite($socket, "DATA\r\n");
                            $rcv = fread($socket, 1024);
                            if (strtolower(substr($rcv, 0, 3)) == '354') {
                                $_to_name = preg_replace('#@.*$#', '', is_array($to_name) ? $to_name[$i] : $to_name); // preg_replace is because some servers may reject sending names that look like e-mail addresses. Composr tries this from recommend module.
                                if (count($to_email) == 1) {
                                    if ($_to_name == '') {
                                        fwrite($socket, 'To: ' . $to_email[$i] . "\r\n");
                                    } else {
                                        fwrite($socket, 'To: ' . $_to_name . ' <' . $to_email[$i] . '>' . "\r\n");
                                    }
                                } else {
                                    fwrite($socket, 'To: ' . $_to_name . "\r\n");
                                }
                                fwrite($socket, 'Subject: ' . $tightened_subject . "\r\n");
                                $headers = preg_replace('#^\.#m', '..', $headers);
                                $sending_message = preg_replace('#^\.#m', '..', $sending_message);
                                fwrite($socket, $headers . "\r\n\r\n");
                                fwrite($socket, $sending_message);
                                fwrite($socket, "\r\n.\r\n");
                                $rcv = fread($socket, 1024);
                                if (strtolower(substr($rcv, 0, 3)) != '250') {
                                    $error = do_lang('MAIL_ERROR_DATA') . ' (' . $rcv . ')';
                                }
                                fwrite($socket, "QUIT\r\n");
                                $rcv = fread($socket, 1024);
                            } else {
                                $error = do_lang('MAIL_ERROR_DATA') . ' (' . $rcv . ')';
                            }
                        }
                    } else {
                        $error = do_lang('MAIL_ERROR_FROM') . ' (' . $rcv . ')';
                    }

                    if (@fwrite($socket, "RSET\r\n") === false) { // Cut out. At least one server does this
                        @fclose($socket);
                        $socket = null;
                    } else {
                        $rcv = fread($socket, 1024);
                    }
                }

                if (!is_null($socket)) {
                    fclose($socket);
                }
                if (is_null($error)) {
                    $worked = true;
                }
            } else {
                $error = do_lang('MAIL_ERROR_CONNECT', $host, strval($port));
            }
        }
    } else {
        $worked = false;
        foreach ($to_email as $i => $to) {
            $bak = $GLOBALS['SUPPRESS_ERROR_DEATH'];
            $GLOBALS['SUPPRESS_ERROR_DEATH'] = true;

            $additional = '';
            if (get_option('enveloper_override') == '1') {
                if (is_email_address($website_email)) { // Required for security
                    $additional = '-f ' . $website_email;
                }
            }
            $_to_name = preg_replace('#@.*$#', '', is_array($to_name) ? $to_name[$i] : $to_name); // preg_replace is because some servers may reject sending names that look like e-mail addresses. Composr tries this from recommend module.
            if (($_to_name == '') || (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN')) {
                $to_line = $to;
            } else {
                $to_line = '"' . $_to_name . '" <' . $to . '>';
            }

            // DKIM
            if (trim($dkim_private_key) != '') {
                $signature = new DKIMSignature(trim($dkim_private_key, " \t\r\n\"'"), '', get_domain(), get_option('dkim_selector'));
                $signed_headers = str_replace("\r\n", $line_term, $signature->get_signed_headers($to_line, $tightened_subject, str_replace($line_term, "\r\n", $sending_message), str_replace($line_term, "\r\n", $headers)));
            }

            //if (function_exists('mb_language')) mb_language('en'); Stop overridden mbstring mail function from messing and base64'ing stuff. Actually we don't need this as we make sure to pass through as headers with blank message, bypassing any filtering.
            $php_errormsg = mixed();
            if (get_value('manualproc_mail') === '1') {
                require_code('mail2');
                $worked = manualproc_mail($to_line, $tightened_subject, $sending_message, $signed_headers . $headers, $additional);
            } else {
                if ((str_replace(array('on', 'true', 'yes'), array('1', '1', '1'), strtolower(ini_get('safe_mode'))) == '1') || ($additional == '')) {
                    $worked = mail($to_line, $tightened_subject, $sending_message, $signed_headers . $headers);
                } else {
                    $worked = mail($to_line, $tightened_subject, $sending_message, $signed_headers . $headers, $additional);
                }
            }
            if ((!$worked) && (isset($php_errormsg))) {
                $error = $php_errormsg;
            }
            $GLOBALS['SUPPRESS_ERROR_DEATH'] = $bak;
        }
    }

    if (!$coming_out_of_queue) {
        cms_profile_end_for('mail_wrap', $subject_line);
    }

    if (!$worked) {
        $SENDING_MAIL = false;
        require_code('site');
        attach_message(!is_null($error) ? make_string_tempcode($error) : do_lang_tempcode('MAIL_FAIL', escape_html(get_option('staff_address'))), 'warn');

        require_code('files2');
        clean_temporary_mail_attachments($attachments);

        return false;
    }

    $SENDING_MAIL = false;

    require_code('files2');
    clean_temporary_mail_attachments($attachments);

    return true;
}

/**
 * Download a URL, for use as an inline mail image.
 *
 * @param  URLPATH $img URL
 * @param  ?MEMBER $as Convert Comcode->tempcode as this member (a privilege thing: we don't want people being able to use admin rights by default!) (null: guest)
 * @param  integer $total_filesize Reference to where total filesize is being held
 * @return ?array A tuple: Mime type filename, file contents (null: error)
 */
function _get_image_for_cid($img, $as, &$total_filesize)
{
    $file_path_stub = convert_url_to_path($img);
    $mime_type = get_mime_type(get_file_extension($img), has_privilege($as, 'comcode_dangerous'));
    $filename = basename($img);
    if (!is_null($file_path_stub)) {
        $total_filesize += @filesize($file_path_stub);
        if ($total_filesize > 1024 * 1024 * 5) {
            return null; // Too large to process into an email
        }

        $file_contents = @file_get_contents($file_path_stub);
    } else {
        $file_contents = mixed();
        $matches = array();
        if ((preg_match('#^' . preg_quote(find_script('attachment'), '#') . '\?id=(\d+)&amp;thumb=(0|1)#', $img, $matches) != 0) && (strpos($img, 'forum_db=1') === false)) {
            $rows = $GLOBALS['SITE_DB']->query_select('attachments', array('*'), array('id' => intval($matches[1])), 'ORDER BY a_add_time DESC');
            require_code('attachments');
            if ((array_key_exists(0, $rows)) && (has_attachment_access($as, intval($matches[1])))) {
                $myrow = $rows[0];

                if ($matches[2] == '1') {
                    $full = $myrow['a_thumb_url'];
                } else {
                    $full = $myrow['a_url'];
                }

                if (url_is_local($full)) {
                    $_full = get_custom_file_base() . '/' . rawurldecode($full);
                    if (file_exists($_full)) {
                        $filename = $myrow['a_original_filename'];
                        require_code('mime_types');
                        $total_filesize += @filesize($_full);
                        if ($total_filesize > 1024 * 1024 * 5) {
                            return null; // Too large to process into an email
                        }
                        $file_contents = file_get_contents($_full);
                        $mime_type = get_mime_type(get_file_extension($filename), has_privilege($as, 'comcode_dangerous'));
                    } else {
                        return null;
                    }
                }
            }
        }
        if ($file_contents === null) {
            $file_contents = http_download_file($img, 1024 * 1024 * 5, false);
            if (is_null($file_contents)) {
                return null;
            }
            $total_filesize += strlen($file_contents);
            if ($total_filesize > 1024 * 1024 * 5) {
                return null; // Too large to process into an email
            }
            if (!is_null($GLOBALS['HTTP_DOWNLOAD_MIME_TYPE'])) {
                $mime_type = $GLOBALS['HTTP_DOWNLOAD_MIME_TYPE'];
            }
            if (!is_null($GLOBALS['HTTP_FILENAME'])) {
                $filename = $GLOBALS['HTTP_FILENAME'];
            }
        }
    }

    return array($mime_type, $filename, $file_contents);
}

/**
 * Filter out any CSS selector blocks from the given CSS if they definitely do not affect the given (X)HTML.
 * While this is a clever algorithm, it isn't so clever as to actually try and match each selector against a DOM tree. If any segment of a compound selector matches, match is assumed.
 *
 * @param  ID_TEXT $c CSS file
 * @param  ?ID_TEXT $theme Theme (null: default)
 * @param  string $context (X) HTML context under which CSS is filtered
 * @return string Filtered CSS
 */
function filter_css($c, $theme, $context)
{
    if (is_null($theme)) {
        $theme = $GLOBALS['FORUM_DRIVER']->get_theme();
    }

    // Reduce input parameters to critical components, and cache on - saves a lot of time if multiple emails sent by script
    static $cache = array();
    $simple_sig = preg_replace('#\s+(?!class)(?!id)[\w\-]+="[^"<>]*"#', '', preg_replace('#[^<>]*(<[^<>]+>)[^<>]*#s', '${1}', $context));
    $simple_sig .= $c . $theme;
    if (isset($cache[$simple_sig])) {
        return $cache[$simple_sig];
    }

    $_css = do_template($c, null, user_lang(), true/*can't fail on this error because it could be an email from queue, with different addon state*/, null, '.css', 'css', $theme);
    $css = $_css->evaluate();

    // Find out all our IDs
    $ids = array();
    $matches = array();
    $count = preg_match_all('#\sid=["\']([^"\']*)["\']#', $context, $matches);
    for ($i = 0; $i < $count; $i++) {
        $ids[$matches[1][$i]] = true;
    }

    // Find out all our classes
    $classes = array();
    $count = preg_match_all('#\sclass=["\']([^"\']*)["\']#', $context, $matches);
    for ($i = 0; $i < $count; $i++) {
        if ($matches[1][$i] == '') {
            continue;
        }
        $classes = array_merge($classes, preg_split('#\s+#', $matches[1][$i], -1, PREG_SPLIT_NO_EMPTY));
    }
    $classes = array_flip($classes);

    // Find all our XHTML tags
    $tags = array();
    $count = preg_match_all('#<(\w+)([^\w])#', $context, $matches);
    for ($i = 0; $i < $count; $i++) {
        $tags[$matches[1][$i]] = true;
    }

    // Strip comments from CSS. This optimises, and also avoids us needing to do a sophisticated parse
    $css = preg_replace('#/\*.*\*/#Us', '', $css);

    // Strip all media rules, we don't support parsing it, and e-mails will not be that complex
    $middle_regexp = '[^\{\}]*' . '\{[^\{\}]*\}' . '[^\{\}]*';
    $css = preg_replace('#@media[^\{\}]*\{(' . $middle_regexp . ')*\}#s', '', $css);

    // Find and process each CSS selector block
    $stack = array();
    $css_new = '';
    $last_pos = 0;
    do {
        $pos1 = strpos($css, '{', $last_pos);
        $pos2 = strpos($css, '}', $last_pos);
        if (($pos1 === false) && ($pos2 === false)) {
            break;
        }

        if (($pos1 === false) || (($pos2 !== false) && ($pos2 < $pos1))) {
            if (count($stack) != 0) {
                $start = array_pop($stack);
                if (count($stack) == 0) { // We've finished a top-level section
                    $real_start = strrpos(substr($css, 0, $start), '}');
                    $real_start = ($real_start === false) ? 0 : ($real_start + 1);
                    $selectors = explode(',', trim(substr($css, $real_start, $start - $real_start)));
                    $applies = false;
                    foreach ($selectors as $selector) {
                        $selector = trim($selector);

                        if (strpos($selector, '::selection') !== false) {
                            continue;
                        }
                        if (strpos($selector, 'a[href^="mailto:"]') !== false) {
                            continue;
                        }
                        if (strpos($selector, 'a[target="_blank"]') !== false) {
                            continue;
                        }

                        $matches = array();

                        // ID selectors
                        $num_matches = preg_match_all('#\#(\w+)#', $selector, $matches);
                        for ($i = 0; $i < $num_matches; $i++) {
                            if (!isset($ids[$matches[1][$i]])) {
                                continue 2;
                            }
                        }

                        // Class name selectors
                        $num_matches = preg_match_all('#\.(\w+)#', $selector, $matches);
                        for ($i = 0; $i < $num_matches; $i++) {
                            if (!isset($classes[$matches[1][$i]])) {
                                continue 2;
                            }
                        }

                        // Tag selectors
                        $num_matches = preg_match_all('#(^|\s|>)(\w+)#', $selector, $matches);
                        for ($i = 0; $i < $num_matches; $i++) {
                            if (!isset($tags[$matches[2][$i]])) {
                                continue 2;
                            }
                        }

                        $applies = true;
                        break;
                    }
                    if ($applies) {
                        $css_new .= trim(substr($css, $real_start, $pos2 - $real_start + 1)) . "\n\n"; // Append section
                    }
                }
            } else {
                //return $css; // Parsing error, extra close
                // But actually it's best we let it continue on
            }
            $last_pos = $pos2 + 1;
        } else {
            array_push($stack, $pos1);
            $last_pos = $pos1 + 1;
        }
    } while (true);

    $cache[$simple_sig] = $css_new;

    return $css_new;
}

/**
 * Entry script to process a form that needs to be emailed.
 */
function form_to_email_entry_script()
{
    require_lang('mail');
    form_to_email();

    global $PAGE_NAME_CACHE;
    $PAGE_NAME_CACHE = '_form_to_email';
    $title = get_screen_title('MAIL_SENT');
    $text = do_lang_tempcode('MAIL_SENT_TEXT', escape_html(post_param_string('to_written_name', get_site_name())));
    $redirect = get_param_string('redirect', null);
    if (!is_null($redirect)) {
        require_code('site2');
        assign_refresh($redirect, 0.0);
        $tpl = redirect_screen($title, $redirect, $text);
    } else {
        $tpl = do_template('INFORM_SCREEN', array('_GUID' => 'e577a4df79eefd9064c14240cc99e947', 'TITLE' => $title, 'TEXT' => $text));
    }
    $echo = globalise($tpl, null, '', true, true);
    $echo->evaluate_echo();
}

/**
 * Send the posted form over email to the staff address.
 *
 * @param  ?string $subject The subject of the email (null: from posted subject parameter).
 * @param  string $intro The intro text to the mail (blank: none).
 * @param  ?array $fields A map of fields to field titles to transmit. (null: all posted fields, except subject and email)
 * @param  ?string $to_email Email address to send to (null: look from post environment / staff address).
 * @param  string $outro The outro text to the mail (blank: none).
 * @param  boolean $is_via_post Whether $fields refers to some POSTed fields, as opposed to a direct field->value map.
 */
function form_to_email($subject = null, $intro = '', $fields = null, $to_email = null, $outro = '', $is_via_post = true)
{
    $details = _form_to_email(null, $subject, $intro, $fields, $to_email, $outro, $is_via_post);
    list($subject, $message_raw, $to_email, $to_name, $from_email, $from_name, $attachments) = $details;

    if (addon_installed('captcha')) {
        if (post_param_integer('_security', 0) == 1) {
            require_code('captcha');
            enforce_captcha();
        }
    }

    mail_wrap($subject, $message_raw, is_null($to_email) ? null : array($to_email), $to_name, $from_email, $from_name, 3, $attachments, false, null, false, false, false, 'MAIL', count($attachments) != 0);

    if ($from_email != '' && get_option('message_received_emails') == '1') {
        mail_wrap(do_lang('YOUR_MESSAGE_WAS_SENT_SUBJECT', $subject), do_lang('YOUR_MESSAGE_WAS_SENT_BODY', $message_raw), array($from_email), empty($from_name) ? null : $from_name, '', '', 3, null, false, get_member());
    }
}

/**
 * Worker funtion for form_to_email.
 *
 * @param  ?array $extra_boring_fields Fields to skip in addition to the normal skipped ones (null: just the normal skipped ones)
 * @param  ?string $subject The subject of the email (null: from posted subject parameter).
 * @param  string $intro The intro text to the mail (blank: none).
 * @param  ?array $fields A map of fields to field titles to transmit. (null: all posted fields, except subject and email)
 * @param  ?string $to_email Email address to send to (null: look from post environment / staff address).
 * @param  string $outro The outro text to the mail (blank: none).
 * @param  boolean $is_via_post Whether $fields refers to some POSTed fields, as opposed to a direct field->value map.
 * @return array A tuple: subject, message, to e-mail, to name, from e-mail, from name, attachments
 *
 * @ignore
 */
function _form_to_email($extra_boring_fields = null, $subject = null, $intro = '', $fields = null, $to_email = null, $outro = '', $is_via_post = true)
{
    if (empty($subject)) {
        $subject = post_param_string('subject', get_site_name());
    }

    if (is_null($fields)) {
        $fields = array();
        $boring_fields = array( // NB: Keep in sync with static_export.php
            'MAX_FILE_SIZE',
            'perform_webstandards_check',
            '_validated',
            'posting_ref_id',
            'f_face',
            'f_colour',
            'f_size',
            'x',
            'y',
            'stub',
            'name',
            'subject',
            'email',
            'to_members_email',
            'to_written_name',
            'redirect',
            'http_referer',
            'session_id',
            'csrf_token',
            md5(get_site_name() . ': antispam'),
        );
        if (!is_null($extra_boring_fields)) {
            $boring_fields = array_merge($boring_fields, $extra_boring_fields);
        }
        foreach (array_diff(array_keys($_POST), $boring_fields) as $key) {
            $is_hidden =  // NB: Keep in sync with static_export.php
                (strpos($key, 'hour') !== false) ||
                (strpos($key, 'access_') !== false) ||
                (strpos($key, 'minute') !== false) ||
                (strpos($key, 'confirm') !== false) ||
                (strpos($key, 'pre_f_') !== false) ||
                (strpos($key, 'tick_on_form__') !== false) ||
                (strpos($key, 'label_for__') !== false) ||
                (strpos($key, 'description_for__') !== false) ||
                (strpos($key, 'wysiwyg_version_of_') !== false) ||
                (strpos($key, 'is_wysiwyg') !== false) ||
                (strpos($key, 'require__') !== false) ||
                (strpos($key, 'tempcodecss__') !== false) ||
                (strpos($key, 'comcode__') !== false) ||
                (strpos($key, '_parsed') !== false) ||
                (substr($key, 0, 1) == '_') ||
                (substr($key, 0, 9) == 'hidFileID') ||
                (substr($key, 0, 11) == 'hidFileName');
            if ($is_hidden) {
                continue;
            }

            if (substr($key, 0, 1) != '_') {
                $label = post_param_string('label_for__' . $key, titleify($key));
                $description = post_param_string('description_for__' . $key, '');
                $_label = $label . (($description == '') ? '' : (' (' . $description . ')'));

                if ($is_via_post) {
                    $fields[$key] = $_label;
                } else {
                    $fields[$label] = post_param_string($key, null);
                }
            }
        }
    }

    $from_email = trim(post_param_string('email', ''));

    $message_raw = '';
    if ($intro != '') {
        $message_raw .= $intro . "\n\n------------\n\n";
    }

    if ($is_via_post) {
        foreach ($fields as $field_name => $field_title) {
            $field_val = post_param_string($field_name, null);
            if (!is_null($field_val)) {
                _append_form_to_email($message_raw, post_param_integer('tick_on_form__' . $field_name, null) !== null, $field_title, $field_val, count($fields));

                if (($from_email == '') && ($field_val != '') && (post_param_string('field_tagged__' . $field_name, '') == 'email')) {
                    $from_email = $field_val;
                }
            }
        }
    } else {
        foreach ($fields as $field_title => $field_val) {
            if (!is_null($field_val)) {
                _append_form_to_email($message_raw, false, $field_title, $field_val, count($fields));
            }
        }
    }

    if ($outro != '') {
        $message_raw .= "\n\n------------\n\n" . $outro;
    }

    if ($from_email == '') {
        $from_email = $GLOBALS['FORUM_DRIVER']->get_member_email_address(get_member());
    }
    $from_name = post_param_string('name', $GLOBALS['FORUM_DRIVER']->get_username(get_member(), true));

    $to_name = mixed();
    if ((is_null($to_email)) && (!is_null(get_value('allow_member_mail_relay')))) {
        $to = post_param_integer('to_members_email', null);
        if (!is_null($to)) {
            $to_email = $GLOBALS['FORUM_DRIVER']->get_member_email_address($to);
            $to_name = $GLOBALS['FORUM_DRIVER']->get_username($to, true);
        }
    }

    $attachments = array();
    require_code('uploads');
    is_plupload(true);
    foreach ($_FILES as $file) {
        $attachments[$file['tmp_name']] = $file['name'];
    }

    return array($subject, $message_raw, $to_email, $to_name, $from_email, $from_name, $attachments);
}

/**
 * Append a value to a text e-mail.
 *
 * @param  string $message_raw Text-email (altered by reference).
 * @param  boolean $is_tick Whether it is a tick field.
 * @param  string $field_title Field title.
 * @param  string $field_val Field value.
 * @param  integer $num_fields Number of fields for e-mail.
 *
 * @ignore
 */
function _append_form_to_email(&$message_raw, $is_tick, $field_title, $field_val, $num_fields)
{
    $prefix = '';
    if ($num_fields != 1) {
        $prefix .= '[b]' . $field_title . '[/b]:';
        if (strpos($prefix, "\n") !== false || strpos($field_title, ' (') !== false) {
            $prefix .= "\n";
        } else {
            $prefix .= " ";
        }
    }

    if ($is_tick && in_array($field_val, array('', '0', '1'))) {
        $message_raw .= $prefix;
        $message_raw .= ($field_val == '1') ? do_lang('YES') : do_lang('NO');
    } else {
        if ($field_val == '') {
            return; // We won't show blank values, gets long
        }

        $message_raw .= $prefix;
        if ($field_val == '') {
            $message_raw .= '(' . do_lang('EMPTY') . ')';
        } else {
            $message_raw .= $field_val;
        }
    }

    $message_raw .= "\n\n";
}
