摘要

由于WP Core中存在错误,使用wp_mail()发送多部分电子邮件(html /文本)(以减少电子邮件最终进入垃圾邮件文件夹的可能性)将讽刺地导致您的域被Hotmail(和其他Microsoft电子邮件)。

这是一个复杂的问题,我将详细分解以帮助某人找到可行的解决方案,并最终在内核中实现。

这将是一个有益的阅读。让我们开始...

错误

避免时事通讯电子邮件最终被放入垃圾邮件文件夹的最常见建议是发送多部分邮件。 />多部分(MIME)是指在一封电子邮件中同时发送电子邮件的HTML和TEXT部分。客户端收到多部分消息时,如果可以呈现HTML,则接受HTML版本,否则将呈现纯文本版本。

这被证明是可行的。发送到gmail时,我们所有的电子邮件都将放入垃圾邮件文件夹,直到当它们到达主收件箱时我们将邮件更改为多部分。好东西。

现在,当通过wp_mail()发送多段消息时,它将两次输出内容类型(multipart / *),一次带有边界(如果是自定义设置),一次则没有边界。此行为导致电子邮件被显示为原始消息,而不是包括某些所有Microsoft(Hotmail,Outlook等)在内的某些电子邮件的一部分。

Microsoft会将此消息标记为垃圾邮件,并且收件人将手动标记通过的少量邮件。不幸的是,Microsoft电子邮件地址被广泛使用。我们有40%的订户使用它。

Microsoft近期通过一次电子邮件交换确认了此消息。

邮件标记将导致域被完全阻止。这意味着该邮件将不会发送到垃圾邮件文件夹,甚至根本不会传递给收件人。

到目前为止,我们已经屏蔽了3次主域名。

因为这是WP内核中的错误,所以发送多部分消息的每个域都被阻止。问题是大多数网站管理员都不知道为什么。我在进行研究时发现这一点,并看到其他用户在论坛等上进行讨论。这需要深入研究原始代码,并对这些电子邮件的工作方式有充分的了解,我们将继续下一步...

让我们将其分解为代码

创建一个hotmail / outlook帐户。然后,运行以下代码:

// Set $to to an hotmail.com or outlook.com email
$to = "YourEmail@hotmail.com";

$subject = 'wp_mail testing multipart';

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

Hello world! This is plain text...


------=_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>Hello World! This is 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"';


// send email
wp_mail( $to, $subject, $message, $headers );


如果要更改默认内容类型,请使用:

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


这将发送多部分消息。

因此,如果您检查消息的完整原始来源,您会注意到内容类型被添加了两次,一次都没有边界:

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


这就是问题所在。

问题的根源在于pluggable.php-如果我们在这里查看:

// Set Content-Type and charset
    // If we don't have a content-type from the input headers
    if ( !isset( $content_type ) )
        $content_type = 'text/plain';

    /**
     * Filter the wp_mail() content type.
     *
     * @since 2.3.0
     *
     * @param string $content_type Default wp_mail() content type.
     */
    $content_type = apply_filters( 'wp_mail_content_type', $content_type );

    $phpmailer->ContentType = $content_type;

    // Set whether it's plaintext, depending on $content_type
    if ( 'text/html' == $content_type )
        $phpmailer->IsHTML( true );

    // If we don't have a charset from the input headers
    if ( !isset( $charset ) )
        $charset = get_bloginfo( 'charset' );

    // Set the content-type and charset

    /**
     * Filter the default wp_mail() charset.
     *
     * @since 2.3.0
     *
     * @param string $charset Default email charset.
     */
    $phpmailer->CharSet = apply_filters( 'wp_mail_charset', $charset );

    // Set custom headers
    if ( !empty( $headers ) ) {
        foreach( (array) $headers as $name => $content ) {
            $phpmailer->AddCustomHeader( sprintf( '%1$s: %2$s', $name, $content ) );
        }

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

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


潜在的解决方案

所以您想知道,为什么不在Trac上报告此事?我已经有了。令我惊讶的是,五年前创建了另一张票证,概述了相同的问题。

面对现实吧,已经过去了五年了。在互联网时代,大约是30岁。这个问题显然已经被放弃了,并且基本上永远不会得到解决(...除非我们在这里解决,否则)。

我在这里找到了一个很好的线索解决方案,但是当他的解决方案有效时,它会破坏没有设置自定义$headers的电子邮件。

这就是我们每次崩溃的地方。多部分版本可以正常工作,而正常的未设置$headers消息不起作用,或者不建议使用。

我们提出的解决方案是:

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

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

    $phpmailer->ContentType = $content_type;

    // Set whether it's plaintext, depending on $content_type
    if ( 'text/html' == $content_type )
        $phpmailer->IsHTML( true );

    // If we don't have a charset from the input headers
    if ( !isset( $charset ) )
        $charset = get_bloginfo( 'charset' );
}

