2024-04-18 01:44:37 +02:00
|
|
|
using System;
|
2024-09-14 18:41:33 +02:00
|
|
|
using System.Collections.Generic;
|
2024-04-18 01:44:37 +02:00
|
|
|
using System.Collections.ObjectModel;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
|
|
|
using CommunityToolkit.Mvvm.Input;
|
|
|
|
|
using CommunityToolkit.Mvvm.Messaging;
|
|
|
|
|
using MailKit;
|
2025-02-16 11:43:30 +01:00
|
|
|
|
2024-04-18 01:44:37 +02:00
|
|
|
using MimeKit;
|
2025-11-23 20:56:57 +01:00
|
|
|
using MimeKit.Cryptography;
|
2024-04-18 01:44:37 +02:00
|
|
|
using Serilog;
|
|
|
|
|
using Wino.Core.Domain;
|
2024-11-10 23:28:25 +01:00
|
|
|
using Wino.Core.Domain.Entities.Mail;
|
|
|
|
|
using Wino.Core.Domain.Entities.Shared;
|
2024-04-18 01:44:37 +02:00
|
|
|
using Wino.Core.Domain.Enums;
|
2026-02-16 01:39:53 +01:00
|
|
|
using Wino.Core.Domain.Exceptions;
|
2024-04-18 01:44:37 +02:00
|
|
|
using Wino.Core.Domain.Interfaces;
|
|
|
|
|
using Wino.Core.Domain.Models.MailItem;
|
|
|
|
|
using Wino.Core.Domain.Models.Menus;
|
|
|
|
|
using Wino.Core.Domain.Models.Navigation;
|
2025-10-21 01:27:29 +02:00
|
|
|
using Wino.Core.Domain.Models.Printing;
|
2024-04-18 01:44:37 +02:00
|
|
|
using Wino.Core.Domain.Models.Reader;
|
2025-10-04 23:10:07 +02:00
|
|
|
using Wino.Core.Services;
|
2024-04-18 01:44:37 +02:00
|
|
|
using Wino.Mail.ViewModels.Data;
|
2026-04-11 01:04:59 +02:00
|
|
|
using Wino.Mail.ViewModels.Models;
|
2024-08-05 00:36:26 +02:00
|
|
|
using Wino.Messaging.Client.Mails;
|
2025-06-21 01:40:25 +02:00
|
|
|
using Wino.Messaging.UI;
|
2024-11-09 19:18:06 +01:00
|
|
|
using IMailService = Wino.Core.Domain.Interfaces.IMailService;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
namespace Wino.Mail.ViewModels;
|
|
|
|
|
|
|
|
|
|
public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
2025-06-21 01:40:25 +02:00
|
|
|
IRecipient<ThumbnailAdded>,
|
2025-02-16 11:54:23 +01:00
|
|
|
ITransferProgress // For listening IMAP message download progress.
|
2024-04-18 01:44:37 +02:00
|
|
|
{
|
2026-04-11 01:04:59 +02:00
|
|
|
public event EventHandler CloseRequested;
|
|
|
|
|
public event EventHandler<ComposeDraftRequestedEventArgs> ComposeRequested;
|
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
private readonly IMailDialogService _dialogService;
|
|
|
|
|
private readonly IUnderlyingThemeService _underlyingThemeService;
|
|
|
|
|
|
|
|
|
|
private readonly IMimeFileService _mimeFileService;
|
|
|
|
|
private readonly Core.Domain.Interfaces.IMailService _mailService;
|
2026-02-16 01:39:53 +01:00
|
|
|
private readonly IFolderService _folderService;
|
2025-02-16 11:54:23 +01:00
|
|
|
private readonly IFileService _fileService;
|
|
|
|
|
private readonly IWinoRequestDelegator _requestDelegator;
|
|
|
|
|
private readonly IContactService _contactService;
|
|
|
|
|
private readonly IClipboardService _clipboardService;
|
|
|
|
|
private readonly IUnsubscriptionService _unsubscriptionService;
|
|
|
|
|
private readonly IApplicationConfiguration _applicationConfiguration;
|
|
|
|
|
private bool forceImageLoading = false;
|
|
|
|
|
|
|
|
|
|
private MailItemViewModel initializedMailItemViewModel = null;
|
|
|
|
|
private MimeMessageInformation initializedMimeMessageInformation = null;
|
|
|
|
|
|
|
|
|
|
// Func to get WebView2 to save current HTML as PDF to given location.
|
2026-04-11 15:07:22 +02:00
|
|
|
// Used in 'Save as' functionality.
|
2025-02-16 11:54:23 +01:00
|
|
|
public Func<string, Task<bool>> SaveHTMLasPDFFunc { get; set; }
|
2026-04-11 15:07:22 +02:00
|
|
|
public Func<WebView2PrintSettingsModel, Task<Stream>> RenderPdfStreamFuncAsync { get; set; }
|
2026-04-11 01:04:59 +02:00
|
|
|
public Func<string, Task> RenderHtmlAsyncFunc { get; set; }
|
|
|
|
|
public Func<Task> ClearRenderedHtmlAsyncFunc { get; set; }
|
2025-10-20 21:10:14 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
#region Properties
|
|
|
|
|
|
|
|
|
|
public bool ShouldDisplayDownloadProgress => IsIndetermineProgress || (CurrentDownloadPercentage > 0 && CurrentDownloadPercentage <= 100);
|
|
|
|
|
public bool CanUnsubscribe => CurrentRenderModel?.UnsubscribeInfo?.CanUnsubscribe ?? false;
|
2025-11-23 20:56:57 +01:00
|
|
|
public bool IsSmimeSigned => (CurrentRenderModel?.Signatures?.Count ?? 0) > 0;
|
|
|
|
|
public bool IsSmimeEncrypted => CurrentRenderModel?.IsSmimeEncrypted ?? false;
|
2025-10-03 15:46:38 +02:00
|
|
|
public bool IsJunkMail => initializedMailItemViewModel?.MailCopy.AssignedFolder != null && initializedMailItemViewModel.MailCopy.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk;
|
2025-11-23 20:56:57 +01:00
|
|
|
public bool SmimeSignaturesValid => CurrentRenderModel?.Signatures?.Any(x => x.Value) ?? false;
|
|
|
|
|
public bool SmimeSignaturesInvalid => !SmimeSignaturesValid;
|
2025-02-16 11:54:23 +01:00
|
|
|
|
|
|
|
|
public bool IsImageRenderingDisabled
|
2024-04-18 01:44:37 +02:00
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
get
|
2024-04-18 01:44:37 +02:00
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
if (IsJunkMail)
|
2024-04-18 01:44:37 +02:00
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
return !forceImageLoading;
|
2024-04-18 01:44:37 +02:00
|
|
|
}
|
2025-02-16 11:54:23 +01:00
|
|
|
else
|
2024-04-18 01:44:37 +02:00
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
return !CurrentRenderModel?.MailRenderingOptions?.LoadImages ?? false;
|
2024-04-18 01:44:37 +02:00
|
|
|
}
|
|
|
|
|
}
|
2025-02-16 11:54:23 +01:00
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
private bool isDarkWebviewRenderer;
|
|
|
|
|
public bool IsDarkWebviewRenderer
|
|
|
|
|
{
|
|
|
|
|
get => isDarkWebviewRenderer;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (SetProperty(ref isDarkWebviewRenderer, value))
|
2025-02-16 11:43:30 +01:00
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
InitializeCommandBarItems();
|
2025-02-16 11:43:30 +01:00
|
|
|
}
|
2025-02-16 11:35:43 +01:00
|
|
|
}
|
2025-02-16 11:54:23 +01:00
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
[ObservableProperty]
|
|
|
|
|
[NotifyPropertyChangedFor(nameof(ShouldDisplayDownloadProgress))]
|
2025-02-16 14:38:53 +01:00
|
|
|
public partial bool IsIndetermineProgress { get; set; }
|
2025-02-16 11:54:23 +01:00
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
[NotifyPropertyChangedFor(nameof(ShouldDisplayDownloadProgress))]
|
2025-02-16 14:38:53 +01:00
|
|
|
public partial double CurrentDownloadPercentage { get; set; }
|
2025-02-16 11:54:23 +01:00
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
[NotifyPropertyChangedFor(nameof(CanUnsubscribe))]
|
2025-11-23 20:56:57 +01:00
|
|
|
[NotifyPropertyChangedFor(nameof(IsSmimeSigned))]
|
|
|
|
|
[NotifyPropertyChangedFor(nameof(IsSmimeEncrypted))]
|
|
|
|
|
[NotifyPropertyChangedFor(nameof(SmimeSignaturesValid))]
|
|
|
|
|
[NotifyPropertyChangedFor(nameof(SmimeSignaturesInvalid))]
|
2025-02-16 14:38:53 +01:00
|
|
|
public partial MailRenderModel CurrentRenderModel { get; set; }
|
2025-02-16 11:54:23 +01:00
|
|
|
|
|
|
|
|
[ObservableProperty]
|
2025-02-16 14:38:53 +01:00
|
|
|
public partial string Subject { get; set; }
|
2025-02-16 11:54:23 +01:00
|
|
|
|
|
|
|
|
[ObservableProperty]
|
2025-02-16 14:38:53 +01:00
|
|
|
public partial string FromAddress { get; set; }
|
2025-02-16 11:54:23 +01:00
|
|
|
|
|
|
|
|
[ObservableProperty]
|
2025-02-16 14:38:53 +01:00
|
|
|
public partial string FromName { get; set; }
|
2025-02-16 11:54:23 +01:00
|
|
|
|
|
|
|
|
[ObservableProperty]
|
2026-02-25 01:41:48 +01:00
|
|
|
public partial IMailItemDisplayInformation CurrentMailItemDisplayInformation { get; set; }
|
2025-02-16 11:54:23 +01:00
|
|
|
|
|
|
|
|
[ObservableProperty]
|
2025-02-16 14:38:53 +01:00
|
|
|
public partial DateTime CreationDate { get; set; }
|
|
|
|
|
public ObservableCollection<AccountContactViewModel> ToItems { get; set; } = [];
|
|
|
|
|
public ObservableCollection<AccountContactViewModel> CcItems { get; set; } = [];
|
|
|
|
|
public ObservableCollection<AccountContactViewModel> BccItems { get; set; } = [];
|
2025-02-16 11:54:23 +01:00
|
|
|
public ObservableCollection<MailAttachmentViewModel> Attachments { get; set; } = [];
|
2026-04-03 19:50:52 +02:00
|
|
|
public ObservableCollection<IMenuOperation> MenuItems { get; set; } = [];
|
2025-02-16 11:54:23 +01:00
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
public INativeAppService NativeAppService { get; }
|
|
|
|
|
public IStatePersistanceService StatePersistenceService { get; }
|
|
|
|
|
public IPreferencesService PreferencesService { get; }
|
|
|
|
|
public IPrintService PrintService { get; }
|
2026-04-03 11:56:25 +02:00
|
|
|
public Guid? CurrentMailAccountId => initializedMailItemViewModel?.MailCopy.AssignedAccount?.Id;
|
|
|
|
|
public Guid? CurrentMailFileId => initializedMailItemViewModel?.MailCopy.FileId;
|
2025-02-16 11:54:23 +01:00
|
|
|
|
|
|
|
|
public MailRenderingPageViewModel(IMailDialogService dialogService,
|
2025-11-23 20:56:57 +01:00
|
|
|
INativeAppService nativeAppService,
|
|
|
|
|
IUnderlyingThemeService underlyingThemeService,
|
|
|
|
|
IMimeFileService mimeFileService,
|
|
|
|
|
IMailService mailService,
|
2026-02-16 01:39:53 +01:00
|
|
|
IFolderService folderService,
|
2025-11-23 20:56:57 +01:00
|
|
|
IFileService fileService,
|
|
|
|
|
IWinoRequestDelegator requestDelegator,
|
|
|
|
|
IStatePersistanceService statePersistenceService,
|
|
|
|
|
IContactService contactService,
|
|
|
|
|
IClipboardService clipboardService,
|
|
|
|
|
IUnsubscriptionService unsubscriptionService,
|
|
|
|
|
IPreferencesService preferencesService,
|
|
|
|
|
IPrintService printService,
|
|
|
|
|
IApplicationConfiguration applicationConfiguration)
|
2025-02-16 11:54:23 +01:00
|
|
|
{
|
|
|
|
|
_dialogService = dialogService;
|
|
|
|
|
NativeAppService = nativeAppService;
|
|
|
|
|
StatePersistenceService = statePersistenceService;
|
|
|
|
|
_contactService = contactService;
|
|
|
|
|
PreferencesService = preferencesService;
|
|
|
|
|
PrintService = printService;
|
|
|
|
|
_applicationConfiguration = applicationConfiguration;
|
|
|
|
|
_clipboardService = clipboardService;
|
|
|
|
|
_unsubscriptionService = unsubscriptionService;
|
|
|
|
|
_underlyingThemeService = underlyingThemeService;
|
|
|
|
|
_mimeFileService = mimeFileService;
|
|
|
|
|
_mailService = mailService;
|
2026-02-16 01:39:53 +01:00
|
|
|
_folderService = folderService;
|
2025-02-16 11:54:23 +01:00
|
|
|
_fileService = fileService;
|
|
|
|
|
_requestDelegator = requestDelegator;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private async Task CopyClipboard(string copyText)
|
|
|
|
|
{
|
|
|
|
|
try
|
2025-02-16 11:43:30 +01:00
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
await _clipboardService.CopyClipboardAsync(copyText);
|
2025-02-16 11:35:43 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
_dialogService.InfoBarMessage(Translator.ClipboardTextCopied_Title, string.Format(Translator.ClipboardTextCopied_Message, copyText), InfoBarMessageType.Information);
|
2025-02-16 11:43:30 +01:00
|
|
|
}
|
2025-02-16 11:54:23 +01:00
|
|
|
catch (Exception)
|
2025-02-16 11:35:43 +01:00
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error, string.Format(Translator.ClipboardTextCopyFailed_Message, copyText), InfoBarMessageType.Error);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-02-16 11:35:43 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
[RelayCommand]
|
|
|
|
|
private async Task ForceImageLoading()
|
|
|
|
|
{
|
|
|
|
|
if (initializedMailItemViewModel == null && initializedMimeMessageInformation == null) return;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
await RenderAsync(initializedMimeMessageInformation, ignoreJunkFilter: true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private async Task UnsubscribeAsync()
|
|
|
|
|
{
|
|
|
|
|
if (!(CurrentRenderModel?.UnsubscribeInfo?.CanUnsubscribe ?? false)) return;
|
|
|
|
|
|
|
|
|
|
bool confirmed;
|
|
|
|
|
|
|
|
|
|
// Try to unsubscribe by http first.
|
|
|
|
|
if (CurrentRenderModel.UnsubscribeInfo.HttpLink is not null)
|
|
|
|
|
{
|
|
|
|
|
if (!Uri.IsWellFormedUriString(CurrentRenderModel.UnsubscribeInfo.HttpLink, UriKind.RelativeOrAbsolute))
|
2025-02-16 11:43:30 +01:00
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
_dialogService.InfoBarMessage(Translator.Info_UnsubscribeLinkInvalidTitle, Translator.Info_UnsubscribeLinkInvalidMessage, InfoBarMessageType.Error);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-02-16 11:35:43 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
// Support for List-Unsubscribe-Post header. It can be done without launching browser.
|
|
|
|
|
// https://datatracker.ietf.org/doc/html/rfc8058
|
|
|
|
|
if (CurrentRenderModel.UnsubscribeInfo.IsOneClick)
|
|
|
|
|
{
|
|
|
|
|
confirmed = await _dialogService.ShowConfirmationDialogAsync(string.Format(Translator.DialogMessage_UnsubscribeConfirmationOneClickMessage, FromName), Translator.DialogMessage_UnsubscribeConfirmationTitle, Translator.Unsubscribe);
|
|
|
|
|
if (!confirmed) return;
|
|
|
|
|
|
|
|
|
|
bool isOneClickUnsubscribed = await _unsubscriptionService.OneClickUnsubscribeAsync(CurrentRenderModel.UnsubscribeInfo);
|
|
|
|
|
|
|
|
|
|
if (isOneClickUnsubscribed)
|
2024-05-01 14:23:23 +02:00
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
_dialogService.InfoBarMessage(Translator.Unsubscribe, string.Format(Translator.Info_UnsubscribeSuccessMessage, FromName), InfoBarMessageType.Success);
|
2024-05-01 14:23:23 +02:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error, Translator.Info_UnsubscribeErrorMessage, InfoBarMessageType.Error);
|
2024-05-01 14:23:23 +02:00
|
|
|
}
|
|
|
|
|
}
|
2025-02-16 11:54:23 +01:00
|
|
|
else
|
2024-05-01 14:23:23 +02:00
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
confirmed = await _dialogService.ShowConfirmationDialogAsync(string.Format(Translator.DialogMessage_UnsubscribeConfirmationGoToWebsiteMessage, FromName), Translator.DialogMessage_UnsubscribeConfirmationTitle, Translator.DialogMessage_UnsubscribeConfirmationGoToWebsiteConfirmButton);
|
2024-05-01 14:23:23 +02:00
|
|
|
if (!confirmed) return;
|
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
await NativeAppService.LaunchUriAsync(new Uri(CurrentRenderModel.UnsubscribeInfo.HttpLink));
|
2024-05-01 14:23:23 +02:00
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
}
|
2025-02-16 11:54:23 +01:00
|
|
|
else if (CurrentRenderModel.UnsubscribeInfo.MailToLink is not null)
|
2025-02-16 11:43:30 +01:00
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
confirmed = await _dialogService.ShowConfirmationDialogAsync(string.Format(Translator.DialogMessage_UnsubscribeConfirmationMailtoMessage, FromName, new string(CurrentRenderModel.UnsubscribeInfo.MailToLink.Skip(7).ToArray())), Translator.DialogMessage_UnsubscribeConfirmationTitle, Translator.Unsubscribe);
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
if (!confirmed) return;
|
|
|
|
|
|
|
|
|
|
// TODO: Implement automatic mail send after user confirms the action.
|
|
|
|
|
// Currently it will launch compose page and user should manually press send button.
|
|
|
|
|
await NativeAppService.LaunchUriAsync(new Uri(CurrentRenderModel.UnsubscribeInfo.MailToLink));
|
2024-04-18 01:44:37 +02:00
|
|
|
}
|
2025-02-16 11:54:23 +01:00
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
[RelayCommand]
|
2026-04-03 19:50:52 +02:00
|
|
|
private async Task OperationClicked(IMenuOperation menuItem)
|
2025-02-16 11:54:23 +01:00
|
|
|
{
|
2026-04-03 19:50:52 +02:00
|
|
|
if (menuItem is not MailOperationMenuItem mailOperationMenuItem) return;
|
2025-02-16 11:54:23 +01:00
|
|
|
|
2026-04-03 19:50:52 +02:00
|
|
|
await HandleMailOperationAsync(mailOperationMenuItem.Operation);
|
2025-02-16 11:54:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task HandleMailOperationAsync(MailOperation operation)
|
|
|
|
|
{
|
2026-02-16 01:39:53 +01:00
|
|
|
try
|
2025-02-16 11:54:23 +01:00
|
|
|
{
|
2026-04-03 19:50:52 +02:00
|
|
|
if (operation == MailOperation.SaveAs)
|
2025-02-16 11:54:23 +01:00
|
|
|
{
|
2026-02-16 01:39:53 +01:00
|
|
|
await SaveAsAsync();
|
2025-10-21 01:27:29 +02:00
|
|
|
}
|
2026-02-16 01:39:53 +01:00
|
|
|
else if (operation == MailOperation.Print)
|
2025-10-21 01:27:29 +02:00
|
|
|
{
|
2026-04-11 15:07:22 +02:00
|
|
|
var printingResult = await PrintAsync();
|
2026-02-16 01:39:53 +01:00
|
|
|
|
|
|
|
|
// TODO: More detailed printing result handling.
|
|
|
|
|
if (printingResult == PrintingResult.Submitted)
|
2025-02-16 11:43:30 +01:00
|
|
|
{
|
2026-02-16 01:39:53 +01:00
|
|
|
_dialogService.InfoBarMessage(Translator.DialogMessage_PrintingSuccessTitle, Translator.DialogMessage_PrintingSuccessMessage, InfoBarMessageType.Success);
|
|
|
|
|
}
|
2026-04-11 15:07:22 +02:00
|
|
|
else if (printingResult == PrintingResult.Canceled)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-16 01:39:53 +01:00
|
|
|
else if (printingResult == PrintingResult.Failed)
|
2025-02-16 11:54:23 +01:00
|
|
|
{
|
2026-02-16 01:39:53 +01:00
|
|
|
_dialogService.InfoBarMessage(Translator.DialogMessage_PrintingFailedTitle, Translator.DialogMessage_PrintingFailedMessage, InfoBarMessageType.Error);
|
2025-02-16 11:54:23 +01:00
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2026-02-16 01:39:53 +01:00
|
|
|
}
|
|
|
|
|
else if (operation == MailOperation.ViewMessageSource)
|
|
|
|
|
{
|
|
|
|
|
await _dialogService.ShowMessageSourceDialogAsync(initializedMimeMessageInformation.MimeMessage.ToString());
|
|
|
|
|
}
|
|
|
|
|
else if (operation == MailOperation.Reply || operation == MailOperation.ReplyAll || operation == MailOperation.Forward)
|
|
|
|
|
{
|
|
|
|
|
if (initializedMailItemViewModel == null) return;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2026-02-16 01:39:53 +01:00
|
|
|
// Create new draft.
|
|
|
|
|
var draftOptions = new DraftCreationOptions()
|
|
|
|
|
{
|
|
|
|
|
Reason = operation switch
|
|
|
|
|
{
|
|
|
|
|
MailOperation.Reply => DraftCreationReason.Reply,
|
|
|
|
|
MailOperation.ReplyAll => DraftCreationReason.ReplyAll,
|
|
|
|
|
MailOperation.Forward => DraftCreationReason.Forward,
|
|
|
|
|
_ => DraftCreationReason.Empty
|
|
|
|
|
},
|
|
|
|
|
ReferencedMessage = new ReferencedMessage()
|
|
|
|
|
{
|
|
|
|
|
MimeMessage = initializedMimeMessageInformation.MimeMessage,
|
|
|
|
|
MailCopy = initializedMailItemViewModel.MailCopy
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(initializedMailItemViewModel.MailCopy.AssignedAccount.Id, draftOptions).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
var draftPreparationRequest = new DraftPreparationRequest(initializedMailItemViewModel.MailCopy.AssignedAccount, draftMailCopy, draftBase64MimeMessage, draftOptions.Reason, initializedMailItemViewModel.MailCopy);
|
|
|
|
|
|
|
|
|
|
await _requestDelegator.ExecuteAsync(draftPreparationRequest);
|
2026-04-11 01:04:59 +02:00
|
|
|
ComposeRequested?.Invoke(this, new ComposeDraftRequestedEventArgs(draftMailCopy.UniqueId));
|
2025-02-16 11:54:23 +01:00
|
|
|
|
2026-02-16 01:39:53 +01:00
|
|
|
}
|
|
|
|
|
else if (initializedMailItemViewModel != null)
|
|
|
|
|
{
|
|
|
|
|
// All other operations require a mail item.
|
|
|
|
|
var prepRequest = new MailOperationPreperationRequest(operation, initializedMailItemViewModel.MailCopy);
|
|
|
|
|
await _requestDelegator.ExecuteAsync(prepRequest);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (UnavailableSpecialFolderException unavailableSpecialFolderException)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.Info_MissingFolderTitle,
|
|
|
|
|
string.Format(Translator.Info_MissingFolderMessage, unavailableSpecialFolderException.SpecialFolderType),
|
|
|
|
|
InfoBarMessageType.Warning,
|
|
|
|
|
Translator.SettingConfigureSpecialFolders_Button,
|
|
|
|
|
() =>
|
|
|
|
|
{
|
|
|
|
|
_dialogService.HandleSystemFolderConfigurationDialogAsync(unavailableSpecialFolderException.AccountId, _folderService);
|
|
|
|
|
});
|
2025-02-16 11:54:23 +01:00
|
|
|
}
|
2026-02-16 01:39:53 +01:00
|
|
|
catch (NotImplementedException)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.ShowNotSupportedMessage();
|
2025-02-16 11:54:23 +01:00
|
|
|
}
|
2026-02-16 01:39:53 +01:00
|
|
|
catch (Exception ex)
|
2025-02-16 11:43:30 +01:00
|
|
|
{
|
2026-02-16 01:39:53 +01:00
|
|
|
Log.Error(ex, "Mail operation execution failed. Operation: {Operation}", operation);
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.Info_RequestCreationFailedTitle, ex.Message, InfoBarMessageType.Error);
|
2025-02-16 11:54:23 +01:00
|
|
|
}
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
private CancellationTokenSource renderCancellationTokenSource = new CancellationTokenSource();
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
|
|
|
|
{
|
|
|
|
|
base.OnNavigatedTo(mode, parameters);
|
2024-09-10 10:14:13 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
renderCancellationTokenSource.Cancel();
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
initializedMailItemViewModel = null;
|
|
|
|
|
initializedMimeMessageInformation = null;
|
2026-02-25 01:41:48 +01:00
|
|
|
CurrentMailItemDisplayInformation = null;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2026-04-11 01:04:59 +02:00
|
|
|
if (ClearRenderedHtmlAsyncFunc != null)
|
|
|
|
|
{
|
|
|
|
|
await ExecuteUIThread(async () => await ClearRenderedHtmlAsyncFunc());
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
// This page can be accessed for 2 purposes.
|
|
|
|
|
// 1. Rendering a mail item when the user selects.
|
|
|
|
|
// 2. Rendering an existing EML file with MimeMessage.
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
// MimeMessage rendering must be readonly and no command bar items must be shown except common
|
|
|
|
|
// items like dark/light editor, zoom, print etc.
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
// Configure common rendering properties first.
|
|
|
|
|
IsDarkWebviewRenderer = _underlyingThemeService.IsUnderlyingThemeDark();
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
renderCancellationTokenSource = new CancellationTokenSource();
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
// Mime content might not be available for now and might require a download.
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (parameters is MailItemViewModel selectedMailItemViewModel)
|
|
|
|
|
await RenderAsync(selectedMailItemViewModel, renderCancellationTokenSource.Token);
|
|
|
|
|
else if (parameters is MimeMessageInformation mimeMessageInformation)
|
|
|
|
|
await RenderAsync(mimeMessageInformation);
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
InitializeCommandBarItems();
|
|
|
|
|
}
|
|
|
|
|
catch (OperationCanceledException)
|
2025-02-16 11:35:43 +01:00
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
Log.Information("Canceled mail rendering.");
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.Info_MailRenderingFailedTitle, string.Format(Translator.Info_MailRenderingFailedMessage, ex.Message), InfoBarMessageType.Error);
|
2025-02-16 11:35:43 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
Log.Error(ex, "Failed to render mail.");
|
2025-02-16 11:35:43 +01:00
|
|
|
}
|
2025-02-16 11:54:23 +01:00
|
|
|
}
|
2025-02-16 11:43:30 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
private async Task HandleSingleItemDownloadAsync(MailItemViewModel mailItemViewModel)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
// To show the progress on the UI.
|
|
|
|
|
CurrentDownloadPercentage = 1;
|
|
|
|
|
|
2025-10-04 23:10:07 +02:00
|
|
|
// Download missing MIME message using SynchronizationManager
|
|
|
|
|
await SynchronizationManager.Instance.DownloadMimeMessageAsync(
|
2025-10-18 11:45:10 +02:00
|
|
|
mailItemViewModel.MailCopy,
|
|
|
|
|
mailItemViewModel.MailCopy.AssignedAccount.Id);
|
2025-02-16 11:54:23 +01:00
|
|
|
}
|
|
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
{
|
|
|
|
|
Log.Information("MIME download is canceled.");
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error, ex.Message, InfoBarMessageType.Error);
|
|
|
|
|
}
|
|
|
|
|
finally
|
2024-04-18 01:44:37 +02:00
|
|
|
{
|
2024-09-10 10:14:13 +02:00
|
|
|
ResetProgress();
|
2025-02-16 11:54:23 +01:00
|
|
|
}
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
private async Task RenderAsync(MailItemViewModel mailItemViewModel, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
ResetProgress();
|
2025-10-03 15:46:38 +02:00
|
|
|
var isMimeExists = await _mimeFileService.IsMimeExistAsync(mailItemViewModel.MailCopy.AssignedAccount.Id, mailItemViewModel.MailCopy.FileId);
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
if (!isMimeExists)
|
|
|
|
|
{
|
|
|
|
|
await HandleSingleItemDownloadAsync(mailItemViewModel);
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
// Find the MIME for this item and render it.
|
|
|
|
|
var mimeMessageInformation = await _mimeFileService.GetMimeMessageInformationAsync(mailItemViewModel.MailCopy.FileId,
|
2025-10-03 15:46:38 +02:00
|
|
|
mailItemViewModel.MailCopy.AssignedAccount.Id,
|
2025-02-16 11:54:23 +01:00
|
|
|
cancellationToken).ConfigureAwait(false);
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
if (mimeMessageInformation == null)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.Info_MessageCorruptedTitle, Translator.Info_MessageCorruptedMessage, InfoBarMessageType.Error);
|
|
|
|
|
return;
|
2024-04-18 01:44:37 +02:00
|
|
|
}
|
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
initializedMailItemViewModel = mailItemViewModel;
|
2026-02-25 01:41:48 +01:00
|
|
|
await ExecuteUIThread(() => { CurrentMailItemDisplayInformation = mailItemViewModel; });
|
2025-02-16 11:54:23 +01:00
|
|
|
await RenderAsync(mimeMessageInformation);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task RenderAsync(MimeMessageInformation mimeMessageInformation, bool ignoreJunkFilter = false)
|
|
|
|
|
{
|
|
|
|
|
forceImageLoading = ignoreJunkFilter;
|
2024-09-14 18:41:33 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
var message = mimeMessageInformation.MimeMessage;
|
|
|
|
|
var messagePath = mimeMessageInformation.Path;
|
2025-02-16 11:35:43 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
initializedMimeMessageInformation = mimeMessageInformation;
|
2025-02-16 11:35:43 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
// TODO: Handle S/MIME decryption.
|
|
|
|
|
// initializedMimeMessageInformation.MimeMessage.Body is MultipartSigned
|
2025-02-16 11:35:43 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
var renderingOptions = PreferencesService.GetRenderingOptions();
|
2025-02-16 11:35:43 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
// Prepare account contacts info in advance, to avoid UI shifts after clearing collections.
|
|
|
|
|
var toAccountContacts = await GetAccountContacts(message.To);
|
|
|
|
|
var ccAccountContacts = await GetAccountContacts(message.Cc);
|
|
|
|
|
var bccAccountContacts = await GetAccountContacts(message.Bcc);
|
2025-02-16 11:35:43 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
await ExecuteUIThread(() =>
|
|
|
|
|
{
|
|
|
|
|
Attachments.Clear();
|
|
|
|
|
ToItems.Clear();
|
|
|
|
|
CcItems.Clear();
|
|
|
|
|
BccItems.Clear();
|
|
|
|
|
|
|
|
|
|
foreach (var item in toAccountContacts)
|
|
|
|
|
ToItems.Add(item);
|
|
|
|
|
foreach (var item in ccAccountContacts)
|
|
|
|
|
CcItems.Add(item);
|
|
|
|
|
foreach (var item in bccAccountContacts)
|
|
|
|
|
BccItems.Add(item);
|
|
|
|
|
|
|
|
|
|
Subject = string.IsNullOrWhiteSpace(message.Subject) ? Translator.MailItemNoSubject : message.Subject;
|
|
|
|
|
|
|
|
|
|
// TODO: FromName and FromAddress is probably not correct here for mail lists.
|
|
|
|
|
FromAddress = message.From.Mailboxes.FirstOrDefault()?.Address ?? Translator.UnknownAddress;
|
|
|
|
|
FromName = message.From.Mailboxes.FirstOrDefault()?.Name ?? Translator.UnknownSender;
|
2025-11-24 20:54:57 +01:00
|
|
|
|
2025-11-14 12:56:37 +01:00
|
|
|
// Use the received date from MailCopy if available, otherwise fall back to the sent date from MIME message
|
|
|
|
|
CreationDate = initializedMailItemViewModel?.MailCopy.CreationDate ?? message.Date.DateTime;
|
2025-02-16 11:54:23 +01:00
|
|
|
|
|
|
|
|
// Automatically disable images for Junk folder to prevent pixel tracking.
|
|
|
|
|
// This can only work for selected mail item rendering, not for EML file rendering.
|
|
|
|
|
if (initializedMailItemViewModel != null &&
|
2025-10-03 15:46:38 +02:00
|
|
|
initializedMailItemViewModel.MailCopy.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk)
|
2024-04-18 01:44:37 +02:00
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
renderingOptions.LoadImages = false;
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
// Load images if forced.
|
|
|
|
|
if (ignoreJunkFilter)
|
|
|
|
|
{
|
|
|
|
|
renderingOptions.LoadImages = true;
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
CurrentRenderModel = _mimeFileService.GetMailRenderModel(message, messagePath, renderingOptions);
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
foreach (var attachment in CurrentRenderModel.Attachments)
|
|
|
|
|
{
|
|
|
|
|
Attachments.Add(new MailAttachmentViewModel(attachment));
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
OnPropertyChanged(nameof(IsImageRenderingDisabled));
|
2024-07-09 01:05:16 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
StatePersistenceService.IsReadingMail = true;
|
|
|
|
|
});
|
2026-04-11 01:04:59 +02:00
|
|
|
|
|
|
|
|
if (RenderHtmlAsyncFunc != null)
|
|
|
|
|
{
|
|
|
|
|
await ExecuteUIThread(async () => await RenderHtmlAsyncFunc(CurrentRenderModel.RenderHtml));
|
|
|
|
|
}
|
2025-02-16 11:54:23 +01:00
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 14:38:53 +01:00
|
|
|
private async Task<List<AccountContactViewModel>> GetAccountContacts(InternetAddressList internetAddresses)
|
2025-02-16 11:54:23 +01:00
|
|
|
{
|
2025-02-16 14:38:53 +01:00
|
|
|
List<AccountContactViewModel> accounts = [];
|
2025-02-16 11:54:23 +01:00
|
|
|
foreach (var item in internetAddresses)
|
2024-09-14 18:41:33 +02:00
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
if (item is MailboxAddress mailboxAddress)
|
2024-09-14 18:41:33 +02:00
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
var foundContact = await _contactService.GetAddressInformationByAddressAsync(mailboxAddress.Address).ConfigureAwait(false)
|
|
|
|
|
?? new AccountContact() { Name = mailboxAddress.Name, Address = mailboxAddress.Address };
|
|
|
|
|
|
2025-02-16 14:38:53 +01:00
|
|
|
var contactViewModel = new AccountContactViewModel(foundContact);
|
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
// Make sure that user account first in the list.
|
2025-10-03 15:46:38 +02:00
|
|
|
if (string.Equals(contactViewModel.Address, initializedMailItemViewModel?.MailCopy.AssignedAccount?.Address, StringComparison.OrdinalIgnoreCase))
|
2024-09-14 18:41:33 +02:00
|
|
|
{
|
2025-02-16 14:38:53 +01:00
|
|
|
contactViewModel.IsMe = true;
|
|
|
|
|
accounts.Insert(0, contactViewModel);
|
2024-09-14 18:41:33 +02:00
|
|
|
}
|
2025-02-16 11:54:23 +01:00
|
|
|
else
|
2024-09-14 18:41:33 +02:00
|
|
|
{
|
2025-02-16 14:38:53 +01:00
|
|
|
accounts.Add(contactViewModel);
|
2024-09-14 18:41:33 +02:00
|
|
|
}
|
|
|
|
|
}
|
2025-02-16 11:54:23 +01:00
|
|
|
else if (item is GroupAddress groupAddress)
|
|
|
|
|
{
|
|
|
|
|
accounts.AddRange(await GetAccountContacts(groupAddress.Members));
|
|
|
|
|
}
|
2025-02-16 11:43:30 +01:00
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-18 20:51:02 +01:00
|
|
|
if (accounts.Count > 0)
|
|
|
|
|
accounts[^1].IsSemicolon = false;
|
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
return accounts;
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
|
|
|
|
|
{
|
|
|
|
|
base.OnNavigatedFrom(mode, parameters);
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-11-24 20:54:57 +01:00
|
|
|
renderCancellationTokenSource?.Cancel();
|
|
|
|
|
renderCancellationTokenSource?.Dispose();
|
|
|
|
|
renderCancellationTokenSource = null;
|
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
CurrentDownloadPercentage = 0d;
|
2024-06-26 20:00:10 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
initializedMailItemViewModel = null;
|
|
|
|
|
initializedMimeMessageInformation = null;
|
2026-02-25 01:41:48 +01:00
|
|
|
CurrentMailItemDisplayInformation = null;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
forceImageLoading = false;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
StatePersistenceService.IsReadingMail = false;
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
private void ResetProgress()
|
|
|
|
|
{
|
|
|
|
|
CurrentDownloadPercentage = 0;
|
|
|
|
|
IsIndetermineProgress = false;
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
private void InitializeCommandBarItems()
|
|
|
|
|
{
|
|
|
|
|
MenuItems.Clear();
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
// Save As PDF
|
|
|
|
|
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.SaveAs, true, true));
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
// Print
|
|
|
|
|
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.Print, true, true));
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
if (initializedMailItemViewModel == null)
|
|
|
|
|
return;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.Seperator));
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
// You can't do these to draft items.
|
|
|
|
|
if (!initializedMailItemViewModel.IsDraft)
|
|
|
|
|
{
|
|
|
|
|
// Reply
|
|
|
|
|
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.Reply));
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
// Reply All
|
|
|
|
|
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.ReplyAll));
|
2025-02-01 18:13:36 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
// Forward
|
|
|
|
|
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.Forward));
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
if (initializedMimeMessageInformation?.MimeMessage != null)
|
|
|
|
|
{
|
|
|
|
|
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.ViewMessageSource, true, true));
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
// Archive - Unarchive
|
2025-10-03 15:46:38 +02:00
|
|
|
if (initializedMailItemViewModel.MailCopy.AssignedFolder.SpecialFolderType == SpecialFolderType.Archive)
|
2025-02-16 11:54:23 +01:00
|
|
|
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.UnArchive));
|
|
|
|
|
else
|
|
|
|
|
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.Archive));
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
// Delete
|
|
|
|
|
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.SoftDelete));
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
// Flag - Clear Flag
|
|
|
|
|
if (initializedMailItemViewModel.IsFlagged)
|
|
|
|
|
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.ClearFlag));
|
|
|
|
|
else
|
|
|
|
|
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.SetFlag));
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
// Secondary items.
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
// Read - Unread
|
|
|
|
|
if (initializedMailItemViewModel.IsRead)
|
|
|
|
|
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.MarkAsUnread, true, false));
|
|
|
|
|
else
|
|
|
|
|
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.MarkAsRead, true, false));
|
2026-04-08 15:31:14 +02:00
|
|
|
|
|
|
|
|
if (initializedMailItemViewModel.MailCopy.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk)
|
|
|
|
|
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.MarkAsNotJunk, true, true));
|
|
|
|
|
else if (!initializedMailItemViewModel.IsDraft &&
|
|
|
|
|
initializedMailItemViewModel.MailCopy.AssignedFolder.SpecialFolderType != SpecialFolderType.Sent)
|
|
|
|
|
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.MoveToJunk, true, true));
|
2025-02-16 11:54:23 +01:00
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2026-04-07 16:48:46 +02:00
|
|
|
protected override async void OnMailUpdated(MailCopy updatedMail, EntityUpdateSource source, MailCopyChangeFlags changedProperties)
|
2025-02-16 11:54:23 +01:00
|
|
|
{
|
2026-03-01 12:07:15 +01:00
|
|
|
base.OnMailUpdated(updatedMail, source, changedProperties);
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
if (initializedMailItemViewModel == null) return;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
// Check if the updated mail is the same mail item we are rendering.
|
|
|
|
|
// This is done with UniqueId to include FolderId into calculations.
|
2025-10-03 15:46:38 +02:00
|
|
|
if (initializedMailItemViewModel.MailCopy.UniqueId != updatedMail.UniqueId) return;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
await ExecuteUIThread(() => { InitializeCommandBarItems(); });
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2026-04-11 01:04:59 +02:00
|
|
|
protected override async void OnMailRemoved(MailCopy removedMail, EntityUpdateSource source)
|
|
|
|
|
{
|
|
|
|
|
base.OnMailRemoved(removedMail, source);
|
|
|
|
|
|
|
|
|
|
if (initializedMailItemViewModel?.MailCopy == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (initializedMailItemViewModel.MailCopy.UniqueId != removedMail.UniqueId)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
await ExecuteUIThread(() => CloseRequested?.Invoke(this, EventArgs.Empty));
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
[RelayCommand]
|
|
|
|
|
private async Task OpenAttachmentAsync(MailAttachmentViewModel attachmentViewModel)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var fileFolderPath = Path.Combine(initializedMimeMessageInformation.Path, attachmentViewModel.FileName);
|
|
|
|
|
var directoryInfo = new DirectoryInfo(initializedMimeMessageInformation.Path);
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
var fileExists = File.Exists(fileFolderPath);
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
if (!fileExists)
|
|
|
|
|
await SaveAttachmentInternalAsync(attachmentViewModel, initializedMimeMessageInformation.Path);
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
await LaunchFileInternalAsync(fileFolderPath);
|
2025-02-16 11:35:43 +01:00
|
|
|
}
|
2025-02-16 11:54:23 +01:00
|
|
|
catch (Exception ex)
|
2025-02-16 11:35:43 +01:00
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
Log.Error(ex, "Failed to open attachment.");
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
_dialogService.InfoBarMessage(Translator.Info_AttachmentOpenFailedTitle, Translator.Info_AttachmentOpenFailedMessage, InfoBarMessageType.Error);
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
[RelayCommand]
|
|
|
|
|
private async Task SaveAttachmentAsync(MailAttachmentViewModel attachmentViewModel)
|
|
|
|
|
{
|
|
|
|
|
if (attachmentViewModel == null)
|
|
|
|
|
return;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
attachmentViewModel.IsBusy = true;
|
2025-02-16 11:35:43 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
var pickedPath = await _dialogService.PickWindowsFolderAsync();
|
2025-02-16 11:43:30 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
if (string.IsNullOrEmpty(pickedPath)) return;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
await SaveAttachmentInternalAsync(attachmentViewModel, pickedPath);
|
|
|
|
|
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.Info_AttachmentSaveSuccessTitle, Translator.Info_AttachmentSaveSuccessMessage, InfoBarMessageType.Success);
|
2025-02-16 11:35:43 +01:00
|
|
|
}
|
2025-02-16 11:54:23 +01:00
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Log.Error(ex, "Failed to save attachment.");
|
2025-02-16 11:43:30 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
_dialogService.InfoBarMessage(Translator.Info_AttachmentSaveFailedTitle, Translator.Info_AttachmentSaveFailedMessage, InfoBarMessageType.Error);
|
|
|
|
|
}
|
|
|
|
|
finally
|
2024-08-05 00:36:26 +02:00
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
attachmentViewModel.IsBusy = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-08-05 00:36:26 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
[RelayCommand]
|
|
|
|
|
private async Task SaveAllAttachmentsAsync()
|
|
|
|
|
{
|
|
|
|
|
var pickedPath = await _dialogService.PickWindowsFolderAsync();
|
2024-08-05 00:36:26 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
if (string.IsNullOrEmpty(pickedPath)) return;
|
2024-08-05 00:36:26 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
try
|
|
|
|
|
{
|
2024-08-05 00:36:26 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
foreach (var attachmentViewModel in Attachments)
|
2025-02-16 11:43:30 +01:00
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
await SaveAttachmentInternalAsync(attachmentViewModel, pickedPath);
|
2025-02-16 11:43:30 +01:00
|
|
|
}
|
2024-11-09 19:18:06 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
_dialogService.InfoBarMessage(Translator.Info_AttachmentSaveSuccessTitle, Translator.Info_AttachmentSaveSuccessMessage, InfoBarMessageType.Success);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
2025-02-16 11:35:43 +01:00
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
Log.Error(ex, "Failed to save attachment.");
|
2025-02-16 11:43:30 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
_dialogService.InfoBarMessage(Translator.Info_AttachmentSaveFailedTitle, Translator.Info_AttachmentSaveFailedMessage, InfoBarMessageType.Error);
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-11-09 19:18:06 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
private async Task SaveAsAsync()
|
|
|
|
|
{
|
|
|
|
|
try
|
2024-11-09 19:18:06 +01:00
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
var pickedFolder = await _dialogService.PickWindowsFolderAsync();
|
2024-11-09 19:18:06 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
if (string.IsNullOrEmpty(pickedFolder)) return;
|
2024-11-09 19:18:06 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
var pdfFilePath = Path.Combine(pickedFolder, $"{initializedMailItemViewModel.FromAddress}.pdf");
|
2024-11-09 19:18:06 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
bool isSaved = await SaveHTMLasPDFFunc(pdfFilePath);
|
2024-11-09 19:18:06 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
if (isSaved)
|
2024-11-09 19:18:06 +01:00
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
_dialogService.InfoBarMessage(Translator.Info_PDFSaveSuccessTitle,
|
2025-11-23 20:56:57 +01:00
|
|
|
string.Format(Translator.Info_PDFSaveSuccessMessage, pdfFilePath),
|
|
|
|
|
InfoBarMessageType.Success);
|
2024-11-09 19:18:06 +01:00
|
|
|
}
|
|
|
|
|
}
|
2025-02-16 11:54:23 +01:00
|
|
|
catch (Exception ex)
|
2025-02-16 11:35:43 +01:00
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
Log.Error(ex, "Failed to save as PDF.");
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.Info_PDFSaveFailedTitle, ex.Message, InfoBarMessageType.Error);
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2026-04-11 15:07:22 +02:00
|
|
|
private async Task<PrintingResult> PrintAsync()
|
|
|
|
|
{
|
|
|
|
|
if (RenderPdfStreamFuncAsync == null)
|
|
|
|
|
return PrintingResult.Failed;
|
|
|
|
|
|
|
|
|
|
var windowHandle = NativeAppService.GetCoreWindowHwnd();
|
|
|
|
|
if (windowHandle == IntPtr.Zero)
|
|
|
|
|
return PrintingResult.Failed;
|
|
|
|
|
|
|
|
|
|
var printTitle = string.IsNullOrWhiteSpace(Subject)
|
|
|
|
|
? Translator.MailItemNoSubject
|
|
|
|
|
: Subject;
|
|
|
|
|
|
|
|
|
|
return await PrintService.PrintAsync(windowHandle, printTitle, RenderPdfStreamFuncAsync);
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
// Returns created file path.
|
|
|
|
|
private async Task<string> SaveAttachmentInternalAsync(MailAttachmentViewModel attachmentViewModel, string saveFolderPath)
|
|
|
|
|
{
|
|
|
|
|
var fullFilePath = Path.Combine(saveFolderPath, attachmentViewModel.FileName);
|
|
|
|
|
var stream = await _fileService.GetFileStreamAsync(saveFolderPath, attachmentViewModel.FileName);
|
2025-02-16 11:35:43 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
using (stream)
|
|
|
|
|
{
|
|
|
|
|
await attachmentViewModel.MimeContent.DecodeToAsync(stream);
|
2025-02-16 11:35:43 +01:00
|
|
|
}
|
2025-02-16 11:43:30 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
return fullFilePath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task LaunchFileInternalAsync(string filePath)
|
|
|
|
|
{
|
|
|
|
|
try
|
2025-02-16 11:35:43 +01:00
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
await NativeAppService.LaunchFileAsync(filePath);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.Info_FileLaunchFailedTitle, ex.Message, InfoBarMessageType.Error);
|
2024-04-18 01:44:37 +02:00
|
|
|
}
|
2025-02-16 11:54:23 +01:00
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
void ITransferProgress.Report(long bytesTransferred, long totalSize)
|
|
|
|
|
=> _ = ExecuteUIThread(() => { CurrentDownloadPercentage = bytesTransferred * 100 / Math.Max(1, totalSize); });
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
// For upload.
|
|
|
|
|
void ITransferProgress.Report(long bytesTransferred) { }
|
2024-07-09 01:05:16 +02:00
|
|
|
|
2026-04-11 01:04:59 +02:00
|
|
|
public async Task RefreshMailItemAsync(MailItemViewModel mailItemViewModel)
|
2025-02-16 11:54:23 +01:00
|
|
|
{
|
2026-04-11 01:04:59 +02:00
|
|
|
if (mailItemViewModel == null || mailItemViewModel.IsDraft) return;
|
2026-02-25 01:41:48 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
try
|
2025-02-16 11:35:43 +01:00
|
|
|
{
|
2026-04-11 01:04:59 +02:00
|
|
|
await RenderAsync(mailItemViewModel, renderCancellationTokenSource.Token);
|
2025-02-16 11:54:23 +01:00
|
|
|
}
|
|
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
{
|
|
|
|
|
Log.Information("Canceled mail rendering.");
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.Info_MailRenderingFailedTitle, string.Format(Translator.Info_MailRenderingFailedMessage, ex.Message), InfoBarMessageType.Error);
|
2024-08-05 00:36:26 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
Log.Error(ex, "Failed to render mail.");
|
2024-08-05 00:36:26 +02:00
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
}
|
2025-06-21 01:40:25 +02:00
|
|
|
|
|
|
|
|
public void Receive(ThumbnailAdded message)
|
|
|
|
|
{
|
|
|
|
|
UpdateThumbnails(ToItems, message.Email);
|
|
|
|
|
UpdateThumbnails(CcItems, message.Email);
|
|
|
|
|
UpdateThumbnails(BccItems, message.Email);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UpdateThumbnails(ObservableCollection<AccountContactViewModel> items, string email)
|
|
|
|
|
{
|
|
|
|
|
if (Dispatcher == null || items.Count == 0) return;
|
|
|
|
|
|
|
|
|
|
Dispatcher.ExecuteOnUIThread(() =>
|
|
|
|
|
{
|
|
|
|
|
foreach (var item in items)
|
|
|
|
|
{
|
|
|
|
|
if (item.Address.Equals(email, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
item.ThumbnailUpdatedEvent = !item.ThumbnailUpdatedEvent;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-10-21 01:27:29 +02:00
|
|
|
|
2025-11-23 20:56:57 +01:00
|
|
|
[RelayCommand]
|
|
|
|
|
private async Task ShowSmimeSigningCertificateInfoAsync()
|
|
|
|
|
{
|
|
|
|
|
if (IsSmimeSigned)
|
|
|
|
|
{
|
|
|
|
|
MimePart signaturePart;
|
|
|
|
|
if (initializedMimeMessageInformation?.MimeMessage?.Body is MultipartSigned signed && signed[1] is MimePart signaturePart1)
|
|
|
|
|
{
|
|
|
|
|
signaturePart = signaturePart1;
|
|
|
|
|
}
|
|
|
|
|
else if (initializedMimeMessageInformation?.MimeMessage?.Body is ApplicationPkcs7Mime pkcs7)
|
|
|
|
|
{
|
|
|
|
|
signaturePart = null;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
//_dialogService.InfoBarMessage(Translator.Info_SmimeSignatureNotFoundTitle, Translator.Info_SmimeSignatureNotFoundMessage, InfoBarMessageType.Error);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string info = $"{Translator.SmimeSignaturesInMessage}:\n";
|
|
|
|
|
foreach (var (signature, valid) in CurrentRenderModel.Signatures)
|
|
|
|
|
{
|
|
|
|
|
info += string.Format(Translator.SmimeSignatureEntry, valid ? "✅" : "❌", signature.SignerCertificate.Name, signature.SignerCertificate.Fingerprint, signature.SignerCertificate.CreationDate, signature.SignerCertificate.ExpirationDate);
|
|
|
|
|
}
|
|
|
|
|
await ShowSmimeCertificateInfoAsync(signaturePart, info, Translator.SmimeSigningCertificateInfoTitle);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task ShowSmimeCertificateInfoAsync(MimePart certificateAttachment, string additionalInfo = "", string title = null)
|
|
|
|
|
{
|
|
|
|
|
{
|
|
|
|
|
if (certificateAttachment == null)
|
|
|
|
|
{
|
|
|
|
|
await _dialogService.ShowConfirmationDialogAsync(
|
|
|
|
|
$"{additionalInfo}\n{Translator.SmimeNoCertificateFileFound}", title ?? Translator.SmimeCertificateInfoTitle, Translator.Buttons_OK);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
var fileName = certificateAttachment.FileName ?? "smime.p7s";
|
|
|
|
|
var contentType = certificateAttachment.ContentType?.MimeType ?? "application/pkcs7-signature";
|
|
|
|
|
var size = certificateAttachment.Content?.Stream?.Length ?? 0;
|
|
|
|
|
var info = string.Format(Translator.SmimeCertificateFileInfo, fileName, contentType, size);
|
|
|
|
|
|
|
|
|
|
var result = await _dialogService.ShowConfirmationDialogAsync(
|
|
|
|
|
$"{additionalInfo}\n{info}", title ?? Translator.SmimeCertificateInfoTitle,
|
|
|
|
|
Translator.SmimeSaveCertificate);
|
|
|
|
|
if (result)
|
|
|
|
|
{
|
|
|
|
|
var pickedPath = await _dialogService.PickFilePathAsync(fileName);
|
|
|
|
|
if (!string.IsNullOrEmpty(pickedPath))
|
|
|
|
|
{
|
|
|
|
|
var pickedDirectory = Path.GetDirectoryName(pickedPath);
|
|
|
|
|
var pickedFileName = Path.GetFileName(pickedPath);
|
|
|
|
|
await using (var stream = await _fileService.GetFileStreamAsync(pickedDirectory, pickedFileName))
|
|
|
|
|
{
|
|
|
|
|
await certificateAttachment.Content!.DecodeToAsync(stream);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.SmimeCertificate, string.Format(Translator.SmimeCertificateSavedTo, pickedPath),
|
|
|
|
|
InfoBarMessageType.Success);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-21 01:27:29 +02:00
|
|
|
protected override void RegisterRecipients()
|
|
|
|
|
{
|
|
|
|
|
base.RegisterRecipients();
|
2025-11-24 20:54:57 +01:00
|
|
|
|
2025-10-21 01:27:29 +02:00
|
|
|
Messenger.Register<ThumbnailAdded>(this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override void UnregisterRecipients()
|
|
|
|
|
{
|
|
|
|
|
base.UnregisterRecipients();
|
2025-11-24 20:54:57 +01:00
|
|
|
|
2025-10-21 01:27:29 +02:00
|
|
|
Messenger.Unregister<ThumbnailAdded>(this);
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
}
|