Files
kaffeekasse-saas/app/Services/MailerService.php
T
2026-06-15 18:36:57 +02:00

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 fuer den Mailversand ist ungueltig.');
}
if ($subject === '' || $textBody === '') {
throw new RuntimeException('Betreff und Inhalt fuer 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-Neustart',
];
$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));
}
}