// Set the content-type and charset

/**
 * Filter the default wp_mail() charset.
 *
 * @since 2.3.0
 *
 * @param string $charset Default email charset.
 */
$phpmailer->CharSet = apply_filters( 'wp_mail_charset', $charset );

// Set custom headers
if ( !empty( $headers ) ) {
    foreach( (array) $headers as $name => $content ) {
        $phpmailer->AddCustomHeader( sprintf( '%1$s: %2$s', $name, $content ) );
    }

}


是的,我知道,编辑核心文件是禁忌,请坐下来...这是一个绝望的修复,并且是为内核提供修复的错误尝试。

我们修复程序的问题在于,默认电子邮件(例如新注册,评论,密码重置等)将作为空白邮件发送。因此,我们有一个有效的wp_mail()脚本,该脚本将发送多部分消息,但没有其他内容。

做什么

这里的目的是找到一种使用核心wp_mail()函数(而不是自定义sendmail)发送常规(纯文本)和多部分消息的方法。功能)。

尝试解决此问题时,您将遇到的主要问题是花在发送虚假消息,检查是否收到虚假消息以及基本上打开一盒阿司匹林和诅咒上的时间在Microsoft,因为不幸的是,这里的gremlin是WordPress,您已经习惯了他们的IE问题。

更新

@bonger发布的解决方案允许$message成为包含内容类型键替代项的数组。我已经确认它可以在所有情况下使用。

我们将允许这个问题一直悬而未决,直到赏金用完为止,以提高人们对该问题的认识为止,也许可以解决该问题。随时发布$message可以是字符串的替代解决方案。

评论

由于wp_mail()函数是可插入的,不是将替代品定义为必须使用的插件(在wp-content / mu-plugins中)对您不是一个好的解决方案(以及其他所有人,都无法通过核心修复)?在这种情况下,设置$ phpmailer-> ContentType = $ content_type后,不会将多部分/边界检查移至; (而不是其他)不起作用?

@bonger您能写出详细说明您的解决方案的答案吗?

不需要编辑核心,因为wp_mail是可插入的。将原始功能复制到插件中,根据需要进行编辑,然后激活插件。 WordPress将使用您编辑的功能而不是原始功能,而无需编辑核心。

@ChristineCooper我很犹豫地执行此操作,正如您所说的那样,测试是一种皇家痛苦,但请查看@ rmccue / @ MattyRob在Trac中建议的补丁core.trac.wordpress.org/ticket/15448,这看起来是一种非常不错的方法去吧,我将基于此发布未经测试的答案...

@ChristineCooper如果您简单地挂接到phpmailer并在$ phpmailer-> AltBody中设置文本正文,是否会发生相同的错误?

#1 楼

wp_mail()的以下版本在票证https://core.trac.wordpress.org/ticket/15448中应用了@ rmccue / @ MattyRob的修补程序,刷新了4.2.2,使$message成为包含内容的数组类型的键替代项:

