Отправка многосоставных (text/html) писем через wp_mail() может привести к блокировке вашего домена
Краткое описание
Из-за ошибки в 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
может быть строкой.

Следующая версия функции 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 );
Обратите внимание, я не тестировал этот код с реальными письмами...

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

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

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

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

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

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

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

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

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

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

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

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

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
перед отправкой.

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

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

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

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

Для тех, кто использует хук 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
К сожалению, я не могу прокомментировать другие решения, использующие упомянутый хук, чтобы предупредить их об этой проблеме, так как у меня пока недостаточно репутации для комментирования.

Я только что выпустил плагин, позволяющий пользователям использовать 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.

Моё простое решение — использовать 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.

Это может не быть точным ответом на изначальный пост, но это альтернатива некоторым решениям, предложенным здесь, касающимся установки альтернативного тела письма.
По сути, мне нужно было (и я хотел) установить отдельное альтернативное тело письма (т.е. обычный текст) в дополнение к 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'];
}
}
}

Эта версия функции 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() );
}
}

Если вы не хотите создавать конфликты в коде ядра 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
.

Внимательно изучил реализацию функции 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
добавляется как пользовательский при выполнении следующих условий:
- Определен пользовательский заголовок (в вашем случае это
MIME-Version
). - MIME-тип заголовка
Content-Type
содержит строкуmultipart
. - Установлена граница для многочастного сообщения (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()
в ядре, несмотря на ее недостатки.
