L'invio di email multipart (text/html) tramite wp_mail() potrebbe causare il ban del tuo dominio
Riepilogo
A causa di un bug nel Core di WordPress, l'invio di email multipart (html/text) con wp_mail() (per ridurre la possibilità che le email finiscano nelle cartelle spam) ironicamente farà sì che il tuo dominio venga bloccato da Hotmail (e altre email Microsoft).
Questo è un problema complesso che cercherò di analizzare in dettaglio nel tentativo di aiutare qualcuno a trovare una soluzione funzionante che potrebbe eventualmente essere implementata nel core.
Sarà una lettura gratificante. Iniziamo...
Il bug
Il consiglio più comune per evitare che le email della newsletter finiscano nelle cartelle spam è quello di inviare messaggi multipart.
Multi-part (mime) si riferisce all'invio sia della parte HTML che di quella TESTO di un messaggio email in una singola email. Quando un client riceve un messaggio multipart, accetta la versione HTML se può renderizzare l'HTML, altrimenti presenta la versione in testo semplice.
Questo è dimostrato che funziona. Quando inviavamo a Gmail, tutte le nostre email finivano nelle cartelle spam finché non abbiamo cambiato i messaggi in multipart, quando sono arrivati nella casella principale. Ottimo risultato.
Ora, quando si inviano messaggi multipart tramite wp_mail(), viene emesso il Content Type (multipart/*) due volte, una volta con boundary (se impostato manualmente) e una volta senza. Questo comportamento fa sì che l'email venga visualizzata come messaggio raw e non multipart su alcune email, incluse tutte quelle Microsoft (Hotmail, Outlook, ecc...)
Microsoft contrassegnerà questo messaggio come spam, e i pochi messaggi che passano verranno segnalati manualmente dal destinatario. Sfortunatamente, gli indirizzi email Microsoft sono ampiamente utilizzati. Il 40% dei nostri iscritti li utilizza.
Questo è stato confermato da Microsoft tramite uno scambio di email che abbiamo avuto recentemente.
La segnalazione dei messaggi porterà al blocco completo del dominio. Ciò significa che i messaggi non verranno inviati alla cartella spam, non verranno proprio consegnati al destinatario.
Il nostro dominio principale è stato bloccato già 3 volte finora.
Poiché questo è un bug nel core di WordPress, ogni dominio che invia messaggi multipart viene bloccato. Il problema è che la maggior parte dei webmaster non sa perché. L'ho confermato durante la mia ricerca vedendo altri utenti discuterne nei forum ecc. Richiede di addentrarsi nel codice raw e avere una buona conoscenza di come funzionano questi tipi di messaggi email, che andremo ad analizzare ora...
Analizziamo il codice
Crea un account hotmail/outlook. Quindi, esegui il seguente codice:
// Imposta $to con un'email hotmail.com o outlook.com
$to = "TuaEmail@hotmail.com";
$subject = 'test wp_mail multipart';
$message = '------=_Part_18243133_1346573420.1408991447668
Content-Type: text/plain; charset=UTF-8
Ciao mondo! Questo è testo semplice...
------=_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>Ciao Mondo! Questo è 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"';
// invia email
wp_mail( $to, $subject, $message, $headers );
E se vuoi cambiare il content type predefinito, usa:
add_filter( 'wp_mail_content_type', 'set_content_type' );
function set_content_type( $content_type ) {
return 'multipart/alternative';
}
Questo invierà un messaggio multipart.
Quindi se controlli il codice sorgente completo del messaggio, noterai che il content type viene aggiunto due volte, una volta senza boundary:
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary="====f230673f9d7c359a81ffebccb88e5d61=="
MIME-Version: 1.0
Content-Type: multipart/alternative; charset=
Questo è il problema.
L'origine del problema si trova in pluggable.php
- se guardiamo da qualche parte qui:
// Imposta Content-Type e charset
// Se non abbiamo un content-type dagli header di input
if ( !isset( $content_type ) )
$content_type = 'text/plain';
/**
* Filtra il content type di wp_mail().
*
* @since 2.3.0
*
* @param string $content_type Content type predefinito di wp_mail().
*/
$content_type = apply_filters( 'wp_mail_content_type', $content_type );
$phpmailer->ContentType = $content_type;
// Imposta se è testo semplice, in base a $content_type
if ( 'text/html' == $content_type )
$phpmailer->IsHTML( true );
// Se non abbiamo un charset dagli header di input
if ( !isset( $charset ) )
$charset = get_bloginfo( 'charset' );
// Imposta il content-type e charset
/**
* Filtra il charset predefinito di wp_mail().
*
* @since 2.3.0
*
* @param string $charset Charset email predefinito.
*/
$phpmailer->CharSet = apply_filters( 'wp_mail_charset', $charset );
// Imposta gli header personalizzati
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;
}
}
}
Potenziali soluzioni
Ti starai chiedendo, perché non hai segnalato questo su trac? L'ho già fatto. Con mia grande sorpresa, un ticket diverso era stato creato 5 anni fa che descriveva lo stesso problema.
Ammettiamolo, sono passati cinque anni. In anni internet, è più come 30. Il problema è stato chiaramente abbandonato e praticamente non verrà mai risolto (...a meno che non lo risolviamo qui).
Ho trovato un ottimo thread qui che offre una soluzione, ma mentre la sua soluzione funziona, rompe le email che non hanno $headers
personalizzati impostati.
È qui che ci blocchiamo ogni volta. O la versione multipart funziona bene e i messaggi normali senza $headers
impostati no, o viceversa.
La soluzione che abbiamo trovato è stata:
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;
// Imposta se è plaintext, in base a $content_type
if ( 'text/html' == $content_type )
$phpmailer->IsHTML( true );
// Se non abbiamo un charset dagli header di input
if ( !isset( $charset ) )
$charset = get_bloginfo( 'charset' );
}
// Imposta il content-type e charset
/**
* Filtra il charset predefinito di wp_mail().
*
* @since 2.3.0
*
* @param string $charset Charset email predefinito.
*/
$phpmailer->CharSet = apply_filters( 'wp_mail_charset', $charset );
// Imposta gli header personalizzati
if ( !empty( $headers ) ) {
foreach( (array) $headers as $name => $content ) {
$phpmailer->AddCustomHeader( sprintf( '%1$s: %2$s', $name, $content ) );
}
}
Sì, lo so, modificare i file core è tabù, siediti... questa era una correzione disperata e un tentativo poco efficace di fornire una correzione per il core.
Il problema con la nostra correzione è che le email predefinite come nuove registrazioni, commenti, reset password ecc. verranno consegnate come messaggi vuoti. Quindi abbiamo uno script wp_mail() funzionante che invierà messaggi multipart ma nient'altro.
Cosa fare
L'obiettivo qui è trovare un modo per inviare sia messaggi normali (testo semplice) che multipart usando la funzione wp_mail() del core (non una funzione sendmail personalizzata).
Quando si tenta di risolvere questo problema, il problema principale che incontrerai è la quantità di tempo che passerai a inviare messaggi di prova, controllare se vengono ricevuti e praticamente aprire una scatola di aspirina e maledire Microsoft perché sei abituato ai loro problemi con IE mentre il gremlin qui è sfortunatamente WordPress.
Aggiornamento
La soluzione pubblicata da @bonger permette a $message
di essere un array contenente alternative con chiave content-type. Ho confermato che funziona in tutti gli scenari.
Permetteremo a questa domanda di rimanere aperta fino alla scadenza della taglia per sensibilizzare sul problema, forse a un livello tale da essere corretto nel core. Sentiti libero di pubblicare una soluzione alternativa dove $message
può essere una stringa.
La seguente versione di wp_mail()
include la patch applicata da @rmccue/@MattyRob nel ticket https://core.trac.wordpress.org/ticket/15448, aggiornata per la 4.2.2, che permette a $message
di essere un array contenente alternative con chiavi per il content-type:
/**
* Invia email, simile alla funzione mail di PHP
*
* Un valore di ritorno true non significa automaticamente che l'utente ha ricevuto
* l'email con successo. Indica solo che il metodo utilizzato è stato in grado di
* elaborare la richiesta senza errori.
*
* Utilizzando gli hook 'wp_mail_from' e 'wp_mail_from_name' è possibile creare
* un indirizzo from come 'Nome <email@indirizzo.com>' quando entrambi sono impostati. Se
* è impostato solo 'wp_mail_from', verrà utilizzato solo l'indirizzo email senza nome.
*
* Il content-type predefinito è 'text/plain' che non consente l'uso di HTML.
* Tuttavia, è possibile impostare il content-type dell'email utilizzando il filtro
* 'wp_mail_content_type'.
*
* Se $message è un array, la chiave di ciascun elemento viene utilizzata per aggiungere
* un allegato con il valore usato come corpo. L'elemento 'text/plain' è utilizzato come
* versione testo del corpo, mentre 'text/html' è usato come versione HTML del corpo.
* Tutti gli altri tipi sono aggiunti come allegati.
*
* Il charset predefinito è basato sul charset utilizzato nel blog. Il charset può
* essere impostato usando il filtro 'wp_mail_charset'.
*
* @since 1.2.1
*
* @uses PHPMailer
*
* @param string|array $to Array o lista separata da virgole di indirizzi email a cui inviare il messaggio.
* @param string $subject Oggetto dell'email
* @param string|array $message Contenuto del messaggio
* @param string|array $headers Opzionale. Intestazioni aggiuntive.
* @param string|array $attachments Opzionale. File da allegare.
* @return bool Indica se il contenuto dell'email è stato inviato con successo.
*/
function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() ) {
// Compatta gli input, applica i filtri e li estrae nuovamente
/**
* Filtra gli argomenti di wp_mail().
*
* @since 2.2.0
*
* @param array $args Un array compatto degli argomenti di wp_mail(), inclusi i valori
* "to", subject, message, headers e 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;
// (Ri)crea l'istanza, se mancante
if ( ! ( $phpmailer instanceof PHPMailer ) ) {
require_once ABSPATH . WPINC . '/class-phpmailer.php';
require_once ABSPATH . WPINC . '/class-smtp.php';
$phpmailer = new PHPMailer( true );
}
// Intestazioni
if ( empty( $headers ) ) {
$headers = array();
} else {
if ( !is_array( $headers ) ) {
// Suddivide le intestazioni, così questa funzione può accettare sia
// stringhe di intestazioni che array di intestazioni.
$tempheaders = explode( "\n", str_replace( "\r\n", "\n", $headers ) );
} else {
$tempheaders = $headers;
}
$headers = array();
$cc = array();
$bcc = array();
// Se ci sono effettivamente contenuti
if ( !empty( $tempheaders ) ) {
// Itera attraverso le intestazioni grezze
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;
}
// Suddivide nome e contenuto
list( $name, $content ) = explode( ':', trim( $header ), 2 );
// Pulizia
$name = trim( $name );
$content = trim( $content );
switch ( strtolower( $name ) ) {
// Principalmente per legacy -- processa un'intestazione From: se presente
case 'from':
$bracket_pos = strpos( $content, '<' );
if ( $bracket_pos !== false ) {
// Il testo prima dell'email tra parentesi è il nome "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 di impostare un $from_email vuoto.
} elseif ( '' !== trim( $content ) ) {
$from_email = trim( $content );
}
break;
case 'content-type':
if ( is_array($message) ) {
// Email multipart, ignora l'intestazione 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 di impostare un $content_type vuoto.
} 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:
// Aggiunge all'array principale delle intestazioni
$headers[trim( $name )] = trim( $content );
break;
}
}
}
}
// Svuota i valori che potrebbero essere impostati
$phpmailer->ClearAllRecipients();
$phpmailer->ClearAttachments();
$phpmailer->ClearCustomHeaders();
$phpmailer->ClearReplyTos();
$phpmailer->Body= '';
$phpmailer->AltBody= '';
// Email e nome del mittente
// Se non abbiamo un nome dalle intestazioni di input
if ( !isset( $from_name ) )
$from_name = 'WordPress';
/* Se non abbiamo un'email dalle intestazioni di input, usa wordpress@$sitename
* Alcuni host bloccheranno l'invio di email da questo indirizzo se non esiste ma
* non ci sono alternative semplici. Usare admin_email potrebbe sembrare un'altra
* opzione ma alcuni host potrebbero rifiutare l'invio da un dominio sconosciuto. Vedi
* https://core.trac.wordpress.org/ticket/5007.
*/
if ( !isset( $from_email ) ) {
// Ottiene il dominio del sito e rimuove www.
$sitename = strtolower( $_SERVER['SERVER_NAME'] );
if ( substr( $sitename, 0, 4 ) == 'www.' ) {
$sitename = substr( $sitename, 4 );
}
$from_email = 'wordpress@' . $sitename;
}
/**
* Filtra l'indirizzo email del mittente.
*
* @since 2.2.0
*
* @param string $from_email Indirizzo email del mittente.
*/
$phpmailer->From = apply_filters( 'wp_mail_from', $from_email );
/**
* Filtra il nome associato all'indirizzo email "from".
*
* @since 2.3.0
*
* @param string $from_name Nome associato all'indirizzo email "from".
*/
$phpmailer->FromName = apply_filters( 'wp_mail_from_name', $from_name );
// Imposta gli indirizzi destinatari
if ( !is_array( $to ) )
$to = explode( ',', $to );
foreach ( (array) $to as $recipient ) {
try {
// Suddivide $recipient in nome e indirizzo se nel 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;
}
}
// Se non abbiamo un charset dalle intestazioni di input
if ( !isset( $charset ) )
$charset = get_bloginfo( 'charset' );
// Imposta il content-type e il charset
/**
* Filtra il charset predefinito di wp_mail().
*
* @since 2.3.0
*
* @param string $charset Charset predefinito per le email.
*/
$phpmailer->CharSet = apply_filters( 'wp_mail_charset', $charset );
// Imposta oggetto e corpo dell'email
$phpmailer->Subject = $subject;
if ( is_string($message) ) {
$phpmailer->Body = $message;
// Imposta Content-Type e charset
// Se non abbiamo un content-type dalle intestazioni di input
if ( !isset( $content_type ) )
$content_type = 'text/plain';
/**
* Filtra il content-type di wp_mail().
*
* @since 2.3.0
*
* @param string $content_type Content-type predefinito di wp_mail().
*/
$content_type = apply_filters( 'wp_mail_content_type', $content_type );
$phpmailer->ContentType = $content_type;
// Imposta se è testo semplice, in base a $content_type
if ( 'text/html' == $content_type )
$phpmailer->IsHTML( true );
// Per compatibilità all'indietro, le nuove email multipart dovrebbero usare
// lo stile array per $message. Questo comunque non ha mai funzionato bene
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);
}
}
}
}
// Aggiunge eventuali destinatari CC e BCC
if ( !empty( $cc ) ) {
foreach ( (array) $cc as $recipient ) {
try {
// Suddivide $recipient in nome e indirizzo se nel 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 {
// Suddivide $recipient in nome e indirizzo se nel 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;
}
}
}
// Imposta l'uso della funzione mail() di PHP
$phpmailer->IsMail();
// Imposta intestazioni personalizzate
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;
}
}
}
/**
* Azione eseguita dopo l'inizializzazione di PHPMailer.
*
* @since 2.2.0
*
* @param PHPMailer &$phpmailer L'istanza PHPMailer, passata per riferimento.
*/
do_action_ref_array( 'phpmailer_init', array( &$phpmailer ) );
// Invia!
try {
return $phpmailer->Send();
} catch ( phpmailerException $e ) {
return false;
}
}
Quindi se inserisci questo nel tuo file, ad esempio "wp-content/mu-plugins/functions.php", sovrascriverà la versione di WP. Ha un utilizzo semplice senza dover modificare manualmente le intestazioni, ad esempio:
// Imposta $to con un'email hotmail.com o outlook.com
$to = "LaTuaEmail@hotmail.com";
$subject = 'Test multipart di wp_mail';
$message['text/plain'] = 'Ciao mondo! Questo è testo semplice...';
$message['text/html'] = '<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
<p>Ciao Mondo! Questo è 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'; } );
// invia email
wp_mail( $to, $subject, $message );
remove_filter( 'wp_mail_from', $from_func );
remove_filter( 'wp_mail_from_name', $from_name_func );
Nota: non ho testato questo codice con email reali...

