add email sent for event

This commit is contained in:
root
2026-04-18 10:51:22 -04:00
parent c13dcb07d1
commit f04934039e
8 changed files with 388 additions and 2 deletions
+157
View File
@@ -170,6 +170,163 @@ class EmailController extends Controller
}
}
/**
* Send one email with a single To recipient and many CC recipients.
*
* @param string $recipient
* @param string[] $ccRecipients
* @param string $subject
* @param string $htmlMessage
* @param string|null $profile
* @param string|null $replyToEmail
* @param string|null $replyToName
* @param array $attachments
*/
public function sendEmailWithCc(
string $recipient,
array $ccRecipients,
string $subject,
string $htmlMessage,
?string $profile = null,
?string $replyToEmail = null,
?string $replyToName = null,
array $attachments = []
): bool {
$ccRecipients = array_values(array_unique(array_filter(array_map(static function ($email) {
$email = trim((string) $email);
return filter_var($email, FILTER_VALIDATE_EMAIL) ? $email : null;
}, $ccRecipients))));
if ($ccRecipients === []) {
return $this->sendEmail($recipient, $subject, $htmlMessage, $profile, $replyToEmail, $replyToName, $attachments);
}
$autoload = APPPATH . '../vendor/autoload.php';
if (is_file($autoload)) {
require_once $autoload;
}
$profile = $this->resolveProfile($profile);
$cfg = $this->getProfileConfig($profile);
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;
}
$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);
$targetHost = $cfg['host'];
$targetPort = (int) $cfg['port'];
$resolved = @gethostbyname($targetHost);
if (!$resolved || $resolved === $targetHost) {
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 {
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->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 ($ccRecipients as $ccRecipient) {
$mail->addCC($ccRecipient);
}
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) {
log_message('info', "[mail:$profile] Sent to {$recipient} with " . count($ccRecipients) . " CC recipient(s), 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
{
+119 -2
View File
@@ -12,8 +12,10 @@ use App\Models\EnrollmentModel;
use App\Models\InvoiceModel;
use App\Models\InvoiceEventModel;
use App\Models\ClassSectionModel;
use App\Models\ParentModel;
use App\Models\PaymentModel;
use App\Models\CalendarModel;
use App\Services\EmailService;
use Config\Database;
#use ???
use App\Controllers\View\InvoiceController;
@@ -32,6 +34,8 @@ class EventController extends ResourceController
protected $studentClassModel;
protected $classSectionModel;
protected $paymentModel;
protected $parentModel;
protected $emailService;
protected $schoolYear;
protected $semester;
protected $categories;
@@ -52,6 +56,8 @@ class EventController extends ResourceController
$this->classSectionModel = new ClassSectionModel();
$this->paymentModel = new PaymentModel();
$this->enrollmentModel = new EnrollmentModel();
$this->parentModel = new ParentModel();
$this->emailService = new EmailService();
$this->schoolYear = $this->configModel->getConfig('school_year');
$this->semester = $this->configModel->getConfig('semester');
@@ -167,7 +173,13 @@ class EventController extends ResourceController
}
}
return redirect()->to('/administrator/events')->with('success', 'Event created successfully');
$redirect = redirect()->to('/administrator/events')->with('success', 'Event created successfully');
if ($eventId && $this->request->getPost('send_email_parent')) {
$emailStatus = $this->broadcastEventToParents((int) $eventId);
$redirect = $this->applyEventEmailFlash($redirect, $emailStatus);
}
return $redirect;
}
return view('administrator/events/create_event', [
@@ -208,7 +220,12 @@ class EventController extends ResourceController
]);
if ($updated) {
return redirect()->to('/administrator/events')->with('success', 'Event updated successfully');
$redirect = redirect()->to('/administrator/events')->with('success', 'Event updated successfully');
if ($this->request->getPost('send_email_parent')) {
$emailStatus = $this->broadcastEventToParents((int) $id);
$redirect = $this->applyEventEmailFlash($redirect, $emailStatus);
}
return $redirect;
} else {
log_message('debug', 'GET detected');
return redirect()->back()->with('error', 'Failed to update event');
@@ -221,6 +238,106 @@ class EventController extends ResourceController
]);
}
private function broadcastEventToParents(int $eventId): array
{
$event = $this->eventModel->find($eventId);
if (!$event) {
return ['ok' => false, 'count' => 0, 'message' => 'Event email was skipped because the event could not be loaded.'];
}
$emails = $this->collectParentBroadcastEmails((string) ($event['school_year'] ?? ''));
if ($emails === []) {
return ['ok' => false, 'count' => 0, 'message' => 'Event email was skipped because no parent email addresses were found.'];
}
$subject = 'School Event: ' . trim((string) ($event['event_name'] ?? 'Upcoming Event'));
$innerBody = view('emails/event_broadcast', [
'event' => $event,
'eventUrl' => base_url('parent/events'),
'flyerUrl' => !empty($event['flyer']) ? base_url('uploads/' . ltrim((string) $event['flyer'], '/')) : null,
]);
$body = view('emails/_wrap_layout', [
'title' => $subject,
'subject' => $subject,
'body_html' => $innerBody,
]);
$ok = $this->emailService->sendCc($emails, $subject, $body, 'general');
$batchCount = $this->emailService->getLastCcBatchCount();
return [
'ok' => $ok,
'count' => count($emails),
'batches' => $batchCount,
'message' => $ok
? 'Event email was sent in ' . $batchCount . ' CC batch(es) for ' . count($emails) . ' recipient(s).'
: 'The event was saved, but sending the parent CC email failed.',
];
}
private function collectParentBroadcastEmails(string $schoolYear = ''): array
{
$emails = [];
$db = Database::connect();
$parentRole = $db->table('roles')
->select('id')
->where('LOWER(name)', 'parent')
->get()
->getRow();
if ($parentRole) {
$firstParentQuery = $db->table('users')
->select('users.email')
->join('user_roles', 'user_roles.user_id = users.id')
->where('user_roles.role_id', (int) $parentRole->id)
->where('users.email IS NOT NULL', null, false)
->where('users.email !=', '');
if ($schoolYear !== '' && $db->fieldExists('school_year', 'users')) {
$firstParentQuery->where('users.school_year', $schoolYear);
}
foreach ($firstParentQuery->get()->getResultArray() as $row) {
$email = trim((string) ($row['email'] ?? ''));
if ($email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL)) {
$emails[strtolower($email)] = $email;
}
}
}
$secondParentQuery = $db->table('parents')
->select('secondparent_email')
->where('secondparent_email IS NOT NULL', null, false)
->where('secondparent_email !=', '');
if ($schoolYear !== '' && $db->fieldExists('school_year', 'parents')) {
$secondParentQuery->where('school_year', $schoolYear);
}
foreach ($secondParentQuery->get()->getResultArray() as $row) {
$email = trim((string) ($row['secondparent_email'] ?? ''));
if ($email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL)) {
$emails[strtolower($email)] = $email;
}
}
return array_values($emails);
}
private function applyEventEmailFlash($redirect, array $emailStatus)
{
if (!empty($emailStatus['message'])) {
return $redirect->with(
($emailStatus['ok'] ?? false) ? 'email_status' : 'email_error',
$emailStatus['message']
);
}
return $redirect;
}
public function delete($id = null)
{