通过wp_mail()发送多部分(文本/html)电子邮件可能会导致您的域名被禁止

时间:2015-06-18 作者:Christine Cooper

摘要

由于WP Core中的错误,发送multipart 电子邮件(html/文本)wp_mail() (为了减少电子邮件进入垃圾邮件文件夹的可能性)具有讽刺意味的是,你的域名会被Hotmail(和其他Microsoft电子邮件)阻止。

这是一个复杂的问题,我将对其进行详细分析,以帮助他人找到一个可行的解决方案,最终可能在core中实现。

It\'s going to be a rewarding read. Let\'s begin...

错误

避免新闻稿电子邮件被垃圾文件夹截留的最常见建议是发送多部分邮件。

多部分(mime)是指在一封电子邮件中同时发送电子邮件的HTML和文本部分。当客户端接收到多部分消息时,如果它可以呈现HTML,则它接受HTML版本,否则它将呈现纯文本版本。

这被证明是有效的。在发送到gmail时,我们所有的电子邮件都会被放在垃圾邮件文件夹中,直到我们将邮件发送到主收件箱时改为多部分邮件。很棒的东西。

现在,当通过wp\\u mail()发送多部分消息时,它会输出两次内容类型(multipart/*),一次带边界(如果自定义设置),一次不带边界。这种行为导致电子邮件显示为原始消息,而不是在某些电子邮件上显示为多部分,包括all Microsoft(Hotmail、Outlook等)

Microsoft将此邮件标记为垃圾邮件,收件人将手动标记收到的少数邮件<不幸的是,Microsoft电子邮件地址被广泛使用。40%的用户使用它。

微软最近通过电子邮件交流证实了这一点。

标记消息将导致域完全blocked. 这意味着邮件不会发送到垃圾邮件文件夹,they will not even be delivered 致收件人。

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

因为这是WP核心中的一个bug,every 正在阻止发送多部分消息的域。问题是大多数站长不知道为什么。我在做研究和看到其他用户在论坛上讨论这一点时已经证实了这一点。这需要深入研究原始代码,并对这些类型的电子邮件如何工作有很好的了解,我们接下来将继续。。。

让我们将其分解为代码,创建一个hotmail/outlook帐户。然后,运行以下代码:

// Set $to to an hotmail.com or outlook.com email
$to = "[email protected]";

$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 <[email protected]>\\r\\n";
$headers .= \'Content-Type: multipart/alternative;boundary="----=_Part_18243133_1346573420.1408991447668"\';


// send email
wp_mail( $to, $subject, $message, $headers );
如果你想改变default content type, 使用:

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;
            }
        }
    }
潜在的解决方案,why have you not reported this at trac?already have. 令我大吃一惊的是different ticket 创建于5年前,概述了相同的问题。

让我们面对现实吧,已经五年了。在互联网时代,这更像是30岁。这个问题显然已经被抛弃,基本上永远不会得到解决(……除非我们在这里解决它)。

我找到了一个很棒的thread here 提供了一个解决方案,但虽然他的解决方案有效,但它会中断没有自定义的电子邮件$headers 设置

那就是我们每次撞车的地方。要么多部分版本工作正常,要么正常取消设置$headers 信息不会,或是用V字形表示诗句。

我们提出的解决方案是:

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 ) );
    }

}
是的,我知道,编辑核心文件是禁忌,坐下。。。这是一个绝望的修复,也是为core提供修复的拙劣尝试。

我们解决的问题是,新注册、评论、密码重置等默认电子邮件将作为空白消息发送。因此,我们有一个工作中的wp\\u mail()脚本,它将发送多部分消息,而不发送其他消息。

要做的是找到一种发送普通(纯文本)和多部分消息的方法using the core wp_mail() function (不是自定义sendmail功能)。

当你试图解决这个问题时,你会遇到的主要问题是,你会花大量时间发送虚假消息,检查是否收到这些消息,基本上打开一盒阿斯匹林,诅咒微软,因为你习惯了他们的IE问题,而这里的小精灵不幸是WordPress。

更新@bonger发布的解决方案允许$message 是一个包含内容类型键控交替的数组。我已经证实,它在所有情况下都有效。

我们会让这个问题一直悬而未决,直到赏金耗尽,以提高人们对这个问题的认识,maybe to a level where it will be fixed in core. 请随时发布替代解决方案,其中$message 可以是字符串。

9 个回复
最合适的回答,由SO网友:bonger 整理而成

以下版本的wp_mail() 在票据中应用了@rmccue/@MattyRob的补丁https://core.trac.wordpress.org/ticket/15448, 刷新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 protected]>\' 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 <[email protected]>"
            $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 <[email protected]>"
                $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 <[email protected]>"
                $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;
    }
}
因此,如果您将其放在eg“wp-content/mu-plugins/functions.php”文件中,那么它将覆盖wp版本。它有一个很好的用法,没有任何混乱的标题,例如:

// Set $to to an hotmail.com or outlook.com email
$to = "[email protected]";

$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 \'[email protected]\'; } );
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 );
请注意,我还没有用实际的电子邮件测试这一点。。。

SO网友:majick

TLDR, 简单的解决方案是:

add_action(\'phpmailer_init\',\'wp_mail_set_text_body\');
function wp_mail_set_text_body($phpmailer) {
     if (empty($phpmailer->AltBody)) {$phpmailer->AltBody = wp_strip_all_tags($phpmailer->Body);}
}
那么您根本不需要显式设置标题,标题边界已经为您正确设置。

请继续阅读至,以获取有关以下原因的详细解释。。。

这根本不是WordPress的bug,而是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, 应该绕过有问题的bug。这还意味着不需要显式设置内容类型,因为一旦AltBody 设置为multipart/alternative 通过phpmailer.

因此,简单的答案是:

add_action(\'phpmailer_init\',\'wp_mail_set_text_body\');
function wp_mail_set_text_body($phpmailer) {
     if (empty($phpmailer->AltBody)) {$phpmailer->AltBody = wp_strip_all_tags($phpmailer->Body);}
}
然后,无需显式设置标头,只需执行以下操作:

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

 wp_mail($to,$subject,$message);
不幸的是phpmailer 类是受保护的,如果不是这样,一个有效的替代方法就是简单地检查并重写MIMEHeaders 通过的属性phpmailer_init 发送前挂起。

SO网友:chifliiiii

我只是released a plugin 为了让用户在WordPress上使用html模板,我现在正在dev version 添加简单文本回退。我做了以下测试,在我的测试中,我只看到添加了一个边界,电子邮件对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对象,将消息加载到HTML模板中,并将其设置为Body属性。我还获取了原始消息并设置了AltBody属性。

SO网友:Frugan

我的简单解决方案是使用html2texthttps://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 也是关于的要点。

SO网友:Tanuki

对于任何使用phpmailer_init 钩子添加自己的“AltBody”:

备选文本正文为reused 对于正在发送的不同连续邮件,除非手动清除!WordPress没有在中清除它wp_mail() 因为它不希望使用此属性。

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

幸运的是,有一个简单的解决方法。这包括altbody更换钻头;请注意,您确实需要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

不幸的是,我无法对使用上述挂钩的其他解决方案发表评论,以警告他们这一点,因为我还没有足够的代表发表评论

SO网友:Olly

这可能不是对这里最初帖子的确切回答,但它是这里提供的关于设置alt body的一些解决方案的替代方案

本质上,我需要(想要)为HTML部分另外设置一个不同的altbody(即纯文本),而不是依赖于一些转换/条带标记等。

所以我想出了一个似乎效果很好的方法:

/* 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\'];
        }
    }   
}

SO网友:Paul Schreiber

此版本的wp_mail() 基于@bonger的代码。它有以下变化:

代码样式修复(通过PHPC)处理$message为数组或字符串的情况(确保与WP 5.x兼容)

  • 抛出异常,而不是返回false
    <?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 protected]>\' 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 <[email protected]>"
                $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 <[email protected]>"
                    $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 <[email protected]>"
                    $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() );
        }
    }
    

  • SO网友:Joshua Reyes

    如果您不想在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.

    SO网友:TheAddonDepot

    仔细查看了wp_mail($to, $subject, $message, $headers, $attachments) 在里面pluggable.php 并找到了一种不需要修补核心的解决方案。

    这个wp_mail() 函数检查$headers 标准标头类型的特定集合的参数,即from, content-type, cc, bccreply-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 标题。大多数用户代理都会自动添加该标头,因此删除它不应该是一个问题。

    但是如果要添加自定义标题,该怎么办without 生成副本Content-Type 标题?

    SOLUTION:不要显式设置Content-Type 中的标题$headers 数组添加自定义标头时,请执行以下操作:

    $headers = \'boundary="----=_Part_18243133_1346573420.1408991447668"\\r\\n\';
    $headers .= "MIME-Version: 1.0\\r\\n";
    $headers .= "From: Foo <[email protected]>\\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-typecharset 分别地这样,您就可以满足执行用于设置自定义标头的代码块的条件,而无需显式添加Content-Type: [mime-type]; [boundary];.

    无需触摸核心wp_mail() 实现,尽管可能有缺陷。

    结束

    相关推荐

    Facebook预览Open Graph Object Debugger

    如果我试图在Facebook上公开我的网站,我的特色图片会出错。我尝试使用facebook debug 但我没有任何感兴趣的信息。我使用此函数,但无法解决://* FACEBOOK *// function insert_fb_in_head() { global $post; if ( !is_singular()) // Se non è un post o una pagina return;