Ho aggiunto questo ai plugin indispensabili e ho eseguito il codice di test; ha funzionato. Ho testato le notifiche predefinite del core (come la notifica per nuovo utente, ecc.) e ha funzionato anche quello. Continuerò a fare test questo fine settimana per vedere come i plugin funzioneranno con questo e fondamentalmente se tutto funziona. Controllerò specificamente i dati grezzi del messaggio. Sarà un compito molto dispendioso in termini di tempo, ma state certi che vi farò sapere quando avrò finito. Se c'è uno scenario in cui wp_mail() non funzionerà (quando invece dovrebbe), per favore fatemelo sapere. Grazie per questa risposta.

Ottime cose, ho dato un'occhiata all'output e sembra buono - in effetti la patch fa semplicemente sì che wp_mail utilizzi l'elaborazione standard solida di PHPMailer nel caso in cui venga passato un array, mentre in caso contrario torna al materiale approssimativo di WP (per compatibilità all'indietro), quindi dovrebbe andare bene (ovviamente il merito qui va agli autori della patch)... Da ora in poi lo userò (e alla fine lo retrofitterò) - e grazie anche a te per le informazioni sull'uso sia di html che di plain per ridurre le possibilità di essere marchiati come spam...

L'abbiamo testato in tutti gli scenari possibili e funziona alla grande. Manderemo una newsletter domani e vedremo se riceviamo lamentele dagli utenti. Le uniche piccole modifiche che abbiamo dovuto fare sono state sanificare/desanificare l'array quando viene inserito nel db (abbiamo i messaggi in una coda nel db dove un cron li invia in piccoli lotti). Lascerò questa domanda aperta e in attesa fino a quando il bounty scadrà, in modo da poter portare consapevolezza su questo problema. Speriamo che questa patch, o un'alternativa, venga aggiunta al core. O, ancora più importante, perché no. Cosa stanno pensando!

