using System.Globalization;
using System.Security.Cryptography;
using System.Text;
namespace AIStudio.Tools.Mail;
///
/// Gives a mail the key of its document in the index, the same in every folder it lies in.
///
///
/// The key follows the mail rather than its place: a mail moved to another folder, or found again
/// after the server reset the UIDs of a folder (UIDVALIDITY), keeps its key, so only its location
/// changes and nothing gets embedded again. In the order the server can tell, it comes from:
///
/// - the EMAILID of RFC 8474, which the server keeps for a mail across all its folders;
/// - the X-GM-MSGID, which is the same for Gmail;
/// - a hash over Message-ID, Date, From, Subject and the size, all of which a server reports
/// without the mail itself being fetched. Two mails alike in all of these are copies of one mail.
///
/// The key is "mail:" and a SHA-256 in hex, whichever of the three it comes from. The value it
/// hashes names its origin, so an EMAILID which happens to read like a Gmail id cannot meet one.
/// The key goes through no Path API, see "Indexed data sources" in AGENTS.md.
///
public static class MailContentKey
{
public const string PREFIX = "mail:";
///
/// Builds the key of a mail.
///
/// What the server reported about the mail.
/// The key, starting with "mail:".
public static string Create(MailIdentity identity)
{
if (!string.IsNullOrWhiteSpace(identity.EmailId))
return Hash($"emailid\n{identity.EmailId.Trim()}");
if (identity.GmailMessageId is { } gmailMessageId)
return Hash($"x-gm-msgid\n{gmailMessageId.ToString(CultureInfo.InvariantCulture)}");
var fields = new StringBuilder("headers");
fields.Append('\n').Append(NormalizeMessageId(identity.MessageId));
fields.Append('\n').Append(identity.Date?.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture));
fields.Append('\n').Append(string.Join(',', identity.FromAddresses.Select(address => address.Trim().ToLowerInvariant())));
fields.Append('\n').Append(NormalizeWhitespace(identity.Subject));
fields.Append('\n').Append(identity.Size.ToString(CultureInfo.InvariantCulture));
return Hash(fields.ToString());
}
private static string Hash(string value) => PREFIX + Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(value)));
///
/// The Message-ID without its angle brackets, which one reader keeps and the next one drops.
///
private static string NormalizeMessageId(string? messageId)
{
var trimmed = messageId?.Trim() ?? string.Empty;
return trimmed.Length > 1 && trimmed[0] == '<' && trimmed[^1] == '>' ? trimmed[1..^1].Trim() : trimmed;
}
///
/// A subject with every run of whitespace as one space, since folding a long header line
/// leaves line breaks behind which not every reader unfolds alike.
///
private static string NormalizeWhitespace(string? text) => string.Join(' ', (text ?? string.Empty).Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries));
}