Files
LittleBeast/src/Std/Lib/Mailer.php
2026-05-04 14:15:29 +09:00

276 lines
8.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/*************************************************************
# 076 License
Copyright (c) テクニカル諏訪子
Permission is hereby granted to any person obtaining a copy of the software
Little Beast (the "Software") to use, modify, merge, copy, publish, distribute,
sublicense, and/or sell copies of the Software, subject to the following conditions:
1. **Origin Attribution**:
- You must not misrepresent the origin of the Software; you must not claim
you created the original Software.
- If the Software is used in a product, you must either:
a. Provide clear attribution in the product's documentation, user interface,
or other visible areas, **OR**
b. Pay the original developers a fee they specify in writing.
2. **Usage Restriction**:
- The Software, or any derivative works, dependencies, or libraries
incorporating it, must not be used for censorship or to suppress freedom of
speech, expression, or creativity. Prohibited uses include, but are not
limited to:
- Censorship of so-called "hate speech", visuals, non-mainstream opinions,
ideas, or objective reality.
- Tools or systems designed to restrict access to information or
artistic works.
3. **Notice Preservation**:
- This license and the above copyright notice must remain intact in all copies
of the source code.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
namespace Std\Lib;
use LogType;
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;
}
}
}