Files
chat/lib/mailer.php
T
2026-06-08 15:44:15 -04:00

94 lines
4.8 KiB
PHP

<?php
function sendMailgunMessage(string $to, string $subject, string $text, string $html = ''): array {
$key = trim((string)getConfigVal('mailgun.api_key', ''));
$domain = trim((string)getConfigVal('mailgun.domain', ''));
$from = trim((string)getConfigVal('mailgun.from', ''));
$base = rtrim(trim((string)getConfigVal('mailgun.api_base', '')) ?: 'https://api.mailgun.net/v3', '/');
if ($key === '' || $domain === '' || $from === '') {
throw new RuntimeException('Mailgun is not configured');
}
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) throw new InvalidArgumentException('Invalid recipient email');
if (!function_exists('curl_init')) throw new RuntimeException('PHP cURL extension is required');
$fields = ['from' => $from, 'to' => $to, 'subject' => $subject, 'text' => $text];
if ($html !== '') $fields['html'] = $html;
$curl = curl_init($base . '/' . rawurlencode($domain) . '/messages');
curl_setopt_array($curl, [
CURLOPT_USERPWD => 'api:' . $key,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $fields,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
]);
$body = curl_exec($curl);
$status = (int)curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
$error = curl_error($curl);
curl_close($curl);
if ($body === false || $status < 200 || $status >= 300) {
throw new RuntimeException('Mailgun request failed: ' . ($error ?: 'HTTP ' . $status));
}
return json_decode((string)$body, true) ?: ['success' => true];
}
function issueEmailToken(array $user, string $email, string $purpose, int $ttl = 900): array {
$token = bin2hex(random_bytes(24));
$code = (string)random_int(100000, 999999);
$db = getDB();
$db->prepare("UPDATE email_tokens SET used_at=? WHERE user_id=? AND purpose=? AND used_at IS NULL")
->execute([time(), $user['id'], $purpose]);
$db->prepare("INSERT INTO email_tokens
(user_id,email,purpose,token_hash,code_hash,expires_at,created_at) VALUES (?,?,?,?,?,?,?)")
->execute([
$user['id'], strtolower($email), $purpose, hash('sha256', $token),
password_hash($code, PASSWORD_DEFAULT), time() + $ttl, time(),
]);
return ['token' => $token, 'code' => $code, 'expires_at' => time() + $ttl];
}
function sendVerificationEmail(array $user, string $email): void {
$issued = issueEmailToken($user, $email, 'verify_email', 1800);
$url = appBaseUrl() . '/index.html?verify=' . rawurlencode($issued['token']);
$title = (string)getConfigVal('ui.app_title', 'CyberChat');
$text = "Verify your $title email.\n\nCode: {$issued['code']}\nVerification link: $url\n\nThis expires in 30 minutes.";
$html = '<p>Verify your ' . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . ' email.</p>'
. '<p>Your code is <strong style="font-size:20px">' . $issued['code'] . '</strong></p>'
. '<p><a href="' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '">Open verification screen</a></p>'
. '<p>This expires in 30 minutes.</p>';
sendMailgunMessage($email, 'Verify your ' . $title . ' email', $text, $html);
}
function consumeEmailVerification(array $user, string $token = '', string $code = ''): bool {
$stmt = getDB()->prepare("SELECT * FROM email_tokens
WHERE user_id=? AND purpose='verify_email' AND used_at IS NULL AND expires_at>? ORDER BY id DESC LIMIT 1");
$stmt->execute([$user['id'], time()]);
$row = $stmt->fetch();
if (!$row || (int)($row['attempts'] ?? 0) >= 5) return false;
$valid = ($token !== '' && hash_equals($row['token_hash'], hash('sha256', $token)))
|| ($code !== '' && password_verify($code, $row['code_hash']));
if (!$valid) {
getDB()->prepare("UPDATE email_tokens SET attempts=attempts+1 WHERE id=?")->execute([$row['id']]);
return false;
}
$db = getDB();
$db->beginTransaction();
$db->prepare("UPDATE email_tokens SET used_at=? WHERE id=?")->execute([time(), $row['id']]);
$db->prepare("UPDATE users SET email=?,email_verified_at=? WHERE id=?")
->execute([$row['email'], time(), $user['id']]);
$db->commit();
return true;
}
function sendTwoFactorCode(array $user, string $challengeId, string $code): void {
$title = (string)getConfigVal('ui.app_title', 'CyberChat');
$url = appBaseUrl() . '/index.html?login_challenge=' . rawurlencode($challengeId);
sendMailgunMessage(
(string)$user['email'],
$title . ' login code',
"Your login code is $code.\n\nOpen the authentication screen: $url\n\nIt expires in 10 minutes.",
'<p>Your login code is <strong style="font-size:22px">' . $code . '</strong>.</p>'
. '<p><a href="' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '">Open authentication screen</a></p>'
. '<p>It expires in 10 minutes.</p>'
);
}