Cloudflare Worker that serves sender-domain avatar images for Thunderbird webmail.
The service accepts a normalized domain on avatars.thunderbird.net and
always returns a PNG, falling back to a 1x1 transparent pixel on errors:
https://avatars.thunderbird.net/example.com?v=3
The optional v query parameter is a cache version chosen by the caller.
Changing it forces a fresh edge/browser cache entry when avatar lookup
behavior changes. It is not sent to upstream providers.
- Accepts only
GET,HEAD, andOPTIONS. Other methods get a 405 fallback. - Normalizes and validates the domain before making any upstream request.
- Fetches favicons only from
https://www.google.com/s2/favicons. Because Google reliably redirects this endpoint to its CDN, redirects are followed and the final response is required to come fromwww.google.comor a*.gstatic.comsubdomain over HTTPS. Baregstatic.comand anyhttp:downgrade are rejected. - Returns only PNG bodies up to 256 KiB, validating the PNG signature, chunk structure, IHDR/PLTE/IDAT/IEND ordering, color-type/bit-depth combinations, and CRC-32 of every chunk before responding or caching. Provider response headers are not trusted.
- Successful responses, validation failures, and Google 4xx responses are all
cached in Cloudflare's default Cache API under a versioned key
(
/__cache/<provider-version>/<domain>). Validation failures are negative- cached for one hour; transient network failures (502) are not cached. HEADrequests share theGETcache and populate it on miss, so aHEADcosts no more than aGETand never bypasses the edge.- Sends wildcard CORS headers,
X-Content-Type-Options: nosniff, andCross-Origin-Resource-Policy: cross-originon every PNG response. - Keeps Worker observability logging disabled.
- Google's favicon service responds with a generic "globe" placeholder for
domains it does not know about. The worker has no signal to distinguish
this from a real favicon and will cache the placeholder under the requested
domain for the configured TTL. Operators that want to fail-closed for
unknown domains should change the
vquery parameter or bumpAVATAR_CACHE_PROVIDER_VERSIONafter fixing the heuristic.
Use Node.js 24 or newer.
npm ci
npm run typecheck
npm testRun locally with Wrangler:
npx wrangler devThe production custom domain avatars.thunderbird.net is attached to the
Worker once in Cloudflare using human credentials. The steady-state GitHub
Actions deploy updates only the Worker script, so the CI token does not
need DNS or Worker route permissions.
Required repository secrets:
CLOUDFLARE_ACCOUNT_IDCLOUDFLARE_API_TOKEN
The deploy token is scoped with only Workers script write/edit permissions.