In this article, I'll show you how to implement email sending functionality using Gmail API in Cloudflare Workers. This is part 3 of the series, focusing on the implementation details.
Implementation Steps
1. Configure wrangler.toml
First, set up your environment variables in wrangler.toml
. Store your service account key as an environment variable - never hardcode it in your source code.
name = "contact-form"
pages_build_output_dir = "./dist"
[vars]
ENVIRONMENT = "development"
BCC_EMAIL = "your-bcc@example.com"
SERVICE_ACCOUNT_EMAIL = "xxxxxxxxxxxxx.iam.gserviceaccount.com"
SERVICE_ACCOUNT_KEY = "your-private-key"
IMPERSONATED_USER = "your-email@example.com"
COMPANY_NAME = "Your Company Name"
COMPANY_EMAIL = "contact@example.com"
COMPANY_WEBSITE = "https://example.com"
EMAIL_SUBJECT = "Contact Form Submission"
2. Implement the Contact Form Handler
Here's the complete implementation of the contact form handler (contact-form.ts
):
export interface Env {
ENVIRONMENT: string;
BCC_EMAIL: string;
SERVICE_ACCOUNT_EMAIL: string;
SERVICE_ACCOUNT_KEY: string;
IMPERSONATED_USER: string;
COMPANY_NAME: string;
COMPANY_EMAIL: string;
COMPANY_WEBSITE: string;
EMAIL_SUBJECT: string;
}
function isLocalhost(env: Env) {
return env.ENVIRONMENT == 'development';
}
function conditionalLog(env: Env, message: string, ...args: unknown[]): void {
if (isLocalhost(env)) {
console.log(message, ...args);
}
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
if (request.method === 'OPTIONS') {
return handleOptionsRequest(env);
}
try {
if (request.method !== 'POST') {
return createResponse({ success: false, error: 'Only POST method is allowed' }, 405, env);
}
const formData = await request.formData();
const validation = validateRequest(formData);
if (!validation.isValid) {
return createResponse({ success: false, error: validation.error }, 400, env);
}
const emailContent = createEmailContent(formData, env);
const success = await sendEmail(formData, emailContent, env);
if (success) {
return createResponse({ success: true, message: 'Email sent successfully' }, 200, env);
} else {
throw new Error('Failed to send email');
}
} catch (error) {
console.error('Error in fetch:', error);
let errorMessage = 'An unexpected error occurred';
if (error instanceof Error) {
errorMessage = error.message;
}
return createResponse({ success: false, error: errorMessage }, 500, env);
}
}
}
function createResponse(body: any, status: number, env: Env): Response {
const headers: Record<string, string> = {
'Content-Type': 'application/json'
}
if (isLocalhost(env)) {
headers['Access-Control-Allow-Origin'] = '*'
headers['Access-Control-Allow-Methods'] = 'POST, OPTIONS'
headers['Access-Control-Allow-Headers'] = 'Content-Type'
} else {
headers['Access-Control-Allow-Origin'] = env.COMPANY_WEBSITE
headers['Access-Control-Allow-Methods'] = 'POST, OPTIONS'
headers['Access-Control-Allow-Headers'] = 'Content-Type'
}
return new Response(JSON.stringify(body), { status, headers })
}
function handleOptionsRequest(env: Env): Response {
return createResponse(null, 204, env)
}
function validateRequest(formData: FormData): { isValid: boolean; error?: string } {
const name = formData.get('name') as string
const email = formData.get('email') as string
const company = formData.get('company') as string
const message = formData.get('message') as string
if (!name || !email || !company || !message) {
return { isValid: false, error: 'Missing required fields' }
}
if (!validateEmail(email)) {
return { isValid: false, error: 'Invalid email address' }
}
return { isValid: true }
}
function validateEmail(email: string): boolean {
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailPattern.test(email)
}
function createEmailContent(formData: FormData, env: Env): string {
const name = formData.get('name') as string
const company = formData.get('company') as string
const message = formData.get('message') as string
return `
${company}
${name}
Thank you for contacting ${env.COMPANY_NAME}.
● Inquiry Details:
${message}
---------
While we may not be able to respond to all inquiries,
we assure you that we read every message we receive.
Thank you for your interest in our company.
${env.COMPANY_NAME}
`
}
function headersToArray(headers: Headers): [string, string][] {
const result: [string, string][] = []
headers.forEach((value, key) => {
result.push([key, value])
})
return result
}
async function sendEmail(formData: FormData, content: string, env: Env): Promise<boolean> {
try {
const accessToken = await getAccessToken(env)
const to = formData.get('email') as string
if (!to || !validateEmail(to)) {
throw new Error('Invalid email address')
}
const subject = `=?UTF-8?B?${base64Encode(env.EMAIL_SUBJECT)}?=`
const from = `=?UTF-8?B?${base64Encode(env.COMPANY_NAME)}?= <${env.COMPANY_EMAIL}>`
const bcc = env.BCC_EMAIL
const emailParts = [
`From: ${from}`,
`To: ${to}`,
`Subject: ${subject}`,
`Bcc: ${bcc}`,
'MIME-Version: 1.0',
'Content-Type: text/plain; charset=UTF-8',
'Content-Transfer-Encoding: base64',
'',
base64Encode(content)
]
const email = emailParts.join('\r\n')
const response = await fetch('https://www.googleapis.com/gmail/v1/users/me/messages/send', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ raw: base64UrlEncode(email) })
})
conditionalLog(env, `Gmail API Response Status: ${response.status} ${response.statusText}`)
conditionalLog(
env,
'Gmail API Response Headers:',
Object.fromEntries(headersToArray(response.headers))
)
const responseBody = await response.text()
conditionalLog(env, 'Gmail API Response Body:', responseBody)
if (!response.ok) {
throw new Error(
`Failed to send email: ${response.status} ${response.statusText}. Response: ${responseBody}`
)
}
return true
} catch (error) {
console.error('Error in sendEmail:', error)
throw error
}
}
3. Key Features
The implementation includes:
- Form validation
- OAuth 2.0 token generation
- Gmail API integration
- CORS handling
- Environment-based configuration
- Error handling and logging
- Email content creation with proper encoding
4. Important Technical Notes
Library Constraints: Cloudflare Workers has limitations on libraries. You can't use native Node.js modules; you must write browser-compatible code.
Authentication: The implementation uses service account authentication with JWT tokens.
CORS: The code includes proper CORS handling for both development and production environments.
Error Handling: Comprehensive error handling is implemented throughout the code.
Environment Variables: Variables are managed through Cloudflare's environment variable system.
5. Deployment and Testing
- Local Testing:
npx wrangler pages dev
- Production Deployment:
- Environment variables are automatically synced from wrangler.toml
- Change ENVIRONMENT to 'production' after deployment
6. Known Limitations
- VSCode debugger doesn't work with Pages Functions (unlike Workers)
- Environment variables management differs between Workers and Pages Functions
- Some Node.js modules and features are not available in the Workers runtime
Conclusion
This implementation provides a secure and scalable way to send emails using Gmail API through Cloudflare Workers. The code is designed to be maintainable, secure, and production-ready, with proper error handling and environment-specific configurations.
The complete source code can be found in the repository, along with detailed setup instructions from parts 1 and 2 of this series.
Top comments (0)