Files
AI-Studio/app/MindWork AI Studio/Tools/Mail/MailAttachmentPieces.cs
T
Thorsten Sommer c4400c0ff6
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
Added mailboxes as local data sources (#1022)
2026-10-04 12:26:26 +02:00

90 lines
4.0 KiB
C#

using MimeKit;
using MimeKit.IO;
using MimeKit.IO.Filters;
using MimeKit.Utils;
namespace AIStudio.Tools.Mail;
/// <summary>
/// Writes out an attachment as the file it was, from the pieces a server delivers it in.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public static class MailAttachmentPieces
{
/// <summary>
/// How large a piece is, as it travels.
/// </summary>
public const int PIECE_BYTES = 4 * 1024 * 1024;
/// <summary>
/// The largest attachment, as it travels, which can be fetched at all.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public const long MAX_OCTETS = int.MaxValue;
private const int COPY_BUFFER_BYTES = 81_920;
/// <summary>
/// Fetches an attachment piece by piece and writes it out decoded.
/// </summary>
/// <param name="fetchPieceAsync">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.</param>
/// <param name="transferEncoding">The transfer encoding of the attachment, e.g. "base64", or null when the mail names none.</param>
/// <param name="destination">Where the decoded attachment goes. It is left open.</param>
/// <param name="pieceBytes">How large a piece is, cf. PIECE_BYTES.</param>
/// <param name="token">The cancellation token.</param>
/// <exception cref="InvalidDataException">The transfer encoding is unknown, or the server delivered more than it was asked for.</exception>
public static async Task WriteDecodedAsync(Func<int, CancellationToken, Task<Stream>> 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.");
}
}