El envío de correos multipart (text/html) vía wp_mail() probablemente hará que tu dominio sea bloqueado
Resumen
Debido a un error en el núcleo de WP, enviar correos multipart (html/text) con wp_mail() (para reducir la probabilidad de que los correos terminen en carpetas de spam) irónicamente resultará en que tu dominio sea bloqueado por Hotmail (y otros correos de Microsoft).
Este es un problema complejo que intentaré desglosar en gran detalle para ayudar a encontrar una solución viable que eventualmente pueda implementarse en el núcleo.
Será una lectura gratificante. Empecemos...
El error
El consejo más común para evitar que tus correos de newsletter terminen en carpetas de spam es enviar mensajes multipart.
Multi-part (mime) se refiere a enviar tanto una parte HTML como TEXT de un mensaje de correo en un solo email. Cuando un cliente recibe un mensaje multipart, acepta la versión HTML si puede renderizar HTML, de lo contrario presenta la versión en texto plano.
Está comprobado que funciona. Al enviar a Gmail, todos nuestros correos iban a carpetas de spam hasta que cambiamos los mensajes a multipart cuando llegaron a la bandeja principal. Excelente.
Ahora, cuando se envían mensajes multipart a través de wp_mail(), genera el Content Type (multipart/*) dos veces, una con boundary (si se establece personalmente) y otra sin él. Este comportamiento hace que el correo se muestre como un mensaje sin formato y no multipart en algunos clientes de correo, incluyendo todos los de Microsoft (Hotmail, Outlook, etc...)
Microsoft marcará este mensaje como basura, y los pocos mensajes que llegan serán marcados manualmente por el destinatario. Desafortunadamente, las direcciones de correo de Microsoft son ampliamente utilizadas. El 40% de nuestros suscriptores las usan.
Esto fue confirmado por Microsoft a través de un intercambio de correos que tuvimos recientemente.
El marcado de los mensajes resultará en que el dominio sea completamente bloqueado. Esto significa que los mensajes no serán enviados a la carpeta de spam, ni siquiera serán entregados al destinatario.
Hemos tenido nuestro dominio principal bloqueado 3 veces hasta ahora.
Debido a que este es un error en el núcleo de WP, todos los dominios que envían mensajes multipart están siendo bloqueados. El problema es que la mayoría de los webmasters no saben por qué. He confirmado esto al hacer mi investigación y ver a otros usuarios discutiendo esto en foros, etc. Requiere profundizar en el código crudo y tener un buen conocimiento de cómo funcionan este tipo de mensajes de correo, lo cual veremos a continuación...
Vamos a desglosarlo en código
Crea una cuenta de hotmail/outlook. Luego, ejecuta el siguiente código:
// Establece $to a un correo hotmail.com u outlook.com
$to = "TuCorreo@hotmail.com";
$subject = 'Prueba wp_mail multipart';
$message = '------=_Part_18243133_1346573420.1408991447668
Content-Type: text/plain; charset=UTF-8
¡Hola mundo! Esto es texto plano...
------=_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>¡Hola Mundo! Esto es 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"';
// enviar email
wp_mail( $to, $subject, $message, $headers );
Y si quieres cambiar el tipo de contenido predeterminado, usa:
add_filter( 'wp_mail_content_type', 'set_content_type' );
function set_content_type( $content_type ) {
return 'multipart/alternative';
}
Esto enviará un mensaje multipart.
Si verificas el código fuente completo del mensaje, notarás que el tipo de contenido se agrega dos veces, una vez sin boundary:
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary="====f230673f9d7c359a81ffebccb88e5d61=="
MIME-Version: 1.0
Content-Type: multipart/alternative; charset=
Ese es el problema.
El origen del problema está en pluggable.php
- si miramos por aquí:
// Establecer Content-Type y charset
// Si no tenemos un content-type de los headers de entrada
if ( !isset( $content_type ) )
$content_type = 'text/plain';
/**
* Filtrar el content type de wp_mail().
*
* @since 2.3.0
*
* @param string $content_type Content type predeterminado de wp_mail().
*/
$content_type = apply_filters( 'wp_mail_content_type', $content_type );
$phpmailer->ContentType = $content_type;
// Establecer si es texto plano, dependiendo de $content_type
if ( 'text/html' == $content_type )
$phpmailer->IsHTML( true );
// Si no tenemos un charset de los headers de entrada
if ( !isset( $charset ) )
$charset = get_bloginfo( 'charset' );
// Establecer el content-type y charset
/**
* Filtrar el charset predeterminado de wp_mail().
*
* @since 2.3.0
*
* @param string $charset Charset predeterminado del email.
*/
$phpmailer->CharSet = apply_filters( 'wp_mail_charset', $charset );
// Establecer headers personalizados
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;
}
}
}
Soluciones potenciales
Así que te preguntarás, ¿por qué no has reportado esto en trac? Ya lo he hecho. Para mi gran sorpresa, se creó un ticket diferente hace 5 años describiendo el mismo problema.
Seamos realistas, han pasado cinco años. En años de internet, eso es más como 30. El problema claramente ha sido abandonado y básicamente nunca será arreglado (...a menos que lo resolvamos aquí).
Encontré un excelente hilo aquí ofreciendo una solución, pero aunque su solución funciona, rompe los correos que no tienen $headers
personalizados establecidos.
Ahí es donde nos estrellamos cada vez. O la versión multipart funciona bien y los mensajes normales sin $headers
establecidos no, o viceversa.
La solución que desarrollamos fue:
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;
// Establecer si es texto plano, dependiendo de $content_type
if ( 'text/html' == $content_type )
$phpmailer->IsHTML( true );
// Si no tenemos un charset de los headers de entrada
if ( !isset( $charset ) )
$charset = get_bloginfo( 'charset' );
}
// Establecer el content-type y charset
/**
* Filtrar el charset predeterminado de wp_mail().
*
* @since 2.3.0
*
* @param string $charset Charset predeterminado del email.
*/
$phpmailer->CharSet = apply_filters( 'wp_mail_charset', $charset );
// Establecer headers personalizados
if ( !empty( $headers ) ) {
foreach( (array) $headers as $name => $content ) {
$phpmailer->AddCustomHeader( sprintf( '%1$s: %2$s', $name, $content ) );
}
}
Sí, lo sé, editar archivos del núcleo es tabú, siéntate... esta fue una solución desesperada y un pobre intento de proporcionar una corrección para el núcleo.
El problema con nuestra solución es que los correos predeterminados como nuevos registros, comentarios, restablecimiento de contraseña, etc. se entregarán como mensajes en blanco. Así que tenemos un script wp_mail() funcional que enviará mensajes multipart pero nada más.
Qué hacer
El objetivo aquí es encontrar una manera de enviar tanto mensajes normales (texto plano) como multipart usando la función wp_mail() del núcleo (no una función sendmail personalizada).
Al intentar resolver esto, el principal problema que encontrarás es la cantidad de tiempo que pasarás enviando mensajes de prueba, verificando si se reciben y básicamente abriendo una caja de aspirinas y maldiciendo a Microsoft porque estás acostumbrado a sus problemas con IE mientras que el gremlim aquí es desafortunadamente WordPress.
Actualización
La solución publicada por @bonger permite que $message
sea un array que contiene alternativas con claves de tipo de contenido. He confirmado que funciona en todos los escenarios.
Permitiremos que esta pregunta permanezca abierta hasta que se agote la recompensa para crear conciencia sobre el problema, tal vez a un nivel donde se arreglará en el núcleo. Siéntete libre de publicar una solución alternativa donde $message
pueda ser una cadena.

