@bobfrankston/iflow-direct 0.1.39 → 0.1.40
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/imap-compat.d.ts +13 -2
- package/imap-compat.js +15 -2
- package/imap-native.d.ts +18 -0
- package/imap-native.js +48 -0
- package/imap-protocol.d.ts +15 -0
- package/imap-protocol.js +37 -0
- package/package.json +1 -1
package/imap-compat.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { type NativeFolder } from "./imap-native.js";
|
|
|
7
7
|
import { FetchedMessage } from "./fetched-message.js";
|
|
8
8
|
import type { ImapClientConfig } from "./types.js";
|
|
9
9
|
import type { TransportFactory } from "./transport.js";
|
|
10
|
+
import type * as proto from "./imap-protocol.js";
|
|
10
11
|
/** Special folder detection result */
|
|
11
12
|
export interface SpecialFolders {
|
|
12
13
|
inbox?: string;
|
|
@@ -102,8 +103,18 @@ export declare class CompatImapClient {
|
|
|
102
103
|
createmailbox(name: string): Promise<void>;
|
|
103
104
|
/** Append a message to a mailbox */
|
|
104
105
|
appendMessage(mailbox: string, message: string | Uint8Array, flags?: string[]): Promise<number | null>;
|
|
105
|
-
/**
|
|
106
|
-
|
|
106
|
+
/** Cached CAPABILITY set parsed at connect/login. Callers gate
|
|
107
|
+
* optional features (NOTIFY, QRESYNC, MOVE, ...) on this. */
|
|
108
|
+
getCapabilities(): Set<string>;
|
|
109
|
+
/** Watch a mailbox for new messages (IDLE). Optionally engage RFC 5465
|
|
110
|
+
* NOTIFY so the server also pushes STATUS responses for non-selected
|
|
111
|
+
* mailboxes named in `opts.notifySpec` — `opts.onMailboxStatus` fires
|
|
112
|
+
* for each. Capability check is the caller's responsibility; pass
|
|
113
|
+
* notifySpec only when `getCapabilities().has("NOTIFY")`. */
|
|
114
|
+
watchMailbox(mailbox: string, onNew: (count: number) => void, opts?: {
|
|
115
|
+
notifySpec?: string;
|
|
116
|
+
onMailboxStatus?: (mailbox: string, data: proto.StatusData) => void;
|
|
117
|
+
}): Promise<() => Promise<void>>;
|
|
107
118
|
/** Copy a message to another server (cross-account) */
|
|
108
119
|
moveMessageToServer(msg: any, fromMailbox: string, targetClient: CompatImapClient, toMailbox: string): Promise<void>;
|
|
109
120
|
/** Rename a mailbox (via native access) */
|
package/imap-compat.js
CHANGED
|
@@ -274,10 +274,23 @@ export class CompatImapClient {
|
|
|
274
274
|
const data = typeof message === "string" ? message : new TextDecoder().decode(message);
|
|
275
275
|
return this.native.appendMessage(mailbox, data, flags);
|
|
276
276
|
}
|
|
277
|
-
/**
|
|
278
|
-
|
|
277
|
+
/** Cached CAPABILITY set parsed at connect/login. Callers gate
|
|
278
|
+
* optional features (NOTIFY, QRESYNC, MOVE, ...) on this. */
|
|
279
|
+
getCapabilities() {
|
|
280
|
+
return this.native.getCapabilities();
|
|
281
|
+
}
|
|
282
|
+
/** Watch a mailbox for new messages (IDLE). Optionally engage RFC 5465
|
|
283
|
+
* NOTIFY so the server also pushes STATUS responses for non-selected
|
|
284
|
+
* mailboxes named in `opts.notifySpec` — `opts.onMailboxStatus` fires
|
|
285
|
+
* for each. Capability check is the caller's responsibility; pass
|
|
286
|
+
* notifySpec only when `getCapabilities().has("NOTIFY")`. */
|
|
287
|
+
async watchMailbox(mailbox, onNew, opts) {
|
|
279
288
|
await this.ensureConnected();
|
|
280
289
|
await this.native.select(mailbox);
|
|
290
|
+
if (opts?.onMailboxStatus)
|
|
291
|
+
this.native.onMailboxStatus = opts.onMailboxStatus;
|
|
292
|
+
if (opts?.notifySpec)
|
|
293
|
+
await this.native.notify(opts.notifySpec);
|
|
281
294
|
return this.native.startIdle(onNew);
|
|
282
295
|
}
|
|
283
296
|
/** Copy a message to another server (cross-account) */
|
package/imap-native.d.ts
CHANGED
|
@@ -77,6 +77,11 @@ export declare class NativeImapClient {
|
|
|
77
77
|
private idleTag;
|
|
78
78
|
private idleCallback;
|
|
79
79
|
private idleRefreshTimer;
|
|
80
|
+
/** RFC 5465 NOTIFY: fires on unsolicited STATUS responses for non-selected
|
|
81
|
+
* mailboxes (the server pushes these when the client has issued NOTIFY
|
|
82
|
+
* SET with a PERSONAL group). Distinct from `idleCallback` which only
|
|
83
|
+
* fires for EXISTS on the currently-selected mailbox. */
|
|
84
|
+
onMailboxStatus: ((mailbox: string, data: proto.StatusData) => void) | null;
|
|
80
85
|
/** Set by startIdle's stop closure so auto-suspend knows not to resume after a command finishes. */
|
|
81
86
|
private idleStopped;
|
|
82
87
|
private verbose;
|
|
@@ -104,6 +109,11 @@ export declare class NativeImapClient {
|
|
|
104
109
|
private authenticate;
|
|
105
110
|
private starttls;
|
|
106
111
|
capability(): Promise<Set<string>>;
|
|
112
|
+
/** Return the cached CAPABILITY set parsed at connect/login time. Callers
|
|
113
|
+
* use this to gate optional code paths (NOTIFY, QRESYNC, MOVE, etc.)
|
|
114
|
+
* without re-issuing CAPABILITY. Returns a defensive copy so callers
|
|
115
|
+
* can't mutate internal state. */
|
|
116
|
+
getCapabilities(): Set<string>;
|
|
107
117
|
private parseCapabilities;
|
|
108
118
|
logout(): Promise<void>;
|
|
109
119
|
select(mailbox: string): Promise<MailboxInfo>;
|
|
@@ -179,6 +189,14 @@ export declare class NativeImapClient {
|
|
|
179
189
|
expunge(): Promise<void>;
|
|
180
190
|
/** Append a message to a mailbox */
|
|
181
191
|
appendMessage(mailbox: string, message: string | Uint8Array, flags?: string[]): Promise<number | null>;
|
|
192
|
+
/** Issue NOTIFY SET so the server starts pushing unsolicited STATUS
|
|
193
|
+
* responses for the mailboxes named in `spec` (beyond the currently
|
|
194
|
+
* selected one). Capability gated — callers must check
|
|
195
|
+
* `getCapabilities().has("NOTIFY")` before calling. Set
|
|
196
|
+
* `onMailboxStatus` before issuing if you want to react. Issue AFTER
|
|
197
|
+
* SELECT and BEFORE startIdle — the server holds the spec for the
|
|
198
|
+
* lifetime of the connection. */
|
|
199
|
+
notify(spec: string): Promise<void>;
|
|
182
200
|
startIdle(onNewMail: (count: number) => void): Promise<() => Promise<void>>;
|
|
183
201
|
/**
|
|
184
202
|
* If IDLE is currently active, send DONE and wait for its tagged OK so the
|
package/imap-native.js
CHANGED
|
@@ -40,6 +40,11 @@ export class NativeImapClient {
|
|
|
40
40
|
idleTag = null;
|
|
41
41
|
idleCallback = null;
|
|
42
42
|
idleRefreshTimer = null;
|
|
43
|
+
/** RFC 5465 NOTIFY: fires on unsolicited STATUS responses for non-selected
|
|
44
|
+
* mailboxes (the server pushes these when the client has issued NOTIFY
|
|
45
|
+
* SET with a PERSONAL group). Distinct from `idleCallback` which only
|
|
46
|
+
* fires for EXISTS on the currently-selected mailbox. */
|
|
47
|
+
onMailboxStatus = null;
|
|
43
48
|
/** Set by startIdle's stop closure so auto-suspend knows not to resume after a command finishes. */
|
|
44
49
|
idleStopped = true;
|
|
45
50
|
verbose;
|
|
@@ -224,6 +229,13 @@ export class NativeImapClient {
|
|
|
224
229
|
}
|
|
225
230
|
return this.capabilities;
|
|
226
231
|
}
|
|
232
|
+
/** Return the cached CAPABILITY set parsed at connect/login time. Callers
|
|
233
|
+
* use this to gate optional code paths (NOTIFY, QRESYNC, MOVE, etc.)
|
|
234
|
+
* without re-issuing CAPABILITY. Returns a defensive copy so callers
|
|
235
|
+
* can't mutate internal state. */
|
|
236
|
+
getCapabilities() {
|
|
237
|
+
return new Set(this.capabilities);
|
|
238
|
+
}
|
|
227
239
|
parseCapabilities(text) {
|
|
228
240
|
const caps = text.replace(/^CAPABILITY\s*/i, "").split(/\s+/);
|
|
229
241
|
this.capabilities.clear();
|
|
@@ -623,6 +635,22 @@ export class NativeImapClient {
|
|
|
623
635
|
const uidMatch = tagged.text.match(/APPENDUID\s+\d+\s+(\d+)/i);
|
|
624
636
|
return uidMatch ? parseInt(uidMatch[1]) : null;
|
|
625
637
|
}
|
|
638
|
+
// ── NOTIFY (RFC 5465) ──
|
|
639
|
+
/** Issue NOTIFY SET so the server starts pushing unsolicited STATUS
|
|
640
|
+
* responses for the mailboxes named in `spec` (beyond the currently
|
|
641
|
+
* selected one). Capability gated — callers must check
|
|
642
|
+
* `getCapabilities().has("NOTIFY")` before calling. Set
|
|
643
|
+
* `onMailboxStatus` before issuing if you want to react. Issue AFTER
|
|
644
|
+
* SELECT and BEFORE startIdle — the server holds the spec for the
|
|
645
|
+
* lifetime of the connection. */
|
|
646
|
+
async notify(spec) {
|
|
647
|
+
const tag = proto.nextTag();
|
|
648
|
+
const responses = await this.sendCommand(tag, proto.notifyCommand(tag, spec));
|
|
649
|
+
const tagged = responses.find(r => r.tag === tag);
|
|
650
|
+
if (!tagged || tagged.type !== "OK") {
|
|
651
|
+
throw new Error(`NOTIFY SET failed: ${tagged?.text || "unknown"}`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
626
654
|
// ── IDLE ──
|
|
627
655
|
async startIdle(onNewMail) {
|
|
628
656
|
this.idleCallback = onNewMail;
|
|
@@ -1156,6 +1184,26 @@ export class NativeImapClient {
|
|
|
1156
1184
|
}
|
|
1157
1185
|
continue;
|
|
1158
1186
|
}
|
|
1187
|
+
// RFC 5465 NOTIFY: unsolicited STATUS responses for non-selected
|
|
1188
|
+
// mailboxes arrive when the server has accepted a NOTIFY SET that
|
|
1189
|
+
// included a PERSONAL (or other) event group. Only route to the
|
|
1190
|
+
// callback when IDLE is active — a STATUS response outside IDLE
|
|
1191
|
+
// is the result of a direct STATUS command and belongs to the
|
|
1192
|
+
// pending command's response set (the fall-through below).
|
|
1193
|
+
if (this.idleTag && resp.tag === "*" && resp.type === "STATUS" && this.onMailboxStatus) {
|
|
1194
|
+
const parsed = proto.parseStatusResponseFull(resp.text);
|
|
1195
|
+
if (parsed) {
|
|
1196
|
+
try {
|
|
1197
|
+
this.onMailboxStatus(parsed.mailbox, parsed.data);
|
|
1198
|
+
}
|
|
1199
|
+
catch (err) {
|
|
1200
|
+
if (this.verbose)
|
|
1201
|
+
console.error(` [imap] onMailboxStatus threw: ${err.message}`);
|
|
1202
|
+
}
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
// Parse failed — fall through so the response isn't silently dropped.
|
|
1206
|
+
}
|
|
1159
1207
|
// Collect untagged responses for the pending command
|
|
1160
1208
|
if (resp.tag === "*" && this.pendingCommand) {
|
|
1161
1209
|
this.pendingCommand.responses.push(resp);
|
package/imap-protocol.d.ts
CHANGED
|
@@ -95,6 +95,13 @@ export declare function moveCommand(tag: string, uid: number, destination: strin
|
|
|
95
95
|
export declare function appendCommand(tag: string, mailbox: string, flags: string[], size: number): string;
|
|
96
96
|
/** Build IDLE command */
|
|
97
97
|
export declare function idleCommand(tag: string): string;
|
|
98
|
+
/** Build NOTIFY SET command (RFC 5465). `spec` is the full event group list,
|
|
99
|
+
* e.g. "(SELECTED (MessageNew MessageExpunge FlagChange)) (PERSONAL
|
|
100
|
+
* (MessageNew MessageExpunge FlagChange MailboxName))" — the server then
|
|
101
|
+
* pushes unsolicited STATUS responses for non-selected mailboxes in the
|
|
102
|
+
* spec while the connection is idle. Capability gated: only issue when
|
|
103
|
+
* CAPABILITY response contains NOTIFY. */
|
|
104
|
+
export declare function notifyCommand(tag: string, spec: string): string;
|
|
98
105
|
/** Build DONE command (ends IDLE) */
|
|
99
106
|
export declare function doneCommand(): string;
|
|
100
107
|
/** Build STARTTLS command */
|
|
@@ -117,6 +124,14 @@ export declare function parseResponseLine(line: string): ImapResponse;
|
|
|
117
124
|
export declare function parseListResponse(text: string): ListData | null;
|
|
118
125
|
/** Parse STATUS response: * STATUS "mailbox" (MESSAGES n UIDNEXT n ...) */
|
|
119
126
|
export declare function parseStatusResponse(text: string): StatusData | null;
|
|
127
|
+
/** Parse a STATUS response keeping the mailbox name. Used by NOTIFY's
|
|
128
|
+
* unsolicited STATUS pushes where the mailbox identifies which folder
|
|
129
|
+
* changed — `parseStatusResponse` discards that. Mailbox may be quoted
|
|
130
|
+
* ("Sent") or atom-shaped (Sent); the regex handles both. */
|
|
131
|
+
export declare function parseStatusResponseFull(text: string): {
|
|
132
|
+
mailbox: string;
|
|
133
|
+
data: StatusData;
|
|
134
|
+
} | null;
|
|
120
135
|
/** Parse UID SEARCH response: * SEARCH 1 2 3 4 5 */
|
|
121
136
|
export declare function parseSearchResponse(text: string): number[];
|
|
122
137
|
/** Parse FLAGS from a FETCH or SELECT response */
|
package/imap-protocol.js
CHANGED
|
@@ -79,6 +79,15 @@ export function appendCommand(tag, mailbox, flags, size) {
|
|
|
79
79
|
export function idleCommand(tag) {
|
|
80
80
|
return buildCommand(tag, "IDLE");
|
|
81
81
|
}
|
|
82
|
+
/** Build NOTIFY SET command (RFC 5465). `spec` is the full event group list,
|
|
83
|
+
* e.g. "(SELECTED (MessageNew MessageExpunge FlagChange)) (PERSONAL
|
|
84
|
+
* (MessageNew MessageExpunge FlagChange MailboxName))" — the server then
|
|
85
|
+
* pushes unsolicited STATUS responses for non-selected mailboxes in the
|
|
86
|
+
* spec while the connection is idle. Capability gated: only issue when
|
|
87
|
+
* CAPABILITY response contains NOTIFY. */
|
|
88
|
+
export function notifyCommand(tag, spec) {
|
|
89
|
+
return buildCommand(tag, `NOTIFY SET ${spec}`);
|
|
90
|
+
}
|
|
82
91
|
/** Build DONE command (ends IDLE) */
|
|
83
92
|
export function doneCommand() {
|
|
84
93
|
return "DONE\r\n";
|
|
@@ -182,6 +191,34 @@ export function parseStatusResponse(text) {
|
|
|
182
191
|
}
|
|
183
192
|
return data;
|
|
184
193
|
}
|
|
194
|
+
/** Parse a STATUS response keeping the mailbox name. Used by NOTIFY's
|
|
195
|
+
* unsolicited STATUS pushes where the mailbox identifies which folder
|
|
196
|
+
* changed — `parseStatusResponse` discards that. Mailbox may be quoted
|
|
197
|
+
* ("Sent") or atom-shaped (Sent); the regex handles both. */
|
|
198
|
+
export function parseStatusResponseFull(text) {
|
|
199
|
+
// Match: "quoted name" (data) OR atomname (data)
|
|
200
|
+
const m = text.match(/^"((?:[^"\\]|\\.)*)"\s*\((.*)\)\s*$/) || text.match(/^(\S+)\s*\((.*)\)\s*$/);
|
|
201
|
+
if (!m)
|
|
202
|
+
return null;
|
|
203
|
+
const mailbox = m[1].replace(/\\(.)/g, "$1");
|
|
204
|
+
const data = {};
|
|
205
|
+
const pairs = m[2].split(/\s+/);
|
|
206
|
+
for (let i = 0; i < pairs.length - 1; i += 2) {
|
|
207
|
+
const key = pairs[i].toUpperCase();
|
|
208
|
+
const val = parseInt(pairs[i + 1]);
|
|
209
|
+
if (key === "MESSAGES")
|
|
210
|
+
data.messages = val;
|
|
211
|
+
else if (key === "RECENT")
|
|
212
|
+
data.recent = val;
|
|
213
|
+
else if (key === "UIDNEXT")
|
|
214
|
+
data.uidNext = val;
|
|
215
|
+
else if (key === "UIDVALIDITY")
|
|
216
|
+
data.uidValidity = val;
|
|
217
|
+
else if (key === "UNSEEN")
|
|
218
|
+
data.unseen = val;
|
|
219
|
+
}
|
|
220
|
+
return { mailbox, data };
|
|
221
|
+
}
|
|
185
222
|
/** Parse UID SEARCH response: * SEARCH 1 2 3 4 5 */
|
|
186
223
|
export function parseSearchResponse(text) {
|
|
187
224
|
if (!text.trim())
|