Ho notato casualmente che hai eseguito un aggiornamento al ticket trac collegato. Si tratta di un aggiornamento a questo codice? Se è così, potresti cortesemente pubblicare questo aggiornamento modificando anche la tua risposta qui in modo che questa risposta rimanga aggiornata? Grazie mille.

Ciao, no è stato solo un refresh della patch rispetto all'attuale trunk in modo che si fonda senza conflitti (nella speranza che riceva un po' di attenzione), il codice è esattamente lo stesso...

In realtà l'ho appena modificato per renderlo esattamente identico (uno spazio dopo un foreach!)...

Eccellente, ottima iniziativa. Per favore continua con questo approccio. Forse alla fine lo aggiungeranno al core nel prossimo futuro... dovrebbero davvero farlo.

Ho eseguito il tuo codice attraverso PHPCS e ho pubblicato una versione aggiornata: https://gist.github.com/paulschreiber/a1057785f6117f72188f3b619e994702

Nelle versioni più recenti di WordPress, questo genera un errore. apply_filters( 'wp_mail', ... ) chiama wp_staticize_emoji_for_email, che a sua volta chiama wp_staticize_emoji. Questa si aspetta che $message sia una stringa, mentre qui è un array.

@PaulSchreiber La tua modifica risolve la parte che dici sia problematica? Grazie.

