beb3bf9d1d
* feat: add S/MIME certificate management - Introduced `ISmimeCertificateService` interface for managing S/MIME certificates. - Implemented `SmimeCertificateService` class to handle certificate operations. - Updated `WinoPage` enum to include `SignatureAndEncryptionPage`. - Added resource entries in `resources.json` for S/MIME related messages. - Created `SignatureAndEncryptionPage` view and logic for user interaction. - Modified configuration files to integrate the new service and page. - Updated project files to include necessary dependencies for certificate management. * refactor(SmimeCertificateService): ♻️ Use constant for certificate name Refactored the `SmimeCertificateService` to replace the hardcoded string "Wino Mail Certificate" with a constant `CertificateFriendlyName`. This change enhances code maintainability by centralizing the definition of the certificate's friendly name. • Introduced a constant for the certificate's friendly name. • Updated the certificate retrieval and import logic to use the new constant. * feat(alias): ✨ Add S/Mime certificate selection for every alias Added new properties and methods in `MailAccountAlias` to manage signing and encryption certificates, including their thumbprints. This enhancement allows for better handling of S/Mime certificates within the application. • Introduced new properties for signing and encryption certificates. • Updated `resources.json` with new translations for S/Mime certificates. • Enhanced `AliasManagementPageViewModel` to include a dependency on the S/Mime certificate service and updated alias loading methods. • Modified `AliasManagementPage.xaml` to include ComboBox controls for selecting certificates. • Implemented methods in `AliasManagementPage.xaml.cs` to handle certificate selection from dropdowns. This change improves the user experience by allowing users to select and manage their S/Mime certificates directly within the alias management interface. * feat(mail): ✨ Add S/MIME support and file picker updates Enhanced the `MailRenderModel` class by adding a new property `IsSmimeSigned` to indicate if an email is S/MIME signed. The constructor has been updated to accept `MailRenderingOptions`. Updated the file selection logic in `DialogServiceBase` to replace the `FolderPicker` with a `FileSavePicker`, streamlining the process of saving files. Removed unnecessary commented code and added logic to handle file extensions. In `MailRenderingPageViewModel`, a new property `IsSmimeSigned` reflects the S/MIME status of the current render model, along with a new method `ShowSmimeCertificateInfoAsync` to display S/MIME certificate details. Added a `HyperlinkButton` in `MailRenderingPage.xaml` to indicate S/MIME status, which is only visible for signed emails, providing a tooltip and command for more information. In `MimeFileService`, implemented logic to detect S/MIME signatures in messages and exclude S/MIME signature parts from attachments. * refactor(viewmodel): ♻️ Replace dialog service messages Refactored the `SignatureAndEncryptionPageViewModel.cs` to replace calls to `_dialogService.ShowMessageAsync` with `_dialogService.InfoBarMessage`. This change improves the handling of success messages during certificate import and removal processes. * feat(mail): ✨ Add S/MIME encryption indicator Implemented support for S/MIME email handling in the MailRenderingPageViewModel. This includes the addition of a new property to check if an email is encrypted and updates to methods for displaying S/MIME certificate information. A new column was added in the MailRenderingPage.xaml to indicate if an email is encrypted, along with updated tooltips and commands. The MimeFileService was also modified to detect S/MIME encryption and to exclude S/MIME signature certificates during attachment processing. * fix: Added missing property * feat: Added S/Mime decryption and signing verification and improvements * i18n(resources): 🌐 Add S/MIME translation strings Added new translation strings for S/MIME functionalities in `resources.json`, including messages for signatures and certificates in both English and Italian. The code has been updated to utilize these new translation strings, enhancing the application's internationalization. Updated `MailRenderingPageViewModel.cs` to use the new translation strings for signature and certificate messages, improving code readability and consistency with translations. Additionally, the tooltips for S/MIME signing and encryption buttons in `MailRenderingPage.xaml` have been updated to use the new translation strings, enhancing the user experience for Italian-speaking users. * fix: Extract body from MultipartSigned message * feat(smime): ✨ Enhance S/MIME certificate handling Updated the `SmimeCertificateService` to improve the loading of PKCS12 certificate collections by adding `X509KeyStorageFlags.DefaultKeySet` and `X509KeyStorageFlags.Exportable` for better key management. In `ComposePageViewModel`, imported necessary namespaces for S/MIME certificate handling and added a new dependency for `ISmimeCertificateService`. Implemented logic in `OpenAttachmentAsync` to load alias certificates and manage message signing and encryption based on user-selected certificates. This change enhances the security and flexibility of email handling within the application. * feat: Replaced Smime encryption certificate combobox with checkbox Cert selection is useless for encryption * feat: Added S/Mime togglebuttons when composing an email * i18n(translations): 🌐 Add new composer translations Added new translation strings for composer features, including themes, text formatting, and S/MIME signing and encryption options. Updated button labels to utilize these new strings, enhancing the application's internationalization. Additionally, removed an obsolete string related to S/MIME certificate file information. * Example for relay command and fix settings pages runtime error * refactor(viewmodel): ♻️ Update certificate import/export commands Refactored the certificate import and export commands in the `SignatureAndEncryptionPageViewModel`. Changed methods from `async void` to `async Task` for better error handling and tracking of asynchronous operations. Added `[RelayCommand]` attributes to improve adherence to the MVVM pattern. Updated the XAML file to bind buttons directly to the new command methods, removing the need for event handlers. This enhances separation of concerns and simplifies the code. Removed obsolete event handlers from the code-behind file, streamlining the implementation. * fix: export folderPath parameter contains file name * fix: QRESYNC initial modseq should be 1 (#734) * Fix typo in reorder accounts dialog (#754) * fix: Missing commas in translations files * fix: merge issues * Fix mege conflicts. * Some more conflict fixes. * Fixing context. * Fixing saving file with suggested file name. --------- Co-authored-by: Aleh Khantsevich <aleh.khantsevich@gmail.com> Co-authored-by: Konstantin Shkel <null+github@pcho.la> Co-authored-by: Cas Cornelissen <cas.cornelissen@onefinity.io> Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
653 lines
24 KiB
C#
653 lines
24 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
using System.Threading.Tasks;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using CommunityToolkit.Mvvm.Messaging;
|
|
using MimeKit;
|
|
using MimeKit.Cryptography;
|
|
using Wino.Core.Domain;
|
|
using Wino.Core.Domain.Entities.Mail;
|
|
using Wino.Core.Domain.Entities.Shared;
|
|
using Wino.Core.Domain.Enums;
|
|
using Wino.Core.Domain.Exceptions;
|
|
using Wino.Core.Domain.Interfaces;
|
|
using Wino.Core.Domain.Models.MailItem;
|
|
using Wino.Core.Domain.Models.Navigation;
|
|
using Wino.Core.Extensions;
|
|
using Wino.Core.Services;
|
|
using Wino.Mail.ViewModels.Data;
|
|
using Wino.Messaging.Client.Mails;
|
|
|
|
namespace Wino.Mail.ViewModels;
|
|
|
|
public partial class ComposePageViewModel : MailBaseViewModel
|
|
{
|
|
public Func<Task<string>> GetHTMLBodyFunction;
|
|
|
|
// When we send the message or discard it, we need to block the mime update
|
|
// Update is triggered when we leave the page.
|
|
private bool isUpdatingMimeBlocked = false;
|
|
|
|
private bool canSendMail => ComposingAccount != null && !IsLocalDraft && CurrentMimeMessage != null;
|
|
|
|
[NotifyCanExecuteChangedFor(nameof(DiscardCommand))]
|
|
[NotifyCanExecuteChangedFor(nameof(SendCommand))]
|
|
[ObservableProperty]
|
|
private MimeMessage currentMimeMessage = null;
|
|
|
|
private readonly BodyBuilder bodyBuilder = new BodyBuilder();
|
|
|
|
public bool IsLocalDraft => CurrentMailDraftItem?.MailCopy?.IsLocalDraft ?? true;
|
|
|
|
#region Properties
|
|
|
|
[ObservableProperty]
|
|
[NotifyPropertyChangedFor(nameof(IsLocalDraft))]
|
|
[NotifyCanExecuteChangedFor(nameof(DiscardCommand))]
|
|
[NotifyCanExecuteChangedFor(nameof(SendCommand))]
|
|
private MailItemViewModel currentMailDraftItem;
|
|
|
|
[ObservableProperty]
|
|
private bool isImportanceSelected;
|
|
|
|
[ObservableProperty]
|
|
private MessageImportance selectedMessageImportance;
|
|
|
|
[ObservableProperty]
|
|
private bool isCCBCCVisible;
|
|
|
|
[ObservableProperty]
|
|
private string subject;
|
|
|
|
[ObservableProperty]
|
|
[NotifyCanExecuteChangedFor(nameof(DiscardCommand))]
|
|
[NotifyCanExecuteChangedFor(nameof(SendCommand))]
|
|
private MailAccount composingAccount;
|
|
|
|
[ObservableProperty]
|
|
public partial List<MailAccountAlias> AvailableAliases { get; set; }
|
|
[ObservableProperty]
|
|
public partial MailAccountAlias SelectedAlias { get; set; }
|
|
[ObservableProperty]
|
|
public partial bool IsDraggingOverComposerGrid { get; set; }
|
|
[ObservableProperty]
|
|
public partial bool IsDraggingOverFilesDropZone { get; set; }
|
|
[ObservableProperty]
|
|
public partial bool IsDraggingOverImagesDropZone { get; set; }
|
|
[ObservableProperty]
|
|
public partial bool IsSmimeSignatureEnabled { get; set; }
|
|
[ObservableProperty]
|
|
public partial bool IsSmimeEncryptionEnabled { get; set; }
|
|
|
|
[ObservableProperty]
|
|
public partial X509Certificate2 SelectedSigningCertificate { get; set; }
|
|
|
|
public ObservableCollection<X509Certificate2> AvailableCertificates = [];
|
|
|
|
public bool AreCertificatesAvailable => AvailableCertificates.Count > 0;
|
|
|
|
public ObservableCollection<MailAttachmentViewModel> IncludedAttachments { get; set; } = [];
|
|
public ObservableCollection<MailAccount> Accounts { get; set; } = [];
|
|
public ObservableCollection<AccountContact> ToItems { get; set; } = [];
|
|
public ObservableCollection<AccountContact> CCItems { get; set; } = [];
|
|
public ObservableCollection<AccountContact> BCCItems { get; set; } = [];
|
|
|
|
#endregion
|
|
|
|
public INativeAppService NativeAppService { get; }
|
|
|
|
private readonly IMailDialogService _dialogService;
|
|
private readonly IMailService _mailService;
|
|
private readonly IMimeFileService _mimeFileService;
|
|
private readonly IFileService _fileService;
|
|
private readonly IFolderService _folderService;
|
|
private readonly IAccountService _accountService;
|
|
private readonly IWinoRequestDelegator _worker;
|
|
public readonly IFontService FontService;
|
|
public readonly IPreferencesService PreferencesService;
|
|
public readonly IContactService ContactService;
|
|
public readonly ISmimeCertificateService _smimeCertificateService;
|
|
|
|
public ComposePageViewModel(IMailDialogService dialogService,
|
|
IMailService mailService,
|
|
IMimeFileService mimeFileService,
|
|
IFileService fileService,
|
|
INativeAppService nativeAppService,
|
|
IFolderService folderService,
|
|
IAccountService accountService,
|
|
IWinoRequestDelegator worker,
|
|
IContactService contactService,
|
|
IFontService fontService,
|
|
IPreferencesService preferencesService,
|
|
ISmimeCertificateService smimeCertificateService)
|
|
{
|
|
NativeAppService = nativeAppService;
|
|
ContactService = contactService;
|
|
FontService = fontService;
|
|
PreferencesService = preferencesService;
|
|
|
|
_folderService = folderService;
|
|
_dialogService = dialogService;
|
|
_mailService = mailService;
|
|
_mimeFileService = mimeFileService;
|
|
_fileService = fileService;
|
|
_accountService = accountService;
|
|
_worker = worker;
|
|
_smimeCertificateService = smimeCertificateService;
|
|
|
|
foreach (var cert in _smimeCertificateService.GetCertificates(emailAddress: SelectedAlias?.AliasAddress))
|
|
{
|
|
if (cert != null)
|
|
{
|
|
AvailableCertificates.Add(cert);
|
|
}
|
|
}
|
|
}
|
|
|
|
partial void OnSelectedAliasChanged(MailAccountAlias value)
|
|
{
|
|
if (value != null)
|
|
{
|
|
IsSmimeSignatureEnabled = value.SelectedSigningCertificateThumbprint != null;
|
|
IsSmimeEncryptionEnabled = value.IsSmimeEncryptionEnabled;
|
|
|
|
AvailableCertificates.Clear();
|
|
var certs = _smimeCertificateService.GetCertificates(emailAddress: SelectedAlias.AliasAddress);
|
|
foreach (var cert in certs)
|
|
{
|
|
AvailableCertificates.Add(cert);
|
|
}
|
|
SelectedSigningCertificate = AvailableCertificates
|
|
.Where(c => c.Thumbprint == SelectedAlias.SelectedSigningCertificateThumbprint).FirstOrDefault() ?? AvailableCertificates.FirstOrDefault();
|
|
}
|
|
}
|
|
|
|
partial void OnSelectedSigningCertificateChanged(X509Certificate2 value)
|
|
{
|
|
IsSmimeSignatureEnabled = value != null;
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task OpenAttachmentAsync(MailAttachmentViewModel attachmentViewModel)
|
|
{
|
|
if (string.IsNullOrEmpty(attachmentViewModel.FilePath)) return;
|
|
|
|
try
|
|
{
|
|
await NativeAppService.LaunchFileAsync(attachmentViewModel.FilePath);
|
|
}
|
|
catch
|
|
{
|
|
_dialogService.InfoBarMessage(Translator.Info_FailedToOpenFileTitle, Translator.Info_FailedToOpenFileMessage, InfoBarMessageType.Error);
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task SaveAttachmentAsync(MailAttachmentViewModel attachmentViewModel)
|
|
{
|
|
if (attachmentViewModel.Content == null) return;
|
|
var pickedFilePath = await _dialogService.PickFilePathAsync(attachmentViewModel.FileName);
|
|
if (string.IsNullOrWhiteSpace(pickedFilePath)) return;
|
|
|
|
try
|
|
{
|
|
await _fileService.CopyFileAsync(attachmentViewModel.FilePath, pickedFilePath);
|
|
}
|
|
catch
|
|
{
|
|
_dialogService.InfoBarMessage(Translator.Info_FailedToOpenFileTitle, Translator.Info_FailedToOpenFileMessage, InfoBarMessageType.Error);
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task AttachFilesAsync()
|
|
{
|
|
var pickedFiles = await _dialogService.PickFilesAsync("*");
|
|
|
|
if (pickedFiles?.Count == 0) return;
|
|
|
|
foreach (var file in pickedFiles)
|
|
{
|
|
var attachmentViewModel = new MailAttachmentViewModel(file);
|
|
IncludedAttachments.Add(attachmentViewModel);
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void RemoveAttachment(MailAttachmentViewModel attachmentViewModel)
|
|
=> IncludedAttachments.Remove(attachmentViewModel);
|
|
|
|
[RelayCommand(CanExecute = nameof(canSendMail))]
|
|
private async Task SendAsync()
|
|
{
|
|
// TODO: More detailed mail validations.
|
|
|
|
if (!ToItems.Any())
|
|
{
|
|
await _dialogService.ShowMessageAsync(Translator.DialogMessage_ComposerMissingRecipientMessage,
|
|
Translator.DialogMessage_ComposerValidationFailedTitle,
|
|
WinoCustomMessageDialogIcon.Warning);
|
|
return;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(Subject))
|
|
{
|
|
var isConfirmed = await _dialogService.ShowConfirmationDialogAsync(Translator.DialogMessage_EmptySubjectConfirmationMessage, Translator.DialogMessage_EmptySubjectConfirmation, Translator.Buttons_Yes);
|
|
|
|
if (!isConfirmed) return;
|
|
}
|
|
|
|
if (SelectedAlias == null)
|
|
{
|
|
_dialogService.InfoBarMessage(Translator.DialogMessage_AliasNotSelectedTitle, Translator.DialogMessage_AliasNotSelectedMessage, InfoBarMessageType.Error);
|
|
return;
|
|
}
|
|
|
|
// Save mime changes before sending.
|
|
await UpdateMimeChangesAsync().ConfigureAwait(false);
|
|
|
|
isUpdatingMimeBlocked = true;
|
|
|
|
var assignedAccount = CurrentMailDraftItem.MailCopy.AssignedAccount;
|
|
var sentFolder = await _folderService.GetSpecialFolderByAccountIdAsync(assignedAccount.Id, SpecialFolderType.Sent);
|
|
|
|
|
|
// Load alias certs
|
|
var certs = _smimeCertificateService.GetCertificates(emailAddress: SelectedAlias.AliasAddress);
|
|
|
|
if (IsSmimeSignatureEnabled)
|
|
{
|
|
var signingCertificate = !string.IsNullOrEmpty(SelectedAlias.SelectedSigningCertificateThumbprint)
|
|
? certs.FirstOrDefault(c => c?.Thumbprint == SelectedAlias.SelectedSigningCertificateThumbprint)
|
|
: null;
|
|
|
|
var signer = new CmsSigner(signingCertificate) { DigestAlgorithm = DigestAlgorithm.Sha1 };
|
|
|
|
if (IsSmimeEncryptionEnabled)
|
|
{
|
|
var recipients = new CmsRecipientCollection();
|
|
var cmsRecipients = CurrentMimeMessage.To.Mailboxes
|
|
.Select(mailbox => new CmsRecipient(
|
|
_smimeCertificateService.GetCertificates(emailAddress: mailbox.Address).FirstOrDefault() ?? _smimeCertificateService.GetCertificates(StoreName.AddressBook, emailAddress: mailbox.Address).FirstOrDefault()
|
|
));
|
|
foreach (var recipient in cmsRecipients)
|
|
{
|
|
recipients.Add(recipient);
|
|
}
|
|
|
|
CurrentMimeMessage.Body = ApplicationPkcs7Mime.SignAndEncrypt(signer, recipients, CurrentMimeMessage.Body);
|
|
}
|
|
else
|
|
{
|
|
// CurrentMimeMessage.Body = MultipartSigned.Create(signer, CurrentMimeMessage.Body);
|
|
CurrentMimeMessage.Body = ApplicationPkcs7Mime.Sign(signer, CurrentMimeMessage.Body);
|
|
}
|
|
}
|
|
else if (IsSmimeEncryptionEnabled)
|
|
{
|
|
// var encryptionCertificate = !string.IsNullOrEmpty(SelectedAlias.SelectedEncryptionCertificateThumbprint)
|
|
// ? certs.FirstOrDefault(c => c?.Thumbprint == SelectedAlias.SelectedEncryptionCertificateThumbprint)
|
|
// : null;
|
|
// Encrypt the message if encryption certificate is selected.
|
|
CurrentMimeMessage.Body = ApplicationPkcs7Mime.Encrypt(CurrentMimeMessage.To.Mailboxes, CurrentMimeMessage.Body);
|
|
}
|
|
|
|
using MemoryStream memoryStream = new();
|
|
CurrentMimeMessage.WriteTo(FormatOptions.Default, memoryStream);
|
|
byte[] buffer = memoryStream.GetBuffer();
|
|
int count = (int)memoryStream.Length;
|
|
|
|
var base64EncodedMessage = Convert.ToBase64String(buffer);
|
|
var draftSendPreparationRequest = new SendDraftPreparationRequest(CurrentMailDraftItem.MailCopy,
|
|
SelectedAlias,
|
|
sentFolder,
|
|
CurrentMailDraftItem.MailCopy.AssignedFolder,
|
|
CurrentMailDraftItem.MailCopy.AssignedAccount.Preferences,
|
|
base64EncodedMessage);
|
|
|
|
await _worker.ExecuteAsync(draftSendPreparationRequest);
|
|
}
|
|
|
|
public async Task UpdateMimeChangesAsync()
|
|
{
|
|
if (isUpdatingMimeBlocked || CurrentMimeMessage == null || ComposingAccount == null || CurrentMailDraftItem == null) return;
|
|
|
|
// Save recipients.
|
|
|
|
SaveAddressInfo(ToItems, CurrentMimeMessage.To);
|
|
SaveAddressInfo(CCItems, CurrentMimeMessage.Cc);
|
|
SaveAddressInfo(BCCItems, CurrentMimeMessage.Bcc);
|
|
|
|
SaveImportance();
|
|
SaveSubject();
|
|
SaveFromAddress();
|
|
SaveReplyToAddress();
|
|
|
|
await SaveAttachmentsAsync();
|
|
await SaveBodyAsync();
|
|
await UpdateMailCopyAsync();
|
|
|
|
// Save mime file.
|
|
await _mimeFileService.SaveMimeMessageAsync(CurrentMailDraftItem.MailCopy.FileId, CurrentMimeMessage, ComposingAccount.Id).ConfigureAwait(false);
|
|
}
|
|
|
|
private async Task UpdateMailCopyAsync()
|
|
{
|
|
CurrentMailDraftItem.Subject = CurrentMimeMessage.Subject;
|
|
CurrentMailDraftItem.PreviewText = CurrentMimeMessage.TextBody;
|
|
CurrentMailDraftItem.FromAddress = SelectedAlias.AliasAddress;
|
|
CurrentMailDraftItem.HasAttachments = CurrentMimeMessage.Attachments.Any();
|
|
|
|
// Update database.
|
|
await _mailService.UpdateMailAsync(CurrentMailDraftItem.MailCopy);
|
|
}
|
|
|
|
private async Task SaveAttachmentsAsync()
|
|
{
|
|
bodyBuilder.Attachments.Clear();
|
|
|
|
foreach (var path in IncludedAttachments)
|
|
{
|
|
if (path.Content == null) continue;
|
|
|
|
await bodyBuilder.Attachments.AddAsync(path.FileName, new MemoryStream(path.Content));
|
|
}
|
|
}
|
|
|
|
private void SaveImportance()
|
|
{
|
|
CurrentMimeMessage.Importance = IsImportanceSelected ? SelectedMessageImportance : MessageImportance.Normal;
|
|
}
|
|
|
|
private void SaveSubject()
|
|
{
|
|
if (Subject != null)
|
|
{
|
|
CurrentMimeMessage.Subject = Subject;
|
|
}
|
|
}
|
|
|
|
private async Task SaveBodyAsync()
|
|
{
|
|
if (GetHTMLBodyFunction != null)
|
|
{
|
|
bodyBuilder.SetHtmlBody(await GetHTMLBodyFunction());
|
|
}
|
|
|
|
CurrentMimeMessage.Body = bodyBuilder.ToMessageBody();
|
|
}
|
|
|
|
[RelayCommand(CanExecute = nameof(canSendMail))]
|
|
private async Task DiscardAsync()
|
|
{
|
|
if (ComposingAccount == null)
|
|
{
|
|
_dialogService.InfoBarMessage(Translator.Info_MessageCorruptedTitle, Translator.Info_MessageCorruptedMessage, InfoBarMessageType.Error);
|
|
return;
|
|
}
|
|
|
|
var confirmation = await _dialogService.ShowConfirmationDialogAsync(Translator.DialogMessage_DiscardDraftConfirmationMessage,
|
|
Translator.DialogMessage_DiscardDraftConfirmationTitle,
|
|
Translator.Buttons_Yes);
|
|
|
|
if (confirmation)
|
|
{
|
|
isUpdatingMimeBlocked = true;
|
|
|
|
// Don't send delete request for local drafts. Just delete the record and mime locally.
|
|
if (CurrentMailDraftItem.MailCopy.IsLocalDraft)
|
|
{
|
|
await _mailService.DeleteMailAsync(ComposingAccount.Id, CurrentMailDraftItem.Id);
|
|
}
|
|
else
|
|
{
|
|
var deletePackage = new MailOperationPreperationRequest(MailOperation.HardDelete, CurrentMailDraftItem.MailCopy, ignoreHardDeleteProtection: true);
|
|
await _worker.ExecuteAsync(deletePackage).ConfigureAwait(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
|
|
{
|
|
base.OnNavigatedFrom(mode, parameters);
|
|
|
|
/// Do not put any code here.
|
|
/// Make sure to use Page's OnNavigatedTo instead.
|
|
}
|
|
|
|
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
|
{
|
|
base.OnNavigatedTo(mode, parameters);
|
|
|
|
if (parameters != null && parameters is MailItemViewModel mailItem)
|
|
{
|
|
CurrentMailDraftItem = mailItem;
|
|
|
|
await TryPrepareComposeAsync(true);
|
|
}
|
|
}
|
|
|
|
private async Task<bool> InitializeComposerAccountAsync()
|
|
{
|
|
if (CurrentMailDraftItem == null) return false;
|
|
|
|
if (ComposingAccount != null) return true;
|
|
|
|
var composingAccount = await _accountService.GetAccountAsync(CurrentMailDraftItem.MailCopy.AssignedAccount.Id).ConfigureAwait(false);
|
|
if (composingAccount == null) return false;
|
|
|
|
var aliases = await _accountService.GetAccountAliasesAsync(composingAccount.Id).ConfigureAwait(false);
|
|
|
|
if (aliases == null || !aliases.Any()) return false;
|
|
|
|
// MailAccountAlias primaryAlias = aliases.Find(a => a.IsPrimary) ?? aliases.First();
|
|
|
|
// Auto-select the correct alias from the message itself.
|
|
// If can't, fallback to primary alias.
|
|
|
|
MailAccountAlias primaryAlias = null;
|
|
|
|
if (!string.IsNullOrEmpty(CurrentMailDraftItem.FromAddress))
|
|
{
|
|
primaryAlias = aliases.Find(a => a.AliasAddress == CurrentMailDraftItem.FromAddress);
|
|
}
|
|
|
|
primaryAlias ??= await _accountService.GetPrimaryAccountAliasAsync(ComposingAccount.Id).ConfigureAwait(false);
|
|
|
|
await ExecuteUIThread(() =>
|
|
{
|
|
ComposingAccount = composingAccount;
|
|
AvailableAliases = aliases;
|
|
SelectedAlias = primaryAlias;
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
private async Task TryPrepareComposeAsync(bool downloadIfNeeded)
|
|
{
|
|
if (CurrentMailDraftItem == null) return;
|
|
|
|
bool isComposerInitialized = await InitializeComposerAccountAsync();
|
|
|
|
if (!isComposerInitialized) return;
|
|
|
|
retry:
|
|
|
|
// Replying existing message.
|
|
MimeMessageInformation mimeMessageInformation = null;
|
|
|
|
try
|
|
{
|
|
mimeMessageInformation = await _mimeFileService.GetMimeMessageInformationAsync(CurrentMailDraftItem.MailCopy.FileId, ComposingAccount.Id).ConfigureAwait(false);
|
|
}
|
|
catch (FileNotFoundException)
|
|
{
|
|
if (downloadIfNeeded)
|
|
{
|
|
downloadIfNeeded = false;
|
|
|
|
// Download missing MIME message using SynchronizationManager
|
|
await SynchronizationManager.Instance.DownloadMimeMessageAsync(
|
|
CurrentMailDraftItem.MailCopy,
|
|
CurrentMailDraftItem.MailCopy.AssignedAccount.Id);
|
|
|
|
goto retry;
|
|
}
|
|
else
|
|
_dialogService.InfoBarMessage(Translator.Info_ComposerMissingMIMETitle, Translator.Info_ComposerMissingMIMEMessage, InfoBarMessageType.Error);
|
|
|
|
return;
|
|
}
|
|
catch (IOException)
|
|
{
|
|
_dialogService.InfoBarMessage(Translator.Busy, Translator.Exception_MailProcessing, InfoBarMessageType.Warning);
|
|
}
|
|
catch (ComposerMimeNotFoundException)
|
|
{
|
|
_dialogService.InfoBarMessage(Translator.Info_ComposerMissingMIMETitle, Translator.Info_ComposerMissingMIMEMessage, InfoBarMessageType.Error);
|
|
}
|
|
|
|
if (mimeMessageInformation == null)
|
|
return;
|
|
|
|
var replyingMime = mimeMessageInformation.MimeMessage;
|
|
var mimeFilePath = mimeMessageInformation.Path;
|
|
|
|
var renderModel = _mimeFileService.GetMailRenderModel(replyingMime, mimeFilePath);
|
|
|
|
await ExecuteUIThread(async () =>
|
|
{
|
|
// Extract information
|
|
|
|
CurrentMimeMessage = replyingMime;
|
|
|
|
ToItems.Clear();
|
|
CCItems.Clear();
|
|
BCCItems.Clear();
|
|
|
|
await LoadAddressInfoAsync(replyingMime.To, ToItems);
|
|
await LoadAddressInfoAsync(replyingMime.Cc, CCItems);
|
|
await LoadAddressInfoAsync(replyingMime.Bcc, BCCItems);
|
|
|
|
LoadAttachments();
|
|
|
|
if (replyingMime.Cc.Any() || replyingMime.Bcc.Any())
|
|
IsCCBCCVisible = true;
|
|
|
|
Subject = replyingMime.Subject;
|
|
|
|
Messenger.Send(new CreateNewComposeMailRequested(renderModel));
|
|
});
|
|
}
|
|
|
|
private void LoadAttachments()
|
|
{
|
|
if (CurrentMimeMessage == null) return;
|
|
|
|
foreach (var attachment in CurrentMimeMessage.Attachments)
|
|
{
|
|
if (attachment.IsAttachment && attachment is MimePart attachmentPart)
|
|
{
|
|
IncludedAttachments.Add(new MailAttachmentViewModel(attachmentPart));
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task LoadAddressInfoAsync(InternetAddressList list, ObservableCollection<AccountContact> collection)
|
|
{
|
|
foreach (var item in list)
|
|
{
|
|
if (item is MailboxAddress mailboxAddress)
|
|
{
|
|
var foundContact = await ContactService.GetAddressInformationByAddressAsync(mailboxAddress.Address).ConfigureAwait(false)
|
|
?? new AccountContact() { Name = mailboxAddress.Name, Address = mailboxAddress.Address };
|
|
|
|
await ExecuteUIThread(() => { collection.Add(foundContact); });
|
|
}
|
|
else if (item is GroupAddress groupAddress)
|
|
await LoadAddressInfoAsync(groupAddress.Members, collection);
|
|
}
|
|
}
|
|
|
|
private void SaveFromAddress()
|
|
{
|
|
if (SelectedAlias == null) return;
|
|
|
|
CurrentMimeMessage.From.Clear();
|
|
|
|
// Try to get the sender name from the alias. If not, fallback to account sender name.
|
|
var senderName = SelectedAlias.AliasSenderName ?? ComposingAccount.SenderName;
|
|
|
|
CurrentMimeMessage.From.Add(new MailboxAddress(senderName, SelectedAlias.AliasAddress));
|
|
}
|
|
|
|
private void SaveReplyToAddress()
|
|
{
|
|
if (SelectedAlias == null) return;
|
|
|
|
if (!string.IsNullOrEmpty(SelectedAlias.ReplyToAddress))
|
|
{
|
|
if (!CurrentMimeMessage.ReplyTo.Any(a => a is MailboxAddress mailboxAddress && mailboxAddress.Address == SelectedAlias.ReplyToAddress))
|
|
{
|
|
CurrentMimeMessage.ReplyTo.Clear();
|
|
CurrentMimeMessage.ReplyTo.Add(new MailboxAddress(SelectedAlias.ReplyToAddress, SelectedAlias.ReplyToAddress));
|
|
}
|
|
}
|
|
}
|
|
|
|
private void SaveAddressInfo(IEnumerable<AccountContact> addresses, InternetAddressList list)
|
|
{
|
|
list.Clear();
|
|
|
|
foreach (var item in addresses)
|
|
list.Add(new MailboxAddress(item.Name, item.Address));
|
|
}
|
|
|
|
public async Task<AccountContact> GetAddressInformationAsync(string tokenText, ObservableCollection<AccountContact> collection)
|
|
{
|
|
// Get model from the service. This will make sure the name is properly included if there is any record.
|
|
|
|
var info = await ContactService.GetAddressInformationByAddressAsync(tokenText)
|
|
?? new AccountContact() { Name = tokenText, Address = tokenText };
|
|
|
|
// Don't add if there is already that address in the collection.
|
|
if (collection.Any(a => a.Address == info.Address))
|
|
return null;
|
|
|
|
return info;
|
|
}
|
|
|
|
public void NotifyAddressExists()
|
|
{
|
|
_dialogService.InfoBarMessage(Translator.Info_ContactExistsTitle, Translator.Info_ContactExistsMessage, InfoBarMessageType.Warning);
|
|
}
|
|
|
|
public void NotifyInvalidEmail(string address)
|
|
{
|
|
_dialogService.InfoBarMessage(Translator.Info_InvalidAddressTitle, string.Format(Translator.Info_InvalidAddressMessage, address), InfoBarMessageType.Warning);
|
|
}
|
|
|
|
protected override async void OnMailUpdated(MailCopy updatedMail)
|
|
{
|
|
base.OnMailUpdated(updatedMail);
|
|
|
|
if (CurrentMailDraftItem == null) return;
|
|
|
|
if (updatedMail.UniqueId == CurrentMailDraftItem.MailCopy.UniqueId)
|
|
{
|
|
await ExecuteUIThread(() =>
|
|
{
|
|
CurrentMailDraftItem.MailCopy = updatedMail;
|
|
DiscardCommand.NotifyCanExecuteChanged();
|
|
SendCommand.NotifyCanExecuteChanged();
|
|
});
|
|
}
|
|
}
|
|
}
|