1+ import { NextResponse } from 'next/server' ;
2+ import nodemailer from 'nodemailer' ;
3+ import { z } from 'zod' ;
4+
5+ // Updated Zod schema - REMOVED jobFile as it will be handled separately
6+ const ContactFormSchema = z . object ( {
7+ // Core fields
8+ name : z . string ( ) . min ( 1 , { message : 'Name is required.' } ) ,
9+ email : z . string ( ) . email ( { message : 'Invalid email address.' } ) ,
10+ company : z . string ( ) . optional ( ) ,
11+ topic : z . string ( ) . min ( 1 , { message : 'Topic is required.' } ) ,
12+ message : z . string ( ) . min ( 1 , { message : 'Message cannot be empty.' } ) ,
13+ _gotcha : z . string ( ) . optional ( ) , // Honeypot field
14+
15+ // Optional conditional fields (matching frontend structure)
16+ jobRole : z . string ( ) . optional ( ) ,
17+ jobCompensation : z . string ( ) . optional ( ) ,
18+ jobWhyMe : z . string ( ) . optional ( ) ,
19+ // jobFile is removed from schema, handled directly from FormData
20+ freelanceOverview : z . string ( ) . optional ( ) ,
21+ freelanceTimeline : z . string ( ) . optional ( ) ,
22+ freelanceBudget : z . string ( ) . optional ( ) ,
23+ pressEventName : z . string ( ) . optional ( ) ,
24+ pressTopic : z . string ( ) . optional ( ) ,
25+ pressTiming : z . string ( ) . optional ( ) ,
26+ } ) ;
27+
28+ // Nodemailer transporter setup
29+ const transporter = nodemailer . createTransport ( {
30+ host : process . env . SMTP_HOST ,
31+ port : parseInt ( process . env . SMTP_PORT || '465' , 10 ) , // Default to 465 if not set
32+ secure : parseInt ( process . env . SMTP_PORT || '465' , 10 ) === 465 , // true for 465, false for other ports
33+ auth : {
34+ user : process . env . SMTP_USER ,
35+ pass : process . env . SMTP_PASS ,
36+ } ,
37+ } ) ;
38+
39+ // Helper function to build email body parts
40+ const buildEmailSection = ( label : string , value : string | undefined | null ) : string => {
41+ return value ? `${ label } : ${ value } \n` : '' ;
42+ } ;
43+
44+ const buildHtmlEmailSection = ( label : string , value : string | undefined | null ) : string => {
45+ if ( ! value ) return '' ;
46+ // Basic sanitization/escaping might be needed for HTML context depending on source
47+ const displayValue = value . replace ( / \n / g, '<br>' ) ;
48+ return `<p><strong>${ label } :</strong> ${ displayValue } </p>` ;
49+ } ;
50+
51+ export async function POST ( request : Request ) {
52+ // Check required environment variables
53+ if ( ! process . env . SMTP_HOST || ! process . env . SMTP_USER || ! process . env . SMTP_PASS || ! process . env . CONTACT_FORM_RECEIVER_EMAIL ) {
54+ console . error ( 'Missing required SMTP environment variables' ) ;
55+ return NextResponse . json ( { error : 'Server configuration error.' } , { status : 500 } ) ;
56+ }
57+
58+ try {
59+ // Read the request body as FormData instead of JSON
60+ const formData = await request . formData ( ) ;
61+
62+ // Extract text fields from FormData
63+ const body : Record < string , any > = { } ;
64+ for ( const [ key , value ] of formData . entries ( ) ) {
65+ // Skip the file field for Zod validation
66+ if ( key !== 'jobFile' ) {
67+ body [ key ] = value ;
68+ }
69+ }
70+
71+ // Extract the file (if any)
72+ const jobFile = formData . get ( 'jobFile' ) as File | null ;
73+
74+ // Validate honeypot first
75+ if ( body . _gotcha ) {
76+ console . log ( 'Honeypot field filled, likely a bot.' ) ;
77+ // Return a generic success to not alert the bot, but don't send email
78+ return NextResponse . json ( { success : true } ) ;
79+ }
80+
81+ // Validate the TEXT form data using the updated schema
82+ const validationResult = ContactFormSchema . safeParse ( body ) ;
83+
84+ if ( ! validationResult . success ) {
85+ console . error ( 'Validation Errors:' , validationResult . error . flatten ( ) ) ;
86+ return NextResponse . json (
87+ { error : 'Invalid form data' , details : validationResult . error . flatten ( ) . fieldErrors } ,
88+ { status : 400 } ,
89+ ) ;
90+ }
91+
92+ // Destructure all potential fields from the validated data
93+ const {
94+ name,
95+ email,
96+ message,
97+ topic,
98+ company,
99+ jobRole,
100+ jobCompensation,
101+ jobWhyMe,
102+ freelanceOverview,
103+ freelanceTimeline,
104+ freelanceBudget,
105+ pressEventName,
106+ pressTopic,
107+ pressTiming
108+ } = validationResult . data ;
109+
110+ // Construct email content dynamically based on provided fields
111+ let plainText = `Core Information:
112+ ` ;
113+ plainText += buildEmailSection ( 'Name' , name ) ;
114+ plainText += buildEmailSection ( 'Email' , email ) ;
115+ plainText += buildEmailSection ( 'Company' , company ) ;
116+ plainText += buildEmailSection ( 'Topic' , topic ) ;
117+ plainText += buildEmailSection ( 'Message' , message ) ;
118+
119+ let htmlText = `<h2>Core Information:</h2>` ;
120+ htmlText += buildHtmlEmailSection ( 'Name' , name ) ;
121+ htmlText += buildHtmlEmailSection ( 'Email' , `<a href="mailto:${ email } ">${ email } </a>` ) ; // Link email
122+ htmlText += buildHtmlEmailSection ( 'Company' , company ) ;
123+ htmlText += buildHtmlEmailSection ( 'Topic' , topic ) ;
124+ htmlText += buildHtmlEmailSection ( 'Message' , message ) ;
125+
126+ // Add conditional sections to the email
127+ if ( topic === 'Job Opportunity' ) {
128+ const section =
129+ buildEmailSection ( 'Role/Title' , jobRole ) +
130+ buildEmailSection ( 'Compensation' , jobCompensation ) +
131+ buildEmailSection ( 'Why Me?' , jobWhyMe ) ;
132+ if ( section ) {
133+ plainText += `
134+ Job Opportunity Details:
135+ ${ section } `;
136+ htmlText += `<hr><h2>Job Opportunity Details:</h2>` +
137+ buildHtmlEmailSection ( 'Role/Title' , jobRole ) +
138+ buildHtmlEmailSection ( 'Compensation' , jobCompensation ) +
139+ buildHtmlEmailSection ( 'Why Me?' , jobWhyMe ) ;
140+ }
141+ // Add note about attachment if file exists
142+ if ( jobFile ) {
143+ plainText += buildEmailSection ( 'Resume/JD' , 'See attachment' ) ;
144+ htmlText += buildHtmlEmailSection ( 'Resume/JD' , 'See attachment' ) ;
145+ }
146+ } else if ( topic === 'Freelance / Consulting' ) {
147+ const section =
148+ buildEmailSection ( 'Project Overview' , freelanceOverview ) +
149+ buildEmailSection ( 'Timeline' , freelanceTimeline ) +
150+ buildEmailSection ( 'Budget' , freelanceBudget ) ;
151+ if ( section ) {
152+ plainText += `
153+ Freelance / Consulting Details:
154+ ${ section } `;
155+ htmlText += `<hr><h2>Freelance / Consulting Details:</h2>` +
156+ buildHtmlEmailSection ( 'Project Overview' , freelanceOverview ) +
157+ buildHtmlEmailSection ( 'Timeline' , freelanceTimeline ) +
158+ buildHtmlEmailSection ( 'Budget' , freelanceBudget ) ;
159+ }
160+ } else if ( topic === 'Press / Speaking' ) {
161+ const section =
162+ buildEmailSection ( 'Event/Publication' , pressEventName ) +
163+ buildEmailSection ( 'Topic/Angle' , pressTopic ) +
164+ buildEmailSection ( 'Timing/Deadline' , pressTiming ) ;
165+ if ( section ) {
166+ plainText += `
167+ Press / Speaking Details:
168+ ${ section } `;
169+ htmlText += `<hr><h2>Press / Speaking Details:</h2>` +
170+ buildHtmlEmailSection ( 'Event/Publication' , pressEventName ) +
171+ buildHtmlEmailSection ( 'Topic/Angle' , pressTopic ) +
172+ buildHtmlEmailSection ( 'Timing/Deadline' , pressTiming ) ;
173+ }
174+ }
175+
176+
177+ // Prepare mail options
178+ const mailOptions : nodemailer . SendMailOptions = {
179+ from : `"${ name } " <${ process . env . SMTP_USER } >` , // Send FROM the configured user, but show sender's name
180+ replyTo : email , // Set Reply-To to the actual sender's email
181+ to : process . env . CONTACT_FORM_RECEIVER_EMAIL , // The email address handled by Forward Email
182+ subject : `[New Contact] from ${ name } about ${ topic } ` ,
183+ text : plainText . trim ( ) , // Trim whitespace
184+ html : htmlText , // Use dynamically generated HTML
185+ attachments : [ ] , // Initialize attachments array
186+ } ;
187+
188+ // Add attachment if file exists
189+ if ( jobFile ) {
190+ // Read file content into a buffer
191+ const fileBuffer = Buffer . from ( await jobFile . arrayBuffer ( ) ) ;
192+ mailOptions . attachments ?. push ( {
193+ filename : jobFile . name ,
194+ content : fileBuffer ,
195+ contentType : jobFile . type , // Use the content type provided by the browser
196+ } ) ;
197+ }
198+
199+
200+ try {
201+ const info = await transporter . sendMail ( mailOptions ) ;
202+ console . log ( 'Nodemailer Success: Message sent: %s' , info . messageId ) ;
203+ return NextResponse . json ( { success : true } ) ;
204+ } catch ( mailError ) {
205+ console . error ( 'Nodemailer Error sending mail:' , mailError ) ;
206+ return NextResponse . json ( { error : 'Failed to send message.' } , { status : 500 } ) ;
207+ }
208+
209+ } catch ( error ) {
210+ console . error ( 'API Route Error:' , error ) ;
211+ // Handle potential FormData parsing errors or other unexpected issues
212+ // Note: Specific error handling might need adjustment for FormData vs JSON
213+ return NextResponse . json ( { error : 'An unexpected error occurred.' } , { status : 500 } ) ;
214+ }
215+ }
0 commit comments