mirror of
https://github.com/MAKS-IT-COM/maksit-postclient.git
synced 2026-09-30 03:18:09 +02:00
(feature): move expired mail to trash and keep trash by days
This commit is contained in:
parent
817836ab28
commit
863d9d949d
13
CHANGELOG.md
13
CHANGELOG.md
@ -6,6 +6,19 @@ The format follows [Keep a Changelog](https://keepachangelog.com/) and [Semantic
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.3.2] - 2026-09-19
|
||||||
|
|
||||||
|
Expired folder mail goes to Trash; Trash can be kept forever or purged after a number of days.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Settings → Retention…**: folder days (`0` = keep forever) now **move** older messages to Trash after Get Messages, instead of deleting them permanently. The Trash row uses the same days field: `0` keeps Trash forever, a number of days **permanently deletes** old Trash. The grid shows the action per row.
|
||||||
|
- Delete (toolbar, **Del**, empty folder, delete rules) moves messages to Trash. If Trash is missing, it is created. Deleting from Trash is still permanent. Local stores honor Trash when emptying a folder.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Local folder stores no longer skip Trash when emptying a folder (messages go to **Deleted Items**).
|
||||||
|
|
||||||
## [0.3.1] - 2026-09-19
|
## [0.3.1] - 2026-09-19
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@ -167,7 +167,7 @@ IMAP **QUOTA** from the gestore (when the server supports it) is a thin bar on t
|
|||||||
| New store | File menu: empty bucket (`Inbox`, `Drafts`, `Sent Items`, `Deleted Items`) with `postclient.store.json` and `mail.db`. |
|
| New store | File menu: empty bucket (`Inbox`, `Drafts`, `Sent Items`, `Deleted Items`) with `postclient.store.json` and `mail.db`. |
|
||||||
| Attach store | File menu: pick a folder that already has `postclient.store.json`. Recovers mail + search after an AppData wipe. |
|
| Attach store | File menu: pick a folder that already has `postclient.store.json`. Recovers mail + search after an AppData wipe. |
|
||||||
| Move / Detach store | Copy the whole directory (or unregister it). The folder stays on disk when detached. |
|
| Move / Detach store | Copy the whole directory (or unregister it). The folder stays on disk when detached. |
|
||||||
| Retention | **Settings → Retention…**: days per folder (`0` = forever). Older messages are deleted permanently after Get Messages. |
|
| Retention | **Settings → Retention…**: days per folder (`0` = forever). Expired folder mail is moved to Trash after Get Messages. On Trash, `0` keeps it forever and a number of days permanently deletes older Trash. |
|
||||||
| Rules | **Settings → Rules**: **Import rules…** reads this app’s JSON export or a legacy `.rwz` file. **Export rules…** writes JSON. Each rule is bound to an **account**; a move folder can live on another mailbox (including a store). They run on Get Messages, on import, and from **Run all rules**. |
|
| Rules | **Settings → Rules**: **Import rules…** reads this app’s JSON export or a legacy `.rwz` file. **Export rules…** writes JSON. Each rule is bound to an **account**; a move folder can live on another mailbox (including a store). They run on Get Messages, on import, and from **Run all rules**. |
|
||||||
| Print | Readable HTML of the (optionally unwrapped) message |
|
| Print | Readable HTML of the (optionally unwrapped) message |
|
||||||
| Save PDF | Same content as a PDF |
|
| Save PDF | Same content as a PDF |
|
||||||
|
|||||||
@ -30,6 +30,11 @@
|
|||||||
<display_length compare="ge">768</display_length>
|
<display_length compare="ge">768</display_length>
|
||||||
</requires>
|
</requires>
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="0.3.2" date="2026-09-19">
|
||||||
|
<description>
|
||||||
|
<p>Retention moves expired folder mail to Trash. Trash can be kept forever or purged after a number of days. Delete sends messages to Trash.</p>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
<release version="0.3.1" date="2026-09-19">
|
<release version="0.3.1" date="2026-09-19">
|
||||||
<description>
|
<description>
|
||||||
<p>The main window opens on launch from the Windows setup. Closing the window exits the app.</p>
|
<p>The main window opens on launch from the Windows setup. Closing the window exits the app.</p>
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Version>0.3.1</Version>
|
<Version>0.3.2</Version>
|
||||||
<Product>Postclient</Product>
|
<Product>Postclient</Product>
|
||||||
<AssemblyTitle>Postclient</AssemblyTitle>
|
<AssemblyTitle>Postclient</AssemblyTitle>
|
||||||
<Authors>Maksym Sadovnychyy</Authors>
|
<Authors>Maksym Sadovnychyy</Authors>
|
||||||
|
|||||||
@ -145,7 +145,7 @@ public sealed class LocalMailImport {
|
|||||||
if (!MailRuleEngine.Matches(rule, from, to, subject, text, hasAttachment))
|
if (!MailRuleEngine.Matches(rule, from, to, subject, text, hasAttachment))
|
||||||
continue;
|
continue;
|
||||||
if (rule.Action == MailRuleAction.Delete)
|
if (rule.Action == MailRuleAction.Delete)
|
||||||
folder = MailRuleEngine.ResolveFolder("Trash", catalog) ?? "Trash";
|
folder = MailRetention.ResolveTrash(catalog);
|
||||||
else if (rule.Action == MailRuleAction.Move)
|
else if (rule.Action == MailRuleAction.Move)
|
||||||
folder = MailRuleEngine.ExactFolder(rule.Folder, catalog) ?? folder;
|
folder = MailRuleEngine.ExactFolder(rule.Folder, catalog) ?? folder;
|
||||||
if (rule.Action == MailRuleAction.MarkRead)
|
if (rule.Action == MailRuleAction.MarkRead)
|
||||||
|
|||||||
@ -247,12 +247,21 @@ public sealed class LocalStoreSession : IMailSession, IFileMailStore {
|
|||||||
string? trashFolder,
|
string? trashFolder,
|
||||||
CancellationToken cancellationToken = default) =>
|
CancellationToken cancellationToken = default) =>
|
||||||
_io.RunAsync(() => {
|
_io.RunAsync(() => {
|
||||||
_ = trashFolder;
|
|
||||||
var dir = FolderDir(folder);
|
var dir = FolderDir(folder);
|
||||||
if (!Directory.Exists(dir))
|
if (!Directory.Exists(dir))
|
||||||
return Task.FromResult(Result.Ok());
|
return Task.FromResult(Result.Ok());
|
||||||
foreach (var file in Directory.EnumerateFiles(dir, "*.eml")) {
|
var files = Directory.EnumerateFiles(dir, "*.eml").ToList();
|
||||||
|
var toTrash = !string.IsNullOrWhiteSpace(trashFolder)
|
||||||
|
&& !folder.Equals(trashFolder, StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (toTrash)
|
||||||
|
Directory.CreateDirectory(FolderDir(trashFolder!));
|
||||||
|
foreach (var file in files) {
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
if (toTrash) {
|
||||||
|
var dest = MessagePath(trashFolder!, NextUid(trashFolder!));
|
||||||
|
File.Copy(file, dest, overwrite: false);
|
||||||
|
}
|
||||||
|
|
||||||
File.Delete(file);
|
File.Delete(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -112,29 +112,89 @@ public sealed class MailWorkerHost : IDisposable {
|
|||||||
var count = 0;
|
var count = 0;
|
||||||
var configuration = _files.Current;
|
var configuration = _files.Current;
|
||||||
configuration.EnsureDefaults();
|
configuration.EnsureDefaults();
|
||||||
foreach (var rule in configuration.Retention) {
|
foreach (var job in MailRetention.Jobs(configuration.Retention)) {
|
||||||
if (rule.Days <= 0 || string.IsNullOrWhiteSpace(rule.MailboxId) || string.IsNullOrWhiteSpace(rule.Folder))
|
var known = _archive.ListFolders(job.MailboxId);
|
||||||
|
var folder = MailRetention.BindFolder(job.Folder, known);
|
||||||
|
if (string.IsNullOrWhiteSpace(folder))
|
||||||
continue;
|
continue;
|
||||||
var cutoff = DateTimeOffset.UtcNow.AddDays(-rule.Days);
|
var cutoff = DateTimeOffset.UtcNow.AddDays(-job.Days);
|
||||||
var ids = _archive.UidsOlderThan(rule.MailboxId, rule.Folder, cutoff);
|
var ids = _archive.UidsOlderThan(job.MailboxId, folder, cutoff);
|
||||||
if (ids.Count == 0)
|
if (ids.Count == 0)
|
||||||
continue;
|
continue;
|
||||||
var paths = _archive.RemoveUids(rule.MailboxId, rule.Folder, ids);
|
if (MailRetention.IsTrash(folder)) {
|
||||||
foreach (var path in paths) {
|
DeleteFiles(_archive.RemoveUids(job.MailboxId, folder, ids));
|
||||||
try {
|
count += ids.Count;
|
||||||
if (File.Exists(path))
|
continue;
|
||||||
File.Delete(path);
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
count += ids.Count;
|
var trash = MailRetention.ResolveTrash(known);
|
||||||
|
if (folder.Equals(trash, StringComparison.OrdinalIgnoreCase)) {
|
||||||
|
DeleteFiles(_archive.RemoveUids(job.MailboxId, folder, ids));
|
||||||
|
count += ids.Count;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
count += MoveToTrash(configuration, job.MailboxId, folder, trash, ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(count.ToString());
|
return Ok(count.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int MoveToTrash(
|
||||||
|
Configuration configuration,
|
||||||
|
string mailboxId,
|
||||||
|
string folder,
|
||||||
|
string trash,
|
||||||
|
IReadOnlyList<uint> ids) {
|
||||||
|
var moved = new List<uint>();
|
||||||
|
foreach (var uid in ids) {
|
||||||
|
try {
|
||||||
|
var source = _archive.EmlPath(mailboxId, folder, uid);
|
||||||
|
var destUid = _archive.NextUid(mailboxId, trash);
|
||||||
|
var dest = RetentionDestPath(configuration, mailboxId, trash, destUid);
|
||||||
|
if (!string.IsNullOrWhiteSpace(source)
|
||||||
|
&& File.Exists(source)
|
||||||
|
&& !source.Equals(dest, StringComparison.OrdinalIgnoreCase)) {
|
||||||
|
var dir = Path.GetDirectoryName(dest);
|
||||||
|
if (!string.IsNullOrWhiteSpace(dir))
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
File.Copy(source, dest, overwrite: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
_archive.CopyIndexed(mailboxId, folder, uid, mailboxId, trash, dest, destUid);
|
||||||
|
moved.Add(uid);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moved.Count > 0)
|
||||||
|
DeleteFiles(_archive.RemoveUids(mailboxId, folder, moved));
|
||||||
|
return moved.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RetentionDestPath(
|
||||||
|
Configuration configuration,
|
||||||
|
string mailboxId,
|
||||||
|
string folder,
|
||||||
|
uint uid) {
|
||||||
|
var box = configuration.FindMailbox(mailboxId);
|
||||||
|
if (box is not null)
|
||||||
|
return MailArchiveLayout.EmlPath(box, configuration.Mailboxes, folder, uid);
|
||||||
|
return ArchiveFiles.EmlPath(mailboxId, folder, uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DeleteFiles(IEnumerable<string> paths) {
|
||||||
|
foreach (var path in paths) {
|
||||||
|
try {
|
||||||
|
if (!string.IsNullOrWhiteSpace(path) && File.Exists(path))
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose() =>
|
public void Dispose() =>
|
||||||
_semantic?.Dispose();
|
_semantic?.Dispose();
|
||||||
|
|
||||||
|
|||||||
@ -68,6 +68,12 @@ public sealed class Configuration {
|
|||||||
Semantic ??= new SemanticSearchSettings();
|
Semantic ??= new SemanticSearchSettings();
|
||||||
Semantic.Normalize();
|
Semantic.Normalize();
|
||||||
Rules ??= [];
|
Rules ??= [];
|
||||||
|
foreach (var rule in Rules) {
|
||||||
|
rule.MailboxId ??= "";
|
||||||
|
rule.FolderMailboxId ??= "";
|
||||||
|
rule.Folder ??= "";
|
||||||
|
}
|
||||||
|
|
||||||
Retention ??= [];
|
Retention ??= [];
|
||||||
foreach (var row in Retention) {
|
foreach (var row in Retention) {
|
||||||
row.MailboxId ??= "";
|
row.MailboxId ??= "";
|
||||||
|
|||||||
68
src/MaksIT.PostClient.Shared/MailRetention.cs
Normal file
68
src/MaksIT.PostClient.Shared/MailRetention.cs
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
namespace MaksIT.PostClient.Shared;
|
||||||
|
|
||||||
|
|
||||||
|
public static class MailRetention {
|
||||||
|
public const string TrashFolder = "Deleted Items";
|
||||||
|
|
||||||
|
public static bool IsTrash(string? name, string? fullName = null) =>
|
||||||
|
MailFolderRole.Kind(name, fullName ?? name) == "trash";
|
||||||
|
|
||||||
|
public static string ResolveTrash(IEnumerable<string>? folders) {
|
||||||
|
foreach (var folder in folders ?? []) {
|
||||||
|
if (string.IsNullOrWhiteSpace(folder))
|
||||||
|
continue;
|
||||||
|
if (IsTrash(folder))
|
||||||
|
return folder;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TrashFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ResolveTrash(IReadOnlyList<(string Name, string FullName)> folders) {
|
||||||
|
foreach (var folder in folders) {
|
||||||
|
if (string.IsNullOrWhiteSpace(folder.FullName))
|
||||||
|
continue;
|
||||||
|
if (MailFolderRole.Kind(folder.Name, folder.FullName) == "trash")
|
||||||
|
return folder.FullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TrashFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string BindFolder(string? wanted, IEnumerable<string>? folders) {
|
||||||
|
var list = (folders ?? [])
|
||||||
|
.Where(folder => !string.IsNullOrWhiteSpace(folder))
|
||||||
|
.Select(folder => (folder, folder))
|
||||||
|
.ToList();
|
||||||
|
return MailRuleEngine.ExactFolder(wanted, list) ?? (wanted ?? "").Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<FolderRetention> Jobs(IEnumerable<FolderRetention>? retention) {
|
||||||
|
var map = new Dictionary<string, FolderRetention>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var row in retention ?? [])
|
||||||
|
Put(map, row);
|
||||||
|
return map.Values.Where(row => row.Days > 0).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Put(Dictionary<string, FolderRetention> map, FolderRetention row) {
|
||||||
|
if (string.IsNullOrWhiteSpace(row.MailboxId) || string.IsNullOrWhiteSpace(row.Folder))
|
||||||
|
return;
|
||||||
|
var mailboxId = row.MailboxId.Trim();
|
||||||
|
var folder = row.Folder.Trim();
|
||||||
|
var days = Math.Max(0, row.Days);
|
||||||
|
if (days <= 0)
|
||||||
|
return;
|
||||||
|
var key = mailboxId + "\n" + folder;
|
||||||
|
if (map.TryGetValue(key, out var existing)) {
|
||||||
|
if (days < existing.Days)
|
||||||
|
existing.Days = days;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
map[key] = new FolderRetention {
|
||||||
|
MailboxId = mailboxId,
|
||||||
|
Folder = folder,
|
||||||
|
Days = days
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -118,6 +118,12 @@ public sealed partial class UiCopy {
|
|||||||
|
|
||||||
public string RetentionDays { get; init; } = "";
|
public string RetentionDays { get; init; } = "";
|
||||||
|
|
||||||
|
public string RetentionAction { get; init; } = "";
|
||||||
|
|
||||||
|
public string RetentionToTrash { get; init; } = "";
|
||||||
|
|
||||||
|
public string RetentionPurge { get; init; } = "";
|
||||||
|
|
||||||
public string RetentionRun { get; init; } = "";
|
public string RetentionRun { get; init; } = "";
|
||||||
|
|
||||||
public string LocalStore { get; init; } = "";
|
public string LocalStore { get; init; } = "";
|
||||||
@ -616,8 +622,11 @@ public sealed partial class UiCopy {
|
|||||||
DetachStoreDone = "Detached {0}. The folder is still on disk.",
|
DetachStoreDone = "Detached {0}. The folder is still on disk.",
|
||||||
MoveStoreDone = "Moved store {0}.",
|
MoveStoreDone = "Moved store {0}.",
|
||||||
Retention = "_Retention…",
|
Retention = "_Retention…",
|
||||||
RetentionHint = "Per folder. 0 keeps mail forever. A number of days permanently deletes older messages (not Trash).",
|
RetentionHint = "Per folder. 0 keeps mail forever. Folder days move older messages to Trash. Trash days permanently delete, or 0 to keep Trash forever.",
|
||||||
RetentionDays = "Days",
|
RetentionDays = "Days",
|
||||||
|
RetentionAction = "When expired",
|
||||||
|
RetentionToTrash = "Move to Trash",
|
||||||
|
RetentionPurge = "Delete permanently",
|
||||||
RetentionRun = "Run now",
|
RetentionRun = "Run now",
|
||||||
LocalStore = "Local store",
|
LocalStore = "Local store",
|
||||||
LocalStoreAppData = "App data (default)",
|
LocalStoreAppData = "App data (default)",
|
||||||
@ -880,8 +889,11 @@ public sealed partial class UiCopy {
|
|||||||
DetachStoreDone = "Sganciato {0}. La cartella resta sul disco.",
|
DetachStoreDone = "Sganciato {0}. La cartella resta sul disco.",
|
||||||
MoveStoreDone = "Spostato l’archivio {0}.",
|
MoveStoreDone = "Spostato l’archivio {0}.",
|
||||||
Retention = "_Conservazione…",
|
Retention = "_Conservazione…",
|
||||||
RetentionHint = "Per cartella. 0 conserva per sempre. Un numero di giorni elimina in modo permanente i messaggi più vecchi (non nel Cestino).",
|
RetentionHint = "Per cartella. 0 conserva per sempre. I giorni della cartella spostano i messaggi più vecchi nel Cestino. I giorni del Cestino eliminano in modo permanente, oppure 0 per tenere il Cestino per sempre.",
|
||||||
RetentionDays = "Giorni",
|
RetentionDays = "Giorni",
|
||||||
|
RetentionAction = "Alla scadenza",
|
||||||
|
RetentionToTrash = "Sposta nel Cestino",
|
||||||
|
RetentionPurge = "Elimina in modo permanente",
|
||||||
RetentionRun = "Esegui ora",
|
RetentionRun = "Esegui ora",
|
||||||
LocalStore = "Archivio locale",
|
LocalStore = "Archivio locale",
|
||||||
LocalStoreAppData = "Dati app (predefinito)",
|
LocalStoreAppData = "Dati app (predefinito)",
|
||||||
|
|||||||
@ -60,8 +60,11 @@ public sealed partial class UiCopy {
|
|||||||
DetachStoreDone = "{0} gelöst. Der Ordner bleibt auf der Platte.",
|
DetachStoreDone = "{0} gelöst. Der Ordner bleibt auf der Platte.",
|
||||||
MoveStoreDone = "Speicher {0} verschoben.",
|
MoveStoreDone = "Speicher {0} verschoben.",
|
||||||
Retention = "_Aufbewahrung…",
|
Retention = "_Aufbewahrung…",
|
||||||
RetentionHint = "Pro Ordner. 0 behält dauerhaft. Eine Tageszahl löscht ältere Nachrichten endgültig (nicht in den Papierkorb).",
|
RetentionHint = "Pro Ordner. 0 behält dauerhaft. Ordner-Tage verschieben ältere Nachrichten in den Papierkorb. Papierkorb-Tage löschen endgültig, oder 0 behält den Papierkorb dauerhaft.",
|
||||||
RetentionDays = "Tage",
|
RetentionDays = "Tage",
|
||||||
|
RetentionAction = "Nach Ablauf",
|
||||||
|
RetentionToTrash = "In den Papierkorb",
|
||||||
|
RetentionPurge = "Endgültig löschen",
|
||||||
RetentionRun = "Jetzt ausführen",
|
RetentionRun = "Jetzt ausführen",
|
||||||
LocalStore = "Lokaler Speicher",
|
LocalStore = "Lokaler Speicher",
|
||||||
LocalStoreAppData = "App-Daten (Standard)",
|
LocalStoreAppData = "App-Daten (Standard)",
|
||||||
|
|||||||
@ -60,8 +60,11 @@ public sealed partial class UiCopy {
|
|||||||
DetachStoreDone = "Desadjuntado {0}. La carpeta sigue en el disco.",
|
DetachStoreDone = "Desadjuntado {0}. La carpeta sigue en el disco.",
|
||||||
MoveStoreDone = "Almacén {0} movido.",
|
MoveStoreDone = "Almacén {0} movido.",
|
||||||
Retention = "_Retención…",
|
Retention = "_Retención…",
|
||||||
RetentionHint = "Por carpeta. 0 conserva para siempre. Un número de días borra de forma permanente los mensajes más antiguos (no a la Papelera).",
|
RetentionHint = "Por carpeta. 0 conserva para siempre. Los días de la carpeta envían los mensajes antiguos a la Papelera. Los días de la Papelera borran de forma permanente, o 0 para conservarla siempre.",
|
||||||
RetentionDays = "Días",
|
RetentionDays = "Días",
|
||||||
|
RetentionAction = "Al caducar",
|
||||||
|
RetentionToTrash = "Mover a la Papelera",
|
||||||
|
RetentionPurge = "Eliminar de forma permanente",
|
||||||
RetentionRun = "Ejecutar ahora",
|
RetentionRun = "Ejecutar ahora",
|
||||||
LocalStore = "Almacén local",
|
LocalStore = "Almacén local",
|
||||||
LocalStoreAppData = "Datos de la app (predeterminado)",
|
LocalStoreAppData = "Datos de la app (predeterminado)",
|
||||||
|
|||||||
@ -60,8 +60,11 @@ public sealed partial class UiCopy {
|
|||||||
DetachStoreDone = "{0} détaché. Le dossier reste sur le disque.",
|
DetachStoreDone = "{0} détaché. Le dossier reste sur le disque.",
|
||||||
MoveStoreDone = "Magasin {0} déplacé.",
|
MoveStoreDone = "Magasin {0} déplacé.",
|
||||||
Retention = "_Rétention…",
|
Retention = "_Rétention…",
|
||||||
RetentionHint = "Par dossier. 0 conserve pour toujours. Un nombre de jours supprime définitivement les messages plus anciens (pas la Corbeille).",
|
RetentionHint = "Par dossier. 0 conserve pour toujours. Les jours du dossier envoient les messages plus anciens vers la Corbeille. Les jours de la Corbeille suppriment définitivement, ou 0 pour la garder pour toujours.",
|
||||||
RetentionDays = "Jours",
|
RetentionDays = "Jours",
|
||||||
|
RetentionAction = "À l’échéance",
|
||||||
|
RetentionToTrash = "Déplacer vers la Corbeille",
|
||||||
|
RetentionPurge = "Supprimer définitivement",
|
||||||
RetentionRun = "Exécuter maintenant",
|
RetentionRun = "Exécuter maintenant",
|
||||||
LocalStore = "Magasin local",
|
LocalStore = "Magasin local",
|
||||||
LocalStoreAppData = "Données de l’app (défaut)",
|
LocalStoreAppData = "Données de l’app (défaut)",
|
||||||
|
|||||||
@ -174,6 +174,29 @@ public class MailRuleEngineTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class MailRetentionTests {
|
||||||
|
[Fact]
|
||||||
|
public void Jobs_SkipsZeroDays() {
|
||||||
|
var jobs = MailRetention.Jobs(
|
||||||
|
[
|
||||||
|
new FolderRetention { MailboxId = "box", Folder = "INBOX", Days = 0 },
|
||||||
|
new FolderRetention { MailboxId = "box", Folder = "Pratiche", Days = 30 }
|
||||||
|
]);
|
||||||
|
var job = Assert.Single(jobs);
|
||||||
|
Assert.Equal("box", job.MailboxId);
|
||||||
|
Assert.Equal("Pratiche", job.Folder);
|
||||||
|
Assert.Equal(30, job.Days);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveTrash_PrefersExistingTrashFolder() {
|
||||||
|
Assert.Equal("Cestino", MailRetention.ResolveTrash(["INBOX", "Cestino"]));
|
||||||
|
Assert.Equal(MailRetention.TrashFolder, MailRetention.ResolveTrash(["INBOX"]));
|
||||||
|
Assert.True(MailRetention.IsTrash("Deleted Items"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public class MapiRestrictionTests {
|
public class MapiRestrictionTests {
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Parse_SubjectContainsAndMarkRead() {
|
public void Parse_SubjectContainsAndMarkRead() {
|
||||||
|
|||||||
@ -430,6 +430,53 @@ public class PstMailSessionTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class LocalStoreSessionTests {
|
||||||
|
[Fact]
|
||||||
|
public async Task EmptyFolder_MovesToTrash() {
|
||||||
|
var dir = Path.Combine(Path.GetTempPath(), "postclient-store-" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
try {
|
||||||
|
var box = new MailboxAccount {
|
||||||
|
IncomingProtocol = MailProtocol.Store,
|
||||||
|
Provider = MailProvider.Store,
|
||||||
|
StorePath = dir
|
||||||
|
};
|
||||||
|
LocalStoreSidecar.Write(dir, box.Id, "Mail");
|
||||||
|
MailArchiveLayout.EnsureSystemFolders(dir);
|
||||||
|
await using var session = new LocalStoreSession();
|
||||||
|
var token = TestContext.Current.CancellationToken;
|
||||||
|
var connected = await session.ConnectAsync(box, "", token);
|
||||||
|
Assert.True(connected.IsSuccess, string.Join(" ", connected.Messages));
|
||||||
|
var appended = await session.AppendAsync("Inbox", StoreEml("keep"), token);
|
||||||
|
Assert.True(appended.IsSuccess, string.Join(" ", appended.Messages));
|
||||||
|
var listed = await session.ListMessagesAsync("Inbox", null, token);
|
||||||
|
Assert.True(listed.IsSuccess, string.Join(" ", listed.Messages));
|
||||||
|
Assert.Single(listed.Value!.Present!);
|
||||||
|
var emptied = await session.EmptyFolderAsync("Inbox", MailRetention.TrashFolder, token);
|
||||||
|
Assert.True(emptied.IsSuccess, string.Join(" ", emptied.Messages));
|
||||||
|
var inbox = await session.ListMessagesAsync("Inbox", null, token);
|
||||||
|
Assert.True(inbox.IsSuccess, string.Join(" ", inbox.Messages));
|
||||||
|
Assert.Empty(inbox.Value!.Present ?? []);
|
||||||
|
var trash = await session.ListMessagesAsync(MailRetention.TrashFolder, null, token);
|
||||||
|
Assert.True(trash.IsSuccess, string.Join(" ", trash.Messages));
|
||||||
|
Assert.Single(trash.Value!.Present ?? []);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (Directory.Exists(dir))
|
||||||
|
Directory.Delete(dir, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] StoreEml(string subject) =>
|
||||||
|
System.Text.Encoding.ASCII.GetBytes(
|
||||||
|
"From: test@example.com\r\nTo: you@example.com\r\nSubject: "
|
||||||
|
+ subject
|
||||||
|
+ "\r\nDate: Mon, 14 Sep 2026 08:00:00 +0000\r\nMessage-ID: <"
|
||||||
|
+ subject
|
||||||
|
+ "@example.com>\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nbody\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public class MailSessionGateTests {
|
public class MailSessionGateTests {
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RunAsync_DoesNotUseCallerSynchronizationContext() {
|
public async Task RunAsync_DoesNotUseCallerSynchronizationContext() {
|
||||||
|
|||||||
@ -439,7 +439,7 @@ public class LocalStoreTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Retention_ZeroKeeps_PositiveDeletesPermanently() {
|
public void Retention_ZeroKeeps_PositiveMovesToTrashThenPurgesTrash() {
|
||||||
var root = Path.Combine(Path.GetTempPath(), "postclient-ret-" + Guid.NewGuid().ToString("N"));
|
var root = Path.Combine(Path.GetTempPath(), "postclient-ret-" + Guid.NewGuid().ToString("N"));
|
||||||
Directory.CreateDirectory(root);
|
Directory.CreateDirectory(root);
|
||||||
var settings = Path.Combine(root, "settings.json");
|
var settings = Path.Combine(root, "settings.json");
|
||||||
@ -472,6 +472,15 @@ public class LocalStoreTests {
|
|||||||
"");
|
"");
|
||||||
File.WriteAllText(Path.Combine(root, "1.eml"), "old");
|
File.WriteAllText(Path.Combine(root, "1.eml"), "old");
|
||||||
var files = new ConfigurationFileService(settings);
|
var files = new ConfigurationFileService(settings);
|
||||||
|
files.Current.Mailboxes =
|
||||||
|
[
|
||||||
|
new MailboxAccount {
|
||||||
|
Id = "box",
|
||||||
|
IncomingProtocol = MailProtocol.Store,
|
||||||
|
Provider = MailProvider.Store,
|
||||||
|
StorePath = root
|
||||||
|
}
|
||||||
|
];
|
||||||
files.Current.Retention =
|
files.Current.Retention =
|
||||||
[
|
[
|
||||||
new FolderRetention { MailboxId = "box", Folder = "INBOX", Days = 0 }
|
new FolderRetention { MailboxId = "box", Folder = "INBOX", Days = 0 }
|
||||||
@ -490,7 +499,23 @@ public class LocalStoreTests {
|
|||||||
var left = catalog.Search("box", "INBOX", "IMU");
|
var left = catalog.Search("box", "INBOX", "IMU");
|
||||||
Assert.Single(left);
|
Assert.Single(left);
|
||||||
Assert.Equal(2u, left[0].Uid);
|
Assert.Equal(2u, left[0].Uid);
|
||||||
|
var trashed = catalog.Search("box", MailRetention.TrashFolder, "IMU");
|
||||||
|
Assert.Single(trashed);
|
||||||
|
Assert.Equal("Old", trashed[0].Subject);
|
||||||
Assert.False(File.Exists(Path.Combine(root, "1.eml")));
|
Assert.False(File.Exists(Path.Combine(root, "1.eml")));
|
||||||
|
Assert.True(File.Exists(MailArchiveLayout.EmlPath(
|
||||||
|
files.Current.Mailboxes[0],
|
||||||
|
files.Current.Mailboxes,
|
||||||
|
MailRetention.TrashFolder,
|
||||||
|
trashed[0].Uid)));
|
||||||
|
files.Current.Retention =
|
||||||
|
[
|
||||||
|
new FolderRetention { MailboxId = "box", Folder = MailRetention.TrashFolder, Days = 5 }
|
||||||
|
];
|
||||||
|
files.Save(files.Current);
|
||||||
|
using var purgeHost = new MailWorkerHost(catalog, files);
|
||||||
|
purgeHost.Retention(new WorkerRequest { Op = "retention" });
|
||||||
|
Assert.Empty(catalog.Search("box", MailRetention.TrashFolder, "IMU"));
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
Directory.Delete(root, recursive: true);
|
Directory.Delete(root, recursive: true);
|
||||||
|
|||||||
@ -987,26 +987,47 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
|||||||
public async Task RunRetentionAsync() {
|
public async Task RunRetentionAsync() {
|
||||||
var configuration = _files.Current;
|
var configuration = _files.Current;
|
||||||
configuration.EnsureDefaults();
|
configuration.EnsureDefaults();
|
||||||
foreach (var rule in configuration.Retention) {
|
foreach (var job in MailRetention.Jobs(configuration.Retention)) {
|
||||||
if (rule.Days <= 0 || string.IsNullOrWhiteSpace(rule.MailboxId) || string.IsNullOrWhiteSpace(rule.Folder))
|
var box = Mailboxes.FirstOrDefault(m => m.Id.Equals(job.MailboxId, StringComparison.OrdinalIgnoreCase));
|
||||||
continue;
|
|
||||||
var box = Mailboxes.FirstOrDefault(m => m.Id.Equals(rule.MailboxId, StringComparison.OrdinalIgnoreCase));
|
|
||||||
if (box is null || box.IsLocalStore)
|
if (box is null || box.IsLocalStore)
|
||||||
continue;
|
continue;
|
||||||
var cutoff = DateTimeOffset.UtcNow.AddDays(-rule.Days);
|
var known = FoldersFor(box).Select(f => f.FullName).ToList();
|
||||||
var ids = _archive.UidsOlderThan(rule.MailboxId, rule.Folder, cutoff);
|
if (known.Count == 0)
|
||||||
|
known = _archive.ListFolders(box.Id).ToList();
|
||||||
|
var folder = MailRetention.BindFolder(job.Folder, known);
|
||||||
|
if (string.IsNullOrWhiteSpace(folder))
|
||||||
|
continue;
|
||||||
|
var cutoff = DateTimeOffset.UtcNow.AddDays(-job.Days);
|
||||||
|
var ids = _archive.UidsOlderThan(box.Id, folder, cutoff);
|
||||||
if (ids.Count == 0)
|
if (ids.Count == 0)
|
||||||
continue;
|
continue;
|
||||||
var session = SessionFor(box);
|
var session = SessionFor(box);
|
||||||
if (session is { IsConnected: true })
|
if (session is not { IsConnected: true })
|
||||||
await session.SetMessageFlagsAsync(rule.Folder, ids, new MailFlagUpdate { Deleted = true });
|
continue;
|
||||||
|
if (MailRetention.IsTrash(folder)) {
|
||||||
|
await session.SetMessageFlagsAsync(folder, ids, new MailFlagUpdate { Deleted = true });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session is not { SupportsFolders: true })
|
||||||
|
continue;
|
||||||
|
var trash = MailRetention.ResolveTrash(
|
||||||
|
FoldersFor(box).Select(f => (f.Name, f.FullName)).ToList());
|
||||||
|
if (folder.Equals(trash, StringComparison.OrdinalIgnoreCase)) {
|
||||||
|
await session.SetMessageFlagsAsync(folder, ids, new MailFlagUpdate { Deleted = true });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var moved = await session.MoveMessagesAsync(folder, ids, trash, CancellationToken.None);
|
||||||
|
if (moved.IsSuccess)
|
||||||
|
DropArchived(box.Id, folder, ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await Task.Run(() => _worker.Call(new WorkerRequest { Op = "retention" }))
|
var result = await Task.Run(() => _worker.Call(new WorkerRequest { Op = "retention" }))
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
await Dispatcher.UIThread.InvokeAsync(() => {
|
await Dispatcher.UIThread.InvokeAsync(() => {
|
||||||
Status = result.Ok
|
Status = result.Ok
|
||||||
? "Retention deleted " + (result.Value ?? "0") + " messages."
|
? "Retention processed " + (result.Value ?? "0") + " messages."
|
||||||
: result.Error ?? "Retention failed.";
|
: result.Error ?? "Retention failed.";
|
||||||
});
|
});
|
||||||
if (SelectedFolder is not null)
|
if (SelectedFolder is not null)
|
||||||
@ -1598,20 +1619,22 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
|||||||
var rows = TargetRows();
|
var rows = TargetRows();
|
||||||
if (rows.Count == 0)
|
if (rows.Count == 0)
|
||||||
return;
|
return;
|
||||||
var trash = Folders.Concat(FoldersFor(SelectedMailbox))
|
var box = SelectedMailbox;
|
||||||
.FirstOrDefault(f => MailFolderRole.Kind(f.Name, f.FullName) == "trash");
|
var folder = SelectedFolder;
|
||||||
if (trash is not null
|
if (box is null || folder is null)
|
||||||
&& SelectedFolder is not null
|
|
||||||
&& !trash.FullName.Equals(SelectedFolder.FullName, StringComparison.OrdinalIgnoreCase)) {
|
|
||||||
await MoveRowsAsync(rows, trash.FullName);
|
|
||||||
return;
|
return;
|
||||||
|
if (!MailRetention.IsTrash(folder.Name, folder.FullName)) {
|
||||||
|
var trash = await EnsureTrashFolderAsync(box, CancellationToken.None);
|
||||||
|
if (!string.IsNullOrWhiteSpace(trash)
|
||||||
|
&& !trash.Equals(folder.FullName, StringComparison.OrdinalIgnoreCase)) {
|
||||||
|
await MoveRowsAsync(rows, trash);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await ApplyFlagsAsync(new MailFlagUpdate { Deleted = true }, null, rows))
|
if (!await ApplyFlagsAsync(new MailFlagUpdate { Deleted = true }, null, rows))
|
||||||
return;
|
return;
|
||||||
if (SelectedMailbox is null || SelectedFolder is null)
|
DropArchived(box.Id, folder.FullName, TargetIds(rows));
|
||||||
return;
|
|
||||||
DropArchived(SelectedMailbox.Id, SelectedFolder.FullName, TargetIds(rows));
|
|
||||||
RemoveRows(rows);
|
RemoveRows(rows);
|
||||||
Status = "Deleted.";
|
Status = "Deleted.";
|
||||||
}
|
}
|
||||||
@ -1647,17 +1670,16 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
|||||||
var folder = SelectedFolder;
|
var folder = SelectedFolder;
|
||||||
if (session is not { IsConnected: true, SupportsFolders: true } || box is null || folder is null)
|
if (session is not { IsConnected: true, SupportsFolders: true } || box is null || folder is null)
|
||||||
return;
|
return;
|
||||||
var trash = Folders.Concat(FoldersFor(SelectedMailbox))
|
var trash = await EnsureTrashFolderAsync(box, CancellationToken.None);
|
||||||
.FirstOrDefault(f => MailFolderRole.Kind(f.Name, f.FullName) == "trash");
|
var purge = string.IsNullOrWhiteSpace(trash)
|
||||||
var purge = trash is null
|
|| trash.Equals(folder.FullName, StringComparison.OrdinalIgnoreCase);
|
||||||
|| trash.FullName.Equals(folder.FullName, StringComparison.OrdinalIgnoreCase);
|
|
||||||
var confirm = string.Format(purge ? Copy.EmptyTrashConfirm : Copy.EmptyFolderConfirm, folder.Name);
|
var confirm = string.Format(purge ? Copy.EmptyTrashConfirm : Copy.EmptyFolderConfirm, folder.Name);
|
||||||
if (!await ConfirmAsync(Copy.EmptyFolder, confirm))
|
if (!await ConfirmAsync(Copy.EmptyFolder, confirm))
|
||||||
return;
|
return;
|
||||||
await RunAsync(Copy.EmptyingFolder, async token => {
|
await RunAsync(Copy.EmptyingFolder, async token => {
|
||||||
var result = await session.EmptyFolderAsync(
|
var result = await session.EmptyFolderAsync(
|
||||||
folder.FullName,
|
folder.FullName,
|
||||||
purge ? null : trash!.FullName,
|
purge ? null : trash,
|
||||||
token);
|
token);
|
||||||
if (!result.IsSuccess) {
|
if (!result.IsSuccess) {
|
||||||
Status = string.Join(" ", result.Messages);
|
Status = string.Join(" ", result.Messages);
|
||||||
@ -2687,6 +2709,30 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
|||||||
return _foldersByMailbox.TryGetValue(box.Id, out var rows) ? rows : [];
|
return _foldersByMailbox.TryGetValue(box.Id, out var rows) ? rows : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private FolderRowViewModel? TrashFolderOf(MailboxAccount? box) {
|
||||||
|
if (box is null)
|
||||||
|
return null;
|
||||||
|
return FoldersFor(box)
|
||||||
|
.Concat(Folders)
|
||||||
|
.FirstOrDefault(f => MailRetention.IsTrash(f.Name, f.FullName));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string?> EnsureTrashFolderAsync(MailboxAccount box, CancellationToken token) {
|
||||||
|
var existing = TrashFolderOf(box);
|
||||||
|
if (existing is not null)
|
||||||
|
return existing.FullName;
|
||||||
|
var session = SessionFor(box);
|
||||||
|
if (session is not { IsConnected: true, SupportsFolders: true })
|
||||||
|
return null;
|
||||||
|
var created = await session.CreateFolderAsync(MailRetention.TrashFolder, null, token);
|
||||||
|
if (!created.IsSuccess)
|
||||||
|
created = await session.CreateFolderAsync("Trash", null, token);
|
||||||
|
if (created.IsSuccess)
|
||||||
|
await LoadFoldersAsync(box, session, token);
|
||||||
|
return TrashFolderOf(box)?.FullName
|
||||||
|
?? (created.IsSuccess ? MailRetention.TrashFolder : null);
|
||||||
|
}
|
||||||
|
|
||||||
private IReadOnlyList<MailRule> ActiveRules() =>
|
private IReadOnlyList<MailRule> ActiveRules() =>
|
||||||
_files.Current.Rules ?? [];
|
_files.Current.Rules ?? [];
|
||||||
|
|
||||||
@ -2794,7 +2840,7 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
|||||||
continue;
|
continue;
|
||||||
touched.Add(header.Id);
|
touched.Add(header.Id);
|
||||||
if (rule.Action == MailRuleAction.Delete) {
|
if (rule.Action == MailRuleAction.Delete) {
|
||||||
var trash = MailRuleEngine.ResolveFolder("Trash", folders);
|
var trash = MailRetention.ResolveTrash(folders);
|
||||||
if (!string.IsNullOrWhiteSpace(trash))
|
if (!string.IsNullOrWhiteSpace(trash))
|
||||||
AddUid(moves, trash, header.Id);
|
AddUid(moves, trash, header.Id);
|
||||||
}
|
}
|
||||||
@ -2908,7 +2954,7 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
|||||||
return;
|
return;
|
||||||
var unique = copied.Distinct().ToList();
|
var unique = copied.Distinct().ToList();
|
||||||
if (sourceSession is { IsConnected: true, SupportsFolders: true }) {
|
if (sourceSession is { IsConnected: true, SupportsFolders: true }) {
|
||||||
var trash = MailRuleEngine.ResolveFolder("Trash", sourceFolders);
|
var trash = MailRetention.ResolveTrash(sourceFolders);
|
||||||
if (!string.IsNullOrWhiteSpace(trash) && !trash.Equals(sourceFolder, StringComparison.OrdinalIgnoreCase))
|
if (!string.IsNullOrWhiteSpace(trash) && !trash.Equals(sourceFolder, StringComparison.OrdinalIgnoreCase))
|
||||||
await sourceSession.MoveMessagesAsync(sourceFolder, unique, trash, token);
|
await sourceSession.MoveMessagesAsync(sourceFolder, unique, trash, token);
|
||||||
else
|
else
|
||||||
@ -4107,11 +4153,11 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
|||||||
if (ids.Count == 0)
|
if (ids.Count == 0)
|
||||||
return;
|
return;
|
||||||
if (session is { IsConnected: true, SupportsFolders: true }) {
|
if (session is { IsConnected: true, SupportsFolders: true }) {
|
||||||
var trash = FoldersFor(source)
|
var trash = MailRetention.ResolveTrash(
|
||||||
.FirstOrDefault(f => MailFolderRole.Kind(f.Name, f.FullName) == "trash");
|
FoldersFor(source).Select(f => (f.Name, f.FullName)).ToList());
|
||||||
if (trash is not null
|
if (!string.IsNullOrWhiteSpace(trash)
|
||||||
&& !trash.FullName.Equals(sourceFolder, StringComparison.OrdinalIgnoreCase))
|
&& !trash.Equals(sourceFolder, StringComparison.OrdinalIgnoreCase))
|
||||||
await session.MoveMessagesAsync(sourceFolder, ids, trash.FullName, token);
|
await session.MoveMessagesAsync(sourceFolder, ids, trash, token);
|
||||||
else
|
else
|
||||||
await session.SetMessageFlagsAsync(sourceFolder, ids, new MailFlagUpdate { Deleted = true }, token);
|
await session.SetMessageFlagsAsync(sourceFolder, ids, new MailFlagUpdate { Deleted = true }, token);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,11 @@ public sealed class RetentionRowViewModel : ObservableObject {
|
|||||||
|
|
||||||
public required string Folder { get; init; }
|
public required string Folder { get; init; }
|
||||||
|
|
||||||
|
public required bool IsTrash { get; init; }
|
||||||
|
|
||||||
|
public string ActionLabel =>
|
||||||
|
IsTrash ? UiLocale.Copy.RetentionPurge : UiLocale.Copy.RetentionToTrash;
|
||||||
|
|
||||||
private int _days;
|
private int _days;
|
||||||
|
|
||||||
public int Days {
|
public int Days {
|
||||||
@ -38,8 +43,10 @@ public sealed partial class RetentionViewModel : ObservableObject {
|
|||||||
foreach (var box in mailboxes) {
|
foreach (var box in mailboxes) {
|
||||||
var listed = folders(box.Id);
|
var listed = folders(box.Id);
|
||||||
var names = listed.Count > 0
|
var names = listed.Count > 0
|
||||||
? listed.Select(f => f.FullName)
|
? listed.Select(f => f.FullName).ToList()
|
||||||
: MailArchiveLayout.SystemFolders;
|
: MailArchiveLayout.SystemFolders.ToList();
|
||||||
|
if (!names.Any(folder => MailRetention.IsTrash(folder)))
|
||||||
|
names.Add(MailRetention.TrashFolder);
|
||||||
foreach (var folder in names.Distinct(StringComparer.OrdinalIgnoreCase)) {
|
foreach (var folder in names.Distinct(StringComparer.OrdinalIgnoreCase)) {
|
||||||
var rule = saved.FirstOrDefault(r =>
|
var rule = saved.FirstOrDefault(r =>
|
||||||
r.MailboxId.Equals(box.Id, StringComparison.OrdinalIgnoreCase)
|
r.MailboxId.Equals(box.Id, StringComparison.OrdinalIgnoreCase)
|
||||||
@ -48,6 +55,7 @@ public sealed partial class RetentionViewModel : ObservableObject {
|
|||||||
MailboxId = box.Id,
|
MailboxId = box.Id,
|
||||||
MailboxLabel = box.Label,
|
MailboxLabel = box.Label,
|
||||||
Folder = folder,
|
Folder = folder,
|
||||||
|
IsTrash = MailRetention.IsTrash(folder),
|
||||||
Days = rule?.Days ?? 0
|
Days = rule?.Days ?? 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,6 +41,10 @@
|
|||||||
Binding="{Binding Folder}"
|
Binding="{Binding Folder}"
|
||||||
IsReadOnly="True"
|
IsReadOnly="True"
|
||||||
Width="*" />
|
Width="*" />
|
||||||
|
<DataGridTextColumn Header="{Binding Copy.RetentionAction, ElementName=Root}"
|
||||||
|
Binding="{Binding ActionLabel}"
|
||||||
|
IsReadOnly="True"
|
||||||
|
Width="140" />
|
||||||
<DataGridTextColumn Header="{Binding Copy.RetentionDays, ElementName=Root}"
|
<DataGridTextColumn Header="{Binding Copy.RetentionDays, ElementName=Root}"
|
||||||
Binding="{Binding Days}"
|
Binding="{Binding Days}"
|
||||||
Width="90" />
|
Width="90" />
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user