Те, кто используют русскоязычную версию 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 (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));
+    }
 }
 
 ?>

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

Добавить в закладки
  • del.ici.ous
  • Digg
  • Furl
  • Google
  • Simpy
  • Spurl
  • Y! MyWeb
  • БобрДобр
  • Мистер Вонг
  • Yandex.Закладки
  • Текст 2.0
  • News2
  • AddScoop
  • RuSpace
  • RUmarkz
  • Memori
  • Google Bookmarks
  • Писали
  • СМИ 2
  • Моё Место
  • 100 Закладок
  • Ваау!
  • Technorati
  • RuCity
  • LinkStore
  • NewsLand
  • Lopas
  • Закладки - IN.UA
  • Connotea
  • Bibsonomy
  • Trucking Bookmarks
  • Communizm
  • UCA
  • Slashdot
  • Magnolia
  • Blogmarks
  • Current
  • Meneame
  • Oknotizie
  • Diigo
  • Funp
  • Hugg
  • Dealspl.us
  • N4G
  • Mister Wong
  • Faves
  • Yigg
  • Fresqui
  • Care2
  • Kirtsy
  • Sphinn

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

27
Сен
2008

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

  1. Vladimir says:

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

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

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

  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:

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

Подписаться на 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>

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

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