La siguiente versión de wp_mail()
incluye el parche aplicado por @rmccue/@MattyRob en el ticket https://core.trac.wordpress.org/ticket/15448, actualizado para 4.2.2, que permite que $message
sea un array que contenga alternativas con claves de tipo de contenido:
/**
* Envía correo, similar a la función mail de PHP
*
* Un valor de retorno verdadero no significa automáticamente que el usuario recibió el
* correo electrónico exitosamente. Solo significa que el método utilizado pudo
* procesar la solicitud sin errores.
*
* Usar los hooks 'wp_mail_from' y 'wp_mail_from_name' permiten crear una
* dirección de origen como 'Nombre <email@address.com>' cuando ambos están configurados. Si
* solo 'wp_mail_from' está configurado, entonces solo se usará la dirección de correo sin
* nombre.
*
* El tipo de contenido predeterminado es 'text/plain' que no permite usar HTML.
* Sin embargo, puedes configurar el tipo de contenido del correo usando el
* filtro 'wp_mail_content_type'.
*
* Si $message es un array, la clave de cada elemento se usa para agregar como adjunto
* con el valor usado como cuerpo. El elemento 'text/plain' se usa como la
* versión de texto del cuerpo, y el elemento 'text/html' se usa como la versión HTML
* del cuerpo. Todos los otros tipos se agregan como adjuntos.
*
* El charset predeterminado se basa en el charset usado en el blog. El charset puede
* configurarse usando el filtro 'wp_mail_charset'.
*
* @since 1.2.1
*
* @uses PHPMailer
*
* @param string|array $to Array o lista separada por comas de direcciones de correo a enviar.
* @param string $subject Asunto del correo
* @param string|array $message Contenido del mensaje
* @param string|array $headers Opcional. Cabeceras adicionales.
* @param string|array $attachments Opcional. Archivos para adjuntar.
* @return bool Si el contenido del correo fue enviado exitosamente.
*/
function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() ) {
// Compacta la entrada, aplica los filtros y extrae de nuevo
/**
* Filtra los argumentos de wp_mail().
*
* @since 2.2.0
*
* @param array $args Array compactado de argumentos de wp_mail(), incluyendo los valores "to",
* subject, message, headers y 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;
// (Re)crea si ha desaparecido
if ( ! ( $phpmailer instanceof PHPMailer ) ) {
require_once ABSPATH . WPINC . '/class-phpmailer.php';
require_once ABSPATH . WPINC . '/class-smtp.php';
$phpmailer = new PHPMailer( true );
}
// Cabeceras
if ( empty( $headers ) ) {
$headers = array();
} else {
if ( !is_array( $headers ) ) {
// Explode las cabeceras para que esta función pueda tomar tanto
// cabeceras string como un array de cabeceras.
$tempheaders = explode( "\n", str_replace( "\r\n", "\n", $headers ) );
} else {
$tempheaders = $headers;
}
$headers = array();
$cc = array();
$bcc = array();
// Si realmente tiene contenido
if ( !empty( $tempheaders ) ) {
// Itera a través de las cabeceras crudas
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;
}
// Explode
list( $name, $content ) = explode( ':', trim( $header ), 2 );
// Limpieza
$name = trim( $name );
$content = trim( $content );
switch ( strtolower( $name ) ) {
// Principalmente por legado - procesa cabecera From: si está presente
case 'from':
$bracket_pos = strpos( $content, '<' );
if ( $bracket_pos !== false ) {
// Texto antes del correo entre corchetes es el nombre "From".
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 );
// Evita configurar un $from_email vacío.
} elseif ( '' !== trim( $content ) ) {
$from_email = trim( $content );
}
break;
case 'content-type':
if ( is_array($message) ) {
// Correo multiparte, ignora cabecera 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 = '';
}
// Evita configurar un $content_type vacío.
} 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:
// Agrégalo a nuestro gran array de cabeceras
$headers[trim( $name )] = trim( $content );
break;
}
}
}
}
// Vacía los valores que pueden estar configurados
$phpmailer->ClearAllRecipients();
$phpmailer->ClearAttachments();
$phpmailer->ClearCustomHeaders();
$phpmailer->ClearReplyTos();
$phpmailer->Body= '';
$phpmailer->AltBody= '';
// Correo y nombre de origen
// Si no tenemos un nombre de las cabeceras de entrada
if ( !isset( $from_name ) )
$from_name = 'WordPress';
/* Si no tenemos un correo de las cabeceras de entrada, por defecto wordpress@$sitename
* Algunos hosts bloquearán correos salientes de esta dirección si no existe pero
* no hay alternativa fácil. Usar admin_email podría parecer otra
* opción pero algunos hosts pueden rechazar retransmitir correo de un dominio desconocido. Ver
* https://core.trac.wordpress.org/ticket/5007.
*/
if ( !isset( $from_email ) ) {
// Obtiene el dominio del sitio y elimina www.
$sitename = strtolower( $_SERVER['SERVER_NAME'] );
if ( substr( $sitename, 0, 4 ) == 'www.' ) {
$sitename = substr( $sitename, 4 );
}
$from_email = 'wordpress@' . $sitename;
}
/**
* Filtra la dirección de correo de origen.
*
* @since 2.2.0
*
* @param string $from_email Dirección de correo de origen.
*/
$phpmailer->From = apply_filters( 'wp_mail_from', $from_email );
/**
* Filtra el nombre asociado a la dirección de correo "from".
*
* @since 2.3.0
*
* @param string $from_name Nombre asociado a la dirección de correo "from".
*/
$phpmailer->FromName = apply_filters( 'wp_mail_from_name', $from_name );
// Configura direcciones de destino
if ( !is_array( $to ) )
$to = explode( ',', $to );
foreach ( (array) $to as $recipient ) {
try {
// Divide $recipient en nombre y dirección si está en formato "Foo <bar@baz.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;
}
}
// Si no tenemos un charset de las cabeceras de entrada
if ( !isset( $charset ) )
$charset = get_bloginfo( 'charset' );
// Configura content-type y charset
/**
* Filtra el charset predeterminado de wp_mail().
*
* @since 2.3.0
*
* @param string $charset Charset predeterminado del correo.
*/
$phpmailer->CharSet = apply_filters( 'wp_mail_charset', $charset );
// Configura asunto y cuerpo del correo
$phpmailer->Subject = $subject;
if ( is_string($message) ) {
$phpmailer->Body = $message;
// Configura Content-Type y charset
// Si no tenemos content-type de las cabeceras de entrada
if ( !isset( $content_type ) )
$content_type = 'text/plain';
/**
* Filtra el tipo de contenido de wp_mail().
*
* @since 2.3.0
*
* @param string $content_type Tipo de contenido predeterminado de wp_mail().
*/
$content_type = apply_filters( 'wp_mail_content_type', $content_type );
$phpmailer->ContentType = $content_type;
// Configura si es texto plano, dependiendo de $content_type
if ( 'text/html' == $content_type )
$phpmailer->IsHTML( true );
// Por compatibilidad hacia atrás, nuevos correos multiparte deberían usar
// el estilo array para $message. Esto nunca funcionó bien de todos modos
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);
}
}
}
}
// Agrega cualquier CC y BCC
if ( !empty( $cc ) ) {
foreach ( (array) $cc as $recipient ) {
try {
// Divide $recipient en nombre y dirección si está en formato "Foo <bar@baz.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 {
// Divide $recipient en nombre y dirección si está en formato "Foo <bar@baz.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;
}
}
}
// Configura para usar mail() de PHP
$phpmailer->IsMail();
// Configura cabeceras personalizadas
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;
}
}
}
/**
* Se ejecuta después de inicializar PHPMailer.
*
* @since 2.2.0
*
* @param PHPMailer &$phpmailer Instancia PHPMailer, pasada por referencia.
*/
do_action_ref_array( 'phpmailer_init', array( &$phpmailer ) );
// ¡Envía!
try {
return $phpmailer->Send();
} catch ( phpmailerException $e ) {
return false;
}
}
Si colocas esto en tu archivo, por ejemplo en "wp-content/mu-plugins/functions.php", entonces sobrescribirá la versión de WP. Tiene un uso agradable sin necesidad de manipular cabeceras, por ejemplo:
// Configura $to con un correo de hotmail.com u outlook.com
$to = "TuCorreo@hotmail.com";
$subject = 'Prueba de multiparte con wp_mail';
$message['text/plain'] = '¡Hola mundo! Esto es texto plano...';
$message['text/html'] = '<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
<p>¡Hola mundo! Esto es 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'; } );
// envía el correo
wp_mail( $to, $subject, $message );
remove_filter( 'wp_mail_from', $from_func );
remove_filter( 'wp_mail_from_name', $from_name_func );
Por favor ten en cuenta que no he probado esto con correos reales...

