Logo

dev-resources.site

for different kinds of informations.

Implementing Gmail API with Cloudflare Workers - Part 3: Implementation

Published at
11/23/2024
Categories
cloudflare
typescript
gmail
webdev
Author
roboword
Author
8 person written this
roboword
open
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 = "[email protected]"
SERVICE_ACCOUNT_EMAIL = "xxxxxxxxxxxxx.iam.gserviceaccount.com"
SERVICE_ACCOUNT_KEY = "your-private-key"
IMPERSONATED_USER = "[email protected]"
COMPANY_NAME = "Your Company Name"
COMPANY_EMAIL = "[email protected]"
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.

cloudflare Article's
30 articles in total
Favicon
Building a JAMStack App with Eleventy.js, CloudFlare Workers and AthenaHealth APIs - Part 2
Favicon
Building a JAMStack App with Eleventy.js, CloudFlare Workers and AthenaHealth APIs - Part 1
Favicon
How I Set Up My Custom Domain and Email for Substack
Favicon
Using Cloudflare SSL with Elastic Beanstalk instances
Favicon
Traefik using owned SSL certificate
Favicon
Traefik Cloudflare DNS Challenge
Favicon
Use Cloudflare Snippets to set up a Docker Registry Mirror
Favicon
Cloudflare PyPI Mirror
Favicon
Secure Self-Hosting with Cloudflare Tunnels and Docker: Zero Trust Security
Favicon
How to Process Incoming Emails and Trigger Webhooks, In-App Actions, and More Using Cloudflare Email Workers and D1 Database
Favicon
12 things I learned about hosting serverless sites on Cloudflare
Favicon
Dynamic DNS sync with Cloudflare
Favicon
Solusi Comment Reply WordPress Error Karena Rocket Loader Cloudflare
Favicon
Implementing Gmail Sending with Cloudflare Workers - Setup Guide
Favicon
Next.js Optimization for Dynamic Apps: Vercel Edge vs. Traditional SSR
Favicon
Building Vhisper: Voice Notes App with AI Transcription and Post-Processing
Favicon
Host Responded to 4 TCP SYN Probes on Port 24567 from Source Port 53(PCI DSS Cloudflare Resolved)
Favicon
Fighting with Redirects: A Journey of Astro Site Migration
Favicon
Using PostHog in Remix Loaders and Actions on Cloudflare Pages
Favicon
Implementing Gmail API with Cloudflare Workers - Part 3: Implementation
Favicon
Cloudflare Zaraz VS WP Complianz
Favicon
Implementing Gmail Sending with Cloudflare Workers - Development Guide
Favicon
Implementing Cloudflare Workflows
Favicon
Building Honcanator: The AI Goose Generator
Favicon
Retrieval Augmented Geese - Semantic Search with the HONC Stack
Favicon
Simplify serverless scaling and data management with Fauna and Cloudflare Workers
Favicon
[Cloudflare] Redirect To Another Domain
Favicon
Step-by-Step Guide to Hosting Your Website on a VPS Using Caddy Server and Cloudflare
Favicon
Building an AI Cron Builder with Cloudflare Pages and Next.js
Favicon
Cloudflare + Remix + PostgreSQL with Prisma Accelerate's Self Hosting

Featured ones: