Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 49 additions & 75 deletions IntelligenceX.Tools/IntelligenceX.Tools.Email/EmailImapGetTool.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
using System;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using IntelligenceX.Json;
using IntelligenceX.Tools;
using IntelligenceX.Tools.Common;
using MailKit;
using MailKit.Net.Imap;
using MimeKit;
using Mailozaurr;

namespace IntelligenceX.Tools.Email;

Expand Down Expand Up @@ -81,69 +79,61 @@ private async Task<string> ExecuteAsync(ToolPipelineContext<GetRequest> context,
var folder = request.Folder ?? imap.DefaultFolder;
var maxBodyBytes = request.MaxBodyBytes;

using var client = await ImapClientFactory.ConnectAsync(imap, cancellationToken).ConfigureAwait(false);
using var client = await ImapSessionService
.ConnectAsync(EmailSessionRequests.BuildImapSessionRequest(imap), cancellationToken)
.ConfigureAwait(false);
try {
var mailFolder = ResolveFolder(client, folder);
await mailFolder.OpenAsync(FolderAccess.ReadOnly, cancellationToken).ConfigureAwait(false);
var uid = new UniqueId((uint)request.Uid);
var message = await mailFolder.GetMessageAsync(uid, cancellationToken).ConfigureAwait(false);

var attachments = message.Attachments.Select(static a => a is MimePart part
? new {
FileName = part.FileName ?? string.Empty,
ContentType = part.ContentType?.MimeType ?? string.Empty
}
: new {
FileName = string.Empty,
ContentType = a.ContentType?.MimeType ?? string.Empty
}).ToArray();

var text = TruncateUtf8(message.TextBody, maxBodyBytes, out var textTruncated);
var html = TruncateUtf8(message.HtmlBody, maxBodyBytes, out var htmlTruncated);
var fromText = string.Join(", ", message.From.Mailboxes.Select(static m => m.ToString()));
var toText = string.Join(", ", message.To.Mailboxes.Select(static m => m.ToString()));
var hasAttachments = attachments.Length > 0;
var message = await ImapMessageReader
.ReadAsync(
client,
new ImapMessageReadRequest(
Uid: new MailKit.UniqueId((uint)request.Uid),
Folder: folder,
MaxBodyBytes: maxBodyBytes),
cancellationToken)
.ConfigureAwait(false);

var attachments = message.Attachments.Select(static attachment => new {
FileName = attachment.FileName,
ContentType = attachment.ContentType
}).ToArray();

var root = new {
Uid = (long)uid.Id,
Folder = folder ?? string.Empty,
Subject = message.Subject ?? string.Empty,
From = fromText,
To = toText,
DateUtc = message.Date.UtcDateTime.ToString("O"),
TextBody = text ?? string.Empty,
TextTruncated = textTruncated,
HtmlBody = html ?? string.Empty,
HtmlTruncated = htmlTruncated,
HasAttachments = hasAttachments,
Uid = message.Uid,
Folder = message.Folder,
Subject = message.Subject,
From = message.From,
To = message.To,
DateUtc = message.DateUtc.ToString("O"),
TextBody = message.TextBody,
TextTruncated = message.TextTruncated,
HtmlBody = message.HtmlBody,
HtmlTruncated = message.HtmlTruncated,
HasAttachments = message.HasAttachments,
Attachments = attachments
};

var summaryPreview = text ?? string.Empty;
const int previewMax = 1000;
if (summaryPreview.Length > previewMax) {
summaryPreview = summaryPreview.Substring(0, previewMax) + "...";
}
var summaryPreview = CreateSummaryPreview(message.TextBody);

var summaryMarkdown = ToolMarkdown.SummaryFacts(
title: "IMAP message",
facts: new (string Key, string Value)[] {
("UID", uid.Id.ToString()),
("Folder", folder ?? string.Empty),
("Subject", message.Subject ?? string.Empty),
("From", fromText),
("Date (UTC)", message.Date.UtcDateTime.ToString("O")),
("Attachments", hasAttachments ? "yes" : "no"),
("Text truncated", textTruncated ? "yes" : "no"),
("HTML truncated", htmlTruncated ? "yes" : "no")
("UID", message.Uid.ToString()),
("Folder", message.Folder),
("Subject", message.Subject),
("From", message.From),
("Date (UTC)", message.DateUtc.ToString("O")),
("Attachments", message.HasAttachments ? "yes" : "no"),
("Text truncated", message.TextTruncated ? "yes" : "no"),
("HTML truncated", message.HtmlTruncated ? "yes" : "no")
},
codeLanguage: "text",
codeContent: summaryPreview);

var meta = ToolOutputHints.Meta(count: 1, truncated: textTruncated || htmlTruncated)
var meta = ToolOutputHints.Meta(count: 1, truncated: message.TextTruncated || message.HtmlTruncated)
.Add("max_body_bytes", maxBodyBytes)
.Add("text_truncated", textTruncated)
.Add("html_truncated", htmlTruncated)
.Add("text_truncated", message.TextTruncated)
.Add("html_truncated", message.HtmlTruncated)
.Add("attachments_count", attachments.Length);

return ToolResultV2.OkModel(
Expand Down Expand Up @@ -188,32 +178,16 @@ private static bool TryReadPositiveInt64(JsonObject? arguments, string key, out
return false;
}

private static IMailFolder ResolveFolder(ImapClient client, string? folder) {
if (string.IsNullOrWhiteSpace(folder)) {
return client.Inbox;
}
try {
return client.GetFolder(folder);
} catch (FolderNotFoundException) {
if (client.PersonalNamespaces.Count == 0) {
throw;
}
return client.GetFolder(client.PersonalNamespaces[0]).GetSubfolder(folder);
internal static string CreateSummaryPreview(string? textBody, int previewMax = 1000) {
if (previewMax < 1) {
previewMax = 1;
}
}

private static string? TruncateUtf8(string? value, long maxBytes, out bool truncated) {
truncated = false;
if (string.IsNullOrEmpty(value)) {
return value;
}
var bytes = Encoding.UTF8.GetBytes(value);
if (bytes.LongLength <= maxBytes) {
return value;
var summaryPreview = textBody ?? string.Empty;
if (summaryPreview.Length > previewMax) {
summaryPreview = summaryPreview.Substring(0, previewMax) + "...";
}
truncated = true;
var slice = bytes.AsSpan(0, (int)Math.Min(maxBytes, int.MaxValue));
return Encoding.UTF8.GetString(slice);
}

return summaryPreview;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ private async Task<string> ExecuteAsync(ToolPipelineContext<SearchRequest> conte
var request = context.Request;
var folder = request.Folder ?? imap.DefaultFolder;

using var client = await ImapClientFactory.ConnectAsync(imap, cancellationToken).ConfigureAwait(false);
using var client = await ImapSessionService
.ConnectAsync(EmailSessionRequests.BuildImapSessionRequest(imap), cancellationToken)
.ConfigureAwait(false);
try {
var messages = await MailboxSearcher.SearchImapAsync(
client,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Mailozaurr;

namespace IntelligenceX.Tools.Email;

internal static class EmailSessionRequests {
internal static ImapSessionRequest BuildImapSessionRequest(ImapAccountOptions options) {
return new ImapSessionRequest {
Connection = new ImapConnectionRequest(
options.Server,
options.Port,
EmailToolBase.ParseSecureSocketOptions(options.SecureSocketOptions),
timeout: options.TimeoutMs,
skipCertificateRevocation: options.SkipCertificateRevocation,
skipCertificateValidation: options.SkipCertificateValidation,
retryCount: options.RetryCount,
retryDelayMilliseconds: options.RetryDelayMilliseconds,
retryDelayBackoff: options.RetryDelayBackoff),
UserName = options.UserName,
Secret = options.Password
};
}

internal static SmtpSessionRequest BuildSmtpSessionRequest(SmtpAccountOptions options, bool dryRun) {
return new SmtpSessionRequest {
Server = options.Server,
Port = options.Port,
SecureSocketOptions = EmailToolBase.ParseSecureSocketOptions(options.SecureSocketOptions),
UseSsl = options.UseSsl,
TimeoutMs = options.TimeoutMs,
RetryCount = options.RetryCount,
DryRun = dryRun,
UserName = options.UserName,
Password = options.Password
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,22 +61,16 @@ private async Task<string> ExecuteRequestAsync(

cancellationToken.ThrowIfCancellationRequested();

var smtp = SmtpClientFactory.Create(smtpOptions, dryRun: true);
var smtp = new Smtp();

try {
var connectAuthResult = await smtp.ConnectAndAuthenticateAsync(
smtpOptions.Server,
smtpOptions.Port,
smtpOptions.UserName,
smtpOptions.Password,
EmailToolBase.ParseSecureSocketOptions(smtpOptions.SecureSocketOptions),
smtpOptions.UseSsl,
ProtocolAuthMode.Basic,
cancellationToken).ConfigureAwait(false);
var connectAuthResult = await SmtpSessionService
.ConnectAndAuthenticateAsync(smtp, EmailSessionRequests.BuildSmtpSessionRequest(smtpOptions, dryRun: true), cancellationToken)
.ConfigureAwait(false);
if (!connectAuthResult.IsSuccess) {
return ToolResultV2.Error(
connectAuthResult.ErrorCode,
connectAuthResult.Error,
connectAuthResult.ErrorCode ?? "smtp_probe_failed",
connectAuthResult.Error ?? "SMTP probe failed.",
isTransient: connectAuthResult.IsTransient);
}

Expand Down Expand Up @@ -121,7 +115,7 @@ private async Task<string> ExecuteRequestAsync(
$"SMTP probe failed. {ex.Message}",
isTransient: true);
} finally {
SmtpClientFactory.DisposeQuietly(smtp);
SmtpSessionService.DisposeQuietly(smtp);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ private async Task<string> ExecuteRequestAsync(
}

var request = context.Request;
var smtp = SmtpClientFactory.Create(smtpOptions, dryRun: !request.Send);
var smtp = new Smtp();

try {
smtp.From = request.From;
Expand All @@ -165,19 +165,13 @@ private async Task<string> ExecuteRequestAsync(
// Ensure the MIME message exists before attempting send.
await smtp.CreateMessageAsync(cancellationToken).ConfigureAwait(false);

var connectAuthResult = await smtp.ConnectAndAuthenticateAsync(
smtpOptions.Server,
smtpOptions.Port,
smtpOptions.UserName,
smtpOptions.Password,
EmailToolBase.ParseSecureSocketOptions(smtpOptions.SecureSocketOptions),
smtpOptions.UseSsl,
ProtocolAuthMode.Basic,
cancellationToken).ConfigureAwait(false);
var connectAuthResult = await SmtpSessionService
.ConnectAndAuthenticateAsync(smtp, EmailSessionRequests.BuildSmtpSessionRequest(smtpOptions, dryRun: !request.Send), cancellationToken)
.ConfigureAwait(false);
if (!connectAuthResult.IsSuccess) {
return ToolResultV2.Error(
connectAuthResult.ErrorCode,
connectAuthResult.Error,
connectAuthResult.ErrorCode ?? "connect_failed",
connectAuthResult.Error ?? "SMTP connect/authenticate failed.",
isTransient: connectAuthResult.IsTransient);
}

Expand Down Expand Up @@ -227,7 +221,7 @@ private async Task<string> ExecuteRequestAsync(
meta: meta,
summaryTitle: "SMTP send");
} finally {
SmtpClientFactory.DisposeQuietly(smtp);
SmtpSessionService.DisposeQuietly(smtp);
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("IntelligenceX.Tools.Tests")]

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using IntelligenceX.Tools.Email;
using Xunit;

namespace IntelligenceX.Tools.Tests;

public sealed class EmailImapGetToolTests {
[Fact]
public void CreateSummaryPreview_WhenTextBodyIsNull_ShouldReturnEmptyString() {
var preview = EmailImapGetTool.CreateSummaryPreview(textBody: null);

Assert.Equal(string.Empty, preview);
}

[Fact]
public void CreateSummaryPreview_WhenTextBodyExceedsLimit_ShouldTruncate() {
var preview = EmailImapGetTool.CreateSummaryPreview(new string('a', 12), previewMax: 5);

Assert.Equal("aaaaa...", preview);
}
}
Loading