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:
+