He añadido esto a los plugins imprescindibles y ejecuté el código de prueba; funcionó. He probado las notificaciones predeterminadas del núcleo (notificación de nuevo usuario, etc.) y también funcionó. Continuaré realizando pruebas este fin de semana y veré cómo funcionarán los plugins con esto y básicamente si todo funciona. Revisaré específicamente los datos brutos del mensaje. Será una tarea que consumirá mucho tiempo, pero descuiden, informaré cuando termine. Si hay un escenario donde wp_mail() no funcione (cuando debería hacerlo), por favor hágamelo saber. Gracias por esta respuesta.

Buen material, he revisado visualmente la salida y se ve bien - de hecho, el parche simplemente hace que wp_mail use el procesamiento estándar sólido como una roca de PHPMailer en el caso de pasar un array, y de lo contrario recurre a las cosas dudosas de WP (para compatibilidad hacia atrás) así que debería estar bien (obviamente el crédito aquí es para los autores del parche)... Voy a empezar a usarlo desde ahora (y a adaptarlo eventualmente) - y gracias de vuelta por la información sobre usar tanto html como texto plano para reducir las posibilidades de ser marcado como spam...

Lo hemos probado en todos los escenarios posibles y funciona genial. Enviaremos un boletín mañana y veremos si recibimos quejas de los usuarios. Los únicos cambios menores que tuvimos que hacer fueron sanear/desanear el array cuando se inserta en la base de datos (tenemos mensajes en una cola en la base de datos donde un cron los envía en pequeños lotes). Dejaré esta pregunta abierta y pendiente hasta que se acabe la recompensa para poder dar visibilidad a este problema. Con suerte, este parche, o una alternativa, se añadirá al núcleo. O más importante, ¿por qué no? ¡En qué están pensando!

