Skip to main content

newsprint — your newsletters, on paper

newsprint

Turn a week of email newsletters into a small stack of paper.

newsprint fetches the messages you starred, strips each one down to its article content, typesets it onto quarter-sheet cells, imposes four to a sheet side, and prints it duplex — then unstars the mail and files it away, so your inbox is clear and your reading is somewhere a screen isn't.

Star what you want during the week; run it once when you want the paper. It was written for reading over Shabbat, away from screens, but nothing in it is specific to that: it suits a flight, a commute, a weekend, or anyone who would rather read long things on paper than on a phone.

Requirements

  • Python 3.12 or later
  • uv
  • An IMAP account (including Gmail), and a folder you star newsletters into
  • Pango, Cairo and gdk-pixbuf, for WeasyPrint — see Install
  • macOS or Linux. Both are tested in CI on Python 3.12 and 3.14. Windows is not — see Windows for what stands in the way.
  • A printer reachable via CUPS (lp), if you want newsprint to do the printing. With --no-print it hands you a PDF instead, which is a complete workflow on a machine with no printer configured at all.

Install

Install it as a tool, which puts newsprint on your PATH so you never type uv run:

uv tool install newsprint

Or, to try it without installing anything:

uvx newsprint --help

To install from a clone, run uv tool install . in the checkout. For working on newsprint rather than with it, see Development.

WeasyPrint needs Pango, Cairo and gdk-pixbuf, which are system libraries rather than Python packages — install them first:

brew install pango                      # macOS
sudo apt install libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz0b   # Debian/Ubuntu

Windows

newsprint is developed and tested on macOS and Linux, and CI runs both. It is not tested on Windows, and two things stand between it and a working run there:

  • There is no lp. Printing goes through CUPS, which Windows does not have, so --no-print is not optional — it is how you would always run it. newsprint --no-print --output C:\Users\you\Reading builds the packet and leaves you a PDF to print from whatever you normally print with. The imposition is already done, so print it at 100% ("Actual size", not "Fit to page") or the four cells land in the wrong places.
  • WeasyPrint's system libraries. Pango, Cairo and gdk-pixbuf are not Python packages, and on Windows they come from the GTK3 runtime rather than from pip or uv. WeasyPrint's own first steps guide covers the Windows installation.

Nothing else is known to be in the way. The IMAP client is standard library, the password lookup uses the Windows Credential Locker through keyring, and the preview step does nothing rather than failing when neither open nor xdg-open exists — the PDF's path is printed either way.

That is a description of the obstacles, not a promise: nobody has run it there. If you try it, an issue saying how far you got would be useful whichever way it goes.

Setup

The quickest way is to let newsprint ask:

newsprint --setup

It checks the credentials before writing anything, and lists your server's own folder names to choose from — worth doing even if you would rather hand- write the file, because the folder is the one setting nobody can guess from outside. The hierarchy separator is not standardized (. on some servers, / on others), and on Gmail a "folder" is a label.

To write the file by hand instead, copy the example config and fill in your mail account and printer:

cp config.example.toml ~/.config/newsprint/config.toml
$EDITOR ~/.config/newsprint/config.toml

The IMAP password is never stored in the config file. Put it in your system keychain instead:

keyring set <mail.host> <mail.user>

for example:

keyring set imap.gmail.com you@gmail.com

