Handling of OutlookSynchronizer alias.

This commit is contained in:
Burak Kaan Köse
2024-08-18 22:45:23 +02:00
parent e13e0efcc6
commit 3bb156f4da
11 changed files with 578 additions and 366 deletions

View File

@@ -28,7 +28,7 @@ namespace Wino.Core.Authenticators
public string ClientId { get; } = "b19c2035-d740-49ff-b297-de6ec561b208";
private readonly string[] MailScope = ["email", "mail.readwrite", "offline_access", "mail.send"];
private readonly string[] MailScope = ["email", "mail.readwrite", "offline_access", "mail.send", "Mail.Send.Shared", "Mail.ReadWrite.Shared"];
public override MailProviderType ProviderType => MailProviderType.Outlook;

View File

@@ -1,5 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Graph.Models;
using MimeKit;
using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums;
@@ -61,5 +65,143 @@ namespace Wino.Core.Extensions
return mailCopy;
}
public static Message AsOutlookMessage(this MimeMessage mime, string threadId)
{
var fromAddress = GetRecipients(mime.From).ElementAt(0);
var toAddresses = GetRecipients(mime.To).ToList();
var ccAddresses = GetRecipients(mime.Cc).ToList();
var bccAddresses = GetRecipients(mime.Bcc).ToList();
var replyToAddresses = GetRecipients(mime.ReplyTo).ToList();
var message = new Message()
{
Subject = mime.Subject,
Importance = GetImportance(mime.Importance),
Body = new ItemBody() { ContentType = BodyType.Html, Content = mime.HtmlBody },
IsDraft = false,
IsRead = true, // Sent messages are always read.
ToRecipients = toAddresses,
CcRecipients = ccAddresses,
BccRecipients = bccAddresses,
From = fromAddress,
InternetMessageId = GetMessageIdHeader(mime.MessageId),
ConversationId = threadId,
InternetMessageHeaders = GetHeaderList(mime),
ReplyTo = replyToAddresses,
Attachments = []
};
foreach (var part in mime.BodyParts)
{
if (part.IsAttachment)
{
// File attachment.
using var memory = new MemoryStream();
((MimePart)part).Content.DecodeTo(memory);
var bytes = memory.ToArray();
var fileAttachment = new FileAttachment()
{
ContentId = part.ContentId,
Name = part.ContentDisposition?.FileName ?? part.ContentType.Name,
ContentBytes = bytes,
};
message.Attachments.Add(fileAttachment);
}
else if (part.ContentDisposition != null && part.ContentDisposition.Disposition == "inline")
{
// Inline attachment.
using var memory = new MemoryStream();
((MimePart)part).Content.DecodeTo(memory);
var bytes = memory.ToArray();
var inlineAttachment = new FileAttachment()
{
IsInline = true,
ContentId = part.ContentId,
Name = part.ContentDisposition?.FileName ?? part.ContentType.Name,
ContentBytes = bytes
};
message.Attachments.Add(inlineAttachment);
}
}
return message;
}
#region Mime to Outlook Message Helpers
private static IEnumerable<Recipient> GetRecipients(this InternetAddressList internetAddresses)
{
foreach (var address in internetAddresses)
{
if (address is MailboxAddress mailboxAddress)
yield return new Recipient() { EmailAddress = new EmailAddress() { Address = mailboxAddress.Address, Name = mailboxAddress.Name } };
else if (address is GroupAddress groupAddress)
{
// TODO: Group addresses are not directly supported.
// It'll be individually added.
foreach (var mailbox in groupAddress.Members)
if (mailbox is MailboxAddress groupMemberMailAddress)
yield return new Recipient() { EmailAddress = new EmailAddress() { Address = groupMemberMailAddress.Address, Name = groupMemberMailAddress.Name } };
}
}
}
private static Importance? GetImportance(MessageImportance importance)
{
return importance switch
{
MessageImportance.Low => Importance.Low,
MessageImportance.Normal => Importance.Normal,
MessageImportance.High => Importance.High,
_ => null
};
}
private static List<InternetMessageHeader> GetHeaderList(this MimeMessage mime)
{
// Graph API only allows max of 5 headers.
// Here we'll try to ignore some headers that are not neccessary.
// Outlook API will generate them automatically.
string[] headersToIgnore = ["Date", "To", "MIME-Version", "From", "Subject", "Message-Id"];
var headers = new List<InternetMessageHeader>();
int includedHeaderCount = 0;
foreach (var header in mime.Headers)
{
if (!headersToIgnore.Contains(header.Field))
{
headers.Add(new InternetMessageHeader() { Name = header.Field, Value = header.Value });
includedHeaderCount++;
}
if (includedHeaderCount >= 5) break;
}
return headers;
}
private static string GetMessageIdHeader(string messageId)
{
// Message-Id header must always start with "X-" or "x-".
if (string.IsNullOrEmpty(messageId)) return string.Empty;
if (!messageId.StartsWith("x-") || !messageId.StartsWith("X-"))
return $"X-{messageId}";
return messageId;
}
#endregion
}
}

