Zend_Mailで日本語のFrom情報を扱う

以前の記事で、Fromの名前の部分が化けてしまうということを書きましたが、
解決方法がわかったので、ここに残しておきます。

参考になったのは http://iandeth.dyndns.org/mt/ian/archives/000628.html です。
ここに、日本語のヘッダーを扱うためには、ISO-2022-JPに文字コードを変換して、
さらにbase64でエンコードして、さらに “=?ISO-2022-JP?B?”と”?=”で囲む必要があると。
今まで、mb_encode_mimeheaderを何も考えずに使っていましたが、
内部では上記のbase64エンコード以降のことをやってくれていたんですね。

ここまでを踏まえて、Zend_Mailのソースを見たところ、メールヘッダーはすべて、
base64ではなく、printableでエンコードされていました。
文字化けの原因はここでした。日本語に対応した処理でなかったとうことです。

しかし、printableでエンコードしている部分( _encodeHeaderメソッド)を、
base64でエンコードするようにしてしまうと、送信日時等の日本語が入ることがない
ヘッダーが受信側で正確にデコードされなくなってしまいます。

base64でエンコードしないとおかしくなるのは、Fromです。(場合によっては、Subjectも)
なので、setFromメソッドで、_encodeHeaderメソッドを呼び出している部分を、
mb_encode_mimeheaderを使ってエンコードするように変更したらOKです。

