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