コンテンツへスキップ

FuelPHPのEmailパッケージをS/MIMEに対応させるお話です。(今回は電子署名のみ)

Emailパッケージを拡張するのには、以下のような方法で行いました。

#! /bin/blog: [FuelPHP]ネームスペースを上書きしてパッケージのクラスを書き換える

変更が必要なのは Email\Email_Driver だけなので、Emailパッケージから必要なファイルをコピーして以下のような構成を作ります。

fuel
└ packages
    └myemail
        ├─ bootstrap.php
        |─ classes
        |    └─ email
        |         └─ driver.php
        └─ config
             └─ email.php

bootstrap.php は以下のように指定して、コピーした方のファイルでEmail_Driverを上書きします。
また、fuel/app/config 以下の config.php のalways_load に myemail を追加するのも忘れずに。

<?php
\Autoloader::add_core_namespace('Email');

\Autoloader::add_classes(array(
	/**
	 * Email classes.
	 */
	'Email\\Email_Driver'                      => __DIR__.'/classes/email/driver.php',

));

これで下準備は完了です。それでは、コピーしたファイルに変更を加えていきましょう。
driver.php
は build_message メソッドを以下のように変更します。

	protected function build_message($no_bcc = false)
	{
		$newline = $this->config['newline'];
		$charset = $this->config['charset'];
		$encoding = $this->config['encoding'];

		$headers = '';
		$parts = array('Date', 'Return-Path', 'From', 'To', 'Cc', 'Bcc', 'Reply-To', 'Subject', 'Message-ID', 'X-Priority', 'X-Mailer', 'MIME-Version', 'Content-Type');
		$no_bcc and array_splice($parts, 5, 1);
		$this->config['smime']['enabled'] and array_splice($parts, -2, 2);
		
		foreach ($parts as $part)
		{
			$headers .= $this->get_header($part);
		}

		foreach ($this->extra_headers as $header => $value)
		{
			$headers .= $header.': '.$value.$newline;
		}

		$headers .= $newline;

		$body = '';

		if ($this->type === 'plain' or $this->type === 'html')
		{
			if ($this->config['smime']['enabled'])
			{
				$body .= 'Content-Type: text/plain; charset="'.$charset.'"'.$newline;
				$body .= 'Content-Transfer-Encoding: '.$encoding.$newline.$newline;
			}
			$body .= $this->body;
		}
		else
		{
			if ($this->config['smime']['enabled'])
			{
				$body .= $this->get_header('Content-Type').$newline;
			}
			
			switch ($this->type)
			{
				case 'html_alt':
					$body .= '--'.$this->boundaries[0].$newline;
					$body .= 'Content-Type: text/plain; charset="'.$charset.'"'.$newline;
					$body .= 'Content-Transfer-Encoding: '.$encoding.$newline.$newline;
					$body .= $this->alt_body.$newline.$newline;
					$body .= '--'.$this->boundaries[0].$newline;
					$body .= 'Content-Type: text/html; charset="'.$charset.'"'.$newline;
					$body .= 'Content-Transfer-Encoding: '.$encoding.$newline.$newline;
					$body .= $this->body.$newline.$newline;
					$body .= '--'.$this->boundaries[0].'--';
					break;
				case 'plain_attach':
				case 'html_attach':
				case 'html_inline':
					$body .= '--'.$this->boundaries[0].$newline;
					$text_type = (stripos($this->type, 'html') !== false) ? 'html' : 'plain';
					$body .= 'Content-Type: text/'.$text_type.'; charset="'.$charset.'"'.$newline;
					$body .= 'Content-Transfer-Encoding: '.$encoding.$newline.$newline;
					$body .= $this->body.$newline.$newline;
					$attach_type = (stripos($this->type, 'attach') !== false) ? 'attachment' : 'inline';
					$body .= $this->get_attachment_headers($attach_type, $this->boundaries[0]);
					$body .= '--'.$this->boundaries[0].'--';
					break;
				case 'html_alt_inline':
					$body .= '--'.$this->boundaries[0].$newline;
					$body .= 'Content-Type: text/plain'.'; charset="'.$charset.'"'.$newline;
					$body .= 'Content-Transfer-Encoding: '.$encoding.$newline.$newline;
					$body .= $this->alt_body.$newline.$newline;
					$body .= '--'.$this->boundaries[0].$newline;
					$body .= 'Content-Type: multipart/related;'.$newline."\tboundary=\"{$this->boundaries[1]}\"".$newline.$newline;
					$body .= '--'.$this->boundaries[1].$newline;
					$body .= 'Content-Type: text/html; charset="'.$charset.'"'.$newline;
					$body .= 'Content-Transfer-Encoding: '.$encoding.$newline.$newline;
					$body .= $this->body.$newline.$newline;
					$body .= $this->get_attachment_headers('inline', $this->boundaries[1]);
					$body .= '--'.$this->boundaries[1].'--'.$newline.$newline;
					$body .= '--'.$this->boundaries[0].'--';
					break;
				case 'html_alt_attach':
				case 'html_inline_attach':
					$body .= '--'.$this->boundaries[0].$newline;
					$body .= 'Content-Type: multipart/alternative;'.$newline."\t boundary=\"{$this->boundaries[1]}\"".$newline.$newline;
					if (stripos($this->type, 'alt') !== false)
					{
						$body .= '--'.$this->boundaries[1].$newline;
						$body .= 'Content-Type: text/plain; charset="'.$charset.'"'.$newline;
						$body .= 'Content-Transfer-Encoding: '.$encoding.$newline.$newline;
						$body .= $this->alt_body.$newline.$newline;
					}
					$body .= '--'.$this->boundaries[1].$newline;
					$body .= 'Content-Type: text/html; charset="'.$charset.'"'.$newline;
					$body .= 'Content-Transfer-Encoding: '.$encoding.$newline.$newline;
					$body .= $this->body.$newline.$newline;
					if (stripos($this->type, 'inline') !== false)
					{
						$body .= $this->get_attachment_headers('inline', $this->boundaries[1]);
						$body .= $this->alt_body.$newline.$newline;
					}
					$body .= '--'.$this->boundaries[1].'--'.$newline.$newline;
					$body .= $this->get_attachment_headers('attachment', $this->boundaries[0]);
					$body .= '--'.$this->boundaries[0].'--';
					break;
				case 'html_alt_inline_attach':
					$body .= '--'.$this->boundaries[0].$newline;
					$body .= 'Content-Type: multipart/alternative;'.$newline."\t boundary=\"{$this->boundaries[1]}\"".$newline.$newline;
					$body .= '--'.$this->boundaries[1].$newline;
					$body .= 'Content-Type: text/plain; charset="'.$charset.'"'.$newline;
					$body .= 'Content-Transfer-Encoding: '.$encoding.$newline.$newline;
					$body .= $this->alt_body.$newline.$newline;
					$body .= '--'.$this->boundaries[1].$newline;
					$body .= 'Content-Type: multipart/related;'.$newline."\t boundary=\"{$this->boundaries[2]}\"".$newline.$newline;
					$body .= '--'.$this->boundaries[2].$newline;
					$body .= 'Content-Type: text/html; charset="'.$charset.'"'.$newline;
					$body .= 'Content-Transfer-Encoding: '.$encoding.$newline.$newline;
					$body .= $this->body.$newline.$newline;
					$body .= $this->get_attachment_headers('inline', $this->boundaries[2]);
					$body .= $this->alt_body.$newline.$newline;
					$body .= '--'.$this->boundaries[2].'--'.$newline.$newline;
					$body .= '--'.$this->boundaries[1].'--'.$newline.$newline;
					$body .= $this->get_attachment_headers('attachment', $this->boundaries[0]);
					$body .= '--'.$this->boundaries[0].'--';
					break;
			}

		}
		
		if ($this->config['smime']['enabled'])
		{
			$tmp_dir = $this->config['smime']['tmp_dir'];
			$infilename = 'email_'.md5($body.microtime());
			$outfilename = $infilename.'_out';
			$signcert = 'file://'.$this->config['smime']['signcert'];
			$privkey = 'file://'.$this->config['smime']['privkey'];
			$headers = substr($headers, 0, -strlen($newline)*2);
			
			if ( ! is_writable($tmp_dir))
			{
				throw new \FuelException('Cannot write into directory "'.$tmp_dir.'"');
			}
			if ( ! is_readable($signcert) || ! is_readable($privkey))
			{
				throw new \FuelException('Cannot read certificate or key.');
			}
			
			if ($this->config['smime']['keypass'])
			{
				$privkey = array($privkey, $this->config['smime']['keypass']);
			}
			
			\File::create($tmp_dir, $infilename, $body);

			openssl_pkcs7_sign($tmp_dir.$infilename, $tmp_dir.$outfilename, $signcert, $privkey, array($headers));

			$body = \File::read($tmp_dir.$outfilename, true);

			\File::delete($tmp_dir.$infilename);
			\File::delete($tmp_dir.$outfilename);
			
			$headers = '';
			$body = strtr($body, array_fill_keys(array("\r\n", "\r", "\n"), $newline));
		}

		return array(
			'header' => $headers,
			'body' => $body,
		);
	}

