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 を追加するのも忘れずに。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?php
\Autoloader::add_core_namespace('Email');

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

));

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

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
<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 には、以下のような設定を追加します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
'defaults' => array(
		
		<省略>
		
		/**
		 * S/MIME
		 */
		'smime' => array(
			'enabled' => false,
			'tmp_dir' => APPPATH.'tmp/',
			'signcert' => '',
			'privkey' => '',
			'keypass' => '',
		),
	),

tmp_dir は、openssl_pkcs7_sign の入出力に必要なファイルが読み書きされます。適切なパーミッションが与えられたディレクトリを指定する必要があります。

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

これで設定が正しければS/MIME電子署名メールが送れるはずです。

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