@ChristineCooper Credo di sì. La mia versione non passa più 'message' a compact().

@PaulSchreiber Potresti gentilmente pubblicare il tuo codice come risposta in modo da avere il codice aggiornato qui su WPSE? Grazie mille.

TLDR, la soluzione semplice è:
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);}
}
In questo modo non è necessario impostare esplicitamente gli header, i limiti dei boundary verranno impostati correttamente automaticamente.
Continua a leggere per una spiegazione dettagliata del perché...
Questo non è realmente un bug di WordPress, ma piuttosto di phpmailer
nel non gestire correttamente gli header personalizzati... se osservi 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:
// Cattura i casi 'plain': e case '':
$result .= $this->textLine('Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet);
$ismultipart = false;
break;
}
Puoi vedere che il caso default problematico è quello che produce la riga extra dell'header con il charset e senza boundary. Impostare il content type tramite filter non risolve da solo perché il caso alt
qui viene impostato su message_type
verificando che AltBody
non sia vuoto piuttosto che dal content type.
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);
}
Alla fine ciò significa che non appena alleghi un file o un'immagine inline, o imposti AltBody
, il bug problematico dovrebbe essere bypassato. Significa anche che non c'è bisogno di impostare esplicitamente il content type perché non appena c'è un AltBody
viene impostato a multipart/alternative
da phpmailer
.
Quindi la soluzione semplice è:
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);}
}
In questo modo non devi impostare esplicitamente gli header, puoi semplicemente fare:
$message ='<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
<p>Ciao Mondo! Questo è HTML...</p>
</body>
</html>';
wp_mail($to,$subject,$message);
Sfortunatamente molte delle funzioni e proprietà nella classe phpmailer
sono protected, se non fosse così un'alternativa valida sarebbe semplicemente verificare e sovrascrivere la proprietà MIMEHeaders
tramite l'hook phpmailer_init
prima dell'invio.

