Решение проблемы битых заголовков в email

Те, кто используют русскоязычную версию WordPress, наверняка не раз сталкивались с проблемой битого заголовка Subject в уведомлениях WordPress. Навреное, проще проиллюстрировать:

Очевидно, что это не хорошо :-) Более того, битая кодировка может служить критерием для определения письма спамом.

Для того, чтобы убедиться, что такое отображение письма — это не ошибка почтового клиента, я написал маленький тестовый скрипт, который отправляет письма на GMail:

[-]
View Code PHP
<?php
    require_once('wp-config.php');
    wp_mail('blablabla@gmail.com', '[1234567890] New Comment On: Пятерка порадовавших меня запросов', 'Test Message');
?>

Когда Google отобразил битый Subject, стало понятно, что виноват всё-таки WordPress.

Если посмотреть на исходный текст самого письма, то увидим такие строки:

[-]
View Code eMail (mbox)

Она заслуживает пристального внимания. В соответствии с RFC 2822 WordPress (а точнее — PHP Mailer) разбил длинный заголовок на три фрагмента, каждый из которых не превышает 78 байт. Очевидно, что проблема заключается в том, что скрипт разбивал строку, закодированную BASE64, что привело к тому, что многобайтовые символы UTF-8 были разорваны.

Код это подтверждает:

[-]
View Code PHP
          $encoded = base64_encode($str);
          $maxlen -= $maxlen % 4;
          $encoded = trim(chunk_split($encoded, $maxlen, "\n"));

Есть два варианта исправления. Но оба сводятся к редактирования исходного текста WordPress. Иначе никак.

Простой вариант заключается в изменении верхней границы допустимой длины заголовка сообщения. В принципе, это не сильно противоречит стандарту:

There are two limits that this standard places on the number of characters in a line. Each line of characters MUST be no more than 998 characters, and SHOULD be no more than 78 characters, excluding the CRLF.

То есть, исправив длину, мы проигнорируем SHOULD, но будем принмать во внимание MUST. Лучше, чем ничего.

Итак, патч в формате unified diff (должен применяться к файлу wp-includes/class-phpmailer.php):

--- class-phpmailer.php.orig    2008-06-14 19:36:13.000000000 +0300
+++ class-phpmailer.php 2008-09-27 02:39:26.000000000 +0300
@@ -1160,7 +1160,7 @@
       if ($x == 0)
         return ($str);
 
