Отправка многосоставных (text/html) писем через wp_mail() может привести к блокировке вашего домена

18 июн. 2015 г., 18:37:09
Просмотры: 26.6K
Голосов: 44

Краткое описание

Из-за ошибки в WordPress Core, отправка многосоставных писем (html/text) через wp_mail() (для уменьшения вероятности попадания писем в спам) иронично приводит к блокировке вашего домена Hotmail (и другими почтовыми сервисами Microsoft).

Это сложная проблема, которую я постараюсь подробно разобрать, чтобы помочь найти рабочее решение, которое в итоге может быть реализовано в ядре.

Это будет познавательное чтение. Начнем...

Ошибка

Самый распространенный совет, чтобы избежать попадания ваших информационных писем в папку спама - отправлять многосоставные сообщения.

Многосоставные (mime) сообщения подразумевают отправку как HTML, так и текстовой версии письма в одном сообщении. Когда клиент получает многосоставное сообщение, он принимает HTML версию, если может отображать HTML, в противном случае показывает текстовую версию.

Это проверено на практике. При отправке в gmail все наши письма попадали в спам до тех пор, пока мы не изменили сообщения на многосоставные - после этого они стали приходить в основной ящик. Отлично.

Однако при отправке многосоставных сообщений через wp_mail(), Content Type (multipart/*) выводится дважды - один раз с boundary (если задан вручную) и один раз без него. Это поведение приводит к тому, что письмо отображается как сырое сообщение, а не многосоставное в некоторых почтовых клиентах, включая все сервисы Microsoft (Hotmail, Outlook и т.д.)

Microsoft помечает такие сообщения как спам, а те немногие письма, которые проходят, помечаются получателями вручную. К сожалению, почтовые адреса Microsoft широко используются. 40% наших подписчиков используют их.

Это подтверждено Microsoft в недавней переписке с ними.

Пометка сообщений приводит к полной блокировке домена. Это означает, что сообщения не просто попадают в папку спама, а вообще не доставляются получателю.

Наш основной домен уже был заблокирован 3 раза.

Поскольку это ошибка в ядре WordPress, каждый домен, отправляющий многосоставные сообщения, блокируется. Проблема в том, что большинство веб-мастеров не знают почему. Я подтвердил это при исследовании, увидев обсуждения других пользователей на форумах. Требуется углубиться в исходный код и иметь хорошее понимание того, как работают такие email сообщения, что мы рассмотрим далее...

Давайте разберем код

Создайте аккаунт hotmail/outlook. Затем выполните следующий код:

// Установите $to на email hotmail.com или outlook.com 
$to = "YourEmail@hotmail.com";

$subject = 'wp_mail тестирование многосоставного сообщения';

$message = '------=_Part_18243133_1346573420.1408991447668
Content-Type: text/plain; charset=UTF-8

Привет мир! Это обычный текст...


------=_Part_18243133_1346573420.1408991447668
Content-Type: text/html; charset=UTF-8

<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>

<p>Привет Мир! Это HTML...</p>

</body>
</html>


------=_Part_18243133_1346573420.1408991447668--';

$headers = "MIME-Version: 1.0\r\n";
$headers .= "From: Foo <foo@bar.com>\r\n";
$headers .= 'Content-Type: multipart/alternative;boundary="----=_Part_18243133_1346573420.1408991447668"';


// отправка email
wp_mail( $to, $subject, $message, $headers );

И если вы хотите изменить тип содержимого по умолчанию, используйте:

add_filter( 'wp_mail_content_type', 'set_content_type' );
function set_content_type( $content_type ) {
    return 'multipart/alternative';
}

Это отправит многосоставное сообщение.

Если вы проверите полный исходный код сообщения, вы заметите, что тип содержимого добавляется дважды, один раз без boundary:

MIME-Version: 1.0
Content-Type: multipart/alternative;
         boundary="====f230673f9d7c359a81ffebccb88e5d61=="
MIME-Version: 1.0
Content-Type: multipart/alternative; charset=

В этом и заключается проблема.

Источник проблемы находится в pluggable.php - если мы посмотрим где-то здесь:

// Установка Content-Type и кодировки
    // Если у нас нет типа содержимого из входящих заголовков
    if ( !isset( $content_type ) )
        $content_type = 'text/plain';

    /**
     * Фильтр для wp_mail() content type.
     *
     * @since 2.3.0
     *
     * @param string $content_type Тип содержимого wp_mail() по умолчанию.
     */
    $content_type = apply_filters( 'wp_mail_content_type', $content_type );

    $phpmailer->ContentType = $content_type;

    // Устанавливаем, является ли это обычным текстом, в зависимости от $content_type
    if ( 'text/html' == $content_type )
        $phpmailer->IsHTML( true );

    // Если у нас нет кодировки из входящих заголовков
    if ( !isset( $charset ) )
        $charset = get_bloginfo( 'charset' );

    // Установка типа содержимого и кодировки

    /**
     * Фильтр для кодировки wp_mail() по умолчанию.
     *
     * @since 2.3.0
     *
     * @param string $charset Кодировка email по умолчанию.
     */
    $phpmailer->CharSet = apply_filters( 'wp_mail_charset', $charset );

    // Установка пользовательских заголовков
    if ( !empty( $headers ) ) {
        foreach( (array) $headers as $name => $content ) {
            $phpmailer->AddCustomHeader( sprintf( '%1$s: %2$s', $name, $content ) );
        }

        if ( false !== stripos( $content_type, 'multipart' ) && ! empty($boundary) )
            $phpmailer->AddCustomHeader( sprintf( "Content-Type: %s;\n\t boundary=\"%s\"", $content_type, $boundary ) );
    }

    if ( !empty( $attachments ) ) {
        foreach ( $attachments as $attachment ) {
            try {
                $phpmailer->AddAttachment($attachment);
            } catch ( phpmailerException $e ) {
                continue;
            }
        }
    }

Возможные решения

Вы спросите, почему вы не сообщили об этом в trac? Я уже сообщил. К моему большому удивлению, другой тикет был создан 5 лет назад, описывающий ту же проблему.

Давайте смотреть правде в глаза - прошло полдесятилетия. В интернет-годах это примерно как 30 лет. Проблема явно заброшена и по сути никогда не будет исправлена (...если только мы не решим ее здесь).

Я нашел отличную тему здесь, предлагающую решение, но хотя его решение работает, оно ломает письма, которые не имеют пользовательских $headers.

Здесь мы каждый раз терпим неудачу. Либо многосоставная версия работает нормально, а обычные сообщения без установленных $headers не работают, либо наоборот.

Решение, к которому мы пришли:

if ( false !== stripos( $content_type, 'multipart' ) && ! empty($boundary) ) {
    $phpmailer->ContentType = $content_type . "; boundary=" . $boundary;
}
else {

        $content_type = apply_filters( 'wp_mail_content_type', $content_type );

    $phpmailer->ContentType = $content_type;

    // Устанавливаем, является ли это обычным текстом, в зависимости от $content_type
    if ( 'text/html' == $content_type )
        $phpmailer->IsHTML( true );

    // Если у нас нет кодировки из входящих заголовков
    if ( !isset( $charset ) )
        $charset = get_bloginfo( 'charset' );
}

// Установка типа содержимого и кодировки

/**
 * Фильтр для кодировки wp_mail() по умолчанию.
 *
 * @since 2.3.0
 *
 * @param string $charset Кодировка email по умолчанию.
 */
$phpmailer->CharSet = apply_filters( 'wp_mail_charset', $charset );

// Установка пользовательских заголовков
if ( !empty( $headers ) ) {
    foreach( (array) $headers as $name => $content ) {
        $phpmailer->AddCustomHeader( sprintf( '%1$s: %2$s', $name, $content ) );
    }

}

Да, я знаю, редактирование файлов ядра - табу, успокойтесь... это было отчаянное исправление и слабая попытка предоставить исправление для ядра.

Проблема нашего исправления в том, что письма по умолчанию, такие как новые регистрации, комментарии, сброс пароля и т.д., будут доставляться как пустые сообщения. Таким образом, у нас есть рабочий скрипт wp_mail(), который будет отправлять многосоставные сообщения, но ничего больше.

Что делать

Цель здесь - найти способ отправлять как обычные (текстовые), так и многосоставные сообщения используя основную функцию wp_mail() (не пользовательскую функцию sendmail).

При попытке решить эту проблему, основная сложность, с которой вы столкнетесь - это количество времени, которое вы потратите на отправку тестовых сообщений, проверку их получения и в основном открывание упаковки аспирина и проклинание Microsoft, потому что вы привыкли к их проблемам с IE, в то время как гремлин здесь, к сожалению, WordPress.

Обновление

Решение, опубликованное @bonger, позволяет $message быть массивом, содержащим альтернативы с ключами типа содержимого. Я подтвердил, что оно работает во всех сценариях.

Мы оставим этот вопрос открытым до истечения срока вознаграждения, чтобы повысить осведомленность о проблеме, возможно, до уровня, когда она будет исправлена в ядре. Не стесняйтесь публиковать альтернативное решение, где $message может быть строкой.

7
Комментарии

Поскольку функция wp_mail() является заменяемой, не будет ли определение вашей замены в виде обязательного плагина (в wp-content/mu-plugins) хорошим решением для вас (и всех остальных, если исправление ядра не сработает)? В каком случае перенос проверки multipart/boundary после установки $phpmailer->ContentType = $content_type; (вместо else) не сработает?

bonger bonger
18 июн. 2015 г. 21:35:18

@bonger Не могли бы вы написать ответ с подробным описанием вашего решения?

Christine Cooper Christine Cooper
18 июн. 2015 г. 22:07:28

Вам не нужно редактировать ядро, потому что wp_mail является заменяемой функцией. Скопируйте оригинальную функцию в плагин, отредактируйте её по своему усмотрению и активируйте плагин. WordPress будет использовать вашу изменённую функцию вместо оригинальной, без необходимости редактировать ядро.

gmazzap gmazzap
19 июн. 2015 г. 04:41:45

@ChristineCooper Я колеблюсь сделать это, так как, как вы говорите, тестирование — это настоящая головная боль, но глядя на патч https://core.trac.wordpress.org/ticket/15448, предложенный в треке @rmccue/@MattyRob, это выглядит действительно хорошим решением, поэтому я опубликую непроверенный ответ, основанный на этом...

bonger bonger
19 июн. 2015 г. 08:17:54

Это ни в коем случае не решение текущей проблемы, но, думаю, стоит отметить: если вы серьезно относитесь к тому, чтобы ваши письма доходили до адресата, вам стоит рассмотреть использование внешнего SMTP-сервера. Плагин SMTP полностью обойдет эту проблему, что может объяснять, почему никто не озаботился её исправлением в течение пяти лет.

Mathew Tinsley Mathew Tinsley
19 июн. 2015 г. 09:03:30

@ChristineCooper если вы просто подключитесь к phpmailer и установите текстовое тело в $phpmailer->AltBody, та же ошибка происходит?

chifliiiii chifliiiii
10 июл. 2015 г. 19:52:50

Нашел способ использовать wp_mail() без изменений ядра. Смотрите мой ответ ниже.

TheAddonDepot TheAddonDepot
20 дек. 2020 г. 01:08:31
Показать остальные 2 комментариев
Все ответы на вопрос 9
14
20

Следующая версия функции wp_mail() включает патч от @rmccue/@MattyRob из тикета https://core.trac.wordpress.org/ticket/15448, обновленный для версии 4.2.2, который позволяет передавать массив $message с альтернативными версиями контента по типам:

/**
 * Отправка почты, аналогично PHP-функции mail()
 *
 * Возвращение true не гарантирует успешную доставку письма получателю.
 * Это лишь означает, что метод смог обработать запрос без ошибок.
 *
 * Используя хуки 'wp_mail_from' и 'wp_mail_from_name', можно задать адрес отправителя
 * в формате 'Имя <email@address.com>', если установлены оба параметра. Если задан только
 * 'wp_mail_from', будет использован только email без имени.
 *
 * По умолчанию тип контента 'text/plain', что не позволяет использовать HTML.
 * Однако вы можете изменить тип контента письма с помощью фильтра
 * 'wp_mail_content_type'.
 *
 * Если $message - это массив, ключи используются для указания типа вложения,
 * а значения - как тело письма. Элемент 'text/plain' используется как текстовая версия,
 * 'text/html' - как HTML-версия. Все остальные типы добавляются как вложения.
 *
 * Кодировка по умолчанию соответствует кодировке блога. Её можно изменить
 * с помощью фильтра 'wp_mail_charset'.
 *
 * @since 1.2.1
 *
 * @uses PHPMailer
 *
 * @param string|array $to Массив или строка с email-адресами получателей.
 * @param string $subject Тема письма
 * @param string|array $message Содержимое письма
 * @param string|array $headers Дополнительные заголовки. Необязательно.
 * @param string|array $attachments Файлы для вложения. Необязательно.
 * @return bool Успешно ли отправлено письмо.
 */
function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() ) {
    // Компактируем входные данные, применяем фильтры и извлекаем обратно

    /**
     * Фильтр аргументов функции wp_mail().
     *
     * @since 2.2.0
     *
     * @param array $args Компактный массив аргументов wp_mail(), включая значения
     *                    "to", subject, message, headers и attachments.
     */
    $atts = apply_filters( 'wp_mail', compact( 'to', 'subject', 'message', 'headers', 'attachments' ) );

    if ( isset( $atts['to'] ) ) {
        $to = $atts['to'];
    }

    if ( isset( $atts['subject'] ) ) {
        $subject = $atts['subject'];
    }

    if ( isset( $atts['message'] ) ) {
        $message = $atts['message'];
    }

    if ( isset( $atts['headers'] ) ) {
        $headers = $atts['headers'];
    }

    if ( isset( $atts['attachments'] ) ) {
        $attachments = $atts['attachments'];
    }

    if ( ! is_array( $attachments ) ) {
        $attachments = explode( "\n", str_replace( "\r\n", "\n", $attachments ) );
    }
    global $phpmailer;

    // (Пере)создаём объект, если он отсутствует
    if ( ! ( $phpmailer instanceof PHPMailer ) ) {
        require_once ABSPATH . WPINC . '/class-phpmailer.php';
        require_once ABSPATH . WPINC . '/class-smtp.php';
        $phpmailer = new PHPMailer( true );
    }

    // Заголовки
    if ( empty( $headers ) ) {
        $headers = array();
    } else {
        if ( !is_array( $headers ) ) {
            // Разбиваем заголовки, чтобы функция могла принимать
            // как строку, так и массив заголовков.
            $tempheaders = explode( "\n", str_replace( "\r\n", "\n", $headers ) );
        } else {
            $tempheaders = $headers;
        }
        $headers = array();
        $cc = array();
        $bcc = array();

        // Если заголовки не пустые
        if ( !empty( $tempheaders ) ) {
            // Обрабатываем сырые заголовки
            foreach ( (array) $tempheaders as $header ) {
                if ( strpos($header, ':') === false ) {
                    if ( false !== stripos( $header, 'boundary=' ) ) {
                        $parts = preg_split('/boundary=/i', trim( $header ) );
                        $boundary = trim( str_replace( array( "'", '"' ), '', $parts[1] ) );
                    }
                    continue;
                }
                // Разделяем на части
                list( $name, $content ) = explode( ':', trim( $header ), 2 );

                // Очищаем
                $name    = trim( $name    );
                $content = trim( $content );

                switch ( strtolower( $name ) ) {
                    // В основном для совместимости - обрабатываем From: заголовок
                    case 'from':
                        $bracket_pos = strpos( $content, '<' );
                        if ( $bracket_pos !== false ) {
                            // Текст перед email в скобках - это имя "От".
                            if ( $bracket_pos > 0 ) {
                                $from_name = substr( $content, 0, $bracket_pos - 1 );
                                $from_name = str_replace( '"', '', $from_name );
                                $from_name = trim( $from_name );
                            }

                            $from_email = substr( $content, $bracket_pos + 1 );
                            $from_email = str_replace( '>', '', $from_email );
                            $from_email = trim( $from_email );

                        // Избегаем пустого $from_email.
                        } elseif ( '' !== trim( $content ) ) {
                            $from_email = trim( $content );
                        }
                        break;
                    case 'content-type':
                        if ( is_array($message) ) {
                            // Мультипартное письмо, игнорируем content-type
                            break;
                        }
                        if ( strpos( $content, ';' ) !== false ) {
                            list( $type, $charset_content ) = explode( ';', $content );
                            $content_type = trim( $type );
                            if ( false !== stripos( $charset_content, 'charset=' ) ) {
                                $charset = trim( str_replace( array( 'charset=', '"' ), '', $charset_content ) );
                            } elseif ( false !== stripos( $charset_content, 'boundary=' ) ) {
                                $boundary = trim( str_replace( array( 'BOUNDARY=', 'boundary=', '"' ), '', $charset_content ) );
                                $charset = '';
                            }

                        // Избегаем пустого $content_type.
                        } elseif ( '' !== trim( $content ) ) {
                            $content_type = trim( $content );
                        }
                        break;
                    case 'cc':
                        $cc = array_merge( (array) $cc, explode( ',', $content ) );
                        break;
                    case 'bcc':
                        $bcc = array_merge( (array) $bcc, explode( ',', $content ) );
                        break;
                    default:
                        // Добавляем в общий массив заголовков
                        $headers[trim( $name )] = trim( $content );
                        break;
                }
            }
        }
    }

    // Очищаем значения, которые могли быть установлены
    $phpmailer->ClearAllRecipients();
    $phpmailer->ClearAttachments();
    $phpmailer->ClearCustomHeaders();
    $phpmailer->ClearReplyTos();

    $phpmailer->Body= '';
    $phpmailer->AltBody= '';

    // Email и имя отправителя
    // Если имя не задано в заголовках
    if ( !isset( $from_name ) )
        $from_name = 'WordPress';

    /* Если email не задан в заголовках, используем wordpress@$sitename
     * Некоторые хостеры блокируют исходящую почту с этого адреса, если он не существует,
     * но альтернативы нет. Использование admin_email может показаться вариантом,
     * но некоторые хостеры могут отказать в релеинге почты с неизвестного домена.
     * См. https://core.trac.wordpress.org/ticket/5007.
     */

    if ( !isset( $from_email ) ) {
        // Получаем домен сайта без www.
        $sitename = strtolower( $_SERVER['SERVER_NAME'] );
        if ( substr( $sitename, 0, 4 ) == 'www.' ) {
            $sitename = substr( $sitename, 4 );
        }

        $from_email = 'wordpress@' . $sitename;
    }

    /**
     * Фильтр email-адреса отправителя.
     *
     * @since 2.2.0
     *
     * @param string $from_email Email-адрес отправителя.
     */
    $phpmailer->From = apply_filters( 'wp_mail_from', $from_email );

    /**
     * Фильтр имени, связанного с email-адресом отправителя.
     *
     * @since 2.3.0
     *
     * @param string $from_name Имя отправителя.
     */
    $phpmailer->FromName = apply_filters( 'wp_mail_from_name', $from_name );

    // Устанавливаем адреса получателей
    if ( !is_array( $to ) )
        $to = explode( ',', $to );

    foreach ( (array) $to as $recipient ) {
        try {
            // Разбиваем $recipient на имя и адрес, если формат "Имя <email@domain.com>"
            $recipient_name = '';
            if( preg_match( '/(.*)<(.+)>/', $recipient, $matches ) ) {
                if ( count( $matches ) == 3 ) {
                    $recipient_name = $matches[1];
                    $recipient = $matches[2];
                }
            }
            $phpmailer->AddAddress( $recipient, $recipient_name);
        } catch ( phpmailerException $e ) {
            continue;
        }
    }

    // Если кодировка не задана в заголовках
    if ( !isset( $charset ) )
        $charset = get_bloginfo( 'charset' );

    // Устанавливаем content-type и кодировку

    /**
     * Фильтр кодировки по умолчанию для wp_mail().
     *
     * @since 2.3.0
     *
     * @param string $charset Кодировка письма по умолчанию.
     */
    $phpmailer->CharSet = apply_filters( 'wp_mail_charset', $charset );

    // Устанавливаем тему и тело письма
    $phpmailer->Subject = $subject;

    if ( is_string($message) ) {
        $phpmailer->Body = $message;

        // Устанавливаем Content-Type и кодировку
        // Если content-type не задан в заголовках
        if ( !isset( $content_type ) )
            $content_type = 'text/plain';

        /**
         * Фильтр типа контента для wp_mail().
         *
         * @since 2.3.0
         *
         * @param string $content_type Тип контента по умолчанию.
         */
        $content_type = apply_filters( 'wp_mail_content_type', $content_type );

        $phpmailer->ContentType = $content_type;

        // Устанавливаем plaintext в зависимости от $content_type
        if ( 'text/html' == $content_type )
            $phpmailer->IsHTML( true );

        // Для обратной совместимости, новые мультипартные письма должны использовать
        // массив $message. Это всё равно никогда не работало хорошо
        if ( false !== stripos( $content_type, 'multipart' ) && ! empty($boundary) )
            $phpmailer->AddCustomHeader( sprintf( "Content-Type: %s;\n\t boundary=\"%s\"", $content_type, $boundary ) );
    }
    elseif ( is_array($message) ) {
        foreach ($message as $type => $bodies) {
            foreach ((array) $bodies as $body) {
                if ($type === 'text/html') {
                    $phpmailer->Body = $body;
                }
                elseif ($type === 'text/plain') {
                    $phpmailer->AltBody = $body;
                }
                else {
                    $phpmailer->AddAttachment($body, '', 'base64', $type);
                }
            }
        }
    }

    // Добавляем CC и BCC получателей
    if ( !empty( $cc ) ) {
        foreach ( (array) $cc as $recipient ) {
            try {
                // Разбиваем $recipient на имя и адрес, если формат "Имя <email@domain.com>"
                $recipient_name = '';
                if( preg_match( '/(.*)<(.+)>/', $recipient, $matches ) ) {
                    if ( count( $matches ) == 3 ) {
                        $recipient_name = $matches[1];
                        $recipient = $matches[2];
                    }
                }
                $phpmailer->AddCc( $recipient, $recipient_name );
            } catch ( phpmailerException $e ) {
                continue;
            }
        }
    }

    if ( !empty( $bcc ) ) {
        foreach ( (array) $bcc as $recipient) {
            try {
                // Разбиваем $recipient на имя и адрес, если формат "Имя <email@domain.com>"
                $recipient_name = '';
                if( preg_match( '/(.*)<(.+)>/', $recipient, $matches ) ) {
                    if ( count( $matches ) == 3 ) {
                        $recipient_name = $matches[1];
                        $recipient = $matches[2];
                    }
                }
                $phpmailer->AddBcc( $recipient, $recipient_name );
            } catch ( phpmailerException $e ) {
                continue;
            }
        }
    }

    // Используем PHP-функцию mail()
    $phpmailer->IsMail();

    // Устанавливаем кастомные заголовки
    if ( !empty( $headers ) ) {
        foreach ( (array) $headers as $name => $content ) {
            $phpmailer->AddCustomHeader( sprintf( '%1$s: %2$s', $name, $content ) );
        }
    }

    if ( !empty( $attachments ) ) {
        foreach ( $attachments as $attachment ) {
            try {
                $phpmailer->AddAttachment($attachment);
            } catch ( phpmailerException $e ) {
                continue;
            }
        }
    }

    /**
     * Действие после инициализации PHPMailer.
     *
     * @since 2.2.0
     *
     * @param PHPMailer &$phpmailer Объект PHPMailer, передаётся по ссылке.
     */
    do_action_ref_array( 'phpmailer_init', array( &$phpmailer ) );

    // Отправляем!
    try {
        return $phpmailer->Send();
    } catch ( phpmailerException $e ) {
        return false;
    }
}