Non posso ringraziarti abbastanza per questa soluzione, ha funzionato immediatamente, subito pronta all'uso. Se fossi in te terrei solo lo 'snippet della risposta semplice' da solo alla fine del tuo post. :)

@scooterlord Prego, è stato sicuramente un po' complicato risolvere questo problema! Ho aggiunto una soluzione riassuntiva all'inizio della risposta per chi vuole solo la correzione.

Non è fantastico quando puoi semplicemente copiare/incollare snippet e funzionano subito senza problemi? :)

wp_strip_all_tags potrebbe essere migliore di strip_tags. Quest'ultimo renderà il contenuto dei tag <style>, che sono comuni nelle email HTML.

Per chiunque stia utilizzando l'hook phpmailer_init
per aggiungere il proprio 'AltBody':
Il corpo alternativo del testo viene riutilizzato per diverse email consecutive inviate, a meno che non venga cancellato manualmente! WordPress non lo cancella in wp_mail()
perché non si aspetta che questa proprietà venga utilizzata.
Ciò può portare a situazioni in cui i destinatari ricevono email non destinate a loro. Fortunatamente, la maggior parte delle persone che utilizzano client di posta con supporto HTML non vedrà la versione testuale, ma rimane comunque un problema di sicurezza.
Fortunatamente, c'è una soluzione semplice. Questo include anche la parte per sostituire l'altbody; nota che avrai bisogno della libreria 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 proprietà AltBody è impostata, quindi WordPress deve aver già usato questo
// oggetto $phpmailer per inviare la mail, quindi cancelliamo
// la proprietà AltBody
$phpmailer->AltBody = '';
}
// Restituiamo gli attributi non modificati
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 );
}
}
Ecco anche un gist per un plugin WP che ho modificato per risolvere questo problema: https://gist.github.com/youri--/c4618740b7c50c549314eaebc9f78661
Sfortunatamente non posso commentare le altre soluzioni che utilizzano l'hook menzionato per avvisarli di questo problema, poiché non ho abbastanza reputazione per commentare.

