DEV Community

Cover image for Implementing Gmail API with Cloudflare Workers - Part 3: Implementation
Taka Saito
Taka Saito

Posted on

Implementing Gmail API with Cloudflare Workers - Part 3: Implementation

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"
Enter fullscreen mode Exit fullscreen mode

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
    }
}

Enter fullscreen mode Exit fullscreen mode

3. Key Features

The implementation includes:

  1. Form validation
  2. OAuth 2.0 token generation
  3. Gmail API integration
  4. CORS handling
  5. Environment-based configuration
  6. Error handling and logging
  7. Email content creation with proper encoding

4. Important Technical Notes

  1. Library Constraints: Cloudflare Workers has limitations on libraries. You can't use native Node.js modules; you must write browser-compatible code.

  2. Authentication: The implementation uses service account authentication with JWT tokens.

  3. CORS: The code includes proper CORS handling for both development and production environments.

  4. Error Handling: Comprehensive error handling is implemented throughout the code.

  5. Environment Variables: Variables are managed through Cloudflare's environment variable system.

5. Deployment and Testing

  1. Local Testing:
npx wrangler pages dev
Enter fullscreen mode Exit fullscreen mode
  1. Production Deployment:
  2. Environment variables are automatically synced from wrangler.toml
  3. Change ENVIRONMENT to 'production' after deployment

6. Known Limitations

  1. VSCode debugger doesn't work with Pages Functions (unlike Workers)
  2. Environment variables management differs between Workers and Pages Functions
  3. 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)