feat(02-02): install nodemailer and create contact mailer + server action

- Add nodemailer@^7.0.7 and @types/nodemailer (v7 required by next-auth peer dep)
- Create src/lib/contact-mailer.ts: Nodemailer SMTP transporter + sendContactEmail()
- Create src/lib/contact-action.ts: Server Action with Zod validation, honeypot check
- SMTP credentials read from CONTACT_EMAIL_USER/PASS/SMTP_HOST/PORT env vars
- Add CONTACT_* placeholder vars to .env.local (gitignored, for local setup docs)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Chandler Copeland
2026-03-19 14:59:23 -06:00
parent 0cdf3e5f7d
commit 39f233dbb4
4 changed files with 113 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
'use server'
import { z } from 'zod'
import { sendContactEmail } from './contact-mailer'
const ContactSchema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Valid email address required'),
phone: z.string().min(7, 'Phone number is required'),
message: z.string().min(10, 'Message must be at least 10 characters'),
website: z.string(), // honeypot — must remain empty
})
export type ContactState = {
status: 'idle' | 'success' | 'error'
message?: string
}
export async function submitContact(
_prev: ContactState,
formData: FormData,
): Promise<ContactState> {
const raw = {
name: formData.get('name'),
email: formData.get('email'),
phone: formData.get('phone'),
message: formData.get('message'),
website: formData.get('website') ?? '',
}
const parsed = ContactSchema.safeParse(raw)
if (!parsed.success) {
return { status: 'error', message: 'Please fill in all required fields correctly.' }
}
// Honeypot check — if filled, silently succeed (don't reveal rejection to bots)
if (parsed.data.website !== '') {
return { status: 'success' }
}
try {
await sendContactEmail(parsed.data)
return { status: 'success' }
} catch (err) {
console.error('Contact form email error:', err)
return { status: 'error', message: 'Something went wrong. Please try again or call directly.' }
}
}

View File

@@ -0,0 +1,31 @@
import nodemailer from 'nodemailer'
const transporter = nodemailer.createTransport({
host: process.env.CONTACT_SMTP_HOST,
port: Number(process.env.CONTACT_SMTP_PORT ?? 587),
secure: false, // STARTTLS on port 587
auth: {
user: process.env.CONTACT_EMAIL_USER,
pass: process.env.CONTACT_EMAIL_PASS,
},
})
export async function sendContactEmail(data: {
name: string
email: string
phone: string
message: string
}) {
await transporter.sendMail({
from: `"Teressa Copeland Homes Website" <${process.env.CONTACT_EMAIL_USER}>`,
replyTo: data.email,
to: process.env.CONTACT_EMAIL_USER,
subject: `New contact from ${data.name} — Teressa Copeland Homes`,
text: `Name: ${data.name}\nEmail: ${data.email}\nPhone: ${data.phone}\n\nMessage:\n${data.message}`,
html: `<p><strong>Name:</strong> ${data.name}</p>
<p><strong>Email:</strong> <a href="mailto:${data.email}">${data.email}</a></p>
<p><strong>Phone:</strong> ${data.phone}</p>
<p><strong>Message:</strong></p>
<p>${data.message.replace(/\n/g, '<br/>')}</p>`,
})
}