diff --git a/Wino.Core.Domain/Enums/MailOperation.cs b/Wino.Core.Domain/Enums/MailOperation.cs
index 3e6f285f..9d482958 100644
--- a/Wino.Core.Domain/Enums/MailOperation.cs
+++ b/Wino.Core.Domain/Enums/MailOperation.cs
@@ -19,6 +19,8 @@ public enum FolderSynchronizerOperation
RenameFolder,
EmptyFolder,
MarkFolderRead,
+ DeleteFolder,
+ CreateSubFolder,
}
public enum CalendarSynchronizerOperation
diff --git a/Wino.Core.Domain/Enums/WinoPage.cs b/Wino.Core.Domain/Enums/WinoPage.cs
index 3e34cb73..56bc5ce3 100644
--- a/Wino.Core.Domain/Enums/WinoPage.cs
+++ b/Wino.Core.Domain/Enums/WinoPage.cs
@@ -32,5 +32,6 @@ public enum WinoPage
CalendarSettingsPage,
CalendarAccountSettingsPage,
EventDetailsPage,
- SignatureAndEncryptionPage
+ SignatureAndEncryptionPage,
+ StoragePage
}
diff --git a/Wino.Core.Domain/Interfaces/IMailService.cs b/Wino.Core.Domain/Interfaces/IMailService.cs
index 317edbc0..ced42c2e 100644
--- a/Wino.Core.Domain/Interfaces/IMailService.cs
+++ b/Wino.Core.Domain/Interfaces/IMailService.cs
@@ -170,4 +170,9 @@ public interface IMailService
/// Folder ID.
/// Number of recent mails to return.
Task> GetRecentMailIdsForFolderAsync(Guid folderId, int count);
+
+ ///
+ /// Returns all mail copies for the account created before the given UTC date.
+ ///
+ Task> GetMailCopiesBeforeDateAsync(Guid accountId, DateTime cutoffDateUtc);
}
diff --git a/Wino.Core.Domain/Interfaces/IMimeStorageService.cs b/Wino.Core.Domain/Interfaces/IMimeStorageService.cs
new file mode 100644
index 00000000..9c67ebea
--- /dev/null
+++ b/Wino.Core.Domain/Interfaces/IMimeStorageService.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Wino.Core.Domain.Interfaces;
+
+public interface IMimeStorageService
+{
+ Task GetMimeRootPathAsync();
+ Task> GetAccountsMimeStorageSizesAsync(IEnumerable accountIds);
+ Task DeleteAccountMimeStorageAsync(Guid accountId);
+ Task DeleteAccountMimeStorageOlderThanAsync(Guid accountId, DateTime cutoffDateUtc);
+}
diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json
index 4ba0ed11..b93d0b56 100644
--- a/Wino.Core.Domain/Translations/en_US/resources.json
+++ b/Wino.Core.Domain/Translations/en_US/resources.json
@@ -739,6 +739,33 @@
"SettingsStartupItem_Title": "Startup Item",
"SettingsStore_Description": "Show some love ❤️",
"SettingsStore_Title": "Rate in Store",
+ "SettingsStorage_Title": "Storage",
+ "SettingsStorage_Description": "Scan and manage MIME cache stored in your local data folder.",
+ "SettingsStorage_ScanFolder": "Scan local data folder",
+ "SettingsStorage_NoLocalMimeDataFound": "No local MIME data found.",
+ "SettingsStorage_NoAccountsFound": "No accounts found.",
+ "SettingsStorage_TotalUsage": "Total local MIME usage: {0}",
+ "SettingsStorage_AccountUsageDescription": "{0} used in local MIME cache",
+ "SettingsStorage_DeleteAll_Title": "Delete all MIME content",
+ "SettingsStorage_DeleteAll_Description": "Delete this account's entire MIME cache folder.",
+ "SettingsStorage_DeleteAll_Button": "Delete all",
+ "SettingsStorage_DeleteAll_Confirm_Title": "Delete all MIME content",
+ "SettingsStorage_DeleteAll_Confirm_Message": "Delete all local MIME data for {0}?",
+ "SettingsStorage_DeleteAll_Success": "All MIME content was deleted.",
+ "SettingsStorage_DeleteOld_Title": "Delete old MIME content",
+ "SettingsStorage_DeleteOld_Description": "Delete MIME files based on mail creation date in local database.",
+ "SettingsStorage_DeleteOld_1Month": "> 1 month",
+ "SettingsStorage_DeleteOld_3Months": "> 3 months",
+ "SettingsStorage_DeleteOld_6Months": "> 6 months",
+ "SettingsStorage_DeleteOld_1Year": "> 1 year",
+ "SettingsStorage_DeleteOld_Confirm_Title": "Delete old MIME content",
+ "SettingsStorage_DeleteOld_Confirm_Message": "Delete local MIME data older than {0} for {1}?",
+ "SettingsStorage_DeleteOld_Success": "Deleted {0} MIME folder(s) older than {1}.",
+ "SettingsStorage_1Month": "1 month",
+ "SettingsStorage_3Months": "3 months",
+ "SettingsStorage_6Months": "6 months",
+ "SettingsStorage_1Year": "1 year",
+ "SettingsStorage_Months": "{0} months",
"SettingsTaskbarBadge_Description": "Include unread mail count in taskbar icon.",
"SettingsTaskbarBadge_Title": "Taskbar Badge",
"SettingsThreads_Description": "Organize messages into conversation threads.",
diff --git a/Wino.Core.ViewModels/SettingOptionsPageViewModel.cs b/Wino.Core.ViewModels/SettingOptionsPageViewModel.cs
index 97cdf782..df4b5ce4 100644
--- a/Wino.Core.ViewModels/SettingOptionsPageViewModel.cs
+++ b/Wino.Core.ViewModels/SettingOptionsPageViewModel.cs
@@ -34,6 +34,7 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel
WinoPage.CalendarSettingsPage => Translator.SettingsCalendarSettings_Title,
WinoPage.SignatureAndEncryptionPage => Translator.SettingsSignatureAndEncryption_Title,
WinoPage.KeyboardShortcutsPage => Translator.Settings_KeyboardShortcuts_Title,
+ WinoPage.StoragePage => Translator.SettingsStorage_Title,
_ => throw new NotImplementedException()
};
diff --git a/Wino.Core/Helpers/SynchronizationActionHelper.cs b/Wino.Core/Helpers/SynchronizationActionHelper.cs
index 87985236..e431b512 100644
--- a/Wino.Core/Helpers/SynchronizationActionHelper.cs
+++ b/Wino.Core/Helpers/SynchronizationActionHelper.cs
@@ -102,6 +102,8 @@ public static class SynchronizationActionHelper
RenameFolderRequest => Translator.SyncAction_RenamingFolder,
EmptyFolderRequest => Translator.SyncAction_EmptyingFolder,
MarkFolderAsReadRequest => Translator.SyncAction_MarkingFolderAsRead,
+ DeleteFolderRequest => Translator.FolderOperation_Delete,
+ CreateSubFolderRequest => Translator.FolderOperation_CreateSubFolder,
_ => null
};
}
diff --git a/Wino.Core/Requests/Folder/CreateSubFolderRequest.cs b/Wino.Core/Requests/Folder/CreateSubFolderRequest.cs
new file mode 100644
index 00000000..a655cf49
--- /dev/null
+++ b/Wino.Core/Requests/Folder/CreateSubFolderRequest.cs
@@ -0,0 +1,11 @@
+using Wino.Core.Domain.Entities.Mail;
+using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Models.Requests;
+
+namespace Wino.Core.Requests.Folder;
+
+public record CreateSubFolderRequest(MailItemFolder Folder, string NewFolderName) : FolderRequestBase(Folder, FolderSynchronizerOperation.CreateSubFolder)
+{
+ public override void ApplyUIChanges() { }
+ public override void RevertUIChanges() { }
+}
diff --git a/Wino.Core/Requests/Folder/DeleteFolderRequest.cs b/Wino.Core/Requests/Folder/DeleteFolderRequest.cs
new file mode 100644
index 00000000..d865bee1
--- /dev/null
+++ b/Wino.Core/Requests/Folder/DeleteFolderRequest.cs
@@ -0,0 +1,11 @@
+using Wino.Core.Domain.Entities.Mail;
+using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Models.Requests;
+
+namespace Wino.Core.Requests.Folder;
+
+public record DeleteFolderRequest(MailItemFolder Folder) : FolderRequestBase(Folder, FolderSynchronizerOperation.DeleteFolder)
+{
+ public override void ApplyUIChanges() { }
+ public override void RevertUIChanges() { }
+}
diff --git a/Wino.Core/Services/WinoRequestDelegator.cs b/Wino.Core/Services/WinoRequestDelegator.cs
index 35ca5edc..4881383d 100644
--- a/Wino.Core/Services/WinoRequestDelegator.cs
+++ b/Wino.Core/Services/WinoRequestDelegator.cs
@@ -130,6 +130,11 @@ public class WinoRequestDelegator : IWinoRequestDelegator
await QueueRequestAsync(request, accountId);
await SendSyncActionsAddedAsync([request], accountId);
await QueueSynchronizationAsync(accountId);
+
+ if (folderRequest.Action is FolderOperation.Delete or FolderOperation.CreateSubFolder)
+ {
+ await QueueFoldersOnlySynchronizationAsync(accountId);
+ }
}
public async Task ExecuteAsync(DraftPreparationRequest draftPreperationRequest)
@@ -203,6 +208,18 @@ public class WinoRequestDelegator : IWinoRequestDelegator
return Task.CompletedTask;
}
+ private Task QueueFoldersOnlySynchronizationAsync(Guid accountId)
+ {
+ var options = new MailSynchronizationOptions()
+ {
+ AccountId = accountId,
+ Type = MailSynchronizationType.FoldersOnly
+ };
+
+ WeakReferenceMessenger.Default.Send(new NewMailSynchronizationRequested(options));
+ return Task.CompletedTask;
+ }
+
private async Task SendSyncActionsAddedAsync(IEnumerable requests, Guid accountId, string accountName = null)
{
if (accountName == null)
diff --git a/Wino.Core/Services/WinoRequestProcessor.cs b/Wino.Core/Services/WinoRequestProcessor.cs
index 04f1fc17..87af29a0 100644
--- a/Wino.Core/Services/WinoRequestProcessor.cs
+++ b/Wino.Core/Services/WinoRequestProcessor.cs
@@ -255,15 +255,29 @@ public class WinoRequestProcessor : IWinoRequestProcessor
change = new MarkFolderAsReadRequest(folder, unreadItems);
break;
- //case FolderOperation.Delete:
- // var isConfirmed = await _dialogService.ShowConfirmationDialogAsync($"'{folderStructure.FolderName}' is going to be deleted. Do you want to continue?", "Are you sure?", "Yes delete.");
+ case FolderOperation.Delete:
+ var deleteQuestion = string.Format(Translator.DialogMessage_DeleteAccountConfirmationMessage, folder.FolderName);
+ var shouldDelete = await _dialogService.ShowConfirmationDialogAsync(deleteQuestion, Translator.FolderOperation_Delete, Translator.FolderOperation_Delete);
- // if (isConfirmed)
- // change = new DeleteFolderRequest(accountId, folderStructure.RemoteFolderId, folderStructure.FolderId);
+ if (shouldDelete)
+ {
+ change = new DeleteFolderRequest(folder);
+ }
- // break;
- //default:
- // throw new NotImplementedException();
+ break;
+ case FolderOperation.CreateSubFolder:
+ var subFolderName = await _dialogService.ShowTextInputDialogAsync(
+ string.Empty,
+ Translator.FolderOperation_CreateSubFolder,
+ Translator.DialogMessage_RenameFolderMessage,
+ Translator.FolderOperation_CreateSubFolder);
+
+ if (!string.IsNullOrWhiteSpace(subFolderName))
+ {
+ change = new CreateSubFolderRequest(folder, subFolderName.Trim());
+ }
+
+ break;
}
return change;
diff --git a/Wino.Core/Synchronizers/GmailSynchronizer.cs b/Wino.Core/Synchronizers/GmailSynchronizer.cs
index 43bd7938..26eadbd4 100644
--- a/Wino.Core/Synchronizers/GmailSynchronizer.cs
+++ b/Wino.Core/Synchronizers/GmailSynchronizer.cs
@@ -1253,40 +1253,16 @@ public class GmailSynchronizer : WinoSynchronizer> MarkFolderAsRead(MarkFolderAsReadRequest request)
=> MarkRead(new BatchMarkReadRequest(request.MailsToMarkRead.Select(a => new MarkReadRequest(a, true))));
+ public override List> DeleteFolder(DeleteFolderRequest request)
+ {
+ var networkCall = _gmailService.Users.Labels.Delete("me", request.Folder.RemoteFolderId);
+ return [new HttpRequestBundle(networkCall, request, request)];
+ }
+
+ public override List> CreateSubFolder(CreateSubFolderRequest request)
+ {
+ var parentLabelName = request.Folder.FolderName;
+
+ try
+ {
+ var parentLabel = _gmailService.Users.Labels.Get("me", request.Folder.RemoteFolderId).Execute();
+ if (!string.IsNullOrWhiteSpace(parentLabel?.Name))
+ {
+ parentLabelName = parentLabel.Name;
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Warning(ex, "Failed to resolve full parent label name for {FolderId}. Falling back to local folder name.", request.Folder.RemoteFolderId);
+ }
+
+ var label = new Label()
+ {
+ Name = $"{parentLabelName}/{request.NewFolderName}"
+ };
+
+ var networkCall = _gmailService.Users.Labels.Create(label, "me");
+ return [new HttpRequestBundle(networkCall, request, request)];
+ }
+
#endregion
#region Request Execution
@@ -1834,8 +1842,7 @@ public class GmailSynchronizer : WinoSynchronizer
/// Creates new mail packages for the given message.
/// AssignedFolder is null since the LabelId is parsed out of the Message.
- /// NOTE: This method does NOT download MIME content during synchronization.
- /// MIME is only downloaded when user explicitly reads the message.
+ /// If Gmail Message includes Raw payload, MIME is parsed and attached to packages.
///
/// Gmail message to create package for (must have Metadata format).
/// Null, not used.
@@ -1846,24 +1853,74 @@ public class GmailSynchronizer : WinoSynchronizer();
+ MimeMessage mimeMessage = null;
+
+ // Raw format is used in delta sync and does not populate Payload.Headers.
+ // Parse MIME from Raw so we can resolve draft mapping header and persist mime content.
+ if (!string.IsNullOrEmpty(message?.Raw))
+ {
+ try
+ {
+ mimeMessage = message.GetGmailMimeMessage();
+ }
+ catch (Exception ex)
+ {
+ _logger.Warning(ex, "Failed to parse MIME from raw Gmail message {MessageId}", message?.Id);
+ }
+ }
// Create base MailCopy from metadata only - NO MIME download
var baseMailCopy = await CreateMinimalMailCopyAsync(message, assignedFolder, cancellationToken);
- // Check for local draft mapping using X-Wino-Draft-Id header from metadata.
- // If this is a Wino-created draft, the local copy was already mapped by the CreateDraft response handler
- // with the correct Gmail Draft ID. We must NOT call MapLocalDraftAsync here because
- // baseMailCopy.DraftId is derived from CreateMinimalMailCopyAsync (not the real Draft resource ID),
- // which would overwrite the correctly mapped DraftId and break SendDraft.
+ if (mimeMessage != null)
+ {
+ // Raw responses don't include metadata headers. Backfill important fields from MIME.
+ EnrichMailCopyFromMime(baseMailCopy, mimeMessage);
+ }
+
+ // Check for local draft mapping using X-Wino-Draft-Id header.
+ // For Metadata format we read from Payload.Headers.
+ // For Raw format (Payload is null), we read from parsed MIME headers.
if (baseMailCopy.IsDraft)
{
- var draftIdHeader = message.Payload?.Headers?.FirstOrDefault(h => h.Name.Equals(Domain.Constants.WinoLocalDraftHeader, StringComparison.OrdinalIgnoreCase))?.Value;
+ var draftIdHeader = message.Payload?.Headers?.FirstOrDefault(h => h.Name.Equals(Domain.Constants.WinoLocalDraftHeader, StringComparison.OrdinalIgnoreCase))?.Value
+ ?? mimeMessage?.Headers?.FirstOrDefault(h => h.Field.Equals(Domain.Constants.WinoLocalDraftHeader, StringComparison.OrdinalIgnoreCase))?.Value;
if (!string.IsNullOrEmpty(draftIdHeader) && Guid.TryParse(draftIdHeader, out _))
{
- // This message belongs to an existing local draft copy.
- // Skip creating a new mail copy - the local copy was already mapped by the response handler.
- return null;
+ if (Guid.TryParse(draftIdHeader, out Guid localDraftCopyUniqueId))
+ {
+ // This message belongs to existing local draft copy.
+ // Map remote ids to local copy and skip creating duplicate rows.
+ bool isMappingSuccessful = await _gmailChangeProcessor.MapLocalDraftAsync(
+ Account.Id,
+ localDraftCopyUniqueId,
+ baseMailCopy.Id,
+ baseMailCopy.DraftId,
+ baseMailCopy.ThreadId).ConfigureAwait(false);
+
+ if (isMappingSuccessful)
+ {
+ // Keep local draft MIME in sync with the fetched remote raw MIME if available.
+ if (mimeMessage != null)
+ {
+ var mappedDraftCopies = await _gmailChangeProcessor.GetMailCopiesAsync([baseMailCopy.Id]).ConfigureAwait(false);
+ if (mappedDraftCopies != null)
+ {
+ var savedFileIds = new HashSet();
+ foreach (var mappedCopy in mappedDraftCopies)
+ {
+ if (mappedCopy.FileId == Guid.Empty || !savedFileIds.Add(mappedCopy.FileId))
+ continue;
+
+ await _gmailChangeProcessor.SaveMimeFileAsync(mappedCopy.FileId, mimeMessage, Account.Id).ConfigureAwait(false);
+ }
+ }
+ }
+
+ return null;
+ }
+ }
}
}
@@ -1887,12 +1944,16 @@ public class GmailSynchronizer : WinoSynchronizer> DeleteFolder(DeleteFolderRequest request)
+ {
+ return CreateSingleTaskBundle(async (client, item) =>
+ {
+ var folder = await client.GetFolderAsync(request.Folder.RemoteFolderId).ConfigureAwait(false);
+ await folder.DeleteAsync().ConfigureAwait(false);
+ }, request, request);
+ }
+
+ public override List> CreateSubFolder(CreateSubFolderRequest request)
+ {
+ return CreateSingleTaskBundle(async (client, item) =>
+ {
+ var parentFolder = await client.GetFolderAsync(request.Folder.RemoteFolderId).ConfigureAwait(false);
+ await parentFolder.CreateAsync(request.NewFolderName, true).ConfigureAwait(false);
+ }, request, request);
+ }
+
#endregion
public override async Task> CreateNewMailPackagesAsync(ImapMessageCreationPackage message, MailItemFolder assignedFolder, CancellationToken cancellationToken = default)
diff --git a/Wino.Core/Synchronizers/OutlookSynchronizer.cs b/Wino.Core/Synchronizers/OutlookSynchronizer.cs
index c1b73098..aad1bef1 100644
--- a/Wino.Core/Synchronizers/OutlookSynchronizer.cs
+++ b/Wino.Core/Synchronizers/OutlookSynchronizer.cs
@@ -1507,6 +1507,23 @@ public class OutlookSynchronizer : WinoSynchronizer> MarkFolderAsRead(MarkFolderAsReadRequest request)
=> MarkRead(new BatchMarkReadRequest(request.MailsToMarkRead.Select(a => new MarkReadRequest(a, true))));
+ public override List> DeleteFolder(DeleteFolderRequest request)
+ {
+ var networkCall = _graphClient.Me.MailFolders[request.Folder.RemoteFolderId].ToDeleteRequestInformation();
+ return [new HttpRequestBundle(networkCall, request)];
+ }
+
+ public override List> CreateSubFolder(CreateSubFolderRequest request)
+ {
+ var requestBody = new MailFolder
+ {
+ DisplayName = request.NewFolderName
+ };
+
+ var networkCall = _graphClient.Me.MailFolders[request.Folder.RemoteFolderId].ChildFolders.ToPostRequestInformation(requestBody);
+ return [new HttpRequestBundle(networkCall, request)];
+ }
+
#endregion
public override async Task ExecuteNativeRequestsAsync(List> batchedRequests, CancellationToken cancellationToken = default)
diff --git a/Wino.Core/Synchronizers/WinoSynchronizer.cs b/Wino.Core/Synchronizers/WinoSynchronizer.cs
index ecd96502..17a4a7d5 100644
--- a/Wino.Core/Synchronizers/WinoSynchronizer.cs
+++ b/Wino.Core/Synchronizers/WinoSynchronizer.cs
@@ -203,6 +203,12 @@ public abstract class WinoSynchronizer> RenameFolder(RenameFolderRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public virtual List> EmptyFolder(EmptyFolderRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public virtual List> MarkFolderAsRead(MarkFolderAsReadRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
+ public virtual List> DeleteFolder(DeleteFolderRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
+ public virtual List> CreateSubFolder(CreateSubFolderRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
#endregion
diff --git a/Wino.Mail.ViewModels/Data/AccountStorageItemViewModel.cs b/Wino.Mail.ViewModels/Data/AccountStorageItemViewModel.cs
new file mode 100644
index 00000000..b82046a1
--- /dev/null
+++ b/Wino.Mail.ViewModels/Data/AccountStorageItemViewModel.cs
@@ -0,0 +1,40 @@
+using System.Windows.Input;
+using CommunityToolkit.Mvvm.ComponentModel;
+using Wino.Core.Domain.Entities.Shared;
+using Wino.Core.Extensions;
+
+namespace Wino.Mail.ViewModels.Data;
+
+public partial class AccountStorageItemViewModel(MailAccount account, long sizeBytes, ICommand deleteAllCommand, ICommand deleteOneMonthCommand, ICommand deleteThreeMonthsCommand, ICommand deleteSixMonthsCommand, ICommand deleteYearCommand) : ObservableObject
+{
+ public MailAccount Account { get; } = account;
+
+ [ObservableProperty]
+ public partial bool IsBusy { get; set; }
+
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(SizeText))]
+ public partial long SizeBytes { get; set; } = sizeBytes;
+
+ [ObservableProperty]
+ public partial string SizeDescription { get; set; } = string.Empty;
+
+ [ObservableProperty]
+ public partial ICommand DeleteAllCommand { get; set; } = deleteAllCommand;
+
+ [ObservableProperty]
+ public partial ICommand DeleteOneMonthCommand { get; set; } = deleteOneMonthCommand;
+
+ [ObservableProperty]
+ public partial ICommand DeleteThreeMonthsCommand { get; set; } = deleteThreeMonthsCommand;
+
+ [ObservableProperty]
+ public partial ICommand DeleteSixMonthsCommand { get; set; } = deleteSixMonthsCommand;
+
+ [ObservableProperty]
+ public partial ICommand DeleteYearCommand { get; set; } = deleteYearCommand;
+
+ public string AccountName => string.IsNullOrWhiteSpace(Account.Name) ? Account.Address ?? string.Empty : Account.Name;
+ public string AccountAddress => Account.Address ?? string.Empty;
+ public string SizeText => SizeBytes.GetBytesReadable();
+}
diff --git a/Wino.Mail.ViewModels/MailListPageViewModel.cs b/Wino.Mail.ViewModels/MailListPageViewModel.cs
index 38597738..2e37769e 100644
--- a/Wino.Mail.ViewModels/MailListPageViewModel.cs
+++ b/Wino.Mail.ViewModels/MailListPageViewModel.cs
@@ -1074,6 +1074,9 @@ public partial class MailListPageViewModel : MailBaseViewModel,
case SynchronizationCompletedState.Success:
UpdateBarMessage(InfoBarMessageType.Success, ActiveFolder.FolderName, Translator.SynchronizationFolderReport_Success);
break;
+ case SynchronizationCompletedState.PartiallyCompleted:
+ UpdateBarMessage(InfoBarMessageType.Warning, ActiveFolder.FolderName, Translator.SynchronizationFolderReport_Failed);
+ break;
case SynchronizationCompletedState.Failed:
UpdateBarMessage(InfoBarMessageType.Error, ActiveFolder.FolderName, Translator.SynchronizationFolderReport_Failed);
break;
diff --git a/Wino.Mail.ViewModels/StoragePageViewModel.cs b/Wino.Mail.ViewModels/StoragePageViewModel.cs
new file mode 100644
index 00000000..58226a7c
--- /dev/null
+++ b/Wino.Mail.ViewModels/StoragePageViewModel.cs
@@ -0,0 +1,237 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Serilog;
+using Wino.Core.Domain;
+using Wino.Core.Domain.Interfaces;
+using Wino.Core.Domain.Models.Navigation;
+using Wino.Core.Extensions;
+using Wino.Mail.ViewModels.Data;
+
+namespace Wino.Mail.ViewModels;
+
+public partial class StoragePageViewModel(
+ IAccountService accountService,
+ IMimeStorageService mimeStorageService,
+ IMailDialogService dialogService) : MailBaseViewModel
+{
+ private readonly ILogger _logger = Log.ForContext();
+ private readonly IAccountService _accountService = accountService;
+ private readonly IMimeStorageService _mimeStorageService = mimeStorageService;
+ private readonly IMailDialogService _dialogService = dialogService;
+
+ public ObservableCollection AccountStorageItems { get; } = [];
+
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(IsBusy))]
+ public partial bool IsLoading { get; set; }
+
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(IsBusy))]
+ public partial bool IsCleaning { get; set; }
+
+ [ObservableProperty]
+ public partial string MimeRootPath { get; set; } = string.Empty;
+
+ [ObservableProperty]
+ public partial string SummaryText { get; set; } = "";
+
+ public bool IsBusy => IsLoading || IsCleaning;
+
+ public override async void OnNavigatedTo(NavigationMode mode, object parameters)
+ {
+ base.OnNavigatedTo(mode, parameters);
+ await ExecuteUIThread(() => { SummaryText = Translator.SettingsStorage_NoLocalMimeDataFound; });
+
+ await RefreshStorageAsync();
+ }
+
+ partial void OnIsLoadingChanged(bool value)
+ {
+ UpdateAccountBusyState();
+ }
+
+ partial void OnIsCleaningChanged(bool value)
+ {
+ UpdateAccountBusyState();
+ }
+
+ private void UpdateAccountBusyState()
+ {
+ Dispatcher.ExecuteOnUIThread(() =>
+ {
+ foreach (var item in AccountStorageItems)
+ {
+ item.IsBusy = IsBusy;
+ }
+ });
+ }
+
+ [RelayCommand]
+ private async Task RefreshStorageAsync()
+ {
+ if (IsBusy) return;
+
+ await ExecuteUIThread(() => { IsLoading = true; });
+
+ try
+ {
+ var mimeRootPath = await _mimeStorageService.GetMimeRootPathAsync().ConfigureAwait(false);
+ var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
+ var sizeMap = await _mimeStorageService.GetAccountsMimeStorageSizesAsync(accounts.Select(a => a.Id)).ConfigureAwait(false);
+
+ var storageItems = accounts
+ .Select(account =>
+ {
+ sizeMap.TryGetValue(account.Id, out var accountSize);
+ var viewModel = new AccountStorageItemViewModel(account, accountSize, DeleteAllCommand, DeleteOlderThanOneMonthCommand, DeleteOlderThanThreeMonthsCommand, DeleteOlderThanSixMonthsCommand, DeleteOlderThanOneYearCommand);
+ viewModel.SizeDescription = string.Format(Translator.SettingsStorage_AccountUsageDescription, viewModel.SizeText);
+ return viewModel;
+ })
+ .OrderByDescending(a => a.SizeBytes)
+ .ToList();
+
+ await ExecuteUIThread(() =>
+ {
+ MimeRootPath = mimeRootPath;
+ AccountStorageItems.Clear();
+
+ foreach (var item in storageItems)
+ {
+ AccountStorageItems.Add(item);
+ }
+
+ var total = storageItems.Sum(a => a.SizeBytes);
+ SummaryText = storageItems.Count == 0
+ ? Translator.SettingsStorage_NoAccountsFound
+ : string.Format(Translator.SettingsStorage_TotalUsage, total.GetBytesReadable());
+ });
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "Failed to refresh storage data.");
+ await ExecuteUIThread(() =>
+ {
+ _dialogService.InfoBarMessage(Translator.GeneralTitle_Error, ex.Message, Core.Domain.Enums.InfoBarMessageType.Error);
+ });
+ }
+ finally
+ {
+ await ExecuteUIThread(() => { IsLoading = false; });
+ }
+ }
+
+ [RelayCommand]
+ private async Task DeleteAllAsync(AccountStorageItemViewModel accountItem)
+ {
+ if (accountItem == null || IsBusy) return;
+
+ bool approved = await _dialogService.ShowConfirmationDialogAsync(
+ string.Format(Translator.SettingsStorage_DeleteAll_Confirm_Message, accountItem.AccountName),
+ Translator.SettingsStorage_DeleteAll_Confirm_Title,
+ Translator.Buttons_Delete);
+
+ if (!approved) return;
+
+ await ExecuteUIThread(() => { IsCleaning = true; });
+
+ try
+ {
+ await _mimeStorageService.DeleteAccountMimeStorageAsync(accountItem.Account.Id).ConfigureAwait(false);
+ await ExecuteUIThread(() =>
+ {
+ _dialogService.InfoBarMessage(Translator.GeneralTitle_Info, Translator.SettingsStorage_DeleteAll_Success, Core.Domain.Enums.InfoBarMessageType.Success);
+ });
+ await RefreshStorageAsync();
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "Failed to delete all MIME content for account {AccountId}", accountItem.Account.Id);
+ await ExecuteUIThread(() =>
+ {
+ _dialogService.InfoBarMessage(Translator.GeneralTitle_Error, ex.Message, Core.Domain.Enums.InfoBarMessageType.Error);
+ });
+ }
+ finally
+ {
+ await ExecuteUIThread(() => { IsCleaning = false; });
+ }
+ }
+
+ [RelayCommand]
+ private Task DeleteOlderThanOneMonthAsync(AccountStorageItemViewModel accountItem)
+ => DeleteOlderThanAsync(accountItem, 1);
+
+ [RelayCommand]
+ private Task DeleteOlderThanThreeMonthsAsync(AccountStorageItemViewModel accountItem)
+ => DeleteOlderThanAsync(accountItem, 3);
+
+ [RelayCommand]
+ private Task DeleteOlderThanSixMonthsAsync(AccountStorageItemViewModel accountItem)
+ => DeleteOlderThanAsync(accountItem, 6);
+
+ [RelayCommand]
+ private Task DeleteOlderThanOneYearAsync(AccountStorageItemViewModel accountItem)
+ => DeleteOlderThanAsync(accountItem, 12);
+
+ private async Task DeleteOlderThanAsync(AccountStorageItemViewModel accountItem, int months)
+ {
+ if (accountItem == null || IsBusy) return;
+
+ string rangeText = GetRangeText(months);
+
+ bool approved = await _dialogService.ShowConfirmationDialogAsync(
+ string.Format(Translator.SettingsStorage_DeleteOld_Confirm_Message, rangeText, accountItem.AccountName),
+ Translator.SettingsStorage_DeleteOld_Confirm_Title,
+ Translator.Buttons_Delete);
+
+ if (!approved) return;
+
+ await ExecuteUIThread(() => { IsCleaning = true; });
+
+ try
+ {
+ var cutoffDateUtc = DateTime.UtcNow.AddMonths(-months);
+ var deletedDirectoryCount = await _mimeStorageService
+ .DeleteAccountMimeStorageOlderThanAsync(accountItem.Account.Id, cutoffDateUtc)
+ .ConfigureAwait(false);
+
+ await ExecuteUIThread(() =>
+ {
+ _dialogService.InfoBarMessage(
+ Translator.GeneralTitle_Info,
+ string.Format(Translator.SettingsStorage_DeleteOld_Success, deletedDirectoryCount, rangeText),
+ Core.Domain.Enums.InfoBarMessageType.Success);
+ });
+
+ await RefreshStorageAsync();
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "Failed to delete MIME content by cutoff for account {AccountId}", accountItem.Account.Id);
+ await ExecuteUIThread(() =>
+ {
+ _dialogService.InfoBarMessage(Translator.GeneralTitle_Error, ex.Message, Core.Domain.Enums.InfoBarMessageType.Error);
+ });
+ }
+ finally
+ {
+ await ExecuteUIThread(() => { IsCleaning = false; });
+ }
+ }
+
+ private static string GetRangeText(int months)
+ {
+ return months switch
+ {
+ 1 => Translator.SettingsStorage_1Month,
+ 3 => Translator.SettingsStorage_3Months,
+ 6 => Translator.SettingsStorage_6Months,
+ 12 => Translator.SettingsStorage_1Year,
+ _ => string.Format(Translator.SettingsStorage_Months, months)
+ };
+ }
+}
diff --git a/Wino.Mail.WinUI/App.xaml.cs b/Wino.Mail.WinUI/App.xaml.cs
index c57b2679..1309dc8c 100644
--- a/Wino.Mail.WinUI/App.xaml.cs
+++ b/Wino.Mail.WinUI/App.xaml.cs
@@ -21,6 +21,7 @@ using Wino.Mail.Services;
using Wino.Mail.ViewModels;
using Wino.Mail.WinUI.Interfaces;
using Wino.Mail.WinUI.Services;
+using Wino.Messaging.UI;
using Wino.Messaging.Client.Accounts;
using Wino.Messaging.Server;
using Wino.Services;
@@ -92,7 +93,6 @@ public partial class App : WinoApplication,
services.AddTransient(typeof(AliasManagementPageViewModel));
services.AddTransient(typeof(ContactsPageViewModel));
services.AddTransient(typeof(SignatureAndEncryptionPageViewModel));
-
services.AddTransient(typeof(CalendarPageViewModel));
services.AddTransient(typeof(CalendarSettingsPageViewModel));
services.AddTransient(typeof(CalendarAccountSettingsPageViewModel));
@@ -362,8 +362,70 @@ public partial class App : WinoApplication,
WeakReferenceMessenger.Default.Register(this);
}
- public void Receive(NewMailSynchronizationRequested message) => _synchronizationManager?.SynchronizeMailAsync(message.Options);
- public void Receive(NewCalendarSynchronizationRequested message) => _synchronizationManager?.SynchronizeCalendarAsync(message.Options);
+ public async void Receive(NewMailSynchronizationRequested message)
+ {
+ if (_synchronizationManager == null) return;
+
+ MailSynchronizationResult syncResult;
+
+ try
+ {
+ syncResult = await _synchronizationManager.SynchronizeMailAsync(message.Options);
+ }
+ catch (Exception ex)
+ {
+ // Defensive fallback to guarantee completion message emission.
+ syncResult = MailSynchronizationResult.Failed(ex);
+ }
+
+ WeakReferenceMessenger.Default.Send(new AccountSynchronizationCompleted(
+ message.Options.AccountId,
+ syncResult.CompletedState,
+ message.Options.GroupedSynchronizationTrackingId));
+
+ if (syncResult.CompletedState == SynchronizationCompletedState.Failed ||
+ syncResult.CompletedState == SynchronizationCompletedState.PartiallyCompleted)
+ {
+ var dialogService = Services.GetRequiredService();
+ var errorMessage = GetSynchronizationFailureMessage(message.Options.Type, syncResult.Exception?.Message);
+ var severity = syncResult.CompletedState == SynchronizationCompletedState.PartiallyCompleted
+ ? InfoBarMessageType.Warning
+ : InfoBarMessageType.Error;
+
+ dialogService.InfoBarMessage(Translator.Info_SyncFailedTitle, errorMessage, severity);
+ }
+ }
+
+ public async void Receive(NewCalendarSynchronizationRequested message)
+ {
+ if (_synchronizationManager == null) return;
+
+ var calendarSyncResult = await _synchronizationManager.SynchronizeCalendarAsync(message.Options);
+
+ if (calendarSyncResult.CompletedState == SynchronizationCompletedState.Failed)
+ {
+ var dialogService = Services.GetRequiredService();
+ dialogService.InfoBarMessage(
+ Translator.Info_SyncFailedTitle,
+ Translator.Exception_FailedToSynchronizeFolders,
+ InfoBarMessageType.Error);
+ }
+ }
+
+ private static string GetSynchronizationFailureMessage(MailSynchronizationType synchronizationType, string? exceptionMessage)
+ {
+ if (!string.IsNullOrWhiteSpace(exceptionMessage))
+ {
+ return exceptionMessage;
+ }
+
+ return synchronizationType switch
+ {
+ MailSynchronizationType.Alias => Translator.Exception_FailedToSynchronizeAliases,
+ MailSynchronizationType.UpdateProfile => Translator.Exception_FailedToSynchronizeProfileInformation,
+ _ => Translator.Exception_FailedToSynchronizeFolders
+ };
+ }
///
/// Handles activation redirected from another instance (single-instancing).
diff --git a/Wino.Mail.WinUI/Services/NavigationService.cs b/Wino.Mail.WinUI/Services/NavigationService.cs
index 6915420f..ecfc435d 100644
--- a/Wino.Mail.WinUI/Services/NavigationService.cs
+++ b/Wino.Mail.WinUI/Services/NavigationService.cs
@@ -66,6 +66,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
WinoPage.KeyboardShortcutsPage => typeof(KeyboardShortcutsPage),
WinoPage.ContactsPage => typeof(ContactsPage),
WinoPage.SignatureAndEncryptionPage => typeof(SignatureAndEncryptionPage),
+ WinoPage.StoragePage => typeof(StoragePage),
WinoPage.CalendarPage => typeof(CalendarPage),
WinoPage.EventDetailsPage => typeof(EventDetailsPage),
WinoPage.CalendarSettingsPage => typeof(CalendarSettingsPage),
diff --git a/Wino.Mail.WinUI/Styles/CustomMessageDialogStyles.xaml b/Wino.Mail.WinUI/Styles/CustomMessageDialogStyles.xaml
index 8bb40c59..a888b8f1 100644
--- a/Wino.Mail.WinUI/Styles/CustomMessageDialogStyles.xaml
+++ b/Wino.Mail.WinUI/Styles/CustomMessageDialogStyles.xaml
@@ -15,14 +15,14 @@
HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="F1 M 0 9.375 C 0 8.509115 0.110677 7.677409 0.332031 6.879883 C 0.553385 6.082357 0.867513 5.335287 1.274414 4.638672 C 1.681315 3.942059 2.169596 3.30892 2.739258 2.739258 C 3.308919 2.169598 3.942057 1.681316 4.638672 1.274414 C 5.335286 0.867514 6.082356 0.553387 6.879883 0.332031 C 7.677409 0.110678 8.509114 0 9.375 0 C 10.240885 0 11.072591 0.110678 11.870117 0.332031 C 12.667643 0.553387 13.414713 0.867514 14.111328 1.274414 C 14.807942 1.681316 15.44108 2.169598 16.010742 2.739258 C 16.580402 3.30892 17.068684 3.942059 17.475586 4.638672 C 17.882486 5.335287 18.196613 6.082357 18.417969 6.879883 C 18.639322 7.677409 18.75 8.509115 18.75 9.375 C 18.75 10.240886 18.637695 11.072592 18.413086 11.870117 C 18.188477 12.667644 17.872721 13.413086 17.46582 14.106445 C 17.058918 14.799805 16.570637 15.431315 16.000977 16.000977 C 15.431314 16.570639 14.799804 17.05892 14.106445 17.46582 C 13.413085 17.872721 12.666015 18.188477 11.865234 18.413086 C 11.064453 18.637695 10.234375 18.75 9.375 18.75 C 8.509114 18.75 7.675781 18.639322 6.875 18.417969 C 6.074219 18.196615 5.327148 17.882486 4.633789 17.475586 C 3.94043 17.068686 3.308919 16.580404 2.739258 16.010742 C 2.169596 15.441081 1.681315 14.80957 1.274414 14.116211 C 0.867513 13.422852 0.553385 12.675781 0.332031 11.875 C 0.110677 11.074219 0 10.240886 0 9.375 Z M 17.5 9.375 C 17.5 8.626303 17.403971 7.905273 17.211914 7.211914 C 17.019855 6.518556 16.746418 5.87077 16.391602 5.268555 C 16.036783 4.666342 15.613606 4.119467 15.12207 3.62793 C 14.630533 3.136395 14.083658 2.713217 13.481445 2.358398 C 12.879231 2.003582 12.231445 1.730145 11.538086 1.538086 C 10.844727 1.346029 10.123697 1.25 9.375 1.25 C 8.626302 1.25 7.905273 1.346029 7.211914 1.538086 C 6.518555 1.730145 5.870768 2.003582 5.268555 2.358398 C 4.666341 2.713217 4.119466 3.136395 3.62793 3.62793 C 3.136393 4.119467 2.713216 4.666342 2.358398 5.268555 C 2.003581 5.87077 1.730143 6.518556 1.538086 7.211914 C 1.346029 7.905273 1.25 8.626303 1.25 9.375 C 1.25 10.123698 1.346029 10.844727 1.538086 11.538086 C 1.730143 12.231445 2.001953 12.879232 2.353516 13.481445 C 2.705078 14.083659 3.128255 14.632162 3.623047 15.126953 C 4.117838 15.621745 4.666341 16.044922 5.268555 16.396484 C 5.870768 16.748047 6.518555 17.019857 7.211914 17.211914 C 7.905273 17.403971 8.626302 17.5 9.375 17.5 C 10.123697 17.5 10.844727 17.403971 11.538086 17.211914 C 12.231445 17.019857 12.879231 16.748047 13.481445 16.396484 C 14.083658 16.044922 14.63216 15.621745 15.126953 15.126953 C 15.621744 14.632162 16.044922 14.083659 16.396484 13.481445 C 16.748047 12.879232 17.019855 12.231445 17.211914 11.538086 C 17.403971 10.844727 17.5 10.123698 17.5 9.375 Z M 8.4375 5.625 C 8.4375 5.364584 8.528646 5.14323 8.710938 4.960938 C 8.893229 4.778646 9.114583 4.6875 9.375 4.6875 C 9.635416 4.6875 9.856771 4.778646 10.039062 4.960938 C 10.221354 5.14323 10.3125 5.364584 10.3125 5.625 C 10.3125 5.885417 10.221354 6.106771 10.039062 6.289062 C 9.856771 6.471354 9.635416 6.5625 9.375 6.5625 C 9.114583 6.5625 8.893229 6.471354 8.710938 6.289062 C 8.528646 6.106771 8.4375 5.885417 8.4375 5.625 Z M 8.75 13.125 L 8.75 8.125 C 8.75 7.95573 8.811849 7.809246 8.935547 7.685547 C 9.059244 7.56185 9.205729 7.5 9.375 7.5 C 9.544271 7.5 9.690755 7.56185 9.814453 7.685547 C 9.93815 7.809246 10 7.95573 10 8.125 L 10 13.125 C 10 13.294271 9.93815 13.440756 9.814453 13.564453 C 9.690755 13.688151 9.544271 13.75 9.375 13.75 C 9.205729 13.75 9.059244 13.688151 8.935547 13.564453 C 8.811849 13.440756 8.75 13.294271 8.75 13.125 Z "
- Fill="#0f80d7" />
+ Fill="{ThemeResource SystemAccentColor}" />
+ Fill="{ThemeResource SystemAccentColor}" />
-
-
+
+
diff --git a/Wino.Mail.WinUI/Views/Abstract/StoragePageAbstract.cs b/Wino.Mail.WinUI/Views/Abstract/StoragePageAbstract.cs
new file mode 100644
index 00000000..e3fee54a
--- /dev/null
+++ b/Wino.Mail.WinUI/Views/Abstract/StoragePageAbstract.cs
@@ -0,0 +1,6 @@
+using Wino.Mail.ViewModels;
+using Wino.Mail.WinUI;
+
+namespace Wino.Views.Abstract;
+
+public abstract class StoragePageAbstract : BasePage { }
diff --git a/Wino.Mail.WinUI/Views/Mail/MailListPage.xaml b/Wino.Mail.WinUI/Views/Mail/MailListPage.xaml
index 00593681..a0f48fb5 100644
--- a/Wino.Mail.WinUI/Views/Mail/MailListPage.xaml
+++ b/Wino.Mail.WinUI/Views/Mail/MailListPage.xaml
@@ -210,14 +210,14 @@
+ Translation="0,0,20">
@@ -421,6 +421,7 @@
HorizontalContentAlignment="Stretch"
toolkitExt:ListViewExtensions.ItemContainerStretchDirection="Horizontal"
toolkitExt:ScrollViewerExtensions.VerticalScrollBarMargin="0"
+ CanDragItems="True"
ChoosingItemContainer="WinoListViewChoosingItemContainer"
IsItemClickEnabled="True"
ItemClick="WinoListViewItemClicked"
diff --git a/Wino.Mail.WinUI/Views/SettingOptionsPage.xaml b/Wino.Mail.WinUI/Views/SettingOptionsPage.xaml
index ea62099e..f8a98333 100644
--- a/Wino.Mail.WinUI/Views/SettingOptionsPage.xaml
+++ b/Wino.Mail.WinUI/Views/SettingOptionsPage.xaml
@@ -409,6 +409,38 @@
Glyph="" />
+
+
diff --git a/Wino.Mail.WinUI/Views/Settings/StoragePage.xaml b/Wino.Mail.WinUI/Views/Settings/StoragePage.xaml
new file mode 100644
index 00000000..fa0254ff
--- /dev/null
+++ b/Wino.Mail.WinUI/Views/Settings/StoragePage.xaml
@@ -0,0 +1,113 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ M5 7h14a3 3 0 0 1 2.995 2.824L22 10v4a3 3 0 0 1-2.824 2.995L19 17H5a3 3 0 0 1-2.995-2.824L2 14v-4a3 3 0 0 1 2.824-2.995L5 7h14H5Zm14 1.5H5A1.5 1.5 0 0 0 3.5 10v4A1.5 1.5 0 0 0 5 15.5h14a1.5 1.5 0 0 0 1.5-1.5v-4A1.5 1.5 0 0 0 19 8.5ZM18 10a1 1 0 1 1 0 2 1 1 0 0 1 0-2Zm-4 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Wino.Mail.WinUI/Views/Settings/StoragePage.xaml.cs b/Wino.Mail.WinUI/Views/Settings/StoragePage.xaml.cs
new file mode 100644
index 00000000..ab436941
--- /dev/null
+++ b/Wino.Mail.WinUI/Views/Settings/StoragePage.xaml.cs
@@ -0,0 +1,11 @@
+using Wino.Views.Abstract;
+
+namespace Wino.Views.Settings;
+
+public sealed partial class StoragePage : StoragePageAbstract
+{
+ public StoragePage()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/Wino.Mail.WinUI/Views/SettingsPage.xaml.cs b/Wino.Mail.WinUI/Views/SettingsPage.xaml.cs
index 7415560d..a3cc9fb8 100644
--- a/Wino.Mail.WinUI/Views/SettingsPage.xaml.cs
+++ b/Wino.Mail.WinUI/Views/SettingsPage.xaml.cs
@@ -47,6 +47,9 @@ public sealed partial class SettingsPage : SettingsPageAbstract,
case WinoPage.PersonalizationPage:
WeakReferenceMessenger.Default.Send(new BreadcrumbNavigationRequested(Translator.SettingsPersonalization_Title, WinoPage.PersonalizationPage));
break;
+ case WinoPage.StoragePage:
+ WeakReferenceMessenger.Default.Send(new BreadcrumbNavigationRequested(Translator.SettingsStorage_Title, WinoPage.StoragePage));
+ break;
}
}
}
diff --git a/Wino.Services/MailService.cs b/Wino.Services/MailService.cs
index c571eb2d..7483c5ba 100644
--- a/Wino.Services/MailService.cs
+++ b/Wino.Services/MailService.cs
@@ -1183,4 +1183,17 @@ public class MailService : BaseDatabaseService, IMailService
return await Connection.QueryScalarsAsync(sql, mailCopyIds.Cast