Если поместить этот код в файл, например, "wp-content/mu-plugins/functions.php", он переопределит стандартную версию WP. Это позволяет удобно отправлять письма без возни с заголовками, например:

// Устанавливаем $to на email hotmail.com или outlook.com
$to = "YourEmail@hotmail.com";

$subject = 'Тестирование multipart в wp_mail';

$message['text/plain'] = 'Привет, мир! Это plain text...';
$message['text/html'] = '<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>

<p>Привет, мир! Это HTML...</p> 

</body>
</html>';

add_filter( 'wp_mail_from', $from_func = function ( $from_email ) { return 'foo@bar.com'; } );
add_filter( 'wp_mail_from_name', $from_name_func = function ( $from_name ) { return 'Foo'; } );

// Отправляем письмо
wp_mail( $to, $subject, $message );

remove_filter( 'wp_mail_from', $from_func );
remove_filter( 'wp_mail_from_name', $from_name_func );

Обратите внимание, я не тестировал этот код с реальными письмами...

19 июн. 2015 г. 08:34:00
Комментарии

Я добавил это в список обязательных плагинов и запустил тестовый код; все сработало. Я протестировал стандартные уведомления ядра (например, уведомление о новом пользователе), и они тоже работают. В эти выходные я продолжу тестирование, проверю, как плагины будут работать с этим, и в целом, все ли функционирует корректно. Особое внимание уделю анализу исходных данных сообщения. Это займет много времени, но будьте уверены, я сообщу о результатах. Если возникнет ситуация, когда wp_mail() не сработает (хотя должна бы), пожалуйста, дайте мне знать. Спасибо за ответ.

