1
1
import { NextRequest , NextResponse } from "next/server"
2
+ import { SendRawEmailCommand , SESClient } from "@aws-sdk/client-ses"
2
3
3
4
const ENTERPRISE_EMAIL = "[email protected] "
5
+ const SES_FROM_EMAIL = "[email protected] "
4
6
const RATE_LIMIT_WINDOW_MS = 60 * 1000 // 1 minute
5
7
const MAX_REQUESTS_PER_WINDOW = 3
6
8
7
- // Simple in-memory rate limiting (in production, use Redis or similar)
8
- const ipRequestHistory = new Map < string , number [ ] > ( )
9
+ // Configure SES client
10
+ const sesClient = new SESClient ( {
11
+ region : process . env . SES_REGION || "us-east-1" ,
12
+ credentials : {
13
+ accessKeyId : process . env . SES_ACCESS_KEY_ID ! ,
14
+ secretAccessKey : process . env . SES_SECRET_ACCESS_KEY ! ,
15
+ } ,
16
+ } )
9
17
10
- function getClientIP ( request : NextRequest ) : string {
11
- const forwarded = request . headers . get ( "x-forwarded-for" )
12
- const realIP = request . headers . get ( "x-real-ip" )
18
+ // Log the region being used for debugging
19
+ console . log ( "Using AWS SES region:" , process . env . SES_REGION || "us-east-1" )
13
20
14
- if ( forwarded ) return forwarded . split ( "," ) [ 0 ] . trim ( )
15
- if ( realIP ) return realIP
16
- return "unknown"
17
- }
21
+ // Simple in-memory rate limiting (in production, use Redis or similar)
22
+ const requestHistory = new Map < string , number [ ] > ( )
18
23
19
- function isRateLimited ( ip : string ) : boolean {
24
+ function isRateLimited ( identifier : string ) : boolean {
20
25
const now = Date . now ( )
21
- const requests = ipRequestHistory . get ( ip ) || [ ]
26
+ const requests = requestHistory . get ( identifier ) || [ ]
22
27
23
28
// Filter out requests outside the current window
24
29
const recentRequests = requests . filter (
25
30
( timestamp ) => now - timestamp < RATE_LIMIT_WINDOW_MS
26
31
)
27
32
28
33
// Update the history
29
- ipRequestHistory . set ( ip , recentRequests )
34
+ requestHistory . set ( identifier , recentRequests )
30
35
31
36
// Check if we're over the limit
32
37
if ( recentRequests . length >= MAX_REQUESTS_PER_WINDOW ) return true
33
38
34
39
// Add this request
35
40
recentRequests . push ( now )
36
- ipRequestHistory . set ( ip , recentRequests )
41
+ requestHistory . set ( identifier , recentRequests )
37
42
38
43
return false
39
44
}
@@ -53,12 +58,72 @@ function validateEmail(email: string): boolean {
53
58
return emailRegex . test ( email )
54
59
}
55
60
61
+ function createRawEmail (
62
+ fromEmail : string ,
63
+ toEmail : string ,
64
+ replyToEmail : string ,
65
+ subject : string ,
66
+ textBody : string
67
+ ) : string {
68
+ const boundary = `----=_Part_${ Date . now ( ) } _${ Math . random ( ) . toString ( 36 ) } `
69
+
70
+ return [
71
+ `From: ${ fromEmail } ` ,
72
+ `To: ${ toEmail } ` ,
73
+ `Reply-To: ${ replyToEmail } ` ,
74
+ `Subject: ${ subject } ` ,
75
+ `MIME-Version: 1.0` ,
76
+ `Content-Type: multipart/alternative; boundary="${ boundary } "` ,
77
+ `` ,
78
+ `--${ boundary } ` ,
79
+ `Content-Type: text/plain; charset=UTF-8` ,
80
+ `Content-Transfer-Encoding: 7bit` ,
81
+ `` ,
82
+ textBody ,
83
+ `` ,
84
+ `--${ boundary } --` ,
85
+ ] . join ( "\r\n" )
86
+ }
87
+
88
+ async function sendEmail ( userEmail : string , message : string ) : Promise < void > {
89
+ const subject = "Enterprise Inquiry from ethereum.org"
90
+ const textBody = `
91
+ New enterprise inquiry received:
92
+
93
+ From: ${ userEmail }
94
+ Timestamp: ${ new Date ( ) . toISOString ( ) }
95
+
96
+ Message:
97
+ ${ message }
98
+
99
+ ---
100
+ This message was sent via the enterprise contact form on ethereum.org/enterprise.
101
+ Reply to this email to respond directly to the sender.
102
+ ` . trim ( )
103
+
104
+ const rawEmail = createRawEmail (
105
+ SES_FROM_EMAIL ,
106
+ ENTERPRISE_EMAIL ,
107
+ userEmail ,
108
+ subject ,
109
+ textBody
110
+ )
111
+
112
+ const command = new SendRawEmailCommand ( {
113
+ RawMessage : {
114
+ Data : new TextEncoder ( ) . encode ( rawEmail ) ,
115
+ } ,
116
+ } )
117
+
118
+ await sesClient . send ( command )
119
+ }
120
+
56
121
export async function POST ( request : NextRequest ) {
57
122
try {
58
- const clientIP = getClientIP ( request )
123
+ // Rate limiting based on a simple identifier (could be improved with real session tracking)
124
+ const userAgent = request . headers . get ( "user-agent" ) || "unknown"
59
125
60
- // Rate limiting
61
- if ( isRateLimited ( clientIP ) ) {
126
+ if ( isRateLimited ( userAgent ) ) {
62
127
return NextResponse . json (
63
128
{ error : "Too many requests. Please try again later." } ,
64
129
{ status : 429 }
@@ -88,56 +153,15 @@ export async function POST(request: NextRequest) {
88
153
)
89
154
}
90
155
91
- // Create email content
92
- const emailSubject = "Enterprise Inquiry from ethereum.org"
93
- const emailBody = `
94
- New enterprise inquiry received:
95
-
96
- From: ${ sanitizedEmail }
97
- IP: ${ clientIP }
98
- Timestamp: ${ new Date ( ) . toISOString ( ) }
99
-
100
- Message:
101
- ${ sanitizedMessage }
102
-
103
- ---
104
- This message was sent via the enterprise contact form on ethereum.org/enterprise.
105
- ` . trim ( )
106
-
107
- // Submit to Netlify Forms
156
+ // Send email via AWS SES
108
157
try {
109
- const formData = new URLSearchParams ( )
110
- formData . append ( "form-name" , "enterprise-contact" )
111
- formData . append ( "email" , sanitizedEmail )
112
- formData . append ( "message" , sanitizedMessage )
113
- formData . append ( "subject" , emailSubject )
114
- formData . append ( "ip" , clientIP )
115
- formData . append ( "timestamp" , new Date ( ) . toISOString ( ) )
116
-
117
- const netlifyResponse = await fetch ( "/__forms.html" , {
118
- method : "POST" ,
119
- headers : {
120
- "Content-Type" : "application/x-www-form-urlencoded" ,
121
- } ,
122
- body : formData . toString ( ) ,
123
- } )
124
-
125
- if ( ! netlifyResponse . ok ) {
126
- throw new Error ( `Netlify Forms error: ${ netlifyResponse . status } ` )
127
- }
128
- } catch ( netlifyError ) {
129
- console . error ( "Netlify Forms submission failed:" , netlifyError )
130
- // Log the submission details for manual follow-up
131
- console . log ( "Enterprise Contact Form Submission (Netlify failed):" , {
132
- to : ENTERPRISE_EMAIL ,
133
- subject : emailSubject ,
134
- body : emailBody ,
135
- from : sanitizedEmail ,
136
- ip : clientIP ,
137
- timestamp : new Date ( ) . toISOString ( ) ,
138
- } )
139
- // Continue without throwing - we don't want to show user an error
140
- // if Netlify is down but the form validation passed
158
+ await sendEmail ( sanitizedEmail , sanitizedMessage )
159
+ } catch ( emailError ) {
160
+ console . error ( "AWS SES email sending failed:" , emailError )
161
+ return NextResponse . json (
162
+ { error : "Failed to send message. Please try again later." } ,
163
+ { status : 500 }
164
+ )
141
165
}
142
166
143
167
return NextResponse . json (
0 commit comments