config/email.php には、以下の設定を追加します。

	'defaults' => array(
		
		<省略>
		
		/**
		 * S/MIME
		 */
		'smime' => array(
			'enabled' => false,
			'tmp_dir' => APPPATH.'tmp/',
			'signcert' => '',
			'privkey' => '',
			'keypass' => '',
		),
	),

実際使うときは、もちろんこれを fuel/app/config以下にコピーして書き換えてください。
tmp_dir は、openssl_pkcs7_sign の入出力に必要なファイルが読み書きされます。
適切なパーミッションが与えられたディレクトリを指定する必要があります。

証明書と秘密鍵はsigncert, privkeyでファイルパスを指定します。PEM形式じゃないとダメっぽいです。
詳しことは「PHP: OpenSSL キー/証明書パラメータ」をご覧ください。

これで設定が正しければS/MIME電子署名メールが送れるはずです。
注意としては、Emailのドライバがmail, smtp, sendmailでないとうまくいかないはずです。
mailgunだとWeb API経由で送信しており、こちらで組み立てたメール本文がそのまま配送されないためです。
mailgunでもSMTP経由で送信することも可能ですので、smtpドライバを用いてmailgunのSMTPサーバーで設定を行いましょう。

FuelPHPのArrクラスでは、非常に便利な配列操作のヘルパー関数が用意されているのですが、ありそうでなかったので…

