@lanes-sh/link 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +144 -0
- package/bin/lanes +42 -0
- package/instructions/agents/lanes-link-scout.md +73 -0
- package/instructions/skills/lanes-link/SKILL.md +187 -0
- package/package.json +95 -0
- package/src/audit/chain.ts +278 -0
- package/src/audit/conformance.ts +374 -0
- package/src/audit/fanout.ts +97 -0
- package/src/audit/index.ts +218 -0
- package/src/audit/stdout.ts +60 -0
- package/src/auth/index.ts +220 -0
- package/src/auth/oauth/metadata.ts +75 -0
- package/src/auth/oauth/server.ts +385 -0
- package/src/auth/oauth/store.ts +215 -0
- package/src/auth/oidc.ts +206 -0
- package/src/auth/remote.ts +72 -0
- package/src/cli/argv.ts +94 -0
- package/src/cli/callback-page.ts +256 -0
- package/src/cli/commands/connect/accounts.ts +94 -0
- package/src/cli/commands/connect/authorise.ts +298 -0
- package/src/cli/commands/connect/client.ts +284 -0
- package/src/cli/commands/connect/index.ts +398 -0
- package/src/cli/commands/connect/outcome.ts +119 -0
- package/src/cli/commands/connect/requirements.ts +103 -0
- package/src/cli/commands/connect/scopes-gate.ts +146 -0
- package/src/cli/commands/connect/settle.ts +136 -0
- package/src/cli/commands/connect/setup.ts +276 -0
- package/src/cli/commands/mcp/assets.ts +189 -0
- package/src/cli/commands/mcp/harnesses.ts +143 -0
- package/src/cli/commands/mcp/list.ts +70 -0
- package/src/cli/commands/mcp/register.ts +199 -0
- package/src/cli/commands/mcp/stdio.ts +57 -0
- package/src/cli/commands/mcp.ts +22 -0
- package/src/cli/commands/operate/attach.ts +121 -0
- package/src/cli/commands/operate/audit.ts +119 -0
- package/src/cli/commands/operate/inspect.ts +355 -0
- package/src/cli/commands/operate/outputs.ts +206 -0
- package/src/cli/commands/operate/policy.ts +80 -0
- package/src/cli/commands/operate/serve.ts +55 -0
- package/src/cli/commands/operate/status.ts +133 -0
- package/src/cli/commands/operate/token.ts +70 -0
- package/src/cli/commands/operate.ts +27 -0
- package/src/cli/commands/owner/memory.ts +110 -0
- package/src/cli/commands/owner/shared.ts +125 -0
- package/src/cli/commands/owner/skills.ts +92 -0
- package/src/cli/commands/owner/vault.ts +140 -0
- package/src/cli/commands/owner.ts +46 -0
- package/src/cli/commands/profile/removal.ts +278 -0
- package/src/cli/commands/profile/remove.ts +315 -0
- package/src/cli/commands/profile.ts +156 -0
- package/src/cli/commands/secrets.ts +176 -0
- package/src/cli/commands/setup.ts +150 -0
- package/src/cli/commands/target.ts +310 -0
- package/src/cli/config-edit.ts +397 -0
- package/src/cli/endpoint-url.ts +77 -0
- package/src/cli/identity.ts +109 -0
- package/src/cli/lanes.ts +78 -0
- package/src/cli/main.ts +333 -0
- package/src/cli/oauth-error.ts +13 -0
- package/src/cli/oauth-exchange.ts +146 -0
- package/src/cli/oauth.ts +354 -0
- package/src/cli/output.ts +184 -0
- package/src/cli/prompt.ts +180 -0
- package/src/cli/publish.ts +185 -0
- package/src/cli/runtime/discovery.ts +123 -0
- package/src/cli/runtime/open.ts +342 -0
- package/src/cli/runtime/registry.ts +185 -0
- package/src/cli/runtime/select.ts +124 -0
- package/src/cli/runtime.ts +34 -0
- package/src/cli/scopes.ts +63 -0
- package/src/cli/usage.ts +111 -0
- package/src/cli/version.ts +25 -0
- package/src/connectivity/auth/README.md +36 -0
- package/src/connectivity/auth/api-key/index.ts +43 -0
- package/src/connectivity/auth/authorize.ts +49 -0
- package/src/connectivity/auth/basic/index.ts +68 -0
- package/src/connectivity/auth/bearer/index.ts +13 -0
- package/src/connectivity/auth/credential.ts +19 -0
- package/src/connectivity/auth/header/index.ts +18 -0
- package/src/connectivity/auth/index.ts +35 -0
- package/src/connectivity/auth/none/index.ts +12 -0
- package/src/connectivity/auth/oauth-authcode/broker.ts +261 -0
- package/src/connectivity/auth/oauth-authcode/index.ts +64 -0
- package/src/connectivity/auth/oauth-authcode/provider.ts +279 -0
- package/src/connectivity/auth/oauth-authcode/refresh.ts +118 -0
- package/src/connectivity/auth/resolve.ts +61 -0
- package/src/connectivity/auth/strategy/index.ts +14 -0
- package/src/connectivity/capability.ts +164 -0
- package/src/connectivity/connector.ts +175 -0
- package/src/connectivity/context.ts +77 -0
- package/src/connectivity/index.ts +94 -0
- package/src/connectivity/mail/attachments.ts +368 -0
- package/src/connectivity/mail/compose.ts +73 -0
- package/src/connectivity/mail/index.ts +40 -0
- package/src/connectivity/mail/message.ts +82 -0
- package/src/connectivity/mail/nodemailer.d.ts +83 -0
- package/src/connectivity/mail/staging.ts +143 -0
- package/src/connectivity/mail/url.ts +301 -0
- package/src/connectivity/manifest/auth.ts +135 -0
- package/src/connectivity/manifest/bundles.ts +23 -0
- package/src/connectivity/manifest/connector.ts +168 -0
- package/src/connectivity/manifest/credential-ref.ts +73 -0
- package/src/connectivity/manifest/identity.ts +43 -0
- package/src/connectivity/manifest/index.ts +46 -0
- package/src/connectivity/manifest/primitives.ts +20 -0
- package/src/connectivity/manifest/provider.ts +208 -0
- package/src/connectivity/manifest/requirements.ts +146 -0
- package/src/connectivity/manifest/setup.ts +55 -0
- package/src/connectivity/provider.ts +163 -0
- package/src/connectivity/transports/README.md +33 -0
- package/src/connectivity/transports/composite/index.ts +68 -0
- package/src/connectivity/transports/dav/calendar.ts +217 -0
- package/src/connectivity/transports/dav/capabilities.ts +151 -0
- package/src/connectivity/transports/dav/client.ts +226 -0
- package/src/connectivity/transports/dav/contacts.ts +75 -0
- package/src/connectivity/transports/dav/ical.ts +412 -0
- package/src/connectivity/transports/dav/index.ts +143 -0
- package/src/connectivity/transports/dav/operations.ts +43 -0
- package/src/connectivity/transports/dav/request.ts +161 -0
- package/src/connectivity/transports/dav/xml.ts +123 -0
- package/src/connectivity/transports/factory.ts +181 -0
- package/src/connectivity/transports/fs/capabilities.ts +88 -0
- package/src/connectivity/transports/fs/commands.ts +258 -0
- package/src/connectivity/transports/fs/index.ts +121 -0
- package/src/connectivity/transports/fs/operations.ts +45 -0
- package/src/connectivity/transports/fs/paths.ts +120 -0
- package/src/connectivity/transports/fs/result.ts +12 -0
- package/src/connectivity/transports/http/index.ts +255 -0
- package/src/connectivity/transports/imap/attachment.ts +166 -0
- package/src/connectivity/transports/imap/capabilities.ts +158 -0
- package/src/connectivity/transports/imap/client.ts +398 -0
- package/src/connectivity/transports/imap/commands.ts +385 -0
- package/src/connectivity/transports/imap/index.ts +152 -0
- package/src/connectivity/transports/imap/operations.ts +64 -0
- package/src/connectivity/transports/imap/parse.ts +130 -0
- package/src/connectivity/transports/imap/parser.ts +272 -0
- package/src/connectivity/transports/imap/result.ts +15 -0
- package/src/connectivity/transports/imap/send.ts +92 -0
- package/src/connectivity/transports/imap/socket.ts +111 -0
- package/src/connectivity/transports/imap/utf7.ts +136 -0
- package/src/connectivity/transports/index.ts +20 -0
- package/src/connectivity/transports/local/index.ts +173 -0
- package/src/connectivity/transports/mcp/index.ts +215 -0
- package/src/deployments/README.md +63 -0
- package/src/deployments/adapters/audit-blob.ts +203 -0
- package/src/deployments/adapters/filesystem.ts +184 -0
- package/src/deployments/adapters/gcp-secret-manager.ts +492 -0
- package/src/deployments/adapters/gcs.ts +191 -0
- package/src/deployments/adapters/otlp.ts +128 -0
- package/src/deployments/adapters/s3.ts +195 -0
- package/src/deployments/azure/README.md +21 -0
- package/src/deployments/bootstrap.ts +177 -0
- package/src/deployments/deploy.ts +290 -0
- package/src/deployments/driver.ts +157 -0
- package/src/deployments/drivers.ts +35 -0
- package/src/deployments/gcp/Dockerfile +70 -0
- package/src/deployments/gcp/cloudbuild.yaml +31 -0
- package/src/deployments/gcp/driver.ts +175 -0
- package/src/deployments/gcp/gcloud.ts +178 -0
- package/src/deployments/gcp/provision.ts +290 -0
- package/src/deployments/gcp/survey.ts +319 -0
- package/src/deployments/local/README.md +12 -0
- package/src/deployments/prepare.ts +257 -0
- package/src/deployments/steps.ts +137 -0
- package/src/deployments/target.ts +295 -0
- package/src/deployments/upload.ts +207 -0
- package/src/dispatch/context.ts +195 -0
- package/src/dispatch/dispatch.ts +350 -0
- package/src/dispatch/index.ts +32 -0
- package/src/dispatch/staging.ts +102 -0
- package/src/policy/index.ts +179 -0
- package/src/policy/limits.ts +77 -0
- package/src/profile/authorization.ts +81 -0
- package/src/profile/files.ts +71 -0
- package/src/profile/index.ts +76 -0
- package/src/profile/layout.ts +123 -0
- package/src/profile/load.ts +199 -0
- package/src/profile/primitives.ts +45 -0
- package/src/profile/schema.ts +347 -0
- package/src/profile/secret-detection.ts +162 -0
- package/src/profile/targets.ts +152 -0
- package/src/profile/workspace.ts +262 -0
- package/src/providers/custom/index.ts +21 -0
- package/src/providers/custom/load.ts +115 -0
- package/src/providers/custom/template.ts +156 -0
- package/src/providers/example/provider.ts +207 -0
- package/src/providers/google/calendar/index.ts +66 -0
- package/src/providers/google/calendar/redact.ts +40 -0
- package/src/providers/google/contacts/index.ts +50 -0
- package/src/providers/google/contacts/redact.ts +21 -0
- package/src/providers/google/docs/index.ts +45 -0
- package/src/providers/google/drive/hints.ts +28 -0
- package/src/providers/google/drive/index.ts +34 -0
- package/src/providers/google/drive/redact.ts +39 -0
- package/src/providers/google/drive-mcp/index.ts +21 -0
- package/src/providers/google/gmail/api.ts +42 -0
- package/src/providers/google/gmail/attachment.ts +142 -0
- package/src/providers/google/gmail/hints.ts +55 -0
- package/src/providers/google/gmail/index.ts +112 -0
- package/src/providers/google/gmail/redact.ts +56 -0
- package/src/providers/google/gmail/send.ts +365 -0
- package/src/providers/google/gmail-mcp/index.ts +35 -0
- package/src/providers/google/index.ts +10 -0
- package/src/providers/google/shared/oauth.ts +122 -0
- package/src/providers/google/shared/scopes.ts +99 -0
- package/src/providers/google/shared/setup.ts +80 -0
- package/src/providers/google/sheets/hints.ts +45 -0
- package/src/providers/google/sheets/index.ts +70 -0
- package/src/providers/google/sheets/redact.ts +45 -0
- package/src/providers/google/specs/calendar.v3.json +1829 -0
- package/src/providers/google/specs/docs.v1.json +381 -0
- package/src/providers/google/specs/drive.v3.json +2208 -0
- package/src/providers/google/specs/gmail.v1.json +2578 -0
- package/src/providers/google/specs/people.v1.json +506 -0
- package/src/providers/google/specs/sheets.v4.json +1269 -0
- package/src/providers/google/specs/tasks.v1.json +840 -0
- package/src/providers/google/specs/vendor.ts +661 -0
- package/src/providers/google/tasks/index.ts +53 -0
- package/src/providers/google/tasks/redact.ts +34 -0
- package/src/providers/harness.ts +95 -0
- package/src/providers/icloud/calendar/index.ts +27 -0
- package/src/providers/icloud/contacts/index.ts +17 -0
- package/src/providers/icloud/drive/index.ts +47 -0
- package/src/providers/icloud/index.ts +8 -0
- package/src/providers/icloud/mail/index.ts +37 -0
- package/src/providers/icloud/shared/setup.ts +66 -0
- package/src/providers/index.ts +93 -0
- package/src/providers/linear/index.ts +11 -0
- package/src/providers/linear/scopes.ts +7 -0
- package/src/providers/memory/provider.ts +429 -0
- package/src/providers/notion/index.ts +19 -0
- package/src/providers/owner.ts +49 -0
- package/src/providers/scopes.ts +26 -0
- package/src/providers/setup/plan.ts +141 -0
- package/src/providers/setup/provider.ts +323 -0
- package/src/providers/shared/frontmatter.ts +119 -0
- package/src/providers/skills/provider.ts +283 -0
- package/src/providers/skills/store.ts +252 -0
- package/src/providers/vault/provider.ts +194 -0
- package/src/registry/index.ts +36 -0
- package/src/registry/policy-bridge.ts +32 -0
- package/src/registry/reconcile.ts +313 -0
- package/src/registry/registry.ts +240 -0
- package/src/secrets/document.ts +293 -0
- package/src/secrets/index.ts +154 -0
- package/src/secrets/system.ts +173 -0
- package/src/secrets/vault.ts +336 -0
- package/src/server/attachments.ts +197 -0
- package/src/server/container.ts +96 -0
- package/src/server/edge.ts +53 -0
- package/src/server/endpoint.ts +352 -0
- package/src/server/generations.ts +362 -0
- package/src/server/harness.ts +400 -0
- package/src/server/index.ts +331 -0
- package/src/server/logging.ts +41 -0
- package/src/server/mcp/build.ts +68 -0
- package/src/server/mcp/icon.ts +145 -0
- package/src/server/mcp/index.ts +32 -0
- package/src/server/mcp/instructions.ts +245 -0
- package/src/server/mcp/naming.ts +39 -0
- package/src/server/mcp/prompts.ts +78 -0
- package/src/server/mcp/resources.ts +106 -0
- package/src/server/mcp/routing.ts +117 -0
- package/src/server/mcp/schema.ts +78 -0
- package/src/server/mcp/tools.ts +186 -0
- package/src/server/mcp/visibility.ts +132 -0
- package/src/server/oauth.ts +222 -0
- package/src/server/rebinding.ts +53 -0
- package/src/server/stdio.ts +192 -0
- package/src/stores/blobs/conformance.ts +223 -0
- package/src/stores/blobs/index.ts +125 -0
- package/src/stores/blobs/testing.ts +49 -0
- package/src/stores/state/index.ts +247 -0
- package/src/stores/state/keys.ts +68 -0
- package/src/stores/state/testing.ts +41 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import PostalMime from 'postal-mime';
|
|
2
|
+
import type { AuditLogger } from '#audit';
|
|
3
|
+
import type { ConnectionInfo, ToolResult } from '#connectivity';
|
|
4
|
+
import type { BlobStore } from '#stores/blobs';
|
|
5
|
+
import { receiptFor, resolveAttachments } from '#connectivity/mail';
|
|
6
|
+
import { mailboxAttachments } from './attachment.ts';
|
|
7
|
+
import { quoted, type ImapClient, type ImapSession } from './client.ts';
|
|
8
|
+
import { asText, itemValue } from './parser.ts';
|
|
9
|
+
import { decodeMailboxName, encodeMailboxName } from './utf7.ts';
|
|
10
|
+
import type { Sender, SmtpTarget } from './send.ts';
|
|
11
|
+
import { error, json } from './result.ts';
|
|
12
|
+
import { SETTABLE_FLAGS } from './operations.ts';
|
|
13
|
+
import type { ImapConnectorOptions } from './index.ts';
|
|
14
|
+
import {
|
|
15
|
+
addressList,
|
|
16
|
+
formatAddress,
|
|
17
|
+
allowedFlags,
|
|
18
|
+
envelopeSummary,
|
|
19
|
+
flagList,
|
|
20
|
+
searchCriteria,
|
|
21
|
+
stripHtml,
|
|
22
|
+
uidList,
|
|
23
|
+
} from './parse.ts';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The seven operations, as free functions over a session.
|
|
27
|
+
*
|
|
28
|
+
* Separate from `index.ts` because the connector's job is to open a session and
|
|
29
|
+
* pick one of these; theirs is to know what a mailbox can do.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
export async function listMailboxes(
|
|
34
|
+
session: ImapSession,
|
|
35
|
+
args: Readonly<Record<string, unknown>>,
|
|
36
|
+
): Promise<ToolResult> {
|
|
37
|
+
const pattern = String(args['pattern'] ?? '*');
|
|
38
|
+
const result = await session.command(`LIST "" ${quoted(encodeMailboxName(pattern))}`);
|
|
39
|
+
|
|
40
|
+
const mailboxes = result.untagged
|
|
41
|
+
.filter((tokens) => asText(tokens[1]) === 'LIST')
|
|
42
|
+
.map((tokens) => {
|
|
43
|
+
const flags = tokens[2]?.kind === 'list' ? tokens[2].items : [];
|
|
44
|
+
return {
|
|
45
|
+
name: decodeMailboxName(asText(tokens[4]) ?? ''),
|
|
46
|
+
delimiter: asText(tokens[3]) ?? null,
|
|
47
|
+
// `\Sent`, `\Drafts`, `\Junk` and friends: the only reliable way to know
|
|
48
|
+
// which folder is which, since the *names* are localised.
|
|
49
|
+
flags: flags.map((flag) => asText(flag)).filter(Boolean),
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return json({ mailboxes });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function searchMessages(
|
|
57
|
+
session: ImapSession,
|
|
58
|
+
args: Readonly<Record<string, unknown>>,
|
|
59
|
+
): Promise<ToolResult> {
|
|
60
|
+
const name = String(args['mailbox'] ?? 'INBOX');
|
|
61
|
+
const limit = Math.min(Number(args['limit'] ?? 25) || 25, 100);
|
|
62
|
+
|
|
63
|
+
// EXAMINE, not SELECT: read-only by construction rather than by care.
|
|
64
|
+
await session.command(`EXAMINE ${quoted(encodeMailboxName(name))}`);
|
|
65
|
+
|
|
66
|
+
const criteria = searchCriteria(args);
|
|
67
|
+
const found = await session.command(`UID SEARCH ${criteria}`);
|
|
68
|
+
|
|
69
|
+
const uids = found.untagged
|
|
70
|
+
.filter((tokens) => asText(tokens[1]) === 'SEARCH')
|
|
71
|
+
.flatMap((tokens) => tokens.slice(2).map((token) => Number(asText(token))))
|
|
72
|
+
.filter((uid) => Number.isFinite(uid));
|
|
73
|
+
|
|
74
|
+
if (uids.length === 0) return json({ mailbox: name, criteria, messages: [] });
|
|
75
|
+
|
|
76
|
+
// Highest UIDs are the most recent, and the tail is what anyone means by
|
|
77
|
+
// "search my mail" — asking for all of a 40,000-message mailbox is not.
|
|
78
|
+
const wanted = uids.slice(-limit).reverse();
|
|
79
|
+
|
|
80
|
+
const fetched = await session.command(
|
|
81
|
+
`UID FETCH ${wanted.join(',')} (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE)`,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const byUid = new Map<number, Record<string, unknown>>();
|
|
85
|
+
for (const tokens of fetched.untagged) {
|
|
86
|
+
if (asText(tokens[2]) !== 'FETCH' || tokens[3]?.kind !== 'list') continue;
|
|
87
|
+
const items = tokens[3].items;
|
|
88
|
+
const uid = Number(asText(itemValue(items, 'UID')));
|
|
89
|
+
if (!Number.isFinite(uid)) continue;
|
|
90
|
+
|
|
91
|
+
byUid.set(uid, {
|
|
92
|
+
uid,
|
|
93
|
+
date: asText(itemValue(items, 'INTERNALDATE')) ?? null,
|
|
94
|
+
size: Number(asText(itemValue(items, 'RFC822.SIZE'))) || null,
|
|
95
|
+
flags: flagList(itemValue(items, 'FLAGS')),
|
|
96
|
+
...envelopeSummary(itemValue(items, 'ENVELOPE')),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return json({
|
|
101
|
+
mailbox: name,
|
|
102
|
+
criteria,
|
|
103
|
+
messages: wanted.map((uid) => byUid.get(uid)).filter(Boolean),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function getMessage(
|
|
108
|
+
session: ImapSession,
|
|
109
|
+
args: Readonly<Record<string, unknown>>,
|
|
110
|
+
maxBodyBytes: number,
|
|
111
|
+
): Promise<ToolResult> {
|
|
112
|
+
const name = String(args['mailbox'] ?? 'INBOX');
|
|
113
|
+
const uid = Number(args['uid']);
|
|
114
|
+
if (!Number.isFinite(uid)) return error('uid is required and must be a number.');
|
|
115
|
+
|
|
116
|
+
await session.command(`EXAMINE ${quoted(encodeMailboxName(name))}`);
|
|
117
|
+
|
|
118
|
+
// BODY.PEEK, not BODY: fetching the body must not silently mark the message
|
|
119
|
+
// read behind the operator's back.
|
|
120
|
+
const fetched = await session.command(`UID FETCH ${uid} (UID FLAGS BODY.PEEK[])`);
|
|
121
|
+
|
|
122
|
+
const record = fetched.untagged.find(
|
|
123
|
+
(tokens) => asText(tokens[2]) === 'FETCH' && tokens[3]?.kind === 'list',
|
|
124
|
+
);
|
|
125
|
+
if (!record || record[3]?.kind !== 'list') {
|
|
126
|
+
return error(`No message with UID ${uid} in ${name}.`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const items = record[3].items;
|
|
130
|
+
const body = itemValue(items, 'BODY');
|
|
131
|
+
if (!body || body.kind !== 'literal') return error(`Message ${uid} returned no body.`);
|
|
132
|
+
|
|
133
|
+
const mail = await PostalMime.parse(body.bytes);
|
|
134
|
+
|
|
135
|
+
const text = mail.text ?? (mail.html ? stripHtml(mail.html) : '');
|
|
136
|
+
const truncated = text.length > maxBodyBytes;
|
|
137
|
+
|
|
138
|
+
return json({
|
|
139
|
+
uid,
|
|
140
|
+
mailbox: name,
|
|
141
|
+
flags: flagList(itemValue(items, 'FLAGS')),
|
|
142
|
+
subject: mail.subject ?? null,
|
|
143
|
+
from: mail.from ? formatAddress(mail.from) : null,
|
|
144
|
+
to: (mail.to ?? []).map(formatAddress),
|
|
145
|
+
cc: (mail.cc ?? []).map(formatAddress),
|
|
146
|
+
date: mail.date ?? null,
|
|
147
|
+
message_id: mail.messageId ?? null,
|
|
148
|
+
in_reply_to: mail.inReplyTo ?? null,
|
|
149
|
+
...(args['include_body'] === false
|
|
150
|
+
? {}
|
|
151
|
+
: {
|
|
152
|
+
body: truncated
|
|
153
|
+
? `${text.slice(0, maxBodyBytes)}\n[truncated — ${text.length - maxBodyBytes} more characters]`
|
|
154
|
+
: text,
|
|
155
|
+
body_truncated: truncated,
|
|
156
|
+
}),
|
|
157
|
+
// Metadata only. Handing over attachment *content* needs somewhere for an
|
|
158
|
+
// agent to fetch bytes from, which is what MCP resources are for and does
|
|
159
|
+
// not exist yet — so this says what is there rather than pretending.
|
|
160
|
+
attachments: mail.attachments.map((attachment) => ({
|
|
161
|
+
filename: attachment.filename ?? null,
|
|
162
|
+
mime_type: attachment.mimeType,
|
|
163
|
+
bytes:
|
|
164
|
+
typeof attachment.content === 'string'
|
|
165
|
+
? attachment.content.length
|
|
166
|
+
: attachment.content.byteLength,
|
|
167
|
+
})),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function markMessages(
|
|
172
|
+
session: ImapSession,
|
|
173
|
+
args: Readonly<Record<string, unknown>>,
|
|
174
|
+
): Promise<ToolResult> {
|
|
175
|
+
const name = String(args['mailbox'] ?? 'INBOX');
|
|
176
|
+
const uids = uidList(args['uids']);
|
|
177
|
+
if (uids.length === 0) return error('uids must list at least one message.');
|
|
178
|
+
|
|
179
|
+
const add = allowedFlags(args['add_flags']);
|
|
180
|
+
const remove = allowedFlags(args['remove_flags']);
|
|
181
|
+
if (add.length === 0 && remove.length === 0) {
|
|
182
|
+
return error(`Nothing to change. Settable flags are ${SETTABLE_FLAGS.join(' ')}.`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
await session.command(`SELECT ${quoted(encodeMailboxName(name))}`);
|
|
186
|
+
if (add.length > 0) await session.command(`UID STORE ${uids.join(',')} +FLAGS (${add.join(' ')})`);
|
|
187
|
+
if (remove.length > 0) {
|
|
188
|
+
await session.command(`UID STORE ${uids.join(',')} -FLAGS (${remove.join(' ')})`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return json({ mailbox: name, uids, added: add, removed: remove });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function moveMessages(
|
|
195
|
+
session: ImapSession,
|
|
196
|
+
args: Readonly<Record<string, unknown>>,
|
|
197
|
+
): Promise<ToolResult> {
|
|
198
|
+
const name = String(args['mailbox'] ?? 'INBOX');
|
|
199
|
+
const named = args['destination'] === undefined ? '' : String(args['destination']);
|
|
200
|
+
const flag = args['destination_flag'] === undefined ? '' : String(args['destination_flag']);
|
|
201
|
+
const uids = uidList(args['uids']);
|
|
202
|
+
|
|
203
|
+
// Exactly one, refused rather than resolved by precedence. ADR-017 makes the
|
|
204
|
+
// same call on attachment sources, for the same reason: silently preferring
|
|
205
|
+
// one would make the other look like it worked, and which folder the mail
|
|
206
|
+
// went to is the last thing anyone thinks to check.
|
|
207
|
+
if (named && flag) {
|
|
208
|
+
return error('Pass either destination or destination_flag, not both.');
|
|
209
|
+
}
|
|
210
|
+
if (!named && !flag) return error('destination or destination_flag is required.');
|
|
211
|
+
if (uids.length === 0) return error('uids must list at least one message.');
|
|
212
|
+
|
|
213
|
+
let destination = named;
|
|
214
|
+
if (flag) {
|
|
215
|
+
const resolved = await findMailboxByFlag(session, flag);
|
|
216
|
+
if (!resolved) {
|
|
217
|
+
const available = await mailboxNames(session);
|
|
218
|
+
return error(
|
|
219
|
+
`No mailbox advertises ${flag}. LIST reported: ${available.join(', ') || 'nothing'}. ` +
|
|
220
|
+
'Pass destination with one of those names instead.',
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
destination = resolved;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
await session.command(`SELECT ${quoted(encodeMailboxName(name))}`);
|
|
227
|
+
// UID MOVE only, never emulated with COPY + \Deleted + EXPUNGE: that fallback
|
|
228
|
+
// is how a failed move becomes deleted mail.
|
|
229
|
+
await session.command(`UID MOVE ${uids.join(',')} ${quoted(encodeMailboxName(destination))}`);
|
|
230
|
+
|
|
231
|
+
return json({ moved: uids.length, from: name, to: destination });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function sendMessage(
|
|
235
|
+
client: ImapClient,
|
|
236
|
+
options: ImapConnectorOptions,
|
|
237
|
+
send: Sender,
|
|
238
|
+
args: Readonly<Record<string, unknown>>,
|
|
239
|
+
audit: AuditLogger,
|
|
240
|
+
storage: BlobStore,
|
|
241
|
+
connection: ConnectionInfo,
|
|
242
|
+
): Promise<ToolResult> {
|
|
243
|
+
if (!options.smtp) return error('This account has no SMTP server configured, so it cannot send.');
|
|
244
|
+
|
|
245
|
+
const to = (args['to'] as string[] | undefined) ?? [];
|
|
246
|
+
if (to.length === 0) return error('to must list at least one recipient.');
|
|
247
|
+
|
|
248
|
+
// Resolved before the credential is touched, because this is the step that
|
|
249
|
+
// fails on a caller's mistake — a wrong path, a URL that will not fetch — and
|
|
250
|
+
// failing there should not have opened a session or a socket first.
|
|
251
|
+
//
|
|
252
|
+
// The budget is three quarters of what the host accepts: attachments travel
|
|
253
|
+
// base64, so this is the raw weight that fits once encoded. The composed
|
|
254
|
+
// message is measured exactly below; this only avoids reading megabytes to then
|
|
255
|
+
// refuse them.
|
|
256
|
+
const encodedLimit = options.smtp.maxMessageBytes;
|
|
257
|
+
const attachments = await resolveAttachments(args['attachments'], {
|
|
258
|
+
maxTotalBytes: Math.floor((encodedLimit * 3) / 4),
|
|
259
|
+
mailbox: mailboxAttachments(client),
|
|
260
|
+
storage,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Recorded before the send, so an attachment that was read off disk is in the
|
|
264
|
+
// log even if submission then fails. `redact` cannot express this: it can only
|
|
265
|
+
// keep an argument verbatim, and the raw argument may be an inline base64 file
|
|
266
|
+
// — the last thing an audit log should grow. These are the resolved facts
|
|
267
|
+
// instead, which is what `annotate` is for.
|
|
268
|
+
if (attachments.length > 0) {
|
|
269
|
+
audit.annotate({
|
|
270
|
+
attachments: attachments.map((attachment) => ({
|
|
271
|
+
filename: attachment.filename,
|
|
272
|
+
bytes: attachment.bytes.byteLength,
|
|
273
|
+
content_type: attachment.contentType,
|
|
274
|
+
sha256: attachment.sha256,
|
|
275
|
+
origin: attachment.origin,
|
|
276
|
+
})),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// The call wins, then the connection's own default. Neither is invented: with
|
|
281
|
+
// nothing configured the header stays a bare address rather than guessing a
|
|
282
|
+
// name for someone, which would be worse than showing none.
|
|
283
|
+
const configured = connection.config['from_name'];
|
|
284
|
+
const fromName =
|
|
285
|
+
(args['from_name'] as string | undefined) ??
|
|
286
|
+
(typeof configured === 'string' && configured.trim() !== '' ? configured : undefined);
|
|
287
|
+
|
|
288
|
+
const credential = await options.credential();
|
|
289
|
+
const sent = await send({
|
|
290
|
+
target: options.smtp,
|
|
291
|
+
credential,
|
|
292
|
+
from: credential.username,
|
|
293
|
+
...(fromName ? { fromName } : {}),
|
|
294
|
+
message: {
|
|
295
|
+
to,
|
|
296
|
+
cc: args['cc'] as string[] | undefined,
|
|
297
|
+
bcc: args['bcc'] as string[] | undefined,
|
|
298
|
+
subject: String(args['subject'] ?? ''),
|
|
299
|
+
text: args['text'] as string | undefined,
|
|
300
|
+
html: args['html'] as string | undefined,
|
|
301
|
+
inReplyTo: args['in_reply_to'] as string | undefined,
|
|
302
|
+
...(attachments.length > 0 ? { attachments } : {}),
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// File a copy, best effort. The message is already delivered by this point, so
|
|
307
|
+
// a failure to append is worth reporting but must not read as a failure to
|
|
308
|
+
// send — that would have someone send it a second time.
|
|
309
|
+
let filed: string | null = null;
|
|
310
|
+
try {
|
|
311
|
+
filed = await client.run(
|
|
312
|
+
async (session) => {
|
|
313
|
+
const sentMailbox = await findSentMailbox(session);
|
|
314
|
+
if (!sentMailbox) return null;
|
|
315
|
+
await session.commandWithLiteral(
|
|
316
|
+
`APPEND ${quoted(encodeMailboxName(sentMailbox))} (\\Seen) {${sent.raw.length}}`,
|
|
317
|
+
sent.raw,
|
|
318
|
+
);
|
|
319
|
+
return sentMailbox;
|
|
320
|
+
},
|
|
321
|
+
{ retry: false },
|
|
322
|
+
);
|
|
323
|
+
} catch {
|
|
324
|
+
filed = null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const cc = (args['cc'] as string[] | undefined) ?? [];
|
|
328
|
+
|
|
329
|
+
return json({
|
|
330
|
+
sent: true,
|
|
331
|
+
message_id: sent.messageId,
|
|
332
|
+
recipients: to.length + cc.length,
|
|
333
|
+
filed_in: filed,
|
|
334
|
+
// Names, sizes and digests, never content. What went out is worth confirming
|
|
335
|
+
// — a caller that named a path deserves to know which file it got — but
|
|
336
|
+
// handing the bytes back would undo the point of resolving them here.
|
|
337
|
+
...(attachments.length > 0 ? { attachments: attachments.map(receiptFor) } : {}),
|
|
338
|
+
...(filed ? {} : { note: 'Sent, but no copy could be filed in the Sent mailbox.' }),
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Which mailbox carries a given special-use attribute.
|
|
344
|
+
*
|
|
345
|
+
* By the flag, never by name: iCloud calls Sent `Sent Messages`, Gmail
|
|
346
|
+
* `[Gmail]/Sent Mail`, and a German Dovecot `Gesendet`. Hardcoding any of those
|
|
347
|
+
* is how a sent copy silently stops being filed — and Junk has the same trap
|
|
348
|
+
* with more spellings, which is what generalised this from `findSentMailbox`.
|
|
349
|
+
*/
|
|
350
|
+
export async function findMailboxByFlag(
|
|
351
|
+
session: ImapSession,
|
|
352
|
+
flag: string,
|
|
353
|
+
): Promise<string | null> {
|
|
354
|
+
const wanted = flag.toLowerCase();
|
|
355
|
+
const result = await session.command('LIST "" "*"');
|
|
356
|
+
|
|
357
|
+
for (const tokens of result.untagged) {
|
|
358
|
+
if (asText(tokens[1]) !== 'LIST') continue;
|
|
359
|
+
const flags = tokens[2]?.kind === 'list' ? tokens[2].items : [];
|
|
360
|
+
if (flags.some((candidate) => asText(candidate)?.toLowerCase() === wanted)) {
|
|
361
|
+
return decodeMailboxName(asText(tokens[4]) ?? '');
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Which mailbox is "Sent" — the send path's single caller of the above. */
|
|
369
|
+
export async function findSentMailbox(session: ImapSession): Promise<string | null> {
|
|
370
|
+
return findMailboxByFlag(session, '\\Sent');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Every mailbox name LIST reported, for an error that can say what was there. */
|
|
374
|
+
async function mailboxNames(session: ImapSession): Promise<string[]> {
|
|
375
|
+
const result = await session.command('LIST "" "*"');
|
|
376
|
+
return result.untagged
|
|
377
|
+
.filter((tokens) => asText(tokens[1]) === 'LIST')
|
|
378
|
+
.map((tokens) => decodeMailboxName(asText(tokens[4]) ?? ''))
|
|
379
|
+
.filter((name) => name.length > 0);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
// Shaping arguments and results
|
|
384
|
+
// ---------------------------------------------------------------------------
|
|
385
|
+
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import PostalMime from 'postal-mime';
|
|
2
|
+
import {
|
|
3
|
+
type Connector,
|
|
4
|
+
type DiscoveredCapability,
|
|
5
|
+
type ToolResult,
|
|
6
|
+
} from '#connectivity';
|
|
7
|
+
import {
|
|
8
|
+
createImapClient,
|
|
9
|
+
quoted,
|
|
10
|
+
type ImapClient,
|
|
11
|
+
type ImapCredential,
|
|
12
|
+
type ImapSession,
|
|
13
|
+
} from './client.ts';
|
|
14
|
+
import { asText, itemValue, type ImapToken } from './parser.ts';
|
|
15
|
+
import { decodeMailboxName, encodeMailboxName } from './utf7.ts';
|
|
16
|
+
import { sendOverSmtp, type Sender, type SmtpTarget } from './send.ts';
|
|
17
|
+
import type { SocketFactory } from './socket.ts';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The `imap` connector — a mailbox, over the protocol every mail host speaks.
|
|
21
|
+
*
|
|
22
|
+
* Vendor-neutral by construction: iCloud, Fastmail, and a company Dovecot are
|
|
23
|
+
* the same six manifest fields. There is no iCloud anywhere in this file, and
|
|
24
|
+
* that is the test a new connector kind has to pass — protocol code, not vendor
|
|
25
|
+
* code.
|
|
26
|
+
*
|
|
27
|
+
* Unlike `http`, there is nothing to read a capability list *from*: IMAP
|
|
28
|
+
* describes its extensions through CAPABILITY but never its operations, so the
|
|
29
|
+
* set below is fixed by RFC 3501 rather than by the vendor. `discover()` still
|
|
30
|
+
* does real work — it logs in, so a wrong password fails at `connect` rather
|
|
31
|
+
* than mid-task three days later, and it conditions the set on what the server
|
|
32
|
+
* actually supports.
|
|
33
|
+
*
|
|
34
|
+
* **Reading never marks anything read.** Every read path uses `EXAMINE` rather
|
|
35
|
+
* than `SELECT` and `BODY.PEEK[]` rather than `BODY[]`. Marking a message seen
|
|
36
|
+
* is reachable only through `mark_messages`, which is in the write bundle —
|
|
37
|
+
* never as an argument to a read capability, because an argument that flips a
|
|
38
|
+
* capability's bundle defeats the split the policy is expressed in.
|
|
39
|
+
*
|
|
40
|
+
* **Nothing here can destroy mail.** No `EXPUNGE`, and `\Deleted` is not in the
|
|
41
|
+
* flag allowlist. An agent that can permanently erase a mailbox is a different
|
|
42
|
+
* risk class from one that can read it, and IMAP's delete is not recoverable
|
|
43
|
+
* through this connector. If it is ever wanted, it is a third bundle.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { imapCapabilities } from './capabilities.ts';
|
|
47
|
+
import { OPERATIONS } from './operations.ts';
|
|
48
|
+
import { error, json } from './result.ts';
|
|
49
|
+
import {
|
|
50
|
+
getMessage,
|
|
51
|
+
listMailboxes,
|
|
52
|
+
markMessages,
|
|
53
|
+
moveMessages,
|
|
54
|
+
searchMessages,
|
|
55
|
+
sendMessage,
|
|
56
|
+
} from './commands.ts';
|
|
57
|
+
|
|
58
|
+
export interface ImapConnectorOptions {
|
|
59
|
+
/**
|
|
60
|
+
* One line to append when the server refuses the credential, in the
|
|
61
|
+
* provider's own words. See the identical field on `DavConnectorOptions`.
|
|
62
|
+
*/
|
|
63
|
+
readonly troubleshooting?: string | undefined;
|
|
64
|
+
readonly host: string;
|
|
65
|
+
readonly port: number;
|
|
66
|
+
readonly smtp?: SmtpTarget | undefined;
|
|
67
|
+
readonly maxBodyBytes: number;
|
|
68
|
+
readonly credential: () => Promise<ImapCredential>;
|
|
69
|
+
/** Injected in tests, so no test needs a server. */
|
|
70
|
+
readonly socket?: SocketFactory | undefined;
|
|
71
|
+
readonly send?: Sender | undefined;
|
|
72
|
+
readonly idleMs?: number | undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
export function createImapConnector(options: ImapConnectorOptions): Connector {
|
|
77
|
+
const client: ImapClient = createImapClient({
|
|
78
|
+
host: options.host,
|
|
79
|
+
port: options.port,
|
|
80
|
+
credential: options.credential,
|
|
81
|
+
...(options.socket ? { socket: options.socket } : {}),
|
|
82
|
+
...(options.idleMs === undefined ? {} : { idleMs: options.idleMs }),
|
|
83
|
+
...(options.troubleshooting === undefined ? {} : { troubleshooting: options.troubleshooting }),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const send = options.send ?? sendOverSmtp;
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
kind: 'imap',
|
|
90
|
+
|
|
91
|
+
async discover(): Promise<DiscoveredCapability[]> {
|
|
92
|
+
// Logging in is the point of discovering: a rejected app-specific
|
|
93
|
+
// password should stop `connect`, not surface later as a failed tool call.
|
|
94
|
+
const supportsMove = await client.run(async (session) => session.capabilities.has('MOVE'));
|
|
95
|
+
|
|
96
|
+
return imapCapabilities({ supportsMove, canSend: options.smtp !== undefined });
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
async identify(): Promise<string | null> {
|
|
100
|
+
// The account is the name the *server accepted*, which is a stronger claim
|
|
101
|
+
// than the one the operator typed — a typo fails here rather than becoming
|
|
102
|
+
// a permanent mislabel in config.
|
|
103
|
+
return client.run(async (session) => session.username);
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
async invoke(capability, args, context): Promise<ToolResult> {
|
|
107
|
+
const operation = String(capability.target?.['operation'] ?? capability.name);
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
switch (operation) {
|
|
111
|
+
case OPERATIONS.listMailboxes:
|
|
112
|
+
return await client.run((session) => listMailboxes(session, args));
|
|
113
|
+
case OPERATIONS.searchMessages:
|
|
114
|
+
return await client.run((session) => searchMessages(session, args));
|
|
115
|
+
case OPERATIONS.getMessage:
|
|
116
|
+
return await client.run((session) => getMessage(session, args, options.maxBodyBytes));
|
|
117
|
+
case OPERATIONS.markMessages:
|
|
118
|
+
// Never retried. A repeated flag change is harmless, but keeping the
|
|
119
|
+
// rule uniform is what stops the exceptions from multiplying.
|
|
120
|
+
return await client.run((session) => markMessages(session, args), { retry: false });
|
|
121
|
+
case OPERATIONS.moveMessages:
|
|
122
|
+
return await client.run((session) => moveMessages(session, args), { retry: false });
|
|
123
|
+
case OPERATIONS.sendMessage:
|
|
124
|
+
// The only operation handed the context: what it attached is worth
|
|
125
|
+
// recording in a way `redact` cannot express, since the raw argument
|
|
126
|
+
// may itself be a base64 file.
|
|
127
|
+
return await sendMessage(
|
|
128
|
+
client,
|
|
129
|
+
options,
|
|
130
|
+
send,
|
|
131
|
+
args,
|
|
132
|
+
context.provider.audit,
|
|
133
|
+
context.provider.storage,
|
|
134
|
+
context.provider.connection,
|
|
135
|
+
);
|
|
136
|
+
default:
|
|
137
|
+
return error(`Unknown operation "${operation}".`);
|
|
138
|
+
}
|
|
139
|
+
} catch (failure) {
|
|
140
|
+
// A protocol rejection is the server answering, not a fault of ours, so
|
|
141
|
+
// it comes back as a tool error the agent can read — the same treatment
|
|
142
|
+
// `http.ts` gives a non-2xx.
|
|
143
|
+
return error((failure as Error).message);
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
close: () => client.close(),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Re-exported for the tests that cover IMAP's two peculiar encodings directly.
|
|
152
|
+
export { imapDate, searchCriteria } from './parse.ts';
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The operation names an IMAP connection exposes, and the argument shapes they
|
|
3
|
+
* share.
|
|
4
|
+
*
|
|
5
|
+
* Their own file because `capabilities.ts` declares them and `index.ts`
|
|
6
|
+
* dispatches on them: a name spelled differently in the two places is a tool
|
|
7
|
+
* that lists and cannot be called.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const OPERATIONS = {
|
|
11
|
+
listMailboxes: 'list_mailboxes',
|
|
12
|
+
searchMessages: 'search_messages',
|
|
13
|
+
getMessage: 'get_message',
|
|
14
|
+
markMessages: 'mark_messages',
|
|
15
|
+
moveMessages: 'move_messages',
|
|
16
|
+
sendMessage: 'send_message',
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
/** The flags an agent may set. `\Deleted` is deliberately absent. */
|
|
20
|
+
export const SETTABLE_FLAGS = ['\\Seen', '\\Flagged', '\\Answered', '\\Draft'] as const;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The RFC 6154 special-use attributes a move may name a destination by.
|
|
24
|
+
*
|
|
25
|
+
* A different allowlist from `SETTABLE_FLAGS` and not to be merged with it:
|
|
26
|
+
* these are mailbox attributes reported by LIST, not message flags set by
|
|
27
|
+
* STORE. `\All` is omitted because it is a virtual mailbox — a MOVE into it
|
|
28
|
+
* means nothing — and `\Deleted` is not in this vocabulary at all.
|
|
29
|
+
*
|
|
30
|
+
* This exists because naming a mailbox is not portable. `findSentMailbox`
|
|
31
|
+
* already carried the argument: iCloud spells Sent `Sent Messages`, Gmail
|
|
32
|
+
* `[Gmail]/Sent Mail`, a German Dovecot `Gesendet`. Junk is the same trap with
|
|
33
|
+
* more spellings — `Junk`, `Spam`, `Junk E-mail` — and it is the one an agent
|
|
34
|
+
* asked to "mark this as spam" has to get right, because moving mail to a
|
|
35
|
+
* mailbox that does not exist under that name is how the move silently fails.
|
|
36
|
+
*
|
|
37
|
+
* Deliberately not a `report_junk` operation of its own. That would be
|
|
38
|
+
* `move_messages` under a second name — the `labels.patch` objection. The one
|
|
39
|
+
* thing that would make it more than a rename is the trained signal, and on
|
|
40
|
+
* IMAP that is the `$Junk` keyword plus removing `$NotJunk`; those are
|
|
41
|
+
* keywords, not system flags, so admitting them means widening
|
|
42
|
+
* `SETTABLE_FLAGS` into a different class of thing, with its own argument to
|
|
43
|
+
* make. Until that argument is made, a `report_junk` would advertise a training
|
|
44
|
+
* signal it does not send. The move to `\Junk` is what the server actually
|
|
45
|
+
* learns from, so the move is the honest capability.
|
|
46
|
+
*/
|
|
47
|
+
export const SPECIAL_USE_FLAGS = ['\\Junk', '\\Archive', '\\Trash', '\\Sent', '\\Drafts'] as const;
|
|
48
|
+
|
|
49
|
+
export const object = (
|
|
50
|
+
properties: Record<string, unknown>,
|
|
51
|
+
required: string[] = [],
|
|
52
|
+
): Record<string, unknown> => ({
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties,
|
|
55
|
+
...(required.length > 0 ? { required } : {}),
|
|
56
|
+
additionalProperties: false,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export const mailboxArgument = {
|
|
60
|
+
type: 'string',
|
|
61
|
+
default: 'INBOX',
|
|
62
|
+
description: 'Mailbox name as list_mailboxes reports it.',
|
|
63
|
+
};
|
|
64
|
+
|