Christine Cooper Christine Cooper
20 июн. 2015 г. 18:43:49

Отлично, я бегло просмотрел вывод, и выглядит всё хорошо — по сути, патч просто заставляет wp_mail использовать стандартную, надежную обработку PHPMailer в случае передачи массива, а в остальных случаях по умолчанию возвращается к старому ненадежному подходу WP (для обратной совместимости), так что всё должно быть в порядке (очевидно, заслуга здесь принадлежит авторам патча)... Я начну использовать это решение с сегодняшнего дня (и постепенно внедрю его во всех проектах) — и спасибо за информацию об использовании как HTML, так и plain-текста, чтобы уменьшить вероятность попадания в спам...

bonger bonger
21 июн. 2015 г. 00:02:10

Мы протестировали это во всех возможных сценариях, и всё работает отлично. Завтра мы отправим рассылку, и посмотрим, будут ли жалобы от пользователей. Единственные небольшие изменения, которые нам пришлось внести — это санитизация/десанитизация массива при вставке в базу данных (у нас есть очередь сообщений в БД, которые cron отправляет небольшими партиями). Я оставлю этот вопрос открытым до истечения срока награды, чтобы привлечь внимание к этой проблеме. Надеюсь, этот патч или альтернативное решение будет добавлено в ядро. Или, что ещё важнее, почему бы и нет? О чём они вообще думают!

