Files
casadoc/public/userportal/api/_mail.php
T
2026-08-09 08:22:33 +03:00

147 lines
4.7 KiB
PHP

<?php
// SMTP delivery via PHPMailer (already a project dependency).
require_once __DIR__ . '/../../../vendor/autoload.php';
require_once __DIR__ . '/_debug.php';
use Dotenv\Dotenv;
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
Dotenv::createImmutable([dirname(__DIR__, 2), dirname(__DIR__, 3)])->safeLoad();
/** Treats an empty value and the literal "null" from .env as "not set". */
function mail_env(string $key, string $default = ''): string
{
$value = (string) ($_ENV[$key] ?? getenv($key) ?: '');
return ($value === '' || $value === 'null') ? $default : $value;
}
function mail_encryption(int $port): string
{
$enc = strtolower(mail_env('MAIL_ENCRYPTION'));
if ($enc === 'ssl' || $enc === 'tls') {
return $enc;
}
if ($enc === 'none') {
return '';
}
return in_array($port, [465, 2465], true) ? 'ssl' : 'tls';
}
/** Never surfaces the SMTP error to the caller: it can leak host and credentials. */
function send_mail(string $to, string $subject, string $html): bool
{
$port = (int) mail_env('MAIL_PORT', '587');
$enc = mail_encryption($port);
$where = sprintf(
'%s@%s:%s (%s)',
mail_env('MAIL_USERNAME'),
mail_env('MAIL_HOST', 'localhost'),
$port,
$enc ?: 'none'
);
$mail = new PHPMailer(true);
try {
$mail->isSMTP();
$mail->Host = mail_env('MAIL_HOST', 'localhost');
$mail->Port = $port;
$mail->SMTPAuth = true;
$mail->Username = mail_env('MAIL_USERNAME');
$mail->Password = mail_env('MAIL_PASSWORD');
$mail->SMTPSecure = $enc;
$mail->CharSet = 'UTF-8';
$mail->Timeout = 10;
$mail->getSMTPInstance()->Timelimit = 15;
$from = mail_env('MAIL_FROM_ADDRESS', 'noreply@casadoc.app');
// Without this the Message-ID is generated as <...@localhost>, which some
// providers treat as a spam signal.
if ($domain = substr(strrchr($from, '@') ?: '', 1)) {
$mail->Hostname = $domain;
}
$mail->setFrom($from, mail_env('MAIL_FROM_NAME', 'CasaDoc'));
$mail->addAddress($to);
$mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = $html;
// TEMPORARY
$queued = '';
$mail->SMTPDebug = SMTP::DEBUG_CONNECTION;
$mail->Debugoutput = static function (string $str, int $level) use (&$queued): void {
foreach (preg_split('/\R/', rtrim($str)) as $line) {
if (trim($line) === '') {
continue;
}
if (preg_match('/SERVER -> CLIENT: (250 (?!SIZE)\S.*)$/', $line, $m)) {
$queued = trim($m[1]);
}
debug_log(" smtp[{$level}] " . rtrim($line));
}
};
debug_log(sprintf(
'mail: sending to %s via %s | from=%s | subject=%s | %d bytes html',
$to,
$where,
$from,
$subject,
strlen($html)
));
$started = microtime(true);
$mail->send();
debug_log(sprintf(
'mail: SENT to %s in %d ms | message-id=%s | accepted by server: %s',
$to,
(int) round((microtime(true) - $started) * 1000),
$mail->getLastMessageID() ?: '(none)',
$queued ?: '(not captured)'
));
return true;
} catch (Throwable $e) {
// Without the connection details a failure is indistinguishable from a
// wrong recipient address. The password is never logged.
$reason = sprintf(
'mail FAILED to %s via %s: %s',
$to,
$where,
$mail->ErrorInfo ?: $e->getMessage()
);
error_log($reason);
debug_log($reason);
if ($e->getMessage() !== '' && $e->getMessage() !== $mail->ErrorInfo) {
debug_log(' exception: ' . get_class($e) . ': ' . $e->getMessage());
}
return false;
}
}
function send_code_mail(string $to, string $code, bool $isReset = false): bool
{
$subject = $isReset ? 'CasaDoc password reset code' : 'CasaDoc verification code';
$intro = $isReset
? 'Use this code to reset your CasaDoc password:'
: 'Use this code to confirm your e-mail address:';
$html = '<p>' . $intro . '</p>'
. '<p style="font-size:28px;font-weight:bold;letter-spacing:4px">' . htmlspecialchars($code) . '</p>'
. '<p>The code expires in 15 minutes. If you did not request it, ignore this e-mail.</p>';
debug_log(sprintf(
'code: %s code %s for %s',
$isReset ? 'password reset' : 'verification',
$code,
$to
));
return send_mail($to, $subject, $html);
}