Files
Wino-Mail/Wino.Services/Extensions/MailkitClientExtensions.cs
T
2026-02-14 12:52:17 +01:00

262 lines
9.8 KiB
C#

using System;
using System.Linq;
using MailKit;
using MimeKit;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums;
namespace Wino.Services.Extensions;
public static class MailkitClientExtensions
{
public static char MailCopyUidSeparator = '_';
public static uint ResolveUid(string mailCopyId)
{
var splitted = mailCopyId.Split(MailCopyUidSeparator);
if (splitted.Length > 1 && uint.TryParse(splitted[1], out uint parsedUint)) return parsedUint;
throw new ArgumentOutOfRangeException(nameof(mailCopyId), mailCopyId, "Invalid mailCopyId format.");
}
public static UniqueId ResolveUidStruct(string mailCopyId)
=> new UniqueId(ResolveUid(mailCopyId));
public static string CreateUid(Guid folderId, uint messageUid)
=> $"{folderId}{MailCopyUidSeparator}{messageUid}";
public static MailImportance GetImportance(this MimeMessage messageSummary)
{
if (messageSummary.Headers != null && messageSummary.Headers.Contains(HeaderId.Importance))
{
var rawImportance = messageSummary.Headers[HeaderId.Importance];
return rawImportance switch
{
"Low" => MailImportance.Low,
"High" => MailImportance.High,
_ => MailImportance.Normal,
};
}
return MailImportance.Normal;
}
public static bool GetIsRead(this MessageFlags? flags)
=> flags.GetValueOrDefault().HasFlag(MessageFlags.Seen);
public static bool GetIsFlagged(this MessageFlags? flags)
=> flags.GetValueOrDefault().HasFlag(MessageFlags.Flagged);
public static string GetThreadId(this IMessageSummary messageSummary)
{
// First check whether we have the default values.
if (!string.IsNullOrEmpty(messageSummary.ThreadId))
return messageSummary.ThreadId;
if (messageSummary.GMailThreadId != null)
return messageSummary.GMailThreadId.ToString();
return default;
}
public static string GetMessageId(this MimeMessage mimeMessage)
=> mimeMessage.MessageId;
public static string GetReferences(this MessageIdList messageIdList)
=> string.Join(";", messageIdList);
public static string GetInReplyTo(this MimeMessage mimeMessage)
{
if (mimeMessage.Headers.Contains(HeaderId.InReplyTo))
{
// Normalize if <> brackets are there.
var inReplyTo = mimeMessage.Headers[HeaderId.InReplyTo];
if (inReplyTo.StartsWith("<") && inReplyTo.EndsWith(">"))
return inReplyTo.Substring(1, inReplyTo.Length - 2);
return inReplyTo;
}
return string.Empty;
}
private static string GetPreviewText(this MimeMessage message)
{
if (string.IsNullOrEmpty(message.HtmlBody))
return message.TextBody;
else
return HtmlAgilityPackExtensions.GetPreviewText(message.HtmlBody);
}
public static MailCopy GetMailDetails(this IMessageSummary messageSummary, MailItemFolder folder, MimeMessage mime = null)
{
// IMAP UIDs are unique only within a folder.
// MailCopy.Id maps to {FolderId}_{UID} for deterministic folder-local identity.
var envelope = messageSummary.Envelope;
var messageUid = CreateUid(folder.Id, messageSummary.UniqueId.Id);
var subject = mime?.Subject ?? envelope?.Subject ?? string.Empty;
var previewText = mime != null ? mime.GetPreviewText() : GetPreviewText(messageSummary, subject);
// Prefer InternalDate (server received time). Fall back to envelope date and finally UTC now.
var creationDate = messageSummary.InternalDate?.UtcDateTime
?? envelope?.Date?.UtcDateTime
?? DateTime.UtcNow;
var messageId = mime?.GetMessageId() ?? envelope?.MessageId ?? string.Empty;
var fromName = mime != null ? GetActualSenderName(mime) : GetEnvelopeSenderName(envelope);
var fromAddress = mime != null ? GetActualSenderAddress(mime) : GetEnvelopeSenderAddress(envelope);
var references = mime?.References?.GetReferences() ?? messageSummary.References?.GetReferences();
var inReplyTo = mime != null ? mime.GetInReplyTo() : envelope?.InReplyTo ?? string.Empty;
var hasAttachments = mime != null ? mime.Attachments.Any() : false;
var itemType = mime != null ? GetMailItemTypeFromMime(mime) : MailItemType.Mail;
var copy = new MailCopy()
{
Id = messageUid,
CreationDate = creationDate,
ThreadId = messageSummary.GetThreadId(),
MessageId = messageId,
Subject = subject,
IsRead = messageSummary.Flags.GetIsRead(),
IsFlagged = messageSummary.Flags.GetIsFlagged(),
PreviewText = previewText,
FromAddress = fromAddress,
FromName = fromName,
IsFocused = false,
Importance = mime != null ? mime.GetImportance() : MailImportance.Normal,
References = references,
InReplyTo = inReplyTo,
HasAttachments = hasAttachments,
FileId = Guid.NewGuid(),
ItemType = itemType
};
return copy;
}
private static string GetPreviewText(IMessageSummary messageSummary, string subjectFallback)
{
if (!string.IsNullOrWhiteSpace(messageSummary.PreviewText))
return messageSummary.PreviewText;
return subjectFallback ?? string.Empty;
}
private static string GetEnvelopeSenderName(Envelope envelope)
{
var mailbox = envelope?.From?.Mailboxes?.FirstOrDefault() ?? envelope?.Sender?.Mailboxes?.FirstOrDefault();
if (mailbox == null)
return Translator.UnknownSender;
return string.IsNullOrWhiteSpace(mailbox.Name) ? mailbox.Address : mailbox.Name;
}
private static string GetEnvelopeSenderAddress(Envelope envelope)
{
var mailbox = envelope?.From?.Mailboxes?.FirstOrDefault() ?? envelope?.Sender?.Mailboxes?.FirstOrDefault();
return mailbox?.Address ?? Translator.UnknownSender;
}
/// <summary>
/// Determines MailItemType based on MIME message content type.
/// Calendar invitations have text/calendar content type with METHOD parameter.
/// </summary>
private static MailItemType GetMailItemTypeFromMime(MimeMessage mime)
{
if (mime == null) return MailItemType.Mail;
// Check if the message contains text/calendar content
var calendarPart = mime.BodyParts.OfType<MimePart>()
.FirstOrDefault(p => p.ContentType?.MimeType?.Equals("text/calendar", StringComparison.OrdinalIgnoreCase) == true);
if (calendarPart != null)
{
// Check the METHOD parameter to determine invitation type
var method = calendarPart.ContentType.Parameters
.FirstOrDefault(p => p.Name.Equals("method", StringComparison.OrdinalIgnoreCase))?.Value?.ToUpperInvariant();
if (!string.IsNullOrEmpty(method))
{
return method switch
{
"REQUEST" => MailItemType.CalendarInvitation,
"CANCEL" => MailItemType.CalendarCancellation,
"REPLY" => MailItemType.CalendarResponse,
_ => MailItemType.Mail
};
}
// If no method specified, assume it's an invitation
return MailItemType.CalendarInvitation;
}
return MailItemType.Mail;
}
// TODO: Name and Address parsing should be handled better.
// At some point Wino needs better contact management.
public static string GetActualSenderName(MimeMessage message)
{
if (message == null)
return string.Empty;
return message.From.Mailboxes.FirstOrDefault()?.Name ?? message.Sender?.Name ?? Translator.UnknownSender;
// From MimeKit
// The "From" header specifies the author(s) of the message.
// If more than one MimeKit.MailboxAddress is added to the list of "From" addresses,
// the MimeKit.MimeMessage.Sender should be set to the single MimeKit.MailboxAddress
// of the personal actually sending the message.
// Also handle: https://stackoverflow.com/questions/46474030/mailkit-from-address
//if (message.Sender != null)
// return string.IsNullOrEmpty(message.Sender.Name) ? message.Sender.Address : message.Sender.Name;
//else if (message.From?.Mailboxes != null)
//{
// var firstAvailableName = message.From.Mailboxes.FirstOrDefault(a => !string.IsNullOrEmpty(a.Name))?.Name;
// if (string.IsNullOrEmpty(firstAvailableName))
// {
// var firstAvailableAddress = message.From.Mailboxes.FirstOrDefault(a => !string.IsNullOrEmpty(a.Address))?.Address;
// if (!string.IsNullOrEmpty(firstAvailableAddress))
// {
// return firstAvailableAddress;
// }
// }
// return firstAvailableName;
//}
//// No sender, no from, I don't know what to do.
//return Translator.UnknownSender;
}
// TODO: This is wrong.
public static string GetActualSenderAddress(MimeMessage message)
{
return message.From.Mailboxes.FirstOrDefault()?.Address ?? message.Sender?.Address ?? Translator.UnknownSender;
//if (mime == null)
// return string.Empty;
//bool hasSingleFromMailbox = mime.From.Mailboxes.Count() == 1;
//if (hasSingleFromMailbox)
// return mime.From.Mailboxes.First().GetAddress(idnEncode: true);
//else if (mime.Sender != null)
// return mime.Sender.GetAddress(idnEncode: true);
//else
// return Translator.UnknownSender;
}
}