See config.example.toml for the full set of options, including a note on what Gmail specifically needs (an app password, and a folder path that isn't INBOX/toprint).

Choosing what to print

Star the messages you want, then run the tool. That is the whole workflow, and it is not specific to any mail client: the tool never talks to your mail program at all. It connects to your IMAP server and asks it for

UID SEARCH FLAGGED

\Flagged is a system flag defined by IMAP itself (RFC 3501), so whatever you use to star a message — Thunderbird's star, Apple Mail's flag, Outlook's follow-up flag, Gmail's star, your phone — is setting the same server-side bit that this reads back. Star from anywhere; run this from your desktop.

Two things select a message: the folder narrows, the star picks. The tool selects one mailbox (mail.folder) and searches for flagged messages inside it.

How messages get into that folder, and how you decide what to star, is entirely up to you — the tool only reads the result. The author's setup, as one example: mail rules file every newsletter subscription into a toprint folder as it arrives, and then during the week he stars the subset he actually wants to read on paper. Everything unstarred stays in the folder and is offered at run time. You could equally star straight from your phone as things arrive, or star nothing during the week and pick entirely from the checklist.

Anything in that folder you did not star is offered to you at run time, in a checklist, so a newsletter you forgot to star is one keypress away rather than a lost cause.

The checklist scrolls, and typing filters it — useful when --since reaches back a few weeks and the list runs to a hundred:

key what it does
any letter narrows the list to publications and subjects matching what you typed
esc clears the filter outright
backspace deletes one character of it
space toggles the row under the cursor
↑ ↓ move the cursor (j/k cannot navigate here — they are filter text)
enter confirm
ctrl-c cancel, selecting nothing

Filtering matches the publication as well as the subject, and keeps whole groups: typing axios brings up every Axios newsletter, not only the ones that happen to repeat "Axios" in their own headline.

What you have already ticked stays ticked while you filter, so the way to pick from several publications is to filter, tick, backspace out, and filter again.

Gmail

Gmail has no folders, only labels — but a label is exactly what IMAP shows as a mailbox, so the workflow maps over cleanly:

Other clients Gmail
A rule moves newsletters into a toprint folder A filter applies a toprint label
folder = "INBOX/toprint" folder = "toprint" — a label is a top-level mailbox, not nested under INBOX
Star the ones to print Star the ones to print

You can also skip the label and set folder = "INBOX", relying on stars alone. That is less setup, but the "what else arrived this week" checklist then offers your whole inbox rather than just newsletters.

Gmail also needs an app password rather than your account password; see config.example.toml, which covers both points.

Two Gmail details worth checking on your first run, with --no-retire so nothing is modified:

  • Superstars. Gmail can show several star colors. They are widely reported to all map to the one \Flagged bit over IMAP, so "only red stars" is unlikely to survive the protocol — confirm before relying on it.
  • Retiring. Messages are unstarred, marked read, and moved to the server's \Trash mailbox, which on Gmail is [Gmail]/Trash. That discovery is automatic, but worth watching once.

Usage

newsprint

This fetches the starred messages, builds the imposed PDF, opens it in Preview for a look, and asks before printing. Useful flags:

  • --dry-run — build and preview the PDF, but print nothing and retire nothing. Exactly equivalent to passing --no-print --no-retire together; it exists as one flag because "show me what I would get" is a thing you want often enough to have a name.
  • --no-retire — print for real, but leave mail untouched (the messages stay starred and will be reprinted next run). Useful for checking that a real printout looks right without consuming the print queue.
  • --output PATH — write the finished PDF somewhere you can find it instead of a temp directory. An existing directory gets a dated and timed file inside it (newsprint-2026-09-11-1432.pdf), so building a second packet the same day sits beside the first rather than replacing it. Set title under [packet] and that name is used instead (family-shabbat-reading-2026-09-11-1432.pdf). Anything that is not an existing directory is taken as the file path to write.
  • --no-print — build the PDF but do not send it to a printer; print it yourself from the file. Still offers to retire the mail, after asking whether the printing actually worked. --no-print --output ~/reading/ is a complete workflow on a machine with no printer configured at all.
  • --no-wait — do not wait for the printer. A normal run blocks until the job finishes and retires the mail only if every side printed; this returns as soon as the queue accepts the document, which means a job that stops partway will still retire the mail. See Waiting for the printer.
  • --reprint-from PATH — print part of a packet again instead of building a new one, for finishing a job that stopped partway, and then retire the mail that run was holding. Say which sides with --pages 37-72 or --after-cell 144. See Finishing a job that stopped partway.
  • --unretire — undo the last retirement: move those messages back out of the trash and re-star them. Builds nothing and prints nothing. See Undoing a retirement.
  • --no-preview — skip opening the PDF in Preview.
  • --paper a4 / --paper letter — override the configured paper size for one run.
  • --cells-per-side 2 / --cells-per-side 4 — override how many newsletters go on each side of a sheet. See Bigger type.
  • --since YYYY-MM-DD — the first day the unstarred review should include, instead of letting the last successful run decide. Reach further back after a few weeks away (--since 2026-08-01), or narrow it to the last day or two. Starred messages are unaffected — those are always collected, however old.
  • --publications — list the publications in your folder, as newsprint names them, with how many messages each has. Builds nothing and prints nothing; it exists so you can write [images] rules without guessing at names.
  • --config PATH — use a config file other than the default.

Run newsprint --help for the full list, and newsprint --version for the release, project page and author.

Bigger type

Four newsletters a side is the default: the sheet prints portrait and each newsletter gets a quarter of it, A6 on A4 paper.

Two a side turns the sheet landscape and puts the two newsletters side by side, so each one is A5 — portrait, twice the area, and the fold that separates them runs down the middle of the sheet. That is the shape of an ordinary booklet.

The extra area is room for larger type rather than larger type by itself: at the same font size a bigger cell just makes the lines longer, and past about 75 characters a line they get hard to follow. Measured on a real packet:

layout font_size_pt characters per line
four a side 9 (default four a side) 55
two a side 9 88 — too long
two a side 12 (default two a side) 66
two a side 14 55
two a side 18 44 — getting choppy

So the type size follows the layout by itself — 9pt four a side, 12pt two a side, both well inside the comfortable band. All you set is the layout:

[print]
cells_per_side = 2

or --cells-per-side 2 for one run. Set font_size_pt under [layout] only to override that, in either direction.

Half as many newsletters to a side means twice as many sheets for the same reading, which is the trade.

Reading from more than one folder

folders takes either one folder or a list of them:

[mail]
folders = "INBOX/toprint"
[mail]
folders = ["INBOX/toprint", "INBOX/work-reading"]

folder, the older singular name, is deprecated. It still works — it is what every config written before this says, and nothing is going to stop reading it — but a run that finds it says so, and offers to rename the key for you. Accepting changes that one key and nothing else in the file: not the value, not your comments, not a single other line.

Setting both is a config half-edited. folders wins, and newsprint says which one it ignored rather than leaving you to notice a missing folder in the packet.

Each folder is opened in turn on its own connection, and every message remembers which one it came from — a uid is issued by a folder and means nothing outside it, so two folders can both hold a uid 4 and they are different messages. Retirement sends each back to its own folder, and so does --unretire.

Where packets go

With no --output, a finished packet is written to [output] directory — ~/.local/state/newsprint/packets by default — rather than to a temp directory the system later deletes. That matters because the packet is the only copy of a run's work: if a job stops halfway, or you want the same reading again next week, it is still there to print.

Each run then removes packets in that directory older than [output] keep_days, 30 by default. Set it to 0 to keep them all. The sweep only ever touches PDFs directly in that directory, and never touches a directory you named yourself with --output.

[output]
directory = "~/.local/state/newsprint/packets"
keep_days = 30

Summary pages (optional)

newsprint can put one or two extra pages at the front of the packet, written by Claude from the cleaned text of everything in it:

  • This Week's Topics — what actually recurs across the week's reading. Not a list of subjects, which the contents page already gives you, but what connects them.
  • A page of your own — only if you ask for one, by adding a [summary.personal] section. You describe in your own words what you are watching for in your reading, and give the page a heading; it flags things worth chasing. This was hardcoded to the author's own newsletter until newsprint became something other people install, which is why it is free text in your config rather than a setting.

This is off by default, and stays off unless you turn it on. With no API key configured, nothing is sent anywhere and the packet prints exactly as it otherwise would, with one line saying the summary was skipped and why. There is no degraded mode and no silent failure: a missing key, no network, an API error, or a timeout all end the same way — the packet still prints, just without these pages.

To turn it on you need an Anthropic API key. The key never goes in config.toml. Point the config at a file that holds it instead:

[summary]
enabled = true

That is the whole of it. Every other key has a default, so set one only to change it:

key default
api_key_file ~/.env — a dotenv-format file
api_key_var ANTHROPIC_API_KEY
model claude-opus-5
timeout_seconds 120.0

Optionally, add a page of your own. Leave this section out entirely and you get only the topics page — its presence is the switch, so there is no flag to set:

[summary.personal]
title = "Bamboo Weekly Candidates"
looking_for = """
I write a newsletter of pandas exercises built on real public datasets.
Flag things with public data plausibly behind them.
"""

Only api_key_var is read from that file, and the key is never written to the run log or echoed on failure.

What it costs, and what leaves your machine. One API call per run, carrying the cleaned text of every newsletter in the packet — on a typical week that is roughly 40k input tokens and a few hundred out. That text goes to Anthropic. If that is not something you want for your mail, leave the feature off; everything else works without it.

Images

Most images in a newsletter are decoration — a masthead, a social icon, a tracking pixel — and on a quarter-sheet page they cost space the article needs. So newsprint drops images by default, and keeps only the ones the author treated as part of their argument.

An image is kept when it is at least 300px wide and the surrounding text says it matters:

  • the line before it ends in a colon, or reads like "here's the chart";
  • the line after it opens a caption — Source:, Chart:, Figure, Credit:, Data:;
  • the block after it is nothing but a parenthesised link, which is how Platformer cites the screenshots in "Those good posts".

Some newsletters are built out of charts and never introduce one in words. Apricitas Economics sets fifteen to an issue: no lead-in, no caption, alt="". For those, three structural signs stand in for the missing sentence, all three required:

  • a paragraph of real prose on both sides — a figure inside an argument interrupts the argument, while a masthead sits above all of it and a footer logo below, with nothing on the far side;
  • no alt and no title — which reads backwards and is not. A sender labels a masthead, a section header or a sponsor's logo precisely so it still reads as that brand when images are blocked; an author setting a chart for people to look at rarely bothers;
  • a height in pixels rather than auto, since a content image carries its own dimensions through the mail template while chrome is laid out fluidly.

A kept image's URL is written by whoever sent the mail, so it is checked before anything is fetched: http and https only, and only to public addresses. A newsletter cannot make your machine probe your router, a service on localhost, or a cloud metadata endpoint, and cannot read a file off your disk with a file:// URL. A data: image, which carries its own bytes and never touches the network, is left alone. Every kept image across the 268-newsletter archive is https, so nothing real is turned away.

A kept image is fetched at print time, converted to grayscale, and resized down to the cell's text column — 87mm at 200dpi, so 685px. A chart set on a dark ground is turned over, so it prints as ink on white like the type around it: white-on-black is a screen convention, and on paper it is a page of toner. The test is the image's median grey level — Apricitas Economics' charts measure 41 of 255 against an ordinary chart's 240, so nothing real sits near the line. Nothing else is ever fetched: a dropped image's URL is recorded for the run report and never requested, which is also why tracking pixels never phone home.

Among the images that are not kept, one carrying substantial alt text can leave a text placeholder in the flow instead, so the sentence around it still makes sense:

[figure: US-China trade balances as a percent of GDP, 2010-2026]

In practice that is rare — most newsletter images ship with alt="".

If an image cannot be fetched — a dead URL, a login wall, a timeout — the run says so and prints the packet without it.

Measured across a 268-newsletter archive: 1.51 images kept per newsletter, and one text placeholder in the entire corpus. The introduced-figure rule alone accounts for 0.97 of that; the uncaptioned rule adds the rest, and it is what the chart-heavy publications live on — Apricitas Economics goes from none of its charts to thirteen, and Prof G Markets, Derek Thompson and David Skilling appear for the first time.

Charts cost paper. A packet that was seven sheet sides can be ten once the charts are in it, and a publication that sets its charts on a black ground prints as a black ground.

What gets stripped

Beyond images, each newsletter is reduced to what you would actually read: the masthead, the social bar, the "forwarded this email?" banner, the sponsor block, and the footer — unsubscribe link, copyright line, mailing address, "you are receiving this email because".

Both ends are cut from a marker rather than trimmed inwards from the edge, and for the same reason. A backwards trim from the end stops on the promotional line senders put below the footer ("Powered by Buttondown, the easiest way to start and grow your newsletter"); a forward trim from the start stops on the preheader sitting above the browser bar. Each is a content-shaped line standing between the edge and the chrome, so the cut starts from the chrome instead.

Measured across the 268-newsletter archive: chrome lines reaching the page fall from 51 across 25 messages to 4 across 4, and 399 lines go without a single message growing by one.

Images from one publication

Some publications send charts worth the toner; others send the same decorative motif every week. [images] says what to do about a named one:

[images]
"the bulwark" = "none"    # never keep an image from these
"apricitas"   = "all"     # keep every image wide enough to be a figure

The key is any part of the publication's name, matched without regard to case, because the names senders use run to 65 characters — "Benjamin Bennett Alexander from Python and Data Analysis Insights" — and one real archive holds 102 distinct ones. "bulwark" covers all four of the Bulwark's newsletters at once. Where two rules match, the longer one wins, so a broad rule can be given an exception.

none keeps nothing. all skips the judgement about whether the author introduced the figure, but still refuses anything too narrow to be one — a tracking pixel is not a figure at any setting, and nothing that is refused is ever fetched. With no entry a publication gets the usual rules.

Run newsprint --publications to see the names your own mail uses.

Waiting for the printer

A run does not end when lp accepts the document. It waits for the job, first while CUPS sends it and then while the printer prints it:

Spooled as Brother_MFC_L2700DW_series-8731.
  Queued: 107 sides sent to the printer.
  Printing: sheet 47 of 54  (94 sides)  12m50s, ~2m left
  Printed 107 sides on 54 sheets.

The Printing line repaints in place, and its clock ticks on every poll — worth knowing, because the print queue is empty for almost all of this. CUPS marks the job finished as soon as the printer's memory holds the document, so there is nothing to watch in Printers & Scanners while the printer works alone for another quarter of an hour. That line is the only sign the run is alive.

Counts are the document's, never the printer's. Printed on both sides, a packet with an odd number of sides runs one blank back: 105 sides is 53 sheets, and the printer honestly reports 106 impressions. Reporting that back would be a fraction over 100% and a total nobody could find in the tray, so newsprint clamps what it shows to the document — while still treating that 106th side as the proof of a finished job.

Only then is the mail retired, and only if the count came out whole. This is not a theoretical precaution. lp exits as soon as CUPS has the file, and — this is the part that surprises — a job that stops partway through is still reported by CUPS as completed. A run on 2026-09-18 put 67 of 107 sides on paper, and the mail was unstarred and filed eleven seconds after handoff, while the printer still had seven minutes of work ahead of it. Nothing in job-state distinguishes that from a clean finish.

CUPS's own count of sides is not the answer either: it counts a side once it has sent it, so it runs ahead of paper by however many pages the printer is holding. On 2026-10-02 CUPS reported 66 of 116 sides while the printer, which had run out of memory and cancelled the job, had printed 60. So once CUPS has sent everything, newsprint asks the printer itself for its record of the job, follows it until the printer stops, and goes by the printer's count. A short job stops the run:

! The printer stopped after 60 of 116 sides (30 sheets):
  job-canceled-at-device. 56 sides never printed.
  To finish, print sides 61-116 of the PDF.
  Mail untouched: all 40 messages stay starred.
  PDF kept at ~/.local/state/newsprint/packets/...-1227.pdf

To ask the printer, newsprint needs its address. It reads the queue's device from CUPS and, for a Bonjour printer (most network printers on a Mac), looks it up with ippfind, which ships with macOS and is in the cups-ipp-utils package on most Linux distributions. If that does not find yours, set it in the config:

[print]
printer_uri = "ipp://BRW0123456789AB.local:631/ipp/print"

A printer connected by USB, or reached as a raw socket or over LPD, keeps no job record that can be asked. For those the run goes by CUPS's count, as before, and says so: "Sent 107 sides; this printer cannot report how many reached paper." If such a job stops short, the message gives CUPS's number and asks you to check the last sheet before reprinting.

Every message stays starred, so the next run builds the same packet again, and the PDF is there if you would rather print the missing pages yourself. The same happens if the job's state cannot be read at all, or if the printer cannot be found or has already forgotten the job — not knowing whether it printed is treated as a failure, never as a success.

The wait has no timeout, and giving up early would leave exactly the uncertainty it exists to remove. Ctrl-C is safe: nothing has been retired yet. For an unattended run, --no-wait returns as soon as the job is queued, with the old caveat that a short print will still retire the mail.

When the count stops moving

An earlier version of this section said a printer out of paper finishes the job once someone puts paper in it. That is not always true. On 2026-11-05 a Brother MFC-L2700DW ran its tray dry at side 37 of 72, discarded the job outright, and left the connection open; refilling the tray changed nothing, because there was no longer a job to resume.

Nothing a person can easily look at showed it. The queue said it was printing. The printer called itself idle with no state reasons, paper ready and toner at 80%, and held no job of its own. CUPS kept the job in processing with its impression count parked at 37. The only honest signal was on the socket:

tcp4  0  130160  192.168.x.x.52810 -> 192.168.x.x.631  ESTABLISHED  ipp:94074

A send queue frozen at 130,160 bytes of a 131,072-byte buffer — the printer had stopped reading and never reopened its receive window. A healthy job fills and drains that queue; a wedged one holds it full and still. Since nobody should have to find that out from netstat, newsprint now says so after ten minutes without a change, and again on every ten minutes after that — a run can be left alone, and silence following one warning reads the same as having recovered:

  Queueing: 37 of 72 sides...
  ! No change for 10m00s - stuck at 37 of 72 sides.
    The printer may have taken the job and then dropped it. Running
    out of paper does exactly that, and it leaves the connection open,
    so the queue, the printer and CUPS all go on reporting health.
    Check the tray, then `lpstat -p`. A count still frozen there means
    the job is gone, and cancelling it is the only way forward.
    Ctrl-C is safe: no mail has been touched yet.

To clear a wedged job, cancel it by name — cancel Brother_..._native-8759 — and not with cancel -x, which purges the job record so newsprint can no longer read how far it got and has to report the vaguer "cannot report how many reached paper". Cancelling does not kill the blocked backend: it survives as an orphan still holding the socket, and CUPS then advertises a phantom now printing <queue>-0 that the next job queues behind. sudo launchctl kickstart -k system/org.cups.cupsd clears both (on Linux, restart cups.service).

Then finish the packet with --reprint-from, below.

Finishing a job that stopped partway

The packet is still on disk, so all that is missing is a page range — and nothing about that range is as obvious as it looks. One PDF page is one sheet side, and CUPS re-imposes whatever range it is given from scratch: page 1 of the range becomes the front of a sheet, whatever it was in the original document. Resume on an even side and every back prints as a front, so the remainder of the packet comes out inside-out, and nothing looks wrong until the stack is in your hands.

--reprint-from does that arithmetic, and takes the one number you can actually read off the paper: the last cell that came out whole, which is stamped on the page.

newsprint --reprint-from ~/.local/state/newsprint/packets/...-1926.pdf \
          --after-cell 144
Reprinting reuven-s-shabbat-reading-2026-11-05-1926.pdf
  Sides 37-72 of 72 - 36 sides on 18 sheets.
  Mail: the run of 2026-11-05T17:27:06 is still holding 25 message(s).

Print sides 37-72 to Brother_MFC_L2700DW_native? [y/N]:

Four cells go on a side, so cell 144 is 36 whole sides, which is 18 whole sheets, so the reprint starts at side 37. A cell part way down a side — 143, say — leaves sheet 18 printed on one face only, and because a duplex job cannot start on a back, that whole sheet goes through again. That costs one sheet of paper instead of a ruined remainder.

--pages 37-72 says the range directly, --pages 37- runs to the end, and an even first side is refused rather than printed:

Error: Page 38 is the back of a sheet, and a duplex reprint cannot start on
a back: CUPS lays out whatever range it is given from scratch, so every
back would print as a front and the rest of the packet would come out
inside-out.
Start on an odd side instead - --pages 37-72 reprints one sheet you already
have and gets the rest right.

A reprint waits on the printer exactly as an ordinary run does, watchdog and all, and if it too stops short it names the sides of the original packet to pick up from. --dry-run shows the whole plan — range, sheets and mail — without spending any paper. And if you finished the packet by hand and only the mail is left over, --no-print sends nothing to the printer and offers the retirement on its own.

By hand, the same thing is lp -d <queue> -o media=A4 -o sides=two-sided-long-edge -o print-scaling=none -o page-ranges=37-72 <pdf> — which also leaves the mail for you to unstar, 25 messages at a time.

How the mail is found, and why it is checked. A run writes down the messages it is holding before anything is printed, so the record of the run that stopped is what names them. But a uid is only a number issued by a folder, and a record can outlive the mail it names — so nothing is taken on trust: each folder's uids are fetched headers-first, and retired only where the Message-ID is one that run actually printed. Anything else is reported and left exactly where it is.

Records written before 0.23.0 carry a flat uid list with no folder beside it. With one folder configured that is unambiguous; with several, uid 4 could have come from either and they would be different messages, so newsprint refuses to guess and leaves the mail starred for the next ordinary run to collect. The retirement itself is recorded like any other, so --unretire undoes it.

Undoing a retirement

Retiring is the one irreversible thing newsprint does: printed messages are marked read, unstarred, and moved to your trash. If a run retires something it should not have, newsprint --unretire puts the last batch back.

newsprint --unretire

It reads the most recent retirement from the run log, finds those messages in the trash by Message-ID — the uids the run recorded name nothing once a message has moved — moves them back to the folder they came from, and re-stars them. It asks before touching anything.

Two limits worth knowing:

  • It cannot outlive your trash. Once the mail host empties it, the messages are gone and no run log will bring them back. Unretire soon or not at all.
  • Messages come back starred but read. Retirement never records whether a message had already been read before the run, so that cannot be undone honestly, and is not guessed at.

Only the most recent retirement can be undone. If you need an older one, its run log entry is in ~/.local/state/newsprint/runs/ and lists the Message-IDs, which your mail client can search for.

Filtering

clean.py removes only what it can confidently identify as chrome (an "Unsubscribe" line, a "view in browser" bar, and the like); everything else passes through. So an unsubscribe line, a mailing address, or a similar bit of boilerplate can still appear on a printout if it doesn't clearly match that check — trim.py's final-cell judgment is the actual backstop against a filler page, not clean.py's removal.

Development

The test suite runs on a clone with no extra setup, and reaches 100% statement and branch coverage there:

uv sync
uv run pytest --cov=newsprint --cov-branch --cov-fail-under=100

A few tests need tests/fixtures/ — a local archive of real newsletters, built by make fixtures from the author's own mail. That directory is gitignored, because it is other people's copyrighted writing and not the author's to redistribute. Those tests assert corpus-wide properties ("no real newsletter loses a headline to this rule") and skip cleanly when the archive is absent; every line and branch they cover also has a unit test, so the coverage guarantee does not depend on having them.

Rendering newsletters to PDF uses WeasyPrint, which depends on Pango, Cairo, and gdk-pixbuf. On macOS, install them with Homebrew:

brew install pango cairo gdk-pixbuf

Homebrew's lib directory isn't on the default dlopen search path, so WeasyPrint can fail to import with an error like cannot load library 'libgobject-2.0-0' unless something points the dynamic linker at it. newsprint._libpath handles this automatically: before render.py imports WeasyPrint, it sets DYLD_FALLBACK_LIBRARY_PATH to whichever of /opt/homebrew/lib or /usr/local/lib has the library, if the variable isn't already set. So running the tool, newsprint, or the test suite all work out of the box on macOS with a Homebrew install in one of those two locations — no manual export needed.

If your Homebrew libraries live somewhere else, set DYLD_FALLBACK_LIBRARY_PATH yourself before running; an explicit value always takes priority:

export DYLD_FALLBACK_LIBRARY_PATH="$(brew --prefix)/lib"
uv run pytest

Run the tests and linter with:

make test
make lint

License

MIT. See LICENSE.

Metadata

Release files for newsprint 0.23.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for newsprint 0.23.0
File Size Uploaded Provenance Actions
newsprint-0.23.0.tar.gz 184.2 kB Without provenance Details for newsprint-0.23.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for newsprint 0.23.0
File Interpreter ABI Platform Actions
newsprint-0.23.0-py3-none-any.whl Python 3 none any Details for newsprint-0.23.0-py3-none-any.whl

Total release size: 367.4 kB

Release files / newsprint-0.23.0.tar.gz

Download URL newsprint-0.23.0.tar.gz
Size 184.2 kB
Tags Source
SHA-256 checksum
How to use checksums
23175224d77662b55474d68772379268f59cab7c3c659b5aad179cabc38878cd
BLAKE2b-256 checksum
How to use checksums
5ab9a160157697e07fba65f6cfca2f9e4cb1dfcac40827689c586e58012395de
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.7 {"installer":{"name":"uv","version":"0.13.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / newsprint-0.23.0-py3-none-any.whl

Download URL newsprint-0.23.0-py3-none-any.whl
Size 183.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a83c46b7a5b9768d8f4550f45041ed7af2b08802eb56c5a1ead9f4a05ac3fd74
BLAKE2b-256 checksum
How to use checksums
ce66a8285134bd06b04463f98c54ed3eae80bec76e36bebf6c5ba01dc2f66842
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.7 {"installer":{"name":"uv","version":"0.13.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for newsprint 0.23.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.23.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page