Noté aleatoriamente que realizaste una actualización en el ticket de trac enlazado. ¿Es esta una actualización de este código? Si es así, ¿podrías amablemente publicar esta actualización editando tu respuesta aquí también para que esta respuesta se mantenga actualizada? Muchas gracias.

Hola, no, solo fue una actualización del parche contra la versión actual del trunk para que se fusione sin conflictos (con la esperanza de que reciba algo de atención), el código es exactamente el mismo...

De hecho, acabo de editarlo para que sea exactamente igual (¡un espacio después de un foreach!)...

Excelente, gran iniciativa. Por favor continúa con este enfoque. Quizás finalmente lo agreguen al núcleo en un futuro cercano... realmente deberían hacerlo.

Ejecuté tu código a través de PHPCS y publiqué una versión actualizada: https://gist.github.com/paulschreiber/a1057785f6117f72188f3b619e994702

En versiones posteriores de WordPress, esto falla. apply_filters( 'wp_mail', ... ) llama a wp_staticize_emoji_for_email, que a su vez llama a wp_staticize_emoji. Esto espera que $message sea un string, pero aquí es un array.

@PaulSchreiber ¿Tu modificación resuelve la parte que indicas que está fallando? Gracias.

@ChristineCooper Creo que sí. Mi versión ya no pasa 'message' a compact().

