Ars Longa, Vita Brevis

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

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

Битый заголовок Subject

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

Для того, чтобы убедиться, что такое отображение письма — это не ошибка почтового клиента, я написал маленький тестовый скрипт, который отправляет письма на 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 (Unknown Language)

Она заслуживает пристального внимания. В соответствии с 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));
+    }
 }
 
 ?>

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

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

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

  1. [Сентябрь 27, 2008 8:04 дп] Vladimir:

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

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

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

    #1
  2. [Сентябрь 27, 2008 10:00 пп] Макисим Покровский:

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

    #2
  3. [Сентябрь 28, 2008 1:20 дп] Vladimir:

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

    #3
  4. [Октябрь 9, 2008 7:13 дп] Yohan:

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

    #4
  5. [Октябрь 21, 2008 2:27 пп] Даша:

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

    #5
  6. [Октябрь 21, 2008 7:31 пп] Vladimir:

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

    #6

RSS лента комментариев к этой записи. TrackBack URL

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

Изображения должны быть включены!

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

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

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