mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-05 08:29:40 +00:00
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
139 lines
6.0 KiB
C#
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.");
|
|
} |