Refactoring outlook draft creation and sending.

This commit is contained in:
Burak Kaan Köse
2024-08-19 03:44:16 +02:00
parent 3bb156f4da
commit 07bb90dda9
7 changed files with 115 additions and 39 deletions

View File

@@ -2,13 +2,18 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using MimeKit; using MimeKit;
using Wino.Core.Domain.Entities; using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Extensions; using Wino.Core.Domain.Extensions;
namespace Wino.Core.Domain.Models.MailItem; namespace Wino.Core.Domain.Models.MailItem;
public class DraftPreparationRequest 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)); 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. // This is additional work when deserialization needed, but not much to do atm.
Base64LocalDraftMimeMessage = base64EncodedMimeMessage; Base64LocalDraftMimeMessage = base64EncodedMimeMessage;
Reason = reason;
} }
[JsonConstructor] [JsonConstructor]
@@ -29,6 +35,7 @@ public class DraftPreparationRequest
public MailCopy ReferenceMailCopy { get; set; } public MailCopy ReferenceMailCopy { get; set; }
public string Base64LocalDraftMimeMessage { get; set; } public string Base64LocalDraftMimeMessage { get; set; }
public DraftCreationReason Reason { get; set; }
[JsonIgnore] [JsonIgnore]
private MimeMessage createdLocalDraftMimeMessage; private MimeMessage createdLocalDraftMimeMessage;
@@ -44,5 +51,5 @@ public class DraftPreparationRequest
} }
} }
public MailAccount Account { get; } public MailAccount Account { get; set; }
} }

View File

