@growth-labs/mailer 0.1.3
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 +89 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +3 -0
- package/dist/components/index.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/tracking.d.ts +3 -0
- package/dist/middleware/tracking.d.ts.map +1 -0
- package/dist/middleware/tracking.js +13 -0
- package/dist/middleware/tracking.js.map +1 -0
- package/dist/options.d.ts +160 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +51 -0
- package/dist/options.js.map +1 -0
- package/dist/queue/consumer.d.ts +8 -0
- package/dist/queue/consumer.d.ts.map +1 -0
- package/dist/queue/consumer.js +83 -0
- package/dist/queue/consumer.js.map +1 -0
- package/dist/routes/confirm.d.ts +3 -0
- package/dist/routes/confirm.d.ts.map +1 -0
- package/dist/routes/confirm.js +59 -0
- package/dist/routes/confirm.js.map +1 -0
- package/dist/routes/subscribe.d.ts +3 -0
- package/dist/routes/subscribe.d.ts.map +1 -0
- package/dist/routes/subscribe.js +87 -0
- package/dist/routes/subscribe.js.map +1 -0
- package/dist/routes/track-click.d.ts +3 -0
- package/dist/routes/track-click.d.ts.map +1 -0
- package/dist/routes/track-click.js +45 -0
- package/dist/routes/track-click.js.map +1 -0
- package/dist/routes/track-open.d.ts +3 -0
- package/dist/routes/track-open.d.ts.map +1 -0
- package/dist/routes/track-open.js +40 -0
- package/dist/routes/track-open.js.map +1 -0
- package/dist/routes/unsubscribe.d.ts +4 -0
- package/dist/routes/unsubscribe.d.ts.map +1 -0
- package/dist/routes/unsubscribe.js +81 -0
- package/dist/routes/unsubscribe.js.map +1 -0
- package/dist/routes/webhook.d.ts +3 -0
- package/dist/routes/webhook.d.ts.map +1 -0
- package/dist/routes/webhook.js +30 -0
- package/dist/routes/webhook.js.map +1 -0
- package/dist/schema.d.ts +564 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +47 -0
- package/dist/schema.js.map +1 -0
- package/dist/types.d.ts +106 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/bindings.d.ts +20 -0
- package/dist/utils/bindings.d.ts.map +1 -0
- package/dist/utils/bindings.js +19 -0
- package/dist/utils/bindings.js.map +1 -0
- package/dist/utils/bounce.d.ts +29 -0
- package/dist/utils/bounce.d.ts.map +1 -0
- package/dist/utils/bounce.js +59 -0
- package/dist/utils/bounce.js.map +1 -0
- package/dist/utils/index.d.ts +12 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +9 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/providers.d.ts +31 -0
- package/dist/utils/providers.d.ts.map +1 -0
- package/dist/utils/providers.js +109 -0
- package/dist/utils/providers.js.map +1 -0
- package/dist/utils/scheduling.d.ts +89 -0
- package/dist/utils/scheduling.d.ts.map +1 -0
- package/dist/utils/scheduling.js +110 -0
- package/dist/utils/scheduling.js.map +1 -0
- package/dist/utils/send.d.ts +42 -0
- package/dist/utils/send.d.ts.map +1 -0
- package/dist/utils/send.js +193 -0
- package/dist/utils/send.js.map +1 -0
- package/dist/utils/subscribers.d.ts +23 -0
- package/dist/utils/subscribers.d.ts.map +1 -0
- package/dist/utils/subscribers.js +200 -0
- package/dist/utils/subscribers.js.map +1 -0
- package/dist/utils/templates.d.ts +16 -0
- package/dist/utils/templates.d.ts.map +1 -0
- package/dist/utils/templates.js +426 -0
- package/dist/utils/templates.js.map +1 -0
- package/dist/utils/tokens.d.ts +13 -0
- package/dist/utils/tokens.d.ts.map +1 -0
- package/dist/utils/tokens.js +62 -0
- package/dist/utils/tokens.js.map +1 -0
- package/dist/utils/tracking.d.ts +26 -0
- package/dist/utils/tracking.d.ts.map +1 -0
- package/dist/utils/tracking.js +49 -0
- package/dist/utils/tracking.js.map +1 -0
- package/dist/utils/urls.d.ts +7 -0
- package/dist/utils/urls.d.ts.map +1 -0
- package/dist/utils/urls.js +34 -0
- package/dist/utils/urls.js.map +1 -0
- package/dist/vite-plugin.d.ts +4 -0
- package/dist/vite-plugin.d.ts.map +1 -0
- package/dist/vite-plugin.js +18 -0
- package/dist/vite-plugin.js.map +1 -0
- package/package.json +85 -0
- package/src/astro.d.ts +4 -0
- package/src/components/PreferenceCenter.astro +147 -0
- package/src/components/SubscribeForm.astro +161 -0
- package/src/components/index.ts +2 -0
- package/src/index.ts +101 -0
- package/src/middleware/tracking.ts +18 -0
- package/src/options.ts +65 -0
- package/src/queue/consumer.ts +99 -0
- package/src/routes/confirm.ts +68 -0
- package/src/routes/preferences.astro +137 -0
- package/src/routes/subscribe.ts +107 -0
- package/src/routes/track-click.ts +57 -0
- package/src/routes/track-open.ts +51 -0
- package/src/routes/unsubscribe.ts +96 -0
- package/src/routes/webhook.ts +48 -0
- package/src/schema.ts +56 -0
- package/src/types.ts +145 -0
- package/src/utils/bindings.ts +28 -0
- package/src/utils/bounce.ts +77 -0
- package/src/utils/index.ts +47 -0
- package/src/utils/providers.ts +141 -0
- package/src/utils/scheduling.ts +188 -0
- package/src/utils/send.ts +282 -0
- package/src/utils/subscribers.ts +277 -0
- package/src/utils/templates.ts +459 -0
- package/src/utils/tokens.ts +91 -0
- package/src/utils/tracking.ts +58 -0
- package/src/utils/urls.ts +49 -0
- package/src/virtual.d.ts +32 -0
- package/src/vite-plugin.ts +21 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { config } from 'virtual:growth-labs/mailer/config';
|
|
2
|
+
import { drizzle } from 'drizzle-orm/d1';
|
|
3
|
+
import { sendTransactional } from '../utils/send.js';
|
|
4
|
+
import { createSubscriber } from '../utils/subscribers.js';
|
|
5
|
+
import { generateToken } from '../utils/tokens.js';
|
|
6
|
+
export const POST = async ({ request, locals }) => {
|
|
7
|
+
// 1. Parse request body
|
|
8
|
+
const body = (await request.json());
|
|
9
|
+
// 2. Validate required fields
|
|
10
|
+
if (!body.email || !body.turnstileToken) {
|
|
11
|
+
return Response.json({ error: 'Missing required fields' }, { status: 400 });
|
|
12
|
+
}
|
|
13
|
+
// 3. Basic email format validation
|
|
14
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(body.email)) {
|
|
15
|
+
return Response.json({ error: 'Invalid email format' }, { status: 400 });
|
|
16
|
+
}
|
|
17
|
+
// 4. Validate Turnstile token
|
|
18
|
+
const turnstileResponse = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: { 'Content-Type': 'application/json' },
|
|
21
|
+
body: JSON.stringify({
|
|
22
|
+
secret: config.turnstileSecretKey,
|
|
23
|
+
response: body.turnstileToken,
|
|
24
|
+
remoteip: request.headers.get('cf-connecting-ip') ?? undefined,
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
const turnstileResult = (await turnstileResponse.json());
|
|
28
|
+
if (!turnstileResult.success) {
|
|
29
|
+
return Response.json({ error: 'Turnstile verification failed' }, { status: 400 });
|
|
30
|
+
}
|
|
31
|
+
// 5. Resolve bindings
|
|
32
|
+
const runtimeEnv = locals.runtime?.env;
|
|
33
|
+
const d1 = runtimeEnv[config.d1Binding];
|
|
34
|
+
const queue = runtimeEnv[config.queueBinding];
|
|
35
|
+
const db = drizzle(d1);
|
|
36
|
+
const env = { DB: d1, QUEUE: queue };
|
|
37
|
+
// 6. Create subscriber
|
|
38
|
+
try {
|
|
39
|
+
const { subscriber, isNew } = await createSubscriber(db, {
|
|
40
|
+
email: body.email,
|
|
41
|
+
name: body.name,
|
|
42
|
+
source: body.source ?? 'form',
|
|
43
|
+
preferences: body.preferences,
|
|
44
|
+
doubleOptIn: config.doubleOptIn,
|
|
45
|
+
});
|
|
46
|
+
// 7. Send confirmation or welcome email
|
|
47
|
+
if (config.doubleOptIn && (isNew || subscriber.status === 'pending')) {
|
|
48
|
+
const confirmToken = await generateToken(config.signingSecret, {
|
|
49
|
+
subscriberId: subscriber.id,
|
|
50
|
+
action: 'confirm',
|
|
51
|
+
});
|
|
52
|
+
const confirmUrl = `${config.siteUrl}${config.confirmPath}?token=${confirmToken}`;
|
|
53
|
+
await sendTransactional(env, config, {
|
|
54
|
+
to: body.email,
|
|
55
|
+
subscriberId: subscriber.id,
|
|
56
|
+
subject: `Confirm your ${config.senderName} subscription`,
|
|
57
|
+
template: 'confirmation',
|
|
58
|
+
data: {
|
|
59
|
+
name: body.name,
|
|
60
|
+
confirmUrl,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
else if (!config.doubleOptIn && isNew) {
|
|
65
|
+
await sendTransactional(env, config, {
|
|
66
|
+
to: body.email,
|
|
67
|
+
subscriberId: subscriber.id,
|
|
68
|
+
subject: `Welcome to ${config.senderName}`,
|
|
69
|
+
template: 'welcome',
|
|
70
|
+
data: { name: body.name },
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// Return success (always 200 to avoid email enumeration)
|
|
74
|
+
return Response.json({
|
|
75
|
+
success: true,
|
|
76
|
+
requiresConfirmation: config.doubleOptIn,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
81
|
+
if (message.includes('bounced') || message.includes('complained')) {
|
|
82
|
+
return Response.json({ error: 'This email address cannot be subscribed' }, { status: 422 });
|
|
83
|
+
}
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=subscribe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscribe.js","sourceRoot":"","sources":["../../src/routes/subscribe.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"track-click.d.ts","sourceRoot":"","sources":["../../src/routes/track-click.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAKrC,eAAO,MAAM,GAAG,EAAE,QAkDjB,CAAA"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { config } from 'virtual:growth-labs/mailer/config';
|
|
2
|
+
import { and, eq, inArray } from 'drizzle-orm';
|
|
3
|
+
import { drizzle } from 'drizzle-orm/d1';
|
|
4
|
+
import { emailSends } from '../schema.js';
|
|
5
|
+
export const GET = async ({ params, url, locals }) => {
|
|
6
|
+
const trackingId = params.trackingId;
|
|
7
|
+
const destination = url.searchParams.get('url');
|
|
8
|
+
if (!trackingId || !destination) {
|
|
9
|
+
return new Response('Bad request', { status: 400 });
|
|
10
|
+
}
|
|
11
|
+
// Validate: must be http or https (prevent javascript:, data:, etc.)
|
|
12
|
+
try {
|
|
13
|
+
const destUrl = new URL(destination);
|
|
14
|
+
if (!['http:', 'https:'].includes(destUrl.protocol)) {
|
|
15
|
+
return new Response('Invalid redirect', { status: 400 });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return new Response('Invalid URL', { status: 400 });
|
|
20
|
+
}
|
|
21
|
+
// Update status to 'clicked' only when it hasn't already reached 'clicked'
|
|
22
|
+
try {
|
|
23
|
+
const runtimeEnv = locals.runtime?.env;
|
|
24
|
+
if (runtimeEnv) {
|
|
25
|
+
const d1 = runtimeEnv[config.d1Binding];
|
|
26
|
+
const db = drizzle(d1);
|
|
27
|
+
await db
|
|
28
|
+
.update(emailSends)
|
|
29
|
+
.set({
|
|
30
|
+
status: 'clicked',
|
|
31
|
+
clickedAt: new Date().toISOString(),
|
|
32
|
+
})
|
|
33
|
+
.where(and(eq(emailSends.trackingId, trackingId), inArray(emailSends.status, ['sent', 'delivered', 'opened'])));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Never fail the redirect on DB errors
|
|
38
|
+
}
|
|
39
|
+
// 302 redirect to original destination
|
|
40
|
+
return new Response(null, {
|
|
41
|
+
status: 302,
|
|
42
|
+
headers: { Location: destination },
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=track-click.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"track-click.js","sourceRoot":"","sources":["../../src/routes/track-click.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAA;AAE1D,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAEzC,MAAM,CAAC,MAAM,GAAG,GAAa,KAAK,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE;IAC9D,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAA;IACpC,MAAM,WAAW,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;IAE/C,IAAI,CAAC,UAAU,IAAI,CAAC,WAAW,EAAE,CAAC;QACjC,OAAO,IAAI,QAAQ,CAAC,aAAa,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IACpD,CAAC;IAED,qEAAqE;IACrE,IAAI,CAAC;QACJ,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,CAAA;QACpC,IAAI,CAAC,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;YACrD,OAAO,IAAI,QAAQ,CAAC,kBAAkB,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;QACzD,CAAC;IACF,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,QAAQ,CAAC,aAAa,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IACpD,CAAC;IAED,2EAA2E;IAC3E,IAAI,CAAC;QACJ,MAAM,UAAU,GACf,MAGA,CAAC,OAAO,EAAE,GAAG,CAAA;QACd,IAAI,UAAU,EAAE,CAAC;YAChB,MAAM,EAAE,GAAG,UAAU,CAAC,MAAM,CAAC,SAAS,CAAe,CAAA;YACrD,MAAM,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC,CAAA;YACtB,MAAM,EAAE;iBACN,MAAM,CAAC,UAAU,CAAC;iBAClB,GAAG,CAAC;gBACJ,MAAM,EAAE,SAAS;gBACjB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;aACnC,CAAC;iBACD,KAAK,CACL,GAAG,CACF,EAAE,CAAC,UAAU,CAAC,UAAU,EAAE,UAAU,CAAC,EACrC,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC,CAC3D,CACD,CAAA;QACH,CAAC;IACF,CAAC;IAAC,MAAM,CAAC;QACR,uCAAuC;IACxC,CAAC;IAED,uCAAuC;IACvC,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE;QACzB,MAAM,EAAE,GAAG;QACX,OAAO,EAAE,EAAE,QAAQ,EAAE,WAAW,EAAE;KAClC,CAAC,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"track-open.d.ts","sourceRoot":"","sources":["../../src/routes/track-open.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAMrC,eAAO,MAAM,GAAG,EAAE,QA2CjB,CAAA"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { config } from 'virtual:growth-labs/mailer/config';
|
|
2
|
+
import { and, eq, inArray } from 'drizzle-orm';
|
|
3
|
+
import { drizzle } from 'drizzle-orm/d1';
|
|
4
|
+
import { emailSends } from '../schema.js';
|
|
5
|
+
import { TRANSPARENT_GIF } from '../utils/tracking.js';
|
|
6
|
+
export const GET = async ({ params, locals }) => {
|
|
7
|
+
const trackingId = params.trackingId;
|
|
8
|
+
if (!trackingId) {
|
|
9
|
+
return new Response(null, { status: 400 });
|
|
10
|
+
}
|
|
11
|
+
// Update status to 'opened' only when currently 'sent' or 'delivered'
|
|
12
|
+
// to avoid downgrading from 'clicked'.
|
|
13
|
+
try {
|
|
14
|
+
const runtimeEnv = locals.runtime?.env;
|
|
15
|
+
if (runtimeEnv) {
|
|
16
|
+
const d1 = runtimeEnv[config.d1Binding];
|
|
17
|
+
const db = drizzle(d1);
|
|
18
|
+
await db
|
|
19
|
+
.update(emailSends)
|
|
20
|
+
.set({
|
|
21
|
+
status: 'opened',
|
|
22
|
+
openedAt: new Date().toISOString(),
|
|
23
|
+
})
|
|
24
|
+
.where(and(eq(emailSends.trackingId, trackingId), inArray(emailSends.status, ['sent', 'delivered'])));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Never fail the pixel response on DB errors
|
|
29
|
+
}
|
|
30
|
+
// 1x1 transparent GIF
|
|
31
|
+
return new Response(TRANSPARENT_GIF, {
|
|
32
|
+
status: 200,
|
|
33
|
+
headers: {
|
|
34
|
+
'Content-Type': 'image/gif',
|
|
35
|
+
'Cache-Control': 'no-store, no-cache, must-revalidate',
|
|
36
|
+
'Content-Length': String(TRANSPARENT_GIF.length),
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=track-open.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"track-open.js","sourceRoot":"","sources":["../../src/routes/track-open.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAA;AAE1D,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAEtD,MAAM,CAAC,MAAM,GAAG,GAAa,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE;IACzD,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAA;IACpC,IAAI,CAAC,UAAU,EAAE,CAAC;QACjB,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAC3C,CAAC;IAED,sEAAsE;IACtE,uCAAuC;IACvC,IAAI,CAAC;QACJ,MAAM,UAAU,GACf,MAGA,CAAC,OAAO,EAAE,GAAG,CAAA;QACd,IAAI,UAAU,EAAE,CAAC;YAChB,MAAM,EAAE,GAAG,UAAU,CAAC,MAAM,CAAC,SAAS,CAAe,CAAA;YACrD,MAAM,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC,CAAA;YACtB,MAAM,EAAE;iBACN,MAAM,CAAC,UAAU,CAAC;iBAClB,GAAG,CAAC;gBACJ,MAAM,EAAE,QAAQ;gBAChB,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;aAClC,CAAC;iBACD,KAAK,CACL,GAAG,CACF,EAAE,CAAC,UAAU,CAAC,UAAU,EAAE,UAAU,CAAC,EACrC,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CACjD,CACD,CAAA;QACH,CAAC;IACF,CAAC;IAAC,MAAM,CAAC;QACR,6CAA6C;IAC9C,CAAC;IAED,sBAAsB;IACtB,OAAO,IAAI,QAAQ,CAAC,eAAe,EAAE;QACpC,MAAM,EAAE,GAAG;QACX,OAAO,EAAE;YACR,cAAc,EAAE,WAAW;YAC3B,eAAe,EAAE,qCAAqC;YACtD,gBAAgB,EAAE,MAAM,CAAC,eAAe,CAAC,MAAM,CAAC;SAChD;KACD,CAAC,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unsubscribe.d.ts","sourceRoot":"","sources":["../../src/routes/unsubscribe.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAmDrC,eAAO,MAAM,GAAG,EAAE,QAyBjB,CAAA;AAGD,eAAO,MAAM,IAAI,EAAE,QAelB,CAAA"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { config } from 'virtual:growth-labs/mailer/config';
|
|
2
|
+
import { drizzle } from 'drizzle-orm/d1';
|
|
3
|
+
import { sendTransactional } from '../utils/send.js';
|
|
4
|
+
import { getSubscriberById, unsubscribeSubscriber } from '../utils/subscribers.js';
|
|
5
|
+
import { generateToken, verifyToken } from '../utils/tokens.js';
|
|
6
|
+
import { buildSiteUrl } from '../utils/urls.js';
|
|
7
|
+
async function processUnsubscribe(locals, token) {
|
|
8
|
+
// Verify token
|
|
9
|
+
const payload = await verifyToken(config.signingSecret, token);
|
|
10
|
+
if (!payload || payload.action !== 'unsubscribe') {
|
|
11
|
+
return { error: 'Invalid or expired token', status: 400 };
|
|
12
|
+
}
|
|
13
|
+
// Resolve bindings
|
|
14
|
+
const runtimeEnv = locals.runtime?.env;
|
|
15
|
+
const d1 = runtimeEnv[config.d1Binding];
|
|
16
|
+
const queue = runtimeEnv[config.queueBinding];
|
|
17
|
+
const db = drizzle(d1);
|
|
18
|
+
const env = { DB: d1, QUEUE: queue };
|
|
19
|
+
// Get subscriber
|
|
20
|
+
const subscriber = await getSubscriberById(db, payload.subscriberId);
|
|
21
|
+
if (!subscriber) {
|
|
22
|
+
return { error: 'Subscriber not found', status: 404 };
|
|
23
|
+
}
|
|
24
|
+
// Unsubscribe
|
|
25
|
+
await unsubscribeSubscriber(db, payload.subscriberId);
|
|
26
|
+
// Send confirmation email
|
|
27
|
+
await sendTransactional(env, config, {
|
|
28
|
+
to: subscriber.email,
|
|
29
|
+
subscriberId: subscriber.id,
|
|
30
|
+
subject: `You've been unsubscribed from ${config.senderName}`,
|
|
31
|
+
template: 'unsubscribe-confirm',
|
|
32
|
+
data: {
|
|
33
|
+
name: subscriber.name,
|
|
34
|
+
resubscribeUrl: buildSiteUrl(config.siteUrl, config.subscribePath),
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
success: true,
|
|
39
|
+
subscriberId: payload.subscriberId,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// GET: Link from email footer
|
|
43
|
+
export const GET = async ({ url, locals }) => {
|
|
44
|
+
const token = url.searchParams.get('token');
|
|
45
|
+
if (!token) {
|
|
46
|
+
return Response.json({ error: 'Missing token' }, { status: 400 });
|
|
47
|
+
}
|
|
48
|
+
const result = await processUnsubscribe(locals, token);
|
|
49
|
+
if ('error' in result) {
|
|
50
|
+
return Response.json({ error: result.error }, { status: result.status });
|
|
51
|
+
}
|
|
52
|
+
// Generate preferences token for the redirect
|
|
53
|
+
const preferencesToken = await generateToken(config.signingSecret, {
|
|
54
|
+
subscriberId: result.subscriberId,
|
|
55
|
+
action: 'preferences',
|
|
56
|
+
});
|
|
57
|
+
const preferencesUrl = buildSiteUrl(config.siteUrl, config.preferencesPath, {
|
|
58
|
+
token: preferencesToken,
|
|
59
|
+
unsubscribed: true,
|
|
60
|
+
});
|
|
61
|
+
return new Response(null, {
|
|
62
|
+
status: 302,
|
|
63
|
+
headers: { Location: preferencesUrl },
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
// POST: RFC 8058 List-Unsubscribe-Post
|
|
67
|
+
export const POST = async ({ request, locals }) => {
|
|
68
|
+
// RFC 8058: body is "List-Unsubscribe=One-Click"
|
|
69
|
+
// Token comes from List-Unsubscribe header URL
|
|
70
|
+
const url = new URL(request.url);
|
|
71
|
+
const token = url.searchParams.get('token');
|
|
72
|
+
if (!token) {
|
|
73
|
+
return new Response('Missing token', { status: 400 });
|
|
74
|
+
}
|
|
75
|
+
const result = await processUnsubscribe(locals, token);
|
|
76
|
+
if ('error' in result) {
|
|
77
|
+
return new Response(result.error, { status: result.status });
|
|
78
|
+
}
|
|
79
|
+
return new Response('OK', { status: 200 });
|
|
80
|
+
};
|
|
81
|
+
//# sourceMappingURL=unsubscribe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unsubscribe.js","sourceRoot":"","sources":["../../src/routes/unsubscribe.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../src/routes/webhook.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAYrC,eAAO,MAAM,IAAI,EAAE,QAkClB,CAAA"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { config } from 'virtual:growth-labs/mailer/config';
|
|
2
|
+
import { drizzle } from 'drizzle-orm/d1';
|
|
3
|
+
import { handleBounce, handleComplaint, handleDelivery } from '../utils/bounce.js';
|
|
4
|
+
export const POST = async ({ request, locals }) => {
|
|
5
|
+
const body = (await request.json());
|
|
6
|
+
if (!body.type || !body.email) {
|
|
7
|
+
return Response.json({ error: 'Invalid payload' }, { status: 400 });
|
|
8
|
+
}
|
|
9
|
+
const runtimeEnv = locals.runtime?.env;
|
|
10
|
+
if (!runtimeEnv) {
|
|
11
|
+
return Response.json({ error: 'Runtime not available' }, { status: 500 });
|
|
12
|
+
}
|
|
13
|
+
const d1 = runtimeEnv[config.d1Binding];
|
|
14
|
+
const db = drizzle(d1);
|
|
15
|
+
switch (body.type) {
|
|
16
|
+
case 'delivery':
|
|
17
|
+
if (body.trackingId) {
|
|
18
|
+
await handleDelivery(db, body.trackingId);
|
|
19
|
+
}
|
|
20
|
+
break;
|
|
21
|
+
case 'bounce':
|
|
22
|
+
await handleBounce(db, body.email, body.bounceType ?? 'hard');
|
|
23
|
+
break;
|
|
24
|
+
case 'complaint':
|
|
25
|
+
await handleComplaint(db, body.email);
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
return Response.json({ ok: true });
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=webhook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook.js","sourceRoot":"","sources":["../../src/routes/webhook.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAA;AAE1D,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AACxC,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAUlF,MAAM,CAAC,MAAM,IAAI,GAAa,KAAK,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE;IAC3D,MAAM,IAAI,GAAG,CAAC,MAAM,OAAO,CAAC,IAAI,EAAE,CAAmB,CAAA;IAErD,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;QAC/B,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,iBAAiB,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IACpE,CAAC;IAED,MAAM,UAAU,GACf,MAGA,CAAC,OAAO,EAAE,GAAG,CAAA;IACd,IAAI,CAAC,UAAU,EAAE,CAAC;QACjB,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,uBAAuB,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAC1E,CAAC;IAED,MAAM,EAAE,GAAG,UAAU,CAAC,MAAM,CAAC,SAAS,CAAe,CAAA;IACrD,MAAM,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC,CAAA;IAEtB,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,UAAU;YACd,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;gBACrB,MAAM,cAAc,CAAC,EAAE,EAAE,IAAI,CAAC,UAAU,CAAC,CAAA;YAC1C,CAAC;YACD,MAAK;QACN,KAAK,QAAQ;YACZ,MAAM,YAAY,CAAC,EAAE,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,IAAI,MAAM,CAAC,CAAA;YAC7D,MAAK;QACN,KAAK,WAAW;YACf,MAAM,eAAe,CAAC,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,CAAA;YACrC,MAAK;IACP,CAAC;IAED,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAA;AACnC,CAAC,CAAA"}
|