レポジトリ種類: SVN
<?php
namespace Site\Lib;
class Mailer {
private $socket;
private string $host;
private int $port;
private ?string $user;
private ?string $pass;
private ?string $pgpKey;
private ?string $pgpPass;
/**
* コンストラクタ
*/
public function __construct() {
if (!MAILER_ENABLED) {
throw new \Exception("メーラーは無効です。");
}
$this->host = MAILINFO['host'];
$this->port = MAILINFO['port'];
$this->user = MAILINFO['user'];
$this->pass = MAILINFO['pass'];
}
/**
* ソケット接続を開く。
*
* @return void
*/
public function connect(): void {
$this->socket = fsockopen($this->host, $this->port, $errno, $err, 30);
if (!$this->socket) {
$msg = "接続に失敗: {$err} ({$errno})";
logger(\LogType::Mailer, $msg);
throw new \Exception($msg);
}
$this->readResponse();
}
/**
* ソケット接続を切断する。
*
* @return void
*/
public function disconnect(): void {
$this->sendCommand('QUIT', 221);
fclose($this->socket);
}
/**
* サーバーで認証する。
* ユーザー名とパスワードが指定されている場合はAUTH LOGINを使用する。
*
* @return void
*/
public function authenticate(): void {
$ehloRes = $this->sendCommand('EHLO '.$this->host, 250);
// STARTTLSは対応するかどうか確認
if (strpos($ehloRes, 'STARTTLS') !== false) {
$this->sendCommand('STARTTLS', 220);
// TLS暗号化
if (!stream_socket_enable_crypto(
$this->socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
$msg = "TLSハンドシェイクに失敗";
logger(\LogType::Mailer, $msg);
throw new \Exception($msg);
}
$this->sendCommand('EHLO '.gethostname(), 250);
}
if ($this->user && $this->pass) {
$this->sendCommand('AUTH LOGIN', 334);
$this->sendCommand(base64_encode($this->user), 334);
$this->sendCommand(base64_encode($this->pass), 235);
}
}
/**
* TLSハンドシェイクに失敗する。
*
* @param string $to 受信者のメールアドレス。
* @param string $subject メールの件名。
* @param string $body メールの本文。
* @param string|null $toName 受信者の名前。
* @param string|null $replyTo 返信先メールアドレス。
* @param string|null $replyToName 返信先の名前。
* @param bool $pgpSign PGPで署名するかどうか(現在は正常に動作していません)。
*
* @return void
*/
public function send(
string $to,
string $subject,
string $body,
?string $toName = null,
?string $replyTo = null,
?string $replyToName = null,
bool $pgpSign = false
): void {
$from = MAILINFO['from'];
$this->sendCommand("MAIL FROM: <{$from}>", 250);
$this->sendCommand("RCPT TO: <{$to}>", 250);
$this->sendCommand('DATA', 354);
$headers = "Date: ".date('r')."\r\n"; // RFC 2822
$encSubject = mb_encode_mimeheader($subject, 'UTF-8', 'Q');
$headers .= "Subject: {$encSubject}\r\n";
$fromHeader = mb_encode_mimeheader(SITEINFO['title'], 'UTF-8', 'Q')." <{$from}>";
$headers .= "From: {$fromHeader}\r\n";
$toHeader = $toName
? mb_encode_mimeheader($toName, 'UTF-8', 'Q')." <{$to}>" : $to;
$headers .= "To: {$toHeader}\r\n";
if ($replyTo) {
$replyToHeader = $replyToName
? mb_encode_mimeheader($replyToName, 'UTF-8', 'Q')." <{$replyTo}>" : $replyTo;
$headers .= "Reply-To: {$replyToHeader}\r\n";
}
$headers .= "MIME-Version: 1.0\r\n";
if ($pgpSign) {
$boundary = uniqid('BOUNDARY_');
$headers .= "Content-Type: multipart/signed;\r\n";
$headers .= " protocol=\"application/pgp-signature\";\r\n";
$headers .= " micalg=php-sha512;\r\n";
$headers .= " boundary=\"{$boundary}\"\r\n";
} else {
$headers .= "Content-Type: text/plain; charset=utf-8\r\n";
$headers .= "Content-Transfer-Encoding: quoted-printable\r\n";
}
$headers .= "X-Mailer: 076 Little Beast\r\n";
$encBody = quoted_printable_encode($body);
$data = $headers."\r\n".$encBody;
if ($pgpSign) {
$signature = $this->signMessage($data);
$data = "--{$boundary}\r\n"
.$data."\r\n"
."Content-Type: application/pgp-signature; name=\"signature.asc\"\r\n"
."Content-Disposition: attachment; filename=\"signature.asc\"\r\n\r\n"
.$signature."\r\n"
."--{$boundary}--\r\n";
}
$data .= "\r\n.\r\n";
fwrite($this->socket, $data);
$response = $this->readResponse();
if (substr($response, 0, 3) != '250') {
$msg = "メール送信に失敗: {$response}";
logger(\LogType::Mailer, $msg);
throw new \Exception($msg);
}
}
/**
* お好みのタイムゾーンを設定する。
* 設定しない場合、php.iniで設定されたタイムゾーンがデフォルトになる。
* それも設定されていない場合、GMTタイムゾーンがデフォルトになる。
*
* @param string $zone IANAタイムゾーンデータベース標準のタイムゾーン(例:Asia/Tokyo)
* @return void
*/
public function setTimezone(string $zone): void {
date_default_timezone_set($zone);
}
/**
* PGPキーとオプションでパスフレーズを設定する。
*
* @param string $keypath PGP署名に使用する秘密鍵へのパス。
* @param string|null $passphrase 設定されている場合、署名用のパスフレーズ。
*
* @return void
*/
public function enablePGP(string $keypath, ?string $passphrase = null): void {
$this->pgpKey = file_get_contents($keypath);
$this->pgpPass = $passphrase;
}
// 機能性メソッド
private function sendCommand(string $command, int $retcode): string {
fwrite($this->socket, $command."\r\n");
$res = $this->readResponse();
if (substr($res, 0, 3) != $retcode) {
$msg = "「{$command}」に対する予期しないレスポンス: {$res}";
logger(\LogType::Mailer, $msg);
throw new \Exception($msg);
}
return $res;
}
private function readResponse(): string {
$res = '';
while ($line = fgets($this->socket, 515)) {
$res .= $line;
if (substr($line, 3, 1) == ' ') break; // レスポンスの終了だ
}
return $res;
}
private function signMessage(string $message): string {
if (extension_loaded('gnupg')) {// gnupg延長は有効の場合
$gpg = new \gnupg();
$gpg->addsignkey($this->pgpKey, $this->pgpPass);
$gpg->setsignmode(\gnupg::SIG_MODE_DETACH);
return $gpg->sign($message);
} else { // なければ、CLIツールを使う(gnupgをインストールは必須)
$tmp = tempnam(sys_get_temp_dir(), 'pgpmsg');
file_put_contents($tmp, $message);
$sig = shell_exec(
"gpg --batch "
.($this->pgpPass ? "--passphrase {$this->pgpPass} " : '')
."--detach-sign --armor {$tmp} 2>&1"
);
unlink($tmp);
return $sig;
}
}
}