logFile = $base . '/debug/mailer_debug.log'; } private function log(string $msg, array $ctx = []): void { if (!defined('APP_DEBUG') || APP_DEBUG !== true) { return; } $line = '[' . date('Y-m-d H:i:s') . '] ' . $msg; if ($ctx) { $line .= ' ' . json_encode($ctx, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } $line .= "\n"; $dir = dirname($this->logFile); if (!is_dir($dir)) { @mkdir($dir, 0775, true); } // For clarity keep only the latest run in the log: truncate once per request if ($this->logCleared === false) { @file_put_contents($this->logFile, ''); $this->logCleared = true; } @file_put_contents($this->logFile, $line, FILE_APPEND); } private function templates(): array { $env = $this->app->config()->env; $root = __DIR__ . '/../../config/emailtemplates.php'; $envPath = __DIR__ . "/../../config/{$env}/emailtemplates.php"; $file = is_file($root) ? $root : $envPath; $emailtemplates = []; if (is_file($file)) { /** @noinspection PhpIncludeInspection */ include $file; // populates $emailtemplates variable from included file } return is_array($emailtemplates ?? null) ? $emailtemplates : []; } private function renderTemplate(string $key, array $vars): array { $templates = $this->templates(); $id = $templates[$key] ?? $key; $this->log('template_resolved_id', ['key' => $key, 'id' => $id]); $apiBase = getenv('EMAILTEMPLATE_API_BASE') ?: ''; $apiToken = getenv('EMAILTEMPLATE_API_TOKEN') ?: ''; if ($apiBase && $apiToken) { $payload = [ 'template' => $id, 'placeholders' => $vars, ]; $payload['token'] = $apiToken; $payloadForLog = $payload; $payloadForLog['token'] = '[hidden length ' . strlen((string)$apiToken) . ']'; $this->log('template_api_request_payload', [ 'url' => $apiBase, 'payload' => $payloadForLog, ]); $this->log('template_api_request', ['template' => $id, 'placeholders' => array_keys($vars)]); $ctx = stream_context_create([ 'http' => [ 'method' => 'POST', 'header' => "Content-Type: application/json\r\n", 'timeout' => 15, 'content' => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ], ]); $resp = @file_get_contents($apiBase, false, $ctx); if ($resp !== false) { $status = null; if (isset($http_response_header) && is_array($http_response_header)) { foreach ($http_response_header as $hdr) { if (preg_match('~^HTTP/\\S+\\s+(\\d+)~i', $hdr, $m)) { $status = (int)$m[1]; break; } } } $this->log('template_api_response_raw', [ 'status' => $status, 'body' => $resp, ]); $decoded = json_decode($resp, true); if (is_array($decoded) && !empty($decoded['ok']) && !empty($decoded['html'])) { $this->log('template_api_success', ['template' => $id, 'subject' => $decoded['subject'] ?? null, 'html_len' => strlen((string)$decoded['html'])]); return [ 'id' => $id, 'subject' => $decoded['subject'] ?? 'Papa-Kind-Treff', 'html' => $decoded['html'], ]; } $this->log('template_api_response_invalid', ['template' => $id, 'response' => $decoded]); } else { $this->log('template_api_unreachable', ['template' => $id]); } } // Fallback: einfacher Text $subject = 'Papa-Kind-Treff'; $body = $id; foreach ($vars as $k => $v) { $body = str_replace(['{' . $k . '}', '{{' . $k . '}}'], (string)$v, $body); } $this->log('template_fallback_used', ['template' => $id]); return [ 'id' => $id, 'subject' => $subject, 'html' => nl2br(htmlspecialchars($body, ENT_QUOTES)), ]; } public function sendTemplate(string $templateKey, string $to, array $vars = []): void { if (!filter_var($to, FILTER_VALIDATE_EMAIL)) { throw new \InvalidArgumentException('Invalid recipient email.'); } $tpl = $this->renderTemplate($templateKey, $vars); $resolvedId = $tpl['id'] ?? $templateKey; $subject = $tpl['subject'] ?? 'Papa-Kind-Treff'; $html = $tpl['html'] ?? ''; $this->log('mail_rendered_template', [ 'template_key' => $templateKey, 'template_id' => $resolvedId, 'subject' => $subject, 'html_len' => strlen((string)$html), 'html_preview' => substr((string)$html, 0, 200), ]); $transport = getenv('MAIL_TRANSPORT') ?: 'mail'; $fromEmail = getenv('MAIL_FROM') ?: 'no-reply@' . $this->app->config()->primaryDomain; $fromName = getenv('MAIL_FROM_NAME') ?: 'Papa-Kind-Treff'; $this->log('mail_send_start', [ 'template_key' => $templateKey, 'template_id' => $resolvedId, 'to' => $to, 'transport' => $transport, 'subject' => $subject ]); if ($transport === 'smtp') { $this->sendSmtp($to, $subject, $html, $fromEmail, $fromName); } else { $this->sendMailFn($to, $subject, $html, $fromEmail, $fromName); } } private function sendMailFn(string $to, string $subject, string $html, string $from, string $fromName): void { $headers = []; if ($from) { $headers[] = 'From: ' . sprintf('"%s" <%s>', addslashes($fromName), $from); } $headers[] = 'Content-Type: text/html; charset=utf-8'; $ok = @mail($to, $subject, $html, implode("\r\n", $headers)); $this->log('mail_mail_transport', ['to' => $to, 'ok' => $ok]); if (!$ok) { throw new \RuntimeException('mail() transport failed'); } } private function sendSmtp(string $to, string $subject, string $html, string $from, string $fromName): void { $host = getenv('SMTP_HOST') ?: ''; $port = (int)(getenv('SMTP_PORT') ?: 587); $user = getenv('SMTP_USER') ?: ''; $pass = getenv('SMTP_PASS') ?: ''; $secure = strtolower(getenv('SMTP_SECURE') ?: 'tls'); // tls|ssl|none if (!$host) { $this->log('mail_smtp_missing_host_fallback_mail', []); $this->sendMailFn($to, $subject, $html, $from, $fromName); return; } $proto = ($secure === 'ssl') ? 'ssl://' : ''; $fp = @stream_socket_client($proto . $host . ':' . $port, $errno, $errstr, 15, STREAM_CLIENT_CONNECT); if (!$fp) { $this->log('mail_smtp_connect_failed', ['host' => $host, 'port' => $port, 'error' => $errstr]); $this->sendMailFn($to, $subject, $html, $from, $fromName); return; } stream_set_timeout($fp, 15); $transcript = []; $readResponse = function (array $expectCodes = [], string $label = 'read') use ($fp, &$transcript): array { $lines = []; while (($line = fgets($fp, 515)) !== false) { $line = rtrim($line, "\r\n"); $lines[] = $line; $transcript[] = $label . ': ' . $line; // SMTP multiline: code + '-' means more lines, code + ' ' means end if (strlen($line) >= 4 && $line[3] === ' ') { break; } } $code = 0; if ($lines) { $code = (int)substr($lines[0], 0, 3); } return [ 'ok' => !$expectCodes || in_array($code, $expectCodes, true), 'code' => $code, 'lines' => $lines, ]; }; $write = function (string $cmd, string $label = 'write', bool $mask = false) use ($fp, &$transcript): void { $transcript[] = $label . ': ' . ($mask ? '[omitted]' : $cmd); fwrite($fp, $cmd . "\r\n"); }; $resp = $readResponse([220], 'greeting'); if (!$resp['ok']) { fclose($fp); $this->log('mail_smtp_greeting_failed', ['host' => $host, 'port' => $port, 'resp' => $resp]); $this->log('mail_smtp_transcript', ['host' => $host, 'port' => $port, 'secure' => $secure, 'steps' => $transcript]); $this->sendMailFn($to, $subject, $html, $from, $fromName); return; } $write('EHLO ' . $this->app->config()->primaryDomain); $resp = $readResponse([250], 'ehlo'); if (!$resp['ok']) { fclose($fp); $this->log('mail_smtp_ehlo_failed', ['host' => $host, 'port' => $port, 'resp' => $resp]); $this->log('mail_smtp_transcript', ['host' => $host, 'port' => $port, 'secure' => $secure, 'steps' => $transcript]); $this->sendMailFn($to, $subject, $html, $from, $fromName); return; } if ($secure === 'tls') { $write('STARTTLS'); $resp = $readResponse([220], 'starttls'); if (!$resp['ok'] || !stream_socket_enable_crypto($fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { fclose($fp); $this->log('mail_smtp_starttls_failed', ['host' => $host, 'port' => $port, 'resp' => $resp]); $this->log('mail_smtp_transcript', ['host' => $host, 'port' => $port, 'secure' => $secure, 'steps' => $transcript]); $this->sendMailFn($to, $subject, $html, $from, $fromName); return; } $write('EHLO ' . $this->app->config()->primaryDomain); $resp = $readResponse([250], 'ehlo-tls'); if (!$resp['ok']) { fclose($fp); $this->log('mail_smtp_ehlo_tls_failed', ['host' => $host, 'port' => $port, 'resp' => $resp]); $this->log('mail_smtp_transcript', ['host' => $host, 'port' => $port, 'secure' => $secure, 'steps' => $transcript]); $this->sendMailFn($to, $subject, $html, $from, $fromName); return; } } if ($user !== '') { $write('AUTH LOGIN'); $resp = $readResponse([334], 'auth-login'); if (!$resp['ok']) { fclose($fp); $this->log('mail_smtp_auth_login_failed', ['host' => $host, 'port' => $port, 'resp' => $resp]); $this->log('mail_smtp_transcript', ['host' => $host, 'port' => $port, 'secure' => $secure, 'steps' => $transcript]); $this->sendMailFn($to, $subject, $html, $from, $fromName); return; } $write(base64_encode($user), 'auth-user', true); $resp = $readResponse([334], 'auth-user'); if (!$resp['ok']) { fclose($fp); $this->log('mail_smtp_auth_user_failed', ['host' => $host, 'port' => $port, 'resp' => $resp]); $this->log('mail_smtp_transcript', ['host' => $host, 'port' => $port, 'secure' => $secure, 'steps' => $transcript]); $this->sendMailFn($to, $subject, $html, $from, $fromName); return; } $write(base64_encode($pass), 'auth-pass', true); $resp = $readResponse([235], 'auth-pass'); if (!$resp['ok']) { fclose($fp); $this->log('mail_smtp_auth_pass_failed', ['host' => $host, 'port' => $port, 'resp' => $resp]); $this->log('mail_smtp_transcript', ['host' => $host, 'port' => $port, 'secure' => $secure, 'steps' => $transcript]); $this->sendMailFn($to, $subject, $html, $from, $fromName); return; } } $write('MAIL FROM: <' . $from . '>'); $resp = $readResponse([250], 'mail-from'); if (!$resp['ok']) { fclose($fp); $this->log('mail_smtp_mailfrom_failed', ['host' => $host, 'port' => $port, 'resp' => $resp]); $this->log('mail_smtp_transcript', ['host' => $host, 'port' => $port, 'secure' => $secure, 'steps' => $transcript]); $this->sendMailFn($to, $subject, $html, $from, $fromName); return; } $write('RCPT TO: <' . $to . '>'); $resp = $readResponse([250, 251], 'rcpt-to'); if (!$resp['ok']) { fclose($fp); $this->log('mail_smtp_rcpt_failed', ['host' => $host, 'port' => $port, 'resp' => $resp]); $this->log('mail_smtp_transcript', ['host' => $host, 'port' => $port, 'secure' => $secure, 'steps' => $transcript]); $this->sendMailFn($to, $subject, $html, $from, $fromName); return; } $write('DATA'); $resp = $readResponse([354], 'data-start'); if (!$resp['ok']) { fclose($fp); $this->log('mail_smtp_data_start_failed', ['host' => $host, 'port' => $port, 'resp' => $resp]); $this->log('mail_smtp_transcript', ['host' => $host, 'port' => $port, 'secure' => $secure, 'steps' => $transcript]); $this->sendMailFn($to, $subject, $html, $from, $fromName); return; } $msg = "From: {$fromName} <{$from}>\r\n"; $msg .= "To: <{$to}>\r\n"; $msg .= "Subject: {$subject}\r\n"; $msg .= "MIME-Version: 1.0\r\n"; $msg .= "Content-Type: text/html; charset=utf-8\r\n\r\n"; $msg .= $html . "\r\n.\r\n"; $write($msg, 'data', false); $resp = $readResponse([250], 'data-end'); $write('QUIT'); $readResponse([221], 'quit'); fclose($fp); $this->log('mail_smtp_transcript', ['host' => $host, 'port' => $port, 'secure' => $secure, 'steps' => $transcript]); if (!$resp['ok']) { $this->log('mail_smtp_send_failed', ['host' => $host, 'port' => $port, 'resp' => $resp]); $this->sendMailFn($to, $subject, $html, $from, $fromName); return; } $this->log('mail_smtp_sent', ['to' => $to, 'host' => $host, 'port' => $port, 'secure' => $secure]); } }