@@ -3,14 +3,11 @@ using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Domain.Models.Requests namespace Wino.Core.Domain.Models.Requests
{ {
/// <summary> /// <summary>
/// Encapsulates request to queue and account for synchronizer. /// Encapsulates request to queue and account for synchronizer.
/// </summary> /// </summary>
/// <param name="AccountId"><inheritdoc/></param> /// <param name="AccountId">Which account to execute this request for.</param>
/// <param name="Request"></param>
/// <param name="Request">Prepared request for the server.</param> /// <param name="Request">Prepared request for the server.</param>
/// <param name="AccountId">Whihc account to execute this request for.</param>
public record ServerRequestPackage(Guid AccountId, IRequestBase Request) : IClientMessage public record ServerRequestPackage(Guid AccountId, IRequestBase Request) : IClientMessage
{ {
public override string ToString() => $"Server Package: {Request.GetType().Name}"; public override string ToString() => $"Server Package: {Request.GetType().Name}";

View File

@@ -66,7 +66,7 @@ namespace Wino.Core.Extensions
return mailCopy; 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 fromAddress = GetRecipients(mime.From).ElementAt(0);
var toAddresses = GetRecipients(mime.To).ToList(); var toAddresses = GetRecipients(mime.To).ToList();
@@ -85,13 +85,19 @@ namespace Wino.Core.Extensions
CcRecipients = ccAddresses, CcRecipients = ccAddresses,
BccRecipients = bccAddresses, BccRecipients = bccAddresses,
From = fromAddress, From = fromAddress,
InternetMessageId = GetMessageIdHeader(mime.MessageId), InternetMessageId = GetProperId(mime.MessageId),
ConversationId = threadId,
InternetMessageHeaders = GetHeaderList(mime),
ReplyTo = replyToAddresses, ReplyTo = replyToAddresses,
Attachments = [] 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) foreach (var part in mime.BodyParts)
{ {
if (part.IsAttachment) if (part.IsAttachment)
@@ -172,7 +178,10 @@ namespace Wino.Core.Extensions
// Here we'll try to ignore some headers that are not neccessary. // Here we'll try to ignore some headers that are not neccessary.
// Outlook API will generate them automatically. // 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[] headersToIgnore = ["Date", "To", "MIME-Version", "From", "Subject", "Message-Id"];
string[] headersToModify = ["In-Reply-To", "Reply-To", "References", "Thread-Topic"];
var headers = new List<InternetMessageHeader>(); var headers = new List<InternetMessageHeader>();
@@ -182,7 +191,15 @@ namespace Wino.Core.Extensions
{ {
if (!headersToIgnore.Contains(header.Field)) 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++; includedHeaderCount++;
} }
@@ -192,15 +209,15 @@ namespace Wino.Core.Extensions
return headers; return headers;
} }
private static string GetMessageIdHeader(string messageId) private static string GetProperId(string id)
{ {
// Message-Id header must always start with "X-" or "x-". // Outlook requires some identifiers to start with "X-" or "x-".
if (string.IsNullOrEmpty(messageId)) return string.Empty; if (string.IsNullOrEmpty(id)) return string.Empty;
if (!messageId.StartsWith("x-") || !messageId.StartsWith("X-")) if (!id.StartsWith("x-") || !id.StartsWith("X-"))
return $"X-{messageId}"; return $"X-{id}";
return messageId; return id;
} }
#endregion #endregion
} }

View File

@@ -5,12 +5,10 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Graph; using Microsoft.Graph;
using Microsoft.Graph.Me.SendMail;
using Microsoft.Graph.Models; using Microsoft.Graph.Models;
using Microsoft.Kiota.Abstractions; using Microsoft.Kiota.Abstractions;
using Microsoft.Kiota.Abstractions.Authentication; using Microsoft.Kiota.Abstractions.Authentication;
@@ -604,22 +602,50 @@ namespace Wino.Core.Synchronizers
{ {
if (item is CreateDraftRequest createDraftRequest) 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()); if (reason == DraftCreationReason.Empty)
var base64Encoded = Convert.ToBase64String(plainTextBytes); {
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 //var requestInformation = _graphClient.Me.Messages.ToPostRequestInformation(new Message());
requestInformation.Headers.Add("Content-Type", "text/plain");
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. // Alias support is lacking with direct MIMEs.
// Therefore we convert the MIME message to Outlook message and use proper APIs. // 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() // sendDraftPreparationRequest.MailItem.ThreadId
{ var outlookMessage = mimeMessage.AsOutlookMessage(false);
Message = outlookMessage
});
var sendMailRequest = new HttpRequestBundle<RequestInformation>(sendMailPostRequest, request); // Update draft.
return [deleteBundle, sendMailRequest]; var patchDraftRequest = _graphClient.Me.Messages[mailCopyId].ToPatchRequestInformation(outlookMessage);
var patchDraftRequestBundle = new HttpRequestBundle<RequestInformation>(patchDraftRequest, request);
// Send draft.
var sendDraftRequest = _graphClient.Me.Messages[mailCopyId].Send.ToPostRequestInformation();
var sendDraftRequestBundle = new HttpRequestBundle<RequestInformation>(sendDraftRequest, request);
//var sendMailPostRequest = _graphClient.Me.SendMail.ToPostRequestInformation(new SendMailPostRequestBody()
//{
// Message = outlookMessage
//});
//var sendMailRequest = new HttpRequestBundle<RequestInformation>(sendMailPostRequest, request);
return [sendDraftRequestBundle];
//return [deleteBundle, sendMailRequest];
} }
public override IEnumerable<IRequestBundle<RequestInformation>> Archive(BatchArchiveRequest request) public override IEnumerable<IRequestBundle<RequestInformation>> Archive(BatchArchiveRequest request)
@@ -712,17 +751,32 @@ namespace Wino.Core.Synchronizers
request.ApplyUIChanges(); request.ApplyUIChanges();
await batchContent.AddBatchRequestStepAsync(nativeRequest).ConfigureAwait(false); var batchRequestId = await batchContent.AddBatchRequestStepAsync(nativeRequest).ConfigureAwait(false);
// Map BundleId to batch request step's key. // Map BundleId to batch request step's key.
// This is how we can identify which step succeeded or failed in the bundle. // 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()) if (!batchContent.BatchRequestSteps.Any())
continue; 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. // Execute batch. This will collect responses from network call for each batch step.
var batchRequestResponse = await _graphClient.Batch.PostAsync(batchContent).ConfigureAwait(false); var batchRequestResponse = await _graphClient.Batch.PostAsync(batchContent).ConfigureAwait(false);
@@ -742,6 +796,7 @@ namespace Wino.Core.Synchronizers
var httpResponseMessage = await batchRequestResponse.GetResponseByIdAsync(bundleId); var httpResponseMessage = await batchRequestResponse.GetResponseByIdAsync(bundleId);
var codes = await batchRequestResponse.GetResponsesStatusCodesAsync();
using (httpResponseMessage) using (httpResponseMessage)
{ {
await ProcessSingleNativeRequestResponseAsync(bundle, httpResponseMessage, cancellationToken).ConfigureAwait(false); await ProcessSingleNativeRequestResponseAsync(bundle, httpResponseMessage, cancellationToken).ConfigureAwait(false);

View File

@@ -788,7 +788,7 @@ namespace Wino.Mail.ViewModels
var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(account.Id, draftOptions).ConfigureAwait(false); 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); await _winoRequestDelegator.ExecuteAsync(draftPreparationRequest);
} }

View File

@@ -273,7 +273,7 @@ namespace Wino.Mail.ViewModels
var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(initializedMailItemViewModel.AssignedAccount.Id, draftOptions).ConfigureAwait(false); 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); await _requestDelegator.ExecuteAsync(draftPreparationRequest);