View File

@@ -109,12 +109,9 @@ namespace Wino.Core.Services
var resourcePath = await GetMimeResourcePathAsync(accountId, fileId).ConfigureAwait(false);
var completeFilePath = GetEMLPath(resourcePath);
var fileStream = File.Create(completeFilePath);
using var fileStream = File.Open(completeFilePath, FileMode.OpenOrCreate);
using (fileStream)
{
await mimeMessage.WriteToAsync(fileStream).ConfigureAwait(false);
}
await mimeMessage.WriteToAsync(fileStream).ConfigureAwait(false);
return true;
}

View File

@@ -10,6 +10,7 @@ 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;
@@ -17,8 +18,8 @@ using Microsoft.Kiota.Http.HttpClientLibrary.Middleware;
using Microsoft.Kiota.Http.HttpClientLibrary.Middleware.Options;
using MimeKit;
using MoreLinq.Extensions;
using Newtonsoft.Json.Linq;
using Serilog;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions;
@@ -633,41 +634,26 @@ namespace Wino.Core.Synchronizers
var mailCopyId = sendDraftPreparationRequest.MailItem.Id;
var mimeMessage = sendDraftPreparationRequest.Mime;
var batchDeleteRequest = new BatchDeleteRequest(new List<IRequest>()
{
var batchDeleteRequest = new BatchDeleteRequest(
[
new DeleteRequest(sendDraftPreparationRequest.MailItem)
});
]);
var deleteBundle = Delete(batchDeleteRequest).ElementAt(0);
mimeMessage.Prepare(EncodingConstraint.None);
// Convert mime message to Outlook message.
// Outlook synchronizer does not send MIME messages directly anymore.
// Alias support is lacking with direct MIMEs.
// Therefore we convert the MIME message to Outlook message and use proper APIs.
var plainTextBytes = Encoding.UTF8.GetBytes(mimeMessage.ToString());
var base64Encoded = Convert.ToBase64String(plainTextBytes);
var outlookMessage = mimeMessage.AsOutlookMessage(sendDraftPreparationRequest.MailItem.ThreadId);
var outlookMessage = new Message()
{
ConversationId = sendDraftPreparationRequest.MailItem.ThreadId
};
// Apply importance here as well just in case.
if (mimeMessage.Importance != MessageImportance.Normal)
outlookMessage.Importance = mimeMessage.Importance == MessageImportance.High ? Importance.High : Importance.Low;
var body = new Microsoft.Graph.Me.SendMail.SendMailPostRequestBody()
var sendMailPostRequest = _graphClient.Me.SendMail.ToPostRequestInformation(new SendMailPostRequestBody()
{
Message = outlookMessage
};
});
var sendRequest = _graphClient.Me.SendMail.ToPostRequestInformation(body);
sendRequest.Headers.Clear();
sendRequest.Headers.Add("Content-Type", "text/plain");
var stream = new MemoryStream(Encoding.UTF8.GetBytes(base64Encoded));
sendRequest.SetStreamContent(stream, "text/plain");
var sendMailRequest = new HttpRequestBundle<RequestInformation>(sendRequest, request);
var sendMailRequest = new HttpRequestBundle<RequestInformation>(sendMailPostRequest, request);
return [deleteBundle, sendMailRequest];
}
@@ -675,8 +661,6 @@ namespace Wino.Core.Synchronizers
public override IEnumerable<IRequestBundle<RequestInformation>> Archive(BatchArchiveRequest request)
=> Move(new BatchMoveRequest(request.Items, request.FromFolder, request.ToFolder));
public override async Task DownloadMissingMimeMessageAsync(IMailItem mailItem,
MailKit.ITransferProgress transferProgress = null,
CancellationToken cancellationToken = default)
@@ -774,7 +758,10 @@ namespace Wino.Core.Synchronizers
if (!httpResponseMessage.IsSuccessStatusCode)
{
throw new SynchronizerException(string.Format(Translator.Exception_SynchronizerFailureHTTP, httpResponseMessage.StatusCode));
var content = await httpResponseMessage.Content.ReadAsStringAsync();
var errorJson = JObject.Parse(content);
throw new SynchronizerException($"({httpResponseMessage.StatusCode}) {errorJson["error"]["code"]} - {errorJson["error"]["message"]}");
}
else if (bundle is HttpRequestBundle<RequestInformation, Message> messageBundle)
{