Ktai Entry でデコメール(背景は除く)

WordPress × 携帯とくれば IKEDA Yuriko さん作の Ktai EntryKtai Style ですね。これらプラグインのおかげで「携帯ぢゃなぃとブログできなぃょぅ」というクライアント様にも WordPress でご提案ができるわけです。
ところが、Ktai Entry+Ktai Style は各社バラバラな絵文字にも対応しているというのに、このクライアント様は「デコレメールのマイ絵文字が使いたい」らしい。なんやねんそれ。docomo はデコメール、au はデコレーションメール。まったく器が小さいよキミ達。Matt を見習いなさい。すごく若い女性と付き合い始めた友達から「ぉはよぅ(絵文字)今夜(絵文字)飲もぅョ(絵文字)(絵文字)」なんてメールが送られてくる俺の身にもなれ。
つまりはどれも HTML メールのことらしい。マイ絵文字はインライン画像の扱いだろう。背景画像は一大イベント時くらいしか使われないので無視します。よし、今回もきっと大丈夫、なんとかなるはずだ。

今回最も重要なポイント

XTREME なくせになるべく core には手を入れない方針の wpxtreme ですが、今回はプラグインのソースを直接いじり倒します。このような mods は kz が睡眠時間を削ってハイテンションになるのが目的ですので、この記事に関してプラグイン作者様に問い合わせ等されませんようよろしくお願いするぜ。

2010.3.5 追記
・このカスタマイズ内容は開発中の(次期) Ktai Entry に盛り込まれました。
・通常の携帯絵文字は、東海圏内では使用されているのを見なくなったため丸無視しています。
 あんまりかわいくない携帯絵文字もどうしても使いたい場合は、次期 Ktai Entry のリリースを待ちましょう。
・ごく一部の機種(docomo F906i )では添付画像がインライン展開されます。
 これはその携帯がバカなのでその旨クライアントにお伝えください。
・このカスタマイズでは、元ソースへの変更が少なくなるように変わった実装をしています。
 プラグイン作者様による素直な実装をお望みの方は、次期 Ktai Entry のリリースを待ちましょう。

メール内にあるマイ絵文字の情報を取得する

実際に Ktai Entry(バージョン 0.8.11) をセットアップしたサイトに、携帯からデコメを送信して詳細なログを出してみると、ソレらしい情報はすぐ見つかります(以下、例)。

