using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using AIStudio.Tools.Databases.IndexStore;
using MailKit;
using MimeKit;
namespace AIStudio.Tools.Mail;
///
/// Turns what the server reported about a mail into what AI Studio keeps of it.
///
///
/// Everything is read from the header block the sync fetches with every mail, not from the
/// ENVELOPE of IMAP: a server fills in Sender and Reply-To from From when the mail has neither, and
/// the trust check of a later step has to know whether the sender set them.
///
public static class MailSummaryReader
{
///
/// Reads what tells the mail apart from all others on the server, for MailContentKey.
///
/// The summary, fetched with its header block.
/// The identity of the mail.
public static MailIdentity ReadIdentity(IMessageSummary summary)
{
var headers = GetHeaders(summary);
return new(
summary.EmailId,
summary.GMailMessageId,
headers[HeaderId.MessageId],
MailHeaders.ReadDate(headers),
MailHeaders.ReadMailboxes(headers, HeaderId.From).Select(mailbox => mailbox.Address).ToList(),
headers[HeaderId.Subject],
summary.Size ?? 0);
}
///
/// Puts together what MailTextBuilder reads a mail from.
///
/// The summary, fetched with its header block and its structure.
/// The text parts of the mail, or null when they were not fetched because the mail is encrypted.
/// The source for MailTextBuilder.
public static MailTextSource ReadTextSource(IMessageSummary summary, MailTextParts? textParts)
{
var attachmentNames = ReadAttachments(summary)
.Select(attachment => attachment.FileName ?? string.Empty)
.Where(name => !string.IsNullOrWhiteSpace(name))
.ToList();
return new(GetHeaders(summary), textParts?.HtmlBody, textParts?.TextBody, attachmentNames, MailEncryptionDetection.Detect(summary.Body));
}
///
/// Reads which attachments a reader of the mail sees, from its structure.
///
/// The summary, fetched with its structure.
/// The attachments in their order, without the signature of a signed mail, cf. MailAttachmentRules.IsSignature.
public static IReadOnlyList ReadAttachments(IMessageSummary summary) => summary.Attachments
.Where(attachment => !MailAttachmentRules.IsSignature(attachment.ContentType))
.ToList();
///
/// Reads every address of the header block together with the header it comes from.
///
/// The header block.
/// The addresses, grouped by their header in the order From, Sender, Reply-To, To, Cc, Bcc.
public static IReadOnlyList ReadAddresses(HeaderList headers)
{
(HeaderId HeaderId, MailAddressRole Role)[] roles =
[
(HeaderId.From, MailAddressRole.FROM),
(HeaderId.Sender, MailAddressRole.SENDER),
(HeaderId.ReplyTo, MailAddressRole.REPLY_TO),
(HeaderId.To, MailAddressRole.TO),
(HeaderId.Cc, MailAddressRole.CC),
(HeaderId.Bcc, MailAddressRole.BCC),
];
return roles
.SelectMany(role => MailHeaders.ReadMailboxes(headers, role.HeaderId).Select(mailbox => new MailAddressRecord(
role.Role,
MailTextNormalization.NormalizeHeaderValue(mailbox.Address),
MailTextNormalization.NormalizeHeaderValue(mailbox.Name))))
.Where(address => address.Address.Length > 0)
.ToList();
}
///
/// Reads the flags AI Studio keeps of a mail.
///
/// The flags the server reported, or null when it reported none.
/// The flags.
public static MailFlags ReadFlags(MessageFlags? flags)
{
var value = flags ?? MessageFlags.None;
return new(value.HasFlag(MessageFlags.Seen), value.HasFlag(MessageFlags.Flagged), value.HasFlag(MessageFlags.Answered));
}
///
/// Writes the header block out the way the server delivered it, encoded words and all.
///
///
/// This is what the trust check reads, and it needs the header block as it is: the decoded
/// text of a header hides whether it was encoded, and how.
///
/// The header block.
/// The header block as text.
public static string ReadHeaderBlock(HeaderList headers)
{
using var stream = new MemoryStream();
headers.WriteTo(FormatOptions.Default, stream);
return Encoding.UTF8.GetString(stream.ToArray());
}
///
/// Hashes the mail as the server delivered it, independent of how AI Studio turns it into text.
///
///
/// A mail on an IMAP server never changes, so its header block, its structure and its size are
/// enough to tell it from any other mail, without fetching the whole of it.
///
/// The summary, fetched with its header block and its structure.
/// The hash in hex.
public static string ComputeMailHash(IMessageSummary summary)
{
var source = string.Join('\n',
ReadHeaderBlock(GetHeaders(summary)),
summary.Body?.ToString() ?? string.Empty,
(summary.Size ?? 0).ToString(CultureInfo.InvariantCulture));
return Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(source)));
}
private static HeaderList GetHeaders(IMessageSummary summary) => summary.Headers ?? throw new InvalidOperationException("The mail was fetched without its header block.");
}