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 (2)
This is a really neat way to use Cloudflare Workers! As a Cloudways user who also works with APIs, I'm always interested in seeing how others use these tools.
If you're finding yourself working more with serverless functions and APIs, I'd definitely recommend checking out Cloudways.
missing the accessToken function