/**
 * Send mail, similar to PHP's mail
 *
 * A true return value does not automatically mean that the user received the
 * email successfully. It just only means that the method used was able to
 * process the request without any errors.
 *
 * Using the two 'wp_mail_from' and 'wp_mail_from_name' hooks allow from
 * creating a from address like 'Name <email@address.com>' when both are set. If
 * just 'wp_mail_from' is set, then just the email address will be used with no
 * name.
 *
 * The default content type is 'text/plain' which does not allow using HTML.
 * However, you can set the content type of the email by using the
 * 'wp_mail_content_type' filter.
 *
 * If $message is an array, the key of each is used to add as an attachment
 * with the value used as the body. The 'text/plain' element is used as the
 * text version of the body, with the 'text/html' element used as the HTML
 * version of the body. All other types are added as attachments.
 *
 * The default charset is based on the charset used on the blog. The charset can
 * be set using the 'wp_mail_charset' filter.
 *
 * @since 1.2.1
 *
 * @uses PHPMailer
 *
 * @param string|array $to Array or comma-separated list of email addresses to send message.
 * @param string $subject Email subject
 * @param string|array $message Message contents
 * @param string|array $headers Optional. Additional headers.
 * @param string|array $attachments Optional. Files to attach.
 * @return bool Whether the email contents were sent successfully.
 */