Christine Cooper Christine Cooper
21 июн. 2015 г. 18:53:57

Я случайно заметил, что вы обновили связанный тикет в Trac. Это обновление данного кода? Если да, не могли бы вы также опубликовать это обновление, отредактировав ваш ответ здесь, чтобы он оставался актуальным? Большое спасибо.

Christine Cooper Christine Cooper
9 сент. 2015 г. 02:49:04

Привет, нет, это было просто обновление патча для текущей версии trunk, чтобы он сливался без конфликтов (в надежде привлечь к нему внимание), код остался точно таким же...

bonger bonger
9 сент. 2015 г. 07:05:13

Вообще-то только что отредактировал, чтобы сделать его полностью идентичным (пробел после foreach!)...

bonger bonger
9 сент. 2015 г. 07:12:48

Отлично, прекрасная инициатива. Пожалуйста, продолжайте в том же духе. Возможно, в ближайшем будущем это наконец добавят в ядро... Они действительно должны это сделать.

Christine Cooper Christine Cooper
9 сент. 2015 г. 16:42:20

Я проверил ваш код через PHPCS и опубликовал обновленную версию: https://gist.github.com/paulschreiber/a1057785f6117f72188f3b619e994702

Paul Schreiber Paul Schreiber
17 янв. 2019 г. 16:29:15

В более поздних версиях WordPress это ломается. apply_filters( 'wp_mail', ... ) вызывает wp_staticize_emoji_for_email, которая в свою очередь вызывает wp_staticize_emoji. Ожидается, что $message будет строкой, а здесь это массив.