それから、記事タイトルが何言ってるのか正直自分もよく分からないので例を。

array(
    array(
        'group' =&gt; 'CV1',
        'name' =&gt; 'Miku'
    ),
    array(
        'group' =&gt; 'CV2',
        'name' =&gt; 'Rin'
    ),
    array(
        'group' =&gt; 'CV2',
        'name' =&gt; 'Len'
    )
);

のような入力を与えた時に、

array(
    'CV1' =&gt; array(
        array(
            'group' =&gt; 'CV1',
            'name' =&gt; 'Miku'
        )
    ),
    'CV2' =&gt; array(
        array(
            'group' =&gt; 'CV2',
            'name' =&gt; 'Rin'
        ),
        array(
            'group' =&gt; 'CV2',
            'name' =&gt; 'Len'
        )
    )
);

あるいは、

array(
    'CV1' =&gt; array(
        'Miku'
    ),
    'CV2' =&gt; array(
        'Rin',
        'Len'
    )
);

のような出力を得たい、ということです。あるキーの同じ値同士でまとめるような感覚でしょうか。

とりあえず以下のように書いてみました。

class Arr extends \Fuel\Core\Arr
{
	/**
	 * @param array  $assoc     変換する配列
	 * @param string $key_field keyとなる連想配列の添字
	 * @param string $val_field valueとなる連想配列の添字
	 * @return array
	 */
	public static function merge_assoc_key($assoc, $key_field, $val_field = null)
	{
		$output = array_fill_keys(array_column($assoc, $key_field, $key_field), array());

		foreach ($assoc as $elm)
		{
			array_push($output[$elm[$key_field]], $val_field ? $elm[$val_field] : $elm);
		}

		return $output;
	}
}

パフォーマンスがどうか全く分からんですけど、

Arr::merge_assoc_key($input, 'group');
Arr::merge_assoc_key($input, 'group', 'name');

みたいな感じで、期待した結果が得られます。

データベースから取ってきた配列をこんな感じで整形したいことがそこそこある気がします、よね?

書いてて思ったけど、PHPには豊富な配列操作の標準関数が用意されるけども、マニュアルを眺めてあーこれこれ、こんなんあったね、とかやってるあたりまだ未熟だなぁ…

[追記]

LaravelのCollectionクラスにはmapToGroupsというメソッドがあるようです。そうそう、こういうのが欲しかったんだよ。

FuelPHPのマイグレーションでMroongaのラッパーモードを利用したテーブルを作成する方法です。
備忘録として書いておきます。

Mroongaのラッパーモードを利用するには、テーブルオプションを ENGINE = Mroonga COMMENT = 'engine "InnoDB"' のように指定する必要があります。

