Magically remove the proxy addresses from email links and images.
Mail providers rewrite the URLs in a message to point at their own servers. Gmail turns an image
URL into ci3.googleusercontent.com/meips/…, keeping the original after a #:
https://ci3.googleusercontent.com/meips/ADKq_NY…=s0-d-e1-ft#https://internal.example.com/x.png
└──── the original, intact ────┘
When the original lives somewhere the provider's servers cannot reach — a VPN-only host, an internal staging environment — the image simply never loads. Unproxy sends the request straight to the original instead.
Unwrapping happens at the network layer, via declarativeNetRequest. Unproxy has no content
script by default and cannot read your email. That property is the reason for the architecture,
not a side effect of it.
| Path | Contents |
|---|---|
chrome/ |
The extension itself — see its README for install and usage |
docs/adr/ |
Architecture decisions, with the experiments behind them |
PRIVACY.md |
Privacy policy, as published for the Web Store listing |
.github/workflows/ |
CI and release automation |
Everything runs from chrome/. There are no runtime dependencies and no build step; the two
devDependencies are ESLint and its globals list.
cd chrome
npm ci
npm run lint # ESLint
npm run lint:fix # …and fix what can be fixed
npm test # unit tests, plain node, no browser needed
npm run build # package dist/unproxy-<version>.zip
npm run icons # rebuild PNG icons from assets/logo.svg (needs rsvg-convert)The tests cover URL parsing and rule compilation, which is all of the logic that runs without a
browser. They deliberately include the OAuth-callback case: a rule that redirected ?redirect_uri=…
would break logins across the web, so that must stay impossible.
To load the extension while working on it, open chrome://extensions, turn on Developer mode,
choose Load unpacked, and pick the chrome/ folder.
ci.yml runs on every push to main and every pull request:
- Lint — ESLint over
src/,options/,test/andtools/ - Unit tests —
node test/rules.test.mjs - Build — produces the store zip
- Validate manifest —
tools/check-manifest.mjschecks the things the Web Store otherwise only reports at upload time: version format, manifest/package version drift, missing icons, a missing service worker or options page, an over-long description
The built zip is uploaded as a workflow artifact, so any commit's package can be downloaded and loaded unpacked without building it locally.
release.yml publishes to the Chrome Web Store when a v* tag is
pushed. It re-runs lint, tests and the manifest check first — a tag can point at a commit CI never
ran on, so those gates are not redundant.
# 1. Bump the version in BOTH chrome/manifest.json and chrome/package.json.
# The build fails on drift, deliberately: a mismatch would ship a version
# the store rejects as already published.
# 2. Commit, then tag.
git tag v0.2.0
git push origin v0.2.0The tag must match manifest.json's version or the job refuses to publish, rather than failing
halfway through a release.
--auto-publish submits for review; it does not put the update live immediately. Every release
still goes through Google's review queue.
Unproxy collects nothing, transmits nothing, and has no server. See PRIVACY.md.
MIT — see LICENSE.