Contact form logic
This commit is contained in:
@@ -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');
|
||||
}
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user