1
<?php
2
declare(strict_types=1);
3
4
namespace Machweis\Backend;
5
6
final readonly class SmtpMailer
7
{
8
public function __construct(private Config $config) {}
9
10
public function sendPdf(string $recipient, string $subject, string $body, string $filename, string $pdf): void
11
{
12
if (!filter_var($recipient, FILTER_VALIDATE_EMAIL) || !filter_var($this->config->mailFromAddress, FILTER_VALIDATE_EMAIL) || $this->config->smtpHost === '' || $this->config->smtpUser === '' || $this->config->smtpPassword === '') throw new \RuntimeException('Transactional mail is not configured.');
13
$socket = stream_socket_client('ssl://' . $this->config->smtpHost . ':' . $this->config->smtpPort, $code, $error, 15, STREAM_CLIENT_CONNECT);
14
if (!is_resource($socket)) throw new \RuntimeException('SMTP connection failed.');
15
stream_set_timeout($socket, 15);
16
$this->expect($socket, [220]); $this->command($socket, 'EHLO machweis', [250]);
17
$this->command($socket, 'AUTH LOGIN', [334]); $this->command($socket, base64_encode($this->config->smtpUser), [334]); $this->command($socket, base64_encode($this->config->smtpPassword), [235]);
18
$this->command($socket, 'MAIL FROM:<' . $this->config->mailFromAddress . '>', [250]); $this->command($socket, 'RCPT TO:<' . $recipient . '>', [250, 251]); $this->command($socket, 'DATA', [354]);
19
$boundary = 'machweis-' . bin2hex(random_bytes(12));
20
$headers = 'From: ' . $this->header($this->config->mailFromName) . ' <' . $this->config->mailFromAddress . ">\r\n" . 'To: <' . $recipient . ">\r\n" . 'Subject: ' . $this->header($subject) . "\r\nMIME-Version: 1.0\r\nContent-Type: multipart/mixed; boundary=\"$boundary\"\r\n";
21
$message = $headers . "\r\n--$boundary\r\nContent-Type: text/plain; charset=UTF-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\n" . str_replace("\n.", "\n..", $body) . "\r\n--$boundary\r\nContent-Type: application/pdf; name=\"" . $this->header($filename) . "\"\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment; filename=\"" . $this->header($filename) . "\"\r\n\r\n" . chunk_split(base64_encode($pdf)) . "--$boundary--\r\n.\r\n";
22
fwrite($socket, $message); $this->expect($socket, [250]); $this->command($socket, 'QUIT', [221]); fclose($socket);
23
}
24
private function command($socket, string $command, array $expected): void { fwrite($socket, $command . "\r\n"); $this->expect($socket, $expected); }
25
private function expect($socket, array $expected): void { $line = ''; do { $part = fgets($socket, 1024); if ($part === false) throw new \RuntimeException('SMTP response failed.'); $line = $part; } while (isset($line[3]) && $line[3] === '-'); if (!in_array((int) substr($line, 0, 3), $expected, true)) throw new \RuntimeException('SMTP rejected delivery.'); }
26
private function header(string $value): string { return str_replace(["\r", "\n"], '', $value); }
27
}
28