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."); }