Ho appena rilasciato un plugin per permettere agli utenti di utilizzare template HTML su WordPress e sto attualmente lavorando alla versione di sviluppo per aggiungere un semplice fallback testuale. Ho implementato il seguente codice e nei miei test vedo solo un boundary aggiunto e le email arrivano correttamente su Hotmail.
add_action( 'phpmailer_init', array($this->mailer, 'send_email' ) );
/**
* Modifica il body del php mailer con l'email finale
*
* @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 );
}
In pratica, ciò che faccio qui è modificare l'oggetto phpmailer, caricare il messaggio all'interno di un template HTML e impostarlo nella proprietà Body. Inoltre, prendo il messaggio originale e lo imposto nella proprietà AltBody.

La mia semplice soluzione è utilizzare html2text https://github.com/soundasleep/html2text in questo modo:
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 );
}
}
Qui https://gist.github.com/frugan-it/6c4d22cd856456480bd77b988b5c9e80 trovi anche un gist sull'argomento.

Questo potrebbe non essere una risposta esatta al post iniziale qui, ma è un'alternativa ad alcune delle soluzioni fornite riguardo all'impostazione di un corpo alternativo (alt body).
In sostanza, avevo bisogno (e volevo) impostare un corpo alternativo distinto (cioè testo semplice) in aggiunta alla parte HTML invece di affidarmi a qualche conversione/striptags e simili.
Quindi ho ideato questa soluzione che sembra funzionare bene:
/* impostazione delle parti del messaggio per wp_mail() */
$markup = array();
$markup['html'] = '<html>qualche html</html>';
$markup['plaintext'] = 'qualche testo semplice';
/* messaggio che stiamo inviando */
$message = maybe_serialize($markup);
/* impostazione distinta del corpo alternativo */
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'];
}
}
}

