来自 PHP 的电子邮件已损坏主题标头编码TL;DR问题和解决方案它是如何工作的?选择主题以外的标题需要不同的处理

2022-08-30 10:08:11

我的PHP脚本向用户发送电子邮件,当电子邮件到达他们的邮箱时,主题行()在我的主题文本末尾添加了类似字符的字符。这显然是编码问题。电子邮件内容本身很好,只是主题行被破坏了。$subjecta^£

我已经搜索了所有内容,但找不到如何正确编码我的主题

这是我的标题。请注意,我与 和 一起使用。Content-Typecharset=utf-8Content-Transfer-Encoding: 8bit

//set all necessary headers
$headers = "From: $sender_name<$from>\n";
$headers .= "Reply-To: $sender_name<$from>\n";
$headers .= "X-Sender: $sender_name<$from>\n";
$headers .= "X-Mailer: PHP4\n"; //mailer
$headers .= "X-Priority: 3\n"; //1 UrgentMessage, 3 Normal
$headers .= "MIME-Version: 1.0\n";
$headers .= "X-MSMail-Priority: High\n";
$headers .= "Importance: 3\n";
$headers .= "Date: $date\n";
$headers .= "Delivered-to: $to\n";
$headers .= "Return-Path: $sender_name<$from>\n";
$headers .= "Envelope-from: $sender_name<$from>\n";
$headers .= "Content-Transfer-Encoding: 8bit\n";
$headers .= "Content-Type: text/plain; charset=UTF-8\n";

答案 1

更新有关更实用和最新的答案,请查看Palec的答案


Content-Type 中指定的字符编码仅描述邮件正文的字符编码,而不描述标头。您需要将编码字语法带引号的可打印编码Base64 编码一起使用

encoded-word = "=?" charset "?" encoding "?" encoded-text "?="

您可以将imap_8bit用于带引号的可打印编码,将base64_encode用于 Base64 编码:

"Subject: =?UTF-8?B?".base64_encode($subject)."?="
"Subject: =?UTF-8?Q?".imap_8bit($subject)."?="

答案 2

TL;DR

$preferences = ['input-charset' => 'UTF-8', 'output-charset' => 'UTF-8'];
$encoded_subject = iconv_mime_encode('Subject', $subject, $preferences);
$encoded_subject = substr($encoded_subject, strlen('Subject: '));
mail($to, $encoded_subject, $message, $headers);

mb_internal_encoding('UTF-8');
$encoded_subject = mb_encode_mimeheader($subject, 'UTF-8', 'B', "\r\n", strlen('Subject: '));
mail($to, $encoded_subject, $message, $headers);

问题和解决方案

和 标头仅适用于邮件正文。对于标头,有一种机制用于指定 RFC 2047 中指定的标头编码。Content-TypeContent-Transfer-Encoding

您应该通过iconv_mime_encode()对自 PHP 5 起存在的 via iconv_mime_encode() 进行编码:Subject

$preferences = ["input-charset" => "UTF-8", "output-charset" => "UTF-8"];
$encoded_subject = iconv_mime_encode("Subject", $subject, $preferences);

更改输入字符集以匹配字符串$subject的编码。应将输出字符集保留为 UTF-8。在 PHP 5.4 之前,使用 array() 而不是 []

现在是(不带尾随换行符)$encoded_subject

Subject: =?UTF-8?B?VmVyeSBsb25nIHRleHQgY29udGFpbmluZyBzcGVjaWFsIGM=?=
 =?UTF-8?B?aGFyYWN0ZXJzIGxpa2UgxJvFocSNxZnFvsO9w6HDrcOpPD4/PSsqIHA=?=
 =?UTF-8?B?cm9kdWNlcyBzZXZlcmFsIGVuY29kZWQtd29yZHMsIHNwYW5uaW5nIG0=?=
 =?UTF-8?B?dWx0aXBsZSBsaW5lcw==?=

用于包含:$subject

Very long text containing special characters like ěščřžýáíé<>?=+* produces several encoded-words, spanning multiple lines

它是如何工作的?

该函数拆分文本,将每个片段分别编码为<编码字>标记,并在它们之间折叠空格。编码词是其中:iconv_mime_encode()=?<charset>?<encoding>?<encoded-text>?=

