using MimeKit;
using MimeKit.IO;
using MimeKit.IO.Filters;
using MimeKit.Utils;
namespace AIStudio.Tools.Mail;
///
/// Writes out an attachment as the file it was, from the pieces a server delivers it in.
///
///
/// An IMAP client holds every answer of the server in memory as a whole, so an attachment fetched
/// at once would take as much memory as it is large. Fetched piece by piece, with each piece decoded
/// and written out at once, an attachment takes no more memory than one piece, however large it is.
/// This keeps AI Studio usable on machines with little memory, too.
///
public static class MailAttachmentPieces
{
///
/// How large a piece is, as it travels.
///
public const int PIECE_BYTES = 4 * 1024 * 1024;
///
/// The largest attachment, as it travels, which can be fetched at all.
///
///
/// The IMAP client addresses a piece by an offset of type int. With Base64, this allows for files
/// of about 1.5 GB, whereas mail servers rarely accept a mail a tenth as large.
///
public const long MAX_OCTETS = int.MaxValue;
private const int COPY_BUFFER_BYTES = 81_920;
///
/// Fetches an attachment piece by piece and writes it out decoded.
///
/// Fetches the piece which starts at the given offset and is at most pieceBytes long. A shorter piece is the last one, and a piece beyond the end is empty.
/// The transfer encoding of the attachment, e.g. "base64", or null when the mail names none.
/// Where the decoded attachment goes. It is left open.
/// How large a piece is, cf. PIECE_BYTES.
/// The cancellation token.
/// The transfer encoding is unknown, or the server delivered more than it was asked for.
public static async Task WriteDecodedAsync(Func> fetchPieceAsync, string? transferEncoding, Stream destination, int pieceBytes, CancellationToken token)
{
var encoding = GetEncoding(transferEncoding);
await using var decoded = new FilteredStream(destination);
if (encoding is ContentEncoding.Base64 or ContentEncoding.QuotedPrintable or ContentEncoding.UUEncode)
decoded.Add(DecoderFilter.Create(encoding));
var buffer = new byte[COPY_BUFFER_BYTES];
for (var offset = 0L; offset <= MAX_OCTETS; offset += pieceBytes)
{
var pieceLength = 0L;
await using (var piece = await fetchPieceAsync((int)offset, token))
{
int read;
while ((read = await piece.ReadAsync(buffer, token)) > 0)
{
pieceLength += read;
if (pieceLength > pieceBytes)
throw new InvalidDataException("The server delivered a piece of an attachment larger than asked for.");
await decoded.WriteAsync(buffer.AsMemory(0, read), token);
}
}
// A piece shorter than asked for is the last one:
if (pieceLength < pieceBytes)
{
await decoded.FlushAsync(token);
return;
}
}
throw new InvalidDataException("The attachment is larger than any attachment which can be fetched.");
}
private static ContentEncoding GetEncoding(string? transferEncoding)
{
// Without a transfer encoding, the content travels as it is, cf. RFC 2045, section 6.1:
if (string.IsNullOrWhiteSpace(transferEncoding))
return ContentEncoding.Default;
if (MimeUtils.TryParse(transferEncoding, out ContentEncoding encoding))
return encoding;
throw new InvalidDataException("The attachment has a transfer encoding unknown to AI Studio.");
}
}