@PaulSchreiber ¿Podrías amablemente publicar tu código como respuesta para que tengamos el código actualizado aquí en WPSE? Muchas gracias.

Resumen rápido, la solución simple es:
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);}
}
Luego no necesitas configurar los encabezados explícitamente, los límites de los encabezados se establecen correctamente automáticamente.
Continúa leyendo para una explicación detallada del por qué...
Esto realmente no es un error de WordPress, es uno de phpmailer
al no permitir encabezados personalizados... si miras 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:
// Captura los casos 'plain': y '':
$result .= $this->textLine('Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet);
$ismultipart = false;
break;
}
Puedes ver que el caso default problemático es el que está generando la línea extra del encabezado con el charset y sin límite. Configurar el tipo de contenido por filtro no resuelve esto por sí solo solo porque el caso alt
aquí se establece en message_type
al verificar que AltBody
no esté vacío en lugar del tipo de contenido.
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);
}
Al final, lo que esto significa es que tan pronto como adjuntes un archivo o imagen en línea, o establezcas el AltBody
, se debería evitar el error problemático. También significa que no hay necesidad de configurar explícitamente el tipo de contenido porque tan pronto como hay un AltBody
, se establece como multipart/alternative
por phpmailer
.
Así que la respuesta simple es:
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);}
}
Luego no necesitas configurar los encabezados explícitamente, puedes simplemente hacer:
$message ='<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
<p>¡Hola mundo! Esto es HTML...</p>
</body>
</html>';
wp_mail($to,$subject,$message);
Desafortunadamente, muchas de las funciones y propiedades en la clase phpmailer
están protegidas, si no fuera por eso, una alternativa válida sería simplemente verificar y sobrescribir la propiedad MIMEHeaders
a través del hook phpmailer_init
antes de enviar.

No tengo palabras para agradecerte esta solución, funcionó inmediatamente, sin necesidad de ajustes. Si fuera tú, dejaría solo el 'fragmento de respuesta simple' al final de tu publicación. :)

@scooterlord De nada, ¡definitivamente requirió un poco de esfuerzo resolver esto! He añadido un resumen de la solución al principio de la respuesta para quienes solo quieren la solución rápida.

¿No es genial cuando puedes copiar/pegar fragmentos de código y simplemente funcionan? :)

wp_strip_all_tags podría ser mejor que strip_tags. Este último renderizará el contenido de las etiquetas <style>, que son comunes en los correos HTML.

Para aquellos que están usando el hook phpmailer_init
para añadir su propio 'AltBody':
El cuerpo alternativo de texto se reutiliza para diferentes correos consecutivos que se envían, ¡a menos que lo limpies manualmente! WordPress no lo limpia en wp_mail()
porque no espera que se use esta propiedad.
Esto resulta en que los destinatarios potencialmente reciban correos no destinados a ellos. Afortunadamente, la mayoría de las personas que usan clientes de correo con HTML habilitado no verán la versión de texto, pero sigue siendo básicamente un problema de seguridad.
Por suerte, hay una solución fácil. Esto incluye el fragmento de reemplazo del altbody; ten en cuenta que necesitas la biblioteca PHP Html2Text:
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() ) {
// La propiedad AltBody está configurada, por lo que WordPress ya debe haber usado este
// objeto $phpmailer recientemente para enviar correo, así que vamos a
// limpiar la propiedad AltBody
$phpmailer->AltBody = '';
}
// Devolver los atributos sin modificar
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 );
}
}
Aquí también hay un gist para un plugin de WP que modifiqué para solucionar este problema: https://gist.github.com/youri--/c4618740b7c50c549314eaebc9f78661
Desafortunadamente, no puedo comentar en las otras soluciones que usan el hook mencionado anteriormente, para advertirles de esto, ya que no tengo suficiente reputación aún para comentar.

