@xmanrui/dsh-im 4.22.0 → 4.23.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/README.en.md +3 -3
- package/README.md +3 -3
- package/lib/client.js +2505 -1152
- package/lib/index.js +290 -299
- package/package.json +8 -1
- package/plugin-src/client/channel-card-meta.js +2 -0
- package/plugin-src/client/channel-logos.js +11 -0
- package/plugin-src/client/channels/dingtalk/api.js +5 -1
- package/plugin-src/client/channels/dingtalk/index.js +15 -13
- package/plugin-src/client/channels/email/api.js +52 -0
- package/plugin-src/client/channels/email/index.js +728 -0
- package/plugin-src/client/channels/email/styles.js +51 -0
- package/plugin-src/client/channels/feishu/api.js +4 -2
- package/plugin-src/client/channels/feishu/index.js +16 -3
- package/plugin-src/client/channels/imessage/index.js +2 -1
- package/plugin-src/client/channels/office/api.js +2 -0
- package/plugin-src/client/channels/office/index.js +6 -5
- package/plugin-src/client/channels/qq/api.js +7 -0
- package/plugin-src/client/channels/qq/index.js +16 -3
- package/plugin-src/client/channels/shared/token-api.js +5 -1
- package/plugin-src/client/channels/shared/token-channel.js +28 -3
- package/plugin-src/client/channels/slack/index.js +2 -1
- package/plugin-src/client/channels/wecom/api.js +7 -1
- package/plugin-src/client/channels/wecom/index.js +16 -3
- package/plugin-src/client/channels/wecom-app/api.js +6 -1
- package/plugin-src/client/channels/wecom-app/index.js +16 -3
- package/plugin-src/client/channels/weixin/api.js +2 -1
- package/plugin-src/client/channels/weixin/connection-error.js +1 -71
- package/plugin-src/client/channels/whatsapp/api.js +5 -0
- package/plugin-src/client/channels/whatsapp/index.js +16 -3
- package/plugin-src/client/connection-error.js +87 -0
- package/plugin-src/client/global-settings.js +81 -1
- package/plugin-src/client/i18n.js +61 -0
- package/plugin-src/client/index.js +30 -2
- package/plugin-src/client/last-message-error.js +2 -1
- package/plugin-src/client/styles.js +6 -0
- package/plugin-src/host/build.mjs +4 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +2 -0
- package/plugin-src/host/channels/dingtalk/rpc.mjs +7 -4
- package/plugin-src/host/channels/email/availability.mjs +60 -0
- package/plugin-src/host/channels/email/index.mjs +36 -0
- package/plugin-src/host/channels/email/production.mjs +58 -0
- package/plugin-src/host/channels/email/rpc.mjs +163 -0
- package/plugin-src/host/channels/feishu/production.mjs +3 -0
- package/plugin-src/host/channels/feishu/rpc.mjs +7 -4
- package/plugin-src/host/channels/imessage/rpc.mjs +13 -6
- package/plugin-src/host/channels/office/rpc.mjs +3 -1
- package/plugin-src/host/channels/qq/production.mjs +2 -0
- package/plugin-src/host/channels/qq/rpc.mjs +5 -3
- package/plugin-src/host/channels/shared/production.mjs +52 -5
- package/plugin-src/host/channels/shared/rpc.mjs +5 -3
- package/plugin-src/host/channels/shared/startup-error.mjs +4 -3
- package/plugin-src/host/channels/shared/startup.mjs +11 -6
- package/plugin-src/host/channels/slack/production.mjs +2 -0
- package/plugin-src/host/channels/slack/rpc.mjs +5 -3
- package/plugin-src/host/channels/wecom/production.mjs +2 -0
- package/plugin-src/host/channels/wecom/rpc.mjs +5 -3
- package/plugin-src/host/channels/wecom-app/production.mjs +2 -0
- package/plugin-src/host/channels/wecom-app/rpc.mjs +5 -3
- package/plugin-src/host/channels/weixin/production.mjs +2 -0
- package/plugin-src/host/channels/whatsapp/production.mjs +2 -0
- package/plugin-src/host/channels/whatsapp/rpc.mjs +5 -3
- package/plugin-src/host/delivery-adapter.mjs +11 -0
- package/plugin-src/host/image-input-rpc.mjs +24 -0
- package/plugin-src/host/inbound-ttl-rpc.mjs +6 -1
- package/plugin-src/host/index.mjs +3 -0
- package/plugin-src/management-rpc.mjs +12 -2
- package/scripts/verify-package.mjs +3 -1
- package/src/channels/dingtalk/connection-error.mjs +5 -12
- package/src/channels/dingtalk/device-auth.mjs +4 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +6 -4
- package/src/channels/dingtalk/dingtalk-controller.mjs +60 -45
- package/src/channels/dingtalk/dingtalk-runtime.mjs +20 -4
- package/src/channels/discord/discord-api.mjs +3 -3
- package/src/channels/discord/discord-runtime.mjs +18 -12
- package/src/channels/email/config-store.mjs +194 -0
- package/src/channels/email/email-api.mjs +20 -0
- package/src/channels/email/email-bridge.mjs +17 -0
- package/src/channels/email/email-controller.mjs +824 -0
- package/src/channels/email/email-runtime.mjs +692 -0
- package/src/channels/email/harness-client.mjs +7 -0
- package/src/channels/email/mail-format.mjs +91 -0
- package/src/channels/email/state-store.mjs +176 -0
- package/src/channels/email/transport.mjs +64 -0
- package/src/channels/email/transports/agent-mail.mjs +593 -0
- package/src/channels/email/transports/agently-cli.mjs +288 -0
- package/src/channels/email/transports/imap-smtp.mjs +165 -0
- package/src/channels/feishu/bridge.mjs +8 -7
- package/src/channels/feishu/feishu-runtime.mjs +15 -8
- package/src/channels/feishu/group-message-permission-manager.mjs +2 -1
- package/src/channels/feishu/message-utils.mjs +3 -2
- package/src/channels/feishu/multi-bot-controller.mjs +56 -47
- package/src/channels/feishu/registration-manager.mjs +8 -4
- package/src/channels/feishu/repair-manager.mjs +2 -1
- package/src/channels/imessage/runtime.mjs +9 -5
- package/src/channels/office/office-controller.mjs +23 -12
- package/src/channels/office/office-runtime.mjs +6 -3
- package/src/channels/office/office-transport.mjs +1 -0
- package/src/channels/qq/qq-bridge.mjs +7 -5
- package/src/channels/qq/qq-controller.mjs +56 -42
- package/src/channels/qq/qq-runtime.mjs +16 -11
- package/src/channels/shared/bot-workspace-store.mjs +29 -10
- package/src/channels/shared/connection-error.mjs +191 -0
- package/src/channels/shared/connection-test.mjs +3 -1
- package/src/channels/shared/conversation-state-store.mjs +46 -1
- package/src/channels/shared/diagnostic-details.mjs +95 -0
- package/src/channels/shared/harness-client.mjs +63 -18
- package/src/channels/shared/i18n-en/diagnostics.mjs +55 -0
- package/src/channels/shared/i18n-en/email.mjs +25 -0
- package/src/channels/shared/i18n-en/image-input.mjs +17 -0
- package/src/channels/shared/i18n-en/shared-a.mjs +1 -0
- package/src/channels/shared/i18n-en/shared-c.mjs +1 -1
- package/src/channels/shared/i18n-en.mjs +6 -0
- package/src/channels/shared/image-input-policy.mjs +35 -0
- package/src/channels/shared/image-input-settings-store.mjs +60 -0
- package/src/channels/shared/image-input.mjs +124 -0
- package/src/channels/shared/image-prompt.mjs +15 -7
- package/src/channels/shared/message-failure.mjs +12 -0
- package/src/channels/shared/semantic/reply-reference.mjs +3 -3
- package/src/channels/shared/text-harness-bridge.mjs +90 -11
- package/src/channels/shared/token-bot-controller.mjs +52 -47
- package/src/channels/slack/slack-api.mjs +3 -3
- package/src/channels/slack/slack-controller.mjs +59 -51
- package/src/channels/slack/slack-runtime.mjs +18 -12
- package/src/channels/telegram/telegram-api.mjs +2 -2
- package/src/channels/telegram/telegram-runtime.mjs +17 -13
- package/src/channels/wecom/wecom-bridge.mjs +15 -6
- package/src/channels/wecom/wecom-controller.mjs +51 -37
- package/src/channels/wecom/wecom-runtime.mjs +10 -6
- package/src/channels/wecom-app/wecom-app-bridge.mjs +7 -5
- package/src/channels/wecom-app/wecom-app-controller.mjs +50 -37
- package/src/channels/wecom-app/wecom-app-runtime.mjs +2 -1
- package/src/channels/weixin/connection-error.mjs +26 -92
- package/src/channels/weixin/diagnostic-details.mjs +1 -63
- package/src/channels/weixin/weixin-bridge.mjs +7 -5
- package/src/channels/whatsapp/whatsapp-controller.mjs +34 -17
- package/src/channels/whatsapp/whatsapp-runtime.mjs +35 -8
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared mail formatting and thread helpers.
|
|
3
|
+
*
|
|
4
|
+
* These are transport-independent: quote stripping, thread resolution, address
|
|
5
|
+
* normalization, and the TLS-mode derivation used by the IMAP/SMTP transport.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Reply text longer than this is truncated so it survives mail gateways. */
|
|
9
|
+
const MAX_REPLY_CHARS = 100_000;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Quoted-history markers. Mail clients append the whole prior conversation to
|
|
13
|
+
* every reply; leaving it in place would grow the prompt each turn and confuse
|
|
14
|
+
* the model, so the body is cut at the first marker.
|
|
15
|
+
*/
|
|
16
|
+
const QUOTE_MARKERS = [
|
|
17
|
+
/^On .{10,120} wrote:$/mi,
|
|
18
|
+
/^-{2,}\s*Original Message\s*-{2,}$/mi,
|
|
19
|
+
/^在 .{4,60}(写道|寫道)[::]?\s*$/mi,
|
|
20
|
+
/^-{2,}\s*原始邮件\s*-{2,}$/mi,
|
|
21
|
+
/^\s*_{10,}\s*$/m,
|
|
22
|
+
/^From:\s.+$/mi,
|
|
23
|
+
/^发件人[::]\s*.+$/mi,
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/** Cut a mail body at the first quoted-history marker. */
|
|
27
|
+
export function stripQuotedHistory(text) {
|
|
28
|
+
const body = String(text ?? '').replace(/\r\n/g, '\n');
|
|
29
|
+
let cut = body.length;
|
|
30
|
+
for (const marker of QUOTE_MARKERS) {
|
|
31
|
+
const match = marker.exec(body);
|
|
32
|
+
if (match && match.index > 0 && match.index < cut) cut = match.index;
|
|
33
|
+
}
|
|
34
|
+
return body
|
|
35
|
+
.slice(0, cut)
|
|
36
|
+
.split('\n')
|
|
37
|
+
.filter((line) => !/^\s*>/.test(line))
|
|
38
|
+
.join('\n')
|
|
39
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
40
|
+
.trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Derive the transport security mode from the port, which is how mail clients
|
|
45
|
+
* decide in practice: 465 is implicit TLS, 587 and 25 negotiate STARTTLS, and
|
|
46
|
+
* anything else is left to the explicit `secure` flag. Treating 587 as
|
|
47
|
+
* implicit TLS is the classic misconfiguration that makes SMTP hang.
|
|
48
|
+
*/
|
|
49
|
+
export function smtpSecurity(port, secure) {
|
|
50
|
+
if (secure === true) return { secure: true };
|
|
51
|
+
if (secure === false) return { secure: false, requireTLS: true };
|
|
52
|
+
if (port === 465) return { secure: true };
|
|
53
|
+
if (port === 587 || port === 25 || port === 2525) return { secure: false, requireTLS: true };
|
|
54
|
+
return { secure: false };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** IMAP uses implicit TLS on 993 and STARTTLS otherwise. */
|
|
58
|
+
export function imapSecurity(port, secure) {
|
|
59
|
+
if (secure !== undefined) return { secure: secure !== false };
|
|
60
|
+
return { secure: port === 993 };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Collect every message id from a header value (References / In-Reply-To). */
|
|
64
|
+
export function parseMessageIds(value) {
|
|
65
|
+
if (!value) return [];
|
|
66
|
+
const raw = Array.isArray(value) ? value.join(' ') : String(value);
|
|
67
|
+
return (raw.match(/<[^<>@\s]+@[^<>\s]+>/g) ?? []).map((id) => id.trim());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the thread key for a message: an existing conversation is reused when
|
|
72
|
+
* the reply chain points at a message we have seen before, otherwise the
|
|
73
|
+
* message starts a new conversation.
|
|
74
|
+
*/
|
|
75
|
+
export function resolveThreadKey({ messageId, references = [], inReplyTo = [], conversationMap }) {
|
|
76
|
+
const chain = [...parseMessageIds(references), ...parseMessageIds(inReplyTo)];
|
|
77
|
+
for (let index = chain.length - 1; index >= 0; index -= 1) {
|
|
78
|
+
const key = conversationMap.get(chain[index]);
|
|
79
|
+
if (key) return key;
|
|
80
|
+
}
|
|
81
|
+
return parseMessageIds(messageId)[0] ?? `email:${Date.now()}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Lowercase a bare address, keeping only the addr-spec part. */
|
|
85
|
+
export function normalizeAddress(value) {
|
|
86
|
+
const text = String(value ?? '').trim().toLowerCase();
|
|
87
|
+
const angled = /<([^<>]+)>/.exec(text);
|
|
88
|
+
return (angled ? angled[1] : text).trim();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { MAX_REPLY_CHARS };
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { ConversationStateStore } from '../shared/conversation-state-store.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Email adds one need on top of the shared conversation store: mail threads are
|
|
5
|
+
* identified by Message-ID headers, so the inbound chain (References /
|
|
6
|
+
* In-Reply-To) must resolve back to the conversation key we already created.
|
|
7
|
+
* The map is bounded — only the most recent ids are kept, since a thread that
|
|
8
|
+
* has been quiet for thousands of messages is not worth unbounded growth.
|
|
9
|
+
*/
|
|
10
|
+
const MAX_THREAD_IDS = 2_000;
|
|
11
|
+
|
|
12
|
+
/** A conversation key that pins the chat to one existing Harness session. */
|
|
13
|
+
export const BOUND_KEY_PREFIX = 'bound:';
|
|
14
|
+
|
|
15
|
+
export class EmailStateStore extends ConversationStateStore {
|
|
16
|
+
// Loaded from persisted state on first use. Held in memory because threading
|
|
17
|
+
// reads it synchronously, and written back on every change so a restart does
|
|
18
|
+
// not turn a continuing thread into a brand-new conversation.
|
|
19
|
+
#threadIds = null;
|
|
20
|
+
|
|
21
|
+
/** The Message-ID → conversation map, loaded once from persisted state. */
|
|
22
|
+
#threads() {
|
|
23
|
+
if (this.#threadIds) return this.#threadIds;
|
|
24
|
+
const stored = this.extensionState().threadIds;
|
|
25
|
+
const map = new Map();
|
|
26
|
+
if (stored && typeof stored === 'object' && !Array.isArray(stored)) {
|
|
27
|
+
for (const [id, key] of Object.entries(stored)) {
|
|
28
|
+
if (typeof id === 'string' && id && typeof key === 'string' && key) map.set(id, key);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
this.#threadIds = map;
|
|
32
|
+
return map;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Record that `messageId` belongs to `conversationKey`. */
|
|
36
|
+
rememberThreadId(messageId, conversationKey) {
|
|
37
|
+
if (typeof messageId !== 'string' || !messageId || typeof conversationKey !== 'string') return;
|
|
38
|
+
const threads = this.#threads();
|
|
39
|
+
// Re-insert so the freshest ids survive eviction.
|
|
40
|
+
threads.delete(messageId);
|
|
41
|
+
threads.set(messageId, conversationKey);
|
|
42
|
+
while (threads.size > MAX_THREAD_IDS) {
|
|
43
|
+
threads.delete(threads.keys().next().value);
|
|
44
|
+
}
|
|
45
|
+
// Persisted, so a reply arriving after a restart still joins its thread.
|
|
46
|
+
this.extensionState().threadIds = Object.fromEntries(threads);
|
|
47
|
+
void this.persist();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Conversation key previously associated with this Message-ID, if any. */
|
|
51
|
+
conversationForThreadId(messageId) {
|
|
52
|
+
return this.#threads().get(messageId) ?? null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Read-only view used by the threading resolver. */
|
|
56
|
+
get threadMap() {
|
|
57
|
+
return this.#threads();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A QR authorization in flight. Persisted because the code stays valid for
|
|
62
|
+
* ten minutes, which easily outlives a plugin reload — losing it meant an
|
|
63
|
+
* already-completed scan could never be redeemed.
|
|
64
|
+
*/
|
|
65
|
+
pendingAuth() {
|
|
66
|
+
const value = this.extensionState().pendingAuth;
|
|
67
|
+
if (!value || typeof value !== 'object') return null;
|
|
68
|
+
if (typeof value.pollUrl !== 'string' || !value.pollUrl) return null;
|
|
69
|
+
if (!Number.isFinite(value.expiresAt)) return null;
|
|
70
|
+
return { pollUrl: value.pollUrl, expiresAt: value.expiresAt, transport: value.transport };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async setPendingAuth(value) {
|
|
74
|
+
if (!value) {
|
|
75
|
+
delete this.extensionState().pendingAuth;
|
|
76
|
+
} else {
|
|
77
|
+
this.extensionState().pendingAuth = {
|
|
78
|
+
pollUrl: String(value.pollUrl ?? ''),
|
|
79
|
+
expiresAt: Number(value.expiresAt ?? 0),
|
|
80
|
+
...(value.transport ? { transport: String(value.transport) } : {}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
await this.persist();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Fixed-session bindings, both scopes in one document:
|
|
88
|
+
* { account: <sessionId|null>, senders: { <address>: <sessionId> } }
|
|
89
|
+
* A sender entry wins over the account default; with neither set the
|
|
90
|
+
* conversation starts a fresh session per thread (the original behaviour).
|
|
91
|
+
*/
|
|
92
|
+
emailBindings() {
|
|
93
|
+
const bindings = this.extensionState().emailBindings;
|
|
94
|
+
if (!bindings || typeof bindings !== 'object') return { account: null, senders: {} };
|
|
95
|
+
const senders = {};
|
|
96
|
+
if (bindings.senders && typeof bindings.senders === 'object' && !Array.isArray(bindings.senders)) {
|
|
97
|
+
for (const [address, sessionId] of Object.entries(bindings.senders)) {
|
|
98
|
+
if (typeof address === 'string' && address
|
|
99
|
+
&& typeof sessionId === 'string' && sessionId) senders[address] = sessionId;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
account: typeof bindings.account === 'string' && bindings.account ? bindings.account : null,
|
|
104
|
+
senders,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async setEmailBindings(value) {
|
|
109
|
+
const next = value && typeof value === 'object' ? value : {};
|
|
110
|
+
const senders = {};
|
|
111
|
+
if (next.senders && typeof next.senders === 'object' && !Array.isArray(next.senders)) {
|
|
112
|
+
for (const [address, sessionId] of Object.entries(next.senders)) {
|
|
113
|
+
if (typeof address === 'string' && address
|
|
114
|
+
&& typeof sessionId === 'string' && sessionId) senders[address] = sessionId;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
this.extensionState().emailBindings = {
|
|
118
|
+
account: typeof next.account === 'string' && next.account ? next.account : null,
|
|
119
|
+
senders,
|
|
120
|
+
};
|
|
121
|
+
await this.persist();
|
|
122
|
+
return this.emailBindings();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Resolve the session a message should use, honouring the binding order:
|
|
127
|
+
* a sender-specific binding, then the account-wide binding, then null which
|
|
128
|
+
* means "start a new session for this thread".
|
|
129
|
+
*/
|
|
130
|
+
boundSessionFor(senderAddress) {
|
|
131
|
+
const { account, senders } = this.emailBindings();
|
|
132
|
+
const address = typeof senderAddress === 'string' ? senderAddress.trim().toLowerCase() : '';
|
|
133
|
+
if (address && senders[address]) return senders[address];
|
|
134
|
+
return account;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Mail cursors are not always numbers: IMAP addresses messages by an integer
|
|
139
|
+
* UID, while the Agent mailbox pages by an opaque string id. The shared store
|
|
140
|
+
* only accepts integers, so the mailbox keeps its own cursor and returns it
|
|
141
|
+
* through the inherited accessor.
|
|
142
|
+
*/
|
|
143
|
+
cursor() {
|
|
144
|
+
const value = this.extensionState().mailCursor;
|
|
145
|
+
return value === undefined ? null : value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async setCursor(cursor) {
|
|
149
|
+
if (typeof cursor !== 'string' && !Number.isSafeInteger(cursor)) {
|
|
150
|
+
throw new TypeError('Invalid update cursor');
|
|
151
|
+
}
|
|
152
|
+
if (typeof cursor === 'string' && cursor === '') {
|
|
153
|
+
throw new TypeError('Invalid update cursor');
|
|
154
|
+
}
|
|
155
|
+
this.extensionState().mailCursor = cursor;
|
|
156
|
+
await this.persist();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A bound conversation key maps straight to the pinned session. Without this,
|
|
161
|
+
* the resolver would miss the mapping and create a brand-new session even
|
|
162
|
+
* though the user asked for a fixed one. The bridge prefixes the key with the
|
|
163
|
+
* chat kind (`direct:`), so the marker is searched rather than assumed to be
|
|
164
|
+
* at the start.
|
|
165
|
+
*/
|
|
166
|
+
sessionFor(key) {
|
|
167
|
+
if (typeof key === 'string') {
|
|
168
|
+
const marker = key.indexOf(BOUND_KEY_PREFIX);
|
|
169
|
+
if (marker !== -1) {
|
|
170
|
+
const sessionId = key.slice(marker + BOUND_KEY_PREFIX.length);
|
|
171
|
+
if (sessionId) return sessionId;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return super.sessionFor(key);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mailbox transport contract.
|
|
3
|
+
*
|
|
4
|
+
* A transport moves mail; it owns nothing else. Conversation mapping, the
|
|
5
|
+
* sender allowlist, quote stripping, session binding, and loop prevention all
|
|
6
|
+
* live in the runtime and are shared by every transport — so adding a mail
|
|
7
|
+
* protocol means writing one adapter here, not another channel.
|
|
8
|
+
*
|
|
9
|
+
* Implementations:
|
|
10
|
+
* imap-smtp any standard mailbox, polled over IMAP and sent over SMTP
|
|
11
|
+
* agent-mail Tencent Agent Mail (agent.qq.com), HTTP + OAuth, long-polled
|
|
12
|
+
*
|
|
13
|
+
* Required methods:
|
|
14
|
+
* connect() open the underlying connection
|
|
15
|
+
* disconnect() close it; must be safe to call twice
|
|
16
|
+
* latestUid() highest cursor value currently present
|
|
17
|
+
* listMessages({ afterUid, limit, allowSenders })
|
|
18
|
+
* messages newer than the cursor, already
|
|
19
|
+
* filtered to allowlisted senders when a
|
|
20
|
+
* set is supplied
|
|
21
|
+
* sendReply({ to, subject, text, inReplyTo, references, attachments })
|
|
22
|
+
* send inside the originating conversation
|
|
23
|
+
* sendText({ to, subject, text, attachments })
|
|
24
|
+
* send a standalone message
|
|
25
|
+
*
|
|
26
|
+
* A message returned by listMessages() is a mailparser-shaped object with a
|
|
27
|
+
* `uid` added:
|
|
28
|
+
* { uid, messageId, from, to, cc, subject, text, html, attachments, headers }
|
|
29
|
+
* `attachments[]` entries carry { filename, contentType, size, content }.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** Methods every transport must provide. */
|
|
33
|
+
export const TRANSPORT_METHODS = Object.freeze([
|
|
34
|
+
'connect',
|
|
35
|
+
'disconnect',
|
|
36
|
+
'latestUid',
|
|
37
|
+
'listMessages',
|
|
38
|
+
'sendReply',
|
|
39
|
+
'sendText',
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
/** Default reply subject: one "Re:" prefix so threads stay grouped. */
|
|
43
|
+
export function replySubject(subject) {
|
|
44
|
+
const text = String(subject ?? '').trim();
|
|
45
|
+
if (!text) return 'Re: (no subject)';
|
|
46
|
+
return /^re:/i.test(text) ? text : `Re: ${text}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Assert that a value satisfies the transport contract. Called when a
|
|
51
|
+
* transport is constructed so a malformed adapter fails at the boundary
|
|
52
|
+
* rather than part-way through a poll.
|
|
53
|
+
*/
|
|
54
|
+
export function assertTransport(transport, label = 'transport') {
|
|
55
|
+
if (!transport || typeof transport !== 'object') {
|
|
56
|
+
throw new TypeError(`${label} must be an object`);
|
|
57
|
+
}
|
|
58
|
+
for (const method of TRANSPORT_METHODS) {
|
|
59
|
+
if (typeof transport[method] !== 'function') {
|
|
60
|
+
throw new TypeError(`${label} must implement ${method}()`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return transport;
|
|
64
|
+
}
|