Contact form logic
This commit is contained in:
12
.env.example
Normal file
12
.env.example
Normal file
@@ -0,0 +1,12 @@
|
||||
# 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
|
137
check-email-delivery.cjs
Normal file
137
check-email-delivery.cjs
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Email Delivery Test Script
|
||||
*
|
||||
* This script tests email delivery by sending test emails to different addresses
|
||||
* and with different configurations to help diagnose delivery issues.
|
||||
*/
|
||||
|
||||
const nodemailer = require('nodemailer');
|
||||
require('dotenv').config();
|
||||
|
||||
// Environment variables
|
||||
const {
|
||||
SMTP_HOST = 'smtp.protonmail.ch',
|
||||
SMTP_PORT = '587',
|
||||
SMTP_USER = '',
|
||||
SMTP_PASS = '',
|
||||
ADMIN_EMAIL = 'richard@bergsma.it',
|
||||
WEBSITE_NAME = 'bergsma.it',
|
||||
NODE_ENV = 'development'
|
||||
} = process.env;
|
||||
|
||||
// Set to production mode for this test
|
||||
const isProduction = true;
|
||||
|
||||
console.log('Email Delivery 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
|
||||
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,
|
||||
},
|
||||
tls: {
|
||||
rejectUnauthorized: false,
|
||||
ciphers: 'SSLv3'
|
||||
},
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Verify connection
|
||||
console.log('Testing SMTP connection...');
|
||||
transporter.verify(function(error, success) {
|
||||
if (error) {
|
||||
console.error('SMTP Connection Error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('SMTP Connection Successful!');
|
||||
runTests();
|
||||
});
|
||||
|
||||
// Run a series of email delivery tests
|
||||
async function runTests() {
|
||||
try {
|
||||
// Test 1: Basic email to admin
|
||||
console.log('\nTest 1: Sending basic email to admin...');
|
||||
await sendTestEmail(
|
||||
ADMIN_EMAIL,
|
||||
'Email Delivery Test 1 - Basic',
|
||||
'This is a basic test email to the admin address.',
|
||||
`<p>This is a basic test email to the admin address.</p><p>Time: ${new Date().toISOString()}</p>`
|
||||
);
|
||||
|
||||
// Test 2: Email with different From address
|
||||
console.log('\nTest 2: Sending email with different From address...');
|
||||
await sendTestEmail(
|
||||
ADMIN_EMAIL,
|
||||
'Email Delivery Test 2 - Different From',
|
||||
'This email uses a different From address.',
|
||||
`<p>This email uses a different From address.</p><p>Time: ${new Date().toISOString()}</p>`,
|
||||
`"Test Sender" <${SMTP_USER}>`
|
||||
);
|
||||
|
||||
// Test 3: Email with contact form format
|
||||
console.log('\nTest 3: Sending email in contact form format...');
|
||||
const contactFormHtml = `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #eee;">
|
||||
<h2>New Contact Form Submission</h2>
|
||||
<p><strong>From:</strong> Test User (test@example.com)</p>
|
||||
<p><strong>Submitted on:</strong> ${new Date().toLocaleString()}</p>
|
||||
<div style="background-color: #f9f9f9; padding: 15px; border-left: 3px solid #007bff; margin: 15px 0;">
|
||||
<p><strong>Message:</strong></p>
|
||||
<p>This is a test message simulating a contact form submission.</p>
|
||||
</div>
|
||||
<div style="font-size: 12px; color: #777; margin-top: 20px; padding-top: 10px; border-top: 1px solid #eee;">
|
||||
<p>This is an automated email from your website contact form.</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
await sendTestEmail(
|
||||
ADMIN_EMAIL,
|
||||
'New Contact Form Submission (Test)',
|
||||
'This is a test message simulating a contact form submission.',
|
||||
contactFormHtml
|
||||
);
|
||||
|
||||
console.log('\nAll tests completed successfully!');
|
||||
console.log('\nPlease check your inbox (and spam folder) for the test emails.');
|
||||
console.log('If you received some emails but not others, this can help identify the issue.');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error running tests:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to send a test email
|
||||
async function sendTestEmail(to, subject, text, html, from = `"${WEBSITE_NAME}" <${ADMIN_EMAIL}>`) {
|
||||
try {
|
||||
const info = await transporter.sendMail({
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
text,
|
||||
html
|
||||
});
|
||||
|
||||
console.log(`Email sent successfully!`);
|
||||
console.log(`Message ID: ${info.messageId}`);
|
||||
console.log(`Response: ${info.response}`);
|
||||
return info;
|
||||
} catch (error) {
|
||||
console.error('Error sending email:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
101
email-test.cjs
Normal file
101
email-test.cjs
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* ProtonMail SMTP Test Script
|
||||
*
|
||||
* This script tests the SMTP configuration for sending emails through ProtonMail.
|
||||
* Run with: node email-test.cjs
|
||||
*/
|
||||
|
||||
const nodemailer = require('nodemailer');
|
||||
require('dotenv').config();
|
||||
|
||||
// Get SMTP settings from environment variables
|
||||
const {
|
||||
SMTP_HOST = 'smtp.protonmail.ch',
|
||||
SMTP_PORT = '587',
|
||||
SMTP_USER,
|
||||
SMTP_PASS,
|
||||
ADMIN_EMAIL,
|
||||
WEBSITE_NAME = 'Website'
|
||||
} = process.env;
|
||||
|
||||
console.log('Email 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(`Environment: ${process.env.NODE_ENV || 'development'}`);
|
||||
console.log('----------------------');
|
||||
|
||||
// Create a transporter with ProtonMail-specific settings
|
||||
const 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,
|
||||
},
|
||||
tls: {
|
||||
// Do not fail on invalid certs
|
||||
rejectUnauthorized: false,
|
||||
// Specific ciphers for ProtonMail
|
||||
ciphers: 'SSLv3'
|
||||
},
|
||||
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('\nTroubleshooting Tips:');
|
||||
console.error('1. Check if your SMTP credentials are correct');
|
||||
console.error('2. For ProtonMail, ensure you\'re using an app-specific password');
|
||||
console.error('3. If using ProtonMail Bridge, make sure it\'s running');
|
||||
console.error('4. Verify your server allows outgoing connections on the SMTP port');
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('SMTP Connection Successful!');
|
||||
|
||||
// Send a test email
|
||||
console.log('\nSending a test email...');
|
||||
const mailOptions = {
|
||||
from: `"${WEBSITE_NAME}" <${SMTP_USER}>`,
|
||||
to: ADMIN_EMAIL,
|
||||
subject: 'Email Configuration Test',
|
||||
text: 'This is a test email to verify your website\'s email configuration is working correctly.',
|
||||
html: `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #eee; border-radius: 5px;">
|
||||
<h2 style="color: #333;">Email Configuration Test</h2>
|
||||
<p>This is a test email to verify your website's email configuration is working correctly.</p>
|
||||
<p><strong>Configuration Details:</strong></p>
|
||||
<ul>
|
||||
<li>SMTP Host: ${SMTP_HOST}</li>
|
||||
<li>SMTP Port: ${SMTP_PORT}</li>
|
||||
<li>From: ${SMTP_USER}</li>
|
||||
<li>To: ${ADMIN_EMAIL}</li>
|
||||
<li>Time: ${new Date().toISOString()}</li>
|
||||
</ul>
|
||||
<p style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee; font-size: 12px; color: #666;">
|
||||
This is an automated test email. If you received this, your email configuration is working correctly.
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
|
||||
transporter.sendMail(mailOptions, (error, info) => {
|
||||
if (error) {
|
||||
console.error('Error sending test email:', error);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('Test email sent successfully!');
|
||||
console.log('Message ID:', info.messageId);
|
||||
console.log('Response:', info.response);
|
||||
console.log('\nIf you received the test email, your email configuration is working correctly.');
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
239
package-lock.json
generated
239
package-lock.json
generated
@@ -20,9 +20,14 @@
|
||||
"astro": "^5.2.3",
|
||||
"astro-embed": "^0.9.0",
|
||||
"astro-icon": "^1.1.5",
|
||||
"csrf": "^3.1.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"form-data": "^4.0.2",
|
||||
"limax": "4.1.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"node-fetch": "^3.3.2",
|
||||
"nodemailer": "^6.10.0",
|
||||
"rate-limiter-flexible": "^5.0.5",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"unpic": "^3.22.0"
|
||||
@@ -3959,6 +3964,19 @@
|
||||
"version": "0.0.1",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@@ -4458,6 +4476,20 @@
|
||||
"uncrypto": "^0.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/csrf": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz",
|
||||
"integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"rndm": "1.2.0",
|
||||
"tsscmp": "1.0.6",
|
||||
"uid-safe": "2.1.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/css-select": {
|
||||
"version": "5.1.0",
|
||||
"license": "BSD-2-Clause",
|
||||
@@ -4733,7 +4765,7 @@
|
||||
"version": "16.4.7",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -4750,6 +4782,20 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"license": "MIT"
|
||||
@@ -4815,11 +4861,56 @@
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz",
|
||||
"integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ=="
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/esast-util-from-estree": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz",
|
||||
@@ -5608,12 +5699,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
||||
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
@@ -5691,7 +5784,6 @@
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@@ -5726,6 +5818,43 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/github-slugger": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz",
|
||||
@@ -5799,6 +5928,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -5826,9 +5967,35 @@
|
||||
"unenv": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
@@ -6977,6 +7144,15 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-definitions": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz",
|
||||
@@ -8206,6 +8382,15 @@
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
|
||||
"integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g=="
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz",
|
||||
"integrity": "sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nopt": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz",
|
||||
@@ -8984,6 +9169,21 @@
|
||||
"resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz",
|
||||
"integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="
|
||||
},
|
||||
"node_modules/random-bytes": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
|
||||
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/rate-limiter-flexible": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-5.0.5.tgz",
|
||||
"integrity": "sha512-+/dSQfo+3FYwYygUs/V2BBdwGa9nFtakDwKt4l0bnvNB53TNT++QSFewwHX9qXrZJuMe9j+TUaU21lm5ARgqdQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
|
||||
@@ -9429,6 +9629,12 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/rndm": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz",
|
||||
"integrity": "sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.31.0.tgz",
|
||||
@@ -10213,6 +10419,15 @@
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsscmp": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
|
||||
"integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6.x"
|
||||
}
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"dev": true,
|
||||
@@ -10295,6 +10510,18 @@
|
||||
"integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/uid-safe": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
|
||||
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"random-bytes": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/uint8arrays": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz",
|
||||
|
@@ -34,9 +34,14 @@
|
||||
"astro": "^5.2.3",
|
||||
"astro-embed": "^0.9.0",
|
||||
"astro-icon": "^1.1.5",
|
||||
"csrf": "^3.1.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"form-data": "^4.0.2",
|
||||
"limax": "4.1.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"node-fetch": "^3.3.2",
|
||||
"nodemailer": "^6.10.0",
|
||||
"rate-limiter-flexible": "^5.0.5",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"unpic": "^3.22.0"
|
||||
|
69
protonmail-test.cjs
Normal file
69
protonmail-test.cjs
Normal file
@@ -0,0 +1,69 @@
|
||||
// Simple ProtonMail SMTP test
|
||||
const nodemailer = require('nodemailer');
|
||||
require('dotenv').config();
|
||||
|
||||
// Get SMTP settings from environment variables
|
||||
const {
|
||||
SMTP_HOST = 'smtp.protonmail.ch',
|
||||
SMTP_PORT = '587',
|
||||
SMTP_USER,
|
||||
SMTP_PASS,
|
||||
ADMIN_EMAIL
|
||||
} = process.env;
|
||||
|
||||
console.log('ProtonMail SMTP 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}`);
|
||||
|
||||
// ProtonMail specific configuration
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: SMTP_HOST,
|
||||
port: parseInt(SMTP_PORT, 10),
|
||||
secure: false, // For ProtonMail, use false for port 587
|
||||
auth: {
|
||||
user: SMTP_USER,
|
||||
pass: SMTP_PASS,
|
||||
},
|
||||
tls: {
|
||||
// Do not fail on invalid certs
|
||||
rejectUnauthorized: false,
|
||||
// Specific ciphers for ProtonMail
|
||||
ciphers: 'SSLv3'
|
||||
},
|
||||
logger: true,
|
||||
debug: true // Include SMTP traffic in the logs
|
||||
});
|
||||
|
||||
// Verify connection
|
||||
console.log('\nTesting connection to ProtonMail SMTP server...');
|
||||
transporter.verify(function(error, _success) {
|
||||
if (error) {
|
||||
console.error('Connection failed:', error);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('Server is ready to take our messages');
|
||||
|
||||
// Send test email
|
||||
console.log('\nSending test email...');
|
||||
transporter.sendMail({
|
||||
from: SMTP_USER,
|
||||
to: ADMIN_EMAIL,
|
||||
subject: 'ProtonMail SMTP Test',
|
||||
text: 'This is a test email from your website contact form.',
|
||||
html: '<p>This is a test email from your website contact form.</p>'
|
||||
}, (err, info) => {
|
||||
if (err) {
|
||||
console.error('Error sending email:', err);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('Message sent successfully!');
|
||||
console.log('Message ID:', info.messageId);
|
||||
console.log('Response:', info.response);
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
97
protonmail-test2.cjs
Normal file
97
protonmail-test2.cjs
Normal file
@@ -0,0 +1,97 @@
|
||||
// ProtonMail SMTP test with alternative configuration
|
||||
const nodemailer = require('nodemailer');
|
||||
require('dotenv').config();
|
||||
|
||||
// Get SMTP settings from environment variables
|
||||
const {
|
||||
SMTP_USER,
|
||||
SMTP_PASS,
|
||||
ADMIN_EMAIL
|
||||
} = process.env;
|
||||
|
||||
console.log('ProtonMail SMTP Test (Alternative Configuration)');
|
||||
console.log('----------------------------------------------');
|
||||
console.log(`SMTP User: ${SMTP_USER}`);
|
||||
console.log(`Admin Email: ${ADMIN_EMAIL}`);
|
||||
|
||||
// Try alternative ProtonMail configuration
|
||||
// ProtonMail Bridge typically uses localhost:1025 or localhost:1143
|
||||
const transporterOptions = {
|
||||
host: 'mail.protonmail.ch',
|
||||
port: 443,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: SMTP_USER,
|
||||
pass: SMTP_PASS,
|
||||
},
|
||||
tls: {
|
||||
rejectUnauthorized: false
|
||||
},
|
||||
logger: true,
|
||||
debug: true
|
||||
};
|
||||
|
||||
console.log('\nUsing configuration:');
|
||||
console.log(`Host: ${transporterOptions.host}`);
|
||||
console.log(`Port: ${transporterOptions.port}`);
|
||||
console.log(`Secure: ${transporterOptions.secure}`);
|
||||
|
||||
const transporter = nodemailer.createTransport(transporterOptions);
|
||||
|
||||
// Verify connection
|
||||
console.log('\nTesting connection to ProtonMail SMTP server...');
|
||||
transporter.verify(function(error, _success) {
|
||||
if (error) {
|
||||
console.error('Connection failed:', error);
|
||||
console.log('\nTrying alternative port (25)...');
|
||||
|
||||
// Try port 25
|
||||
const transporter2 = nodemailer.createTransport({
|
||||
...transporterOptions,
|
||||
port: 25,
|
||||
secure: false
|
||||
});
|
||||
|
||||
transporter2.verify(function(error2, _success2) {
|
||||
if (error2) {
|
||||
console.error('Connection with port 25 also failed:', error2);
|
||||
|
||||
console.log('\nImportant ProtonMail SMTP Notes:');
|
||||
console.log('1. ProtonMail requires the Bridge application for SMTP access from third-party apps');
|
||||
console.log('2. The Bridge runs locally and provides SMTP access via localhost:1025 or similar');
|
||||
console.log('3. You may need to install and configure ProtonMail Bridge on your server');
|
||||
console.log('4. Or use ProtonMail\'s API instead of SMTP for sending emails');
|
||||
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('Connection successful with port 25!');
|
||||
sendTestEmail(transporter2);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('Server is ready to take our messages');
|
||||
sendTestEmail(transporter);
|
||||
}
|
||||
});
|
||||
|
||||
function sendTestEmail(transport) {
|
||||
// Send test email
|
||||
console.log('\nSending test email...');
|
||||
transport.sendMail({
|
||||
from: SMTP_USER,
|
||||
to: ADMIN_EMAIL,
|
||||
subject: 'ProtonMail SMTP Test (Alternative Config)',
|
||||
text: 'This is a test email from your website contact form.',
|
||||
html: '<p>This is a test email from your website contact form.</p>'
|
||||
}, (err, info) => {
|
||||
if (err) {
|
||||
console.error('Error sending email:', err);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('Message sent successfully!');
|
||||
console.log('Message ID:', info.messageId);
|
||||
console.log('Response:', info.response);
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
}
|
@@ -452,3 +452,211 @@ import { UI } from 'astrowind:config';
|
||||
Observer.start();
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Contact Form Handling -->
|
||||
<script is:inline>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
setupContactForm();
|
||||
});
|
||||
|
||||
document.addEventListener('astro:after-swap', () => {
|
||||
setupContactForm();
|
||||
});
|
||||
|
||||
// Fetch CSRF token from the server
|
||||
async function fetchCsrfToken() {
|
||||
try {
|
||||
const response = await fetch('/api/contact?csrf=true');
|
||||
const data = await response.json();
|
||||
return data.csrfToken;
|
||||
} catch (error) {
|
||||
console.error('Error fetching CSRF token:', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async function setupContactForm() {
|
||||
const contactForm = document.getElementById('contact-form');
|
||||
if (!contactForm) return;
|
||||
|
||||
// Get CSRF token and set it in the form
|
||||
const csrfTokenInput = contactForm.querySelector('#csrf_token');
|
||||
if (csrfTokenInput) {
|
||||
const token = await fetchCsrfToken();
|
||||
csrfTokenInput.value = token;
|
||||
}
|
||||
|
||||
// Form validation and submission
|
||||
contactForm.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Reset previous error messages
|
||||
resetFormErrors();
|
||||
|
||||
// Client-side validation
|
||||
if (!validateForm(contactForm)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
const submitButton = contactForm.querySelector('button[type="submit"]');
|
||||
const originalButtonText = submitButton.innerHTML;
|
||||
submitButton.disabled = true;
|
||||
submitButton.innerHTML = 'Sending...';
|
||||
|
||||
try {
|
||||
const formData = new FormData(contactForm);
|
||||
|
||||
// Add timestamp to help prevent duplicate submissions
|
||||
formData.append('timestamp', Date.now().toString());
|
||||
|
||||
const response = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Show success message
|
||||
document.getElementById('form-success').classList.remove('hidden');
|
||||
contactForm.reset();
|
||||
|
||||
// Get a new CSRF token for next submission
|
||||
const newToken = await fetchCsrfToken();
|
||||
if (csrfTokenInput) {
|
||||
csrfTokenInput.value = newToken;
|
||||
}
|
||||
} else {
|
||||
// Show error messages
|
||||
document.getElementById('form-error').classList.remove('hidden');
|
||||
|
||||
if (result.errors) {
|
||||
Object.keys(result.errors).forEach(field => {
|
||||
const inputElement = contactForm.querySelector(`[name="${field}"]`);
|
||||
if (inputElement) {
|
||||
const feedbackElement = inputElement.closest('div').querySelector('.invalid-feedback');
|
||||
if (feedbackElement) {
|
||||
feedbackElement.textContent = result.errors[field];
|
||||
feedbackElement.classList.remove('hidden');
|
||||
inputElement.classList.add('border-red-500');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If CSRF token is invalid, get a new one
|
||||
if (result.errors && result.errors.csrf) {
|
||||
const newToken = await fetchCsrfToken();
|
||||
if (csrfTokenInput) {
|
||||
csrfTokenInput.value = newToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error submitting form:', error);
|
||||
document.getElementById('form-error').classList.remove('hidden');
|
||||
} finally {
|
||||
// Restore button state
|
||||
submitButton.disabled = false;
|
||||
submitButton.innerHTML = originalButtonText;
|
||||
}
|
||||
});
|
||||
|
||||
// Add input validation on blur
|
||||
contactForm.querySelectorAll('input, textarea').forEach(input => {
|
||||
input.addEventListener('blur', function() {
|
||||
validateInput(this);
|
||||
});
|
||||
|
||||
input.addEventListener('input', function() {
|
||||
// Remove error styling when user starts typing
|
||||
this.classList.remove('border-red-500');
|
||||
const feedbackElement = this.closest('div').querySelector('.invalid-feedback');
|
||||
if (feedbackElement) {
|
||||
feedbackElement.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function validateForm(form) {
|
||||
let isValid = true;
|
||||
|
||||
// Validate all inputs
|
||||
form.querySelectorAll('input, textarea').forEach(input => {
|
||||
if (!validateInput(input)) {
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
function validateInput(input) {
|
||||
if (!input.required) return true;
|
||||
|
||||
let isValid = true;
|
||||
const feedbackElement = input.closest('div').querySelector('.invalid-feedback');
|
||||
|
||||
// Reset previous error
|
||||
if (feedbackElement) {
|
||||
feedbackElement.classList.add('hidden');
|
||||
}
|
||||
input.classList.remove('border-red-500');
|
||||
|
||||
// Check if empty
|
||||
if (input.required && !input.value.trim()) {
|
||||
isValid = false;
|
||||
if (feedbackElement) {
|
||||
feedbackElement.textContent = 'This field is required';
|
||||
feedbackElement.classList.remove('hidden');
|
||||
}
|
||||
input.classList.add('border-red-500');
|
||||
}
|
||||
|
||||
// Email validation
|
||||
if (input.type === 'email' && input.value.trim()) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(input.value.trim())) {
|
||||
isValid = false;
|
||||
if (feedbackElement) {
|
||||
feedbackElement.textContent = 'Please enter a valid email address';
|
||||
feedbackElement.classList.remove('hidden');
|
||||
}
|
||||
input.classList.add('border-red-500');
|
||||
}
|
||||
}
|
||||
|
||||
// Textarea minimum length
|
||||
if (input.tagName === 'TEXTAREA' && input.value.trim().length < 10) {
|
||||
isValid = false;
|
||||
if (feedbackElement) {
|
||||
feedbackElement.textContent = 'Please enter at least 10 characters';
|
||||
feedbackElement.classList.remove('hidden');
|
||||
}
|
||||
input.classList.add('border-red-500');
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
function resetFormErrors() {
|
||||
// Hide all error messages
|
||||
document.querySelectorAll('.invalid-feedback').forEach(el => {
|
||||
el.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Remove error styling
|
||||
document.querySelectorAll('input, textarea').forEach(input => {
|
||||
input.classList.remove('border-red-500');
|
||||
});
|
||||
|
||||
// Hide form-level messages
|
||||
document.getElementById('form-success')?.classList.add('hidden');
|
||||
document.getElementById('form-error')?.classList.add('hidden');
|
||||
}
|
||||
</script>
|
||||
|
@@ -5,16 +5,28 @@ import Button from '~/components/ui/Button.astro';
|
||||
const { inputs, textarea, disclaimer, button = 'Contact us', description = '' } = Astro.props;
|
||||
---
|
||||
|
||||
<form>
|
||||
<form id="contact-form" action="/api/contact" method="POST" class="needs-validation" novalidate>
|
||||
|
||||
<!-- Form status messages -->
|
||||
<div id="form-success" class="hidden mb-6 p-4 bg-green-100 border border-green-200 text-green-700 rounded-lg">
|
||||
Your message has been sent successfully. We will get back to you soon!
|
||||
</div>
|
||||
|
||||
<div id="form-error" class="hidden mb-6 p-4 bg-red-100 border border-red-200 text-red-700 rounded-lg">
|
||||
There was an error sending your message. Please try again.
|
||||
</div>
|
||||
|
||||
<!-- CSRF Token - Will be populated via JavaScript -->
|
||||
<input type="hidden" name="csrf_token" id="csrf_token" value="" />
|
||||
{
|
||||
inputs &&
|
||||
inputs.map(
|
||||
({ type = 'text', name, label = '', autocomplete = 'on', placeholder = '' }) =>
|
||||
({ type = 'text', name, label = '', autocomplete = 'on', placeholder = '', required = true }) =>
|
||||
name && (
|
||||
<div class="mb-6">
|
||||
{label && (
|
||||
<label for={name} class="block text-sm font-medium">
|
||||
{label}
|
||||
{label}{required && <span class="text-red-600">*</span>}
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
@@ -24,7 +36,9 @@ const { inputs, textarea, disclaimer, button = 'Contact us', description = '' }
|
||||
autocomplete={autocomplete}
|
||||
placeholder={placeholder}
|
||||
class="py-3 px-4 block w-full text-md rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900"
|
||||
required={required}
|
||||
/>
|
||||
<div class="invalid-feedback hidden text-red-600 text-sm mt-1"></div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
@@ -32,9 +46,9 @@ const { inputs, textarea, disclaimer, button = 'Contact us', description = '' }
|
||||
|
||||
{
|
||||
textarea && (
|
||||
<div>
|
||||
<div class="mb-6">
|
||||
<label for="textarea" class="block text-sm font-medium">
|
||||
{textarea.label}
|
||||
{textarea.label}<span class="text-red-600">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="textarea"
|
||||
@@ -42,26 +56,30 @@ const { inputs, textarea, disclaimer, button = 'Contact us', description = '' }
|
||||
rows={textarea.rows ? textarea.rows : 4}
|
||||
placeholder={textarea.placeholder}
|
||||
class="py-3 px-4 block w-full text-md rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900"
|
||||
required
|
||||
/>
|
||||
<div class="invalid-feedback hidden text-red-600 text-sm mt-1"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
disclaimer && (
|
||||
<div class="mt-3 flex items-start">
|
||||
<div class="mt-3 flex items-start mb-6">
|
||||
<div class="flex mt-0.5">
|
||||
<input
|
||||
id="disclaimer"
|
||||
name="disclaimer"
|
||||
type="checkbox"
|
||||
class="cursor-pointer mt-1 py-3 px-4 block w-full text-md rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<label for="disclaimer" class="cursor-pointer select-none text-sm text-gray-600 dark:text-gray-400">
|
||||
{disclaimer.label}
|
||||
{disclaimer.label}<span class="text-red-600">*</span>
|
||||
</label>
|
||||
<div class="invalid-feedback hidden text-red-600 text-sm mt-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
72
src/email-templates/README.md
Normal file
72
src/email-templates/README.md
Normal file
@@ -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
|
111
src/email-templates/admin-notification.ts
Normal file
111
src/email-templates/admin-notification.ts
Normal file
@@ -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 `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>New Contact Form Submission</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
background-color: #f5f5f5;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.content {
|
||||
background-color: #ffffff;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
.message-box {
|
||||
background-color: #f9f9f9;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
border-left: 3px solid #007bff;
|
||||
margin: 15px 0;
|
||||
}
|
||||
.footer {
|
||||
font-size: 12px;
|
||||
color: #777;
|
||||
margin-top: 20px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
.meta {
|
||||
font-size: 12px;
|
||||
color: #777;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h2>New Contact Form Submission</h2>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p><strong>From:</strong> ${name} (${email})</p>
|
||||
<p><strong>Submitted on:</strong> ${submittedAt}</p>
|
||||
|
||||
<div class="message-box">
|
||||
<p><strong>Message:</strong></p>
|
||||
<p>${message.replace(/\n/g, '<br>')}</p>
|
||||
</div>
|
||||
|
||||
<div class="meta">
|
||||
<p><strong>Additional Information:</strong></p>
|
||||
<p>IP Address: ${ipAddress || 'Not available'}</p>
|
||||
<p>User Agent: ${userAgent || 'Not available'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>This is an automated email from your website contact form.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
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.
|
||||
`;
|
||||
}
|
120
src/email-templates/user-confirmation.ts
Normal file
120
src/email-templates/user-confirmation.ts
Normal file
@@ -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 `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Thank you for contacting us</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
background-color: #f5f5f5;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.content {
|
||||
background-color: #ffffff;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
.message-box {
|
||||
background-color: #f9f9f9;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
border-left: 3px solid #28a745;
|
||||
margin: 15px 0;
|
||||
}
|
||||
.footer {
|
||||
font-size: 12px;
|
||||
color: #777;
|
||||
margin-top: 20px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h2>Thank you for contacting ${websiteName}</h2>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Dear ${name},</p>
|
||||
|
||||
<p>Thank you for reaching out to us. We have received your message and will get back to you as soon as possible.</p>
|
||||
|
||||
<div class="message-box">
|
||||
<p><strong>Your message (submitted on ${submittedAt}):</strong></p>
|
||||
<p>${message.replace(/\n/g, '<br>')}</p>
|
||||
</div>
|
||||
|
||||
<p>If you have any additional questions or information to provide, please feel free to reply to this email.</p>
|
||||
|
||||
<p>Best regards,<br>
|
||||
The ${websiteName} Team</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>If you did not submit this contact form, please disregard this email or contact us at ${contactEmail}.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
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}.
|
||||
`;
|
||||
}
|
259
src/pages/api/contact.ts
Normal file
259
src/pages/api/contact.ts
Normal file
@@ -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<string, string> = {};
|
||||
|
||||
// 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'
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
@@ -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'}`}
|
||||
/>
|
||||
|
||||
<!-- Features2 Widget ************** -->
|
||||
|
31
src/test-email.ts
Normal file
31
src/test-email.ts
Normal file
@@ -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);
|
||||
});
|
1
src/types.d.ts
vendored
1
src/types.d.ts
vendored
@@ -169,6 +169,7 @@ export interface Input {
|
||||
label?: string;
|
||||
autocomplete?: string;
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export interface Textarea {
|
||||
|
426
src/utils/email-handler.ts
Normal file
426
src/utils/email-handler.ts
Normal file
@@ -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<string, { token: string; expires: Date }>();
|
||||
|
||||
// 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<boolean> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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<boolean>((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');
|
||||
}
|
||||
});
|
||||
}
|
50
test-contact-curl.sh
Executable file
50
test-contact-curl.sh
Executable file
@@ -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."
|
133
test-contact-form.cjs
Normal file
133
test-contact-form.cjs
Normal file
@@ -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);
|
||||
});
|
31
test-email.ts
Normal file
31
test-email.ts
Normal file
@@ -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);
|
||||
});
|
88
test-smtp.js
Normal file
88
test-smtp.js
Normal file
@@ -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: '<p>This is a test email to verify SMTP configuration is working correctly.</p>'
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
Reference in New Issue
Block a user