Paul Schreiber Paul Schreiber
17 янв. 2019 г. 17:03:46

@PaulSchreiber Решает ли ваше изменение ту часть, которая, по вашему утверждению, вызывает проблемы? Спасибо.

Christine Cooper Christine Cooper
19 февр. 2020 г. 12:54:21

@ChristineCooper Полагаю, что да. В моей версии больше не передается 'message' в compact().

Paul Schreiber Paul Schreiber
19 февр. 2020 г. 18:06:30

@PaulSchreiber Не могли бы вы любезно опубликовать ваш код в качестве ответа, чтобы у нас была обновленная версия кода прямо здесь, на WPSE? Большое спасибо.

Christine Cooper Christine Cooper
19 февр. 2020 г. 19:27:34

@ChristineCooper готово.

Paul Schreiber Paul Schreiber
19 февр. 2020 г. 23:29:38

Этот отличный подход всё ещё не попал в ядро WordPress? В документации нет упоминания об использовании массивов или multipart.

Thomas Ebert Thomas Ebert
12 нояб. 2020 г. 10:45:50
Показать остальные 9 комментариев
5
11

TLDR, простое решение:

add_action('phpmailer_init','wp_mail_set_text_body');
function wp_mail_set_text_body($phpmailer) {
     if (empty($phpmailer->AltBody)) {$phpmailer->AltBody = wp_strip_all_tags($phpmailer->Body);}
}

Тогда вам вообще не нужно явно устанавливать заголовки, границы заголовков будут установлены правильно автоматически.

Читайте дальше, чтобы узнать подробное объяснение почему...

На самом деле это не ошибка WordPress, а проблема phpmailer, которая не учитывает пользовательские заголовки... если посмотреть в class-phpmailer.php:

public function getMailMIME()
{
    $result = '';
    $ismultipart = true;
    switch ($this->message_type) {
        case 'inline':
            $result .= $this->headerLine('Content-Type', 'multipart/related;');
            $result .= $this->textLine("\tboundary=\"" . $this->boundary[1] . '"');
            break;
        case 'attach':
        case 'inline_attach':
        case 'alt_attach':
        case 'alt_inline_attach':
            $result .= $this->headerLine('Content-Type', 'multipart/mixed;');
            $result .= $this->textLine("\tboundary=\"" . $this->boundary[1] . '"');
            break;
        case 'alt':
        case 'alt_inline':
            $result .= $this->headerLine('Content-Type', 'multipart/alternative;');
            $result .= $this->textLine("\tboundary=\"" . $this->boundary[1] . '"');
            break;
        default:
            // Обрабатывает case 'plain': и case '':
            $result .= $this->textLine('Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet);
            $ismultipart = false;
            break;
    }

Видно, что проблемный случай по умолчанию выводит лишнюю строку заголовка с кодировкой и без границы. Установка типа контента через фильтр сама по себе не решает эту проблему только потому, что случай alt здесь определяется по message_type, проверяя, что AltBody не пуст, а не по типу контента.

protected function setMessageType()
{
    $type = array();
    if ($this->alternativeExists()) {
        $type[] = 'alt';
    }
    if ($this->inlineImageExists()) {
        $type[] = 'inline';
    }
    if ($this->attachmentExists()) {
        $type[] = 'attach';
    }
    $this->message_type = implode('_', $type);
    if ($this->message_type == '') {
        $this->message_type = 'plain';
    }
}

public function alternativeExists()
{
    return !empty($this->AltBody);
}

В итоге это означает, что как только вы прикрепляете файл или встраиваете изображение, или устанавливаете AltBody, проблемная ошибка должна быть обойдена. Это также означает, что нет необходимости явно устанавливать тип контента, потому что как только есть AltBody, он автоматически устанавливается в multipart/alternative через phpmailer.

Итак, простое решение:

add_action('phpmailer_init','wp_mail_set_text_body');
function wp_mail_set_text_body($phpmailer) {
     if (empty($phpmailer->AltBody)) {$phpmailer->AltBody = wp_strip_all_tags($phpmailer->Body);}
}

Тогда вам не нужно явно устанавливать заголовки, вы можете просто сделать:

 $message ='<html>
 <head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
 </head>
 <body>
     <p>Hello World! Это HTML...</p> 
 </body>
 </html>';

 wp_mail($to,$subject,$message);

К сожалению, многие функции и свойства в классе phpmailer защищены, если бы не это, допустимым вариантом было бы просто проверить и переопределить свойство MIMEHeaders через хук phpmailer_init перед отправкой.

24 мая 2016 г. 17:50:33
Комментарии

Не могу выразить достаточно благодарности за это решение, оно сработало сразу, прямо "из коробки". Если бы я был на вашем месте, я бы оставил только 'простой фрагмент ответа' в самом конце вашего поста. :)

scooterlord scooterlord
8 янв. 2021 г. 17:14:00

@scooterlord Пожалуйста, пришлось немного покопаться, чтобы разобраться с этим! Я добавил краткое решение в начале ответа для тех, кто просто хочет исправление.

majick majick
12 янв. 2021 г. 06:37:12

Разве не прекрасно, когда можно просто скопировать/вставить фрагмент кода, и он сразу работает? :)

scooterlord scooterlord
12 янв. 2021 г. 09:56:45

wp_strip_all_tags может быть лучше, чем strip_tags. Последний будет отображать содержимое тегов <style>, которые часто встречаются в HTML-письмах.

James Beninger James Beninger
14 янв. 2022 г. 18:46:32

@JamesBeninger ах, хорошее замечание! Я отредактировал ответ, чтобы изменить это.

majick majick
15 янв. 2022 г. 07:32:41
0

Для тех, кто использует хук phpmailer_init для добавления собственного 'AltBody':

Альтернативный текстовый вариант письма переиспользуется для разных последовательно отправляемых писем, если вы не очистите его вручную! WordPress не очищает его в wp_mail(), так как не ожидает использования этого свойства.

Это может привести к тому, что получатели будут получать письма, не предназначенные для них. К счастью, большинство пользователей почтовых клиентов с поддержкой HTML не увидят текстовую версию, но это всё равно представляет собой проблему безопасности.

К счастью, есть простое решение. Включает замену альтернативного текста; обратите внимание, что вам понадобится библиотека Html2Text для PHP:

add_filter( 'wp_mail', 'wpse191923_force_phpmailer_reinit_for_multiple_mails', -1 );
function wpse191923_force_phpmailer_reinit_for_multiple_mails( $wp_mail_atts ) {
  global $phpmailer;

  if ( $phpmailer instanceof PHPMailer && $phpmailer->alternativeExists() ) {
    // Свойство AltBody установлено, значит WordPress уже использовал этот
    // объект $phpmailer для отправки письма, поэтому очистим
    // свойство AltBody
    $phpmailer->AltBody = '';
  }

  // Возвращаем нетронутые атрибуты
  return $wp_mail_atts;
}

add_action( 'phpmailer_init', 'wpse191923_phpmailer_init_altbody', 1000, 1 );
function wpse191923_phpmailer_init_altbody( $phpmailer ) {
  if ( ( $phpmailer->ContentType == 'text/html' ) && empty( $phpmailer->AltBody ) ) {
    if ( ! class_exists( 'Html2Text\Html2Text' ) ) {
      require_once( 'Html2Text.php' );
    }
    if ( ! class_exists( 'Html2Text\Html2TextException' ) ) {
      require_once( 'Html2TextException.php' );
    }
    $phpmailer->AltBody = Html2Text\Html2Text::convert( $phpmailer->Body );
  }
}

