178 lines
5.1 KiB
PHP
178 lines
5.1 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use RuntimeException;
|
|
|
|
final class MailerService
|
|
{
|
|
private readonly string $rootPath;
|
|
|
|
public function __construct(private readonly array $config = [])
|
|
{
|
|
$this->rootPath = dirname(__DIR__, 2);
|
|
}
|
|
|
|
public function send(string $to, string $subject, string $textBody, array $headers = []): bool
|
|
{
|
|
$to = trim($to);
|
|
$subject = trim($subject);
|
|
$textBody = trim($textBody);
|
|
|
|
if ($to === '' || !filter_var($to, FILTER_VALIDATE_EMAIL)) {
|
|
throw new RuntimeException('Die Zieladresse für den Mailversand ist ungültig.');
|
|
}
|
|
|
|
if ($subject === '' || $textBody === '') {
|
|
throw new RuntimeException('Betreff und Inhalt für den Mailversand sind erforderlich.');
|
|
}
|
|
|
|
$subjectLine = $this->encodeSubject($this->sanitizeHeaderValue($this->subjectPrefix() . $subject));
|
|
$headerLines = $this->buildHeaders($headers);
|
|
$body = $this->normalizeBody($textBody);
|
|
|
|
$sent = false;
|
|
|
|
if (function_exists('mail')) {
|
|
$sent = @mail($to, $subjectLine, $body, implode("\r\n", $headerLines));
|
|
}
|
|
|
|
if ($sent) {
|
|
return true;
|
|
}
|
|
|
|
if (!$this->usesLogFallback()) {
|
|
throw new RuntimeException('Die E-Mail konnte nicht versendet werden.');
|
|
}
|
|
|
|
$this->writeFallbackLog([
|
|
'to' => $to,
|
|
'subject' => $this->subjectPrefix() . $subject,
|
|
'headers' => $headerLines,
|
|
'body' => $textBody,
|
|
]);
|
|
|
|
return false;
|
|
}
|
|
|
|
private function buildHeaders(array $headers): array
|
|
{
|
|
$headerLines = [
|
|
'MIME-Version: 1.0',
|
|
'Content-Type: text/plain; charset=UTF-8',
|
|
'Content-Transfer-Encoding: 8bit',
|
|
'X-Mailer: Kaffeeliste',
|
|
];
|
|
|
|
$from = trim((string) ($this->config['from'] ?? ''));
|
|
|
|
if ($from !== '') {
|
|
$headerLines[] = 'From: ' . $this->sanitizeHeaderValue($from);
|
|
}
|
|
|
|
$replyTo = trim((string) ($this->config['reply_to'] ?? ''));
|
|
|
|
if ($replyTo !== '') {
|
|
$headerLines[] = 'Reply-To: ' . $this->sanitizeHeaderValue($replyTo);
|
|
}
|
|
|
|
foreach ($headers as $name => $value) {
|
|
if (is_int($name)) {
|
|
$line = trim((string) $value);
|
|
|
|
if ($line !== '' && str_contains($line, ':')) {
|
|
[$rawName, $rawValue] = array_pad(explode(':', $line, 2), 2, '');
|
|
$this->appendHeader($headerLines, $rawName, $rawValue);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
$this->appendHeader($headerLines, (string) $name, (string) $value);
|
|
}
|
|
|
|
return $headerLines;
|
|
}
|
|
|
|
private function appendHeader(array &$headerLines, string $name, string $value): void
|
|
{
|
|
$name = trim($name);
|
|
|
|
if ($name === '' || preg_match('/[^A-Za-z0-9-]/', $name)) {
|
|
return;
|
|
}
|
|
|
|
$headerLines[] = $name . ': ' . $this->sanitizeHeaderValue($value);
|
|
}
|
|
|
|
private function writeFallbackLog(array $payload): void
|
|
{
|
|
$path = $this->resolveLogPath();
|
|
$directory = dirname($path);
|
|
|
|
if (!is_dir($directory) && !mkdir($directory, 0775, true) && !is_dir($directory)) {
|
|
throw new RuntimeException('Das Mail-Log-Verzeichnis konnte nicht erstellt werden.');
|
|
}
|
|
|
|
$encodedPayload = json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
|
|
|
if (!is_string($encodedPayload)) {
|
|
$encodedPayload = '{"error":"mail payload could not be encoded"}';
|
|
}
|
|
|
|
$entry = '[' . gmdate(DATE_ATOM) . "] mail_fallback\n" . $encodedPayload . "\n\n";
|
|
|
|
if (file_put_contents($path, $entry, FILE_APPEND | LOCK_EX) === false) {
|
|
throw new RuntimeException('Das Mail-Fallback-Log konnte nicht geschrieben werden.');
|
|
}
|
|
}
|
|
|
|
private function resolveLogPath(): string
|
|
{
|
|
$configuredPath = trim((string) ($this->config['log_file'] ?? ''));
|
|
|
|
if ($configuredPath === '') {
|
|
return $this->rootPath . '/storage/logs/mail.log';
|
|
}
|
|
|
|
if (str_starts_with($configuredPath, '/')) {
|
|
return $configuredPath;
|
|
}
|
|
|
|
return $this->rootPath . '/' . ltrim($configuredPath, '/');
|
|
}
|
|
|
|
private function usesLogFallback(): bool
|
|
{
|
|
return (bool) ($this->config['log_fallback'] ?? true);
|
|
}
|
|
|
|
private function subjectPrefix(): string
|
|
{
|
|
return (string) ($this->config['subject_prefix'] ?? '');
|
|
}
|
|
|
|
private function sanitizeHeaderValue(string $value): string
|
|
{
|
|
return trim(str_replace(["\r", "\n"], ' ', $value));
|
|
}
|
|
|
|
private function encodeSubject(string $subject): string
|
|
{
|
|
if ($subject === '') {
|
|
return $subject;
|
|
}
|
|
|
|
if (function_exists('mb_encode_mimeheader')) {
|
|
return mb_encode_mimeheader($subject, 'UTF-8', 'B', "\r\n");
|
|
}
|
|
|
|
return $subject;
|
|
}
|
|
|
|
private function normalizeBody(string $textBody): string
|
|
{
|
|
return str_replace("\n", "\r\n", str_replace(["\r\n", "\r"], "\n", $textBody));
|
|
}
|
|
}
|