File: //proc/662498/root/home/retile.ru/public_html/system/library/mail/smtp.php
<?php
declare(strict_types=1);
namespace Mail;
class Smtp {
/** @var string */
public $smtp_hostname = '';
/** @var string */
public $smtp_username = '';
/** @var string */
public $smtp_password = '';
/** @var int|string */
public $smtp_port = 25;
/** @var int|string */
public $smtp_timeout = 5;
/** @var bool */
public $verp = false;
/** @var string */
public $parameter = '';
/** @var array<int, string>|string */
public $to = [];
/** @var string */
public $from = '';
/** @var string */
public $sender = '';
/** @var string */
public $reply_to = '';
/** @var string */
public $subject = '';
/** @var string */
public $text = '';
/** @var string */
public $html = '';
/** @var array<int, string>|string */
public $attachments = [];
public function send(): void {
$recipients = $this->normalizeRecipients();
$this->from = 'cart@retile.ru';
$this->sender = 'cart@retile.ru';
$this->reply_to = 'cart@retile.ru';
if (!$recipients) {
throw new \Exception('Error: No recipients specified for SMTP message!');
}
if (!$this->from) {
throw new \Exception('Error: The "from" address is not configured for SMTP message!');
}
$boundary = '----=_NextPart_' . md5((string) microtime(true));
$replyToName = $this->reply_to ?: $this->sender;
$replyToAddress = $this->reply_to ?: $this->from;
$serverIdentity = $this->getServerIdentity();
$header = 'MIME-Version: 1.0' . PHP_EOL;
$header .= 'To: <' . implode(',', $recipients) . '>' . PHP_EOL;
$header .= 'Subject: =?UTF-8?B?' . base64_encode($this->subject) . '?=' . PHP_EOL;
$header .= 'Date: ' . date('D, d M Y H:i:s O') . PHP_EOL;
$header .= 'From: =?UTF-8?B?' . base64_encode($this->sender ?: $this->from) . '?= <' . $this->from . '>' . PHP_EOL;
$header .= 'Reply-To: =?UTF-8?B?' . base64_encode($replyToName) . '?= <' . $replyToAddress . '>' . PHP_EOL;
$header .= 'Return-Path: ' . $this->from . PHP_EOL;
$header .= 'X-Mailer: PHP/' . PHP_VERSION . PHP_EOL;
$header .= 'Content-Type: multipart/mixed; boundary="' . $boundary . '"' . PHP_EOL . PHP_EOL;
if (!$this->html) {
$message = '--' . $boundary . PHP_EOL;
$message .= 'Content-Type: text/plain; charset="utf-8"' . PHP_EOL;
$message .= 'Content-Transfer-Encoding: 8bit' . PHP_EOL . PHP_EOL;
$message .= ($this->text ?: '') . PHP_EOL;
} else {
$message = '--' . $boundary . PHP_EOL;
$message .= 'Content-Type: multipart/alternative; boundary="' . $boundary . '_alt"' . PHP_EOL . PHP_EOL;
$message .= '--' . $boundary . '_alt' . PHP_EOL;
$message .= 'Content-Type: text/plain; charset="utf-8"' . PHP_EOL;
$message .= 'Content-Transfer-Encoding: 8bit' . PHP_EOL . PHP_EOL;
$message .= ($this->text ?: 'This is a HTML email and your email client software does not support HTML email!') . PHP_EOL;
$message .= '--' . $boundary . '_alt' . PHP_EOL;
$message .= 'Content-Type: text/html; charset="utf-8"' . PHP_EOL;
$message .= 'Content-Transfer-Encoding: 8bit' . PHP_EOL . PHP_EOL;
$message .= $this->html . PHP_EOL;
$message .= '--' . $boundary . '_alt--' . PHP_EOL;
}
foreach ($this->normalizeAttachments() as $attachment) {
if (!is_file($attachment) || !is_readable($attachment)) {
continue;
}
$content = file_get_contents($attachment);
if ($content === false) {
continue;
}
$filename = basename($attachment);
$message .= '--' . $boundary . PHP_EOL;
$message .= 'Content-Type: application/octet-stream; name="' . $filename . '"' . PHP_EOL;
$message .= 'Content-Transfer-Encoding: base64' . PHP_EOL;
$message .= 'Content-Disposition: attachment; filename="' . $filename . '"' . PHP_EOL;
$message .= 'Content-ID: <' . urlencode($filename) . '>' . PHP_EOL;
$message .= 'X-Attachment-Id: ' . urlencode($filename) . PHP_EOL . PHP_EOL;
$message .= chunk_split(base64_encode($content));
}
$message .= '--' . $boundary . '--' . PHP_EOL;
$hostname = $this->resolveHostname();
$port = (int) $this->smtp_port ?: 25;
$timeout = max(1, (int) $this->smtp_timeout ?: 5);
$handle = @fsockopen($hostname, $port, $errno, $errstr, $timeout);
if (!$handle) {
throw new \Exception(sprintf(
'Error: %s (%s) while connecting to SMTP host %s:%s',
$errstr ?: 'Unable to connect',
$errno ?: 'n/a',
$hostname,
$port
));
}
stream_set_timeout($handle, $timeout, 0);
try {
$greeting = $this->readReply($handle);
if (substr($greeting, 0, 3) !== '220') {
throw new \Exception('Error: SMTP server greeting was not accepted: ' . trim($greeting));
}
$this->sendCommand($handle, 'EHLO ' . $serverIdentity, ['250'], true);
if ($this->isTls()) {
$this->sendCommand($handle, 'STARTTLS', ['220']);
$cryptoEnabled = @stream_socket_enable_crypto(
$handle,
true,
STREAM_CRYPTO_METHOD_TLS_CLIENT
| (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT') ? STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT : 0)
| (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') ? STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT : 0)
);
if ($cryptoEnabled !== true) {
throw new \Exception('Error: Failed to enable TLS on SMTP connection.');
}
$this->sendCommand($handle, 'EHLO ' . $serverIdentity, ['250']);
}
if ($this->smtp_username !== '' && $this->smtp_password !== '') {
$this->sendCommand($handle, 'AUTH LOGIN', ['334']);
$this->sendCommand($handle, base64_encode($this->smtp_username), ['334']);
$this->sendCommand($handle, base64_encode($this->smtp_password), ['235']);
} else {
$this->sendCommand($handle, 'HELO ' . $serverIdentity, ['250']);
}
$mailFromCommand = $this->verp ? 'MAIL FROM: <' . $this->from . '> XVERP' : 'MAIL FROM: <' . $this->from . '>';
$this->sendCommand($handle, $mailFromCommand, ['250']);
foreach ($recipients as $recipient) {
$this->sendCommand($handle, 'RCPT TO: <' . $recipient . '>', ['250', '251']);
}
$this->sendCommand($handle, 'DATA', ['354']);
foreach ($this->prepareMessageLines($header, $message) as $line) {
$this->write($handle, $line);
}
$this->sendCommand($handle, '.', ['250']);
$this->sendCommand($handle, 'QUIT', ['221']);
} finally {
fclose($handle);
}
}
/**
* @return array<int, string>
*/
private function normalizeRecipients(): array {
if (is_array($this->to)) {
$recipients = $this->to;
} elseif (is_string($this->to) && $this->to !== '') {
$recipients = array_map('trim', explode(',', $this->to));
} else {
$recipients = [];
}
return array_values(array_filter(array_map('trim', $recipients), static function ($value) {
return $value !== '';
}));
}
/**
* @return array<int, string>
*/
private function normalizeAttachments(): array {
if (empty($this->attachments)) {
return [];
}
$attachments = is_array($this->attachments) ? $this->attachments : [$this->attachments];
return array_values(array_filter(array_map(static function ($attachment) {
return is_string($attachment) ? trim($attachment) : '';
}, $attachments), static function ($path) {
return $path !== '';
}));
}
private function resolveHostname(): string {
if ($this->isTls()) {
return substr($this->smtp_hostname, 6);
}
return $this->smtp_hostname ?: 'localhost';
}
private function isTls(): bool {
return strncmp($this->smtp_hostname, 'tls://', 6) === 0;
}
private function getServerIdentity(): string {
$identity = getenv('SERVER_NAME') ?: ($_SERVER['SERVER_NAME'] ?? '');
return $identity ?: 'localhost';
}
private function sendCommand($handle, string $command, array $expectedCodes, bool $ignoreInitial220 = false): string {
$this->write($handle, $command . "\r\n");
$reply = $this->readReply($handle, $ignoreInitial220);
$code = substr($reply, 0, 3);
foreach ($expectedCodes as $expectedCode) {
if ((string) $expectedCode === $code) {
return $reply;
}
}
throw new \Exception(sprintf(
'Error: Unexpected SMTP response for "%s": %s',
trim($command),
trim($reply)
));
}
private function readReply($handle, bool $ignoreInitial220 = false): string {
$reply = '';
while (($line = fgets($handle, 515)) !== false) {
if (
$ignoreInitial220
&& $reply === ''
&& substr($line, 0, 3) === '220'
&& (strlen($line) >= 4 && $line[3] === ' ')
) {
continue;
}
$reply .= $line;
if (strlen($line) >= 4 && $line[3] === ' ') {
break;
}
}
if ($reply === '') {
throw new \Exception('Error: Empty response from SMTP server.');
}
return $reply;
}
private function prepareMessageLines(string $header, string $message): array {
$normalized = str_replace(["\r\n", "\r"], "\n", $header . $message);
$lines = explode("\n", $normalized);
$isWindows = strncasecmp(PHP_OS, 'WIN', 3) === 0;
$output = [];
foreach ($lines as $line) {
$chunks = str_split($line, 998);
foreach ($chunks as $chunk) {
if ($isWindows) {
$chunk = str_replace("\n", "\r\n", $chunk);
}
$output[] = $chunk . "\r\n";
}
}
return $output;
}
private function write($handle, string $data): void {
if (false === fwrite($handle, $data)) {
throw new \Exception('Error: Unable to write to SMTP socket.');
}
}
}