From 07bb90dda9bcf6de2c2abdeb083146cd9b81f2c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Mon, 19 Aug 2024 03:44:16 +0200 Subject: [PATCH] Refactoring outlook draft creation and sending. --- ...=> ICustomFolderSynchronizationRequest.cs} | 0 .../MailItem/DraftPreparationRequest.cs | 11 ++- .../Models/Requests/ServerRequestPackage.cs | 5 +- .../Extensions/OutlookIntegratorExtensions.cs | 39 +++++--- .../Synchronizers/OutlookSynchronizer.cs | 95 +++++++++++++++---- Wino.Mail.ViewModels/AppShellViewModel.cs | 2 +- .../MailRenderingPageViewModel.cs | 2 +- 7 files changed, 115 insertions(+), 39 deletions(-) rename Wino.Core.Domain/Interfaces/{AfterRequestExecutionSynchronizationInterfaces.cs => ICustomFolderSynchronizationRequest.cs} (100%) diff --git a/Wino.Core.Domain/Interfaces/AfterRequestExecutionSynchronizationInterfaces.cs b/Wino.Core.Domain/Interfaces/ICustomFolderSynchronizationRequest.cs similarity index 100% rename from Wino.Core.Domain/Interfaces/AfterRequestExecutionSynchronizationInterfaces.cs rename to Wino.Core.Domain/Interfaces/ICustomFolderSynchronizationRequest.cs diff --git a/Wino.Core.Domain/Models/MailItem/DraftPreparationRequest.cs b/Wino.Core.Domain/Models/MailItem/DraftPreparationRequest.cs index 8d7809b6..af8bf6b1 100644 --- a/Wino.Core.Domain/Models/MailItem/DraftPreparationRequest.cs +++ b/Wino.Core.Domain/Models/MailItem/DraftPreparationRequest.cs @@ -2,13 +2,18 @@ using System.Text.Json.Serialization; using MimeKit; using Wino.Core.Domain.Entities; +using Wino.Core.Domain.Enums; 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) + public DraftPreparationRequest(MailAccount account, + MailCopy createdLocalDraftCopy, + string base64EncodedMimeMessage, + DraftCreationReason reason, + MailCopy referenceMailCopy = null) { Account = account ?? throw new ArgumentNullException(nameof(account)); @@ -19,6 +24,7 @@ public class DraftPreparationRequest // This is additional work when deserialization needed, but not much to do atm. Base64LocalDraftMimeMessage = base64EncodedMimeMessage; + Reason = reason; } [JsonConstructor] @@ -29,6 +35,7 @@ public class DraftPreparationRequest public MailCopy ReferenceMailCopy { get; set; } public string Base64LocalDraftMimeMessage { get; set; } + public DraftCreationReason Reason { get; set; } [JsonIgnore] private MimeMessage createdLocalDraftMimeMessage; @@ -44,5 +51,5 @@ public class DraftPreparationRequest } } - public MailAccount Account { get; } + public MailAccount Account { get; set; } } diff --git a/Wino.Core.Domain/Models/Requests/ServerRequestPackage.cs b/Wino.Core.Domain/Models/Requests/ServerRequestPackage.cs index 98ca5792..31a408a2 100644 --- a/Wino.Core.Domain/Models/Requests/ServerRequestPackage.cs +++ b/Wino.Core.Domain/Models/Requests/ServerRequestPackage.cs @@ -3,14 +3,11 @@ using Wino.Core.Domain.Interfaces; namespace Wino.Core.Domain.Models.Requests { - /// /// Encapsulates request to queue and account for synchronizer. /// - /// - /// + /// Which account to execute this request for. /// Prepared request for the server. - /// Whihc account to execute this request for. public record ServerRequestPackage(Guid AccountId, IRequestBase Request) : IClientMessage { public override string ToString() => $"Server Package: {Request.GetType().Name}"; diff --git a/Wino.Core/Extensions/OutlookIntegratorExtensions.cs b/Wino.Core/Extensions/OutlookIntegratorExtensions.cs index 269bd55f..12e635c4 100644 --- a/Wino.Core/Extensions/OutlookIntegratorExtensions.cs +++ b/Wino.Core/Extensions/OutlookIntegratorExtensions.cs @@ -66,7 +66,7 @@ namespace Wino.Core.Extensions return mailCopy; } - public static Message AsOutlookMessage(this MimeMessage mime, string threadId) + public static Message AsOutlookMessage(this MimeMessage mime, bool includeInternetHeaders) { var fromAddress = GetRecipients(mime.From).ElementAt(0); var toAddresses = GetRecipients(mime.To).ToList(); @@ -85,13 +85,19 @@ namespace Wino.Core.Extensions CcRecipients = ccAddresses, BccRecipients = bccAddresses, From = fromAddress, - InternetMessageId = GetMessageIdHeader(mime.MessageId), - ConversationId = threadId, - InternetMessageHeaders = GetHeaderList(mime), + InternetMessageId = GetProperId(mime.MessageId), ReplyTo = replyToAddresses, Attachments = [] }; + // Headers are only included when creating the draft. + // When sending, they are not included. Graph will throw an error. + + if (includeInternetHeaders) + { + message.InternetMessageHeaders = GetHeaderList(mime); + } + foreach (var part in mime.BodyParts) { if (part.IsAttachment) @@ -172,7 +178,10 @@ namespace Wino.Core.Extensions // Here we'll try to ignore some headers that are not neccessary. // Outlook API will generate them automatically. + // Some headers also require to start with X- or x-. + string[] headersToIgnore = ["Date", "To", "MIME-Version", "From", "Subject", "Message-Id"]; + string[] headersToModify = ["In-Reply-To", "Reply-To", "References", "Thread-Topic"]; var headers = new List(); @@ -182,7 +191,15 @@ namespace Wino.Core.Extensions { if (!headersToIgnore.Contains(header.Field)) { - headers.Add(new InternetMessageHeader() { Name = header.Field, Value = header.Value }); + if (headersToModify.Contains(header.Field)) + { + headers.Add(new InternetMessageHeader() { Name = $"X-{header.Field}", Value = header.Value }); + } + else + { + headers.Add(new InternetMessageHeader() { Name = header.Field, Value = header.Value }); + } + includedHeaderCount++; } @@ -192,15 +209,15 @@ namespace Wino.Core.Extensions return headers; } - private static string GetMessageIdHeader(string messageId) + private static string GetProperId(string id) { - // Message-Id header must always start with "X-" or "x-". - if (string.IsNullOrEmpty(messageId)) return string.Empty; + // Outlook requires some identifiers to start with "X-" or "x-". + if (string.IsNullOrEmpty(id)) return string.Empty; - if (!messageId.StartsWith("x-") || !messageId.StartsWith("X-")) - return $"X-{messageId}"; + if (!id.StartsWith("x-") || !id.StartsWith("X-")) + return $"X-{id}"; - return messageId; + return id; } #endregion } diff --git a/Wino.Core/Synchronizers/OutlookSynchronizer.cs b/Wino.Core/Synchronizers/OutlookSynchronizer.cs index d2591118..7b2c0054 100644 --- a/Wino.Core/Synchronizers/OutlookSynchronizer.cs +++ b/Wino.Core/Synchronizers/OutlookSynchronizer.cs @@ -5,12 +5,10 @@ using System.IO; using System.Linq; using System.Net; using System.Net.Http; -using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.Graph; -using Microsoft.Graph.Me.SendMail; using Microsoft.Graph.Models; using Microsoft.Kiota.Abstractions; using Microsoft.Kiota.Abstractions.Authentication; @@ -604,22 +602,50 @@ namespace Wino.Core.Synchronizers { if (item is CreateDraftRequest createDraftRequest) { - createDraftRequest.DraftPreperationRequest.CreatedLocalDraftMimeMessage.Prepare(EncodingConstraint.None); + var reason = createDraftRequest.DraftPreperationRequest.Reason; + var message = createDraftRequest.DraftPreperationRequest.CreatedLocalDraftMimeMessage.AsOutlookMessage(true); - var plainTextBytes = Encoding.UTF8.GetBytes(createDraftRequest.DraftPreperationRequest.CreatedLocalDraftMimeMessage.ToString()); - var base64Encoded = Convert.ToBase64String(plainTextBytes); + if (reason == DraftCreationReason.Empty) + { + return _graphClient.Me.Messages.ToPostRequestInformation(message); + } + else if (reason == DraftCreationReason.Reply) + { + return _graphClient.Me.Messages[createDraftRequest.DraftPreperationRequest.ReferenceMailCopy.Id].CreateReply.ToPostRequestInformation(new Microsoft.Graph.Me.Messages.Item.CreateReply.CreateReplyPostRequestBody() + { + Message = message + }); + } + else if (reason == DraftCreationReason.ReplyAll) + { + return _graphClient.Me.Messages[createDraftRequest.DraftPreperationRequest.ReferenceMailCopy.Id].CreateReplyAll.ToPostRequestInformation(new Microsoft.Graph.Me.Messages.Item.CreateReplyAll.CreateReplyAllPostRequestBody() + { + Message = message + }); + } + else if (reason == DraftCreationReason.Forward) + { + return _graphClient.Me.Messages[createDraftRequest.DraftPreperationRequest.ReferenceMailCopy.Id].CreateForward.ToPostRequestInformation(new Microsoft.Graph.Me.Messages.Item.CreateForward.CreateForwardPostRequestBody() + { + Message = message + }); + //createDraftRequest.DraftPreperationRequest.CreatedLocalDraftMimeMessage.Prepare(EncodingConstraint.None); - var requestInformation = _graphClient.Me.Messages.ToPostRequestInformation(new Message()); + //var plainTextBytes = Encoding.UTF8.GetBytes(createDraftRequest.DraftPreperationRequest.CreatedLocalDraftMimeMessage.ToString()); + //var base64Encoded = Convert.ToBase64String(plainTextBytes); - requestInformation.Headers.Clear();// replace the json content header - requestInformation.Headers.Add("Content-Type", "text/plain"); + //var requestInformation = _graphClient.Me.Messages.ToPostRequestInformation(new Message()); - requestInformation.SetStreamContent(new MemoryStream(Encoding.UTF8.GetBytes(base64Encoded)), "text/plain"); + //requestInformation.Headers.Clear();// replace the json content header + //requestInformation.Headers.Add("Content-Type", "text/plain"); - return requestInformation; + //requestInformation.SetStreamContent(new MemoryStream(Encoding.UTF8.GetBytes(base64Encoded)), "text/plain"); + + //return requestInformation; + } } - return default; + throw new Exception("Invalid create draft request type."); }); } @@ -646,16 +672,29 @@ namespace Wino.Core.Synchronizers // Alias support is lacking with direct MIMEs. // Therefore we convert the MIME message to Outlook message and use proper APIs. - var outlookMessage = mimeMessage.AsOutlookMessage(sendDraftPreparationRequest.MailItem.ThreadId); - var sendMailPostRequest = _graphClient.Me.SendMail.ToPostRequestInformation(new SendMailPostRequestBody() - { - Message = outlookMessage - }); + // sendDraftPreparationRequest.MailItem.ThreadId + var outlookMessage = mimeMessage.AsOutlookMessage(false); - var sendMailRequest = new HttpRequestBundle(sendMailPostRequest, request); + // Update draft. - return [deleteBundle, sendMailRequest]; + var patchDraftRequest = _graphClient.Me.Messages[mailCopyId].ToPatchRequestInformation(outlookMessage); + var patchDraftRequestBundle = new HttpRequestBundle(patchDraftRequest, request); + + // Send draft. + + var sendDraftRequest = _graphClient.Me.Messages[mailCopyId].Send.ToPostRequestInformation(); + var sendDraftRequestBundle = new HttpRequestBundle(sendDraftRequest, request); + + //var sendMailPostRequest = _graphClient.Me.SendMail.ToPostRequestInformation(new SendMailPostRequestBody() + //{ + // Message = outlookMessage + //}); + + //var sendMailRequest = new HttpRequestBundle(sendMailPostRequest, request); + + return [sendDraftRequestBundle]; + //return [deleteBundle, sendMailRequest]; } public override IEnumerable> Archive(BatchArchiveRequest request) @@ -712,17 +751,32 @@ namespace Wino.Core.Synchronizers request.ApplyUIChanges(); - await batchContent.AddBatchRequestStepAsync(nativeRequest).ConfigureAwait(false); + var batchRequestId = await batchContent.AddBatchRequestStepAsync(nativeRequest).ConfigureAwait(false); // Map BundleId to batch request step's key. // This is how we can identify which step succeeded or failed in the bundle. - bundle.BundleId = batchContent.BatchRequestSteps.ElementAt(i).Key; + bundle.BundleId = batchRequestId;//batchContent.BatchRequestSteps.ElementAt(i).Key; } if (!batchContent.BatchRequestSteps.Any()) continue; + // Set execution type to serial instead of parallel if needed. + // Each step will depend on the previous one. + + if (itemCount > 1) + { + for (int i = 1; i < itemCount; i++) + { + var currentStep = batchContent.BatchRequestSteps.ElementAt(i); + var previousStep = batchContent.BatchRequestSteps.ElementAt(i - 1); + + currentStep.Value.DependsOn = [previousStep.Key]; + } + + } + // Execute batch. This will collect responses from network call for each batch step. var batchRequestResponse = await _graphClient.Batch.PostAsync(batchContent).ConfigureAwait(false); @@ -742,6 +796,7 @@ namespace Wino.Core.Synchronizers var httpResponseMessage = await batchRequestResponse.GetResponseByIdAsync(bundleId); + var codes = await batchRequestResponse.GetResponsesStatusCodesAsync(); using (httpResponseMessage) { await ProcessSingleNativeRequestResponseAsync(bundle, httpResponseMessage, cancellationToken).ConfigureAwait(false); diff --git a/Wino.Mail.ViewModels/AppShellViewModel.cs b/Wino.Mail.ViewModels/AppShellViewModel.cs index 4a9ac7cd..0acc62da 100644 --- a/Wino.Mail.ViewModels/AppShellViewModel.cs +++ b/Wino.Mail.ViewModels/AppShellViewModel.cs @@ -788,7 +788,7 @@ namespace Wino.Mail.ViewModels var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(account.Id, draftOptions).ConfigureAwait(false); - var draftPreparationRequest = new DraftPreparationRequest(account, draftMailCopy, draftBase64MimeMessage); + var draftPreparationRequest = new DraftPreparationRequest(account, draftMailCopy, draftBase64MimeMessage, draftOptions.Reason); await _winoRequestDelegator.ExecuteAsync(draftPreparationRequest); } diff --git a/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs b/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs index 305a7ece..7fe96b2e 100644 --- a/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs +++ b/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs @@ -273,7 +273,7 @@ namespace Wino.Mail.ViewModels var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(initializedMailItemViewModel.AssignedAccount.Id, draftOptions).ConfigureAwait(false); - var draftPreparationRequest = new DraftPreparationRequest(initializedMailItemViewModel.AssignedAccount, draftMailCopy, draftBase64MimeMessage, initializedMailItemViewModel.MailCopy); + var draftPreparationRequest = new DraftPreparationRequest(initializedMailItemViewModel.AssignedAccount, draftMailCopy, draftBase64MimeMessage, draftOptions.Reason, initializedMailItemViewModel.MailCopy); await _requestDelegator.ExecuteAsync(draftPreparationRequest);