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; } }