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 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; }
}

View File

@@ -3,14 +3,11 @@ using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Domain.Models.Requests
{
/// <summary>
/// Encapsulates request to queue and account for synchronizer.
/// </summary>
/// <param name="AccountId"><inheritdoc/></param>
/// <param name="Request"></param>
/// <param name="AccountId">Which account to execute this request for.</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 override string ToString() => $"Server Package: {Request.GetType().Name}";

View File

@@ -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<InternetMessageHeader>();
@@ -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
}

View File

@@ -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<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)
@@ -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);

View File

@@ -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);
}

View File

@@ -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);