mirror of
https://github.com/MAKS-IT-COM/maksit-postclient.git
synced 2026-10-01 03:48:10 +02:00
3909 lines
130 KiB
C#
3909 lines
130 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.Diagnostics;
|
|
using Avalonia.Threading;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using MaksIT.Results;
|
|
using MaksIT.PostClient.Client;
|
|
using MaksIT.PostClient.Shared;
|
|
|
|
|
|
namespace MaksIT.PostClient.UI.ViewModels;
|
|
|
|
|
|
public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
|
private readonly ConfigurationFileService _files;
|
|
private readonly ISecretStore _secrets;
|
|
private readonly IMailSessionFactory _sessions;
|
|
private readonly IMailAuthService _auth;
|
|
private readonly ISentReceiptStore _receipts;
|
|
private readonly MailArchiveStore _archive;
|
|
private readonly ISemanticSearchService _semantic;
|
|
private readonly IAppUpdateService _updates;
|
|
private readonly Dictionary<string, IMailSession> _connections = new(StringComparer.OrdinalIgnoreCase);
|
|
private readonly Dictionary<string, List<FolderRowViewModel>> _foldersByMailbox = new(StringComparer.OrdinalIgnoreCase);
|
|
private readonly HashSet<string> _connecting = new(StringComparer.OrdinalIgnoreCase);
|
|
private CancellationTokenSource? _work;
|
|
private CancellationTokenSource? _readingLoad;
|
|
private CancellationTokenSource? _folderLoad;
|
|
private CancellationTokenSource? _backfill;
|
|
private CancellationTokenSource? _search;
|
|
private int _searchGen;
|
|
private int _indexPause;
|
|
private bool _syncingList;
|
|
private readonly HashSet<string> _indexMailboxes = new(StringComparer.OrdinalIgnoreCase);
|
|
private readonly HashSet<string> _catalogDone = new(StringComparer.OrdinalIgnoreCase);
|
|
private readonly HashSet<string> _catalogFolders = new(StringComparer.OrdinalIgnoreCase);
|
|
private const int IndexCatalogChunk = 2_000;
|
|
private MailMessageBody? _reading;
|
|
private MessageRowViewModel? _pendingWindow;
|
|
private bool _syncingTree;
|
|
private bool _suppressFolderLoad;
|
|
private bool _applyingProvider;
|
|
private bool _accountSettingsOpen;
|
|
private bool _syncingBodyKind;
|
|
private bool _updateBusy;
|
|
private string _bodyPreference = MailBodyKind.Html;
|
|
private string _listSortHeader = "Date";
|
|
private bool _listSortDescending = true;
|
|
private OAuthTokenSet? _pendingOauth;
|
|
|
|
public ISemanticSearchService SemanticSearch =>
|
|
_semantic;
|
|
|
|
public bool AccountSettingsOpen {
|
|
get => _accountSettingsOpen;
|
|
set => _accountSettingsOpen = value;
|
|
}
|
|
|
|
[ObservableProperty]
|
|
private MailboxAccount? selectedMailbox;
|
|
|
|
[ObservableProperty]
|
|
private FolderRowViewModel? selectedFolder;
|
|
|
|
[ObservableProperty]
|
|
private FolderNodeViewModel? selectedFolderNode;
|
|
|
|
[ObservableProperty]
|
|
private MessageRowViewModel? selectedMessage;
|
|
|
|
[ObservableProperty]
|
|
private string status = "";
|
|
|
|
[ObservableProperty]
|
|
private UiCopy copy = UiCopy.For(UiLanguage.En);
|
|
|
|
[ObservableProperty]
|
|
private string folderStats = "";
|
|
|
|
[ObservableProperty]
|
|
private bool isBusy;
|
|
|
|
[ObservableProperty]
|
|
private string editorName = "";
|
|
|
|
[ObservableProperty]
|
|
private string editorAddress = "";
|
|
|
|
[ObservableProperty]
|
|
private string editorUsername = "";
|
|
|
|
[ObservableProperty]
|
|
private string editorPassword = "";
|
|
|
|
[ObservableProperty]
|
|
private ChoiceRow editorProvider = ChoiceRow.Provider(MailProvider.Imap);
|
|
|
|
[ObservableProperty]
|
|
private ChoiceRow editorCertifiedKind = ChoiceRow.Certified(MailCertifiedKind.Ordinary);
|
|
|
|
[ObservableProperty]
|
|
private string editorGoogleClientId = "";
|
|
|
|
[ObservableProperty]
|
|
private string editorGoogleClientSecret = "";
|
|
|
|
[ObservableProperty]
|
|
private string editorMicrosoftClientId = "";
|
|
|
|
[ObservableProperty]
|
|
private bool showPasswordFields = true;
|
|
|
|
[ObservableProperty]
|
|
private bool showOAuthButtons;
|
|
|
|
[ObservableProperty]
|
|
private bool showGoogleSignIn;
|
|
|
|
[ObservableProperty]
|
|
private bool showMicrosoftSignIn;
|
|
|
|
[ObservableProperty]
|
|
[NotifyPropertyChangedFor(nameof(ShowAuthHint))]
|
|
private string oauthHint = "";
|
|
|
|
public bool ShowAuthHint =>
|
|
!string.IsNullOrWhiteSpace(OauthHint);
|
|
|
|
[ObservableProperty]
|
|
private ChoiceRow editorIncomingProtocol = ChoiceRow.Incoming(MailProtocol.Imap);
|
|
|
|
[ObservableProperty]
|
|
private ChoiceRow editorIncomingSecurity = ChoiceRow.Security(MailSecurity.Ssl);
|
|
|
|
[ObservableProperty]
|
|
private string editorIncomingHost = "";
|
|
|
|
[ObservableProperty]
|
|
private string editorStorePath = "";
|
|
|
|
[ObservableProperty]
|
|
private decimal editorIncomingPort = 993;
|
|
|
|
[ObservableProperty]
|
|
private ChoiceRow editorSmtpSecurity = ChoiceRow.Security(MailSecurity.Ssl);
|
|
|
|
[ObservableProperty]
|
|
private string editorSmtpHost = "";
|
|
|
|
[ObservableProperty]
|
|
private decimal editorSmtpPort = 465;
|
|
|
|
[ObservableProperty]
|
|
private string readingSubject = "";
|
|
|
|
[ObservableProperty]
|
|
private string readingFrom = "";
|
|
|
|
[ObservableProperty]
|
|
private string readingTo = "";
|
|
|
|
[ObservableProperty]
|
|
private string readingCc = "";
|
|
|
|
[ObservableProperty]
|
|
private string readingDate = "";
|
|
|
|
[ObservableProperty]
|
|
private string readingBody = "";
|
|
|
|
[ObservableProperty]
|
|
private string readingHtmlDocument = "";
|
|
|
|
[ObservableProperty]
|
|
private string readingSource = "";
|
|
|
|
[ObservableProperty]
|
|
private string readingEnvelope = "";
|
|
|
|
[ObservableProperty]
|
|
private bool hasReading;
|
|
|
|
[ObservableProperty]
|
|
private bool hasReadingCc;
|
|
|
|
[ObservableProperty]
|
|
private bool hasReadingEnvelope;
|
|
|
|
[ObservableProperty]
|
|
private bool hasReadingFiles;
|
|
|
|
[ObservableProperty]
|
|
private bool unwrapEnvelope;
|
|
|
|
[ObservableProperty]
|
|
private bool groupConversations;
|
|
|
|
[ObservableProperty]
|
|
private string readingBodyKind = MailBodyKind.Html;
|
|
|
|
[ObservableProperty]
|
|
private string readingLayout = MailLayout.Stacked;
|
|
|
|
[ObservableProperty]
|
|
private string messageFilter = "";
|
|
|
|
[ObservableProperty]
|
|
private string practiceLabel = "";
|
|
|
|
[ObservableProperty]
|
|
private string quotaLine = "";
|
|
|
|
[ObservableProperty]
|
|
private string indexLine = "";
|
|
|
|
[ObservableProperty]
|
|
private string semanticLine = "";
|
|
|
|
[ObservableProperty]
|
|
private string readingFattura = "";
|
|
|
|
[ObservableProperty]
|
|
private bool hasReadingFattura;
|
|
|
|
public ObservableCollection<MailboxAccount> Mailboxes { get; } = [];
|
|
|
|
public ObservableCollection<FolderRowViewModel> Folders { get; } = [];
|
|
|
|
public ObservableCollection<FolderNodeViewModel> FolderTree { get; } = [];
|
|
|
|
public ObservableCollection<MessageRowViewModel> Messages { get; } = [];
|
|
|
|
public ObservableCollection<MessageRowViewModel> VisibleMessages { get; } = [];
|
|
|
|
public ObservableCollection<MailFileAttachment> ReadingFiles { get; } = [];
|
|
|
|
public List<MessageRowViewModel> SelectedMessages { get; } = [];
|
|
|
|
public IReadOnlyList<ChoiceRow> IncomingProtocols =>
|
|
ChoiceRow.IncomingProtocols;
|
|
|
|
public IReadOnlyList<ChoiceRow> SecurityModes =>
|
|
ChoiceRow.SecurityModes;
|
|
|
|
public IReadOnlyList<ChoiceRow> Providers =>
|
|
ChoiceRow.ProvidersFor(_files.Current.Features, EditorProvider.Id);
|
|
|
|
public IReadOnlyList<ChoiceRow> CertifiedKinds =>
|
|
ChoiceRow.CertifiedKinds;
|
|
|
|
public bool ShowPstFields =>
|
|
MailProvider.IsPst(EditorProvider.Id);
|
|
|
|
public bool ShowServerFields =>
|
|
!ShowPstFields;
|
|
|
|
public bool ShowCertifiedKind =>
|
|
ShowServerFields && MailProvider.Normalize(EditorProvider.Id) == MailProvider.Imap;
|
|
|
|
public IReadOnlyList<ChoiceRow> Languages { get; } =
|
|
UiLanguage.All.Select(id => new ChoiceRow { Id = id, Title = UiLanguage.Title(id) }).ToList();
|
|
|
|
public ChoiceRow SelectedLanguage {
|
|
get => Languages.First(l => l.Id == UiLocale.Id);
|
|
set {
|
|
if (value is null || value.Id == UiLocale.Id)
|
|
return;
|
|
SetLanguage(value.Id);
|
|
}
|
|
}
|
|
|
|
public bool IsEnglish =>
|
|
UiLocale.Id == UiLanguage.En;
|
|
|
|
public bool IsItalian =>
|
|
UiLocale.Id == UiLanguage.It;
|
|
|
|
public bool IsFrench =>
|
|
UiLocale.Id == UiLanguage.Fr;
|
|
|
|
public bool IsGerman =>
|
|
UiLocale.Id == UiLanguage.De;
|
|
|
|
public bool IsSpanish =>
|
|
UiLocale.Id == UiLanguage.Es;
|
|
|
|
public bool HtmlEngineReady { get; private set; } = true;
|
|
|
|
public bool ShowHtmlBody =>
|
|
HasReading && HtmlEngineReady && MailBodyKind.IsHtml(ReadingBodyKind);
|
|
|
|
public bool ShowTextBody =>
|
|
HasReading && (MailBodyKind.IsText(ReadingBodyKind)
|
|
|| (!HtmlEngineReady && MailBodyKind.IsHtml(ReadingBodyKind)));
|
|
|
|
public bool ShowSourceBody =>
|
|
HasReading && MailBodyKind.IsSource(ReadingBodyKind);
|
|
|
|
public bool ShowFatturaBody =>
|
|
HasReading && UseFatturaPa && MailBodyKind.IsFattura(ReadingBodyKind);
|
|
|
|
public bool UseFascicolo =>
|
|
FeatureGate.On(AppFeature.Fascicolo);
|
|
|
|
public bool UseFatturaPa =>
|
|
FeatureGate.On(AppFeature.FatturaPa);
|
|
|
|
public bool UseEnvelopeTools =>
|
|
FeatureGate.On(AppFeature.PecEnvelope) || FeatureGate.On(AppFeature.RemEvidence);
|
|
|
|
public bool UseDeliveryColumn =>
|
|
UseEnvelopeTools
|
|
|| FeatureGate.On(AppFeature.FrEvidence)
|
|
|| FeatureGate.On(AppFeature.DeEvidence)
|
|
|| FeatureGate.On(AppFeature.EsEvidence)
|
|
|| FeatureGate.On(AppFeature.ChEvidence);
|
|
|
|
public bool UseTypeColumn =>
|
|
UseEnvelopeTools;
|
|
|
|
public bool ShowFatturaPaTools =>
|
|
HasReadingFattura && UseFatturaPa;
|
|
|
|
public bool IsHtmlKind =>
|
|
MailBodyKind.IsHtml(ReadingBodyKind);
|
|
|
|
public bool IsTextKind =>
|
|
MailBodyKind.IsText(ReadingBodyKind);
|
|
|
|
public bool IsSourceKind =>
|
|
MailBodyKind.IsSource(ReadingBodyKind);
|
|
|
|
public bool IsFatturaKind =>
|
|
MailBodyKind.IsFattura(ReadingBodyKind);
|
|
|
|
public bool IsWideLayout {
|
|
get => MailLayout.IsWide(ReadingLayout);
|
|
set => ReadingLayout = value ? MailLayout.Wide : MailLayout.Stacked;
|
|
}
|
|
|
|
public bool IsStackedLayout {
|
|
get => MailLayout.IsStacked(ReadingLayout);
|
|
set {
|
|
if (value)
|
|
ReadingLayout = MailLayout.Stacked;
|
|
else
|
|
OnPropertyChanged(nameof(IsStackedLayout));
|
|
}
|
|
}
|
|
|
|
public string LayoutToggleLabel =>
|
|
IsWideLayout ? Copy.ThreeColumns : Copy.ListReading;
|
|
|
|
public event Action<ComposeViewModel>? ComposeRequested;
|
|
|
|
public event Action<MessageWindowViewModel>? MessageWindowRequested;
|
|
|
|
public event Action? AccountSettingsRequested;
|
|
|
|
public event Action? FeaturesRequested;
|
|
|
|
public event Action? SemanticSearchRequested;
|
|
|
|
public event Action? MessageListSelectionRestoreRequested;
|
|
|
|
public event Action? RulesRequested;
|
|
|
|
public event Action? AccountSaved;
|
|
|
|
public event Action<string, string>? NoticeRequested;
|
|
|
|
public event Action? AboutRequested;
|
|
|
|
public event Func<PromptRequest, Task<string?>>? PromptRequested;
|
|
|
|
public event Action? ExportArchiveRequested;
|
|
|
|
public event Action? ExportFascicoloRequested;
|
|
|
|
public event Action? ImportEmlRequested;
|
|
|
|
public event Action? ImportPstRequested;
|
|
|
|
public event Action? AttachPstRequested;
|
|
|
|
public event Action? CreatePstRequested;
|
|
|
|
public event Func<Task<string?>>? PickStorePathRequested;
|
|
|
|
public event Action? ImportThunderbirdRequested;
|
|
|
|
public event Action<string>? PrintHtmlRequested;
|
|
|
|
public event Action<byte[], string>? SavePdfRequested;
|
|
|
|
public event Action<byte[], string>? SaveAttachmentsZipRequested;
|
|
|
|
public MainViewModel(
|
|
ConfigurationFileService files,
|
|
ISecretStore secrets,
|
|
IMailSessionFactory sessions,
|
|
IMailAuthService auth,
|
|
ISentReceiptStore receipts,
|
|
MailArchiveStore archive,
|
|
ISemanticSearchService semantic,
|
|
IAppUpdateService updates) {
|
|
_files = files;
|
|
_secrets = secrets;
|
|
_sessions = sessions;
|
|
_auth = auth;
|
|
_receipts = receipts;
|
|
_archive = archive;
|
|
_semantic = semantic;
|
|
_updates = updates;
|
|
files.Current.EnsureDefaults();
|
|
FeatureGate.Use(files.Current.Features);
|
|
UiLocale.Apply(files.Current.Language);
|
|
copy = UiLocale.Copy;
|
|
status = copy.AddMailboxThenGet;
|
|
unwrapEnvelope = files.Current.UnwrapEnvelope;
|
|
groupConversations = files.Current.GroupConversations;
|
|
_bodyPreference = MailBodyKind.Preference(files.Current.ReadingBodyKind);
|
|
readingBodyKind = _bodyPreference;
|
|
readingLayout = MailLayout.Normalize(files.Current.ReadingLayout);
|
|
messageFilter = files.Current.Layout?.MessageFilter ?? "";
|
|
var savedSort = files.Current.Layout?.ColumnSort;
|
|
if (savedSort is not null && !string.IsNullOrWhiteSpace(savedSort.Header)) {
|
|
_listSortHeader = savedSort.Header;
|
|
_listSortDescending = string.Equals(
|
|
savedSort.Direction,
|
|
nameof(System.ComponentModel.ListSortDirection.Descending),
|
|
StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
editorGoogleClientId = files.Current.GoogleClientId;
|
|
editorGoogleClientSecret = StoredGoogleClientSecret();
|
|
editorMicrosoftClientId = files.Current.MicrosoftClientId;
|
|
ReloadMailboxes();
|
|
semanticLine = semantic.StatusLine;
|
|
semantic.Changed += OnSemanticChanged;
|
|
semantic.Start();
|
|
}
|
|
|
|
public async ValueTask DisposeAsync() {
|
|
StopBackfill();
|
|
_folderLoad?.Cancel();
|
|
_folderLoad?.Dispose();
|
|
_folderLoad = null;
|
|
_search?.Cancel();
|
|
_search?.Dispose();
|
|
_search = null;
|
|
_readingLoad?.Cancel();
|
|
_readingLoad?.Dispose();
|
|
_readingLoad = null;
|
|
_work?.Cancel();
|
|
_semantic.Changed -= OnSemanticChanged;
|
|
_semantic.Dispose();
|
|
List<IMailSession> sessions;
|
|
lock (_connections) {
|
|
sessions = [.. _connections.Values];
|
|
_connections.Clear();
|
|
}
|
|
|
|
foreach (var session in sessions)
|
|
await session.DisposeAsync();
|
|
_work?.Dispose();
|
|
_archive.Dispose();
|
|
}
|
|
|
|
private void OnSemanticChanged() =>
|
|
Dispatcher.UIThread.Post(() => {
|
|
var line = _semantic.StatusLine;
|
|
if (SemanticLine != line)
|
|
SemanticLine = line;
|
|
});
|
|
|
|
[RelayCommand]
|
|
private void NewMailbox() {
|
|
_pendingOauth = null;
|
|
SelectedMailbox = null;
|
|
EditorName = "";
|
|
EditorAddress = "";
|
|
EditorUsername = "";
|
|
EditorPassword = "";
|
|
EditorGoogleClientId = _files.Current.GoogleClientId;
|
|
EditorGoogleClientSecret = "";
|
|
EditorMicrosoftClientId = _files.Current.MicrosoftClientId;
|
|
_applyingProvider = true;
|
|
EditorProvider = ChoiceRow.Provider(MailProvider.Imap);
|
|
EditorCertifiedKind = ChoiceRow.Certified(MailCertifiedKind.Ordinary);
|
|
_applyingProvider = false;
|
|
EditorIncomingProtocol = ChoiceRow.Incoming(MailProtocol.Imap);
|
|
EditorIncomingSecurity = ChoiceRow.Security(MailSecurity.Ssl);
|
|
EditorIncomingHost = "";
|
|
EditorStorePath = "";
|
|
EditorIncomingPort = MailSecurity.DefaultIncomingPort(MailProtocol.Imap, MailSecurity.Ssl);
|
|
EditorSmtpSecurity = ChoiceRow.Security(MailSecurity.Ssl);
|
|
EditorSmtpHost = "";
|
|
EditorSmtpPort = MailSecurity.DefaultSmtpPort(MailSecurity.Ssl);
|
|
UpdateAuthUi();
|
|
AccountSettingsRequested?.Invoke();
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void OpenFeatures() =>
|
|
FeaturesRequested?.Invoke();
|
|
|
|
[RelayCommand]
|
|
private void OpenSemanticSearch() =>
|
|
SemanticSearchRequested?.Invoke();
|
|
|
|
[RelayCommand]
|
|
private void OpenRules() =>
|
|
RulesRequested?.Invoke();
|
|
|
|
[RelayCommand]
|
|
private void ShowAbout() =>
|
|
AboutRequested?.Invoke();
|
|
|
|
[RelayCommand(CanExecute = nameof(CanCheckForUpdates))]
|
|
private async Task CheckForUpdatesAsync() {
|
|
_updateBusy = true;
|
|
CheckForUpdatesCommand.NotifyCanExecuteChanged();
|
|
Status = Copy.UpdateChecking;
|
|
try {
|
|
var check = await _updates.CheckAsync();
|
|
if (!check.IsSuccess) {
|
|
var msg = string.Join(" ", check.Messages);
|
|
Status = msg.Contains("404", StringComparison.Ordinal)
|
|
? Copy.UpdateNone
|
|
: string.Format(Copy.UpdateFailed, msg);
|
|
return;
|
|
}
|
|
|
|
var info = check.Value;
|
|
if (info is null || !info.IsNewer) {
|
|
Status = string.Format(Copy.UpdateLatest, AppVersion.Display());
|
|
return;
|
|
}
|
|
|
|
var latest = info.Release.VersionText;
|
|
var installed = AppVersion.Format(info.Installed);
|
|
var hasAsset = info.Asset is not null;
|
|
var accepted = PromptRequested is null
|
|
? null
|
|
: await PromptRequested(new PromptRequest {
|
|
Title = Copy.CheckUpdates.Replace("_", "", StringComparison.Ordinal),
|
|
Message = hasAsset
|
|
? string.Format(Copy.UpdateAvailable, latest, installed)
|
|
: string.Format(Copy.UpdateNoAsset, latest, installed),
|
|
ConfirmOnly = true,
|
|
ConfirmLabel = hasAsset ? Copy.UpdateDownload : Copy.UpdateOpenPage
|
|
});
|
|
if (accepted is null) {
|
|
Status = string.Format(Copy.UpdateAvailable, latest, installed);
|
|
return;
|
|
}
|
|
|
|
if (hasAsset) {
|
|
Status = Copy.UpdateDownloading;
|
|
var downloaded = await _updates.DownloadAsync(info.Asset!);
|
|
if (!downloaded.IsSuccess || string.IsNullOrWhiteSpace(downloaded.Value)) {
|
|
Status = string.Join(" ", downloaded.Messages);
|
|
OpenUrl(info.Release.HtmlUrl);
|
|
return;
|
|
}
|
|
|
|
if (!_updates.TryLaunch(downloaded.Value))
|
|
OpenUrl(info.Release.HtmlUrl);
|
|
else
|
|
Status = "";
|
|
return;
|
|
}
|
|
|
|
OpenUrl(info.Release.HtmlUrl);
|
|
}
|
|
finally {
|
|
_updateBusy = false;
|
|
CheckForUpdatesCommand.NotifyCanExecuteChanged();
|
|
}
|
|
}
|
|
|
|
private bool CanCheckForUpdates() =>
|
|
!_updateBusy;
|
|
|
|
private static void OpenUrl(string? url) {
|
|
if (string.IsNullOrWhiteSpace(url))
|
|
return;
|
|
try {
|
|
Process.Start(new ProcessStartInfo {
|
|
FileName = url,
|
|
UseShellExecute = true
|
|
});
|
|
}
|
|
catch {
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void OpenAccountSettings() {
|
|
if (_pendingOauth is not null) {
|
|
AccountSettingsRequested?.Invoke();
|
|
return;
|
|
}
|
|
|
|
if (SelectedMailbox is null) {
|
|
NewMailbox();
|
|
return;
|
|
}
|
|
|
|
FillEditor(SelectedMailbox);
|
|
AccountSettingsRequested?.Invoke();
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void SetLanguage(string? id) {
|
|
var language = UiLanguage.Normalize(id);
|
|
var configuration = _files.Current;
|
|
configuration.Language = language;
|
|
_files.Save(configuration);
|
|
ApplyLanguage();
|
|
}
|
|
|
|
private void ApplyLanguage() {
|
|
var previous = Copy;
|
|
UiLocale.Apply(_files.Current.Language);
|
|
Copy = UiLocale.Copy;
|
|
if (string.IsNullOrWhiteSpace(Status) || Status == previous.AddMailboxThenGet)
|
|
Status = Copy.AddMailboxThenGet;
|
|
RelocalizeFolders();
|
|
RefreshFolderStats();
|
|
UpdateAuthUi();
|
|
EditorIncomingSecurity = ChoiceRow.Security(EditorIncomingSecurity.Id);
|
|
EditorSmtpSecurity = ChoiceRow.Security(EditorSmtpSecurity.Id);
|
|
EditorCertifiedKind = ChoiceRow.Certified(EditorCertifiedKind.Id);
|
|
OnPropertyChanged(nameof(SecurityModes));
|
|
OnPropertyChanged(nameof(CertifiedKinds));
|
|
OnPropertyChanged(nameof(SelectedLanguage));
|
|
OnPropertyChanged(nameof(IsEnglish));
|
|
OnPropertyChanged(nameof(IsItalian));
|
|
OnPropertyChanged(nameof(IsFrench));
|
|
OnPropertyChanged(nameof(IsGerman));
|
|
OnPropertyChanged(nameof(IsSpanish));
|
|
OnPropertyChanged(nameof(LayoutToggleLabel));
|
|
NotifyFeatures();
|
|
foreach (var row in Messages)
|
|
row.RefreshMarks();
|
|
}
|
|
|
|
private void RelocalizeFolders() {
|
|
foreach (var rows in _foldersByMailbox.Values) {
|
|
foreach (var folder in rows)
|
|
folder.Name = MailFolderRole.DisplayName(null, folder.FullName);
|
|
}
|
|
|
|
NotifyFolderCounts();
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void EditMailbox() =>
|
|
OpenAccountSettings();
|
|
|
|
[RelayCommand]
|
|
private void SaveMailbox() =>
|
|
WriteMailbox(closeSettings: true);
|
|
|
|
private void WriteMailbox(bool closeSettings) {
|
|
if (ShowPstFields) {
|
|
if (string.IsNullOrWhiteSpace(EditorStorePath) || !File.Exists(EditorStorePath.Trim())) {
|
|
Status = Copy.PstPathRequired;
|
|
return;
|
|
}
|
|
}
|
|
else if (string.IsNullOrWhiteSpace(EditorIncomingHost) || string.IsNullOrWhiteSpace(EditorAddress)) {
|
|
Status = "Address and incoming host are required.";
|
|
return;
|
|
}
|
|
|
|
var configuration = _files.Current;
|
|
configuration.EnsureDefaults();
|
|
var box = MailboxToSave();
|
|
box.DisplayName = EditorName.Trim();
|
|
box.Address = EditorAddress.Trim();
|
|
box.Username = EditorUsername.Trim();
|
|
if (ShowPstFields) {
|
|
var path = EditorStorePath.Trim();
|
|
box.Provider = MailProvider.Pst;
|
|
box.IncomingProtocol = MailProtocol.Pst;
|
|
box.StorePath = path;
|
|
box.ImapHost = "";
|
|
box.AuthKind = MailAuthKind.Password;
|
|
box.CertifiedKind = MailCertifiedKind.Ordinary;
|
|
if (string.IsNullOrWhiteSpace(box.Address))
|
|
box.Address = Path.GetFileNameWithoutExtension(path);
|
|
if (string.IsNullOrWhiteSpace(box.DisplayName))
|
|
box.DisplayName = Path.GetFileName(path);
|
|
}
|
|
else {
|
|
box.IncomingProtocol = EditorIncomingProtocol.Id;
|
|
box.IncomingSecurity = EditorIncomingSecurity.Id;
|
|
box.ImapHost = EditorIncomingHost.Trim();
|
|
box.StorePath = "";
|
|
box.ImapPort = EditorIncomingPort <= 0
|
|
? MailSecurity.DefaultIncomingPort(box.IncomingProtocol, box.IncomingSecurity)
|
|
: (int)EditorIncomingPort;
|
|
box.SmtpSecurity = EditorSmtpSecurity.Id;
|
|
box.SmtpHost = string.IsNullOrWhiteSpace(EditorSmtpHost) ? EditorIncomingHost.Trim() : EditorSmtpHost.Trim();
|
|
box.SmtpPort = EditorSmtpPort <= 0
|
|
? MailSecurity.DefaultSmtpPort(box.SmtpSecurity)
|
|
: (int)EditorSmtpPort;
|
|
box.Provider = MailProvider.Normalize(EditorProvider.Id);
|
|
box.CertifiedKind = EditorCertifiedKind.Id;
|
|
if (_pendingOauth is not null)
|
|
box.AuthKind = MailAuthKind.Normalize(_pendingOauth.Provider);
|
|
else if (MailProvider.UsesOAuth(box.Provider) && _auth.HasTokens(box.Id))
|
|
box.AuthKind = MailAuthKind.Normalize(
|
|
box.Provider == MailProvider.Outlook ? MailAuthKind.Microsoft : MailAuthKind.Google);
|
|
else
|
|
box.AuthKind = MailAuthKind.Password;
|
|
MailProvider.Apply(box);
|
|
}
|
|
if (configuration.FindMailbox(box.Id) is null)
|
|
configuration.Mailboxes.Add(box);
|
|
configuration.SelectedMailboxId = box.Id;
|
|
PersistClientIds(configuration);
|
|
PersistGoogleClientSecret();
|
|
_files.Save(configuration);
|
|
if (!string.IsNullOrWhiteSpace(EditorPassword))
|
|
_secrets.Put(FileSecretStore.MailboxKey(box.Id), EditorPassword);
|
|
if (_pendingOauth is not null) {
|
|
_auth.SaveTokens(box.Id, _pendingOauth);
|
|
_pendingOauth = null;
|
|
}
|
|
ReloadMailboxes();
|
|
SelectedMailbox = Mailboxes.FirstOrDefault(m => m.Id == box.Id);
|
|
Status = "Mailbox saved. Secrets stay in the OS secret file, not settings.json.";
|
|
if (!closeSettings)
|
|
return;
|
|
AccountSettingsOpen = false;
|
|
AccountSaved?.Invoke();
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task RemoveMailboxAsync() {
|
|
if (SelectedMailbox is null)
|
|
return;
|
|
await RemoveMailboxCoreAsync(SelectedMailbox.Id);
|
|
Status = "Mailbox removed.";
|
|
}
|
|
|
|
[RelayCommand(CanExecute = nameof(CanDetachPst))]
|
|
private async Task DetachPstAsync() {
|
|
var box = SelectedFolderNode?.Mailbox;
|
|
if (box is null || !box.IsPstStore)
|
|
return;
|
|
var label = box.Label;
|
|
await RemoveMailboxCoreAsync(box.Id);
|
|
Status = string.Format(Copy.DetachPstDone, label);
|
|
}
|
|
|
|
private bool CanDetachPst() =>
|
|
ShowDetachPst;
|
|
|
|
private async Task RemoveMailboxCoreAsync(string id) {
|
|
var configuration = _files.Current;
|
|
configuration.Mailboxes.RemoveAll(m => m.Id == id);
|
|
if (configuration.SelectedMailboxId == id)
|
|
configuration.SelectedMailboxId = configuration.Mailboxes.FirstOrDefault()?.Id;
|
|
foreach (var rule in configuration.Rules ?? []) {
|
|
if (rule.MailboxId.Equals(id, StringComparison.OrdinalIgnoreCase))
|
|
rule.MailboxId = "";
|
|
if (rule.FolderMailboxId.Equals(id, StringComparison.OrdinalIgnoreCase))
|
|
rule.FolderMailboxId = "";
|
|
}
|
|
|
|
_files.Save(configuration);
|
|
_secrets.Delete(FileSecretStore.MailboxKey(id));
|
|
_auth.DeleteTokens(id);
|
|
await DisconnectMailboxAsync(id);
|
|
ReloadMailboxes();
|
|
AccountSaved?.Invoke();
|
|
}
|
|
|
|
[RelayCommand(CanExecute = nameof(CanGetMessages))]
|
|
private async Task GetMessagesAsync() {
|
|
if (ActiveSession is { IsConnected: true })
|
|
await RefreshAsync();
|
|
else
|
|
await ConnectAsync();
|
|
}
|
|
|
|
[RelayCommand(CanExecute = nameof(CanConnect))]
|
|
private async Task ConnectAsync() {
|
|
if (SelectedMailbox is null)
|
|
return;
|
|
var box = SelectedMailbox;
|
|
if (SessionFor(box) is not { IsConnected: true }) {
|
|
var password = PasswordFor(box);
|
|
var oauth = MailAuthKind.IsOAuth(box.AuthKind) || _auth.HasTokens(box.Id);
|
|
if (!box.IsPstStore && string.IsNullOrWhiteSpace(password) && !oauth) {
|
|
Status = "Enter the password once in Account Settings, then Get Messages.";
|
|
if (!AccountSettingsOpen) {
|
|
FillEditor(box);
|
|
AccountSettingsRequested?.Invoke();
|
|
}
|
|
return;
|
|
}
|
|
|
|
await RunAsync(Copy.Connecting, async token => {
|
|
var ok = await ConnectMailboxCoreAsync(box, password, token);
|
|
if (!ok)
|
|
return;
|
|
if (!string.IsNullOrWhiteSpace(EditorPassword) && SelectedMailbox?.Id == box.Id)
|
|
_secrets.Put(FileSecretStore.MailboxKey(box.Id), EditorPassword);
|
|
if (_auth.HasTokens(box.Id) && !MailAuthKind.IsOAuth(box.AuthKind))
|
|
box.AuthKind = MailProvider.Normalize(box.Provider) == MailProvider.Outlook
|
|
? MailAuthKind.Microsoft
|
|
: MailAuthKind.Google;
|
|
var configuration = _files.Current;
|
|
configuration.SelectedMailboxId = box.Id;
|
|
_files.Save(configuration);
|
|
Status = "Connected to " + box.Label + ".";
|
|
});
|
|
}
|
|
else {
|
|
SyncFoldersCollection(box);
|
|
RebuildFolderTree();
|
|
}
|
|
|
|
_ = ConnectIdleMailboxesAsync();
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task RefreshAsync() {
|
|
if (SelectedFolder is null)
|
|
return;
|
|
await RunAsync(
|
|
Copy.GettingMessages,
|
|
token => LoadMessagesAsync(SelectedFolder.FullName, token, resumeBodies: true));
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void OpenArchiveFolder() {
|
|
AppPaths.EnsureDirectories();
|
|
try {
|
|
Process.Start(new ProcessStartInfo {
|
|
FileName = AppPaths.DataDirectory(),
|
|
UseShellExecute = true
|
|
});
|
|
}
|
|
catch (Exception ex) {
|
|
Status = ex.Message;
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void ExportArchive() =>
|
|
ExportArchiveRequested?.Invoke();
|
|
|
|
public void ExportArchiveTo(string destDir) {
|
|
try {
|
|
_archive.Checkpoint();
|
|
ArchiveFiles.ExportTo(destDir);
|
|
Status = "Exported archive to " + destDir + ". " + ArchiveFiles.Hint();
|
|
}
|
|
catch (Exception ex) {
|
|
Status = ex.Message;
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void ExportFascicolo() =>
|
|
ExportFascicoloRequested?.Invoke();
|
|
|
|
[RelayCommand(CanExecute = nameof(HasOpenMessage))]
|
|
private void PrintMessage() {
|
|
if (_reading is null)
|
|
return;
|
|
var html = MessagePrint.Html(_reading, UnwrapEnvelope);
|
|
var path = Path.Combine(Path.GetTempPath(), "postclient-print.html");
|
|
File.WriteAllText(path, html);
|
|
PrintHtmlRequested?.Invoke(path);
|
|
}
|
|
|
|
[RelayCommand(CanExecute = nameof(HasOpenMessage))]
|
|
private void SaveMessagePdf() {
|
|
if (_reading is null)
|
|
return;
|
|
SavePdfRequested?.Invoke(MessagePrint.Pdf(_reading, UnwrapEnvelope), MessagePrint.EmlFileName(_reading.Header) + ".pdf");
|
|
}
|
|
|
|
[RelayCommand(CanExecute = nameof(HasReadingAttachments))]
|
|
private void SaveAttachmentsZip() {
|
|
if (ReadingFiles.Count == 0)
|
|
return;
|
|
var name = AttachmentZip.SuggestedName(_reading?.Header.Subject);
|
|
SaveAttachmentsZipRequested?.Invoke(AttachmentZip.FromFiles(ReadingFiles), name);
|
|
Status = "Save the ZIP on this PC — originals stay in the message.";
|
|
}
|
|
|
|
private bool HasReadingAttachments() =>
|
|
HasReadingFiles;
|
|
|
|
[RelayCommand]
|
|
private void ImportEml() =>
|
|
ImportEmlRequested?.Invoke();
|
|
|
|
[RelayCommand]
|
|
private void ImportPst() =>
|
|
ImportPstRequested?.Invoke();
|
|
|
|
[RelayCommand]
|
|
private void AttachPst() =>
|
|
AttachPstRequested?.Invoke();
|
|
|
|
[RelayCommand]
|
|
private void CreatePst() =>
|
|
CreatePstRequested?.Invoke();
|
|
|
|
[RelayCommand]
|
|
private void ImportThunderbird() =>
|
|
ImportThunderbirdRequested?.Invoke();
|
|
|
|
[RelayCommand]
|
|
private void ApplyPracticeLabel() {
|
|
if (SelectedMailbox is null || SelectedFolder is null || PracticeLabel.Trim().Length == 0)
|
|
return;
|
|
var name = PracticeLabel.Trim();
|
|
foreach (var row in TargetRows()) {
|
|
_archive.AddLabel(SelectedMailbox.Id, row.Header.Folder, row.Header.Id, name);
|
|
var labels = string.IsNullOrWhiteSpace(row.Header.Labels)
|
|
? name
|
|
: row.Header.Labels + ", " + name;
|
|
row.Header.Labels = labels;
|
|
row.RefreshMarks();
|
|
}
|
|
|
|
Status = "Label “" + name + "” stays on this PC — not a shared studio table.";
|
|
RebuildVisible();
|
|
}
|
|
|
|
private bool HasOpenMessage() =>
|
|
_reading is not null;
|
|
|
|
public async Task ExportFascicoloToAsync(string destDir) {
|
|
if (SelectedMailbox is null)
|
|
return;
|
|
Directory.CreateDirectory(destDir);
|
|
var rows = TargetRows();
|
|
if (rows.Count == 0 && SelectedMessage is not null)
|
|
rows = [SelectedMessage];
|
|
var copied = 0;
|
|
var index = new List<string>();
|
|
foreach (var row in rows) {
|
|
var eml = _archive.EmlPath(SelectedMailbox.Id, row.Header.Folder, row.Header.Id);
|
|
if (string.IsNullOrWhiteSpace(eml) || !File.Exists(eml)) {
|
|
if (_reading is { } body && body.Header.Id == row.Header.Id && body.RawEml.Length > 0) {
|
|
eml = Path.Combine(destDir, MessagePrint.EmlFileName(row.Header));
|
|
await File.WriteAllBytesAsync(eml, body.RawEml);
|
|
}
|
|
else
|
|
continue;
|
|
}
|
|
else {
|
|
var dest = Path.Combine(destDir, MessagePrint.EmlFileName(row.Header));
|
|
File.Copy(eml, dest, overwrite: true);
|
|
eml = dest;
|
|
}
|
|
|
|
copied++;
|
|
index.Add(row.Header.Date.ToLocalTime().ToString("yyyy-MM-dd HH:mm") + " " + row.Header.Subject);
|
|
}
|
|
|
|
if (_reading is not null)
|
|
await File.WriteAllBytesAsync(Path.Combine(destDir, "messaggio.pdf"), MessagePrint.Pdf(_reading, UnwrapEnvelope));
|
|
await File.WriteAllLinesAsync(Path.Combine(destDir, "index.txt"), index);
|
|
Status = "Fascicolo: " + copied + " .eml in " + destDir + " (this PC only).";
|
|
}
|
|
|
|
public async Task ImportEmlPathsAsync(IReadOnlyList<string> paths) {
|
|
if (SelectedMailbox is null) {
|
|
Status = "Select a mailbox first.";
|
|
return;
|
|
}
|
|
|
|
var folder = SelectedFolder?.FullName ?? "Imported";
|
|
var import = new LocalMailImport(_archive);
|
|
var count = await import.ImportEmlFilesAsync(
|
|
SelectedMailbox.Id,
|
|
folder,
|
|
paths,
|
|
ActiveSession,
|
|
UnwrapEnvelope,
|
|
CancellationToken.None,
|
|
ActiveRules(),
|
|
FoldersForRules(SelectedMailbox.Id));
|
|
Status = "Imported " + count + " .eml into " + folder + ".";
|
|
if (SelectedFolder is not null)
|
|
await LoadMessagesAsync(SelectedFolder.FullName, CancellationToken.None);
|
|
}
|
|
|
|
public async Task ImportPstPathAsync(string path) {
|
|
if (SelectedMailbox is null) {
|
|
Status = "Select a mailbox first.";
|
|
return;
|
|
}
|
|
|
|
var import = new LocalMailImport(_archive);
|
|
var count = await PstImport.ImportAsync(
|
|
import,
|
|
SelectedMailbox.Id,
|
|
path,
|
|
ActiveSession,
|
|
UnwrapEnvelope,
|
|
CancellationToken.None,
|
|
ActiveRules(),
|
|
FoldersForRules(SelectedMailbox.Id));
|
|
Status = "Imported " + count + " messages from PST.";
|
|
if (SelectedFolder is not null)
|
|
await LoadMessagesAsync(SelectedFolder.FullName, CancellationToken.None);
|
|
}
|
|
|
|
public async Task AttachPstPathAsync(string path) {
|
|
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) {
|
|
Status = Copy.PstPathRequired;
|
|
return;
|
|
}
|
|
|
|
var configuration = _files.Current;
|
|
configuration.EnsureDefaults();
|
|
var existing = configuration.Mailboxes.FirstOrDefault(m =>
|
|
m.IsPstStore && m.DataFile.Equals(path, StringComparison.OrdinalIgnoreCase));
|
|
var box = existing ?? new MailboxAccount {
|
|
DisplayName = Path.GetFileName(path),
|
|
Address = Path.GetFileNameWithoutExtension(path),
|
|
StorePath = path,
|
|
IncomingProtocol = MailProtocol.Pst,
|
|
Provider = MailProvider.Pst
|
|
};
|
|
if (existing is null)
|
|
configuration.Mailboxes.Add(box);
|
|
else
|
|
box.StorePath = path;
|
|
configuration.SelectedMailboxId = box.Id;
|
|
_files.Save(configuration);
|
|
ReloadMailboxes();
|
|
SelectedMailbox = Mailboxes.FirstOrDefault(m => m.Id == box.Id);
|
|
Status = "Attached " + box.Label + " as a mailbox.";
|
|
await ConnectAsync();
|
|
}
|
|
|
|
public async Task CreatePstPathAsync(string path) {
|
|
if (string.IsNullOrWhiteSpace(path))
|
|
return;
|
|
try {
|
|
var folder = Path.GetDirectoryName(path);
|
|
if (!string.IsNullOrEmpty(folder))
|
|
Directory.CreateDirectory(folder);
|
|
if (File.Exists(path))
|
|
File.Delete(path);
|
|
var name = Path.GetFileNameWithoutExtension(path);
|
|
OutlookPst.CreateEmpty(path, string.IsNullOrWhiteSpace(name) ? "Postclient" : name);
|
|
}
|
|
catch (Exception ex) {
|
|
Status = ex.Message;
|
|
return;
|
|
}
|
|
|
|
await AttachPstPathAsync(path);
|
|
if (SelectedMailbox is { IsPstStore: true } box)
|
|
Status = string.Format(Copy.CreatePstDone, box.Label);
|
|
}
|
|
|
|
public async Task ImportThunderbirdAsync() {
|
|
if (SelectedMailbox is null) {
|
|
Status = "Select a mailbox first.";
|
|
return;
|
|
}
|
|
|
|
var import = new LocalMailImport(_archive);
|
|
var count = await import.ImportThunderbirdAsync(
|
|
SelectedMailbox.Id,
|
|
ActiveSession,
|
|
UnwrapEnvelope,
|
|
CancellationToken.None,
|
|
ActiveRules(),
|
|
FoldersForRules(SelectedMailbox.Id));
|
|
Status = count == 0
|
|
? "No Thunderbird mbox files found."
|
|
: "Imported " + count + " messages from Thunderbird.";
|
|
if (SelectedFolder is not null)
|
|
await LoadMessagesAsync(SelectedFolder.FullName, CancellationToken.None);
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void StartCompose() =>
|
|
OpenCompose("Write: " + (SelectedMailbox?.Address ?? ""), null, null, "", "");
|
|
|
|
[RelayCommand(CanExecute = nameof(CanReply))]
|
|
private void Reply() {
|
|
OpenCompose(
|
|
"Reply",
|
|
Recipients(ReadingFrom),
|
|
null,
|
|
ReplySubject(),
|
|
QuotedBody());
|
|
}
|
|
|
|
[RelayCommand(CanExecute = nameof(CanReply))]
|
|
private void ReplyAll() {
|
|
var self = SelectedMailbox?.Address ?? "";
|
|
var to = DistinctRecipients(Recipients(ReadingFrom).Concat(Recipients(ReadingTo)), self);
|
|
var toSet = new HashSet<string>(to.Select(r => r.Address), StringComparer.OrdinalIgnoreCase);
|
|
var cc = DistinctRecipients(Recipients(ReadingCc), self)
|
|
.Where(r => !toSet.Contains(r.Address))
|
|
.ToList();
|
|
OpenCompose("Reply All", to, cc, ReplySubject(), QuotedBody());
|
|
}
|
|
|
|
[RelayCommand(CanExecute = nameof(CanReply))]
|
|
private void Forward() {
|
|
var subject = SelectedMessage is null
|
|
? ""
|
|
: (SelectedMessage.Header.Subject.StartsWith("Fwd:", StringComparison.OrdinalIgnoreCase)
|
|
? SelectedMessage.Header.Subject
|
|
: "Fwd: " + SelectedMessage.Header.Subject);
|
|
OpenCompose("Forward", null, null, subject, QuotedBody());
|
|
}
|
|
|
|
[RelayCommand(CanExecute = nameof(CanReply))]
|
|
private void OpenMessageWindow(MessageRowViewModel? row) {
|
|
row ??= SelectedMessage;
|
|
if (row is null)
|
|
return;
|
|
if (IsLoadedBody(row.Header) && _reading is not null) {
|
|
if (!row.Header.IsSeen)
|
|
_ = MarkOpenedReadAsync(row.Header, CancellationToken.None);
|
|
ShowMessageWindow(_reading);
|
|
return;
|
|
}
|
|
|
|
_pendingWindow = row;
|
|
if (!ReferenceEquals(SelectedMessage, row))
|
|
SelectedMessage = row;
|
|
}
|
|
|
|
[RelayCommand(CanExecute = nameof(CanSignIn))]
|
|
private async Task SignInGoogleAsync() =>
|
|
await SignInProviderAsync(MailAuthKind.Google);
|
|
|
|
[RelayCommand(CanExecute = nameof(CanSignIn))]
|
|
private async Task SignInMicrosoftAsync() =>
|
|
await SignInProviderAsync(MailAuthKind.Microsoft);
|
|
|
|
public void SetSelectedMessages(IEnumerable<MessageRowViewModel> rows) {
|
|
SelectedMessages.Clear();
|
|
foreach (var row in rows)
|
|
SelectedMessages.Add(row);
|
|
NotifyMessageCommands();
|
|
}
|
|
|
|
public void RestoreMessageGridSelection() =>
|
|
MessageListSelectionRestoreRequested?.Invoke();
|
|
|
|
[RelayCommand(CanExecute = nameof(CanOrganize))]
|
|
private async Task MoveToFolderAsync(FolderRowViewModel? folder) {
|
|
if (folder is null)
|
|
return;
|
|
await MoveRowsAsync(TargetRows(), folder.FullName);
|
|
}
|
|
|
|
public bool CanDropMessagesOn(FolderNodeViewModel? node, string sourceMailboxId, string fromFolder) {
|
|
if (node?.Mailbox is null)
|
|
return false;
|
|
return MailDrop.CanDropMessages(
|
|
node.Mailbox.IncomingProtocol,
|
|
node.Mailbox.Id,
|
|
node.Folder?.FullName,
|
|
sourceMailboxId,
|
|
fromFolder);
|
|
}
|
|
|
|
public bool CanDropFolderOn(FolderNodeViewModel? node, string sourceMailboxId, string sourceFolder) {
|
|
if (node?.Mailbox is null || string.IsNullOrWhiteSpace(sourceFolder))
|
|
return false;
|
|
var row = FoldersFor(MailboxById(sourceMailboxId))
|
|
.FirstOrDefault(f => f.FullName.Equals(sourceFolder, StringComparison.OrdinalIgnoreCase));
|
|
var destParent = node.IsAccount || node.Folder is null ? null : node.Folder.FullName;
|
|
return MailDrop.CanDropFolder(
|
|
node.Mailbox.IncomingProtocol,
|
|
node.Mailbox.Id,
|
|
destParent,
|
|
sourceMailboxId,
|
|
sourceFolder,
|
|
MailFolderRole.IsCustom(row?.Name ?? MailFolderPath.Leaf(sourceFolder), sourceFolder));
|
|
}
|
|
|
|
public Task DropMessagesAsync(
|
|
FolderNodeViewModel dest,
|
|
string sourceMailboxId,
|
|
string fromFolder,
|
|
IReadOnlyList<uint> ids) {
|
|
if (dest.Mailbox is null)
|
|
return Task.CompletedTask;
|
|
return MoveIdsAsync(
|
|
ids,
|
|
dest.Folder?.FullName ?? "",
|
|
fromFolder,
|
|
destMailboxId: dest.Mailbox.Id,
|
|
sourceMailboxId: sourceMailboxId);
|
|
}
|
|
|
|
public async Task DropFolderAsync(
|
|
FolderNodeViewModel dest,
|
|
string sourceMailboxId,
|
|
string sourceFolder) {
|
|
if (dest.Mailbox is null || !CanDropFolderOn(dest, sourceMailboxId, sourceFolder))
|
|
return;
|
|
var sourceBox = MailboxById(sourceMailboxId);
|
|
var destBox = dest.Mailbox;
|
|
if (sourceBox is null)
|
|
return;
|
|
var destParent = dest.IsAccount || dest.Folder is null ? null : dest.Folder.FullName;
|
|
var openFolder = SelectedMailbox?.Id == sourceBox.Id ? SelectedFolder?.FullName : null;
|
|
await RunAsync(Copy.Moving, async token => {
|
|
var sourceSession = await EnsureMailboxReadyAsync(sourceBox, token);
|
|
var destSession = destBox.Id.Equals(sourceBox.Id, StringComparison.OrdinalIgnoreCase)
|
|
? sourceSession
|
|
: await EnsureMailboxReadyAsync(destBox, token);
|
|
if (sourceSession is not { IsConnected: true, SupportsFolders: true }
|
|
|| destSession is not { IsConnected: true, SupportsFolders: true }) {
|
|
Status = Copy.CannotMoveSystemFolder;
|
|
return;
|
|
}
|
|
|
|
var leaf = MailFolderPath.UniqueLeaf(
|
|
MailFolderPath.Leaf(sourceFolder),
|
|
SiblingNames(destBox, destParent, sourceFolder));
|
|
if (destBox.Id.Equals(sourceBox.Id, StringComparison.OrdinalIgnoreCase)) {
|
|
var renamed = await sourceSession.RenameFolderAsync(sourceFolder, destParent, leaf, token);
|
|
if (!renamed.IsSuccess) {
|
|
Status = string.Join(" ", renamed.Messages);
|
|
return;
|
|
}
|
|
|
|
await LoadFoldersAsync(sourceBox, sourceSession, token);
|
|
var destPath = FindMovedFolder(sourceBox, destParent, leaf) ?? MailFolderPath.Combine(destParent, leaf);
|
|
RememberMovedFolder(sourceBox.Id, sourceFolder, destPath, null);
|
|
await SelectAfterFolderMoveAsync(sourceBox, sourceFolder, destPath, openFolder, token);
|
|
Status = Copy.FolderMoved;
|
|
return;
|
|
}
|
|
|
|
var created = await destSession.CreateFolderAsync(leaf, destParent, token);
|
|
if (!created.IsSuccess) {
|
|
Status = string.Join(" ", created.Messages);
|
|
return;
|
|
}
|
|
|
|
await LoadFoldersAsync(destBox, destSession, token);
|
|
var destRoot = FindMovedFolder(destBox, destParent, leaf) ?? MailFolderPath.Combine(destParent, leaf);
|
|
var slice = FolderSlice(sourceBox, sourceFolder);
|
|
var failed = 0;
|
|
var copied = 0;
|
|
foreach (var folder in slice) {
|
|
var destPath = MailFolderPath.Rewrite(folder.FullName, sourceFolder, destRoot) ?? destRoot;
|
|
if (!folder.FullName.Equals(sourceFolder, StringComparison.OrdinalIgnoreCase)) {
|
|
var nested = await destSession.CreateFolderAsync(
|
|
MailFolderPath.Leaf(destPath),
|
|
MailFolderPath.Parent(destPath),
|
|
token);
|
|
if (!nested.IsSuccess) {
|
|
failed++;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
foreach (var id in await FolderUidsAsync(sourceBox, sourceSession, folder.FullName, token)) {
|
|
if (await CopyUidToMailboxAsync(sourceBox, sourceSession, destBox, folder.FullName, destPath, id, token))
|
|
copied++;
|
|
else
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
if (failed > 0) {
|
|
await LoadFoldersAsync(destBox, destSession, token);
|
|
Status = string.Format(Copy.MovedMessages, copied);
|
|
return;
|
|
}
|
|
|
|
foreach (var folder in slice.OrderByDescending(f => f.FullName.Length)) {
|
|
var deleted = await sourceSession.DeleteFolderAsync(folder.FullName, token);
|
|
if (deleted.IsSuccess)
|
|
ForgetLocalFolder(sourceBox.Id, folder.FullName);
|
|
}
|
|
|
|
RememberMovedFolder(sourceBox.Id, sourceFolder, destRoot, destBox.Id);
|
|
await LoadFoldersAsync(sourceBox, sourceSession, token);
|
|
await LoadFoldersAsync(destBox, destSession, token);
|
|
await SelectAfterFolderMoveAsync(sourceBox, sourceFolder, destRoot, openFolder, token, destBox);
|
|
Status = Copy.FolderMoved;
|
|
});
|
|
}
|
|
|
|
[RelayCommand(CanExecute = nameof(CanOrganize))]
|
|
private async Task MarkReadAsync() =>
|
|
await ApplyFlagsAsync(new MailFlagUpdate { Seen = true }, row => row.Header.IsSeen = true);
|
|
|
|
[RelayCommand(CanExecute = nameof(CanOrganize))]
|
|
private async Task MarkUnreadAsync() =>
|
|
await ApplyFlagsAsync(new MailFlagUpdate { Seen = false }, row => row.Header.IsSeen = false);
|
|
|
|
[RelayCommand(CanExecute = nameof(CanOrganize))]
|
|
private async Task ToggleFlagAsync(MessageRowViewModel? row) {
|
|
var rows = row is null ? TargetRows() : [row];
|
|
if (rows.Count == 0)
|
|
return;
|
|
var flag = !rows[0].Header.IsFlagged;
|
|
await ApplyFlagsAsync(
|
|
new MailFlagUpdate { Flagged = flag },
|
|
r => r.Header.IsFlagged = flag,
|
|
rows);
|
|
}
|
|
|
|
[RelayCommand(CanExecute = nameof(CanOrganize))]
|
|
private async Task SetPriorityAsync(string? priority) {
|
|
if (string.IsNullOrWhiteSpace(priority))
|
|
return;
|
|
await ApplyFlagsAsync(
|
|
new MailFlagUpdate { Priority = priority },
|
|
row => row.Header.Priority = priority);
|
|
}
|
|
|
|
[RelayCommand(CanExecute = nameof(CanOrganize))]
|
|
private async Task DeleteMessagesAsync() {
|
|
var rows = TargetRows();
|
|
if (rows.Count == 0)
|
|
return;
|
|
var trash = Folders.Concat(FoldersFor(SelectedMailbox))
|
|
.FirstOrDefault(f => MailFolderRole.Kind(f.Name, f.FullName) == "trash");
|
|
if (trash is not null
|
|
&& SelectedFolder is not null
|
|
&& !trash.FullName.Equals(SelectedFolder.FullName, StringComparison.OrdinalIgnoreCase)) {
|
|
await MoveRowsAsync(rows, trash.FullName);
|
|
return;
|
|
}
|
|
|
|
if (!await ApplyFlagsAsync(new MailFlagUpdate { Deleted = true }, null, rows))
|
|
return;
|
|
if (SelectedMailbox is null || SelectedFolder is null)
|
|
return;
|
|
DropArchived(SelectedMailbox.Id, SelectedFolder.FullName, TargetIds(rows));
|
|
RemoveRows(rows);
|
|
Status = "Deleted.";
|
|
}
|
|
|
|
[RelayCommand(CanExecute = nameof(CanCreateFolder))]
|
|
private async Task CreateFolderAsync() {
|
|
var session = ActiveSession;
|
|
var box = SelectedMailbox;
|
|
if (session is not { IsConnected: true, SupportsFolders: true } || box is null)
|
|
return;
|
|
var name = await PromptAsync(Copy.NewFolder, Copy.FolderName, confirmOnly: false, Copy.FolderName);
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
return;
|
|
var parent = SelectedFolder is { } folder && MailFolderRole.IsCustom(folder.Name, folder.FullName)
|
|
? folder.FullName
|
|
: null;
|
|
await RunAsync(Copy.CreatingFolder, async token => {
|
|
var result = await session.CreateFolderAsync(name.Trim(), parent, token);
|
|
if (!result.IsSuccess) {
|
|
Status = string.Join(" ", result.Messages);
|
|
return;
|
|
}
|
|
|
|
await LoadFoldersAsync(box, session, token);
|
|
Status = Copy.FolderCreated;
|
|
});
|
|
}
|
|
|
|
[RelayCommand(CanExecute = nameof(CanEmptyFolder))]
|
|
private async Task EmptyFolderAsync() {
|
|
var session = ActiveSession;
|
|
var box = SelectedMailbox;
|
|
var folder = SelectedFolder;
|
|
if (session is not { IsConnected: true, SupportsFolders: true } || box is null || folder is null)
|
|
return;
|
|
var trash = Folders.Concat(FoldersFor(SelectedMailbox))
|
|
.FirstOrDefault(f => MailFolderRole.Kind(f.Name, f.FullName) == "trash");
|
|
var purge = trash is null
|
|
|| trash.FullName.Equals(folder.FullName, StringComparison.OrdinalIgnoreCase);
|
|
var confirm = string.Format(purge ? Copy.EmptyTrashConfirm : Copy.EmptyFolderConfirm, folder.Name);
|
|
if (!await ConfirmAsync(Copy.EmptyFolder, confirm))
|
|
return;
|
|
await RunAsync(Copy.EmptyingFolder, async token => {
|
|
var result = await session.EmptyFolderAsync(
|
|
folder.FullName,
|
|
purge ? null : trash!.FullName,
|
|
token);
|
|
if (!result.IsSuccess) {
|
|
Status = string.Join(" ", result.Messages);
|
|
return;
|
|
}
|
|
|
|
ForgetLocalFolder(box.Id, folder.FullName);
|
|
folder.Total = 0;
|
|
folder.Unread = 0;
|
|
if (SelectedFolder?.FullName.Equals(folder.FullName, StringComparison.OrdinalIgnoreCase) == true) {
|
|
Messages.Clear();
|
|
VisibleMessages.Clear();
|
|
SelectedMessage = null;
|
|
ClearReading();
|
|
RefreshFolderStats();
|
|
}
|
|
|
|
NotifyFolderCounts();
|
|
Status = Copy.FolderEmptied;
|
|
});
|
|
}
|
|
|
|
[RelayCommand(CanExecute = nameof(CanEmptyFolder))]
|
|
private async Task MarkFolderReadAsync() =>
|
|
await MarkFolderSeenAsync(true);
|
|
|
|
[RelayCommand(CanExecute = nameof(CanEmptyFolder))]
|
|
private async Task MarkFolderUnreadAsync() =>
|
|
await MarkFolderSeenAsync(false);
|
|
|
|
[RelayCommand(CanExecute = nameof(CanDeleteCustomFolder))]
|
|
private async Task DeleteCustomFolderAsync() {
|
|
var session = ActiveSession;
|
|
var box = SelectedMailbox;
|
|
var folder = SelectedFolder;
|
|
if (session is not { IsConnected: true, SupportsFolders: true } || box is null || folder is null)
|
|
return;
|
|
if (!MailFolderRole.IsCustom(folder.Name, folder.FullName)) {
|
|
Status = Copy.CannotDeleteSystemFolder;
|
|
return;
|
|
}
|
|
|
|
var confirm = string.Format(Copy.DeleteFolderConfirm, folder.Name);
|
|
if (!await ConfirmAsync(Copy.DeleteFolder, confirm))
|
|
return;
|
|
var fullName = folder.FullName;
|
|
var open = SelectedFolder?.FullName.Equals(fullName, StringComparison.OrdinalIgnoreCase) == true;
|
|
await RunAsync(Copy.DeletingFolder, async token => {
|
|
var result = await session.DeleteFolderAsync(fullName, token);
|
|
if (!result.IsSuccess) {
|
|
Status = string.Join(" ", result.Messages);
|
|
return;
|
|
}
|
|
|
|
ForgetLocalFolder(box.Id, fullName);
|
|
if (open) {
|
|
Messages.Clear();
|
|
VisibleMessages.Clear();
|
|
SelectedMessage = null;
|
|
ClearReading();
|
|
}
|
|
|
|
await LoadFoldersAsync(box, session, token);
|
|
Status = Copy.FolderDeleted;
|
|
});
|
|
}
|
|
|
|
private async Task MarkFolderSeenAsync(bool seen) {
|
|
var session = ActiveSession;
|
|
var box = SelectedMailbox;
|
|
var folder = SelectedFolder;
|
|
if (session is not { IsConnected: true, SupportsFolders: true } || box is null || folder is null)
|
|
return;
|
|
await RunAsync(Copy.UpdatingFlags, async token => {
|
|
var result = await session.SetFolderSeenAsync(folder.FullName, seen, token);
|
|
if (!result.IsSuccess) {
|
|
Status = string.Join(" ", result.Messages);
|
|
return;
|
|
}
|
|
|
|
_archive.SetSeen(box.Id, folder.FullName, seen);
|
|
foreach (var row in Messages) {
|
|
row.Header.IsSeen = seen;
|
|
row.RefreshMarks();
|
|
}
|
|
|
|
folder.Unread = seen ? 0 : folder.Total;
|
|
NotifyFolderCounts();
|
|
RefreshFolderStats();
|
|
Status = seen ? Copy.FolderMarkedRead : Copy.FolderMarkedUnread;
|
|
});
|
|
}
|
|
|
|
private async Task<bool> ConfirmAsync(string title, string message) {
|
|
var result = await PromptAsync(title, message, confirmOnly: true);
|
|
return result is not null;
|
|
}
|
|
|
|
private async Task<string?> PromptAsync(string title, string message, bool confirmOnly, string? placeholder = null) {
|
|
if (PromptRequested is null)
|
|
return confirmOnly ? "" : null;
|
|
return await PromptRequested(new PromptRequest {
|
|
Title = title,
|
|
Message = message,
|
|
ConfirmOnly = confirmOnly,
|
|
Placeholder = placeholder ?? ""
|
|
});
|
|
}
|
|
|
|
private void ForgetLocalFolder(string mailboxId, string folder) =>
|
|
DropArchived(mailboxId, folder);
|
|
|
|
private void DropArchived(
|
|
string mailboxId,
|
|
string folder,
|
|
IReadOnlyCollection<uint>? uids = null) {
|
|
DeleteEmlFiles(
|
|
uids is null
|
|
? _archive.RemoveFolder(mailboxId, folder)
|
|
: _archive.RemoveUids(mailboxId, folder, uids));
|
|
_semantic.NotifySettingsChanged();
|
|
}
|
|
|
|
private static void DeleteEmlFiles(IEnumerable<string> paths) {
|
|
foreach (var path in paths) {
|
|
try {
|
|
if (!string.IsNullOrWhiteSpace(path) && File.Exists(path))
|
|
File.Delete(path);
|
|
}
|
|
catch {
|
|
}
|
|
}
|
|
}
|
|
|
|
private string ReplySubject() {
|
|
if (SelectedMessage is null)
|
|
return "";
|
|
return SelectedMessage.Header.Subject.StartsWith("Re:", StringComparison.OrdinalIgnoreCase)
|
|
? SelectedMessage.Header.Subject
|
|
: "Re: " + SelectedMessage.Header.Subject;
|
|
}
|
|
|
|
private string QuotedBody() =>
|
|
string.IsNullOrWhiteSpace(ReadingBody) ? "" : "\n\n--- Original message ---\n" + ReadingBody;
|
|
|
|
private void OpenCompose(
|
|
string title,
|
|
IReadOnlyList<MailRecipient>? to,
|
|
IReadOnlyList<MailRecipient>? cc,
|
|
string subject,
|
|
string body) {
|
|
if (!TryComposeIdentities(out var identities, out var selected) || selected is null) {
|
|
Status = "Add a mailbox to send From.";
|
|
return;
|
|
}
|
|
|
|
var compose = new ComposeViewModel(identities, selected, title, EnsureComposeSessionAsync);
|
|
if (to is { Count: > 0 })
|
|
compose.ToField.AddFromText(JoinRecipients(to));
|
|
if (cc is { Count: > 0 })
|
|
compose.CcField.AddFromText(JoinRecipients(cc));
|
|
compose.Subject = subject;
|
|
compose.Body = body;
|
|
if (_reading is not null && title.StartsWith("Reply", StringComparison.OrdinalIgnoreCase)) {
|
|
compose.InReplyTo = _reading.Header.MessageId;
|
|
compose.References = MailId.ThreadLine(_reading.Header.InReplyTo, _reading.Header.MessageId);
|
|
}
|
|
|
|
compose.Sent += RememberSent;
|
|
ComposeRequested?.Invoke(compose);
|
|
}
|
|
|
|
private bool TryComposeIdentities(out List<ComposeIdentity> identities, out ComposeIdentity? selected) {
|
|
identities = [];
|
|
selected = null;
|
|
foreach (var box in Mailboxes) {
|
|
var row = new ComposeIdentity { Account = box, Session = SessionFor(box) };
|
|
identities.Add(row);
|
|
if (SelectedMailbox is not null && box.Id == SelectedMailbox.Id)
|
|
selected = row;
|
|
}
|
|
|
|
selected ??= identities.FirstOrDefault();
|
|
return identities.Count > 0;
|
|
}
|
|
|
|
private async Task<IMailSession?> EnsureComposeSessionAsync(ComposeIdentity identity, CancellationToken token) {
|
|
var box = identity.Account;
|
|
if (SessionFor(box) is { IsConnected: true } live)
|
|
return live;
|
|
var password = PasswordFor(box);
|
|
var oauth = MailAuthKind.IsOAuth(box.AuthKind) || _auth.HasTokens(box.Id);
|
|
if (string.IsNullOrWhiteSpace(password) && !oauth)
|
|
return null;
|
|
var ok = await ConnectMailboxCoreAsync(box, password, token);
|
|
return ok ? SessionFor(box) : null;
|
|
}
|
|
|
|
private void RememberSent(MailSendRequest request, string messageId, string mailboxId) {
|
|
var box = _files.Current.FindMailbox(mailboxId) ?? SelectedMailbox;
|
|
if (box is null)
|
|
return;
|
|
if (TracksCertifiedReceipts(box)) {
|
|
_receipts.Remember(new SentDispatch {
|
|
MailboxId = box.Id,
|
|
MessageId = messageId,
|
|
Subject = request.Subject ?? "",
|
|
To = request.To.Select(r => r.Address).ToList(),
|
|
SentAt = DateTimeOffset.UtcNow,
|
|
Status = ReceiptStatus.Submitted
|
|
});
|
|
Status = string.Format(Copy.SentFromCertified, box.Label);
|
|
return;
|
|
}
|
|
|
|
Status = string.Format(Copy.SentFrom, box.Label);
|
|
}
|
|
|
|
private static bool TracksCertifiedReceipts(MailboxAccount? box) =>
|
|
box is not null && box.TracksCertifiedReceipts;
|
|
|
|
private bool TracksCertifiedReceipts(string mailboxId) {
|
|
var box = _files.Current.FindMailbox(mailboxId)
|
|
?? Mailboxes.FirstOrDefault(m => m.Id.Equals(mailboxId, StringComparison.OrdinalIgnoreCase));
|
|
return TracksCertifiedReceipts(box);
|
|
}
|
|
|
|
private void DropOrdinaryReceiptWaits() {
|
|
foreach (var box in Mailboxes) {
|
|
if (!TracksCertifiedReceipts(box))
|
|
_receipts.ForgetMailbox(box.Id);
|
|
}
|
|
}
|
|
|
|
private bool CanConnect() =>
|
|
!IsBusy && SelectedMailbox is not null;
|
|
|
|
private bool CanGetMessages() =>
|
|
!IsBusy && SelectedMailbox is not null;
|
|
|
|
private bool CanReply() =>
|
|
!IsBusy && SelectedMessage is not null && ActiveSession is { IsConnected: true };
|
|
|
|
private bool HasMessageSelection() =>
|
|
SelectedMessages.Count > 0 || SelectedMessage is not null;
|
|
|
|
private bool CanSessionFolders() =>
|
|
!IsBusy && ActiveSession is { IsConnected: true, SupportsFolders: true };
|
|
|
|
private bool CanOrganize() =>
|
|
CanSessionFolders() && HasMessageSelection();
|
|
|
|
private bool CanManageFolders() =>
|
|
CanSessionFolders() && SelectedMailbox is not null;
|
|
|
|
private bool CanCreateFolder() =>
|
|
CanManageFolders();
|
|
|
|
private bool CanEmptyFolder() =>
|
|
CanManageFolders() && SelectedFolder is not null;
|
|
|
|
private bool CanDeleteCustomFolder() =>
|
|
CanEmptyFolder()
|
|
&& MailFolderRole.IsCustom(SelectedFolder?.Name, SelectedFolder?.FullName);
|
|
|
|
partial void OnEditorIncomingProtocolChanged(ChoiceRow value) {
|
|
ApplyIncomingPortDefault();
|
|
if (!_applyingProvider && MailProvider.HasHostPreset(EditorProvider.Id))
|
|
ApplyProviderPreset();
|
|
}
|
|
|
|
partial void OnEditorIncomingSecurityChanged(ChoiceRow value) =>
|
|
ApplyIncomingPortDefault();
|
|
|
|
partial void OnEditorSmtpSecurityChanged(ChoiceRow value) {
|
|
if (MailSecurity.IsDefaultSmtpPort((int)EditorSmtpPort))
|
|
EditorSmtpPort = MailSecurity.DefaultSmtpPort(value.Id);
|
|
}
|
|
|
|
private void ApplyIncomingPortDefault() {
|
|
if (MailSecurity.IsDefaultIncomingPort((int)EditorIncomingPort))
|
|
EditorIncomingPort = MailSecurity.DefaultIncomingPort(EditorIncomingProtocol.Id, EditorIncomingSecurity.Id);
|
|
}
|
|
|
|
partial void OnEditorProviderChanged(ChoiceRow value) {
|
|
if (_applyingProvider)
|
|
return;
|
|
ApplyProviderPreset();
|
|
UpdateAuthUi();
|
|
}
|
|
|
|
partial void OnEditorCertifiedKindChanged(ChoiceRow value) {
|
|
if (_applyingProvider)
|
|
return;
|
|
UpdateAuthUi();
|
|
}
|
|
|
|
public void ApplyListSort(string header, bool descending) {
|
|
_listSortHeader = string.IsNullOrWhiteSpace(header) ? "Date" : header;
|
|
_listSortDescending = descending;
|
|
RebuildVisible();
|
|
}
|
|
|
|
partial void OnUnwrapEnvelopeChanged(bool value) {
|
|
var configuration = _files.Current;
|
|
configuration.UnwrapEnvelope = value;
|
|
_files.Save(configuration);
|
|
if (_reading is not null)
|
|
ApplyReading(_reading);
|
|
}
|
|
|
|
partial void OnGroupConversationsChanged(bool value) {
|
|
var configuration = _files.Current;
|
|
configuration.GroupConversations = value;
|
|
_files.Save(configuration);
|
|
RebuildVisible();
|
|
}
|
|
|
|
partial void OnReadingBodyKindChanged(string value) {
|
|
if (_syncingBodyKind)
|
|
return;
|
|
var kind = MailBodyKind.Normalize(value);
|
|
if (!MailBodyKind.IsSource(kind)) {
|
|
_bodyPreference = MailBodyKind.Preference(kind);
|
|
var configuration = _files.Current;
|
|
configuration.ReadingBodyKind = _bodyPreference;
|
|
_files.Save(configuration);
|
|
}
|
|
|
|
NotifyBodyKind();
|
|
if (_reading is not null)
|
|
ApplyReading(_reading, keepKind: true);
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void SetReadingKind(string? kind) {
|
|
var next = MailBodyKind.Normalize(kind);
|
|
if (MailBodyKind.IsFattura(next) && !UseFatturaPa)
|
|
next = MailBodyKind.Html;
|
|
ReadingBodyKind = next;
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void ViewFatturaPa() {
|
|
if (UseFatturaPa)
|
|
SetReadingKind(MailBodyKind.Fattura);
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void SetReadingLayout(string? layout) {
|
|
ReadingLayout = MailLayout.Normalize(layout);
|
|
}
|
|
|
|
private void ApplyBodyKind(string kind) {
|
|
_syncingBodyKind = true;
|
|
ReadingBodyKind = kind;
|
|
_syncingBodyKind = false;
|
|
NotifyBodyKind();
|
|
}
|
|
|
|
partial void OnReadingLayoutChanged(string value) {
|
|
var layout = MailLayout.Normalize(value);
|
|
if (layout != value) {
|
|
ReadingLayout = layout;
|
|
return;
|
|
}
|
|
|
|
var configuration = _files.Current;
|
|
configuration.ReadingLayout = layout;
|
|
_files.Save(configuration);
|
|
OnPropertyChanged(nameof(IsWideLayout));
|
|
OnPropertyChanged(nameof(IsStackedLayout));
|
|
OnPropertyChanged(nameof(LayoutToggleLabel));
|
|
}
|
|
|
|
partial void OnHasReadingChanged(bool value) =>
|
|
NotifyBodyKind();
|
|
|
|
private void NotifyBodyKind() {
|
|
OnPropertyChanged(nameof(ShowHtmlBody));
|
|
OnPropertyChanged(nameof(ShowTextBody));
|
|
OnPropertyChanged(nameof(ShowSourceBody));
|
|
OnPropertyChanged(nameof(ShowFatturaBody));
|
|
OnPropertyChanged(nameof(IsHtmlKind));
|
|
OnPropertyChanged(nameof(IsTextKind));
|
|
OnPropertyChanged(nameof(IsSourceKind));
|
|
OnPropertyChanged(nameof(IsFatturaKind));
|
|
OnPropertyChanged(nameof(HtmlEngineReady));
|
|
OnPropertyChanged(nameof(ShowFatturaPaTools));
|
|
}
|
|
|
|
public void NotifyFeatures() {
|
|
FeatureGate.Use(_files.Current.Features);
|
|
OnPropertyChanged(nameof(Providers));
|
|
OnPropertyChanged(nameof(UseFascicolo));
|
|
OnPropertyChanged(nameof(UseFatturaPa));
|
|
OnPropertyChanged(nameof(UseEnvelopeTools));
|
|
OnPropertyChanged(nameof(UseDeliveryColumn));
|
|
OnPropertyChanged(nameof(UseTypeColumn));
|
|
OnPropertyChanged(nameof(ShowFatturaPaTools));
|
|
NotifyBodyKind();
|
|
if (MailBodyKind.IsFattura(ReadingBodyKind) && !UseFatturaPa)
|
|
SetReadingKind(MailBodyKind.Html);
|
|
}
|
|
|
|
partial void OnHasReadingFatturaChanged(bool value) =>
|
|
OnPropertyChanged(nameof(ShowFatturaPaTools));
|
|
|
|
public void DisableHtmlEngine(string? reason = null) {
|
|
if (!HtmlEngineReady)
|
|
return;
|
|
HtmlEngineReady = false;
|
|
NotifyBodyKind();
|
|
Status = string.IsNullOrWhiteSpace(reason) ? WebViewSetup.MissingEngineHint() : reason;
|
|
}
|
|
|
|
partial void OnMessageFilterChanged(string value) {
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
RebuildVisible();
|
|
else
|
|
ScheduleSearch(immediate: false);
|
|
}
|
|
|
|
private bool CanSignIn() =>
|
|
!IsBusy;
|
|
|
|
partial void OnSelectedMailboxChanged(MailboxAccount? value) {
|
|
ConnectCommand.NotifyCanExecuteChanged();
|
|
GetMessagesCommand.NotifyCanExecuteChanged();
|
|
NotifyMessageCommands();
|
|
if (value is null)
|
|
return;
|
|
SyncFoldersCollection(value);
|
|
if (SessionFor(value) is { IsConnected: true })
|
|
return;
|
|
if (!AccountSettingsOpen)
|
|
_ = ConnectAsync();
|
|
}
|
|
|
|
partial void OnSelectedFolderNodeChanged(FolderNodeViewModel? value) {
|
|
if (_syncingTree || value is null)
|
|
return;
|
|
if (value.Mailbox is { } box && SelectedMailbox?.Id != box.Id)
|
|
SelectedMailbox = box;
|
|
if (value.Folder is { } folder)
|
|
SelectedFolder = folder;
|
|
else if (value.IsAccount && value.Mailbox is { } account)
|
|
SelectedFolder = InboxOf(account);
|
|
OnPropertyChanged(nameof(ShowFolderContext));
|
|
OnPropertyChanged(nameof(ShowDetachPst));
|
|
DetachPstCommand.NotifyCanExecuteChanged();
|
|
}
|
|
|
|
public bool ShowFolderContext =>
|
|
SelectedFolderNode is { IsAccount: false, Folder: not null };
|
|
|
|
public bool ShowDetachPst =>
|
|
!IsBusy
|
|
&& SelectedFolderNode is { IsAccount: true, Mailbox: { } box }
|
|
&& box.IsPstStore;
|
|
|
|
partial void OnIsBusyChanged(bool value) {
|
|
OnPropertyChanged(nameof(ShowDetachPst));
|
|
DetachPstCommand.NotifyCanExecuteChanged();
|
|
}
|
|
|
|
partial void OnSelectedFolderChanged(FolderRowViewModel? value) {
|
|
RefreshFolderStats();
|
|
NotifyFolderCommands();
|
|
if (value is null || _syncingTree || _suppressFolderLoad)
|
|
return;
|
|
KickFolderLoad(value.FullName);
|
|
}
|
|
|
|
partial void OnSelectedMessageChanged(MessageRowViewModel? value) {
|
|
NotifyMessageCommands();
|
|
if (_syncingList || value is null)
|
|
return;
|
|
_ = OpenMessageAsync(value);
|
|
}
|
|
|
|
private async Task OpenMessageAsync(MessageRowViewModel row) {
|
|
_readingLoad?.Cancel();
|
|
_readingLoad?.Dispose();
|
|
var cts = new CancellationTokenSource();
|
|
_readingLoad = cts;
|
|
try {
|
|
Status = Copy.OpeningMessage;
|
|
await LoadBodyAsync(row.Header, cts.Token);
|
|
}
|
|
catch (OperationCanceledException) {
|
|
}
|
|
}
|
|
|
|
private void FillEditor(MailboxAccount box) {
|
|
_pendingOauth = null;
|
|
EditorName = box.DisplayName;
|
|
EditorAddress = box.Address;
|
|
EditorUsername = box.Username;
|
|
EditorPassword = "";
|
|
EditorGoogleClientId = _files.Current.GoogleClientId;
|
|
EditorGoogleClientSecret = StoredGoogleClientSecret();
|
|
EditorMicrosoftClientId = _files.Current.MicrosoftClientId;
|
|
_applyingProvider = true;
|
|
EditorIncomingProtocol = ChoiceRow.Incoming(box.IncomingProtocol);
|
|
EditorIncomingSecurity = ChoiceRow.Security(box.IncomingSecurity, box.ImapSsl);
|
|
EditorIncomingHost = box.ImapHost;
|
|
EditorStorePath = box.DataFile;
|
|
EditorIncomingPort = box.ImapPort;
|
|
EditorSmtpSecurity = ChoiceRow.Security(box.SmtpSecurity, box.SmtpSsl);
|
|
EditorSmtpHost = box.SmtpHost;
|
|
EditorSmtpPort = box.SmtpPort;
|
|
EditorProvider = ChoiceRow.Provider(box.IsPstStore ? MailProvider.Pst : box.Provider);
|
|
EditorCertifiedKind = ChoiceRow.Certified(
|
|
MailCertifiedKind.ForProvider(box.Provider, box.CertifiedKind));
|
|
_applyingProvider = false;
|
|
UpdateAuthUi();
|
|
}
|
|
|
|
private async Task SignInProviderAsync(string kind) {
|
|
PersistClientIds(_files.Current);
|
|
PersistGoogleClientSecret();
|
|
_files.Save(_files.Current);
|
|
IsBusy = true;
|
|
SignInGoogleCommand.NotifyCanExecuteChanged();
|
|
SignInMicrosoftCommand.NotifyCanExecuteChanged();
|
|
var preview = kind == MailAuthKind.Microsoft
|
|
? OAuthClientId.Preview(EditorMicrosoftClientId)
|
|
: OAuthClientId.Preview(EditorGoogleClientId);
|
|
Status = string.IsNullOrEmpty(preview)
|
|
? "Waiting for browser sign-in…"
|
|
: "Waiting for browser sign-in (" + preview + ")…";
|
|
Result<OAuthTokenSet>? result = null;
|
|
Exception? error = null;
|
|
try {
|
|
result = await _auth.SignInAsync(kind, EditorAddress).ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex) {
|
|
error = ex;
|
|
}
|
|
|
|
Dispatcher.UIThread.Post(() => {
|
|
IsBusy = false;
|
|
SignInGoogleCommand.NotifyCanExecuteChanged();
|
|
SignInMicrosoftCommand.NotifyCanExecuteChanged();
|
|
ConnectCommand.NotifyCanExecuteChanged();
|
|
GetMessagesCommand.NotifyCanExecuteChanged();
|
|
if (error is not null) {
|
|
Status = error.Message;
|
|
OauthHint = Status;
|
|
return;
|
|
}
|
|
|
|
if (result is null) {
|
|
Status = "Sign-in failed.";
|
|
return;
|
|
}
|
|
|
|
CompleteSignIn(kind, result);
|
|
});
|
|
}
|
|
|
|
private void CompleteSignIn(string kind, Result<OAuthTokenSet> result) {
|
|
if (!result.IsSuccess || result.Value is null) {
|
|
var text = string.Join(" ", result.Messages);
|
|
Status = string.IsNullOrWhiteSpace(text) ? "Sign-in failed." : text;
|
|
OauthHint = Status;
|
|
return;
|
|
}
|
|
|
|
_pendingOauth = result.Value;
|
|
if (!string.IsNullOrWhiteSpace(result.Value.Email)) {
|
|
EditorAddress = result.Value.Email;
|
|
EditorUsername = result.Value.Email;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(EditorName))
|
|
EditorName = EditorAddress;
|
|
_applyingProvider = true;
|
|
EditorProvider = ChoiceRow.Provider(
|
|
kind == MailAuthKind.Microsoft ? MailProvider.Outlook : MailProvider.Gmail);
|
|
_applyingProvider = false;
|
|
ApplyProviderPreset();
|
|
UpdateAuthUi();
|
|
if (string.IsNullOrWhiteSpace(EditorAddress) || string.IsNullOrWhiteSpace(EditorIncomingHost)) {
|
|
Status = "Signed in as "
|
|
+ (string.IsNullOrWhiteSpace(result.Value.Email) ? "the account" : result.Value.Email)
|
|
+ ". Fill Address and incoming host, then Save Account.";
|
|
return;
|
|
}
|
|
|
|
WriteMailbox(closeSettings: false);
|
|
Status = "Signed in as "
|
|
+ (string.IsNullOrWhiteSpace(result.Value.Email) ? "the account" : result.Value.Email)
|
|
+ ". Connecting…";
|
|
_ = ConnectAsync();
|
|
}
|
|
|
|
private MailboxAccount MailboxToSave() {
|
|
var byAddress = _files.Current.FindMailboxByAddress(EditorAddress);
|
|
if (byAddress is not null)
|
|
return byAddress;
|
|
if (SelectedMailbox is null)
|
|
return new MailboxAccount();
|
|
if (_pendingOauth is not null
|
|
&& !string.IsNullOrWhiteSpace(SelectedMailbox.Address)
|
|
&& !SelectedMailbox.Address.Equals(EditorAddress.Trim(), StringComparison.OrdinalIgnoreCase))
|
|
return new MailboxAccount();
|
|
return SelectedMailbox;
|
|
}
|
|
|
|
private void ApplyProviderPreset() {
|
|
if (MailProvider.IsPst(EditorProvider.Id)) {
|
|
EditorCertifiedKind = ChoiceRow.Certified(MailCertifiedKind.Ordinary);
|
|
NotifyStoreUi();
|
|
UpdateAuthUi();
|
|
return;
|
|
}
|
|
|
|
var box = new MailboxAccount {
|
|
Provider = EditorProvider.Id,
|
|
IncomingProtocol = EditorIncomingProtocol.Id,
|
|
CertifiedKind = EditorCertifiedKind.Id
|
|
};
|
|
MailProvider.Apply(box);
|
|
EditorIncomingHost = box.ImapHost;
|
|
EditorIncomingPort = box.ImapPort;
|
|
EditorIncomingSecurity = ChoiceRow.Security(box.IncomingSecurity);
|
|
EditorSmtpHost = box.SmtpHost;
|
|
EditorSmtpPort = box.SmtpPort;
|
|
EditorSmtpSecurity = ChoiceRow.Security(box.SmtpSecurity);
|
|
EditorCertifiedKind = ChoiceRow.Certified(box.CertifiedKind);
|
|
NotifyStoreUi();
|
|
}
|
|
|
|
private void UpdateAuthUi() {
|
|
var provider = MailProvider.Normalize(EditorProvider.Id);
|
|
ShowOAuthButtons = MailProvider.UsesOAuth(provider);
|
|
ShowGoogleSignIn = provider == MailProvider.Gmail;
|
|
ShowMicrosoftSignIn = provider == MailProvider.Outlook;
|
|
var oauth = _pendingOauth is not null
|
|
|| (SelectedMailbox is not null
|
|
&& (MailAuthKind.IsOAuth(SelectedMailbox.AuthKind) || _auth.HasTokens(SelectedMailbox.Id)));
|
|
ShowPasswordFields = ShowServerFields && (provider != MailProvider.Outlook || !oauth);
|
|
OauthHint = provider == MailProvider.Gmail
|
|
? GmailHint()
|
|
: provider == MailProvider.Outlook
|
|
? OutlookHint()
|
|
: MailCertifiedKind.TracksReceipts(EditorCertifiedKind.Id) || MailProvider.IsPec(provider)
|
|
? Copy.PecHint
|
|
: MailProvider.IsPst(provider)
|
|
? Copy.PstHint
|
|
: "";
|
|
NotifyStoreUi();
|
|
}
|
|
|
|
private void NotifyStoreUi() {
|
|
OnPropertyChanged(nameof(ShowPstFields));
|
|
OnPropertyChanged(nameof(ShowServerFields));
|
|
OnPropertyChanged(nameof(ShowCertifiedKind));
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task BrowseStorePathAsync() {
|
|
var pick = PickStorePathRequested;
|
|
if (pick is null)
|
|
return;
|
|
var path = await pick();
|
|
if (!string.IsNullOrWhiteSpace(path))
|
|
EditorStorePath = path;
|
|
}
|
|
|
|
private string GmailHint() {
|
|
var stored = _secrets.Get(FileSecretStore.OAuthClientSecretKey(MailAuthKind.Google));
|
|
var hasSecret = stored.IsSuccess && !string.IsNullOrWhiteSpace(stored.Value);
|
|
return Copy.GmailHintPrefix
|
|
+ (hasSecret ? Copy.GmailHintStored : Copy.GmailHintPaste)
|
|
+ Copy.GmailHintImap;
|
|
}
|
|
|
|
private string OutlookHint() {
|
|
var preview = OAuthClientId.Preview(EditorMicrosoftClientId);
|
|
var usingId = string.IsNullOrEmpty(preview)
|
|
? Copy.OutlookHintGuid
|
|
: string.Format(Copy.OutlookHintNamed, preview);
|
|
return usingId + Copy.OutlookHintPasswords;
|
|
}
|
|
|
|
partial void OnEditorGoogleClientIdChanged(string value) {
|
|
if (ShowGoogleSignIn)
|
|
UpdateAuthUi();
|
|
}
|
|
|
|
partial void OnEditorMicrosoftClientIdChanged(string value) {
|
|
if (ShowMicrosoftSignIn)
|
|
UpdateAuthUi();
|
|
}
|
|
|
|
private void PersistClientIds(Configuration configuration) {
|
|
configuration.GoogleClientId = OAuthClientId.Normalize(EditorGoogleClientId);
|
|
configuration.MicrosoftClientId = OAuthClientId.Normalize(EditorMicrosoftClientId);
|
|
}
|
|
|
|
private void PersistPstStorePath(MailboxAccount box, string path) {
|
|
if (string.IsNullOrWhiteSpace(path) || box.StorePath.Equals(path, StringComparison.OrdinalIgnoreCase))
|
|
return;
|
|
box.StorePath = path;
|
|
var configuration = _files.Current;
|
|
var stored = configuration.FindMailbox(box.Id);
|
|
if (stored is not null)
|
|
stored.StorePath = path;
|
|
configuration.EnsureDefaults();
|
|
_files.Save(configuration);
|
|
var notice = (SessionFor(box) as PstMailSession)?.LastNotice;
|
|
Dispatcher.UIThread.Post(() => {
|
|
if (SelectedMailbox?.Id == box.Id)
|
|
EditorStorePath = path;
|
|
if (!string.IsNullOrWhiteSpace(notice))
|
|
Status = notice;
|
|
});
|
|
}
|
|
|
|
private void PersistGoogleClientSecret() {
|
|
var secret = OAuthClientSecret.Normalize(EditorGoogleClientSecret);
|
|
if (string.IsNullOrWhiteSpace(secret) && EditorGoogleClientId.TrimStart().StartsWith('{'))
|
|
secret = OAuthClientSecret.Normalize(EditorGoogleClientId);
|
|
if (string.IsNullOrWhiteSpace(secret))
|
|
return;
|
|
_secrets.Put(FileSecretStore.OAuthClientSecretKey(MailAuthKind.Google), secret);
|
|
}
|
|
|
|
private string StoredGoogleClientSecret() {
|
|
var stored = _secrets.Get(FileSecretStore.OAuthClientSecretKey(MailAuthKind.Google));
|
|
return stored.IsSuccess ? stored.Value ?? "" : "";
|
|
}
|
|
|
|
private void ReloadMailboxes() {
|
|
Mailboxes.Clear();
|
|
foreach (var box in _files.Current.Mailboxes)
|
|
Mailboxes.Add(box);
|
|
DropOrdinaryReceiptWaits();
|
|
var selected = _files.Current.FindMailbox(_files.Current.SelectedMailboxId);
|
|
SelectedMailbox = selected is null
|
|
? Mailboxes.FirstOrDefault()
|
|
: Mailboxes.FirstOrDefault(m => m.Id == selected.Id);
|
|
RebuildFolderTree();
|
|
}
|
|
|
|
private void RebuildFolderTree() {
|
|
_syncingTree = true;
|
|
var keep = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
var byId = FolderTree
|
|
.Where(n => n.Mailbox is not null)
|
|
.ToDictionary(n => n.Mailbox!.Id, StringComparer.OrdinalIgnoreCase);
|
|
var index = 0;
|
|
foreach (var box in Mailboxes) {
|
|
keep.Add(box.Id);
|
|
if (!byId.TryGetValue(box.Id, out var node)) {
|
|
node = FolderNodeViewModel.Account(box);
|
|
FolderTree.Insert(index, node);
|
|
}
|
|
else {
|
|
var current = index < FolderTree.Count ? FolderTree[index] : null;
|
|
if (!ReferenceEquals(current, node)) {
|
|
FolderTree.Remove(node);
|
|
FolderTree.Insert(Math.Min(index, FolderTree.Count), node);
|
|
}
|
|
}
|
|
|
|
SyncFolderChildren(node, box);
|
|
index++;
|
|
}
|
|
|
|
for (var i = FolderTree.Count - 1; i >= 0; i--) {
|
|
var id = FolderTree[i].Mailbox?.Id;
|
|
if (id is null || !keep.Contains(id))
|
|
FolderTree.RemoveAt(i);
|
|
}
|
|
|
|
var selectedBoxId = SelectedFolderNode?.Mailbox?.Id ?? SelectedMailbox?.Id;
|
|
var selectedFolder = SelectedFolderNode?.Folder?.FullName ?? SelectedFolder?.FullName;
|
|
SelectedFolderNode = FindTreeNode(selectedBoxId, selectedFolder)
|
|
?? FolderNodeFor(SelectedMailbox, "inbox")
|
|
?? FolderTree.FirstOrDefault(n => n.Mailbox?.Id == SelectedMailbox?.Id)?.Children.FirstOrDefault()
|
|
?? FolderTree.FirstOrDefault(n => n.Mailbox?.Id == SelectedMailbox?.Id);
|
|
_syncingTree = false;
|
|
}
|
|
|
|
private void SyncFolderChildren(FolderNodeViewModel node, MailboxAccount box) {
|
|
node.Children.Clear();
|
|
foreach (var folder in FoldersFor(box)
|
|
.OrderBy(f => MailFolderRole.SortKey(f.Name, f.FullName))
|
|
.ThenBy(f => f.Name, StringComparer.OrdinalIgnoreCase))
|
|
node.Children.Add(FolderNodeViewModel.ForFolder(box, folder));
|
|
node.NotifyCounts();
|
|
}
|
|
|
|
private FolderNodeViewModel? FindTreeNode(string? mailboxId, string? folder) {
|
|
if (string.IsNullOrWhiteSpace(mailboxId))
|
|
return null;
|
|
var account = FolderTree.FirstOrDefault(n => n.Mailbox?.Id == mailboxId);
|
|
if (account is null)
|
|
return null;
|
|
if (string.IsNullOrWhiteSpace(folder))
|
|
return account.Children.FirstOrDefault() ?? account;
|
|
return account.Children.FirstOrDefault(n =>
|
|
n.Folder?.FullName.Equals(folder, StringComparison.OrdinalIgnoreCase) == true)
|
|
?? account.Children.FirstOrDefault()
|
|
?? account;
|
|
}
|
|
|
|
private FolderNodeViewModel? FolderNodeFor(MailboxAccount? box, string kind) {
|
|
if (box is null)
|
|
return null;
|
|
var account = FolderTree.FirstOrDefault(n => n.Mailbox?.Id == box.Id);
|
|
return account?.Children.FirstOrDefault(n => MailFolderRole.Kind(n.Folder?.Name, n.Folder?.FullName) == kind);
|
|
}
|
|
|
|
private async Task LoadFoldersAsync(MailboxAccount box, IMailSession session, CancellationToken token) {
|
|
PauseIndexing();
|
|
try {
|
|
var result = await session.ListFoldersAsync(token).ConfigureAwait(false);
|
|
if (!result.IsSuccess) {
|
|
await UiAsync(() => Status = string.Join(" ", result.Messages)).ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
var rows = (result.Value ?? [])
|
|
.Select(folder => new FolderRowViewModel {
|
|
FullName = folder.FullName,
|
|
Name = folder.Name,
|
|
Unread = folder.Unread,
|
|
Total = folder.Total
|
|
})
|
|
.ToList();
|
|
var loadFolder = "";
|
|
await UiAsync(() => {
|
|
_foldersByMailbox[box.Id] = rows;
|
|
RebuildFolderTree();
|
|
if (SelectedMailbox?.Id != box.Id)
|
|
return;
|
|
SyncFoldersCollection(box);
|
|
var wanted = SelectedFolderNode?.Mailbox?.Id == box.Id ? SelectedFolderNode.Folder?.FullName : null;
|
|
var previous = SelectedFolder?.FullName;
|
|
_suppressFolderLoad = true;
|
|
try {
|
|
SelectedFolder = rows.FirstOrDefault(f => f.FullName.Equals(wanted, StringComparison.OrdinalIgnoreCase))
|
|
?? InboxOf(box)
|
|
?? rows.FirstOrDefault();
|
|
}
|
|
finally {
|
|
_suppressFolderLoad = false;
|
|
}
|
|
|
|
if (SelectedFolder is not null
|
|
&& (previous is null
|
|
|| !SelectedFolder.FullName.Equals(previous, StringComparison.OrdinalIgnoreCase)))
|
|
loadFolder = SelectedFolder.FullName;
|
|
}).ConfigureAwait(false);
|
|
|
|
if (!string.IsNullOrWhiteSpace(loadFolder))
|
|
KickFolderLoad(loadFolder);
|
|
|
|
var quota = await session.GetQuotaAsync(token).ConfigureAwait(false);
|
|
if (quota.IsSuccess && quota.Value is { } info) {
|
|
var line = info.Line();
|
|
await UiAsync(() => {
|
|
if (SelectedMailbox?.Id == box.Id)
|
|
QuotaLine = line;
|
|
}).ConfigureAwait(false);
|
|
}
|
|
}
|
|
finally {
|
|
ResumeIndexing();
|
|
}
|
|
}
|
|
|
|
private void SyncFoldersCollection(MailboxAccount box) {
|
|
Folders.Clear();
|
|
foreach (var folder in FoldersFor(box))
|
|
Folders.Add(folder);
|
|
}
|
|
|
|
private List<FolderRowViewModel> FoldersFor(MailboxAccount? box) {
|
|
if (box is null)
|
|
return [];
|
|
return _foldersByMailbox.TryGetValue(box.Id, out var rows) ? rows : [];
|
|
}
|
|
|
|
private IReadOnlyList<MailRule> ActiveRules() =>
|
|
_files.Current.Rules ?? [];
|
|
|
|
public IReadOnlyList<(string Name, string FullName)> FoldersForRules(string mailboxId) {
|
|
if (string.IsNullOrWhiteSpace(mailboxId))
|
|
return [];
|
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
var rows = new List<(string Name, string FullName)>();
|
|
void Add(string name, string full) {
|
|
if (string.IsNullOrWhiteSpace(full) || !seen.Add(full.Trim()))
|
|
return;
|
|
rows.Add((name.Trim(), full.Trim()));
|
|
}
|
|
|
|
var box = Mailboxes.FirstOrDefault(m => m.Id.Equals(mailboxId, StringComparison.OrdinalIgnoreCase));
|
|
foreach (var folder in FoldersFor(box))
|
|
Add(folder.Name, folder.FullName);
|
|
foreach (var path in _archive.ListFolders(mailboxId)) {
|
|
var slash = path.Replace('\\', '/').TrimEnd('/');
|
|
var i = slash.LastIndexOf('/');
|
|
Add(i < 0 ? slash : slash[(i + 1)..], path);
|
|
}
|
|
|
|
return rows;
|
|
}
|
|
|
|
public async Task EnsureFoldersForRulesAsync(string mailboxId) {
|
|
var box = Mailboxes.FirstOrDefault(m => m.Id.Equals(mailboxId, StringComparison.OrdinalIgnoreCase));
|
|
if (box is null)
|
|
return;
|
|
await ConnectMailboxCoreAsync(box, PasswordFor(box), CancellationToken.None);
|
|
if (FoldersFor(box).Count > 0)
|
|
return;
|
|
if (SessionFor(box) is { IsConnected: true } live)
|
|
await LoadFoldersAsync(box, live, CancellationToken.None);
|
|
}
|
|
|
|
public async Task<string> RunAllRulesAsync() {
|
|
var rules = MailRuleEngine.Ready(ActiveRules()).ToList();
|
|
if (rules.Count == 0)
|
|
return Copy.RulesNone;
|
|
var matched = 0;
|
|
foreach (var box in Mailboxes.ToList()) {
|
|
if (!rules.Any(r => MailRuleEngine.TargetsMailbox(r, box.Id)))
|
|
continue;
|
|
await EnsureFoldersForRulesAsync(box.Id);
|
|
foreach (var rule in rules.Where(r => MailRuleEngine.TargetsMailbox(r, box.Id)))
|
|
await EnsureFoldersForRulesAsync(MailRuleEngine.FolderMailbox(rule));
|
|
var session = SessionFor(box);
|
|
var folders = FoldersForRules(box.Id);
|
|
var names = folders.Select(f => f.FullName).ToList();
|
|
foreach (var extra in _archive.ListFolders(box.Id)) {
|
|
if (!names.Any(n => n.Equals(extra, StringComparison.OrdinalIgnoreCase)))
|
|
names.Add(extra);
|
|
}
|
|
|
|
foreach (var folder in names) {
|
|
var headers = _archive.ListFolder(box.Id, folder).Select(MailArchiveMap.ToHeader).ToList();
|
|
if (headers.Count == 0)
|
|
continue;
|
|
matched += await ApplyRulesToFolderAsync(box, session, folder, headers, folders, CancellationToken.None);
|
|
}
|
|
}
|
|
|
|
if (SelectedMailbox is not null && SelectedFolder is not null)
|
|
ShowArchive(SelectedMailbox.Id, SelectedFolder.FullName);
|
|
var line = string.Format(Copy.RulesRan, matched);
|
|
Status = line;
|
|
return line;
|
|
}
|
|
|
|
private async Task ApplyIncomingRulesAsync(
|
|
string folder,
|
|
IReadOnlyList<MailMessageHeader> headers,
|
|
CancellationToken token) {
|
|
var box = SelectedMailbox;
|
|
if (box is null)
|
|
return;
|
|
foreach (var rule in MailRuleEngine.Ready(ActiveRules()).Where(r => MailRuleEngine.TargetsMailbox(r, box.Id)))
|
|
await EnsureFoldersForRulesAsync(MailRuleEngine.FolderMailbox(rule));
|
|
await ApplyRulesToFolderAsync(box, ActiveSession, folder, headers, FoldersForRules(box.Id), token);
|
|
}
|
|
|
|
private async Task<int> ApplyRulesToFolderAsync(
|
|
MailboxAccount box,
|
|
IMailSession? session,
|
|
string folder,
|
|
IReadOnlyList<MailMessageHeader> headers,
|
|
IReadOnlyList<(string Name, string FullName)> folders,
|
|
CancellationToken token) {
|
|
var rules = MailRuleEngine.Ready(ActiveRules()).ToList();
|
|
if (rules.Count == 0 || headers.Count == 0)
|
|
return 0;
|
|
var moves = new Dictionary<string, List<uint>>(StringComparer.OrdinalIgnoreCase);
|
|
var copies = new Dictionary<(string MailboxId, string Folder), List<uint>>();
|
|
var read = new List<uint>();
|
|
var flagged = new List<uint>();
|
|
var touched = new HashSet<uint>();
|
|
foreach (var header in headers) {
|
|
foreach (var rule in rules) {
|
|
var destFolders = DestFoldersFor(rule, box.Id, folders);
|
|
if (!MailRuleEngine.CanApply(rule, box.Id, destFolders))
|
|
continue;
|
|
if (!MailRuleEngine.Matches(rule, header.From, "", header.Subject, "", header.HasAttachments))
|
|
continue;
|
|
touched.Add(header.Id);
|
|
if (rule.Action == MailRuleAction.Delete) {
|
|
var trash = MailRuleEngine.ResolveFolder("Trash", folders);
|
|
if (!string.IsNullOrWhiteSpace(trash))
|
|
AddUid(moves, trash, header.Id);
|
|
}
|
|
else if (rule.Action == MailRuleAction.Move) {
|
|
var dest = MailRuleEngine.ExactFolder(rule.Folder, destFolders);
|
|
var destBox = MailRuleEngine.FolderMailbox(rule);
|
|
if (!string.IsNullOrWhiteSpace(dest)) {
|
|
if (destBox.Equals(box.Id, StringComparison.OrdinalIgnoreCase)) {
|
|
if (!dest.Equals(folder, StringComparison.OrdinalIgnoreCase))
|
|
AddUid(moves, dest, header.Id);
|
|
}
|
|
else
|
|
AddCopy(copies, destBox, dest, header.Id);
|
|
}
|
|
}
|
|
|
|
if (rule.Action == MailRuleAction.MarkRead)
|
|
read.Add(header.Id);
|
|
if (rule.Action == MailRuleAction.Flag)
|
|
flagged.Add(header.Id);
|
|
if (rule.Action == MailRuleAction.Label && !string.IsNullOrWhiteSpace(rule.Label))
|
|
_archive.AddLabel(box.Id, folder, header.Id, rule.Label);
|
|
if (rule.Stop)
|
|
break;
|
|
}
|
|
}
|
|
|
|
var movedIds = moves.SelectMany(p => p.Value).ToHashSet();
|
|
foreach (var pair in copies)
|
|
foreach (var id in pair.Value)
|
|
movedIds.Add(id);
|
|
read.RemoveAll(movedIds.Contains);
|
|
flagged.RemoveAll(movedIds.Contains);
|
|
if (session is { IsConnected: true, SupportsFolders: true }) {
|
|
foreach (var pair in moves) {
|
|
var moved = await session.MoveMessagesAsync(folder, pair.Value, pair.Key, token);
|
|
if (moved.IsSuccess)
|
|
DropArchived(box.Id, folder, pair.Value);
|
|
}
|
|
|
|
if (read.Count > 0) {
|
|
await session.SetMessageFlagsAsync(folder, read, new MailFlagUpdate { Seen = true }, token);
|
|
_archive.UpdateFlags(
|
|
box.Id,
|
|
folder,
|
|
headers.Where(h => read.Contains(h.Id))
|
|
.Select(h => (h.Id, true, h.IsFlagged || flagged.Contains(h.Id))));
|
|
}
|
|
|
|
if (flagged.Count > 0) {
|
|
await session.SetMessageFlagsAsync(folder, flagged, new MailFlagUpdate { Flagged = true }, token);
|
|
_archive.UpdateFlags(
|
|
box.Id,
|
|
folder,
|
|
headers.Where(h => flagged.Contains(h.Id))
|
|
.Select(h => (h.Id, h.IsSeen || read.Contains(h.Id), true)));
|
|
}
|
|
}
|
|
|
|
if (copies.Count > 0)
|
|
await CopyRuleMovesAsync(box, session, folder, folders, copies, token);
|
|
|
|
return touched.Count;
|
|
}
|
|
|
|
private IReadOnlyList<(string Name, string FullName)> DestFoldersFor(
|
|
MailRule rule,
|
|
string sourceId,
|
|
IReadOnlyList<(string Name, string FullName)> sourceFolders) {
|
|
var destId = MailRuleEngine.FolderMailbox(rule);
|
|
if (string.IsNullOrWhiteSpace(destId) || destId.Equals(sourceId, StringComparison.OrdinalIgnoreCase))
|
|
return sourceFolders;
|
|
return FoldersForRules(destId);
|
|
}
|
|
|
|
private static void AddCopy(
|
|
Dictionary<(string MailboxId, string Folder), List<uint>> map,
|
|
string mailboxId,
|
|
string folder,
|
|
uint id) {
|
|
var key = (mailboxId, folder);
|
|
if (!map.TryGetValue(key, out var ids)) {
|
|
ids = [];
|
|
map[key] = ids;
|
|
}
|
|
|
|
if (!ids.Contains(id))
|
|
ids.Add(id);
|
|
}
|
|
|
|
private async Task CopyRuleMovesAsync(
|
|
MailboxAccount source,
|
|
IMailSession? sourceSession,
|
|
string sourceFolder,
|
|
IReadOnlyList<(string Name, string FullName)> sourceFolders,
|
|
Dictionary<(string MailboxId, string Folder), List<uint>> copies,
|
|
CancellationToken token) {
|
|
var copied = new List<uint>();
|
|
foreach (var pair in copies) {
|
|
var destBox = Mailboxes.FirstOrDefault(m => m.Id.Equals(pair.Key.MailboxId, StringComparison.OrdinalIgnoreCase));
|
|
if (destBox is null)
|
|
continue;
|
|
await EnsureFoldersForRulesAsync(destBox.Id);
|
|
foreach (var id in pair.Value) {
|
|
if (await CopyUidToMailboxAsync(source, sourceSession, destBox, sourceFolder, pair.Key.Folder, id, token))
|
|
copied.Add(id);
|
|
}
|
|
}
|
|
|
|
if (copied.Count == 0)
|
|
return;
|
|
var unique = copied.Distinct().ToList();
|
|
if (sourceSession is { IsConnected: true, SupportsFolders: true }) {
|
|
var trash = MailRuleEngine.ResolveFolder("Trash", sourceFolders);
|
|
if (!string.IsNullOrWhiteSpace(trash) && !trash.Equals(sourceFolder, StringComparison.OrdinalIgnoreCase))
|
|
await sourceSession.MoveMessagesAsync(sourceFolder, unique, trash, token);
|
|
else
|
|
await sourceSession.SetMessageFlagsAsync(sourceFolder, unique, new MailFlagUpdate { Deleted = true }, token);
|
|
}
|
|
|
|
DropArchived(source.Id, sourceFolder, unique);
|
|
}
|
|
|
|
private async Task<bool> CopyUidToMailboxAsync(
|
|
MailboxAccount source,
|
|
IMailSession? sourceSession,
|
|
MailboxAccount dest,
|
|
string sourceFolder,
|
|
string destFolder,
|
|
uint id,
|
|
CancellationToken token) {
|
|
var destSession = SessionFor(dest);
|
|
if (destSession is not { IsConnected: true })
|
|
return false;
|
|
byte[]? eml = null;
|
|
var path = _archive.EmlPath(source.Id, sourceFolder, id) ?? ArchiveFiles.EmlPath(source.Id, sourceFolder, id);
|
|
if (!string.IsNullOrWhiteSpace(path) && File.Exists(path))
|
|
eml = await File.ReadAllBytesAsync(path, token);
|
|
else if (sourceSession is { IsConnected: true }) {
|
|
var body = await sourceSession.GetMessageAsync(sourceFolder, id, token);
|
|
if (body.IsSuccess && body.Value?.RawEml is { Length: > 0 } raw)
|
|
eml = raw;
|
|
}
|
|
|
|
if (eml is null || eml.Length == 0)
|
|
return false;
|
|
var appended = await destSession.AppendAsync(destFolder, eml, token);
|
|
return appended.IsSuccess;
|
|
}
|
|
|
|
private static void AddUid(Dictionary<string, List<uint>> map, string folder, uint id) {
|
|
if (!map.TryGetValue(folder, out var ids)) {
|
|
ids = [];
|
|
map[folder] = ids;
|
|
}
|
|
|
|
if (!ids.Contains(id))
|
|
ids.Add(id);
|
|
}
|
|
|
|
private FolderRowViewModel? InboxOf(MailboxAccount box) =>
|
|
FoldersFor(box).FirstOrDefault(f => MailFolderRole.Kind(f.Name, f.FullName) == "inbox")
|
|
?? FoldersFor(box).FirstOrDefault();
|
|
|
|
private IMailSession? ActiveSession =>
|
|
SessionFor(SelectedMailbox);
|
|
|
|
private IMailSession? SessionFor(MailboxAccount? box) {
|
|
if (box is null)
|
|
return null;
|
|
lock (_connections)
|
|
return _connections.TryGetValue(box.Id, out var session) ? session : null;
|
|
}
|
|
|
|
private string PasswordFor(MailboxAccount box) {
|
|
if (SelectedMailbox?.Id == box.Id && !string.IsNullOrWhiteSpace(EditorPassword))
|
|
return EditorPassword;
|
|
var stored = _secrets.Get(FileSecretStore.MailboxKey(box.Id));
|
|
return stored.Value ?? "";
|
|
}
|
|
|
|
private async Task<bool> ConnectMailboxCoreAsync(MailboxAccount box, string password, CancellationToken token) {
|
|
while (true) {
|
|
token.ThrowIfCancellationRequested();
|
|
lock (_connections) {
|
|
if (_connecting.Add(box.Id))
|
|
break;
|
|
}
|
|
|
|
await Task.Delay(50, token).ConfigureAwait(false);
|
|
}
|
|
|
|
PauseIndexing();
|
|
try {
|
|
if (SessionFor(box) is { IsConnected: true } live) {
|
|
var missingFolders = false;
|
|
await UiAsync(() => missingFolders = !_foldersByMailbox.ContainsKey(box.Id)).ConfigureAwait(false);
|
|
if (missingFolders)
|
|
await LoadFoldersAsync(box, live, token).ConfigureAwait(false);
|
|
return true;
|
|
}
|
|
|
|
var session = _sessions.Create(box);
|
|
var connected = await session.ConnectAsync(box, password, token).ConfigureAwait(false);
|
|
if (!connected.IsSuccess) {
|
|
await session.DisposeAsync().ConfigureAwait(false);
|
|
await UiAsync(() => {
|
|
if (SelectedMailbox?.Id == box.Id)
|
|
Status = string.Join(" ", connected.Messages);
|
|
}).ConfigureAwait(false);
|
|
return false;
|
|
}
|
|
|
|
IMailSession? previous;
|
|
lock (_connections) {
|
|
_connections.TryGetValue(box.Id, out previous);
|
|
_connections[box.Id] = session;
|
|
}
|
|
|
|
if (previous is not null)
|
|
await previous.DisposeAsync().ConfigureAwait(false);
|
|
if (session is PstMailSession pst)
|
|
pst.StorePathChanged += path => PersistPstStorePath(box, path);
|
|
await LoadFoldersAsync(box, session, token).ConfigureAwait(false);
|
|
await UiAsync(() => EnsureIndexing(box.Id)).ConfigureAwait(false);
|
|
return true;
|
|
}
|
|
finally {
|
|
ResumeIndexing();
|
|
lock (_connections)
|
|
_connecting.Remove(box.Id);
|
|
}
|
|
}
|
|
|
|
private async Task ConnectIdleMailboxesAsync() {
|
|
foreach (var box in Mailboxes.ToList()) {
|
|
if (SessionFor(box) is { IsConnected: true })
|
|
continue;
|
|
lock (_connections) {
|
|
if (_connecting.Contains(box.Id))
|
|
continue;
|
|
}
|
|
var oauth = MailAuthKind.IsOAuth(box.AuthKind) || _auth.HasTokens(box.Id);
|
|
var password = PasswordFor(box);
|
|
if (!box.IsPstStore && string.IsNullOrWhiteSpace(password) && !oauth)
|
|
continue;
|
|
try {
|
|
await ConnectMailboxCoreAsync(box, password, CancellationToken.None).ConfigureAwait(false);
|
|
}
|
|
catch {
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ApplyFolderSync(string mailboxId, string folder, MailFolderSync sync) {
|
|
if (sync.Flags.Count > 0)
|
|
_archive.UpdateFlags(mailboxId, folder, sync.Flags.Select(f => (f.Id, f.IsSeen, f.IsFlagged)));
|
|
_archive.UpsertHeaders(mailboxId, sync.Headers.Select(MailArchiveMap.FromHeader));
|
|
if (sync.Present is not null) {
|
|
var keep = sync.Present.ToHashSet();
|
|
var gone = _archive.Uids(mailboxId, folder).Where(uid => !keep.Contains(uid)).ToList();
|
|
if (gone.Count > 0)
|
|
DropArchived(mailboxId, folder, gone);
|
|
}
|
|
|
|
_semantic.NotifySettingsChanged();
|
|
}
|
|
|
|
private async Task LoadMessagesAsync(string folder, CancellationToken token, bool resumeBodies = false) {
|
|
var box = SelectedMailbox;
|
|
if (box is null)
|
|
return;
|
|
await ShowArchiveAsync(box.Id, folder, token).ConfigureAwait(false);
|
|
var session = SessionFor(box);
|
|
if (session is not { IsConnected: true }) {
|
|
await UiAsync(() => Status = ArchiveFiles.Hint()).ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
PauseIndexing();
|
|
try {
|
|
var known = await Task.Run(() => _archive.Uids(box.Id, folder), token).ConfigureAwait(false);
|
|
var result = await session.ListMessagesAsync(folder, known, token).ConfigureAwait(false);
|
|
if (!result.IsSuccess) {
|
|
await UiAsync(() =>
|
|
Status = string.Join(" ", result.Messages) + " Showing local archive. " + ArchiveFiles.Hint())
|
|
.ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
token.ThrowIfCancellationRequested();
|
|
var sync = result.Value ?? new MailFolderSync();
|
|
ApplyFolderSync(box.Id, folder, sync);
|
|
var fresh = sync.Headers.Where(h => !known.Contains(h.Id)).ToList();
|
|
if (known.Count > 0) {
|
|
await UiAsync(() => {
|
|
if (SelectedMailbox?.Id == box.Id)
|
|
NoticeNewMail(fresh);
|
|
}).ConfigureAwait(false);
|
|
}
|
|
await ApplyIncomingRulesAsync(folder, fresh, token).ConfigureAwait(false);
|
|
await ShowArchiveAsync(box.Id, folder, token).ConfigureAwait(false);
|
|
if (TracksCertifiedReceipts(box) && MailFolderRole.Kind(null, folder) == "sent") {
|
|
await IngestReceiptFoldersAsync(token).ConfigureAwait(false);
|
|
await UiAsync(OverlaySentMarks).ConfigureAwait(false);
|
|
}
|
|
|
|
await UiAsync(() => Status = ArchiveFiles.Hint()).ConfigureAwait(false);
|
|
if (resumeBodies)
|
|
await UiAsync(() => EnsureBodyBackfill(box.Id)).ConfigureAwait(false);
|
|
}
|
|
finally {
|
|
ResumeIndexing();
|
|
}
|
|
}
|
|
|
|
private async Task IngestReceiptFoldersAsync(CancellationToken token) {
|
|
var session = ActiveSession;
|
|
if (session is null || SelectedMailbox is null || !TracksCertifiedReceipts(SelectedMailbox))
|
|
return;
|
|
foreach (var folder in FoldersFor(SelectedMailbox)) {
|
|
var kind = MailFolderRole.Kind(folder.Name, folder.FullName);
|
|
if (kind is not "inbox" and not "receipts")
|
|
continue;
|
|
var listed = await session.ListMessagesAsync(
|
|
folder.FullName,
|
|
_archive.Uids(SelectedMailbox.Id, folder.FullName),
|
|
token);
|
|
if (!listed.IsSuccess)
|
|
continue;
|
|
var sync = listed.Value ?? new MailFolderSync();
|
|
ApplyFolderSync(SelectedMailbox.Id, folder.FullName, sync);
|
|
foreach (var header in sync.Headers)
|
|
ReconcileHeader(header, folder.FullName);
|
|
}
|
|
}
|
|
|
|
private void OverlaySentMarks() {
|
|
if (SelectedMailbox is null || !TracksCertifiedReceipts(SelectedMailbox))
|
|
return;
|
|
foreach (var row in Messages) {
|
|
row.Header.DeliveryStatus = _receipts.StatusFor(
|
|
SelectedMailbox.Id,
|
|
row.Header.MessageId,
|
|
row.Header.Subject);
|
|
row.RefreshMarks();
|
|
}
|
|
}
|
|
|
|
private void ReconcileHeader(MailMessageHeader header, string folder) {
|
|
if (SelectedMailbox is null)
|
|
return;
|
|
if (TracksCertifiedReceipts(SelectedMailbox)
|
|
&& header.EnvelopeKind is EnvelopeKind.PecReceipt or EnvelopeKind.EidasRem)
|
|
_receipts.ApplyEvidence(
|
|
SelectedMailbox.Id,
|
|
header.InReplyTo,
|
|
null,
|
|
header.Subject,
|
|
header.EnvelopeTipo);
|
|
if (TracksCertifiedReceipts(SelectedMailbox) && MailFolderRole.Kind(null, folder) == "sent")
|
|
header.DeliveryStatus = _receipts.StatusFor(SelectedMailbox.Id, header.MessageId, header.Subject);
|
|
else if (string.IsNullOrWhiteSpace(header.DeliveryStatus))
|
|
header.DeliveryStatus = ReceiptStatus.FromTipo(header.EnvelopeTipo);
|
|
}
|
|
|
|
private void RebuildVisible() {
|
|
if (SelectedMailbox is null || SelectedFolder is null) {
|
|
VisibleMessages.Clear();
|
|
SelectedMessages.Clear();
|
|
SelectedMessage = null;
|
|
NotifyMessageCommands();
|
|
RestoreMessageGridSelection();
|
|
RefreshFolderStats();
|
|
return;
|
|
}
|
|
|
|
if (MessageFilter.Trim().Length == 0)
|
|
ApplyVisibleRows([.. Messages]);
|
|
else
|
|
ScheduleSearch(immediate: true);
|
|
}
|
|
|
|
private void ScheduleSearch(bool immediate) {
|
|
_search?.Cancel();
|
|
_search?.Dispose();
|
|
var cts = new CancellationTokenSource();
|
|
_search = cts;
|
|
var gen = Interlocked.Increment(ref _searchGen);
|
|
var query = MessageFilter ?? "";
|
|
var mailboxId = SelectedMailbox?.Id;
|
|
var folder = SelectedFolder?.FullName;
|
|
_ = RunSearchAsync(query, mailboxId, folder, gen, immediate, cts.Token);
|
|
}
|
|
|
|
private async Task RunSearchAsync(
|
|
string query,
|
|
string? mailboxId,
|
|
string? folder,
|
|
int gen,
|
|
bool immediate,
|
|
CancellationToken token) {
|
|
try {
|
|
var trimmed = query.Trim();
|
|
if (!immediate && trimmed.Length > 0)
|
|
await Task.Delay(280, token).ConfigureAwait(false);
|
|
if (token.IsCancellationRequested || gen != _searchGen)
|
|
return;
|
|
|
|
if (string.IsNullOrWhiteSpace(mailboxId) || string.IsNullOrWhiteSpace(folder)) {
|
|
await Dispatcher.UIThread.InvokeAsync(() => {
|
|
if (gen != _searchGen)
|
|
return;
|
|
VisibleMessages.Clear();
|
|
RefreshFolderStats();
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (trimmed.Length == 0) {
|
|
await Dispatcher.UIThread.InvokeAsync(() => {
|
|
if (gen != _searchGen)
|
|
return;
|
|
ApplyVisibleRows([.. Messages]);
|
|
});
|
|
return;
|
|
}
|
|
|
|
var hits = await Task.Run(() => {
|
|
float[]? vector = null;
|
|
if (_semantic.IsReady && trimmed.Length >= 2)
|
|
vector = _semantic.EmbedQuery(trimmed);
|
|
return _archive.Search(mailboxId, folder, trimmed, vector);
|
|
}, token).ConfigureAwait(false);
|
|
|
|
if (token.IsCancellationRequested || gen != _searchGen)
|
|
return;
|
|
|
|
await Dispatcher.UIThread.InvokeAsync(() => {
|
|
if (gen != _searchGen)
|
|
return;
|
|
if (SelectedMailbox?.Id != mailboxId
|
|
|| SelectedFolder?.FullName != folder
|
|
|| !string.Equals(MessageFilter.Trim(), trimmed, StringComparison.Ordinal))
|
|
return;
|
|
var map = Messages
|
|
.GroupBy(m => m.Header.Id)
|
|
.ToDictionary(g => g.Key, g => g.First());
|
|
var rows = new List<MessageRowViewModel>();
|
|
foreach (var hit in hits) {
|
|
if (map.TryGetValue(hit.Uid, out var row))
|
|
rows.Add(row);
|
|
}
|
|
|
|
ApplyVisibleRows(rows);
|
|
});
|
|
}
|
|
catch (OperationCanceledException) {
|
|
}
|
|
}
|
|
|
|
private void ApplyVisibleRows(List<MessageRowViewModel> rows) {
|
|
var keep = SelectedMessages.Where(rows.Contains).ToList();
|
|
var keepOne = SelectedMessage is not null && rows.Contains(SelectedMessage)
|
|
? SelectedMessage
|
|
: keep.FirstOrDefault();
|
|
_syncingList = true;
|
|
VisibleMessages.Clear();
|
|
try {
|
|
if (SelectedMailbox is null || SelectedFolder is null)
|
|
return;
|
|
|
|
if (!GroupConversations) {
|
|
rows.Sort(CompareRows);
|
|
foreach (var row in rows) {
|
|
row.SetChain(0, 1);
|
|
VisibleMessages.Add(row);
|
|
}
|
|
}
|
|
else {
|
|
var map = rows
|
|
.GroupBy(r => r.Header.Id)
|
|
.ToDictionary(g => g.Key, g => g.First());
|
|
var links = MailChain.Order(rows.Select(ChainItem).ToList(), true);
|
|
if (IsDateSort) {
|
|
if (!_listSortDescending)
|
|
links = MailListOrder.ReverseThreads(links);
|
|
}
|
|
else
|
|
links = MailListOrder.ReorderThreads(links, (a, b) => CompareRows(map[a], map[b]));
|
|
|
|
foreach (var link in links) {
|
|
if (!map.TryGetValue(link.Uid, out var row))
|
|
continue;
|
|
row.SetChain(link.Depth, link.Size);
|
|
VisibleMessages.Add(row);
|
|
}
|
|
}
|
|
|
|
SetSelectedMessages(keep.Where(VisibleMessages.Contains));
|
|
SelectedMessage = keepOne is not null && VisibleMessages.Contains(keepOne)
|
|
? keepOne
|
|
: keep.FirstOrDefault(VisibleMessages.Contains);
|
|
}
|
|
finally {
|
|
_syncingList = false;
|
|
RefreshFolderStats();
|
|
NotifyMessageCommands();
|
|
RestoreMessageGridSelection();
|
|
}
|
|
}
|
|
|
|
private static MailChainItem ChainItem(MessageRowViewModel row) =>
|
|
new() {
|
|
Uid = row.Header.Id,
|
|
MessageId = row.Header.MessageId,
|
|
InReplyTo = row.Header.InReplyTo,
|
|
Date = row.Header.Date
|
|
};
|
|
|
|
private bool IsDateSort =>
|
|
string.IsNullOrWhiteSpace(_listSortHeader)
|
|
|| _listSortHeader.Equals("Date", StringComparison.OrdinalIgnoreCase);
|
|
|
|
private int CompareRows(MessageRowViewModel a, MessageRowViewModel b) {
|
|
var cmp = _listSortHeader switch {
|
|
"Unread" => a.UnreadSort.CompareTo(b.UnreadSort),
|
|
"Flag" => a.FlagSort.CompareTo(b.FlagSort),
|
|
"Priority" => a.PrioritySort.CompareTo(b.PrioritySort),
|
|
"Attachments" => a.AttachmentSort.CompareTo(b.AttachmentSort),
|
|
"Delivery" => a.DeliverySort.CompareTo(b.DeliverySort),
|
|
"Type" => string.Compare(a.Badge, b.Badge, StringComparison.OrdinalIgnoreCase),
|
|
"From" => string.Compare(a.From, b.From, StringComparison.OrdinalIgnoreCase),
|
|
"Label" => string.Compare(a.Labels, b.Labels, StringComparison.OrdinalIgnoreCase),
|
|
"Subject" => string.Compare(a.Subject, b.Subject, StringComparison.OrdinalIgnoreCase),
|
|
_ => a.SortDate.CompareTo(b.SortDate)
|
|
};
|
|
if (cmp == 0)
|
|
cmp = a.Header.Id.CompareTo(b.Header.Id);
|
|
return _listSortDescending ? -cmp : cmp;
|
|
}
|
|
|
|
private async Task LoadBodyAsync(MailMessageHeader header, CancellationToken token) {
|
|
var box = SelectedMailbox;
|
|
if (box is null)
|
|
return;
|
|
var eml = _archive.EmlPath(box.Id, header.Folder, header.Id)
|
|
?? ArchiveFiles.EmlPath(box.Id, header.Folder, header.Id);
|
|
var body = await MimeBody.TryFromEmlAsync(header.Folder, header.Id, eml, token).ConfigureAwait(false);
|
|
if (body is null && SessionFor(box) is { IsConnected: true } session) {
|
|
PauseIndexing();
|
|
try {
|
|
var result = await session.GetMessageAsync(header.Folder, header.Id, token).ConfigureAwait(false);
|
|
if (!result.IsSuccess || result.Value is null) {
|
|
await UiAsync(() => Status = string.Join(" ", result.Messages)).ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
body = result.Value;
|
|
}
|
|
finally {
|
|
ResumeIndexing();
|
|
}
|
|
}
|
|
|
|
if (body is null) {
|
|
await UiAsync(() =>
|
|
Status = "Message is not in the local archive yet. Connect and Get Messages.")
|
|
.ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
StoreArchivedBody(box.Id, header, body);
|
|
await Dispatcher.UIThread.InvokeAsync(async () => {
|
|
if (SelectedMessage is null
|
|
|| SelectedMessage.Header.Id != header.Id
|
|
|| !SelectedMessage.Header.Folder.Equals(header.Folder, StringComparison.OrdinalIgnoreCase))
|
|
return;
|
|
|
|
_reading = body;
|
|
ApplyReading(body);
|
|
if (TracksCertifiedReceipts(box)
|
|
&& (body.Envelope.Kind == EnvelopeKind.PecReceipt || body.Envelope.Kind == EnvelopeKind.EidasRem)) {
|
|
_receipts.ApplyEvidence(
|
|
box.Id,
|
|
body.Envelope.Msgid,
|
|
body.Envelope.Identificativo,
|
|
body.Envelope.Oggetto,
|
|
body.Envelope.Tipo);
|
|
if (SelectedFolder is not null
|
|
&& MailFolderRole.Kind(SelectedFolder.Name, SelectedFolder.FullName) == "sent")
|
|
OverlaySentMarks();
|
|
var explain = ReceiptStatus.Explain(body.Envelope.Tipo);
|
|
if (!string.IsNullOrWhiteSpace(explain))
|
|
Status = explain;
|
|
}
|
|
|
|
await MarkOpenedReadAsync(header, token).ConfigureAwait(true);
|
|
if (_pendingWindow is { } pending
|
|
&& pending.Header.Id == header.Id
|
|
&& pending.Header.Folder.Equals(header.Folder, StringComparison.OrdinalIgnoreCase)) {
|
|
_pendingWindow = null;
|
|
ShowMessageWindow(body);
|
|
}
|
|
});
|
|
}
|
|
|
|
private bool IsLoadedBody(MailMessageHeader header) =>
|
|
_reading is { } body
|
|
&& body.Header.Id == header.Id
|
|
&& body.Header.Folder.Equals(header.Folder, StringComparison.OrdinalIgnoreCase);
|
|
|
|
private void ShowMessageWindow(MailMessageBody body) {
|
|
if (!TryComposeIdentities(out var identities, out var selected) || selected is null)
|
|
return;
|
|
var vm = new MessageWindowViewModel(
|
|
body,
|
|
identities,
|
|
selected,
|
|
UnwrapEnvelope,
|
|
_bodyPreference,
|
|
HtmlEngineReady,
|
|
EnsureComposeSessionAsync);
|
|
vm.ComposeRequested += compose => {
|
|
compose.Sent += RememberSent;
|
|
ComposeRequested?.Invoke(compose);
|
|
};
|
|
MessageWindowRequested?.Invoke(vm);
|
|
}
|
|
|
|
private async Task MarkOpenedReadAsync(MailMessageHeader header, CancellationToken token) {
|
|
var row = Messages.FirstOrDefault(m =>
|
|
m.Header.Id == header.Id
|
|
&& m.Header.Folder.Equals(header.Folder, StringComparison.OrdinalIgnoreCase));
|
|
if (row is null || row.Header.IsSeen || SelectedFolder is null)
|
|
return;
|
|
var session = ActiveSession;
|
|
if (session is not { IsConnected: true })
|
|
return;
|
|
if (session.SupportsFolders) {
|
|
var result = await session.SetMessageFlagsAsync(
|
|
SelectedFolder.FullName,
|
|
[row.Header.Id],
|
|
new MailFlagUpdate { Seen = true },
|
|
token);
|
|
if (!result.IsSuccess)
|
|
return;
|
|
}
|
|
|
|
ApplyUnreadDelta(SelectedFolder, [row], true);
|
|
row.Header.IsSeen = true;
|
|
row.RefreshMarks();
|
|
if (SelectedMailbox is not null)
|
|
_archive.UpsertHeaders(SelectedMailbox.Id, [MailArchiveMap.FromHeader(row.Header)]);
|
|
NotifyFolderCounts();
|
|
RefreshFolderStats();
|
|
}
|
|
|
|
private void ApplyReading(MailMessageBody body, bool keepKind = false) {
|
|
var inner = UnwrapEnvelope && body.Envelope.HasInnerMessage;
|
|
ReadingSubject = inner && !string.IsNullOrWhiteSpace(body.InnerSubject)
|
|
? body.InnerSubject
|
|
: body.Header.Subject;
|
|
ReadingFrom = inner && !string.IsNullOrWhiteSpace(body.InnerFrom)
|
|
? body.InnerFrom
|
|
: body.Header.From;
|
|
ReadingTo = inner && !string.IsNullOrWhiteSpace(body.InnerTo)
|
|
? body.InnerTo
|
|
: body.To;
|
|
ReadingCc = inner && !string.IsNullOrWhiteSpace(body.InnerCc)
|
|
? body.InnerCc
|
|
: body.Cc;
|
|
HasReadingCc = !string.IsNullOrWhiteSpace(ReadingCc);
|
|
ReadingDate = body.Header.Date == DateTimeOffset.MinValue
|
|
? ""
|
|
: body.Header.Date.ToLocalTime().ToString("f");
|
|
var files = inner ? body.InnerAttachmentFiles : body.AttachmentFiles;
|
|
var html = inner ? body.InnerHtml : body.Html;
|
|
var text = inner ? body.InnerText : body.Text;
|
|
var markup = MessageHtml.HasMarkup(html);
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
text = MessageText.StripHtml(html);
|
|
if (markup) {
|
|
var cid = MessageHtml.DataUris(files.Select(f => (f.ContentId, f.ContentType, f.Bytes)));
|
|
ReadingHtmlDocument = MessageHtml.Document(MessageHtml.InlineCid(html, cid));
|
|
}
|
|
else if (keepKind && MailBodyKind.IsHtml(ReadingBodyKind))
|
|
ReadingHtmlDocument = MessageHtml.Document(MessageHtml.FromPlain(text));
|
|
else
|
|
ReadingHtmlDocument = "";
|
|
ReadingBody = text ?? "";
|
|
ReadingSource = body.RawEml.Length == 0
|
|
? ""
|
|
: System.Text.Encoding.UTF8.GetString(body.RawEml);
|
|
ReadingEnvelope = body.Envelope.Line;
|
|
HasReadingEnvelope = !string.IsNullOrWhiteSpace(ReadingEnvelope);
|
|
ReadingFiles.Clear();
|
|
foreach (var file in files) {
|
|
if (IsInlineImage(file, html ?? ""))
|
|
continue;
|
|
ReadingFiles.Add(file);
|
|
}
|
|
HasReadingFiles = ReadingFiles.Count > 0;
|
|
HasReading = true;
|
|
var fattura = FatturaPaDocument.FromAttachments(files);
|
|
ReadingFattura = fattura?.Text ?? "";
|
|
HasReadingFattura = fattura is not null;
|
|
if (!keepKind)
|
|
ApplyBodyKind(fattura is not null && UseFatturaPa
|
|
? MailBodyKind.Fattura
|
|
: MailBodyKind.DefaultView(html, inner ? body.InnerText : body.Text, _bodyPreference));
|
|
}
|
|
|
|
private static bool IsInlineImage(MailFileAttachment file, string html) {
|
|
if (string.IsNullOrWhiteSpace(file.ContentId))
|
|
return false;
|
|
if (!file.ContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
|
|
return false;
|
|
return html.Contains("cid:" + file.ContentId, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private void ClearReading() {
|
|
_reading = null;
|
|
_pendingWindow = null;
|
|
HasReading = false;
|
|
HasReadingCc = false;
|
|
ReadingSubject = "";
|
|
ReadingFrom = "";
|
|
ReadingTo = "";
|
|
ReadingCc = "";
|
|
ReadingDate = "";
|
|
ReadingBody = "";
|
|
ReadingHtmlDocument = "";
|
|
ReadingSource = "";
|
|
ReadingEnvelope = "";
|
|
HasReadingEnvelope = false;
|
|
HasReadingFiles = false;
|
|
HasReadingFattura = false;
|
|
ReadingFattura = "";
|
|
ReadingFiles.Clear();
|
|
}
|
|
|
|
private void StoreArchivedBody(string mailboxId, MailMessageHeader header, MailMessageBody body) {
|
|
if (body.RawEml.Length == 0)
|
|
return;
|
|
try {
|
|
var path = ArchiveFiles.EmlPath(mailboxId, header.Folder, header.Id);
|
|
File.WriteAllBytes(path, body.RawEml);
|
|
_archive.UpsertBody(
|
|
mailboxId,
|
|
MailArchiveMap.FromHeader(header),
|
|
path,
|
|
MailArchiveMap.BodyText(body, UnwrapEnvelope),
|
|
MailArchiveMap.AttachmentIndex(body, UnwrapEnvelope));
|
|
_semantic.NotifySettingsChanged();
|
|
}
|
|
catch {
|
|
}
|
|
}
|
|
|
|
private static void ShowArchiveHint() {
|
|
}
|
|
|
|
private async Task ShowArchiveAsync(string mailboxId, string folder, CancellationToken token) {
|
|
var rows = await Task.Run(() => _archive.ListFolder(mailboxId, folder), token).ConfigureAwait(false);
|
|
token.ThrowIfCancellationRequested();
|
|
await UiAsync(() => {
|
|
if (SelectedMailbox?.Id != mailboxId
|
|
|| SelectedFolder is null
|
|
|| !SelectedFolder.FullName.Equals(folder, StringComparison.OrdinalIgnoreCase))
|
|
return;
|
|
BindArchive(mailboxId, folder, rows);
|
|
}).ConfigureAwait(false);
|
|
}
|
|
|
|
private void ShowArchive(string mailboxId, string folder) =>
|
|
BindArchive(mailboxId, folder, _archive.ListFolder(mailboxId, folder));
|
|
|
|
private void BindArchive(string mailboxId, string folder, IReadOnlyList<MailArchiveHeader> rows) {
|
|
Messages.Clear();
|
|
SelectedMessages.Clear();
|
|
SelectedMessage = null;
|
|
ClearReading();
|
|
foreach (var row in rows) {
|
|
var header = MailArchiveMap.ToHeader(row);
|
|
ReconcileHeader(header, folder);
|
|
Messages.Add(new MessageRowViewModel { Header = header });
|
|
}
|
|
|
|
if (TracksCertifiedReceipts(mailboxId) && MailFolderRole.Kind(null, folder) == "sent")
|
|
OverlaySentMarks();
|
|
ApplyOpenedFolderCounts();
|
|
RebuildVisible();
|
|
}
|
|
|
|
private void NoticeNewMail(IReadOnlyList<MailMessageHeader> newcomers) {
|
|
if (newcomers.Count == 0)
|
|
return;
|
|
RaiseNotice(newcomers.Where(h => h.EnvelopeKind == EnvelopeKind.PecTransport).ToList(), "PEC arrived");
|
|
RaiseNotice(newcomers.Where(h => h.EnvelopeKind == EnvelopeKind.PecReceipt).ToList(), "Ricevuta arrived");
|
|
RaiseNotice(newcomers.Where(h => h.EnvelopeKind == EnvelopeKind.EidasRem).ToList(), "REM arrived");
|
|
}
|
|
|
|
private void RaiseNotice(IReadOnlyList<MailMessageHeader> rows, string title) {
|
|
if (rows.Count == 0)
|
|
return;
|
|
var body = rows.Count == 1 ? rows[0].Subject : rows.Count + " new messages";
|
|
NoticeRequested?.Invoke(title, body);
|
|
}
|
|
|
|
private void StopBackfill() {
|
|
_indexMailboxes.Clear();
|
|
_catalogDone.Clear();
|
|
_catalogFolders.Clear();
|
|
_backfill?.Cancel();
|
|
_backfill?.Dispose();
|
|
_backfill = null;
|
|
IndexLine = "";
|
|
}
|
|
|
|
private void EnsureIndexing(string mailboxId) {
|
|
if (string.IsNullOrWhiteSpace(mailboxId))
|
|
return;
|
|
_indexMailboxes.Add(mailboxId);
|
|
if (_backfill is not null)
|
|
return;
|
|
StartIndexing();
|
|
}
|
|
|
|
private void EnsureBodyBackfill(string mailboxId) {
|
|
if (string.IsNullOrWhiteSpace(mailboxId) || _backfill is not null)
|
|
return;
|
|
if (_archive.MissingBodyCount([mailboxId]) == 0)
|
|
return;
|
|
EnsureIndexing(mailboxId);
|
|
}
|
|
|
|
private void StopIndexingMailbox(string mailboxId) {
|
|
_indexMailboxes.Remove(mailboxId);
|
|
_catalogDone.Remove(mailboxId);
|
|
_catalogFolders.RemoveWhere(key =>
|
|
key.StartsWith(mailboxId + "\n", StringComparison.OrdinalIgnoreCase));
|
|
if (_indexMailboxes.Count > 0)
|
|
return;
|
|
_backfill?.Cancel();
|
|
_backfill?.Dispose();
|
|
_backfill = null;
|
|
IndexLine = "";
|
|
}
|
|
|
|
private void StartIndexing() {
|
|
if (_backfill is not null || _indexMailboxes.Count == 0)
|
|
return;
|
|
var cts = new CancellationTokenSource();
|
|
_backfill = cts;
|
|
_ = IndexAllAsync(cts);
|
|
}
|
|
|
|
private async Task IndexAllAsync(CancellationTokenSource cts) {
|
|
var token = cts.Token;
|
|
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
|
|
try {
|
|
while (!token.IsCancellationRequested) {
|
|
await WaitIfIndexingPausedAsync(token).ConfigureAwait(false);
|
|
var snapshot = await Dispatcher.UIThread.InvokeAsync(CaptureIndexSnapshot);
|
|
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
|
|
if (snapshot.Count == 0)
|
|
break;
|
|
|
|
var ids = snapshot.Select(s => s.Id).ToList();
|
|
var sessions = snapshot.ToDictionary(
|
|
s => s.Id,
|
|
s => s.Session,
|
|
StringComparer.OrdinalIgnoreCase);
|
|
var cataloged = false;
|
|
foreach (var box in snapshot) {
|
|
if (box.CatalogDone
|
|
|| box.Session is not { IsConnected: true }
|
|
|| !box.Session.SupportsFolders)
|
|
continue;
|
|
var pendingFolders = false;
|
|
foreach (var folder in box.Folders) {
|
|
token.ThrowIfCancellationRequested();
|
|
var open = box.OpenFolder is not null
|
|
&& folder.FullName.Equals(box.OpenFolder, StringComparison.OrdinalIgnoreCase);
|
|
if (open || box.CatalogedFolders.Contains(folder.FullName))
|
|
continue;
|
|
pendingFolders = true;
|
|
cataloged = true;
|
|
await WaitIfIndexingPausedAsync(token).ConfigureAwait(false);
|
|
await SetIndexLineAsync(MailIndexProgress.Left(_archive.MissingBodyCount(ids)), token)
|
|
.ConfigureAwait(false);
|
|
var known = _archive.Uids(box.Id, folder.FullName);
|
|
var listed = await box.Session
|
|
.ListMessagesAsync(folder.FullName, known, token, IndexCatalogChunk)
|
|
.ConfigureAwait(false);
|
|
if (!listed.IsSuccess)
|
|
continue;
|
|
var sync = listed.Value ?? new MailFolderSync();
|
|
ApplyFolderSync(box.Id, folder.FullName, sync);
|
|
var counts = _archive.FolderCounts(box.Id, folder.FullName);
|
|
await UiAsync(() => RefreshIndexedFolderCounts(box.Id, folder.FullName, counts))
|
|
.ConfigureAwait(false);
|
|
if (!sync.Incomplete) {
|
|
await UiAsync(() => _catalogFolders.Add(CatalogKey(box.Id, folder.FullName)))
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
await Task.Delay(15, token).ConfigureAwait(false);
|
|
}
|
|
|
|
if (!pendingFolders)
|
|
await UiAsync(() => _catalogDone.Add(box.Id)).ConfigureAwait(false);
|
|
}
|
|
|
|
await WaitIfIndexingPausedAsync(token).ConfigureAwait(false);
|
|
var pending = _archive.MissingBodies(ids, 16);
|
|
if (pending.Count == 0) {
|
|
if (!cataloged)
|
|
break;
|
|
continue;
|
|
}
|
|
|
|
await SetIndexLineAsync(MailIndexProgress.Left(_archive.MissingBodyCount(ids)), token)
|
|
.ConfigureAwait(false);
|
|
var fetched = 0;
|
|
foreach (var item in pending) {
|
|
token.ThrowIfCancellationRequested();
|
|
await WaitIfIndexingPausedAsync(token).ConfigureAwait(false);
|
|
if (!sessions.TryGetValue(item.MailboxId, out var session) || session is not { IsConnected: true })
|
|
continue;
|
|
try {
|
|
var result = await session.GetMessageAsync(item.Folder, item.Uid, token).ConfigureAwait(false);
|
|
if (result.IsSuccess && result.Value is not null)
|
|
StoreArchivedBody(item.MailboxId, result.Value.Header, result.Value);
|
|
fetched++;
|
|
}
|
|
catch (OperationCanceledException) {
|
|
throw;
|
|
}
|
|
catch {
|
|
}
|
|
}
|
|
|
|
if (fetched == 0 && !cataloged)
|
|
break;
|
|
}
|
|
}
|
|
catch (OperationCanceledException) {
|
|
return;
|
|
}
|
|
finally {
|
|
await Dispatcher.UIThread.InvokeAsync(() => {
|
|
if (_backfill != cts)
|
|
return;
|
|
_backfill.Dispose();
|
|
_backfill = null;
|
|
if (token.IsCancellationRequested)
|
|
return;
|
|
foreach (var id in _indexMailboxes.ToList()) {
|
|
if (_catalogDone.Contains(id) && _archive.MissingBodyCount([id]) == 0)
|
|
_indexMailboxes.Remove(id);
|
|
}
|
|
|
|
IndexLine = "";
|
|
if (_indexMailboxes.Any(id => SessionFor(MailboxById(id)) is { IsConnected: true }))
|
|
StartIndexing();
|
|
});
|
|
}
|
|
}
|
|
|
|
private List<IndexMailboxSnap> CaptureIndexSnapshot() {
|
|
var rows = new List<IndexMailboxSnap>();
|
|
foreach (var id in _indexMailboxes.ToList()) {
|
|
var box = MailboxById(id);
|
|
var session = SessionFor(box);
|
|
var cataloged = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var folder in FoldersFor(box)) {
|
|
if (_catalogFolders.Contains(CatalogKey(id, folder.FullName)))
|
|
cataloged.Add(folder.FullName);
|
|
}
|
|
|
|
rows.Add(new IndexMailboxSnap(
|
|
id,
|
|
session,
|
|
FoldersFor(box).Select(f => (f.FullName, f.Name)).ToList(),
|
|
SelectedMailbox?.Id == id ? SelectedFolder?.FullName : null,
|
|
_catalogDone.Contains(id),
|
|
cataloged));
|
|
}
|
|
|
|
return rows;
|
|
}
|
|
|
|
private readonly record struct IndexMailboxSnap(
|
|
string Id,
|
|
IMailSession? Session,
|
|
IReadOnlyList<(string FullName, string Name)> Folders,
|
|
string? OpenFolder,
|
|
bool CatalogDone,
|
|
IReadOnlySet<string> CatalogedFolders);
|
|
|
|
private async Task SetIndexLineAsync(string line, CancellationToken token) =>
|
|
await UiAsync(() => {
|
|
if (!token.IsCancellationRequested)
|
|
IndexLine = line;
|
|
}).ConfigureAwait(false);
|
|
|
|
private MailboxAccount? MailboxById(string mailboxId) =>
|
|
Mailboxes.FirstOrDefault(m => m.Id.Equals(mailboxId, StringComparison.OrdinalIgnoreCase))
|
|
?? _files.Current.FindMailbox(mailboxId);
|
|
|
|
private void RefreshIndexedFolderCounts(string mailboxId, string folder, (int Total, int Unread) counts) {
|
|
var view = FoldersFor(MailboxById(mailboxId))
|
|
.FirstOrDefault(f => f.FullName.Equals(folder, StringComparison.OrdinalIgnoreCase));
|
|
if (view is null)
|
|
return;
|
|
view.Total = counts.Total;
|
|
view.Unread = counts.Unread;
|
|
NotifyFolderCounts();
|
|
}
|
|
|
|
private async Task DisconnectMailboxAsync(string mailboxId) {
|
|
StopIndexingMailbox(mailboxId);
|
|
IMailSession? session;
|
|
lock (_connections)
|
|
_connections.Remove(mailboxId, out session);
|
|
if (session is not null)
|
|
await session.DisposeAsync();
|
|
_foldersByMailbox.Remove(mailboxId);
|
|
if (SelectedMailbox?.Id == mailboxId) {
|
|
Messages.Clear();
|
|
RebuildVisible();
|
|
ClearReading();
|
|
Folders.Clear();
|
|
FolderStats = "";
|
|
}
|
|
|
|
RebuildFolderTree();
|
|
}
|
|
|
|
private void NotifyMessageCommands() {
|
|
ReplyCommand.NotifyCanExecuteChanged();
|
|
ReplyAllCommand.NotifyCanExecuteChanged();
|
|
ForwardCommand.NotifyCanExecuteChanged();
|
|
MoveToFolderCommand.NotifyCanExecuteChanged();
|
|
MarkReadCommand.NotifyCanExecuteChanged();
|
|
MarkUnreadCommand.NotifyCanExecuteChanged();
|
|
ToggleFlagCommand.NotifyCanExecuteChanged();
|
|
SetPriorityCommand.NotifyCanExecuteChanged();
|
|
DeleteMessagesCommand.NotifyCanExecuteChanged();
|
|
OpenMessageWindowCommand.NotifyCanExecuteChanged();
|
|
PrintMessageCommand.NotifyCanExecuteChanged();
|
|
SaveMessagePdfCommand.NotifyCanExecuteChanged();
|
|
SaveAttachmentsZipCommand.NotifyCanExecuteChanged();
|
|
NotifyFolderCommands();
|
|
}
|
|
|
|
private void NotifyFolderCommands() {
|
|
CreateFolderCommand.NotifyCanExecuteChanged();
|
|
EmptyFolderCommand.NotifyCanExecuteChanged();
|
|
MarkFolderReadCommand.NotifyCanExecuteChanged();
|
|
MarkFolderUnreadCommand.NotifyCanExecuteChanged();
|
|
DeleteCustomFolderCommand.NotifyCanExecuteChanged();
|
|
DetachPstCommand.NotifyCanExecuteChanged();
|
|
OnPropertyChanged(nameof(ShowFolderContext));
|
|
OnPropertyChanged(nameof(ShowDetachPst));
|
|
}
|
|
|
|
partial void OnHasReadingFilesChanged(bool value) =>
|
|
SaveAttachmentsZipCommand.NotifyCanExecuteChanged();
|
|
|
|
private IReadOnlyList<MessageRowViewModel> TargetRows() =>
|
|
SelectedMessages.Count > 0
|
|
? SelectedMessages.ToList()
|
|
: SelectedMessage is { } one ? [one] : [];
|
|
|
|
private IReadOnlyList<uint> TargetIds(IReadOnlyList<MessageRowViewModel>? rows = null) =>
|
|
(rows ?? TargetRows()).Select(r => r.Header.Id).ToList();
|
|
|
|
private async Task MoveRowsAsync(IReadOnlyList<MessageRowViewModel> rows, string destFolder) {
|
|
if (rows.Count == 0)
|
|
return;
|
|
await MoveIdsAsync(TargetIds(rows), destFolder, rows: rows);
|
|
}
|
|
|
|
private async Task MoveIdsAsync(
|
|
IReadOnlyList<uint> ids,
|
|
string destFolder,
|
|
string? fromFolder = null,
|
|
IReadOnlyList<MessageRowViewModel>? rows = null,
|
|
string? destMailboxId = null,
|
|
string? sourceMailboxId = null) {
|
|
var sourceBox = MailboxById(sourceMailboxId ?? SelectedMailbox?.Id ?? "")
|
|
?? SelectedMailbox;
|
|
var destBox = MailboxById(destMailboxId ?? sourceBox?.Id ?? "")
|
|
?? sourceBox;
|
|
if (sourceBox is null || destBox is null || ids.Count == 0)
|
|
return;
|
|
var source = fromFolder ?? SelectedFolder?.FullName;
|
|
if (string.IsNullOrWhiteSpace(source))
|
|
return;
|
|
await RunAsync(Copy.Moving, async token => {
|
|
var sourceSession = await EnsureMailboxReadyAsync(sourceBox, token);
|
|
var destSession = destBox.Id.Equals(sourceBox.Id, StringComparison.OrdinalIgnoreCase)
|
|
? sourceSession
|
|
: await EnsureMailboxReadyAsync(destBox, token);
|
|
var dest = destFolder;
|
|
if (string.IsNullOrWhiteSpace(dest))
|
|
dest = InboxOf(destBox)?.FullName ?? "";
|
|
if (string.IsNullOrWhiteSpace(dest)
|
|
|| destSession is not { IsConnected: true, SupportsFolders: true }) {
|
|
Status = Copy.CannotMoveSystemFolder;
|
|
return;
|
|
}
|
|
|
|
if (sourceBox.Id.Equals(destBox.Id, StringComparison.OrdinalIgnoreCase)
|
|
&& source.Equals(dest, StringComparison.OrdinalIgnoreCase)) {
|
|
Status = Copy.AlreadyInFolder;
|
|
return;
|
|
}
|
|
|
|
if (sourceBox.Id.Equals(destBox.Id, StringComparison.OrdinalIgnoreCase)) {
|
|
if (sourceSession is not { IsConnected: true, SupportsFolders: true }) {
|
|
Status = Copy.CannotMoveSystemFolder;
|
|
return;
|
|
}
|
|
|
|
var result = await sourceSession.MoveMessagesAsync(source, ids, dest, token);
|
|
if (!result.IsSuccess) {
|
|
Status = string.Join(" ", result.Messages);
|
|
return;
|
|
}
|
|
|
|
ApplyFolderMoveCounts(sourceBox, destBox, source, dest, ids.Count, rows);
|
|
DropArchived(sourceBox.Id, source, ids.ToList());
|
|
RemoveDroppedRows(sourceBox.Id, source, ids);
|
|
if (SelectedMailbox?.Id == destBox.Id && SelectedFolder is not null)
|
|
await LoadMessagesAsync(SelectedFolder.FullName, token);
|
|
Status = string.Format(Copy.MovedMessages, ids.Count);
|
|
return;
|
|
}
|
|
|
|
if (sourceSession is null) {
|
|
Status = Copy.CannotMoveSystemFolder;
|
|
return;
|
|
}
|
|
|
|
var copied = new List<uint>();
|
|
foreach (var id in ids) {
|
|
if (await CopyUidToMailboxAsync(sourceBox, sourceSession, destBox, source, dest, id, token))
|
|
copied.Add(id);
|
|
}
|
|
|
|
if (copied.Count == 0) {
|
|
Status = string.Format(Copy.MovedMessages, 0);
|
|
return;
|
|
}
|
|
|
|
await RemoveCopiedUidsAsync(sourceBox, sourceSession, source, copied, token);
|
|
ApplyFolderMoveCounts(sourceBox, destBox, source, dest, copied.Count, rows);
|
|
RemoveDroppedRows(sourceBox.Id, source, copied);
|
|
await LoadFoldersAsync(destBox, destSession, token);
|
|
if (SelectedMailbox?.Id == destBox.Id && SelectedFolder is not null)
|
|
await LoadMessagesAsync(SelectedFolder.FullName, token);
|
|
else if (SelectedMailbox?.Id == sourceBox.Id && SelectedFolder is not null)
|
|
await LoadMessagesAsync(SelectedFolder.FullName, token);
|
|
Status = string.Format(Copy.MovedMessages, copied.Count);
|
|
});
|
|
}
|
|
|
|
private async Task<IMailSession?> EnsureMailboxReadyAsync(MailboxAccount box, CancellationToken token) {
|
|
await ConnectMailboxCoreAsync(box, PasswordFor(box), token);
|
|
var session = SessionFor(box);
|
|
if (session is not { IsConnected: true })
|
|
return session;
|
|
if (FoldersFor(box).Count == 0)
|
|
await LoadFoldersAsync(box, session, token);
|
|
return session;
|
|
}
|
|
|
|
private async Task<List<uint>> FolderUidsAsync(
|
|
MailboxAccount box,
|
|
IMailSession session,
|
|
string folder,
|
|
CancellationToken token) {
|
|
var ids = new HashSet<uint>(_archive.Uids(box.Id, folder));
|
|
var listed = await session.ListMessagesAsync(folder, ids.Count == 0 ? null : ids, token);
|
|
if (listed.IsSuccess && listed.Value?.Present is { } present) {
|
|
foreach (var id in present)
|
|
ids.Add(id);
|
|
}
|
|
|
|
return ids.ToList();
|
|
}
|
|
|
|
private IReadOnlyList<FolderRowViewModel> FolderSlice(MailboxAccount box, string root) =>
|
|
FoldersFor(box)
|
|
.Where(folder => MailFolderPath.IsSelfOrUnder(folder.FullName, root))
|
|
.OrderBy(folder => folder.FullName.Length)
|
|
.ThenBy(folder => folder.FullName, StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
|
|
private IEnumerable<string> SiblingNames(MailboxAccount box, string? destParent, string? exceptFolder) {
|
|
foreach (var folder in FoldersFor(box)) {
|
|
if (exceptFolder is not null
|
|
&& folder.FullName.Equals(exceptFolder, StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
if (MailFolderPath.SameParent(folder.FullName, destParent))
|
|
yield return folder.Name;
|
|
}
|
|
}
|
|
|
|
private string? FindMovedFolder(MailboxAccount box, string? destParent, string leaf) =>
|
|
FoldersFor(box).FirstOrDefault(folder =>
|
|
MailFolderPath.Leaf(folder.FullName).Equals(leaf, StringComparison.OrdinalIgnoreCase)
|
|
&& MailFolderPath.SameParent(folder.FullName, destParent))?.FullName;
|
|
|
|
private void RememberMovedFolder(string sourceMailboxId, string from, string to, string? destMailboxId) {
|
|
_archive.RewriteFolderPrefix(sourceMailboxId, from, to);
|
|
if (MailRuleEngine.RetargetFolders(ActiveRules(), sourceMailboxId, from, to, destMailboxId) > 0)
|
|
_files.Save(_files.Current);
|
|
}
|
|
|
|
private async Task SelectAfterFolderMoveAsync(
|
|
MailboxAccount source,
|
|
string from,
|
|
string to,
|
|
string? openFolder,
|
|
CancellationToken token,
|
|
MailboxAccount? destBox = null) {
|
|
if (SelectedMailbox?.Id != source.Id)
|
|
return;
|
|
if (string.IsNullOrWhiteSpace(openFolder) || !MailFolderPath.IsSelfOrUnder(openFolder, from))
|
|
return;
|
|
FolderRowViewModel? next = null;
|
|
if (destBox is null || destBox.Id.Equals(source.Id, StringComparison.OrdinalIgnoreCase)) {
|
|
var path = MailFolderPath.Rewrite(openFolder, from, to);
|
|
next = FoldersFor(source).FirstOrDefault(f =>
|
|
path is not null && f.FullName.Equals(path, StringComparison.OrdinalIgnoreCase))
|
|
?? FoldersFor(source).FirstOrDefault(f =>
|
|
f.FullName.Equals(to, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
SelectedFolder = next ?? InboxOf(source) ?? FoldersFor(source).FirstOrDefault();
|
|
if (SelectedFolder is not null)
|
|
await LoadMessagesAsync(SelectedFolder.FullName, token);
|
|
}
|
|
|
|
private async Task RemoveCopiedUidsAsync(
|
|
MailboxAccount source,
|
|
IMailSession? session,
|
|
string sourceFolder,
|
|
IReadOnlyList<uint> ids,
|
|
CancellationToken token) {
|
|
if (ids.Count == 0)
|
|
return;
|
|
if (session is { IsConnected: true, SupportsFolders: true }) {
|
|
var trash = FoldersFor(source)
|
|
.FirstOrDefault(f => MailFolderRole.Kind(f.Name, f.FullName) == "trash");
|
|
if (trash is not null
|
|
&& !trash.FullName.Equals(sourceFolder, StringComparison.OrdinalIgnoreCase))
|
|
await session.MoveMessagesAsync(sourceFolder, ids, trash.FullName, token);
|
|
else
|
|
await session.SetMessageFlagsAsync(sourceFolder, ids, new MailFlagUpdate { Deleted = true }, token);
|
|
}
|
|
|
|
DropArchived(source.Id, sourceFolder, ids.ToList());
|
|
}
|
|
|
|
private void RemoveDroppedRows(string mailboxId, string folder, IReadOnlyList<uint> ids) {
|
|
if (SelectedMailbox?.Id != mailboxId
|
|
|| SelectedFolder is null
|
|
|| !SelectedFolder.FullName.Equals(folder, StringComparison.OrdinalIgnoreCase))
|
|
return;
|
|
var rows = Messages.Where(row => ids.Contains(row.Header.Id)).ToList();
|
|
if (rows.Count > 0)
|
|
RemoveRows(rows);
|
|
}
|
|
|
|
private async Task<bool> ApplyFlagsAsync(
|
|
MailFlagUpdate update,
|
|
Action<MessageRowViewModel>? apply,
|
|
IReadOnlyList<MessageRowViewModel>? rows = null) {
|
|
var session = ActiveSession;
|
|
if (session is not { IsConnected: true, SupportsFolders: true } || SelectedFolder is null)
|
|
return false;
|
|
var targets = rows ?? TargetRows();
|
|
if (targets.Count == 0)
|
|
return false;
|
|
var ok = false;
|
|
await RunAsync(Copy.UpdatingFlags, async token => {
|
|
var result = await session.SetMessageFlagsAsync(
|
|
SelectedFolder.FullName,
|
|
TargetIds(targets),
|
|
update,
|
|
token);
|
|
if (!result.IsSuccess) {
|
|
Status = string.Join(" ", result.Messages);
|
|
return;
|
|
}
|
|
|
|
if (update.Seen is { } seen)
|
|
ApplyUnreadDelta(SelectedFolder, targets, seen);
|
|
if (apply is not null) {
|
|
foreach (var row in targets) {
|
|
apply(row);
|
|
row.RefreshMarks();
|
|
}
|
|
|
|
if (SelectedMailbox is not null)
|
|
_archive.UpdateFlags(
|
|
SelectedMailbox.Id,
|
|
SelectedFolder.FullName,
|
|
targets.Select(r => (r.Header.Id, r.Header.IsSeen, r.Header.IsFlagged)));
|
|
}
|
|
|
|
NotifyFolderCounts();
|
|
RefreshFolderStats();
|
|
Status = "Updated.";
|
|
ok = true;
|
|
});
|
|
return ok;
|
|
}
|
|
|
|
private void RemoveRows(IReadOnlyList<MessageRowViewModel> rows) {
|
|
ApplyRemovedCounts(rows);
|
|
foreach (var row in rows) {
|
|
Messages.Remove(row);
|
|
VisibleMessages.Remove(row);
|
|
}
|
|
|
|
if (SelectedMessage is not null && rows.Contains(SelectedMessage))
|
|
SelectedMessage = VisibleMessages.FirstOrDefault();
|
|
SelectedMessages.Clear();
|
|
NotifyMessageCommands();
|
|
RestoreMessageGridSelection();
|
|
RefreshFolderStats();
|
|
}
|
|
|
|
private void ApplyOpenedFolderCounts() {
|
|
if (SelectedFolder is null)
|
|
return;
|
|
var unread = Messages.Count(m => !m.Header.IsSeen);
|
|
SelectedFolder.Total = Messages.Count;
|
|
SelectedFolder.Unread = unread;
|
|
|
|
NotifyFolderCounts();
|
|
}
|
|
|
|
private void ApplyUnreadDelta(FolderRowViewModel folder, IReadOnlyList<MessageRowViewModel> rows, bool seen) {
|
|
var change = seen
|
|
? rows.Count(r => !r.Header.IsSeen)
|
|
: rows.Count(r => r.Header.IsSeen);
|
|
folder.Unread = Math.Max(0, folder.Unread + (seen ? -change : change));
|
|
}
|
|
|
|
private void ApplyRemovedCounts(IReadOnlyList<MessageRowViewModel> rows) {
|
|
if (SelectedFolder is not { } folder)
|
|
return;
|
|
folder.Total = Math.Max(0, folder.Total - rows.Count);
|
|
folder.Unread = Math.Max(0, folder.Unread - rows.Count(r => !r.Header.IsSeen));
|
|
NotifyFolderCounts();
|
|
}
|
|
|
|
private void ApplyFolderMoveCounts(
|
|
MailboxAccount sourceBox,
|
|
MailboxAccount destBox,
|
|
string source,
|
|
string destFolder,
|
|
int count,
|
|
IReadOnlyList<MessageRowViewModel>? rows) {
|
|
var unread = rows?.Count(r => !r.Header.IsSeen) ?? 0;
|
|
var from = FolderRow(sourceBox, source);
|
|
var dest = FolderRow(destBox, destFolder);
|
|
if (from is not null) {
|
|
from.Total = Math.Max(0, from.Total - count);
|
|
from.Unread = Math.Max(0, from.Unread - unread);
|
|
}
|
|
|
|
if (dest is not null) {
|
|
dest.Total += count;
|
|
dest.Unread += unread;
|
|
}
|
|
|
|
NotifyFolderCounts();
|
|
}
|
|
|
|
private FolderRowViewModel? FolderRow(MailboxAccount? box, string fullName) =>
|
|
FoldersFor(box).FirstOrDefault(f =>
|
|
f.FullName.Equals(fullName, StringComparison.OrdinalIgnoreCase));
|
|
|
|
private void NotifyFolderCounts() {
|
|
foreach (var account in FolderTree) {
|
|
foreach (var child in account.Children)
|
|
child.NotifyCounts();
|
|
account.NotifyCounts();
|
|
}
|
|
}
|
|
|
|
private void RefreshFolderStats() {
|
|
if (SelectedFolder is null) {
|
|
FolderStats = "";
|
|
return;
|
|
}
|
|
|
|
FolderStats = MailFolderStats.Line(
|
|
SelectedFolder.Name,
|
|
SelectedFolder.Total,
|
|
SelectedFolder.Unread,
|
|
VisibleMessages.Count);
|
|
}
|
|
|
|
private static IReadOnlyList<MailRecipient> Recipients(string? text) =>
|
|
RecipientParser.Parse(text);
|
|
|
|
private static List<MailRecipient> DistinctRecipients(IEnumerable<MailRecipient> rows, string self) =>
|
|
rows
|
|
.Where(r => r.IsValid && !r.Address.Equals(self, StringComparison.OrdinalIgnoreCase))
|
|
.DistinctBy(r => r.Address, StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
|
|
private static string JoinRecipients(IEnumerable<MailRecipient> rows) =>
|
|
string.Join(", ", rows.Select(r => r.Tooltip));
|
|
|
|
private void KickFolderLoad(string folder) {
|
|
if (!Dispatcher.UIThread.CheckAccess()) {
|
|
Dispatcher.UIThread.Post(() => KickFolderLoad(folder));
|
|
return;
|
|
}
|
|
|
|
_folderLoad?.Cancel();
|
|
_folderLoad?.Dispose();
|
|
var cts = new CancellationTokenSource();
|
|
_folderLoad = cts;
|
|
Status = Copy.OpeningFolder;
|
|
_ = OpenFolderAsync(folder, cts.Token);
|
|
}
|
|
|
|
private async Task OpenFolderAsync(string folder, CancellationToken token) {
|
|
try {
|
|
await LoadMessagesAsync(folder, token).ConfigureAwait(false);
|
|
}
|
|
catch (OperationCanceledException) {
|
|
}
|
|
}
|
|
|
|
private void PauseIndexing() =>
|
|
Interlocked.Increment(ref _indexPause);
|
|
|
|
private void ResumeIndexing() =>
|
|
Interlocked.Decrement(ref _indexPause);
|
|
|
|
private async Task WaitIfIndexingPausedAsync(CancellationToken token) {
|
|
while (Volatile.Read(ref _indexPause) > 0) {
|
|
token.ThrowIfCancellationRequested();
|
|
await Task.Delay(40, token).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
private static string CatalogKey(string mailboxId, string folder) =>
|
|
mailboxId + "\n" + folder;
|
|
|
|
private async Task UiAsync(Action work) {
|
|
if (Dispatcher.UIThread.CheckAccess())
|
|
work();
|
|
else
|
|
await Dispatcher.UIThread.InvokeAsync(work);
|
|
|
|
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
|
|
}
|
|
|
|
private async Task RunAsync(string busy, Func<CancellationToken, Task> work) {
|
|
_work?.Cancel();
|
|
_work?.Dispose();
|
|
_work = new CancellationTokenSource();
|
|
IsBusy = true;
|
|
ConnectCommand.NotifyCanExecuteChanged();
|
|
GetMessagesCommand.NotifyCanExecuteChanged();
|
|
SignInGoogleCommand.NotifyCanExecuteChanged();
|
|
SignInMicrosoftCommand.NotifyCanExecuteChanged();
|
|
NotifyMessageCommands();
|
|
Status = busy;
|
|
try {
|
|
await work(_work.Token);
|
|
}
|
|
catch (OperationCanceledException) {
|
|
Status = Copy.Cancelled;
|
|
}
|
|
finally {
|
|
IsBusy = false;
|
|
ConnectCommand.NotifyCanExecuteChanged();
|
|
GetMessagesCommand.NotifyCanExecuteChanged();
|
|
SignInGoogleCommand.NotifyCanExecuteChanged();
|
|
SignInMicrosoftCommand.NotifyCanExecuteChanged();
|
|
NotifyMessageCommands();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
public sealed class PromptRequest {
|
|
public required string Title { get; init; }
|
|
|
|
public required string Message { get; init; }
|
|
|
|
public string Placeholder { get; init; } = "";
|
|
|
|
public bool ConfirmOnly { get; init; }
|
|
|
|
public string? ConfirmLabel { get; init; }
|
|
}
|