using System.Text.Json;
using ClawdDotNet.Core.Tools;
using MailKit;
using MailKit.Net.Imap;
using MailKit.Net.Smtp;
using MailKit.Search;
using Microsoft.Extensions.Logging;
using MimeKit;
using ClawdDotNet.Core.State;
namespace ClawdDotNet.Tools.Mail;
public sealed class MailTool : IAgentTool, IToolJobProvider
{
public string Name => "Mail";
/// #715: Brings text from outside into the context — mails from anyone.
public bool ReadsForeignContent => true;
public string Description => "Allows sending and receiving emails via SMTP and IMAP. read_message lists a message's attachments; read_attachment returns an attachment's text (Word, Excel, text and CSV files); save_attachment stores it in your workspace.";
public JsonElement InputSchema { get; } = JsonDocument.Parse("""
{
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["send", "read_inbox", "read_message", "mark_read", "read_attachment", "save_attachment"],
"description": "The action to perform"
},
"to": {
"type": "string",
"description": "Recipient email (only for send)"
},
"subject": {
"type": "string",
"description": "Subject (only for send)"
},
"body": {
"type": "string",
"description": "Body of the email (only for send)"
},
"limit": {
"type": "integer",
"description": "Maximum number of emails to retrieve (only for read_inbox)"
},
"messageId": {
"type": "string",
"description": "The message ID (for read_message, mark_read, read_attachment, save_attachment)"
},
"attachment": {
"type": "string",
"description": "read_attachment, save_attachment: the attachment's number from read_message, or its file name"
},
"fileName": {
"type": "string",
"description": "save_attachment: the file name in your workspace (default: the attachment's name)"
}
},
"required": ["action"]
}
""").RootElement.Clone();
public async Task ExecuteAsync(
JsonElement input,
AgentToolContext context,
CancellationToken ct)
{
var action = input.GetProperty("action").GetString()
?? throw new ArgumentException("'action' is required");
try
{
return action switch
{
"send" => await HandleSendAsync(input, context, ct),
"read_inbox" => await HandleReadInboxAsync(input, context, ct),
"read_message" => await HandleReadMessageAsync(input, context, ct),
"mark_read" => await HandleMarkReadAsync(input, context, ct),
"read_attachment" or "save_attachment" => await HandleAttachmentAsync(action, input, context, ct),
_ => ToolResult.Fail($"Unknown action: {action}")
};
}
catch (Exception ex)
{
context.Logger.LogError(ex, "Error during Mail action {Action}", action);
return ToolResult.Fail($"Error: {ex.Message}");
}
}
private async Task HandleSendAsync(JsonElement input, AgentToolContext context, CancellationToken ct)
{
var to = input.TryGetProperty("to", out var t) ? t.GetString() : null;
var subject = (input.TryGetProperty("subject", out var s) ? s.GetString() : null) ?? "(No Subject)";
var body = (input.TryGetProperty("body", out var b) ? b.GetString() : null) ?? "";
if (string.IsNullOrWhiteSpace(to)) return ToolResult.Fail("'to' is required.");
// Sicherheitsprüfung: Erlaubte Empfänger
if (!IsRecipientAllowed(to, context))
{
return ToolResult.Fail($"Security error: Sending to '{to}' is not allowed.");
}
if (!TryReadAccess(context, "smtpHost", "smtpPort", 587, out var access, out var fehlt))
return ToolResult.Fail(fehlt);
var message = new MimeMessage();
message.From.Add(new MailboxAddress("ClawdDotNet", access.UserName));
message.To.Add(MailboxAddress.Parse(to));
message.Subject = subject;
message.Body = new TextPart("plain") { Text = body };
using var client = new SmtpClient();
await client.ConnectAsync(access.Host, access.Port, true, ct);
await client.AuthenticateAsync(access.UserName, access.Password, ct);
await client.SendAsync(message, ct);
await client.DisconnectAsync(true, ct);
return ToolResult.Ok($"Email successfully sent to {to}.");
}
private async Task HandleReadInboxAsync(JsonElement input, AgentToolContext context, CancellationToken ct)
{
var limit = input.TryGetProperty("limit", out var l) ? l.GetInt32() : 10;
if (!TryReadAccess(context, "imapHost", "imapPort", 993, out var access, out var fehlt))
return ToolResult.Fail(fehlt);
using var client = new ImapClient();
await client.ConnectAsync(access.Host, access.Port, true, ct);
await client.AuthenticateAsync(access.UserName, access.Password, ct);
await client.Inbox!.OpenAsync(FolderAccess.ReadOnly, ct);
var messages = new List