Send mail locally again: message_queue + PHPMailer over SMTP
Outbound email was relayed to the hub's /notify endpoint, so this install had
no SMTP configuration of its own and no way to see what had been sent. Bring
delivery back in-house, following the fotografia.andmore.pt stack rather than
inventing a second shape for it.
- Submodule bumped 603ff84 -> ea3616a, the pin fotografia already uses. That
is what provides functions_mail.php (shared transport), functions_settings.php
(platform_smtp_config + encrypted settings) and compat_secrets.php. Verified
no function-name collisions with this repo's own config/functions_settings.php:
platform-tools namespaces everything platform_*.
- PHPMailer 7.1.1 vendored at vendor/PHPMailer/src/. platform_mail_available()
looks for exactly this hand-vendored layout before falling back to composer's
autoloader, so the host needs no shell and no composer install. LGPL-2.1
license text included.
- config/functions_queue.php replaced: was a thin facade over the hub relay,
now fotografia's real queue. Rows are claimed atomically, retried with
exponential backoff up to max_attempts, and park in 'failed' with the real
SMTP rejection in last_error.
- SMTP resolution matches the fleet: .env wins for the SMTP_* constants, while
the actual send goes through platform_smtp_config($pdo), which is DB-first.
That ordering is deliberate -- the database is the half an operator can
change from the backoffice without shell access. Empty SMTP_HOST falls back
to PHP mail().
- Sistema -> Comunicações (views/sysadmin/comms.php): credentials plus a live
SMTP connection test and a verdict on what would happen to a message sent
right now. Secret-valued settings are write-only in the form and encrypted
at rest (AES-256-GCM, key from .env, never in the DB dump). Which keys count
as secret is platform_settings_is_secret()'s decision, not a list kept here.
- Sistema -> Mensagens (views/sysadmin/messages.php): the queue monitor.
Filters, per-message detail with last_error, and retry/cancel/archive.
Retry flushes inline so the operator sees the outcome immediately.
- cron.php drains the queue every 5 minutes, serialised with a lock file, and
runs retention once a day. Over HTTP it answers 404 until CRON_TOKEN is set
to something other than replace_me. Deliberately not a copy of fotografia's
cron: the session-reminder, download-digest and photo-archive steps have no
modules here and would be dead code.
- config/functions_notify_client.php deleted, and ANDMORE_ID_URL with it --
the hub relay was its only caller.
Verified without a database: PHP lint across the tree; PHPMailer builds a full
MIME message with RFC 2047 UTF-8 subject encoding from the three vendored files;
platform_mail_available() finds them; platform_mail_make() returns a correctly
configured mailer; the diagnosis states report Operacional / Não configurado.
NOT verified: the settings encryption round-trip -- this machine's PHP has no
openssl extension, so platform_secret_key() returns null and values pass through
in clear. The code degrades safely (stores the value rather than losing it) and
the Comunicações page warns when the key is unavailable.
Deployment: apply database/migration_20260913_message_queue.sql, set SMTP in
Comunicações or .env, set APP_SECRET_KEY, and schedule cron.php.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3626080599
commit
d5cb50b432
16 files changed
+8982
-136
No files matched your search
+31
-3
@@ -11,15 +11,15 @@ if (session_status() === PHP_SESSION_NONE) session_start();
|
||||
|
||||
require_once __DIR__ . '/config.php'; // APP_ENV + hub notify URL + error_handler
|
||||
require_once __DIR__ . '/functions_settings.php'; // setting() + system_settings cache
|
||||
require_once __DIR__ . '/functions_secrets.php'; // setting_or_env()/setting_secret() + AES-GCM at rest
|
||||
require_once __DIR__ . '/functions_db.php'; // generic DB helpers
|
||||
require_once __DIR__ . '/functions_user.php'; // auth_login/register, password reset, user creators
|
||||
require_once __DIR__ . '/auth.php'; // require_login/admin/staff/sysadmin(), role helpers
|
||||
require_once __DIR__ . '/functions_throttle.php'; // per-IP attempt throttle for the auth endpoints
|
||||
require_once __DIR__ . '/functions_migrations.php';// platform_update.php migrations runner
|
||||
|
||||
// Outbound delivery is centralized at id.andmore.pt — no local PHPMailer/SMTP.
|
||||
require_once __DIR__ . '/functions_notify_client.php'; // notify_remote_send()
|
||||
require_once __DIR__ . '/functions_queue.php'; // enqueue_*/flush_message façade → relay
|
||||
// Outbound delivery happens in this app — PHPMailer over SMTP, no central relay.
|
||||
require_once __DIR__ . '/functions_queue.php'; // message_queue + enqueue_*/flush_message
|
||||
require_once __DIR__ . '/functions_csrf.php';
|
||||
require_once __DIR__ . '/functions_html.php'; // allowlist HTML sanitiser (clean_richtext/clean_map_embed)
|
||||
require_once __DIR__ . '/ci_compat.php'; // CI4 view-helper shim (script_tag/link_tag/view/esc/session)
|
||||
@@ -48,6 +48,34 @@ define('EMAIL_FROM', (string) setting('email_from', 'noreply
|
||||
define('EMAIL_FROM_NAME', (string) setting('email_from_name', 'FitMe 3070'));
|
||||
define('EMAIL_SUBJECT_PREFIX', (string) setting('email_subject_prefix', '[FitMe 3070] '));
|
||||
|
||||
// ─── SMTP ───────────────────────────────────────────────────────────
|
||||
// .env wins, then system_settings (smtp_password encrypted at rest). These
|
||||
// constants are what the diagnostics page shows; the actual send resolves
|
||||
// through platform_smtp_config($pdo), which is DB-first — that is the value an
|
||||
// operator can change from the backoffice without shell access.
|
||||
// An empty SMTP_HOST makes PHPMailer fall back to PHP mail().
|
||||
define('SMTP_HOST', setting_or_env('smtp_host', 'SMTP_HOST'));
|
||||
define('SMTP_PORT', (int) (setting_or_env('smtp_port', 'SMTP_PORT', '587') ?: 587));
|
||||
define('SMTP_SECURE', setting_or_env('smtp_secure', 'SMTP_SECURE', 'tls'));
|
||||
define('SMTP_USERNAME', setting_or_env('smtp_username', 'SMTP_USERNAME'));
|
||||
define('SMTP_PASSWORD', setting_secret('smtp_password', 'SMTP_PASSWORD'));
|
||||
|
||||
// ─── Twilio (SMS / WhatsApp) ───────────────────────────────────────
|
||||
// Optional. Left empty the sms/whatsapp drivers refuse with a clear message
|
||||
// instead of half-sending; email is unaffected.
|
||||
define('TWILIO_SID', setting_or_env('twilio_sid', 'TWILIO_SID'));
|
||||
define('TWILIO_TOKEN', setting_secret('twilio_token', 'TWILIO_TOKEN'));
|
||||
define('TWILIO_SMS_FROM', setting_or_env('twilio_sms_from', 'TWILIO_SMS_FROM'));
|
||||
define('TWILIO_WHATSAPP_FROM', setting_or_env('twilio_whatsapp_from', 'TWILIO_WHATSAPP_FROM'));
|
||||
|
||||
// ─── Dev safety nets ───────────────────────────────────────────────
|
||||
// Only consulted when APP_ENV !== 'prod', so a misconfigured production install
|
||||
// can never silently swallow mail.
|
||||
// EMAIL_DEV_PRINT — skip SMTP entirely, write to logs/message_dev.log.
|
||||
// EMAIL_DEV_REDIRECT — send everything to this address instead of the real one.
|
||||
define('EMAIL_DEV_PRINT', filter_var(env('EMAIL_DEV_PRINT', 'false'), FILTER_VALIDATE_BOOLEAN));
|
||||
define('EMAIL_DEV_REDIRECT', (string) env('EMAIL_DEV_REDIRECT', ''));
|
||||
|
||||
// ─── URL helper ───────────────────────────────────────────────────────────────
|
||||
// Always use for Location: redirects and internal links.
|
||||
function base_url(string $path = ''): string {
|
||||
|
||||
Reference in new issue
Block a user