fix: switch contact form to use local SMTP (Maddy) instead of Mailgun

Changed contact form email delivery to use local Maddy SMTP server for better reliability.

**Changes:**
- Created new SMTP service (lib/smtp.ts) using nodemailer
- Configured to use localhost:25 (Maddy SMTP)
- Updated contact API to use smtpService instead of mailgunService
- Installed nodemailer and @types/nodemailer

**Benefits:**
- Simpler configuration (no external API dependencies)
- Local email delivery (more reliable for internal emails)
- No API rate limits or authentication issues
- Direct delivery to contact@biblical-guide.com

**Roundcube still uses Mailgun SMTP** for outgoing emails from webmail interface.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-10 14:05:16 +00:00
parent 30132bb534
commit 9158ffa637
4 changed files with 1521 additions and 3 deletions

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server'
import { mailgunService } from '@/lib/mailgun'
import { smtpService } from '@/lib/smtp'
import { z } from 'zod'
export const runtime = 'nodejs'
@@ -46,8 +46,8 @@ export async function POST(request: NextRequest) {
}, { status: 400 })
}
// Send email using Mailgun
const emailResult = await mailgunService.sendContactForm({
// Send email using local SMTP server (Maddy)
const emailResult = await smtpService.sendContactForm({
name,
email,
subject,

111
lib/smtp.ts Normal file
View File

@@ -0,0 +1,111 @@
import nodemailer from 'nodemailer'
interface EmailOptions {
to: string | string[]
subject: string
text?: string
html?: string
from?: string
replyTo?: string
}
interface SendEmailResult {
success: boolean
messageId?: string
error?: string
}
class SMTPService {
private transporter: any
constructor() {
// Configure nodemailer to use local Maddy SMTP server
this.transporter = nodemailer.createTransport({
host: 'localhost',
port: 25,
secure: false, // Use STARTTLS
tls: {
rejectUnauthorized: false // Accept self-signed certificates
},
// Maddy accepts mail on port 25 without authentication for localhost
requireTLS: false
})
}
async sendEmail(options: EmailOptions): Promise<SendEmailResult> {
try {
const mailOptions = {
from: options.from || 'Biblical Guide <no-reply@biblical-guide.com>',
to: Array.isArray(options.to) ? options.to.join(', ') : options.to,
subject: options.subject,
text: options.text,
html: options.html,
replyTo: options.replyTo
}
const info = await this.transporter.sendMail(mailOptions)
return {
success: true,
messageId: info.messageId
}
} catch (error) {
console.error('SMTP send error:', error)
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}
}
}
async sendContactForm(data: {
name: string
email: string
subject: string
message: string
}): Promise<SendEmailResult> {
const html = `
<h2>New Contact Form Submission</h2>
<p><strong>Name:</strong> ${data.name}</p>
<p><strong>Email:</strong> ${data.email}</p>
<p><strong>Subject:</strong> ${data.subject}</p>
<p><strong>Message:</strong></p>
<div style="background: #f5f5f5; padding: 15px; border-radius: 5px;">
${data.message.replace(/\n/g, '<br>')}
</div>
`
const text = `
New Contact Form Submission
Name: ${data.name}
Email: ${data.email}
Subject: ${data.subject}
Message:
${data.message}
`
return this.sendEmail({
to: 'contact@biblical-guide.com',
subject: `Contact Form: ${data.subject}`,
html,
text,
replyTo: data.email
})
}
async testConnection(): Promise<{ success: boolean; error?: string }> {
try {
await this.transporter.verify()
return { success: true }
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Connection failed'
}
}
}
}
export const smtpService = new SMTPService()

1405
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -67,6 +67,7 @@
"negotiator": "^1.0.0",
"next": "^15.5.3",
"next-intl": "^4.3.9",
"nodemailer": "^7.0.9",
"openai": "^5.22.0",
"pdf-parse": "^1.1.1",
"pg": "^8.16.3",
@@ -92,6 +93,7 @@
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/jsonwebtoken": "^9.0.10",
"@types/nodemailer": "^7.0.2",
"ignore-loader": "^0.1.2",
"tsx": "^4.20.5"
}