Также доступен gist с модифицированным плагином для WP, который исправляет эту проблему: https://gist.github.com/youri--/c4618740b7c50c549314eaebc9f78661

К сожалению, я не могу прокомментировать другие решения, использующие упомянутый хук, чтобы предупредить их об этой проблеме, так как у меня пока недостаточно репутации для комментирования.

16 окт. 2018 г. 14:33:36
0

Я только что выпустил плагин, позволяющий пользователям использовать HTML-шаблоны в WordPress, и сейчас работаю над dev-версией, чтобы добавить простой текстовый фолбэк. Я сделал следующее, и в моих тестах добавляется только одна граница, а письма нормально доходят до Hotmail.

add_action( 'phpmailer_init', array($this->mailer, 'send_email' ) );

/**
* Модифицирует тело письма в php mailer финальной версией email
*
* @since 1.0.0
* @param object $phpmailer
*/
function send_email( $phpmailer ) {

    $message            =  $this->add_template( apply_filters( 'mailtpl/email_content', $phpmailer->Body ) );
    $phpmailer->AltBody =  $this->replace_placeholders( strip_tags($phpmailer->Body) );
    $phpmailer->Body    =  $this->replace_placeholders( $message );
}

По сути, здесь я модифицирую объект phpmailer, загружаю сообщение в HTML-шаблон и устанавливаю его в свойство Body. Также я беру оригинальное сообщение и устанавливаю свойство AltBody.

10 июл. 2015 г. 21:43:30
0

Моё простое решение — использовать html2text https://github.com/soundasleep/html2text следующим образом:

add_action( 'phpmailer_init', 'phpmailer_init' );

//http://wordpress.stackexchange.com/a/191974
//http://stackoverflow.com/a/2564472
function phpmailer_init( $phpmailer )
{
  if( $phpmailer->ContentType == 'text/html' ) {
    $phpmailer->AltBody = Html2Text\Html2Text::convert( $phpmailer->Body );
  }
}

Также доступен Gist по теме: https://gist.github.com/frugan-it/6c4d22cd856456480bd77b988b5c9e80.

11 окт. 2016 г. 13:50:18
0

Это может не быть точным ответом на изначальный пост, но это альтернатива некоторым решениям, предложенным здесь, касающимся установки альтернативного тела письма.

По сути, мне нужно было (и я хотел) установить отдельное альтернативное тело письма (т.е. обычный текст) в дополнение к HTML-части, вместо того чтобы полагаться на какое-то преобразование/striptags и тому подобное.

Вот что я придумал, и это, кажется, работает отлично:

/* установка частей сообщения для wp_mail() */
$markup = array();
$markup['html'] = '<html>некоторый html</html>';
$markup['plaintext'] = 'некоторый обычный текст';
/* сообщение, которое мы отправляем */    
$message = maybe_serialize($markup);


/* установка альтернативного тела письма отдельно */
add_action('phpmailer_init', array($this, 'set_alt_mail_body'));

function set_alt_mail_body($phpmailer){
    if( $phpmailer->ContentType == 'text/html' ) {
        $body_parts = maybe_unserialize($phpmailer->Body);

        if(!empty($body_parts['html'])){
            $phpmailer->MsgHTML($body_parts['html']);
        }

        if(!empty($body_parts['plaintext'])){
            $phpmailer->AltBody = $body_parts['plaintext'];
        }
    }   
}
25 янв. 2017 г. 16:56:04
1

Эта версия функции wp_mail() основана на коде @bonger. Внесены следующие изменения:

  • Исправления стиля кода (через PHPCS)
  • Обработка случаев, когда $message является массивом или строкой (обеспечивает совместимость с WP 5.x)
  • Выбрасывание исключения вместо возврата false
  • Короткий синтаксис массивов
<?php
/**
 * Адаптировано из https://wordpress.stackexchange.com/a/191974/8591
 *
 * Отправка почты, аналогично PHP-функции mail()
 *
 * Возврат true не означает, что пользователь успешно получил письмо.
 * Это лишь указывает на то, что метод смог обработать запрос без ошибок.
 *
 * Использование хуков 'wp_mail_from' и 'wp_mail_from_name' позволяет
 * создать адрес отправителя в формате 'Имя <email@address.com>', если оба установлены.
 * Если задан только 'wp_mail_from', будет использован только email без имени.
 *
 * По умолчанию тип контента - 'text/plain', что не позволяет использовать HTML.
 * Однако можно изменить тип контента письма с помощью фильтра 'wp_mail_content_type'.
 *
 * Если $message является массивом, ключ каждого элемента используется для добавления вложения,
 * а значение - как тело письма. Элемент 'text/plain' используется как текстовая версия тела,
 * а 'text/html' - как HTML-версия. Все остальные типы добавляются как вложения.
 *
 * Кодировка по умолчанию основана на кодировке блога. Её можно изменить
 * с помощью фильтра 'wp_mail_charset'.
 *
 * @since 1.2.1
 *
 * @uses PHPMailer
 *
 * @param string|array $to Массив или список email-адресов через запятую для отправки.
 * @param string $subject Тема письма
 * @param string|array $message Содержимое письма
 * @param string|array $headers Дополнительные заголовки. Необязательно.
 * @param string|array $attachments Файлы для прикрепления. Необязательно.
 * @return bool Было ли письмо успешно отправлено.
 */
