diff --git a/Wino.Core.Domain/Interfaces/ILaunchProtocolService.cs b/Wino.Core.Domain/Interfaces/ILaunchProtocolService.cs index 5c141d4b..3e11c83d 100644 --- a/Wino.Core.Domain/Interfaces/ILaunchProtocolService.cs +++ b/Wino.Core.Domain/Interfaces/ILaunchProtocolService.cs @@ -1,10 +1,16 @@ -using System.Collections.Specialized; +using Wino.Core.Domain.Models.Launch; -namespace Wino.Core.Domain.Interfaces +namespace Wino.Core.Domain.Interfaces; + +public interface ILaunchProtocolService { - public interface ILaunchProtocolService - { - object LaunchParameter { get; set; } - NameValueCollection MailtoParameters { get; set; } - } + /// + /// Used to handle toasts. + /// + object LaunchParameter { get; set; } + + /// + /// Used to handle mailto links. + /// + MailToUri MailToUri { get; set; } } diff --git a/Wino.Core.Domain/Interfaces/IMailService.cs b/Wino.Core.Domain/Interfaces/IMailService.cs index e314d854..1f9065ef 100644 --- a/Wino.Core.Domain/Interfaces/IMailService.cs +++ b/Wino.Core.Domain/Interfaces/IMailService.cs @@ -11,7 +11,6 @@ namespace Wino.Core.Domain.Interfaces { Task GetSingleMailItemAsync(string mailCopyId, string remoteFolderId); Task GetSingleMailItemAsync(Guid uniqueMailId); - Task CreateDraftAsync(MailAccount composerAccount, string generatedReplyMimeMessageBase64, MimeMessage replyingMimeMessage = null, IMailItem replyingMailItem = null); Task> FetchMailsAsync(MailListInitializationOptions options); /// @@ -44,23 +43,12 @@ namespace Wino.Core.Domain.Interfaces /// /// Maps new mail item with the existing local draft copy. - /// /// /// /// /// Task MapLocalDraftAsync(string newMailCopyId, string newDraftId, string newThreadId); - /// - /// Creates a draft message with the given options. - /// - /// Account to create draft for. - /// Draft creation options. - /// - /// Base64 encoded string of MimeMessage object. - /// This is mainly for serialization purposes. - /// - Task CreateDraftMimeBase64Async(Guid accountId, DraftCreationOptions options); Task UpdateMailAsync(MailCopy mailCopy); /// @@ -106,9 +94,18 @@ namespace Wino.Core.Domain.Interfaces /// Checks whether the mail exists in the folder. /// When deciding Create or Update existing mail, we need to check if the mail exists in the folder. /// - /// Message id + /// MailCopy id /// Folder's local id. /// Whether mail exists in the folder or not. Task IsMailExistsAsync(string mailCopyId, Guid folderId); + + /// + /// Creates a draft MailCopy and MimeMessage based on the given options. + /// For forward/reply it would include the referenced message. + /// + /// Account which should have new draft. + /// Options like new email/forward/draft. + /// Draft MailCopy and Draft MimeMessage as base64. + Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(MailAccount composerAccount, DraftCreationOptions draftCreationOptions); } } diff --git a/Wino.Core.Domain/Interfaces/IWinoRequestDelegator.cs b/Wino.Core.Domain/Interfaces/IWinoRequestDelegator.cs index 19906904..07147724 100644 --- a/Wino.Core.Domain/Interfaces/IWinoRequestDelegator.cs +++ b/Wino.Core.Domain/Interfaces/IWinoRequestDelegator.cs @@ -16,7 +16,7 @@ namespace Wino.Core.Domain.Interfaces /// Queues new draft creation request for synchronizer. /// /// A class that holds the parameters for creating a draft. - Task ExecuteAsync(DraftPreperationRequest draftPreperationRequest); + Task ExecuteAsync(DraftPreparationRequest draftPreperationRequest); /// /// Queues a new request for synchronizer to send a draft. diff --git a/Wino.Core.Domain/Models/Launch/MailToUri.cs b/Wino.Core.Domain/Models/Launch/MailToUri.cs new file mode 100644 index 00000000..119204de --- /dev/null +++ b/Wino.Core.Domain/Models/Launch/MailToUri.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace Wino.Core.Domain.Models.Launch; + +public class MailToUri +{ + public string Subject { get; private set; } + public string Body { get; private set; } + public List To { get; } = []; + public List Cc { get; } = []; + public List Bcc { get; } = []; + public Dictionary OtherParameters { get; } = []; + + public MailToUri(string mailToUrl) + { + ParseMailToUrl(mailToUrl); + } + + private void ParseMailToUrl(string mailToUrl) + { + if (string.IsNullOrWhiteSpace(mailToUrl)) + throw new ArgumentException("mailtoUrl cannot be null or empty.", nameof(mailToUrl)); + + if (!mailToUrl.StartsWith("mailto:", StringComparison.OrdinalIgnoreCase)) + throw new ArgumentException("URL must start with 'mailto:'.", nameof(mailToUrl)); + + var mailToWithoutScheme = mailToUrl.Substring(7); // Remove "mailto:" + var components = mailToWithoutScheme.Split('?'); + if (!string.IsNullOrEmpty(components[0])) + { + To.AddRange(components[0].Split(',').Select(email => HttpUtility.UrlDecode(email).Trim())); + } + + if (components.Length <= 1) + { + return; + } + + var parameters = components[1].Split('&'); + + foreach (var parameter in parameters) + { + var keyValue = parameter.Split('='); + if (keyValue.Length != 2) + continue; + + var key = keyValue[0].ToLowerInvariant(); + var value = HttpUtility.UrlDecode(keyValue[1]); + + switch (key) + { + case "to": + To.AddRange(value.Split(',').Select(email => email.Trim())); + break; + case "subject": + Subject = value; + break; + case "body": + Body = value; + break; + case "cc": + Cc.AddRange(value.Split(',').Select(email => email.Trim())); + break; + case "bcc": + Bcc.AddRange(value.Split(',').Select(email => email.Trim())); + break; + default: + OtherParameters[key] = value; + break; + } + } + } +} diff --git a/Wino.Core.Domain/Models/MailItem/DraftCreationOptions.cs b/Wino.Core.Domain/Models/MailItem/DraftCreationOptions.cs index cc8e8280..43882dd9 100644 --- a/Wino.Core.Domain/Models/MailItem/DraftCreationOptions.cs +++ b/Wino.Core.Domain/Models/MailItem/DraftCreationOptions.cs @@ -1,42 +1,27 @@ -using System.Collections.Specialized; -using System.Linq; -using System.Text.Json.Serialization; -using MimeKit; +using MimeKit; using Wino.Core.Domain.Entities; using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Models.Launch; -namespace Wino.Core.Domain.Models.MailItem +namespace Wino.Core.Domain.Models.MailItem; + +public class DraftCreationOptions { - public class DraftCreationOptions - { - [JsonIgnore] - public MimeMessage ReferenceMimeMessage { get; set; } - public MailCopy ReferenceMailCopy { get; set; } - public DraftCreationReason Reason { get; set; } + public DraftCreationReason Reason { get; set; } - #region Mailto Protocol Related Stuff + /// + /// Used for forward/reply + /// + public ReferencedMessage ReferencedMessage { get; set; } - public const string MailtoSubjectParameterKey = "subject"; - public const string MailtoBodyParameterKey = "body"; - public const string MailtoToParameterKey = "mailto"; - public const string MailtoCCParameterKey = "cc"; - public const string MailtoBCCParameterKey = "bcc"; - - public NameValueCollection MailtoParameters { get; set; } - - private bool IsMailtoParameterExists(string parameterKey) - => MailtoParameters != null - && MailtoParameters.AllKeys.Contains(parameterKey); - - public bool TryGetMailtoValue(string key, out string value) - { - bool valueExists = IsMailtoParameterExists(key); - - value = valueExists ? MailtoParameters[key] : string.Empty; - - return valueExists; - } - - #endregion - } + /// + /// Used to create mails from Mailto links + /// + public MailToUri MailToUri { get; set; } +} + +public class ReferencedMessage +{ + public MailCopy MailCopy { get; set; } + public MimeMessage MimeMessage { get; set; } } diff --git a/Wino.Core.Domain/Models/MailItem/DraftPreparationRequest.cs b/Wino.Core.Domain/Models/MailItem/DraftPreparationRequest.cs new file mode 100644 index 00000000..8d7809b6 --- /dev/null +++ b/Wino.Core.Domain/Models/MailItem/DraftPreparationRequest.cs @@ -0,0 +1,48 @@ +using System; +using System.Text.Json.Serialization; +using MimeKit; +using Wino.Core.Domain.Entities; +using Wino.Core.Domain.Extensions; + +namespace Wino.Core.Domain.Models.MailItem; + +public class DraftPreparationRequest +{ + public DraftPreparationRequest(MailAccount account, MailCopy createdLocalDraftCopy, string base64EncodedMimeMessage, MailCopy referenceMailCopy = null) + { + Account = account ?? throw new ArgumentNullException(nameof(account)); + + CreatedLocalDraftCopy = createdLocalDraftCopy ?? throw new ArgumentNullException(nameof(createdLocalDraftCopy)); + ReferenceMailCopy = referenceMailCopy; + + // MimeMessage is not serializable with System.Text.Json. Convert to base64 string. + // This is additional work when deserialization needed, but not much to do atm. + + Base64LocalDraftMimeMessage = base64EncodedMimeMessage; + } + + [JsonConstructor] + private DraftPreparationRequest() { } + + public MailCopy CreatedLocalDraftCopy { get; set; } + + public MailCopy ReferenceMailCopy { get; set; } + + public string Base64LocalDraftMimeMessage { get; set; } + + [JsonIgnore] + private MimeMessage createdLocalDraftMimeMessage; + + [JsonIgnore] + public MimeMessage CreatedLocalDraftMimeMessage + { + get + { + createdLocalDraftMimeMessage ??= Base64LocalDraftMimeMessage.GetMimeMessageFromBase64(); + + return createdLocalDraftMimeMessage; + } + } + + public MailAccount Account { get; } +} diff --git a/Wino.Core.Domain/Models/MailItem/DraftPreperationRequest.cs b/Wino.Core.Domain/Models/MailItem/DraftPreperationRequest.cs deleted file mode 100644 index e2a88536..00000000 --- a/Wino.Core.Domain/Models/MailItem/DraftPreperationRequest.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Text.Json.Serialization; -using MimeKit; -using Wino.Core.Domain.Entities; -using Wino.Core.Domain.Extensions; - -namespace Wino.Core.Domain.Models.MailItem -{ - public class DraftPreperationRequest : DraftCreationOptions - { - public DraftPreperationRequest(MailAccount account, MailCopy createdLocalDraftCopy, string base64EncodedMimeMessage) - { - Account = account ?? throw new ArgumentNullException(nameof(account)); - - CreatedLocalDraftCopy = createdLocalDraftCopy ?? throw new ArgumentNullException(nameof(createdLocalDraftCopy)); - - // MimeMessage is not serializable with System.Text.Json. Convert to base64 string. - // This is additional work when deserialization needed, but not much to do atm. - - Base64LocalDraftMimeMessage = base64EncodedMimeMessage; - } - - [JsonConstructor] - private DraftPreperationRequest() { } - - public MailCopy CreatedLocalDraftCopy { get; set; } - - public string Base64LocalDraftMimeMessage { get; set; } - - [JsonIgnore] - private MimeMessage createdLocalDraftMimeMessage; - - [JsonIgnore] - public MimeMessage CreatedLocalDraftMimeMessage - { - get - { - if (createdLocalDraftMimeMessage == null) - { - createdLocalDraftMimeMessage = Base64LocalDraftMimeMessage.GetMimeMessageFromBase64(); - } - - return createdLocalDraftMimeMessage; - } - } - - public MailAccount Account { get; } - } -} diff --git a/Wino.Core/Requests/CreateDraftRequest.cs b/Wino.Core/Requests/CreateDraftRequest.cs index 5e3ca403..026c5d4e 100644 --- a/Wino.Core/Requests/CreateDraftRequest.cs +++ b/Wino.Core/Requests/CreateDraftRequest.cs @@ -11,7 +11,7 @@ using Wino.Messaging.UI; namespace Wino.Core.Requests { - public record CreateDraftRequest(DraftPreperationRequest DraftPreperationRequest) + public record CreateDraftRequest(DraftPreparationRequest DraftPreperationRequest) : RequestBase(DraftPreperationRequest.CreatedLocalDraftCopy, MailSynchronizerOperation.CreateDraft), ICustomFolderSynchronizationRequest { @@ -36,7 +36,7 @@ namespace Wino.Core.Requests } [EditorBrowsable(EditorBrowsableState.Never)] - public record class BatchCreateDraftRequest(IEnumerable Items, DraftPreperationRequest DraftPreperationRequest) + public record class BatchCreateDraftRequest(IEnumerable Items, DraftPreparationRequest DraftPreperationRequest) : BatchRequestBase(Items, MailSynchronizerOperation.CreateDraft) { public override void ApplyUIChanges() diff --git a/Wino.Core/Services/LaunchProtocolService.cs b/Wino.Core/Services/LaunchProtocolService.cs new file mode 100644 index 00000000..52252b2c --- /dev/null +++ b/Wino.Core/Services/LaunchProtocolService.cs @@ -0,0 +1,10 @@ +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Launch; + +namespace Wino.Core.Services; + +public class LaunchProtocolService : ILaunchProtocolService +{ + public object LaunchParameter { get; set; } + public MailToUri MailToUri { get; set; } +} diff --git a/Wino.Core/Services/MailService.cs b/Wino.Core/Services/MailService.cs index 81e6f981..43b5df39 100644 --- a/Wino.Core/Services/MailService.cs +++ b/Wino.Core/Services/MailService.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.Kiota.Abstractions.Extensions; @@ -32,7 +31,6 @@ namespace Wino.Core.Services private readonly IMimeFileService _mimeFileService; private readonly IPreferencesService _preferencesService; - private readonly ILogger _logger = Log.ForContext(); public MailService(IDatabaseService databaseService, @@ -53,18 +51,9 @@ namespace Wino.Core.Services _preferencesService = preferencesService; } - public async Task CreateDraftAsync(MailAccount composerAccount, - string generatedReplyMimeMessageBase64, - MimeMessage replyingMimeMessage = null, - IMailItem replyingMailItem = null) + public async Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(MailAccount composerAccount, DraftCreationOptions draftCreationOptions) { - var createdDraftMimeMessage = generatedReplyMimeMessageBase64.GetMimeMessageFromBase64(); - - bool isImapAccount = composerAccount.ServerInformation != null; - - string fromName; - - fromName = composerAccount.SenderName; + var createdDraftMimeMessage = await CreateDraftMimeAsync(composerAccount, draftCreationOptions); var draftFolder = await _folderService.GetSpecialFolderByAccountIdAsync(composerAccount.Id, SpecialFolderType.Draft); @@ -78,7 +67,7 @@ namespace Wino.Core.Services Id = Guid.NewGuid().ToString(), // This will be replaced after network call with the remote draft id. CreationDate = DateTime.UtcNow, FromAddress = composerAccount.Address, - FromName = fromName, + FromName = composerAccount.SenderName, HasAttachments = false, Importance = MailImportance.Normal, Subject = createdDraftMimeMessage.Subject, @@ -93,28 +82,25 @@ namespace Wino.Core.Services }; // If replying, add In-Reply-To, ThreadId and References. - bool isReplying = replyingMimeMessage != null; - - if (isReplying) + if (draftCreationOptions.ReferencedMessage != null) { - if (replyingMimeMessage.References != null) - copy.References = string.Join(",", replyingMimeMessage.References); + if (draftCreationOptions.ReferencedMessage.MimeMessage.References != null) + copy.References = string.Join(",", draftCreationOptions.ReferencedMessage.MimeMessage.References); - if (!string.IsNullOrEmpty(replyingMimeMessage.MessageId)) - copy.InReplyTo = replyingMimeMessage.MessageId; + if (!string.IsNullOrEmpty(draftCreationOptions.ReferencedMessage.MimeMessage.MessageId)) + copy.InReplyTo = draftCreationOptions.ReferencedMessage.MimeMessage.MessageId; - if (!string.IsNullOrEmpty(replyingMailItem?.ThreadId)) - copy.ThreadId = replyingMailItem.ThreadId; + if (!string.IsNullOrEmpty(draftCreationOptions.ReferencedMessage.MailCopy?.ThreadId)) + copy.ThreadId = draftCreationOptions.ReferencedMessage.MailCopy.ThreadId; } await Connection.InsertAsync(copy); - await _mimeFileService.SaveMimeMessageAsync(copy.FileId, createdDraftMimeMessage, composerAccount.Id); ReportUIChange(new DraftCreated(copy, composerAccount)); - return copy; + return (copy, createdDraftMimeMessage.GetBase64MimeMessage()); } public async Task> GetMailsByFolderIdAsync(Guid folderId) @@ -629,85 +615,45 @@ namespace Wino.Core.Services } } - public async Task CreateDraftMimeBase64Async(Guid accountId, DraftCreationOptions draftCreationOptions) + private async Task CreateDraftMimeAsync(MailAccount account, DraftCreationOptions draftCreationOptions) { // This unique id is stored in mime headers for Wino to identify remote message with local copy. // Same unique id will be used for the local copy as well. // Synchronizer will map this unique id to the local draft copy after synchronization. - - var messageUniqueId = Guid.NewGuid(); - var message = new MimeMessage() { - Headers = { { Constants.WinoLocalDraftHeader, messageUniqueId.ToString() } } + Headers = { { Constants.WinoLocalDraftHeader, Guid.NewGuid().ToString() } }, + From = { new MailboxAddress(account.SenderName, account.Address) } }; var builder = new BodyBuilder(); - var account = await _accountService.GetAccountAsync(accountId).ConfigureAwait(false); + var signature = await GetSignature(account, draftCreationOptions.Reason); - if (account == null) + _ = draftCreationOptions.Reason switch { - _logger.Warning("Can't create draft mime message because account {AccountId} does not exist.", accountId); + DraftCreationReason.Empty => CreateEmptyDraft(builder, message, draftCreationOptions, signature), + _ => CreateReferencedDraft(builder, message, draftCreationOptions, account, signature), + }; - return null; + if (!string.IsNullOrEmpty(builder.HtmlBody)) + { + builder.TextBody = HtmlAgilityPackExtensions.GetPreviewText(builder.HtmlBody); } - var reason = draftCreationOptions.Reason; - var referenceMessage = draftCreationOptions.ReferenceMimeMessage; + message.Body = builder.ToMessageBody(); - message.From.Add(new MailboxAddress(account.SenderName, account.Address)); + return message; + } - // It contains empty blocks with inlined font, to make sure when users starts typing,it will follow selected font. - var gapHtml = CreateHtmlGap(); + private string CreateHtmlGap() + { + var template = $"""

"""; + return string.Concat(Enumerable.Repeat(template, 2)); + } - // Manage "To" - if (reason == DraftCreationReason.Reply || reason == DraftCreationReason.ReplyAll) - { - // Reply to the sender of the message - - if (referenceMessage.ReplyTo.Count > 0) - message.To.AddRange(referenceMessage.ReplyTo); - else if (referenceMessage.From.Count > 0) - message.To.AddRange(referenceMessage.From); - else if (referenceMessage.Sender != null) - message.To.Add(referenceMessage.Sender); - - if (reason == DraftCreationReason.ReplyAll) - { - // Include all of the other original recipients - message.To.AddRange(referenceMessage.To); - - // Find self and remove - var self = message.To.FirstOrDefault(a => a is MailboxAddress mailboxAddress && mailboxAddress.Address == account.Address); - - if (self != null) - message.To.Remove(self); - - message.Cc.AddRange(referenceMessage.Cc); - } - - // Manage "ThreadId-ConversationId" - if (!string.IsNullOrEmpty(referenceMessage.MessageId)) - { - message.InReplyTo = referenceMessage.MessageId; - - message.References.AddRange(referenceMessage.References); - - message.References.Add(referenceMessage.MessageId); - } - - message.Headers.Add("Thread-Topic", referenceMessage.Subject); - - builder.HtmlBody = CreateHtmlForReferencingMessage(referenceMessage); - } - - if (reason == DraftCreationReason.Forward) - { - builder.HtmlBody = CreateHtmlForReferencingMessage(referenceMessage); - } - - // Append signatures if needed. + private async Task GetSignature(MailAccount account, DraftCreationReason reason) + { if (account.Preferences.IsSignatureEnabled) { var signatureId = reason == DraftCreationReason.Empty ? @@ -718,26 +664,88 @@ namespace Wino.Core.Services { var signature = await _signatureService.GetSignatureAsync(signatureId.Value); - if (string.IsNullOrWhiteSpace(builder.HtmlBody)) - { - builder.HtmlBody = $"{gapHtml}{signature.HtmlBody}"; - } - else - { - builder.HtmlBody = $"{gapHtml}{signature.HtmlBody}{gapHtml}{builder.HtmlBody}"; - } + return signature.HtmlBody; } } - else + + return null; + } + + private MimeMessage CreateEmptyDraft(BodyBuilder builder, MimeMessage message, DraftCreationOptions draftCreationOptions, string signature) + { + builder.HtmlBody = CreateHtmlGap(); + if (draftCreationOptions.MailToUri != null) { - builder.HtmlBody = $"{gapHtml}{builder.HtmlBody}"; + if (draftCreationOptions.MailToUri.Subject != null) + message.Subject = draftCreationOptions.MailToUri.Subject; + + if (draftCreationOptions.MailToUri.Body != null) + { + builder.HtmlBody = $"""
{draftCreationOptions.MailToUri.Body}
""" + builder.HtmlBody; + } + + if (draftCreationOptions.MailToUri.To.Any()) + message.To.AddRange(draftCreationOptions.MailToUri.To.Select(x => new MailboxAddress(x, x))); + + if (draftCreationOptions.MailToUri.Cc.Any()) + message.Cc.AddRange(draftCreationOptions.MailToUri.Cc.Select(x => new MailboxAddress(x, x))); + + if (draftCreationOptions.MailToUri.Bcc.Any()) + message.Bcc.AddRange(draftCreationOptions.MailToUri.Bcc.Select(x => new MailboxAddress(x, x))); + } + + if (signature != null) + builder.HtmlBody += signature; + + return message; + } + + private MimeMessage CreateReferencedDraft(BodyBuilder builder, MimeMessage message, DraftCreationOptions draftCreationOptions, MailAccount account, string signature) + { + var reason = draftCreationOptions.Reason; + var referenceMessage = draftCreationOptions.ReferencedMessage.MimeMessage; + + var gap = CreateHtmlGap(); + builder.HtmlBody = gap + CreateHtmlForReferencingMessage(referenceMessage); + + if (signature != null) + { + builder.HtmlBody = gap + signature + builder.HtmlBody; + } + + // Manage "To" + if (reason == DraftCreationReason.Reply || reason == DraftCreationReason.ReplyAll) + { + // Reply to the sender of the message + if (referenceMessage.ReplyTo.Count > 0) + message.To.AddRange(referenceMessage.ReplyTo); + else if (referenceMessage.From.Count > 0) + message.To.AddRange(referenceMessage.From); + else if (referenceMessage.Sender != null) + message.To.Add(referenceMessage.Sender); + + if (reason == DraftCreationReason.ReplyAll) + { + // Include all of the other original recipients + message.To.AddRange(referenceMessage.To.Where(x => x is MailboxAddress mailboxAddress && !mailboxAddress.Address.Equals(account.Address, StringComparison.OrdinalIgnoreCase))); + message.Cc.AddRange(referenceMessage.Cc.Where(x => x is MailboxAddress mailboxAddress && !mailboxAddress.Address.Equals(account.Address, StringComparison.OrdinalIgnoreCase))); + } + + // Manage "ThreadId-ConversationId" + if (!string.IsNullOrEmpty(referenceMessage.MessageId)) + { + message.InReplyTo = referenceMessage.MessageId; + message.References.AddRange(referenceMessage.References); + message.References.Add(referenceMessage.MessageId); + } + + message.Headers.Add("Thread-Topic", referenceMessage.Subject); } // Manage Subject if (reason == DraftCreationReason.Forward && !referenceMessage.Subject.StartsWith("FW: ", StringComparison.OrdinalIgnoreCase)) message.Subject = $"FW: {referenceMessage.Subject}"; - else if ((reason == DraftCreationReason.Reply || reason == DraftCreationReason.ReplyAll) && - !referenceMessage.Subject.StartsWith("RE: ", StringComparison.OrdinalIgnoreCase)) + else if ((reason == DraftCreationReason.Reply || reason == DraftCreationReason.ReplyAll) && !referenceMessage.Subject.StartsWith("RE: ", StringComparison.OrdinalIgnoreCase)) message.Subject = $"RE: {referenceMessage.Subject}"; else if (referenceMessage != null) message.Subject = referenceMessage.Subject; @@ -751,63 +759,7 @@ namespace Wino.Core.Services } } - if (!string.IsNullOrEmpty(builder.HtmlBody)) - { - builder.TextBody = HtmlAgilityPackExtensions.GetPreviewText(builder.HtmlBody); - } - - message.Body = builder.ToMessageBody(); - - // Apply mail-to protocol parameters if exists. - - if (draftCreationOptions.MailtoParameters != null) - { - if (draftCreationOptions.TryGetMailtoValue(DraftCreationOptions.MailtoSubjectParameterKey, out string subjectParameter)) - message.Subject = subjectParameter; - - if (draftCreationOptions.TryGetMailtoValue(DraftCreationOptions.MailtoBodyParameterKey, out string bodyParameter)) - { - builder.TextBody = bodyParameter; - builder.HtmlBody = bodyParameter; - - message.Body = builder.ToMessageBody(); - } - - static InternetAddressList ExtractRecipients(string parameterValue) - { - var list = new InternetAddressList(); - - var splittedRecipients = parameterValue.Split(','); - - foreach (var recipient in splittedRecipients) - list.Add(new MailboxAddress(recipient, recipient)); - - return list; - - } - - if (draftCreationOptions.TryGetMailtoValue(DraftCreationOptions.MailtoToParameterKey, out string toParameter)) - message.To.AddRange(ExtractRecipients(toParameter)); - - if (draftCreationOptions.TryGetMailtoValue(DraftCreationOptions.MailtoCCParameterKey, out string ccParameter)) - message.Cc.AddRange(ExtractRecipients(ccParameter)); - - if (draftCreationOptions.TryGetMailtoValue(DraftCreationOptions.MailtoBCCParameterKey, out string bccParameter)) - message.Bcc.AddRange(ExtractRecipients(bccParameter)); - } - else - { - // Update TextBody from existing HtmlBody if exists. - } - - using MemoryStream memoryStream = new(); - message.WriteTo(FormatOptions.Default, memoryStream); - byte[] buffer = memoryStream.GetBuffer(); - int count = (int)memoryStream.Length; - - return Convert.ToBase64String(buffer); - - // return message; + return message; // Generates html representation of To/Cc/From/Time and so on from referenced message. string CreateHtmlForReferencingMessage(MimeMessage referenceMessage) @@ -820,28 +772,22 @@ namespace Wino.Core.Services visitor.Visit(referenceMessage); htmlMimeInfo += $""" -
- - From: {ParticipantsToHtml(referenceMessage.From)}
- Sent: {referenceMessage.Date.ToLocalTime()}
- To: {ParticipantsToHtml(referenceMessage.To)}
- {(referenceMessage.Cc.Count > 0 ? $"Cc: {ParticipantsToHtml(referenceMessage.Cc)}
" : string.Empty)} - Subject: {referenceMessage.Subject} -
-
 
- {visitor.HtmlBody} -
- """; +
+ + From: {ParticipantsToHtml(referenceMessage.From)}
+ Sent: {referenceMessage.Date.ToLocalTime()}
+ To: {ParticipantsToHtml(referenceMessage.To)}
+ {(referenceMessage.Cc.Count > 0 ? $"Cc: {ParticipantsToHtml(referenceMessage.Cc)}
" : string.Empty)} + Subject: {referenceMessage.Subject} +
+
 
+ {visitor.HtmlBody} +
+ """; return htmlMimeInfo; } - string CreateHtmlGap() - { - var template = $"""

"""; - return string.Concat(Enumerable.Repeat(template, 5)); - } - static string ParticipantsToHtml(InternetAddressList internetAddresses) => string.Join("; ", internetAddresses.Mailboxes .Select(x => $"{x.Name ?? Translator.UnknownSender} <{x.Address ?? Translator.UnknownAddress}>")); diff --git a/Wino.Core/Services/WinoRequestDelegator.cs b/Wino.Core/Services/WinoRequestDelegator.cs index 6a270239..b47196ba 100644 --- a/Wino.Core/Services/WinoRequestDelegator.cs +++ b/Wino.Core/Services/WinoRequestDelegator.cs @@ -111,7 +111,7 @@ namespace Wino.Core.Services QueueSynchronization(accountId); } - public async Task ExecuteAsync(DraftPreperationRequest draftPreperationRequest) + public async Task ExecuteAsync(DraftPreparationRequest draftPreperationRequest) { var request = new CreateDraftRequest(draftPreperationRequest); diff --git a/Wino.Mail.ViewModels/AppShellViewModel.cs b/Wino.Mail.ViewModels/AppShellViewModel.cs index 5ae4b34a..f037ab81 100644 --- a/Wino.Mail.ViewModels/AppShellViewModel.cs +++ b/Wino.Mail.ViewModels/AppShellViewModel.cs @@ -302,7 +302,7 @@ namespace Wino.Mail.ViewModels } else { - bool hasMailtoActivation = _launchProtocolService.MailtoParameters != null; + bool hasMailtoActivation = _launchProtocolService.MailToUri != null; if (hasMailtoActivation) { @@ -774,16 +774,13 @@ namespace Wino.Mail.ViewModels var draftOptions = new DraftCreationOptions { Reason = DraftCreationReason.Empty, - - // Include mail to parameters for parsing mailto if any. - MailtoParameters = _launchProtocolService.MailtoParameters + MailToUri = _launchProtocolService.MailToUri }; - var createdBase64EncodedMimeMessage = await _mailService.CreateDraftMimeBase64Async(account.Id, draftOptions).ConfigureAwait(false); - var createdDraftMailMessage = await _mailService.CreateDraftAsync(account, createdBase64EncodedMimeMessage).ConfigureAwait(false); + var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(account, draftOptions).ConfigureAwait(false); - var draftPreperationRequest = new DraftPreperationRequest(account, createdDraftMailMessage, createdBase64EncodedMimeMessage); - await _winoRequestDelegator.ExecuteAsync(draftPreperationRequest); + var draftPreparationRequest = new DraftPreparationRequest(account, draftMailCopy, draftBase64MimeMessage); + await _winoRequestDelegator.ExecuteAsync(draftPreparationRequest); } protected override async void OnAccountUpdated(MailAccount updatedAccount) diff --git a/Wino.Mail.ViewModels/ComposePageViewModel.cs b/Wino.Mail.ViewModels/ComposePageViewModel.cs index e99adb70..966daf57 100644 --- a/Wino.Mail.ViewModels/ComposePageViewModel.cs +++ b/Wino.Mail.ViewModels/ComposePageViewModel.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; -using System.Text.RegularExpressions; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -58,7 +57,7 @@ namespace Wino.Mail.ViewModels private MessageImportance selectedMessageImportance; [ObservableProperty] - private bool isCCBCCVisible = true; + private bool isCCBCCVisible; [ObservableProperty] private string subject; @@ -77,21 +76,20 @@ namespace Wino.Mail.ViewModels [ObservableProperty] private bool isDraggingOverImagesDropZone; - public ObservableCollection IncludedAttachments { get; set; } = new ObservableCollection(); - - public ObservableCollection Accounts { get; set; } = new ObservableCollection(); - public ObservableCollection ToItems { get; set; } = new ObservableCollection(); - public ObservableCollection CCItemsItems { get; set; } = new ObservableCollection(); - public ObservableCollection BCCItems { get; set; } = new ObservableCollection(); + public ObservableCollection IncludedAttachments { get; set; } = []; + public ObservableCollection Accounts { get; set; } = []; + public ObservableCollection ToItems { get; set; } = []; + public ObservableCollection CCItems { get; set; } = []; + public ObservableCollection BCCItems { get; set; } = []; - public List ToolbarSections { get; set; } = new List() - { + public List ToolbarSections { get; set; } = + [ new EditorToolbarSection(){ SectionType = EditorToolbarSectionType.Format }, new EditorToolbarSection(){ SectionType = EditorToolbarSectionType.Insert }, new EditorToolbarSection(){ SectionType = EditorToolbarSectionType.Draw }, new EditorToolbarSection(){ SectionType = EditorToolbarSectionType.Options } - }; + ]; private EditorToolbarSection selectedToolbarSection; @@ -190,7 +188,7 @@ namespace Wino.Mail.ViewModels // Save recipients. SaveAddressInfo(ToItems, CurrentMimeMessage.To); - SaveAddressInfo(CCItemsItems, CurrentMimeMessage.Cc); + SaveAddressInfo(CCItems, CurrentMimeMessage.Cc); SaveAddressInfo(BCCItems, CurrentMimeMessage.Bcc); SaveImportance(); @@ -239,12 +237,7 @@ namespace Wino.Mail.ViewModels { if (GetHTMLBodyFunction != null) { - var htmlBody = await GetHTMLBodyFunction(); - - if (!string.IsNullOrEmpty(htmlBody)) - { - bodyBuilder.HtmlBody = Regex.Unescape(htmlBody); - } + bodyBuilder.HtmlBody = await GetHTMLBodyFunction(); } if (!string.IsNullOrEmpty(bodyBuilder.HtmlBody)) @@ -309,7 +302,7 @@ namespace Wino.Mail.ViewModels // Check if there is any delivering mail address from protocol launch. - if (_launchProtocolService.MailtoParameters != null) + if (_launchProtocolService.MailToUri != null) { // TODO //var requestedMailContact = await GetAddressInformationAsync(_launchProtocolService.MailtoParameters, ToItems); @@ -322,7 +315,7 @@ namespace Wino.Mail.ViewModels // DialogService.InfoBarMessage("Invalid Address", "Address is not a valid e-mail address.", InfoBarMessageType.Warning); // Clear the address. - _launchProtocolService.MailtoParameters = null; + _launchProtocolService.MailToUri = null; } } @@ -427,15 +420,18 @@ namespace Wino.Mail.ViewModels // Extract information ToItems.Clear(); - CCItemsItems.Clear(); + CCItems.Clear(); BCCItems.Clear(); LoadAddressInfo(replyingMime.To, ToItems); - LoadAddressInfo(replyingMime.Cc, CCItemsItems); + LoadAddressInfo(replyingMime.Cc, CCItems); LoadAddressInfo(replyingMime.Bcc, BCCItems); LoadAttachments(replyingMime.Attachments); + if (replyingMime.Cc.Any() || replyingMime.Bcc.Any()) + IsCCBCCVisible = true; + Subject = replyingMime.Subject; CurrentMimeMessage = replyingMime; diff --git a/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs b/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs index 8aae0408..bf1a722d 100644 --- a/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs +++ b/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs @@ -117,7 +117,7 @@ namespace Wino.Mail.ViewModels #endregion public INativeAppService NativeAppService { get; } - public IStatePersistanceService StatePersistanceService { get; } + public IStatePersistanceService StatePersistenceService { get; } public IPreferencesService PreferencesService { get; } public MailRenderingPageViewModel(IDialogService dialogService, @@ -127,14 +127,14 @@ namespace Wino.Mail.ViewModels Core.Domain.Interfaces.IMailService mailService, IFileService fileService, IWinoRequestDelegator requestDelegator, - IStatePersistanceService statePersistanceService, + IStatePersistanceService statePersistenceService, IClipboardService clipboardService, IUnsubscriptionService unsubscriptionService, IPreferencesService preferencesService, IWinoServerConnectionManager winoServerConnectionManager) : base(dialogService) { NativeAppService = nativeAppService; - StatePersistanceService = statePersistanceService; + StatePersistenceService = statePersistenceService; PreferencesService = preferencesService; _winoServerConnectionManager = winoServerConnectionManager; _clipboardService = clipboardService; @@ -255,37 +255,27 @@ namespace Wino.Mail.ViewModels if (initializedMailItemViewModel == null) return; // Create new draft. - var draftOptions = new DraftCreationOptions(); - - if (operation == MailOperation.Reply) - draftOptions.Reason = DraftCreationReason.Reply; - else if (operation == MailOperation.ReplyAll) - draftOptions.Reason = DraftCreationReason.ReplyAll; - else if (operation == MailOperation.Forward) - draftOptions.Reason = DraftCreationReason.Forward; - - // TODO: Separate mailto related stuff out of DraftCreationOptions and provide better - // model for draft preperation request. Right now it's a mess. - - draftOptions.ReferenceMailCopy = initializedMailItemViewModel.MailCopy; - draftOptions.ReferenceMimeMessage = initializedMimeMessageInformation.MimeMessage; - - var createdMimeMessage = await _mailService.CreateDraftMimeBase64Async(initializedMailItemViewModel.AssignedAccount.Id, draftOptions).ConfigureAwait(false); - - var createdDraftMailMessage = await _mailService.CreateDraftAsync(initializedMailItemViewModel.AssignedAccount, - createdMimeMessage, - initializedMimeMessageInformation.MimeMessage, - initializedMailItemViewModel).ConfigureAwait(false); - - var draftPreperationRequest = new DraftPreperationRequest(initializedMailItemViewModel.AssignedAccount, - createdDraftMailMessage, - createdMimeMessage) + var draftOptions = new DraftCreationOptions() { - ReferenceMimeMessage = initializedMimeMessageInformation.MimeMessage, - ReferenceMailCopy = initializedMailItemViewModel.MailCopy + 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 + } }; - await _requestDelegator.ExecuteAsync(draftPreperationRequest); + var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(initializedMailItemViewModel.AssignedAccount, draftOptions).ConfigureAwait(false); + + var draftPreparationRequest = new DraftPreparationRequest(initializedMailItemViewModel.AssignedAccount, draftMailCopy, draftBase64MimeMessage, initializedMailItemViewModel.MailCopy); + + await _requestDelegator.ExecuteAsync(draftPreparationRequest); } else if (initializedMailItemViewModel != null) @@ -453,7 +443,7 @@ namespace Wino.Mail.ViewModels OnPropertyChanged(nameof(IsImageRenderingDisabled)); - StatePersistanceService.IsReadingMail = true; + StatePersistenceService.IsReadingMail = true; }); } @@ -477,7 +467,7 @@ namespace Wino.Mail.ViewModels Attachments.Clear(); MenuItems.Clear(); - StatePersistanceService.IsReadingMail = false; + StatePersistenceService.IsReadingMail = false; } private void LoadAddressInfo(InternetAddressList list, ObservableCollection collection) diff --git a/Wino.Mail/Activation/ProtocolActivationHandler.cs b/Wino.Mail/Activation/ProtocolActivationHandler.cs index 37e116dd..e8ff99ed 100644 --- a/Wino.Mail/Activation/ProtocolActivationHandler.cs +++ b/Wino.Mail/Activation/ProtocolActivationHandler.cs @@ -1,8 +1,8 @@ using System.Threading.Tasks; -using System.Web; using CommunityToolkit.Mvvm.Messaging; using Windows.ApplicationModel.Activation; using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Launch; using Wino.Messaging.Client.Authorization; using Wino.Messaging.Client.Shell; @@ -36,11 +36,7 @@ namespace Wino.Activation else if (protocolString.StartsWith(MailtoProtocolTag)) { // mailto activation. Try to parse params. - - var replaced = protocolString.Replace(MailtoProtocolTag, "mailto="); - replaced = Wino.Core.Extensions.StringExtensions.ReplaceFirst(replaced, "?", "&"); - - _launchProtocolService.MailtoParameters = HttpUtility.ParseQueryString(replaced); + _launchProtocolService.MailToUri = new MailToUri(protocolString); if (_nativeAppService.IsAppRunning()) { diff --git a/Wino.Mail/Services/LaunchProtocolService.cs b/Wino.Mail/Services/LaunchProtocolService.cs deleted file mode 100644 index 40a699ed..00000000 --- a/Wino.Mail/Services/LaunchProtocolService.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Specialized; -using Wino.Core.Domain.Interfaces; - -namespace Wino.Core.UWP.Services -{ - public class LaunchProtocolService : ILaunchProtocolService - { - public object LaunchParameter { get; set; } - public NameValueCollection MailtoParameters { get; set; } - } -} diff --git a/Wino.Mail/Views/ComposePage.xaml b/Wino.Mail/Views/ComposePage.xaml index 9db25cf4..571d05f0 100644 --- a/Wino.Mail/Views/ComposePage.xaml +++ b/Wino.Mail/Views/ComposePage.xaml @@ -493,7 +493,7 @@ VerticalAlignment="Center" Click="ShowCCBCCClicked" GotFocus="CCBBCGotFocus" - Visibility="{x:Bind ViewModel.IsCCBCCVisible, Mode=OneWay}"> + Visibility="{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(ViewModel.IsCCBCCVisible), Mode=OneWay}"> + Visibility="{x:Bind ViewModel.IsCCBCCVisible, Mode=OneWay}" /> + Visibility="{x:Bind ViewModel.IsCCBCCVisible, Mode=OneWay}" /> + Visibility="{x:Bind ViewModel.IsCCBCCVisible, Mode=OneWay}" /> + Visibility="{x:Bind ViewModel.IsCCBCCVisible, Mode=OneWay}" /> - CommandBarItems.xaml @@ -892,4 +891,4 @@ --> - + \ No newline at end of file