DBUtil::create_table メソッドの第5引数 $engineでストレージエンジンの指定は可能なのですが、コメントの指定をどうしようかと悩んでいました。
create_tableメソッドのソースを読んでみると、$engineはエスケープなども行われずにそのまま文字列結合しているようなので、COMMENTも一緒にぶち込むことにしました。
見た目は良くないですが、Coreを拡張するのも面倒なので…

class Create_test
{
	public function up()
	{
		\DBUtil::create_table('test', array(
			'id' =&gt; array('constraint' =&gt; 11, 'type' =&gt; 'int', 'auto_increment' =&gt; true, 'unsigned' =&gt; true),
			'content' =&gt; array('constraint' =&gt; 255, 'type' =&gt; 'varchar'),
		), array('id'), false, "Mroonga COMMENT  = 'engine \"InnoDB\"'", 'utf8mb4_general_ci');

		\DBUtil::create_index('test', 'content', 'idx_content', 'fulltext');

	}

	public function down()
	{
		\DBUtil::drop_table('test');
	}
}

他に良い方法があれば是非教えてくださいー

1

以前にPHP 5.4系にアップデートしようと試みましたが、運用中のPukiWiki (1.4.7 UTF-8版)が対応せず断念したことがありました。

該当記事: サーバー保守 ・ 

しかしながら、remiリポジトリも5.4系に移行したこともあり、重い腰を上げていざアップデートへ。
あと、eAcceleratorがエラーで入らなかったのでAPCをインストール。(⇒ 仮メモ: CentOS5でRemiのPHP5.4にupdateするとエラー


さて、PukiWikiの修正に入りましょう。

まずは、hex2bin関数がPHPの組み込み関数と競合する問題(PukiWiki-Dev: BugTrack2/349)の修正。
PHP5.3に戻すこともないのでhex2bin関数をコメントアウトします。

/*
// Inversion of bin2hex()
function hex2bin($hex_string)
{
	// preg_match : Avoid warning : pack(): Type H: illegal hex digit ...
	// (string)   : Always treat as string (not int etc). See BugTrack2/31
	return preg_match('/^[0-9a-f]+$/i', $hex_string) ?
		pack('H*', (string)$hex_string) : $hex_string;
}
*/

加えて、コール時の参照渡し(Call-time pass-by-reference)が廃止されているために動かないプラグインがいくつか。
BugTrackの方にはls2とpcommentが挙がっていますが、前回試みた際に発生したMenuバーが表示されない問題はonlineプラグインにありました。

// array_walk($args, 'plugin_ls2_check_arg', & $params);
array_walk($args, 'plugin_ls2_check_arg', $params);
// array_walk($args, 'plugin_pcomment_check_arg', & $params);
array_walk($args, 'plugin_pcomment_check_arg', $params);
// if (plugin_online_check_online(& $count, $host)) {
if (plugin_online_check_online($count, $host)) {

これでたぶんFatal errorは解決するので、ほぼ問題なく動くと思います。
(EUC-JP版PukiWikiの場合、厄介なhtmlspecialcharsの非互換問題の対応が必要です。)

あとはslashdot:martianの日記: PHP5.4 と PukiWikiを(2)(3)とかを。
参照渡し周りとか箇所が多いのでコードは省略。
また、設定してなければタイムゾーンを設定。

date.timezone = "Asia/Tokyo"

やっぱエラー直したい!という方はこちらnowsky様「PukiWiki/error」のWikiが詳しいです。

こんな感じで一応動いています。触っているうちに何かエラーが発生するかもしれません。
PukiWikiでもCVS版であれば、hex2binの修正だけでどうやら動くようです。
しかしながら、本家PukiWikiはほぼ開発停止のようなものですからね・・・
CVS版を取り込んでいるPukiWiki Plus!で運用してみたサイトもあるのですが、なかなか良い感じです。
更にこれを発展させPHP5.4対応のPukiWiki Advanceというのもあり、かなり改変が加えられていて前衛的な機能が実装されています。
機会があれば試してみようと思います。

やっぱりPHPを5.3.11へダウングレードした。
昨日だか一昨日だかにPHP 5.4.1, 5.3.11がリリースされたみたいなのでついでというか。
バグフィックスが主みたいですけどね。

まぁPukiWikiの件もありますし、しばらくはこれでいきましょう。
ただ、別のWikiないし汎用CMSに乗り換えようかとも考えています。

[追記]
できました ⇒ PHP5.4系にアップデート