+
- {disclaimer.label}
+ {disclaimer.label}*
+
)
diff --git a/src/email-templates/README.md b/src/email-templates/README.md
new file mode 100644
index 0000000..78ced15
--- /dev/null
+++ b/src/email-templates/README.md
@@ -0,0 +1,72 @@
+# Email Handling System
+
+This directory contains the email templates and utilities for the contact form email handling system.
+
+## Features
+
+- **Secure SMTP Authentication**: Uses environment variables for credentials
+- **Email Templates**: Customizable templates for both user confirmation and admin notification emails
+- **Rate Limiting**: Prevents abuse by limiting the number of submissions per IP address
+- **CSRF Protection**: Prevents cross-site request forgery attacks
+- **Email Validation**: Ensures valid email addresses are provided
+- **Spam Prevention**: Multiple checks to detect and block spam submissions
+- **Error Handling**: Proper error handling with client feedback
+- **Logging**: Comprehensive logging of email sending attempts
+
+## Configuration
+
+The email system is configured using environment variables in the `.env` file:
+
+```
+# SMTP Configuration
+SMTP_HOST=smtp.example.com
+SMTP_PORT=587
+SMTP_USER=your-email@example.com
+SMTP_PASS=your-password
+
+# Email Settings
+ADMIN_EMAIL=admin@example.com
+WEBSITE_NAME=Your Website Name
+
+# Environment
+NODE_ENV=development
+```
+
+In development mode, emails are logged to the console instead of being sent. Set `NODE_ENV=production` to send actual emails.
+
+## Files
+
+- `admin-notification.ts`: Template for emails sent to the admin
+- `user-confirmation.ts`: Template for confirmation emails sent to users
+- `../utils/email-handler.ts`: Core email handling functionality
+
+## How It Works
+
+1. When a user submits the contact form, the client-side JavaScript validates the form and sends it to the `/api/contact` endpoint.
+2. The endpoint validates the form data, checks for CSRF token validity, and performs rate limiting and spam detection.
+3. If all checks pass, two emails are sent:
+ - A notification email to the admin with the form data
+ - A confirmation email to the user acknowledging receipt of their message
+4. The system logs all email sending attempts for monitoring and debugging.
+
+## Development vs. Production
+
+- In development mode (`NODE_ENV=development`), emails are logged to the console instead of being sent.
+- In production mode (`NODE_ENV=production`), emails are sent using the configured SMTP server.
+
+## Security Considerations
+
+- SMTP credentials are stored in environment variables, not in the code
+- CSRF tokens are used to prevent cross-site request forgery
+- Rate limiting prevents abuse of the contact form
+- Form data is validated both on the client and server side
+- Spam detection helps prevent unwanted messages
+
+## Testing
+
+To test the email system:
+
+1. Configure the `.env` file with your SMTP settings
+2. Submit the contact form on the website
+3. Check the logs for email sending attempts
+4. In production mode, check your inbox for the actual emails
\ No newline at end of file
diff --git a/src/email-templates/admin-notification.ts b/src/email-templates/admin-notification.ts
new file mode 100644
index 0000000..7842424
--- /dev/null
+++ b/src/email-templates/admin-notification.ts
@@ -0,0 +1,111 @@
+interface AdminNotificationProps {
+ name: string;
+ email: string;
+ message: string;
+ submittedAt: string;
+ ipAddress?: string;
+ userAgent?: string;
+}
+
+export function getAdminNotificationSubject(): string {
+ return 'New Contact Form Submission from bergsma.it';
+}
+
+export function getAdminNotificationHtml(props: AdminNotificationProps): string {
+ const { name, email, message, submittedAt, ipAddress, userAgent } = props;
+
+ return `
+
+
+
+
+
+
New Contact Form Submission
+
+
+
+
+
+
From: ${name} (${email})
+
Submitted on: ${submittedAt}
+
+
+
Message:
+
${message.replace(/\n/g, ' ')}
+
+
+
+
+
+
+
+ `;
+}
+
+export function getAdminNotificationText(props: AdminNotificationProps): string {
+ const { name, email, message, submittedAt, ipAddress, userAgent } = props;
+
+ return `
+New Contact Form Submission
+
+From: ${name} (${email})
+Submitted on: ${submittedAt}
+
+Message:
+${message}
+
+Additional Information:
+IP Address: ${ipAddress || 'Not available'}
+User Agent: ${userAgent || 'Not available'}
+
+This is an automated email from your website contact form.
+ `;
+}
\ No newline at end of file
diff --git a/src/email-templates/user-confirmation.ts b/src/email-templates/user-confirmation.ts
new file mode 100644
index 0000000..924b38d
--- /dev/null
+++ b/src/email-templates/user-confirmation.ts
@@ -0,0 +1,120 @@
+interface UserConfirmationProps {
+ name: string;
+ email: string;
+ message: string;
+ submittedAt: string;
+ websiteName?: string;
+ contactEmail?: string;
+}
+
+export function getUserConfirmationSubject(websiteName: string = 'bergsma.it'): string {
+ return `Thank you for contacting ${websiteName}`;
+}
+
+export function getUserConfirmationHtml(props: UserConfirmationProps): string {
+ const {
+ name,
+ message,
+ submittedAt,
+ websiteName = 'bergsma.it',
+ contactEmail = 'richard@bergsma.it'
+ } = props;
+
+ return `
+
+
+
+
+
+
Thank you for contacting us
+
+
+
+
+
+
Dear ${name},
+
+
Thank you for reaching out to us. We have received your message and will get back to you as soon as possible.
+
+
+
Your message (submitted on ${submittedAt}):
+
${message.replace(/\n/g, ' ')}
+
+
+
If you have any additional questions or information to provide, please feel free to reply to this email.
+
+
Best regards,
+ The ${websiteName} Team
+
+
+
+
+ `;
+}
+
+export function getUserConfirmationText(props: UserConfirmationProps): string {
+ const {
+ name,
+ message,
+ submittedAt,
+ websiteName = 'bergsma.it',
+ contactEmail = 'richard@bergsma.it'
+ } = props;
+
+ return `
+Thank you for contacting ${websiteName}
+
+Dear ${name},
+
+Thank you for reaching out to us. We have received your message and will get back to you as soon as possible.
+
+Your message (submitted on ${submittedAt}):
+${message}
+
+If you have any additional questions or information to provide, please feel free to reply to this email.
+
+Best regards,
+The ${websiteName} Team
+
+If you did not submit this contact form, please disregard this email or contact us at ${contactEmail}.
+ `;
+}
\ No newline at end of file
diff --git a/src/pages/api/contact.ts b/src/pages/api/contact.ts
new file mode 100644
index 0000000..fb5184a
--- /dev/null
+++ b/src/pages/api/contact.ts
@@ -0,0 +1,259 @@
+import type { APIRoute } from 'astro';
+import {
+ generateCsrfToken,
+ validateCsrfToken,
+ checkRateLimit,
+ sendAdminNotification,
+ sendUserConfirmation
+} from '../../utils/email-handler';
+
+// Enhanced email validation with more comprehensive regex
+const isValidEmail = (email: string): boolean => {
+ // Simpler regex to avoid escape character issues
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ return emailRegex.test(email);
+};
+
+// Enhanced spam protection - check for common spam patterns and characteristics
+const isSpam = (content: string, name: string, email: string): boolean => {
+ // Convert to lowercase for case-insensitive matching
+ const lowerContent = content.toLowerCase();
+ const lowerName = name.toLowerCase();
+ const lowerEmail = email.toLowerCase();
+
+ // Common spam keywords
+ const spamPatterns = [
+ 'viagra', 'cialis', 'casino', 'lottery', 'prize', 'winner',
+ 'free money', 'buy now', 'click here', 'earn money', 'make money',
+ 'investment opportunity', 'bitcoin', 'cryptocurrency', 'forex',
+ 'weight loss', 'diet pill', 'enlargement', 'cheap medication'
+ ];
+
+ // Check for spam keywords in content
+ if (spamPatterns.some(pattern => lowerContent.includes(pattern))) {
+ return true;
+ }
+
+ // Check for spam keywords in name or email
+ if (spamPatterns.some(pattern => lowerName.includes(pattern) || lowerEmail.includes(pattern))) {
+ return true;
+ }
+
+ // Check for excessive capitalization (shouting)
+ const uppercaseRatio = (content.match(/[A-Z]/g) || []).length / content.length;
+ if (uppercaseRatio > 0.5 && content.length > 20) {
+ return true;
+ }
+
+ // Check for excessive special characters
+ const specialChars = "!@#$%^&*()_+-=[]{}\\|;:'\",.<>/?";
+ let specialCharCount = 0;
+
+ for (let i = 0; i < content.length; i++) {
+ if (specialChars.includes(content[i])) {
+ specialCharCount++;
+ }
+ }
+
+ const specialCharRatio = specialCharCount / content.length;
+ if (specialCharRatio > 0.3 && content.length > 20) {
+ return true;
+ }
+
+ // Check for excessive URLs - count http:// and https:// occurrences
+ const urlCount = content.split('http').length - 1;
+ if (urlCount > 2) {
+ return true;
+ }
+
+ return false;
+};
+
+// GET handler for CSRF token generation and API testing
+export const GET: APIRoute = async ({ request }) => {
+ const url = new URL(request.url);
+ const csrfRequested = url.searchParams.get('csrf') === 'true';
+
+ if (csrfRequested) {
+ // Generate and return a CSRF token
+ const csrfToken = generateCsrfToken();
+
+ return new Response(
+ JSON.stringify({
+ csrfToken
+ }),
+ {
+ status: 200,
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ }
+ );
+ }
+
+ // Default response for GET requests
+ return new Response(
+ JSON.stringify({
+ message: 'Contact API endpoint is working. Please use POST to submit the form.'
+ }),
+ {
+ status: 200,
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ }
+ );
+};
+
+export const POST: APIRoute = async ({ request, clientAddress }) => {
+ try {
+ console.log('Contact form submission received');
+
+ // Get client IP address for rate limiting
+ const ipAddress = clientAddress || '0.0.0.0';
+ console.log('Client IP:', ipAddress);
+
+ // Check rate limit
+ const rateLimitCheck = await checkRateLimit(ipAddress);
+ console.log('Rate limit check:', rateLimitCheck);
+
+ if (rateLimitCheck.limited) {
+ console.log('Rate limit exceeded');
+ return new Response(
+ JSON.stringify({
+ success: false,
+ errors: {
+ rateLimit: rateLimitCheck.message
+ }
+ }),
+ {
+ status: 429,
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Retry-After': '3600'
+ }
+ }
+ );
+ }
+
+ // Get form data
+ const formData = await request.formData();
+ console.log('Form data received');
+
+ // Log all form data keys
+ console.log('Form data keys:', [...formData.keys()]);
+
+ const name = formData.get('name')?.toString() || '';
+ const email = formData.get('email')?.toString() || '';
+ const message = formData.get('message')?.toString() || '';
+ const disclaimer = formData.get('disclaimer')?.toString() === 'on';
+ const csrfToken = formData.get('csrf_token')?.toString() || '';
+
+ console.log('Form data values:', { name, email, messageLength: message.length, disclaimer, csrfToken: csrfToken ? 'present' : 'missing' });
+
+ // Get user agent for logging and spam detection
+ const userAgent = request.headers.get('user-agent') || 'Unknown';
+
+ // Validate form data
+ const errors: Record
= {};
+
+ // Validate CSRF token
+ if (!validateCsrfToken(csrfToken)) {
+ errors.csrf = 'Invalid or expired security token. Please refresh the page and try again.';
+ }
+
+ if (!name || name.length < 2) {
+ errors.name = 'Name is required and must be at least 2 characters';
+ }
+
+ if (!email || !isValidEmail(email)) {
+ errors.email = 'A valid email address is required';
+ }
+
+ if (!message || message.length < 10) {
+ errors.message = 'Message is required and must be at least 10 characters';
+ }
+
+ if (!disclaimer) {
+ errors.disclaimer = 'You must agree to the disclaimer';
+ }
+
+ // Check for spam
+ if (isSpam(message, name, email)) {
+ errors.spam = 'Your message was flagged as potential spam. Please revise your message and try again.';
+ }
+
+ // If there are validation errors, return them
+ if (Object.keys(errors).length > 0) {
+ return new Response(
+ JSON.stringify({
+ success: false,
+ errors
+ }),
+ {
+ status: 400,
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ }
+ );
+ }
+
+ // Send emails
+ console.log('Attempting to send admin notification email');
+ const adminEmailSent = await sendAdminNotification(name, email, message, ipAddress, userAgent);
+ console.log('Admin email sent result:', adminEmailSent);
+
+ console.log('Attempting to send user confirmation email');
+ const userEmailSent = await sendUserConfirmation(name, email, message);
+ console.log('User email sent result:', userEmailSent);
+
+ // Check if emails were sent successfully
+ if (!adminEmailSent || !userEmailSent) {
+ console.error('Failed to send one or more emails:', { adminEmailSent, userEmailSent });
+
+ return new Response(
+ JSON.stringify({
+ success: false,
+ message: 'There was an issue sending your message. Please try again later.'
+ }),
+ {
+ status: 500,
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ }
+ );
+ }
+
+ // Return success response
+ return new Response(
+ JSON.stringify({
+ success: true,
+ message: 'Your message has been sent successfully. We will get back to you soon!'
+ }),
+ {
+ status: 200,
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ }
+ );
+
+ } catch (error) {
+ console.error('Error processing contact form:', error);
+
+ return new Response(
+ JSON.stringify({
+ success: false,
+ message: 'An error occurred while processing your request. Please try again later.'
+ }),
+ {
+ status: 500,
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ }
+ );
+ }
+};
\ No newline at end of file
diff --git a/src/pages/contact.astro b/src/pages/contact.astro
index 70157e0..42f1a0e 100644
--- a/src/pages/contact.astro
+++ b/src/pages/contact.astro
@@ -37,7 +37,7 @@ const metadata = {
label:
'By submitting this contact form, you acknowledge and agree to the collection of your personal information.',
}}
- description="Our support team typically responds within 24 business hours."
+ description={`Our support team typically responds within 24 business hours. Emails will be sent to: ${process.env.ADMIN_EMAIL || 'richard@bergsma.it'}`}
/>
diff --git a/src/test-email.ts b/src/test-email.ts
new file mode 100644
index 0000000..4963695
--- /dev/null
+++ b/src/test-email.ts
@@ -0,0 +1,31 @@
+import { testEmailConfiguration, sendAdminNotification } from '../utils/email-handler';
+import 'dotenv/config';
+
+async function runEmailTest() {
+ console.log('Starting email configuration test...');
+
+ // Test the SMTP connection
+ const configTest = await testEmailConfiguration();
+ console.log(`Configuration test result: ${configTest ? 'SUCCESS' : 'FAILED'}`);
+
+ if (configTest) {
+ // Try sending a test email
+ console.log('Attempting to send a test email...');
+ const emailResult = await sendAdminNotification(
+ 'Test User',
+ 'test@example.com',
+ 'This is a test message sent at ' + new Date().toISOString(),
+ '127.0.0.1',
+ 'Email Test Script'
+ );
+
+ console.log(`Test email result: ${emailResult ? 'SENT' : 'FAILED'}`);
+ }
+
+ console.log('Email test completed');
+}
+
+runEmailTest().catch(error => {
+ console.error('Error running email test:', error);
+ process.exit(1);
+});
\ No newline at end of file
diff --git a/src/types.d.ts b/src/types.d.ts
index d15356e..c5c76c9 100644
--- a/src/types.d.ts
+++ b/src/types.d.ts
@@ -169,6 +169,7 @@ export interface Input {
label?: string;
autocomplete?: string;
placeholder?: string;
+ required?: boolean;
}
export interface Textarea {
diff --git a/src/utils/email-handler.ts b/src/utils/email-handler.ts
new file mode 100644
index 0000000..fc03d47
--- /dev/null
+++ b/src/utils/email-handler.ts
@@ -0,0 +1,426 @@
+import nodemailer from 'nodemailer';
+import { RateLimiterMemory } from 'rate-limiter-flexible';
+import { createHash } from 'crypto';
+import { getAdminNotificationHtml, getAdminNotificationText, getAdminNotificationSubject } from '../email-templates/admin-notification';
+import { getUserConfirmationHtml, getUserConfirmationText, getUserConfirmationSubject } from '../email-templates/user-confirmation';
+import 'dotenv/config';
+
+// Environment variables
+const {
+ SMTP_HOST = '',
+ SMTP_PORT = '587',
+ SMTP_USER = '',
+ SMTP_PASS = '',
+ ADMIN_EMAIL = 'richard@bergsma.it',
+ WEBSITE_NAME = 'bergsma.it',
+ NODE_ENV = 'development'
+} = process.env;
+
+// Email configuration
+const isProduction = NODE_ENV === 'production';
+
+// Create a transporter for sending emails
+let transporter: nodemailer.Transporter;
+
+// Initialize the transporter based on environment
+function initializeTransporter() {
+ if (isProduction && SMTP_HOST && SMTP_USER && SMTP_PASS) {
+ // Production: Use SMTP server
+
+ // ProtonMail specific configuration
+ // ProtonMail often requires using their Bridge application for SMTP
+ const isProtonMail = SMTP_HOST.includes('protonmail');
+
+ transporter = nodemailer.createTransport({
+ host: SMTP_HOST,
+ port: parseInt(SMTP_PORT, 10),
+ secure: parseInt(SMTP_PORT, 10) === 465, // true for 465, false for other ports
+ auth: {
+ user: SMTP_USER,
+ pass: SMTP_PASS,
+ },
+ // ProtonMail specific settings
+ ...(isProtonMail && {
+ tls: {
+ // Do not fail on invalid certs
+ rejectUnauthorized: false,
+ // Specific ciphers for ProtonMail
+ ciphers: 'SSLv3'
+ }
+ }),
+ debug: true, // Enable debug output for troubleshooting
+ });
+
+ // Verify SMTP connection configuration
+ transporter.verify(function(error, _success) {
+ if (error) {
+ console.error('SMTP connection error:', error);
+ } else {
+ console.log('SMTP server is ready to take our messages');
+ }
+ });
+ } else {
+ // Development: Log emails to console
+ transporter = nodemailer.createTransport({
+ streamTransport: true,
+ newline: 'unix',
+ buffer: true,
+ });
+ }
+}
+
+// Rate limiter configuration
+const rateLimiter = new RateLimiterMemory({
+ points: 5, // 5 attempts
+ duration: 3600, // per hour
+});
+
+// CSRF protection
+const csrfTokens = new Map();
+
+// Generate a CSRF token
+export function generateCsrfToken(): string {
+ const token = createHash('sha256')
+ .update(Math.random().toString())
+ .digest('hex');
+
+ // Token expires after 1 hour
+ const expires = new Date();
+ expires.setHours(expires.getHours() + 1);
+
+ csrfTokens.set(token, { token, expires });
+
+ // Clean up expired tokens
+ for (const [key, value] of csrfTokens.entries()) {
+ if (value.expires < new Date()) {
+ csrfTokens.delete(key);
+ }
+ }
+
+ return token;
+}
+
+// Validate a CSRF token
+export function validateCsrfToken(token: string): boolean {
+ const storedToken = csrfTokens.get(token);
+
+ if (!storedToken) {
+ return false;
+ }
+
+ if (storedToken.expires < new Date()) {
+ csrfTokens.delete(token);
+ return false;
+ }
+
+ return true;
+}
+
+// Check rate limit for an IP address
+export async function checkRateLimit(ipAddress: string): Promise<{ limited: boolean; message?: string }> {
+ try {
+ await rateLimiter.consume(ipAddress);
+ return { limited: false };
+ } catch (error) {
+ if (error instanceof Error) {
+ return {
+ limited: true,
+ message: 'Too many requests. Please try again later.',
+ };
+ }
+ // RateLimiterRes with msBeforeNext property
+ interface RateLimiterResponse {
+ msBeforeNext: number;
+ }
+ const resetTime = Math.ceil((error as RateLimiterResponse).msBeforeNext / 1000 / 60);
+ return {
+ limited: true,
+ message: `Too many requests. Please try again in ${resetTime} minutes.`,
+ };
+ }
+}
+
+// Log email sending attempts
+export function logEmailAttempt(
+ success: boolean,
+ recipient: string,
+ subject: string,
+ error?: Error
+): void {
+ const timestamp = new Date().toISOString();
+ const status = success ? 'SUCCESS' : 'FAILURE';
+ const errorMessage = error ? `: ${error.message}` : '';
+
+ const logMessage = `[${timestamp}] [EMAIL ${status}] To: ${recipient}, Subject: ${subject}${errorMessage}`;
+
+ if (isProduction) {
+ // In production, you might want to log to a file or a logging service
+ console.log(logMessage);
+ } else {
+ // In development, log to console
+ console.log(logMessage);
+ }
+}
+
+// Send an email
+export async function sendEmail(
+ to: string,
+ subject: string,
+ html: string,
+ text: string
+): Promise {
+ console.log(`Attempting to send email to: ${to}`);
+ console.log(`Subject: ${subject}`);
+
+ // Initialize transporter if not already done
+ if (!transporter) {
+ console.log('Initializing transporter');
+ initializeTransporter();
+ }
+
+ try {
+ const mailOptions = {
+ from: `"${WEBSITE_NAME}" <${ADMIN_EMAIL}>`,
+ to,
+ subject,
+ html,
+ text,
+ };
+
+ console.log('Mail options:', {
+ from: `"${WEBSITE_NAME}" <${ADMIN_EMAIL}>`,
+ to,
+ subject,
+ textLength: text.length,
+ htmlLength: html.length
+ });
+
+ console.log('Sending email via transporter');
+ const info = await transporter.sendMail(mailOptions);
+ console.log('Email sent, info:', info.messageId);
+
+ if (!isProduction) {
+ // In development, log the email content
+ console.log('Email sent (development mode):');
+ console.log('To:', to);
+ console.log('Subject:', subject);
+ console.log('Preview:', nodemailer.getTestMessageUrl(info));
+
+ if (info.message) {
+ // For stream transport, we can get the message content
+ console.log('Message:', info.message.toString());
+ }
+ }
+
+ logEmailAttempt(true, to, subject);
+ return true;
+ } catch (error) {
+ logEmailAttempt(false, to, subject, error as Error);
+
+ // Enhanced error logging for SMTP issues
+ if (isProduction) {
+ console.error('Error sending email:', error);
+
+ // Log more detailed information for SMTP errors
+ if (error instanceof Error) {
+ console.error('Error name:', error.name);
+ console.error('Error message:', error.message);
+
+ // Log additional details for specific error types
+ if (error.name === 'Error' && error.message.includes('ECONNREFUSED')) {
+ console.error('SMTP Connection Refused: Check if the SMTP server is reachable and the port is correct');
+ } else if (error.message.includes('Invalid login')) {
+ console.error('SMTP Authentication Failed: Check your username and password');
+ } else if (error.message.includes('certificate')) {
+ console.error('SSL/TLS Certificate Error: There might be an issue with the server certificate');
+ }
+ }
+ }
+
+ return false;
+ }
+}
+
+// Send admin notification email
+export async function sendAdminNotification(
+ name: string,
+ email: string,
+ message: string,
+ ipAddress?: string,
+ userAgent?: string
+): Promise {
+ console.log('sendAdminNotification called with:', { name, email, messageLength: message.length });
+
+ // Validate inputs
+ if (!name || name.trim() === '') {
+ console.error('Cannot send admin notification: name is empty');
+ return false;
+ }
+
+ if (!email || email.trim() === '') {
+ console.error('Cannot send admin notification: email is empty');
+ return false;
+ }
+
+ if (!message || message.trim() === '') {
+ console.error('Cannot send admin notification: message is empty');
+ return false;
+ }
+
+ if (!ADMIN_EMAIL || ADMIN_EMAIL.trim() === '') {
+ console.error('Cannot send admin notification: ADMIN_EMAIL is not configured');
+ return false;
+ }
+
+ const submittedAt = new Date().toLocaleString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+
+ const props = {
+ name,
+ email,
+ message,
+ submittedAt,
+ ipAddress,
+ userAgent,
+ };
+
+ console.log('Generating admin notification email content');
+ const subject = getAdminNotificationSubject();
+ const html = getAdminNotificationHtml(props);
+ const text = getAdminNotificationText(props);
+
+ console.log(`Sending admin notification to: ${ADMIN_EMAIL}`);
+ console.log('Admin email environment variables:', {
+ SMTP_HOST,
+ SMTP_PORT,
+ SMTP_USER,
+ ADMIN_EMAIL,
+ WEBSITE_NAME,
+ NODE_ENV,
+ isProduction
+ });
+
+ // Add a backup email address to ensure delivery
+ const recipients = ADMIN_EMAIL;
+ // Uncomment and modify the line below to add a backup email address
+ // const recipients = `${ADMIN_EMAIL}, your-backup-email@example.com`;
+
+ return sendEmail(recipients, subject, html, text);
+}
+
+// Send user confirmation email
+export async function sendUserConfirmation(
+ name: string,
+ email: string,
+ message: string
+): Promise {
+ console.log('sendUserConfirmation called with:', { name, email, messageLength: message.length });
+
+ if (!email || email.trim() === '') {
+ console.error('Cannot send user confirmation: email is empty');
+ return false;
+ }
+
+ const submittedAt = new Date().toLocaleString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+
+ const props = {
+ name,
+ email,
+ message,
+ submittedAt,
+ websiteName: WEBSITE_NAME,
+ contactEmail: ADMIN_EMAIL,
+ };
+
+ console.log('Generating user confirmation email content');
+ const subject = getUserConfirmationSubject(WEBSITE_NAME);
+ const html = getUserConfirmationHtml(props);
+ const text = getUserConfirmationText(props);
+
+ console.log(`Sending user confirmation to: ${email}`);
+ return sendEmail(email, subject, html, text);
+}
+
+// Initialize the email system
+export function initializeEmailSystem(): void {
+ initializeTransporter();
+
+ // Log initialization
+ console.log(`Email system initialized in ${isProduction ? 'production' : 'development'} mode`);
+ if (!isProduction) {
+ console.log('Emails will be logged to console instead of being sent');
+ }
+}
+
+// Initialize on import
+initializeEmailSystem();
+
+// Test email function to verify configuration
+export async function testEmailConfiguration(): Promise {
+ if (!isProduction) {
+ console.log('Email testing skipped in development mode');
+ return true;
+ }
+
+ try {
+ // Initialize transporter if not already done
+ if (!transporter) {
+ initializeTransporter();
+ }
+
+ console.log('Testing email configuration...');
+ console.log(`SMTP Host: ${SMTP_HOST}`);
+ console.log(`SMTP Port: ${SMTP_PORT}`);
+ console.log(`SMTP User: ${SMTP_USER}`);
+ console.log(`From Email: ${ADMIN_EMAIL}`);
+
+ // Verify connection to SMTP server
+ const connectionResult = await new Promise((resolve) => {
+ transporter.verify(function(error, _success) {
+ if (error) {
+ console.error('SMTP connection test failed:', error);
+ resolve(false);
+ } else {
+ console.log('SMTP connection successful');
+ resolve(true);
+ }
+ });
+ });
+
+ if (!connectionResult) {
+ return false;
+ }
+
+ console.log('Email configuration test completed successfully');
+ return true;
+ } catch (error) {
+ console.error('Error testing email configuration:', error);
+ return false;
+ }
+}
+
+// Run a test of the email configuration
+if (isProduction) {
+ testEmailConfiguration().then(success => {
+ if (success) {
+ console.log('Email system is properly configured');
+ } else {
+ console.error('Email system configuration test failed');
+ console.log('Note: If you continue to have issues with ProtonMail SMTP:');
+ console.log('1. Ensure ProtonMail Bridge is installed and running if sending from a desktop/server');
+ console.log('2. Verify you\'re using an app-specific password, not your main account password');
+ console.log('3. Check if your server allows outgoing connections on the SMTP port');
+ }
+ });
+}
\ No newline at end of file
diff --git a/test-contact-curl.sh b/test-contact-curl.sh
new file mode 100755
index 0000000..70ef842
--- /dev/null
+++ b/test-contact-curl.sh
@@ -0,0 +1,50 @@
+#!/bin/bash
+
+# Test script for the contact form API using curl
+# This script simulates a form submission to the contact form API
+
+API_URL="http://localhost:4321/api/contact"
+ADMIN_EMAIL="richard@bergsma.it"
+
+echo "Starting contact form test with curl..."
+echo "API URL: $API_URL"
+
+# Step 1: Get CSRF token
+echo "Getting CSRF token..."
+CSRF_RESPONSE=$(curl -s "$API_URL?csrf=true")
+echo "CSRF Response: $CSRF_RESPONSE"
+
+# Extract CSRF token
+CSRF_TOKEN=$(echo $CSRF_RESPONSE | grep -o '"csrfToken":"[^"]*"' | cut -d'"' -f4)
+echo "CSRF Token: $CSRF_TOKEN"
+
+if [ -z "$CSRF_TOKEN" ]; then
+ echo "Failed to get CSRF token. Aborting test."
+ exit 1
+fi
+
+# Step 2: Submit the form
+echo "Submitting form..."
+FORM_RESPONSE=$(curl -s -X POST "$API_URL" \
+ -H "Content-Type: application/x-www-form-urlencoded" \
+ -H "Accept: application/json" \
+ -H "User-Agent: test-contact-curl-script" \
+ --data-urlencode "name=Test User" \
+ --data-urlencode "email=$ADMIN_EMAIL" \
+ --data-urlencode "message=This is a test message from the test-contact-curl.sh script. $(date)" \
+ --data-urlencode "disclaimer=on" \
+ --data-urlencode "csrf_token=$CSRF_TOKEN" \
+ --data-urlencode "timestamp=$(date +%s)")
+
+echo "Form submission response: $FORM_RESPONSE"
+
+# Check if submission was successful
+if echo "$FORM_RESPONSE" | grep -q '"success":true'; then
+ echo "Form submission successful!"
+else
+ echo "Form submission failed."
+ echo "Response: $FORM_RESPONSE"
+ exit 1
+fi
+
+echo "Test completed successfully."
\ No newline at end of file
diff --git a/test-contact-form.cjs b/test-contact-form.cjs
new file mode 100644
index 0000000..1a53c77
--- /dev/null
+++ b/test-contact-form.cjs
@@ -0,0 +1,133 @@
+/**
+ * Test script for the contact form API
+ * This script simulates a form submission to the contact form API
+ */
+
+const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
+const FormData = require('form-data');
+require('dotenv').config();
+
+// URL of the contact form API
+const API_URL = 'http://localhost:4321/api/contact';
+
+// Function to get a CSRF token
+async function getCsrfToken() {
+ try {
+ console.log(`Fetching CSRF token from ${API_URL}?csrf=true`);
+ const response = await fetch(`${API_URL}?csrf=true`);
+ console.log('CSRF response status:', response.status);
+
+ if (!response.ok) {
+ console.error('CSRF request failed:', response.statusText);
+ return null;
+ }
+
+ const text = await response.text();
+ console.log('CSRF response text:', text);
+
+ try {
+ const data = JSON.parse(text);
+ console.log('CSRF token data:', data);
+ return data.csrfToken;
+ } catch (parseError) {
+ console.error('Error parsing CSRF response:', parseError);
+ console.error('Response was not valid JSON:', text);
+ return null;
+ }
+ } catch (error) {
+ console.error('Error getting CSRF token:', error);
+ return null;
+ }
+}
+
+// Function to submit the form
+async function submitForm(csrfToken) {
+ console.log('Creating form data for submission');
+
+ // Create form data
+ const formData = new FormData();
+ const testEmail = process.env.ADMIN_EMAIL || 'richard@bergsma.it';
+ const testMessage = 'This is a test message from the test-contact-form.cjs script. ' + new Date().toISOString();
+
+ formData.append('name', 'Test User');
+ formData.append('email', testEmail);
+ formData.append('message', testMessage);
+ formData.append('disclaimer', 'on');
+ formData.append('csrf_token', csrfToken);
+ formData.append('timestamp', Date.now().toString());
+
+ console.log('Submitting form with data:', {
+ name: 'Test User',
+ email: testEmail,
+ messageLength: testMessage.length,
+ disclaimer: 'on',
+ csrfToken: csrfToken ? 'present' : 'missing',
+ });
+
+ try {
+ console.log(`Sending POST request to ${API_URL}`);
+ const response = await fetch(API_URL, {
+ method: 'POST',
+ body: formData,
+ headers: {
+ 'Accept': 'application/json',
+ 'User-Agent': 'test-contact-form-script'
+ }
+ });
+
+ console.log('Response status:', response.status);
+
+ if (!response.ok) {
+ console.error('Form submission failed with status:', response.status, response.statusText);
+ }
+
+ const text = await response.text();
+ console.log('Response text:', text);
+
+ try {
+ const result = JSON.parse(text);
+ console.log('Form submission result:', result);
+ return result;
+ } catch (parseError) {
+ console.error('Error parsing response:', parseError);
+ console.error('Response was not valid JSON:', text);
+ return { success: false, error: 'Invalid JSON response' };
+ }
+ } catch (error) {
+ console.error('Error submitting form:', error);
+ return { success: false, error: error.message };
+ }
+}
+
+// Main function
+async function main() {
+ console.log('Starting contact form test...');
+ console.log(`API URL: ${API_URL}`);
+
+ // Get CSRF token
+ console.log('Getting CSRF token...');
+ const csrfToken = await getCsrfToken();
+
+ if (!csrfToken) {
+ console.error('Failed to get CSRF token. Aborting test.');
+ process.exit(1);
+ }
+
+ console.log('CSRF token received:', csrfToken ? 'Yes' : 'No');
+
+ // Submit the form
+ console.log('Submitting form...');
+ const result = await submitForm(csrfToken);
+
+ if (result.success) {
+ console.log('Form submission successful!');
+ } else {
+ console.error('Form submission failed:', result);
+ }
+}
+
+// Run the test
+main().catch(error => {
+ console.error('Unhandled error:', error);
+ process.exit(1);
+});
\ No newline at end of file
diff --git a/test-email.ts b/test-email.ts
new file mode 100644
index 0000000..070c57b
--- /dev/null
+++ b/test-email.ts
@@ -0,0 +1,31 @@
+import { testEmailConfiguration, sendAdminNotification } from './src/utils/email-handler.js';
+import 'dotenv/config';
+
+async function runEmailTest() {
+ console.log('Starting email configuration test...');
+
+ // Test the SMTP connection
+ const configTest = await testEmailConfiguration();
+ console.log(`Configuration test result: ${configTest ? 'SUCCESS' : 'FAILED'}`);
+
+ if (configTest) {
+ // Try sending a test email
+ console.log('Attempting to send a test email...');
+ const emailResult = await sendAdminNotification(
+ 'Test User',
+ 'test@example.com',
+ 'This is a test message sent at ' + new Date().toISOString(),
+ '127.0.0.1',
+ 'Email Test Script'
+ );
+
+ console.log(`Test email result: ${emailResult ? 'SENT' : 'FAILED'}`);
+ }
+
+ console.log('Email test completed');
+}
+
+runEmailTest().catch(error => {
+ console.error('Error running email test:', error);
+ process.exit(1);
+});
\ No newline at end of file
diff --git a/test-smtp.js b/test-smtp.js
new file mode 100644
index 0000000..89c2004
--- /dev/null
+++ b/test-smtp.js
@@ -0,0 +1,88 @@
+const nodemailer = require('nodemailer');
+require('dotenv').config();
+
+// Get SMTP settings from environment variables
+const {
+ SMTP_HOST,
+ SMTP_PORT,
+ SMTP_USER,
+ SMTP_PASS,
+ ADMIN_EMAIL,
+ WEBSITE_NAME
+} = process.env;
+
+console.log('SMTP Configuration Test');
+console.log('----------------------');
+console.log(`SMTP Host: ${SMTP_HOST}`);
+console.log(`SMTP Port: ${SMTP_PORT}`);
+console.log(`SMTP User: ${SMTP_USER}`);
+console.log(`Admin Email: ${ADMIN_EMAIL}`);
+console.log(`Website Name: ${WEBSITE_NAME}`);
+console.log('----------------------');
+
+// Create a transporter
+const transporter = nodemailer.createTransport({
+ host: SMTP_HOST,
+ port: parseInt(SMTP_PORT, 10),
+ secure: parseInt(SMTP_PORT, 10) === 465,
+ auth: {
+ user: SMTP_USER,
+ pass: SMTP_PASS,
+ },
+ // Required for ProtonMail
+ tls: {
+ ciphers: 'SSLv3',
+ rejectUnauthorized: false
+ },
+ debug: true, // Enable debug output
+});
+
+// Test the connection
+console.log('Testing SMTP connection...');
+transporter.verify((error, success) => {
+ if (error) {
+ console.error('SMTP Connection Error:', error);
+ console.error('Error Name:', error.name);
+ console.error('Error Message:', error.message);
+
+ // Provide troubleshooting advice based on error
+ if (error.message.includes('ECONNREFUSED')) {
+ console.error('\nTroubleshooting: Connection refused');
+ console.error('- Check if the SMTP server address is correct');
+ console.error('- Verify the port is correct and not blocked by a firewall');
+ console.error('- Ensure your internet connection is working');
+ } else if (error.message.includes('Invalid login') || error.message.includes('authentication failed')) {
+ console.error('\nTroubleshooting: Authentication failed');
+ console.error('- Verify your username and password are correct');
+ console.error('- For ProtonMail, ensure you\'re using an app-specific password');
+ console.error('- Check if 2FA is enabled and properly configured');
+ } else if (error.message.includes('certificate')) {
+ console.error('\nTroubleshooting: SSL/TLS Certificate Error');
+ console.error('- The server\'s SSL certificate could not be verified');
+ console.error('- This might be resolved by setting rejectUnauthorized: false (already set)');
+ }
+ } else {
+ console.log('SMTP Connection Successful!');
+ console.log('The server is ready to accept messages');
+
+ // Send a test email
+ console.log('\nSending a test email...');
+ const mailOptions = {
+ from: `"${WEBSITE_NAME}" <${ADMIN_EMAIL}>`,
+ to: ADMIN_EMAIL,
+ subject: 'SMTP Test Email',
+ text: 'This is a test email to verify SMTP configuration is working correctly.',
+ html: 'This is a test email to verify SMTP configuration is working correctly.
'
+ };
+
+ transporter.sendMail(mailOptions, (error, info) => {
+ if (error) {
+ console.error('Error sending test email:', error);
+ } else {
+ console.log('Test email sent successfully!');
+ console.log('Message ID:', info.messageId);
+ console.log('Response:', info.response);
+ }
+ });
+ }
+});
\ No newline at end of file