mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-05 23: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
140 lines
6.9 KiB
C#
140 lines
6.9 KiB
C#
using System.Globalization;
|
|
using System.Text;
|
|
|
|
using MimeKit;
|
|
|
|
namespace AIStudio.Tools.Mail;
|
|
|
|
/// <summary>
|
|
/// Turns a mail into the text AI Studio indexes: a header block, then the text a reader of the mail sees.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The text comes from the HTML part whenever there is one. It is what the reader sees, while
|
|
/// the plain text part is only what the sender's program claims the mail says, and the two may
|
|
/// say different things. The plain text part is read when there is no HTML part, or when nothing
|
|
/// visible remains of it. See MailHtmlConverter for what the HTML loses on the way.
|
|
///
|
|
/// An encrypted mail keeps its header block alone, together with a line saying that and how it is
|
|
/// encrypted. Nothing of the encrypted content gets in, neither its blob nor the names of its
|
|
/// attachments, and a model can tell the user why it does not know what the mail says.
|
|
///
|
|
/// The header block is written in English whatever the language of the app, since it is data for
|
|
/// the index and the model, like the field names of a tool result. All of it comes from the
|
|
/// sender and goes through the prompt injection filter just as the body does.
|
|
///
|
|
/// The folder is left out of the header block on purpose. A mail which moves to another folder is
|
|
/// not embedded again, so a folder written into its text would soon name the wrong one. Where a
|
|
/// mail lies is known from its locations in the index instead.
|
|
/// </remarks>
|
|
public static class MailTextBuilder
|
|
{
|
|
/// <summary>
|
|
/// How many addresses of one header are named before the rest is only counted. A mail to a
|
|
/// whole department would fill the first chunk with addresses otherwise.
|
|
/// </summary>
|
|
private const int MAX_LISTED_ADDRESSES = 10;
|
|
|
|
/// <summary>
|
|
/// How many attachments are named before the rest is only counted.
|
|
/// </summary>
|
|
private const int MAX_LISTED_ATTACHMENTS = 20;
|
|
|
|
/// <summary>
|
|
/// Builds the text of one mail.
|
|
/// </summary>
|
|
/// <param name="source">The parts of the mail.</param>
|
|
/// <returns>The text of the mail.</returns>
|
|
public static MailText Build(MailTextSource source)
|
|
{
|
|
var encryptionKind = source.StructureEncryption;
|
|
if (encryptionKind is MailEncryptionKind.NONE && (MailEncryptionDetection.ContainsInlinePgpMessage(source.HtmlBody) || MailEncryptionDetection.ContainsInlinePgpMessage(source.TextBody)))
|
|
encryptionKind = MailEncryptionKind.PGP_INLINE;
|
|
|
|
var isReadable = encryptionKind is MailEncryptionKind.NONE;
|
|
var importance = MailImportanceDetection.Detect(source.Headers);
|
|
var subject = MailTextNormalization.NormalizeHeaderValue(source.Headers[HeaderId.Subject]);
|
|
IReadOnlyList<string> attachmentNames = isReadable ? source.AttachmentNames : [];
|
|
|
|
var headerBlock = BuildHeaderBlock(source.Headers, subject, importance, attachmentNames, encryptionKind);
|
|
var (body, bodySource) = isReadable ? ReadBody(source) : (string.Empty, MailBodySource.NONE);
|
|
return new(headerBlock, body, subject, encryptionKind, importance, bodySource);
|
|
}
|
|
|
|
private static (string Body, MailBodySource Source) ReadBody(MailTextSource source)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(source.HtmlBody))
|
|
{
|
|
var markdown = MailHtmlConverter.ToMarkdown(source.HtmlBody);
|
|
if (markdown.Length > 0)
|
|
return (markdown, MailBodySource.HTML);
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(source.TextBody))
|
|
return (string.Empty, MailBodySource.NONE);
|
|
|
|
var text = MailTextNormalization.NormalizeBody(source.TextBody).Trim();
|
|
return text.Length > 0 ? (text, MailBodySource.PLAIN_TEXT) : (string.Empty, MailBodySource.NONE);
|
|
}
|
|
|
|
private static string BuildHeaderBlock(HeaderList headers, string subject, MailImportance importance, IReadOnlyList<string> attachmentNames, MailEncryptionKind encryptionKind)
|
|
{
|
|
var block = new StringBuilder();
|
|
AppendLine(block, "From", FormatAddresses(headers, HeaderId.From));
|
|
AppendLine(block, "To", FormatAddresses(headers, HeaderId.To));
|
|
AppendLine(block, "Cc", FormatAddresses(headers, HeaderId.Cc));
|
|
AppendLine(block, "Subject", subject);
|
|
AppendLine(block, "Date", MailHeaders.ReadDate(headers)?.ToString("yyyy-MM-dd HH:mm zzz", CultureInfo.InvariantCulture) ?? string.Empty);
|
|
AppendLine(block, "Importance", importance switch
|
|
{
|
|
MailImportance.HIGH => "high",
|
|
MailImportance.LOW => "low",
|
|
|
|
// Normal is what nearly every mail is, so it is not worth a line:
|
|
_ => string.Empty,
|
|
});
|
|
AppendLine(block, "Attachments", FormatList(attachmentNames.Select(MailTextNormalization.NormalizeHeaderValue).Where(name => name.Length > 0).ToList(), MAX_LISTED_ATTACHMENTS));
|
|
AppendLine(block, "Content", DescribeEncryption(encryptionKind));
|
|
return block.ToString().TrimEnd('\n');
|
|
}
|
|
|
|
private static void AppendLine(StringBuilder block, string label, string value)
|
|
{
|
|
if (value.Length > 0)
|
|
block.Append(label).Append(": ").Append(value).Append('\n');
|
|
}
|
|
|
|
/// <summary>
|
|
/// Names the addresses of all headers of one kind, e.g. both To lines of a mail which has two.
|
|
/// </summary>
|
|
private static string FormatAddresses(HeaderList headers, HeaderId headerId) =>
|
|
FormatList(MailHeaders.ReadMailboxes(headers, headerId).Select(FormatAddress).Where(address => address.Length > 0).ToList(), MAX_LISTED_ADDRESSES);
|
|
|
|
private static string FormatAddress(MailboxAddress mailbox)
|
|
{
|
|
var address = MailTextNormalization.NormalizeHeaderValue(mailbox.Address);
|
|
var name = MailTextNormalization.NormalizeHeaderValue(mailbox.Name);
|
|
if (name.Length is 0 || name.Equals(address, StringComparison.OrdinalIgnoreCase))
|
|
return address;
|
|
|
|
return address.Length is 0 ? name : $"{name} <{address}>";
|
|
}
|
|
|
|
private static string FormatList(IReadOnlyList<string> items, int maxListed)
|
|
{
|
|
if (items.Count <= maxListed)
|
|
return string.Join(", ", items);
|
|
|
|
return $"{string.Join(", ", items.Take(maxListed))}, and {(items.Count - maxListed).ToString(CultureInfo.InvariantCulture)} more";
|
|
}
|
|
|
|
private static string DescribeEncryption(MailEncryptionKind encryptionKind) => encryptionKind switch
|
|
{
|
|
MailEncryptionKind.NONE => string.Empty,
|
|
MailEncryptionKind.SMIME => "encrypted with S/MIME, AI Studio cannot read it",
|
|
MailEncryptionKind.SMIME_OPAQUE_SIGNED => "wrapped into an S/MIME signature, AI Studio cannot read it",
|
|
MailEncryptionKind.PGP_MIME => "encrypted with PGP/MIME, AI Studio cannot read it",
|
|
MailEncryptionKind.PGP_INLINE => "encrypted with PGP, AI Studio cannot read it",
|
|
MailEncryptionKind.MICROSOFT_IRM => "protected by Microsoft Information Rights Management, AI Studio cannot read it",
|
|
_ => "encrypted, AI Studio cannot read it",
|
|
};
|
|
} |