Skip to content

Convert-by-Email Service

The convert-by-email service lets registered customers email a document to convert@pdf-html.com and receive an accessible HTML version back as an email reply. No browser, no login, no software to install.

Address split: Inbound email goes to convert@pdf-html.com (Cloudflare Email Routing), but outbound replies come from convert@theaccessible.org (Gmail API). This is because theaccessible.org MX records point to Google Workspace and can’t be moved to Cloudflare without breaking existing email. pdf-html.com is used solely for Cloudflare Email Routing inbound. convert@theaccessible.org is configured as an alias on the admin’s Google Workspace account.

How It Works

User emails PDF/DOCX/HTML to convert@pdf-html.com
β”‚
β–Ό
Cloudflare Email Routing
β”‚ (MX records for pdf-html.com point to Cloudflare)
β”‚ (rule: convert@pdf-html.com β†’ convert-email Worker)
β–Ό
convert-email Worker (workers/convert/)
β”‚
β”œβ”€ 1. Parse MIME, extract attachment (postal-mime)
β”œβ”€ 2. Look up sender in Supabase (customers table)
β”œβ”€ 3. Validate file type, size (25MB max)
β”œβ”€ 4. Estimate pages, pre-check credits
β”œβ”€ 5. Send acknowledgment email via Gmail API
β”œβ”€ 6. Store source file in R2 (temporary)
β”œβ”€ 7. Call accessible-pdf-api via Service Binding
β”œβ”€ 8. Calculate actual credits (1 standard, 2 complex per page)
β”œβ”€ 9. Deduct credits in Supabase (atomic RPC)
β”œβ”€ 10. Send result email with accessible HTML attached
β”œβ”€ 11. Log job in Supabase (conversion_jobs table)
└─ 12. Clean up R2 (async)

Infrastructure

ComponentDetails
Workerconvert-email on Cloudflare Workers
Worker URLhttps://convert-email.larry-c6c.workers.dev
PipelineService Binding to accessible-pdf-api (no auth, no network hop)
R2 Bucketconvert-email-files (temporary file storage during processing)
SupabaseProject vuvwmfxssjosfphzpzim β€” customers and conversion_jobs tables
Outbound emailGmail API via Google Workspace service account with domain-wide delegation
Inbound emailCloudflare Email Routing: convert@pdf-html.com β†’ Worker

Source Files

All source is in workers/convert/src/:

FilePurpose
index.jsMain entry point. Handles the full email-to-reply flow. Also exposes /health and /test-send HTTP endpoints.
gmail.jsGmail API module. JWT signing with Web Crypto API, token exchange, MIME building, send/reply/attachment methods.
templates.jsBranded HTML email templates for every reply type (unregistered, no attachment, unsupported file, insufficient credits, processing started, success, error).
supabase.jsREST client for customer lookups, credit deduction (via atomic RPC), and job logging.
mime.jsMIME parsing via postal-mime, file type detection, base64 utilities.
credits.jsCredit calculation: 1 per standard page, 2 per complex page.

Gmail API Auth Chain

The Worker sends all outbound email via the Gmail API using a Google Workspace service account:

  1. Worker loads the service account JSON key from the GOOGLE_SERVICE_ACCOUNT_KEY secret
  2. Creates a JWT signed with the service account’s RSA private key (Web Crypto API β€” no npm dependencies)
  3. Exchanges the JWT for an access token at https://oauth2.googleapis.com/token
  4. Calls POST gmail.googleapis.com/gmail/v1/users/me/messages/send with a base64url-encoded MIME message
  5. Google sends the email as convert@pdf-html.com

The access token is cached in memory for up to 1 hour within a single Worker invocation.

Google Cloud setup (already configured)

  • Project: TheAccessibleOrg-Email
  • API: Gmail API enabled
  • Service account: convert-email-sender with a JSON key stored as a Worker secret

Google Admin Console (already configured)

  • Domain-wide delegation: The service account’s Client ID is authorized for scope https://www.googleapis.com/auth/gmail.send
  • This allows the service account to send email as any user in the domain

Supabase Schema

customers

ColumnTypeNotes
iduuidPrimary key
emailtextUnique, indexed β€” the sender’s email
nametext
plan_typetextfree or paid
credits_remainingintegerPaid tier only
lifetime_free_pages_usedintegerDefault 0, max 10
created_attimestamptz

conversion_jobs

ColumnTypeNotes
iduuidPrimary key
customer_iduuidFK to customers
sender_emailtext
filenametext
file_typetextpdf, docx, html
file_size_bytesinteger
total_pagesinteger
standard_pagesinteger
complex_pagesinteger
credits_chargedinteger
statustextsuccess or error
error_messagetextNullable
created_attimestamptz

RPC Functions

  • deduct_credits(p_customer_id, p_amount) β€” Atomic credit deduction. Raises exception if insufficient.
  • use_free_pages(p_customer_id, p_pages) β€” Atomic free page usage. Raises exception if lifetime limit (10) exceeded.

Credit System

Page typeCredits
Standard (text, images, simple lists)1 per page
Complex (equations, tables, forms, multi-column)2 per page

Free tier: 10 lifetime pages (complex pages count as 2 from the allocation).

The Worker estimates page count from file size before processing (for pre-check), then uses the actual page classification returned by the pipeline for the final charge.

Secrets

Set via npx wrangler secret put <NAME> from the workers/convert/ directory:

SecretPurpose
GOOGLE_SERVICE_ACCOUNT_KEYFull JSON contents of the Google service account key file
SUPABASE_SERVICE_KEYSupabase service_role key for server-side access

The PIPELINE_API_KEY secret is not needed β€” the pipeline is called via a Cloudflare Service Binding which bypasses auth.

Email Routing Setup

In the Cloudflare dashboard for pdf-html.com:

  1. Email > Email Routing > Routing Rules
  2. Add rule: convert@pdf-html.com β†’ Send to Worker β†’ convert-email

MX records for pdf-html.com must point to Cloudflare (configured automatically when Email Routing is enabled).

Deployment

Terminal window
cd workers/convert
npm install
npx wrangler deploy

Testing

Health check

Terminal window
curl https://convert-email.larry-c6c.workers.dev/health

Test Gmail sending

Terminal window
curl -X POST https://convert-email.larry-c6c.workers.dev/test-send \
-H "Content-Type: application/json" \
-d '{"to":"larry@anglin.com","subject":"Test","body":"Hello from convert-email"}'

Full flow test

Send an email with a PDF attached to convert@pdf-html.com from an email address that exists in the customers table.

Error Handling

  • Unregistered sender: Gets a branded reply explaining how to register.
  • No attachment: Gets a reply asking them to attach a file.
  • Unsupported file type: Gets a reply listing supported formats (PDF, DOCX, HTML).
  • File too large: Gets an error reply (25MB limit).
  • Insufficient credits: Gets a reply with their balance and a link to purchase more.
  • Pipeline failure: Gets an error reply. No credits charged. Error logged to conversion_jobs.
  • Gmail send failure: Logged to console. The Worker catches and logs notification failures separately so they don’t mask the original error.

Reusing GmailSender in Other Workers

The GmailSender class in src/gmail.js is a standalone module. To use it in another Worker:

  1. Copy gmail.js into the other Worker
  2. Add the same two secrets (GOOGLE_SERVICE_ACCOUNT_KEY, GMAIL_SENDER)
  3. Import and call new GmailSender(env).send(to, subject, text, html)

No additional Google configuration needed β€” the service account has domain-wide delegation.