function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() ) {
    // Compact the input, apply the filters, and extract them back out

    /**
     * Filter the wp_mail() arguments.
     *
     * @since 2.2.0
     *
     * @param array $args A compacted array of wp_mail() arguments, including the "to" email,
     *                    subject, message, headers, and attachments values.
     */
    $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)create it, if it's gone missing
    if ( ! ( $phpmailer instanceof PHPMailer ) ) {
        require_once ABSPATH . WPINC . '/class-phpmailer.php';
        require_once ABSPATH . WPINC . '/class-smtp.php';
        $phpmailer = new PHPMailer( true );
    }

    // Headers
    if ( empty( $headers ) ) {
        $headers = array();
    } else {
        if ( !is_array( $headers ) ) {
            // Explode the headers out, so this function can take both
            // string headers and an array of headers.
            $tempheaders = explode( "\n", str_replace( "\r\n", "\n", $headers ) );
        } else {
            $tempheaders = $headers;
        }
        $headers = array();
        $cc = array();
        $bcc = array();

        // If it's actually got contents
        if ( !empty( $tempheaders ) ) {
            // Iterate through the raw headers
            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 them out
                list( $name, $content ) = explode( ':', trim( $header ), 2 );

                // Cleanup crew
                $name    = trim( $name    );
                $content = trim( $content );

                switch ( strtolower( $name ) ) {
                    // Mainly for legacy -- process a From: header if it's there
                    case 'from':
                        $bracket_pos = strpos( $content, '<' );
                        if ( $bracket_pos !== false ) {
                            // Text before the bracketed email is the "From" name.
                            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 );

                        // Avoid setting an empty $from_email.
                        } elseif ( '' !== trim( $content ) ) {
                            $from_email = trim( $content );
                        }
                        break;
                    case 'content-type':
                        if ( is_array($message) ) {
                            // Multipart email, ignore the content-type header
                            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 = '';
                            }

                        // Avoid setting an empty $content_type.
                        } elseif ( '' !== trim( $content ) ) {
                            $content_type = trim( $content );
                        }
                        break;
                    case 'cc':
                        $cc = array_merge( (array) $cc, explode( ',', $content ) );
                        break;
                    case 'bcc':
                        $bcc = array_merge( (array) $bcc, explode( ',', $content ) );
                        break;
                    default:
                        // Add it to our grand headers array
                        $headers[trim( $name )] = trim( $content );
                        break;
                }
            }
        }
    }

    // Empty out the values that may be set
    $phpmailer->ClearAllRecipients();
    $phpmailer->ClearAttachments();
    $phpmailer->ClearCustomHeaders();
    $phpmailer->ClearReplyTos();

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

    // From email and name
    // If we don't have a name from the input headers
    if ( !isset( $from_name ) )
        $from_name = 'WordPress';

    /* If we don't have an email from the input headers default to wordpress@$sitename
     * Some hosts will block outgoing mail from this address if it doesn't exist but
     * there's no easy alternative. Defaulting to admin_email might appear to be another
     * option but some hosts may refuse to relay mail from an unknown domain. See
     * https://core.trac.wordpress.org/ticket/5007.
     */

    if ( !isset( $from_email ) ) {
        // Get the site domain and get rid of www.
        $sitename = strtolower( $_SERVER['SERVER_NAME'] );
        if ( substr( $sitename, 0, 4 ) == 'www.' ) {
            $sitename = substr( $sitename, 4 );
        }

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

    /**
     * Filter the email address to send from.
     *
     * @since 2.2.0
     *
     * @param string $from_email Email address to send from.
     */
    $phpmailer->From = apply_filters( 'wp_mail_from', $from_email );

    /**
     * Filter the name to associate with the "from" email address.
     *
     * @since 2.3.0
     *
     * @param string $from_name Name associated with the "from" email address.
     */
    $phpmailer->FromName = apply_filters( 'wp_mail_from_name', $from_name );

    // Set destination addresses
    if ( !is_array( $to ) )
        $to = explode( ',', $to );

    foreach ( (array) $to as $recipient ) {
        try {
            // Break $recipient into name and address parts if in the format "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;
        }
    }

    // If we don't have a charset from the input headers
    if ( !isset( $charset ) )
        $charset = get_bloginfo( 'charset' );

    // Set the content-type and charset

    /**
     * Filter the default wp_mail() charset.
     *
     * @since 2.3.0
     *
     * @param string $charset Default email charset.
     */
    $phpmailer->CharSet = apply_filters( 'wp_mail_charset', $charset );

    // Set mail's subject and body
    $phpmailer->Subject = $subject;

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

        // Set Content-Type and charset
        // If we don't have a content-type from the input headers
        if ( !isset( $content_type ) )
            $content_type = 'text/plain';

        /**
         * Filter the wp_mail() content type.
         *
         * @since 2.3.0
         *
         * @param string $content_type Default wp_mail() content type.
         */
        $content_type = apply_filters( 'wp_mail_content_type', $content_type );

        $phpmailer->ContentType = $content_type;

        // Set whether it's plaintext, depending on $content_type
        if ( 'text/html' == $content_type )
            $phpmailer->IsHTML( true );

        // For backwards compatibility, new multipart emails should use
        // the array style $message. This never really worked well anyway
        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);
                }
            }
        }
    }

    // Add any CC and BCC recipients
    if ( !empty( $cc ) ) {
        foreach ( (array) $cc as $recipient ) {
            try {
                // Break $recipient into name and address parts if in the format "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 {
                // Break $recipient into name and address parts if in the format "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;
            }
        }
    }

    // Set to use PHP's mail()
    $phpmailer->IsMail();

    // Set custom headers
    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;
            }
        }
    }

    /**
     * Fires after PHPMailer is initialized.
     *
     * @since 2.2.0
     *
     * @param PHPMailer &$phpmailer The PHPMailer instance, passed by reference.
     */
    do_action_ref_array( 'phpmailer_init', array( &$phpmailer ) );

    // Send!
    try {
        return $phpmailer->Send();
    } catch ( phpmailerException $e ) {
        return false;
    }
}


因此,如果将其放在“ wp-content / mu-plugins / functions.php”文件中,它将覆盖WP版。它具有很好的用法而不会弄乱标题,例如:

// Set $to to an hotmail.com or outlook.com email
$to = "YourEmail@hotmail.com";

$subject = 'wp_mail testing multipart';

$message['text/plain'] = 'Hello world! This is plain text...';
$message['text/html'] = '<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>

<p>Hello World! This is 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'; } );

// send email
wp_mail( $to, $subject, $message );

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


请注意,我还没有用实际的电子邮件对此进行过测试...

评论


我添加了它,使其必须具有插件并运行了测试代码;有效。我已经测试了默认的核心通知(新用户通知等),并且它也起作用。我将在本周末继续进行测试,并查看插件将如何工作以及基本上一切正常。我将特别浏览消息的原始数据。这将是一项非常耗时的任务,但是请放心,我会在完成后向您报告。如果存在wp_mail()无法正常工作的情况(本来应该这样),请告诉我。感谢您的回答。

–克里斯汀·库珀♦
15年6月20日在15:43

好东西,我看了看输出,它看起来不错-实际上,该补丁仅使wp_mail在传递数组的情况下使用PHPMailer的标准坚如磐石处理,否则默认为狡猾的WP东西(以实现向后兼容性)所以应该很好(显然,这里的补丁作者感到很荣幸)...我将从现在开始使用它(并最终进行改装)-并感谢您再次使用html / plain减少被垃圾邮件污染的机会...

–bonger
15年6月20日在21:02

我们已经在所有可能的情况下对其进行了测试,并且效果很好。我们明天将发布新闻通讯,我们将查看是否收到用户的任何投诉。我们需要做的仅有的微小更改是在将数组插入db中时对它进行清理/清理(在que中有消息,cron在其中将cron批量发送出去)。我将允许悬而未决的问题悬而未决,直到赏金用完为止,以便我们提高对此问题的认识。希望此补丁或替代方法将添加到核心中。更重要的是,为什么不呢?他们在想什么!

–克里斯汀·库珀♦
15年6月21日在15:53

我随机注意到您对链接的跟踪单进行了更新。这是此代码的更新吗?如果是这样,您是否也可以通过在此处编辑答案来发布此更新,以使该答案保持最新?非常感谢你。

–克里斯汀·库珀♦
2015年9月8日23:49



嗨,不,这只是针对当前主干的补丁更新,以便它可以合并而不会发生冲突(希望引起注意),代码完全相同...

–bonger
2015年9月9日下午4:05

#2 楼

这根本不是一个WordPress错误,它是一个phpmailer之一,它不允许自定义标头...如果您查看class-phpmailer.php

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


您可以看到令人讨厌的默认情况是输出带有字符集且无边界的额外标题行的情况。通过过滤器设置内容类型本身并不能单独解决,这是因为通过检查alt是否为空而不是内容类型,在message_type上设置了AltBody的大小写。

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

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


最后,这意味着一旦附加文件或嵌入式图像或设置AltBody,就应该绕过令人讨厌的错误。这也意味着不需要显式设置内容类型,因为一旦存在AltBody,它就会被multipart/alternative设置为phpmailer

所以简单的答案是:

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


那么您无需显式设置标题,您只需执行以下操作即可:

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

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


不幸的是,许多功能和phpmailer类中的属性受到保护,如果不是这样,则有效的选择是在发送之前通过MIMEHeaders钩子简单地检查并覆盖phpmailer_init属性。

#3 楼

对于使用phpmailer_init挂钩添加自己的'AltBody'的任何人:

除非您手动清除,否则替代文本正文将重新用于发送的不同连续邮件! WordPress不会在wp_mail()中清除它,因为它不希望使用此属性。

这导致收件人可能会收到不适合他们的邮件。幸运的是,大多数使用支持HTML的邮件客户端的人都看不到文本版本,但基本上还是一个安全问题。

幸运的是,有一个简单的解决方法。这包括高身替换位;请注意,您确实需要Html2Text PHP库:

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

  if ( $phpmailer instanceof PHPMailer && $phpmailer->alternativeExists() ) {
    // AltBody property is set, so WordPress must already have used this
    // $phpmailer object just now to send mail, so let's
    // clear the AltBody property
    $phpmailer->AltBody = '';
  }

  // Return untouched atts
  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 );
  }
}


这也是我修改来解决此问题的WP插件的要旨:https://gist.github.com / youri-/ c4618740b7c50c549314eaebc9f78661

由于我没有足够的代表要发表评论,因此我无法对使用上述钩子的其他解决方案发表评论,以警告他们。 >

#4 楼

我刚刚发布了一个插件,以使用户可以在WordPress上使用html模板,而我现在在dev版本上玩,以添加简单的文本后备广告。我做了以下操作,在测试中,我只看到添加了一个边界,并且电子邮件对Hotmail的响应良好。

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

