@growth-labs/mailer 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -4
- package/dist/options.d.ts +3 -0
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +1 -0
- package/dist/options.js.map +1 -1
- package/dist/queue/consumer.d.ts +1 -0
- package/dist/queue/consumer.d.ts.map +1 -1
- package/dist/queue/consumer.js +22 -0
- package/dist/queue/consumer.js.map +1 -1
- package/dist/routes/confirm.d.ts.map +1 -1
- package/dist/routes/confirm.js +15 -4
- package/dist/routes/confirm.js.map +1 -1
- package/dist/routes/subscribe.d.ts.map +1 -1
- package/dist/routes/subscribe.js +20 -4
- package/dist/routes/subscribe.js.map +1 -1
- package/dist/routes/track-click.d.ts.map +1 -1
- package/dist/routes/track-click.js +13 -4
- package/dist/routes/track-click.js.map +1 -1
- package/dist/routes/track-open.d.ts.map +1 -1
- package/dist/routes/track-open.js +13 -4
- package/dist/routes/track-open.js.map +1 -1
- package/dist/routes/unsubscribe.d.ts.map +1 -1
- package/dist/routes/unsubscribe.js +21 -8
- package/dist/routes/unsubscribe.js.map +1 -1
- package/dist/routes/webhook.d.ts.map +1 -1
- package/dist/routes/webhook.js +28 -5
- package/dist/routes/webhook.js.map +1 -1
- package/dist/utils/analytics.d.ts +24 -0
- package/dist/utils/analytics.d.ts.map +1 -0
- package/dist/utils/analytics.js +74 -0
- package/dist/utils/analytics.js.map +1 -0
- package/dist/utils/bindings.d.ts +4 -10
- package/dist/utils/bindings.d.ts.map +1 -1
- package/dist/utils/bindings.js +7 -7
- package/dist/utils/bindings.js.map +1 -1
- package/dist/utils/index.d.ts +2 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/providers.js +1 -1
- package/dist/utils/providers.js.map +1 -1
- package/package.json +7 -1
- package/src/cloudflare-workers.d.ts +3 -0
- package/src/options.ts +1 -0
- package/src/queue/consumer.ts +27 -1
- package/src/routes/confirm.ts +16 -5
- package/src/routes/preferences.astro +5 -9
- package/src/routes/subscribe.ts +21 -5
- package/src/routes/track-click.ts +14 -8
- package/src/routes/track-open.ts +14 -8
- package/src/routes/unsubscribe.ts +26 -9
- package/src/routes/webhook.ts +30 -10
- package/src/utils/analytics.ts +118 -0
- package/src/utils/bindings.ts +9 -11
- package/src/utils/index.ts +6 -5
- package/src/utils/providers.ts +1 -1
- package/src/virtual.d.ts +1 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ResolvedMailerOptions } from '../options.js';
|
|
2
|
+
export type MailerAnalyticsEvent = 'newsletter_subscribed' | 'newsletter_confirmed' | 'newsletter_unsubscribed' | 'newsletter_opened' | 'newsletter_clicked' | 'newsletter_delivered' | 'newsletter_bounced' | 'newsletter_complained' | 'newsletter_sent' | 'newsletter_send_failed';
|
|
3
|
+
interface AnalyticsContext {
|
|
4
|
+
locals?: {
|
|
5
|
+
cfContext?: {
|
|
6
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
interface EmitMailerAnalyticsOptions {
|
|
11
|
+
request?: Request;
|
|
12
|
+
context?: AnalyticsContext;
|
|
13
|
+
label?: Record<string, unknown>;
|
|
14
|
+
contentSlug?: string;
|
|
15
|
+
eventValue?: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function emitMailerAnalyticsEvent(options: ResolvedMailerOptions, bindingsEnv: Record<string, unknown>, eventName: MailerAnalyticsEvent, emitOptions?: EmitMailerAnalyticsOptions): boolean;
|
|
18
|
+
export declare function buildMailerAnalyticsDataPoint(options: ResolvedMailerOptions, eventName: MailerAnalyticsEvent, emitOptions?: EmitMailerAnalyticsOptions): {
|
|
19
|
+
blobs: string[];
|
|
20
|
+
doubles: number[];
|
|
21
|
+
indexes: string[];
|
|
22
|
+
};
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=analytics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../../src/utils/analytics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAE1D,MAAM,MAAM,oBAAoB,GAC7B,uBAAuB,GACvB,sBAAsB,GACtB,yBAAyB,GACzB,mBAAmB,GACnB,oBAAoB,GACpB,sBAAsB,GACtB,oBAAoB,GACpB,uBAAuB,GACvB,iBAAiB,GACjB,wBAAwB,CAAA;AAM3B,UAAU,gBAAgB;IACzB,MAAM,CAAC,EAAE;QACR,SAAS,CAAC,EAAE;YACX,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAA;SAC1C,CAAA;KACD,CAAA;CACD;AAED,UAAU,0BAA0B;IACnC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,OAAO,CAAC,EAAE,gBAAgB,CAAA;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,wBAAwB,CACvC,OAAO,EAAE,qBAAqB,EAC9B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,SAAS,EAAE,oBAAoB,EAC/B,WAAW,GAAE,0BAA+B,GAC1C,OAAO,CAeT;AAED,wBAAgB,6BAA6B,CAC5C,OAAO,EAAE,qBAAqB,EAC9B,SAAS,EAAE,oBAAoB,EAC/B,WAAW,GAAE,0BAA+B,GAC1C;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CA+B3D"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export function emitMailerAnalyticsEvent(options, bindingsEnv, eventName, emitOptions = {}) {
|
|
2
|
+
if (!options.analyticsEnabled)
|
|
3
|
+
return false;
|
|
4
|
+
const analyticsBinding = bindingsEnv[options.analyticsBinding];
|
|
5
|
+
if (!analyticsBinding?.writeDataPoint)
|
|
6
|
+
return false;
|
|
7
|
+
const dataPoint = buildMailerAnalyticsDataPoint(options, eventName, emitOptions);
|
|
8
|
+
const write = analyticsBinding.writeDataPoint(dataPoint);
|
|
9
|
+
const waitUntil = emitOptions.context?.locals?.cfContext?.waitUntil;
|
|
10
|
+
if (waitUntil) {
|
|
11
|
+
waitUntil(write.catch(() => undefined));
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
void write.catch(() => undefined);
|
|
15
|
+
}
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
export function buildMailerAnalyticsDataPoint(options, eventName, emitOptions = {}) {
|
|
19
|
+
const url = emitOptions.request ? new URL(emitOptions.request.url) : new URL(options.siteUrl);
|
|
20
|
+
const siteId = siteIdFromUrl(options.siteUrl);
|
|
21
|
+
const label = emitOptions.label ? JSON.stringify(emitOptions.label) : '';
|
|
22
|
+
return {
|
|
23
|
+
blobs: [
|
|
24
|
+
eventName,
|
|
25
|
+
siteId,
|
|
26
|
+
'',
|
|
27
|
+
'',
|
|
28
|
+
url.toString(),
|
|
29
|
+
url.pathname,
|
|
30
|
+
emitOptions.request?.headers.get('referer') ?? '',
|
|
31
|
+
'',
|
|
32
|
+
'',
|
|
33
|
+
'',
|
|
34
|
+
'',
|
|
35
|
+
emitOptions.request?.headers.get('cf-ipcountry') ?? '',
|
|
36
|
+
'',
|
|
37
|
+
'',
|
|
38
|
+
'',
|
|
39
|
+
emitOptions.contentSlug ?? '',
|
|
40
|
+
'newsletter',
|
|
41
|
+
categoryForMailerEvent(eventName),
|
|
42
|
+
label,
|
|
43
|
+
'false',
|
|
44
|
+
],
|
|
45
|
+
doubles: [Date.now(), emitOptions.eventValue ?? 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
46
|
+
indexes: [eventName],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function siteIdFromUrl(siteUrl) {
|
|
50
|
+
try {
|
|
51
|
+
return new URL(siteUrl).hostname.replace(/^www\./, '');
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return siteUrl;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function categoryForMailerEvent(eventName) {
|
|
58
|
+
switch (eventName) {
|
|
59
|
+
case 'newsletter_subscribed':
|
|
60
|
+
case 'newsletter_confirmed':
|
|
61
|
+
return 'conversion';
|
|
62
|
+
case 'newsletter_opened':
|
|
63
|
+
case 'newsletter_clicked':
|
|
64
|
+
return 'interaction';
|
|
65
|
+
case 'newsletter_delivered':
|
|
66
|
+
case 'newsletter_bounced':
|
|
67
|
+
case 'newsletter_complained':
|
|
68
|
+
case 'newsletter_sent':
|
|
69
|
+
case 'newsletter_send_failed':
|
|
70
|
+
case 'newsletter_unsubscribed':
|
|
71
|
+
return 'newsletter';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=analytics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analytics.js","sourceRoot":"","sources":["../../src/utils/analytics.ts"],"names":[],"mappings":"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"}
|
package/dist/utils/bindings.d.ts
CHANGED
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
import { drizzle } from 'drizzle-orm/d1';
|
|
2
2
|
type DrizzleDB = ReturnType<typeof drizzle>;
|
|
3
|
-
export interface RuntimeLocals {
|
|
4
|
-
runtime?: {
|
|
5
|
-
env: Record<string, unknown>;
|
|
6
|
-
};
|
|
7
|
-
}
|
|
8
3
|
/**
|
|
9
|
-
* Resolve Cloudflare bindings from the
|
|
4
|
+
* Resolve Cloudflare bindings from the Astro 6 Cloudflare Workers env object.
|
|
10
5
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* the virtual config so they stay in sync with the consumer's `astro.config`.
|
|
6
|
+
* Binding names come from the virtual config so they stay in sync with the
|
|
7
|
+
* consumer's `astro.config`.
|
|
14
8
|
*/
|
|
15
|
-
export declare function resolveBindings(
|
|
9
|
+
export declare function resolveBindings(bindingsEnv?: Record<string, unknown>): {
|
|
16
10
|
db: DrizzleDB;
|
|
17
11
|
queue: Queue;
|
|
18
12
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bindings.d.ts","sourceRoot":"","sources":["../../src/utils/bindings.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"bindings.d.ts","sourceRoot":"","sources":["../../src/utils/bindings.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AAExC,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAA;AAE3C;;;;;GAKG;AACH,wBAAgB,eAAe,CAC9B,WAAW,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAA4C,GAC7E;IACF,EAAE,EAAE,SAAS,CAAA;IACb,KAAK,EAAE,KAAK,CAAA;CACZ,CAQA"}
|
package/dist/utils/bindings.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
+
import { env as cloudflareEnv } from 'cloudflare:workers';
|
|
1
2
|
import { config } from 'virtual:growth-labs/mailer/config';
|
|
2
3
|
import { drizzle } from 'drizzle-orm/d1';
|
|
3
4
|
/**
|
|
4
|
-
* Resolve Cloudflare bindings from the
|
|
5
|
+
* Resolve Cloudflare bindings from the Astro 6 Cloudflare Workers env object.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* the virtual config so they stay in sync with the consumer's `astro.config`.
|
|
7
|
+
* Binding names come from the virtual config so they stay in sync with the
|
|
8
|
+
* consumer's `astro.config`.
|
|
9
9
|
*/
|
|
10
|
-
export function resolveBindings(
|
|
11
|
-
const d1 =
|
|
12
|
-
const queue =
|
|
10
|
+
export function resolveBindings(bindingsEnv = cloudflareEnv) {
|
|
11
|
+
const d1 = bindingsEnv[config.d1Binding];
|
|
12
|
+
const queue = bindingsEnv[config.queueBinding];
|
|
13
13
|
if (!d1)
|
|
14
14
|
throw new Error(`[mailer] D1 binding "${config.d1Binding}" not found`);
|
|
15
15
|
if (!queue)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bindings.js","sourceRoot":"","sources":["../../src/utils/bindings.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAA;AAC1D,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"bindings.js","sourceRoot":"","sources":["../../src/utils/bindings.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,IAAI,aAAa,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAA;AAC1D,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AAIxC;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAC9B,cAAuC,aAAwC;IAK/E,MAAM,EAAE,GAAG,WAAW,CAAC,MAAM,CAAC,SAAS,CAAe,CAAA;IACtD,MAAM,KAAK,GAAG,WAAW,CAAC,MAAM,CAAC,YAAY,CAAU,CAAA;IAEvD,IAAI,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,MAAM,CAAC,SAAS,aAAa,CAAC,CAAA;IAC/E,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,2BAA2B,MAAM,CAAC,YAAY,aAAa,CAAC,CAAA;IAExF,OAAO,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,CAAA;AAClC,CAAC"}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
export { buildMailerAnalyticsDataPoint, emitMailerAnalyticsEvent, type MailerAnalyticsEvent, } from './analytics.js';
|
|
1
2
|
export { handleBounce, handleComplaint, handleDelivery, updateSendStatus, } from './bounce.js';
|
|
2
3
|
export type { CloudflareEmailSender } from './providers.js';
|
|
3
|
-
export { CloudflareEmailProvider, getProvider, sleep
|
|
4
|
+
export { CloudflareEmailProvider, getProvider, sleep } from './providers.js';
|
|
4
5
|
export type { CampaignSchedule, DigestSchedule } from './scheduling.js';
|
|
5
6
|
export { executeCampaignSchedule, executeDigestSchedule, prepareCampaign, prepareDigest, sendBatchCampaigns, } from './scheduling.js';
|
|
6
7
|
export type { MailerEnv } from './send.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,YAAY,EACZ,eAAe,EACf,cAAc,EACd,gBAAgB,GAChB,MAAM,aAAa,CAAA;AACpB,YAAY,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AAC3D,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,6BAA6B,EAC7B,wBAAwB,EACxB,KAAK,oBAAoB,GACzB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EACN,YAAY,EACZ,eAAe,EACf,cAAc,EACd,gBAAgB,GAChB,MAAM,aAAa,CAAA;AACpB,YAAY,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AAC3D,OAAO,EAAE,uBAAuB,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAC5E,YAAY,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACvE,OAAO,EACN,uBAAuB,EACvB,qBAAqB,EACrB,eAAe,EACf,aAAa,EACb,kBAAkB,GAClB,MAAM,iBAAiB,CAAA;AACxB,YAAY,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAC1C,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AACvE,OAAO,EACN,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,qBAAqB,EACrB,iBAAiB,GACjB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACN,YAAY,EACZ,WAAW,EACX,mBAAmB,EACnB,iBAAiB,EACjB,WAAW,GACX,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACxD,OAAO,EACN,mBAAmB,EACnB,uBAAuB,EACvB,eAAe,GACf,MAAM,eAAe,CAAA"}
|
package/dist/utils/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
export { buildMailerAnalyticsDataPoint, emitMailerAnalyticsEvent, } from './analytics.js';
|
|
1
2
|
export { handleBounce, handleComplaint, handleDelivery, updateSendStatus, } from './bounce.js';
|
|
2
|
-
export { CloudflareEmailProvider, getProvider, sleep
|
|
3
|
+
export { CloudflareEmailProvider, getProvider, sleep } from './providers.js';
|
|
3
4
|
export { executeCampaignSchedule, executeDigestSchedule, prepareCampaign, prepareDigest, sendBatchCampaigns, } from './scheduling.js';
|
|
4
5
|
export { sendCampaign, sendDigest, sendTransactional } from './send.js';
|
|
5
6
|
export { confirmSubscriber, countSubscribers, createSubscriber, getSubscriberBatch, getSubscriberByEmail, getSubscriberById, unsubscribeSubscriber, updatePreferences, } from './subscribers.js';
|
package/dist/utils/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,YAAY,EACZ,eAAe,EACf,cAAc,EACd,gBAAgB,GAChB,MAAM,aAAa,CAAA;AAEpB,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,6BAA6B,EAC7B,wBAAwB,GAExB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EACN,YAAY,EACZ,eAAe,EACf,cAAc,EACd,gBAAgB,GAChB,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,uBAAuB,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAE5E,OAAO,EACN,uBAAuB,EACvB,qBAAqB,EACrB,eAAe,EACf,aAAa,EACb,kBAAkB,GAClB,MAAM,iBAAiB,CAAA;AAExB,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AACvE,OAAO,EACN,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,qBAAqB,EACrB,iBAAiB,GACjB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACN,YAAY,EACZ,WAAW,EACX,mBAAmB,EACnB,iBAAiB,EACjB,WAAW,GACX,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACxD,OAAO,EACN,mBAAmB,EACnB,uBAAuB,EACvB,eAAe,GACf,MAAM,eAAe,CAAA"}
|
package/dist/utils/providers.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"providers.js","sourceRoot":"","sources":["../../src/utils/providers.ts"],"names":[],"mappings":"AAaA,kCAAkC;AAElC,MAAM,OAAO,uBAAuB;IAC1B,IAAI,GAAG,YAAY,CAAA;IACX,MAAM,CAAuB;IAE9C,YAAY,WAAkC;QAC7C,IAAI,CAAC,MAAM,GAAG,WAAW,CAAA;IAC1B,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,KAAoB;QAC9B,IAAI,CAAC;YACJ,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;gBACtB,EAAE,EAAE,KAAK,CAAC,EAAE;gBACZ,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;aACnD,CAAC,CAAA;YAEF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,CAAA;QAC3C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;YAChE,MAAM,WAAW,GAAG,sBAAsB,CAAC,OAAO,CAAC,CAAA;YAEnD,OAAO;gBACN,OAAO,EAAE,KAAK;gBACd,KAAK,EAAE,OAAO;gBACd,SAAS,EAAE,WAAW;aACtB,CAAA;QACF,CAAC;IACF,CAAC;CACD;AAED,
|
|
1
|
+
{"version":3,"file":"providers.js","sourceRoot":"","sources":["../../src/utils/providers.ts"],"names":[],"mappings":"AAaA,kCAAkC;AAElC,MAAM,OAAO,uBAAuB;IAC1B,IAAI,GAAG,YAAY,CAAA;IACX,MAAM,CAAuB;IAE9C,YAAY,WAAkC;QAC7C,IAAI,CAAC,MAAM,GAAG,WAAW,CAAA;IAC1B,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,KAAoB;QAC9B,IAAI,CAAC;YACJ,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;gBACtB,EAAE,EAAE,KAAK,CAAC,EAAE;gBACZ,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;aACnD,CAAC,CAAA;YAEF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,CAAA;QAC3C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;YAChE,MAAM,WAAW,GAAG,sBAAsB,CAAC,OAAO,CAAC,CAAA;YAEnD,OAAO;gBACN,OAAO,EAAE,KAAK;gBACd,KAAK,EAAE,OAAO;gBACd,SAAS,EAAE,WAAW;aACtB,CAAA;QACF,CAAC;IACF,CAAC;CACD;AAED,2BAA2B;AAE3B,MAAM,UAAU,WAAW,CAAC,WAAkC;IAC7D,OAAO,IAAI,uBAAuB,CAAC,WAAW,CAAC,CAAA;AAChD,CAAC;AAED,kBAAkB;AAElB,MAAM,UAAU,KAAK,CAAC,EAAU;IAC/B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAA;AACzD,CAAC;AAED,SAAS,sBAAsB,CAAC,OAAe;IAC9C,MAAM,QAAQ,GAAG;QAChB,SAAS;QACT,SAAS;QACT,cAAc;QACd,YAAY;QACZ,WAAW;QACX,QAAQ;QACR,GAAG;QACH,cAAc;QACd,qBAAqB;QACrB,iBAAiB;KACjB,CAAA;IACD,MAAM,KAAK,GAAG,OAAO,CAAC,WAAW,EAAE,CAAA;IACnC,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;AAC/C,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@growth-labs/mailer",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"types": "./dist/index.d.ts",
|
|
5
6
|
"exports": {
|
|
6
7
|
".": {
|
|
7
8
|
"types": "./dist/index.d.ts",
|
|
@@ -52,6 +53,10 @@
|
|
|
52
53
|
"src",
|
|
53
54
|
"README.md"
|
|
54
55
|
],
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public",
|
|
58
|
+
"registry": "https://registry.npmjs.org/"
|
|
59
|
+
},
|
|
55
60
|
"peerDependencies": {
|
|
56
61
|
"astro": "^6.0.0"
|
|
57
62
|
},
|
|
@@ -75,6 +80,7 @@
|
|
|
75
80
|
},
|
|
76
81
|
"scripts": {
|
|
77
82
|
"build": "tsc",
|
|
83
|
+
"check": "biome check src/",
|
|
78
84
|
"test": "vitest run",
|
|
79
85
|
"test:watch": "vitest"
|
|
80
86
|
}
|
package/src/options.ts
CHANGED
|
@@ -50,6 +50,7 @@ export const mailerOptionsSchema = z.object({
|
|
|
50
50
|
|
|
51
51
|
// ─── Optional peer: analytics ───
|
|
52
52
|
analyticsEnabled: z.boolean().default(false),
|
|
53
|
+
analyticsBinding: z.string().default('ANALYTICS'),
|
|
53
54
|
})
|
|
54
55
|
|
|
55
56
|
export type MailerOptions = z.input<typeof mailerOptionsSchema>
|
package/src/queue/consumer.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { drizzle } from 'drizzle-orm/d1'
|
|
2
2
|
import type { ResolvedMailerOptions } from '../options.js'
|
|
3
3
|
import type { EmailProvider, EmailQueueMessage } from '../types.js'
|
|
4
|
+
import { emitMailerAnalyticsEvent } from '../utils/analytics.js'
|
|
4
5
|
import { updateSendStatus } from '../utils/bounce.js'
|
|
5
6
|
import type { CloudflareEmailSender } from '../utils/providers.js'
|
|
6
7
|
import { CloudflareEmailProvider, sleep } from '../utils/providers.js'
|
|
7
8
|
|
|
8
9
|
export async function handleEmailQueue(
|
|
9
10
|
batch: MessageBatch<EmailQueueMessage>,
|
|
10
|
-
env: {
|
|
11
|
+
env: {
|
|
12
|
+
DB: D1Database
|
|
13
|
+
EMAIL_SENDER?: CloudflareEmailSender
|
|
14
|
+
[key: string]: unknown
|
|
15
|
+
},
|
|
11
16
|
options: ResolvedMailerOptions,
|
|
12
17
|
): Promise<void> {
|
|
13
18
|
const db = drizzle(env.DB)
|
|
@@ -68,11 +73,32 @@ export async function handleEmailQueue(
|
|
|
68
73
|
await updateSendStatus(db, recipient.trackingId, 'sent', {
|
|
69
74
|
sentAt: new Date().toISOString(),
|
|
70
75
|
})
|
|
76
|
+
emitMailerAnalyticsEvent(options, env, 'newsletter_sent', {
|
|
77
|
+
contentSlug: recipient.trackingId,
|
|
78
|
+
label: {
|
|
79
|
+
trackingId: recipient.trackingId,
|
|
80
|
+
subscriberId: recipient.subscriberId,
|
|
81
|
+
email: recipient.email,
|
|
82
|
+
campaignId: message.body.campaignId,
|
|
83
|
+
type,
|
|
84
|
+
},
|
|
85
|
+
})
|
|
71
86
|
} else {
|
|
72
87
|
await updateSendStatus(db, recipient.trackingId, 'bounced', {
|
|
73
88
|
bouncedAt: new Date().toISOString(),
|
|
74
89
|
bounceType: 'hard',
|
|
75
90
|
})
|
|
91
|
+
emitMailerAnalyticsEvent(options, env, 'newsletter_send_failed', {
|
|
92
|
+
contentSlug: recipient.trackingId,
|
|
93
|
+
label: {
|
|
94
|
+
trackingId: recipient.trackingId,
|
|
95
|
+
subscriberId: recipient.subscriberId,
|
|
96
|
+
email: recipient.email,
|
|
97
|
+
campaignId: message.body.campaignId,
|
|
98
|
+
type,
|
|
99
|
+
error: result.error,
|
|
100
|
+
},
|
|
101
|
+
})
|
|
76
102
|
console.error(`[mailer] Send failed for ${recipient.email}: ${result.error}`)
|
|
77
103
|
}
|
|
78
104
|
}
|
package/src/routes/confirm.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import { env as cloudflareEnv } from 'cloudflare:workers'
|
|
1
2
|
import { config } from 'virtual:growth-labs/mailer/config'
|
|
2
3
|
import type { APIRoute } from 'astro'
|
|
3
4
|
import { drizzle } from 'drizzle-orm/d1'
|
|
4
|
-
import
|
|
5
|
+
import { emitMailerAnalyticsEvent } from '../utils/analytics.js'
|
|
5
6
|
import type { MailerEnv } from '../utils/send.js'
|
|
6
7
|
import { sendTransactional } from '../utils/send.js'
|
|
7
8
|
import { confirmSubscriber, getSubscriberById } from '../utils/subscribers.js'
|
|
8
9
|
import { verifyToken } from '../utils/tokens.js'
|
|
9
10
|
|
|
10
|
-
export const GET: APIRoute = async (
|
|
11
|
+
export const GET: APIRoute = async (context) => {
|
|
12
|
+
const { url } = context
|
|
11
13
|
const token = url.searchParams.get('token')
|
|
12
14
|
if (!token) {
|
|
13
15
|
return Response.json({ error: 'Missing token' }, { status: 400 })
|
|
@@ -20,9 +22,9 @@ export const GET: APIRoute = async ({ url, locals }) => {
|
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
// Resolve bindings
|
|
23
|
-
const
|
|
24
|
-
const d1 =
|
|
25
|
-
const queue =
|
|
25
|
+
const bindingsEnv = cloudflareEnv as Record<string, unknown>
|
|
26
|
+
const d1 = bindingsEnv[config.d1Binding] as D1Database
|
|
27
|
+
const queue = bindingsEnv[config.queueBinding] as Queue
|
|
26
28
|
const db = drizzle(d1)
|
|
27
29
|
const env: MailerEnv = { DB: d1, QUEUE: queue }
|
|
28
30
|
|
|
@@ -58,6 +60,15 @@ export const GET: APIRoute = async ({ url, locals }) => {
|
|
|
58
60
|
data: { name: subscriber.name },
|
|
59
61
|
})
|
|
60
62
|
|
|
63
|
+
emitMailerAnalyticsEvent(config, bindingsEnv, 'newsletter_confirmed', {
|
|
64
|
+
context,
|
|
65
|
+
contentSlug: subscriber.id,
|
|
66
|
+
label: {
|
|
67
|
+
subscriberId: subscriber.id,
|
|
68
|
+
email: subscriber.email,
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
|
|
61
72
|
// Redirect to site with confirmed flag
|
|
62
73
|
return new Response(null, {
|
|
63
74
|
status: 302,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { config } from "virtual:growth-labs/mailer/config";
|
|
3
|
+
import { env as cloudflareEnv } from "cloudflare:workers";
|
|
3
4
|
import { drizzle } from "drizzle-orm/d1";
|
|
4
5
|
import PreferenceCenter from "../components/PreferenceCenter.astro";
|
|
5
6
|
import {
|
|
@@ -50,16 +51,11 @@ if (Astro.request.method === "POST" && payload.action !== "preferences") {
|
|
|
50
51
|
return new Response("Invalid or expired token", { status: 400 });
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
).runtime?.env;
|
|
58
|
-
if (!runtimeEnv) {
|
|
59
|
-
return new Response("Runtime not available", { status: 500 });
|
|
54
|
+
const bindingsEnv = cloudflareEnv as Record<string, unknown>;
|
|
55
|
+
const d1 = bindingsEnv[config.d1Binding] as D1Database;
|
|
56
|
+
if (!d1) {
|
|
57
|
+
return new Response("D1 binding not available", { status: 500 });
|
|
60
58
|
}
|
|
61
|
-
|
|
62
|
-
const d1 = runtimeEnv[config.d1Binding] as D1Database;
|
|
63
59
|
const db = drizzle(d1);
|
|
64
60
|
|
|
65
61
|
// Handle POST (form submission)
|
package/src/routes/subscribe.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import { env as cloudflareEnv } from 'cloudflare:workers'
|
|
1
2
|
import { config } from 'virtual:growth-labs/mailer/config'
|
|
2
3
|
import type { APIRoute } from 'astro'
|
|
3
4
|
import { drizzle } from 'drizzle-orm/d1'
|
|
4
|
-
import
|
|
5
|
+
import { emitMailerAnalyticsEvent } from '../utils/analytics.js'
|
|
5
6
|
import type { MailerEnv } from '../utils/send.js'
|
|
6
7
|
import { sendTransactional } from '../utils/send.js'
|
|
7
8
|
import { createSubscriber } from '../utils/subscribers.js'
|
|
8
9
|
import { generateToken } from '../utils/tokens.js'
|
|
9
10
|
|
|
10
|
-
export const POST: APIRoute = async (
|
|
11
|
+
export const POST: APIRoute = async (context) => {
|
|
12
|
+
const { request } = context
|
|
11
13
|
// 1. Parse request body
|
|
12
14
|
const body = (await request.json()) as {
|
|
13
15
|
email?: string
|
|
@@ -48,9 +50,9 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
// 5. Resolve bindings
|
|
51
|
-
const
|
|
52
|
-
const d1 =
|
|
53
|
-
const queue =
|
|
53
|
+
const bindingsEnv = cloudflareEnv as Record<string, unknown>
|
|
54
|
+
const d1 = bindingsEnv[config.d1Binding] as D1Database
|
|
55
|
+
const queue = bindingsEnv[config.queueBinding] as Queue
|
|
54
56
|
const db = drizzle(d1)
|
|
55
57
|
const env: MailerEnv = { DB: d1, QUEUE: queue }
|
|
56
58
|
|
|
@@ -92,6 +94,20 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
92
94
|
})
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
emitMailerAnalyticsEvent(config, bindingsEnv, 'newsletter_subscribed', {
|
|
98
|
+
request,
|
|
99
|
+
context,
|
|
100
|
+
contentSlug: subscriber.id,
|
|
101
|
+
label: {
|
|
102
|
+
subscriberId: subscriber.id,
|
|
103
|
+
email: subscriber.email,
|
|
104
|
+
source: body.source ?? 'form',
|
|
105
|
+
preferences: body.preferences ?? [],
|
|
106
|
+
requiresConfirmation: config.doubleOptIn,
|
|
107
|
+
isNew,
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
|
|
95
111
|
// Return success (always 200 to avoid email enumeration)
|
|
96
112
|
return Response.json({
|
|
97
113
|
success: true,
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { env as cloudflareEnv } from 'cloudflare:workers'
|
|
1
2
|
import { config } from 'virtual:growth-labs/mailer/config'
|
|
2
3
|
import type { APIRoute } from 'astro'
|
|
3
4
|
import { and, eq, inArray } from 'drizzle-orm'
|
|
4
5
|
import { drizzle } from 'drizzle-orm/d1'
|
|
5
6
|
import { emailSends } from '../schema.js'
|
|
7
|
+
import { emitMailerAnalyticsEvent } from '../utils/analytics.js'
|
|
6
8
|
|
|
7
|
-
export const GET: APIRoute = async (
|
|
9
|
+
export const GET: APIRoute = async (context) => {
|
|
10
|
+
const { params, request, url } = context
|
|
8
11
|
const trackingId = params.trackingId
|
|
9
12
|
const destination = url.searchParams.get('url')
|
|
10
13
|
|
|
@@ -24,13 +27,9 @@ export const GET: APIRoute = async ({ params, url, locals }) => {
|
|
|
24
27
|
|
|
25
28
|
// Update status to 'clicked' only when it hasn't already reached 'clicked'
|
|
26
29
|
try {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
).runtime?.env
|
|
32
|
-
if (runtimeEnv) {
|
|
33
|
-
const d1 = runtimeEnv[config.d1Binding] as D1Database
|
|
30
|
+
const bindingsEnv = cloudflareEnv as Record<string, unknown>
|
|
31
|
+
if (bindingsEnv) {
|
|
32
|
+
const d1 = bindingsEnv[config.d1Binding] as D1Database
|
|
34
33
|
const db = drizzle(d1)
|
|
35
34
|
await db
|
|
36
35
|
.update(emailSends)
|
|
@@ -49,6 +48,13 @@ export const GET: APIRoute = async ({ params, url, locals }) => {
|
|
|
49
48
|
// Never fail the redirect on DB errors
|
|
50
49
|
}
|
|
51
50
|
|
|
51
|
+
emitMailerAnalyticsEvent(config, cloudflareEnv as Record<string, unknown>, 'newsletter_clicked', {
|
|
52
|
+
request,
|
|
53
|
+
context,
|
|
54
|
+
contentSlug: trackingId,
|
|
55
|
+
label: { trackingId, destination },
|
|
56
|
+
})
|
|
57
|
+
|
|
52
58
|
// 302 redirect to original destination
|
|
53
59
|
return new Response(null, {
|
|
54
60
|
status: 302,
|
package/src/routes/track-open.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { env as cloudflareEnv } from 'cloudflare:workers'
|
|
1
2
|
import { config } from 'virtual:growth-labs/mailer/config'
|
|
2
3
|
import type { APIRoute } from 'astro'
|
|
3
4
|
import { and, eq, inArray } from 'drizzle-orm'
|
|
4
5
|
import { drizzle } from 'drizzle-orm/d1'
|
|
5
6
|
import { emailSends } from '../schema.js'
|
|
7
|
+
import { emitMailerAnalyticsEvent } from '../utils/analytics.js'
|
|
6
8
|
import { TRANSPARENT_GIF } from '../utils/tracking.js'
|
|
7
9
|
|
|
8
|
-
export const GET: APIRoute = async (
|
|
10
|
+
export const GET: APIRoute = async (context) => {
|
|
11
|
+
const { params, request } = context
|
|
9
12
|
const trackingId = params.trackingId
|
|
10
13
|
if (!trackingId) {
|
|
11
14
|
return new Response(null, { status: 400 })
|
|
@@ -14,13 +17,9 @@ export const GET: APIRoute = async ({ params, locals }) => {
|
|
|
14
17
|
// Update status to 'opened' only when currently 'sent' or 'delivered'
|
|
15
18
|
// to avoid downgrading from 'clicked'.
|
|
16
19
|
try {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
).runtime?.env
|
|
22
|
-
if (runtimeEnv) {
|
|
23
|
-
const d1 = runtimeEnv[config.d1Binding] as D1Database
|
|
20
|
+
const bindingsEnv = cloudflareEnv as Record<string, unknown>
|
|
21
|
+
if (bindingsEnv) {
|
|
22
|
+
const d1 = bindingsEnv[config.d1Binding] as D1Database
|
|
24
23
|
const db = drizzle(d1)
|
|
25
24
|
await db
|
|
26
25
|
.update(emailSends)
|
|
@@ -39,6 +38,13 @@ export const GET: APIRoute = async ({ params, locals }) => {
|
|
|
39
38
|
// Never fail the pixel response on DB errors
|
|
40
39
|
}
|
|
41
40
|
|
|
41
|
+
emitMailerAnalyticsEvent(config, cloudflareEnv as Record<string, unknown>, 'newsletter_opened', {
|
|
42
|
+
request,
|
|
43
|
+
context,
|
|
44
|
+
contentSlug: trackingId,
|
|
45
|
+
label: { trackingId },
|
|
46
|
+
})
|
|
47
|
+
|
|
42
48
|
// 1x1 transparent GIF
|
|
43
49
|
return new Response(TRANSPARENT_GIF, {
|
|
44
50
|
status: 200,
|
|
@@ -1,14 +1,19 @@
|
|
|
1
|
+
import { env as cloudflareEnv } from 'cloudflare:workers'
|
|
1
2
|
import { config } from 'virtual:growth-labs/mailer/config'
|
|
2
3
|
import type { APIRoute } from 'astro'
|
|
3
4
|
import { drizzle } from 'drizzle-orm/d1'
|
|
4
|
-
import
|
|
5
|
+
import { emitMailerAnalyticsEvent } from '../utils/analytics.js'
|
|
5
6
|
import type { MailerEnv } from '../utils/send.js'
|
|
6
7
|
import { sendTransactional } from '../utils/send.js'
|
|
7
8
|
import { getSubscriberById, unsubscribeSubscriber } from '../utils/subscribers.js'
|
|
8
9
|
import { generateToken, verifyToken } from '../utils/tokens.js'
|
|
9
10
|
import { buildSiteUrl } from '../utils/urls.js'
|
|
10
11
|
|
|
11
|
-
async function processUnsubscribe(
|
|
12
|
+
async function processUnsubscribe(
|
|
13
|
+
token: string,
|
|
14
|
+
context?: Parameters<APIRoute>[0],
|
|
15
|
+
request?: Request,
|
|
16
|
+
) {
|
|
12
17
|
// Verify token
|
|
13
18
|
const payload = await verifyToken(config.signingSecret, token)
|
|
14
19
|
if (!payload || payload.action !== 'unsubscribe') {
|
|
@@ -16,9 +21,9 @@ async function processUnsubscribe(locals: RuntimeLocals, token: string) {
|
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
// Resolve bindings
|
|
19
|
-
const
|
|
20
|
-
const d1 =
|
|
21
|
-
const queue =
|
|
24
|
+
const bindingsEnv = cloudflareEnv as Record<string, unknown>
|
|
25
|
+
const d1 = bindingsEnv[config.d1Binding] as D1Database
|
|
26
|
+
const queue = bindingsEnv[config.queueBinding] as Queue
|
|
22
27
|
const db = drizzle(d1)
|
|
23
28
|
const env: MailerEnv = { DB: d1, QUEUE: queue }
|
|
24
29
|
|
|
@@ -43,6 +48,16 @@ async function processUnsubscribe(locals: RuntimeLocals, token: string) {
|
|
|
43
48
|
},
|
|
44
49
|
})
|
|
45
50
|
|
|
51
|
+
emitMailerAnalyticsEvent(config, bindingsEnv, 'newsletter_unsubscribed', {
|
|
52
|
+
request,
|
|
53
|
+
context,
|
|
54
|
+
contentSlug: subscriber.id,
|
|
55
|
+
label: {
|
|
56
|
+
subscriberId: subscriber.id,
|
|
57
|
+
email: subscriber.email,
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
|
|
46
61
|
return {
|
|
47
62
|
success: true,
|
|
48
63
|
subscriberId: payload.subscriberId,
|
|
@@ -50,13 +65,14 @@ async function processUnsubscribe(locals: RuntimeLocals, token: string) {
|
|
|
50
65
|
}
|
|
51
66
|
|
|
52
67
|
// GET: Link from email footer
|
|
53
|
-
export const GET: APIRoute = async (
|
|
68
|
+
export const GET: APIRoute = async (context) => {
|
|
69
|
+
const { url, request } = context
|
|
54
70
|
const token = url.searchParams.get('token')
|
|
55
71
|
if (!token) {
|
|
56
72
|
return Response.json({ error: 'Missing token' }, { status: 400 })
|
|
57
73
|
}
|
|
58
74
|
|
|
59
|
-
const result = await processUnsubscribe(
|
|
75
|
+
const result = await processUnsubscribe(token, context, request)
|
|
60
76
|
if ('error' in result) {
|
|
61
77
|
return Response.json({ error: result.error }, { status: result.status })
|
|
62
78
|
}
|
|
@@ -78,7 +94,8 @@ export const GET: APIRoute = async ({ url, locals }) => {
|
|
|
78
94
|
}
|
|
79
95
|
|
|
80
96
|
// POST: RFC 8058 List-Unsubscribe-Post
|
|
81
|
-
export const POST: APIRoute = async (
|
|
97
|
+
export const POST: APIRoute = async (context) => {
|
|
98
|
+
const { request } = context
|
|
82
99
|
// RFC 8058: body is "List-Unsubscribe=One-Click"
|
|
83
100
|
// Token comes from List-Unsubscribe header URL
|
|
84
101
|
const url = new URL(request.url)
|
|
@@ -87,7 +104,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
87
104
|
return new Response('Missing token', { status: 400 })
|
|
88
105
|
}
|
|
89
106
|
|
|
90
|
-
const result = await processUnsubscribe(
|
|
107
|
+
const result = await processUnsubscribe(token, context, request)
|
|
91
108
|
if ('error' in result) {
|
|
92
109
|
return new Response(result.error, { status: result.status })
|
|
93
110
|
}
|