--abhadblejledldkd_999SH
Content-Type:multipart/alternative;
boundary="abhadblejledldkd_999SH"
--abhadblejledldkd_999SH
Content-Type:text/plain;charset=ISO-2022-JP
Content-Transfer-Encoding:7bit
$BEj9F%F%9%H$G$9 (B 
--abhadblejledldkd_999SH
Content-Type:text/html;charset=ISO-2022-JP
Content-Transfer-Encoding:quoted-printable
<HTML><HEAD><meta http-equiv=3D=22Content-Type=22 content=3D=22text/html=
; charset=3DISO-2022-JP=22></HEAD><BODY><DIV>=1B=24BEj9F%F%9%H=24G=249=1B=
 (B<IMG src=3D=22cid:01.091234.012345__999SH=40softbank.ne.jp=22></DIV>
</BODY></HTML>
--abhadblejledldkd_999SH--
--abhadblejledldkd_999SH
Content-Type:image/gif;name="usagi_yorokobi.gif"
Content-Transfer-Encoding:base64
Content-Disposition:inline;filename="usagi_yorokobi.gif"
Content-ID:<01.091234.012345__999SH@softbank.ne.jp>
R0lGODlhFAAUA(gif データなので省略)
--abhadblejledldkd_999SH--

なんすか usagi_yorokobi って。画像がインラインか添付かは Content-Disposition で判断できます(実は正しく設定していない携帯があるのでできません)。IMG タグの src には Content-ID の値が指定されています。ざーっと見てなんとなく仕組みがわかりましたね。それで OK です。深追いする必要はありません。しつこいタイプのあなたは「MIMEフォーマット」などで調べると良いです。
Ktai Style ではこの辺りのデコード処理には Mail_mimeDecode.php を使用されています。同梱の バージョン 1.48 では Content-ID は丸無視されてるので優しく拾ってあげましょう。
Mail_mimeDecode.php 283行目に以下を挿入。

case 'content-id':
  $content_id = $this->_parseHeaderValue($headers[$key]['value']);
  if(preg_match('/<([^>]+)>/i', $content_id['value'], $regs))
    $return->cid = $regs[1];
  else
    $return->cid = $content_id['value'];
  break;

Content-ID の値は < と > で囲まれるはずなんですが、ベタ書きになっているワヤな携帯もあるので対策。

HTMLを採用する

Ktai Style では上記 Content-Type:text/plain 部分、つまりプレーンテキストのみを取得しています。今回必要なのは Content-Type:text/html の部分なので、その辺をなんとか拾ってみます。
post.php 640行目〜 get_mime_parts 関数を以下に置換。

private function get_mime_parts($part) {
  $contents = new stdClass;
  $contents->text = '';
  $contents->text_type = NULL;
  $contents->html = '';
  $contents->images = array();
  switch (strtolower($part->ctype_primary)) {
    case 'multipart':
      foreach ($part->parts as $p) {
        $part_content = $this->get_mime_parts($p);
        if ($part_content->text_type == 'plain') {
          $contents->text .= $part_content->text;
          $contents->text_type = $part_content->text_type;
        }
        elseif ($part_content->text_type == 'html') {
          $contents->html .= $part_content->html;
          $contents->text_type = $part_content->text_type;
        }
        $contents->images = array_merge($contents->images, $part_content->images);
      }
    break;
    case 'text':
      $charset = $this->get_charset($part->ctype_parameters);
      if (is_object($this->operator)) {
        $text = $this->operator->pickup_pics($text, $charset);
      }
      if ($charset == 'auto') {
        $charset = KE_DETECT_ORDER;
      }
      $this->debug_print(sprintf(__('Detect text/%1$s part encoding as "%2$s"', 'ktai_entry_log'), $part->ctype_secondary, $charset));

      if ($part->ctype_secondary == 'plain' || $part->ctype_secondary == 'x-pmaildx') {
        if ($contents->text_type && $contents->text_type != 'plain') {
          $this->debug_print(sprintf(__('Skipped %1$s/%2$s part', 'ktai_entry_log'), $part->ctype_primary, $part->ctype_secondary));
          continue;
        }
        $contents->text_type = 'plain';
        //text = trim($part->body); // 2010.3.5 $が抜けてたぜ
        $text = trim($part->body);
	$contents->text .= mb_convert_encoding($text, get_bloginfo('charset'), $charset);
	$this->debug_print('text='.$text);
	$this->debug_print('plain='.$contents->text);
      } elseif ($part->ctype_secondary == 'html') {
        if ($contents->text_type && $contents->text_type != 'html') {
          $this->debug_print(sprintf(__('Skipped %1$s/%2$s part', 'ktai_entry_log'), $part->ctype_primary, $part->ctype_secondary));
          continue;
        }
        $contents->text_type = 'html';
        $text = mb_convert_encoding(trim($part->body), get_bloginfo('charset'), $charset);
        $text = preg_replace('@</DIV>@i', "</DIV>\n", $text);
        $contents->html .= strip_tags($text, '<img><title>');
        $this->debug_print('text='.$text);
        $this->debug_print('html='.$contents->html);
      }
    break;
    case 'image':
      $name = $this->get_filename($part->d_parameters, $part->ctype_parameters);
      $this->debug_print(sprintf(__('Found %1$s/%2$s part with filename: %3$s', 'ktai_entry_log'), $part->ctype_primary, $part->ctype_secondary, $name));
      if (! $this->validate_extension($name, $part->ctype_primary, $part->ctype_secondary)) {
        $this->debug_print(sprintf(__('Invalid filename "%1$s" for mime type "%2$s/%3$s"', 'ktai_entry_log'), $name, $part->ctype_primary, $part->ctype_secondary));
        continue;
      }
      $contents->images[] = array(
        'name'   => $name, 
        'p_type' => strtolower($part->ctype_primary), 
        's_type' => strtolower($part->ctype_secondary), 
        'body'   => $part->body,
        'cid'    => $part->cid,
        'disp'   => strtolower($part->disposition)
      );
    break;
  }		
  return $contents;
}
そして post.php 283行目に以下を挿入。
if($contents->html != '') $contents->text = $contents->html;

これで HTML,Content-ID,Content-Disposition が取得できました。

IMG タグの src を Content-ID から画像ファイルパスに置換する

画像をアップロード(メディアの投稿の動作)する際に、Content-ID,Content-Disposition の値をメタデータ(カスタムフィールド)として保存します。
post.php 1031行目に以下を挿入。

$metas = array(
  'cid' => $img['cid'],
  'disp' => $img['disp'],
);

post.php 1054行目 wp_update_attachment_metadata($id, wp_generate_attachment_metadata($id, $file));を以下に置換。

wp_update_attachment_metadata($id, array_merge($metas, wp_generate_attachment_metadata($id, $file)));

post.php 1281行目 $img[] = $html;を以下に置換。

$metas = wp_get_attachment_metadata($id);
// ※1 if(isset($metas['disp']) && $metas['disp'] == 'inline' && isset($metas['cid']) && $metas['cid'] != ''){
if(isset($metas['cid']) && $metas['cid'] != ''){
  // ※2 $content = str_replace('cid:'.$metas['cid'], $file[2], $content);
  $content = preg_replace('/(cid:)*'.$metas['cid'].'/i', $file[2], $content);
  $content = preg_replace('@<img\s+(.+)\s*/?>@i', '<img class="demoji" $1 />', $content);
 if (preg_match('/alt=(""|\'\')/', $content, $alt)) {
   $content = str_replace($alt[0], 'alt="' . basename($file[2]) . '"', $content);
  } elseif (! preg_match('/alt=/', $content)) {
    $content = str_replace('<img ', '<img alt="' . basename($file[2]) . '" ', $content);
  }
}
else
  $img[] = $html;

※1 インライン画像なのに Content-Disposition:inline になってない携帯があるので使えず。

※2 インライン画像なのに <IMG src="cid: の書式になっていない携帯があるので使えず。

以上で、デコメが正しく投稿されるようになります。PC での表示も Ktai Style での表示も特に何もしなくてもマイ絵文字がうまいこと表示されます。マイ絵文字の IMG タグには "demoji" クラスを付けてますので、困った時は CSS でなんとかしてください。
添付画像は、これまで通り添付画像になります。と思ったら添付画像なのにインライン画像と同じ扱いにしてしまってる携帯があるので、ソイツだけ添付画像が本文の最後にインライン的に表示されます。画像のサイズか ! gif かで判断するしかないかな。ま、それはチャレンジされる方のお楽しみにとっておきましょう。

投稿エラーを返信する

携帯メールでガンガン投稿されるのはいいですけど、画像のアップロードエラーなんかがたまにあったりするとお問い合わせ対応がスタートしてしまいます。なので、メール送信者(=投稿者)または管理者に「エラーがあったよ」と通知してあげると親切かもしれません。「エラーメールが届いたら(本文をちょこっと変更して)再度送信してください。」とクライアント様にお伝えしておけばオール OK になりますね、ふー。

retrieve.php 140行目〜 retrieve 関数を以下に置換します。notify_error 関数は追加です。

public function retrieve($count) {
  for ($i=1; $i <= $count; $i++) :
    $lines = $this->pop3->get($i);
    $err = $contents = $this->post->parse(str_replace("\r\n", "\n", implode('', $lines)));
    if (is_ke_error($contents)) {
      $this->display(sprintf(__('Error at #%1$d: %2$s', 'ktai_entry_log'), $i, $contents->getMessage()));
      $contents = '';
    }else{
      $err = $result = $this->post->insert($contents);
      if (is_ke_error($result)) {
        $this->display(sprintf(__('Error at #%1$d: %2$s', 'ktai_entry_log'), $i, $result->getMessage()));
      }
    }
    if (is_ke_error($err)) {
      $user_id = 0;
      switch($err->getCode()){
	case KE_ALREADY_POSTED:
	  //  $user_id = -1; break;
	case KE_NO_SENDER_ADDRESS:
	case KE_INVALID_RECIPIENT_ADDRESS:
	case KE_NO_SENDER_ADDRESS:
	case KE_NOT_REGISTERED_ADDRESS:
          break;
	default:
	  $user_id = $contents->post_author;
      }
      if($user_id > -1) $this->notify_error($user_id, $contents);
      continue;
    }
    if (! $this->pop3->delete($i)) {
      $error = $this->pop3->ERROR;
      $this->pop3->reset();
      $this->display(sprintf(__('Can\'t delete message #%1$d: %2$s', 'ktai_entry_log'), $i, $error));
      break;
    }else{
      $this->display(sprintf(__('Mission complete, message "%d" deleted.', 'ktai_entry_log'), $i));
    }
  endfor;
  $this->pop3->quit();
  return;
}

private function notify_error($user_id, $contents) {
  $to = '';
  if($user_id === 0)
    $to = get_bloginfo('admin_email');
  else
    $to = $contents->from;

  $blogname = get_option('blogname');
  $message = __('Post error on blog.', 'ktai_entry') . "\r\n";
  if($user_id === 0)
    $message .= __('Check post!') . "\r\n";
  else
    $message .= sprintf(__('Title: %s', 'ktai_entry'), $contents->post_title) . "\r\n";
  $subject = sprintf(__('Error:"%s"', 'ktai_entry'), $blogname);
  $wp_email = 'info@' . preg_replace('#^www\.#', '', strtolower($_SERVER['SERVER_NAME']));
  $from = "From: \"$blogname\" $lt;$wp_email>";
  if($user_id !== 0)
    $from .= "\nBcc:".get_bloginfo('admin_email');
 
  $headers = "$from\n"
    . "MIME-Version: 1.0\n"
    . "Content-Type: text/plain; charset=ISO-2022-JP\n";

  if (function_exists('mb_language')) {
    mb_language('Japanese');
    mb_internal_encoding(get_bloginfo('charset'));
    mb_send_mail($to, $subject, $message, $headers);
  }else{
    wp_mail($to, $subject, $message, $headers);
  }
  $this->display(sprintf(__('Send:%1$s: %2$s', 'ktai_entry_log'), $to, $subject));
  return;
}

元コードの変更がなるべく少なくなるようにしてみましたが、見てみると長いな。コードの書き方はなるべく Ktai Entry のソースに倣いました。メタメタにいじってごめんなさい。それにしても携帯のマチマチな実装には参った。オープンでグローバルなスタンダードにしろよって話。

参照
動作確認バージョン
  • WordPress 2.9.1
  • Ktai Entry 0.8.11

18 Comments

  • BLOG POST: Ktai Entry でデコメール(背景は除く) http://bit.ly/c2rFNl

  • @kzxtreme デコメ対応のソース参考にさせてもらっていいですか? http://bit.ly/c2rFNl もうちょっといじりたいところもなきにしもあらずですが。

  • ああ http://bit.ly/c2rFNl の方法だと PEAR パッケージに手を入れないといけない。それだとサーバーに入ってる PEAR を使うことができない (添付の PEAR を使うことが必須) のが少し困る。

  • デコメ対応は以前から検討事項になってるので、パッチ(?)の提供は助かります。上記のソースだと 0.9.0 系統にはそのまま入れられないので、かなり修正が必要のようです。あと、普通の絵文字がまったく認識されない気がしますが、そのへんはどうでしょう??

  • kz kz

    普通の絵文字はウチの近所では絶滅したので割り切ってサヨナラをしました。
    やってることは
    ・Content-ID を取得
    ・text/html を使う
    ・src の Content-ID をパスに置換
    だけなので楽チンですけど
    普通の絵文字と開発版を絡めてうまく収まると良いな、と願いつつ
    0.9.0-beta2 をダウロードしてみます。

  • Ktai Entry 0.9.0 に組み込んでみました。元ソースは、普通の絵文字があったときも demoji クラスを付けてしまうような気がしたので、そのへんは修正しました。あと、画像メタデータに cid, disposition を入れる意義がよく分からなかったので、これも廃止して、ひたすら変数で引き渡すことにしました。
    notify 回りは、retrieve.php に do_action を作りました。これにより、Ktai Entry をいじらずに notify を送る別プラグインを作ればいいことになります。

    • kz kz

      Awesome!!
      メタデータにしたのはソース変更が少ないかなとか
      マイブームだったとかそんなんです。
      リリース楽しみにしてます。

    • Justice Justice

      Ktai Entry デコメ対応で検索してこちらにたどり着きました。
      リリース心待ちにしております。

  • BLOG: Ktai Entry でデコメール(背景は除く) http://bit.ly/dkyCdz

  • RT @kzxtreme: BLOG: Ktai Entry でデコメール(背景は除く) http://bit.ly/dkyCdz

  • RT @wordpress_fan: RT @kzxtreme: BLOG: Ktai Entry でデコメール(背景は除く) http://bit.ly/dkyCdz

  • WPのデコメ対応

  • ブログ投稿にデコレメールの絵文字使えればいいのになー。期待はしてるんだけど直接いじるのがなんかこう… http://bit.ly/fTUNEZ

  • Ktai Entry でデコメール(背景は除く) http://t.co/yXph3G3A @kzxtremeさんから

  • “Ktai Entry でデコメール(背景は除く)  |  wpxtreme” http://t.co/b86aVNjs