Acabo de publicar un plugin que permite a los usuarios utilizar plantillas HTML en WordPress y ahora mismo estoy probando en la versión de desarrollo para añadir un simple fallback de texto. Hice lo siguiente y en mis pruebas solo veo un límite añadido y los correos están llegando correctamente a Hotmail.
add_action( 'phpmailer_init', array($this->mailer, 'send_email' ) );
/**
* Modifica el cuerpo del php mailer con el email final
*
* @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 );
}
Básicamente lo que hago aquí es modificar el objeto phpmailer, cargar el mensaje dentro de una plantilla HTML y asignarlo a la propiedad Body. Además, tomo el mensaje original y lo asigno a la propiedad AltBody.

Mi solución simple es usar html2text https://github.com/soundasleep/html2text de esta manera:
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 );
}
}
Aquí https://gist.github.com/frugan-it/6c4d22cd856456480bd77b988b5c9e80 también hay un gist sobre el tema.

Esto puede no ser una respuesta exacta a la publicación inicial aquí, pero es una alternativa a algunas de las soluciones proporcionadas aquí con respecto a establecer un cuerpo alternativo.
Básicamente, necesitaba (y quería) establecer un altbody distinto (es decir, texto plano) además de la parte HTML en lugar de depender de alguna conversión/striptags y demás.
Así que se me ocurrió esto, que parece funcionar bien:
/* establecer las partes del mensaje para wp_mail() */
$markup = array();
$markup['html'] = '<html>algo de html</html>';
$markup['plaintext'] = 'algo de texto plano';
/* mensaje que estamos enviando */
$message = maybe_serialize($markup);
/* establecer el cuerpo alternativo de manera distinta */
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'];
}
}
}