您可以通过或直接通过 解码为 UTF-8 字符串(捷克语)。=?CP1250?B?QWhvaiwgc3bsdGU=?=Ahoj, světeHello, worldiconv("CP1250", "UTF-8", base64_decode("QWhvaiwgc3bsdGU="))iconv_mime_decode("=?CP1250?B?QWhvaiwgc3bsdGU=?=", 0, "UTF-8")

编码为编码词更为复杂,因为规范要求每个编码字标记的长度最多为 75 个字节,并且包含任何编码字标记的每行的长度必须最多为 76 个字节(包括续读行开头的空白)。不要自己实现编码。您真正需要知道的是,iconv_mime_encode() 遵守规范。

有趣的相关阅读是维基百科文章Unicode和电子邮件

选择

一个基本的选项是仅使用一组受限制的字符。ASCII 保证工作。正如user2250504所建议的那样,ISO Latin 1(ISO-8859-1)也可能有效,因为它通常在未指定编码时用作回退。但是这些字符集非常小,您可能无法编码所需的所有字符。此外,RFC没有说明拉丁语1是否应该工作。

正如Paul Norman所回答的那样,你也可以使用mb_encode_mimeheader(),但很容易错误地使用它。

  1. 必须使用 mb_internal_encoding() 来设置 mbstring 函数的内部使用编码。这些函数期望输入字符串采用此编码。注意:的第二个参数与输入字符串无关(尽管手册中说了什么)。它对应于编码的单词中的 (请参阅上面的“它是如何工作的?”)。输入字符串从内部编码重新编码到此编码,然后传递到 B 或 Q 编码。mb_*mb_encode_mimeheader()<charset>

    从 PHP 5.6 开始,可能不需要设置内部编码,因为底层mbstring.internal_encoding配置选项已被弃用,取而代之的是 default_charset 选项,默认情况下,该选项已设置为 UTF-8。请注意,这只是默认值,在代码中依赖默认值可能不合适。

  2. 必须在输入字符串中包含标头名称和冒号。RFC对行长度施加了严格的限制,并且第一行也必须成立!另一种方法是摆弄第五个参数(;截至2015年9月的最后一个参数),但这甚至不那么方便。$indent

  3. 该实现可能存在错误。即使使用得当,也可能得到损坏的输出。至少这是手册页上许多评论所说的。我没有设法找到任何问题,但我知道编码单词的实现很棘手。如果您在mb_encode_mimeheader()iconv_mime_encode()中发现潜在或实际的错误,请在评论中告诉我。

使用至少还有一个好处:它并不总是编码所有标题内容,这节省了空间并使文本可读。只有非 ASCII 部分需要编码。与上面示例类似的输出是:mb_encode_mimeheader()iconv_mime_encode()

Subject: Very long text containing special characters like
 =?UTF-8?B?xJvFocSNxZnFvsO9w6HDrcOpPD4/PSsqIHByb2R1Y2VzIHNldmVyYWwgZW5j?=
 =?UTF-8?B?b2RlZC13b3Jkcywgc3Bhbm5pbmcgbXVsdGlwbGUgbGluZXM=?=

使用示例:mb_encode_mimeheader()

mb_internal_encoding('UTF-8');
$encoded_subject = mb_encode_mimeheader("Subject: $subject", 'UTF-8');
$encoded_subject = substr($encoded_subject, strlen('Subject: '));
mail($to, $encoded_subject, $message, $headers);

这是本文顶部 TL;DR 中代码段的替代方法。它实际上并没有为 保留空间,而是将其放在那里,然后将其删除,以便能够在 愚蠢的界面上使用它。Subject: mail()

如果你比 iconv 更喜欢 mbstring 函数,你可能想使用 mb_send_mail()。它在内部使用mail(),但自动对消息的主题和正文进行编码。同样,请小心使用

主题以外的标题需要不同的处理

请注意,您不得假定对于可能包含非 ASCII 字符的所有标头,对标头的全部内容进行编码是可以的。例如,发件人、收件人、抄送、密件抄送和回复收件人可能包含其所包含的地址的名称,但只能对名称进行编码,而不能对地址进行编码。原因是令牌可以仅替换 和 令牌,并且仅在某些情况下(请参阅 RFC 2047 的 §5)。<encoded-word><text><ctext><word>

其他标头中非 ASCII 文本的编码是一个相关但不同的问题。如果您想了解有关此主题的更多信息,请搜索。如果您没有找到答案,请提出另一个问题并在评论中指出我。


推荐