1. Amazon SES Identity Verification
In the AWS SES Sandbox environment, both sender and recipient email addresses must be explicitly verified before sending messages.
aws ses verify-email-identity \
--email-address your-email@example.com \
--region us-west-2
2. IAM Permissions Policy for Amazon SES
Grant the Lambda execution role authorization to dispatch emails via Amazon SES.
cat <<EOF > lambda-ses-policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ses:SendEmail",
"ses:SendRawEmail"
],
"Resource": "*"
}
]
}
EOF
aws iam put-role-policy \
--role-name visitor-counter-lambda-role \
--policy-name LambdaSESAccess \
--policy-document file://lambda-ses-policy.json
3. Lambda Contact Handler Code
This Python function handles preflight CORS OPTIONS requests, validates payload fields, and sends formatted email notifications via boto3.
cat << 'EOF' > contact_function.py
import json
import boto3
SENDER_EMAIL = "your-email@example.com"
RECIPIENT_EMAIL = "your-email@example.com"
ses_client = boto3.client('ses', region_name='us-west-2')
def lambda_handler(event, context):
cors_headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key',
'Access-Control-Allow-Methods': 'POST,OPTIONS'
}
# Handle CORS preflight OPTIONS request
if event.get('requestContext', {}).get('http', {}).get('method') == 'OPTIONS':
return {'statusCode': 200, 'headers': cors_headers, 'body': ''}
try:
body = json.loads(event.get('body', '{}'))
name = body.get('name', '').strip()
sender = body.get('email', '').strip()
message = body.get('message', '').strip()
if not name or not sender or not message:
return {
'statusCode': 400,
'headers': cors_headers,
'body': json.dumps({'error': 'Missing required fields.'})
}
subject = f"New Portfolio Contact from {name}"
body_text = f"Name: {name}\nEmail: {sender}\n\nMessage:\n{message}"
ses_client.send_email(
Source=SENDER_EMAIL,
Destination={'ToAddresses': [RECIPIENT_EMAIL]},
Message={
'Subject': {'Data': subject},
'Body': {'Text': {'Data': body_text}}
},
ReplyToAddresses=[sender]
)
return {
'statusCode': 200,
'headers': cors_headers,
'body': json.dumps({'message': 'Message sent successfully!'})
}
except Exception as e:
return {
'statusCode': 500,
'headers': cors_headers,
'body': json.dumps({'error': 'Internal server error', 'details': str(e)})
}
EOF
zip contact_function.zip contact_function.py
ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text)
aws lambda create-function \
--function-name send-contact-email \
--runtime python3.12 \
--role arn:aws:iam::${ACCOUNT_ID}:role/visitor-counter-lambda-role \
--handler contact_function.lambda_handler \
--zip-file fileb://contact_function.zip \
--timeout 10 \
--region us-west-2
4. HTTP API Gateway Route & Integration
Expose the function under the POST /api/contact path on your existing HTTP API Gateway instance.
# 1. Create API Integration
INTEGRATION_ID=$(aws apigatewayv2 create-integration \
--api-id oasi55hqkb \
--integration-type AWS_PROXY \
--integration-uri arn:aws:lambda:us-west-2:${ACCOUNT_ID}:function:send-contact-email \
--payload-format-version 2.0 \
--region us-west-2 \
--query "IntegrationId" --output text)
# 2. Add POST /api/contact Route
aws apigatewayv2 create-route \
--api-id oasi55hqkb \
--route-key 'POST /api/contact' \
--target "integrations/${INTEGRATION_ID}" \
--region us-west-2
# 3. Grant Invoke Permission
aws lambda add-permission \
--function-name send-contact-email \
--statement-id apigateway-contact-perm \
--action lambda:InvokeFunction \
--principal apigateway.amazonaws.com \
--source-arn "arn:aws:execute-api:us-west-2:${ACCOUNT_ID}:oasi55hqkb/*/*" \
--region us-west-2
5. Frontend HTML Structure & CSS Styling
Add the styled container to your site markup and extend your stylesheet for focus states, input fields, and status feedback colors.
HTML Form Markup:
<div class="contact-form-container">
<form id="contact-form">
<div class="form-field">
<label for="name">Name</label>
<input type="text" id="name" placeholder="John Doe" required />
</div>
<div class="form-field">
<label for="email">Email</label>
<input type="email" id="email" placeholder="john@example.com" required />
</div>
<div class="form-field">
<label for="message">Message</label>
<textarea id="message" placeholder="Write your message here..." required></textarea>
</div>
<button type="submit" id="submit-btn">Send Message</button>
<p id="form-status"></p>
</form>
</div>
CSS Styling (styles.css / blog.css):
.contact-form-container {
max-width: 600px;
margin: 2rem 0;
padding: 2rem;
background-color: #0f172a;
border: 1px solid #1e293b;
border-radius: 12px;
}
#contact-form {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.form-field {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-field label {
font-family: 'Plus Jakarta Sans', sans-serif;
font-size: 0.85rem;
font-weight: 600;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.05em;
}
#contact-form input,
#contact-form textarea {
width: 100%;
padding: 0.875rem 1rem;
font-family: 'Plus Jakarta Sans', sans-serif;
font-size: 0.95rem;
color: #f8fafc;
background-color: #1e293b;
border: 1px solid #334155;
border-radius: 8px;
outline: none;
transition: all 0.2s ease-in-out;
box-sizing: border-box;
}
#contact-form input:focus,
#contact-form textarea:focus {
border-color: #38bdf8;
box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.15);
background-color: #0f172a;
}
#submit-btn {
align-self: flex-start;
padding: 0.875rem 1.75rem;
font-family: 'Plus Jakarta Sans', sans-serif;
font-weight: 600;
font-size: 0.95rem;
color: #0f172a;
background-color: #38bdf8;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease-in-out;
}
#submit-btn:hover {
background-color: #7dd3fc;
transform: translateY(-1px);
}
#submit-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
#form-status {
font-family: 'JetBrains Mono', monospace;
font-size: 0.875rem;
margin-top: 0.5rem;
min-height: 1.25rem;
}
#form-status.success { color: #4ade80; }
#form-status.error { color: #f87171; }
6. JavaScript Form Handler (`contact.js`)
Encapsulate form submission logic within a DOMContentLoaded wrapper to prevent execution delays on DOM elements.
document.addEventListener("DOMContentLoaded", () => {
const form = document.getElementById("contact-form");
if (!form) return;
form.addEventListener("submit", async (e) => {
e.preventDefault();
const statusEl = document.getElementById("form-status");
const submitBtn = document.getElementById("submit-btn");
statusEl.className = "";
statusEl.innerText = "Sending...";
submitBtn.disabled = true;
const payload = {
name: document.getElementById("name").value,
email: document.getElementById("email").value,
message: document.getElementById("message").value
};
try {
const response = await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
const result = await response.json();
if (response.ok) {
statusEl.className = "success";
statusEl.innerText = "Message sent successfully!";
form.reset();
} else {
statusEl.className = "error";
statusEl.innerText = result.error || "Failed to send message.";
}
} catch (err) {
console.error("Contact Form Error:", err);
statusEl.className = "error";
statusEl.innerText = "Error sending message. Please try again.";
} finally {
submitBtn.disabled = false;
}
});
});
7. Deployment & Cache Invalidation
Deploy asset updates to S3 and invalidate CloudFront edge caches.
aws cloudfront create-invalidation \
--distribution-id d1nrzvrxt6zac1 \
--paths "/*"
8. Architectural Insights & Troubleshooting
- CORS Preflight (OPTIONS): Browsers automatically send an
OPTIONSrequest prior to cross-originPOSTrequests with JSON payloads. Lambda must handle this explicitly and return 200 OK with allowed headers. - CloudFront Path Alignment: Since CloudFront proxies the
/api/*pattern to API Gateway, the API Gateway route must match the forwarded path (POST /api/contact). - DOM Ready Execution: When storing JavaScript in dedicated script files, wrap execution logic inside
DOMContentLoadedor use thedeferattribute to prevent target elements from evaluating tonull.