Esta versión de wp_mail()
está basada en el código de @bonger. Incluye estos cambios:
- Correcciones de estilo de código (vía PHPCS)
- Manejo de casos donde $message es un array o un string (asegura compatibilidad con WP 5.x)
- Lanza una excepción en lugar de retornar false
- Sintaxis de array corta
<?php
/**
* Adaptado de https://wordpress.stackexchange.com/a/191974/8591
*
* Envía correo, similar a la función mail de PHP
*
* Un valor de retorno true no significa automáticamente que el usuario recibió el
* correo exitosamente. Solo indica que el método usado pudo procesar la solicitud
* sin errores.
*
* Usando los hooks 'wp_mail_from' y 'wp_mail_from_name' se puede crear una dirección
* de remitente como 'Nombre <email@direccion.com>' cuando ambos están configurados. Si
* solo 'wp_mail_from' está configurado, solo se usará la dirección de correo sin nombre.
*
* El tipo de contenido por defecto es 'text/plain' que no permite usar HTML.
* Sin embargo, puedes configurar el tipo de contenido del correo usando el filtro
* 'wp_mail_content_type'.
*
* Si $message es un array, la clave de cada elemento se usa para agregar como adjunto
* con el valor usado como cuerpo. El elemento 'text/plain' se usa como la versión de texto
* del cuerpo, con el elemento 'text/html' usado como la versión HTML del cuerpo.
* Todos los otros tipos se agregan como adjuntos.
*
* El charset por defecto se basa en el charset usado en el blog. El charset puede
* configurarse usando el filtro 'wp_mail_charset'.
*
* @since 1.2.1
*
* @uses PHPMailer
*
* @param string|array $to Array o lista separada por comas de direcciones de correo a enviar.
* @param string $subject Asunto del correo
* @param string|array $message Contenido del mensaje
* @param string|array $headers Opcional. Cabeceras adicionales.
* @param string|array $attachments Opcional. Archivos para adjuntar.
* @return bool Indica si el contenido del correo fue enviado exitosamente.
*/
public static function wp_mail( $to, $subject, $message, $headers = '', $attachments = [] ) {
// Compactar la entrada, aplicar los filtros y extraerlos nuevamente
/**
* Filtra los argumentos de wp_mail().
*
* @since 2.2.0
*
* @param array $args Array compactado de argumentos de wp_mail(), incluyendo los valores
* "to" (destinatario), subject (asunto), headers (cabeceras), y attachments (adjuntos).
*/
$atts = apply_filters( 'wp_mail', compact( 'to', 'subject', 'headers', 'attachments' ) );
// Como $message es un array, y wp_staticize_emoji_for_email() espera strings, procesarlo item por item
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;
// (Re)crearlo, si ha desaparecido
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
}
// Cabeceras
if ( empty( $headers ) ) {
$headers = [];
} else {
if ( ! is_array( $headers ) ) {
// Explotar las cabeceras, para que esta función pueda tomar tanto
// cabeceras string como un array de cabeceras.
$tempheaders = explode( "\n", str_replace( "\r\n", "\n", $headers ) );
} else {
$tempheaders = $headers;
}
$headers = [];
$cc = [];
$bcc = [];
// Si realmente tiene contenido
if ( ! empty( $tempheaders ) ) {
// Iterar a través de las cabeceras crudas
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;
}
// Explotarlas
list( $name, $content ) = explode( ':', trim( $header ), 2 );
// Limpieza
$name = trim( $name );
$content = trim( $content );
switch ( strtolower( $name ) ) {
// Principalmente por legado - procesar una cabecera From: si está presente
case 'from':
$bracket_pos = strpos( $content, '<' );
if ( false !== $bracket_pos ) {
// Texto antes del correo entre corchetes es el nombre "From".
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 );
// Evitar configurar un $from_email vacío.
} elseif ( '' !== trim( $content ) ) {
$from_email = trim( $content );
}
break;
case 'content-type':
if ( is_array( $message ) ) {
// Correo multiparte, ignorar la cabecera 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 = '';
}
// Evitar configurar un $content_type vacío.
} 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:
// Agregarlo a nuestro gran array de cabeceras
$headers[ trim( $name ) ] = trim( $content );
break;
}
}
}
}
// Vaciar los valores que pueden estar configurados
$phpmailer->ClearAllRecipients();
$phpmailer->ClearAttachments();
$phpmailer->ClearCustomHeaders();
$phpmailer->ClearReplyTos();
$phpmailer->Body = ''; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$phpmailer->AltBody = ''; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
// Correo y nombre del remitente
// Si no tenemos un nombre de las cabeceras de entrada
if ( ! isset( $from_name ) ) {
$from_name = 'WordPress';
}
/* Si no tenemos un correo de las cabeceras de entrada, usar wordpress@$sitename
* Algunos hosts bloquearán correos salientes de esta dirección si no existe pero
* no hay una alternativa fácil. Usar admin_email podría parecer otra opción pero
* algunos hosts pueden rehusarse a reenviar correos de un dominio desconocido. Ver
* https://core.trac.wordpress.org/ticket/5007.
*/
if ( ! isset( $from_email ) ) {
// Obtener el dominio del sitio y eliminar 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;
}
/**
* Filtra la dirección de correo desde la que se envía.
*
* @since 2.2.0
*
* @param string $from_email Dirección de correo desde la que se envía.
*/
$phpmailer->From = apply_filters( 'wp_mail_from', $from_email ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
/**
* Filtra el nombre asociado con la dirección de correo "from".
*
* @since 2.3.0
*
* @param string $from_name Nombre asociado con la dirección de correo "from".
*/
$phpmailer->FromName = apply_filters( 'wp_mail_from_name', $from_name ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
// Configurar direcciones de destino
if ( ! is_array( $to ) ) {
$to = explode( ',', $to );
}
foreach ( (array) $to as $recipient ) {
try {
// Separar $recipient en nombre y dirección si está en formato "Foo <bar@baz.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;
}
}
// Si no tenemos un charset de las cabeceras de entrada
if ( ! isset( $charset ) ) {
$charset = get_bloginfo( 'charset' );
}
// Configurar el content-type y charset
/**
* Filtra el charset por defecto de wp_mail().
*
* @since 2.3.0
*
* @param string $charset Charset por defecto del correo.
*/
$phpmailer->CharSet = apply_filters( 'wp_mail_charset', $charset ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
// Configurar asunto y cuerpo del correo
$phpmailer->Subject = $subject; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
if ( is_string( $message ) ) {
$phpmailer->Body = $message; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
// Configurar Content-Type y charset
// Si no tenemos un content-type de las cabeceras de entrada
if ( ! isset( $content_type ) ) {
$content_type = 'text/plain';
}
/**
* Filtra el tipo de contenido de wp_mail().
*
* @since 2.3.0
*
* @param string $content_type Tipo de contenido por defecto de wp_mail().
*/
$content_type = apply_filters( 'wp_mail_content_type', $content_type );
$phpmailer->ContentType = $content_type; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
// Configurar si es texto plano, dependiendo de $content_type
if ( 'text/html' === $content_type ) {
$phpmailer->IsHTML( true );
}
// Por compatibilidad hacia atrás, nuevos correos multiparte deben usar
// el estilo array $message. Esto nunca funcionó bien de todos modos
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 );
}
}
}
}
// Agregar cualquier destinatario CC y BCC
if ( ! empty( $cc ) ) {
foreach ( (array) $cc as $recipient ) {
try {
// Separar $recipient en nombre y dirección si está en formato "Foo <bar@baz.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 {
// Separar $recipient en nombre y dirección si está en formato "Foo <bar@baz.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;
}
}
}
// Configurar para usar la función mail() de PHP
$phpmailer->IsMail();
// Configurar cabeceras personalizadas
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;
}
}
}
/**
* Se ejecuta después de inicializar PHPMailer.
*
* @since 2.2.0
*
* @param PHPMailer &$phpmailer La instancia de PHPMailer, pasada por referencia.
*/
do_action_ref_array( 'phpmailer_init', [ &$phpmailer ] );
// ¡Enviar!
try {
return $phpmailer->Send();
} catch ( phpmailerException $e ) {
return new WP_Error( 'email-error', $e->getMessage() );
}
}

