Refactoring outlook draft creation and sending.
This commit is contained in:
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}";
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user