@nimbus-dev/connectors 0.2.0 → 0.2.2
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.md +18 -4
- package/connectors/airflow/src/search-filter.ts +2 -15
- package/connectors/airflow/src/server.ts +2 -87
- package/connectors/airflow/src/tools.ts +74 -0
- package/connectors/apple/src/adapters.ts +326 -0
- package/connectors/apple/src/apple-mail-core.ts +4 -8
- package/connectors/apple/src/calendar-tools.ts +28 -8
- package/connectors/apple/src/server.ts +23 -512
- package/connectors/apple/src/tools.ts +18 -44
- package/connectors/argocd/src/server.ts +25 -38
- package/connectors/athena/src/tools.ts +15 -34
- package/connectors/aws/src/server.ts +2 -183
- package/connectors/aws/src/tools.ts +197 -0
- package/connectors/azure/src/server.ts +2 -131
- package/connectors/azure/src/tools.ts +133 -0
- package/connectors/bigeye/src/search-filter.ts +4 -15
- package/connectors/bigeye/src/server.ts +38 -38
- package/connectors/bigquery/src/tools.ts +1 -26
- package/connectors/bitbucket/src/server.ts +2 -244
- package/connectors/bitbucket/src/tools.ts +241 -0
- package/connectors/bitrise/src/server.ts +2 -79
- package/connectors/bitrise/src/tools.ts +76 -0
- package/connectors/canva/src/search-filter.ts +2 -11
- package/connectors/canva/src/server.ts +2 -73
- package/connectors/canva/src/tools.ts +66 -0
- package/connectors/circleci/src/server.ts +2 -176
- package/connectors/circleci/src/tools.ts +193 -0
- package/connectors/cloud-logging/src/tools.ts +14 -27
- package/connectors/cloudwatch/src/tools.ts +15 -34
- package/connectors/codemagic/src/server.ts +2 -70
- package/connectors/codemagic/src/tools.ts +71 -0
- package/connectors/confluence/src/server.ts +2 -301
- package/connectors/confluence/src/tools.ts +320 -0
- package/connectors/dagster/src/server.ts +2 -214
- package/connectors/dagster/src/tools.ts +187 -0
- package/connectors/databricks/src/search-filter.ts +7 -7
- package/connectors/databricks/src/server.ts +2 -77
- package/connectors/databricks/src/tools.ts +66 -0
- package/connectors/datadog/src/server.ts +2 -48
- package/connectors/datadog/src/tools.ts +48 -0
- package/connectors/dataprofile/src/profile.ts +16 -46
- package/connectors/dbt/src/server.ts +2 -92
- package/connectors/dbt/src/tools.ts +80 -0
- package/connectors/dependencytrack/src/server.ts +2 -76
- package/connectors/dependencytrack/src/tools.ts +65 -0
- package/connectors/discord/src/server.ts +2 -85
- package/connectors/discord/src/tools.ts +82 -0
- package/connectors/elasticsearch/src/tools.ts +7 -37
- package/connectors/fastmail/src/jmap-client.ts +202 -0
- package/connectors/fastmail/src/jmap-core.ts +8 -21
- package/connectors/fastmail/src/server.ts +2 -187
- package/connectors/fastmail/src/tools.ts +4 -23
- package/connectors/figma/src/search-filter.ts +2 -11
- package/connectors/figma/src/server.ts +2 -113
- package/connectors/figma/src/tools.ts +99 -0
- package/connectors/firebase/src/server.ts +2 -90
- package/connectors/firebase/src/token.ts +2 -5
- package/connectors/firebase/src/tools.ts +106 -0
- package/connectors/flagsmith/src/server.ts +2 -98
- package/connectors/flagsmith/src/tools.ts +95 -0
- package/connectors/flux/src/server.ts +52 -53
- package/connectors/gcp/src/server.ts +2 -132
- package/connectors/gcp/src/tools.ts +123 -0
- package/connectors/github/src/server.ts +2 -328
- package/connectors/github/src/tools.ts +311 -0
- package/connectors/github-actions/src/server.ts +2 -205
- package/connectors/github-actions/src/tools.ts +209 -0
- package/connectors/gitlab/src/server.ts +2 -384
- package/connectors/gitlab/src/tools.ts +392 -0
- package/connectors/gmail/src/server.ts +2 -270
- package/connectors/gmail/src/tools.ts +209 -0
- package/connectors/google-drive/src/server.ts +2 -524
- package/connectors/google-drive/src/tools.ts +529 -0
- package/connectors/google-meet/src/server.ts +2 -82
- package/connectors/google-meet/src/tools.ts +87 -0
- package/connectors/google-photos/src/server.ts +2 -132
- package/connectors/google-photos/src/tools.ts +138 -0
- package/connectors/grafana/src/server.ts +2 -53
- package/connectors/grafana/src/tools.ts +53 -0
- package/connectors/great-expectations/src/gx-parse.ts +14 -45
- package/connectors/greenhouse/src/server.ts +2 -63
- package/connectors/greenhouse/src/tools.ts +68 -0
- package/connectors/hubspot/src/server.ts +2 -77
- package/connectors/hubspot/src/tools.ts +70 -0
- package/connectors/iac/src/server.ts +2 -137
- package/connectors/iac/src/tools.ts +152 -0
- package/connectors/imap/src/imap-core.ts +29 -82
- package/connectors/imap/src/server.ts +33 -220
- package/connectors/intercom/src/server.ts +2 -61
- package/connectors/intercom/src/tools.ts +58 -0
- package/connectors/jenkins/src/jenkins-api.ts +2 -6
- package/connectors/jenkins/src/server.ts +2 -215
- package/connectors/jenkins/src/tools.ts +240 -0
- package/connectors/jira/src/server.ts +2 -301
- package/connectors/jira/src/tools.ts +325 -0
- package/connectors/kubernetes/src/server.ts +2 -155
- package/connectors/kubernetes/src/tools.ts +159 -0
- package/connectors/launchdarkly/src/server.ts +2 -75
- package/connectors/launchdarkly/src/tools.ts +72 -0
- package/connectors/lever/src/search-filter.ts +4 -12
- package/connectors/lever/src/server.ts +2 -60
- package/connectors/lever/src/tools.ts +61 -0
- package/connectors/linear/src/server.ts +2 -459
- package/connectors/linear/src/tools.ts +482 -0
- package/connectors/localdb/src/sql-scan.ts +13 -41
- package/connectors/localdb/src/tools.ts +1 -1
- package/connectors/looker/src/search-filter.ts +2 -15
- package/connectors/looker/src/server.ts +35 -70
- package/connectors/mendeley/src/server.ts +2 -55
- package/connectors/mendeley/src/tools.ts +54 -0
- package/connectors/mercury/src/server.ts +2 -57
- package/connectors/mercury/src/tools.ts +50 -0
- package/connectors/metabase/src/search-filter.ts +4 -15
- package/connectors/metabase/src/server.ts +2 -79
- package/connectors/metabase/src/tools.ts +64 -0
- package/connectors/miro/src/server.ts +2 -72
- package/connectors/miro/src/tools.ts +65 -0
- package/connectors/mlflow/src/search-filter.ts +3 -7
- package/connectors/mlflow/src/server.ts +20 -40
- package/connectors/monte-carlo/src/server.ts +50 -57
- package/connectors/netlify/src/search-filter.ts +6 -14
- package/connectors/netlify/src/server.ts +2 -60
- package/connectors/netlify/src/tools.ts +53 -0
- package/connectors/newrelic/src/server.ts +2 -40
- package/connectors/newrelic/src/tools.ts +37 -0
- package/connectors/notion/src/server.ts +2 -335
- package/connectors/notion/src/tools.ts +344 -0
- package/connectors/obsidian/src/server.ts +2 -425
- package/connectors/obsidian/src/tools.ts +466 -0
- package/connectors/onedrive/src/server.ts +2 -249
- package/connectors/onedrive/src/tools.ts +253 -0
- package/connectors/outlook/src/server.ts +2 -351
- package/connectors/outlook/src/tools.ts +357 -0
- package/connectors/pagerduty/src/server.ts +2 -132
- package/connectors/pagerduty/src/tools.ts +126 -0
- package/connectors/pipedrive/src/server.ts +2 -58
- package/connectors/pipedrive/src/tools.ts +63 -0
- package/connectors/powerbi/src/search-filter.ts +2 -15
- package/connectors/powerbi/src/server.ts +43 -68
- package/connectors/prefect/src/search-filter.ts +3 -16
- package/connectors/prefect/src/server.ts +2 -90
- package/connectors/prefect/src/tools.ts +83 -0
- package/connectors/protonmail/src/mail-core.ts +16 -75
- package/connectors/protonmail/src/server.ts +34 -214
- package/connectors/raindrop/src/server.ts +2 -103
- package/connectors/raindrop/src/tools.ts +103 -0
- package/connectors/ramp/src/server.ts +2 -105
- package/connectors/ramp/src/tools.ts +98 -0
- package/connectors/readwise/src/server.ts +2 -88
- package/connectors/readwise/src/tools.ts +81 -0
- package/connectors/sagemaker/src/tools.ts +19 -38
- package/connectors/salesforce/src/search-filter.ts +4 -11
- package/connectors/salesforce/src/server.ts +2 -90
- package/connectors/salesforce/src/tools.ts +75 -0
- package/connectors/semgrep/src/search-filter.ts +3 -10
- package/connectors/semgrep/src/server.ts +2 -104
- package/connectors/semgrep/src/tools.ts +92 -0
- package/connectors/sentry/src/server.ts +2 -58
- package/connectors/sentry/src/tools.ts +53 -0
- package/connectors/slack/src/server.ts +2 -352
- package/connectors/slack/src/tools.ts +376 -0
- package/connectors/snowflake/src/search-filter.ts +4 -19
- package/connectors/snowflake/src/server.ts +6 -25
- package/connectors/snyk/src/server.ts +2 -140
- package/connectors/snyk/src/tools.ts +136 -0
- package/connectors/sonarqube/src/server.ts +2 -111
- package/connectors/sonarqube/src/tools.ts +98 -0
- package/connectors/stackoverflow/src/server.ts +2 -77
- package/connectors/stackoverflow/src/tools.ts +66 -0
- package/connectors/storybook/src/storybook-parse.ts +3 -6
- package/connectors/stripe/src/server.ts +2 -61
- package/connectors/stripe/src/tools.ts +54 -0
- package/connectors/superset/src/search-filter.ts +4 -15
- package/connectors/superset/src/server.ts +2 -115
- package/connectors/superset/src/tools.ts +98 -0
- package/connectors/tableau/src/search-filter.ts +2 -15
- package/connectors/tableau/src/server.ts +13 -65
- package/connectors/teams/src/server.ts +2 -249
- package/connectors/teams/src/tools.ts +283 -0
- package/connectors/testflight/src/server.ts +2 -70
- package/connectors/testflight/src/tools.ts +79 -0
- package/connectors/vercel/src/search-filter.ts +2 -10
- package/connectors/vercel/src/server.ts +2 -75
- package/connectors/vercel/src/tools.ts +73 -0
- package/connectors/vertex-ai/src/tools.ts +23 -33
- package/connectors/wiz/src/search-filter.ts +6 -30
- package/connectors/wiz/src/server.ts +2 -188
- package/connectors/wiz/src/tools.ts +168 -0
- package/connectors/zendesk/src/server.ts +2 -73
- package/connectors/zendesk/src/tools.ts +60 -0
- package/connectors/zoom/src/search-filter.ts +1 -7
- package/connectors/zoom/src/server.ts +2 -94
- package/connectors/zoom/src/tools.ts +93 -0
- package/connectors/zotero/src/server.ts +2 -78
- package/connectors/zotero/src/tools.ts +67 -0
- package/package.json +15 -12
- package/shared/access-token-cache.ts +123 -0
- package/shared/atlassian-json-fetch.ts +6 -16
- package/shared/audit-chain.ts +13 -6
- package/shared/cli-json-kit.ts +150 -0
- package/shared/collection-tool-kit.ts +143 -0
- package/shared/connector-mode.ts +3 -2
- package/shared/cursor-list-tool.ts +29 -0
- package/shared/env-json-api.ts +209 -0
- package/shared/fetch-json-text.ts +27 -0
- package/shared/github-rest.ts +62 -0
- package/shared/graphql-json.ts +60 -0
- package/shared/imap-mail-core.ts +91 -0
- package/shared/imap-tool-kit.ts +57 -23
- package/shared/imapflow-adapter.ts +417 -0
- package/shared/rest-tool-kit.ts +77 -7
- package/shared/run-cli-json.ts +38 -16
- package/shared/search-filter.ts +58 -18
- package/shared/walk-files.ts +118 -0
- package/standalone/src/launcher.ts +19 -7
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* graphql-json — one GraphQL-over-HTTP request, for the connectors whose API is a
|
|
3
|
+
* GraphQL endpoint read as plain JSON (dagster, wiz).
|
|
4
|
+
*
|
|
5
|
+
* Both had written out the same request and the same reading of the response
|
|
6
|
+
* envelope: a present `errors` member fails the call even when `data` came back
|
|
7
|
+
* too, and an absent `data` member fails it as well. That reading is a rule about
|
|
8
|
+
* what a partial GraphQL answer means, and it belongs in one place.
|
|
9
|
+
*
|
|
10
|
+
* Linear and Monte Carlo read the envelope differently (an `errors` ARRAY that
|
|
11
|
+
* must be non-empty, messages joined, a result union) and keep their own clients.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Body-snippet length in the thrown errors. The value both connectors used. */
|
|
15
|
+
const SNIPPET_MAX = 400;
|
|
16
|
+
|
|
17
|
+
export interface GraphqlRequest {
|
|
18
|
+
/** The GraphQL endpoint. */
|
|
19
|
+
readonly url: string;
|
|
20
|
+
/** Service name that prefixes every failure, e.g. `"Wiz 401: ..."`. */
|
|
21
|
+
readonly label: string;
|
|
22
|
+
/** Auth headers; `Content-Type` and `Accept` (both `application/json`) are added after them. */
|
|
23
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
24
|
+
readonly query: string;
|
|
25
|
+
/** Omitted from the request body entirely when undefined. */
|
|
26
|
+
readonly variables?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* POST `query` (and `variables`) to `url` and resolve to the response's `data`.
|
|
31
|
+
*
|
|
32
|
+
* Throws `"<label> <status>: <body snippet>"` on a non-2xx,
|
|
33
|
+
* `"<label> GraphQL error: <errors snippet>"` when the response carries `errors`,
|
|
34
|
+
* and `"<label> GraphQL: response missing \`data\` field"` when it carries no `data`.
|
|
35
|
+
*/
|
|
36
|
+
export async function postGraphql<T>(request: GraphqlRequest): Promise<T> {
|
|
37
|
+
const res = await fetch(request.url, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: {
|
|
40
|
+
...request.headers,
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
Accept: "application/json",
|
|
43
|
+
},
|
|
44
|
+
body: JSON.stringify({ query: request.query, variables: request.variables }),
|
|
45
|
+
});
|
|
46
|
+
const text = await res.text();
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
throw new Error(`${request.label} ${String(res.status)}: ${text.slice(0, SNIPPET_MAX)}`);
|
|
49
|
+
}
|
|
50
|
+
const parsed = JSON.parse(text) as { data?: T; errors?: unknown };
|
|
51
|
+
if (parsed.errors !== undefined) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`${request.label} GraphQL error: ${JSON.stringify(parsed.errors).slice(0, SNIPPET_MAX)}`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (parsed.data === undefined) {
|
|
57
|
+
throw new Error(`${request.label} GraphQL: response missing \`data\` field`);
|
|
58
|
+
}
|
|
59
|
+
return parsed.data;
|
|
60
|
+
}
|
package/shared/imap-mail-core.ts
CHANGED
|
@@ -55,3 +55,94 @@ export function formatAddress(a: MailAddress): string {
|
|
|
55
55
|
}
|
|
56
56
|
return addr;
|
|
57
57
|
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Message ENVELOPE — RFC 2822 header fields only.
|
|
61
|
+
*
|
|
62
|
+
* These five declarations were written out identically in `imap/src/imap-core.ts`
|
|
63
|
+
* and `protonmail/src/mail-core.ts`, differing only in the `Imap`/`Mail` name
|
|
64
|
+
* prefix. Both connectors index the same thing through the same protocol, so
|
|
65
|
+
* there was never a second shape here — only a second copy. Each core now
|
|
66
|
+
* aliases these, keeping its own names for its own callers.
|
|
67
|
+
*/
|
|
68
|
+
export interface MailEnvelope {
|
|
69
|
+
readonly date?: Date | string | null;
|
|
70
|
+
readonly subject?: string | null;
|
|
71
|
+
readonly messageId?: string | null;
|
|
72
|
+
readonly from?: readonly MailAddress[];
|
|
73
|
+
readonly to?: readonly MailAddress[];
|
|
74
|
+
readonly cc?: readonly MailAddress[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Attachment METADATA only — filename, size, mimetype. NEVER the bytes.
|
|
79
|
+
* Derived from BODYSTRUCTURE.
|
|
80
|
+
*/
|
|
81
|
+
export interface MailAttachmentMeta {
|
|
82
|
+
readonly filename: string | null;
|
|
83
|
+
readonly sizeBytes: number | null;
|
|
84
|
+
readonly mimeType: string | null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A single mailbox message reduced to header + attachment-metadata + a capped
|
|
89
|
+
* text preview. This is the ONLY shape these connectors ever materialize; it
|
|
90
|
+
* has no field that could carry attachment content or a full body.
|
|
91
|
+
*/
|
|
92
|
+
export interface MailMessageMeta {
|
|
93
|
+
readonly uid: number;
|
|
94
|
+
readonly mailbox: string;
|
|
95
|
+
readonly uidValidity: string | null;
|
|
96
|
+
readonly envelope: MailEnvelope;
|
|
97
|
+
readonly attachments: readonly MailAttachmentMeta[];
|
|
98
|
+
/** Plain-text body preview, already capped to {@link PREVIEW_MAX_CHARS}. */
|
|
99
|
+
readonly preview: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Listing options. */
|
|
103
|
+
export interface MailListOptions {
|
|
104
|
+
readonly mailbox?: string;
|
|
105
|
+
readonly limit?: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Search options — searches header/envelope fields, never body-content scans. */
|
|
109
|
+
export interface MailSearchOptions {
|
|
110
|
+
readonly query: string;
|
|
111
|
+
readonly mailbox?: string;
|
|
112
|
+
readonly limit?: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Minimal IMAP client surface these connectors depend on. Implemented for real
|
|
117
|
+
* by `shared/imapflow-adapter.ts` and by a fake in tests. Deliberately exposes
|
|
118
|
+
* ONLY header/attachment-metadata + capped-preview reads.
|
|
119
|
+
*/
|
|
120
|
+
export interface MailClient {
|
|
121
|
+
/** List the most recent messages (header + attachment metadata + preview). */
|
|
122
|
+
list(options: MailListOptions): Promise<MailMessageMeta[]>;
|
|
123
|
+
/** Fetch one message's header + attachment metadata + preview by uid. */
|
|
124
|
+
get(uid: number, mailbox?: string): Promise<MailMessageMeta | null>;
|
|
125
|
+
/** Search by subject/from/to substring; returns matching message metas. */
|
|
126
|
+
search(options: MailSearchOptions): Promise<MailMessageMeta[]>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Outgoing message for the SMTP send tool. */
|
|
130
|
+
export interface SendMailInput {
|
|
131
|
+
readonly to: string;
|
|
132
|
+
readonly subject: string;
|
|
133
|
+
readonly body: string;
|
|
134
|
+
readonly cc?: string;
|
|
135
|
+
readonly bcc?: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Result of a send (message id assigned by the SMTP server, if any). */
|
|
139
|
+
export interface SendMailResult {
|
|
140
|
+
readonly messageId: string | null;
|
|
141
|
+
readonly accepted: readonly string[];
|
|
142
|
+
readonly rejected: readonly string[];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Minimal SMTP mailer surface; implemented for real over nodemailer. */
|
|
146
|
+
export interface SmtpMailer {
|
|
147
|
+
send(input: SendMailInput): Promise<SendMailResult>;
|
|
148
|
+
}
|
package/shared/imap-tool-kit.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { WriteToolRegistrar } from "./consent-kit.ts";
|
|
1
|
+
import type { WriteToolConfig, WriteToolRegistrar } from "./consent-kit.ts";
|
|
2
2
|
/**
|
|
3
3
|
* Shared tool-layer helpers for the IMAP/JMAP email connectors (imap, protonmail).
|
|
4
4
|
* Extracted to eliminate byte-identical duplication in tools.ts across those two
|
|
@@ -10,6 +10,10 @@ import type { WriteToolRegistrar } from "./consent-kit.ts";
|
|
|
10
10
|
* connector-specific envelope/meta mappers are deliberately NOT extracted here
|
|
11
11
|
* (they implement different local interfaces and are out of scope per the dedup
|
|
12
12
|
* brief).
|
|
13
|
+
*
|
|
14
|
+
* The mail-SEND pieces — `sendArgs`, {@link outgoingMail} and {@link mailSendConsent} —
|
|
15
|
+
* serve every email connector that sends, whatever its transport: imap, protonmail and
|
|
16
|
+
* apple through the factory below, and fastmail (JMAP), gmail and outlook directly.
|
|
13
17
|
*/
|
|
14
18
|
|
|
15
19
|
import { z } from "zod";
|
|
@@ -54,6 +58,55 @@ export const emailToolSchemas = {
|
|
|
54
58
|
sendArgs,
|
|
55
59
|
} as const;
|
|
56
60
|
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Shared mail-send helpers (every connector that sends or drafts a new message)
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
/** What a send or draft tool hands its transport. */
|
|
66
|
+
export interface OutgoingMail {
|
|
67
|
+
readonly to: string;
|
|
68
|
+
readonly subject: string;
|
|
69
|
+
readonly body: string;
|
|
70
|
+
readonly cc?: string;
|
|
71
|
+
readonly bcc?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The outgoing message for validated `sendArgs`-shaped input: an empty `cc` or `bcc` is
|
|
76
|
+
* left out rather than handed on as an empty header.
|
|
77
|
+
*/
|
|
78
|
+
export function outgoingMail(args: {
|
|
79
|
+
readonly to: string;
|
|
80
|
+
readonly subject: string;
|
|
81
|
+
readonly body: string;
|
|
82
|
+
readonly cc?: string | undefined;
|
|
83
|
+
readonly bcc?: string | undefined;
|
|
84
|
+
}): OutgoingMail {
|
|
85
|
+
return {
|
|
86
|
+
to: args.to,
|
|
87
|
+
subject: args.subject,
|
|
88
|
+
body: args.body,
|
|
89
|
+
...(args.cc !== undefined && args.cc !== "" ? { cc: args.cc } : {}),
|
|
90
|
+
...(args.bcc !== undefined && args.bcc !== "" ? { bcc: args.bcc } : {}),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The consent declaration every mail-send tool shares, given its action type (`mutates`).
|
|
96
|
+
* A sent mail cannot be recalled and nothing remains to query, so the recipient and
|
|
97
|
+
* subject ARE the pre-state; the recipient is what the write scope is checked against.
|
|
98
|
+
*/
|
|
99
|
+
export function mailSendConsent<T extends { readonly to: string; readonly subject: string }>(
|
|
100
|
+
mutates: string,
|
|
101
|
+
): WriteToolConfig<T> {
|
|
102
|
+
return {
|
|
103
|
+
mutates,
|
|
104
|
+
recoverable: false,
|
|
105
|
+
capturePreState: (p) => Promise.resolve({ to: p.to, subject: p.subject }),
|
|
106
|
+
scopeTargetOf: (p) => ({ kind: "recipient", value: p.to }),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
57
110
|
// ---------------------------------------------------------------------------
|
|
58
111
|
// Shared view transformer (imap + protonmail tools.ts — viewMessage)
|
|
59
112
|
// ---------------------------------------------------------------------------
|
|
@@ -259,30 +312,11 @@ export function registerEmailConnectorTools(opts: {
|
|
|
259
312
|
|
|
260
313
|
registerWriteTool(
|
|
261
314
|
`${toolPrefix}_mail_send`,
|
|
262
|
-
{
|
|
263
|
-
mutates: `${toolPrefix}.mail.send`,
|
|
264
|
-
// A sent mail cannot be recalled and nothing remains to query, so the recipient and
|
|
265
|
-
// subject ARE the pre-state.
|
|
266
|
-
recoverable: false,
|
|
267
|
-
capturePreState: (p) => Promise.resolve({ to: p.to, subject: p.subject }),
|
|
268
|
-
scopeTargetOf: (p) => ({ kind: "recipient", value: p.to }),
|
|
269
|
-
},
|
|
315
|
+
mailSendConsent(`${toolPrefix}.mail.send`),
|
|
270
316
|
descriptions.send,
|
|
271
317
|
sendArgs,
|
|
272
|
-
async (
|
|
273
|
-
const
|
|
274
|
-
const input: { to: string; subject: string; body: string; cc?: string; bcc?: string } = {
|
|
275
|
-
to: parsed.data.to,
|
|
276
|
-
subject: parsed.data.subject,
|
|
277
|
-
body: parsed.data.body,
|
|
278
|
-
};
|
|
279
|
-
if (parsed.data.cc !== undefined && parsed.data.cc !== "") {
|
|
280
|
-
input.cc = parsed.data.cc;
|
|
281
|
-
}
|
|
282
|
-
if (parsed.data.bcc !== undefined && parsed.data.bcc !== "") {
|
|
283
|
-
input.bcc = parsed.data.bcc;
|
|
284
|
-
}
|
|
285
|
-
const res = await mailer.send(input);
|
|
318
|
+
async (args): Promise<McpListResult> => {
|
|
319
|
+
const res = await mailer.send(outgoingMail(args));
|
|
286
320
|
return mcpJsonResult({
|
|
287
321
|
messageId: res.messageId,
|
|
288
322
|
accepted: res.accepted,
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* imapflow-adapter — the real IMAP client + SMTP mailer over `imapflow` and
|
|
3
|
+
* `nodemailer`, shared by every IMAP-based mail connector (imap, protonmail).
|
|
4
|
+
*
|
|
5
|
+
* These two class bodies were previously written out in full in each
|
|
6
|
+
* connector's `server.ts`. `imap-tool-kit.ts` deliberately left them alone at
|
|
7
|
+
* the time ("class bodies … are out of scope per the dedup brief"), and the
|
|
8
|
+
* cost of that was measurable on both axes Sonar reports:
|
|
9
|
+
*
|
|
10
|
+
* - duplication: `imap/src/server.ts` 41.2% and `protonmail/src/server.ts`
|
|
11
|
+
* 38.8%, 186 duplicated lines between them — the largest pair in the repo;
|
|
12
|
+
* - coverage: both files were 0%. An entry point that constructs its adapters
|
|
13
|
+
* at module scope cannot be imported by a test without opening real
|
|
14
|
+
* sockets, so every line of transport logic in them was unasserted.
|
|
15
|
+
*
|
|
16
|
+
* Extracting the classes here fixes both: one copy, and one that a test can
|
|
17
|
+
* construct because the `imapflow` client and the `nodemailer` transport are
|
|
18
|
+
* injected seams rather than direct `new` calls. The two connectors differ only
|
|
19
|
+
* in configuration (TLS mode, ports, env var names, mailbox default), so all of
|
|
20
|
+
* it is passed in.
|
|
21
|
+
*
|
|
22
|
+
* HARD SCOPE CONSTRAINT (security), inherited from both connectors: the fetch
|
|
23
|
+
* query below requests ONLY `envelope` + `bodyStructure` + a `PREVIEW_FETCH_BYTES`-
|
|
24
|
+
* capped text part. There is no surface here to request `BODY[]` or an
|
|
25
|
+
* attachment part, so no caller can make this adapter download message bodies
|
|
26
|
+
* or attachment bytes.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
type FetchMessageObject,
|
|
31
|
+
type FetchQueryObject,
|
|
32
|
+
ImapFlow,
|
|
33
|
+
type MessageAddressObject,
|
|
34
|
+
} from "imapflow";
|
|
35
|
+
import { createTransport, type Transporter } from "nodemailer";
|
|
36
|
+
import {
|
|
37
|
+
type BodyStructureNode,
|
|
38
|
+
extractAttachments,
|
|
39
|
+
findTextPlainPart,
|
|
40
|
+
} from "./imap-bodystructure.ts";
|
|
41
|
+
import { clampLimit, type MailAddress, PREVIEW_FETCH_BYTES } from "./imap-mail-core.ts";
|
|
42
|
+
import {
|
|
43
|
+
type EmailMessageMeta,
|
|
44
|
+
type EmailReadClient,
|
|
45
|
+
type EmailSendMailer,
|
|
46
|
+
previewFromParts,
|
|
47
|
+
} from "./imap-tool-kit.ts";
|
|
48
|
+
|
|
49
|
+
/** Mailbox used when a tool call does not name one. */
|
|
50
|
+
export const DEFAULT_MAILBOX = "INBOX";
|
|
51
|
+
|
|
52
|
+
/** Connection settings for one IMAP endpoint. Read from `process.env` by the caller. */
|
|
53
|
+
export interface ImapEndpointConfig {
|
|
54
|
+
readonly host: string;
|
|
55
|
+
readonly port: number;
|
|
56
|
+
readonly user: string;
|
|
57
|
+
readonly pass: string;
|
|
58
|
+
/**
|
|
59
|
+
* Implicit TLS on connect. `true` for a normal IMAPS endpoint on 993; `false`
|
|
60
|
+
* for the ProtonMail Bridge, which speaks STARTTLS on loopback.
|
|
61
|
+
*/
|
|
62
|
+
readonly secure: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Chain verification. Omit (the default) for a real endpoint. Only the
|
|
65
|
+
* ProtonMail Bridge passes `false`, for its self-signed loopback certificate;
|
|
66
|
+
* the connection never leaves 127.0.0.1.
|
|
67
|
+
*/
|
|
68
|
+
readonly rejectUnauthorized?: boolean;
|
|
69
|
+
/** Mailbox used when a call does not name one. Defaults to {@link DEFAULT_MAILBOX}. */
|
|
70
|
+
readonly defaultMailbox?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Connection settings for one SMTP endpoint. */
|
|
74
|
+
export interface SmtpEndpointConfig {
|
|
75
|
+
readonly host: string;
|
|
76
|
+
readonly port: number;
|
|
77
|
+
readonly user: string;
|
|
78
|
+
readonly pass: string;
|
|
79
|
+
readonly secure: boolean;
|
|
80
|
+
readonly rejectUnauthorized?: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* There is deliberately no `requireTLS` option.
|
|
85
|
+
*
|
|
86
|
+
* When `secure` is false this adapter ALWAYS demands STARTTLS, and that is not
|
|
87
|
+
* a default a caller can override — an unencrypted SMTP session is not a
|
|
88
|
+
* configuration this module offers. Without it nodemailer falls back to
|
|
89
|
+
* plaintext when the server does not advertise STARTTLS, and the password goes
|
|
90
|
+
* out in the clear.
|
|
91
|
+
*
|
|
92
|
+
* The gap was live, not hypothetical: `imap` computes `secure: port === 465`,
|
|
93
|
+
* so an operator setting `IMAP_SMTP_PORT=587` — the standard submission port —
|
|
94
|
+
* got `secure: false` with no `requireTLS` at all. Of the three copies this
|
|
95
|
+
* adapter replaced, only Apple's set it.
|
|
96
|
+
|
|
97
|
+
/** Constructs the `imapflow` client. Overridden in tests so no socket is opened. */
|
|
98
|
+
export type ImapFlowFactory = (options: ConstructorParameters<typeof ImapFlow>[0]) => ImapFlow;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The transport options this adapter builds. Declared here rather than reused
|
|
102
|
+
* from nodemailer: `Parameters<typeof createTransport>[0]` resolves to the
|
|
103
|
+
* union `Transport | TransportOptions`, and a plain SMTP option literal is not
|
|
104
|
+
* assignable to that union.
|
|
105
|
+
*/
|
|
106
|
+
export interface SmtpTransportOptions {
|
|
107
|
+
readonly host: string;
|
|
108
|
+
readonly port: number;
|
|
109
|
+
readonly secure: boolean;
|
|
110
|
+
readonly auth: { readonly user: string; readonly pass: string };
|
|
111
|
+
/** Always set by this module, and always `true` when `secure` is false. */
|
|
112
|
+
readonly requireTLS: boolean;
|
|
113
|
+
readonly tls?: { readonly rejectUnauthorized: boolean };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Constructs the `nodemailer` transport. Overridden in tests so no socket is opened. */
|
|
117
|
+
export type TransportFactory = (options: SmtpTransportOptions) => Transporter;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build a {@link MailAddress} from an imapflow envelope address, omitting
|
|
121
|
+
* `name`/`address` when absent — the interface uses `exactOptionalPropertyTypes`,
|
|
122
|
+
* so an explicit `undefined` is not assignable.
|
|
123
|
+
*
|
|
124
|
+
* The parameter is imapflow's own `MessageAddressObject`, not a restated
|
|
125
|
+
* `{ name?: string; address?: string }`. imapflow 2 generates its declarations,
|
|
126
|
+
* which spell the fields `name?: string | undefined`, and under
|
|
127
|
+
* `exactOptionalPropertyTypes` a hand-written shape without the `| undefined`
|
|
128
|
+
* stops accepting the library's value.
|
|
129
|
+
*/
|
|
130
|
+
function toMailAddress(a: MessageAddressObject): MailAddress {
|
|
131
|
+
const out: { name?: string; address?: string } = {};
|
|
132
|
+
if (a.name !== undefined) {
|
|
133
|
+
out.name = a.name;
|
|
134
|
+
}
|
|
135
|
+
if (a.address !== undefined) {
|
|
136
|
+
out.address = a.address;
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function envelopeFromImap(env: FetchMessageObject["envelope"]): EmailMessageMeta["envelope"] {
|
|
142
|
+
if (env === undefined) {
|
|
143
|
+
return {};
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
date: env.date ?? null,
|
|
147
|
+
subject: env.subject ?? null,
|
|
148
|
+
messageId: env.messageId ?? null,
|
|
149
|
+
from: (env.from ?? []).map(toMailAddress),
|
|
150
|
+
to: (env.to ?? []).map(toMailAddress),
|
|
151
|
+
cc: (env.cc ?? []).map(toMailAddress),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Reduce one fetched message to the header + attachment-metadata + preview view. */
|
|
156
|
+
export function toMessageMeta(
|
|
157
|
+
msg: FetchMessageObject,
|
|
158
|
+
mailbox: string,
|
|
159
|
+
uidValidity: string | null,
|
|
160
|
+
): EmailMessageMeta {
|
|
161
|
+
const structure = (msg.bodyStructure ?? null) as BodyStructureNode | null;
|
|
162
|
+
const partKey = findTextPlainPart(structure);
|
|
163
|
+
return {
|
|
164
|
+
uid: msg.uid,
|
|
165
|
+
mailbox,
|
|
166
|
+
uidValidity,
|
|
167
|
+
envelope: envelopeFromImap(msg.envelope),
|
|
168
|
+
attachments: extractAttachments(structure),
|
|
169
|
+
preview: previewFromParts(msg.bodyParts, partKey),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Fetch query that requests ONLY headers + structure + a truncated text body
|
|
175
|
+
* part. There is no `source` / full-body request here by construction.
|
|
176
|
+
*/
|
|
177
|
+
export function previewFetchQuery(): FetchQueryObject {
|
|
178
|
+
return {
|
|
179
|
+
uid: true,
|
|
180
|
+
envelope: true,
|
|
181
|
+
bodyStructure: true,
|
|
182
|
+
internalDate: true,
|
|
183
|
+
// Two candidate text part keys, each capped to PREVIEW_FETCH_BYTES bytes.
|
|
184
|
+
bodyParts: [
|
|
185
|
+
{ key: "1", start: 0, maxLength: PREVIEW_FETCH_BYTES },
|
|
186
|
+
{ key: "TEXT", start: 0, maxLength: PREVIEW_FETCH_BYTES },
|
|
187
|
+
],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* imapflow answers `undefined` from `fetchOne` and `search` when it has no
|
|
193
|
+
* mailbox selected — reachable under the lock `withMailbox` holds only if the
|
|
194
|
+
* connection closed after it was taken.
|
|
195
|
+
*
|
|
196
|
+
* imapflow 1.x did the same at runtime but declared no such return, so the
|
|
197
|
+
* value reached `toMessageMeta` / `.length` and failed as a bare TypeError. The
|
|
198
|
+
* generated declarations of 2.x surface it. It stays a FAILURE, now one that
|
|
199
|
+
* says what happened — not "no such message" or "no results", which would
|
|
200
|
+
* report a dropped connection as an answer.
|
|
201
|
+
*/
|
|
202
|
+
function mailboxNotSelected(mailbox: string): Error {
|
|
203
|
+
return new Error(`imap: mailbox "${mailbox}" is no longer selected (connection closed?)`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Real IMAP client over imapflow. Per-call connect/lock/logout keeps it
|
|
208
|
+
* stateless, so nothing is held open between tool calls.
|
|
209
|
+
*/
|
|
210
|
+
class ImapFlowClient implements EmailReadClient {
|
|
211
|
+
private readonly defaultMailbox: string;
|
|
212
|
+
|
|
213
|
+
constructor(
|
|
214
|
+
private readonly config: ImapEndpointConfig,
|
|
215
|
+
private readonly newImapFlow: ImapFlowFactory,
|
|
216
|
+
) {
|
|
217
|
+
this.defaultMailbox = config.defaultMailbox ?? DEFAULT_MAILBOX;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private newClient(): ImapFlow {
|
|
221
|
+
const { host, port, secure, user, pass, rejectUnauthorized } = this.config;
|
|
222
|
+
return this.newImapFlow({
|
|
223
|
+
host,
|
|
224
|
+
port,
|
|
225
|
+
secure,
|
|
226
|
+
auth: { user, pass },
|
|
227
|
+
logger: false,
|
|
228
|
+
// Only set when the caller asked to relax it — see ImapEndpointConfig.
|
|
229
|
+
...(rejectUnauthorized === undefined ? {} : { tls: { rejectUnauthorized } }),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private async withMailbox<T>(
|
|
234
|
+
mailbox: string,
|
|
235
|
+
fn: (client: ImapFlow, uidValidity: string | null) => Promise<T>,
|
|
236
|
+
): Promise<T> {
|
|
237
|
+
const client = this.newClient();
|
|
238
|
+
await client.connect();
|
|
239
|
+
// `logout` sits in an OUTER finally so the connection is closed even when
|
|
240
|
+
// `getMailboxLock` throws. Acquiring the lock BEFORE the try — which is how
|
|
241
|
+
// imap and protonmail both had it — leaks the connected socket on a locking
|
|
242
|
+
// failure (a mailbox that does not exist, or one another client holds), and
|
|
243
|
+
// this client connects per call, so every such failure leaks another.
|
|
244
|
+
// Apple's copy already nested it this way; the correction travelled the
|
|
245
|
+
// wrong direction when these were three separate copies.
|
|
246
|
+
try {
|
|
247
|
+
const lock = await client.getMailboxLock(mailbox);
|
|
248
|
+
try {
|
|
249
|
+
const mb = client.mailbox;
|
|
250
|
+
const uidValidity = mb === false ? null : String(mb.uidValidity);
|
|
251
|
+
return await fn(client, uidValidity);
|
|
252
|
+
} finally {
|
|
253
|
+
lock.release();
|
|
254
|
+
}
|
|
255
|
+
} finally {
|
|
256
|
+
await client.logout();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async list(options: { mailbox?: string; limit?: number }): Promise<EmailMessageMeta[]> {
|
|
261
|
+
const mailbox = options.mailbox ?? this.defaultMailbox;
|
|
262
|
+
const limit = clampLimit(options.limit);
|
|
263
|
+
return this.withMailbox(mailbox, async (client, uidValidity) => {
|
|
264
|
+
const out: EmailMessageMeta[] = [];
|
|
265
|
+
const status = await client.status(mailbox, { messages: true });
|
|
266
|
+
// `false` is a STATUS the server rejected, read as an empty mailbox. That
|
|
267
|
+
// is what this did under imapflow 1.x too, which returned the same `false`
|
|
268
|
+
// but declared only `StatusObject`, so `status.messages ?? 0` made it 0.
|
|
269
|
+
const total = status === false ? 0 : (status.messages ?? 0);
|
|
270
|
+
if (total === 0) {
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
273
|
+
const start = Math.max(1, total - limit + 1);
|
|
274
|
+
const range = `${start}:${total}`;
|
|
275
|
+
for await (const msg of client.fetch(range, previewFetchQuery())) {
|
|
276
|
+
out.push(toMessageMeta(msg, mailbox, uidValidity));
|
|
277
|
+
}
|
|
278
|
+
// Most-recent first.
|
|
279
|
+
out.sort((a, b) => b.uid - a.uid);
|
|
280
|
+
return out;
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async get(uid: number, mailbox?: string): Promise<EmailMessageMeta | null> {
|
|
285
|
+
const box = mailbox ?? this.defaultMailbox;
|
|
286
|
+
return this.withMailbox(box, async (client, uidValidity) => {
|
|
287
|
+
const msg = await client.fetchOne(String(uid), previewFetchQuery(), { uid: true });
|
|
288
|
+
if (msg === undefined) {
|
|
289
|
+
throw mailboxNotSelected(box);
|
|
290
|
+
}
|
|
291
|
+
return msg === false ? null : toMessageMeta(msg, box, uidValidity);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async search(options: {
|
|
296
|
+
query: string;
|
|
297
|
+
mailbox?: string;
|
|
298
|
+
limit?: number;
|
|
299
|
+
}): Promise<EmailMessageMeta[]> {
|
|
300
|
+
const mailbox = options.mailbox ?? this.defaultMailbox;
|
|
301
|
+
const limit = clampLimit(options.limit);
|
|
302
|
+
const q = options.query;
|
|
303
|
+
return this.withMailbox(mailbox, async (client, uidValidity) => {
|
|
304
|
+
const uids = await client.search(
|
|
305
|
+
{ or: [{ subject: q }, { from: q }, { to: q }] },
|
|
306
|
+
{ uid: true },
|
|
307
|
+
);
|
|
308
|
+
if (uids === undefined) {
|
|
309
|
+
throw mailboxNotSelected(mailbox);
|
|
310
|
+
}
|
|
311
|
+
if (uids === false || uids.length === 0) {
|
|
312
|
+
return [];
|
|
313
|
+
}
|
|
314
|
+
const picked = uids.slice(-limit).sort((a, b) => b - a);
|
|
315
|
+
const out: EmailMessageMeta[] = [];
|
|
316
|
+
for await (const msg of client.fetch(picked, previewFetchQuery(), { uid: true })) {
|
|
317
|
+
out.push(toMessageMeta(msg, mailbox, uidValidity));
|
|
318
|
+
}
|
|
319
|
+
out.sort((a, b) => b.uid - a.uid);
|
|
320
|
+
return out;
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Real SMTP mailer over nodemailer.
|
|
327
|
+
*
|
|
328
|
+
* The envelope `from` is PINNED to the authenticated user and is not a `send`
|
|
329
|
+
* parameter, so no caller can send as someone else (the forced-sender contract
|
|
330
|
+
* the mail connectors document).
|
|
331
|
+
*/
|
|
332
|
+
class NodemailerMailer implements EmailSendMailer {
|
|
333
|
+
private readonly transport: Transporter;
|
|
334
|
+
private readonly from: string;
|
|
335
|
+
|
|
336
|
+
constructor(config: SmtpEndpointConfig, makeTransport: TransportFactory) {
|
|
337
|
+
const { host, port, secure, user, pass, rejectUnauthorized } = config;
|
|
338
|
+
this.from = user;
|
|
339
|
+
this.transport = makeTransport({
|
|
340
|
+
host,
|
|
341
|
+
port,
|
|
342
|
+
secure,
|
|
343
|
+
auth: { user, pass },
|
|
344
|
+
// Implicit TLS already encrypts the session; otherwise STARTTLS is
|
|
345
|
+
// required. Not a default — there is no input to this mailer that
|
|
346
|
+
// produces a transport willing to send in the clear.
|
|
347
|
+
requireTLS: !secure,
|
|
348
|
+
...(rejectUnauthorized === undefined ? {} : { tls: { rejectUnauthorized } }),
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async send(input: {
|
|
353
|
+
to: string;
|
|
354
|
+
subject: string;
|
|
355
|
+
body: string;
|
|
356
|
+
cc?: string;
|
|
357
|
+
bcc?: string;
|
|
358
|
+
}): Promise<{
|
|
359
|
+
messageId: string | null;
|
|
360
|
+
accepted: readonly string[];
|
|
361
|
+
rejected: readonly string[];
|
|
362
|
+
}> {
|
|
363
|
+
const info = await this.transport.sendMail({
|
|
364
|
+
from: this.from,
|
|
365
|
+
to: input.to,
|
|
366
|
+
subject: input.subject,
|
|
367
|
+
text: input.body,
|
|
368
|
+
...(input.cc === undefined ? {} : { cc: input.cc }),
|
|
369
|
+
...(input.bcc === undefined ? {} : { bcc: input.bcc }),
|
|
370
|
+
});
|
|
371
|
+
return {
|
|
372
|
+
messageId: info.messageId ?? null,
|
|
373
|
+
accepted: (info.accepted ?? []).map(String),
|
|
374
|
+
rejected: (info.rejected ?? []).map(String),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* The IMAP read client for `config`. `newImapFlow` defaults to the real
|
|
381
|
+
* `imapflow` constructor; tests pass a fake so no socket is opened.
|
|
382
|
+
*/
|
|
383
|
+
export function createImapFlowClient(
|
|
384
|
+
config: ImapEndpointConfig,
|
|
385
|
+
newImapFlow: ImapFlowFactory = (options) => new ImapFlow(options),
|
|
386
|
+
): EmailReadClient {
|
|
387
|
+
return new ImapFlowClient(config, newImapFlow);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Construct the real nodemailer transport.
|
|
392
|
+
*
|
|
393
|
+
* Written as two explicit branches rather than passing `options` straight
|
|
394
|
+
* through. `NodemailerMailer` has already computed the guarantee, but a
|
|
395
|
+
* computed value is not something a reader — or a static analyser — can check
|
|
396
|
+
* at the call that actually opens the socket. Here it is a literal on both
|
|
397
|
+
* paths: implicit TLS, or STARTTLS required. There is no third branch.
|
|
398
|
+
*/
|
|
399
|
+
function realTransport(options: SmtpTransportOptions): Transporter {
|
|
400
|
+
const { host, port, auth } = options;
|
|
401
|
+
const tls = options.tls === undefined ? {} : { tls: options.tls };
|
|
402
|
+
if (options.secure) {
|
|
403
|
+
return createTransport({ host, port, auth, ...tls, secure: true });
|
|
404
|
+
}
|
|
405
|
+
return createTransport({ host, port, auth, ...tls, secure: false, requireTLS: true });
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* The SMTP mailer for `config`. `makeTransport` defaults to the real nodemailer
|
|
410
|
+
* transport; tests pass a fake so no socket is opened.
|
|
411
|
+
*/
|
|
412
|
+
export function createNodemailerMailer(
|
|
413
|
+
config: SmtpEndpointConfig,
|
|
414
|
+
makeTransport: TransportFactory = realTransport,
|
|
415
|
+
): EmailSendMailer {
|
|
416
|
+
return new NodemailerMailer(config, makeTransport);
|
|
417
|
+
}
|