Questa versione di wp_mail()
è basata sul codice di @bonger. Include queste modifiche:
- Correzioni dello stile del codice (tramite PHPCS)
- Gestione dei casi in cui $message è un array o una stringa (garantisce compatibilità con WP 5.x)
- Genera un'eccezione invece di restituire false
- Sintassi abbreviata degli array
<?php
/**
* Adattato da https://wordpress.stackexchange.com/a/191974/8591
*
* Invia email, simile alla funzione mail di PHP
*
* Un valore di ritorno true non significa automaticamente che l'utente abbia ricevuto
* l'email con successo. Significa solo che il metodo utilizzato è stato in grado di
* processare la richiesta senza errori.
*
* Utilizzando gli hook 'wp_mail_from' e 'wp_mail_from_name' è possibile creare
* un indirizzo mittente come 'Nome <email@indirizzo.com>' quando entrambi sono impostati.
* Se è impostato solo 'wp_mail_from', verrà utilizzato solo l'indirizzo email senza nome.
*
* Il tipo di contenuto predefinito è 'text/plain' che non consente l'uso di HTML.
* Tuttavia, è possibile impostare il tipo di contenuto dell'email utilizzando
* il filtro 'wp_mail_content_type'.
*
* Se $message è un array, la chiave di ogni elemento viene utilizzata per aggiungere
* un allegato con il valore usato come corpo. L'elemento 'text/plain' viene usato
* come versione testuale del corpo, mentre 'text/html' come versione HTML.
* Tutti gli altri tipi vengono aggiunti come allegati.
*
* Il set di caratteri predefinito si basa su quello utilizzato nel blog.
* Il set di caratteri può essere impostato usando il filtro 'wp_mail_charset'.
*
* @since 1.2.1
*
* @uses PHPMailer
*
* @param string|array $to Array o lista di indirizzi email separati da virgola a cui inviare il messaggio.
* @param string $subject Oggetto dell'email
* @param string|array $message Contenuto del messaggio
* @param string|array $headers Opzionale. Intestazioni aggiuntive.
* @param string|array $attachments Opzionale. File da allegare.
* @return bool Indica se il contenuto dell'email è stato inviato con successo.
*/
public static function wp_mail( $to, $subject, $message, $headers = '', $attachments = [] ) {
// Compatta l'input, applica i filtri e li estrae nuovamente
/**
* Filtra gli argomenti di wp_mail().
*
* @since 2.2.0
*
* @param array $args Un array compatto degli argomenti di wp_mail(), inclusi i valori
* "to", subject, message, headers e attachments.
*/
$atts = apply_filters( 'wp_mail', compact( 'to', 'subject', 'headers', 'attachments' ) );
// Poiché $message è un array e wp_staticize_emoji_for_email() si aspetta stringhe, iteriamo un elemento alla volta
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;
// (Ri)crea l'oggetto, se è mancante
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
}
// Intestazioni
if ( empty( $headers ) ) {
$headers = [];
} else {
if ( ! is_array( $headers ) ) {
// Suddivide le intestazioni, così questa funzione può accettare sia
// stringhe di intestazioni che array di intestazioni
$tempheaders = explode( "\n", str_replace( "\r\n", "\n", $headers ) );
} else {
$tempheaders = $headers;
}
$headers = [];
$cc = [];
$bcc = [];
// Se ci sono effettivamente contenuti
if ( ! empty( $tempheaders ) ) {
// Itera attraverso le intestazioni grezze
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;
}
// Suddivide nome e contenuto
list( $name, $content ) = explode( ':', trim( $header ), 2 );
// Pulizia
$name = trim( $name );
$content = trim( $content );
switch ( strtolower( $name ) ) {
// Principalmente per legacy - processa un'intestazione From: se presente
case 'from':
$bracket_pos = strpos( $content, '<' );
if ( false !== $bracket_pos ) {
// Il testo prima dell'email tra parentesi è il nome "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 di impostare un $from_email vuoto
} elseif ( '' !== trim( $content ) ) {
$from_email = trim( $content );
}
break;
case 'content-type':
if ( is_array( $message ) ) {
// Email multipart, ignora l'intestazione 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 = '';
}
// Evita di impostare un $content_type vuoto
} 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:
// Aggiunge all'array principale delle intestazioni
$headers[ trim( $name ) ] = trim( $content );
break;
}
}
}
}
// Svuota i valori che potrebbero essere impostati
$phpmailer->ClearAllRecipients();
$phpmailer->ClearAttachments();
$phpmailer->ClearCustomHeaders();
$phpmailer->ClearReplyTos();
$phpmailer->Body = ''; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$phpmailer->AltBody = ''; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
// Email e nome del mittente
// Se non abbiamo un nome dalle intestazioni di input
if ( ! isset( $from_name ) ) {
$from_name = 'WordPress';
}
/* Se non abbiamo un'email dalle intestazioni di input, usiamo wordpress@$sitename
* Alcuni host bloccheranno le email in uscita da questo indirizzo se non esiste,
* ma non c'è un'alternativa semplice. Usare admin_email potrebbe sembrare un'opzione,
* ma alcuni host potrebbero rifiutare di inoltrare email da un dominio sconosciuto.
* Vedi https://core.trac.wordpress.org/ticket/5007.
*/
if ( ! isset( $from_email ) ) {
// Ottiene il dominio del sito e rimuove 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 l'indirizzo email del mittente.
*
* @since 2.2.0
*
* @param string $from_email Indirizzo email del mittente.
*/
$phpmailer->From = apply_filters( 'wp_mail_from', $from_email ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
/**
* Filtra il nome associato all'indirizzo email "from".
*
* @since 2.3.0
*
* @param string $from_name Nome associato all'indirizzo email "from".
*/
$phpmailer->FromName = apply_filters( 'wp_mail_from_name', $from_name ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
// Imposta gli indirizzi destinatari
if ( ! is_array( $to ) ) {
$to = explode( ',', $to );
}
foreach ( (array) $to as $recipient ) {
try {
// Suddivide $recipient in nome e indirizzo se nel 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;
}
}
// Se non abbiamo un set di caratteri dalle intestazioni di input
if ( ! isset( $charset ) ) {
$charset = get_bloginfo( 'charset' );
}
// Imposta il tipo di contenuto e il set di caratteri
/**
* Filtra il set di caratteri predefinito di wp_mail().
*
* @since 2.3.0
*
* @param string $charset Set di caratteri predefinito per le email.
*/
$phpmailer->CharSet = apply_filters( 'wp_mail_charset', $charset ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
// Imposta oggetto e corpo dell'email
$phpmailer->Subject = $subject; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
if ( is_string( $message ) ) {
$phpmailer->Body = $message; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
// Imposta Content-Type e set di caratteri
// Se non abbiamo un content-type dalle intestazioni di input
if ( ! isset( $content_type ) ) {
$content_type = 'text/plain';
}
/**
* Filtra il tipo di contenuto di wp_mail().
*
* @since 2.3.0
*
* @param string $content_type Tipo di contenuto predefinito di wp_mail().
*/
$content_type = apply_filters( 'wp_mail_content_type', $content_type );
$phpmailer->ContentType = $content_type; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
// Imposta se è testo semplice, in base a $content_type
if ( 'text/html' === $content_type ) {
$phpmailer->IsHTML( true );
}
// Per compatibilità con le versioni precedenti, le nuove email multipart dovrebbero usare
// lo stile array per $message. Comunque non ha mai funzionato bene
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 );
}
}
}
}
// Aggiungi eventuali destinatari CC e BCC
if ( ! empty( $cc ) ) {
foreach ( (array) $cc as $recipient ) {
try {
// Suddivide $recipient in nome e indirizzo se nel 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 {
// Suddivide $recipient in nome e indirizzo se nel 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;
}
}
}
// Imposta l'uso della funzione mail() di PHP
$phpmailer->IsMail();
// Imposta intestazioni personalizzate
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;
}
}
}
/**
* Azione eseguita dopo l'inizializzazione di PHPMailer.
*
* @since 2.2.0
*
* @param PHPMailer &$phpmailer L'istanza PHPMailer, passata per riferimento.
*/
do_action_ref_array( 'phpmailer_init', [ &$phpmailer ] );
// Invia!
try {
return $phpmailer->Send();
} catch ( phpmailerException $e ) {
return new WP_Error( 'email-error', $e->getMessage() );
}
}

