From e4ff1f0a59a97505fd85d6bdc9ebb5a0ebdf543c Mon Sep 17 00:00:00 2001
From: essae
Date: Mon, 12 May 2025 15:06:39 -0400
Subject: [PATCH] Integrate EmailUtility, SubmissionCheck, and Response into
ContactController; finalize contact form logic and template handling
---
app/Controllers/ContactController.php | 81 ++--
app/Utilities/Database.php | 78 ++++
app/Utilities/EmailUtility.php | 393 ++++++++++++++++++
app/Utilities/Response.php | 143 +++++++
app/Utilities/Sanitizer.php | 86 ++++
app/Utilities/SubmissionCheck.php | 53 +++
app/Utilities/Validator.php | 238 +++++++++++
resources/emails/contact_confirmation.php | 4 -
.../emails/contact_internal_email.html | 6 +
9 files changed, 1038 insertions(+), 44 deletions(-)
create mode 100644 app/Utilities/Database.php
create mode 100644 app/Utilities/EmailUtility.php
create mode 100644 app/Utilities/Response.php
create mode 100644 app/Utilities/Sanitizer.php
create mode 100644 app/Utilities/SubmissionCheck.php
create mode 100644 app/Utilities/Validator.php
delete mode 100644 resources/emails/contact_confirmation.php
create mode 100644 resources/templates/emails/contact_internal_email.html
diff --git a/app/Controllers/ContactController.php b/app/Controllers/ContactController.php
index 8a4a035..41d77e4 100644
--- a/app/Controllers/ContactController.php
+++ b/app/Controllers/ContactController.php
@@ -4,7 +4,7 @@
* File: ContactController.php
* Path: /app/Controllers/ContactController.php
* Purpose: Handles form submissions from the Arsha contact form
- * Version: 1.0
+ * Version: 1.1
* Author: Wizdom Networks
* Usage: Routed via Router to handle POST /contact
* ============================================
@@ -12,9 +12,15 @@
namespace WizdomNetworks\WizeWeb\Controllers;
-use WizdomNetworks\WizeWeb\Utils\Logger;
-use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
use WizdomNetworks\WizeWeb\Core\View;
+use WizdomNetworks\WizeWeb\Utilities\Logger;
+use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
+use WizdomNetworks\WizeWeb\Utilities\EmailUtility;
+use WizdomNetworks\WizeWeb\Utilities\Sanitizer;
+use WizdomNetworks\WizeWeb\Utilities\Validator;
+use WizdomNetworks\WizeWeb\Utilities\Response;
+use WizdomNetworks\WizeWeb\Utilities\SubmissionCheck;
+use WizdomNetworks\WizeWeb\Utilities\Database;
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
@@ -23,24 +29,31 @@ class ContactController
public function submit(): void
{
try {
- // Sanitize and validate input
- $firstName = trim($_POST['first_name'] ?? '');
- $lastName = trim($_POST['last_name'] ?? '');
- $email = trim($_POST['email'] ?? '');
- $phone = trim($_POST['phone'] ?? '');
- $message = trim($_POST['message'] ?? '');
+ $firstName = Sanitizer::sanitizeString($_POST['first_name'] ?? '');
+ $lastName = Sanitizer::sanitizeString($_POST['last_name'] ?? '');
+ $email = Sanitizer::sanitizeEmail($_POST['email'] ?? '');
+ $phone = Sanitizer::sanitizePhone($_POST['phone'] ?? '');
+ $message = Sanitizer::sanitizeText($_POST['message'] ?? '');
if (!$firstName || !$lastName || !$email || !$phone || !$message) {
- throw new \Exception("All fields except phone must be filled out.");
+ Response::jsonError('All fields must be filled out.');
+ return;
}
- if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
- throw new \Exception("Invalid email address.");
+ if (!Validator::isValidEmail($email)) {
+ Response::jsonError('Please provide a valid email address.');
+ return;
}
- // Store in database
- $pdo = new \PDO($_ENV['DB_DSN'], $_ENV['DB_USER'], $_ENV['DB_PASS']);
- $stmt = $pdo->prepare("INSERT INTO contact_messages (first_name, last_name, email, phone, message, ip_address, user_agent)
+ // Check if email has submitted recently
+ if (SubmissionCheck::isDuplicateContactEmail($email)) {
+ $lastDate = SubmissionCheck::getLastContactSubmissionDate($email);
+ Response::jsonError("This email already submitted a message on $lastDate. Please wait a week before sending another or contact us directly.");
+ return;
+ }
+
+ $db = Database::getConnection();
+ $stmt = $db->prepare("INSERT INTO contact_messages (first_name, last_name, email, phone, message, ip_address, user_agent)
VALUES (?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([
$firstName,
@@ -54,35 +67,23 @@ class ContactController
Logger::info("Contact form submitted by $firstName $lastName <$email>");
- // Email notification
- $mail = new PHPMailer(true);
- $mail->isSMTP();
- $mail->Host = $_ENV['SMTP_HOST'];
- $mail->Port = $_ENV['SMTP_PORT'];
- $mail->SMTPAuth = $_ENV['SMTP_AUTH'] === 'true';
- $mail->SMTPSecure = $_ENV['SMTP_ENCRYPTION'] !== 'none' ? $_ENV['SMTP_ENCRYPTION'] : '';
- $mail->Username = $_ENV['SMTP_USERNAME'];
- $mail->Password = $_ENV['SMTP_PASSWORD'];
- $mail->setFrom($_ENV['SMTP_FROM_EMAIL'], $_ENV['SMTP_FROM_NAME']);
- $mail->addAddress($_ENV['SALES_EMAILS'] ?? $_ENV['ADMIN_EMAILS']);
+ // Send internal notification email
+ EmailUtility::sendInternalContactAlert([
+ 'first_name' => $firstName,
+ 'last_name' => $lastName,
+ 'email' => $email,
+ 'phone' => $phone,
+ 'message' => $message
+ ]);
- $mail->Subject = "New Contact Message from $firstName $lastName";
- $mail->Body = "You received a message from: \n\n"
- . "Name: $firstName $lastName\n"
- . "Email: $email\n"
- . "Phone: $phone\n"
- . "Message:\n$message\n";
+ // Send confirmation to user
+ EmailUtility::sendContactConfirmation($email, $firstName);
- $mail->send();
-
- http_response_code(200);
- echo json_encode(['success' => true, 'message' => 'Thank you. We will be in touch.']);
+ Response::jsonSuccess('Thank you. We will be in touch shortly.');
} catch (\Throwable $e) {
- Logger::error("Contact form error: " . $e->getMessage());
- ErrorHandler::handleException($e);
- http_response_code(400);
- echo json_encode(['success' => false, 'error' => $e->getMessage()]);
+ ErrorHandler::exception($e);
+ Response::jsonError('An internal error occurred. Please try again later.');
}
}
}
diff --git a/app/Utilities/Database.php b/app/Utilities/Database.php
new file mode 100644
index 0000000..83a610d
--- /dev/null
+++ b/app/Utilities/Database.php
@@ -0,0 +1,78 @@
+connect();
+ }
+
+ /**
+ * Establishes a connection to the database.
+ */
+ private function connect(): void
+ {
+ $dsn = sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', $_ENV['DB_HOST'], $_ENV['DB_NAME']);
+
+ try {
+ $this->connection = new PDO($dsn, $_ENV['DB_USER'], $_ENV['DB_PASSWORD']);
+ $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+ Logger::info('Database connection established successfully.');
+ } catch (PDOException $e) {
+ Logger::error('Database connection failed: ' . $e->getMessage());
+ throw $e;
+ }
+ }
+
+ /**
+ * Executes a query and returns the result.
+ *
+ * @param string $query The SQL query to execute.
+ * @param array $params Parameters for prepared statements (optional).
+ * @return array The query result.
+ */
+ public function query(string $query, array $params = []): array
+ {
+ try {
+ $stmt = $this->connection->prepare($query);
+ $stmt->execute($params);
+ Logger::info('Query executed successfully: ' . $query);
+ return $stmt->fetchAll(PDO::FETCH_ASSOC);
+ } catch (PDOException $e) {
+ Logger::error('Query failed: ' . $query . ' | Error: ' . $e->getMessage());
+ throw $e;
+ }
+ }
+
+ /**
+ * Retrieves the PDO connection instance.
+ *
+ * @return PDO The PDO instance.
+ */
+ public function getConnection(): PDO
+ {
+ return $this->connection;
+ }
+}
diff --git a/app/Utilities/EmailUtility.php b/app/Utilities/EmailUtility.php
new file mode 100644
index 0000000..cc42b91
--- /dev/null
+++ b/app/Utilities/EmailUtility.php
@@ -0,0 +1,393 @@
+ $_ENV['SMTP_HOST'] ?? 'localhost',
+ 'port' => $_ENV['SMTP_PORT'] ?? '25',
+ 'from_email' => $_ENV['SMTP_FROM_EMAIL'] ?? 'concierge@helpdeskplus.ca',
+ 'from_name' => $_ENV['SMTP_FROM_NAME'] ?? 'HelpDesk+',
+ 'auth' => false
+ ];
+
+ foreach (['host', 'port', 'from_email'] as $field) {
+ if (empty($config[$field])) {
+ Logger::logError("Missing email configuration: $field");
+ throw new \RuntimeException("Missing email configuration: $field");
+ }
+ }
+ }
+ else {
+
+ $config = [
+ 'host' => $_ENV['SMTP_HOST'] ?? 'localhost',
+ 'username' => $_ENV['SMTP_USERNAME'] ?? null,
+ 'password' => $_ENV['SMTP_PASSWORD'] ?? null,
+ 'port' => $_ENV['SMTP_PORT'] ?? '25',
+ 'encryption' => $_ENV['SMTP_ENCRYPTION'] ?? 'none',
+ 'from_email' => $_ENV['SMTP_FROM_EMAIL'] ?? 'concierge@helpdeskplus.ca',
+ 'from_name' => $_ENV['SMTP_FROM_NAME'] ?? 'HelpDesk+',
+ 'smtpsecure' => $_ENV['SMTP_AUTH'],
+ 'auth' => true,
+ 'autotls' => true
+ ];
+
+ foreach (['host', 'port', 'username', 'password', 'from_email'] as $field) {
+ if (empty($config[$field])) {
+ Logger::logError("Missing email configuration: $field");
+ throw new \RuntimeException("Missing email configuration: $field");
+ }
+ }
+ }
+ return $config;
+ }
+
+ /**
+ * Render an email template with dynamic data.
+ *
+ * @param string $templatePath The path to the email template file.
+ * @param array $data Key-value pairs for template placeholders.
+ * @return string The rendered email content.
+ */
+ public static function renderTemplate(string $templatePath, array $data): string
+ {
+ if (!file_exists($templatePath)) {
+ Logger::logError("Email template not found: $templatePath");
+ return '';
+ }
+
+ $content = file_get_contents($templatePath);
+ foreach ($data as $key => $value) {
+ $content = str_replace("{{{$key}}}", $value, $content);
+ }
+
+ return $content;
+ }
+
+ /**
+ * Log email status into the database.
+ *
+ * @param string $recipient The recipient email address.
+ * @param string $status The status of the email (e.g., 'queued', 'sent', 'failed').
+ * @param string|null $errorMessage An optional error message.
+ * @return void
+ */
+ private static function logEmailStatus(string $recipient, string $status, ?string $errorMessage = null): void
+ {
+ try {
+ $db = Database::getInstance();
+ $query = "INSERT INTO email_status (recipient, status, error_message, created_at) VALUES (:recipient, :status, :error_message, NOW())";
+ $params = [
+ ':recipient' => $recipient,
+ ':status' => $status,
+ ':error_message' => $errorMessage,
+ ];
+ $db->executeQuery($query, $params);
+ } catch (\Throwable $e) {
+ Logger::logError("Failed to log email status: " . $e->getMessage());
+ }
+ }
+
+ /**
+ * Notify admin or sales team via email.
+ *
+ * @param string $emailType The type of notification (e.g., 'admin', 'sales').
+ * @param string $subject The email subject.
+ * @param string $templatePath Path to the notification template.
+ * @param array $templateData Data for the template placeholders.
+ * @return void
+ */
+ public static function notifyTeam(string $emailType, string $subject, string $templatePath, array $templateData): void
+ {
+ $recipients = $emailType === 'admin' ? explode(',', $_ENV['ADMIN_EMAILS']) : explode(',', $_ENV['SALES_EMAILS']);
+
+ foreach ($recipients as $recipient) {
+ $recipient = trim($recipient);
+ if (!self::sendEmail($recipient, $subject, $templatePath, $templateData)) {
+ Logger::logError("Failed to send $emailType notification to: $recipient");
+ }
+ }
+ }
+
+ /**
+ * Send an email with enhanced error categorization.
+ *
+ * @param string $recipient Recipient email address.
+ * @param string $subject Email subject.
+ * @param string $templatePath Path to the email template.
+ * @param array $templateData Data to replace placeholders in the template.
+ * @param array $options Optional configurations (e.g., CC, BCC).
+ * @param int $retryLimit The maximum number of retries for transient failures.
+ * @return bool Returns true on success, false otherwise.
+ */
+ public static function sendEmail(string $recipient, string $subject, string $templatePath, array $templateData = [], array $options = [], int $retryLimit = 3): bool
+ {
+ $mail = new PHPMailer(true);
+ $config = self::getConfig();
+ $retryCount = 0;
+
+ while ($retryCount <= $retryLimit) {
+ try {
+
+ $mail->isSMTP();
+ $mail->SMTPAutoTLS = false;
+ $mail->SMTPAuth = false;
+ /* If authentication is enabled setup the connection */
+ if ( $config['auth'] === 'true' ){
+ $mail->SMTPAuth = $config['auth'];
+ $mail->Username = $config['username'];
+ $mail->Password = $config['password'];
+ $mail->SMTPSecure = $config['encryption'];
+ $mail->SMTPAutoTLS = $config['autotls'];
+ }
+
+ $mail->Host = $config['host'];
+ $mail->Port = $config['port'];
+/******************************
+ $mail->SMTPDebug = $_ENV['APP_ENV'] === 'development' ? 2 : 0;
+ /$mail->Debugoutput = function ($message, $level) {
+ Logger::logInfo("SMTP Debug [$level]: $message");
+ };
+*******************************/
+ $mail->setFrom($config['from_email'], $config['from_name']);
+ $mail->addAddress($recipient);
+
+ if (!empty($options['cc'])) {
+ foreach ((array)$options['cc'] as $cc) {
+ $mail->addCC($cc);
+ }
+ }
+
+ if (!empty($options['bcc'])) {
+ foreach ((array)$options['bcc'] as $bcc) {
+ $mail->addBCC($bcc);
+ }
+ }
+
+ $mail->isHTML(true);
+ $mail->Subject = $subject;
+ $mail->Body = self::renderTemplate($templatePath, $templateData);
+
+ $mail->send();
+ Logger::logInfo("Email sent to $recipient with subject: $subject");
+ self::logEmailStatus($recipient, 'sent');
+ return true;
+ } catch (Exception $e) {
+ $retryCount++;
+ $error = $mail->ErrorInfo;
+ Logger::logWarning("Email send failed for $recipient (Attempt $retryCount/$retryLimit): $error");
+
+ if (str_contains($error, '452 4.3.1')) {
+ Logger::logWarning("Transient error detected for $recipient: $error");
+ } elseif (str_contains($error, '550')) {
+ Logger::logError("Permanent error detected for $recipient: $error");
+ self::logEmailStatus($recipient, 'failed', $error);
+ return false;
+ } elseif (str_contains($error, '421')) {
+ Logger::logWarning("Rate-limiting error detected for $recipient: $error");
+ } else {
+ Logger::logError("Unhandled SMTP error for $recipient: $error");
+ }
+
+ if (str_contains($error, '452') || str_contains($error, '421')) {
+ if ($retryCount > $retryLimit) {
+ Logger::logError("Exceeded retry limit for email to $recipient: $error");
+ self::logEmailStatus($recipient, 'failed', $error);
+ return false;
+ }
+
+ sleep(5);
+ continue;
+ }
+
+ Logger::logError("Email permanently failed for $recipient: $error");
+ self::logEmailStatus($recipient, 'failed', $error);
+ return false;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Process the email queue and send emails in batches.
+ *
+ * @param int $batchSize Number of emails to process in a single batch.
+ * @param int $maxRetries Maximum retry attempts for failed emails.
+ * @return void
+ */
+ public static function processEmailQueue(int $batchSize = 10, int $maxRetries = 3): void
+ {
+ for ($i = 0; $i < $batchSize; $i++) {
+ $emailData = self::$queueUtility->dequeue('email');
+
+ if ($emailData === null) {
+ Logger::logInfo("No more emails to process in the queue.");
+ break;
+ }
+
+ $success = self::sendEmail(
+ $emailData['recipient'],
+ $emailData['subject'],
+ $emailData['templatePath'],
+ $emailData['templateData'],
+ $emailData['options']
+ );
+
+ if (!$success) {
+ $retries = $emailData['retries'] ?? 0;
+
+ if ($retries < $maxRetries) {
+ $priority = $emailData['priority'] ?? 0;
+ $emailData['retries'] = $retries + 1;
+ self::$queueUtility->enqueue('email', $emailData, $priority);
+ Logger::logWarning(
+ "Email re-queued for recipient: {$emailData['recipient']} (Attempt {$emailData['retries']})"
+ );
+ } else {
+ Logger::logError("Email permanently failed for recipient: {$emailData['recipient']}");
+ self::logEmailStatus($emailData['recipient'], 'failed', 'Max retry limit reached.');
+ }
+ } else {
+ self::logEmailStatus($emailData['recipient'], 'sent');
+ }
+ }
+
+ Logger::logInfo("Email queue processing completed.");
+ }
+
+ /**
+ * Process contact-related email queue.
+ *
+ * @param int $batchSize Number of emails to process in a single batch.
+ * @param int $maxRetries Maximum retry attempts for failed emails.
+ * @return void
+ */
+public static function processContactQueue(int $batchSize = 10, int $maxRetries = 3): void
+{
+ Logger::logInfo("Processing contact email queue...");
+
+ for ($i = 0; $i < $batchSize; $i++) {
+ $emailData = self::$queueUtility->dequeue('contact_email');
+
+ if ($emailData === null) {
+ Logger::logInfo("No more emails to process in the contact queue.");
+ break;
+ }
+
+ $success = self::sendEmail(
+ $emailData['recipient'],
+ $emailData['subject'],
+ $emailData['templatePath'],
+ $emailData['templateData'],
+ $emailData['options']
+ );
+
+ if (!$success) {
+ $retries = $emailData['retries'] ?? 0;
+
+ if ($retries < $maxRetries) {
+ $priority = $emailData['priority'] ?? 0;
+ $emailData['retries'] = $retries + 1;
+ self::$queueUtility->enqueue('contact_email', $emailData, $priority);
+ Logger::logWarning(
+ "Contact email re-queued for recipient: {$emailData['recipient']} (Attempt {$emailData['retries']})"
+ );
+ } else {
+ Logger::logError("Contact email permanently failed for recipient: {$emailData['recipient']}");
+ self::logEmailStatus($emailData['recipient'], 'failed', 'Max retry limit reached.');
+ }
+ } else {
+ self::logEmailStatus($emailData['recipient'], 'sent');
+ }
+ }
+
+ Logger::logInfo("Contact email queue processing completed.");
+}
+
+/**
+ * Retrieve the status of a specific email by recipient.
+ *
+ * @param string $recipient Email address of the recipient.
+ * @return array|null The email status or null if not found.
+ */
+public static function getEmailStatus(string $recipient): ?array
+{
+ try {
+ $db = Database::getInstance();
+ $query = "SELECT * FROM email_status WHERE recipient = :recipient ORDER BY created_at DESC LIMIT 1";
+ $params = [':recipient' => $recipient];
+ return $db->fetchOne($query, $params);
+ } catch (\Throwable $e) {
+ Logger::logError("Failed to retrieve email status for $recipient: " . $e->getMessage());
+ return null;
+ }
+}
+
+/**
+ * Clear the email queue.
+ *
+ * @param string $queueName The name of the queue to clear (default: 'email').
+ * @return void
+ */
+public static function clearQueue(string $queueName = 'email'): void
+{
+ Logger::logInfo("Clearing queue: $queueName");
+
+ try {
+ self::$queueUtility->clearQueue($queueName);
+ Logger::logInfo("Queue $queueName cleared successfully.");
+ } catch (\Throwable $e) {
+ Logger::logError("Failed to clear queue $queueName: " . $e->getMessage());
+ }
+}
+
+/**
+ * List all queued emails in a specific queue.
+ *
+ * @param string $queueName The name of the queue to inspect (default: 'email').
+ * @return array List of queued emails.
+ */
+public static function listQueuedEmails(string $queueName = 'email'): array
+{
+ try {
+ Logger::logInfo("Listing emails in queue: $queueName");
+ return self::$queueUtility->listQueue($queueName);
+ } catch (\Throwable $e) {
+ Logger::logError("Failed to list emails in queue $queueName: " . $e->getMessage());
+ return [];
+ }
+}
+}
diff --git a/app/Utilities/Response.php b/app/Utilities/Response.php
new file mode 100644
index 0000000..5dd253c
--- /dev/null
+++ b/app/Utilities/Response.php
@@ -0,0 +1,143 @@
+ $content], $status);
+ exit;
+ }
+
+ /**
+ * Send a file download response.
+ *
+ * @param string $filePath The file path.
+ * @param string|null $downloadName The name for the downloaded file (optional).
+ * @param array $headers Additional headers to include in the response.
+ * @return void
+ */
+ public static function file(string $filePath, ?string $downloadName = null, array $headers = []): void
+ {
+ if (!file_exists($filePath)) {
+ self::error('File not found.', 404);
+ }
+
+ $downloadName = $downloadName ?? basename($filePath);
+ header('Content-Type: application/octet-stream');
+ header("Content-Disposition: attachment; filename=\"$downloadName\"");
+ header('Content-Length: ' . filesize($filePath));
+ self::sendHeaders($headers);
+ readfile($filePath);
+ self::logResponse(['file' => $downloadName], 200);
+ exit;
+ }
+
+ /**
+ * Send an error response.
+ *
+ * @param string $message The error message.
+ * @param int $status HTTP status code (default: 500).
+ * @param array $headers Additional headers to include in the response.
+ * @return void
+ */
+ public static function error(string $message, int $status = 500, array $headers = []): void
+ {
+ self::json(['success' => false, 'message' => $message], $status, $headers);
+ }
+
+ /**
+ * Predefined response for 400 Bad Request.
+ *
+ * @param string $message The error message.
+ * @return void
+ */
+ public static function badRequest(string $message): void
+ {
+ self::error($message, 400);
+ }
+
+ /**
+ * Predefined response for 404 Not Found.
+ *
+ * @param string $message The error message.
+ * @return void
+ */
+ public static function notFound(string $message): void
+ {
+ self::error($message, 404);
+ }
+
+ /**
+ * Predefined response for 500 Internal Server Error.
+ *
+ * @param string $message The error message.
+ * @return void
+ */
+ public static function serverError(string $message): void
+ {
+ self::error($message, 500);
+ }
+
+ /**
+ * Send custom headers.
+ *
+ * @param array $headers Headers to include in the response.
+ * @return void
+ */
+ private static function sendHeaders(array $headers): void
+ {
+ foreach ($headers as $key => $value) {
+ header("$key: $value");
+ }
+ }
+
+ /**
+ * Log the response if debugging is enabled.
+ *
+ * @param array $data The response data.
+ * @param int $status HTTP status code.
+ * @return void
+ */
+ private static function logResponse(array $data, int $status): void
+ {
+ if (getenv('DEBUG') === 'true') {
+ Logger::logInfo("Response sent with status $status: " . json_encode($data));
+ }
+ }
+}
diff --git a/app/Utilities/Sanitizer.php b/app/Utilities/Sanitizer.php
new file mode 100644
index 0000000..649be2e
--- /dev/null
+++ b/app/Utilities/Sanitizer.php
@@ -0,0 +1,86 @@
+ $value) {
+ if (is_array($value)) {
+ $clean[$key] = self::sanitizeArray($value);
+ } else {
+ $clean[$key] = self::sanitizeString((string) $value);
+ }
+ }
+ return $clean;
+ }
+}
diff --git a/app/Utilities/SubmissionCheck.php b/app/Utilities/SubmissionCheck.php
new file mode 100644
index 0000000..119073f
--- /dev/null
+++ b/app/Utilities/SubmissionCheck.php
@@ -0,0 +1,53 @@
+prepare($sql);
+ $stmt->execute(['email' => $email]);
+
+ $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if ($row && isset($row[$timestampField])) {
+ $last = new \DateTime($row[$timestampField]);
+ $cutoff = (new \DateTime())->modify("-{$days} days");
+
+ if ($last >= $cutoff) {
+ return ['submitted_at' => $last->format('Y-m-d H:i:s')];
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/app/Utilities/Validator.php b/app/Utilities/Validator.php
new file mode 100644
index 0000000..209a05c
--- /dev/null
+++ b/app/Utilities/Validator.php
@@ -0,0 +1,238 @@
+= $minLength;
+
+ if (!$isValid) {
+ Logger::warning("[WARNING] String is shorter than minimum length: $string");
+ }
+
+ return $isValid;
+ } catch (\Throwable $e) {
+ ErrorHandler::exception($e);
+ return false;
+ }
+ }
+
+ /**
+ * Check if a string has a maximum length.
+ *
+ * @param string $string The string to check.
+ * @param int $maxLength The maximum length.
+ * @return bool True if the string meets the maximum length, false otherwise.
+ */
+ public static function hasMaxLength(string $string, int $maxLength): bool
+ {
+ try {
+ Logger::info("[DEBUG] Checking if string has maximum length: $maxLength");
+
+ $isValid = strlen(trim($string)) <= $maxLength;
+
+ if (!$isValid) {
+ Logger::warning("[WARNING] String exceeds maximum length: $string");
+ }
+
+ return $isValid;
+ } catch (\Throwable $e) {
+ ErrorHandler::exception($e);
+ return false;
+ }
+ }
+
+ /**
+ * Validate a string length.
+ *
+ * @param string $input The string to validate.
+ * @param int $min Minimum length.
+ * @param int $max Maximum length.
+ * @return bool True if the string length is valid, false otherwise.
+ */
+ public static function validateStringLength(string $input, int $min, int $max): bool
+ {
+ try {
+ Logger::info("[DEBUG] Validating string length: Input='$input', Min=$min, Max=$max");
+
+ $length = strlen($input);
+ $isValid = $length >= $min && $length <= $max;
+
+ if (!$isValid) {
+ Logger::warning("[WARNING] Invalid string length: $length (Expected between $min and $max)");
+ }
+
+ return $isValid;
+ } catch (\Throwable $e) {
+ ErrorHandler::exception($e);
+ return false;
+ }
+ }
+
+ /**
+ * Validate a boolean value.
+ *
+ * @param mixed $input The input to validate as a boolean.
+ * @return bool True if the input is a valid boolean, false otherwise.
+ */
+ public static function validateBoolean($input): bool
+ {
+ try {
+ Logger::info("[DEBUG] Validating boolean input: $input");
+
+ $isValid = is_bool(filter_var($input, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE));
+
+ if (!$isValid) {
+ Logger::warning("[WARNING] Invalid boolean input: $input");
+ }
+
+ return $isValid;
+ } catch (\Throwable $e) {
+ ErrorHandler::exception($e);
+ return false;
+ }
+ }
+
+ /**
+ * Validate a date format.
+ *
+ * @param string $date The date string to validate.
+ * @param string $format The expected date format (e.g., 'Y-m-d').
+ * @return bool True if the date matches the format, false otherwise.
+ */
+ public static function validateDate(string $date, string $format = 'Y-m-d'): bool
+ {
+ try {
+ Logger::info("[DEBUG] Validating date: $date with format: $format");
+
+ $dateTime = \DateTime::createFromFormat($format, $date);
+ $isValid = $dateTime && $dateTime->format($format) === $date;
+
+ if (!$isValid) {
+ Logger::warning("[WARNING] Invalid date: $date (Expected format: $format)");
+ }
+
+ return $isValid;
+ } catch (\Throwable $e) {
+ ErrorHandler::exception($e);
+ return false;
+ }
+ }
+}
diff --git a/resources/emails/contact_confirmation.php b/resources/emails/contact_confirmation.php
deleted file mode 100644
index 677e364..0000000
--- a/resources/emails/contact_confirmation.php
+++ /dev/null
@@ -1,4 +0,0 @@
-New Contact Form Submission:
+
+ - Name: {{name}}
+ - Email: {{email}}
+ - Message: {{message}}
+