Fixing outlook attachments, re-using compose page and some additional fixes on the mime headers for outlook.

This commit is contained in:
Burak Kaan Köse
2026-02-07 13:10:57 +01:00
parent 1ec8d5bbf2
commit d28de50ec6
10 changed files with 234 additions and 145 deletions
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.Graph.Models;
@@ -138,37 +139,38 @@ public static class OutlookIntegratorExtensions
var bccAddresses = GetRecipients(mime.Bcc).ToList();
var replyToAddresses = GetRecipients(mime.ReplyTo).ToList();
// Prefer HTML body, fall back to plain text.
var (bodyContent, bodyType) = mime.HtmlBody != null
? (mime.HtmlBody, BodyType.Html)
: (mime.TextBody ?? string.Empty, BodyType.Text);
var message = new Message()
{
Subject = mime.Subject,
Importance = GetImportance(mime.Importance),
Body = new ItemBody() { ContentType = BodyType.Html, Content = mime.HtmlBody },
Body = new ItemBody() { ContentType = bodyType, Content = bodyContent },
IsDraft = false,
IsRead = true, // Sent messages are always read.
IsRead = true,
ToRecipients = toAddresses,
CcRecipients = ccAddresses,
BccRecipients = bccAddresses,
From = fromAddress,
InternetMessageId = mime.MessageId,
ReplyTo = replyToAddresses,
Attachments = []
};
// Set ConversationId if provided to maintain threading
if (!string.IsNullOrEmpty(conversationId))
{
message.ConversationId = conversationId;
}
// Headers are only included when creating the draft.
// When sending, they are not included. Graph will throw an error.
// Graph API throws an error if headers are included in send/patch operations.
if (includeInternetHeaders)
{
message.InternetMessageHeaders = GetHeaderList(mime);
}
return message;
}
@@ -367,6 +369,40 @@ public static class OutlookIntegratorExtensions
#region Mime to Outlook Message Helpers
/// <summary>
/// Extracts all attachments (inline and regular) from a MimeMessage
/// and returns them as Graph SDK FileAttachment objects.
/// </summary>
public static List<FileAttachment> ExtractAttachments(this MimeMessage mime)
{
var attachments = new List<FileAttachment>();
foreach (var part in mime.BodyParts)
{
bool isInline = part.ContentDisposition?.Disposition == "inline";
if (!part.IsAttachment && !isInline)
continue;
if (part is not MimePart mimePart || mimePart.Content == null)
continue;
using var memory = new MemoryStream();
mimePart.Content.DecodeTo(memory);
attachments.Add(new FileAttachment()
{
Name = part.ContentDisposition?.FileName ?? part.ContentType.Name,
ContentBytes = memory.ToArray(),
ContentType = part.ContentType.MimeType,
ContentId = part.ContentId,
IsInline = isInline
});
}
return attachments;
}
private static IEnumerable<Recipient> GetRecipients(this InternetAddressList internetAddresses)
{
foreach (var address in internetAddresses)
@@ -399,46 +435,46 @@ public static class OutlookIntegratorExtensions
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.
// Some headers also require to start with X- or x-.
// Prioritize threading headers to keep reply grouping intact.
const int headerLimit = 5;
string[] headersToIgnore = ["Date", "To", "Cc", "Bcc", "MIME-Version", "From", "Subject", "Message-Id"];
string[] headersToModify = ["In-Reply-To", "Reply-To", "References", "Thread-Topic"];
var headers = new List<InternetMessageHeader>();
// PRIORITY: Always include WinoLocalDraftHeader first if it exists
// This is critical for draft mapping functionality
var winoDraftHeader = mime.Headers.FirstOrDefault(h => h.Field == Domain.Constants.WinoLocalDraftHeader);
int includedHeaderCount = 0;
if (winoDraftHeader != null)
void AddHeader(string name, string value)
{
var headerValue = winoDraftHeader.Value.Length >= 995 ? winoDraftHeader.Value.Substring(0, 995) : winoDraftHeader.Value;
headers.Add(new InternetMessageHeader() { Name = winoDraftHeader.Field, Value = headerValue });
includedHeaderCount++;
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(value)) return;
if (headers.Count >= headerLimit) return;
if (headers.Any(h => string.Equals(h.Name, name, StringComparison.OrdinalIgnoreCase))) return;
// No header value should exceed 995 characters.
var headerValue = value.Length >= 995 ? value.Substring(0, 995) : value;
headers.Add(new InternetMessageHeader() { Name = name, Value = headerValue });
}
// Include other headers up to the limit (excluding the already added WinoLocalDraftHeader)
// PRIORITY: Always include WinoLocalDraftHeader first if it exists.
var winoDraftHeader = mime.Headers.FirstOrDefault(h => h.Field == Domain.Constants.WinoLocalDraftHeader);
if (winoDraftHeader != null)
AddHeader(winoDraftHeader.Field, winoDraftHeader.Value);
// Threading headers must be preserved with their real RFC names.
AddHeader("In-Reply-To", mime.Headers[HeaderId.InReplyTo]);
AddHeader("References", mime.Headers[HeaderId.References]);
// Fill remaining slots with custom headers only (avoid Graph restrictions).
foreach (var header in mime.Headers)
{
if (header.Field == Domain.Constants.WinoLocalDraftHeader)
continue; // Already processed above
if (headers.Count >= headerLimit) break;
if (header.Field == Domain.Constants.WinoLocalDraftHeader) continue;
if (headersToIgnore.Contains(header.Field)) continue;
if (string.Equals(header.Field, "In-Reply-To", StringComparison.OrdinalIgnoreCase)) continue;
if (string.Equals(header.Field, "References", StringComparison.OrdinalIgnoreCase)) continue;
if (!headersToIgnore.Contains(header.Field))
{
var headerName = headersToModify.Contains(header.Field) ? $"X-{header.Field}" : header.Field;
// Only include custom headers beyond the core threading ones.
if (!header.Field.StartsWith("X-", StringComparison.OrdinalIgnoreCase)) continue;
// No header value should exceed 995 characters.
var headerValue = header.Value.Length >= 995 ? header.Value.Substring(0, 995) : header.Value;
headers.Add(new InternetMessageHeader() { Name = headerName, Value = headerValue });
includedHeaderCount++;
}
if (includedHeaderCount >= 5) break;
AddHeader(header.Field, header.Value);
}
return headers;