/**
* Modify php mailer body with final email
*
* @since 1.0.0
* @param object $phpmailer
*/
function send_email( $phpmailer ) {

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


所以基本上我在这里所做的就是修改phpmailer object,将消息加载到HTML模板中,并将其设置为Body属性。另外,我还接受了原始消息并设置了AltBody属性。

#5 楼

我的简单解决方案是以这种方式使用html2text https://github.com/soundasleep/html2text:
add_action( 'phpmailer_init', 'phpmailer_init' );

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

这里https://gist.github.com/frugan-it/6c4d22cd856456480bd77b988b5c9e80也是要点关于。

#6 楼

这可能不是这里最初文章的确切答案,但是它是此处提供的一些关于设置替代主体的解决方案的替代方法

基本上,我需要(希望)设置不同的替代主体(即纯文本)添加到HTML部分,而不是依赖于某些转换/标签和诸如此类的内容。

所以我想出了这似乎很好用的方法:

/* setting the message parts for wp_mail()*/
$markup = array();
$markup['html'] = '<html>some html</html>';
$markup['plaintext'] = 'some plaintext';
/* message we are sending */    
$message = maybe_serialize($markup);


/* setting alt body distinctly */
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'];
        }
    }   
}


#7 楼

wp_mail()的此版本基于@bonger的代码。它具有以下更改:


代码样式修复(通过PHPCS)
处理$ message为数组或字符串的情况(确保与WP 5.x兼容)
引发异常而不返回假
短数组语法

 <?php
/**
 * Adapted from https://wordpress.stackexchange.com/a/191974/8591
 *
 * Send mail, similar to PHP's mail
 *
 * A true return value does not automatically mean that the user received the
 * email successfully. It just only means that the method used was able to
 * process the request without any errors.
 *
 * Using the two 'wp_mail_from' and 'wp_mail_from_name' hooks allow from
 * creating a from address like 'Name <email@address.com>' when both are set. If
 * just 'wp_mail_from' is set, then just the email address will be used with no
 * name.
 *
 * The default content type is 'text/plain' which does not allow using HTML.
 * However, you can set the content type of the email by using the
 * 'wp_mail_content_type' filter.
 *
 * If $message is an array, the key of each is used to add as an attachment
 * with the value used as the body. The 'text/plain' element is used as the
 * text version of the body, with the 'text/html' element used as the HTML
 * version of the body. All other types are added as attachments.
 *
 * The default charset is based on the charset used on the blog. The charset can
 * be set using the 'wp_mail_charset' filter.
 *
 * @since 1.2.1
 *
 * @uses PHPMailer
 *
 * @param string|array $to Array or comma-separated list of email addresses to send message.
 * @param string $subject Email subject
 * @param string|array $message Message contents
 * @param string|array $headers Optional. Additional headers.
 * @param string|array $attachments Optional. Files to attach.
 * @return bool Whether the email contents were sent successfully.
 */
