158 行
5.2 KiB
PHP
158 行
5.2 KiB
PHP
<?php
|
|
if (!defined('IN_APP')) {
|
|
http_response_code(403);
|
|
exit('Forbidden');
|
|
}
|
|
class SmtpMailer
|
|
{
|
|
public $log = [];
|
|
public $lastError = '';
|
|
private $host;
|
|
private $port;
|
|
private $enc;
|
|
private $user;
|
|
private $pass;
|
|
public function __construct($host, $port, $enc, $user, $pass)
|
|
{
|
|
$this->host = (string) $host;
|
|
$this->port = (int) $port;
|
|
$this->enc = (string) $enc;
|
|
$this->user = (string) $user;
|
|
$this->pass = (string) $pass;
|
|
}
|
|
public function send($to, $subject, $bodyHtml, $fromEmail, $fromName)
|
|
{
|
|
if ($this->host === '') {
|
|
$this->lastError = 'SMTP 主机未配置';
|
|
return false;
|
|
}
|
|
$port = $this->port ?: ($this->enc === 'ssl' ? 465 : 587);
|
|
$timeout = 20;
|
|
$scheme = $this->enc === 'ssl' ? 'ssl' : 'tcp';
|
|
$opts = [];
|
|
if ($this->enc === 'ssl' || $this->enc === 'tls') {
|
|
$opts['ssl'] = [
|
|
'verify_peer' => false,
|
|
'verify_peer_name' => false,
|
|
'allow_self_signed' => true,
|
|
];
|
|
}
|
|
$ctx = $opts ? stream_context_create($opts) : null;
|
|
$errno = 0;
|
|
$errstr = '';
|
|
$sock = @stream_socket_client(
|
|
$scheme . ':
|
|
$errno, $errstr, $timeout, STREAM_CLIENT_CONNECT, $ctx
|
|
);
|
|
if (!$sock) {
|
|
$this->lastError = "无法连接 SMTP 服务器({$this->host}:{$port}):$errstr ($errno)";
|
|
return false;
|
|
}
|
|
stream_set_timeout($sock, $timeout);
|
|
if ($this->talk($sock, null, [220]) === false) {
|
|
fclose($sock);
|
|
return false;
|
|
}
|
|
$hostHeader = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost';
|
|
if ($this->talk($sock, 'EHLO ' . $hostHeader, [250]) === false) {
|
|
fclose($sock);
|
|
return false;
|
|
}
|
|
if ($this->enc === 'tls') {
|
|
if ($this->talk($sock, 'STARTTLS', [220]) === false) {
|
|
fclose($sock);
|
|
return false;
|
|
}
|
|
if (!@stream_socket_enable_crypto($sock, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
|
|
$this->lastError = 'STARTTLS 加密协商失败';
|
|
fclose($sock);
|
|
return false;
|
|
}
|
|
if ($this->talk($sock, 'EHLO ' . $hostHeader, [250]) === false) {
|
|
fclose($sock);
|
|
return false;
|
|
}
|
|
}
|
|
if ($this->talk($sock, 'AUTH LOGIN', [334]) === false) {
|
|
fclose($sock);
|
|
return false;
|
|
}
|
|
if ($this->talk($sock, base64_encode($this->user), [334]) === false) {
|
|
fclose($sock);
|
|
return false;
|
|
}
|
|
if ($this->talk($sock, base64_encode($this->pass), [235]) === false) {
|
|
fclose($sock);
|
|
return false;
|
|
}
|
|
if ($this->talk($sock, 'MAIL FROM:<' . $fromEmail . '>', [250]) === false) {
|
|
fclose($sock);
|
|
return false;
|
|
}
|
|
$recipients = array_filter(array_map('trim', explode(',', $to)));
|
|
foreach ($recipients as $rcpt) {
|
|
$this->talk($sock, 'RCPT TO:<' . $rcpt . '>', [250, 251]);
|
|
}
|
|
if ($this->talk($sock, 'DATA', [354]) === false) {
|
|
fclose($sock);
|
|
return false;
|
|
}
|
|
$data = $this->buildData($to, $subject, $bodyHtml, $fromEmail, $fromName);
|
|
fwrite($sock, $data);
|
|
if ($this->talk($sock, '.', [250]) === false) {
|
|
fclose($sock);
|
|
return false;
|
|
}
|
|
$this->talk($sock, 'QUIT', [221]);
|
|
fclose($sock);
|
|
return true;
|
|
}
|
|
private function buildData($to, $subject, $body, $fromEmail, $fromName)
|
|
{
|
|
$eol = "\r\n";
|
|
$headers = [];
|
|
$headers[] = 'From: ' . $this->encodeHeader($fromName) . ' <' . $fromEmail . '>';
|
|
$headers[] = 'To: ' . $to;
|
|
$headers[] = 'Subject: ' . $this->encodeHeader($subject);
|
|
$headers[] = 'Date: ' . date('r');
|
|
$headers[] = 'MIME-Version: 1.0';
|
|
$headers[] = 'Content-Type: text/html; charset=UTF-8';
|
|
$headers[] = 'Content-Transfer-Encoding: base64';
|
|
$msg = implode($eol, $headers) . $eol . $eol;
|
|
$msg .= chunk_split(base64_encode($body), 76, $eol);
|
|
return $msg;
|
|
}
|
|
private function encodeHeader($s)
|
|
{
|
|
if (preg_match('/[^\x00-\x7F]/', (string) $s)) {
|
|
return '=?UTF-8?B?' . base64_encode($s) . '?=';
|
|
}
|
|
return $s;
|
|
}
|
|
private function talk($sock, $cmd, $expectCodes)
|
|
{
|
|
if ($cmd !== null) {
|
|
fwrite($sock, $cmd . "\r\n");
|
|
$this->log[] = 'C: ' . $cmd;
|
|
}
|
|
$resp = '';
|
|
while (true) {
|
|
$line = @fgets($sock, 515);
|
|
if ($line === false) {
|
|
break;
|
|
}
|
|
$resp .= $line;
|
|
if (isset($line[3]) && $line[3] === ' ') {
|
|
break;
|
|
}
|
|
}
|
|
$this->log[] = 'S: ' . trim($resp);
|
|
$code = (int) substr($resp, 0, 3);
|
|
if (!in_array($code, (array) $expectCodes, true)) {
|
|
$this->lastError = '服务器返回错误:' . trim($resp);
|
|
return false;
|
|
}
|
|
return $resp;
|
|
}
|
|
}
|