diff --git a/production-email-test.cjs b/production-email-test.cjs
new file mode 100644
index 0000000..16718ad
--- /dev/null
+++ b/production-email-test.cjs
@@ -0,0 +1,152 @@
+/**
+ * Production Email Test Script
+ *
+ * This script tests email delivery in production mode by sending test emails
+ * with various configurations to help diagnose delivery issues.
+ *
+ * Run with: NODE_ENV=production node production-email-test.cjs
+ */
+
+const nodemailer = require('nodemailer');
+require('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 = 'production'
+} = process.env;
+
+// Force production mode
+const isProduction = true;
+
+console.log('Production Email Test');
+console.log('--------------------');
+console.log('Configuration:');
+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(`Mode: ${isProduction ? 'production' : 'development'}`);
+console.log('--------------------');
+
+// Create a transporter
+function createTransporter(options = {}) {
+ const isProtonMail = SMTP_HOST.includes('protonmail');
+
+ return nodemailer.createTransport({
+ host: SMTP_HOST,
+ port: parseInt(SMTP_PORT, 10),
+ secure: parseInt(SMTP_PORT, 10) === 465,
+ auth: {
+ user: SMTP_USER,
+ pass: SMTP_PASS,
+ },
+ ...(isProtonMail && {
+ tls: {
+ rejectUnauthorized: false,
+ ciphers: 'SSLv3'
+ }
+ }),
+ debug: true,
+ ...options
+ });
+}
+
+// Test different transporter configurations
+async function runTests() {
+ try {
+ // Test 1: Basic configuration
+ console.log('\nTest 1: Basic configuration');
+ const transporter1 = createTransporter();
+ await testTransporter(transporter1, 'Basic configuration');
+
+ // Test 2: With secure=false explicitly set
+ console.log('\nTest 2: With secure=false explicitly set');
+ const transporter2 = createTransporter({ secure: false });
+ await testTransporter(transporter2, 'secure=false configuration');
+
+ // Test 3: With requireTLS=true
+ console.log('\nTest 3: With requireTLS=true');
+ const transporter3 = createTransporter({ requireTLS: true });
+ await testTransporter(transporter3, 'requireTLS=true configuration');
+
+ // Test 4: With different from address
+ console.log('\nTest 4: With different from address');
+ const transporter4 = createTransporter();
+ await testTransporter(
+ transporter4,
+ 'Different from address',
+ { from: `"Test" <${SMTP_USER}>` }
+ );
+
+ console.log('\nAll tests completed!');
+
+ } catch (error) {
+ console.error('Error running tests:', error);
+ }
+}
+
+// Test a specific transporter configuration
+async function testTransporter(transporter, testName, options = {}) {
+ console.log(`Testing ${testName}...`);
+
+ try {
+ // Verify connection
+ await new Promise((resolve, reject) => {
+ transporter.verify((error, success) => {
+ if (error) {
+ console.error(`Connection test failed for ${testName}:`, error);
+ reject(error);
+ } else {
+ console.log(`Connection successful for ${testName}`);
+ resolve(success);
+ }
+ });
+ });
+
+ // Send test email
+ const fromAddress = options.from || `"${WEBSITE_NAME}" <${SMTP_USER}>`;
+
+ const info = await transporter.sendMail({
+ from: fromAddress,
+ to: ADMIN_EMAIL,
+ subject: `Production Email Test: ${testName}`,
+ text: `This is a test email from the production-email-test.cjs script using ${testName}.\n\nTime: ${new Date().toISOString()}`,
+ html: `
+
+
Production Email Test: ${testName}
+
This is a test email from the production-email-test.cjs script using ${testName}.
+
Time: ${new Date().toISOString()}
+
Configuration:
+
+ - SMTP Host: ${SMTP_HOST}
+ - SMTP Port: ${SMTP_PORT}
+ - From: ${fromAddress}
+ - To: ${ADMIN_EMAIL}
+
+
+ `
+ });
+
+ console.log(`Email sent successfully for ${testName}!`);
+ console.log(`Message ID: ${info.messageId}`);
+ console.log(`Response: ${info.response}`);
+
+ return info;
+ } catch (error) {
+ console.error(`Error in ${testName}:`, error);
+ throw error;
+ }
+}
+
+// Run the tests
+runTests().catch(error => {
+ console.error('Unhandled error:', error);
+ process.exit(1);
+});
\ No newline at end of file
diff --git a/public/test-contact-form.html b/public/test-contact-form.html
new file mode 100644
index 0000000..a9faca2
--- /dev/null
+++ b/public/test-contact-form.html
@@ -0,0 +1,252 @@
+
+
+
+
+
+ Contact Form Test
+
+
+
+ Contact Form Test
+
+
+ Your message has been sent successfully!
+
+
+
+ There was an error sending your message. Please try again.
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/common/BasicScripts.astro b/src/components/common/BasicScripts.astro
index 0249017..13020e1 100644
--- a/src/components/common/BasicScripts.astro
+++ b/src/components/common/BasicScripts.astro
@@ -465,12 +465,22 @@ import { UI } from 'astrowind:config';
// Fetch CSRF token from the server
async function fetchCsrfToken() {
+ console.log('Fetching CSRF token');
try {
const response = await fetch('/api/contact?csrf=true');
+ console.log('CSRF response status:', response.status);
+
+ if (!response.ok) {
+ console.error('CSRF request failed:', response.statusText);
+ return '';
+ }
+
const data = await response.json();
+ console.log('CSRF token received:', data.csrfToken ? 'yes' : 'no');
return data.csrfToken;
} catch (error) {
console.error('Error fetching CSRF token:', error);
+ console.error('Error details:', error.message);
return '';
}
}
@@ -489,15 +499,19 @@ import { UI } from 'astrowind:config';
// Form validation and submission
contactForm.addEventListener('submit', async function(e) {
e.preventDefault();
+ console.log('Form submitted');
// Reset previous error messages
resetFormErrors();
// Client-side validation
if (!validateForm(contactForm)) {
+ console.log('Form validation failed');
return;
}
+ console.log('Form validation passed');
+
// Show loading state
const submitButton = contactForm.querySelector('button[type="submit"]');
const originalButtonText = submitButton.innerHTML;
@@ -507,9 +521,16 @@ import { UI } from 'astrowind:config';
try {
const formData = new FormData(contactForm);
+ // Log form data
+ console.log('Form data:');
+ for (const [key, value] of formData.entries()) {
+ console.log(`${key}: ${value}`);
+ }
+
// Add timestamp to help prevent duplicate submissions
formData.append('timestamp', Date.now().toString());
+ console.log('Sending form data to /api/contact');
const response = await fetch('/api/contact', {
method: 'POST',
body: formData,
@@ -518,23 +539,40 @@ import { UI } from 'astrowind:config';
}
});
+ console.log('Response status:', response.status);
const result = await response.json();
+ console.log('Response data:', result);
if (result.success) {
+ console.log('Form submission successful');
// Show success message
document.getElementById('form-success').classList.remove('hidden');
contactForm.reset();
// Get a new CSRF token for next submission
+ console.log('Getting new CSRF token');
const newToken = await fetchCsrfToken();
+ console.log('New CSRF token:', newToken ? 'received' : 'not received');
if (csrfTokenInput) {
csrfTokenInput.value = newToken;
}
} else {
- // Show error messages
- document.getElementById('form-error').classList.remove('hidden');
-
+ console.log('Form submission failed:', result);
+ // Show specific error messages instead of generic form error
if (result.errors) {
+ console.log('Form errors:', result.errors);
+
+ // Only show the generic error if there are no field-specific errors
+ // or if there's a server error not related to a specific field
+ const hasFieldErrors = Object.keys(result.errors).some(field =>
+ ['name', 'email', 'message', 'disclaimer', 'csrf'].includes(field)
+ );
+
+ if (!hasFieldErrors) {
+ document.getElementById('form-error').classList.remove('hidden');
+ }
+
+ // Display field-specific errors
Object.keys(result.errors).forEach(field => {
const inputElement = contactForm.querySelector(`[name="${field}"]`);
if (inputElement) {
@@ -543,13 +581,25 @@ import { UI } from 'astrowind:config';
feedbackElement.textContent = result.errors[field];
feedbackElement.classList.remove('hidden');
inputElement.classList.add('border-red-500');
+
+ // Special handling for checkbox
+ if (field === 'disclaimer') {
+ const checkboxContainer = inputElement.closest('.flex.items-start');
+ if (checkboxContainer) {
+ checkboxContainer.classList.add('checkbox-error');
+ }
+ }
}
}
});
+ } else {
+ // If no specific errors, show the generic error
+ document.getElementById('form-error').classList.remove('hidden');
}
// If CSRF token is invalid, get a new one
if (result.errors && result.errors.csrf) {
+ console.log('CSRF token invalid, getting new token');
const newToken = await fetchCsrfToken();
if (csrfTokenInput) {
csrfTokenInput.value = newToken;
@@ -558,6 +608,7 @@ import { UI } from 'astrowind:config';
}
} catch (error) {
console.error('Error submitting form:', error);
+ console.error('Error details:', error.message);
document.getElementById('form-error').classList.remove('hidden');
} finally {
// Restore button state
@@ -608,6 +659,30 @@ import { UI } from 'astrowind:config';
}
input.classList.remove('border-red-500');
+ // Checkbox validation (special case for disclaimer)
+ if (input.type === 'checkbox') {
+ if (input.required && !input.checked) {
+ isValid = false;
+ if (feedbackElement) {
+ feedbackElement.textContent = 'Please check the required consent box before submitting';
+ feedbackElement.classList.remove('hidden');
+ }
+ input.classList.add('border-red-500');
+ // Add red border to the checkbox container for better visibility
+ const checkboxContainer = input.closest('.flex.items-start');
+ if (checkboxContainer) {
+ checkboxContainer.classList.add('checkbox-error');
+ }
+ } else {
+ // Remove error styling when checkbox is checked
+ const checkboxContainer = input.closest('.flex.items-start');
+ if (checkboxContainer) {
+ checkboxContainer.classList.remove('checkbox-error');
+ }
+ }
+ return isValid;
+ }
+
// Check if empty
if (input.required && !input.value.trim()) {
isValid = false;
@@ -655,6 +730,11 @@ import { UI } from 'astrowind:config';
input.classList.remove('border-red-500');
});
+ // Remove checkbox container error styling
+ document.querySelectorAll('.flex.items-start').forEach(container => {
+ container.classList.remove('checkbox-error');
+ });
+
// Hide form-level messages
document.getElementById('form-success')?.classList.add('hidden');
document.getElementById('form-error')?.classList.add('hidden');
diff --git a/src/components/ui/Form.astro b/src/components/ui/Form.astro
index fd3c3ad..4f83fb9 100644
--- a/src/components/ui/Form.astro
+++ b/src/components/ui/Form.astro
@@ -5,6 +5,16 @@ import Button from '~/components/ui/Button.astro';
const { inputs, textarea, disclaimer, button = 'Contact us', description = '' } = Astro.props;
---
+
+