public static function wp_mail( $to, $subject, $message, $headers = '', $attachments = [] ) {
    // Компактируем входные данные, применяем фильтры и извлекаем обратно

    /**
     * Фильтр аргументов wp_mail().
     *
     * @since 2.2.0
     *
     * @param array $args Упакованный массив аргументов wp_mail(): "to", "subject", 
     *                    "message", "headers" и "attachments".
     */
    $atts = apply_filters( 'wp_mail', compact( 'to', 'subject', 'headers', 'attachments' ) );

    // Поскольку $message - массив, а wp_staticize_emoji_for_email() ожидает строки, обрабатываем каждый элемент отдельно
    if ( ! is_array( $message ) ) {
        $message = [ $message ];
    }
    foreach ( $message as $message_part ) {
        $message_part = apply_filters( 'wp_mail', $message_part );
    }
    $atts['message'] = $message;

    if ( isset( $atts['to'] ) ) {
        $to = $atts['to'];
    }

    if ( isset( $atts['subject'] ) ) {
        $subject = $atts['subject'];
    }

    if ( isset( $atts['message'] ) ) {
        $message = $atts['message'];
    }

    if ( isset( $atts['headers'] ) ) {
        $headers = $atts['headers'];
    }

    if ( isset( $atts['attachments'] ) ) {
        $attachments = $atts['attachments'];
    }

    if ( ! is_array( $attachments ) ) {
        $attachments = explode( "\n", str_replace( "\r\n", "\n", $attachments ) );
    }
    global $phpmailer;

    // (Пере)создаём, если отсутствует
    if ( ! ( $phpmailer instanceof PHPMailer ) ) {
        require_once ABSPATH . WPINC . '/class-phpmailer.php';
        require_once ABSPATH . WPINC . '/class-smtp.php';
        $phpmailer = new PHPMailer( true ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
    }

    // Заголовки
    if ( empty( $headers ) ) {
        $headers = [];
    } else {
        if ( ! is_array( $headers ) ) {
            // Разбиваем заголовки, чтобы функция могла принимать как строку, так и массив
            $tempheaders = explode( "\n", str_replace( "\r\n", "\n", $headers ) );
        } else {
            $tempheaders = $headers;
        }
        $headers = [];
        $cc      = [];
        $bcc     = [];

        // Если есть содержимое
        if ( ! empty( $tempheaders ) ) {
            // Обрабатываем каждый заголовок
            foreach ( (array) $tempheaders as $header ) {
                if ( strpos( $header, ':' ) === false ) {
                    if ( false !== stripos( $header, 'boundary=' ) ) {
                        $parts    = preg_split( '/boundary=/i', trim( $header ) );
                        $boundary = trim( str_replace( [ "'", '"' ], '', $parts[1] ) );
                    }
                    continue;
                }
                // Разделяем имя и содержимое
                list( $name, $content ) = explode( ':', trim( $header ), 2 );

                // Очистка
                $name    = trim( $name );
                $content = trim( $content );

                switch ( strtolower( $name ) ) {
                    // В основном для обратной совместимости - обработка From:
                    case 'from':
                        $bracket_pos = strpos( $content, '<' );
                        if ( false !== $bracket_pos ) {
                            // Текст до email в угловых скобках - имя отправителя
                            if ( $bracket_pos > 0 ) {
                                $from_name = substr( $content, 0, $bracket_pos - 1 );
                                $from_name = str_replace( '"', '', $from_name );
                                $from_name = trim( $from_name );
                            }

                            $from_email = substr( $content, $bracket_pos + 1 );
                            $from_email = str_replace( '>', '', $from_email );
                            $from_email = trim( $from_email );

                            // Не устанавливаем пустой $from_email
                        } elseif ( '' !== trim( $content ) ) {
                            $from_email = trim( $content );
                        }
                        break;
                    case 'content-type':
                        if ( is_array( $message ) ) {
                            // Многочастное письмо, игнорируем content-type
                            break;
                        }
                        if ( strpos( $content, ';' ) !== false ) {
                            list( $type, $charset_content ) = explode( ';', $content );
                            $content_type                   = trim( $type );
                            if ( false !== stripos( $charset_content, 'charset=' ) ) {
                                $charset = trim( str_replace( [ 'charset=', '"' ], '', $charset_content ) );
                            } elseif ( false !== stripos( $charset_content, 'boundary=' ) ) {
                                $boundary = trim( str_replace( [ 'BOUNDARY=', 'boundary=', '"' ], '', $charset_content ) );
                                $charset  = '';
                            }

                            // Не устанавливаем пустой $content_type
                        } elseif ( '' !== trim( $content ) ) {
                            $content_type = trim( $content );
                        }
                        break;
                    case 'cc':
                        $cc = array_merge( (array) $cc, explode( ',', $content ) );
                        break;
                    case 'bcc':
                        $bcc = array_merge( (array) $bcc, explode( ',', $content ) );
                        break;
                    default:
                        // Добавляем в общий массив заголовков
                        $headers[ trim( $name ) ] = trim( $content );
                        break;
                }
            }
        }
    }

    // Очищаем возможные предыдущие значения
    $phpmailer->ClearAllRecipients();
    $phpmailer->ClearAttachments();
    $phpmailer->ClearCustomHeaders();
    $phpmailer->ClearReplyTos();

    $phpmailer->Body    = ''; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
    $phpmailer->AltBody = ''; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase

    // Email и имя отправителя
    // Если имя не задано в заголовках
    if ( ! isset( $from_name ) ) {
        $from_name = 'WordPress';
    }

    /* Если email не задан в заголовках, используем wordpress@$sitename
     * Некоторые хосты блокируют исходящую почту с этого адреса, если он не существует,
     * но альтернативы нет. Использование admin_email может показаться вариантом,
     * но некоторые хосты могут отказать в пересылке с неизвестного домена.
     * См. https://core.trac.wordpress.org/ticket/5007.
     */

    if ( ! isset( $from_email ) ) {
        // Получаем домен сайта без www
        $sitename = isset( $_SERVER['SERVER_NAME'] ) ? strtolower( sanitize_text_field( wp_unslash( $_SERVER['SERVER_NAME'] ) ) ) : ''; // phpcs:ignore WordPress.VIP.SuperGlobalInputUsage.AccessDetected
        if ( substr( $sitename, 0, 4 ) === 'www.' ) {
            $sitename = substr( $sitename, 4 );
        }

        $from_email = 'wordpress@' . $sitename;
    }

    /**
     * Фильтр email-адреса отправителя.
     *
     * @since 2.2.0
     *
     * @param string $from_email Email для отправки.
     */
    $phpmailer->From = apply_filters( 'wp_mail_from', $from_email ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase

    /**
     * Фильтр имени отправителя.
     *
     * @since 2.3.0
     *
     * @param string $from_name Имя отправителя.
     */
    $phpmailer->FromName = apply_filters( 'wp_mail_from_name', $from_name ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase

    // Устанавливаем адреса получателей
    if ( ! is_array( $to ) ) {
        $to = explode( ',', $to );
    }

    foreach ( (array) $to as $recipient ) {
        try {
            // Разделяем имя и адрес, если формат "Имя <email@example.com>"
            $recipient_name = '';
            if ( preg_match( '/(.*)<(.+)>/', $recipient, $matches ) ) {
                if ( count( $matches ) === 3 ) {
                    $recipient_name = $matches[1];
                    $recipient      = $matches[2];
                }
            }
            $phpmailer->AddAddress( $recipient, $recipient_name );
        } catch ( phpmailerException $e ) {
            continue;
        }
    }

    // Если кодировка не задана в заголовках
    if ( ! isset( $charset ) ) {
        $charset = get_bloginfo( 'charset' );
    }

    // Устанавливаем тип контента и кодировку

    /**
     * Фильтр кодировки по умолчанию для wp_mail().
     *
     * @since 2.3.0
     *
     * @param string $charset Кодировка письма.
     */
    $phpmailer->CharSet = apply_filters( 'wp_mail_charset', $charset ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase

    // Устанавливаем тему и тело письма
    $phpmailer->Subject = $subject; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase

    if ( is_string( $message ) ) {
        $phpmailer->Body = $message; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase

        // Устанавливаем Content-Type и кодировку
        // Если тип контента не задан в заголовках
        if ( ! isset( $content_type ) ) {
            $content_type = 'text/plain';
        }

        /**
         * Фильтр типа контента для wp_mail().
         *
         * @since 2.3.0
         *
         * @param string $content_type Тип контента.
         */
        $content_type = apply_filters( 'wp_mail_content_type', $content_type );

        $phpmailer->ContentType = $content_type; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase

        // Устанавливаем plaintext в зависимости от $content_type
        if ( 'text/html' === $content_type ) {
            $phpmailer->IsHTML( true );
        }

        // Для обратной совместимости, новые многочастные письма должны использовать
        // массив $message. Это никогда не работало хорошо
        if ( false !== stripos( $content_type, 'multipart' ) && ! empty( $boundary ) ) {
            $phpmailer->AddCustomHeader( sprintf( "Content-Type: %s;\n\t boundary=\"%s\"", $content_type, $boundary ) );
        }
    } elseif ( is_array( $message ) ) {
        foreach ( $message as $type => $bodies ) {
            foreach ( (array) $bodies as $body ) {
                if ( 'text/html' === $type ) {
                    $phpmailer->Body = $body; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
                } elseif ( 'text/plain' === $type ) {
                    $phpmailer->AltBody = $body; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
                } else {
                    $phpmailer->AddAttachment( $body, '', 'base64', $type );
                }
            }
        }
    }

    // Добавляем CC и BCC получателей
    if ( ! empty( $cc ) ) {
        foreach ( (array) $cc as $recipient ) {
            try {
                // Разделяем имя и адрес, если формат "Имя <email@example.com>"
                $recipient_name = '';
                if ( preg_match( '/(.*)<(.+)>/', $recipient, $matches ) ) {
                    if ( count( $matches ) === 3 ) {
                        $recipient_name = $matches[1];
                        $recipient      = $matches[2];
                    }
                }
                $phpmailer->AddCc( $recipient, $recipient_name );
            } catch ( phpmailerException $e ) {
                continue;
            }
        }
    }

    if ( ! empty( $bcc ) ) {
        foreach ( (array) $bcc as $recipient ) {
            try {
                // Разделяем имя и адрес, если формат "Имя <email@example.com>"
                $recipient_name = '';
                if ( preg_match( '/(.*)<(.+)>/', $recipient, $matches ) ) {
                    if ( count( $matches ) === 3 ) {
                        $recipient_name = $matches[1];
                        $recipient      = $matches[2];
                    }
                }
                $phpmailer->AddBcc( $recipient, $recipient_name );
            } catch ( phpmailerException $e ) {
                continue;
            }
        }
    }

    // Используем PHP mail()
    $phpmailer->IsMail();

    // Устанавливаем пользовательские заголовки
    if ( ! empty( $headers ) ) {
        foreach ( (array) $headers as $name => $content ) {
            $phpmailer->AddCustomHeader( sprintf( '%1$s: %2$s', $name, $content ) );
        }
    }

    if ( ! empty( $attachments ) ) {
        foreach ( $attachments as $attachment ) {
            try {
                $phpmailer->AddAttachment( $attachment );
            } catch ( phpmailerException $e ) {
                continue;
            }
        }
    }

    /**
     * Действие после инициализации PHPMailer.
     *
     * @since 2.2.0
     *
     * @param PHPMailer &$phpmailer Экземпляр PHPMailer, передаётся по ссылке.
     */
    do_action_ref_array( 'phpmailer_init', [ &$phpmailer ] );

    // Отправляем!
    try {
        return $phpmailer->Send();
    } catch ( phpmailerException $e ) {
        return new WP_Error( 'email-error', $e->getMessage() );
    }
}
19 февр. 2020 г. 23:27:45
Комментарии

Привет, Пол. Короткий синтаксис массивов появился задолго до PHP 7+ ;)

kaiser kaiser
19 февр. 2020 г. 23:37:39
0

Если вы не хотите создавать конфликты в коде ядра WordPress, я думаю, что альтернативным или самым простым решением будет добавить действие к хуку phpmailer_init, которое выполнится перед фактической отправкой письма в функции wp_mail(). Чтобы упростить объяснение, посмотрите на пример кода ниже:

<?php 

$to = '';
$subject = '';
$from = '';
$body = 'Текст HTML-содержимого, <html>...';

$headers = "FROM: {$from}";

add_action( 'phpmailer_init', function ( $phpmailer ) {
    $phpmailer->AltBody = 'Текстовая версия вашего оригинального HTML-содержимого.';
} );

wp_mail($to, $subject, $body, $headers);

Если вы добавите содержимое в свойство AltBody класса PHPMailer, то тип содержимого по умолчанию автоматически установится в multipart/alternative.

18 сент. 2018 г. 08:24:56
0

Внимательно изучил реализацию функции wp_mail($to, $subject, $message, $headers, $attachments) в файле pluggable.php и нашел решение, которое не требует изменения ядра WordPress.

Функция wp_mail() проверяет аргумент $headers на наличие стандартных типов заголовков, а именно: from, content-type, cc, bcc и reply-to.

Все остальные типы считаются пользовательскими заголовками и обрабатываются отдельно. Но вот в чем дело: когда определяется пользовательский заголовок (как в вашем случае с заголовком MIME-Version), выполняется следующий блок кода (внутри wp_mail()):

// Установка пользовательских заголовков
if ( ! empty( $headers ) ) {
    foreach ( (array) $headers as $name => $content ) {
        $phpmailer->addCustomHeader( sprintf( '%1$s: %2$s', $name, $content ) );
    }

    if ( false !== stripos( $content_type, 'multipart' ) && ! empty( $boundary ) ) {
        $phpmailer->addCustomHeader( sprintf( "Content-Type: %s;\n\t boundary=\"%s\"", $content_type, $boundary ) );
    }
}

Вложенное условие if в приведенном выше фрагменте и является причиной проблемы. По сути, дополнительный заголовок Content-Type добавляется как пользовательский при выполнении следующих условий:

  1. Определен пользовательский заголовок (в вашем случае это MIME-Version).
  2. MIME-тип заголовка Content-Type содержит строку multipart.
  3. Установлена граница для многочастного сообщения (boundary).

Самый быстрый способ исправить ситуацию в вашем случае — удалить заголовок MIME-Version. Большинство почтовых клиентов автоматически добавляют этот заголовок, поэтому его удаление не должно вызвать проблем.

Но что, если вам нужно добавить пользовательские заголовки без создания дублирующего заголовка Content-Type?

РЕШЕНИЕ: НЕ указывайте явно заголовок Content-Type в массиве $headers при добавлении пользовательских заголовков. Вместо этого сделайте следующее:

$headers = 'boundary="----=_Part_18243133_1346573420.1408991447668"\r\n';
$headers .= "MIME-Version: 1.0\r\n";
$headers .= "From: Foo <foo@bar.com>\r\n";

function set_content_type( $content_type ) {
    return 'multipart/alternative';
}

function set_charset( $char_set ) {
    return 'utf-8';
}

add_filter( 'wp_mail_content_type', 'set_content_type' );
add_filter( 'wp_mail_charset', 'set_charset' );

Первая строка в приведенном выше фрагменте может показаться странной, но функция wp_mail() внутренне установит свою переменную $boundary, если определение границы указано на отдельной строке без префикса Content-Type:. Затем вы можете использовать фильтры для установки content-type и charset соответственно. Таким образом, вы удовлетворяете условиям выполнения блока кода для установки пользовательских заголовков без явного добавления Content-Type: [mime-type]; [boundary];.

Нет необходимости изменять реализацию wp_mail() в ядре, несмотря на ее недостатки.

20 дек. 2020 г. 00:41:41