(feature): block remote html images and ship store msix

This commit is contained in:
Maksym Sadovnychyy 2026-10-29 18:24:39 +01:00
parent b520dcfd82
commit 8ff6f91673
27 changed files with 366 additions and 36 deletions

View File

@ -6,6 +6,22 @@ The format follows [Keep a Changelog](https://keepachangelog.com/) and [Semantic
## [Unreleased] ## [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 ## [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.

View File

@ -33,20 +33,22 @@ WinGet listing is not in this repo yet: it will point at the same GitHub setup e
## Microsoft Store (MSIX) ## 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:
Partner Center package identity for this product. The publisher and Store ID are not reserved yet:
| Field | Value | | Field | Value |
|-------|--------| |-------|--------|
| Package/Identity/Name | `MAKS-IT.Postclient` | | 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` | | 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: Store listing copy:

View File

@ -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`. - 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. - 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). - 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. - **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. - 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. - 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.

View File

@ -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`. - 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. - 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). - 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. - **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. - 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. - 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.

View File

@ -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`. - 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. - 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). - 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é. - **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. - 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. - 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.

View File

@ -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`. - 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. - 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). - 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. - **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. - 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. - 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.

View File

@ -1,8 +1,8 @@
# Postclient # Postclient
![Line Coverage](https://img.shields.io/badge/Line%20Coverage-60.4%25-green) ![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) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-46.3%25-yellowgreen)
![Method Coverage](https://img.shields.io/badge/Method%20Coverage-62.8%25-green) ![Method Coverage](https://img.shields.io/badge/Method%20Coverage-63.1%25-green)
![.NET](https://img.shields.io/badge/.NET-10-512BD4) ![.NET](https://img.shields.io/badge/.NET-10-512BD4)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue)](LICENSE.md) [![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) ![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) ![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) [![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) [![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](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). 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`. - 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. - 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). - 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. - **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. - 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.
@ -237,7 +238,7 @@ Turn off a pack to hide its menus, columns, and account presets. Existing mailbo
## Getting started ## 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/`: 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`. 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?** **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 ## Tests

View File

@ -87,6 +87,11 @@
<display_length compare="ge">768</display_length> <display_length compare="ge">768</display_length>
</requires> </requires>
<releases> <releases>
<release version="0.4.4" date="2026-10-29">
<description>
<p>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.</p>
</description>
</release>
<release version="0.4.3" date="2026-10-29"> <release version="0.4.3" date="2026-10-29">
<description> <description>
<p>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.</p> <p>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.</p>

View File

@ -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 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 | | Partner Center language | Code | Listing |
|-------------------------|------|---------| |-------------------------|------|---------|

View File

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

View File

@ -21,6 +21,8 @@ public sealed class Configuration {
public bool GroupConversations { get; set; } = true; public bool GroupConversations { get; set; } = true;
public bool BlockRemoteContent { get; set; } = true;
public string Language { get; set; } = ""; public string Language { get; set; } = "";
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]

View File

