philippurbschat.de/home/app/services/MailService.php

293 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
class MailService {
private static function getBaseUrl(): string {
$base = Config::get('BASE_URL', 'https://philippurbschat.de/');
return rtrim($base, '/') . '/';
}
private static function getFromEmail(): string {
return Config::get('MAIL_FROM', 'hi@philippurbschat.de');
}
private static function getFromName(): string {
return Config::get('MAIL_FROM_NAME', 'Philipp Urbschat');
}
/**
* Send an invitation email with registration link
*/
public static function sendInvitation(string $toEmail, string $token, array $assignedProjects = [], ?string $note = null): bool {
$baseUrl = self::getBaseUrl();
$registerUrl = $baseUrl . 'register?token=' . urlencode($token);
$moduleNames = [];
foreach ($assignedProjects as $slug) {
$mod = ModuleService::getBySlug($slug);
if ($mod) {
$moduleNames[] = strtoupper($mod['name']);
}
}
$modulesText = !empty($moduleNames) ? implode(', ', $moduleNames) : 'Alle Basis-Module';
$subject = 'Persönliche Einladung zu philippurbschat.de';
$bodyHtml = '
<div style="background-color: #020617; color: #e2e8f0; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; padding: 40px 20px; text-align: center;">
<div style="max-width: 540px; margin: 0 auto; background-color: #0b1120; border: 1px solid #1e293b; border-radius: 20px; padding: 35px 30px; text-align: left; box-shadow: 0 10px 30px rgba(0,0,0,0.5);">
<div style="font-size: 20px; font-weight: bold; color: #f8fafc; margin-bottom: 25px; letter-spacing: -0.5px;">
<span style="color: #10b981;">_</span>PhilippUrbschat
</div>
<p style="font-size: 14px; line-height: 1.6; color: #cbd5e1; margin-bottom: 18px;">
Hallo,
</p>
<p style="font-size: 14px; line-height: 1.6; color: #cbd5e1; margin-bottom: 18px;">
du wurdest eingeladen, dir einen persönlichen Zugang auf <strong>philippurbschat.de</strong> anzulegen.
</p>
' . (!empty($note) ? '
<div style="background-color: #020617; border-left: 3px solid #06b6d4; padding: 12px 16px; margin: 20px 0; border-radius: 4px; font-size: 13px; color: #94a3b8;">
' . htmlspecialchars($note) . '
</div>' : '') . '
<div style="background-color: #020617; border: 1px solid #1e293b; border-radius: 12px; padding: 16px; margin: 24px 0;">
<div style="font-size: 11px; text-transform: uppercase; letter-spacing: 1.5px; color: #64748b; margin-bottom: 6px;">Freigeschaltete Module:</div>
<div style="font-size: 13px; font-weight: bold; color: #10b981;">' . htmlspecialchars($modulesText) . '</div>
</div>
<div style="text-align: center; margin: 32px 0;">
<a href="' . htmlspecialchars($registerUrl) . '" style="display: inline-block; background-color: #10b981; color: #020617; font-weight: bold; text-decoration: none; padding: 14px 28px; border-radius: 12px; font-size: 14px; letter-spacing: 0.5px; text-transform: uppercase;">
Jetzt Account erstellen →
</a>
</div>
<p style="font-size: 12px; line-height: 1.6; color: #64748b; margin-top: 30px; word-break: break-all;">
Falls der Button nicht funktioniert, kopiere bitte diesen Link in deinen Browser:<br>
<a href="' . htmlspecialchars($registerUrl) . '" style="color: #06b6d4; text-decoration: underline;">' . htmlspecialchars($registerUrl) . '</a>
</p>
</div>
<div style="max-width: 540px; margin: 20px auto 0; font-size: 11px; color: #475569; text-align: center;">
philippurbschat.de · Leopoldstr. 32 · 32756 Detmold
</div>
</div>';
$bodyText = "Hallo,\n\n"
. "du wurdest eingeladen, dir einen persönlichen Zugang auf philippurbschat.de anzulegen.\n\n"
. (!empty($note) ? "Hinweis: {$note}\n\n" : "")
. "Freigeschaltete Systeme: {$modulesText}\n\n"
. "Öffne folgenden Link, um dein Konto zu erstellen:\n"
. $registerUrl . "\n\n"
. "Viele Grüße,\nPhilipp Urbschat";
return self::send($toEmail, $subject, $bodyHtml, $bodyText);
}
/**
* Send a password reset email
*/
public static function sendPasswordReset(string $toEmail, string $token): bool {
$baseUrl = self::getBaseUrl();
$resetUrl = $baseUrl . 'reset-password?token=' . urlencode($token);
$subject = 'Passwort zurücksetzen philippurbschat.de';
$bodyHtml = '
<div style="background-color: #020617; color: #e2e8f0; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; padding: 40px 20px; text-align: center;">
<div style="max-width: 540px; margin: 0 auto; background-color: #0b1120; border: 1px solid #1e293b; border-radius: 20px; padding: 35px 30px; text-align: left; box-shadow: 0 10px 30px rgba(0,0,0,0.5);">
<div style="font-size: 20px; font-weight: bold; color: #f8fafc; margin-bottom: 25px; letter-spacing: -0.5px;">
<span style="color: #06b6d4;">_</span>Passwort-Reset
</div>
<p style="font-size: 14px; line-height: 1.6; color: #cbd5e1; margin-bottom: 18px;">
Hallo,
</p>
<p style="font-size: 14px; line-height: 1.6; color: #cbd5e1; margin-bottom: 18px;">
wir haben eine Anfrage zum Zurücksetzen deines Passworts auf <strong>philippurbschat.de</strong> erhalten.
</p>
<div style="text-align: center; margin: 32px 0;">
<a href="' . htmlspecialchars($resetUrl) . '" style="display: inline-block; background-color: #06b6d4; color: #020617; font-weight: bold; text-decoration: none; padding: 14px 28px; border-radius: 12px; font-size: 14px; letter-spacing: 0.5px; text-transform: uppercase;">
Neues Passwort vergeben →
</a>
</div>
<p style="font-size: 12px; line-height: 1.6; color: #94a3b8; margin-bottom: 18px;">
Dieser Link ist aus Sicherheitsgründen <strong>1 Stunde</strong> lang gültig.
</p>
<p style="font-size: 12px; line-height: 1.6; color: #64748b; margin-top: 25px; word-break: break-all;">
Falls der Button nicht funktioniert, nutze bitte diesen Link:<br>
<a href="' . htmlspecialchars($resetUrl) . '" style="color: #06b6d4; text-decoration: underline;">' . htmlspecialchars($resetUrl) . '</a>
</p>
<p style="font-size: 11px; line-height: 1.6; color: #475569; margin-top: 25px; border-top: 1px solid #1e293b; padding-top: 15px;">
Wenn du diesen Reset nicht angefordert hast, kannst du diese Nachricht einfach ignorieren. Dein bisheriges Passwort bleibt unverändert gültig.
</p>
</div>
<div style="max-width: 540px; margin: 20px auto 0; font-size: 11px; color: #475569; text-align: center;">
philippurbschat.de · Leopoldstr. 32 · 32756 Detmold
</div>
</div>';
$bodyText = "Hallo,\n\n"
. "wir haben eine Anfrage zum Zurücksetzen deines Passworts auf philippurbschat.de erhalten.\n\n"
. "Klicke auf den folgenden Link, um ein neues Passwort festzulegen (1 Stunde gültig):\n"
. $resetUrl . "\n\n"
. "Falls du diesen Reset nicht angefordert hast, kannst du diese Nachricht ignorieren.\n\n"
. "Viele Grüße,\nPhilipp Urbschat";
return self::send($toEmail, $subject, $bodyHtml, $bodyText);
}
/**
* Internal mail dispatcher
*/
private static function send(string $to, string $subject, string $htmlBody, string $textBody): bool {
$fromEmail = self::getFromEmail();
$fromName = self::getFromName();
// 1. Always log locally for auditing & development
self::logEmail($to, $subject, $textBody);
// 2. Try SMTP if credentials exist
$smtpHost = Config::get('SMTP_HOST');
if (!empty($smtpHost)) {
try {
return self::sendViaSmtp($to, $subject, $htmlBody, $textBody);
} catch (Exception $e) {
error_log("MailService SMTP Error: " . $e->getMessage());
}
}
// 3. Fallback to PHP native mail()
$boundary = "==Multipart_Boundary_x" . md5(uniqid(time()));
$encodedSubject = '=?UTF-8?B?' . base64_encode($subject) . '?=';
$encodedFromName = '=?UTF-8?B?' . base64_encode($fromName) . '?=';
$headers = [];
$headers[] = "MIME-Version: 1.0";
$headers[] = "From: {$encodedFromName} <{$fromEmail}>";
$headers[] = "Reply-To: {$fromEmail}";
$headers[] = "Content-Type: multipart/alternative; boundary=\"{$boundary}\"";
$headers[] = "X-Mailer: PhilippUrbschat-Mailer/1.0";
$message = "--{$boundary}\r\n";
$message .= "Content-Type: text/plain; charset=UTF-8\r\n";
$message .= "Content-Transfer-Encoding: base64\r\n\r\n";
$message .= chunk_split(base64_encode($textBody)) . "\r\n";
$message .= "--{$boundary}\r\n";
$message .= "Content-Type: text/html; charset=UTF-8\r\n";
$message .= "Content-Transfer-Encoding: base64\r\n\r\n";
$message .= chunk_split(base64_encode($htmlBody)) . "\r\n";
$message .= "--{$boundary}--\r\n";
$headerStr = implode("\r\n", $headers);
$additionalParams = "-f " . escapeshellarg($fromEmail);
$sent = @mail($to, $encodedSubject, $message, $headerStr, $additionalParams);
if (!$sent) {
error_log("MailService: mail() returned false. Email was logged to data/mail_log.txt");
}
return true;
}
private static function logEmail(string $to, string $subject, string $body): void {
$logDir = __DIR__ . '/../../data';
if (!is_dir($logDir)) {
@mkdir($logDir, 0775, true);
}
$logFile = $logDir . '/mail_log.txt';
$timestamp = date('Y-m-d H:i:s');
$logEntry = str_repeat('=', 60) . "\n"
. "TIMESTAMP: {$timestamp}\n"
. "TO: {$to}\n"
. "SUBJECT: {$subject}\n"
. "BODY:\n{$body}\n\n";
@file_put_contents($logFile, $logEntry, FILE_APPEND);
}
private static function sendViaSmtp(string $to, string $subject, string $htmlBody, string $textBody): bool {
$host = Config::get('SMTP_HOST');
$port = (int)Config::get('SMTP_PORT', 587);
$user = Config::get('SMTP_USER');
$pass = Config::get('SMTP_PASS');
$from = self::getFromEmail();
$fromName = self::getFromName();
$socket = @fsockopen(($port === 465 ? 'ssl://' : '') . $host, $port, $errno, $errstr, 10);
if (!$socket) {
throw new Exception("Socket connection failed: {$errstr} ({$errno})");
}
$getResponse = function() use ($socket) {
$response = '';
while ($str = fgets($socket, 515)) {
$response .= $str;
if (substr($str, 3, 1) === ' ') break;
}
return $response;
};
$sendCommand = function($cmd) use ($socket, $getResponse) {
fputs($socket, $cmd . "\r\n");
return $getResponse();
};
$getResponse(); // Read greeting
$sendCommand("EHLO " . gethostname());
if ($port === 587) {
$tlsRes = $sendCommand("STARTTLS");
if (strpos($tlsRes, '220') === 0) {
stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
$sendCommand("EHLO " . gethostname());
}
}
if (!empty($user) && !empty($pass)) {
$sendCommand("AUTH LOGIN");
$sendCommand(base64_encode($user));
$sendCommand(base64_encode($pass));
}
$sendCommand("MAIL FROM: <{$from}>");
$sendCommand("RCPT TO: <{$to}>");
$sendCommand("DATA");
$boundary = "==Multipart_Boundary_x" . md5(uniqid(time()));
$encodedSubject = '=?UTF-8?B?' . base64_encode($subject) . '?=';
$encodedFromName = '=?UTF-8?B?' . base64_encode($fromName) . '?=';
$headers = [
"MIME-Version: 1.0",
"From: {$encodedFromName} <{$from}>",
"To: {$to}",
"Subject: {$encodedSubject}",
"Content-Type: multipart/alternative; boundary=\"{$boundary}\"",
"X-Mailer: PhilippUrbschat-Mailer/1.0"
];
$content = implode("\r\n", $headers) . "\r\n\r\n";
$content .= "--{$boundary}\r\n";
$content .= "Content-Type: text/plain; charset=UTF-8\r\n";
$content .= "Content-Transfer-Encoding: base64\r\n\r\n";
$content .= chunk_split(base64_encode($textBody)) . "\r\n";
$content .= "--{$boundary}\r\n";
$content .= "Content-Type: text/html; charset=UTF-8\r\n";
$content .= "Content-Transfer-Encoding: base64\r\n\r\n";
$content .= chunk_split(base64_encode($htmlBody)) . "\r\n";
$content .= "--{$boundary}--\r\n.";
$sendCommand($content);
$sendCommand("QUIT");
fclose($socket);
return true;
}
}