sendpro-flowmailer-mcp 0.1.1

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/.env.example ADDED
@@ -0,0 +1,19 @@
1
+ SENDPRO_ACCOUNT_ID=your-account-id
2
+ SENDPRO_CLIENT_ID=your-client-id
3
+ SENDPRO_CLIENT_SECRET=your-client-secret
4
+
5
+ # Safe default is true. Set to false only when you want write tools enabled.
6
+ SENDPRO_READ_ONLY=true
7
+
8
+ # READ_ONLY is also supported for MCP clients that pass generic env names.
9
+ # READ_ONLY=true
10
+
11
+ SENDPRO_API_BASE_URL=https://api.flowmailer.net
12
+ SENDPRO_AUTH_BASE_URL=https://login.flowmailer.net
13
+ SENDPRO_API_MEDIA_TYPE=application/vnd.flowmailer.v1.12+json
14
+
15
+ # Legacy aliases are also supported:
16
+ # FLOWMAILER_ACCOUNT_ID=your-account-id
17
+ # FLOWMAILER_CLIENT_ID=your-client-id
18
+ # FLOWMAILER_CLIENT_SECRET=your-client-secret
19
+ # FLOWMAILER_READ_ONLY=true
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sociuu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,295 @@
1
+ # Spotler SendPro / FlowMailer MCP Server
2
+
3
+ [![GitHub Release](https://img.shields.io/github/v/release/Mestika/sendpro-flowmailer-mcp?sort=semver&display_name=tag)](https://github.com/Mestika/sendpro-flowmailer-mcp/releases)
4
+ [![npm version](https://img.shields.io/npm/v/sendpro-flowmailer-mcp.svg)](https://www.npmjs.com/package/sendpro-flowmailer-mcp)
5
+ [![npm downloads](https://img.shields.io/npm/dm/sendpro-flowmailer-mcp.svg)](https://www.npmjs.com/package/sendpro-flowmailer-mcp)
6
+ [![CI](https://github.com/Mestika/sendpro-flowmailer-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/Mestika/sendpro-flowmailer-mcp/actions/workflows/ci.yml)
7
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/Mestika/sendpro-flowmailer-mcp/badge)](https://scorecard.dev/viewer/?uri=github.com/Mestika/sendpro-flowmailer-mcp)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
9
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D20-339933)](package.json)
10
+ [![TypeScript](https://img.shields.io/badge/TypeScript-6.x-3178c6?logo=typescript&logoColor=white)](tsconfig.json)
11
+ [![MCP](https://img.shields.io/badge/MCP-stdio-blue)](https://modelcontextprotocol.io/)
12
+ [![Read-only default](https://img.shields.io/badge/read--only-default-success)](#read-only-mode)
13
+
14
+ Unofficial Model Context Protocol server for the Spotler SendPro API, formerly known as FlowMailer.
15
+
16
+ This project is not affiliated with, endorsed by, or maintained by Spotler. It is a community MCP server for teams that need controlled AI-agent access to SendPro account data and, when explicitly enabled, SendPro message operations.
17
+
18
+ <!-- mcp-name: io.github.mestika/sendpro-flowmailer-mcp -->
19
+
20
+ ## What It Does
21
+
22
+ - Connects to SendPro over OAuth2 client credentials.
23
+ - Exposes SendPro operations as MCP tools over stdio.
24
+ - Defaults to read-only mode.
25
+ - Blocks mutating SendPro requests when read-only mode is enabled.
26
+ - Supports current SendPro naming plus legacy FlowMailer environment variables.
27
+ - Includes MCP Registry metadata in [server.json](server.json).
28
+
29
+ ## Install
30
+
31
+ From npm:
32
+
33
+ ```bash
34
+ npx -y sendpro-flowmailer-mcp
35
+ ```
36
+
37
+ Directly from GitHub:
38
+
39
+ ```bash
40
+ npx -y github:Mestika/sendpro-flowmailer-mcp
41
+ ```
42
+
43
+ Local development:
44
+
45
+ ```bash
46
+ git clone https://github.com/Mestika/sendpro-flowmailer-mcp.git
47
+ cd sendpro-flowmailer-mcp
48
+ npm install
49
+ npm run build
50
+ node dist/index.js
51
+ ```
52
+
53
+ ## Configuration
54
+
55
+ Set credentials through your MCP client config, shell environment, or a local `.env` file.
56
+
57
+ | Variable | Required | Default | Description |
58
+ | --- | --- | --- | --- |
59
+ | `SENDPRO_ACCOUNT_ID` | Yes | | SendPro account id. |
60
+ | `SENDPRO_CLIENT_ID` | Yes | | OAuth client id. |
61
+ | `SENDPRO_CLIENT_SECRET` | Yes | | OAuth client secret. |
62
+ | `SENDPRO_READ_ONLY` | No | `true` | Safe-mode flag. When true, only SendPro read requests are allowed. |
63
+ | `READ_ONLY` | No | | Generic override for `SENDPRO_READ_ONLY`; useful for shared MCP config conventions. |
64
+ | `SENDPRO_API_BASE_URL` | No | `https://api.flowmailer.net` | SendPro API base URL. |
65
+ | `SENDPRO_AUTH_BASE_URL` | No | `https://login.flowmailer.net` | OAuth base URL. |
66
+ | `SENDPRO_API_MEDIA_TYPE` | No | `application/vnd.flowmailer.v1.12+json` | SendPro vendor media type. |
67
+
68
+ Legacy aliases are supported:
69
+
70
+ | Current | Spotler alias | Legacy FlowMailer alias |
71
+ | --- | --- | --- |
72
+ | `SENDPRO_ACCOUNT_ID` | `SPOTLER_SENDPRO_ACCOUNT_ID` | `FLOWMAILER_ACCOUNT_ID` |
73
+ | `SENDPRO_CLIENT_ID` | `SPOTLER_SENDPRO_CLIENT_ID` | `FLOWMAILER_CLIENT_ID` |
74
+ | `SENDPRO_CLIENT_SECRET` | `SPOTLER_SENDPRO_CLIENT_SECRET` | `FLOWMAILER_CLIENT_SECRET` |
75
+ | `SENDPRO_READ_ONLY` | `SPOTLER_SENDPRO_READ_ONLY` | `FLOWMAILER_READ_ONLY` |
76
+
77
+ Precedence is: `READ_ONLY`, then `SENDPRO_*`, then `SPOTLER_SENDPRO_*`, then `FLOWMAILER_*`.
78
+
79
+ ## Creating SendPro API Credentials
80
+
81
+ SendPro uses OAuth2 client credentials. You need three values:
82
+
83
+ - account id
84
+ - client id
85
+ - client secret
86
+
87
+ The public SendPro API docs state that the API uses OAuth2 client credentials, that the access token endpoint is `https://login.flowmailer.net/oauth/token`, and that the `client_id`, `client_secret`, `grant_type=client_credentials`, and optional `scope=api` form values are used to request a token. The same docs also expose source-system credential endpoints under `/{account_id}/sources/{source_id}/users`, including endpoints to list, create, get, update, and delete source credentials.
88
+
89
+ In the SendPro dashboard, the exact labels can vary by account, but the usual path is:
90
+
91
+ 1. Sign in to Spotler SendPro.
92
+ 2. Go to Setup.
93
+ 3. Open Sources or Source systems.
94
+ 4. Select the source system that should be used for API access, or create a dedicated source for MCP/automation usage.
95
+ 5. Open the source credentials/users section.
96
+ 6. Create new credentials with a clear description such as `MCP read-only`.
97
+ 7. Copy the generated client id and client secret, and store them in your MCP client environment.
98
+ 8. Use the SendPro account id from your account URL, dashboard context, or API settings as `SENDPRO_ACCOUNT_ID`.
99
+
100
+ For AI-agent usage, prefer credentials that are limited to the minimum access needed. Keep this MCP server in read-only mode unless you explicitly need submit, simulate, or resend operations.
101
+
102
+ Official references:
103
+
104
+ - [SendPro API authentication](https://flowmailer.com/apidoc/sendpro-api.html#_authentication)
105
+ - [POST `/oauth/token`](https://flowmailer.com/apidoc/sendpro-api.html#_post_oauth_token)
106
+ - [Source credential endpoints](https://flowmailer.com/apidoc/sendpro-api.html#_post_account_id_sources_source_id_users)
107
+ - [Spotler SendPro Help Center](https://sendpro.spotler.help/hc/en-gb)
108
+
109
+ ## Codex
110
+
111
+ Add this to `~/.codex/config.toml`:
112
+
113
+ ```toml
114
+ [mcp_servers.sendpro_flowmailer]
115
+ command = "npx"
116
+ args = ["-y", "sendpro-flowmailer-mcp"]
117
+ startup_timeout_sec = 20.0
118
+
119
+ [mcp_servers.sendpro_flowmailer.env]
120
+ SENDPRO_ACCOUNT_ID = "your-account-id"
121
+ SENDPRO_CLIENT_ID = "your-client-id"
122
+ SENDPRO_CLIENT_SECRET = "your-client-secret"
123
+ SENDPRO_READ_ONLY = "true"
124
+ ```
125
+
126
+ To run directly from GitHub:
127
+
128
+ ```toml
129
+ [mcp_servers.sendpro_flowmailer]
130
+ command = "npx"
131
+ args = ["-y", "github:Mestika/sendpro-flowmailer-mcp"]
132
+ startup_timeout_sec = 20.0
133
+ ```
134
+
135
+ ## Claude Desktop
136
+
137
+ Add this to `claude_desktop_config.json`:
138
+
139
+ ```json
140
+ {
141
+ "mcpServers": {
142
+ "sendpro-flowmailer": {
143
+ "command": "npx",
144
+ "args": ["-y", "sendpro-flowmailer-mcp"],
145
+ "env": {
146
+ "SENDPRO_ACCOUNT_ID": "your-account-id",
147
+ "SENDPRO_CLIENT_ID": "your-client-id",
148
+ "SENDPRO_CLIENT_SECRET": "your-client-secret",
149
+ "SENDPRO_READ_ONLY": "true"
150
+ }
151
+ }
152
+ }
153
+ }
154
+ ```
155
+
156
+ ## Read-Only Mode
157
+
158
+ `SENDPRO_READ_ONLY` defaults to `true`.
159
+
160
+ When read-only mode is enabled:
161
+
162
+ - MCP schemas only allow `GET` for the generic request tool.
163
+ - Mutating helper tools are not registered.
164
+ - The low-level client rejects `POST`, `PUT`, `PATCH`, and `DELETE` SendPro API requests before requesting an OAuth token.
165
+
166
+ OAuth itself still uses `POST https://login.flowmailer.net/oauth/token`, because SendPro requires OAuth2 client credentials before any resource can be read. The read-only guard applies to SendPro API resource calls against the configured API base URL.
167
+
168
+ ## Tools
169
+
170
+ Read tools:
171
+
172
+ | Tool | Description |
173
+ | --- | --- |
174
+ | `flowmailer_request` | Generic relative SendPro API request. In read-only mode, only `GET` is accepted. |
175
+ | `flowmailer_endpoint_catalog` | Lists SendPro endpoints known by this server. |
176
+ | `flowmailer_list_messages` | Lists messages using SendPro reference-range paging. |
177
+ | `flowmailer_get_message` | Gets one message by id. |
178
+ | `flowmailer_get_message_archive` | Gets archived content metadata for a message. |
179
+ | `flowmailer_get_message_error_archive` | Gets archived error content for a message. |
180
+ | `flowmailer_get_recipient` | Gets recipient information. |
181
+ | `flowmailer_list_resource` | Lists common resources such as flows, templates, sender domains, sources, and stats. |
182
+
183
+ Write tools, only when `SENDPRO_READ_ONLY=false`:
184
+
185
+ | Tool | Description |
186
+ | --- | --- |
187
+ | `flowmailer_submit_message` | Submits an email or SMS message. |
188
+ | `flowmailer_simulate_message` | Simulates an email or SMS message. |
189
+ | `flowmailer_resend_message` | Resends a message by id. |
190
+
191
+ The tool names currently retain the FlowMailer prefix because the public SendPro API and media types still use FlowMailer naming in several places. Package names and documentation use both SendPro and FlowMailer for discoverability.
192
+
193
+ ## Examples
194
+
195
+ List messages:
196
+
197
+ ```json
198
+ {
199
+ "method": "GET",
200
+ "path": "/{account_id}/messages",
201
+ "matrix": {
202
+ "daterange": "2026-05-01T00:00:00Z,2026-05-20T00:00:00Z"
203
+ },
204
+ "query": {
205
+ "addevents": true,
206
+ "sortfield": "INSERTED",
207
+ "sortorder": "ASC"
208
+ },
209
+ "range": "items=:10"
210
+ }
211
+ ```
212
+
213
+ List flows through the convenience tool:
214
+
215
+ ```json
216
+ {
217
+ "resource": "flows"
218
+ }
219
+ ```
220
+
221
+ Submit a message, only with `SENDPRO_READ_ONLY=false`:
222
+
223
+ ```json
224
+ {
225
+ "body": {
226
+ "messageType": "EMAIL",
227
+ "headerFromAddress": "sender@example.com",
228
+ "headerToAddress": "recipient@example.com",
229
+ "subject": "Hello",
230
+ "text": "Message body"
231
+ }
232
+ }
233
+ ```
234
+
235
+ ## Development
236
+
237
+ ```bash
238
+ npm install
239
+ npm run check
240
+ ```
241
+
242
+ Useful commands:
243
+
244
+ ```bash
245
+ npm run dev
246
+ npm run build
247
+ npm run typecheck
248
+ npm test
249
+ npm run inspect
250
+ ```
251
+
252
+ For repository structure, badges, release hygiene, and maintenance settings, see [docs/repository-health.md](docs/repository-health.md).
253
+
254
+ ## Publishing
255
+
256
+ This package is published to npm as [sendpro-flowmailer-mcp](https://www.npmjs.com/package/sendpro-flowmailer-mcp), and the repository includes MCP Registry metadata for registry submission.
257
+
258
+ For npm:
259
+
260
+ ```bash
261
+ npm publish --access public
262
+ ```
263
+
264
+ For the official MCP Registry, publish [server.json](server.json) after the npm package exists. The npm package includes:
265
+
266
+ ```json
267
+ {
268
+ "mcpName": "io.github.mestika/sendpro-flowmailer-mcp"
269
+ }
270
+ ```
271
+
272
+ ## Releases and Versions
273
+
274
+ This project uses semantic version tags such as `v0.1.0`, `v0.1.1`, and `v1.0.0`.
275
+
276
+ Use:
277
+
278
+ ```bash
279
+ npm run version:patch # bug fixes and small documentation-only release updates
280
+ npm run version:minor # new backwards-compatible tools or features
281
+ npm run version:major # breaking changes
282
+ git push origin main --follow-tags
283
+ ```
284
+
285
+ Then create a GitHub Release from the new tag. The GitHub Release is the public changelog entry; the Git tag is the exact source snapshot. See [docs/releasing.md](docs/releasing.md) for the full checklist.
286
+
287
+ ## Security
288
+
289
+ Never commit real SendPro credentials. Use MCP client environment variables, shell environment variables, or a local `.env` file.
290
+
291
+ See [SECURITY.md](SECURITY.md) for vulnerability reporting and operational notes.
292
+
293
+ ## License
294
+
295
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,11 @@
1
+ export interface FlowMailerConfig {
2
+ accountId: string;
3
+ clientId: string;
4
+ clientSecret: string;
5
+ readOnly: boolean;
6
+ apiBaseUrl: string;
7
+ authBaseUrl: string;
8
+ apiMediaType: string;
9
+ }
10
+ export type Env = Record<string, string | undefined>;
11
+ export declare function loadConfig(env?: Env): FlowMailerConfig;
package/dist/config.js ADDED
@@ -0,0 +1,45 @@
1
+ const ENV_ALIASES = {
2
+ accountId: ["SENDPRO_ACCOUNT_ID", "SPOTLER_SENDPRO_ACCOUNT_ID", "FLOWMAILER_ACCOUNT_ID"],
3
+ clientId: ["SENDPRO_CLIENT_ID", "SPOTLER_SENDPRO_CLIENT_ID", "FLOWMAILER_CLIENT_ID"],
4
+ clientSecret: ["SENDPRO_CLIENT_SECRET", "SPOTLER_SENDPRO_CLIENT_SECRET", "FLOWMAILER_CLIENT_SECRET"],
5
+ readOnly: ["READ_ONLY", "SENDPRO_READ_ONLY", "SPOTLER_SENDPRO_READ_ONLY", "FLOWMAILER_READ_ONLY"],
6
+ apiBaseUrl: ["SENDPRO_API_BASE_URL", "SPOTLER_SENDPRO_API_BASE_URL", "FLOWMAILER_API_BASE_URL"],
7
+ authBaseUrl: ["SENDPRO_AUTH_BASE_URL", "SPOTLER_SENDPRO_AUTH_BASE_URL", "FLOWMAILER_AUTH_BASE_URL"],
8
+ apiMediaType: ["SENDPRO_API_MEDIA_TYPE", "SPOTLER_SENDPRO_API_MEDIA_TYPE", "FLOWMAILER_API_MEDIA_TYPE"]
9
+ };
10
+ export function loadConfig(env = process.env) {
11
+ const accountId = readFirst(env, ENV_ALIASES.accountId);
12
+ const clientId = readFirst(env, ENV_ALIASES.clientId);
13
+ const clientSecret = readFirst(env, ENV_ALIASES.clientSecret);
14
+ const missing = [
15
+ ["account id", ENV_ALIASES.accountId, accountId],
16
+ ["client id", ENV_ALIASES.clientId, clientId],
17
+ ["client secret", ENV_ALIASES.clientSecret, clientSecret]
18
+ ].flatMap(([label, names, value]) => (value ? [] : [`${label}: ${formatAliases(names)}`]));
19
+ if (missing.length > 0) {
20
+ throw new Error(`Missing required SendPro environment variables: ${missing.join("; ")}`);
21
+ }
22
+ return {
23
+ accountId: accountId,
24
+ clientId: clientId,
25
+ clientSecret: clientSecret,
26
+ readOnly: parseReadOnly(readFirst(env, ENV_ALIASES.readOnly) ?? "true"),
27
+ apiBaseUrl: stripTrailingSlash(readFirst(env, ENV_ALIASES.apiBaseUrl) ?? "https://api.flowmailer.net"),
28
+ authBaseUrl: stripTrailingSlash(readFirst(env, ENV_ALIASES.authBaseUrl) ?? "https://login.flowmailer.net"),
29
+ apiMediaType: readFirst(env, ENV_ALIASES.apiMediaType) ?? "application/vnd.flowmailer.v1.12+json"
30
+ };
31
+ }
32
+ function readFirst(env, names) {
33
+ return names.map((name) => env[name]).find((value) => value !== undefined && value.length > 0);
34
+ }
35
+ function formatAliases(names) {
36
+ const [primary, ...aliases] = names;
37
+ return aliases.length === 0 ? primary : `${primary} (aliases: ${aliases.join(", ")})`;
38
+ }
39
+ function parseReadOnly(value) {
40
+ return !["false", "0", "no", "off"].includes(value.trim().toLowerCase());
41
+ }
42
+ function stripTrailingSlash(value) {
43
+ return value.replace(/\/+$/, "");
44
+ }
45
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import type { FlowMailerConfig } from "./config.js";
2
+ export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
3
+ export type ParameterValue = string | number | boolean | null | undefined | Array<string | number | boolean>;
4
+ export interface FlowMailerRequest {
5
+ method: HttpMethod;
6
+ path: string;
7
+ accountId?: string;
8
+ matrix?: Record<string, ParameterValue>;
9
+ query?: Record<string, ParameterValue>;
10
+ body?: unknown;
11
+ range?: string;
12
+ }
13
+ export interface FlowMailerResponse<T = unknown> {
14
+ status: number;
15
+ headers: Record<string, string>;
16
+ data: T | undefined;
17
+ }
18
+ export declare class ReadOnlyViolationError extends Error {
19
+ constructor(method: string, path: string);
20
+ }
21
+ export declare class FlowMailerApiError extends Error {
22
+ readonly status: number;
23
+ readonly body: unknown;
24
+ readonly headers: Record<string, string>;
25
+ constructor(status: number, body: unknown, headers: Record<string, string>);
26
+ }
27
+ export declare class FlowMailerClient {
28
+ private readonly config;
29
+ private readonly fetchImpl;
30
+ private token;
31
+ constructor(config: FlowMailerConfig, fetchImpl?: typeof fetch);
32
+ request<T = unknown>(request: FlowMailerRequest): Promise<FlowMailerResponse<T>>;
33
+ private requestWithToken;
34
+ private getAccessToken;
35
+ private buildApiUrl;
36
+ private resolvePath;
37
+ private buildApiHeaders;
38
+ private parseResponse;
39
+ private assertReadOnlyAllows;
40
+ }
@@ -0,0 +1,191 @@
1
+ export class ReadOnlyViolationError extends Error {
2
+ constructor(method, path) {
3
+ super(`READ_ONLY is enabled; refusing ${method.toUpperCase()} ${path}`);
4
+ this.name = "ReadOnlyViolationError";
5
+ }
6
+ }
7
+ export class FlowMailerApiError extends Error {
8
+ status;
9
+ body;
10
+ headers;
11
+ constructor(status, body, headers) {
12
+ super(`FlowMailer API request failed with status ${status}`);
13
+ this.status = status;
14
+ this.body = body;
15
+ this.headers = headers;
16
+ this.name = "FlowMailerApiError";
17
+ }
18
+ }
19
+ export class FlowMailerClient {
20
+ config;
21
+ fetchImpl;
22
+ token;
23
+ constructor(config, fetchImpl = fetch) {
24
+ this.config = config;
25
+ this.fetchImpl = fetchImpl;
26
+ }
27
+ async request(request) {
28
+ this.assertReadOnlyAllows(request.method, request.path);
29
+ const url = this.buildApiUrl(request);
30
+ return this.requestWithToken(url, request, true);
31
+ }
32
+ async requestWithToken(url, request, allowTokenRefreshRetry) {
33
+ const token = await this.getAccessToken();
34
+ const response = await this.fetchImpl(url, {
35
+ method: request.method,
36
+ headers: this.buildApiHeaders(token, request),
37
+ body: request.body === undefined ? undefined : JSON.stringify(request.body)
38
+ });
39
+ if (response.status === 401 && allowTokenRefreshRetry) {
40
+ this.token = undefined;
41
+ return this.requestWithToken(url, request, false);
42
+ }
43
+ return this.parseResponse(response);
44
+ }
45
+ async getAccessToken() {
46
+ const now = Date.now();
47
+ if (this.token && this.token.expiresAt > now) {
48
+ return this.token.accessToken;
49
+ }
50
+ const body = new URLSearchParams({
51
+ client_id: this.config.clientId,
52
+ client_secret: this.config.clientSecret,
53
+ grant_type: "client_credentials",
54
+ scope: "api"
55
+ });
56
+ const response = await this.fetchImpl(`${this.config.authBaseUrl}/oauth/token`, {
57
+ method: "POST",
58
+ headers: {
59
+ accept: this.config.apiMediaType,
60
+ "content-type": "application/x-www-form-urlencoded"
61
+ },
62
+ body
63
+ });
64
+ const parsed = (await readResponseBody(response));
65
+ if (!response.ok || !parsed?.access_token) {
66
+ throw new FlowMailerApiError(response.status, parsed, selectedHeaders(response.headers));
67
+ }
68
+ const expiresInSeconds = typeof parsed.expires_in === "number" ? parsed.expires_in : 60;
69
+ this.token = {
70
+ accessToken: parsed.access_token,
71
+ expiresAt: now + Math.max(1, expiresInSeconds - 5) * 1000
72
+ };
73
+ return this.token.accessToken;
74
+ }
75
+ buildApiUrl(request) {
76
+ const resolvedPath = this.resolvePath(request.path, request.accountId);
77
+ const matrixPath = appendMatrixParams(resolvedPath, request.matrix);
78
+ const url = new URL(matrixPath, `${this.config.apiBaseUrl}/`);
79
+ appendSearchParams(url, request.query);
80
+ return url.toString();
81
+ }
82
+ resolvePath(path, accountId) {
83
+ if (!path.startsWith("/")) {
84
+ throw new Error("FlowMailer API path must be a relative path starting with /");
85
+ }
86
+ if (path.startsWith("//")) {
87
+ throw new Error("FlowMailer API path must not be protocol-relative");
88
+ }
89
+ return path.replaceAll("{account_id}", encodeURIComponent(accountId ?? this.config.accountId));
90
+ }
91
+ buildApiHeaders(token, request) {
92
+ const headers = {
93
+ authorization: `Bearer ${token}`,
94
+ accept: this.config.apiMediaType
95
+ };
96
+ if (request.range) {
97
+ headers.range = request.range;
98
+ }
99
+ if (request.body !== undefined) {
100
+ headers["content-type"] = this.config.apiMediaType;
101
+ }
102
+ return headers;
103
+ }
104
+ async parseResponse(response) {
105
+ const headers = selectedHeaders(response.headers);
106
+ const data = await readResponseBody(response);
107
+ if (!response.ok) {
108
+ throw new FlowMailerApiError(response.status, data, headers);
109
+ }
110
+ return {
111
+ status: response.status,
112
+ headers,
113
+ data: data
114
+ };
115
+ }
116
+ assertReadOnlyAllows(method, path) {
117
+ if (this.config.readOnly && method !== "GET") {
118
+ throw new ReadOnlyViolationError(method, path);
119
+ }
120
+ }
121
+ }
122
+ function appendMatrixParams(path, matrix) {
123
+ if (!matrix) {
124
+ return path;
125
+ }
126
+ const entries = Object.entries(matrix).flatMap(([key, value]) => {
127
+ const serialized = serializeParameterValue(value);
128
+ return serialized === undefined ? [] : [[key, serialized]];
129
+ });
130
+ if (entries.length === 0) {
131
+ return path;
132
+ }
133
+ const encoded = entries
134
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
135
+ .join(";");
136
+ return `${path};${encoded}`;
137
+ }
138
+ function appendSearchParams(url, query) {
139
+ if (!query) {
140
+ return;
141
+ }
142
+ for (const [key, value] of Object.entries(query)) {
143
+ const serialized = serializeParameterValue(value);
144
+ if (serialized !== undefined) {
145
+ url.searchParams.set(key, serialized);
146
+ }
147
+ }
148
+ }
149
+ function serializeParameterValue(value) {
150
+ if (value === undefined || value === null || value === "") {
151
+ return undefined;
152
+ }
153
+ if (Array.isArray(value)) {
154
+ return value.length === 0 ? undefined : value.map(String).join(",");
155
+ }
156
+ return String(value);
157
+ }
158
+ async function readResponseBody(response) {
159
+ if (response.status === 204 || response.status === 205) {
160
+ return undefined;
161
+ }
162
+ const text = await response.text();
163
+ if (text.length === 0) {
164
+ return undefined;
165
+ }
166
+ const contentType = response.headers.get("content-type") ?? "";
167
+ if (contentType.includes("json") || /^[\[{]/.test(text.trim())) {
168
+ try {
169
+ return JSON.parse(text);
170
+ }
171
+ catch {
172
+ return text;
173
+ }
174
+ }
175
+ return text;
176
+ }
177
+ function selectedHeaders(headers) {
178
+ const keep = [
179
+ "content-range",
180
+ "content-type",
181
+ "location",
182
+ "next-range",
183
+ "retry-after",
184
+ "www-authenticate"
185
+ ];
186
+ return Object.fromEntries(keep.flatMap((name) => {
187
+ const value = headers.get(name);
188
+ return value === null ? [] : [[name, value]];
189
+ }));
190
+ }
191
+ //# sourceMappingURL=flowmailer-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flowmailer-client.js","sourceRoot":"","sources":["../src/flowmailer-client.ts"],"names":[],"mappings":"AA+BA,MAAM,OAAO,sBAAuB,SAAQ,KAAK;IAC/C,YAAY,MAAc,EAAE,IAAY;QACtC,KAAK,CAAC,kCAAkC,MAAM,CAAC,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC;QACxE,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;IACvC,CAAC;CACF;AAED,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAEzB;IACA;IACA;IAHlB,YACkB,MAAc,EACd,IAAa,EACb,OAA+B;QAE/C,KAAK,CAAC,6CAA6C,MAAM,EAAE,CAAC,CAAC;QAJ7C,WAAM,GAAN,MAAM,CAAQ;QACd,SAAI,GAAJ,IAAI,CAAS;QACb,YAAO,GAAP,OAAO,CAAwB;QAG/C,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AAED,MAAM,OAAO,gBAAgB;IAIR;IACA;IAJX,KAAK,CAAyB;IAEtC,YACmB,MAAwB,EACxB,YAA0B,KAAK;QAD/B,WAAM,GAAN,MAAM,CAAkB;QACxB,cAAS,GAAT,SAAS,CAAsB;IAC/C,CAAC;IAEJ,KAAK,CAAC,OAAO,CAAc,OAA0B;QACnD,IAAI,CAAC,oBAAoB,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;QACxD,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QACtC,OAAO,IAAI,CAAC,gBAAgB,CAAI,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IACtD,CAAC;IAEO,KAAK,CAAC,gBAAgB,CAC5B,GAAW,EACX,OAA0B,EAC1B,sBAA+B;QAE/B,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,cAAc,EAAE,CAAC;QAC1C,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE;YACzC,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,OAAO,EAAE,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,OAAO,CAAC;YAC7C,IAAI,EAAE,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC;SAC5E,CAAC,CAAC;QAEH,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,sBAAsB,EAAE,CAAC;YACtD,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC;YACvB,OAAO,IAAI,CAAC,gBAAgB,CAAI,GAAG,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;QACvD,CAAC;QAED,OAAO,IAAI,CAAC,aAAa,CAAI,QAAQ,CAAC,CAAC;IACzC,CAAC;IAEO,KAAK,CAAC,cAAc;QAC1B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC;YAC7C,OAAO,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC;QAChC,CAAC;QAED,MAAM,IAAI,GAAG,IAAI,eAAe,CAAC;YAC/B,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ;YAC/B,aAAa,EAAE,IAAI,CAAC,MAAM,CAAC,YAAY;YACvC,UAAU,EAAE,oBAAoB;YAChC,KAAK,EAAE,KAAK;SACb,CAAC,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,cAAc,EAAE;YAC9E,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,YAAY;gBAChC,cAAc,EAAE,mCAAmC;aACpD;YACD,IAAI;SACL,CAAC,CAAC;QACH,MAAM,MAAM,GAAG,CAAC,MAAM,gBAAgB,CAAC,QAAQ,CAAC,CAAuB,CAAC;QAExE,IAAI,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC;YAC1C,MAAM,IAAI,kBAAkB,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,eAAe,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;QAC3F,CAAC;QAED,MAAM,gBAAgB,GAAG,OAAO,MAAM,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;QACxF,IAAI,CAAC,KAAK,GAAG;YACX,WAAW,EAAE,MAAM,CAAC,YAAY;YAChC,SAAS,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,gBAAgB,GAAG,CAAC,CAAC,GAAG,IAAI;SAC1D,CAAC;QACF,OAAO,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC;IAChC,CAAC;IAEO,WAAW,CAAC,OAA0B;QAC5C,MAAM,YAAY,GAAG,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;QACvE,MAAM,UAAU,GAAG,kBAAkB,CAAC,YAAY,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;QACpE,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,UAAU,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,UAAU,GAAG,CAAC,CAAC;QAC9D,kBAAkB,CAAC,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;QACvC,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAC;IACxB,CAAC;IAEO,WAAW,CAAC,IAAY,EAAE,SAA6B;QAC7D,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAC1B,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAC;QACjF,CAAC;QACD,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YAC1B,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;QACvE,CAAC;QAED,OAAO,IAAI,CAAC,UAAU,CAAC,cAAc,EAAE,kBAAkB,CAAC,SAAS,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;IACjG,CAAC;IAEO,eAAe,CAAC,KAAa,EAAE,OAA0B;QAC/D,MAAM,OAAO,GAA2B;YACtC,aAAa,EAAE,UAAU,KAAK,EAAE;YAChC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,YAAY;SACjC,CAAC;QAEF,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;YAClB,OAAO,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QAChC,CAAC;QACD,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC/B,OAAO,CAAC,cAAc,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC;QACrD,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC;IAEO,KAAK,CAAC,aAAa,CAAI,QAAkB;QAC/C,MAAM,OAAO,GAAG,eAAe,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QAClD,MAAM,IAAI,GAAG,MAAM,gBAAgB,CAAC,QAAQ,CAAC,CAAC;QAE9C,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,kBAAkB,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;QAC/D,CAAC;QAED,OAAO;YACL,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,OAAO;YACP,IAAI,EAAE,IAAqB;SAC5B,CAAC;IACJ,CAAC;IAEO,oBAAoB,CAAC,MAAkB,EAAE,IAAY;QAC3D,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;YAC7C,MAAM,IAAI,sBAAsB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACjD,CAAC;IACH,CAAC;CACF;AAED,SAAS,kBAAkB,CAAC,IAAY,EAAE,MAAkD;IAC1F,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;QAC9D,MAAM,UAAU,GAAG,uBAAuB,CAAC,KAAK,CAAC,CAAC;QAClD,OAAO,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,UAAU,CAAU,CAAC,CAAC;IACtE,CAAC,CAAC,CAAC;IAEH,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,OAAO,GAAG,OAAO;SACpB,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,kBAAkB,CAAC,GAAG,CAAC,IAAI,kBAAkB,CAAC,KAAK,CAAC,EAAE,CAAC;SAChF,IAAI,CAAC,GAAG,CAAC,CAAC;IACb,OAAO,GAAG,IAAI,IAAI,OAAO,EAAE,CAAC;AAC9B,CAAC;AAED,SAAS,kBAAkB,CAAC,GAAQ,EAAE,KAAiD;IACrF,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO;IACT,CAAC;IAED,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACjD,MAAM,UAAU,GAAG,uBAAuB,CAAC,KAAK,CAAC,CAAC;QAClD,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;YAC7B,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;QACxC,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,uBAAuB,CAAC,KAAqB;IACpD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;QAC1D,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC;AAED,KAAK,UAAU,gBAAgB,CAAC,QAAkB;IAChD,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QACvD,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IACnC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;IAC/D,IAAI,WAAW,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;QAC/D,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC1B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,eAAe,CAAC,OAAgB;IACvC,MAAM,IAAI,GAAG;QACX,eAAe;QACf,cAAc;QACd,UAAU;QACV,YAAY;QACZ,aAAa;QACb,kBAAkB;KACnB,CAAC;IACF,OAAO,MAAM,CAAC,WAAW,CACvB,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;QACpB,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAChC,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;IAC/C,CAAC,CAAC,CACH,CAAC;AACJ,CAAC"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";