From 8ff6f91673bb6ccd009f9a93ed278d0936e297dc Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Thu, 29 Oct 2026 18:24:39 +0100 Subject: [PATCH] (feature): block remote html images and ship store msix --- CHANGELOG.md | 16 ++++ CONTRIBUTING.md | 16 ++-- README.de.md | 1 + README.es.md | 1 + README.fr.md | 1 + README.it.md | 1 + README.md | 13 ++-- data/eu.postclient.desktop.metainfo.xml | 5 ++ packaging/microsoft-store/description.md | 2 +- src/Directory.Build.props | 2 +- .../Config/Configuration.cs | 2 + .../Content/MessageHtml.cs | 22 ++++-- .../Localization/UiCopy.cs | 16 ++++ .../Localization/UiCopy.de.cs | 5 ++ .../Localization/UiCopy.es.cs | 5 ++ .../Localization/UiCopy.fr.cs | 5 ++ .../App/SharedTests.cs | 17 ++++ src/MaksIT.PostClient.UI/MainWindow.axaml | 4 + src/MaksIT.PostClient.UI/Themes/Dark.axaml | 6 ++ .../ViewModels/MainViewModel.cs | 63 +++++++++++++-- .../ViewModels/MessageWindowViewModel.cs | 44 +++++++++-- .../Views/Mail/ReadingPaneView.axaml | 15 ++++ .../Windows/MessageWindow.axaml | 15 ++++ utils/engines/release/scriptSettings.json | 21 +++-- utils/plugins/Desktop/DesktopPackSupport.psm1 | 78 ++++++++++++++++++- utils/plugins/Desktop/MsixPack.psm1 | 13 ++++ utils/plugins/Desktop/WindowsInstaller.psm1 | 13 ++++ 27 files changed, 366 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f40772d..9a084fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ The format follows [Keep a Changelog](https://keepachangelog.com/) and [Semantic ## [Unreleased] +## [0.4.4] - 2026-10-29 + +HTML mail blocks remote images by default. Windows setup upgrades keep Identity Hub sibling DLLs. Store MSIX is built with each release. + +### 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. + +### Changed + +- Release builds a Store **MSIX** (`postclient-{version}.msix`) as well as the setup exe and portable zip. Partner Center identity: `MAKS-IT.Postclient` / Store ID `9N7F29XPK5W4` (see CONTRIBUTING). + +### Fixed + +- Windows setup (WiX): upgrades remove the previous product only after the new files are installed, file components keep stable GUIDs, and the pack step requires Identity Hub Client + Contracts DLLs in the publish folder so sibling assemblies cannot be dropped from the installer. + ## [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. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index de61ecb..187399c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,20 +33,22 @@ WinGet listing is not in this repo yet: it will point at the same GitHub setup e ## Microsoft Store (MSIX) -`MsixPack` is present and disabled. It writes `releases/postclient-{version}.msix` from the win-x64 publish once `enabled` is true and `publisher` is the Partner Center identity. It is a full-trust desktop package (`runFullTrust`), x64. The package language stays English (`en-us`): the app picks English, Italian, French, German, or Spanish at runtime. Upload that file on an **MSIX** product in Partner Center. The Store re-signs it. An EXE/MSI product listing cannot take this file. The listing will be published soon. The README badge stays a placeholder until the Store ID exists. Add a Store listing for each UI language. The paste text is under [packaging/microsoft-store](packaging/microsoft-store/description.md). +`MsixPack` is enabled. Each release writes `releases/postclient-{version}.msix` from the win-x64 publish (Windows SDK `makeappx` / `signtool`). It is a full-trust desktop package (`runFullTrust`), x64. The package language stays English (`en-us`): the app picks English, Italian, French, German, or Spanish at runtime. Upload that file on an **MSIX** product in Partner Center. The Store re-signs it. An EXE/MSI product listing cannot take this file. Add a Store listing for each UI language. The paste text is under [packaging/microsoft-store](packaging/microsoft-store/description.md). -The `.msix` is not a GitHub release asset. - -Partner Center package identity for this product. The publisher and Store ID are not reserved yet: +Partner Center package identity for this product: | Field | Value | |-------|--------| | Package/Identity/Name | `MAKS-IT.Postclient` | -| Package/Identity/Publisher | not reserved. `CN=PartnerCenter` stops `MsixPack` | +| Package/Identity/Publisher | `CN=FCC8C0E7-6D5F-4028-B8EE-903B88C0C8F9` | | PublisherDisplayName | `MaksIT` | -| Store ID | not reserved | +| Package Family Name | `MAKS-IT.Postclient_pt3s39h1tn26a` | +| Package SID | `S-1-15-2-1760430311-4196969058-3832155388-2991238057-1261587593-1009432215-1213270591` | +| Store ID | `9N7F29XPK5W4` | -Those name, publisher, and publisher display strings are `packageName`, `publisher`, and `publisherDisplayName` in `utils/engines/release/scriptSettings.json`. Replace `CN=PartnerCenter`, then set `enabled` to true. +Those name, publisher, and publisher display strings are `packageName`, `publisher`, and `publisherDisplayName` in `utils/engines/release/scriptSettings.json`. + +The Store deep link and the Web Store URL are available after the product is live. The `.msix` is not a GitHub release asset. Store listing copy: diff --git a/README.de.md b/README.de.md index e6b9153..0d3b751 100644 --- a/README.de.md +++ b/README.de.md @@ -121,6 +121,7 @@ Systemhinweise erscheinen bei neuer PEC, neuen Ricevute und neuer REM (Windows-T - Der Baum folgt dem Server (Posteingang, Ricevute, Entwürfe, Gesendet, Archiv, Junk, Papierkorb, plus eigene Ordner). Verschachtelte IMAP-Pfade erscheinen als Unterordner, auch Gmail-Labels `[Gmail]/…` unter `[Gmail]`. Entwürfe, Gesendet und Papierkorb als `INBOX.Drafts` / `INBOX.Sent` / `INBOX.Trash` stehen neben dem Posteingang, nicht darunter. Gmail trennt mit `/`, daher bleibt ein Label wie `P.IVA` ein Ordner. Systemordner lassen sich nicht löschen. Ordner anlegen, leeren (in den Papierkorb bzw. endgültig im Papierkorb), alles gelesen/ungelesen, eigenen Ordner löschen, Nachrichten auf einen Ordner ziehen. Welche Konten und Unterordner aufgeklappt oder zugeklappt sind, bleibt in `settings.json`. - Spalten: ungelesen, Kennzeichnung, Anlagen, Zustellung, Typ (PEC / RIC / REM / SIG), von, Betreff, Datum (lokal `yyyy-MM-dd HH:mm`), Größe, Mandatskennzeichen. - Ansicht: HTML (Web-Engine des Systems), Text, Quelltext oder **FatturaPA**. Layout: Liste über dem Lesen, oder drei Spalten (Ordner, Liste, Lesen). +- **Externe Inhalte blockieren** (standardmäßig an) verhindert, dass HTML-Mail Bilder oder Tracking-Pixel aus dem Internet lädt; **Bilder anzeigen** lädt Remotes nur für die geöffnete Nachricht. Eingebettete Anhänge bleiben sichtbar. - **Unterhaltungen gruppieren** rückt Antworten in der Liste ein (Message-ID / In-Reply-To / References, Tiefe bis 8). PEC-Nachweise zum selben Original stehen beieinander. Es gibt kein eigenes Unterhaltungsfenster. - Aktionen: antworten, allen antworten, weiterleiten, in neuem Fenster, gelesen/ungelesen, Kennzeichnung, Priorität, als Spam markieren, kein Spam, löschen. **Umschalt+Klick** und **Strg+Klick** wählen mehrere Nachrichten oder Ordner. Löschen (Leiste, **Entf**, Ordner leeren) verschiebt die Auswahl in den Papierkorb. Aus dem Papierkorb ist Löschen endgültig. - Das Verfassen ist **Klartext**. An, Cc, Bcc sind Adress-Chips. **Anlagen als ZIP senden** packt per Drag-and-Drop übergebene Dateien (optionales Passwort). Der Versand nutzt nur die SMTP-Identität des gewählten Postfachs. diff --git a/README.es.md b/README.es.md index aa22308..692ca65 100644 --- a/README.es.md +++ b/README.es.md @@ -121,6 +121,7 @@ Las notificaciones del sistema avisan de PEC, acuses y REM nuevos (toast de Wind - El árbol sigue al servidor (Bandeja de entrada, Ricevute, Borradores, Enviados, Archivo, No deseado, Papelera, más carpetas propias). Las rutas IMAP anidadas se muestran como carpetas anidadas, incluidas las etiquetas de Gmail `[Gmail]/…` bajo `[Gmail]`. Borradores, Enviados y Papelera que el servidor guarda como `INBOX.Drafts` / `INBOX.Sent` / `INBOX.Trash` quedan junto a la bandeja, no debajo. Gmail usa `/` como separador, así que una etiqueta como `P.IVA` sigue siendo una sola carpeta. Las de sistema no se pueden borrar. Se puede crear una carpeta, vaciarla (a Papelera, o de forma definitiva en Papelera), marcar todo leído/no leído, eliminar una carpeta propia y arrastrar mensajes a una carpeta. Qué cuentas y carpetas anidadas están abiertas o cerradas se guarda en `settings.json`. - Columnas: no leído, marca, adjuntos, entrega, tipo (PEC / RIC / REM / SIG), de, asunto, fecha (local `yyyy-MM-dd HH:mm`), tamaño, etiqueta de expediente. - Vista: HTML (motor web del sistema), texto, fuente o **FatturaPA**. Disposición: lista encima de la lectura, o tres columnas (carpetas, lista, lectura). +- **Bloquear contenido remoto** (activado por defecto) evita que el correo HTML cargue imágenes o píxeles de seguimiento de Internet; **Mostrar imágenes** los carga solo para el mensaje abierto. Los adjuntos incrustados siguen visibles. - **Agrupar conversaciones** sangra las respuestas en la lista (Message-ID / In-Reply-To / References, profundidad máxima 8). Los acuses PEC del mismo original quedan juntos. No hay un panel de conversación aparte. - Acciones: responder, responder a todos, reenviar, abrir en ventana nueva, leído/no leído, marca, prioridad, marcar como spam, no es spam, eliminar. **Mayús+clic** y **Ctrl+clic** seleccionan varios mensajes o carpetas. Eliminar (barra, **Supr**, vaciar carpeta) mueve la selección a Papelera. Desde Papelera, eliminar es definitivo. - La redacción es **texto sin formato**. Para, Cc, Cco son chips de dirección. **Enviar adjuntos como ZIP** empaqueta los archivos soltados (contraseña opcional). El envío usa solo la identidad SMTP del buzón elegido. diff --git a/README.fr.md b/README.fr.md index af8841d..f2aaa73 100644 --- a/README.fr.md +++ b/README.fr.md @@ -121,6 +121,7 @@ Les notifications système signalent les nouvelles PEC, avis et REM (toast Windo - L’arbre suit le serveur (Boîte de réception, Ricevute, Brouillons, Envoyés, Archive, Indésirable, Corbeille, plus dossiers personnalisés). Les chemins IMAP imbriqués apparaissent comme des dossiers imbriqués, y compris les libellés Gmail `[Gmail]/…` sous `[Gmail]`. Brouillons, Envoyés et Corbeille nommés `INBOX.Drafts` / `INBOX.Sent` / `INBOX.Trash` restent à côté de la boîte de réception, pas en dessous. Gmail utilise `/` comme séparateur : un libellé tel que `P.IVA` reste un seul dossier. Les dossiers système ne se suppriment pas. On peut créer un dossier, le vider (vers la Corbeille, ou définitivement dans la Corbeille), tout marquer lu/non lu, supprimer un dossier personnalisé, glisser des messages sur un dossier. L’état ouvert ou fermé des comptes et des dossiers imbriqués est conservé dans `settings.json`. - Colonnes : non lu, drapeau, pièces jointes, livraison, type (PEC / RIC / REM / SIG), de, objet, date (locale `yyyy-MM-dd HH:mm`), taille, étiquette de dossier. - Vue : HTML (moteur web du système), texte, source, ou **FatturaPA**. Disposition : liste au-dessus de la lecture, ou trois colonnes (dossiers, liste, lecture). +- **Bloquer le contenu distant** (activé par défaut) empêche le HTML de charger images et pixels de suivi depuis Internet ; **Afficher les images** les charge seulement pour le message ouvert. Les pièces jointes intégrées restent visibles. - **Grouper les conversations** indente les réponses dans la liste (Message-ID / In-Reply-To / References, profondeur max. 8). Les avis PEC du même original restent ensemble. Il n’y a pas de volet conversation séparé. - Actions : répondre, répondre à tous, transférer, ouvrir dans une nouvelle fenêtre, lu/non lu, drapeau, priorité, marquer comme spam, pas du spam, supprimer. **Maj+clic** et **Ctrl+clic** sélectionnent plusieurs messages ou dossiers. Supprimer (barre, **Suppr**, vider le dossier) envoie la sélection à la Corbeille. Depuis la Corbeille, la suppression est définitive. - La rédaction est en **texte brut**. À, Cc, Cci sont des pastilles d’adresse. **Envoyer les pièces en ZIP** archive les fichiers déposés (mot de passe optionnel). L’envoi n’utilise que l’identité SMTP de la boîte choisie. diff --git a/README.it.md b/README.it.md index 715614d..fe3e84b 100644 --- a/README.it.md +++ b/README.it.md @@ -126,6 +126,7 @@ Le notifiche del sistema segnalano nuove PEC, ricevute e REM (toast Windows, not - L’albero segue il server (Posta in arrivo, Ricevute, Bozze, Inviata, Archivio, Indesiderata, Cestino, più cartelle personalizzate). I percorsi IMAP nidificati sono cartelle nidificate, comprese le etichette Gmail `[Gmail]/…` sotto `[Gmail]`. Bozze, Inviata e Cestino che il server chiama `INBOX.Drafts` / `INBOX.Sent` / `INBOX.Trash` stanno accanto a Posta in arrivo, non sotto. Gmail usa `/` come separatore: un’etichetta come `P.IVA` resta una sola cartella. Le cartelle di sistema non si eliminano. Si può creare una cartella, svuotarla (nel Cestino, o in modo definitivo nel Cestino), segnare tutto letto/non letto, eliminare una cartella personalizzata, trascinare i messaggi su una cartella. Quali account e cartelle nidificate sono aperti o chiusi resta in `settings.json`. - Colonne elenco: non letto, stella, allegati, consegna, tipo (PEC / RIC / REM / SIG), da, oggetto, data (locale `yyyy-MM-dd HH:mm`), dimensione, etichetta di pratica. - Vista: HTML (motore web del sistema), testo, sorgente, o **FatturaPA**. Disposizione: elenco sopra la lettura, oppure tre colonne (cartelle, elenco, lettura). +- **Blocca contenuto remoto** (attivo di default) evita che la posta HTML carichi immagini o pixel di tracking da Internet; **Mostra immagini** li carica solo per il messaggio aperto. Gli allegati incorporati restano visibili. - **Raggruppa conversazioni** indenta le risposte nell’elenco (Message-ID / In-Reply-To / References, profondità massima 8). Le ricevute PEC dello stesso originale stanno insieme. Non c’è un riquadro conversazioni separato. - Azioni: rispondi, rispondi a tutti, inoltra, apri in nuova finestra, letto/non letto, stella, priorità, segna come spam, non spam, elimina. **Maiusc+clic** e **Ctrl+clic** selezionano più messaggi o cartelle. Elimina (barra, **Canc**, svuota cartella) sposta la selezione nel Cestino. Dal Cestino l’eliminazione è definitiva. - La composizione è **testo semplice**. A, Cc, Ccn sono chip di indirizzo. Gli allegati sono elencati nel compositore. **Invia allegati come ZIP** impacca i file trascinati (password opzionale). L’invio usa solo l’identità SMTP della casella scelta. diff --git a/README.md b/README.md index f7a0341..d6714c9 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Postclient ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60.4%25-green) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-47.9%25-yellowgreen) -![Method Coverage](https://img.shields.io/badge/Method%20Coverage-62.8%25-green) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-46.3%25-yellowgreen) +![Method Coverage](https://img.shields.io/badge/Method%20Coverage-63.1%25-green) ![.NET](https://img.shields.io/badge/.NET-10-512BD4) [![License](https://img.shields.io/badge/License-Apache%202.0-blue)](LICENSE.md) ![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20Linux%20%7C%20macOS-0078D6) @@ -20,12 +20,12 @@ It is not a qualified trust service provider, not a PEC operator, not a timestam ![Inbox](assets/screenshots/inbox.png) -![Microsoft Store](https://img.shields.io/badge/Microsoft%20Store-published%20soon-5C2D91) +[![Microsoft Store](https://img.shields.io/badge/Microsoft%20Store-9N7F29XPK5W4-5C2D91)](https://apps.microsoft.com/store/detail/9N7F29XPK5W4?cid=DevShareMCLPCB) [![Windows](https://img.shields.io/badge/Windows-zip%20%26%20setup-0078D6)](https://github.com/MAKS-IT-COM/maksit-postclient/releases/latest) [![Linux](https://img.shields.io/badge/Linux-Flatpak-0078D6)](https://github.com/MAKS-IT-COM/maksit-postclient/releases/latest) [![macOS](https://img.shields.io/badge/macOS-DMG-000000)](https://github.com/MAKS-IT-COM/maksit-postclient/releases/latest) -macOS builds are unsigned: first launch is **Open** from the context menu. The Microsoft Store listing will be published soon. +macOS builds are unsigned: first launch is **Open** from the context menu. The Microsoft Store product is reserved (`9N7F29XPK5W4`); the public listing goes live after the first submission. Changes: [CHANGELOG.md](CHANGELOG.md). Contributing: [CONTRIBUTING.md](CONTRIBUTING.md). @@ -153,6 +153,7 @@ Desktop notices fire for new PEC, ricevute, and REM (Windows toast, macOS notifi - 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. @@ -237,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`). The Microsoft Store listing will be published soon. 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/`: @@ -285,7 +286,7 @@ The local archive (`mail.db` and `.eml`) stays on this computer. IMAP and POP3 s Next to settings (`DPAPI` on Windows, file mode `600` on Linux and macOS), never in `settings.json`. Gmail and Microsoft 365 sign in through Identity Hub. The Hub tokens stay in `secrets.bin`. **Where are the installers?** -[GitHub Releases](https://github.com/MAKS-IT-COM/maksit-postclient/releases). The Microsoft Store listing will be published soon. +[GitHub Releases](https://github.com/MAKS-IT-COM/maksit-postclient/releases). [Microsoft Store](https://apps.microsoft.com/store/detail/9N7F29XPK5W4?cid=DevShareMCLPCB) (product reserved; public page after submission). ## Tests diff --git a/data/eu.postclient.desktop.metainfo.xml b/data/eu.postclient.desktop.metainfo.xml index 6f4deba..5bae836 100644 --- a/data/eu.postclient.desktop.metainfo.xml +++ b/data/eu.postclient.desktop.metainfo.xml @@ -87,6 +87,11 @@ 768 + + +

HTML mail blocks remote images by default (Show images for one message). Windows setup upgrades keep sibling assemblies (Identity Hub Contracts). Release also builds a Store MSIX.

+
+

Gmail and Microsoft sign-in uses Identity Hub IdP-only; mailbox tokens renew automatically while Hub keeps the IdP refresh. What’s New appears after upgrade. Store and Flathub packaging notes are included.

diff --git a/packaging/microsoft-store/description.md b/packaging/microsoft-store/description.md index 05ba213..611fd70 100644 --- a/packaging/microsoft-store/description.md +++ b/packaging/microsoft-store/description.md @@ -2,7 +2,7 @@ The app UI is English, Italian, French, German, or Spanish. Partner Center needs a Store listing for each of those languages. Paste only the fenced text. The MSIX package language stays `en-us`: the app chooses the UI language at runtime, so the package does not ship satellite resources. -The listing is not in the Store yet. Fill the Store ID in [CONTRIBUTING.md](../../CONTRIBUTING.md) when Partner Center reserves the product. +Partner Center Store ID is `9N7F29XPK5W4` (see [CONTRIBUTING.md](../../CONTRIBUTING.md)). The Store deep link and Web Store URL appear after the product is live. | Partner Center language | Code | Listing | |-------------------------|------|---------| diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 2d7ce70..281b50d 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,7 +3,7 @@ latest enable enable - 0.4.3 + 0.4.4 Postclient Postclient Maksym Sadovnychyy diff --git a/src/MaksIT.PostClient.Shared/Config/Configuration.cs b/src/MaksIT.PostClient.Shared/Config/Configuration.cs index 50651ea..171879b 100644 --- a/src/MaksIT.PostClient.Shared/Config/Configuration.cs +++ b/src/MaksIT.PostClient.Shared/Config/Configuration.cs @@ -21,6 +21,8 @@ public sealed class Configuration { public bool GroupConversations { get; set; } = true; + public bool BlockRemoteContent { get; set; } = true; + public string Language { get; set; } = ""; [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] diff --git a/src/MaksIT.PostClient.Shared/Content/MessageHtml.cs b/src/MaksIT.PostClient.Shared/Content/MessageHtml.cs index 8fe292f..3781cfa 100644 --- a/src/MaksIT.PostClient.Shared/Content/MessageHtml.cs +++ b/src/MaksIT.PostClient.Shared/Content/MessageHtml.cs @@ -16,25 +16,31 @@ public static class MessageHtml { private static readonly Regex Head = new( @"]*>", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); - private const string Csp = + private static readonly Regex RemoteRef = new( + @"\b(?:src|srcset|poster|background)\s*=\s*[""']?\s*(?:https?:|//)|url\(\s*['""]?\s*(?:https?:|//)", + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + private const string CspAllow = """"""; + private const string CspBlock = + """"""; private const string Style = ""; - public static string Document(string? html) { + public static string Document(string? html, bool allowRemoteContent = true) { var body = (html ?? "").Trim(); + var csp = allowRemoteContent ? CspAllow : CspBlock; string document; if (LooksLikeDocument(body)) { document = Head.IsMatch(body) - ? Head.Replace(body, m => m.Value + Csp + Style, 1) - : Csp + Style + body; + ? Head.Replace(body, m => m.Value + csp + Style, 1) + : csp + Style + body; } else { document = """ -""" + Csp + Style + """ +""" + csp + Style + """ """ + body + ""; @@ -62,6 +68,12 @@ public static class MessageHtml { return LooksLikeDocument(html) || Markup.IsMatch(html); } + public static bool HasRemoteContent(string? html) { + if (string.IsNullOrWhiteSpace(html)) + return false; + return RemoteRef.IsMatch(html); + } + public static string FromPlain(string? text) { if (string.IsNullOrWhiteSpace(text)) return ""; diff --git a/src/MaksIT.PostClient.Shared/Localization/UiCopy.cs b/src/MaksIT.PostClient.Shared/Localization/UiCopy.cs index 8d10a1a..0bfb792 100644 --- a/src/MaksIT.PostClient.Shared/Localization/UiCopy.cs +++ b/src/MaksIT.PostClient.Shared/Localization/UiCopy.cs @@ -278,6 +278,14 @@ public sealed partial class UiCopy { public string GroupConversationsTip { get; init; } = ""; + public string BlockRemoteContent { get; init; } = ""; + + public string BlockRemoteContentTip { get; init; } = ""; + + public string ShowRemoteImages { get; init; } = ""; + + public string RemoteContentBlocked { get; init; } = ""; + public string ListAboveReading { get; init; } = ""; public string ThreeColumns { get; init; } = ""; @@ -809,6 +817,10 @@ public sealed partial class UiCopy { UnwrapEnvelope = "Unwrap PEC/REM envelope", GroupConversations = "Group conversations", GroupConversationsTip = "Indent replies in this folder. Message-ID / In-Reply-To / References — not a conversation pane.", + BlockRemoteContent = "Block remote content", + BlockRemoteContentTip = "Do not load images or tracking pixels from the internet. Inline attachments still show.", + ShowRemoteImages = "Show images", + RemoteContentBlocked = "Remote images are blocked so open tracking cannot run.", ListAboveReading = "List above reading", ThreeColumns = "Three columns", ThreeColumnsTip = "Folders, list, and reading side by side", @@ -1111,6 +1123,10 @@ public sealed partial class UiCopy { UnwrapEnvelope = "Apri busta PEC/REM", GroupConversations = "Raggruppa conversazioni", GroupConversationsTip = "Indenta le risposte in questa cartella. Message-ID / In-Reply-To / References — non un riquadro conversazioni.", + BlockRemoteContent = "Blocca contenuto remoto", + BlockRemoteContentTip = "Non carica immagini o pixel di tracking da Internet. Gli allegati incorporati restano visibili.", + ShowRemoteImages = "Mostra immagini", + RemoteContentBlocked = "Le immagini remote sono bloccate per evitare il tracking di apertura.", ListAboveReading = "Elenco sopra la lettura", ThreeColumns = "Tre colonne", ThreeColumnsTip = "Cartelle, elenco e lettura affiancati", diff --git a/src/MaksIT.PostClient.Shared/Localization/UiCopy.de.cs b/src/MaksIT.PostClient.Shared/Localization/UiCopy.de.cs index e5a9f42..542d0d9 100644 --- a/src/MaksIT.PostClient.Shared/Localization/UiCopy.de.cs +++ b/src/MaksIT.PostClient.Shared/Localization/UiCopy.de.cs @@ -142,6 +142,11 @@ public sealed partial class UiCopy { UnwrapEnvelope = "PEC-/REM-Umschlag öffnen", GroupConversations = "Unterhaltungen gruppieren", GroupConversationsTip = "Rückt Antworten in diesem Ordner ein. Message-ID / In-Reply-To / References — kein Unterhaltungsfenster.", + BlockRemoteContent = "Externe Inhalte blockieren", + BlockRemoteContentTip = "Lädt keine Bilder oder Tracking-Pixel aus dem Internet. Eingebettete Anhänge bleiben sichtbar.", + ShowRemoteImages = "Bilder anzeigen", + RemoteContentBlocked = "Externe Bilder sind blockiert, damit Öffnungs-Tracking nicht läuft.", + ListAboveReading = "Liste über dem Lesen", ThreeColumns = "Drei Spalten", ThreeColumnsTip = "Ordner, Liste und Lesen nebeneinander", diff --git a/src/MaksIT.PostClient.Shared/Localization/UiCopy.es.cs b/src/MaksIT.PostClient.Shared/Localization/UiCopy.es.cs index eb52444..cddedc6 100644 --- a/src/MaksIT.PostClient.Shared/Localization/UiCopy.es.cs +++ b/src/MaksIT.PostClient.Shared/Localization/UiCopy.es.cs @@ -142,6 +142,11 @@ public sealed partial class UiCopy { UnwrapEnvelope = "Abrir sobre PEC/REM", GroupConversations = "Agrupar conversaciones", GroupConversationsTip = "Sangra las respuestas en esta carpeta. Message-ID / In-Reply-To / References — no un panel de conversación.", + BlockRemoteContent = "Bloquear contenido remoto", + BlockRemoteContentTip = "No carga imágenes ni píxeles de seguimiento de Internet. Los adjuntos incrustados siguen visibles.", + ShowRemoteImages = "Mostrar imágenes", + RemoteContentBlocked = "Las imágenes remotas están bloqueadas para evitar el seguimiento de apertura.", + ListAboveReading = "Lista encima de la lectura", ThreeColumns = "Tres columnas", ThreeColumnsTip = "Carpetas, lista y lectura en paralelo", diff --git a/src/MaksIT.PostClient.Shared/Localization/UiCopy.fr.cs b/src/MaksIT.PostClient.Shared/Localization/UiCopy.fr.cs index 4ce5eb5..5458217 100644 --- a/src/MaksIT.PostClient.Shared/Localization/UiCopy.fr.cs +++ b/src/MaksIT.PostClient.Shared/Localization/UiCopy.fr.cs @@ -142,6 +142,11 @@ public sealed partial class UiCopy { UnwrapEnvelope = "Ouvrir l’enveloppe PEC/REM", GroupConversations = "Grouper les conversations", GroupConversationsTip = "Indente les réponses dans ce dossier. Message-ID / In-Reply-To / References — pas un volet conversation.", + BlockRemoteContent = "Bloquer le contenu distant", + BlockRemoteContentTip = "Ne charge pas les images ni les pixels de suivi depuis Internet. Les pièces jointes intégrées restent visibles.", + ShowRemoteImages = "Afficher les images", + RemoteContentBlocked = "Les images distantes sont bloquées pour empêcher le suivi d’ouverture.", + ListAboveReading = "Liste au-dessus de la lecture", ThreeColumns = "Trois colonnes", ThreeColumnsTip = "Dossiers, liste et lecture côte à côte", diff --git a/src/MaksIT.PostClient.Tests/App/SharedTests.cs b/src/MaksIT.PostClient.Tests/App/SharedTests.cs index f8a4d91..403a2e6 100644 --- a/src/MaksIT.PostClient.Tests/App/SharedTests.cs +++ b/src/MaksIT.PostClient.Tests/App/SharedTests.cs @@ -215,9 +215,26 @@ public class MessageHtmlTests { var html = MessageHtml.Document("

Hello

"); Assert.Contains("

Hello

", html); Assert.Contains("Content-Security-Policy", html); + Assert.Contains("img-src data: https: http:", html); Assert.Contains("", html); } + [Fact] + public void Document_BlocksRemoteImagesWhenAsked() { + var html = MessageHtml.Document("", allowRemoteContent: false); + Assert.Contains("img-src data:", html); + Assert.DoesNotContain("img-src data: https:", html); + } + + [Fact] + public void HasRemoteContent_DetectsHttpImagesNotCid() { + Assert.True(MessageHtml.HasRemoteContent("")); + Assert.True(MessageHtml.HasRemoteContent("style=\"background:url(http://x/y.png)\"")); + Assert.False(MessageHtml.HasRemoteContent("")); + Assert.False(MessageHtml.HasRemoteContent("")); + Assert.False(MessageHtml.HasRemoteContent("link")); + } + [Fact] public void Document_KeepsFullHtml() { var html = MessageHtml.Document("Hi"); diff --git a/src/MaksIT.PostClient.UI/MainWindow.axaml b/src/MaksIT.PostClient.UI/MainWindow.axaml index cd2b91f..a4ac470 100644 --- a/src/MaksIT.PostClient.UI/MainWindow.axaml +++ b/src/MaksIT.PostClient.UI/MainWindow.axaml @@ -67,6 +67,10 @@ ToggleType="CheckBox" IsChecked="{Binding GroupConversations}" ToolTip.Tip="{Binding Copy.GroupConversationsTip}" /> + +