-      $maxlen = 75 - 7 - strlen($this->CharSet);
+      $maxlen = 995 - 7 - strlen($this->CharSet);
       // Try to select the encoding which should produce the shortest output
       if (strlen($str)/3 < $x) {
         $encoding = 'B';

Второе решение (тоже патч) более серьёзное и более надёжное:

[-]
View Code Diff
--- class-phpmailer.php.orig    2008-06-14 19:36:13.000000000 +0300
+++ class-phpmailer.php 2008-09-27 07:54:25.000000000 +0300
@@ -655,6 +655,7 @@
      */
     function WrapText($message, $length, $qp_mode = false) {
         $soft_break = ($qp_mode) ? sprintf(" =%s", $this->LE) : $this->LE;
+        $is_utf8    = ("utf-8" == strtolower($this->CharSet));
 
         $message = $this->FixEOL($message);
         if (substr($message, -1) == $this->LE)
@@ -677,7 +678,9 @@
                     if ($space_left > 20)
                     {
                         $len = $space_left;
-                        if (substr($word, $len - 1, 1) == "=")
+                        if ($is_utf8)
+                          $len = $this->getUtf8CharBoundary($word, $len);
+                        elseif (substr($word, $len - 1, 1) == "=")
                           $len--;
                         elseif (substr($word, $len - 2, 1) == "=")
                           $len -= 2;
@@ -695,7 +698,9 @@
                 while (strlen($word) > 0)
                 {
                     $len = $length;
-                    if (substr($word, $len - 1, 1) == "=")
+                    if ($is_utf8)
+                        $len = $this->getUtf8CharBoundary($word, $len);
+                    elseif (substr($word, $len - 1, 1) == "=")
                         $len--;
                     elseif (substr($word, $len - 2, 1) == "=")
                         $len -= 2;
@@ -1164,9 +1169,14 @@
       // Try to select the encoding which should produce the shortest output
       if (strlen($str)/3 < $x) {
         $encoding = 'B';
+        if (true == function_exists('mb_strlen') && strlen($str) > mb_strlen($str, $this->CharSet)) {
+          $encoded = $this->b64Multibyte($str);
+        }
+        else {
           $encoded = base64_encode($str);
           $maxlen -= $maxlen % 4;
           $encoded = trim(chunk_split($encoded, $maxlen, "\n"));
+        }
       } else {
         $encoding = 'Q';
         $encoded = $this->EncodeQ($str, $position);
@@ -1492,6 +1502,66 @@
     function AddCustomHeader($custom_header) {
         $this->CustomHeader[] = explode(":", $custom_header, 2);
     }
+
+    function getUtf8CharBoundary($s, $max_len)
+    {
+        $lb = 3;
+        while (true) {
+            $x = substr($s, $max_len - $lb, $lb);
+            $pos = strpos($x, "=");
+            if (false !== $pos) {
+                $hex = substr($s, $max_len - $lb + $pos + 1, 2);
+                $dec = hexdec($hex);
+                if ($dec < 128) {
+                    if ($pos > 0) {
+                        $max_len = $max_len - $lb + $pos;
+                    }
+
+                    break;
+                }
+
+                if ($dec >= 192) {
+                    $max_len = $max_len - $lb + $pos;
+                    break;
+                }
+
+                $lb += 3;
+            }
+            else {
+                break;
+            }
+        }
+
+        return $max_len;
+    }
+
+    function b64MultiByte($s)
+    {
+        $start   = "=?{$this->CharSet}?B?";
+        $end     = "?=";
+        $encoded = "";
+
+        $mb_length = mb_strlen($s, $this->CharSet);
+        $str_len   = strlen($s);
+        $length    = 75 - strlen($start) - 2; //2 - strlen($end)
+        $step      = floor(0.75 * $length * $mb_length/$str_len);
+        $average   = $step;
+
+        for ($i=0; $i<$mb_length; $i+=$step) {
+            $lb = 0;
+
+            do {
+                $step = $average - $lb;
+                $tmp  = base64_encode(mb_substr($s, $i, $step, $this->CharSet));
+                ++$lb;
+            }
+            while (strlen($tmp) > $length);
+
+            $encoded .= $tmp . $this->LE;
+        }
+
+        return substr($encoded, 0, -strlen($this->LE));
+    }
 }
 
 ?>

Очень надеюсь, что решение кому-нибудь поможет :-)

Добавить в закладки

Связанные записи

27
Сен
2008

Комментарии к статье «Учим WordPress правильно кодировать письма в UTF-8» (20)  »

  1. Vladimir says:

    Я тут немного подумал и решил выложить пропатченные файлы.

    Первый вариант
    Второй вариант

    Да, и резервные копии никто не отменял

    • Николай Громов (nicothin) says:

      актуально для всех версий WP?

    • Devaka says:

      Владимир, спасибо, полезная штука!
      У меня такой вопрос.. то есть проблема была в классе PHPMailer, насколько правильно я понимаю. Если в WordPress 2.7 проблему пофиксили, то наверняка вышел новый PHPMailer или как-то по другому решили проблему?
      В общем, у меня движок не WordPress, но хочу пофиксить трабл, какие инструкции предложишь?

      • Vladimir says:

        Новый PHPMailer…

        Я бы посоветовал обновить PHPMailer, а потом смотреть, не сломало ли что обновление. Но должно сработать.

  2. Макисим Покровский says:

    Патч через ssh юзать надо?

  3. Vladimir says:

    Патч — по SSH, пропатченные файлы — если развернуть на своём компьютере — можно по FTP залить. Я сжал zip’ом пропатченные PHP-файлы только с той целью, чтобы у сервера не появилось желания их выполнить.

  4. Yohan says:

    Поставил сначало второй вариант - не заработало…
    Все равно, приходили письма вида “Проверьте, п ?жалуйста:”
    А первый вариант заработал!

  5. Даша says:

    А почему бы вам не попробовать написать несколько статей по психологии, у вас отлично получается грамотно излагать свои мысли. Если что, заходите в гости…Буду рада помочь;)

  6. Vladimir says:

    Потому что я по специальности не психолог, а инженер-системотехник (ну еще и референт-переводчик). Если я стану писать статьи по психологии, это то же самое, что рассказывать хирургу, как правильно делать надрез :-) Вообще я стараюсь руководствоваться фразой Апеллеса: Ne sutor supra crepidam judicet

  7. Толяныч says:

    Мне помог второй вариант. Автору респект за исправление ошибки!

  8. masp says:

    решенее неплохое, но как быть с обновлениями …
    лазить в код неудобно

    я тут решил эту проблемму этим плагином,
    непомню где нашол, но он работает для всех верисй

  9. Avdenago says:

    Хм. У меня 2.7.1 и такая же фигня. То есть есть вопросики в заголовке и в теле письма… Куда смотреть?

  10. Стас says:

    У меня версия 2.8.2 и не корректно отображается буквы И, ш в комментариях и в записях и в рубриках…. вместо них квадратик и вопросик, подскажите пожалуйста как исправить проблему, кодировка сайта ютф-8

Подписаться на RSS-ленту комментариев к статье «Учим WordPress правильно кодировать письма в UTF-8» Trackback URL: http://blog.sjinks.org.ua/wordpress/patches/346-teaching-wordpress-to-correctly-encode-utf8-emails/trackback/

Оставить комментарий к записи «Учим WordPress правильно кодировать письма в UTF-8»

Вы можете использовать данные тэги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Оставляя комментарий, Вы выражаете своё согласие с Правилами комментирования.

Подписаться, не комментируя