@volchoklv/newsletter-kit 1.0.2 → 1.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/README.md +11 -11
- package/dist/adapters/email/index.d.ts +4 -119
- package/dist/adapters/email/mailchimp.d.ts +63 -0
- package/dist/adapters/email/mailchimp.js +137 -0
- package/dist/adapters/email/mailchimp.js.map +1 -0
- package/dist/adapters/email/nodemailer.d.ts +39 -0
- package/dist/adapters/email/nodemailer.js +144 -0
- package/dist/adapters/email/nodemailer.js.map +1 -0
- package/dist/adapters/email/resend.d.ts +22 -0
- package/dist/adapters/email/resend.js +144 -0
- package/dist/adapters/email/resend.js.map +1 -0
- package/dist/adapters/storage/index.d.ts +4 -215
- package/dist/adapters/storage/memory.d.ts +29 -0
- package/dist/adapters/storage/memory.js +164 -0
- package/dist/adapters/storage/memory.js.map +1 -0
- package/dist/adapters/storage/prisma.d.ts +80 -0
- package/dist/adapters/storage/prisma.js +155 -0
- package/dist/adapters/storage/prisma.js.map +1 -0
- package/dist/adapters/storage/supabase.d.ts +111 -0
- package/dist/adapters/storage/supabase.js +116 -0
- package/dist/adapters/storage/supabase.js.map +1 -0
- package/dist/components/index.d.ts +2 -2
- package/dist/index.d.ts +8 -4
- package/dist/server/index.d.ts +1 -1
- package/dist/{types-BmajlhNp.d.ts → types-fgF8dmx1.d.ts} +1 -1
- package/package.json +26 -1
package/README.md
CHANGED
|
@@ -44,8 +44,8 @@ npm install @supabase/supabase-js
|
|
|
44
44
|
```ts
|
|
45
45
|
// lib/newsletter.ts
|
|
46
46
|
import { createNewsletterHandlers } from '@volchoklv/newsletter-kit/server';
|
|
47
|
-
import { createResendAdapter } from '@volchoklv/newsletter-kit/adapters/email';
|
|
48
|
-
import { createPrismaAdapter } from '@volchoklv/newsletter-kit/adapters/storage';
|
|
47
|
+
import { createResendAdapter } from '@volchoklv/newsletter-kit/adapters/email/resend';
|
|
48
|
+
import { createPrismaAdapter } from '@volchoklv/newsletter-kit/adapters/storage/prisma';
|
|
49
49
|
import { prisma } from '@/lib/prisma';
|
|
50
50
|
|
|
51
51
|
export const newsletter = createNewsletterHandlers({
|
|
@@ -283,7 +283,7 @@ export function CustomForm() {
|
|
|
283
283
|
### Resend
|
|
284
284
|
|
|
285
285
|
```ts
|
|
286
|
-
import { createResendAdapter } from '@volchoklv/newsletter-kit/adapters/email';
|
|
286
|
+
import { createResendAdapter } from '@volchoklv/newsletter-kit/adapters/email/resend';
|
|
287
287
|
|
|
288
288
|
const emailAdapter = createResendAdapter({
|
|
289
289
|
apiKey: process.env.RESEND_API_KEY!,
|
|
@@ -315,8 +315,8 @@ To send newsletters via [Resend Broadcasts](https://resend.com/broadcasts), sync
|
|
|
315
315
|
|
|
316
316
|
```ts
|
|
317
317
|
import { createNewsletterHandlers } from '@volchoklv/newsletter-kit/server';
|
|
318
|
-
import { createResendAdapter } from '@volchoklv/newsletter-kit/adapters/email';
|
|
319
|
-
import { createPrismaAdapter } from '@volchoklv/newsletter-kit/adapters/storage';
|
|
318
|
+
import { createResendAdapter } from '@volchoklv/newsletter-kit/adapters/email/resend';
|
|
319
|
+
import { createPrismaAdapter } from '@volchoklv/newsletter-kit/adapters/storage/prisma';
|
|
320
320
|
import { Resend } from 'resend';
|
|
321
321
|
import { prisma } from '@/lib/prisma';
|
|
322
322
|
|
|
@@ -368,7 +368,7 @@ Then use Resend's Broadcast feature to send newsletters to your audience.
|
|
|
368
368
|
### Nodemailer (SMTP)
|
|
369
369
|
|
|
370
370
|
```ts
|
|
371
|
-
import { createNodemailerAdapter } from '@volchoklv/newsletter-kit/adapters/email';
|
|
371
|
+
import { createNodemailerAdapter } from '@volchoklv/newsletter-kit/adapters/email/nodemailer';
|
|
372
372
|
|
|
373
373
|
const emailAdapter = createNodemailerAdapter({
|
|
374
374
|
smtp: {
|
|
@@ -387,7 +387,7 @@ const emailAdapter = createNodemailerAdapter({
|
|
|
387
387
|
### Mailchimp
|
|
388
388
|
|
|
389
389
|
```ts
|
|
390
|
-
import { createMailchimpAdapter } from '@volchoklv/newsletter-kit/adapters/email';
|
|
390
|
+
import { createMailchimpAdapter } from '@volchoklv/newsletter-kit/adapters/email/mailchimp';
|
|
391
391
|
|
|
392
392
|
const emailAdapter = createMailchimpAdapter({
|
|
393
393
|
apiKey: process.env.MAILCHIMP_API_KEY!,
|
|
@@ -403,7 +403,7 @@ const emailAdapter = createMailchimpAdapter({
|
|
|
403
403
|
### Prisma
|
|
404
404
|
|
|
405
405
|
```ts
|
|
406
|
-
import { createPrismaAdapter } from '@volchoklv/newsletter-kit/adapters/storage';
|
|
406
|
+
import { createPrismaAdapter } from '@volchoklv/newsletter-kit/adapters/storage/prisma';
|
|
407
407
|
import { prisma } from '@/lib/prisma';
|
|
408
408
|
|
|
409
409
|
const storageAdapter = createPrismaAdapter({ prisma });
|
|
@@ -412,7 +412,7 @@ const storageAdapter = createPrismaAdapter({ prisma });
|
|
|
412
412
|
### Supabase
|
|
413
413
|
|
|
414
414
|
```ts
|
|
415
|
-
import { createSupabaseAdapter } from '@volchoklv/newsletter-kit/adapters/storage';
|
|
415
|
+
import { createSupabaseAdapter } from '@volchoklv/newsletter-kit/adapters/storage/supabase';
|
|
416
416
|
import { createClient } from '@supabase/supabase-js';
|
|
417
417
|
|
|
418
418
|
const supabase = createClient(
|
|
@@ -426,7 +426,7 @@ const storageAdapter = createSupabaseAdapter({ supabase });
|
|
|
426
426
|
### In-Memory (Development/Testing)
|
|
427
427
|
|
|
428
428
|
```ts
|
|
429
|
-
import { createMemoryAdapter } from '@volchoklv/newsletter-kit/adapters/storage';
|
|
429
|
+
import { createMemoryAdapter } from '@volchoklv/newsletter-kit/adapters/storage/memory';
|
|
430
430
|
|
|
431
431
|
const storageAdapter = createMemoryAdapter();
|
|
432
432
|
```
|
|
@@ -436,7 +436,7 @@ const storageAdapter = createMemoryAdapter();
|
|
|
436
436
|
When using Mailchimp or similar that handles storage:
|
|
437
437
|
|
|
438
438
|
```ts
|
|
439
|
-
import { createNoopAdapter } from '@volchoklv/newsletter-kit/adapters/storage';
|
|
439
|
+
import { createNoopAdapter } from '@volchoklv/newsletter-kit/adapters/storage/memory';
|
|
440
440
|
|
|
441
441
|
const storageAdapter = createNoopAdapter();
|
|
442
442
|
```
|
|
@@ -1,119 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
apiKey: string;
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* Email adapter for Resend (https://resend.com)
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* ```ts
|
|
12
|
-
* import { createResendAdapter } from '@volchok/newsletter-kit/adapters/email';
|
|
13
|
-
*
|
|
14
|
-
* const emailAdapter = createResendAdapter({
|
|
15
|
-
* apiKey: process.env.RESEND_API_KEY!,
|
|
16
|
-
* from: 'newsletter@yourdomain.com',
|
|
17
|
-
* adminEmail: 'you@yourdomain.com', // optional
|
|
18
|
-
* });
|
|
19
|
-
* ```
|
|
20
|
-
*/
|
|
21
|
-
declare function createResendAdapter(config: ResendAdapterConfig): EmailAdapter;
|
|
22
|
-
|
|
23
|
-
interface SMTPConfig {
|
|
24
|
-
host: string;
|
|
25
|
-
port: number;
|
|
26
|
-
secure?: boolean;
|
|
27
|
-
auth?: {
|
|
28
|
-
user: string;
|
|
29
|
-
pass: string;
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
interface NodemailerAdapterConfig extends EmailAdapterConfig {
|
|
33
|
-
smtp: SMTPConfig;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Email adapter for Nodemailer/SMTP
|
|
37
|
-
*
|
|
38
|
-
* @example
|
|
39
|
-
* ```ts
|
|
40
|
-
* import { createNodemailerAdapter } from '@volchok/newsletter-kit/adapters/email';
|
|
41
|
-
*
|
|
42
|
-
* const emailAdapter = createNodemailerAdapter({
|
|
43
|
-
* smtp: {
|
|
44
|
-
* host: 'smtp.example.com',
|
|
45
|
-
* port: 587,
|
|
46
|
-
* secure: false,
|
|
47
|
-
* auth: {
|
|
48
|
-
* user: process.env.SMTP_USER!,
|
|
49
|
-
* pass: process.env.SMTP_PASS!,
|
|
50
|
-
* },
|
|
51
|
-
* },
|
|
52
|
-
* from: 'newsletter@yourdomain.com',
|
|
53
|
-
* adminEmail: 'you@yourdomain.com', // optional
|
|
54
|
-
* });
|
|
55
|
-
* ```
|
|
56
|
-
*/
|
|
57
|
-
declare function createNodemailerAdapter(config: NodemailerAdapterConfig): EmailAdapter;
|
|
58
|
-
|
|
59
|
-
interface MailchimpAdapterConfig extends EmailAdapterConfig {
|
|
60
|
-
apiKey: string;
|
|
61
|
-
server: string;
|
|
62
|
-
listId: string;
|
|
63
|
-
/** If true, use Mailchimp for storage too (adds to list on subscribe) */
|
|
64
|
-
useAsStorage?: boolean;
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Email adapter for Mailchimp
|
|
68
|
-
*
|
|
69
|
-
* Note: Mailchimp works differently from other adapters. It manages its own
|
|
70
|
-
* subscriber list, so this adapter can optionally act as both email AND storage.
|
|
71
|
-
*
|
|
72
|
-
* @example
|
|
73
|
-
* ```ts
|
|
74
|
-
* import { createMailchimpAdapter } from '@volchok/newsletter-kit/adapters/email';
|
|
75
|
-
*
|
|
76
|
-
* const emailAdapter = createMailchimpAdapter({
|
|
77
|
-
* apiKey: process.env.MAILCHIMP_API_KEY!,
|
|
78
|
-
* server: 'us1',
|
|
79
|
-
* listId: 'your-list-id',
|
|
80
|
-
* from: 'newsletter@yourdomain.com',
|
|
81
|
-
* useAsStorage: true, // Use Mailchimp's list as storage
|
|
82
|
-
* });
|
|
83
|
-
* ```
|
|
84
|
-
*/
|
|
85
|
-
declare function createMailchimpAdapter(config: MailchimpAdapterConfig): EmailAdapter;
|
|
86
|
-
/**
|
|
87
|
-
* Create a combined Mailchimp adapter that handles both email and storage
|
|
88
|
-
*
|
|
89
|
-
* This is useful when you want Mailchimp to be your single source of truth
|
|
90
|
-
* for subscribers.
|
|
91
|
-
*/
|
|
92
|
-
declare function createMailchimpStorageAdapter(config: MailchimpAdapterConfig): {
|
|
93
|
-
createSubscriber(input: {
|
|
94
|
-
email: string;
|
|
95
|
-
source?: string;
|
|
96
|
-
tags?: string[];
|
|
97
|
-
}): Promise<{
|
|
98
|
-
id: string;
|
|
99
|
-
email: string;
|
|
100
|
-
status: "pending";
|
|
101
|
-
source: string | undefined;
|
|
102
|
-
tags: string[] | undefined;
|
|
103
|
-
createdAt: Date;
|
|
104
|
-
updatedAt: Date;
|
|
105
|
-
}>;
|
|
106
|
-
getSubscriberByEmail(email: string): Promise<{
|
|
107
|
-
id: string;
|
|
108
|
-
email: string;
|
|
109
|
-
status: string;
|
|
110
|
-
tags: string[] | undefined;
|
|
111
|
-
createdAt: Date;
|
|
112
|
-
updatedAt: Date;
|
|
113
|
-
} | null>;
|
|
114
|
-
getSubscriberByToken(): Promise<null>;
|
|
115
|
-
confirmSubscriber(_token: string): Promise<null>;
|
|
116
|
-
unsubscribe(email: string): Promise<boolean>;
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
export { EmailAdapter, EmailAdapterConfig, createMailchimpAdapter, createMailchimpStorageAdapter, createNodemailerAdapter, createResendAdapter };
|
|
1
|
+
export { createResendAdapter } from './resend.js';
|
|
2
|
+
export { createNodemailerAdapter } from './nodemailer.js';
|
|
3
|
+
export { createMailchimpAdapter, createMailchimpStorageAdapter } from './mailchimp.js';
|
|
4
|
+
export { E as EmailAdapter, a as EmailAdapterConfig, b as EmailTemplates } from '../../types-fgF8dmx1.js';
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { a as EmailAdapterConfig, E as EmailAdapter } from '../../types-fgF8dmx1.js';
|
|
2
|
+
|
|
3
|
+
interface MailchimpAdapterConfig extends EmailAdapterConfig {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
server: string;
|
|
6
|
+
listId: string;
|
|
7
|
+
/** If true, use Mailchimp for storage too (adds to list on subscribe) */
|
|
8
|
+
useAsStorage?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Email adapter for Mailchimp
|
|
12
|
+
*
|
|
13
|
+
* Note: Mailchimp works differently from other adapters. It manages its own
|
|
14
|
+
* subscriber list, so this adapter can optionally act as both email AND storage.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { createMailchimpAdapter } from '@volchok/newsletter-kit/adapters/email';
|
|
19
|
+
*
|
|
20
|
+
* const emailAdapter = createMailchimpAdapter({
|
|
21
|
+
* apiKey: process.env.MAILCHIMP_API_KEY!,
|
|
22
|
+
* server: 'us1',
|
|
23
|
+
* listId: 'your-list-id',
|
|
24
|
+
* from: 'newsletter@yourdomain.com',
|
|
25
|
+
* useAsStorage: true, // Use Mailchimp's list as storage
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
declare function createMailchimpAdapter(config: MailchimpAdapterConfig): EmailAdapter;
|
|
30
|
+
/**
|
|
31
|
+
* Create a combined Mailchimp adapter that handles both email and storage
|
|
32
|
+
*
|
|
33
|
+
* This is useful when you want Mailchimp to be your single source of truth
|
|
34
|
+
* for subscribers.
|
|
35
|
+
*/
|
|
36
|
+
declare function createMailchimpStorageAdapter(config: MailchimpAdapterConfig): {
|
|
37
|
+
createSubscriber(input: {
|
|
38
|
+
email: string;
|
|
39
|
+
source?: string;
|
|
40
|
+
tags?: string[];
|
|
41
|
+
}): Promise<{
|
|
42
|
+
id: string;
|
|
43
|
+
email: string;
|
|
44
|
+
status: "pending";
|
|
45
|
+
source: string | undefined;
|
|
46
|
+
tags: string[] | undefined;
|
|
47
|
+
createdAt: Date;
|
|
48
|
+
updatedAt: Date;
|
|
49
|
+
}>;
|
|
50
|
+
getSubscriberByEmail(email: string): Promise<{
|
|
51
|
+
id: string;
|
|
52
|
+
email: string;
|
|
53
|
+
status: string;
|
|
54
|
+
tags: string[] | undefined;
|
|
55
|
+
createdAt: Date;
|
|
56
|
+
updatedAt: Date;
|
|
57
|
+
} | null>;
|
|
58
|
+
getSubscriberByToken(): Promise<null>;
|
|
59
|
+
confirmSubscriber(_token: string): Promise<null>;
|
|
60
|
+
unsubscribe(email: string): Promise<boolean>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export { createMailchimpAdapter, createMailchimpStorageAdapter };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/adapters/email/mailchimp.ts
|
|
4
|
+
function createMailchimpAdapter(config) {
|
|
5
|
+
const { apiKey, server, listId, from, adminEmail, useAsStorage } = config;
|
|
6
|
+
async function getClient() {
|
|
7
|
+
const mailchimp = await import("@mailchimp/mailchimp_marketing");
|
|
8
|
+
mailchimp.default.setConfig({
|
|
9
|
+
apiKey,
|
|
10
|
+
server
|
|
11
|
+
});
|
|
12
|
+
return mailchimp.default;
|
|
13
|
+
}
|
|
14
|
+
async function addOrUpdateMember(email, status) {
|
|
15
|
+
const client = await getClient();
|
|
16
|
+
const subscriberHash = await hashEmail(email);
|
|
17
|
+
try {
|
|
18
|
+
await client.lists.setListMember(listId, subscriberHash, {
|
|
19
|
+
email_address: email,
|
|
20
|
+
status_if_new: status
|
|
21
|
+
});
|
|
22
|
+
} catch (error) {
|
|
23
|
+
await client.lists.addListMember(listId, {
|
|
24
|
+
email_address: email,
|
|
25
|
+
status
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function hashEmail(email) {
|
|
30
|
+
const crypto = await import("crypto");
|
|
31
|
+
return crypto.createHash("md5").update(email.toLowerCase()).digest("hex");
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
async sendConfirmation(email, _token, _confirmUrl) {
|
|
35
|
+
if (useAsStorage) {
|
|
36
|
+
await addOrUpdateMember(email, "pending");
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
async sendWelcome(email) {
|
|
40
|
+
if (useAsStorage) {
|
|
41
|
+
await addOrUpdateMember(email, "subscribed");
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
async sendUnsubscribed(email) {
|
|
45
|
+
if (useAsStorage) {
|
|
46
|
+
const client = await getClient();
|
|
47
|
+
const subscriberHash = await hashEmail(email);
|
|
48
|
+
await client.lists.updateListMember(listId, subscriberHash, {
|
|
49
|
+
status: "unsubscribed"
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
async notifyAdmin(subscriber) {
|
|
54
|
+
if (!adminEmail) return;
|
|
55
|
+
console.warn(
|
|
56
|
+
`[newsletter-kit] Mailchimp adapter: Admin notification not implemented. Consider setting up a Mailchimp webhook or automation. New subscriber: ${subscriber.email}`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function createMailchimpStorageAdapter(config) {
|
|
62
|
+
const { apiKey, server, listId } = config;
|
|
63
|
+
async function getClient() {
|
|
64
|
+
const mailchimp = await import("@mailchimp/mailchimp_marketing");
|
|
65
|
+
mailchimp.default.setConfig({
|
|
66
|
+
apiKey,
|
|
67
|
+
server
|
|
68
|
+
});
|
|
69
|
+
return mailchimp.default;
|
|
70
|
+
}
|
|
71
|
+
async function hashEmail(email) {
|
|
72
|
+
const crypto = await import("crypto");
|
|
73
|
+
return crypto.createHash("md5").update(email.toLowerCase()).digest("hex");
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
async createSubscriber(input) {
|
|
77
|
+
const client = await getClient();
|
|
78
|
+
await client.lists.addListMember(listId, {
|
|
79
|
+
email_address: input.email,
|
|
80
|
+
status: "pending",
|
|
81
|
+
// Mailchimp handles double opt-in
|
|
82
|
+
tags: input.tags,
|
|
83
|
+
merge_fields: {
|
|
84
|
+
SOURCE: input.source || ""
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
id: await hashEmail(input.email),
|
|
89
|
+
email: input.email,
|
|
90
|
+
status: "pending",
|
|
91
|
+
source: input.source,
|
|
92
|
+
tags: input.tags,
|
|
93
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
94
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
async getSubscriberByEmail(email) {
|
|
98
|
+
const client = await getClient();
|
|
99
|
+
const hash = await hashEmail(email);
|
|
100
|
+
try {
|
|
101
|
+
const member = await client.lists.getListMember(listId, hash);
|
|
102
|
+
return {
|
|
103
|
+
id: hash,
|
|
104
|
+
email: member.email_address,
|
|
105
|
+
status: member.status === "subscribed" ? "confirmed" : member.status,
|
|
106
|
+
tags: member.tags?.map((t) => t.name),
|
|
107
|
+
createdAt: new Date(member.timestamp_signup || member.timestamp_opt || Date.now()),
|
|
108
|
+
updatedAt: new Date(member.last_changed || Date.now())
|
|
109
|
+
};
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
// Note: Mailchimp doesn't use tokens - it has its own confirmation system
|
|
115
|
+
async getSubscriberByToken() {
|
|
116
|
+
console.warn("[newsletter-kit] Mailchimp uses its own confirmation system");
|
|
117
|
+
return null;
|
|
118
|
+
},
|
|
119
|
+
async confirmSubscriber(_token) {
|
|
120
|
+
console.warn("[newsletter-kit] Mailchimp handles confirmation automatically");
|
|
121
|
+
return null;
|
|
122
|
+
},
|
|
123
|
+
async unsubscribe(email) {
|
|
124
|
+
const client = await getClient();
|
|
125
|
+
const hash = await hashEmail(email);
|
|
126
|
+
await client.lists.updateListMember(listId, hash, {
|
|
127
|
+
status: "unsubscribed"
|
|
128
|
+
});
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export {
|
|
134
|
+
createMailchimpAdapter,
|
|
135
|
+
createMailchimpStorageAdapter
|
|
136
|
+
};
|
|
137
|
+
//# sourceMappingURL=mailchimp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/adapters/email/mailchimp.ts"],"sourcesContent":["import type { EmailAdapter, EmailAdapterConfig, Subscriber } from '../../types';\n\ninterface MailchimpAdapterConfig extends EmailAdapterConfig {\n apiKey: string;\n server: string; // e.g., 'us1', 'us2', etc.\n listId: string;\n /** If true, use Mailchimp for storage too (adds to list on subscribe) */\n useAsStorage?: boolean;\n}\n\n/**\n * Email adapter for Mailchimp\n * \n * Note: Mailchimp works differently from other adapters. It manages its own\n * subscriber list, so this adapter can optionally act as both email AND storage.\n * \n * @example\n * ```ts\n * import { createMailchimpAdapter } from '@volchok/newsletter-kit/adapters/email';\n * \n * const emailAdapter = createMailchimpAdapter({\n * apiKey: process.env.MAILCHIMP_API_KEY!,\n * server: 'us1',\n * listId: 'your-list-id',\n * from: 'newsletter@yourdomain.com',\n * useAsStorage: true, // Use Mailchimp's list as storage\n * });\n * ```\n */\nexport function createMailchimpAdapter(config: MailchimpAdapterConfig): EmailAdapter {\n const { apiKey, server, listId, from, adminEmail, useAsStorage } = config;\n\n async function getClient() {\n // Dynamic import\n const mailchimp = await import('@mailchimp/mailchimp_marketing');\n mailchimp.default.setConfig({\n apiKey,\n server,\n });\n return mailchimp.default;\n }\n\n async function addOrUpdateMember(email: string, status: 'subscribed' | 'pending') {\n const client = await getClient();\n const subscriberHash = await hashEmail(email);\n\n try {\n await client.lists.setListMember(listId, subscriberHash, {\n email_address: email,\n status_if_new: status,\n });\n } catch (error) {\n // If member doesn't exist, add them\n await client.lists.addListMember(listId, {\n email_address: email,\n status,\n });\n }\n }\n\n async function hashEmail(email: string): Promise<string> {\n const crypto = await import('crypto');\n return crypto.createHash('md5').update(email.toLowerCase()).digest('hex');\n }\n\n // Mailchimp handles its own confirmation emails when status is 'pending'\n // We provide fallback implementations that work with Mailchimp's system\n\n return {\n async sendConfirmation(email: string, _token: string, _confirmUrl: string) {\n if (useAsStorage) {\n // Add to Mailchimp with 'pending' status - Mailchimp sends its own confirmation\n await addOrUpdateMember(email, 'pending');\n }\n // If not using as storage, the storage adapter handles this\n // and we don't send duplicate confirmation emails\n },\n\n async sendWelcome(email: string) {\n // Mailchimp can be configured to send welcome emails automatically\n // via automations. If useAsStorage is true, we update status to subscribed.\n if (useAsStorage) {\n await addOrUpdateMember(email, 'subscribed');\n }\n },\n\n async sendUnsubscribed(email: string) {\n if (useAsStorage) {\n const client = await getClient();\n const subscriberHash = await hashEmail(email);\n await client.lists.updateListMember(listId, subscriberHash, {\n status: 'unsubscribed',\n });\n }\n },\n\n async notifyAdmin(subscriber: Subscriber) {\n if (!adminEmail) return;\n\n // Mailchimp doesn't have a built-in way to notify admins\n // You'd typically set up a webhook or automation for this\n // For now, we log a warning\n console.warn(\n `[newsletter-kit] Mailchimp adapter: Admin notification not implemented. ` +\n `Consider setting up a Mailchimp webhook or automation. ` +\n `New subscriber: ${subscriber.email}`\n );\n },\n };\n}\n\n/**\n * Create a combined Mailchimp adapter that handles both email and storage\n * \n * This is useful when you want Mailchimp to be your single source of truth\n * for subscribers.\n */\nexport function createMailchimpStorageAdapter(config: MailchimpAdapterConfig) {\n const { apiKey, server, listId } = config;\n\n async function getClient() {\n const mailchimp = await import('@mailchimp/mailchimp_marketing');\n mailchimp.default.setConfig({\n apiKey,\n server,\n });\n return mailchimp.default;\n }\n\n async function hashEmail(email: string): Promise<string> {\n const crypto = await import('crypto');\n return crypto.createHash('md5').update(email.toLowerCase()).digest('hex');\n }\n\n return {\n async createSubscriber(input: { email: string; source?: string; tags?: string[] }) {\n const client = await getClient();\n\n await client.lists.addListMember(listId, {\n email_address: input.email,\n status: 'pending', // Mailchimp handles double opt-in\n tags: input.tags,\n merge_fields: {\n SOURCE: input.source || '',\n },\n });\n\n return {\n id: await hashEmail(input.email),\n email: input.email,\n status: 'pending' as const,\n source: input.source,\n tags: input.tags,\n createdAt: new Date(),\n updatedAt: new Date(),\n };\n },\n\n async getSubscriberByEmail(email: string) {\n const client = await getClient();\n const hash = await hashEmail(email);\n\n try {\n const member = await client.lists.getListMember(listId, hash) as {\n email_address: string;\n status: string;\n tags?: Array<{ name: string }>;\n timestamp_signup?: string;\n timestamp_opt?: string;\n last_changed?: string;\n };\n return {\n id: hash,\n email: member.email_address,\n status: member.status === 'subscribed' ? 'confirmed' : member.status,\n tags: member.tags?.map((t) => t.name),\n createdAt: new Date(member.timestamp_signup || member.timestamp_opt || Date.now()),\n updatedAt: new Date(member.last_changed || Date.now()),\n };\n } catch {\n return null;\n }\n },\n\n // Note: Mailchimp doesn't use tokens - it has its own confirmation system\n async getSubscriberByToken() {\n console.warn('[newsletter-kit] Mailchimp uses its own confirmation system');\n return null;\n },\n\n async confirmSubscriber(_token: string) {\n console.warn('[newsletter-kit] Mailchimp handles confirmation automatically');\n return null;\n },\n\n async unsubscribe(email: string) {\n const client = await getClient();\n const hash = await hashEmail(email);\n\n await client.lists.updateListMember(listId, hash, {\n status: 'unsubscribed',\n });\n\n return true;\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { a as EmailAdapterConfig, E as EmailAdapter } from '../../types-fgF8dmx1.js';
|
|
2
|
+
|
|
3
|
+
interface SMTPConfig {
|
|
4
|
+
host: string;
|
|
5
|
+
port: number;
|
|
6
|
+
secure?: boolean;
|
|
7
|
+
auth?: {
|
|
8
|
+
user: string;
|
|
9
|
+
pass: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
interface NodemailerAdapterConfig extends EmailAdapterConfig {
|
|
13
|
+
smtp: SMTPConfig;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Email adapter for Nodemailer/SMTP
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { createNodemailerAdapter } from '@volchok/newsletter-kit/adapters/email';
|
|
21
|
+
*
|
|
22
|
+
* const emailAdapter = createNodemailerAdapter({
|
|
23
|
+
* smtp: {
|
|
24
|
+
* host: 'smtp.example.com',
|
|
25
|
+
* port: 587,
|
|
26
|
+
* secure: false,
|
|
27
|
+
* auth: {
|
|
28
|
+
* user: process.env.SMTP_USER!,
|
|
29
|
+
* pass: process.env.SMTP_PASS!,
|
|
30
|
+
* },
|
|
31
|
+
* },
|
|
32
|
+
* from: 'newsletter@yourdomain.com',
|
|
33
|
+
* adminEmail: 'you@yourdomain.com', // optional
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
declare function createNodemailerAdapter(config: NodemailerAdapterConfig): EmailAdapter;
|
|
38
|
+
|
|
39
|
+
export { createNodemailerAdapter };
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/adapters/email/nodemailer.ts
|
|
4
|
+
function createNodemailerAdapter(config) {
|
|
5
|
+
const { smtp, from, replyTo, templates, adminEmail } = config;
|
|
6
|
+
async function sendEmail(params) {
|
|
7
|
+
const nodemailer = await import("nodemailer");
|
|
8
|
+
const transporter = nodemailer.createTransport(smtp);
|
|
9
|
+
await transporter.sendMail({
|
|
10
|
+
from,
|
|
11
|
+
replyTo,
|
|
12
|
+
to: params.to,
|
|
13
|
+
subject: params.subject,
|
|
14
|
+
html: params.html,
|
|
15
|
+
text: params.text
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const defaultTemplates = {
|
|
19
|
+
confirmation: {
|
|
20
|
+
subject: "Confirm your subscription",
|
|
21
|
+
text: void 0,
|
|
22
|
+
html: ({ confirmUrl, email }) => `
|
|
23
|
+
<!DOCTYPE html>
|
|
24
|
+
<html>
|
|
25
|
+
<head>
|
|
26
|
+
<meta charset="utf-8">
|
|
27
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
28
|
+
</head>
|
|
29
|
+
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
30
|
+
<h1 style="color: #111; font-size: 24px; margin-bottom: 20px;">Confirm your subscription</h1>
|
|
31
|
+
<p>Thanks for subscribing! Please confirm your email address by clicking the button below:</p>
|
|
32
|
+
<a href="${confirmUrl}" style="display: inline-block; background-color: #000; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 6px; margin: 20px 0;">
|
|
33
|
+
Confirm Subscription
|
|
34
|
+
</a>
|
|
35
|
+
<p style="color: #666; font-size: 14px; margin-top: 30px;">
|
|
36
|
+
If you didn't subscribe to this newsletter, you can safely ignore this email.
|
|
37
|
+
</p>
|
|
38
|
+
<p style="color: #999; font-size: 12px; margin-top: 20px;">
|
|
39
|
+
This confirmation was requested for ${email}
|
|
40
|
+
</p>
|
|
41
|
+
</body>
|
|
42
|
+
</html>
|
|
43
|
+
`
|
|
44
|
+
},
|
|
45
|
+
welcome: {
|
|
46
|
+
subject: "Welcome to our newsletter!",
|
|
47
|
+
text: void 0,
|
|
48
|
+
html: ({ email }) => `
|
|
49
|
+
<!DOCTYPE html>
|
|
50
|
+
<html>
|
|
51
|
+
<head>
|
|
52
|
+
<meta charset="utf-8">
|
|
53
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
54
|
+
</head>
|
|
55
|
+
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
56
|
+
<h1 style="color: #111; font-size: 24px; margin-bottom: 20px;">You're subscribed! \u{1F389}</h1>
|
|
57
|
+
<p>Thanks for confirming your subscription. You'll now receive our updates at <strong>${email}</strong>.</p>
|
|
58
|
+
<p>We're excited to have you!</p>
|
|
59
|
+
</body>
|
|
60
|
+
</html>
|
|
61
|
+
`
|
|
62
|
+
},
|
|
63
|
+
unsubscribed: {
|
|
64
|
+
subject: "You have been unsubscribed",
|
|
65
|
+
text: void 0,
|
|
66
|
+
html: ({ email, resubscribeUrl }) => `
|
|
67
|
+
<!DOCTYPE html>
|
|
68
|
+
<html>
|
|
69
|
+
<head>
|
|
70
|
+
<meta charset="utf-8">
|
|
71
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
72
|
+
</head>
|
|
73
|
+
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
74
|
+
<h1 style="color: #111; font-size: 24px; margin-bottom: 20px;">Unsubscribed</h1>
|
|
75
|
+
<p>You have been unsubscribed from our newsletter. We're sorry to see you go!</p>
|
|
76
|
+
${resubscribeUrl ? `<p><a href="${resubscribeUrl}">Changed your mind? Resubscribe here.</a></p>` : ""}
|
|
77
|
+
<p style="color: #999; font-size: 12px; margin-top: 20px;">
|
|
78
|
+
This email was sent to ${email}
|
|
79
|
+
</p>
|
|
80
|
+
</body>
|
|
81
|
+
</html>
|
|
82
|
+
`
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
async sendConfirmation(email, token, confirmUrl) {
|
|
87
|
+
const template = templates?.confirmation ?? defaultTemplates.confirmation;
|
|
88
|
+
const subject = template.subject ?? defaultTemplates.confirmation.subject;
|
|
89
|
+
const html = template.html ?? defaultTemplates.confirmation.html;
|
|
90
|
+
await sendEmail({
|
|
91
|
+
to: email,
|
|
92
|
+
subject,
|
|
93
|
+
html: html({ confirmUrl, email }),
|
|
94
|
+
text: template.text?.({ confirmUrl, email })
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
async sendWelcome(email) {
|
|
98
|
+
const template = templates?.welcome ?? defaultTemplates.welcome;
|
|
99
|
+
const subject = template.subject ?? defaultTemplates.welcome.subject;
|
|
100
|
+
const html = template.html ?? defaultTemplates.welcome.html;
|
|
101
|
+
await sendEmail({
|
|
102
|
+
to: email,
|
|
103
|
+
subject,
|
|
104
|
+
html: html({ email }),
|
|
105
|
+
text: template.text?.({ email })
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
async sendUnsubscribed(email, resubscribeUrl) {
|
|
109
|
+
const template = templates?.unsubscribed ?? defaultTemplates.unsubscribed;
|
|
110
|
+
const subject = template.subject ?? defaultTemplates.unsubscribed.subject;
|
|
111
|
+
const html = template.html ?? defaultTemplates.unsubscribed.html;
|
|
112
|
+
await sendEmail({
|
|
113
|
+
to: email,
|
|
114
|
+
subject,
|
|
115
|
+
html: html({ email, resubscribeUrl }),
|
|
116
|
+
text: template.text?.({ email, resubscribeUrl })
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
async notifyAdmin(subscriber) {
|
|
120
|
+
if (!adminEmail) return;
|
|
121
|
+
await sendEmail({
|
|
122
|
+
to: adminEmail,
|
|
123
|
+
subject: `New newsletter subscriber: ${subscriber.email}`,
|
|
124
|
+
html: `
|
|
125
|
+
<!DOCTYPE html>
|
|
126
|
+
<html>
|
|
127
|
+
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; padding: 20px;">
|
|
128
|
+
<h2>New Newsletter Subscriber</h2>
|
|
129
|
+
<p><strong>Email:</strong> ${subscriber.email}</p>
|
|
130
|
+
<p><strong>Status:</strong> ${subscriber.status}</p>
|
|
131
|
+
${subscriber.source ? `<p><strong>Source:</strong> ${subscriber.source}</p>` : ""}
|
|
132
|
+
${subscriber.tags?.length ? `<p><strong>Tags:</strong> ${subscriber.tags.join(", ")}</p>` : ""}
|
|
133
|
+
<p><strong>Subscribed at:</strong> ${subscriber.createdAt.toISOString()}</p>
|
|
134
|
+
</body>
|
|
135
|
+
</html>
|
|
136
|
+
`
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
export {
|
|
142
|
+
createNodemailerAdapter
|
|
143
|
+
};
|
|
144
|
+
//# sourceMappingURL=nodemailer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/adapters/email/nodemailer.ts"],"sourcesContent":["import type { EmailAdapter, EmailAdapterConfig, EmailTemplates, Subscriber } from '../../types';\n\ninterface SMTPConfig {\n host: string;\n port: number;\n secure?: boolean;\n auth?: {\n user: string;\n pass: string;\n };\n}\n\ninterface NodemailerAdapterConfig extends EmailAdapterConfig {\n smtp: SMTPConfig;\n}\n\n/**\n * Email adapter for Nodemailer/SMTP\n * \n * @example\n * ```ts\n * import { createNodemailerAdapter } from '@volchok/newsletter-kit/adapters/email';\n * \n * const emailAdapter = createNodemailerAdapter({\n * smtp: {\n * host: 'smtp.example.com',\n * port: 587,\n * secure: false,\n * auth: {\n * user: process.env.SMTP_USER!,\n * pass: process.env.SMTP_PASS!,\n * },\n * },\n * from: 'newsletter@yourdomain.com',\n * adminEmail: 'you@yourdomain.com', // optional\n * });\n * ```\n */\nexport function createNodemailerAdapter(config: NodemailerAdapterConfig): EmailAdapter {\n const { smtp, from, replyTo, templates, adminEmail } = config;\n\n async function sendEmail(params: {\n to: string;\n subject: string;\n html: string;\n text?: string;\n }) {\n // Dynamic import to avoid requiring nodemailer as a hard dependency\n const nodemailer = await import('nodemailer');\n const transporter = nodemailer.createTransport(smtp);\n\n await transporter.sendMail({\n from,\n replyTo,\n to: params.to,\n subject: params.subject,\n html: params.html,\n text: params.text,\n });\n }\n\n const defaultTemplates: Required<EmailTemplates> = {\n confirmation: {\n subject: 'Confirm your subscription',\n text: undefined,\n html: ({ confirmUrl, email }: { confirmUrl: string; email: string }) => `\n <!DOCTYPE html>\n <html>\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n </head>\n <body style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;\">\n <h1 style=\"color: #111; font-size: 24px; margin-bottom: 20px;\">Confirm your subscription</h1>\n <p>Thanks for subscribing! Please confirm your email address by clicking the button below:</p>\n <a href=\"${confirmUrl}\" style=\"display: inline-block; background-color: #000; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 6px; margin: 20px 0;\">\n Confirm Subscription\n </a>\n <p style=\"color: #666; font-size: 14px; margin-top: 30px;\">\n If you didn't subscribe to this newsletter, you can safely ignore this email.\n </p>\n <p style=\"color: #999; font-size: 12px; margin-top: 20px;\">\n This confirmation was requested for ${email}\n </p>\n </body>\n </html>\n `,\n },\n welcome: {\n subject: 'Welcome to our newsletter!',\n text: undefined,\n html: ({ email }: { email: string }) => `\n <!DOCTYPE html>\n <html>\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n </head>\n <body style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;\">\n <h1 style=\"color: #111; font-size: 24px; margin-bottom: 20px;\">You're subscribed! 🎉</h1>\n <p>Thanks for confirming your subscription. You'll now receive our updates at <strong>${email}</strong>.</p>\n <p>We're excited to have you!</p>\n </body>\n </html>\n `,\n },\n unsubscribed: {\n subject: 'You have been unsubscribed',\n text: undefined,\n html: ({ email, resubscribeUrl }: { email: string; resubscribeUrl?: string }) => `\n <!DOCTYPE html>\n <html>\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n </head>\n <body style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;\">\n <h1 style=\"color: #111; font-size: 24px; margin-bottom: 20px;\">Unsubscribed</h1>\n <p>You have been unsubscribed from our newsletter. We're sorry to see you go!</p>\n ${resubscribeUrl ? `<p><a href=\"${resubscribeUrl}\">Changed your mind? Resubscribe here.</a></p>` : ''}\n <p style=\"color: #999; font-size: 12px; margin-top: 20px;\">\n This email was sent to ${email}\n </p>\n </body>\n </html>\n `,\n },\n };\n\n return {\n async sendConfirmation(email: string, token: string, confirmUrl: string) {\n const template = templates?.confirmation ?? defaultTemplates.confirmation;\n const subject = template.subject ?? defaultTemplates.confirmation.subject!;\n const html = template.html ?? defaultTemplates.confirmation.html!;\n await sendEmail({\n to: email,\n subject,\n html: html({ confirmUrl, email }),\n text: template.text?.({ confirmUrl, email }),\n });\n },\n\n async sendWelcome(email: string) {\n const template = templates?.welcome ?? defaultTemplates.welcome;\n const subject = template.subject ?? defaultTemplates.welcome.subject!;\n const html = template.html ?? defaultTemplates.welcome.html!;\n await sendEmail({\n to: email,\n subject,\n html: html({ email }),\n text: template.text?.({ email }),\n });\n },\n\n async sendUnsubscribed(email: string, resubscribeUrl?: string) {\n const template = templates?.unsubscribed ?? defaultTemplates.unsubscribed;\n const subject = template.subject ?? defaultTemplates.unsubscribed.subject!;\n const html = template.html ?? defaultTemplates.unsubscribed.html!;\n await sendEmail({\n to: email,\n subject,\n html: html({ email, resubscribeUrl }),\n text: template.text?.({ email, resubscribeUrl }),\n });\n },\n\n async notifyAdmin(subscriber: Subscriber) {\n if (!adminEmail) return;\n\n await sendEmail({\n to: adminEmail,\n subject: `New newsletter subscriber: ${subscriber.email}`,\n html: `\n <!DOCTYPE html>\n <html>\n <body style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; padding: 20px;\">\n <h2>New Newsletter Subscriber</h2>\n <p><strong>Email:</strong> ${subscriber.email}</p>\n <p><strong>Status:</strong> ${subscriber.status}</p>\n ${subscriber.source ? `<p><strong>Source:</strong> ${subscriber.source}</p>` : ''}\n ${subscriber.tags?.length ? `<p><strong>Tags:</strong> ${subscriber.tags.join(', ')}</p>` : ''}\n <p><strong>Subscribed at:</strong> ${subscriber.createdAt.toISOString()}</p>\n </body>\n </html>\n `,\n });\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { a as EmailAdapterConfig, E as EmailAdapter } from '../../types-fgF8dmx1.js';
|
|
2
|
+
|
|
3
|
+
interface ResendAdapterConfig extends EmailAdapterConfig {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Email adapter for Resend (https://resend.com)
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { createResendAdapter } from '@volchok/newsletter-kit/adapters/email';
|
|
12
|
+
*
|
|
13
|
+
* const emailAdapter = createResendAdapter({
|
|
14
|
+
* apiKey: process.env.RESEND_API_KEY!,
|
|
15
|
+
* from: 'newsletter@yourdomain.com',
|
|
16
|
+
* adminEmail: 'you@yourdomain.com', // optional
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
declare function createResendAdapter(config: ResendAdapterConfig): EmailAdapter;
|
|
21
|
+
|
|
22
|
+
export { createResendAdapter };
|