-
Notifications
You must be signed in to change notification settings - Fork 0
Feature Remote images
English · Tiếng Việt · 中文
Since 1.11.0 SkimMail can show the images a message links to. It fetches them itself — through the same egress that account uses for mail — so the sender's server is talking to your instance and never to your browser. Nothing is fetched until you ask: every message still arrives with its remote images withheld.
A remote image in an email is a receipt. The moment your mail client loads it, the sender learns the message was opened, roughly when, and from roughly where. That is why SkimMail has always stripped them.
What it did not have was a way to change your mind. A shipping confirmation that is nothing but images, a ticket, a scanned invoice — all unreadable, with no button anywhere. 1.11.0 keeps the default (withhold) and adds the way out: per message, per sender, and an instance-wide switch for the operator who wants none of it. In 1.11.0 half of that lived only in the API; since 1.11.1 every control has a screen.
| Decision | Who can set it | How far it reaches | Beaten by |
|---|---|---|---|
| Remote images off for the instance | owner |
every user, every message | nothing |
never for a sender |
operator, owner
|
that user's mail from that sender | the instance switch |
always for a sender |
operator, owner
|
that user's mail from that sender | the instance switch, never
|
| Ask — the default | any signed-in reader | one message, until you move away | everything above |
The order is not arbitrary:
- The instance switch wins over everything, including a sender you personally marked "always". An administrator who turns remote images off has made a privacy decision for the whole instance, and a preference saved last month must not quietly reopen it.
-
neverbeatsalways. Between two standing decisions, the one that fetches nothing is the safe way to be wrong. - "Show images" on one message is never written down. Showing an image once is not a standing grant to a sender nobody has vouched for — that is what the second button is for. Open another message and it resets.
Per-sender decisions belong to the user who made them. In AUTH_MODE=users,
two people can disagree about the same newsletter.
When a message has remote images and no standing decision covers it, the reading pane shows a bar above the body:
🛡 Remote images blocked · Show images · Always from this sender · Never from this sender
Show images reloads that one body with the images routed through SkimMail's proxy. It applies to the message you are looking at, in this browser, until you open another one.
If the instance switch is off there is no button — only a line saying so,
because there is nothing you could press that would work. If the sender is on
your never list you get a different line, with a way back; see below.
Always from this sender records a standing decision for that sender address and loads images immediately, on this and every later message from them. Never from this sender, beside it since 1.11.1, records the opposite: nothing from that address is ever fetched again.
A sender you have refused shows this instead of the buttons:
🛡 Images from this sender are never loaded · Change this
Change this moves the sender from never to always — it is an undo for a
decision made in one careless click, not a reset. Sending them back to asking
each time is the one thing with no button; it is "" over the API, below.
Three things worth knowing:
- Both buttons need the
operatororownerrole. On avieweraccount the button is drawn but the request is refused, and the one-off reveal stays as it was. See Users and roles. - The decision is keyed on the From address, lower-cased. It is not a
domain rule and not a pattern:
news@example.comandnoreply@example.comare two separate decisions. - Per-sender decisions belong to the user who made them, so under
AUTH_MODE=usersyourneverlist is yours alone.
Before 1.17.0, that lookup compared against the wrong string for any sender
with a display name — which is nearly all of them. It read the message's
whole From field, Name <addr@host>, and checked it against the bare
address stored on the sender record, so a standing never (or always)
silently did nothing for those messages — they fell back to asking every
time, the same as a sender with no decision at all. Fixed in 1.17.0
(SKIMMAIL-189): the lookup now extracts the address first, the same way
setting the policy always did.
To go back to asking each time, or to script any of this, use the API. Sign in first and keep the session cookie:
curl -sS -c /tmp/skim.cookies -H 'Content-Type: application/json' \
-d '{"username":"owner","password":"YOUR-PASSWORD"}' \
http://localhost:8080/api/auth/loginIn passphrase mode the username is ignored; send the passphrase as password.
If two-factor authentication is enrolled, this call returns a challenge and the
browser is the easier path.
Then set the standing decision for one sender:
curl -sS -b /tmp/skim.cookies -X POST -H 'Content-Type: application/json' \
-d '{"policy":"never"}' \
http://localhost:8080/api/senders/newsletter@example.com/images"policy" takes "always", "never", or "" to go back to asking each time.
The empty string is the only one of the three with no button in the interface.
The instance switch is on by default — "on" meaning readers are allowed to ask. Turning it off means no image is ever fetched for anybody, whatever any per-sender preference says, and the proxy refuses the request even if somebody calls it directly.
Since 1.11.1 it is a checkbox: Settings ▸ Security ▸ Remote images ▸
Allow remote images. Only an owner can change it; everyone else sees the
current state with the control disabled. It is shown even when AUTH_MODE=none,
because it is a property of the instance rather than of an identity — an
instance nobody signs in to still fetches images.
On 1.11.0 there was no screen at all. The same switch over the API, for a script or for that release:
curl -sS -b /tmp/skim.cookies -X PUT -H 'Content-Type: application/json' \
-d '{"remote_images":false}' \
http://localhost:8080/api/settings/readingRead it back — this one is allowed for any signed-in user:
curl -sS -b /tmp/skim.cookies http://localhost:8080/api/settings/readingReaders then see "Remote images are turned off for this instance" where the
buttons used to be. Set true to allow asking again; no per-sender decision is
lost while the switch is off.
- SkimMail fetches, your browser does not. The body is served as its own document with a policy that permits images from SkimMail's own proxy and nothing else at all — no scripts, no fonts, no frames, no connections.
- The fetch uses that account's egress. If you routed the account through a SOCKS5/HTTP proxy, a WireGuard tunnel or a relay, the image leaves the same way its mail does. An account you carefully put behind a proxy is not exposed by a tracking pixel.
-
Almost nothing goes with the request.
Accept: image/*andUser-Agent: SkimMail. No cookies, noReferer, noAccept-Language— a fingerprint, and often a country. -
The address is resolved by SkimMail, not by your proxy, and the
connection is made to the address that was checked. Loopback, private,
link-local (including the cloud metadata address), unspecified and multicast
addresses are refused — so
<img src="http://192.168.1.1/">in a stranger's mail cannot make your own proxy reach into your own network. - A shown image stays in your browser's cache for an hour. That is a privacy setting, not a performance one: every miss is another touch on the sender's server.
What the sender still learns, and this page will not pretend otherwise: that the message was opened, when, and the public IP that account's egress leaves from. Remote images remain a tracking mechanism. SkimMail changes who is visible, not whether something is.
Before 1.18.0, pressing Show images did nothing, for any message, ever. The reading pane renders each body inside a locked-down frame, and a side effect of that lock-down was that the browser stopped attaching your session to anything the frame asked for — so every image request came back "not signed in" before SkimMail's server even tried to fetch the picture, and the image stayed broken however many times you pressed the button or how healthy the sender's server was. Fixed in 1.18.0 (SKIMMAIL-209): the frame is now same-origin for exactly the requests that need your session, and still locked down in every other way that matters — nothing in a message can run code, submit a form, open a window, or navigate anywhere. The one new thing that same-origin change opened, and how it was closed, is on Security.
A fetch that fails now says why, in the server log. Until this release nothing recorded the reason a remote image did not load — eight different causes, from a blocked address to a dead server to a file that was not really an image, all produced the same empty response and not one log line. Each now names itself. Only the image's host is recorded, never its full address — a link in mail is usually unique to the recipient, and the log can be downloaded.
| Formats served | PNG, JPEG, GIF, WebP, BMP, ICO |
| Always refused | SVG, AVIF, anything else |
| Size per image | 5 MiB |
| Time per image | 10 seconds |
| Redirects | at most four, each one re-checked |
| Images per message | the first 200 |
| Browser cache | 1 hour |
-
The type is decided by looking at the bytes, not by believing the
sender's
Content-Type. An SVG served asimage/pngis still refused. - SVG is refused on purpose. It is a document, not a picture: an SVG carrying a script, served from SkimMail's own origin, would be stored XSS in your session.
- AVIF is refused for a duller reason. Go's content sniffer cannot recognise the format, so allowing it would mean trusting the sender's label — the one thing this design does not do. An AVIF image shows as broken and there is no setting that changes it in 1.11.0.
-
Images embedded in the message itself are not shown — only
http://andhttps://sources go through the proxy.cid:anddata:sources are removed by the sanitiser, and relative addresses (/logo.png) are withheld silently, because under the body document they would become requests to SkimMail's own API carrying your session. -
If the account's egress is an HTTP proxy that refuses CONNECT on port 80,
plain-
http://images cannot be fetched through it. HTTPS is unaffected. - A refused image draws the browser's broken-image mark, never a blank pixel. That is deliberate: a blank pixel is indistinguishable from a tracking pixel that loaded successfully, and you would have no way to tell "refused" from "the sender got their receipt".
- It is not a general-purpose proxy. The endpoint takes an index into the images of a message you are already allowed to read — never a URL. That is what stops anyone who can send you mail from turning your instance into an open relay, and it is why there is no "fetch this address" affordance anywhere.
-
It does not prefetch images. Nothing is requested until a human presses
the button, or the sender already carries an
always. - It has no per-account switch. The instance switch is instance-wide; sender decisions follow the user, not the mailbox.
- There is no list of the senders you have decided about. The interface shows the decision on a message from that sender and nowhere else; auditing or clearing them in bulk is not something 1.15.0 can do. The senders panel added to Settings ▸ Security in 1.13.0 is a different list — it holds muted and VIP senders only, not image decisions, even though both are stored against the same correspondent. See Muted and VIP senders.
- It does not remember "show once" — by design, not by omission.
- It does not make opening mail private. See the paragraph above.
- Security — the threat model this sits inside, and encryption at rest
-
Users and roles — why the sender buttons need
operator - Troubleshooting — images that stay broken after you press Show images
- Configuration — per-account egress and the environment reference
SkimMail · skimmail@base101.app · 2026-09-18 · commit f525934