spinupmail 0.1.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/dist/index.cjs ADDED
@@ -0,0 +1,870 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let zod = require("zod");
3
+ //#region ../contracts/src/index.ts
4
+ const apiErrorSchema = zod.z.object({
5
+ error: zod.z.string().min(1),
6
+ details: zod.z.string().min(1).optional()
7
+ });
8
+ const sortDirectionSchema = zod.z.enum(["asc", "desc"]);
9
+ const emailAddressSortBySchema = zod.z.enum([
10
+ "createdAt",
11
+ "address",
12
+ "lastReceivedAt"
13
+ ]);
14
+ const recentAddressActivitySortBySchema = zod.z.enum(["recentActivity", "createdAt"]);
15
+ const maxReceivedEmailActionSchema = zod.z.enum(["cleanAll", "rejectNew"]);
16
+ const emailOrderSchema = sortDirectionSchema;
17
+ const inboundRatePolicySchema = zod.z.object({
18
+ senderDomainSoftMax: zod.z.number().int().positive().optional(),
19
+ senderDomainSoftWindowSeconds: zod.z.number().int().positive().optional(),
20
+ senderDomainBlockMax: zod.z.number().int().positive().optional(),
21
+ senderDomainBlockWindowSeconds: zod.z.number().int().positive().optional(),
22
+ senderAddressBlockMax: zod.z.number().int().positive().optional(),
23
+ senderAddressBlockWindowSeconds: zod.z.number().int().positive().optional(),
24
+ inboxBlockMax: zod.z.number().int().positive().optional(),
25
+ inboxBlockWindowSeconds: zod.z.number().int().positive().optional(),
26
+ dedupeWindowSeconds: zod.z.number().int().positive().optional(),
27
+ initialBlockSeconds: zod.z.number().int().positive().optional(),
28
+ maxBlockSeconds: zod.z.number().int().positive().optional()
29
+ }).partial();
30
+ const domainConfigSchema = zod.z.object({
31
+ items: zod.z.array(zod.z.string().min(1)),
32
+ default: zod.z.string().nullable(),
33
+ forcedLocalPartPrefix: zod.z.string().nullable(),
34
+ maxReceivedEmailsPerOrganization: zod.z.number().int().positive(),
35
+ maxReceivedEmailsPerAddress: zod.z.number().int().positive()
36
+ });
37
+ const organizationStatsItemSchema = zod.z.object({
38
+ organizationId: zod.z.string().min(1),
39
+ memberCount: zod.z.number().int().nonnegative(),
40
+ addressCount: zod.z.number().int().nonnegative(),
41
+ emailCount: zod.z.number().int().nonnegative()
42
+ });
43
+ zod.z.object({ items: zod.z.array(organizationStatsItemSchema) });
44
+ const emailAddressSchema = zod.z.object({
45
+ id: zod.z.string().min(1),
46
+ address: zod.z.string().min(1),
47
+ localPart: zod.z.string().min(1),
48
+ domain: zod.z.string().min(1),
49
+ meta: zod.z.unknown().optional(),
50
+ emailCount: zod.z.number().int().nonnegative(),
51
+ allowedFromDomains: zod.z.array(zod.z.string().min(1)).optional(),
52
+ blockedSenderDomains: zod.z.array(zod.z.string().min(1)).optional(),
53
+ inboundRatePolicy: inboundRatePolicySchema.nullable().optional(),
54
+ maxReceivedEmailCount: zod.z.number().int().positive().nullable(),
55
+ maxReceivedEmailAction: maxReceivedEmailActionSchema.nullable(),
56
+ createdAt: zod.z.string().nullable(),
57
+ createdAtMs: zod.z.number().nullable(),
58
+ expiresAt: zod.z.string().nullable(),
59
+ expiresAtMs: zod.z.number().nullable(),
60
+ lastReceivedAt: zod.z.string().nullable(),
61
+ lastReceivedAtMs: zod.z.number().nullable()
62
+ });
63
+ const createEmailAddressResponseSchema = emailAddressSchema;
64
+ const emailAddressListResponseSchema = zod.z.object({
65
+ items: zod.z.array(emailAddressSchema),
66
+ page: zod.z.number().int().positive(),
67
+ pageSize: zod.z.number().int().positive(),
68
+ totalItems: zod.z.number().int().nonnegative(),
69
+ addressLimit: zod.z.number().int().positive(),
70
+ totalPages: zod.z.number().int().positive(),
71
+ sortBy: emailAddressSortBySchema,
72
+ sortDirection: sortDirectionSchema
73
+ });
74
+ const listEmailAddressesParamsSchema = zod.z.object({
75
+ page: zod.z.number().int().positive().optional(),
76
+ pageSize: zod.z.number().int().positive().optional(),
77
+ search: zod.z.string().optional(),
78
+ sortBy: emailAddressSortBySchema.optional(),
79
+ sortDirection: sortDirectionSchema.optional()
80
+ });
81
+ const listRecentAddressActivityParamsSchema = zod.z.object({
82
+ limit: zod.z.number().int().positive().optional(),
83
+ cursor: zod.z.string().min(1).optional(),
84
+ search: zod.z.string().optional(),
85
+ sortBy: recentAddressActivitySortBySchema.optional(),
86
+ sortDirection: sortDirectionSchema.optional()
87
+ });
88
+ const recentAddressActivityResponseSchema = zod.z.object({
89
+ items: zod.z.array(emailAddressSchema),
90
+ nextCursor: zod.z.string().nullable(),
91
+ totalItems: zod.z.number().int().nonnegative()
92
+ });
93
+ const createEmailAddressRequestSchema = zod.z.object({
94
+ localPart: zod.z.string().min(1),
95
+ ttlMinutes: zod.z.number().int().positive().optional(),
96
+ meta: zod.z.unknown().optional(),
97
+ domain: zod.z.string().min(1).optional(),
98
+ allowedFromDomains: zod.z.array(zod.z.string().min(1)).optional(),
99
+ blockedSenderDomains: zod.z.array(zod.z.string().min(1)).optional(),
100
+ inboundRatePolicy: inboundRatePolicySchema.optional(),
101
+ maxReceivedEmailCount: zod.z.number().int().positive().optional(),
102
+ maxReceivedEmailAction: maxReceivedEmailActionSchema.optional(),
103
+ acceptedRiskNotice: zod.z.literal(true)
104
+ });
105
+ const updateEmailAddressRequestSchema = zod.z.object({
106
+ localPart: zod.z.string().min(1).optional(),
107
+ ttlMinutes: zod.z.number().int().positive().nullable().optional(),
108
+ meta: zod.z.unknown().optional(),
109
+ domain: zod.z.string().min(1).optional(),
110
+ allowedFromDomains: zod.z.array(zod.z.string().min(1)).optional(),
111
+ blockedSenderDomains: zod.z.array(zod.z.string().min(1)).nullable().optional(),
112
+ inboundRatePolicy: inboundRatePolicySchema.nullable().optional(),
113
+ maxReceivedEmailCount: zod.z.number().int().positive().nullable().optional(),
114
+ maxReceivedEmailAction: maxReceivedEmailActionSchema.optional()
115
+ });
116
+ const deleteEmailAddressResponseSchema = zod.z.object({
117
+ id: zod.z.string().min(1),
118
+ address: zod.z.string().min(1),
119
+ deleted: zod.z.literal(true)
120
+ });
121
+ const emailAttachmentSchema = zod.z.object({
122
+ id: zod.z.string().min(1),
123
+ filename: zod.z.string().min(1),
124
+ contentType: zod.z.string().min(1),
125
+ size: zod.z.number().int().nonnegative(),
126
+ disposition: zod.z.string().nullable(),
127
+ contentId: zod.z.string().nullable(),
128
+ inlinePath: zod.z.string().min(1),
129
+ downloadPath: zod.z.string().min(1)
130
+ });
131
+ const emailListItemSchema = zod.z.object({
132
+ id: zod.z.string().min(1),
133
+ addressId: zod.z.string().min(1),
134
+ to: zod.z.string().min(1),
135
+ from: zod.z.string().min(1),
136
+ sender: zod.z.string().nullable().optional(),
137
+ senderLabel: zod.z.string().min(1),
138
+ subject: zod.z.string().nullable().optional(),
139
+ messageId: zod.z.string().nullable().optional(),
140
+ rawSize: zod.z.number().nullable().optional(),
141
+ rawTruncated: zod.z.boolean(),
142
+ isSample: zod.z.boolean(),
143
+ hasHtml: zod.z.boolean(),
144
+ hasText: zod.z.boolean(),
145
+ attachmentCount: zod.z.number().int().nonnegative(),
146
+ receivedAt: zod.z.string().nullable(),
147
+ receivedAtMs: zod.z.number().nullable()
148
+ });
149
+ const emailListResponseSchema = zod.z.object({
150
+ address: zod.z.string().min(1),
151
+ addressId: zod.z.string().min(1),
152
+ items: zod.z.array(emailListItemSchema)
153
+ });
154
+ const listEmailsParamsSchema = zod.z.object({
155
+ address: zod.z.string().min(1).optional(),
156
+ addressId: zod.z.string().min(1).optional(),
157
+ search: zod.z.string().max(30).optional(),
158
+ limit: zod.z.number().int().positive().optional(),
159
+ order: emailOrderSchema.optional(),
160
+ after: zod.z.union([zod.z.string(), zod.z.number()]).optional(),
161
+ before: zod.z.union([zod.z.string(), zod.z.number()]).optional()
162
+ });
163
+ const emailDetailSchema = zod.z.object({
164
+ id: zod.z.string().min(1),
165
+ addressId: zod.z.string().min(1),
166
+ address: zod.z.string().min(1).optional(),
167
+ to: zod.z.string().min(1),
168
+ from: zod.z.string().min(1),
169
+ sender: zod.z.string().nullable().optional(),
170
+ senderLabel: zod.z.string().min(1),
171
+ subject: zod.z.string().nullable().optional(),
172
+ messageId: zod.z.string().nullable().optional(),
173
+ headers: zod.z.unknown(),
174
+ html: zod.z.string().nullable().optional(),
175
+ text: zod.z.string().nullable().optional(),
176
+ raw: zod.z.string().nullable().optional(),
177
+ rawSize: zod.z.number().nullable().optional(),
178
+ rawTruncated: zod.z.boolean(),
179
+ isSample: zod.z.boolean(),
180
+ rawDownloadPath: zod.z.string().optional(),
181
+ attachments: zod.z.array(emailAttachmentSchema),
182
+ receivedAt: zod.z.string().nullable(),
183
+ receivedAtMs: zod.z.number().nullable()
184
+ });
185
+ const deleteEmailResponseSchema = zod.z.object({
186
+ id: zod.z.string().min(1),
187
+ deleted: zod.z.literal(true)
188
+ });
189
+ const emailActivityDaySchema = zod.z.object({
190
+ date: zod.z.string().min(1),
191
+ count: zod.z.number().int().nonnegative()
192
+ });
193
+ const emailActivityResponseSchema = zod.z.object({
194
+ timezone: zod.z.string().min(1),
195
+ daily: zod.z.array(emailActivityDaySchema)
196
+ });
197
+ const emailSummaryDomainSchema = zod.z.object({
198
+ domain: zod.z.string().min(1),
199
+ count: zod.z.number().int().nonnegative()
200
+ });
201
+ const busiestInboxSchema = zod.z.object({
202
+ addressId: zod.z.string().min(1),
203
+ address: zod.z.string().min(1),
204
+ count: zod.z.number().int().nonnegative()
205
+ });
206
+ const dormantInboxSchema = zod.z.object({
207
+ addressId: zod.z.string().min(1),
208
+ address: zod.z.string().min(1),
209
+ createdAt: zod.z.string().nullable()
210
+ });
211
+ const emailSummaryResponseSchema = zod.z.object({
212
+ totalEmailCount: zod.z.number().int().nonnegative(),
213
+ attachmentCount: zod.z.number().int().nonnegative(),
214
+ attachmentSizeTotal: zod.z.number().int().nonnegative(),
215
+ attachmentSizeLimit: zod.z.number().int().nonnegative(),
216
+ topDomains: zod.z.array(emailSummaryDomainSchema),
217
+ busiestInboxes: zod.z.array(busiestInboxSchema),
218
+ dormantInboxes: zod.z.array(dormantInboxSchema)
219
+ });
220
+ const organizationPickerItemSchema = zod.z.object({
221
+ id: zod.z.string().min(1),
222
+ name: zod.z.string().min(1),
223
+ slug: zod.z.string().min(1),
224
+ logo: zod.z.string().nullable().optional()
225
+ });
226
+ const extensionBootstrapUserSchema = zod.z.object({
227
+ id: zod.z.string().min(1),
228
+ email: zod.z.string().email().nullable(),
229
+ name: zod.z.string().nullable(),
230
+ image: zod.z.string().nullable(),
231
+ emailVerified: zod.z.boolean()
232
+ });
233
+ zod.z.object({
234
+ user: extensionBootstrapUserSchema,
235
+ organizations: zod.z.array(organizationPickerItemSchema),
236
+ defaultOrganizationId: zod.z.string().min(1).nullable()
237
+ });
238
+ //#endregion
239
+ //#region src/errors.ts
240
+ var SpinupMailError = class extends Error {
241
+ constructor(message, options) {
242
+ super(message, options);
243
+ this.name = new.target.name;
244
+ }
245
+ };
246
+ var SpinupMailValidationError = class extends SpinupMailError {
247
+ source;
248
+ issues;
249
+ constructor(args) {
250
+ super(args.message, args.cause ? { cause: args.cause } : void 0);
251
+ this.source = args.source;
252
+ this.issues = args.issues ?? [];
253
+ }
254
+ };
255
+ var SpinupMailApiError = class extends SpinupMailError {
256
+ status;
257
+ response;
258
+ body;
259
+ constructor(args) {
260
+ super(args.message);
261
+ this.status = args.status;
262
+ this.response = args.response;
263
+ this.body = args.body;
264
+ }
265
+ };
266
+ var SpinupMailTimeoutError = class extends SpinupMailError {
267
+ timeoutMs;
268
+ constructor(message, timeoutMs, options) {
269
+ super(message, options);
270
+ this.timeoutMs = timeoutMs;
271
+ }
272
+ };
273
+ //#endregion
274
+ //#region src/file.ts
275
+ const parseFilenameFromDisposition = (headerValue) => {
276
+ if (!headerValue) return null;
277
+ const utf8Match = headerValue.match(/filename\*=UTF-8''([^;]+)/i);
278
+ if (utf8Match?.[1]) try {
279
+ return decodeURIComponent(utf8Match[1]);
280
+ } catch {
281
+ return utf8Match[1];
282
+ }
283
+ const fallbackMatch = headerValue.match(/filename="([^"]+)"/i);
284
+ if (fallbackMatch?.[1]) return fallbackMatch[1];
285
+ return null;
286
+ };
287
+ var SpinupMailFile = class {
288
+ filename;
289
+ contentType;
290
+ contentLength;
291
+ response;
292
+ constructor(response) {
293
+ this.response = response;
294
+ this.filename = parseFilenameFromDisposition(response.headers.get("content-disposition"));
295
+ this.contentType = response.headers.get("content-type");
296
+ const contentLength = response.headers.get("content-length");
297
+ const parsed = contentLength ? Number(contentLength) : NaN;
298
+ this.contentLength = Number.isFinite(parsed) ? parsed : null;
299
+ }
300
+ arrayBuffer() {
301
+ return this.response.clone().arrayBuffer();
302
+ }
303
+ text() {
304
+ return this.response.clone().text();
305
+ }
306
+ blob() {
307
+ const clone = this.response.clone();
308
+ if (typeof clone.blob !== "function") throw new Error("Response.blob() is not available in this runtime.");
309
+ return clone.blob();
310
+ }
311
+ };
312
+ //#endregion
313
+ //#region src/client.ts
314
+ const DEFAULT_LIST_ALL_PAGE_SIZE = 50;
315
+ const MAX_LIST_ALL_PAGES = 200;
316
+ const DEFAULT_WAIT_TIMEOUT_MS = 3e4;
317
+ const DEFAULT_POLL_INTERVAL_MS = 1e3;
318
+ const DEFAULT_SPINUPMAIL_BASE_URL = "https://api.spinupmail.com";
319
+ const RANDOM_LOCAL_PART_PREFIX = "sum";
320
+ const RANDOM_LOCAL_PART_SIZE = 12;
321
+ const issuePathToString = (path) => path.length === 0 ? "<root>" : path.map(String).join(".");
322
+ const formatSchemaIssues = (issues) => issues.map((issue) => `${issuePathToString(issue.path)}: ${issue.message}`);
323
+ const validateWithSchema = (schema, value, source, message) => {
324
+ const parsed = schema.safeParse(value);
325
+ if (!parsed.success) throw new SpinupMailValidationError({
326
+ message,
327
+ source,
328
+ issues: formatSchemaIssues(parsed.error.issues),
329
+ cause: parsed.error
330
+ });
331
+ return parsed.data;
332
+ };
333
+ const normalizeString = (value, label) => {
334
+ const normalized = value.trim();
335
+ if (!normalized) throw new SpinupMailValidationError({
336
+ message: `${label} is required.`,
337
+ source: "request"
338
+ });
339
+ return normalized;
340
+ };
341
+ const resolveFetch = (candidate) => {
342
+ if (candidate) return candidate;
343
+ if (typeof globalThis.fetch === "function") return globalThis.fetch.bind(globalThis);
344
+ throw new SpinupMailValidationError({
345
+ message: "A fetch implementation is required in this runtime.",
346
+ source: "request"
347
+ });
348
+ };
349
+ const normalizeBaseUrl = (baseUrl) => normalizeString(baseUrl, "baseUrl").replace(/\/+$/, "");
350
+ const getProcessEnv = () => {
351
+ if (typeof process !== "undefined" && typeof process.env === "object" && process.env !== null) return process.env;
352
+ };
353
+ const readEnvValue = (keys) => {
354
+ const env = getProcessEnv();
355
+ if (!env) return void 0;
356
+ for (const key of keys) {
357
+ const value = env[key]?.trim();
358
+ if (value) return value;
359
+ }
360
+ };
361
+ const createRandomBytes = (size) => {
362
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") return crypto.getRandomValues(new Uint8Array(size));
363
+ return Uint8Array.from(Array.from({ length: size }, () => Math.floor(Math.random() * 256)));
364
+ };
365
+ const generateRandomLocalPart = () => {
366
+ const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
367
+ const bytes = createRandomBytes(RANDOM_LOCAL_PART_SIZE);
368
+ let suffix = "";
369
+ for (const byte of bytes) suffix += alphabet[byte % 36];
370
+ return `${RANDOM_LOCAL_PART_PREFIX}-${suffix}`;
371
+ };
372
+ const resolveOrganizationId = (context, organizationId, orgScoped) => {
373
+ if (!orgScoped) return void 0;
374
+ const resolved = organizationId ?? context.organizationId;
375
+ if (!resolved?.trim()) throw new SpinupMailValidationError({
376
+ message: "organizationId is required for this SpinupMail API method when using API keys.",
377
+ source: "request"
378
+ });
379
+ return resolved.trim();
380
+ };
381
+ const normalizeTimestamp = (value) => {
382
+ if (value === void 0) return void 0;
383
+ if (value instanceof Date) return value.toISOString();
384
+ return typeof value === "number" ? String(value) : value;
385
+ };
386
+ const normalizeText = (value) => (value ?? "").replace(/\s+/g, " ").trim().toLowerCase();
387
+ const matchesText = (value, expected) => {
388
+ if (!expected?.trim()) return true;
389
+ return normalizeText(value).includes(normalizeText(expected));
390
+ };
391
+ const matchesAnyText = (values, expected) => {
392
+ if (!expected?.trim()) return true;
393
+ return values.some((value) => matchesText(value, expected));
394
+ };
395
+ const createQueryString = (values) => {
396
+ const query = new URLSearchParams();
397
+ for (const [key, value] of Object.entries(values)) {
398
+ if (value === void 0) continue;
399
+ query.set(key, String(value));
400
+ }
401
+ const serialized = query.toString();
402
+ return serialized.length > 0 ? `?${serialized}` : "";
403
+ };
404
+ const parseErrorPayload = async (response) => {
405
+ try {
406
+ const payload = await response.clone().json();
407
+ const parsed = apiErrorSchema.safeParse(payload);
408
+ if (parsed.success) return parsed.data;
409
+ return payload;
410
+ } catch {
411
+ const text = await response.clone().text();
412
+ return text ? { error: text } : void 0;
413
+ }
414
+ };
415
+ const requestJson = async (context, options) => {
416
+ const headers = new Headers(context.headers);
417
+ headers.set("x-api-key", context.apiKey);
418
+ headers.set("accept", "application/json");
419
+ const resolvedOrganizationId = resolveOrganizationId(context, options.organizationId, options.orgScoped ?? false);
420
+ if (resolvedOrganizationId) headers.set("x-org-id", resolvedOrganizationId);
421
+ let body;
422
+ if (options.body !== void 0) {
423
+ headers.set("content-type", "application/json");
424
+ body = JSON.stringify(options.body);
425
+ }
426
+ const response = await context.fetch(`${context.baseUrl}${options.path}`, {
427
+ method: options.method ?? "GET",
428
+ headers,
429
+ body,
430
+ signal: options.signal
431
+ });
432
+ if (!response.ok) {
433
+ const errorBody = await parseErrorPayload(response);
434
+ throw new SpinupMailApiError({
435
+ message: typeof errorBody === "object" && errorBody !== null && "error" in errorBody && typeof errorBody.error === "string" ? errorBody.error : response.statusText || "SpinupMail request failed",
436
+ status: response.status,
437
+ response,
438
+ body: errorBody
439
+ });
440
+ }
441
+ let payload;
442
+ try {
443
+ payload = await response.json();
444
+ } catch (error) {
445
+ throw new SpinupMailValidationError({
446
+ message: "SpinupMail returned a non-JSON success response.",
447
+ source: "response",
448
+ cause: error
449
+ });
450
+ }
451
+ return validateWithSchema(options.responseSchema, payload, "response", "SpinupMail returned an unexpected response shape.");
452
+ };
453
+ const requestBinary = async (context, options) => {
454
+ const headers = new Headers(context.headers);
455
+ headers.set("x-api-key", context.apiKey);
456
+ const resolvedOrganizationId = resolveOrganizationId(context, options.organizationId, options.orgScoped ?? false);
457
+ if (resolvedOrganizationId) headers.set("x-org-id", resolvedOrganizationId);
458
+ const response = await context.fetch(`${context.baseUrl}${options.path}`, {
459
+ method: "GET",
460
+ headers,
461
+ signal: options.signal
462
+ });
463
+ if (!response.ok) {
464
+ const errorBody = await parseErrorPayload(response);
465
+ throw new SpinupMailApiError({
466
+ message: typeof errorBody === "object" && errorBody !== null && "error" in errorBody && typeof errorBody.error === "string" ? errorBody.error : response.statusText || "SpinupMail request failed",
467
+ status: response.status,
468
+ response,
469
+ body: errorBody
470
+ });
471
+ }
472
+ return new SpinupMailFile(response);
473
+ };
474
+ const ensureInboxSelector = (options) => {
475
+ if (!options.address && !options.addressId) throw new SpinupMailValidationError({
476
+ message: "Either address or addressId is required.",
477
+ source: "request"
478
+ });
479
+ };
480
+ const validateListEmailsOptions = (options) => {
481
+ ensureInboxSelector(options);
482
+ validateWithSchema(listEmailsParamsSchema, {
483
+ ...options,
484
+ after: normalizeTimestamp(options.after),
485
+ before: normalizeTimestamp(options.before)
486
+ }, "request", "Invalid listEmails options.");
487
+ if (options.search && (options.after !== void 0 || options.before !== void 0 || options.order === "asc")) throw new SpinupMailValidationError({
488
+ message: "search does not support after, before, or order='asc' parameters.",
489
+ source: "request"
490
+ });
491
+ };
492
+ const matchesListItemFilters = (item, options) => {
493
+ if (!matchesText(item.subject, options.subjectIncludes)) return false;
494
+ if (!matchesText(item.to, options.toIncludes)) return false;
495
+ if (!matchesAnyText([
496
+ item.from,
497
+ item.sender,
498
+ item.senderLabel
499
+ ], options.fromIncludes)) return false;
500
+ return options.match ? options.match(item) : true;
501
+ };
502
+ const matchesDetailFilters = (detail, options) => {
503
+ if (!matchesText(`${detail.html ?? ""}\n${detail.text ?? ""}`, options.bodyIncludes)) return false;
504
+ return options.matchDetail ? options.matchDetail(detail) : true;
505
+ };
506
+ const sleep = (ms, signal) => new Promise((resolve, reject) => {
507
+ if (signal?.aborted) {
508
+ reject(signal.reason ?? /* @__PURE__ */ new Error("The operation was aborted."));
509
+ return;
510
+ }
511
+ const timeoutId = setTimeout(() => {
512
+ cleanup();
513
+ resolve();
514
+ }, ms);
515
+ const onAbort = () => {
516
+ clearTimeout(timeoutId);
517
+ cleanup();
518
+ reject(signal?.reason ?? /* @__PURE__ */ new Error("The operation was aborted."));
519
+ };
520
+ const cleanup = () => {
521
+ signal?.removeEventListener("abort", onAbort);
522
+ };
523
+ signal?.addEventListener("abort", onAbort, { once: true });
524
+ });
525
+ const runPollingLoop = async (emails, options, args) => {
526
+ ensureInboxSelector(options);
527
+ validateListEmailsOptions({
528
+ address: options.address,
529
+ addressId: options.addressId,
530
+ search: options.search,
531
+ limit: options.limit,
532
+ order: options.order,
533
+ after: options.after,
534
+ before: options.before,
535
+ organizationId: options.organizationId,
536
+ signal: options.signal
537
+ });
538
+ const startedAt = Date.now();
539
+ const deadline = args.timeoutMs > 0 ? startedAt + args.timeoutMs : Number.NEGATIVE_INFINITY;
540
+ const seenEmailIds = /* @__PURE__ */ new Set();
541
+ let attempts = 0;
542
+ let lastResponse;
543
+ let lastFreshItems;
544
+ while (true) {
545
+ attempts += 1;
546
+ lastResponse = await emails.list({
547
+ address: options.address,
548
+ addressId: options.addressId,
549
+ search: options.search,
550
+ limit: options.limit,
551
+ order: options.order,
552
+ after: options.after,
553
+ before: options.before,
554
+ organizationId: options.organizationId,
555
+ signal: options.signal
556
+ });
557
+ lastFreshItems = lastResponse.items.filter((item) => {
558
+ if (seenEmailIds.has(item.id)) return false;
559
+ seenEmailIds.add(item.id);
560
+ return true;
561
+ });
562
+ const matchedEmail = lastFreshItems.find((item) => matchesListItemFilters(item, options)) ?? null;
563
+ if (matchedEmail) return {
564
+ response: lastResponse,
565
+ items: lastResponse.items,
566
+ freshItems: lastFreshItems,
567
+ matchedEmail,
568
+ timedOut: false,
569
+ attempts,
570
+ elapsedMs: Date.now() - startedAt,
571
+ polledAt: (/* @__PURE__ */ new Date()).toISOString()
572
+ };
573
+ if (args.timeoutMs <= 0 || Date.now() >= deadline) break;
574
+ const remainingMs = deadline - Date.now();
575
+ if (remainingMs <= 0) break;
576
+ await sleep(Math.min(options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS, remainingMs), options.signal);
577
+ }
578
+ const result = {
579
+ response: lastResponse,
580
+ items: lastResponse.items,
581
+ freshItems: lastFreshItems,
582
+ matchedEmail: null,
583
+ timedOut: args.timeoutMs > 0,
584
+ attempts,
585
+ elapsedMs: Date.now() - startedAt,
586
+ polledAt: (/* @__PURE__ */ new Date()).toISOString()
587
+ };
588
+ if (args.throwOnTimeout) throw new SpinupMailTimeoutError(`No matching email arrived before the ${args.timeoutMs}ms timeout elapsed.`, args.timeoutMs);
589
+ return result;
590
+ };
591
+ const waitForEmailDetail = async (emails, options) => {
592
+ ensureInboxSelector(options);
593
+ validateListEmailsOptions({
594
+ address: options.address,
595
+ addressId: options.addressId,
596
+ search: options.search,
597
+ limit: options.limit,
598
+ order: options.order,
599
+ after: options.after,
600
+ before: options.before,
601
+ organizationId: options.organizationId,
602
+ signal: options.signal
603
+ });
604
+ const startedAt = Date.now();
605
+ const timeoutMs = options.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
606
+ const deadline = timeoutMs > 0 ? startedAt + timeoutMs : Number.NEGATIVE_INFINITY;
607
+ const seenEmailIds = /* @__PURE__ */ new Set();
608
+ while (true) {
609
+ const freshCandidates = (await emails.list({
610
+ address: options.address,
611
+ addressId: options.addressId,
612
+ search: options.search,
613
+ limit: options.limit,
614
+ order: options.order,
615
+ after: options.after,
616
+ before: options.before,
617
+ organizationId: options.organizationId,
618
+ signal: options.signal
619
+ })).items.filter((item) => {
620
+ if (seenEmailIds.has(item.id)) return false;
621
+ seenEmailIds.add(item.id);
622
+ return matchesListItemFilters(item, options);
623
+ });
624
+ for (const item of freshCandidates) {
625
+ const detail = await emails.get(item.id, {
626
+ organizationId: options.organizationId,
627
+ signal: options.signal
628
+ });
629
+ if (!matchesDetailFilters(detail, options)) continue;
630
+ if (options.deleteAfterRead) await emails.delete(item.id, {
631
+ organizationId: options.organizationId,
632
+ signal: options.signal
633
+ });
634
+ return detail;
635
+ }
636
+ if (timeoutMs <= 0 || Date.now() >= deadline) break;
637
+ const remainingMs = deadline - Date.now();
638
+ if (remainingMs <= 0) break;
639
+ await sleep(Math.min(options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS, remainingMs), options.signal);
640
+ }
641
+ throw new SpinupMailTimeoutError(`No matching email arrived before the ${timeoutMs}ms timeout elapsed.`, timeoutMs);
642
+ };
643
+ const createSpinupMailClient = (options) => {
644
+ const context = {
645
+ baseUrl: normalizeBaseUrl(options.baseUrl),
646
+ apiKey: normalizeString(options.apiKey, "apiKey"),
647
+ organizationId: options.organizationId?.trim() || void 0,
648
+ fetch: resolveFetch(options.fetch),
649
+ headers: options.headers
650
+ };
651
+ const addresses = {
652
+ list: async (options = {}) => {
653
+ const validated = validateWithSchema(listEmailAddressesParamsSchema, options, "request", "Invalid listEmailAddresses options.");
654
+ return requestJson(context, {
655
+ path: `/api/email-addresses${createQueryString({
656
+ page: validated.page,
657
+ pageSize: validated.pageSize,
658
+ search: validated.search,
659
+ sortBy: validated.sortBy,
660
+ sortDirection: validated.sortDirection
661
+ })}`,
662
+ responseSchema: emailAddressListResponseSchema,
663
+ organizationId: options.organizationId,
664
+ orgScoped: true,
665
+ signal: options.signal
666
+ });
667
+ },
668
+ listAll: async (options = {}) => {
669
+ const items = [];
670
+ let page = 1;
671
+ let totalPages = 1;
672
+ while (page <= totalPages) {
673
+ if (page > MAX_LIST_ALL_PAGES) throw new SpinupMailValidationError({
674
+ message: `Address pagination exceeded the safety limit of ${MAX_LIST_ALL_PAGES} pages.`,
675
+ source: "request"
676
+ });
677
+ const response = await addresses.list({
678
+ ...options,
679
+ page,
680
+ pageSize: options.pageSize ?? DEFAULT_LIST_ALL_PAGE_SIZE
681
+ });
682
+ items.push(...response.items);
683
+ totalPages = response.totalPages;
684
+ page += 1;
685
+ }
686
+ return items;
687
+ },
688
+ listRecentActivity: async (options = {}) => {
689
+ const validated = validateWithSchema(listRecentAddressActivityParamsSchema, options, "request", "Invalid listRecentAddressActivity options.");
690
+ return requestJson(context, {
691
+ path: `/api/email-addresses/recent-activity${createQueryString({
692
+ limit: validated.limit,
693
+ cursor: validated.cursor,
694
+ search: validated.search,
695
+ sortBy: validated.sortBy,
696
+ sortDirection: validated.sortDirection
697
+ })}`,
698
+ responseSchema: recentAddressActivityResponseSchema,
699
+ organizationId: options.organizationId,
700
+ orgScoped: true,
701
+ signal: options.signal
702
+ });
703
+ },
704
+ get: (addressId, options = {}) => requestJson(context, {
705
+ path: `/api/email-addresses/${encodeURIComponent(normalizeString(addressId, "addressId"))}`,
706
+ responseSchema: emailAddressSchema,
707
+ organizationId: options.organizationId,
708
+ orgScoped: true,
709
+ signal: options.signal
710
+ }),
711
+ create: (payload, options = {}) => {
712
+ return requestJson(context, {
713
+ method: "POST",
714
+ path: "/api/email-addresses",
715
+ responseSchema: createEmailAddressResponseSchema,
716
+ body: validateWithSchema(createEmailAddressRequestSchema, {
717
+ ...payload,
718
+ localPart: payload.localPart?.trim() || generateRandomLocalPart()
719
+ }, "request", "Invalid createEmailAddress payload."),
720
+ organizationId: options.organizationId,
721
+ orgScoped: true,
722
+ signal: options.signal
723
+ });
724
+ },
725
+ update: (addressId, payload, options = {}) => requestJson(context, {
726
+ method: "PATCH",
727
+ path: `/api/email-addresses/${encodeURIComponent(normalizeString(addressId, "addressId"))}`,
728
+ responseSchema: emailAddressSchema,
729
+ body: validateWithSchema(updateEmailAddressRequestSchema, payload, "request", "Invalid updateEmailAddress payload."),
730
+ organizationId: options.organizationId,
731
+ orgScoped: true,
732
+ signal: options.signal
733
+ }),
734
+ delete: (addressId, options = {}) => requestJson(context, {
735
+ method: "DELETE",
736
+ path: `/api/email-addresses/${encodeURIComponent(normalizeString(addressId, "addressId"))}`,
737
+ responseSchema: deleteEmailAddressResponseSchema,
738
+ organizationId: options.organizationId,
739
+ orgScoped: true,
740
+ signal: options.signal
741
+ })
742
+ };
743
+ const emails = {
744
+ list: async (options) => {
745
+ validateListEmailsOptions(options);
746
+ return requestJson(context, {
747
+ path: `/api/emails${createQueryString({
748
+ address: options.address,
749
+ addressId: options.addressId,
750
+ search: options.search,
751
+ limit: options.limit,
752
+ order: options.order,
753
+ after: normalizeTimestamp(options.after),
754
+ before: normalizeTimestamp(options.before)
755
+ })}`,
756
+ responseSchema: emailListResponseSchema,
757
+ organizationId: options.organizationId,
758
+ orgScoped: true,
759
+ signal: options.signal
760
+ });
761
+ },
762
+ get: (emailId, options = {}) => requestJson(context, {
763
+ path: `/api/emails/${encodeURIComponent(normalizeString(emailId, "emailId"))}${createQueryString({ raw: options.raw ? 1 : void 0 })}`,
764
+ responseSchema: emailDetailSchema,
765
+ organizationId: options.organizationId,
766
+ orgScoped: true,
767
+ signal: options.signal
768
+ }),
769
+ delete: (emailId, options = {}) => requestJson(context, {
770
+ method: "DELETE",
771
+ path: `/api/emails/${encodeURIComponent(normalizeString(emailId, "emailId"))}`,
772
+ responseSchema: deleteEmailResponseSchema,
773
+ organizationId: options.organizationId,
774
+ orgScoped: true,
775
+ signal: options.signal
776
+ }),
777
+ getRaw: (emailId, options = {}) => requestBinary(context, {
778
+ path: `/api/emails/${encodeURIComponent(normalizeString(emailId, "emailId"))}/raw`,
779
+ organizationId: options.organizationId,
780
+ orgScoped: true,
781
+ signal: options.signal
782
+ }),
783
+ getAttachment: (emailId, attachmentId, options = {}) => requestBinary(context, {
784
+ path: `/api/emails/${encodeURIComponent(normalizeString(emailId, "emailId"))}/attachments/${encodeURIComponent(normalizeString(attachmentId, "attachmentId"))}${createQueryString({ inline: options.inline ? 1 : void 0 })}`,
785
+ organizationId: options.organizationId,
786
+ orgScoped: true,
787
+ signal: options.signal
788
+ })
789
+ };
790
+ return {
791
+ domains: { get: () => requestJson(context, {
792
+ path: "/api/domains",
793
+ responseSchema: domainConfigSchema
794
+ }) },
795
+ addresses,
796
+ emails,
797
+ stats: {
798
+ getEmailActivity: (options = {}) => requestJson(context, {
799
+ path: `/api/organizations/stats/email-activity${createQueryString({
800
+ days: options.days,
801
+ timezone: options.timezone
802
+ })}`,
803
+ responseSchema: emailActivityResponseSchema,
804
+ organizationId: options.organizationId,
805
+ orgScoped: true,
806
+ signal: options.signal
807
+ }),
808
+ getEmailSummary: (options = {}) => requestJson(context, {
809
+ path: "/api/organizations/stats/email-summary",
810
+ responseSchema: emailSummaryResponseSchema,
811
+ organizationId: options.organizationId,
812
+ orgScoped: true,
813
+ signal: options.signal
814
+ })
815
+ },
816
+ inboxes: {
817
+ poll: (options) => runPollingLoop(emails, options, {
818
+ timeoutMs: options.timeoutMs ?? 0,
819
+ throwOnTimeout: false
820
+ }),
821
+ waitForEmail: (options) => waitForEmailDetail(emails, options)
822
+ }
823
+ };
824
+ };
825
+ /**
826
+ * API-key SDK for SpinupMail.
827
+ *
828
+ * Typical usage:
829
+ *
830
+ * ```ts
831
+ * const spinupmail = new SpinupMail();
832
+ * const address = await spinupmail.addresses.create({ acceptedRiskNotice: true });
833
+ * const email = await spinupmail.inboxes.waitForEmail({ addressId: address.id });
834
+ * ```
835
+ */
836
+ var SpinupMail = class {
837
+ domains;
838
+ addresses;
839
+ emails;
840
+ stats;
841
+ inboxes;
842
+ /** Creates a new SDK client using constructor options or environment defaults. */
843
+ constructor(options = {}) {
844
+ const resolvedOptions = typeof options === "string" ? { apiKey: options } : options;
845
+ const apiKey = resolvedOptions.apiKey ?? readEnvValue(["SPINUPMAIL_API_KEY"]);
846
+ const baseUrl = resolvedOptions.baseUrl ?? readEnvValue(["SPINUPMAIL_BASE_URL"]) ?? DEFAULT_SPINUPMAIL_BASE_URL;
847
+ const organizationId = resolvedOptions.organizationId ?? readEnvValue(["SPINUPMAIL_ORGANIZATION_ID", "SPINUPMAIL_ORG_ID"]);
848
+ const client = createSpinupMailClient({
849
+ baseUrl,
850
+ apiKey: normalizeString(apiKey ?? "", "apiKey"),
851
+ organizationId,
852
+ fetch: resolvedOptions.fetch,
853
+ headers: resolvedOptions.headers
854
+ });
855
+ this.domains = client.domains;
856
+ this.addresses = client.addresses;
857
+ this.emails = client.emails;
858
+ this.stats = client.stats;
859
+ this.inboxes = client.inboxes;
860
+ }
861
+ };
862
+ //#endregion
863
+ exports.SpinupMail = SpinupMail;
864
+ exports.SpinupMailApiError = SpinupMailApiError;
865
+ exports.SpinupMailError = SpinupMailError;
866
+ exports.SpinupMailFile = SpinupMailFile;
867
+ exports.SpinupMailTimeoutError = SpinupMailTimeoutError;
868
+ exports.SpinupMailValidationError = SpinupMailValidationError;
869
+
870
+ //# sourceMappingURL=index.cjs.map