Email server. Users/aliases/domains over the admin REST API, mail read via doveadm in the imap container; ten tools. Emits user/alias created/deleted by polling and diffing the admin API, so a change in the web admin is announced as readily as one via a tool. Consumes nothing — deliberately, since mutating mail accounts off another module's event could silently lose mail. Typechecks against the sdk; manifest parses. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
155 lines
6.2 KiB
TypeScript
155 lines
6.2 KiB
TypeScript
// mailu's tools — moved here from the shared sdk (novox/hq ADR 0044), importing mailu's own client.
|
|
// They return structured data; the mesh serves them through the sdk's tool harness. Deletions are
|
|
// guarded by an explicit `confirm`, since removing a mailbox destroys its mail and cannot be undone.
|
|
|
|
import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools";
|
|
import { MailuClient } from "../client.js";
|
|
|
|
export function getMailuTools(mailu: MailuClient): ToolDefinition[] {
|
|
return [
|
|
{
|
|
name: "mailu_list_users",
|
|
description: "List all email accounts on the mail server.",
|
|
input: {},
|
|
run: async () => {
|
|
const users = await mailu.listUsers();
|
|
return { count: users.length, users };
|
|
},
|
|
},
|
|
{
|
|
name: "mailu_create_user",
|
|
description: "Create a new email account.",
|
|
input: {
|
|
email: { type: "string", description: "full address, e.g. user@example.com" },
|
|
password: { type: "string", description: "the account's initial password" },
|
|
},
|
|
run: async (args) => {
|
|
const email = String(args.email);
|
|
await mailu.createUser(email, String(args.password));
|
|
return { created: email };
|
|
},
|
|
},
|
|
{
|
|
name: "mailu_change_password",
|
|
description: "Change the password of an email account.",
|
|
input: {
|
|
email: { type: "string", description: "full address of the account" },
|
|
password: { type: "string", description: "the new password" },
|
|
},
|
|
run: async (args) => {
|
|
const email = String(args.email);
|
|
await mailu.changePassword(email, String(args.password));
|
|
return { changed: email };
|
|
},
|
|
},
|
|
{
|
|
name: "mailu_delete_user",
|
|
description: "Delete an email account. DESTRUCTIVE — removes the mailbox and all its mail.",
|
|
input: {
|
|
email: { type: "string", description: "full address of the account to delete" },
|
|
confirm: { type: "boolean", description: "must be true — deletion is irreversible" },
|
|
},
|
|
run: async (args) => {
|
|
const email = String(args.email);
|
|
if (args.confirm !== true) return { aborted: "confirm must be true to delete a user", email };
|
|
await mailu.deleteUser(email);
|
|
return { deleted: email };
|
|
},
|
|
},
|
|
{
|
|
name: "mailu_list_aliases",
|
|
description: "List all email aliases and where they forward.",
|
|
input: {},
|
|
run: async () => {
|
|
const aliases = await mailu.listAliases();
|
|
return { count: aliases.length, aliases };
|
|
},
|
|
},
|
|
{
|
|
name: "mailu_create_alias",
|
|
description: "Create an email alias forwarding to one or more destinations.",
|
|
input: {
|
|
localpart: { type: "string", description: "the part before @, e.g. 'sales'" },
|
|
domain: { type: "string", description: "the domain, e.g. example.com" },
|
|
destination: { type: "string", description: "destination address(es), comma-separated" },
|
|
wildcard: { type: "boolean", description: "match any localpart under the domain (optional)" },
|
|
},
|
|
run: async (args) => {
|
|
const email = `${String(args.localpart)}@${String(args.domain)}`;
|
|
const destination = String(args.destination).split(",").map((d) => d.trim()).filter(Boolean);
|
|
await mailu.createAlias(email, destination, args.wildcard === true);
|
|
return { created: email, destination };
|
|
},
|
|
},
|
|
{
|
|
name: "mailu_delete_alias",
|
|
description: "Delete an email alias.",
|
|
input: {
|
|
email: { type: "string", description: "the alias address to delete" },
|
|
confirm: { type: "boolean", description: "must be true to confirm deletion" },
|
|
},
|
|
run: async (args) => {
|
|
const email = String(args.email);
|
|
if (args.confirm !== true) return { aborted: "confirm must be true to delete an alias", email };
|
|
await mailu.deleteAlias(email);
|
|
return { deleted: email };
|
|
},
|
|
},
|
|
{
|
|
name: "mailu_list_domains",
|
|
description: "List the mail domains the server handles.",
|
|
input: {},
|
|
run: async () => {
|
|
const domains = await mailu.listDomains();
|
|
return { count: domains.length, domains };
|
|
},
|
|
},
|
|
{
|
|
name: "mailu_read_mail",
|
|
description: "Read recent messages in a user's mailbox — date, from, subject and a preview.",
|
|
input: {
|
|
user: { type: "string", description: "the mailbox owner's address" },
|
|
mailbox: { type: "string", description: "which mailbox (default INBOX)" },
|
|
limit: { type: "number", description: "how many recent messages (default 10)" },
|
|
},
|
|
run: async (args) => {
|
|
const user = String(args.user);
|
|
const mailbox = args.mailbox ? String(args.mailbox) : "INBOX";
|
|
const messages = await mailu.readMail(user, mailbox, args.limit ? Number(args.limit) : 10);
|
|
return { user, mailbox, count: messages.length, messages };
|
|
},
|
|
},
|
|
{
|
|
name: "mailu_search_mail",
|
|
description: "Search a user's mailbox by subject, sender, and/or date.",
|
|
input: {
|
|
user: { type: "string", description: "the mailbox owner's address" },
|
|
subject: { type: "string", description: "substring to match in the subject (optional)" },
|
|
from: { type: "string", description: "sender address or name to match (optional)" },
|
|
since: { type: "string", description: "only messages since a date, e.g. 01-Jan-2026 (optional)" },
|
|
limit: { type: "number", description: "maximum results (default 10)" },
|
|
},
|
|
run: async (args) => {
|
|
const user = String(args.user);
|
|
const criteria = {
|
|
subject: args.subject ? String(args.subject) : undefined,
|
|
from: args.from ? String(args.from) : undefined,
|
|
since: args.since ? String(args.since) : undefined,
|
|
};
|
|
const messages = await mailu.searchMail(user, criteria, args.limit ? Number(args.limit) : 10);
|
|
return { user, count: messages.length, messages };
|
|
},
|
|
},
|
|
];
|
|
}
|
|
|
|
// The tools exist only when the admin API is configured; without it, mailu contributes none rather
|
|
// than failing the whole runtime.
|
|
registerModuleTools("mailu", (env) => {
|
|
try {
|
|
return getMailuTools(MailuClient.fromEnv(env));
|
|
} catch {
|
|
return [];
|
|
}
|
|
});
|