293 lines
14 KiB
PHP
293 lines
14 KiB
PHP
<?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;
|
||
}
|
||
}
|