El envío de correos multipart (text/html) vía wp_mail() probablemente hará que tu dominio sea bloqueado

18 jun 2015, 18:37:09
Vistas: 26.6K
Votos: 44

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.

7
Comentarios

Dado que la función wp_mail() es "pluggable", ¿no sería definir tu reemplazo como un plugin de uso obligatorio (en wp-content/mu-plugins) una buena solución para ti (y para todos los demás, en caso de que no se corrija el núcleo)? ¿En qué caso no funcionaría mover la verificación de multipart/boundary después de establecer $phpmailer->ContentType = $content_type; (en lugar de usarlo en un else)?

bonger bonger
18 jun 2015 21:35:18

@bonger ¿Podrías escribir una respuesta detallando tu solución?

Christine Cooper Christine Cooper
18 jun 2015 22:07:28

No necesitas editar el núcleo, porque wp_mail es pluggable. Copia la función original en un plugin, edítala como necesites y activa el plugin. WordPress usará tu función editada en lugar de la original, sin necesidad de modificar el núcleo.

gmazzap gmazzap
19 jun 2015 04:41:45

@ChristineCooper Dudo en hacer esto ya que, como dices, las pruebas son un verdadero dolor de cabeza, pero viendo el parche https://core.trac.wordpress.org/ticket/15448 sugerido en trac por @rmccue/@MattyRob, parece una muy buena manera de proceder, así que publicaré una respuesta no probada basada en eso...

bonger bonger
19 jun 2015 08:17:54

Esto de ninguna manera es una solución al problema en cuestión, pero creo que vale la pena señalar: Si te tomas en serio que tus correos electrónicos lleguen a su destinatario previsto, deberías considerar usar un servidor SMTP externo. Un plugin SMTP evitaría este problema por completo, lo que puede explicar por qué nadie se ha molestado en solucionarlo durante cinco años.

Mathew Tinsley Mathew Tinsley
19 jun 2015 09:03:30

@ChristineCooper si simplemente te enganchas a phpmailer y estableces el cuerpo de texto en $phpmailer->AltBody, ¿ocurre el mismo error?

chifliiiii chifliiiii
10 jul 2015 19:52:50

Encontré una forma de usar wp_mail() tal cual sin necesidad de modificar el núcleo. Mira mi respuesta a continuación.

TheAddonDepot TheAddonDepot
20 dic 2020 01:08:31
Mostrar los 2 comentarios restantes
Todas las respuestas a la pregunta 9
14
20

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...

19 jun 2015 08:34:00
Comentarios

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.

Christine Cooper Christine Cooper
20 jun 2015 18:43:49

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...

bonger bonger
21 jun 2015 00:02:10

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!

Christine Cooper Christine Cooper
21 jun 2015 18:53:57

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.

Christine Cooper Christine Cooper
9 sept 2015 02:49:04

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...

bonger bonger
9 sept 2015 07:05:13

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

bonger bonger
9 sept 2015 07:12:48

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.

Christine Cooper Christine Cooper
9 sept 2015 16:42:20

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

Paul Schreiber Paul Schreiber
17 ene 2019 16:29:15

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.

Paul Schreiber Paul Schreiber
17 ene 2019 17:03:46

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

Christine Cooper Christine Cooper
19 feb 2020 12:54:21

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

Paul Schreiber Paul Schreiber
19 feb 2020 18:06:30

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

Christine Cooper Christine Cooper
19 feb 2020 19:27:34

@ChristineCooper hecho.

Paul Schreiber Paul Schreiber
19 feb 2020 23:29:38

¿Este gran enfoque todavía no ha llegado al núcleo de WordPress? No hay mención del uso de arrays o multiparte en la documentación.

Thomas Ebert Thomas Ebert
12 nov 2020 10:45:50
Mostrar los 9 comentarios restantes
5
11

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.

24 may 2016 17:50:33
Comentarios

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 scooterlord
8 ene 2021 17:14:00

@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.

majick majick
12 ene 2021 06:37:12

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

scooterlord scooterlord
12 ene 2021 09:56:45

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.

James Beninger James Beninger
14 ene 2022 18:46:32

@JamesBeninger ah, ¡buen apunte! He editado la respuesta para cambiarlo.

majick majick
15 ene 2022 07:32:41
0

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.

16 oct 2018 14:33:36
0

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.

10 jul 2015 21:43:30
0

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.

11 oct 2016 13:50:18
0

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'];
        }
    }   
}
25 ene 2017 16:56:04
1

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() );
    }
}
19 feb 2020 23:27:45
Comentarios

Hola Paul. La sintaxis corta de arrays llegó mucho antes de PHP 7+ ;)

kaiser kaiser
19 feb 2020 23:37:39
0

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.

18 sept 2018 08:24:56
0

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:

  1. Se definió un encabezado personalizado (definiste MIME-Version en el escenario descrito en tu publicación).
  2. El tipo MIME del encabezado Content-Type contiene la cadena multipart.
  3. 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.

20 dic 2020 00:41:41