diff --git a/app/Controllers/ContactController.php b/app/Controllers/ContactController.php
index e31a2ad..e6c4ddd 100644
--- a/app/Controllers/ContactController.php
+++ b/app/Controllers/ContactController.php
@@ -1,9 +1,11 @@
emailService = new EmailService();
+ $this->verificationService = new VerificationService();
+ }
+
+ /**
+ * Renders the landing page containing the contact form.
+ */
public function index(): void
{
View::render('pages/landing');
}
+ /**
+ * Handles form submission: validates, logs, checks abuse, stores, and triggers verification.
+ * If user opted in to the newsletter, flags for follow-up after verification.
+ */
public function submit(): void
{
Logger::info("Executing controller: ContactController::submit");
@@ -42,12 +65,14 @@ class ContactController
'message' => Sanitizer::sanitizeString($_POST['message'] ?? ''),
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
+ 'pending_newsletter_opt_in' => isset($_POST['subscribe_newsletter']) && $_POST['subscribe_newsletter'] === '1' ? 1 : 0
];
foreach ($formData as $key => $value) {
Logger::info("Sanitized input: {$key} = {$value}");
}
+ // Validate required fields and email format
if (
empty($formData['first_name']) ||
empty($formData['last_name']) ||
@@ -58,26 +83,26 @@ class ContactController
!Validator::isEmail($formData['email'])
) {
Logger::info("Validation failed for contact form submission");
- $_SESSION['contact_error'] = 'An internal error occurred. Please try again later.';
+ $_SESSION['contact_error'] = 'Validation error. Please try again.';
SessionHelper::writeClose();
- header("Location: /#contact");
- exit;
+ $this->respondOrRedirect(false, 'Validation error.');
}
$db = Database::getConnection();
+ // Run submission abuse heuristics
$evaluation = SubmissionCheck::evaluate($db, $formData['email'], $formData['phone'], $formData['ip_address']);
Logger::info("Submission evaluation result: " . json_encode($evaluation));
if ($evaluation['action'] === 'block') {
$_SESSION['contact_error'] = "Submission blocked due to suspicious activity. If this is a mistake, please contact us directly.";
Logger::warning("Blocked submission from IP: {$formData['ip_address']}, Reason: {$evaluation['reason']}");
- EmailHelper::alertAdmins('Blocked Submission Detected', "A submission was blocked for the following reason: {$evaluation['reason']}", $formData);
+ $this->emailService->alertAdmins('Blocked Submission Detected', $evaluation['reason'], $formData);
SessionHelper::writeClose();
- header("Location: /#contact");
- exit;
+ $this->respondOrRedirect(false, 'Submission blocked.');
}
+ // Log submission intent
$logId = null;
try {
$logStmt = $db->prepare("INSERT INTO submission_logs (email, phone, ip_address, user_agent, was_saved, reason) VALUES (:email, :phone, :ip, :ua, :saved, :reason)");
@@ -94,46 +119,68 @@ class ContactController
Logger::error("Failed to insert into submission_logs: " . $e->getMessage());
}
+ // Save form content
$contactModel = new ContactModel($db);
$saveSuccess = $contactModel->saveContactForm($formData);
-
$contactId = $db->lastInsertId();
- $verificationCode = bin2hex(random_bytes(16));
- $expiresAt = (new \DateTime('+72 hours'))->format('Y-m-d H:i:s');
+ // Assign verification code
if ($saveSuccess) {
- $stmt = $db->prepare("UPDATE contact_messages SET verification_code = ?, is_verified = 0, verification_expires_at = ? WHERE id = ?");
- $stmt->execute([$verificationCode, $expiresAt, $contactId]);
- ContactService::sendVerificationEmail($formData['email'], $verificationCode);
+ $verificationCode = $this->verificationService->generateCode();
+ $expiresAt = $this->verificationService->getExpirationTime();
+ $this->verificationService->assignCodeToRecord('contact_messages', $contactId, $verificationCode, $expiresAt);
+
+ $this->emailService->sendVerificationEmail(
+ $formData['email'],
+ $verificationCode,
+ 'verify_contact',
+ ['first_name' => $formData['first_name']]
+ );
}
+ // Update log if save succeeded
if ($saveSuccess && $logId) {
$update = $db->prepare("UPDATE submission_logs SET was_saved = 1 WHERE id = :id");
$update->execute([':id' => $logId]);
}
- // Newsletter opt-in logic
- if (!empty($_POST['subscribe_newsletter'])) {
- Logger::info("Contact opted into newsletter: {$formData['email']}");
- NewsletterService::subscribeOrResend($formData['email']);
- }
-
- Logger::info("✅ Writing session flag: contact_success = true");
- Logger::info("✅ Session content before redirect: " . json_encode($_SESSION));
-
SessionHelper::writeClose();
- View::render('pages/contact_check_email');
- return;
+ $this->respondOrRedirect(true, 'Your message was submitted. Please check your email to verify.');
} catch (\Throwable $e) {
Logger::error("Fatal error in ContactController::submit: " . $e->getMessage());
- EmailHelper::alertAdmins('ContactController::submit - Uncaught Exception', $e->getMessage(), $_POST ?? []);
+ $this->emailService->alertAdmins('ContactController::submit - Uncaught Exception', $e->getMessage(), $_POST ?? []);
$_SESSION['contact_error'] = 'An internal error occurred. Please try again later.';
SessionHelper::writeClose();
- Logger::info("✅ Writing session flag: catch contact_error = " . $_SESSION['contact_error']);
- Logger::info("✅ Session content before redirect: " . json_encode($_SESSION));
- header("Location: /#contact");
- exit;
+ $this->respondOrRedirect(false, 'An internal error occurred.');
}
}
+
+ /**
+ * Responds to client depending on request type (AJAX vs standard).
+ * @param bool $success Indicates if the operation succeeded
+ * @param string $message Message to return or display
+ */
+ private function respondOrRedirect(bool $success, string $message): void
+ {
+ $isAjax = isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
+ strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
+
+ Logger::debug('Detected request type: ' . ($_SERVER['HTTP_X_REQUESTED_WITH'] ?? 'none'));
+ Logger::debug('Will respond with: ' . ($isAjax ? 'JSON' : 'HTML fallback'));
+
+ if ($isAjax) {
+ header('Content-Type: application/json');
+ echo json_encode(['success' => $success, 'message' => $message]);
+ exit;
+ }
+
+ if ($success) {
+ View::render('pages/contact_check_email');
+ } else {
+ header("Location: /#contact");
+ }
+
+ exit;
+ }
}
diff --git a/app/Controllers/ResendVerficationController.php b/app/Controllers/ResendVerficationController.php
deleted file mode 100644
index 1742120..0000000
--- a/app/Controllers/ResendVerficationController.php
+++ /dev/null
@@ -1,104 +0,0 @@
- 'Invalid email or type.']);
- return;
- }
-
- try {
- $db = Database::getConnection();
-
- // Rate limit: no more than 3 per day
- $dailyCheck = $db->prepare("SELECT COUNT(*) FROM verification_attempts WHERE email = ? AND type = ? AND attempted_at >= NOW() - INTERVAL 1 DAY");
- $dailyCheck->execute([$email, $type]);
- $dailyCount = $dailyCheck->fetchColumn();
-
- if ($dailyCount >= 3) {
- View::render('pages/verify_failed', ['reason' => 'You have reached the daily resend limit. Please try again tomorrow.']);
- return;
- }
-
- // Rate limit: no more than 1 every 5 minutes
- $recentCheck = $db->prepare("SELECT COUNT(*) FROM verification_attempts WHERE email = ? AND type = ? AND attempted_at >= NOW() - INTERVAL 5 MINUTE");
- $recentCheck->execute([$email, $type]);
- $recentCount = $recentCheck->fetchColumn();
-
- if ($recentCount > 0) {
- View::render('pages/verify_failed', ['reason' => 'You must wait a few minutes before requesting another verification email.']);
- return;
- }
-
- // Log attempt
- $log = $db->prepare("INSERT INTO verification_attempts (email, type, attempted_at, ip_address, user_agent) VALUES (?, ?, NOW(), ?, ?)");
- $log->execute([
- $email,
- $type,
- $_SERVER['REMOTE_ADDR'] ?? 'unknown',
- $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
- ]);
-
- $code = bin2hex(random_bytes(16));
- $expiry = (new \DateTime())->modify('+72 hours')->format('Y-m-d H:i:s');
-
- if ($type === 'newsletter') {
- $stmt = $db->prepare("SELECT id, is_verified FROM subscribers WHERE email = ?");
- $stmt->execute([$email]);
- $row = $stmt->fetch();
-
- if (!$row || (int)$row['is_verified'] === 1) {
- View::render('pages/verify_failed', ['reason' => 'Email is already verified or not found.']);
- return;
- }
-
- $update = $db->prepare("UPDATE subscribers SET verification_code = ?, is_verified = 0, verification_expires_at = ? WHERE id = ?");
- $update->execute([$code, $expiry, $row['id']]);
- NewsletterService::sendVerificationEmail($email, $code);
- }
-
- if ($type === 'contact') {
- $stmt = $db->prepare("SELECT id, is_verified FROM contact_messages WHERE email = ? ORDER BY created_at DESC LIMIT 1");
- $stmt->execute([$email]);
- $row = $stmt->fetch();
-
- if (!$row || (int)$row['is_verified'] === 1) {
- View::render('pages/verify_failed', ['reason' => 'Email is already verified or not found.']);
- return;
- }
-
- $update = $db->prepare("UPDATE contact_messages SET verification_code = ?, is_verified = 0, verification_expires_at = ? WHERE id = ?");
- $update->execute([$code, $expiry, $row['id']]);
- ContactService::sendVerificationEmail($email, $code);
- }
-
- View::render('pages/verify_success', [
- 'type' => $type,
- 'message' => 'We just sent you a new verification link.'
- ]);
- } catch (\Throwable $e) {
- Logger::error("Resend verification failed: " . $e->getMessage());
- View::render('pages/verify_failed', ['reason' => 'Unexpected error occurred.']);
- }
- }
-}
\ No newline at end of file
diff --git a/app/Controllers/ResendVerificationController.php b/app/Controllers/ResendVerificationController.php
new file mode 100644
index 0000000..4a236ad
--- /dev/null
+++ b/app/Controllers/ResendVerificationController.php
@@ -0,0 +1,60 @@
+resendService = new ResendVerificationService();
+ }
+
+ /**
+ * Handles a POST request to resend a verification email.
+ * Validates email and type, and then delegates the resend attempt to the service.
+ * Renders either a success or failure view based on outcome.
+ *
+ * Expects 'email' and 'type' keys to be set in $_POST.
+ *
+ * @return void
+ */
+ public function handle(): void
+ {
+ $email = trim($_POST['email'] ?? '');
+ $type = trim($_POST['type'] ?? '');
+
+ if (!$email || !$type || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
+ View::render('pages/verify_failed', ['reason' => 'Invalid email or type.']);
+ return;
+ }
+
+ $result = $this->resendService->attemptResend($type, $email);
+
+ if (!$result['success']) {
+ View::render('pages/verify_failed', ['reason' => $result['message']]);
+ } else {
+ View::render('pages/verify_success', [
+ 'type' => $type,
+ 'message' => $result['message']
+ ]);
+ }
+ }
+}
diff --git a/app/Controllers/SubscriberController.php b/app/Controllers/SubscriberController.php
index 6f8affb..d9fa604 100644
--- a/app/Controllers/SubscriberController.php
+++ b/app/Controllers/SubscriberController.php
@@ -10,9 +10,9 @@
namespace WizdomNetworks\WizeWeb\Controllers;
use WizdomNetworks\WizeWeb\Core\View;
-use WizdomNetworks\WizeWeb\Utils\Database;
-use WizdomNetworks\WizeWeb\Utils\Logger;
-use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
+use WizdomNetworks\WizeWeb\Utilities\Database;
+use WizdomNetworks\WizeWeb\Utilities\Logger;
+use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
class SubscriberController
{
diff --git a/app/Controllers/UnsubscribeController.php b/app/Controllers/UnsubscribeController.php
index 8d8d161..59c1bb7 100644
--- a/app/Controllers/UnsubscribeController.php
+++ b/app/Controllers/UnsubscribeController.php
@@ -10,9 +10,9 @@
namespace WizdomNetworks\WizeWeb\Controllers;
use WizdomNetworks\WizeWeb\Core\View;
-use WizdomNetworks\WizeWeb\Utils\Database;
-use WizdomNetworks\WizeWeb\Utils\Logger;
-use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
+use WizdomNetworks\WizeWeb\Utilities\Database;
+use WizdomNetworks\WizeWeb\Utilities\Logger;
+use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
class UnsubscribeController
{
diff --git a/app/Controllers/VerificationController.php b/app/Controllers/VerificationController.php
index f372abc..9bf13d0 100644
--- a/app/Controllers/VerificationController.php
+++ b/app/Controllers/VerificationController.php
@@ -1,71 +1,177 @@
'No verification code provided.']);
- return;
- }
+ $this->emailService = new EmailService();
+ }
- $db = Database::getConnection();
+ /**
+ * Handles email verification for newsletter and contact submissions using a unique code.
+ *
+ * - If the code matches an unverified record: marks it as verified and sends confirmations.
+ * - If already verified: shows a message.
+ * - If expired: prompts user to resend.
+ * - If invalid: redirects user to restart the process.
+ *
+ * @param string $code The verification code from the URL path.
+ * @return void
+ */
+public function verify(string $code): void
+{
+ try {
+ if (empty($code)) {
+ Logger::error("Email verification attempted without a code.");
+ View::render('pages/verify_failed', [
+ 'reason' => 'No verification code provided.',
+ 'redirect' => true
+ ]);
+ return;
+ }
- // Check subscribers table
- $stmt = $db->prepare("SELECT id, is_verified, email, verification_expires_at FROM subscribers WHERE verification_code = ?");
+ $db = Database::getConnection();
+ $subscriber = null;
+ $table = null;
+ $type = null;
+
+ // Attempt to locate the subscriber record by code in either table
+ $stmt = $db->prepare("SELECT * FROM subscribers WHERE verification_code = ?");
+ $stmt->execute([$code]);
+ $subscriber = $stmt->fetch();
+
+ if ($subscriber) {
+ $table = 'subscribers';
+ $type = 'newsletter';
+ } else {
+ $stmt = $db->prepare("SELECT * FROM contact_messages WHERE verification_code = ?");
$stmt->execute([$code]);
$subscriber = $stmt->fetch();
- // Log verification attempt (even if failed)
- $logAttempt = $db->prepare("INSERT INTO verification_attempts (email, type, attempted_at, ip_address, user_agent) VALUES (?, ?, NOW(), ?, ?)");
- $logAttempt->execute([
- $subscriber['email'] ?? '[unknown]',
- 'newsletter',
- $_SERVER['REMOTE_ADDR'] ?? 'unknown',
- $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
- ]);
-
if ($subscriber) {
- if (!empty($subscriber['verification_expires_at']) && strtotime($subscriber['verification_expires_at']) < time()) {
- View::render('pages/verify_failed', ['reason' => 'Your verification link has expired. Please request a new one.']);
- return;
- }
+ $table = 'contact_messages';
+ $type = 'contact';
+ }
+ }
- if ((int) $subscriber['is_verified'] === 1) {
- View::render('pages/verify_success', ['type' => 'newsletter', 'message' => 'This subscription has already been verified.']);
- return;
- }
+ // If no record was found at all
+ if (!$subscriber) {
+ Logger::error("Invalid verification code attempted: $code");
+ View::render('pages/verify_failed', [
+ 'reason' => 'That link is invalid. You may need to start a new submission.',
+ 'redirect' => true
+ ]);
+ return;
+ }
- $update = $db->prepare("UPDATE subscribers SET is_verified = 1, verification_code = NULL WHERE id = ?");
- $update->execute([$subscriber['id']]);
+ // Handle expired code case
+ if (!empty($subscriber['verification_expires_at']) && strtotime($subscriber['verification_expires_at']) < time()) {
+ Logger::info("Verification link expired: $code");
+ View::render('pages/verify_failed', [
+ 'reason' => 'Your verification link has expired. Please request a new one.',
+ 'type' => $type ?? 'unknown'
+ ]);
+ return;
+ }
- Logger::info("Subscriber verified: ID " . $subscriber['id']);
- View::render('pages/verify_success', ['type' => 'newsletter']);
- return;
+ // Log the verification attempt regardless of outcome
+ $safeType = in_array($type, ['contact', 'newsletter', 'contact+newsletter'], true) ? $type : 'unknown';
+ $logAttempt = $db->prepare("
+ INSERT INTO verification_attempts (email, type, attempted_at, ip_address, user_agent)
+ VALUES (?, ?, NOW(), ?, ?)
+ ");
+ $logAttempt->execute([
+ $subscriber['email'] ?? '[unknown]',
+ $safeType,
+ $_SERVER['REMOTE_ADDR'] ?? 'unknown',
+ $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
+ ]);
+
+ // If already verified
+ if ((int) $subscriber['is_verified'] === 1) {
+ View::render('pages/verify_success', [
+ 'type' => $type ?? 'unknown',
+ 'message' => 'This submission has already been verified.'
+ ]);
+ return;
+ }
+
+ // Mark the submission as verified
+ $update = $db->prepare("UPDATE $table SET is_verified = 1, verification_code = NULL WHERE id = ?");
+ $update->execute([$subscriber['id']]);
+
+ Logger::info("Subscriber verified: ID {$subscriber['id']} via $type");
+
+ // Handle post-verification logic for contact submissions
+ if ($type === 'contact') {
+ $stmt = $db->prepare("
+ SELECT first_name, last_name, subject, message, pending_newsletter_opt_in
+ FROM contact_messages WHERE id = ?
+ ");
+ $stmt->execute([$subscriber['id']]);
+ $details = $stmt->fetch();
+
+ $emailData = [
+ 'email' => $subscriber['email'],
+ 'first_name' => $details['first_name'] ?? '',
+ 'last_name' => $details['last_name'] ?? '',
+ 'subject' => $details['subject'] ?? '',
+ 'message' => $details['message'] ?? '',
+ 'ip_address' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
+ 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
+ ];
+
+ // If opted in to newsletter from contact form
+ if (!empty($details['pending_newsletter_opt_in'])) {
+ $this->emailService->sendContactAndNewsletterWelcome($emailData);
+
+ $db->prepare("UPDATE contact_messages SET pending_newsletter_opt_in = 0 WHERE id = ?")
+ ->execute([$subscriber['id']]);
+
+ $db->prepare("
+ INSERT INTO subscribers (email, is_verified, created_at)
+ VALUES (?, 1, NOW())
+ ON DUPLICATE KEY UPDATE is_verified = 1
+ ")->execute([$subscriber['email']]);
+
+ $type = 'contact+newsletter'; // Refined to reflect both intents
+ } else {
+ $this->emailService->sendConfirmationToUser($emailData);
}
- Logger::error("Invalid or expired verification code: $code");
- View::render('pages/verify_failed', ['reason' => 'Verification code is invalid or expired.']);
- } catch (\Throwable $e) {
- Logger::error("Verification exception: " . $e->getMessage());
- View::render('pages/verify_failed', ['reason' => 'An error occurred during verification.']);
+ $this->emailService->sendSalesNotification($emailData);
}
+
+ // Final success render
+ View::render('pages/verify_success', [
+ 'type' => $type ?? 'unknown',
+ 'message' => null
+ ]);
+
+ } catch (\Throwable $e) {
+ Logger::error("Verification exception: " . $e->getMessage());
+ View::render('pages/verify_failed', [
+ 'reason' => 'An error occurred during verification.',
+ 'redirect' => true
+ ]);
}
}
+
+}
diff --git a/app/Models/ContactModel.php b/app/Models/ContactModel.php
index 9d7218a..b8edc68 100644
--- a/app/Models/ContactModel.php
+++ b/app/Models/ContactModel.php
@@ -1,9 +1,9 @@
db->prepare("
- INSERT INTO contacts (name, email, message)
- VALUES (:name, :email, :message)
- ");
+ $stmt = $this->db->prepare("INSERT INTO contacts (name, email, message) VALUES (:name, :email, :message)");
$name = trim(($contactData['name'] ?? '') ?: (($contactData['first_name'] ?? '') . ' ' . ($contactData['last_name'] ?? '')));
$stmt->bindParam(':name', $name);
@@ -57,6 +54,7 @@ class ContactModel
/**
* Saves full contact form submission to the `contact_messages` table.
+ * Includes newsletter opt-in flag.
*
* @param array $formData Associative array of form input
* @return bool True on success, false on failure
@@ -64,15 +62,13 @@ class ContactModel
public function saveContactForm(array $formData): bool
{
try {
- $stmt = $this->db->prepare("
- INSERT INTO contact_messages (
+ $stmt = $this->db->prepare("INSERT INTO contact_messages (
first_name, last_name, email, phone, subject, message,
- ip_address, user_agent
+ ip_address, user_agent, pending_newsletter_opt_in
) VALUES (
:first_name, :last_name, :email, :phone, :subject, :message,
- :ip_address, :user_agent
- )
- ");
+ :ip_address, :user_agent, :pending_newsletter_opt_in
+ )");
$stmt->bindParam(':first_name', $formData['first_name']);
$stmt->bindParam(':last_name', $formData['last_name']);
@@ -83,6 +79,9 @@ class ContactModel
$stmt->bindParam(':ip_address', $formData['ip_address']);
$stmt->bindParam(':user_agent', $formData['user_agent']);
+ $newsletterOptIn = $formData['pending_newsletter_opt_in'] ?? 0;
+ $stmt->bindParam(':pending_newsletter_opt_in', $newsletterOptIn);
+
return $stmt->execute();
} catch (Exception $e) {
Logger::error("ContactModel::saveContactForm failed: " . $e->getMessage());
diff --git a/app/Services/EmailService.php b/app/Services/EmailService.php
new file mode 100644
index 0000000..4c1e8bb
--- /dev/null
+++ b/app/Services/EmailService.php
@@ -0,0 +1,165 @@
+verificationService = new VerificationService();
+ }
+
+ /**
+ * Sends a verification email using the specified template and context.
+ *
+ * @param string $email Recipient email address
+ * @param string $code Verification code
+ * @param string $template Email template to render
+ * @param array $context Template variables to inject
+ * @return bool True on success, false on failure
+ */
+ public function sendVerificationEmail(string $email, string $code, string $template, array $context = []): bool
+ {
+ $context['verification_link'] = rtrim($_ENV['APP_URL'], '/') . "/verify/" . $code;
+ $body = EmailHelper::renderTemplate($template, $context);
+ $subject = 'Please verify your email';
+
+ return EmailHelper::send($email, $subject, $body);
+ }
+
+ /**
+ * Handles a new or existing newsletter subscription and sends a verification email.
+ *
+ * @param string $email User's email address
+ * @param string $ip User's IP address
+ * @param string $userAgent User agent string
+ * @return bool True if verification email sent, false otherwise
+ */
+ public function subscribeNewsletter(string $email, string $ip, string $userAgent): bool
+ {
+ try {
+ $db = Database::getConnection();
+
+ $stmt = $db->prepare("SELECT is_verified FROM subscribers WHERE email = ?");
+ $stmt->execute([$email]);
+ $row = $stmt->fetch();
+
+ if ($row && (int)$row['is_verified'] === 1) {
+ Logger::info("Newsletter signup skipped (already verified): $email");
+ return false;
+ }
+
+ $code = $this->verificationService->generateCode();
+ $expiresAt = $this->verificationService->getExpirationTime();
+
+ if ($row) {
+ $stmt = $db->prepare("UPDATE subscribers SET verification_code = ?, ip_address = ?, user_agent = ?, created_at = NOW() WHERE email = ?");
+ $stmt->execute([$code, $ip, $userAgent, $email]);
+ } else {
+ $stmt = $db->prepare("INSERT INTO subscribers (email, verification_code, is_verified, ip_address, user_agent, created_at) VALUES (?, ?, 0, ?, ?, NOW())");
+ $stmt->execute([$email, $code, $ip, $userAgent]);
+ }
+
+ Logger::info("Newsletter subscription initiated for $email, verification code generated.");
+
+ return $this->sendVerificationEmail($email, $code, self::TEMPLATE_VERIFICATION_NEWSLETTER);
+ } catch (\Throwable $e) {
+ Logger::error("Newsletter subscription failed for $email: " . $e->getMessage());
+ ErrorHandler::exception($e);
+ return false;
+ }
+ }
+
+ /**
+ * Sends a confirmation email to a contact form submitter after successful verification.
+ *
+ * @param array $data Associative array containing user data and message details
+ * @return bool True on success, false on failure
+ */
+ public function sendConfirmationToUser(array $data): bool
+ {
+ $body = EmailHelper::renderTemplate(self::TEMPLATE_CONFIRMATION_CONTACT, $data);
+ $subject = 'Your Email is Verified – Wizdom Networks';
+
+ return EmailHelper::send($data['email'], $subject, $body);
+ }
+
+ /**
+ * Sends a notification to the internal sales team when a new contact form submission is received.
+ *
+ * @param array $data The contact form data
+ * @return bool True if at least one email sent, false otherwise
+ */
+ public function sendSalesNotification(array $data): bool
+ {
+ $recipients = $_ENV['SALES_EMAILS'] ?? '';
+ if (empty($recipients)) {
+ return false;
+ }
+
+ $body = EmailHelper::renderTemplate(self::TEMPLATE_SALES_ALERT, $data);
+ $subject = 'New Contact Form Submission';
+
+ foreach (explode(',', $recipients) as $email) {
+ $trimmed = trim($email);
+ if (!empty($trimmed)) {
+ EmailHelper::send($trimmed, $subject, $body);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Sends a unified welcome email when a user both contacts us and subscribes to the newsletter.
+ *
+ * @param array $data Associative array containing contact form fields and metadata
+ * @return bool True on successful send, false otherwise
+ */
+ public function sendContactAndNewsletterWelcome(array $data): bool
+ {
+ $body = EmailHelper::renderTemplate(self::TEMPLATE_CONTACT_NEWSLETTER, $data);
+ $subject = 'Thanks for reaching out – and welcome!';
+
+ return EmailHelper::send($data['email'], $subject, $body);
+ }
+
+ /**
+ * Sends a system alert to configured admin recipients.
+ *
+ * @param string $context Description of the error context or origin
+ * @param string $errorMessage The error message or exception
+ * @param array|string $data Optional contextual data to include in the alert
+ * @return void
+ */
+ public function alertAdmins(string $context, string $errorMessage, $data = []): void
+ {
+ EmailHelper::alertAdmins($context, $errorMessage, $data);
+ }
+}
diff --git a/app/Services/NewsletterService.php b/app/Services/NewsletterService.php
index 4446bf7..952ad24 100644
--- a/app/Services/NewsletterService.php
+++ b/app/Services/NewsletterService.php
@@ -9,9 +9,9 @@
namespace WizdomNetworks\WizeWeb\Services;
-use WizdomNetworks\WizeWeb\Utils\Logger;
-use WizdomNetworks\WizeWeb\Utils\ErrorHandler;
-use WizdomNetworks\WizeWeb\Utils\Database;
+use WizdomNetworks\WizeWeb\Utilities\Logger;
+use WizdomNetworks\WizeWeb\Utilities\ErrorHandler;
+use WizdomNetworks\WizeWeb\Utilities\Database;
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception as MailException;
@@ -81,7 +81,7 @@ class NewsletterService
private static function sendVerificationEmail(string $email, string $code): bool
{
try {
- $verifyUrl = $_ENV['BASE_URL'] . "/verify?code=" . urlencode($code);
+ $verifyUrl = $_ENV['APP_URL'] . "/verify?code=" . urlencode($code);
$mail = new PHPMailer(true);
$mail->isSMTP();
diff --git a/app/Services/ResendVerificationService.php b/app/Services/ResendVerificationService.php
new file mode 100644
index 0000000..ccafbb6
--- /dev/null
+++ b/app/Services/ResendVerificationService.php
@@ -0,0 +1,110 @@
+emailService = new EmailService();
+ $this->verificationService = new VerificationService();
+ }
+
+ /**
+ * Attempts to resend a verification email for a given type and address.
+ * Performs rate limiting checks and logs the attempt if permitted.
+ * Generates and assigns a new verification code and triggers an email send.
+ *
+ * @param string $type Either 'contact' or 'newsletter'
+ * @param string $email Email address to resend to
+ * @return array ['success' => bool, 'message' => string] Outcome and message for user feedback
+ */
+ public function attemptResend(string $type, string $email): array
+ {
+ try {
+ $db = Database::getConnection();
+
+ // Rate limit: no more than 3 per day
+ $stmt = $db->prepare("SELECT COUNT(*) FROM verification_attempts WHERE email = ? AND type = ? AND attempted_at >= NOW() - INTERVAL 1 DAY");
+ $stmt->execute([$email, $type]);
+ if ((int)$stmt->fetchColumn() >= 3) {
+ return ['success' => false, 'message' => 'You have reached the daily resend limit. Please try again tomorrow.'];
+ }
+
+ // Rate limit: no more than 1 every 5 minutes
+ $stmt = $db->prepare("SELECT COUNT(*) FROM verification_attempts WHERE email = ? AND type = ? AND attempted_at >= NOW() - INTERVAL 5 MINUTE");
+ $stmt->execute([$email, $type]);
+ if ((int)$stmt->fetchColumn() > 0) {
+ return ['success' => false, 'message' => 'You must wait a few minutes before requesting another verification email.'];
+ }
+
+ // Log attempt
+ $stmt = $db->prepare("INSERT INTO verification_attempts (email, type, attempted_at, ip_address, user_agent) VALUES (?, ?, NOW(), ?, ?)");
+ $stmt->execute([
+ $email,
+ $type,
+ $_SERVER['REMOTE_ADDR'] ?? 'unknown',
+ $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
+ ]);
+
+ $code = $this->verificationService->generateCode();
+ $expiry = $this->verificationService->getExpirationTime();
+
+ if ($type === 'newsletter') {
+ $stmt = $db->prepare("SELECT id, is_verified FROM subscribers WHERE email = ?");
+ $stmt->execute([$email]);
+ $row = $stmt->fetch();
+
+ if (!$row || (int)$row['is_verified'] === 1) {
+ return ['success' => false, 'message' => 'Email is already verified or not found.'];
+ }
+
+ $this->verificationService->assignCodeToRecord('subscribers', $row['id'], $code, $expiry);
+ $this->emailService->sendVerificationEmail($email, $code, 'verify_newsletter');
+ } elseif ($type === 'contact') {
+ $stmt = $db->prepare("SELECT id, is_verified FROM contact_messages WHERE email = ? ORDER BY created_at DESC LIMIT 1");
+ $stmt->execute([$email]);
+ $row = $stmt->fetch();
+
+ if (!$row || (int)$row['is_verified'] === 1) {
+ return ['success' => false, 'message' => 'Email is already verified or not found.'];
+ }
+
+ $this->verificationService->assignCodeToRecord('contact_messages', $row['id'], $code, $expiry);
+ $this->emailService->sendVerificationEmail($email, $code, 'verify_contact');
+ } else {
+ return ['success' => false, 'message' => 'Invalid verification type specified.'];
+ }
+
+ return ['success' => true, 'message' => 'We just sent you a new verification link.'];
+ } catch (\Throwable $e) {
+ Logger::error("ResendVerificationService::attemptResend exception: " . $e->getMessage());
+ return ['success' => false, 'message' => 'An unexpected error occurred.'];
+ }
+ }
+}
diff --git a/app/Services/VerificationService.php b/app/Services/VerificationService.php
new file mode 100644
index 0000000..e3c803d
--- /dev/null
+++ b/app/Services/VerificationService.php
@@ -0,0 +1,104 @@
+format('Y-m-d H:i:s');
+ }
+
+ /**
+ * Assigns a verification code to a contact or subscriber record.
+ *
+ * @param string $table Table name (e.g., 'subscribers', 'contact_messages')
+ * @param int $id Record ID
+ * @param string $code Verification code
+ * @param string $expiresAt Expiration timestamp
+ * @return bool True on success, false on failure
+ */
+ public function assignCodeToRecord(string $table, int $id, string $code, string $expiresAt): bool
+ {
+ try {
+ $db = Database::getConnection();
+ $stmt = $db->prepare("UPDATE {$table} SET verification_code = ?, is_verified = 0, verification_expires_at = ? WHERE id = ?");
+ return $stmt->execute([$code, $expiresAt, $id]);
+ } catch (Exception $e) {
+ Logger::error("Failed to assign verification code to {$table} ID {$id}: " . $e->getMessage());
+ ErrorHandler::exception($e);
+ return false;
+ }
+ }
+
+ /**
+ * Deletes expired verification codes from a table.
+ *
+ * @param string $table Table name (e.g., 'subscribers', 'contact_messages')
+ * @return int Number of rows deleted
+ */
+ public function deleteExpiredCodes(string $table): int
+ {
+ try {
+ $db = Database::getConnection();
+ $stmt = $db->prepare("UPDATE {$table} SET verification_code = NULL WHERE verification_expires_at IS NOT NULL AND verification_expires_at < NOW()");
+ $stmt->execute();
+ return $stmt->rowCount();
+ } catch (Exception $e) {
+ Logger::error("Failed to clear expired codes in {$table}: " . $e->getMessage());
+ ErrorHandler::exception($e);
+ return 0;
+ }
+ }
+
+ /**
+ * Removes the verification code from a specific record.
+ *
+ * @param string $table Table name
+ * @param int $id Record ID
+ * @return bool
+ */
+ public function clearCode(string $table, int $id): bool
+ {
+ try {
+ $db = Database::getConnection();
+ $stmt = $db->prepare("UPDATE {$table} SET verification_code = NULL WHERE id = ?");
+ return $stmt->execute([$id]);
+ } catch (Exception $e) {
+ Logger::error("Failed to clear verification code for {$table} ID {$id}: " . $e->getMessage());
+ ErrorHandler::exception($e);
+ return false;
+ }
+ }
+}
diff --git a/app/Utilities/EmailHelper.php b/app/Utilities/EmailHelper.php
index 7ec3b39..62f4a3b 100644
--- a/app/Utilities/EmailHelper.php
+++ b/app/Utilities/EmailHelper.php
@@ -1,9 +1,9 @@
isSMTP();
@@ -48,6 +54,12 @@ class EmailHelper
];
}
+ /**
+ * Parses a comma-separated list of emails and returns an array of valid addresses.
+ *
+ * @param string $rawList
+ * @return array
+ */
private static function parseRecipients(string $rawList): array
{
$emails = explode(',', $rawList);
@@ -63,173 +75,83 @@ class EmailHelper
return $validEmails;
}
+ /**
+ * Sends a basic HTML email.
+ *
+ * @param string $to
+ * @param string $subject
+ * @param string $body
+ * @return bool
+ */
public static function send(string $to, string $subject, string $body): bool
-{
- try {
- $mail = self::getMailer();
- $mail->addAddress($to);
- $mail->Subject = $subject;
- $mail->Body = $body;
- $mail->isHTML(true);
-
- return $mail->send();
- } catch (\Throwable $e) {
- Logger::error("Email send failed to $to: " . $e->getMessage());
- return false;
- }
-}
-
-
- private static function buildContactHtmlBody(array $data): string
- {
- return "
- Name: {$data['first_name']} {$data['last_name']}
- Email: {$data['email']}
- Phone: {$data['phone']}
- Subject: {$data['subject']}
- Message:
-
{$data['message']}
- ";
- }
-
- private static function buildErrorReportHtml(string $context, string $errorMessage, $data = []): string
- {
- if (is_string($data)) {
- $decoded = json_decode($data, true);
- $data = is_array($decoded) ? $decoded : ['raw_data' => $data];
- }
-
- $body = "
- Context: {$context}{$errorMessage}
- ";
-
- if (!empty($data)) {
- $body .= "New contact submission received on {$submittedAt}.
-{$data['message']}IP: {$data['ip_address']}
User-Agent: {$data['user_agent']}
Hi {$data['first_name']},
-Thank you for contacting Wizdom Networks. This message confirms that we received your inquiry on {$submittedAt}.
-{$data['message']}We’ll be in touch shortly. If it’s urgent, call us at 416-USE-WISE.
-IP: {$data['ip_address']}
User-Agent: {$data['user_agent']}
Dear {{name}},
- -Thank you for reaching out to HelpDesk+. We’ve received your message and will get back to you shortly. Here’s what you submitted:
- --- -Message: {{message}}
-
If you have any further questions or concerns, feel free to reply to this email.
- -Best regards,
The HelpDesk+ Team
New Contact Form Submission Received
+ +Name: = htmlspecialchars($first_name . ' ' . $last_name) ?>
+Email: = htmlspecialchars($email) ?>
+IP Address: = htmlspecialchars($ip_address) ?>
+User Agent: = htmlspecialchars($user_agent) ?>
Message:
= nl2br(htmlspecialchars($message)) ?>
We've received your message and appreciate you taking the time to contact Wizdom Networks. A member of our team will follow up shortly.
+ +Here’s what you submitted:
+= htmlspecialchars($message) ?>
You also signed up for our newsletter — welcome aboard! We’ll occasionally send you updates, tips, and insights straight from our team.
+ +What to expect:
+Thanks for subscribing to the Wizdom Networks newsletter. We're glad to have you with us.
+ +You can expect expert tips, behind-the-scenes insights, and smart strategies that help make technology work for your business—not the other way around.
+ +If you ever want to update your preferences or tell us more about yourself, just click below.
+ + Update Preferences + + +Hello = htmlspecialchars($first_name ?? 'there') ?>,
+ +You recently requested to re-send your email verification link. Please click below to verify your email address:
+ + + +If you didn't request this, no further action is needed.
+ +— The Wizdom Networks Team
+ + + + diff --git a/resources/views/emails/sales_lead_alert.php b/resources/views/emails/sales_lead_alert.php new file mode 100644 index 0000000..76be271 --- /dev/null +++ b/resources/views/emails/sales_lead_alert.php @@ -0,0 +1,21 @@ + + + +New Sales Lead Submitted
+ +Name: = htmlspecialchars($first_name . ' ' . $last_name) ?>
+Email: = htmlspecialchars($email) ?>
+Phone: = htmlspecialchars($phone) ?>
Message:
= nl2br(htmlspecialchars($message)) ?>
Error Message:
+= htmlspecialchars($errorMessage ?? 'N/A') ?>+ + +
Contextual Data:
+Hello = htmlspecialchars($first_name ?? 'there') ?>,
+ +Your email has been successfully verified. Thank you for connecting with Wizdom Networks.
+ +Here's a summary of your submission:
+We’ll be in touch soon if your message requires a response. In the meantime, feel free to reach out at 416-USE-WISE if you need immediate assistance.
+ +— The Wizdom Networks Team
+ + + + diff --git a/resources/views/emails/verify_contact.php b/resources/views/emails/verify_contact.php new file mode 100644 index 0000000..38a5cd5 --- /dev/null +++ b/resources/views/emails/verify_contact.php @@ -0,0 +1,41 @@ + + + + + + + +Thanks for reaching out to Wizdom Networks. To ensure we received your inquiry and can respond to you promptly, please confirm your email address below:
+ + + + +Hello = htmlspecialchars($first_name ?? 'there') ?>,
+ +Thank you for connecting with Wizdom Networks. Please confirm your email by clicking the link below:
+ + + +This link will expire in 48 hours.
+ +If you didn’t request this, please ignore this email.
+ +— The Wizdom Networks Team
+ + + + diff --git a/resources/views/emails/verify_newsletter.php b/resources/views/emails/verify_newsletter.php new file mode 100644 index 0000000..c83ef5b --- /dev/null +++ b/resources/views/emails/verify_newsletter.php @@ -0,0 +1,43 @@ + + + + + + + +You're almost done! Please confirm your subscription to the Wizdom Networks newsletter by verifying your email address.
+ + + + +