Files
AI-Studio/app/MindWork AI Studio/Tools/Mail/MailSummaryReader.cs
T
Thorsten Sommer c4400c0ff6
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Verify (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Added mailboxes as local data sources (#1022)
2026-10-04 12:26:26 +02:00

139 lines
6.0 KiB
C#

using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using AIStudio.Tools.Databases.IndexStore;
using MailKit;
using MimeKit;
namespace AIStudio.Tools.Mail;
/// <summary>
/// Turns what the server reported about a mail into what AI Studio keeps of it.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public static class MailSummaryReader
{
/// <summary>
/// Reads what tells the mail apart from all others on the server, for MailContentKey.
/// </summary>
/// <param name="summary">The summary, fetched with its header block.</param>
/// <returns>The identity of the mail.</returns>
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);
}
/// <summary>
/// Puts together what MailTextBuilder reads a mail from.
/// </summary>
/// <param name="summary">The summary, fetched with its header block and its structure.</param>
/// <param name="textParts">The text parts of the mail, or null when they were not fetched because the mail is encrypted.</param>
/// <returns>The source for MailTextBuilder.</returns>
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));
}
/// <summary>
/// Reads which attachments a reader of the mail sees, from its structure.
/// </summary>
/// <param name="summary">The summary, fetched with its structure.</param>
/// <returns>The attachments in their order, without the signature of a signed mail, cf. MailAttachmentRules.IsSignature.</returns>
public static IReadOnlyList<BodyPartBasic> ReadAttachments(IMessageSummary summary) => summary.Attachments
.Where(attachment => !MailAttachmentRules.IsSignature(attachment.ContentType))
.ToList();
/// <summary>
/// Reads every address of the header block together with the header it comes from.
/// </summary>
/// <param name="headers">The header block.</param>
/// <returns>The addresses, grouped by their header in the order From, Sender, Reply-To, To, Cc, Bcc.</returns>
public static IReadOnlyList<MailAddressRecord> 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();
}
/// <summary>
/// Reads the flags AI Studio keeps of a mail.
/// </summary>
/// <param name="flags">The flags the server reported, or null when it reported none.</param>
/// <returns>The flags.</returns>
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));
}
/// <summary>
/// Writes the header block out the way the server delivered it, encoded words and all.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="headers">The header block.</param>
/// <returns>The header block as text.</returns>
public static string ReadHeaderBlock(HeaderList headers)
{
using var stream = new MemoryStream();
headers.WriteTo(FormatOptions.Default, stream);
return Encoding.UTF8.GetString(stream.ToArray());
}
/// <summary>
/// Hashes the mail as the server delivered it, independent of how AI Studio turns it into text.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="summary">The summary, fetched with its header block and its structure.</param>
/// <returns>The hash in hex.</returns>
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.");
}