public static function wp_mail( $to, $subject, $message, $headers = '', $attachments = [] ) {
    // Compact the input, apply the filters, and extract them back out

    /**
     * Filter the wp_mail() arguments.
     *
     * @since 2.2.0
     *
     * @param array $args A compacted array of wp_mail() arguments, including the "to" email,
     *                    subject, message, headers, and attachments values.
     */
    $atts = apply_filters( 'wp_mail', compact( 'to', 'subject', 'headers', 'attachments' ) );

    // Since $message is an array, and will wp_staticize_emoji_for_email() expects strings, walk over it one item at a time
    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)create it, if it's gone missing
    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
    }

    // Headers
    if ( empty( $headers ) ) {
        $headers = [];
    } else {
        if ( ! is_array( $headers ) ) {
            // Explode the headers out, so this function can take both
            // string headers and an array of headers.
            $tempheaders = explode( "\n", str_replace( "\r\n", "\n", $headers ) );
        } else {
            $tempheaders = $headers;
        }
        $headers = [];
        $cc      = [];
        $bcc     = [];

        // If it's actually got contents
        if ( ! empty( $tempheaders ) ) {
            // Iterate through the raw headers
            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;
                }
                // Explode them out
                list( $name, $content ) = explode( ':', trim( $header ), 2 );

                // Cleanup crew
                $name    = trim( $name );
                $content = trim( $content );

                switch ( strtolower( $name ) ) {
                    // Mainly for legacy -- process a From: header if it's there
                    case 'from':
                        $bracket_pos = strpos( $content, '<' );
                        if ( false !== $bracket_pos ) {
                            // Text before the bracketed email is the "From" name.
                            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 );

                            // Avoid setting an empty $from_email.
                        } elseif ( '' !== trim( $content ) ) {
                            $from_email = trim( $content );
                        }
                        break;
                    case 'content-type':
                        if ( is_array( $message ) ) {
                            // Multipart email, ignore the content-type header
                            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  = '';
                            }

                            // Avoid setting an empty $content_type.
                        } elseif ( '' !== trim( $content ) ) {
                            $content_type = trim( $content );
                        }
                        break;
                    case 'cc':
                        $cc = array_merge( (array) $cc, explode( ',', $content ) );
                        break;
                    case 'bcc':
                        $bcc = array_merge( (array) $bcc, explode( ',', $content ) );
                        break;
                    default:
                        // Add it to our grand headers array
                        $headers[ trim( $name ) ] = trim( $content );
                        break;
                }
            }
        }
    }

    // Empty out the values that may be set
    $phpmailer->ClearAllRecipients();
    $phpmailer->ClearAttachments();
    $phpmailer->ClearCustomHeaders();
    $phpmailer->ClearReplyTos();

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

    // From email and name
    // If we don't have a name from the input headers
    if ( ! isset( $from_name ) ) {
        $from_name = 'WordPress';
    }

    /* If we don't have an email from the input headers default to wordpress@$sitename
     * Some hosts will block outgoing mail from this address if it doesn't exist but
     * there's no easy alternative. Defaulting to admin_email might appear to be another
     * option but some hosts may refuse to relay mail from an unknown domain. See
     * https://core.trac.wordpress.org/ticket/5007.
     */

    if ( ! isset( $from_email ) ) {
        // Get the site domain and get rid of 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;
    }

    /**
     * Filter the email address to send from.
     *
     * @since 2.2.0
     *
     * @param string $from_email Email address to send from.
     */
    $phpmailer->From = apply_filters( 'wp_mail_from', $from_email ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase

    /**
     * Filter the name to associate with the "from" email address.
     *
     * @since 2.3.0
     *
     * @param string $from_name Name associated with the "from" email address.
     */
    $phpmailer->FromName = apply_filters( 'wp_mail_from_name', $from_name ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase

    // Set destination addresses
    if ( ! is_array( $to ) ) {
        $to = explode( ',', $to );
    }

    foreach ( (array) $to as $recipient ) {
        try {
            // Break $recipient into name and address parts if in the format "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;
        }
    }

    // If we don't have a charset from the input headers
    if ( ! isset( $charset ) ) {
        $charset = get_bloginfo( 'charset' );
    }

    // Set the content-type and charset

    /**
     * Filter the default wp_mail() charset.
     *
     * @since 2.3.0
     *
     * @param string $charset Default email charset.
     */
    $phpmailer->CharSet = apply_filters( 'wp_mail_charset', $charset ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase

    // Set mail's subject and body
    $phpmailer->Subject = $subject; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase

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

        // Set Content-Type and charset
        // If we don't have a content-type from the input headers
        if ( ! isset( $content_type ) ) {
            $content_type = 'text/plain';
        }

        /**
         * Filter the wp_mail() content type.
         *
         * @since 2.3.0
         *
         * @param string $content_type Default wp_mail() content type.
         */
        $content_type = apply_filters( 'wp_mail_content_type', $content_type );

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

        // Set whether it's plaintext, depending on $content_type
        if ( 'text/html' === $content_type ) {
            $phpmailer->IsHTML( true );
        }

        // For backwards compatibility, new multipart emails should use
        // the array style $message. This never really worked well anyway
        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 );
                }
            }
        }
    }

    // Add any CC and BCC recipients
    if ( ! empty( $cc ) ) {
        foreach ( (array) $cc as $recipient ) {
            try {
                // Break $recipient into name and address parts if in the format "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 {
                // Break $recipient into name and address parts if in the format "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;
            }
        }
    }

    // Set to use PHP's mail()
    $phpmailer->IsMail();

    // Set custom headers
    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;
            }
        }
    }

    /**
     * Fires after PHPMailer is initialized.
     *
     * @since 2.2.0
     *
     * @param PHPMailer &$phpmailer The PHPMailer instance, passed by reference.
     */
    do_action_ref_array( 'phpmailer_init', [ &$phpmailer ] );

    // Send!
    try {
        return $phpmailer->Send();
    } catch ( phpmailerException $e ) {
        return new WP_Error( 'email-error', $e->getMessage() );
    }
}
 