Se non vuoi creare alcun conflitto di codice nel core di WordPress, penso che la soluzione alternativa o più semplice sia aggiungere un'azione a phpmailer_init
che verrà eseguita prima dell'effettivo invio dell'email nella funzione wp_mail()
. Per semplificare la mia spiegazione, guarda l'esempio di codice qui sotto:
<?php
$to = '';
$subject = '';
$from = '';
$body = 'Il contenuto html del testo, <html>...';
$headers = "FROM: {$from}";
add_action( 'phpmailer_init', function ( $phpmailer ) {
$phpmailer->AltBody = 'Il contenuto testuale semplice del tuo originale contenuto html.';
} );
wp_mail($to, $subject, $body, $headers);
Se aggiungi il contenuto nella proprietà AltBody
della classe PHPMailer, il tipo di contenuto predefinito verrà automaticamente impostato su multipart/alternative
.

Ho esaminato attentamente l'implementazione di wp_mail($to, $subject, $message, $headers, $attachments)
in pluggable.php
e ho trovato una soluzione che non richiede modifiche al core.
La funzione wp_mail()
controlla l'argomento $headers
per un insieme specifico di tipi di intestazioni standard, ovvero from
, content-type
, cc
, bcc
e reply-to
.
Tutti gli altri tipi sono considerati intestazioni personalizzate e vengono elaborati separatamente. Ma ecco il punto: quando viene definita un'intestazione personalizzata, come nel tuo caso in cui hai impostato l'intestazione MIME-Version
, viene eseguito il seguente blocco di codice (all'interno di wp_mail()
):
// Imposta intestazioni personalizzate
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 ) );
}
}
Quell'istruzione if
annidata nello snippet sopra è il colpevole. Fondamentalmente, viene aggiunta un'altra intestazione Content-Type
come intestazione personalizzata nelle seguenti condizioni:
- È stata definita un'intestazione personalizzata (hai definito
MIME-Version
nello scenario descritto nel tuo post). - Il tipo MIME dell'intestazione
Content-Type
contiene la stringamultipart
. - È stato impostato un boundary per le parti multiple.
La soluzione più rapida nel tuo caso è rimuovere l'intestazione MIME-Version
. La maggior parte degli user agent la aggiunge automaticamente, quindi rimuoverla non dovrebbe essere un problema.
Ma cosa fare se vuoi aggiungere intestazioni personalizzate senza generare un'intestazione Content-Type
duplicata?
SOLUZIONE:
NON impostare esplicitamente l'intestazione Content-Type
nell'array $headers
quando aggiungi intestazioni personalizzate, fai invece quanto segue:
$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 prima riga dello snippet sopra potrebbe sembrare strana, ma la funzione wp_mail()
imposterà internamente la sua variabile $boundary
purché una definizione di boundary appaia su una riga da sola senza essere preceduta da Content-Type:
. Quindi puoi utilizzare i filtri per impostare rispettivamente il content-type
e il charset
. In questo modo soddisfi le condizioni per eseguire il blocco di codice per l'impostazione delle intestazioni personalizzate senza aggiungere esplicitamente Content-Type: [mime-type]; [boundary];
.
Non è necessario modificare l'implementazione core di wp_mail()
, per quanto possa essere difettosa.
