The Porch: the first slice¶
The Porch is where new mail waits, checked and sorted, until you look. It is the first slice because it removes the main source of distress, the inbox that screams, before anything else is built.
Lanes, in the order they are decided¶
Order matters: what protects you most comes first.
| Lane | When | Shown |
|---|---|---|
| Set aside | blocked, forged (DMARC fail), borrowing a name, or a stranger's mail your provider calls spam (below, "Spam: never for people you know") | in the window, at the bottom, with the reason; never deleted; spam has Not spam, which puts it back in its lane for good |
| Caught by your own spam filter | a stranger's mail Sioul's own filter flagged, or moved into the Junk folder, as you chose for its verdict (below) | in the window, folded, no count, never notified; Spam and Not spam on each, and for all |
| Right now | a one-time code, a password, a password reset, a sign-in link or an address to confirm: what you just asked a site for, from its automatic address (no-reply…) too, and sent with a newsletter's headers too | at once, whatever the time: the one exception to the windows |
| A project | a route of a project matches (notes-folder.md) | in the window, under the project's title |
| From yourself | mail from one of your own addresses to another (a file sent from your phone), verified | with people you know, in any hour; never screened, never taken for spam |
| Filed | a newsletter or mailing list (List-Id, List-Unsubscribe, Precedence: bulk), or an automatic address (no-reply…) |
in the window, folded |
| Screener | a stranger (in none of your address books, on no list, not let in), or a sender only a domain on a list names | in the window; you let them in, or not |
| People | someone you know (in your address books, let in, or on a list by their own address), outside any project | in the window |
| Less important accounts | any mail of an account you ranked below the others (social networks, notifications read sometimes, never answered), unless a project takes it | in the window, folded at the bottom; one sentence at the end of the summary, outside its count |
These messages come from an explicit request of yours, a moment ago: they are expected, and needed at once. A forged one is set aside before (DMARC fail, a borrowed brand's name, spam); one from a sender that is only not verified comes right now too, with a warning to use it only if you just asked that site for it, because fake codes are a phishing trick. A message "from yourself" that is not verified gets no pass: a forged own address is a classic trick.
Many sites send their codes and links through the same services as their newsletters, with the same headers (List-Unsubscribe, List-Id, Precedence: bulk, a service's own markers). Those headers never decide that a message is not a code: codes are decided before newsletters, and the headers only narrow where a code is looked for ("Detection", below). A message the detector missed used to land in Filed as a newsletter because of them: what was missed was the wording, not the order.
Each lane says what it holds in one line under its title; its "?" shows how mail lands there, sentence by sentence, with the Porch's own settings that change it (the senders you let in, the words of automatic senders) and where the others are: an address's rank and shield on its card in Accounts, a project's routes on its page in Projects, who is safe, neutral, restricted, a stranger or blocked, and when each comes, in Settings ▸ What reaches you (attention.md). The Porch's ⚙ holds what is the Porch's alone: the projects that have a lane here (the others' mail stays on their page in Projects), where paper letters arrive, and every lane but the projects', empty or not, in the order mail is sorted; and how a message opened here reads (font, size, line spacing: the same settings as in Mail's ⚙ and behind "Aa" in Notes).
A public address, shielded¶
A contact address brings work, and also insults. An account with shield = true has its mail read before you see it (shield.rs):
- its tone: calm, rude (swearing, contempt), or hostile (insults aimed at you, harassment, threats), from word lists in French and English matched on whole words, case and accents aside; an insult near "you" weighs more, and shouting adds to it;
- its topic: work (a job, a mission, a quote), a question, the press, thanks, a donation, other.
Hostile mail goes to a folded "Hostile" lane that shows neither the sender's name nor the subject; opening one asks first (forward it to someone you trust, block, delete, or read anyway). The rest of the address's mail has its own lane, work first, with its topic and, when rude, a mark; the address can have its own fetching pace (fetch_minutes).
With shield_ai = true and a key for Anthropic's API (typed in the settings, kept in the system keyring), the subject and text of each new message are sent once to Claude Haiku, which says the tone, the topic and one neutral line on what it asks; the answer is kept under the Message-ID and a digest of who sent it and what it says (shield::ai_key: a message borrowing a calm one's Message-ID is read again), in $XDG_STATE_HOME/sioul/shield/<account>.toml, and takes precedence over the word lists. An answer not in the form asked is not kept; the message is marked there (malformed, by the same key, how many times) and sent once more at most, at the next round, then never (AiCache::wants, TIMES), so that a message the model will not answer does not go to Anthropic at every round, nor hold one of the twenty places of each. The Porch, sioul shield and the MCP server read it the same way (shield::reading). Off by default: the text leaves the device. sioul shield [--account <id>] [--read] shows what the shield makes of each message.
Sites (secure mailboxes, chats) add their notifications above the lanes, each site with its lines (sites.md). Before them, when there is something, a few cards: where you stopped, until "Done" (StoppedCard.qml, tasks.md); today's two events at once, margins counted, with "Open “…”" for each event and "Don't mention it again" (client.md, Calendars); today's doses not marked yet, from their time on, reminded or not, with "Taken" ("Taken…", asking when, past their half hour) and the doubt under each when another device may know, whatever the hours (doses are not mail; asleep, as "Doses during sleep" says); the doses due while Sioul was closed, or while it ran but their reminder could not be shown (each under its own question), with "Taken…" and "Not taken" and the doubt under each when another device may know more (health.md, "On the Porch"); the week's payments and paper letters, but not while you sleep (areas.md).
Account priority: above, average (the default) or below, in the Accounts page or with sioul account priority <id> <above|average|below>. Mail of the accounts ranked above comes first in every lane, and a folded lane opens by itself when it holds some. Verified codes come at once whatever the account's rank.
Right now: the exception¶
- When it shows: as soon as the message arrives. Each account's inbox stays open with IMAP IDLE (RFC 2177), renewed every five minutes, so a code reaches you within seconds of reaching your provider.
- How it shows: one desktop notification, no sound (none while you sleep: the card waits on the Porch), with the kind of secret, the sender and its trust, the code itself with a button to copy it where the desktop's notifications take buttons (Linux; on Windows and macOS the code is in the text), and how long it stays valid when the message says. In the window, the same card sits on top of the Porch, with a copy button. On a phone, the card alone: no notification for a code there yet (android.md); the card on the home screen shows it too, with its site, until it expires (android.md, "The card on the home screen").
- What it does not do: open the rest of the Porch.
- Afterwards: the code is hidden once expired, and the message goes to its lane. Expired means the validity the message states ("valid 10 minutes", "for 2 days"), else what its kind usually lasts: a code 30 minutes, a sign-in link an hour, a password reset two hours, an address to confirm a day, a temporary password a week (it lasts until you have used it).
- Detection: in French and English, of codes (4–8 digits, two groups of three,
123-456, or a prefixed code likeG-482913; letters and digits right after their words,K7Q-2M9,F7G2K; two-factor, validation and activation codes too; "your Picshare code: …", "123456 is your Picshare code", "votre code Picshare : …"; "enter the code below", "le code suivant", in a message about signing in or checking who you are), passwords (temporary, initial, "your login details"), password resets ("choose a new password"), sign-in links ("magic link", "confirm your login"; or a message about signing in, in its subject or first lines, whose link or button expires within two days or works once: "Sign in to Readwell… This link expires in 15 minutes") and addresses or accounts to confirm ("confirm your email", "activez votre compte"). Years, prices, phone numbers, dates and postcodes are excluded, and so are a shop's code ("promo", "discount", "coupon"…) and the codes that are no secret ("your zip code", "votre code client"). Web addresses are left out of what is read (a tracking link's digits are no code, and its length pushed codes out of reach), with the invisible characters that pad a newsletter's first line; a text version that only says "view this email in your browser" is read with the HTML version after it (crates/sioul-core/src/codes.rs,text::readable,card.rs). - With a newsletter's headers (
List-Unsubscribe,List-Id,Precedence: bulkorlist): read the same, but only the subject and the first 1,000 characters of the text, where a site puts what you asked for; and a password, a reset, a link or an address to confirm named only below the subject counts beside a line that answers a request ("if you did not ask for this, ignore this message", "change it after you sign in"), or beside a link said to expire within two days or to work once. A code needs no such line: it is the code beside its words. A newsletter's article about passwords, resets and sign-in links stays a newsletter (codes::detect_message; tests:crates/sioul-core/tests/porch.rs, fixtures intests/fixtures/porch-bulk/).
Notifications: new mail at the times it may come¶
The Porch keeps mail until you look; you are also told when mail your lists let through now arrives, so that you need not come and look. Batched notifications helped attention and mood where none at all made people more anxious (Fitz et al. 2019): what helps is predictability, not silence. Code: crates/sioul-core/src/mailnote.rs (what is told, the words, the ledger), crates/sioul-app/src/mailnote.rs (batches, the device that tells, the platform's notification).
- When: after a fetch, the inbox's new mail whose row lets it come now (the Mail rows of the one matrix, attention.md:
Attention::mail,decide) makes one quiet notification for the batch, never one per message: how many, in words, and the first three senders with their subjects. "Two e-mails" / "Murena, Your invoice · Alice, Dinner on Friday". Arrivals ten seconds apart or less, every account together, are one batch (a minute at most): Sioul starting makes one notification, not one per address. Open (the desktop's button where it takes buttons; a tap on a phone) shows the Porch. - What waited: mail that came while it could not (outside its row's times, while you sleep, in a pause, Free time included, in a slot of time for you, as its row says, attention.md; while the Porch rests after a pause) waits, never lost. When a time begins in which some of it may come (work starts, you wake, a pause ends), one notification: "The Porch opens: three e-mails wait for you.", the first senders below. Once.
- Never told: mail set aside (forged, spam, a borrowed name), what Sioul's own spam filter flagged or moved (the review queue: on any device, the phone's background step and home card included), the blocked, hostile mail, codes (they keep their own notification, above), what you send yourself, the "Less important accounts" lane, newsletters and mailing lists in the Filed lane unless Include newsletters (an automatic sender there, a bill from no-reply, is told), mail read elsewhere before it came here (the server says it is read); and nothing while its row says shown, not told (◑: your safe senders' mail while you sleep, pause or take Free time, as usual) or not at all (attention.md).
- Quiet: no sound (on Linux the server is asked for none; on a phone, the "New mail" channel at low importance: no sound, no banner); your platform's own notification settings apply, its do-not-disturb included (normal urgency, where codes and reminders pass it). A batch with someone Always through in it goes out at critical urgency on a computer, and on a phone on its own channel, "New mail from people always let through", with a sound: it passes do-not-disturb (Q10). On a phone's lock screen, the count alone, never who.
- Once: each message by its account and Message-ID (else its file's name), told or waiting, on this device (
$XDG_STATE_HOME/sioul/mail-notified.toml, hashed: no address, no Message-ID; forgotten after 30 days). Mail fetched on an address's first fetch (its last two weeks) is never told. - One device tells: your phone and your computer fetch the same mail, and the sharing does not carry what each told (
porch.tomlholds each account's "Done for now" mark, not each message). So only the device you used last tells: the "notices" lease, as for the sites' gathered notification (database.md, "Leases"); the other marks the mail told without a word. Sharing off, each device tells its own. - The settings: Settings ▸ What reaches you ▸ By person ▸ Mail: Tell me of new mail (on:
[reminders] mail) and Include newsletters (off:mail_newsletters). - On a phone: while Sioul runs, as above; while it is away, as Android lets it fetch (android.md, "New mail").
Working hours, and quiet time¶
- Working hours: the seven days of the week, each on or off, from a start to an end (Settings, at the bottom of the left column;
[[window]]withday,start,endin the configuration). Older windows written as a start and a length still open the Porch, but only hours with an end bring quiet time. - Outside them, the Porch says when it opens next, and nothing else of the mail: no counts, no names. Today's doses still show: they are not mail (health.md, "On the Porch"). "Show all mail now" stays possible, quietly, and Reachable opens it (do-not-disturb.md).
- Inside them: a summary in a few sentences, then the lanes.
- With no hours configured, the Porch is always open and nothing is ever quiet.
- Quiet time: whenever it is not work's time: leisure (the evening, days without hours, time off from a day to another with a word, and once the day is closed, "Done for today", tasks.md, which asks how the day felt first, every answer optional, reviews.md), admin hours, a meal, sleep (areas.md). Work rests until it comes back: the Porch shows the codes and links you ask sites for, what you send yourself, and the mail your lists let through now (below); work addresses fold on the Mail page without their dots; work sites keep their notifications for later, chats and dating sites stay unless marked as work; budgets show only what fits now; Projects shows only yours, Time waits behind "Show anyway"; Tasks shows only what fits now. One sentence says until when (
$XDG_STATE_HOME/sioul/quiet.tomlkeeps the overrides). - Sleep: from winding down to waking, and naps (Health). Nothing notifies but doses, the codes you asked for and your Always through people, as usual (areas.md, "Sleep"); what their rows let show still shows on the Porch if you open Sioul.
- Closing is offered, never pressed: once the day's last hours of work or admin are over, the status line offers Close the work day, with one quiet notice, once; in the evening, Close the whole day; nothing during sleep (reviews.md). On a phone the offer takes the place of the sentence below while it stands.
- The way back is never suggested: the sentence in the status line opens a menu: work half an hour to four hours more, back to the usual hours, or, the day it was closed, back to today's plan. Detachment from work in the evening is what recovery needs most, and the mere expectation of work mail undoes it (Sonnentag & Fritz 2015; Becker et al. 2021).
Who may reach you, and when¶
Everyone is in one of five states (sioul_core::reach::Who), the same for mail, calls and messages from other apps:
- Strangers: in none of your address books (no address or number of theirs on a card Sioul reads, synced or on this device only) and on no list.
- Blocked (
blocked-senders.txt): never, on any channel: mail set aside for good, never shown, never counted, never notified; calls refused. - For the people in your address books, three levels:
- Safe (
safe-senders.txt): friends, chosen colleagues, chosen family. Only you put someone there. - Neutral (
neutral-senders.txt): anyone in your address books, unless you chose otherwise. Naming someone here keeps them neutral inside a domain or a category on another list. - Restricted (
restricted-senders.txt): those you would rather hear from only at chosen times (a demanding client, someone whose mail weighs).
- Safe (
What changed for strangers (October 2026): everyone no list named used to be neutral. Now only the people in your address books are, and those you let in from the screener; everyone else is a stranger, with a row of their own on each channel. Mail is unchanged: its strangers' row starts as its neutral row stood. Calls send strangers to voicemail at every time until their row is changed. Mail from a contact, or from an address with an entry of its own, no longer waits in the screener: the screener holds strangers, and senders a domain alone names.
The lists, four files beside the configuration, edited in Settings ▸ What reaches you ▸ By person; each line one of:
- an address, or a pattern with
*:*@example.orgfor everyone there,*@*.example.orgfor its subdomains,news*@shop.example;@example.orgtoo; - a phone number,
tel:+33199001234, however written (compared by its international form,phones::key; one written without its country is read as the Contacts settings' country), or a prefix,tel:+33899*; in the list editors, a number alone; - a contact card,
contact:<its UID>: all its addresses and numbers, now and later (the card's chooser writes it); - a category of your contacts,
category:Friends.
From the person to the group (porch::Senders::judge, judge_number): the address's or the number's own entry; then its card's (contact:; a card with no line of its own whose every address one list holds stands on that list: how "Their mail" chose for a person before cards were on the lists, so their numbers follow); then the card's categories; then patterns and prefixes, the more precise first; then neutral, when you let them in from the screener (known-senders.txt) or a card holds the address or the number; else a stranger. At one level, blocked comes before restricted before neutral before safe: in two categories, one blocked and one safe, a person is blocked. So a friend in a category marked safe can be demoted to neutral alone, and a colleague in Friends stays safe in a domain marked restricted. A message known by its sender's name only (another app's chat) is matched to a card with that very name, case and spaces aside (judge_name); between two cards of one name, the stricter; no card has it, the caller decides. An app where people choose their own name (Discord, Telegram) can only make its sender stricter by that name: a name there keeps Restricted or Blocked, and is otherwise a stranger (appnotes::named).
When each list comes is the one matrix of what reaches you (attention.md), drawn above the lists in Settings ▸ What reaches you ▸ By person, the channel chosen at its top: Mail, Calls, Messages (texts and chats from other apps, on a phone). Rows: Always through, Safe, Neutral, Restricted, Strangers, for calls Hidden numbers (a call that shows no number: someone who hides theirs, a hospital's switchboard; research, call-screening.md 7.2), for messages Groups; Blocked, a fixed row of never. Columns: the seven times (areas.md; the pause and Free time, pauses.md) and the two layers, time for you and do-not-disturb. Each cell a level, from at once to not at all; the usual values, and why, are attention.md's §3. Calls' are the research's (call-screening.md, 8.2), hidden numbers ringing in working and admin hours, when hospitals call that way; what rings whatever the matrix says (emergency numbers, a second call within minutes) is the calls' own (android.md).
The Porch shows a message when Attention::mail says so (at once, or shown, not told); the codes and links you just asked a site for, and what you send yourself, show whatever the time; the blocked never. A message waits, never lost, until a time its row lets it come. The matrix is [attention] in the configuration, shared with your other devices with the settings (attention.md, §7).
Who wrote, and to which address (mail): your safe senders' mail, and your Always through people's, comes to any of your addresses; the others' (neutral, restricted, strangers) only to an address for what now is for, unless the two never meet in your week: then their row alone decides, so that no mail waits for good (areas.md, "Mail").
People on a list: under the lists, everyone placed on one by their card (contact:<UID> lines), by name, with a choice: As their categories say, Safe, Neutral, Restricted, Blocked. A card no address book on this device holds is said as such, and stays on its list.
Your contacts' categories: under them, one row per category your contacts use (and each one a list names), with a choice: no list, Safe, Neutral, Restricted, Blocked. Everyone whose card is in that category, on a server or on this device only, takes it, each of their addresses and numbers. Nothing goes on a list by itself, family and friends included. Category names are compared without case or accents.
- A person's sheet (
PersonSheet.qml,reaches::person), from a message (⋮, "How they reach you…") or a contact's card ("How … reaches you"): what decides now, in a sentence ("Safe, as the category Friends says", "A stranger: in none of your address books, on no list"); their list; Always through; and what each channel does with them at each time, from their rows. From a card, the person, all their addresses and numbers: their card on one of the four lists (contact:<UID>), the own entries of their addresses and numbers taken out, so that the card decides for all of them, those added later too; or "As their categories say"; any address or number of theirs that keeps a list of its own is said under it. From a message without a card, the sender's address on one of the four lists, or "As their categories say" (its own entry taken out of every list: their card, its categories, else their domain, decide). Blocked and Always through exclude each other: one takes them off the other, said. The lists in Settings ▸ What reaches you ▸ By person take patterns and numbers. - Forged mail is judged before and apart: a forged message is set aside whatever the lists say, a safe sender's address included, and weighed as a stranger's, never as the sender it names; the reader offers no list choice on it ("not verified"): it could name someone else. A borrowed name is looked for only when the Porch does not know the sender (
set_aside,Sender::knows): the authenticated mail of someone you know (let in, safe, or named for themselves: their address on a list, their card, its categories, your address books) is never taken for impersonation, and keeps the name it uses; nor is your own mail, proven yours. A domain alone on a neutral or restricted list does not make a sender known. Mail that is not authenticated counts as a stranger's whatever address it shows, so a borrowed name in it is still found, a safe sender's address included. Nobody is judged by the server or the domain they share with another sender (crates/sioul-core/src/porch.rs,Senders).
Calls declined¶
The calls your phones declined (android.md) wait on the Porch of each of your devices, calmly: each at a time its caller may reach you (when their Calls row lets them now; for a row that never rings, in work and admin time: attention.md, Q23), by the matrix and the moment of the device showing it, never during the call, never counted, no badge, never a notification, on any device. On the phone that screened: "While you slept, a number not in your contacts called at 09:30."; on any other device, which phone: "…called your phone at 09:30.", "your other phone" on a phone, "your phone (GS290)" with several. One line per phone, caller, part of the day and day: "called twice, at 09:30 and 09:41"; "three times, the last at 09:52". A call that rang is never a line; after declined calls of the same number, the same day, it is said under their line: "The same number called again at 09:48, and that call rang." crates/sioul-app/qml/CallsSection.qml, calls::view, the words sioul_core::calls::lines; every phone's log read (calls::read_logs), each device's Seen too (calls::read_seen).
- Where: on the phone that screened, always; on your other devices, and a phone's other phones' calls, while Porch ⚙ ▸ Calls ▸ Calls your phones declined is on (
[porch] calls, on unless said; it travels with your settings). Off, each phone lists only its own, and your computers none. The part "Calls" of the sharing decides whether the log reaches a device at all (database.md, "The calls' logs"). - Who: the card's name in your address books, else the name in the phone's own contacts, else "a number not in your contacts", or "a hidden number"; the number under it, as people write it.
- The doubt, said: Sioul cannot see your voicemail: "They may have left a message." When Free mailed the voicemail (android.md), "They left a message (0:42)." and Listen, played here, on a computer too: the voicemail is mail, which every device fetches. The mail itself stays where the Porch puts it, as any mail from Free: the full record, which outlives the list.
- Each line: its own actions, few: Text back first (the phone's messaging app, the number filled in), Call back (the phone app, the number typed: you press call); on a computer, both only when the system has an app for
sms:andtel:links (Linux:xdg-mime; KDE Connect hands them to your phone), else Copy the number; on a phone held upright, their icons alone; Seen last, which takes the line away on every device, with ten seconds to undo. The rest in the line's menu, its ⋮ (a right click, a long press on a touch screen, the Menu key or Shift+F10 too): Add to contacts (Android's own form on a phone; Sioul's own in Contacts on a computer, the number filled in, saved by you), Block (asked once: the number goes on your blocked list astel:…, declined for good and never listed; the list travels, and the phone's table follows), How they reach you… (their sheet: their list, Always through, their calls of the month; for a number in none of your address books, the number's own), Why? ("Numbers not in your contacts go to voicemail while you slept."). On a phone, Android's own blocked numbers (calls and texts, for every app) are one button away. - Kept a month on every device, listed two weeks; a blocked caller's calls are never listed, nor shared; Seen on any device is seen on all (each device's own seen log).
- A person's calls of the month, on their sheet (
PersonSheet.qml,calls::history): one sentence per call, newest first, from every phone, rang or declined, and why a call rang when its row alone would not say it: "Yesterday at 23:10, while you slept: declined, sent to voicemail.", "Today at 06:48, while you slept: it rang. A second call within 15 minutes rings." No count; no block when there was none.
From your phone¶
The messages your phone's notifications brought (android.md), on the Porch of a computer, calmly: texts first, from the phone's SMS app; a chat app's or another app's when you chose it on the phone. Each shows once the phone let its notification through, and while its sender may reach you by the matrix and the moment of the computer showing it: their Messages row (attention.md; Mail's for a mail app, the groups' row for a group, Always through's when the list holds them; the app's and the conversation's own rows set on the phone read over them); a text from a service (a short number, a sender named in letters such as "AMELI") and an app's own notification by the automatons' row, at its times. A row that lets someone through at no time shows them in work and admin time, as for calls, so that no message is never listed. Never as a notification of its own, never counted, no badge. Nothing on the phone itself, whose notifications are there. crates/sioul-app/qml/PhoneMessagesSection.qml, phonemsgs::view, the words sioul_core::phonemsgs::lines.
- One line per conversation, part of the day and day, on one phone: "While you slept, Dr Martin's office wrote:", then each message with its time, "09:41 Your appointment is moved to Thursday 10:30."; a chat's names its app, "During work, Alice wrote on Signal:"; a group's, "In École on WhatsApp:", each message with who wrote it. With several phones, which one: "…wrote to your phone (GS290):". A picture says "a picture"; an app set to who and when says the times only, and "Their words stay on your phone: Signal sends who and when only."
- A code is its own line, never shown: "A code came from your bank at 09:12. It stays on your phone."
- Who: the card's name in your address books for their number, else the name the app gave them, else "a number not in your contacts".
- Each line: Reply first, where its phone says the notification's Reply works (android.md), then Call back for a text from a whole number, and Seen last, which takes the line away on every device, with ten seconds to undo, and never touches the phone's notification nor marks anything read. Where no Reply is offered, Text back comes first for such a text: it opens the conversation on the Texts page when texts are read on this computer (texts.md; texts: SMS phase b), else, as Call back, the computer's app for
sms:andtel:links when it has one (KDE Connect hands them to your phone), else Copy the number. The rest in the line's menu, its ⋮ (a right click, a long press, the Menu key or Shift+F10): Mark as read in Signal, where the notification offers one, Open on the Texts page for a text, How they reach you…, Add to contacts, Block (asked once: the number goes on your blocked list; their calls go to voicemail, their messages wait on the phone for good and never reach your computers). - A reply: Reply opens a box under the line, "Reply to Alice on Signal…" ("Reply in École on WhatsApp…" for a group), with Send (Ctrl+Enter) and Cancel (Escape); the characters left show near the limit only (2,000; a text's 1,600), and what you typed stays while the Porch reads its lines again. It goes through the notification's own Reply, or, for a text from a whole number where the phone it came to sends texts for this computer, as one of Sioul's own texts from that phone, never to a short number. Your reply then shows under their messages, "You · J'arrive.", with its state in words, never a plain "Sent": "Waiting for your phone (it last shared at 14:02).", "Handed to Signal at 14:03.", "Signal shows it in the conversation.", or a text's "Sent at 14:03."; Take back while it waits for the phone, said at once ("Taking it back: your phone will not send it, unless it has taken it already."), and said too late when the phone had taken it already; Send again where it fits (expired, clocks apart, a doubt). Once the notification is gone, holds another conversation, or its Reply is closed to Sioul: "Reply on your phone." The first reply in Signal, Telegram or /e/OS's Message says once that replying marks the conversation read there. Mark as read in Signal asks first: "Signal may tell them that you read it."; once handed, the line is seen on every device. A conversation you answered comes at once for 20 minutes, here and on the phone, but in a pause. Nothing on the phone itself, whose notifications are there; a person's sheet and the agents offer no reply.
- Where: on a computer whose part Messages from your phone is on (Settings ▸ Your folder and sharing ▸ What travels from this device). That switch is the Porch's: off, nothing of them shows, nothing comes.
- Kept a week; a blocked sender's never shared; Seen on any device is seen on all.
- A person's messages of the week, on their sheet, by their number: "Today at 09:41: Your appointment is moved to Thursday 10:30.", "Yesterday at 18:02: a message on Signal; its words stay on your phone."
The Porch's memory¶
- An account the Porch was never closed on shows its first two weeks only: the older mail kept since is the archive's, not the Porch's.
- "Done for now" closes the Porch on what it showed: for each account, the newest message shown (its UIDVALIDITY and UID). Anything newer waits for the next window, even mail that reached the server earlier and was fetched later.
- "Done for now" is not told to the server. Fetching uses EXAMINE and BODY.PEEK: closing the Porch changes no read flag. Opening a message marks it read, as in any client, and the reader's actions (reply, archive, delete, junk) are the mail client's (client.md).
sioul porch --allshows everything again; the state lives in~/.local/state/sioul/porch.toml.
Blocked senders and borrowed names¶
- Blocking an address, a number, a card, or everyone at a domain, sets their mail aside for good: never shown, never counted, never read as a payment, never notified; their calls are refused. Nothing is deleted. The list is
~/.config/sioul/blocked-senders.txt(above); the window blocks from a message or a contact and unblocks from Settings ▸ What reaches you ▸ By person or a person's sheet, the command line hassioul blockandsioul unblock(an address, a pattern, ortel:and a number). Unblocking takes the entry out; when a card, a category or a domain would still block them, they are written neutral. - A name borrowed from a brand or a public service ("lnfos PrimeVlDEO" sending from an unrelated domain), or from one of your own domains (a name made only of "yourdomain" and words like "support" or "mail admin", from an address outside that domain: the fake "your account will be deleted"), is set aside with the reason. Shared providers such as gmail.com are nobody's own domain, and your own name in full is not your domain. A signature proves the domain, not the name shown before it: names are compared by their skeleton, as Unicode's confusable detection does (UTS #39 §4), so lowercase l for capital I, 0 for O, rn for m and Cyrillic letters do not hide the brand. A brand's words count anywhere in a name ("Crédit Agricole Nord de France", "PayPal Kundendienst"), except for a brand whose name is also an everyday word or a place (
brands.everydayof the packs: Amazon, Apple, Outlook, UPS; in France, Orange, La Poste, Impots, Free Mobile, Société Générale): its name counts only when it stands alone, or beside the words of a service (brands.service_wordsof the language packs: "support", "service", "security", "billing", "customer", "delivery", "client", "espace", "suivi"…, and small words such as "your", "votre", "of", "de"), a number, or a label of the brand's own domains ("Amazon.com"). So "Apple Support", "Infos La Poste" and "Service client Orange" borrow a name, while "Orange County Library", "Apple Pie Bakery", "Café de la Poste" and "Jean Orange" do not (lookalike::borrows;namesfor the others). Settings ▸ Words lists both, yours to change. Someone you know keeps the name they use: a sender let in, safe, or named for themselves, when their mail is authenticated (above, "Forged mail is judged before and apart";crates/sioul-core/src/lookalike.rs).
Spam: never for people you know¶
Spam is said of a stranger's mail only, whoever says it: your provider (its X-Spam-* headers, below in "Trust") or Sioul's own filter, trained on your mail (spam-filter.md). porch::triage, in order:
- Blocked, forged, or a stranger borrowing a name: set aside, as above, whatever any filter says.
- Protected from every spam verdict: mail from anyone but a stranger (safe, neutral, restricted: in your address books, on a list, let in from the screener); a code or a password you asked for; mail a project's routes take (a conversation a project pulls in leaves the review queue for the project); your own verified mail; a message you said is not spam, here, on another device or in another mail client (
spam::labels::said_ham: its$NotJunk, or every device's label log). Mail nothing authenticates (SPF and DKIM failed, no DMARC or ARC vouching) has a stranger for its sender whatever its address, and no protection. - Your provider's verdict: a stranger's mail it flags is set aside.
- Hostile mail to a shielded address: its own lane, whatever the filter says.
- Sioul's own filter, once it has a table (Mail ▸ ⚙, "Your own spam filter"), says how likely a stranger's message is spam: probably spam from the spam threshold (95 % unless you change it), maybe spam from the unsure threshold (50 %) up to it, probably not spam below. What is done with each is yours, the matrix (spam-filter.md, "Two thresholds, and what is done with each verdict"): Move to spam (into the account's Junk folder on the server as it arrives, by every device that fetches it,
crates/sioul-app/src/spam.rs), Flag only (it stays where it is), Do nothing (its lane, as if unjudged). Until you choose: probable spam and doubts flagged, the rest nothing.
What the filter flags or moves goes to Caught by your own spam filter (Lane::Review, the review queue in the code): folded, no count beside it, never in the summary's count nor the phone's home card, never notified, opened when you want. Nothing there asks to be confirmed: the training learns from what the filter caught as it is until you say otherwise, a maybe spam not until you say (spam-filter.md, "The labels"), and Not spam is there for a mistake. Each message has its quiet word ("probably spam", "maybe spam"), never why nor how sure (the owner's decision: "your own filter: probably spam" is the whole reason), and Spam and Not spam; Spam for all and Not spam for all above them; ten seconds to undo each. "Done" never closes the queue: flagged mail stays two weeks after it, moved mail as long as it sits in the Junk folder unreviewed, here or after a move by another device (found by its Message-ID in the account's Junk folders, through every device's log of what the filter moved). Said on any device, or marked junk or not junk in another mail client, it leaves the queue everywhere. The lanes' "?" say the matrix as it is.
Reading a message¶
- Its headers apart: who wrote, the address and how far it is verified, to whom, copied to whom, when, the subject.
- Why it is where it is, folded, on request.
- Its attachments apart, folded until asked for: one line each with its kind, name and size. Opening or saving runs the antivirus first (ClamAV: its daemon when it runs, else its scanner); a threat found, and nothing opens and the copy is deleted; no antivirus on the device, and Sioul says the file will not be checked and asks first, with the command that installs one (client.md, "Antivirus"). Mail set aside lists them and does not open them.
- HTML mail, formatted and safe (
ammonia): paragraphs, lists, bold, links; nothing else survives, no images, styles, scripts, forms or frames, so nothing remote loads and nothing runs. Layout tables become plain blocks, headings bold paragraphs. The messages a reply quotes or a forward carries (Gmail's, Apple Mail's, Thunderbird's, Outlook's) are folded under "Show the earlier messages". Read on a thread of its own, "Opening the message…" said meanwhile, and in time that grows with the message, never with its square: a crafted one cannot hold the window (client.md). - Names and subjects as words: a sender's name, a subject, an event's title are drawn as plain text wherever they show: one holding a picture's tag shows the tag, and fetches nothing (client.md).
- Plain text in parts: what this message says; "On …, X wrote:"; the messages it quotes, set off by a bar per level and folded when long; the headers of the messages it forwards or answers (Outlook's "De : / Envoyé : / À : / Objet :"), in a box; the signature, dimmed. Its web links are clickable and shown by their site (
crates/sioul-core/src/reading.rs). - Every link says where it goes before you click: its full address shows under the message while the pointer is on it.
- Not spam, beside "Let this address in", under mail set aside as spam and under the review queue's:
$NotJunkset on the server ($Junktaken off); a message the filter moved into the Junk folder goes back to the inbox, any other stays where it is; it goes back to the lane it would have had, for good and on every device: each sync keeps the keyword, and every device reads every device's label log (spam::labels::said_ham, by account and Message-ID), servers without keywords too; ten seconds to undo. Spam, under the review queue's:$Junkset, into the Junk folder (one the filter moved stays there), a spam label. The next training learns from both (spam-filter.md, "The labels"). - Unsubscribe, above a message that names a way out of its list (
List-Unsubscribe): in one click, by a message or on its page, ten seconds to undo, never for mail set aside, hostile or unproven (client.md, "Unsubscribing").
Trust: Sioul checks every message¶
- When a message is fetched, Sioul checks it itself (Stalwart's
mail-auth, through the system's DNS):- SPF, on the server that handed the message to your provider: the first hop from a public address at the top of the
Receivedchain. Every hop below it could be written by anyone. Walking down from the top, your provider's own hops are passed (nofrom, a private or local address, the delivery into the mailbox, and Microsoft 365's hops between its own servers, by the names they write afterby:*.prod.outlook.com,*.prod.exchangelabs.com,*.outlook.office365.com,*.prod.protection.outlook.com, whose addresses are public); the next line is where the message came in (trust::boundary). Its address is read in brackets ([203.0.113.5], Postfix, Exim, Gmail, sendmail), or in parentheses alone right after the first word, as Microsoft 365 writes it (from mail.sender.example (203.0.113.25) by BN8NAM12FT034.mail.protection.outlook.com, its Internet frontend) and qmail (from unknown (HELO mail.sender.example) (203.0.113.5);(ident@203.0.113.5)); a HELO name in brackets or afterhelo=is the sender's, never the address. A line written another way gives no entry at all, and no SPF check, rather than an address from the sender's own lines below it. - DKIM, each signature against the key its domain publishes. Checking at arrival matters: keys rotate, and some signatures expire within days.
- DMARC, with the domain's own policy. A failure is a forgery when the domain asks receivers to act on it (quarantine, reject). With
p=none, on a mailing list (which rewrites what it relays), or for mail that never came from outside (your provider's own notices, a message to yourself), the sender is only "not verified". A code is never excused by list headers: no list relays your codes, so a code failing DMARC under such a policy is forged, and set aside, whatever itsList-Idsays. - ARC, for forwarded mail, and the reverse DNS of the sending server.
- SPF, on the server that handed the message to your provider: the first hop from a public address at the top of the
- The results go on top of the stored message, as an
Authentication-Resultsheader (RFC 8601) under a name unique to this installation and under.invalid, so no sender can write results that pass for Sioul's. The Porch trusts them first, then the provider's. Mail fetched before is checked once, in the background (sioul verify;--againchecks everything again). - Verified: DMARC pass for the domain the From line shows, else a valid DKIM signature of that same domain (a signature of another domain says nothing of the sender). "The same" is DMARC's relaxed alignment (
trust::aligned): one organisational domain, the name registered under its public suffix, read from the Public Suffix List that thepslcrate carries, somail.example.organdshop.example.orgare one sender, whilevictim.github.ioandattacker.github.io, orco.ukand a name under it, are not. Sioul's own DMARC check at arrival (mail-auth) finds the organisational domain by DMARC's DNS tree walk instead, as the newer DMARC standard does. A DMARC failure always wins over any pass, and a results line that cannot be read to its end (an unclosed quote or comment) proves nothing. Forged: DMARC fail under a policy that says so. Not verified: everything else, with the reason. - Not authenticated: SPF and DKIM both failed, and neither DMARC nor ARC vouches for it (an ARC chain counts when its newest seal is your provider's or one of your own domains': anyone can seal a chain of their own). Nothing proves it comes from the address it shows, so its sender is a stranger for every rule, whoever the address is: no People lane (the screener, where letting them in would change nothing), a stranger's times, no protection from spam, no project route that names a sender (one by the subject, the text or an attachment still takes it), and no list changed from it; "Its sender could not be verified (SPF and DKIM failed): treated as someone you do not know" says why. A blocked address stays blocked: keeping someone out asks for no proof. A code keeps its lane and its warning. "none" (nothing published to check) and a softfail are not failures (
trust::authenticated). - Hovering the shield says each check, its result and the domain it was for, and who checked: Sioul at arrival, or the provider.
- Spam verdicts: SpamAssassin's
X-Spam-FlagandX-Spam-Status; rspamd'sX-Spamd-Result,X-SpamandX-Rspamd-Score; only those your provider wrote, above theReceivedline where the message came in from the Internet (trust::read_spam_verdict). Reading down from the top, the provider's own lines are passed: the delivery into the mailbox (LMTP), filters and servers on private or local addresses, lines withoutfrom. The first line from a public address, or one written another way, ends what is the provider's. Below it a sender writes what it likes, "X-Spam-Flag: NO" too; a filter that writes its verdict at the bottom of the headers, or just below that line, cannot be told from the sender, and is not read. Mail that never came from outside is read above its lowestReceived; mail with none (copied in) has no verdict. - What you say of spam is kept, for the spam filter to learn from and for every device to know: "Junk", "Not junk", "Spam", "Not spam", and blocking a sender from one of their messages, each add a line to this device's own label log,
$XDG_STATE_HOME/sioul/spam/labels/<device>.jsonl(spam::labels), which travels sealed to your other devices, each reading every device's: when, the account, the folder, the message's UIDVALIDITY, UID and Message-ID, spam or ham, and which act; never a word of the message. The keywords$Junkand$NotJunk, set by Sioul or another mail client, are kept at each sync in the message's file name, as Dovecot keeps keywords (a letter each, named in the folder'sdovecot-keywords).
Paper letters¶
The envelope stays outside: a scan, a phone photo or a PDF dropped in a folder (by you, a scanner, or someone who opens the post for you) is read on this device, understood by rules, and waits for the window as a card, like mail. Opening post is part of admin anxiety (Money and Mental Health 2018), and a date written "within two months" is remembered by no one: the card says it as a date. Code: crates/sioul-core/src/letters.rs (reading, keeping, filing), crates/sioul-sync/src/ocr.rs (the text), crates/sioul-app/src/letters.rs, qml/LettersSection.qml.
- Where scans arrive: Porch ⚙ ▸ Paper letters ▸ "Where scans arrive" (unset:
letters/inboxin the notes folder). PDF, PNG, JPEG, TIFF, WebP. A file still being written (less than twenty seconds old) is read the next minute. Each is read once (by its name, size and time). - The place, before any scan (a computer's Porch, 15 October 2026): nothing showed the feature until a scan came. Under "Paper letters" one quiet line says where scans go (
letters-place, the folder named), with Open the folder (made if missing:letters::folder) and Add a scan… (a file chooser for PDF and pictures, several at once: each copied into the folder under a free name, "scan (2).pdf", the chosen file left where it was, read at a coming minute;letters::add, the result on the status line); with no folder at all (no notes folder, none chosen), Choose the folder… opens the Porch's ⚙. Whether this computer reads scans is asked off the window's thread every ten minutes (ocr::reads_here: Tesseract andpdftotextstart); when not, the line gives the install command before any scan comes (letters-reader-missing). No count, no badge. The New menu holds A paper letter (scan)…, the same chooser from any page (not on a phone). - A phone reads none:
letters::tickreturns at once on Android, and the phone's Porch shows the cards a computer made (with the notes part of the sharing), without the line. Before, a phone holding unread scans in its copy of the folder said "no program here reads them" with a computer's install hint. - The text: a PDF's own text when it has one (Poppler's
pdftotext), else its pages at 300 dpi (pdftoppm) read by Tesseract, in French and English when those are installed; photos and images by Tesseract directly. Both are your system's programs, optional as the antivirus is: without them the scans wait, unread, and the Porch says the command that installs them (Fedora:sudo dnf install tesseract tesseract-langpack-fra poppler-utils). Nothing leaves this computer. - What the rules find, French first, English too:
- who: a body that writes to everyone found in the letterhead (CAF, Assurance Maladie, Finances publiques, Urssaf, France Travail, MDPH, préfecture, tribunal, commissaire de justice, hospital, mairie, banks, EDF…), else the letterhead's first line;
- what, the gravest first: a formal notice ("mise en demeure"), a tax notice, a decision with its ways of appeal, a reminder, an appointment ("convocation"), a bill ("avis des sommes à payer", "titre exécutoire", "facture"), an acknowledgment, an attestation, a contract;
- how much: the amount on the line that asks for it ("Montant à payer", "somme de");
- by when: a date after "avant le", "au plus tard le", "date limite", "échéance", "jusqu'au", "no later than", "by"; else a delay ("dans un délai de deux mois à compter de la notification", "sous huitaine", "sous quinzaine", "within 30 days"), counted from the day the scan came (the notification) or from the letter's own date when it says so; the words that set it shown as the letter writes them;
- its own date ("Lyon, le 28 septembre 2026"), an appointment's day and hour ("le jeudi 12 novembre 2026 à 14 h 15"), "recommandée avec accusé de réception", your number with them ("Numéro allocataire", "Référence"), and the project whose name it carries.
- The card, in the window only: who and what, then a sentence each (asks €86.40; by Sunday 15 November ("avant le 15 novembre 2026"); sent registered; dated; your number). "See the scan"; "A task for that date" (in your usual list, due that day, tied to the scan and the project: "Pay the water bill €86.40", "The tax office's decision: contest it or not"); "Into the agenda" for an appointment, an hour long; a project; "Done, filed": the scan moves to
letters/<year>/<day> <sender> <kind>.pdf, its text kept beside the list (letters/<id>.txt). Making the task files the scan first, so the task's tie holds. - Not built: the AI reading a letter line by line, the "is this genuine?" checks (the office's domain, its IBAN), a mail intake address for a helper.
Done¶
In crates/sioul-core, crates/sioul-sync, crates/sioul-cli and crates/sioul-app, with tests:
- reading messages from a Maildir or a folder of
.emlfiles, and the card of each message; - trust v0, the provider's id learned from mail, spam verdicts, the route;
- detection of one-time codes, and their expiry;
- the notes folder and routing, the known senders and "let in", the lanes and their reasons;
- the summary, in English and French;
- admin windows, and the Porch's memory ("Done for now");
- accounts: found from the address (the provider's autoconfig, then Thunderbird's ISPDB, then a guess said as such), passwords in the system keyring, written into the configuration without losing its comments;
- IMAP sync, over TLS (rustls) or STARTTLS, into Maildir: the last 14 days at first, then only what arrives, for every folder but views of others (Gmail's "All Mail"); flags and deletions made elsewhere brought back; one fetch at a time per account, across processes; the server written to only when you act (client.md);
- the watcher: IDLE per account, reconnection with growing pauses, no retry after a refused password, the notification for "right now";
sioul porch,done,card,projects(oncecases, still taken),window,budgets,account add|portal|list|test|password|remove,syncandwatch;- the window (Qt Quick through CXX-Qt): the Porch with codes on top, the lanes and a plain-text reading pane; budgets; accounts with the add form; the status line; keyboard throughout; the desktop's light or dark colours. Built with Qt's development files (building.md), linted, and checked page by page on invented mail.
Next, in this slice¶
- Trust, the rest: Spamhaus DQS for the sending address; lookalike domains (the names are done).
- The screener's memory, continued: known senders learned from your Sent folders.
- The background service on its own: a user service that watches with the window closed, and a tray icon.
- Mail deleted or moved on the server, found faster (CONDSTORE/QRESYNC). It leaves the Porch already: each fetch of the inbox that is not quiet asks the flags of every message kept here and takes out those gone (
mailbox::reconcile); the two extensions would ask only what changed. - One card at a time, the safe-opening card of the design; paper letters already come as cards (above).
- Portals as accounts: web-only mailboxes (Proton without Bridge among them) opening in sealed profiles inside Sioul; their notification emails become "a letter waits".
- Gmail without an app password, for everyone: signing in with Google for mail is built, with a key of your own (google.md, "Mail"). Sioul's own key waits: its maintainer has not registered it yet, and Gmail's scope is restricted, so Google must review it first (architecture.md, "Known hurdles").
Accepted when¶
- A morning's mail sorts as the tests' fixtures do, in both languages.
- A verified code reaches you within seconds, outside any window, without opening anything else.
- Outside a window, nothing but "the Porch opens…" and codes reaches the screen.
- Every lane shows its reason on request; nothing is deleted.