评论


嗨,保罗。短数组语法早于PHP 7+;)

– kaiser
20-2-19在21:37

#8 楼

如果您不想在WordPress核心中创建任何代码冲突,我认为替代或最简单的解决方案是在phpmailer_init函数中实际发送邮件之前向wp_mail()添加操作。为了简化说明,请参见以下代码示例:

<?php 

$to = '';
$subject = '';
$from = '';
$body = 'The text html content, <html>...';

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

add_action( 'phpmailer_init', function ( $phpmailer ) {
    $phpmailer->AltBody = 'The text plain content of your original text html content.';
} );

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


如果在PHPMailer类AltBody属性中添加内容,则默认内容类型将自动设置为multipart/alternative。 />

#9 楼

仔细研究了wp_mail($to, $subject, $message, $headers, $attachments)pluggable.php的实现,发现了不需要修补内核的解决方案。
wp_mail()函数检查$headers参数中是否包含特定的标准标头类型集合,即fromcontent-typeccbccreply-to
所有其他类型均指定为自定义标头并单独处理。但是,在定义了自定义标头后,就像在设置MIME-Version标头的情况下一样,下面的代码块被执行(在wp_mail()内部):
 // Set custom headers
if ( ! empty( $headers ) ) {
    foreach ( (array) $headers as $name => $content ) {
        $phpmailer->addCustomHeader( sprintf( '%1$s: %2$s', $name, $content ) );
    }

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

上述片段中嵌套的if语句是罪魁祸首。基本上,在以下情况下,将另一个Content-Type标头添加为自定义标头:

定义了自定义标头(在您所描述的场景中定义了MIME-Version)。标头包含字符串Content-Type
设置了多部分边界。

这种情况下,最快的解决方法是删除multipart标头。大多数用户代理始终会自动添加该标头,因此将其删除应该不是问题。
如果要添加自定义标头而不生成重复的MIME-Version标头怎么办?
解决方案:
不要在添加自定义标头时,在Content-Type数组中显式设置Content-Type标头,请改为执行以下操作:
 $headers 

上面的代码片段可能令人困惑,但是只要边界定义出现在自己的行上而不以$headers = 'boundary="----=_Part_18243133_1346573420.1408991447668"\r\n'; $headers .= "MIME-Version: 1.0\r\n"; $headers .= "From: Foo <foo@bar.com>\r\n"; function set_content_type( $content_type ) { return 'multipart/alternative'; } function set_charset( $char_set ) { return 'utf-8'; } add_filter( 'wp_mail_content_type', 'set_content_type' ); add_filter( 'wp_mail_charset', 'set_charset' ); 为前缀,wp_mail()函数就会在内部设置其$boundary变量。然后,您可以跟上过滤器挂钩以分别设置Content-Type:content-type。这样,您就可以满足执行用于设置自定义标头的代码块的条件,而无需显式添加charset
无需接触Content-Type: [mime-type]; [boundary];的核心实现,尽管可能有问题。