pi-protonmail 0.1.0 → 0.2.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/CHANGELOG.md +9 -0
- package/package.json +5 -3
- package/src/proton-bridge.ts +252 -3
- package/src/protonmail.ts +299 -0
- package/src/types.ts +37 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.2.0 - 2026-07-02
|
|
6
|
+
|
|
7
|
+
- Added outgoing Proton Bridge tools for draft creation, SMTP sending, message moving, and label application.
|
|
8
|
+
- Added MIME composition with local attachments through `nodemailer`.
|
|
9
|
+
- Added profile `default_from` support for outgoing mail sender resolution.
|
|
10
|
+
- Added Conventional Commits guidance for future repository commits.
|
|
11
|
+
|
|
12
|
+
## 0.1.0
|
|
13
|
+
|
|
5
14
|
- Kept `/protonmail` as the single config command and moved Bridge/status/message handling into `protonmail_*` tools.
|
|
6
15
|
- Replaced the Python Bridge helper with a native TypeScript module and kept attachment import staging under profile-specific workspaces.
|
|
7
16
|
- Aligned the package metadata and docs with the Proton Mail workflow.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-protonmail",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Proton Mail Bridge Pi extension for mailbox discovery and attachment imports.",
|
|
6
6
|
"homepage": "https://github.com/eirenik0/pi-protonmail#readme",
|
|
@@ -50,10 +50,12 @@
|
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"effect": "^3.0.0",
|
|
52
52
|
"imapflow": "^1.1.1",
|
|
53
|
-
"mailparser": "^3.9.0"
|
|
53
|
+
"mailparser": "^3.9.0",
|
|
54
|
+
"nodemailer": "^6.10.1"
|
|
54
55
|
},
|
|
55
56
|
"devDependencies": {
|
|
56
|
-
"@biomejs/biome": "^2.5.0"
|
|
57
|
+
"@biomejs/biome": "^2.5.0",
|
|
58
|
+
"@types/nodemailer": "^8.0.1"
|
|
57
59
|
},
|
|
58
60
|
"peerDependencies": {
|
|
59
61
|
"@earendil-works/pi-coding-agent": "*",
|
package/src/proton-bridge.ts
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
|
-
import { access, copyFile, mkdir, writeFile } from "node:fs/promises";
|
|
1
|
+
import { access, copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { createConnection } from "node:net";
|
|
3
|
-
import { join, relative } from "node:path";
|
|
3
|
+
import { basename, isAbsolute, join, relative } from "node:path";
|
|
4
4
|
|
|
5
5
|
import { ImapFlow } from "imapflow";
|
|
6
6
|
import { simpleParser } from "mailparser";
|
|
7
|
+
import nodemailer from "nodemailer";
|
|
8
|
+
import MailComposer from "nodemailer/lib/mail-composer/index.js";
|
|
9
|
+
import type Mail from "nodemailer/lib/mailer/index.js";
|
|
7
10
|
|
|
8
11
|
import type {
|
|
12
|
+
ApplyLabelsResult,
|
|
9
13
|
BridgeStatusResult,
|
|
14
|
+
CreateDraftResult,
|
|
10
15
|
MailboxInfo,
|
|
11
16
|
MailboxListResult,
|
|
12
17
|
MessageInfo,
|
|
13
18
|
MessageListResult,
|
|
19
|
+
MoveMessageResult,
|
|
14
20
|
ProtonBridgeConfig,
|
|
21
|
+
SendMessageResult,
|
|
15
22
|
} from "./types.ts";
|
|
16
23
|
|
|
17
24
|
export interface ProtonBridgeImportResult {
|
|
@@ -46,7 +53,7 @@ interface ImportOptions {
|
|
|
46
53
|
cwd: string;
|
|
47
54
|
workspaceRoot: string;
|
|
48
55
|
period: string;
|
|
49
|
-
mailbox
|
|
56
|
+
mailbox?: string;
|
|
50
57
|
profile: string;
|
|
51
58
|
unseenOnly?: boolean;
|
|
52
59
|
query?: string;
|
|
@@ -54,6 +61,37 @@ interface ImportOptions {
|
|
|
54
61
|
limit?: number;
|
|
55
62
|
}
|
|
56
63
|
|
|
64
|
+
interface OutgoingMessageOptions {
|
|
65
|
+
cwd: string;
|
|
66
|
+
from: string;
|
|
67
|
+
to: string[];
|
|
68
|
+
cc?: string[];
|
|
69
|
+
bcc?: string[];
|
|
70
|
+
subject: string;
|
|
71
|
+
body: string;
|
|
72
|
+
attachments?: string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface CreateDraftOptions extends OutgoingMessageOptions {
|
|
76
|
+
draftsMailbox?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface SendMessageOptions extends OutgoingMessageOptions {
|
|
80
|
+
saveToMailbox?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface MoveMessageOptions {
|
|
84
|
+
mailbox: string;
|
|
85
|
+
uid: string;
|
|
86
|
+
destination: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface ApplyLabelsOptions {
|
|
90
|
+
mailbox: string;
|
|
91
|
+
uid: string;
|
|
92
|
+
labels: string[];
|
|
93
|
+
}
|
|
94
|
+
|
|
57
95
|
function sanitizeFilename(value: string): string {
|
|
58
96
|
const text = value.replace(/[\\/:*?"<>|\r\n]+/g, "_").trim();
|
|
59
97
|
return text.replace(/\s+/g, " ") || "unnamed";
|
|
@@ -244,6 +282,80 @@ async function ensureDir(path: string): Promise<void> {
|
|
|
244
282
|
await mkdir(path, { recursive: true });
|
|
245
283
|
}
|
|
246
284
|
|
|
285
|
+
function requireBridgeCredentials(
|
|
286
|
+
config: ProtonBridgeConfig,
|
|
287
|
+
): asserts config is ProtonBridgeConfig & {
|
|
288
|
+
username: string;
|
|
289
|
+
password: string;
|
|
290
|
+
} {
|
|
291
|
+
if (!config.username || !config.password)
|
|
292
|
+
throw new Error("Missing Proton Bridge username/password.");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function resolveLocalPath(cwd: string, path: string): string {
|
|
296
|
+
return isAbsolute(path) ? path : join(cwd, path);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function buildAttachmentOptions(
|
|
300
|
+
cwd: string,
|
|
301
|
+
attachments?: string[],
|
|
302
|
+
): Promise<Mail.Attachment[]> {
|
|
303
|
+
const files: Mail.Attachment[] = [];
|
|
304
|
+
for (const attachmentPath of attachments ?? []) {
|
|
305
|
+
const path = resolveLocalPath(cwd, attachmentPath);
|
|
306
|
+
files.push({
|
|
307
|
+
filename: basename(path),
|
|
308
|
+
content: await readFile(path),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return files;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function buildMimeMessage(options: OutgoingMessageOptions, keepBcc = false): Promise<Buffer> {
|
|
315
|
+
const message = new MailComposer({
|
|
316
|
+
from: options.from,
|
|
317
|
+
to: options.to,
|
|
318
|
+
cc: options.cc,
|
|
319
|
+
bcc: options.bcc,
|
|
320
|
+
subject: options.subject,
|
|
321
|
+
text: options.body,
|
|
322
|
+
attachments: await buildAttachmentOptions(options.cwd, options.attachments),
|
|
323
|
+
});
|
|
324
|
+
const node = message.compile();
|
|
325
|
+
if (keepBcc) (node as unknown as { keepBcc: boolean }).keepBcc = true;
|
|
326
|
+
return node.build();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function appendUid(result: unknown): string | undefined {
|
|
330
|
+
if (!result || typeof result !== "object" || !("uid" in result)) return undefined;
|
|
331
|
+
const uid = (result as { uid?: unknown }).uid;
|
|
332
|
+
return uid == null ? undefined : String(uid);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function allRecipients(options: OutgoingMessageOptions): string[] {
|
|
336
|
+
return [...options.to, ...(options.cc ?? []), ...(options.bcc ?? [])].filter(Boolean);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function messageIdFromRaw(raw: Buffer): string | undefined {
|
|
340
|
+
const match = raw.toString("utf8").match(/^Message-ID:\s*(.+)$/im);
|
|
341
|
+
return match?.[1]?.trim();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function resolveLabelMailbox(label: string, mailboxes: MailboxInfo[]): string {
|
|
345
|
+
const trimmed = label.trim();
|
|
346
|
+
const candidates = [trimmed, `Labels/${trimmed}`];
|
|
347
|
+
for (const candidate of candidates) {
|
|
348
|
+
const exact = mailboxes.find((mailbox) => mailbox.name === candidate);
|
|
349
|
+
if (exact) return exact.name;
|
|
350
|
+
}
|
|
351
|
+
for (const candidate of candidates) {
|
|
352
|
+
const lower = candidate.toLowerCase();
|
|
353
|
+
const match = mailboxes.find((mailbox) => mailbox.name.toLowerCase() === lower);
|
|
354
|
+
if (match) return match.name;
|
|
355
|
+
}
|
|
356
|
+
return trimmed;
|
|
357
|
+
}
|
|
358
|
+
|
|
247
359
|
export async function protonBridgeStatus(config: ProtonBridgeConfig): Promise<BridgeStatusResult> {
|
|
248
360
|
const imapProbe = await probePort(config.host, config.imapPort);
|
|
249
361
|
const smtpProbe = await probePort(config.host, config.smtpPort);
|
|
@@ -335,6 +447,143 @@ export async function protonBridgeListMessages(
|
|
|
335
447
|
}
|
|
336
448
|
}
|
|
337
449
|
|
|
450
|
+
export async function protonBridgeCreateDraft(
|
|
451
|
+
config: ProtonBridgeConfig,
|
|
452
|
+
options: CreateDraftOptions,
|
|
453
|
+
): Promise<CreateDraftResult> {
|
|
454
|
+
requireBridgeCredentials(config);
|
|
455
|
+
const mailbox = options.draftsMailbox?.trim() || "Drafts";
|
|
456
|
+
const raw = await buildMimeMessage(options, true);
|
|
457
|
+
let client: ImapFlow | undefined;
|
|
458
|
+
try {
|
|
459
|
+
client = await connectImap(config);
|
|
460
|
+
const result = await client.append(mailbox, raw, ["\\Draft", "\\Seen"], new Date());
|
|
461
|
+
return {
|
|
462
|
+
mailbox,
|
|
463
|
+
uid: appendUid(result),
|
|
464
|
+
from: options.from,
|
|
465
|
+
to: options.to,
|
|
466
|
+
cc: options.cc,
|
|
467
|
+
bcc: options.bcc,
|
|
468
|
+
subject: options.subject,
|
|
469
|
+
attachment_count: options.attachments?.length ?? 0,
|
|
470
|
+
};
|
|
471
|
+
} finally {
|
|
472
|
+
await client?.logout().catch(() => undefined);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export async function protonBridgeSendMessage(
|
|
477
|
+
config: ProtonBridgeConfig,
|
|
478
|
+
options: SendMessageOptions,
|
|
479
|
+
): Promise<SendMessageResult> {
|
|
480
|
+
requireBridgeCredentials(config);
|
|
481
|
+
const raw = await buildMimeMessage(options);
|
|
482
|
+
const security = normalizeSecurity(config.security);
|
|
483
|
+
const transport = nodemailer.createTransport({
|
|
484
|
+
host: config.host,
|
|
485
|
+
port: config.smtpPort,
|
|
486
|
+
secure: security === "ssl",
|
|
487
|
+
ignoreTLS: security === "plain",
|
|
488
|
+
requireTLS: security === "starttls",
|
|
489
|
+
auth: {
|
|
490
|
+
user: config.username,
|
|
491
|
+
pass: config.password,
|
|
492
|
+
},
|
|
493
|
+
tls: {
|
|
494
|
+
rejectUnauthorized: false,
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
let messageId = messageIdFromRaw(raw);
|
|
499
|
+
try {
|
|
500
|
+
const info = await transport.sendMail({
|
|
501
|
+
envelope: {
|
|
502
|
+
from: options.from,
|
|
503
|
+
to: allRecipients(options),
|
|
504
|
+
},
|
|
505
|
+
raw,
|
|
506
|
+
});
|
|
507
|
+
if (typeof info.messageId === "string") messageId = info.messageId;
|
|
508
|
+
} finally {
|
|
509
|
+
transport.close();
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const result: SendMessageResult = {
|
|
513
|
+
from: options.from,
|
|
514
|
+
to: options.to,
|
|
515
|
+
cc: options.cc,
|
|
516
|
+
bcc: options.bcc,
|
|
517
|
+
subject: options.subject,
|
|
518
|
+
attachment_count: options.attachments?.length ?? 0,
|
|
519
|
+
message_id: messageId,
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
const saveToMailbox = options.saveToMailbox?.trim();
|
|
523
|
+
if (saveToMailbox) {
|
|
524
|
+
let client: ImapFlow | undefined;
|
|
525
|
+
try {
|
|
526
|
+
client = await connectImap(config);
|
|
527
|
+
const appendResult = await client.append(saveToMailbox, raw, ["\\Seen"], new Date());
|
|
528
|
+
result.saved_to_mailbox = saveToMailbox;
|
|
529
|
+
result.saved_uid = appendUid(appendResult);
|
|
530
|
+
} finally {
|
|
531
|
+
await client?.logout().catch(() => undefined);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return result;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export async function protonBridgeMoveMessage(
|
|
539
|
+
config: ProtonBridgeConfig,
|
|
540
|
+
options: MoveMessageOptions,
|
|
541
|
+
): Promise<MoveMessageResult> {
|
|
542
|
+
requireBridgeCredentials(config);
|
|
543
|
+
let client: ImapFlow | undefined;
|
|
544
|
+
try {
|
|
545
|
+
client = await connectImap(config);
|
|
546
|
+
await client.mailboxOpen(options.mailbox, { readOnly: false });
|
|
547
|
+
await client.messageMove(options.uid, options.destination, { uid: true });
|
|
548
|
+
return {
|
|
549
|
+
uid: options.uid,
|
|
550
|
+
source: options.mailbox,
|
|
551
|
+
destination: options.destination,
|
|
552
|
+
};
|
|
553
|
+
} finally {
|
|
554
|
+
await client?.logout().catch(() => undefined);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export async function protonBridgeApplyLabels(
|
|
559
|
+
config: ProtonBridgeConfig,
|
|
560
|
+
options: ApplyLabelsOptions,
|
|
561
|
+
): Promise<ApplyLabelsResult> {
|
|
562
|
+
requireBridgeCredentials(config);
|
|
563
|
+
const labels = [...new Set(options.labels.map((label) => label.trim()).filter(Boolean))];
|
|
564
|
+
if (labels.length === 0) throw new Error("At least one label is required.");
|
|
565
|
+
let client: ImapFlow | undefined;
|
|
566
|
+
try {
|
|
567
|
+
client = await connectImap(config);
|
|
568
|
+
const mailboxes = await listMailboxes(client);
|
|
569
|
+
await client.mailboxOpen(options.mailbox, { readOnly: false });
|
|
570
|
+
const labelMailboxes = [
|
|
571
|
+
...new Set(labels.map((label) => resolveLabelMailbox(label, mailboxes))),
|
|
572
|
+
];
|
|
573
|
+
for (const labelMailbox of labelMailboxes) {
|
|
574
|
+
await client.messageCopy(options.uid, labelMailbox, { uid: true });
|
|
575
|
+
}
|
|
576
|
+
return {
|
|
577
|
+
uid: options.uid,
|
|
578
|
+
mailbox: options.mailbox,
|
|
579
|
+
labels,
|
|
580
|
+
label_mailboxes: labelMailboxes,
|
|
581
|
+
};
|
|
582
|
+
} finally {
|
|
583
|
+
await client?.logout().catch(() => undefined);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
338
587
|
export async function protonBridgeImportAttachments(
|
|
339
588
|
config: ProtonBridgeConfig,
|
|
340
589
|
options: ImportOptions,
|
package/src/protonmail.ts
CHANGED
|
@@ -10,19 +10,27 @@ import { Type } from "typebox";
|
|
|
10
10
|
import { PREVIEW_LINES } from "./constants.ts";
|
|
11
11
|
import { openProtonMailHub } from "./hub.ts";
|
|
12
12
|
import {
|
|
13
|
+
protonBridgeApplyLabels as runProtonBridgeApplyLabels,
|
|
14
|
+
protonBridgeCreateDraft as runProtonBridgeCreateDraft,
|
|
13
15
|
protonBridgeImportAttachments as runProtonBridgeImportAttachments,
|
|
14
16
|
protonBridgeListMailboxes as runProtonBridgeListMailboxes,
|
|
15
17
|
protonBridgeListMessages as runProtonBridgeListMessages,
|
|
18
|
+
protonBridgeMoveMessage as runProtonBridgeMoveMessage,
|
|
19
|
+
protonBridgeSendMessage as runProtonBridgeSendMessage,
|
|
16
20
|
protonBridgeStatus as runProtonBridgeStatus,
|
|
17
21
|
} from "./proton-bridge.ts";
|
|
18
22
|
import { resolveSecretReference } from "./secret-refs.ts";
|
|
19
23
|
import type {
|
|
24
|
+
ApplyLabelsResult,
|
|
20
25
|
BridgeStatusResult,
|
|
21
26
|
CommandContext,
|
|
27
|
+
CreateDraftResult,
|
|
22
28
|
MailboxListResult,
|
|
23
29
|
MessageListResult,
|
|
30
|
+
MoveMessageResult,
|
|
24
31
|
ProtonBridgeConfig,
|
|
25
32
|
ProtonMailWorkingProfile,
|
|
33
|
+
SendMessageResult,
|
|
26
34
|
ToolContext,
|
|
27
35
|
} from "./types.ts";
|
|
28
36
|
import {
|
|
@@ -216,6 +224,13 @@ async function listProtonMessages(
|
|
|
216
224
|
return runProtonBridgeListMessages(config, mailbox, period, query, unseenOnly, limit);
|
|
217
225
|
}
|
|
218
226
|
|
|
227
|
+
function resolveOutgoingFrom(from: string | undefined, profile: ProtonMailWorkingProfile): string {
|
|
228
|
+
const resolved = from?.trim() || profile.policy.default_from?.trim();
|
|
229
|
+
if (!resolved)
|
|
230
|
+
throw new Error("No from address provided and the active profile has no default_from.");
|
|
231
|
+
return resolved;
|
|
232
|
+
}
|
|
233
|
+
|
|
219
234
|
function formatStatusSummary(result: BridgeStatusResult): string {
|
|
220
235
|
const lines = [
|
|
221
236
|
"# Proton Bridge Status",
|
|
@@ -296,6 +311,62 @@ function formatMessageSummary(result: MessageListResult, period?: string): strin
|
|
|
296
311
|
return lines.join("\n");
|
|
297
312
|
}
|
|
298
313
|
|
|
314
|
+
function formatCreateDraftSummary(result: CreateDraftResult): string {
|
|
315
|
+
const lines = [
|
|
316
|
+
"# Proton Mail draft created",
|
|
317
|
+
"",
|
|
318
|
+
`- Mailbox: \`${result.mailbox}\``,
|
|
319
|
+
`- UID: ${result.uid ? `\`${result.uid}\`` : "—"}`,
|
|
320
|
+
`- From: ${result.from}`,
|
|
321
|
+
`- To: ${result.to.join(", ")}`,
|
|
322
|
+
`- Subject: ${result.subject}`,
|
|
323
|
+
`- Attachments: ${result.attachment_count}`,
|
|
324
|
+
];
|
|
325
|
+
if (result.cc?.length) lines.push(`- Cc: ${result.cc.join(", ")}`);
|
|
326
|
+
if (result.bcc?.length) lines.push(`- Bcc: ${result.bcc.join(", ")}`);
|
|
327
|
+
return lines.join("\n");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function formatSendSummary(result: SendMessageResult): string {
|
|
331
|
+
const lines = [
|
|
332
|
+
"# Proton Mail message sent",
|
|
333
|
+
"",
|
|
334
|
+
`- Message ID: ${result.message_id ? `\`${result.message_id}\`` : "—"}`,
|
|
335
|
+
`- From: ${result.from}`,
|
|
336
|
+
`- To: ${result.to.join(", ")}`,
|
|
337
|
+
`- Subject: ${result.subject}`,
|
|
338
|
+
`- Attachments: ${result.attachment_count}`,
|
|
339
|
+
];
|
|
340
|
+
if (result.cc?.length) lines.push(`- Cc: ${result.cc.join(", ")}`);
|
|
341
|
+
if (result.bcc?.length) lines.push(`- Bcc: ${result.bcc.join(", ")}`);
|
|
342
|
+
if (result.saved_to_mailbox)
|
|
343
|
+
lines.push(
|
|
344
|
+
`- Saved copy: \`${result.saved_to_mailbox}\`${result.saved_uid ? ` UID ${result.saved_uid}` : ""}`,
|
|
345
|
+
);
|
|
346
|
+
return lines.join("\n");
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function formatMoveSummary(result: MoveMessageResult): string {
|
|
350
|
+
return [
|
|
351
|
+
"# Proton Mail message moved",
|
|
352
|
+
"",
|
|
353
|
+
`- UID: \`${result.uid}\``,
|
|
354
|
+
`- Source: \`${result.source}\``,
|
|
355
|
+
`- Destination: \`${result.destination}\``,
|
|
356
|
+
].join("\n");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function formatApplyLabelsSummary(result: ApplyLabelsResult): string {
|
|
360
|
+
return [
|
|
361
|
+
"# Proton Mail labels applied",
|
|
362
|
+
"",
|
|
363
|
+
`- UID: \`${result.uid}\``,
|
|
364
|
+
`- Mailbox: \`${result.mailbox}\``,
|
|
365
|
+
`- Labels: ${result.labels.map((label) => `\`${label}\``).join(", ")}`,
|
|
366
|
+
`- Label mailboxes: ${result.label_mailboxes.map((label) => `\`${label}\``).join(", ")}`,
|
|
367
|
+
].join("\n");
|
|
368
|
+
}
|
|
369
|
+
|
|
299
370
|
function formatImportSummary(
|
|
300
371
|
result: {
|
|
301
372
|
workspace_root: string;
|
|
@@ -536,6 +607,234 @@ export default function registerProtonBridgeExtension(pi: ExtensionAPI) {
|
|
|
536
607
|
renderResult: renderToolResult,
|
|
537
608
|
});
|
|
538
609
|
|
|
610
|
+
pi.registerTool({
|
|
611
|
+
name: "protonmail_create_draft",
|
|
612
|
+
label: "ProtonMail Create Draft",
|
|
613
|
+
description: "Create a Proton Mail draft through Bridge IMAP APPEND",
|
|
614
|
+
promptSnippet: "Create a draft with optional attachments in the Drafts mailbox",
|
|
615
|
+
promptGuidelines: [
|
|
616
|
+
"Use from when the sender must be explicit; otherwise the active profile default_from is used.",
|
|
617
|
+
"Attachments are local file paths and are embedded into a multipart MIME message before IMAP APPEND.",
|
|
618
|
+
],
|
|
619
|
+
parameters: Type.Object({
|
|
620
|
+
from: Type.Optional(
|
|
621
|
+
Type.String({ description: "Sender address; defaults to profile default_from" }),
|
|
622
|
+
),
|
|
623
|
+
to: Type.Array(Type.String(), { description: "Recipient email addresses" }),
|
|
624
|
+
cc: Type.Optional(Type.Array(Type.String(), { description: "Cc recipient email addresses" })),
|
|
625
|
+
bcc: Type.Optional(
|
|
626
|
+
Type.Array(Type.String(), { description: "Bcc recipient email addresses" }),
|
|
627
|
+
),
|
|
628
|
+
subject: Type.String({ description: "Email subject" }),
|
|
629
|
+
body: Type.String({ description: "Plain-text email body" }),
|
|
630
|
+
attachments: Type.Optional(
|
|
631
|
+
Type.Array(Type.String(), { description: "Local file paths to attach" }),
|
|
632
|
+
),
|
|
633
|
+
draftsMailbox: Type.Optional(
|
|
634
|
+
Type.String({ description: "Drafts mailbox name; defaults to Drafts" }),
|
|
635
|
+
),
|
|
636
|
+
}),
|
|
637
|
+
async execute(
|
|
638
|
+
_id: string,
|
|
639
|
+
params: {
|
|
640
|
+
from?: string;
|
|
641
|
+
to: string[];
|
|
642
|
+
cc?: string[];
|
|
643
|
+
bcc?: string[];
|
|
644
|
+
subject: string;
|
|
645
|
+
body: string;
|
|
646
|
+
attachments?: string[];
|
|
647
|
+
draftsMailbox?: string;
|
|
648
|
+
},
|
|
649
|
+
_signal: AbortSignal,
|
|
650
|
+
_onUpdate: unknown,
|
|
651
|
+
ctx: ToolContext,
|
|
652
|
+
) {
|
|
653
|
+
const profile = await resolveProtonMailActiveProfile(ctx.cwd);
|
|
654
|
+
const config = await getProtonBridgeConfig(profile.policy.default_mailbox);
|
|
655
|
+
if (!config.username || !config.password)
|
|
656
|
+
throw new Error(protonMailSetupHint(profile.profile));
|
|
657
|
+
const result = await runProtonBridgeCreateDraft(config, {
|
|
658
|
+
cwd: ctx.cwd,
|
|
659
|
+
from: resolveOutgoingFrom(params.from, profile),
|
|
660
|
+
to: params.to,
|
|
661
|
+
cc: params.cc,
|
|
662
|
+
bcc: params.bcc,
|
|
663
|
+
subject: params.subject,
|
|
664
|
+
body: params.body,
|
|
665
|
+
attachments: params.attachments,
|
|
666
|
+
draftsMailbox: params.draftsMailbox,
|
|
667
|
+
});
|
|
668
|
+
return {
|
|
669
|
+
content: [{ type: "text", text: trimText(formatCreateDraftSummary(result), 160, 16000) }],
|
|
670
|
+
details: result,
|
|
671
|
+
};
|
|
672
|
+
},
|
|
673
|
+
renderCall(args: { subject: string; to: string[] }, theme: Theme) {
|
|
674
|
+
return new Text(
|
|
675
|
+
`${theme.fg("toolTitle", theme.bold("protonmail_create_draft "))}${theme.fg("dim", `${args.subject} → ${args.to.join(", ")}`)}`,
|
|
676
|
+
0,
|
|
677
|
+
0,
|
|
678
|
+
);
|
|
679
|
+
},
|
|
680
|
+
renderResult: renderToolResult,
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
pi.registerTool({
|
|
684
|
+
name: "protonmail_send",
|
|
685
|
+
label: "ProtonMail Send",
|
|
686
|
+
description: "Send a Proton Mail message through Bridge SMTP",
|
|
687
|
+
promptSnippet: "Send a message with optional attachments through Proton Bridge SMTP",
|
|
688
|
+
promptGuidelines: [
|
|
689
|
+
"Use from when the sender must be explicit; otherwise the active profile default_from is used.",
|
|
690
|
+
"Use saveToMailbox only when a sent or issued copy should also be appended to an IMAP folder.",
|
|
691
|
+
],
|
|
692
|
+
parameters: Type.Object({
|
|
693
|
+
from: Type.Optional(
|
|
694
|
+
Type.String({ description: "Sender address; defaults to profile default_from" }),
|
|
695
|
+
),
|
|
696
|
+
to: Type.Array(Type.String(), { description: "Recipient email addresses" }),
|
|
697
|
+
cc: Type.Optional(Type.Array(Type.String(), { description: "Cc recipient email addresses" })),
|
|
698
|
+
bcc: Type.Optional(
|
|
699
|
+
Type.Array(Type.String(), { description: "Bcc recipient email addresses" }),
|
|
700
|
+
),
|
|
701
|
+
subject: Type.String({ description: "Email subject" }),
|
|
702
|
+
body: Type.String({ description: "Plain-text email body" }),
|
|
703
|
+
attachments: Type.Optional(
|
|
704
|
+
Type.Array(Type.String(), { description: "Local file paths to attach" }),
|
|
705
|
+
),
|
|
706
|
+
saveToMailbox: Type.Optional(
|
|
707
|
+
Type.String({ description: "Optional mailbox for appending a sent copy" }),
|
|
708
|
+
),
|
|
709
|
+
}),
|
|
710
|
+
async execute(
|
|
711
|
+
_id: string,
|
|
712
|
+
params: {
|
|
713
|
+
from?: string;
|
|
714
|
+
to: string[];
|
|
715
|
+
cc?: string[];
|
|
716
|
+
bcc?: string[];
|
|
717
|
+
subject: string;
|
|
718
|
+
body: string;
|
|
719
|
+
attachments?: string[];
|
|
720
|
+
saveToMailbox?: string;
|
|
721
|
+
},
|
|
722
|
+
_signal: AbortSignal,
|
|
723
|
+
_onUpdate: unknown,
|
|
724
|
+
ctx: ToolContext,
|
|
725
|
+
) {
|
|
726
|
+
const profile = await resolveProtonMailActiveProfile(ctx.cwd);
|
|
727
|
+
const config = await getProtonBridgeConfig(profile.policy.default_mailbox);
|
|
728
|
+
if (!config.username || !config.password)
|
|
729
|
+
throw new Error(protonMailSetupHint(profile.profile));
|
|
730
|
+
const result = await runProtonBridgeSendMessage(config, {
|
|
731
|
+
cwd: ctx.cwd,
|
|
732
|
+
from: resolveOutgoingFrom(params.from, profile),
|
|
733
|
+
to: params.to,
|
|
734
|
+
cc: params.cc,
|
|
735
|
+
bcc: params.bcc,
|
|
736
|
+
subject: params.subject,
|
|
737
|
+
body: params.body,
|
|
738
|
+
attachments: params.attachments,
|
|
739
|
+
saveToMailbox: params.saveToMailbox,
|
|
740
|
+
});
|
|
741
|
+
return {
|
|
742
|
+
content: [{ type: "text", text: trimText(formatSendSummary(result), 160, 16000) }],
|
|
743
|
+
details: result,
|
|
744
|
+
};
|
|
745
|
+
},
|
|
746
|
+
renderCall(args: { subject: string; to: string[] }, theme: Theme) {
|
|
747
|
+
return new Text(
|
|
748
|
+
`${theme.fg("toolTitle", theme.bold("protonmail_send "))}${theme.fg("dim", `${args.subject} → ${args.to.join(", ")}`)}`,
|
|
749
|
+
0,
|
|
750
|
+
0,
|
|
751
|
+
);
|
|
752
|
+
},
|
|
753
|
+
renderResult: renderToolResult,
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
pi.registerTool({
|
|
757
|
+
name: "protonmail_move_message",
|
|
758
|
+
label: "ProtonMail Move Message",
|
|
759
|
+
description: "Move a Proton Bridge message between IMAP mailboxes",
|
|
760
|
+
promptSnippet: "Move a message UID from one Proton folder to another",
|
|
761
|
+
promptGuidelines: [
|
|
762
|
+
"Use protonmail_list_messages first when you need to identify the source mailbox UID.",
|
|
763
|
+
"Pass the exact mailbox names returned by Proton Bridge.",
|
|
764
|
+
],
|
|
765
|
+
parameters: Type.Object({
|
|
766
|
+
mailbox: Type.String({ description: "Source mailbox name" }),
|
|
767
|
+
uid: Type.String({ description: "Message UID in the source mailbox" }),
|
|
768
|
+
destination: Type.String({ description: "Destination mailbox name" }),
|
|
769
|
+
}),
|
|
770
|
+
async execute(
|
|
771
|
+
_id: string,
|
|
772
|
+
params: { mailbox: string; uid: string; destination: string },
|
|
773
|
+
_signal: AbortSignal,
|
|
774
|
+
_onUpdate: unknown,
|
|
775
|
+
ctx: ToolContext,
|
|
776
|
+
) {
|
|
777
|
+
const profile = await resolveProtonMailActiveProfile(ctx.cwd);
|
|
778
|
+
const config = await getProtonBridgeConfig(profile.policy.default_mailbox);
|
|
779
|
+
if (!config.username || !config.password)
|
|
780
|
+
throw new Error(protonMailSetupHint(profile.profile));
|
|
781
|
+
const result = await runProtonBridgeMoveMessage(config, params);
|
|
782
|
+
return {
|
|
783
|
+
content: [{ type: "text", text: trimText(formatMoveSummary(result), 160, 16000) }],
|
|
784
|
+
details: result,
|
|
785
|
+
};
|
|
786
|
+
},
|
|
787
|
+
renderCall(args: { mailbox: string; uid: string; destination: string }, theme: Theme) {
|
|
788
|
+
return new Text(
|
|
789
|
+
`${theme.fg("toolTitle", theme.bold("protonmail_move_message "))}${theme.fg("dim", `${args.mailbox} UID ${args.uid} → ${args.destination}`)}`,
|
|
790
|
+
0,
|
|
791
|
+
0,
|
|
792
|
+
);
|
|
793
|
+
},
|
|
794
|
+
renderResult: renderToolResult,
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
pi.registerTool({
|
|
798
|
+
name: "protonmail_apply_labels",
|
|
799
|
+
label: "ProtonMail Apply Labels",
|
|
800
|
+
description: "Apply Proton labels to a message through Bridge IMAP copy",
|
|
801
|
+
promptSnippet: "Apply one or more Proton labels to a message UID",
|
|
802
|
+
promptGuidelines: [
|
|
803
|
+
"Use protonmail_list_mailboxes when you need the exact label mailbox names returned by Proton Bridge.",
|
|
804
|
+
"Bare label names are resolved to matching mailboxes or Labels/<name> when available.",
|
|
805
|
+
],
|
|
806
|
+
parameters: Type.Object({
|
|
807
|
+
mailbox: Type.String({ description: "Source mailbox name" }),
|
|
808
|
+
uid: Type.String({ description: "Message UID in the source mailbox" }),
|
|
809
|
+
labels: Type.Array(Type.String(), { description: "Labels or label mailbox paths to apply" }),
|
|
810
|
+
}),
|
|
811
|
+
async execute(
|
|
812
|
+
_id: string,
|
|
813
|
+
params: { mailbox: string; uid: string; labels: string[] },
|
|
814
|
+
_signal: AbortSignal,
|
|
815
|
+
_onUpdate: unknown,
|
|
816
|
+
ctx: ToolContext,
|
|
817
|
+
) {
|
|
818
|
+
const profile = await resolveProtonMailActiveProfile(ctx.cwd);
|
|
819
|
+
const config = await getProtonBridgeConfig(profile.policy.default_mailbox);
|
|
820
|
+
if (!config.username || !config.password)
|
|
821
|
+
throw new Error(protonMailSetupHint(profile.profile));
|
|
822
|
+
const result = await runProtonBridgeApplyLabels(config, params);
|
|
823
|
+
return {
|
|
824
|
+
content: [{ type: "text", text: trimText(formatApplyLabelsSummary(result), 160, 16000) }],
|
|
825
|
+
details: result,
|
|
826
|
+
};
|
|
827
|
+
},
|
|
828
|
+
renderCall(args: { mailbox: string; uid: string; labels: string[] }, theme: Theme) {
|
|
829
|
+
return new Text(
|
|
830
|
+
`${theme.fg("toolTitle", theme.bold("protonmail_apply_labels "))}${theme.fg("dim", `${args.mailbox} UID ${args.uid} + ${args.labels.join(", ")}`)}`,
|
|
831
|
+
0,
|
|
832
|
+
0,
|
|
833
|
+
);
|
|
834
|
+
},
|
|
835
|
+
renderResult: renderToolResult,
|
|
836
|
+
});
|
|
837
|
+
|
|
539
838
|
pi.registerTool({
|
|
540
839
|
name: "protonmail_import_attachments",
|
|
541
840
|
label: "ProtonMail Import Attachments",
|
package/src/types.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface ProtonMailProfilePolicy {
|
|
|
15
15
|
mailbox_filter?: string;
|
|
16
16
|
default_period?: string;
|
|
17
17
|
import_workspace_root?: string;
|
|
18
|
+
default_from?: string;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export interface ProtonMailWorkspaceConfig {
|
|
@@ -82,6 +83,42 @@ export interface MessageListResult {
|
|
|
82
83
|
messages: MessageInfo[];
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
export interface CreateDraftResult {
|
|
87
|
+
mailbox: string;
|
|
88
|
+
uid?: string;
|
|
89
|
+
from: string;
|
|
90
|
+
to: string[];
|
|
91
|
+
cc?: string[];
|
|
92
|
+
bcc?: string[];
|
|
93
|
+
subject: string;
|
|
94
|
+
attachment_count: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface SendMessageResult {
|
|
98
|
+
from: string;
|
|
99
|
+
to: string[];
|
|
100
|
+
cc?: string[];
|
|
101
|
+
bcc?: string[];
|
|
102
|
+
subject: string;
|
|
103
|
+
attachment_count: number;
|
|
104
|
+
message_id?: string;
|
|
105
|
+
saved_to_mailbox?: string;
|
|
106
|
+
saved_uid?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface MoveMessageResult {
|
|
110
|
+
uid: string;
|
|
111
|
+
source: string;
|
|
112
|
+
destination: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface ApplyLabelsResult {
|
|
116
|
+
uid: string;
|
|
117
|
+
mailbox: string;
|
|
118
|
+
labels: string[];
|
|
119
|
+
label_mailboxes: string[];
|
|
120
|
+
}
|
|
121
|
+
|
|
85
122
|
export type CommandContext = Pick<ExtensionCommandContext, "cwd" | "hasUI" | "ui">;
|
|
86
123
|
|
|
87
124
|
export interface ToolContext {
|