@ -16,25 +16,31 @@ public static class MessageHtml {
private static readonly Regex Head = new( private static readonly Regex Head = new(
@"<head[^>]*>", @"<head[^>]*>",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); 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 =
"""<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src data: https: http:; style-src 'unsafe-inline'; font-src data: https:; media-src data:">"""; """<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src data: https: http:; style-src 'unsafe-inline'; font-src data: https:; media-src data:">""";
private const string CspBlock =
"""<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src data:; style-src 'unsafe-inline'; font-src data:; media-src data:">""";
private const string Style = private const string Style =
"<style>html{color-scheme:light;background:#fff;} img{max-width:100%;height:auto;} a{color:#1565c0;}</style>"; "<style>html{color-scheme:light;background:#fff;} img{max-width:100%;height:auto;} a{color:#1565c0;}</style>";
public static string Document(string? html) { public static string Document(string? html, bool allowRemoteContent = true) {
var body = (html ?? "").Trim(); var body = (html ?? "").Trim();
var csp = allowRemoteContent ? CspAllow : CspBlock;
string document; string document;
if (LooksLikeDocument(body)) { if (LooksLikeDocument(body)) {
document = Head.IsMatch(body) document = Head.IsMatch(body)
? Head.Replace(body, m => m.Value + Csp + Style, 1) ? Head.Replace(body, m => m.Value + csp + Style, 1)
: Csp + Style + body; : csp + Style + body;
} }
else { else {
document = """ document = """
<!DOCTYPE html> <!DOCTYPE html>
<html><head> <html><head>
<meta charset="utf-8"> <meta charset="utf-8">
""" + Csp + Style + """ """ + csp + Style + """
</head> </head>
<body style="margin:12px;background:#fff;color:#222;font:14px/1.45 Segoe UI, Ubuntu, sans-serif;"> <body style="margin:12px;background:#fff;color:#222;font:14px/1.45 Segoe UI, Ubuntu, sans-serif;">
""" + body + "</body></html>"; """ + body + "</body></html>";
@ -62,6 +68,12 @@ public static class MessageHtml {
return LooksLikeDocument(html) || Markup.IsMatch(html); 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) { public static string FromPlain(string? text) {
if (string.IsNullOrWhiteSpace(text)) if (string.IsNullOrWhiteSpace(text))
return ""; return "";

View File

@ -278,6 +278,14 @@ public sealed partial class UiCopy {
public string GroupConversationsTip { get; init; } = ""; 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 ListAboveReading { get; init; } = "";
public string ThreeColumns { get; init; } = ""; public string ThreeColumns { get; init; } = "";
@ -809,6 +817,10 @@ public sealed partial class UiCopy {
UnwrapEnvelope = "Unwrap PEC/REM envelope", UnwrapEnvelope = "Unwrap PEC/REM envelope",
GroupConversations = "Group conversations", GroupConversations = "Group conversations",
GroupConversationsTip = "Indent replies in this folder. Message-ID / In-Reply-To / References — not a conversation pane.", 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", ListAboveReading = "List above reading",
ThreeColumns = "Three columns", ThreeColumns = "Three columns",
ThreeColumnsTip = "Folders, list, and reading side by side", ThreeColumnsTip = "Folders, list, and reading side by side",
@ -1111,6 +1123,10 @@ public sealed partial class UiCopy {
UnwrapEnvelope = "Apri busta PEC/REM", UnwrapEnvelope = "Apri busta PEC/REM",
GroupConversations = "Raggruppa conversazioni", GroupConversations = "Raggruppa conversazioni",
GroupConversationsTip = "Indenta le risposte in questa cartella. Message-ID / In-Reply-To / References — non un riquadro 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", ListAboveReading = "Elenco sopra la lettura",
ThreeColumns = "Tre colonne", ThreeColumns = "Tre colonne",
ThreeColumnsTip = "Cartelle, elenco e lettura affiancati", ThreeColumnsTip = "Cartelle, elenco e lettura affiancati",

View File

@ -142,6 +142,11 @@ public sealed partial class UiCopy {
UnwrapEnvelope = "PEC-/REM-Umschlag öffnen", UnwrapEnvelope = "PEC-/REM-Umschlag öffnen",
GroupConversations = "Unterhaltungen gruppieren", GroupConversations = "Unterhaltungen gruppieren",
GroupConversationsTip = "Rückt Antworten in diesem Ordner ein. Message-ID / In-Reply-To / References — kein Unterhaltungsfenster.", 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", ListAboveReading = "Liste über dem Lesen",
ThreeColumns = "Drei Spalten", ThreeColumns = "Drei Spalten",
ThreeColumnsTip = "Ordner, Liste und Lesen nebeneinander", ThreeColumnsTip = "Ordner, Liste und Lesen nebeneinander",

View File

@ -142,6 +142,11 @@ public sealed partial class UiCopy {
UnwrapEnvelope = "Abrir sobre PEC/REM", UnwrapEnvelope = "Abrir sobre PEC/REM",
GroupConversations = "Agrupar conversaciones", GroupConversations = "Agrupar conversaciones",
GroupConversationsTip = "Sangra las respuestas en esta carpeta. Message-ID / In-Reply-To / References — no un panel de conversación.", 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", ListAboveReading = "Lista encima de la lectura",
ThreeColumns = "Tres columnas", ThreeColumns = "Tres columnas",
ThreeColumnsTip = "Carpetas, lista y lectura en paralelo", ThreeColumnsTip = "Carpetas, lista y lectura en paralelo",

View File

@ -142,6 +142,11 @@ public sealed partial class UiCopy {
UnwrapEnvelope = "Ouvrir l’enveloppe PEC/REM", UnwrapEnvelope = "Ouvrir l’enveloppe PEC/REM",
GroupConversations = "Grouper les conversations", GroupConversations = "Grouper les conversations",
GroupConversationsTip = "Indente les réponses dans ce dossier. Message-ID / In-Reply-To / References — pas un volet conversation.", 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", ListAboveReading = "Liste au-dessus de la lecture",
ThreeColumns = "Trois colonnes", ThreeColumns = "Trois colonnes",
ThreeColumnsTip = "Dossiers, liste et lecture côte à côte", ThreeColumnsTip = "Dossiers, liste et lecture côte à côte",

View File

@ -215,9 +215,26 @@ public class MessageHtmlTests {
var html = MessageHtml.Document("<p>Hello</p>"); var html = MessageHtml.Document("<p>Hello</p>");
Assert.Contains("<p>Hello</p>", html); Assert.Contains("<p>Hello</p>", html);
Assert.Contains("Content-Security-Policy", html); Assert.Contains("Content-Security-Policy", html);
Assert.Contains("img-src data: https: http:", html);
Assert.Contains("<html>", html); Assert.Contains("<html>", html);
} }
[Fact]
public void Document_BlocksRemoteImagesWhenAsked() {
var html = MessageHtml.Document("<img src=\"https://track.example/pixel.gif\">", allowRemoteContent: false);
Assert.Contains("img-src data:", html);
Assert.DoesNotContain("img-src data: https:", html);
}
[Fact]
public void HasRemoteContent_DetectsHttpImagesNotCid() {
Assert.True(MessageHtml.HasRemoteContent("<img src=\"https://track.example/x.gif\">"));
Assert.True(MessageHtml.HasRemoteContent("style=\"background:url(http://x/y.png)\""));
Assert.False(MessageHtml.HasRemoteContent("<img src=\"cid:pic@mail\">"));
Assert.False(MessageHtml.HasRemoteContent("<img src=\"data:image/png;base64,xx\">"));
Assert.False(MessageHtml.HasRemoteContent("<a href=\"https://example.com\">link</a>"));
}
[Fact] [Fact]
public void Document_KeepsFullHtml() { public void Document_KeepsFullHtml() {
var html = MessageHtml.Document("<html><head></head><body>Hi</body></html>"); var html = MessageHtml.Document("<html><head></head><body>Hi</body></html>");

View File

@ -67,6 +67,10 @@
ToggleType="CheckBox" ToggleType="CheckBox"
IsChecked="{Binding GroupConversations}" IsChecked="{Binding GroupConversations}"
ToolTip.Tip="{Binding Copy.GroupConversationsTip}" /> ToolTip.Tip="{Binding Copy.GroupConversationsTip}" />
<MenuItem Header="{Binding Copy.BlockRemoteContent}"
ToggleType="CheckBox"
IsChecked="{Binding BlockRemoteContent}"
ToolTip.Tip="{Binding Copy.BlockRemoteContentTip}" />
<Separator /> <Separator />
<MenuItem Header="{Binding Copy.ListAboveReading}" <MenuItem Header="{Binding Copy.ListAboveReading}"
ToggleType="Radio" ToggleType="Radio"

View File

@ -228,6 +228,12 @@
<Setter Property="BorderBrush" Value="#2a2e33" /> <Setter Property="BorderBrush" Value="#2a2e33" />
<Setter Property="BorderThickness" Value="0,0,0,1" /> <Setter Property="BorderThickness" Value="0,0,0,1" />
</Style> </Style>
<Style Selector="Border.remote-content-banner">
<Setter Property="Background" Value="#1e262e" />
<Setter Property="BorderBrush" Value="#33404c" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="4" />
</Style>
<Style Selector="TextBlock.header-label"> <Style Selector="TextBlock.header-label">
<Setter Property="Foreground" Value="#9aa0a6" /> <Setter Property="Foreground" Value="#9aa0a6" />
<Setter Property="FontSize" Value="12" /> <Setter Property="FontSize" Value="12" />

View File

@ -216,6 +216,12 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
[ObservableProperty] [ObservableProperty]
private bool groupConversations; private bool groupConversations;
[ObservableProperty]
private bool blockRemoteContent = true;
private bool _allowRemoteForMessage;
private bool _readingHasRemoteContent;
[ObservableProperty] [ObservableProperty]
private string readingBodyKind = MailBodyKind.Html; private string readingBodyKind = MailBodyKind.Html;
@ -499,6 +505,7 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
backgroundSyncLabel = BackgroundSyncCaption(copy); backgroundSyncLabel = BackgroundSyncCaption(copy);
unwrapEnvelope = files.Current.UnwrapEnvelope; unwrapEnvelope = files.Current.UnwrapEnvelope;
groupConversations = files.Current.GroupConversations; groupConversations = files.Current.GroupConversations;
blockRemoteContent = files.Current.BlockRemoteContent;
_bodyPreference = MailBodyKind.Preference(files.Current.ReadingBodyKind); _bodyPreference = MailBodyKind.Preference(files.Current.ReadingBodyKind);
readingBodyKind = _bodyPreference; readingBodyKind = _bodyPreference;
readingLayout = MailLayout.Normalize(files.Current.ReadingLayout); readingLayout = MailLayout.Normalize(files.Current.ReadingLayout);
@ -2627,6 +2634,36 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
RebuildVisible(); RebuildVisible();
} }
partial void OnBlockRemoteContentChanged(bool value) {
var configuration = _files.Current;
configuration.BlockRemoteContent = value;
_files.Save(configuration);
_allowRemoteForMessage = false;
if (_reading is not null)
ApplyReading(_reading, keepKind: true);
NotifyRemoteContent();
}
public bool ShowRemoteContentBanner =>
HasReading
&& ShowHtmlBody
&& BlockRemoteContent
&& !_allowRemoteForMessage
&& _readingHasRemoteContent;
[RelayCommand(CanExecute = nameof(CanShowRemoteImages))]
private void ShowRemoteImages() {
if (!CanShowRemoteImages())
return;
_allowRemoteForMessage = true;
if (_reading is not null)
ApplyReading(_reading, keepKind: true);
NotifyRemoteContent();
}
private bool CanShowRemoteImages() =>
ShowRemoteContentBanner;
partial void OnReadingBodyKindChanged(string value) { partial void OnReadingBodyKindChanged(string value) {
if (_syncingBodyKind) if (_syncingBodyKind)
return; return;
@ -2687,6 +2724,11 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
partial void OnHasReadingChanged(bool value) => partial void OnHasReadingChanged(bool value) =>
NotifyBodyKind(); NotifyBodyKind();
private void NotifyRemoteContent() {
OnPropertyChanged(nameof(ShowRemoteContentBanner));
ShowRemoteImagesCommand.NotifyCanExecuteChanged();
}
private void NotifyBodyKind() { private void NotifyBodyKind() {
OnPropertyChanged(nameof(ShowHtmlBody)); OnPropertyChanged(nameof(ShowHtmlBody));
OnPropertyChanged(nameof(ShowTextBody)); OnPropertyChanged(nameof(ShowTextBody));
@ -2697,6 +2739,7 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
OnPropertyChanged(nameof(IsSourceKind)); OnPropertyChanged(nameof(IsSourceKind));
OnPropertyChanged(nameof(IsFatturaKind)); OnPropertyChanged(nameof(IsFatturaKind));
OnPropertyChanged(nameof(HtmlEngineReady)); OnPropertyChanged(nameof(HtmlEngineReady));
NotifyRemoteContent();
OnPropertyChanged(nameof(ShowFatturaPaTools)); OnPropertyChanged(nameof(ShowFatturaPaTools));
} }
@ -4335,6 +4378,7 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
UnwrapEnvelope, UnwrapEnvelope,
_bodyPreference, _bodyPreference,
HtmlEngineReady, HtmlEngineReady,
BlockRemoteContent,
EnsureComposeSessionAsync); EnsureComposeSessionAsync);
vm.ComposeRequested += compose => { vm.ComposeRequested += compose => {
compose.Sent += RememberSent; compose.Sent += RememberSent;
@ -4409,12 +4453,17 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
var markup = MessageHtml.HasMarkup(html); var markup = MessageHtml.HasMarkup(html);
if (string.IsNullOrWhiteSpace(text)) if (string.IsNullOrWhiteSpace(text))
text = MessageText.StripHtml(html); text = MessageText.StripHtml(html);
if (markup) { var inlined = markup
var cid = MessageHtml.DataUris(files.Select(f => (f.ContentId, f.ContentType, f.Bytes))); ? MessageHtml.InlineCid(html, MessageHtml.DataUris(files.Select(f => (f.ContentId, f.ContentType, f.Bytes))))
ReadingHtmlDocument = MessageHtml.Document(MessageHtml.InlineCid(html, cid)); : "";
} _readingHasRemoteContent = MessageHtml.HasRemoteContent(inlined.Length > 0 ? inlined : html);
if (!keepKind)
_allowRemoteForMessage = false;
var allowRemote = !BlockRemoteContent || _allowRemoteForMessage;
if (markup)
ReadingHtmlDocument = MessageHtml.Document(inlined, allowRemote);
else if (keepKind && MailBodyKind.IsHtml(ReadingBodyKind)) else if (keepKind && MailBodyKind.IsHtml(ReadingBodyKind))
ReadingHtmlDocument = MessageHtml.Document(MessageHtml.FromPlain(text)); ReadingHtmlDocument = MessageHtml.Document(MessageHtml.FromPlain(text), allowRemote);
else else
ReadingHtmlDocument = ""; ReadingHtmlDocument = "";
ReadingBody = text ?? ""; ReadingBody = text ?? "";
@ -4438,6 +4487,7 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
ApplyBodyKind(fattura is not null && UseFatturaPa ApplyBodyKind(fattura is not null && UseFatturaPa
? MailBodyKind.Fattura ? MailBodyKind.Fattura
: MailBodyKind.DefaultView(html, inner ? body.InnerText : body.Text, _bodyPreference)); : MailBodyKind.DefaultView(html, inner ? body.InnerText : body.Text, _bodyPreference));
NotifyRemoteContent();
} }
private static bool IsInlineImage(MailFileAttachment file, string html) { private static bool IsInlineImage(MailFileAttachment file, string html) {
@ -4467,6 +4517,9 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
HasReadingFattura = false; HasReadingFattura = false;
ReadingFattura = ""; ReadingFattura = "";
ReadingFiles.Clear(); ReadingFiles.Clear();
_allowRemoteForMessage = false;
_readingHasRemoteContent = false;
NotifyRemoteContent();
} }
private void MarkRowAttachments(MailMessageHeader header) { private void MarkRowAttachments(MailMessageHeader header) {

View File

@ -12,8 +12,11 @@ public partial class MessageWindowViewModel : ObservableObject {
private readonly ComposeIdentity _from; private readonly ComposeIdentity _from;
private readonly Func<ComposeIdentity, CancellationToken, Task<IMailSession?>>? _ensureFrom; private readonly Func<ComposeIdentity, CancellationToken, Task<IMailSession?>>? _ensureFrom;
private readonly string _preference; private readonly string _preference;
private readonly bool _blockRemoteContent;
private bool _syncingKind; private bool _syncingKind;
private bool _htmlReady; private bool _htmlReady;
private bool _allowRemoteForMessage;
private bool _readingHasRemoteContent;
[ObservableProperty] [ObservableProperty]
private string readingSubject = ""; private string readingSubject = "";
@ -100,6 +103,12 @@ public partial class MessageWindowViewModel : ObservableObject {
public bool ShowFatturaPaTools => public bool ShowFatturaPaTools =>
HasReadingFattura && UseFatturaPa; HasReadingFattura && UseFatturaPa;
public bool ShowRemoteContentBanner =>
ShowHtmlBody
&& _blockRemoteContent
&& !_allowRemoteForMessage
&& _readingHasRemoteContent;
public bool HasReadingFattura { get; private set; } public bool HasReadingFattura { get; private set; }
public string ReadingFattura { get; private set; } = ""; public string ReadingFattura { get; private set; } = "";
@ -117,6 +126,7 @@ public partial class MessageWindowViewModel : ObservableObject {
bool unwrap, bool unwrap,
string preference, string preference,
bool htmlReady, bool htmlReady,
bool blockRemoteContent = true,
Func<ComposeIdentity, CancellationToken, Task<IMailSession?>>? ensureFrom = null) { Func<ComposeIdentity, CancellationToken, Task<IMailSession?>>? ensureFrom = null) {
_body = body; _body = body;
_fromAccounts = fromAccounts; _fromAccounts = fromAccounts;
@ -124,6 +134,7 @@ public partial class MessageWindowViewModel : ObservableObject {
_ensureFrom = ensureFrom; _ensureFrom = ensureFrom;
_preference = MailBodyKind.Preference(preference); _preference = MailBodyKind.Preference(preference);
_htmlReady = htmlReady; _htmlReady = htmlReady;
_blockRemoteContent = blockRemoteContent;
unwrapEnvelope = unwrap; unwrapEnvelope = unwrap;
readingBodyKind = MailBodyKind.DefaultView( readingBodyKind = MailBodyKind.DefaultView(
unwrap && body.Envelope.HasInnerMessage ? body.InnerHtml : body.Html, unwrap && body.Envelope.HasInnerMessage ? body.InnerHtml : body.Html,
@ -162,6 +173,18 @@ public partial class MessageWindowViewModel : ObservableObject {
SetReadingKind(MailBodyKind.Fattura); SetReadingKind(MailBodyKind.Fattura);
} }
[RelayCommand(CanExecute = nameof(CanShowRemoteImages))]
private void ShowRemoteImages() {
if (!CanShowRemoteImages())
return;
_allowRemoteForMessage = true;
ApplyReading(true);
NotifyRemoteContent();
}
private bool CanShowRemoteImages() =>
ShowRemoteContentBanner;
[RelayCommand] [RelayCommand]
private void Reply() => private void Reply() =>
OpenCompose("Reply", Recipients(ReadingFrom), null, ReplySubject(), QuotedBody()); OpenCompose("Reply", Recipients(ReadingFrom), null, ReplySubject(), QuotedBody());
@ -234,12 +257,17 @@ public partial class MessageWindowViewModel : ObservableObject {
var markup = MessageHtml.HasMarkup(html); var markup = MessageHtml.HasMarkup(html);
if (string.IsNullOrWhiteSpace(text)) if (string.IsNullOrWhiteSpace(text))
text = MessageText.StripHtml(html); text = MessageText.StripHtml(html);
if (markup) { var inlined = markup
var cid = MessageHtml.DataUris(files.Select(f => (f.ContentId, f.ContentType, f.Bytes))); ? MessageHtml.InlineCid(html, MessageHtml.DataUris(files.Select(f => (f.ContentId, f.ContentType, f.Bytes))))
ReadingHtmlDocument = MessageHtml.Document(MessageHtml.InlineCid(html, cid)); : "";
} _readingHasRemoteContent = MessageHtml.HasRemoteContent(inlined.Length > 0 ? inlined : html);
if (!keepKind)
_allowRemoteForMessage = false;
var allowRemote = !_blockRemoteContent || _allowRemoteForMessage;
if (markup)
ReadingHtmlDocument = MessageHtml.Document(inlined, allowRemote);
else if (keepKind && MailBodyKind.IsHtml(ReadingBodyKind)) else if (keepKind && MailBodyKind.IsHtml(ReadingBodyKind))
ReadingHtmlDocument = MessageHtml.Document(MessageHtml.FromPlain(text)); ReadingHtmlDocument = MessageHtml.Document(MessageHtml.FromPlain(text), allowRemote);
else else
ReadingHtmlDocument = ""; ReadingHtmlDocument = "";
ReadingBody = text ?? ""; ReadingBody = text ?? "";
@ -272,11 +300,17 @@ public partial class MessageWindowViewModel : ObservableObject {
NotifyBodyKind(); NotifyBodyKind();
} }
private void NotifyRemoteContent() {
OnPropertyChanged(nameof(ShowRemoteContentBanner));
ShowRemoteImagesCommand.NotifyCanExecuteChanged();
}
private void NotifyBodyKind() { private void NotifyBodyKind() {
OnPropertyChanged(nameof(ShowHtmlBody)); OnPropertyChanged(nameof(ShowHtmlBody));
OnPropertyChanged(nameof(ShowTextBody)); OnPropertyChanged(nameof(ShowTextBody));
OnPropertyChanged(nameof(ShowSourceBody)); OnPropertyChanged(nameof(ShowSourceBody));
OnPropertyChanged(nameof(ShowFatturaBody)); OnPropertyChanged(nameof(ShowFatturaBody));
NotifyRemoteContent();
OnPropertyChanged(nameof(IsHtmlKind)); OnPropertyChanged(nameof(IsHtmlKind));
OnPropertyChanged(nameof(IsTextKind)); OnPropertyChanged(nameof(IsTextKind));
OnPropertyChanged(nameof(IsSourceKind)); OnPropertyChanged(nameof(IsSourceKind));

View File

@ -53,6 +53,21 @@
<CheckBox Content="{Binding Copy.UnwrapEnvelope}" <CheckBox Content="{Binding Copy.UnwrapEnvelope}"
IsChecked="{Binding UnwrapEnvelope}" IsChecked="{Binding UnwrapEnvelope}"
IsVisible="{Binding UseEnvelopeTools}" /> IsVisible="{Binding UseEnvelopeTools}" />
<Border Classes="remote-content-banner"
IsVisible="{Binding ShowRemoteContentBanner}"
Padding="8,6"
Margin="0,2,0,0">
<DockPanel>
<Button DockPanel.Dock="Right"
Content="{Binding Copy.ShowRemoteImages}"
Classes="mail-tool"
Command="{Binding ShowRemoteImagesCommand}"
Margin="8,0,0,0" />
<TextBlock Text="{Binding Copy.RemoteContentBlocked}"
TextWrapping="Wrap"
VerticalAlignment="Center" />
</DockPanel>
</Border>
<StackPanel Orientation="Horizontal" Spacing="4"> <StackPanel Orientation="Horizontal" Spacing="4">
<Button Content="{Binding Copy.Html}" <Button Content="{Binding Copy.Html}"
Classes="body-kind" Classes="body-kind"

View File

@ -53,6 +53,21 @@
<CheckBox Content="{Binding Copy.UnwrapEnvelope}" <CheckBox Content="{Binding Copy.UnwrapEnvelope}"
IsChecked="{Binding UnwrapEnvelope}" IsChecked="{Binding UnwrapEnvelope}"
IsVisible="{Binding UseEnvelopeTools}" /> IsVisible="{Binding UseEnvelopeTools}" />
<Border Classes="remote-content-banner"
IsVisible="{Binding ShowRemoteContentBanner}"
Padding="8,6"
Margin="0,2,0,0">
<DockPanel>
<Button DockPanel.Dock="Right"
Content="{Binding Copy.ShowRemoteImages}"
Classes="mail-tool"
Command="{Binding ShowRemoteImagesCommand}"
Margin="8,0,0,0" />
<TextBlock Text="{Binding Copy.RemoteContentBlocked}"
TextWrapping="Wrap"
VerticalAlignment="Center" />
</DockPanel>
</Border>
<StackPanel Orientation="Horizontal" Spacing="4"> <StackPanel Orientation="Horizontal" Spacing="4">
<Button Content="{Binding Copy.Html}" <Button Content="{Binding Copy.Html}"
Classes="body-kind" Classes="body-kind"

View File

@ -1,7 +1,7 @@
{ {
"$schema": "https://json-schema.org/draft-07/schema", "$schema": "https://json-schema.org/draft-07/schema",
"title": "Postclient Release Script Settings", "title": "Postclient Release Script Settings",
"description": "Community desktop pipeline (same shape as maksit-wvc210). GitHub assets are siblings: portable zip, Windows setup exe, Flatpak. The zip never contains the installer or Flatpak. Self-contained publish — no .NET SDK on the user PC. Default Windows install is C:\\Program Files\\MaksIT\\Postclient.", "description": "Community desktop pipeline. GitHub assets are siblings: portable zip, Windows setup exe, Flatpak. MsixPack writes a Store .msix (kept in releases/, not a GitHub asset). The zip never contains the installer, Flatpak, or MSIX. Self-contained publish — no .NET SDK on the user PC. Default Windows install is C:\\Program Files\\MaksIT\\Postclient.",
"plugins": [ "plugins": [
{ {
"name": "DotNetReleaseVersion", "name": "DotNetReleaseVersion",
@ -62,20 +62,31 @@
"runtimeIdentifier": "win-x64", "runtimeIdentifier": "win-x64",
"installScope": "perMachine", "installScope": "perMachine",
"iconPath": "..\\..\\..\\src\\MaksIT.PostClient.UI\\Assets\\icon.ico", "iconPath": "..\\..\\..\\src\\MaksIT.PostClient.UI\\Assets\\icon.ico",
"exeNamePattern": "postclient-{version}.exe" "exeNamePattern": "postclient-{version}.exe",
"requiredPublishFiles": [
"MaksIT.PostClient.Client.dll",
"MaksIT.IdentityHub.Client.dll",
"MaksIT.IdentityHub.Contracts.dll"
]
}, },
{ {
"name": "MsixPack", "name": "MsixPack",
"stageLabel": "build", "stageLabel": "build",
"enabled": false, "enabled": true,
"packageName": "MAKS-IT.Postclient", "packageName": "MAKS-IT.Postclient",
"publisher": "CN=PartnerCenter", "publisher": "CN=FCC8C0E7-6D5F-4028-B8EE-903B88C0C8F9",
"publisherDisplayName": "MaksIT", "publisherDisplayName": "MaksIT",
"displayName": "Postclient", "displayName": "Postclient",
"description": "Desktop mail client for the mailbox you already have",
"executableName": "MaksIT.PostClient.UI.exe", "executableName": "MaksIT.PostClient.UI.exe",
"runtimeIdentifier": "win-x64", "runtimeIdentifier": "win-x64",
"iconPath": "..\\..\\..\\src\\MaksIT.PostClient.UI\\Assets\\icon.png", "iconPath": "..\\..\\..\\src\\MaksIT.PostClient.UI\\Assets\\icon.png",
"msixNamePattern": "postclient-{version}.msix" "msixNamePattern": "postclient-{version}.msix",
"requiredPublishFiles": [
"MaksIT.PostClient.Client.dll",
"MaksIT.IdentityHub.Client.dll",
"MaksIT.IdentityHub.Contracts.dll"
]
}, },
{ {
"name": "FlatpakPack", "name": "FlatpakPack",

View File

@ -301,8 +301,13 @@ function New-WixPackageXml {
$null = $package.SetAttribute('Scope', $scope) $null = $package.SetAttribute('Scope', $scope)
$null = $wix.AppendChild($package) $null = $wix.AppendChild($package)
# Install the new payload first, then remove the previous product. Early
# RemoveExistingProducts can leave a half-empty folder when a Burn/MSI run
# aborts after uninstalling the old product (sibling DLLs appear "missing").
$major = $xml.CreateElement('MajorUpgrade', $ns) $major = $xml.CreateElement('MajorUpgrade', $ns)
$null = $major.SetAttribute('DowngradeErrorMessage', 'A newer version is already installed.') $null = $major.SetAttribute('DowngradeErrorMessage', 'A newer version is already installed.')
$null = $major.SetAttribute('Schedule', 'afterInstallExecute')
$null = $major.SetAttribute('AllowSameVersionUpgrades', 'yes')
$null = $package.AppendChild($major) $null = $package.AppendChild($major)
$media = $xml.CreateElement('MediaTemplate', $ns) $media = $xml.CreateElement('MediaTemplate', $ns)
@ -431,7 +436,11 @@ function New-WixPackageXml {
$component = $xml.CreateElement('Component', $ns) $component = $xml.CreateElement('Component', $ns)
$componentId = ConvertTo-WixIdentifier -Value $relative -Prefix 'c' $componentId = ConvertTo-WixIdentifier -Value $relative -Prefix 'c'
$null = $component.SetAttribute('Id', $componentId) $null = $component.SetAttribute('Id', $componentId)
$null = $component.SetAttribute('Guid', '*') # Stable GUIDs across builds so upgrades/repairs replace the same
# logical file instead of orphaning Guid='*' components.
$null = $component.SetAttribute(
'Guid',
(Get-WixFileComponentGuid -UpgradeCode $UpgradeCode -RelativePath $relative).ToString('D'))
$componentIds.Add($componentId) $componentIds.Add($componentId)
$fileNode = $xml.CreateElement('File', $ns) $fileNode = $xml.CreateElement('File', $ns)
$null = $fileNode.SetAttribute('Id', (ConvertTo-WixIdentifier -Value $relative -Prefix 'f')) $null = $fileNode.SetAttribute('Id', (ConvertTo-WixIdentifier -Value $relative -Prefix 'f'))
@ -941,6 +950,71 @@ function Get-DesktopShortcutComponentGuid {
return [guid]::new($hash) return [guid]::new($hash)
} }
function Get-WixFileComponentGuid {
param(
[Parameter(Mandatory = $true)]
[guid]$UpgradeCode,
[Parameter(Mandatory = $true)]
[string]$RelativePath
)
$normalized = ($RelativePath -replace '\\', '/').Trim().ToLowerInvariant()
$identity = "maksit-wix-file|$($UpgradeCode.ToString('D'))|$normalized"
$md5 = [System.Security.Cryptography.MD5]::Create()
try {
$hash = $md5.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($identity))
}
finally {
$md5.Dispose()
}
return [guid]::new($hash)
}
function Assert-DesktopPublishFiles {
param(
[Parameter(Mandatory = $true)]
[string]$PublishDirectory,
[Parameter(Mandatory = $false)]
[string[]]$RequiredFileNames = @()
)
if (-not (Test-Path -LiteralPath $PublishDirectory -PathType Container)) {
throw "Desktop publish directory not found: $PublishDirectory"
}
$names = @(
$RequiredFileNames |
Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) } |
ForEach-Object { [string]$_.Trim() } |
Select-Object -Unique
)
if ($names.Count -eq 0) {
return
}
$missing = [System.Collections.Generic.List[string]]::new()
foreach ($name in $names) {
$direct = Join-Path $PublishDirectory $name
if (Test-Path -LiteralPath $direct -PathType Leaf) {
continue
}
$found = @(
Get-ChildItem -LiteralPath $PublishDirectory -Recurse -File -Filter $name -ErrorAction SilentlyContinue
)
if ($found.Count -eq 0) {
$missing.Add($name)
}
}
if ($missing.Count -gt 0) {
throw "Desktop publish folder is missing required file(s) for packaging: $($missing -join ', '). Publish directory: $PublishDirectory"
}
}
function Get-DerivedBundleUpgradeCode { function Get-DerivedBundleUpgradeCode {
param( param(
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
@ -1477,6 +1551,8 @@ Export-ModuleMember -Function `
Get-PluginPropertyValue, ` Get-PluginPropertyValue, `
Resolve-DesktopPublishDirectory, ` Resolve-DesktopPublishDirectory, `
Resolve-DesktopExecutablePath, ` Resolve-DesktopExecutablePath, `
Get-WixFileComponentGuid, `
Assert-DesktopPublishFiles, `
New-WixPackageXml, ` New-WixPackageXml, `
ConvertTo-MsixPackageVersion, ` ConvertTo-MsixPackageVersion, `
Assert-MsixPackageName, ` Assert-MsixPackageName, `

View File

@ -131,6 +131,7 @@ function Invoke-Plugin {
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineFact" Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineFact"
Import-PluginDependency -ModuleName "ExternalCommandSupport" -RequiredCommand "Invoke-ExternalCommand" Import-PluginDependency -ModuleName "ExternalCommandSupport" -RequiredCommand "Invoke-ExternalCommand"
Import-PluginDependency -ModuleName "DesktopPackSupport" -RequiredCommand "New-MsixManifestXml" Import-PluginDependency -ModuleName "DesktopPackSupport" -RequiredCommand "New-MsixManifestXml"
Import-PluginDependency -ModuleName "DesktopPackSupport" -RequiredCommand "Assert-DesktopPublishFiles"
if (-not $IsWindows -and -not (Test-ExternalCommandTestHandler)) { if (-not $IsWindows -and -not (Test-ExternalCommandTestHandler)) {
throw "MsixPack requires Windows (Windows SDK makeappx.exe and signtool.exe)." throw "MsixPack requires Windows (Windows SDK makeappx.exe and signtool.exe)."
@ -191,6 +192,18 @@ function Invoke-Plugin {
-PublishDir $publishDirSetting ` -PublishDir $publishDirSetting `
-ScriptDir $scriptDir -ScriptDir $scriptDir
$requiredPublishFiles = @(Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'requiredPublishFiles' -Default @())
$requiredPublishFiles = @(
$requiredPublishFiles |
ForEach-Object { [string]$_ } |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
)
if (-not [string]::IsNullOrWhiteSpace($executableName) -and ($requiredPublishFiles -notcontains $executableName)) {
$requiredPublishFiles = @($executableName) + $requiredPublishFiles
}
Assert-DesktopPublishFiles -PublishDirectory $publishDirectory -RequiredFileNames $requiredPublishFiles
$null = Resolve-DesktopExecutablePath ` $null = Resolve-DesktopExecutablePath `
-PublishDirectory $publishDirectory ` -PublishDirectory $publishDirectory `
-ExecutableName $executableName ` -ExecutableName $executableName `

View File

@ -85,6 +85,7 @@ function Invoke-Plugin {
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Add-ReleaseAssetPaths" Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Add-ReleaseAssetPaths"
Import-PluginDependency -ModuleName "ExternalCommandSupport" -RequiredCommand "Invoke-ExternalCommand" Import-PluginDependency -ModuleName "ExternalCommandSupport" -RequiredCommand "Invoke-ExternalCommand"
Import-PluginDependency -ModuleName "DesktopPackSupport" -RequiredCommand "New-WixPackageXml" Import-PluginDependency -ModuleName "DesktopPackSupport" -RequiredCommand "New-WixPackageXml"
Import-PluginDependency -ModuleName "DesktopPackSupport" -RequiredCommand "Assert-DesktopPublishFiles"
$pluginSettings = $Settings $pluginSettings = $Settings
$sharedSettings = $Settings.context $sharedSettings = $Settings.context
@ -130,6 +131,18 @@ function Invoke-Plugin {
-PublishDir $publishDirSetting ` -PublishDir $publishDirSetting `
-ScriptDir $scriptDir -ScriptDir $scriptDir
$requiredPublishFiles = @(Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'requiredPublishFiles' -Default @())
$requiredPublishFiles = @(
$requiredPublishFiles |
ForEach-Object { [string]$_ } |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
)
if (-not [string]::IsNullOrWhiteSpace($executableName) -and ($requiredPublishFiles -notcontains $executableName)) {
$requiredPublishFiles = @($executableName) + $requiredPublishFiles
}
Assert-DesktopPublishFiles -PublishDirectory $publishDirectory -RequiredFileNames $requiredPublishFiles
$executablePath = Resolve-DesktopExecutablePath ` $executablePath = Resolve-DesktopExecutablePath `
-PublishDirectory $publishDirectory ` -PublishDirectory $publishDirectory `
-ExecutableName $executableName ` -ExecutableName $executableName `