Skip to content

Convert-by-Email Service

The convert-by-email service lets registered customers email a PDF to convert@theaccessible.org and receive an accessible HTML version back as an email reply. No browser, no login, no software to install. PDF is the only accepted input format.

Address routing: The user-facing address is convert@theaccessible.org. A Google Workspace default-routing rule rewrites the envelope recipient to convert@pdf-html.com, which has its MX records pointing at Cloudflare Email Routing. CF then dispatches to the convert-email Worker. Replies are sent from convert@theaccessible.org via the Gmail API. The two addresses exist because theaccessible.org MX is on Google Workspace and can’t be moved to Cloudflare without breaking the rest of the org’s mail; pdf-html.com is a dedicated zone used solely as an inbound landing pad for the Worker.

How It Works

User emails PDF to convert@theaccessible.org
β”‚
β–Ό
Google Workspace (theaccessible.org)
β”‚ Default-routing rule: convert@theaccessible.org
β”‚ β†’ "Change envelope recipient" β†’ convert@pdf-html.com
β–Ό
Cloudflare Email Routing (pdf-html.com)
β”‚ (MX records for pdf-html.com point to Cloudflare)
β”‚ (rule: convert@pdf-html.com β†’ convert-email Worker)
β–Ό
convert-email Worker (workers/convert-email/)
β”‚
β”œβ”€ 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 (flat 1 credit 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 emailUser-facing: convert@theaccessible.org (Google Workspace alias) β†’ convert@pdf-html.com (Cloudflare Email Routing) β†’ 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@theaccessible.org

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

Every page costs 1 credit, including complex pages (equations, tables, forms, multi-column) β€” the high-fidelity 2x multiplier was retired 2026-06-12 (HIGH_FIDELITY_CREDIT_MULTIPLIER = 1).

Free tier: 10 lifetime pages.

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

Two pieces β€” Google Workspace alias, then Cloudflare dispatch.

Google Workspace (theaccessible.org):

  1. Admin console β†’ Apps β†’ Google Workspace β†’ Gmail β†’ Default routing β†’ Add setting.
  2. Envelope filter: β€œOnly affect specific envelope recipients” β†’ exact match convert@theaccessible.org.
  3. Action: Change envelope recipient β†’ convert@pdf-html.com.
  4. Save. No mailbox/alias/group needed at the Google end β€” the rule accepts unknown recipients.

Cloudflare (pdf-html.com):

  1. Dashboard β†’ pdf-html.com β†’ Email β†’ Email Routing β†’ Routing Rules.
  2. Add rule: convert@pdf-html.com β†’ Send to Worker β†’ convert-email.
  3. 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@theaccessible.org 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 PDF.
  • Unsupported file type: Gets a reply explaining that only PDF files are accepted via email.
  • 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.