(bugfix): keep paging older imap mail and pass hub covered app id

This commit is contained in:
Maksym Sadovnychyy 2026-11-03 20:25:16 +01:00
parent 243c06879d
commit 70a6894b80
18 changed files with 190 additions and 126 deletions

View File

@ -6,6 +6,18 @@ The format follows [Keep a Changelog](https://keepachangelog.com/) and [Semantic
## [Unreleased]
## [0.4.6] - 2026-11-03
Folder sync keeps paging older IMAP mail after the newest page is already archived. Identity Hub desktop login passes the covered app id.
### Fixed
- Folder sync no longer stops after the newest page when those messages are already archived. Headless and in-app sync keep paging older mail (Gmail and other IMAP) until the folder is fully listed.
- IMAP header sync pages by UID SEARCH instead of shifting sequence numbers, keeps the list cursor across soft cancel and transient errors, and retries an empty FETCH page instead of skipping it.
### Changed
- Bump **MaksIT.IdentityHub.Client** to **4.0.0** (covered applications `{ Id, Name }`; desktop login `?app={Id}`).
- Identity Hub desktop login URL includes covered app id: `/desktop-login?provider=…&app=MaksIT.PostClient` (`IdentityHubAddress.CoveredApplicationId`).
## [0.4.5] - 2026-10-31
Unread and flag updates stay local when the server store is cancelled or races with sync. Pending Seen/Flagged wait for STORE ack before background sync can overwrite them.
@ -13,7 +25,7 @@ Unread and flag updates stay local when the server store is cancelled or races w
### Fixed
- Opening an unread message no longer brings the unread dot back when you select another row: mark-as-read is not cancelled with the reading pane, and IMAP cancel is not treated as a store failure.
- Manual Mark Read/Unread/Flag and folder mark-all-read keep optimistic state off the busy `_work` token; IMAP flag stores use a non-folder CTS and pending archive flags until STORE succeeds (or soft “not connected”, which stays pending and flushes on reconnect).
- Manual Mark Read/Unread/Flag and folder mark-all-read keep optimistic state off the busy `_work` token; IMAP flag stores use a non-folder CTS and pending archive flags until STORE succeeds (or soft "not connected", which stays pending and flushes on reconnect).
- Background sync and header upserts no longer clobber newer local Seen/Flagged; `UpsertHeaders` skips flag overwrite by default and `UpdateFlags` respects pending UIDs.
- IMAP MOVE no longer falls back to copy-then-delete on cancel; remaining UIDs are checked before copy/expunge. Cross-mailbox moves delete each source UID after a successful copy.
- Rules and headless sync write archive flags only after a successful STORE. SyncFolder no longer advances past a folder on list failure.
@ -25,7 +37,7 @@ HTML mail blocks remote images by default. Windows setup upgrades keep Identity
### Added
- **View → Block remote content** stops HTML mail from loading images and tracking pixels from the internet (on by default). Inline attachments still show; **Show images** loads remotes for the open message only.
- **View -> Block remote content** stops HTML mail from loading images and tracking pixels from the internet (on by default). Inline attachments still show; **Show images** loads remotes for the open message only.
### Changed
@ -37,12 +49,12 @@ HTML mail blocks remote images by default. Windows setup upgrades keep Identity
## [0.4.3] - 2026-10-29
Gmail and Microsoft mailbox sign-in uses Identity Hub IdP-only. Hub keeps the IdP refresh in its vault; Postclient renews mailbox tokens automatically. Help shows What’s New after upgrade. Store and Flathub packaging notes.
Gmail and Microsoft mailbox sign-in uses Identity Hub IdP-only. Hub keeps the IdP refresh in its vault; Postclient renews mailbox tokens automatically. Help shows What's New after upgrade. Store and Flathub packaging notes.
### Added
- Microsoft Store and Flathub submission notes under `packaging/` (listing copy in English, Italian, French, German, and Spanish).
- **Help → What’s New** shows the changelog once after each upgrade (seen version is remembered in settings).
- **Help -> What's New** shows the changelog once after each upgrade (seen version is remembered in settings).
- Screenshot tour helpers under `utils/tools/` and `assets/screenshots/` for store listings.
### Changed
@ -53,7 +65,7 @@ Gmail and Microsoft mailbox sign-in uses Identity Hub IdP-only. Hub keeps the Id
### Fixed
- Opening a message, a subfolder, or the star uses the window's own mailbox connection. Background sync keeps a separate connection, so a long sync no longer blocks those updates or puts the old unread mark back.
- Reading a message from an Outlook data file after a write no longer fails with “End of stream”: EML export allows dropping opaque MAPI metadata that has no portable equivalent.
- Reading a message from an Outlook data file after a write no longer fails with "End of stream": EML export allows dropping opaque MAPI metadata that has no portable equivalent.
## [0.4.2] - 2026-10-04
@ -76,12 +88,12 @@ Create bundle asks for the passphrase once, then opens Save. About lists the com
### Changed
- **Help → About** lists Info, Privacy, Security, and Support (`info@`, `privacy@`, `security@`, `support@maks-it.com`) instead of one personal address.
- **Help -> About** lists Info, Privacy, Security, and Support (`info@`, `privacy@`, `security@`, `support@maks-it.com`) instead of one personal address.
- The English, Italian, French, German, and Spanish READMEs describe folder stores, Easy Migration, background sync, spam marks, and the current menus.
### Fixed
- **File → Easy Migration → Create bundle…** asks for the passphrase and the repeat in one window. The save dialog opens after that window closes.
- **File -> Easy Migration -> Create bundle...** asks for the passphrase and the repeat in one window. The save dialog opens after that window closes.
- Windows setup keeps an existing desktop shortcut when the checkbox is off on upgrade. Installing the shortcut removes a second desktop icon, including the "(2)" copy.
## [0.4.0] - 2026-09-27
@ -90,10 +102,10 @@ Background sync runs at boot with nobody signed in. Easy Migration packs this PC
### Added
- **File → Easy Migration** writes a `.postbundle` locked with a passphrase (mail, accounts, secrets, and the shared model folder). **Open bundle…** restores it into this PC’s Postclient folders and rewrites store paths. The background sync service is not reinstalled.
- **File -> Easy Migration** writes a `.postbundle` locked with a passphrase (mail, accounts, secrets, and the shared model folder). **Open bundle...** restores it into this PC's Postclient folders and rewrites store paths. The background sync service is not reinstalled.
- The message search box has an × that clears the text.
- **Settings → Background sync** installs a boot-time service (Windows service as Local Service, Linux systemd system unit). It syncs every saved mailbox with nobody signed in. Passwords are copied into the machine store so the service can sign in to the mail servers. **Share this account** only lets other people on this PC open that mailbox.
- **Mark as spam** remembers that message on this PC and teaches every mailbox, even after the message or mailbox is gone. Similar mail is only a hint. **Not spam** drops that memory and returns the message to Inbox. Moving mail into Junk does not teach the filter. **Settings → Indices… → Spam marks…** lists what was learned.
- **Settings -> Background sync** installs a boot-time service (Windows service as Local Service, Linux systemd system unit). It syncs every saved mailbox with nobody signed in. Passwords are copied into the machine store so the service can sign in to the mail servers. **Share this account** only lets other people on this PC open that mailbox.
- **Mark as spam** remembers that message on this PC and teaches every mailbox, even after the message or mailbox is gone. Similar mail is only a hint. **Not spam** drops that memory and returns the message to Inbox. Moving mail into Junk does not teach the filter. **Settings -> Indices... -> Spam marks...** lists what was learned.
- The meaning-search model lives in the same machine folder (`models` under ProgramData or `/var/lib/maksit/postclient`), so every account and the sync service share one download. An existing copy in the user profile is copied across on startup.
- Layout upgrades live in `Upgrades` (folders, models, archive files, PST stores). Each one records `InstallsOver`, the last release that still has the old layout, so it can be removed once that version is no longer supported. SQL schema stays with the archive store.
@ -166,7 +178,7 @@ First sync paints mail as it arrives, meaning search can use a discrete GPU, and
### Added
- **Help → Logs** opens a copyable window over the app log folder.
- **Help -> Logs** opens a copyable window over the app log folder.
### Changed
@ -189,7 +201,7 @@ Expired folder mail goes to Trash; Trash can be kept forever or purged after a n
### Changed
- **Settings → Retention…**: folder days (`0` = keep forever) now **move** older messages to Trash after Get Messages, instead of deleting them permanently. The Trash row uses the same days field: `0` keeps Trash forever, a number of days **permanently deletes** old Trash. The grid shows the action per row.
- **Settings -> Retention...**: folder days (`0` = keep forever) now **move** older messages to Trash after Get Messages, instead of deleting them permanently. The Trash row uses the same days field: `0` keeps Trash forever, a number of days **permanently deletes** old Trash. The grid shows the action per row.
- Delete (toolbar, **Del**, empty folder, delete rules) moves messages to Trash. If Trash is missing, it is created. Deleting from Trash is still permanent. Local stores honor Trash when emptying a folder.
### Fixed
@ -209,8 +221,8 @@ Folder stores replace attached Outlook data files, Gmail/Microsoft sign-in goes
### Added
- Folder **stores** replace Outlook data files as mailboxes: **File → New store…**, **Attach store…**, **Move store…**, and **Detach store**. Import PST copies into a store under `%LocalAppData%\Postclient\stores\` (or portable `data/stores/`). Each IMAP/POP3 account and each store has its own `mail.db` (FTS + meaning index). Search and move merge or copy across those databases without rebuilding embeddings.
- **Settings → Retention…**: per-folder days (`0` = keep forever). Older mail is deleted permanently (not Trash) after Get Messages.
- Folder **stores** replace Outlook data files as mailboxes: **File -> New store...**, **Attach store...**, **Move store...**, and **Detach store**. Import PST copies into a store under `%LocalAppData%\Postclient\stores\` (or portable `data/stores/`). Each IMAP/POP3 account and each store has its own `mail.db` (FTS + meaning index). Search and move merge or copy across those databases without rebuilding embeddings.
- **Settings -> Retention...**: per-folder days (`0` = keep forever). Older mail is deleted permanently (not Trash) after Get Messages.
- IMAP/POP3 usage on the account node in the folder tree. Gmail and Microsoft sign-in go through **Identity Hub** (`https://identity.maks-it.com/desktop-login?provider=`). Hub JWT is stored in `secrets.bin`; IMAP uses a short-lived mailbox token (XOAUTH2). Override the Hub origin with `POSTCLIENT_IDENTITY_HUB`.
- Folder tree expand/collapse for accounts and nested folders is stored in `settings.json`.
- **Shift+click** and **Ctrl+click** select several messages or folders. Delete (including the **Del** key) and drag-move apply to the whole selection.
@ -241,13 +253,13 @@ On-device meaning search, Outlook data files as mailboxes, rules, extra UI langu
### Added
- **Settings → Indices…**: keyword index (FTS) and optional meaning index (RAG) with EmbeddingGemma 300M. The ~300 MB ONNX package downloads from Hugging Face (`onnx-community/embeddinggemma-300m-ONNX`: graph, weights sidecar, tokenizer). GitHub releases do not carry those files. Vectors stay in `mail.db` on this PC. Device Auto / CPU / GPU (Windows DirectML). Rebuild keyword, rebuild meaning, or repair leftover rows.
- **File → Check for updates…** asks GitHub for the latest release and can download the matching package. **File → About Postclient** matches the WVC210 About card (brand, version, credits, email, license, copyright, maks-it.com).
- Attach a `.pst` / `.ost` as its own mailbox (File menu or account type *data file*). Unicode `.pst` is writable (folders, flags, move, delete). An `.ost` or ANSI `.pst` is copied to a Unicode `.pst` on first write. **New data file** creates an empty Unicode `.pst`. Right-click an attached store to **Detach data file**; the file stays on disk. File → Import still copies mail into another account.
- **Settings → Rules**: import Outlook `.rwz` (Rules Wizard) or JSON, export JSON, and edit rules (enable, conditions, move/delete/flag/label). Each rule belongs to an account; a move can target a folder on another mailbox, including an attached data file. A move folder name stays red until it matches that mailbox (name, full path, or last path segment). **Run all rules** applies them to existing mail. Saved in `settings.json`. They also run on Get Messages and on import.
- **Settings -> Indices...**: keyword index (FTS) and optional meaning index (RAG) with EmbeddingGemma 300M. The ~300 MB ONNX package downloads from Hugging Face (`onnx-community/embeddinggemma-300m-ONNX`: graph, weights sidecar, tokenizer). GitHub releases do not carry those files. Vectors stay in `mail.db` on this PC. Device Auto / CPU / GPU (Windows DirectML). Rebuild keyword, rebuild meaning, or repair leftover rows.
- **File -> Check for updates...** asks GitHub for the latest release and can download the matching package. **File -> About Postclient** matches the WVC210 About card (brand, version, credits, email, license, copyright, maks-it.com).
- Attach a `.pst` / `.ost` as its own mailbox (File menu or account type *data file*). Unicode `.pst` is writable (folders, flags, move, delete). An `.ost` or ANSI `.pst` is copied to a Unicode `.pst` on first write. **New data file** creates an empty Unicode `.pst`. Right-click an attached store to **Detach data file**; the file stays on disk. File -> Import still copies mail into another account.
- **Settings -> Rules**: import Outlook `.rwz` (Rules Wizard) or JSON, export JSON, and edit rules (enable, conditions, move/delete/flag/label). Each rule belongs to an account; a move can target a folder on another mailbox, including an attached data file. A move folder name stays red until it matches that mailbox (name, full path, or last path segment). **Run all rules** applies them to existing mail. Saved in `settings.json`. They also run on Get Messages and on import.
- Compose: **Send attachments as ZIP** packs dropped files into one archive before SMTP. A prompt asks for the ZIP name (from the subject) and an optional password. Attach files by dragging them onto the compose window.
- View → Language: **Français**, **Deutsch**, and **Español** (English and Italian were already there).
- **Settings → Features**: country packs for certified-mail UI (Italy PEC/FatturaPA/fascicolo, Europe eIDAS REM, France/Germany/Spain/Switzerland evidence labels). Ordinary IMAP stays on. Header checkboxes turn a whole pack on or off.
- View -> Language: **Français**, **Deutsch**, and **Español** (English and Italian were already there).
- **Settings -> Features**: country packs for certified-mail UI (Italy PEC/FatturaPA/fascicolo, Europe eIDAS REM, France/Germany/Spain/Switzerland evidence labels). Ordinary IMAP stays on. Header checkboxes turn a whole pack on or off.
### Fixed
@ -256,15 +268,15 @@ On-device meaning search, Outlook data files as mailboxes, rules, extra UI langu
- Selecting one or more messages enables Delete, Move, Flag, and mark read/unread again after drag-and-drop or a list refresh. Opening a message no longer locks the toolbar.
- Move and delete drop keyword and meaning rows for those messages; the meaning indexer wakes so destination mail is re-indexed.
- Indices window stays usable while the meaning model downloads; **Download model** is disabled when the files are already in the models folder.
- **Settings → Rules**: **Import rules…** is the same button for this app’s JSON export and for Outlook `.rwz`.
- **Settings -> Rules**: **Import rules...** is the same button for this app's JSON export and for Outlook `.rwz`.
### Changed
- Keyword/body indexing processes all messages in one queue (like the meaning index), not folder by folder. The status bar shows `Indexing N left` instead of a folder name.
- Toolbar layout control is one toggle: list above reading vs three columns.
- Settings menu: **Indices…** (was Semantic search) manages the keyword index and the meaning index together.
- Settings menu: **Indices...** (was Semantic search) manages the keyword index and the meaning index together.
- Meaning-model download is Hugging Face only (public onnx-community repo). GitHub is not a source for the weights.
- After SMTP send, Sent reconciles gestore ricevute only for PEC/REM accounts (presets, or IMAP/POP3 with mailbox kind PEC/REM). Ordinary IMAP/Gmail/Outlook just reports sent. Leftover “Waiting for ricevuta” marks from ordinary mailboxes are dropped.
- After SMTP send, Sent reconciles gestore ricevute only for PEC/REM accounts (presets, or IMAP/POP3 with mailbox kind PEC/REM). Ordinary IMAP/Gmail/Outlook just reports sent. Leftover "Waiting for ricevuta" marks from ordinary mailboxes are dropped.
- Account Settings: IMAP/POP3 accounts set **mailbox kind** (ordinary, Italian PEC, EU REM) so a custom-domain gestore still waits for ricevute. Presets keep their kind; Gmail, Outlook, and PST stay ordinary.
## [0.1.0] - 2026-09-13
@ -286,5 +298,5 @@ First release of Postclient: a desktop mail client for Windows, Linux, and macOS
- Print / PDF / selected `.eml` fascicolo; save all attachments as ZIP; FatturaPA XML preview (parser, not AI).
- Import EML, Thunderbird mbox, and Outlook PST/OST (no Outlook install).
- Self-contained `win-x64` portable zip and Windows setup exe, Linux Flatpak via WSL Debian (no .NET SDK on the user PC).
- Apple-style squircle product icon (envelope glyph, MAKS.IT cyan→navy) for the exe, window chrome, tray, WiX setup, and Flatpak. Not the brand origami M.
- Apple-style squircle product icon (envelope glyph, MAKS.IT cyan->navy) for the exe, window chrome, tray, WiX setup, and Flatpak. Not the brand origami M.
- Unsigned `osx-arm64` and `osx-x64` DMGs attached when a GitHub Release is published (first launch: Open from the context menu).

View File

@ -63,7 +63,7 @@ Store listing copy:
### System requirements (Properties)
Partner Center → **Properties** → **System requirements**. A blank cell stays unset. Minimum is what the Store may warn on; Recommended does not warn.
Partner Center -> **Properties** -> **System requirements**. A blank cell stays unset. Minimum is what the Store may warn on; Recommended does not warn.
The app is a win-x64 desktop mail client. It does not use a camera, microphone, radio, gamepad, or a specific GPU.

View File

@ -75,7 +75,7 @@ Mehrere Postfächer sind speicherbar. Profile füllen Hosts und Ports:
Zertifizierte IMAP-Postfächer nutzen das Gestore-Passwort oder ein App-Passwort bei 2FA. **De-Mail** ist in der Regel **kein** IMAP. Dasselbe gilt für **AR24** (Frankreich), **IncaMail** (Schweiz) und **Lleida** (Spanien). Hat der Betreiber IMAP/POP3-Hosts genannt, wählen Sie **IMAP / POP3**. Sonst öffnet dieser Client das Postfach nicht; ETSI-REM-Nachweise auf importierten `.eml` kann er trotzdem anzeigen.
Gmail und Microsoft 365 laufen über **Identity Hub** (`https://identity.maks-it.com`). Kontoeinstellungen öffnen `/desktop-login?provider=Google` oder `Microsoft`. Hub-JWT und Refresh-Token bleiben in `secrets.bin`; IMAP nutzt ein kurzlebiges Mailbox-Token (XOAUTH2). Beim Verbinden erneuert der Client die Hub-Sitzung aus dem Refresh-Token, damit Gmail und Outlook nach dem Beenden angemeldet bleiben. Hat ein älterer Build nur das JWT gespeichert, einmal neu in den Kontoeinstellungen anmelden. Override: `POSTCLIENT_IDENTITY_HUB`. Passwort bleibt für PEC und generisches IMAP gültig.
Gmail und Microsoft 365 laufen über **Identity Hub** (`https://identity.maks-it.com`, App-Id `MaksIT.PostClient`). Kontoeinstellungen öffnen `/desktop-login?provider=Google|Microsoft&app=MaksIT.PostClient`. Hub-JWT und Refresh-Token bleiben in `secrets.bin`; IMAP nutzt ein kurzlebiges Mailbox-Token (XOAUTH2). Beim Verbinden erneuert der Client die Hub-Sitzung aus dem Refresh-Token, damit Gmail und Outlook nach dem Beenden angemeldet bleiben. Hat ein älterer Build nur das JWT gespeichert, einmal neu in den Kontoeinstellungen anmelden. Override: `POSTCLIENT_IDENTITY_HUB`. Passwort bleibt für PEC und generisches IMAP gültig.
Passwörter und OAuth-Refresh-Token liegen neben den Einstellungen (`DPAPI` unter Windows, Dateimodus `600` unter Linux/macOS), nie in `settings.json`.

View File

@ -75,7 +75,7 @@ Se pueden guardar varios buzones. Los perfiles rellenan hosts y puertos:
Los buzones IMAP certificados usan la contraseña del gestor, o una contraseña de aplicación si hay 2FA. **Lleida.net** y otras notificaciones certificadas españolas **no** suelen ser IMAP. Tampoco **AR24** (Francia), **De-Mail** (Alemania) ni **IncaMail** (Suiza). Si el operador dio hosts IMAP/POP3, elija **IMAP / POP3**. Si no, este cliente no abre ese buzón; sí puede mostrar evidencia ETSI REM en mensajes importados como `.eml`.
Gmail y Microsoft 365 pasan por **Identity Hub** (`https://identity.maks-it.com`). Ajustes de cuenta abre `/desktop-login?provider=Google` o `Microsoft`. El JWT y el token de actualización del Hub quedan en `secrets.bin`; IMAP usa un token de buzón de corta vida (XOAUTH2). Al conectar, el cliente renueva la sesión Hub con ese token de actualización para que Gmail y Outlook sigan conectados tras cerrar la app. Si una versión anterior guardó solo el JWT, inicie sesión otra vez en Ajustes de cuenta. Override: `POSTCLIENT_IDENTITY_HUB`. La contraseña sigue valiendo para PEC e IMAP genérico.
Gmail y Microsoft 365 pasan por **Identity Hub** (`https://identity.maks-it.com`, app id `MaksIT.PostClient`). Ajustes de cuenta abre `/desktop-login?provider=Google|Microsoft&app=MaksIT.PostClient`. El JWT y el token de actualización del Hub quedan en `secrets.bin`; IMAP usa un token de buzón de corta vida (XOAUTH2). Al conectar, el cliente renueva la sesión Hub con ese token de actualización para que Gmail y Outlook sigan conectados tras cerrar la app. Si una versión anterior guardó solo el JWT, inicie sesión otra vez en Ajustes de cuenta. Override: `POSTCLIENT_IDENTITY_HUB`. La contraseña sigue valiendo para PEC e IMAP genérico.
Las contraseñas y los tokens OAuth quedan junto a la configuración (`DPAPI` en Windows, modo `600` en Linux/macOS), nunca en `settings.json`.

View File

@ -75,7 +75,7 @@ Plusieurs boîtes peuvent être enregistrées. Les profils remplissent hôtes et
Les boîtes IMAP certifiées utilisent le mot de passe du gestionnaire, ou un mot de passe d’application si la 2FA est active. **AR24**, **La Poste LRE** et d’autres LRE françaises ne sont en général **pas** de l’IMAP. **De-Mail** (Allemagne), **IncaMail** (Suisse) et **Lleida** (Espagne) non plus. Si l’opérateur a fourni des hôtes IMAP/POP3, choisissez **IMAP / POP3**. Sinon ce client n’ouvre pas cette boîte ; il peut encore afficher une preuve ETSI REM sur des messages importés en `.eml`.
Gmail et Microsoft 365 passent par **Identity Hub** (`https://identity.maks-it.com`). Les paramètres de compte ouvrent `/desktop-login?provider=Google` ou `Microsoft`. Le JWT Hub et le jeton de rafraîchissement restent dans `secrets.bin` ; IMAP utilise un jeton mailbox de courte durée (XOAUTH2). À la connexion, le client renouvelle la session Hub à partir de ce jeton, pour que Gmail et Outlook restent connectés après la fermeture. Si une version antérieure n’a enregistré que le JWT, reconnectez-vous une fois dans les paramètres de compte. Surcharge : `POSTCLIENT_IDENTITY_HUB`. Le mot de passe reste valable pour PEC et IMAP générique.
Gmail et Microsoft 365 passent par **Identity Hub** (`https://identity.maks-it.com`, id d’app `MaksIT.PostClient`). Les paramètres de compte ouvrent `/desktop-login?provider=Google|Microsoft&app=MaksIT.PostClient`. Le JWT Hub et le jeton de rafraîchissement restent dans `secrets.bin` ; IMAP utilise un jeton mailbox de courte durée (XOAUTH2). À la connexion, le client renouvelle la session Hub à partir de ce jeton, pour que Gmail et Outlook restent connectés après la fermeture. Si une version antérieure n’a enregistré que le JWT, reconnectez-vous une fois dans les paramètres de compte. Surcharge : `POSTCLIENT_IDENTITY_HUB`. Le mot de passe reste valable pour PEC et IMAP générique.
Mots de passe et jetons OAuth restent à côté des réglages (`DPAPI` sous Windows, mode `600` sous Linux/macOS), jamais dans `settings.json`.

View File

@ -80,7 +80,7 @@ Si possono usare più caselle insieme. I profili compilano host e porte:
Le caselle IMAP certificate usano la password del gestore, o una password per le app se c’è il 2FA. **AR24** (Francia), **De-Mail** (Germania), **IncaMail** (Svizzera) e **Lleida** (Spagna) di solito **non** sono IMAP. Se l’operatore ha dato host IMAP/POP3, scegli **IMAP / POP3**. Altrimenti questa applicazione non apre quella casella; può comunque mostrare l’evidenza ETSI REM su messaggi importati come `.eml`.
Gmail e Microsoft 365 accedono tramite **Identity Hub** (`https://identity.maks-it.com`). Impostazioni account apre `/desktop-login?provider=Google` o `Microsoft`. Il JWT Hub e il refresh token restano in `secrets.bin`; IMAP usa un mailbox-token a breve scadenza (XOAUTH2). All’avvio il client rinnova la sessione Hub da quel refresh token, così Gmail e Outlook restano collegati dopo la chiusura. Se una build precedente ha salvato solo il JWT, accedi di nuovo da Impostazioni account. Override: `POSTCLIENT_IDENTITY_HUB`. La password resta valida per PEC e IMAP generico.
Gmail e Microsoft 365 accedono tramite **Identity Hub** (`https://identity.maks-it.com`, app id `MaksIT.PostClient`). Impostazioni account apre `/desktop-login?provider=Google|Microsoft&app=MaksIT.PostClient`. Il JWT Hub e il refresh token restano in `secrets.bin`; IMAP usa un mailbox-token a breve scadenza (XOAUTH2). All’avvio il client rinnova la sessione Hub da quel refresh token, così Gmail e Outlook restano collegati dopo la chiusura. Se una build precedente ha salvato solo il JWT, accedi di nuovo da Impostazioni account. Override: `POSTCLIENT_IDENTITY_HUB`. La password resta valida per PEC e IMAP generico.
Password e token OAuth stanno accanto alle impostazioni (`DPAPI` su Windows, mode `600` su Linux/macOS), mai in `settings.json`.

View File

@ -59,23 +59,23 @@ If you find this project useful, please consider supporting its development:
Typical desktop and web mail treats a PEC **busta** or an ETSI REM evidence part as just another attachment. Postclient is written around that evidence:
- **Unwrap PEC / REM envelopes** — read the inner `postacert.eml` (the message the recipient was meant to see) instead of the gestore wrapper; Type column **PEC**, **RIC**, **REM**, or digitally signed **SIG**.
- **Delivery column from operator evidence** — accettazione, avvenuta-consegna, ETSI `SubmissionAcceptance` / `Delivery` / `Retrieval`, LRE and AR24 avis, De-Mail Zustellung and Abholbestätigung, acuse de recibo. After SMTP send, **Sent** is matched to Inbox/Ricevute ricevute by Message-Id, `daticert.xml`, and subject prefixes. Italian PEC has **no read receipt**: avvenuta consegna is deposit in the certified mailbox.
- **FatturaPA preview** — Italian e-invoice XML as a readable view (XML parser, not generative AI).
- **Fascicolo of original `.eml`** — export selected messages as the files a notaio, PA desk, bank, or court can keep.
- **From is a mailbox picker** — you cannot type a free From; send uses the SMTP identity of the connected account.
- **Practice labels on this PC** — tag a condominium, a client file, an IMU notice. Labels are local, not a studio-wide database.
- **Full-folder local archive** — Get Messages stores the **whole IMAP folder** in `mail.db` plus `.eml` on disk (not a last-N window). Keyword FTS on subject, body, PDF/XML/HTML/CSV/JSON attachment text, and labels.
- **On-device meaning search (RAG)** — optional EmbeddingGemma 300M in **Settings → Indices…**. Vectors stay in `mail.db`. Mail never leaves the machine. Rebuild or repair keyword and meaning indices.
- **Rules across mailboxes** — move, delete, flag, label; a destination folder can live on another account, including an attached **folder store**. Import JSON or a legacy `.rwz` file; export is JSON.
- **Folder stores** — create, attach, detach, or move a directory that holds `.eml` plus its own `mail.db` (keyword + meaning index). **Import Outlook PST…** copies into a store; it does not register the `.pst` as a mailbox. Default store path: `%LocalAppData%\Postclient\stores\{name}` (portable: `{install}/data/stores/{name}`).
- **Country packs** — **Settings → Features** turns Italy (PEC, FatturaPA, fascicolo), Europe (eIDAS REM), France, Germany, Spain, and Switzerland evidence labels on or off. Ordinary IMAP stays available.
- **Unwrap PEC / REM envelopes** - read the inner `postacert.eml` (the message the recipient was meant to see) instead of the gestore wrapper; Type column **PEC**, **RIC**, **REM**, or digitally signed **SIG**.
- **Delivery column from operator evidence** - accettazione, avvenuta-consegna, ETSI `SubmissionAcceptance` / `Delivery` / `Retrieval`, LRE and AR24 avis, De-Mail Zustellung and Abholbestätigung, acuse de recibo. After SMTP send, **Sent** is matched to Inbox/Ricevute ricevute by Message-Id, `daticert.xml`, and subject prefixes. Italian PEC has **no read receipt**: avvenuta consegna is deposit in the certified mailbox.
- **FatturaPA preview** - Italian e-invoice XML as a readable view (XML parser, not generative AI).
- **Fascicolo of original `.eml`** - export selected messages as the files a notaio, PA desk, bank, or court can keep.
- **From is a mailbox picker** - you cannot type a free From; send uses the SMTP identity of the connected account.
- **Practice labels on this PC** - tag a condominium, a client file, an IMU notice. Labels are local, not a studio-wide database.
- **Full-folder local archive** - Get Messages stores the **whole IMAP folder** in `mail.db` plus `.eml` on disk (not a last-N window). Keyword FTS on subject, body, PDF/XML/HTML/CSV/JSON attachment text, and labels.
- **On-device meaning search (RAG)** - optional EmbeddingGemma 300M in **Settings -> Indices...**. Vectors stay in `mail.db`. Mail never leaves the machine. Rebuild or repair keyword and meaning indices.
- **Rules across mailboxes** - move, delete, flag, label; a destination folder can live on another account, including an attached **folder store**. Import JSON or a legacy `.rwz` file; export is JSON.
- **Folder stores** - create, attach, detach, or move a directory that holds `.eml` plus its own `mail.db` (keyword + meaning index). **Import Outlook PST...** copies into a store; it does not register the `.pst` as a mailbox. Default store path: `%LocalAppData%\Postclient\stores\{name}` (portable: `{install}/data/stores/{name}`).
- **Country packs** - **Settings -> Features** turns Italy (PEC, FatturaPA, fascicolo), Europe (eIDAS REM), France, Germany, Spain, and Switzerland evidence labels on or off. Ordinary IMAP stays available.
## Who it is for
### Private individuals
Daily client for one or more addresses: Gmail or Microsoft 365 next to a personal **PEC** (Italy) or an IMAP **REM** mailbox (EU). Search a condominium notice, a tax file, or a PA communication years later without leaving the originals on someone else’s server.
Daily client for one or more addresses: Gmail or Microsoft 365 next to a personal **PEC** (Italy) or an IMAP **REM** mailbox (EU). Search a condominium notice, a tax file, or a PA communication years later without leaving the originals on someone else's server.
Typical use: certified and ordinary mail in one window; see whether the gestore accepted and deposited a PEC; print or save a PDF; export the original `.eml` if a municipality, bank, or court asks for it.
@ -95,19 +95,19 @@ Several mailboxes at once. Presets fill hosts and ports:
|--|--|
| Gmail | IMAP/SMTP; Google sign-in through Identity Hub (`identity.maks-it.com`) or a Gmail app password |
| Microsoft 365 | IMAP/SMTP; Microsoft sign-in through Identity Hub. Most of these accounts reject a mailbox password |
| IT PEC — Aruba | `imaps.pec.aruba.it` / `smtps.pec.aruba.it` (POP3: `pop3s.pec.aruba.it`) |
| IT PEC — InfoCert Legalmail | `mbox.cert.legalmail.it` / `sendm.cert.legalmail.it`. Username is often the InfoCert User ID, not the address |
| IT/EU PEC — Namirial | `imaps.sicurezzapostale.it` / `smtps.sicurezzapostale.it` |
| IT PEC — Poste Italiane | `mail.postecert.it` |
| IT PEC — Register.it | `imap.pec-email.com` / `smtp.pec-email.com` |
| IT PEC — Libero | `mail.postacert.it.net` |
| EU — Intesi Group | `imap.ig-trustmail.com` / `smtp.ig-trustmail.com` |
| IT PEC - Aruba | `imaps.pec.aruba.it` / `smtps.pec.aruba.it` (POP3: `pop3s.pec.aruba.it`) |
| IT PEC - InfoCert Legalmail | `mbox.cert.legalmail.it` / `sendm.cert.legalmail.it`. Username is often the InfoCert User ID, not the address |
| IT/EU PEC - Namirial | `imaps.sicurezzapostale.it` / `smtps.sicurezzapostale.it` |
| IT PEC - Poste Italiane | `mail.postecert.it` |
| IT PEC - Register.it | `imap.pec-email.com` / `smtp.pec-email.com` |
| IT PEC - Libero | `mail.postacert.it.net` |
| EU - Intesi Group | `imap.ig-trustmail.com` / `smtp.ig-trustmail.com` |
| IMAP / POP3 | Any host the operator gave you. Set **mailbox kind** to Italian PEC or EU REM if that mailbox is certified (including a custom domain) |
| Folder store | Local bucket on disk (`postclient.store.json` + `mail.db` + `.eml`). No password and no SMTP. Create, attach, detach, or move from the File menu. |
Certified IMAP mailboxes use the gestore password, or an app password if the gestore has 2FA. **AR24** (France), **De-Mail** (Germany), **IncaMail** (Switzerland), and **Lleida** (Spain) are usually not IMAP. If that operator gave you IMAP/POP3 hosts, choose **IMAP / POP3**. Otherwise this client cannot open that mailbox; it can still display ETSI REM evidence on messages you import as `.eml`.
Gmail and Microsoft 365 sign-in uses **Identity Hub** at `https://identity.maks-it.com` (covered app `MaksIT.PostClient`). Account Settings opens `/desktop-login?provider=Google` or `Microsoft` in the built-in WebView. Hub session material stays in `secrets.bin`; IMAP/SMTP use a short-lived **IdP mailbox token** (XOAUTH2). Hub keeps the IdP refresh in its vault and is transparent to mail protocols — on connect PostClient asks Hub for a fresh mailbox token (and renews the Hub session when needed) so Gmail and Outlook stay signed in. If an older build stored a session without a refresh token, sign in once more from Account Settings. Override the origin with `POSTCLIENT_IDENTITY_HUB` (Compose: `http://localhost:8080`). Password / app password still work for PEC and generic IMAP.
Gmail and Microsoft 365 sign-in uses **Identity Hub** at `https://identity.maks-it.com` (covered app id `MaksIT.PostClient`, commercial name **MaksIT Postclient**). Account Settings opens `/desktop-login?provider=Google|Microsoft&app=MaksIT.PostClient` in the built-in WebView. Hub session material stays in `secrets.bin`; IMAP/SMTP use a short-lived **IdP mailbox token** (XOAUTH2). Hub keeps the IdP refresh in its vault and is transparent to mail protocols - on connect PostClient asks Hub for a fresh mailbox token (and renews the Hub session when needed) so Gmail and Outlook stay signed in. If an older build stored a session without a refresh token, sign in once more from Account Settings. Override the origin with `POSTCLIENT_IDENTITY_HUB` (Compose: `http://localhost:8080`). Password / app password still work for PEC and generic IMAP.
Passwords and Hub tokens stay next to settings (`DPAPI` on Windows, file mode `600` on Linux/macOS), never in `settings.json`.
@ -118,29 +118,29 @@ Italian certified mail is a **busta** produced by the gestore:
- transport message with `X-Trasporto: posta-certificata`, `daticert.xml`, and the original as `postacert.eml`;
- ricevute (`X-Ricevuta`) in **Inbox** and **Ricevute**: accettazione, presa-in-carico, avvenuta-consegna, non-accettazione, mancata-consegna, rilevazione-virus, preavviso-errore-consegna.
View → **Unwrap PEC/REM envelope** shows the inner `postacert.eml`. The Type column marks **PEC** (transport) or **RIC** (ricevuta).
View -> **Unwrap PEC/REM envelope** shows the inner `postacert.eml`. The Type column marks **PEC** (transport) or **RIC** (ricevuta).
Italian PEC has **no read receipt**. **Avvenuta consegna** means the message was deposited in the recipient’s certified mailbox, not that a person opened it.
Italian PEC has **no read receipt**. **Avvenuta consegna** means the message was deposited in the recipient's certified mailbox, not that a person opened it.
## European REM (eIDAS registered electronic mail)
When the message carries ETSI REM evidence (`REMEvidence` / `urn:etsi:rem`, or a part name such as `remevidence` / `rem-md`), the client treats it as **REM**. Event types map onto the same delivery column as PEC:
- `SubmissionAcceptance` → accepted by the QTSP;
- `Delivery` / LRE or AR24 avis de réception / De-Mail Zustellung / acuse de recibo → deposited;
- `ContentConsignment` / `Retrieval` / De-Mail `Abholbestätigung` → the recipient retrieved the content (closest European equivalent of “read”);
- rejection, virus, non-delivery, `DeliveryExpiration` → failed.
- `SubmissionAcceptance` -> accepted by the QTSP;
- `Delivery` / LRE or AR24 avis de réception / De-Mail Zustellung / acuse de recibo -> deposited;
- `ContentConsignment` / `Retrieval` / De-Mail `Abholbestätigung` -> the recipient retrieved the content (closest European equivalent of "read");
- rejection, virus, non-delivery, `DeliveryExpiration` -> failed.
S/MIME (`smime.p7s` / `smime.p7m`) is shown as digitally signed; it is not PEC and not REM.
## Delivery column after send
After SMTP send, Postclient stores the outbound **Message-Id** and, when you open **Sent**, reconciles gestore ricevute from **Inbox** and **Ricevute**. That wait applies only to accounts whose kind is PEC or REM (a PEC/REM preset, or IMAP/POP3 with that mailbox kind). Ordinary Gmail, Microsoft 365, and IMAP just report as sent. Matching uses, in order: `daticert.xml` `<msgid>`, `X-Riferimento-Message-ID`, `In-Reply-To`, PEC `identificativo`, then subject prefixes (`ACCETTAZIONE:`, `CONSEGNA:`, `AVIS DE RECEPTION:`, `ACUSE DE RECIBO:`, `ZUSTELLBESTAETIGUNG:`, `ABHOLBESTAETIGUNG:`, …).
After SMTP send, Postclient stores the outbound **Message-Id** and, when you open **Sent**, reconciles gestore ricevute from **Inbox** and **Ricevute**. That wait applies only to accounts whose kind is PEC or REM (a PEC/REM preset, or IMAP/POP3 with that mailbox kind). Ordinary Gmail, Microsoft 365, and IMAP just report as sent. Matching uses, in order: `daticert.xml` `<msgid>`, `X-Riferimento-Message-ID`, `In-Reply-To`, PEC `identificativo`, then subject prefixes (`ACCETTAZIONE:`, `CONSEGNA:`, `AVIS DE RECEPTION:`, `ACUSE DE RECIBO:`, `ZUSTELLBESTAETIGUNG:`, `ABHOLBESTAETIGUNG:`, ...).
| Evidence | Mark | Meaning |
|--|--|--|
| PEC `accettazione` / `presa-in-carico`, ETSI `SubmissionAcceptance` | Accepted | The gestore took the message |
| PEC `avvenuta-consegna`, ETSI `Delivery`, LRE/AR24 avis, De-Mail Zustellung | Delivered | Deposited in the recipient **certified mailbox** — Italian PEC has **no read receipt** |
| PEC `avvenuta-consegna`, ETSI `Delivery`, LRE/AR24 avis, De-Mail Zustellung | Delivered | Deposited in the recipient **certified mailbox** - Italian PEC has **no read receipt** |
| ETSI `ContentConsignment` / `Retrieval`, De-Mail `Abholbestätigung` | Retrieved | Recipient picked up the content |
| `non-accettazione`, `mancata-consegna`, virus, ETSI rejection | Failed | Gestore reported non-acceptance or failed delivery |
@ -150,13 +150,13 @@ Desktop notices fire for new PEC, ricevute, and REM (Windows toast, macOS notifi
## Reading, folders, compose
- Folder tree follows the server (Inbox, Ricevute/Receipts, Drafts, Sent, Archive, Junk, Trash, plus custom folders). Nested IMAP paths show as nested folders, including Gmail’s `[Gmail]/…` labels under `[Gmail]`. Standard folders that the server stores as `INBOX.Drafts` / `INBOX.Sent` / `INBOX.Trash` sit next to Inbox, not under it. Gmail uses `/` as the separator, so a label like `P.IVA` stays one folder. System folders cannot be deleted. You can create a folder, empty one (to Trash, or permanently in Trash), mark all read/unread, delete a custom folder, and drag messages onto a folder. Which accounts and nested folders are expanded or collapsed is kept in `settings.json`.
- Folder tree follows the server (Inbox, Ricevute/Receipts, Drafts, Sent, Archive, Junk, Trash, plus custom folders). Nested IMAP paths show as nested folders, including Gmail's `[Gmail]/...` labels under `[Gmail]`. Standard folders that the server stores as `INBOX.Drafts` / `INBOX.Sent` / `INBOX.Trash` sit next to Inbox, not under it. Gmail uses `/` as the separator, so a label like `P.IVA` stays one folder. System folders cannot be deleted. You can create a folder, empty one (to Trash, or permanently in Trash), mark all read/unread, delete a custom folder, and drag messages onto a folder. Which accounts and nested folders are expanded or collapsed is kept in `settings.json`.
- List columns: unread, flag, attachments, delivery, type (PEC / RIC / REM / SIG), from, subject, date (local `yyyy-MM-dd HH:mm`), size, practice label.
- View: HTML (OS web engine), plain text, raw source, or **FatturaPA**. Layout: list above reading, or three columns (folders, list, reading).
- **Block remote content** (on by default) keeps HTML mail from loading images or tracking pixels from the internet; use **Show images** on a message when you want remotes for that view only. Inline attachments still show.
- **Group conversations** indents replies in the folder list using Message-ID / In-Reply-To / References (depth up to 8). PEC ricevute that share the same original sit together. There is no separate conversation pane.
- Message actions: reply, reply all, forward, open in a new window, read/unread, flag, priority, mark as spam, not spam, delete. **Shift+click** and **Ctrl+click** select several messages or folders. Delete (toolbar, **Del**, empty folder) moves the selection to Trash. Deleting from Trash is permanent.
- Compose is **plain text**. To, Cc, Bcc are address chips. Attachments are listed on the composer. **Send attachments as ZIP** packs dropped files (optional password). Send uses the selected mailbox’s SMTP identity only.
- Compose is **plain text**. To, Cc, Bcc are address chips. Attachments are listed on the composer. **Send attachments as ZIP** packs dropped files (optional password). Send uses the selected mailbox's SMTP identity only.
HTML uses the OS engine: **WebView2** on Windows (profile under the data folder), **WebKitGTK** on Linux, **WKWebView** on macOS. If the engine is missing, the text body is shown instead.
@ -172,11 +172,11 @@ Fedora: `gtk3 webkit2gtk4.1 libsoup3`.
Get Messages indexes the **whole IMAP folder** into a local SQLite store (`mail.db`) plus `.eml` objects on disk. POP3 only fills Inbox. Indexing runs in the background; the status bar shows progress.
- **Settings → Indices…**: keyword index (FTS) plus optional EmbeddingGemma 300M (~300 MB) meaning index. The ONNX package downloads once into the machine folder (`ProgramData\MaksIT\Postclient\models`, or `/var/lib/maksit/postclient/models` on Linux) and is shared by every account and the sync service. Vectors stay in `mail.db` on this PC. Device: Auto / CPU / GPU (Windows DirectML). Rebuild or repair if search looks wrong. Weights use Google’s Gemma Terms.
- **Settings -> Indices...**: keyword index (FTS) plus optional EmbeddingGemma 300M (~300 MB) meaning index. The ONNX package downloads once into the machine folder (`ProgramData\MaksIT\Postclient\models`, or `/var/lib/maksit/postclient/models` on Linux) and is shared by every account and the sync service. Vectors stay in `mail.db` on this PC. Device: Auto / CPU / GPU (Windows DirectML). Rebuild or repair if search looks wrong. Weights use Google's Gemma Terms.
Search (current folder) covers subject, body, attachment text (PDF and text/XML/HTML/CSV/JSON), practice labels, and meaning when the model is ready. The search box has an × that clears the text. Labels exist only on this PC.
The archive is **only on this PC**. File → Open archive folder / Export archive copies `mail.db` and the `.eml` files. Paths:
The archive is **only on this PC**. File -> Open archive folder / Export archive copies `mail.db` and the `.eml` files. Paths:
| OS | Config | Data |
|--|--|--|
@ -198,27 +198,27 @@ IMAP **QUOTA** from the gestore (when the server supports it) is a thin bar on t
| New store | File menu: empty bucket (`Inbox`, `Drafts`, `Sent Items`, `Deleted Items`) with `postclient.store.json` and `mail.db`. |
| Attach store | File menu: pick a folder that already has `postclient.store.json`. Recovers mail + search after an AppData wipe. |
| Move / Detach store | Copy the whole directory (or unregister it). The folder stays on disk when detached. |
| Retention | **Settings → Retention…**: days per folder (`0` = forever). Expired folder mail is moved to Trash after Get Messages. On Trash, `0` keeps it forever and a number of days permanently deletes older Trash. |
| Background sync | **Settings → Background sync**: a boot-time service (Windows service, Linux system systemd unit). It syncs every saved mailbox with nobody signed in. **Share this account** only lets other people on this PC open that mailbox. |
| Shared account | **Account settings → Share this account with other users on this PC**: that mailbox moves to the machine folder (`ProgramData\MaksIT\Postclient`, or `Public` when ProgramData is not writable; `/var/lib/maksit/postclient` on Linux; `/Users/Shared/MaksIT/Postclient` on macOS). Other users see it when they open Postclient. Passwords are not copied. |
| Easy Migration | **File → Easy Migration → Create bundle…** asks for a passphrase twice in one window, then Save. The `.postbundle` holds mail, accounts, secrets, and the shared model folder. **Open bundle…** restores it into this PC’s Postclient folders and rewrites store paths. Background sync stays off until you turn it on here. |
| Spam marks | **Mark as spam** remembers that message on this PC and teaches every mailbox, even after the message or mailbox is gone. Similar mail is only a hint. **Not spam** drops that memory and returns the message to Inbox. Moving mail into Junk does not teach the filter. **Settings → Indices… → Spam marks…** lists what was learned. |
| Updates and logs | **Help → Check for updates…** asks GitHub for the latest release and can download the package for this system. **Help → Logs** opens a copyable window over the app log folder. **Help → About** lists Info, Privacy, Security, and Support at maks-it.com. |
| Rules | **Settings → Rules**: **Import rules…** reads this app’s JSON export or a legacy `.rwz` file. **Export rules…** writes JSON. Each rule is bound to an **account**; a move folder can live on another mailbox (including a store). They run on Get Messages, on import, and from **Run all rules**. |
| Retention | **Settings -> Retention...**: days per folder (`0` = forever). Expired folder mail is moved to Trash after Get Messages. On Trash, `0` keeps it forever and a number of days permanently deletes older Trash. |
| Background sync | **Settings -> Background sync**: a boot-time service (Windows service, Linux system systemd unit). It syncs every saved mailbox with nobody signed in. **Share this account** only lets other people on this PC open that mailbox. |
| Shared account | **Account settings -> Share this account with other users on this PC**: that mailbox moves to the machine folder (`ProgramData\MaksIT\Postclient`, or `Public` when ProgramData is not writable; `/var/lib/maksit/postclient` on Linux; `/Users/Shared/MaksIT/Postclient` on macOS). Other users see it when they open Postclient. Passwords are not copied. |
| Easy Migration | **File -> Easy Migration -> Create bundle...** asks for a passphrase twice in one window, then Save. The `.postbundle` holds mail, accounts, secrets, and the shared model folder. **Open bundle...** restores it into this PC's Postclient folders and rewrites store paths. Background sync stays off until you turn it on here. |
| Spam marks | **Mark as spam** remembers that message on this PC and teaches every mailbox, even after the message or mailbox is gone. Similar mail is only a hint. **Not spam** drops that memory and returns the message to Inbox. Moving mail into Junk does not teach the filter. **Settings -> Indices... -> Spam marks...** lists what was learned. |
| Updates and logs | **Help -> Check for updates...** asks GitHub for the latest release and can download the package for this system. **Help -> Logs** opens a copyable window over the app log folder. **Help -> About** lists Info, Privacy, Security, and Support at maks-it.com. |
| Rules | **Settings -> Rules**: **Import rules...** reads this app's JSON export or a legacy `.rwz` file. **Export rules...** writes JSON. Each rule is bound to an **account**; a move folder can live on another mailbox (including a store). They run on Get Messages, on import, and from **Run all rules**. |
| Print | Readable HTML of the (optionally unwrapped) message |
| Save PDF | Same content as a PDF |
| Save attachments ZIP | All attachments of the open message |
| Export selected EML (fascicolo) | The original `.eml` files — what a notaio or PA can keep as the message |
| Export selected EML (fascicolo) | The original `.eml` files - what a notaio or PA can keep as the message |
Opened messages are also stored as `.eml` under the data folder. The `.eml` files are the originals; PDF/print are a reading copy.
## Interface language
View → Language: **English**, **Italiano**, **Français**, **Deutsch**, or **Español**. Folder names from the server (Posta in arrivo, Gesendet, Messages envoyés, Enviados, …) are mapped to Inbox/Sent/… in the UI language.
View -> Language: **English**, **Italiano**, **Français**, **Deutsch**, or **Español**. Folder names from the server (Posta in arrivo, Gesendet, Messages envoyés, Enviados, ...) are mapped to Inbox/Sent/... in the UI language.
## Country packs
**Settings → Features** is a checkbox matrix of **certified-mail** UI only. Ordinary IMAP/POP3, search, practice labels, mbox/`.pst` import, and attached data files stay on. Rows are certified-mail features; columns are **Italy**, **Europe**, **France**, **Germany**, **Spain**, **Switzerland**. The header checkbox turns every feature in that column on or off.
**Settings -> Features** is a checkbox matrix of **certified-mail** UI only. Ordinary IMAP/POP3, search, practice labels, mbox/`.pst` import, and attached data files stay on. Rows are certified-mail features; columns are **Italy**, **Europe**, **France**, **Germany**, **Spain**, **Switzerland**. The header checkbox turns every feature in that column on or off.
| Pack | What it unlocks | Default |
|--|--|--|
@ -238,7 +238,7 @@ Turn off a pack to hide its menus, columns, and account presets. Existing mailbo
## Getting started
Install a build from [Releases](https://github.com/MAKS-IT-COM/maksit-postclient/releases): Windows portable zip and setup (`postclient-{version}.exe`, self-contained — no .NET SDK on the PC; default path `C:\Program Files\MaksIT\Postclient`), Linux Flatpak, macOS DMG (`osx-arm64` and `osx-x64`). Microsoft Store product ID `9N7F29XPK5W4` (listing after first submission). WinGet listing comes later from the same setup exe.
Install a build from [Releases](https://github.com/MAKS-IT-COM/maksit-postclient/releases): Windows portable zip and setup (`postclient-{version}.exe`, self-contained - no .NET SDK on the PC; default path `C:\Program Files\MaksIT\Postclient`), Linux Flatpak, macOS DMG (`osx-arm64` and `osx-x64`). Microsoft Store product ID `9N7F29XPK5W4` (listing after first submission). WinGet listing comes later from the same setup exe.
Or from `src/`:
@ -310,7 +310,7 @@ Desktop mail client for PEC, REM, and IMAP. Not a QTSP, a new PEC or REM address
## License
Apache 2.0 — see [LICENSE.md](LICENSE.md).
Apache 2.0 - see [LICENSE.md](LICENSE.md).
© Maksym Sadovnychyy (MAKS-IT)

View File

@ -3,7 +3,7 @@
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Version>0.4.5</Version>
<Version>0.4.6</Version>
<Product>Postclient</Product>
<AssemblyTitle>Postclient</AssemblyTitle>
<Authors>Maksym Sadovnychyy</Authors>

View File

@ -19,7 +19,7 @@ public sealed class ImapMailSession : IMailSession {
private MailAuthMaterial? _material;
private CancellationTokenSource? _alive;
private readonly Dictionary<string, int> _listOffset = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, HashSet<uint>> _listedUids = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, List<UniqueId>> _listUids = new(StringComparer.OrdinalIgnoreCase);
private const MessageSummaryItems FlagItems =
MessageSummaryItems.UniqueId | MessageSummaryItems.Flags;
@ -209,58 +209,75 @@ public sealed class ImapMailSession : IMailSession {
});
}
int offset;
HashSet<uint> listed;
lock (_gate) {
_listOffset.TryGetValue(folder, out offset);
if (!_listedUids.TryGetValue(folder, out listed!)) {
listed = [];
_listedUids[folder] = listed;
}
}
var range = MailListBudget.NewestRange(imapFolder.Count, offset, itemBudget);
if (range.Take <= 0) {
ClearListCursor(folder);
return Result<MailFolderSync>.Ok(new MailFolderSync { Present = listed.ToList() });
}
var chunkRows = await FetchRangeAsync(
imapFolder,
range.Start,
range.End,
ListItems,
extraHeaders,
cancellationToken).ConfigureAwait(false);
foreach (var id in IdsOf(chunkRows))
listed.Add(id);
lock (_gate) {
if (range.Incomplete)
_listOffset[folder] = range.NextOffset;
else
ClearListCursor(folder);
}
return Result<MailFolderSync>.Ok(new MailFolderSync {
Headers = HeadersOf(folder, chunkRows),
Present = range.Incomplete ? null : listed.ToList(),
Incomplete = range.Incomplete
});
return await ListMessagesPagedAsync(
folder, imapFolder, extraHeaders, itemBudget, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException) {
ClearListCursor(folder);
// Keep the list cursor so the next page can resume after Interrupt / soft cancel.
throw;
}
catch (Exception ex) {
ClearListCursor(folder);
// Keep the list cursor across transient IMAP errors so large folders can finish.
return Result<MailFolderSync>.UnprocessableEntity(null, ex.Message);
}
}
private async Task<Result<MailFolderSync>> ListMessagesPagedAsync(
string folder,
IMailFolder imapFolder,
string[]? extraHeaders,
int itemBudget,
CancellationToken cancellationToken) {
List<UniqueId>? plan;
int offset;
lock (_gate) {
_listUids.TryGetValue(folder, out plan);
_listOffset.TryGetValue(folder, out offset);
}
if (plan is null) {
var searched = await imapFolder.SearchAsync(SearchQuery.All, cancellationToken).ConfigureAwait(false);
plan = searched.OrderByDescending(uid => uid.Id).ToList();
lock (_gate) {
_listUids[folder] = plan;
_listOffset[folder] = 0;
offset = 0;
}
}
var range = MailListBudget.UidPage(plan.Count, offset, itemBudget);
if (range.Take <= 0) {
var present = plan.Select(uid => uid.Id).ToList();
ClearListCursor(folder);
return Result<MailFolderSync>.Ok(new MailFolderSync { Present = present });
}
var page = plan.GetRange(range.Start, range.Take);
var chunkRows = await FetchByUidsAsync(imapFolder, page, extraHeaders, cancellationToken)
.ConfigureAwait(false);
// Empty FETCH on a non-empty UID page is usually transient - retry the same page.
if (page.Count > 0 && chunkRows.Count == 0)
return Result<MailFolderSync>.Ok(new MailFolderSync { Incomplete = true });
lock (_gate) {
if (range.Incomplete)
_listOffset[folder] = range.NextOffset;
else
ClearListCursor(folder);
}
return Result<MailFolderSync>.Ok(new MailFolderSync {
Headers = HeadersOf(folder, chunkRows),
Present = range.Incomplete ? null : plan.Select(uid => uid.Id).ToList(),
Incomplete = range.Incomplete
});
}
private void ClearListCursor(string folder) {
lock (_gate) {
_listOffset.Remove(folder);
_listedUids.Remove(folder);
_listUids.Remove(folder);
}
}
@ -897,6 +914,8 @@ public sealed class ImapMailSession : IMailSession {
lock (_gate) {
client = _imap;
_imap = null;
_listOffset.Clear();
_listUids.Clear();
}
if (client is null)

View File

@ -219,8 +219,9 @@ public sealed class MailSyncEngine {
await ApplyRulesAsync(configuration, box, session, folder, slice, names, cancellationToken)
.ConfigureAwait(false);
}
if (sync.Incomplete && fresh.Count == 0)
return true;
// Newest-first pages often have no fresh UIDs once the top of the folder is archived.
// Incomplete still means older pages remain - keep walking instead of marking the folder done.
return !sync.Incomplete;
}

View File

@ -12,11 +12,11 @@
<ItemGroup>
<PackageReference Include="MailKit" Version="4.18.2" />
<PackageReference Include="MaksIT.Core" Version="1.6.14" />
<PackageReference Include="MaksIT.IdentityHub.Client" Version="3.1.3" />
<PackageReference Include="MaksIT.IdentityHub.Client" Version="4.0.0" />
<PackageReference Include="MaksIT.Results" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.14" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.14" />
<PackageReference Include="OfficeIMO.Email" Version="3.5.2" />
<PackageReference Include="OfficeIMO.Email" Version="3.5.3" />
<PackageReference Include="PdfPig" Version="0.1.16" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="Microsoft.ML.Tokenizers" Version="2.0.1" />

View File

@ -4,6 +4,7 @@ namespace MaksIT.PostClient.Shared.Auth;
public static class IdentityHubAddress {
public const string Env = "POSTCLIENT_IDENTITY_HUB";
public const string Production = "https://identity.maks-it.com";
public const string CoveredApplicationId = "MaksIT.PostClient";
public static string Origin() {
var fromEnv = Environment.GetEnvironmentVariable(Env);
@ -16,7 +17,9 @@ public static class IdentityHubAddress {
var provider = MailAuthKind.Normalize(authKind) == MailAuthKind.Microsoft
? "Microsoft"
: "Google";
return Origin() + "/desktop-login?provider=" + provider;
return Origin()
+ "/desktop-login?provider=" + Uri.EscapeDataString(provider)
+ "&app=" + Uri.EscapeDataString(CoveredApplicationId);
}
public static bool IsHubUrl(Uri? uri) {

View File

@ -4,6 +4,7 @@ namespace MaksIT.PostClient.Shared.Mail;
public static class MailListBudget {
public readonly record struct Slice(int Start, int End, int Take, int NextOffset, bool Incomplete);
/// <summary>Newest-first window over a stable sequence index space (POP3 / PST).</summary>
public static Slice NewestRange(int count, int offset, int budget) {
if (count <= 0 || offset >= count || budget <= 0)
return new Slice(0, -1, 0, Math.Max(0, offset), false);
@ -15,4 +16,14 @@ public static class MailListBudget {
var next = offset + take;
return new Slice(start, end, take, next, next < count);
}
/// <summary>Newest-first window over a pre-ordered UID / id list (IMAP SEARCH).</summary>
public static Slice UidPage(int count, int offset, int budget) {
if (count <= 0 || offset >= count || budget <= 0)
return new Slice(0, -1, 0, Math.Max(0, offset), false);
var take = Math.Min(budget, count - offset);
var next = offset + take;
return new Slice(offset, offset + take - 1, take, next, next < count);
}
}

View File

@ -585,6 +585,19 @@ public class MailListBudgetTests {
Assert.Equal(199, last.End);
Assert.False(last.Incomplete);
}
[Fact]
public void UidPage_WalksNewestFirstList() {
var first = MailListBudget.UidPage(1000, 0, 200);
Assert.Equal(0, first.Start);
Assert.Equal(199, first.End);
Assert.Equal(200, first.Take);
Assert.True(first.Incomplete);
var last = MailListBudget.UidPage(1000, 800, 200);
Assert.Equal(800, last.Start);
Assert.Equal(999, last.End);
Assert.False(last.Incomplete);
}
}

View File

@ -11,17 +11,20 @@ namespace MaksIT.PostClient.Tests.Archive;
public class IdentityHubAddressTests {
[Fact]
public void DesktopLoginUrl_UsesOfficialPathAndProvider() {
public void DesktopLoginUrl_UsesOfficialPathProviderAndCoveredApp() {
var previous = Environment.GetEnvironmentVariable(IdentityHubAddress.Env);
try {
Environment.SetEnvironmentVariable(IdentityHubAddress.Env, null);
var google = IdentityHubAddress.DesktopLoginUrl(MailAuthKind.Google);
Assert.StartsWith(IdentityHubAddress.Production, google);
Assert.Contains("/desktop-login?provider=Google", google);
Assert.Contains("&app=" + IdentityHubAddress.CoveredApplicationId, google);
Assert.Equal("MaksIT.PostClient", IdentityHubAddress.CoveredApplicationId);
Assert.DoesNotContain("127.0.0.1", google);
Assert.DoesNotContain("purpose=", google);
var microsoft = IdentityHubAddress.DesktopLoginUrl(MailAuthKind.Microsoft);
Assert.Contains("/desktop-login?provider=Microsoft", microsoft);
Assert.Contains("&app=" + IdentityHubAddress.CoveredApplicationId, microsoft);
}
finally {
Environment.SetEnvironmentVariable(IdentityHubAddress.Env, previous);

View File

@ -11,7 +11,7 @@
<ItemGroup>
<PackageReference Include="coverlet.MTP" Version="10.1.0" />
<PackageReference Include="MaksIT.IdentityHub.Client" Version="3.1.3" />
<PackageReference Include="MaksIT.IdentityHub.Client" Version="4.0.0" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="xunit.v3" Version="4.0.2" />
</ItemGroup>

View File

@ -12,10 +12,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="12.1.4" />
<PackageReference Include="Avalonia" Version="12.1.5" />
<PackageReference Include="Avalonia.Controls.WebView" Version="12.1.1" />
<PackageReference Include="Avalonia.Desktop" Version="12.1.4" />
<PackageReference Include="Avalonia.Themes.Simple" Version="12.1.4" />
<PackageReference Include="Avalonia.Desktop" Version="12.1.5" />
<PackageReference Include="Avalonia.Themes.Simple" Version="12.1.5" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="12.1.2" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageReference Include="MaksIT.Core" Version="1.6.14" />

View File

@ -94,7 +94,9 @@ public partial class MainViewModel {
.ListMessagesAsync(slot.Folder, known, token, MailPipeline.HeaderPage)
.ConfigureAwait(false);
if (!listed.IsSuccess) {
_pipelineSticky = null;
// Keep sticky so a transient list error retries the same folder instead of restarting
// every label from the newest page and never reaching older mail.
_pipelineSticky = key;
if (!token.IsCancellationRequested)
_folderFailUtc[key] = DateTimeOffset.UtcNow;
return true;