using AIStudio.Settings.DataModel;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Validation;
using MailKit;
using MimeKit;
namespace AIStudio.Tools.Mail;
///
/// Decides which attachments of a mail count as such, and which of them get their text read.
///
///
/// Everything here works on the structure of a mail, so nothing of an attachment is fetched before
/// it is clear that its text is wanted. Read are documents alone, the same types a local folder
/// indexes. Whatever else a mail carries, e.g. images or archives, is known by its name only.
///
public static class MailAttachmentRules
{
private const string BASE64_ENCODING = "base64";
private const long BYTES_PER_MEGABYTE = 1024L * 1024L;
///
/// Whether a part is the signature of a signed mail rather than something the sender attached.
///
///
/// A signed mail carries its signature as a part of its own, which many programs mark as an
/// attachment. A reader never sees it as one, since a mail program shows a seal instead, and it
/// has no text to read.
///
/// The content type of the part.
public static bool IsSignature(ContentType contentType) =>
contentType.IsMimeType("application", "pkcs7-signature")
|| contentType.IsMimeType("application", "x-pkcs7-signature")
|| contentType.IsMimeType("application", "pgp-signature");
///
/// Up to which size the text of an attachment is read.
///
///
/// A size beyond what the dialog allows, e.g. from a settings file written by hand, is brought
/// back into that range. Attachments are a cost of their own, and no value should open them up
/// without bounds.
///
/// The mailbox.
/// The size in megabytes, or null when the mailbox reads no attachments at all.
public static int? GetMaxSizeMegabytes(DataSourceMailbox mailbox) => mailbox.IndexAttachments
? Math.Clamp(mailbox.MaxAttachmentSizeMegabytes, DataSourceValidation.MIN_ATTACHMENT_SIZE_MEGABYTES, DataSourceValidation.MAX_ATTACHMENT_SIZE_MEGABYTES)
: null;
///
/// The file type of an attachment, from its name or else from its content type.
///
///
/// Only ever used after checking it against the known document types, which is what keeps a
/// name of the sender's choosing from becoming part of a path.
///
/// The attachment.
/// The extension in lower case and without a dot, or empty when there is none.
public static string GetExtension(BodyPartBasic part)
{
var extension = Path.GetExtension(MailTextNormalization.NormalizeHeaderValue(part.FileName)).TrimStart('.');
if (extension.Length is 0 && MimeTypes.TryGetExtension(part.ContentType.MimeType, out var contentTypeExtension))
extension = contentTypeExtension.TrimStart('.');
return extension.ToLowerInvariant();
}
///
/// How large an attachment is once it is decoded, as far as the structure of the mail tells.
///
///
/// The server reports the size as the attachment travels, which with Base64 is a third more than
/// the file the user sees. The estimate errs a little to the large side, since it counts the line
/// breaks of the encoding as well.
///
/// The attachment.
/// The estimated size in bytes.
public static long EstimateDecodedSize(BodyPartBasic part) => string.Equals(part.ContentTransferEncoding, BASE64_ENCODING, StringComparison.OrdinalIgnoreCase)
? part.Octets / 4L * 3L
: part.Octets;
///
/// Why the text of an attachment is not read.
///
/// The attachment.
/// Up to which size attachments are read, or null when none are, cf. GetMaxSizeMegabytes.
/// The reason, or null when the text is to be read.
public static MailPartTextState? GetReasonToSkip(BodyPartBasic part, int? maxSizeMegabytes)
{
if (maxSizeMegabytes is not { } maxSize)
return MailPartTextState.ATTACHMENTS_DISABLED;
if (!FileTypes.IsAllowedExtension(GetExtension(part), FileTypes.DOCUMENT))
return MailPartTextState.UNSUPPORTED_TYPE;
// Beyond what can be fetched at all, an attachment is too large whatever the limit:
if (EstimateDecodedSize(part) > maxSize * BYTES_PER_MEGABYTE || part.Octets > MailAttachmentPieces.MAX_OCTETS)
return MailPartTextState.TOO_LARGE;
return null;
}
}