..., 'name'=>...], ...] */ public function sendEmail( string $recipient, string $subject, string $htmlMessage, ?string $profile = null, ?string $replyToEmail = null, ?string $replyToName = null, array $attachments = [] ): bool { // Composer autoload (if not already loaded by CI4) $autoload = APPPATH . '../vendor/autoload.php'; if (is_file($autoload)) { require_once $autoload; } $profile = $this->resolveProfile($profile); $cfg = $this->getProfileConfig($profile); // Guard rails: refuse to proceed with missing essentials if (empty($cfg['host']) || empty($cfg['user']) || $cfg['pass'] === '') { log_message('error', "[mail:$profile] Missing SMTP config (host/user/pass). Check .env MAIL_{$this->envKeyFromProfile($profile)}_* or MAIL_DEFAULT_*."); return false; } // Optional env flags $debugEnabled = (bool) env('MAIL_DEBUG', false); $timeout = (int) env('MAIL_TIMEOUT', 15); // seconds $keepAlive = (bool) env('MAIL_KEEPALIVE', false); $verifyPeer = env('MAIL_VERIFY_PEER', 'true'); // 'true'|'false' (string to allow .env) $verifyPeer = filter_var($verifyPeer, FILTER_VALIDATE_BOOLEAN); // Preflight DNS + socket (helps distinguish firewall/DNS vs. auth) $targetHost = $cfg['host']; $targetPort = (int) $cfg['port']; $resolved = @gethostbyname($targetHost); if (!$resolved || $resolved === $targetHost) { // Not fatal (some environments block gethostbyname), but useful log log_message('debug', "[mail:$profile] DNS resolve note: host=$targetHost, resolved=$resolved"); } $sockOk = @fsockopen($targetHost, $targetPort, $errno, $errstr, 5); if (!$sockOk) { log_message('error', "[mail:$profile] Socket preflight failed to {$targetHost}:{$targetPort} (errno=$errno, err=$errstr). Likely firewall/port/encryption mismatch or wrong host."); } else { fclose($sockOk); } $mail = new PHPMailer(true); try { // Capture PHPMailer’s own debug if enabled if ($debugEnabled) { ob_start(); } // PHPMailer core setup $mail->isSMTP(); $mail->Host = $cfg['host']; $mail->Port = $cfg['port']; $mail->SMTPAuth = true; $mail->Username = $cfg['user']; $mail->Password = $cfg['pass']; $mail->CharSet = 'UTF-8'; $mail->Timeout = $timeout; // socket timeout $mail->SMTPKeepAlive = $keepAlive; // reuse connection for multiple sends $mail->SMTPAutoTLS = true; // allow auto TLS upgrade when possible // Encryption mapping: 'tls' => STARTTLS (587), 'ssl' => implicit TLS (465) if ($cfg['encryption'] === 'ssl') { $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS; } else { $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; } // TLS verification options (can relax on demand via .env for on-prem/self-signed) $mail->SMTPOptions = [ 'ssl' => [ 'verify_peer' => $verifyPeer, 'verify_peer_name' => $verifyPeer, 'allow_self_signed' => !$verifyPeer, ], ]; // Optional verbose debugging (set MAIL_DEBUG=true in .env) $mail->SMTPDebug = $debugEnabled ? 3 : 0; $mail->Debugoutput = 'error_log'; // From / Return-Path $mail->setFrom($cfg['fromEmail'], $cfg['fromName']); if (!empty($cfg['returnPath'])) { $mail->Sender = $cfg['returnPath']; } // Configurable Reply-To: env overrides inputs/config; fallback to profile/defaults $mail->clearReplyTos(); $rtEmail = env('MAIL_DEFAULT_REPLY_TO'); $rtName = env('MAIL_DEFAULT_REPLY_TO_NAME'); if (!$rtEmail || !filter_var($rtEmail, FILTER_VALIDATE_EMAIL)) { $rtEmail = $replyToEmail ?: ($cfg['replyTo'] ?: $cfg['fromEmail']); } if (!$rtName) { $rtName = $replyToName ?: ($cfg['replyToName'] ?: $cfg['fromName']); } $rtName = $this->sanitizeReplyToName($rtName, $cfg['fromName']); if ($rtEmail) { $mail->addReplyTo($rtEmail, $rtName); } // DKIM (optional) if (!empty($cfg['dkim']['domain']) && !empty($cfg['dkim']['private']) && !empty($cfg['dkim']['selector'])) { $mail->DKIM_domain = $cfg['dkim']['domain']; $mail->DKIM_private = $cfg['dkim']['private']; // path to private key file $mail->DKIM_selector = $cfg['dkim']['selector']; $mail->DKIM_identity = $cfg['fromEmail']; } // Recipient(s) $mail->addAddress($recipient); // Attachments foreach ($attachments as $att) { if (!empty($att['path']) && is_file($att['path'])) { $mail->addAttachment($att['path'], $att['name'] ?? ''); } } // Content $mail->isHTML(true); $mail->Subject = $subject; $mail->Body = $htmlMessage; // Send! $ok = $mail->send(); $dbg = $debugEnabled ? (ob_get_clean() ?: '') : ''; if ($ok) { log_message('info', "[mail:$profile] Sent to {$recipient}, subj='{$subject}' via {$cfg['host']}:{$cfg['port']}/{$cfg['encryption']}"); return true; } log_message('error', "[mail:$profile] Failed: {$mail->ErrorInfo}. Debug: {$dbg}"); return false; } catch (Exception $e) { $dbg = $debugEnabled ? (ob_get_clean() ?: '') : ''; log_message('error', "[mail:$profile] Exception: {$e->getMessage()} | PHPMailer: {$mail->ErrorInfo} | Debug: {$dbg}"); return false; } } /** Resolve null/alias profile names to a canonical env prefix. */ private function resolveProfile(?string $profile): string { $p = strtolower(trim((string) $profile)); if ($p === '' || $p === 'auto') { $p = strtolower((string) getenv('MAIL_PROFILE_DEFAULT')) ?: 'default'; } return match ($p) { 'comm', 'comms' => 'communication', 'sys', 'system' => 'default', default => $p, // e.g., 'payment', 'admissions', etc. }; } /** Turn 'communication' into 'COMMUNICATION' for env lookups/logs. */ private function envKeyFromProfile(string $profile): string { return strtoupper(preg_replace('/[^A-Z0-9]+/i', '_', $profile)); } /** * Resolve SMTP & identity for any profile with layered fallbacks: * 1) MAIL_{PROFILE}_* * 2) MAIL_DEFAULT_* * 3) legacy SMTP_* (HOST/USER/PASS/PORT/ENCRYPTION) * 4) hard defaults */ private function getProfileConfig(string $profile): array { $key = $this->envKeyFromProfile($profile); // Helper: first non-empty env from a list $envFirst = function (array $keys, $default = null) { foreach ($keys as $k) { $v = env($k); if (is_string($v)) $v = trim($v); if ($v !== null && $v !== '') return $v; } return $default; }; // Core SMTP (env first, then legacy SMTP_*) $host = $envFirst(["MAIL_{$key}_HOST", "MAIL_DEFAULT_HOST", "SMTP_HOST"], ''); $user = $envFirst(["MAIL_{$key}_USER", "MAIL_DEFAULT_USER", "SMTP_USER"], ''); $pass = $envFirst(["MAIL_{$key}_PASS", "MAIL_DEFAULT_PASS", "SMTP_PASS"], ''); $portRaw = $envFirst(["MAIL_{$key}_PORT", "MAIL_DEFAULT_PORT", "SMTP_PORT"], 587); $encRaw = $envFirst(["MAIL_{$key}_ENCRYPTION", "MAIL_DEFAULT_ENCRYPTION", "SMTP_ENCRYPTION"], 'tls'); // Fallback to Config\Email when env is not set (common in local/dev) $cfgEmail = config('Email'); if ($cfgEmail) { if ($host === '' && !empty($cfgEmail->SMTPHost)) { $host = $cfgEmail->SMTPHost; } if ($user === '' && !empty($cfgEmail->SMTPUser)) { $user = $cfgEmail->SMTPUser; } if ($pass === '' && !empty($cfgEmail->SMTPPass)) { $pass = $cfgEmail->SMTPPass; } if ((int) $portRaw <= 0 && !empty($cfgEmail->SMTPPort)) { $portRaw = $cfgEmail->SMTPPort; } if (($encRaw === '' || $encRaw === 'tls') && !empty($cfgEmail->SMTPCrypto)) { $encRaw = $cfgEmail->SMTPCrypto; } } // Identity $fromEmail = $envFirst(["MAIL_{$key}_FROM_EMAIL", "MAIL_DEFAULT_FROM_EMAIL"], $user ?: 'no-reply@alrahmaisgl.org'); $fromName = $envFirst(["MAIL_{$key}_FROM_NAME", "MAIL_DEFAULT_FROM_NAME"], 'Al Rahma Sunday School'); $replyTo = $envFirst(["MAIL_{$key}_REPLY_TO", "MAIL_DEFAULT_REPLY_TO"], ''); $replyToNm = $envFirst(["MAIL_{$key}_REPLY_TO_NAME","MAIL_DEFAULT_REPLY_TO_NAME"], ''); $returnPath = $envFirst(["MAIL_{$key}_RETURN_PATH","MAIL_DEFAULT_RETURN_PATH"], ''); // DKIM (optional) $dkim = [ 'domain' => $envFirst(["MAIL_{$key}_DKIM_DOMAIN", "MAIL_DEFAULT_DKIM_DOMAIN"], ''), 'selector' => $envFirst(["MAIL_{$key}_DKIM_SELECTOR", "MAIL_DEFAULT_DKIM_SELECTOR"], ''), 'private' => $envFirst(["MAIL_{$key}_DKIM_PRIVATE", "MAIL_DEFAULT_DKIM_PRIVATE"], ''), // full path ]; // Normalize types/values $port = (int) $portRaw ?: 587; $encryption = strtolower((string) $encRaw); $encryption = in_array($encryption, ['tls','ssl'], true) ? $encryption : 'tls'; // --- Autocorrect common mistakes (prevents silent handshake failures) --- if ($port === 587 && $encryption === 'ssl') { $encryption = 'tls'; } if ($port === 465 && $encryption === 'tls') { $encryption = 'ssl'; } return [ 'host' => (string) $host, 'user' => (string) $user, 'pass' => (string) $pass, 'port' => $port, // map used above: 'tls' => STARTTLS, 'ssl' => SMTPS 'encryption' => $encryption, 'fromEmail' => (string) $fromEmail, 'fromName' => (string) $fromName, 'replyTo' => (string) $replyTo, 'replyToName' => (string) $this->sanitizeReplyToName($replyToNm, (string) $fromName), 'returnPath' => (string) $returnPath, 'dkim' => $dkim, ]; } /** * Normalize Reply-To display names, removing "No-Reply/No-Replay" placeholders. */ private function sanitizeReplyToName(?string $name, string $fallback): string { $trimmed = trim((string) $name); if ($trimmed === '' || preg_match('/^no[- ]?repl(?:y|ay)$/i', $trimmed)) { return $fallback; } return $trimmed; } }