seedproject-web/api/app/Helpers/Mailer.php
Carlos Arias 1f33296e30 feat: user auth (email + Google OAuth), Mailer, and CLI bridges
Generic framework features, code-only — the tables come from the schema snapshot:

- controllers/account.php — email signup (verified) + Google OAuth (server-side auth-code
  flow, async-popup friendly); native password_hash sessions on sp_users; sp_oauth_accounts +
  sp_user_tokens; PublicController origin+throttle guards; {ok,data,error} envelope.
- Helpers/Mailer.php — transactional email: Brevo HTTP API (Guzzle) -> SMTP fallback
  (PHPMailer) -> logs; config in sp_settings (encrypted).
- cli/rebuild.php — queue a static rebuild (sp_settings rebuild_pending flag) after DB edits.
- cli/resources.php — read a curated source registry (mde_resources) per agent_type.
- db/seed_google_oauth.php, db/seed_email.php — env-seeded encrypted secrets into sp_settings.
- templates/emails/verify.html — verification email template.
- composer.json — declare guzzlehttp/guzzle ^7.10 (account.php + Mailer use GuzzleHttp\Client).

Depends on the schema-snapshot branch (sp_users / sp_oauth_accounts / sp_user_tokens /
sp_settings / mde_resources) plus `composer require guzzlehttp/guzzle`. No data.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMQeUnUrAeexcZ7P2Hxa6G
2026-07-11 09:14:25 -05:00

100 lines
4.3 KiB
PHP

<?php
/**
* Transactional email sender.
*
* Config lives in sp_settings (keyval='email', group='email', encrypted=1):
* { "provider":"brevo", "api_key":<encrypted>, "from_email":"...", "from_name":"..." }
* Seed it with db/seed_email.php (reads BREVO_API_KEY from the environment).
*
* Send order: Brevo HTTP API (if configured) → SMTP via config.php EMAIL* constants
* (if set) → false. Callers treat false as "not sent" and log a fallback (e.g. the
* account verification link) so flows stay testable before email is wired up.
*/
class Mailer
{
/** @return bool true when the provider accepted the message. */
public static function send(string $toEmail, string $toName, string $subject, string $html, string $text = ''): bool
{
$cfg = json_decode((string) \Db::getValue(
"SELECT metval FROM sp_settings WHERE keyval = 'email' AND status = 1 LIMIT 1"
), true) ?: [];
if (strtolower((string) ($cfg['provider'] ?? '')) === 'brevo' && !empty($cfg['api_key'])) {
return self::sendBrevo($cfg, $toEmail, $toName, $subject, $html, $text);
}
$host = defined('EMAILHOST') ? (string) EMAILHOST : '';
if ($host !== '' && !str_starts_with($host, 'ADD')) {
return self::sendSmtp($toEmail, $toName, $subject, $html, $text);
}
return false; // unconfigured
}
private static function fromEmail(array $cfg): string
{
$from = trim((string) ($cfg['from_email'] ?? ''));
return $from !== '' ? $from : 'noreply@' . (parse_url(URL, PHP_URL_HOST) ?: 'localhost');
}
private static function fromName(array $cfg): string
{
$name = trim((string) ($cfg['from_name'] ?? ''));
return $name !== '' ? $name : (defined('PROJECT_NAME') ? PROJECT_NAME : 'Website');
}
private static function sendBrevo(array $cfg, string $toEmail, string $toName, string $subject, string $html, string $text): bool
{
try {
$http = new \GuzzleHttp\Client(['timeout' => 12, 'http_errors' => false]);
$res = $http->post('https://api.brevo.com/v3/smtp/email', [
'headers' => [
'api-key' => \Functions::Decrypt((string) $cfg['api_key']),
'accept' => 'application/json',
'content-type' => 'application/json',
],
'json' => [
'sender' => ['name' => self::fromName($cfg), 'email' => self::fromEmail($cfg)],
'to' => [['email' => $toEmail, 'name' => $toName !== '' ? $toName : $toEmail]],
'subject' => $subject,
'htmlContent' => $html,
'textContent' => $text !== '' ? $text : trim(strip_tags($html)),
],
]);
$code = $res->getStatusCode();
if ($code >= 200 && $code < 300) {
return true;
}
error_log('[mailer] brevo HTTP ' . $code . ': ' . (string) $res->getBody());
return false;
} catch (\Throwable $e) {
error_log('[mailer] brevo exception: ' . $e->getMessage());
return false;
}
}
private static function sendSmtp(string $toEmail, string $toName, string $subject, string $html, string $text): bool
{
try {
$mail = new \PHPMailer\PHPMailer\PHPMailer(true);
$mail->isSMTP();
$mail->Host = EMAILHOST;
$mail->SMTPAuth = true;
$mail->Username = defined('EMAILUSER') ? EMAILUSER : '';
$mail->Password = defined('EMAILPASSWORD') ? EMAILPASSWORD : '';
$mail->SMTPSecure = 'tls';
$mail->Port = 587;
$mail->setFrom('noreply@' . (parse_url(URL, PHP_URL_HOST) ?: 'localhost'), defined('PROJECT_NAME') ? PROJECT_NAME : 'Website');
$mail->addAddress($toEmail, $toName !== '' ? $toName : $toEmail);
$mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = $html;
$mail->AltBody = $text !== '' ? $text : trim(strip_tags($html));
$mail->send();
return true;
} catch (\Throwable $e) {
error_log('[mailer] smtp send failed: ' . $e->getMessage());
return false;
}
}
}