add more controllers and fix tests

This commit is contained in:
root
2026-03-09 11:54:13 -04:00
parent 0c3e9b16f7
commit 1cb3573d4b
74 changed files with 2761 additions and 2728 deletions
@@ -0,0 +1,35 @@
<?php
namespace App\Services\Email;
use Illuminate\Http\UploadedFile;
class EmailAttachmentService
{
public function normalize($files): array
{
if ($files instanceof UploadedFile) {
$files = [$files];
}
if (!is_array($files)) {
return [];
}
$out = [];
foreach ($files as $file) {
if (!$file instanceof UploadedFile || !$file->isValid()) {
continue;
}
$path = $file->getRealPath();
if (!$path || !is_file($path)) {
continue;
}
$out[] = [
'path' => $path,
'name' => $file->getClientOriginalName(),
];
}
return $out;
}
}
+125
View File
@@ -0,0 +1,125 @@
<?php
namespace App\Services\Email;
use PHPMailer\PHPMailer\Exception as MailerException;
use PHPMailer\PHPMailer\PHPMailer;
class EmailDispatchService
{
public function __construct(private EmailProfileService $profiles)
{
}
public function send(
string $recipient,
string $subject,
string $htmlMessage,
?string $profile = null,
?string $replyToEmail = null,
?string $replyToName = null,
array $attachments = []
): bool {
$profile = $this->profiles->resolveProfile($profile);
$cfg = $this->profiles->getProfileConfig($profile);
if (empty($cfg['host']) || empty($cfg['user']) || $cfg['pass'] === '') {
logger()->error("[mail:$profile] Missing SMTP config (host/user/pass). Check MAIL_{$this->profiles->envKeyFromProfile($profile)}_* or MAIL_DEFAULT_*/MAIL_*.");
return false;
}
$debugEnabled = (bool) env('MAIL_DEBUG', false);
$timeout = (int) env('MAIL_TIMEOUT', 15);
$keepAlive = (bool) env('MAIL_KEEPALIVE', false);
$verifyPeer = filter_var(env('MAIL_VERIFY_PEER', 'true'), FILTER_VALIDATE_BOOLEAN);
$mail = new PHPMailer(true);
try {
if ($debugEnabled) {
ob_start();
}
$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;
$mail->SMTPKeepAlive = $keepAlive;
$mail->SMTPAutoTLS = true;
if ($cfg['encryption'] === 'ssl') {
$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
} else {
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
}
$mail->SMTPOptions = [
'ssl' => [
'verify_peer' => $verifyPeer,
'verify_peer_name' => $verifyPeer,
'allow_self_signed' => !$verifyPeer,
],
];
$mail->SMTPDebug = $debugEnabled ? 3 : 0;
$mail->Debugoutput = 'error_log';
$mail->setFrom($cfg['fromEmail'], $cfg['fromName']);
if (!empty($cfg['returnPath'])) {
$mail->Sender = $cfg['returnPath'];
}
$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->profiles->sanitizeReplyToName($rtName, $cfg['fromName']);
if ($rtEmail) {
$mail->addReplyTo($rtEmail, $rtName);
}
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'];
$mail->DKIM_selector = $cfg['dkim']['selector'];
$mail->DKIM_identity = $cfg['fromEmail'];
}
$mail->addAddress($recipient);
foreach ($attachments as $att) {
if (!empty($att['path']) && is_file($att['path'])) {
$mail->addAttachment($att['path'], $att['name'] ?? '');
}
}
$mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = $htmlMessage;
$ok = $mail->send();
$dbg = $debugEnabled ? (ob_get_clean() ?: '') : '';
if ($ok) {
logger()->info("[mail:$profile] Sent to {$recipient}, subj='{$subject}' via {$cfg['host']}:{$cfg['port']}/{$cfg['encryption']}");
return true;
}
logger()->error("[mail:$profile] Failed: {$mail->ErrorInfo}. Debug: {$dbg}");
return false;
} catch (MailerException $e) {
$dbg = $debugEnabled ? (ob_get_clean() ?: '') : '';
logger()->error("[mail:$profile] Exception: {$e->getMessage()} | PHPMailer: {$mail->ErrorInfo} | Debug: {$dbg}");
return false;
}
}
}
@@ -0,0 +1,97 @@
<?php
namespace App\Services\Email;
use Illuminate\Support\Facades\DB;
class EmailExtractorService
{
public function listEmails(): array
{
$users = [];
$parents = [];
$userRows = DB::table('users')->select('email')->get();
foreach ($userRows as $row) {
$email = strtolower(trim((string) ($row->email ?? '')));
if ($email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL)) {
$users[] = $email;
}
}
$users = array_values(array_unique($users));
$parentRows = DB::table('parents')->select('secondparent_email')->get();
foreach ($parentRows as $row) {
$email = strtolower(trim((string) ($row->secondparent_email ?? '')));
if ($email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL)) {
$parents[] = $email;
}
}
$parents = array_values(array_unique($parents));
return [
'users' => $users,
'parents' => $parents,
];
}
public function compare(array $csvEmails): array
{
$emails = $this->listEmails();
$users = $emails['users'];
$parents = $emails['parents'];
$csvEmails = $this->normalizeEmailArray($csvEmails);
$csvSet = array_flip(array_values(array_unique($csvEmails)));
$dbUnion = array_values(array_unique(array_merge($users, $parents)));
$dbSet = array_flip($dbUnion);
$existed = [];
foreach ($csvSet as $email => $_) {
if (isset($dbSet[$email])) {
$existed[] = $email;
}
}
sort($existed);
$needToAdd = [];
foreach ($dbSet as $email => $_) {
if (!isset($csvSet[$email])) {
$needToAdd[] = $email;
}
}
sort($needToAdd);
return [
'existed' => $existed,
'needToAdd' => $needToAdd,
'counts' => [
'csv' => count($csvEmails),
'db' => count($dbUnion),
'users' => count($users),
'parents' => count($parents),
],
];
}
public function normalizeEmailArray(array $arr): array
{
$out = [];
foreach ($arr as $e) {
$email = strtolower(trim((string) $e));
if ($email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL)) {
$out[] = $email;
}
}
return array_values(array_unique($out));
}
public function extractEmailsFromText(string $text): array
{
$pattern = '/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i';
preg_match_all($pattern, $text, $matches);
$emails = $matches[0] ?? [];
return $this->normalizeEmailArray($emails);
}
}
@@ -0,0 +1,95 @@
<?php
namespace App\Services\Email;
class EmailProfileService
{
public function resolveProfile(?string $profile): string
{
$p = strtolower(trim((string) $profile));
if ($p === '' || $p === 'auto') {
$p = strtolower((string) env('MAIL_PROFILE_DEFAULT', 'default')) ?: 'default';
}
return match ($p) {
'comm', 'comms' => 'communication',
'sys', 'system' => 'default',
default => $p,
};
}
public function envKeyFromProfile(string $profile): string
{
return strtoupper(preg_replace('/[^A-Z0-9]+/i', '_', $profile));
}
public function getProfileConfig(string $profile): array
{
$key = $this->envKeyFromProfile($profile);
$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;
};
$host = $envFirst(["MAIL_{$key}_HOST", 'MAIL_DEFAULT_HOST', 'SMTP_HOST', 'MAIL_HOST'], '');
$user = $envFirst(["MAIL_{$key}_USER", 'MAIL_DEFAULT_USER', 'SMTP_USER', 'MAIL_USERNAME'], '');
$pass = $envFirst(["MAIL_{$key}_PASS", 'MAIL_DEFAULT_PASS', 'SMTP_PASS', 'MAIL_PASSWORD'], '');
$portRaw = $envFirst(["MAIL_{$key}_PORT", 'MAIL_DEFAULT_PORT', 'SMTP_PORT', 'MAIL_PORT'], 587);
$encRaw = $envFirst(["MAIL_{$key}_ENCRYPTION", 'MAIL_DEFAULT_ENCRYPTION', 'SMTP_ENCRYPTION', 'MAIL_ENCRYPTION'], 'tls');
$fromEmail = $envFirst(["MAIL_{$key}_FROM_EMAIL", 'MAIL_DEFAULT_FROM_EMAIL', 'MAIL_FROM_ADDRESS'], $user ?: 'no-reply@alrahmaisgl.org');
$fromName = $envFirst(["MAIL_{$key}_FROM_NAME", 'MAIL_DEFAULT_FROM_NAME', 'MAIL_FROM_NAME'], 'Al Rahma Sunday School');
$replyTo = $envFirst(["MAIL_{$key}_REPLY_TO", 'MAIL_DEFAULT_REPLY_TO'], '');
$replyToName = $envFirst(["MAIL_{$key}_REPLY_TO_NAME", 'MAIL_DEFAULT_REPLY_TO_NAME'], '');
$returnPath = $envFirst(["MAIL_{$key}_RETURN_PATH", 'MAIL_DEFAULT_RETURN_PATH'], '');
$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'], ''),
];
$port = (int) $portRaw ?: 587;
$encryption = strtolower((string) $encRaw);
$encryption = in_array($encryption, ['tls', 'ssl'], true) ? $encryption : 'tls';
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,
'encryption' => $encryption,
'fromEmail' => (string) $fromEmail,
'fromName' => (string) $fromName,
'replyTo' => (string) $replyTo,
'replyToName' => (string) $this->sanitizeReplyToName($replyToName, (string) $fromName),
'returnPath' => (string) $returnPath,
'dkim' => $dkim,
];
}
public 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;
}
}
@@ -0,0 +1,38 @@
<?php
namespace App\Services\Email;
class EmailSenderOptionsService
{
public function listSenders(): array
{
$json = env('MAIL_SENDERS', '{}');
$arr = json_decode($json, true);
if (!is_array($arr) || empty($arr)) {
$smtpUser = getenv('SMTP_USER') ?: '';
$name = 'Al Rahma Sunday School';
return [[
'key' => 'general',
'name' => $name,
'email' => $smtpUser,
'label' => trim($name . ($smtpUser ? " <{$smtpUser}>" : '')),
]];
}
$out = [];
foreach ($arr as $key => $info) {
$name = $info['name'] ?? 'Sender';
$email = $info['email'] ?? '';
$out[] = [
'key' => (string) $key,
'name' => (string) $name,
'email' => (string) $email,
'label' => trim($name . ($email ? " <{$email}>" : '')),
];
}
return $out;
}
}