$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 = '
Verify your ' . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . ' email.
' . 'Your code is ' . $issued['code'] . '
' . '' . 'This expires in 30 minutes.
'; 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.", 'Your login code is ' . $code . '.
' . '' . 'It expires in 10 minutes.
' ); }