$this->_encodeHeader(‘”‘.$name.'”‘)

  ↓

mb_encode_mimeheader(‘”‘.$name.'”‘, ‘ISO-2022-JP’)

mb_encode_mimeheaderについては、下記をご参照ください。
http://phpspot.net/php/man/php/function.mb-encode-mimeheader.html

プログラムを設置しているサーバからZend_Mailだけを使って送信する場合は、
必ず文字化けするようなので、この対応は必須です。
メールサーバにSMTP認証して送信する場合(Zend_Mail_Transport_Smtp利用)は、
上記対応をしなくても、文字化けしていない場合もありました。
メールサーバ側で、うまいことやってくれているのかもしれません。

instanceof って便利

Zendを使うようになってから、同時に利用するようになったもの。
if 文での instanceof 判定。判定対象のオブジェクトの型を判定してくれる。

Dbから値を取得した際の処理に便利。

if ($rows instanceof Zend_Db_Table_Row)  の場合、
if ($rows instanceof Zend_Db_Table_RowSet) の場合。両方の処理を準備する。

Zend_Db_Table_Rowsetの場合は、Zend_Db_Table_Rowの場合の処理を
再帰的に呼び出すことでOKな場合も多々あります。

感覚でいうと1次元配列、2次元配列を気にせずに処理できるメソッドができる。
1次元配列と2次元配列を簡単に判定する方法を知らないから、とても便利に感じる。
たとえば、Dbから取得したカラム毎の合計値を取得するなら、
下記のように処理すればOKです。

public function getSumList($rows, $sumList=null)
{
 if (is_null($sumList)) $sumList = $this->getInitial();

 if (!is_null($rows)){
  if ($rows instanceof Zend_Db_Table_Row){
   $list = $rows->toArray();
   foreach ($list as $key => $value) $sumList[$key] += $value;
  }else if ($rows instanceof Zend_Db_Table_Rowset){
   foreach ($rows as $row) $sumList = $this->getSumList($row, $sumList);
  }
 }

 return $sumList;
}

$this->getInitial()は、$sumListに初期値を設定する関数です。
+= するためにnullだと気持ちが悪いので、0を設定しているだけの処理です。
カラム名は、Zend_Db_Table_Abstractクラスのinfo()関数で簡単にとってこれます。

カラム名の取得については、
http://framework.zend.com/manual/ja/zend.db.table.html#zend.db.table.info
10.5.10. テーブルのメタデータ情報の取得 を見てみてください。

【メモ】多次元配列のソート

phpで、多次元配列をできるだけ簡単にソートする方法を探していた。

連想配列がさらに配列になっている場合に、連想配列の指定したキーにて
連想配列の並びをソートしたい。これがやりたいこと。

検索してたら、使えそうな関数を見つけた
usortとarray_multisort

http://jp.php.net/manual/ja/function.usort.php
http://jp.php.net/manual/ja/function.array-multisort.php

先にやらないといけない仕事がはいったので、後でちゃんと調べるためのメモ。

そういえば、ZendFrameworkの新しいバージョンが出てた。1.5.0。
いま使ってるのは、1.0.3。beta版として1.0.4が出てたのに、次が1.5.0。
いったんここで、安定したってことなのかな。

英語のドキュメントは苦手なので、いまいちわからん。
でも、きっとそうだと思う。1.0.4までで試したことの安定板のはず。
マルチパートメールの受信が簡単にできるようになっていたらいいな。
送信簡単だったのに、受信は自作しないといけなかったから。

ブラウザからのファイルダウンロード

http://tkoshima.net/wp/archives/48

IEでだけ、ブラウザ経由のファイルダウンロードがうまくいきませんでした。
ネットで検索して、見つけたのが上記のリンク。
httpヘッダのキャッシュ設定を public にしないと正常にダウンロードできないとのこと。
下記ソースの※の部分を追加したら、IEでも正常に動作するようになりました。
(このソースは、Zend FrameWorkのActionコントローラ内に記述しています)

$fileName は、ダウンロードダイアログに出力するファイル名、
$path は、ダウンロード対象ファイルへのパスになります。

$fileNameは、ブラウザがIEの場合 SJIS に変換しないと日本語が文字化けします。

$this->getResponse()
   ->setHeader("Cache-Control", "public") ※
   ->setHeader("Pragma", "public")        ※
   ->setHeader("Content-Type", "application/octet-stream; name=\"$fileName\"")
   ->setHeader("Content-Disposition", "attachment; filename=\"$fileName\"")
   ->setHeader("Content-Length", filesize($path))
   ->setBody(file_get_contents($path));

メールヘッダーのデコード(Zend_Mail)

Zend_Mail_Storage_Imapを利用して、メッセージを取得する場合、
Zend_Mail_Storage_ImapのgetMessageメソッドにて、メッセージデータを
取得したタイミングで、メールヘッダーもデコードされて、オブジェクトに設定されます。

この際のデコードは、iconv_mime_decode_headersというphp5から使える
phpライブラリで行われています。Zend_Mime_Decodeクラスの中で。
しかし、このとき文字コードが、iconv.internal_encoding(default “ISO-8859-1″)
にてデコードされるため、日本語を含んでいる場合は、正常にデコードされません。

iconv_mime_decode_headersの第3パラメタに’UTF-8’を指定したら、
日本語もUTF-8にて、正常にデコードされて取得することができます。

この対応策にたどり着くまでに、4,5時間悩んでましたが、解決策は1行修正。
Zendのソースには、あまり修正を入れたくないと思っていたので、手間取りました。

メール受信の注意点(Zend_Mail)

例えばPop3でメールを取得する場合、以前書いたこの記事のような方法で取得します。

取得した際、件名などのヘッダー情報は、
$message->subject
としてとりだすことが可能です。

しかし、取り出そうとしたヘッダー情報がない場合は、
nullが返ってくるわけではなく、exceptionが発生します。

そのため、try catchを大きなくくりでしか行っていないと、
大部分の処理が実行されないという結果になります。

件名、本文がないメールも存在するので、ここは注意が必要です。
私もこれで、しばらく作りかけのプログラムが止まっていました。

Zend_Mail_Storage_Imapのバグ

私が使っているZend Frameworkは、最新のStable版(1.0.3)です。

その中で見つけた不具合。
Zend_Mail_Storage_Imapのメソッド、copyMessage。

第一パラメタにメッセージ番号、第2パラメタにコピー先フォルダの
フォルダクラス(Zend_Mail_Storage_Folder)または、
コピー先フォルダのグローバル名($folder->getGlobalName())を
渡すというコメントが書かれています。

しかし、実際はグローバル名しか渡せません。
通常、こういうコメントが書かれているメソッドは、
処理内でパラメタがフォルダクラスかの判定をして、クラスならば
getGlobalName()メソッドで、グローバル名におきかえてくれるのですが、
なぜかcopyMessageの中では行われていませんでした。

お気をつけください。

数値系のValidateの違い

数字系のValidateには、以下があります

・Zend_Validate_Digits
・Zend_Validate_Int
・Zend_Validate_Float
・Zend_Validate_LessThan
・Zend_Validate_GreaterThan
・Zend_Validate_Between

この中で、下の3つはクラス名からのイメージ通りなので割愛します。
Int、Floatもクラス名からイメージ通りなのですが、Digitsも含めて、
どういう風に判定結果が変わるのかを考えると、???となりました。私は。

調べてみたところ、こんな感じです。
・Digitsは、数字のみで構成されていること
・Intは、整数であること
・Floatは、整数または小数点を含む数値であること

これだけでは、まんまだねで終わるので、例で説明します

【例1 : 09022225555】
 Digits⇒○、Int⇒×、Float⇒×

【例2 : 0.01】
 Digits⇒×、Int⇒×、Float⇒○

【例3 : -1】
 Digits⇒×、Int⇒○、Float⇒○

【例4 : 2】
 Digits⇒○、Int⇒○、Float⇒○

Zend_Validate_NotEmptyの注意点

いまさらなんですが、気づいたのでメモしておきます。

Zend_Validate_NotEmptyですが、クラスの中ではphpのemptyメソッドを利用して
判定が行われているので、判定結果もemptyメソッドの処理結果と同様になります。

以前emptyメソッドではまったところなんですが、’0’は空と判定されるのです。
empty(‘0’)は true が判定結果として返されます。

入力チェックで、0 を許容したいところには、Zend_Validate_NotEmptyは使えません。

0 の1文字を許容するところは、数値の入力欄だと思うので、
Zend_Validate_Digitsを利用するといいです。
これは、クラス内で $value === ” という判定を行ってくれますので、
正確に空を判定することができます。

Zend_Db_Tableでのinsert方法の違い

Zend_Db_Tableクラスを使った行のinsert方法は2つ。

1. createRow()を使うもの
2. insertメソッドを使うもの
※$tblは、Zend_Db_Table_Abstractを継承したDbアクセスクラスです

1. はこんな感じ

$row = $tbl->createRow();

$row->password = xxx;
$row->create_on = new Zend_Db_Exp(‘now()’);
$row->save();

2. はこんな感じ

$params = array(
 ’password’ => ‘xxxx’,
 ’create_on’ => new Zend_Db_Exp(‘now()’)
);

$tbl->insert($params);

両者の違いは、設定しなかったカラムの処理方法にあります。
値を設定しなかったカラムに対して、1.はnullを設定し、2.は何も処理をしません。

これはどういうことかというと、1.の場合は、DB設定にてdefault値を設定しても、
行をinsertした後には、カラムの値はnullになっているということです。
これは、結構不便に感じます。

1.は更新時と書き方がほぼ一緒なので覚えやすいのですが、上記default値のことを考慮すると、
insertメソッドを使う方法が、予想外の問題が発生しにくいのではないかと思います。