Электронная почта с PHP нарушила кодировку заголовка субъекта

Мой PHP-скрипт отправляет электронную почту пользователям, и когда электронная почта поступает в их почтовые ящики, в строке темы ( $subject ) есть символы, такие a^£ добавленные в конец моего текста темы. Это явно и проблема кодирования. Сам контент сообщения электронной почты в порядке, только строка темы нарушена.

Я искал все, но не могу найти, как правильно закодировать мой предмет .

Это мой заголовок. Обратите внимание, что я использую Content-Type с charset=utf-8 и Content-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"; 

Обновление Для более практичного и актуального ответа взгляните на ответ Палека .


Указанная кодировка символов в Content-Type описывает только кодировку символов тела сообщения, но не заголовок. Вам нужно использовать синтаксис кодированного слова с кодировкой с кавычками или кодировкой Base64 :

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

Вы можете использовать imap_8bit для imap_8bit кавычки и base64_encode для кодировки Base64:

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

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

Проблема и решение

Заголовки Content-Type и Content-Transfer-Encoding применяются только к телу вашего сообщения. Для заголовков существует механизм для указания их кодирования, указанного в RFC 2047 .

Вы должны закодировать свой Subject помощью iconv_mime_encode() , который существует с PHP 5:

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

Измените input-charset чтобы она соответствовала кодировке вашего $subject string. Вы должны оставить output-charset как UTF-8 . Перед PHP 5.4 используйте array() вместо [] .

Теперь $encoded_subject (без $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() разделяет текст, кодирует каждую часть отдельно в токен <encoded-word> и складывает пробелы между ними. Закодированное слово =?<charset>?<encoding>?<encoded-text>?= Encoded =?<charset>?<encoding>?<encoded-text>?= Где:

  • <encoding> – это либо B (для Base 64 – см. base64_encode() ), либо Q (для Quoted-printable – см. quoted_printable_encode() ),
  • <encoded-text> кодируется строкой <encoding> , которая имеет кодировку <charset> после декодирования.

Вы можете декодировать =?CP1250?B?QWhvaiwgc3bsdGU=?= строку UTF-8 Ahoj, světe ( Hello, world in Czech) через iconv("CP1250", "UTF-8", base64_decode("QWhvaiwgc3bsdGU=")) или непосредственно через iconv_mime_decode("=?CP1250?B?QWhvaiwgc3bsdGU=?=", 0, "UTF-8") .

Кодировка в закодированные слова более сложна, поскольку спецификация требует, чтобы каждый токен с кодированным словом составлял не более 75 байт, и каждая строка, содержащая любой токен с кодированным словом, должна иметь длину не более 76 байт (включая пробел в начале строки продолжения ). Не выполняйте кодировку самостоятельно. Все, что вам действительно нужно знать, это то, что iconv_mime_encode() соответствует спецификации.

Интересное связанное чтение – статья Юникода и электронная почта в Википедии.

альтернативы

Рудиментарный вариант – использовать только ограниченный набор символов. ASCII гарантированно работает. ISO Latin 1 (ISO-8859-1), как предложено пользователем2250504 , вероятно, будет работать, потому что он часто используется в качестве резервной, когда не указывается кодировка. Но эти наборы символов очень малы, и вы, вероятно, не сможете закодировать все нужные вам символы. Более того, RFC ничего не говорят о том, должен ли работать латинский 1 или нет.

Вы также можете использовать mb_encode_mimeheader() , как ответил Пол Норман , но его легко использовать неправильно.

  1. Вы должны использовать mb_internal_encoding() чтобы установить внутренне используемую кодировку функций mbstring. Функции mb_* ожидают, что входные строки будут в этой кодировке. Остерегайтесь: второй параметр mb_encode_mimeheader() имеет ничего общего со строкой ввода (несмотря на то, что говорится в руководстве). Это соответствует <charset> в закодированном слове (см. Раздел «Как это работает» выше). Входная строка перекодируется из внутренней кодировки в это, прежде чем будет передана в кодировку B или Q.

    Установка внутренней кодировки может не понадобиться с PHP 5.6, потому что базовый параметр конфигурации mbstring.internal_encoding устарел в пользу опции default_charset , которая по умолчанию установлена ​​на UTF-8. Обратите внимание, что это только по умолчанию, и может быть нецелесообразно полагаться на значения по умолчанию в вашем коде.

  2. Вы должны указать имя заголовка и двоеточие во входной строке. RFC накладывает сильный предел на длину строки, и он должен также удерживаться для первой строки! Альтернативой является обсуждение пятого параметра ( $indent , последний по состоянию на сентябрь 2015 года), но это еще менее удобно.

  3. У реализации могут быть ошибки. Даже если они используются правильно, вы можете получить поврежденный выход. По крайней мере, это то, что многие комментарии на странице руководства говорят. Мне не удалось найти никаких проблем, но я знаю, что реализация закодированных слов сложна. Если вы обнаружите потенциальные или фактические ошибки в mb_encode_mimeheader() или iconv_mime_encode() , пожалуйста, дайте мне знать в комментариях.

Существует также, по крайней мере, один потенциал для использования mb_encode_mimeheader() : он не всегда кодирует все содержимое заголовка, что экономит пространство и оставляет текст mb_encode_mimeheader() человека. Кодирование требуется только для частей, отличных от ASCII. Вывод, аналогичный iconv_mime_encode() выше примеру 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() .

Если вам нравятся функции mbstring лучше, чем значки, вы можете использовать mb_send_mail() . Он использует mail() внутри, но автоматически кодирует тему и тело сообщения. Опять же, используйте с осторожностью .

Заголовки, отличные от темы, требуют различного лечения

Обратите внимание, что вы не должны предполагать, что кодировка всего содержимого заголовка ОК для всех заголовков, которые могут содержать символы, отличные от ASCII. Например, From, To, Cc, Bcc и Reply-To могут содержать имена для адресов, которые они содержат, но могут быть закодированы только имена, а не адреса. Причина в том, что токен <encoded-word> может заменить токены <text> , <ctext> и <word> и только при определенных обстоятельствах (см. § 5 RFC 2047 ).

Кодирование текста, отличного от ASCII, в других заголовках – это связанный, но другой вопрос. Если вы хотите узнать больше об этой теме, выполните поиск. Если вы не найдете ответа, задайте другой вопрос и укажите мне его в комментариях.

Здесь может быть полезно использовать mb_encode_mimeheader () для строк UTF-8, например

 $subject = mb_encode_mimeheader($subjectText,"UTF-8"); 

Сохраните файл php с соответствующей кодировкой.

В моем случае в Sublime Text я использовал следующий вариант:

Файл> Сохранить с кодировкой> Western (ISO-8859-1) [для бразильского португальца]

Для этого вам не нужно использовать какую-либо команду.