Si no deseas crear ningún conflicto de código en el núcleo de WordPress, creo que la solución alternativa o más simple es agregar una acción a phpmailer_init
que se ejecutará antes del envío real del correo en la función wp_mail()
. Para simplificar mi explicación, mira el siguiente ejemplo de código:
<?php
$to = '';
$subject = '';
$from = '';
$body = 'El contenido html del texto, <html>...';
$headers = "FROM: {$from}";
add_action( 'phpmailer_init', function ( $phpmailer ) {
$phpmailer->AltBody = 'El contenido de texto plano de tu contenido html original.';
} );
wp_mail($to, $subject, $body, $headers);
Si agregas contenido en la propiedad AltBody
de la clase PHPMailer, el tipo de contenido predeterminado se establecerá automáticamente como multipart/alternative
.

Analicé de cerca la implementación de wp_mail($to, $subject, $message, $headers, $attachments)
en pluggable.php
y encontré una solución que no requiere modificar el núcleo.
La función wp_mail()
verifica el argumento $headers
para un conjunto específico de tipos de encabezados estándar, específicamente from
, content-type
, cc
, bcc
y reply-to
.
Todos los demás tipos se designan como encabezados personalizados y se procesan por separado. Pero aquí está el detalle: cuando se define un encabezado personalizado, como en tu caso donde estableciste el encabezado MIME-Version
, se ejecuta el siguiente bloque de código (dentro de wp_mail()
):
// Establecer encabezados personalizados
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 ) );
}
}
Esa declaración if
anidada en el fragmento anterior es la culpable. Básicamente, se agrega otro encabezado Content-Type
como encabezado personalizado bajo las siguientes condiciones:
- Se definió un encabezado personalizado (definiste
MIME-Version
en el escenario descrito en tu publicación). - El tipo MIME del encabezado
Content-Type
contiene la cadenamultipart
. - Se estableció un límite (boundary) para múltiples partes.
La solución más rápida en tu caso es eliminar el encabezado MIME-Version
. La mayoría de los agentes de usuario lo agregan automáticamente de todos modos, por lo que eliminarlo no debería ser un problema.
Pero ¿qué pasa si quieres agregar encabezados personalizados sin generar un encabezado Content-Type
duplicado?
SOLUCIÓN:
NO establezcas explícitamente el encabezado Content-Type
en el arreglo $headers
cuando agregues encabezados personalizados, haz lo siguiente en su lugar:
$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' );
La primera línea del fragmento anterior puede parecer confusa, pero la función wp_mail()
establecerá internamente su variable $boundary
siempre que una definición de límite aparezca en su propia línea sin estar precedida por Content-Type:
. Luego puedes usar filtros para establecer el content-type
y el charset
respectivamente. De esta manera, cumples con las condiciones para ejecutar el bloque de código que establece encabezados personalizados sin agregar explícitamente Content-Type: [mime-type]; [boundary];
.
No es necesario modificar la implementación central de wp_mail()
, aunque pueda tener errores.
