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:

+ +
+ ` + }); + + 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. +
+ +
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+ +
+

Debug Log

+
+
+ + + + \ 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; --- + +
@@ -13,7 +23,7 @@ const { inputs, textarea, disclaimer, button = 'Contact us', description = '' } diff --git a/src/email-templates/admin-notification.ts b/src/email-templates/admin-notification.ts index 7842424..fdaa7b6 100644 --- a/src/email-templates/admin-notification.ts +++ b/src/email-templates/admin-notification.ts @@ -8,7 +8,7 @@ interface AdminNotificationProps { } export function getAdminNotificationSubject(): string { - return 'New Contact Form Submission from bergsma.it'; + return `New Contact Form Submission from ${process.env.WEBSITE_NAME || '365devnet.eu'}`; } export function getAdminNotificationHtml(props: AdminNotificationProps): string { diff --git a/src/email-templates/user-confirmation.ts b/src/email-templates/user-confirmation.ts index 924b38d..416eb85 100644 --- a/src/email-templates/user-confirmation.ts +++ b/src/email-templates/user-confirmation.ts @@ -7,17 +7,17 @@ interface UserConfirmationProps { contactEmail?: string; } -export function getUserConfirmationSubject(websiteName: string = 'bergsma.it'): string { +export function getUserConfirmationSubject(websiteName: string = process.env.WEBSITE_NAME || '365devnet.eu'): string { return `Thank you for contacting ${websiteName}`; } export function getUserConfirmationHtml(props: UserConfirmationProps): string { - const { - name, - message, - submittedAt, - websiteName = 'bergsma.it', - contactEmail = 'richard@bergsma.it' + const { + name, + message, + submittedAt, + websiteName = process.env.WEBSITE_NAME || '365devnet.eu', + contactEmail = process.env.ADMIN_EMAIL || 'richard@bergsma.it' } = props; return ` @@ -92,12 +92,12 @@ export function getUserConfirmationHtml(props: UserConfirmationProps): string { } export function getUserConfirmationText(props: UserConfirmationProps): string { - const { - name, - message, - submittedAt, - websiteName = 'bergsma.it', - contactEmail = 'richard@bergsma.it' + const { + name, + message, + submittedAt, + websiteName = process.env.WEBSITE_NAME || '365devnet.eu', + contactEmail = process.env.ADMIN_EMAIL || 'richard@bergsma.it' } = props; return ` diff --git a/src/pages/api/contact.ts b/src/pages/api/contact.ts index fb5184a..2888614 100644 --- a/src/pages/api/contact.ts +++ b/src/pages/api/contact.ts @@ -162,20 +162,26 @@ export const POST: APIRoute = async ({ request, clientAddress }) => { 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 (!name) { + errors.name = 'Please enter your name'; + } else if (name.length < 2) { + errors.name = 'Your name must be at least 2 characters long'; } - if (!email || !isValidEmail(email)) { - errors.email = 'A valid email address is required'; + if (!email) { + errors.email = 'Please enter your email address'; + } else if (!isValidEmail(email)) { + errors.email = 'Please enter a valid email address (e.g., name@example.com)'; } - if (!message || message.length < 10) { - errors.message = 'Message is required and must be at least 10 characters'; + if (!message) { + errors.message = 'Please enter your message'; + } else if (message.length < 10) { + errors.message = 'Your message must be at least 10 characters long'; } if (!disclaimer) { - errors.disclaimer = 'You must agree to the disclaimer'; + errors.disclaimer = 'Please check the required consent box before submitting'; } // Check for spam diff --git a/src/utils/email-handler.ts b/src/utils/email-handler.ts index fc03d47..90b88db 100644 --- a/src/utils/email-handler.ts +++ b/src/utils/email-handler.ts @@ -13,11 +13,12 @@ const { SMTP_PASS = '', ADMIN_EMAIL = 'richard@bergsma.it', WEBSITE_NAME = 'bergsma.it', - NODE_ENV = 'development' + NODE_ENV = 'production' } = process.env; // Email configuration -const isProduction = NODE_ENV === 'production'; +// Force production mode for testing +const isProduction = true; // NODE_ENV === 'production'; // Create a transporter for sending emails let transporter: nodemailer.Transporter; @@ -31,6 +32,15 @@ function initializeTransporter() { // ProtonMail often requires using their Bridge application for SMTP const isProtonMail = SMTP_HOST.includes('protonmail'); + // Log the email configuration + console.log('Initializing email transporter with:'); + 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(`Environment: ${NODE_ENV}`); + transporter = nodemailer.createTransport({ host: SMTP_HOST, port: parseInt(SMTP_PORT, 10), @@ -179,8 +189,13 @@ export async function sendEmail( } try { + // Ensure from address matches SMTP_USER for ProtonMail + const fromAddress = isProduction ? + `"${WEBSITE_NAME}" <${SMTP_USER}>` : + `"${WEBSITE_NAME}" <${ADMIN_EMAIL}>`; + const mailOptions = { - from: `"${WEBSITE_NAME}" <${ADMIN_EMAIL}>`, + from: fromAddress, to, subject, html, @@ -188,7 +203,7 @@ export async function sendEmail( }; console.log('Mail options:', { - from: `"${WEBSITE_NAME}" <${ADMIN_EMAIL}>`, + from: fromAddress, to, subject, textLength: text.length, @@ -199,6 +214,15 @@ export async function sendEmail( const info = await transporter.sendMail(mailOptions); console.log('Email sent, info:', info.messageId); + // Log additional information in production mode + if (isProduction) { + console.log('Email delivery details:', { + messageId: info.messageId, + response: info.response, + envelope: info.envelope + }); + } + if (!isProduction) { // In development, log the email content console.log('Email sent (development mode):');