@soulbatical/tetra-core 0.1.39 → 0.1.40
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.
Potentially problematic release.
This version of @soulbatical/tetra-core might be problematic. Click here for more details.
- package/dist/core/dualWriteProxy.d.ts +11 -0
- package/dist/core/dualWriteProxy.d.ts.map +1 -1
- package/dist/core/dualWriteProxy.js +142 -198
- package/dist/core/dualWriteProxy.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/shared/email/EmailService.d.ts +9 -1
- package/dist/shared/email/EmailService.d.ts.map +1 -1
- package/dist/shared/email/EmailService.js +83 -7
- package/dist/shared/email/EmailService.js.map +1 -1
- package/dist/shared/email/mailgun.d.ts +4 -1
- package/dist/shared/email/mailgun.d.ts.map +1 -1
- package/dist/shared/email/mailgun.js +41 -10
- package/dist/shared/email/mailgun.js.map +1 -1
- package/dist/shared/email/smtp.d.ts +4 -1
- package/dist/shared/email/smtp.d.ts.map +1 -1
- package/dist/shared/email/smtp.js +14 -2
- package/dist/shared/email/smtp.js.map +1 -1
- package/dist/shared/email/types.d.ts +23 -1
- package/dist/shared/email/types.d.ts.map +1 -1
- package/dist/shared/email/webhookRoutes.d.ts +29 -0
- package/dist/shared/email/webhookRoutes.d.ts.map +1 -0
- package/dist/shared/email/webhookRoutes.js +125 -0
- package/dist/shared/email/webhookRoutes.js.map +1 -0
- package/dist/shared/planner/GoogleCalendarService.d.ts +103 -0
- package/dist/shared/planner/GoogleCalendarService.d.ts.map +1 -0
- package/dist/shared/planner/GoogleCalendarService.js +365 -0
- package/dist/shared/planner/GoogleCalendarService.js.map +1 -0
- package/dist/shared/planner/PlannerService.d.ts +170 -0
- package/dist/shared/planner/PlannerService.d.ts.map +1 -0
- package/dist/shared/planner/PlannerService.js +860 -0
- package/dist/shared/planner/PlannerService.js.map +1 -0
- package/dist/shared/planner/index.d.ts +35 -0
- package/dist/shared/planner/index.d.ts.map +1 -0
- package/dist/shared/planner/index.js +34 -0
- package/dist/shared/planner/index.js.map +1 -0
- package/dist/shared/planner/routes.d.ts +67 -0
- package/dist/shared/planner/routes.d.ts.map +1 -0
- package/dist/shared/planner/routes.js +524 -0
- package/dist/shared/planner/routes.js.map +1 -0
- package/dist/shared/planner/types.d.ts +262 -0
- package/dist/shared/planner/types.d.ts.map +1 -0
- package/dist/shared/planner/types.js +9 -0
- package/dist/shared/planner/types.js.map +1 -0
- package/package.json +1 -1
- package/src/shared/email/migrations/004_add_email_logs_tracking_columns.sql +15 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* EmailService — Core service for sending templated emails.
|
|
3
3
|
*
|
|
4
4
|
* Template lookup with language + org fallback, Handlebars rendering,
|
|
5
|
-
* DB logging, and
|
|
5
|
+
* DB logging, and multi-transport delivery (Gmail, SMTP, Mailgun).
|
|
6
6
|
*
|
|
7
7
|
* @module @soulbatical/tetra-core/email
|
|
8
8
|
*/
|
|
@@ -10,9 +10,11 @@ import Handlebars from 'handlebars';
|
|
|
10
10
|
import { sendMailgunEmail } from './mailgun.js';
|
|
11
11
|
import { sendSmtpEmail } from './smtp.js';
|
|
12
12
|
import { getGmailClient } from './gmail.js';
|
|
13
|
+
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
13
14
|
export class EmailService {
|
|
14
15
|
supabase;
|
|
15
16
|
config;
|
|
17
|
+
templateCache = new Map();
|
|
16
18
|
constructor(supabase, config) {
|
|
17
19
|
this.supabase = supabase;
|
|
18
20
|
this.config = config;
|
|
@@ -36,6 +38,9 @@ export class EmailService {
|
|
|
36
38
|
const html = template
|
|
37
39
|
? this.render(template.body_html, opts.variables)
|
|
38
40
|
: opts.fallbackHtml || `<p>${subject}</p>`;
|
|
41
|
+
const text = template?.body_text
|
|
42
|
+
? this.render(template.body_text, opts.variables)
|
|
43
|
+
: opts.fallbackText || undefined;
|
|
39
44
|
// 2. Create log entry (pending)
|
|
40
45
|
const logId = await this.createLog({
|
|
41
46
|
organizationId: opts.organizationId,
|
|
@@ -44,10 +49,20 @@ export class EmailService {
|
|
|
44
49
|
subject,
|
|
45
50
|
templateSlug: opts.templateSlug,
|
|
46
51
|
variables: opts.variables,
|
|
52
|
+
emailType: opts.emailType,
|
|
53
|
+
metadata: opts.metadata,
|
|
47
54
|
feedbackId: opts.feedbackId,
|
|
48
55
|
});
|
|
49
56
|
// 3. Send via configured transport
|
|
50
|
-
const result = await this.deliver({
|
|
57
|
+
const result = await this.deliver({
|
|
58
|
+
from: this.config.fromAddress,
|
|
59
|
+
to: opts.to,
|
|
60
|
+
subject,
|
|
61
|
+
html,
|
|
62
|
+
text,
|
|
63
|
+
replyTo: opts.replyTo,
|
|
64
|
+
attachments: opts.attachments,
|
|
65
|
+
});
|
|
51
66
|
// 4. Update log
|
|
52
67
|
if (logId) {
|
|
53
68
|
await this.updateLog(logId, {
|
|
@@ -58,6 +73,22 @@ export class EmailService {
|
|
|
58
73
|
}
|
|
59
74
|
return { success: result.success, logId: logId || undefined };
|
|
60
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Clear template cache. Call after template is updated via admin UI.
|
|
78
|
+
*/
|
|
79
|
+
clearCache(slug) {
|
|
80
|
+
if (slug) {
|
|
81
|
+
// Clear all cache keys that start with this slug (all language/org variants)
|
|
82
|
+
for (const key of this.templateCache.keys()) {
|
|
83
|
+
if (key.startsWith(`${slug}:`)) {
|
|
84
|
+
this.templateCache.delete(key);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
this.templateCache.clear();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
61
92
|
/**
|
|
62
93
|
* Resolve transport and send email.
|
|
63
94
|
* Priority: explicit config.transport > auto-detect (gmail > smtp > mailgun).
|
|
@@ -69,7 +100,13 @@ export class EmailService {
|
|
|
69
100
|
}
|
|
70
101
|
if (transport === 'smtp') {
|
|
71
102
|
return sendSmtpEmail({
|
|
72
|
-
|
|
103
|
+
from: params.from,
|
|
104
|
+
to: params.to,
|
|
105
|
+
subject: params.subject,
|
|
106
|
+
html: params.html,
|
|
107
|
+
text: params.text,
|
|
108
|
+
replyTo: params.replyTo,
|
|
109
|
+
attachments: params.attachments,
|
|
73
110
|
smtpHost: this.config.smtpHost,
|
|
74
111
|
smtpPort: this.config.smtpPort ?? 587,
|
|
75
112
|
smtpUser: this.config.smtpUser,
|
|
@@ -78,7 +115,12 @@ export class EmailService {
|
|
|
78
115
|
});
|
|
79
116
|
}
|
|
80
117
|
return sendMailgunEmail({
|
|
81
|
-
|
|
118
|
+
from: params.from,
|
|
119
|
+
to: params.to,
|
|
120
|
+
subject: params.subject,
|
|
121
|
+
html: params.html,
|
|
122
|
+
text: params.text,
|
|
123
|
+
replyTo: params.replyTo,
|
|
82
124
|
apiKey: this.config.mailgunApiKey,
|
|
83
125
|
domain: this.config.mailgunDomain,
|
|
84
126
|
endpoint: this.config.mailgunEndpoint,
|
|
@@ -122,10 +164,15 @@ export class EmailService {
|
|
|
122
164
|
}
|
|
123
165
|
/**
|
|
124
166
|
* Template lookup with language + org fallback chain.
|
|
167
|
+
* Results are cached for 5 minutes.
|
|
125
168
|
*/
|
|
126
169
|
async loadTemplate(slug, language, organizationId) {
|
|
127
170
|
// Try: slug + language + org (if org provided)
|
|
128
171
|
if (organizationId) {
|
|
172
|
+
const cacheKey = `${slug}:${language}:${organizationId}`;
|
|
173
|
+
const cached = this.getCached(cacheKey);
|
|
174
|
+
if (cached !== undefined)
|
|
175
|
+
return cached;
|
|
129
176
|
const { data } = await this.supabase
|
|
130
177
|
.from('email_templates')
|
|
131
178
|
.select('*')
|
|
@@ -135,10 +182,16 @@ export class EmailService {
|
|
|
135
182
|
.eq('is_active', true)
|
|
136
183
|
.limit(1)
|
|
137
184
|
.single();
|
|
138
|
-
if (data)
|
|
185
|
+
if (data) {
|
|
186
|
+
this.setCache(cacheKey, data);
|
|
139
187
|
return data;
|
|
188
|
+
}
|
|
140
189
|
}
|
|
141
190
|
// Try: slug + language + system default (org IS NULL)
|
|
191
|
+
const langKey = `${slug}:${language}:system`;
|
|
192
|
+
const cachedLang = this.getCached(langKey);
|
|
193
|
+
if (cachedLang !== undefined)
|
|
194
|
+
return cachedLang;
|
|
142
195
|
const { data: langDefault } = await this.supabase
|
|
143
196
|
.from('email_templates')
|
|
144
197
|
.select('*')
|
|
@@ -148,10 +201,16 @@ export class EmailService {
|
|
|
148
201
|
.eq('is_active', true)
|
|
149
202
|
.limit(1)
|
|
150
203
|
.single();
|
|
151
|
-
if (langDefault)
|
|
204
|
+
if (langDefault) {
|
|
205
|
+
this.setCache(langKey, langDefault);
|
|
152
206
|
return langDefault;
|
|
207
|
+
}
|
|
153
208
|
// Try: slug + English fallback + system default
|
|
154
209
|
if (language !== 'en') {
|
|
210
|
+
const enKey = `${slug}:en:system`;
|
|
211
|
+
const cachedEn = this.getCached(enKey);
|
|
212
|
+
if (cachedEn !== undefined)
|
|
213
|
+
return cachedEn;
|
|
155
214
|
const { data: enDefault } = await this.supabase
|
|
156
215
|
.from('email_templates')
|
|
157
216
|
.select('*')
|
|
@@ -161,11 +220,26 @@ export class EmailService {
|
|
|
161
220
|
.eq('is_active', true)
|
|
162
221
|
.limit(1)
|
|
163
222
|
.single();
|
|
164
|
-
if (enDefault)
|
|
223
|
+
if (enDefault) {
|
|
224
|
+
this.setCache(enKey, enDefault);
|
|
165
225
|
return enDefault;
|
|
226
|
+
}
|
|
166
227
|
}
|
|
167
228
|
return null;
|
|
168
229
|
}
|
|
230
|
+
getCached(key) {
|
|
231
|
+
const entry = this.templateCache.get(key);
|
|
232
|
+
if (!entry)
|
|
233
|
+
return undefined;
|
|
234
|
+
if (Date.now() - entry.cachedAt > CACHE_TTL) {
|
|
235
|
+
this.templateCache.delete(key);
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
return entry.template;
|
|
239
|
+
}
|
|
240
|
+
setCache(key, template) {
|
|
241
|
+
this.templateCache.set(key, { template, cachedAt: Date.now() });
|
|
242
|
+
}
|
|
169
243
|
/**
|
|
170
244
|
* Render a Handlebars template string with variables.
|
|
171
245
|
* Rejects templates with triple-brace {{{…}}} syntax to prevent escaping bypass.
|
|
@@ -188,6 +262,8 @@ export class EmailService {
|
|
|
188
262
|
subject: opts.subject,
|
|
189
263
|
template_slug: opts.templateSlug || null,
|
|
190
264
|
variables_used: opts.variables || {},
|
|
265
|
+
email_type: opts.emailType || null,
|
|
266
|
+
metadata: opts.metadata || null,
|
|
191
267
|
status: 'pending',
|
|
192
268
|
};
|
|
193
269
|
// feedbackId is app-specific — only include if the column exists
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EmailService.js","sourceRoot":"","sources":["../../../src/shared/email/EmailService.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,UAAU,MAAM,YAAY,CAAC;AAEpC,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"EmailService.js","sourceRoot":"","sources":["../../../src/shared/email/EmailService.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
* Mailgun email helper — native fetch, no axios.
|
|
3
3
|
* Graceful no-op when credentials are missing.
|
|
4
4
|
*/
|
|
5
|
-
import type { MailgunResponse } from './types.js';
|
|
5
|
+
import type { MailgunResponse, EmailAttachment } from './types.js';
|
|
6
6
|
export declare function sendMailgunEmail(params: {
|
|
7
7
|
from: string;
|
|
8
8
|
to: string;
|
|
9
9
|
subject: string;
|
|
10
10
|
html: string;
|
|
11
|
+
text?: string;
|
|
12
|
+
replyTo?: string;
|
|
13
|
+
attachments?: EmailAttachment[];
|
|
11
14
|
apiKey?: string;
|
|
12
15
|
domain?: string;
|
|
13
16
|
endpoint?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mailgun.d.ts","sourceRoot":"","sources":["../../../src/shared/email/mailgun.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"mailgun.d.ts","sourceRoot":"","sources":["../../../src/shared/email/mailgun.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAInE,wBAAsB,gBAAgB,CAAC,MAAM,EAAE;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,eAAe,CAAC,CAyE3B"}
|
|
@@ -10,19 +10,50 @@ export async function sendMailgunEmail(params) {
|
|
|
10
10
|
return { success: false, message: 'Mailgun not configured' };
|
|
11
11
|
}
|
|
12
12
|
const baseUrl = params.endpoint || DEFAULT_EU_ENDPOINT;
|
|
13
|
-
const formData = new URLSearchParams();
|
|
14
|
-
formData.append('from', params.from);
|
|
15
|
-
formData.append('to', params.to);
|
|
16
|
-
formData.append('subject', params.subject);
|
|
17
|
-
formData.append('html', params.html);
|
|
18
13
|
try {
|
|
14
|
+
let body;
|
|
15
|
+
let contentType;
|
|
16
|
+
if (params.attachments?.length) {
|
|
17
|
+
// Use FormData for multipart (attachments)
|
|
18
|
+
const formData = new FormData();
|
|
19
|
+
formData.append('from', params.from);
|
|
20
|
+
formData.append('to', params.to);
|
|
21
|
+
formData.append('subject', params.subject);
|
|
22
|
+
formData.append('html', params.html);
|
|
23
|
+
if (params.text)
|
|
24
|
+
formData.append('text', params.text);
|
|
25
|
+
if (params.replyTo)
|
|
26
|
+
formData.append('h:Reply-To', params.replyTo);
|
|
27
|
+
for (const att of params.attachments) {
|
|
28
|
+
const blob = new Blob([att.data], { type: att.contentType || 'application/octet-stream' });
|
|
29
|
+
formData.append('attachment', blob, att.filename);
|
|
30
|
+
}
|
|
31
|
+
body = formData;
|
|
32
|
+
// Let fetch set the Content-Type with boundary
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
// Simple URL-encoded form (no attachments)
|
|
36
|
+
const formData = new URLSearchParams();
|
|
37
|
+
formData.append('from', params.from);
|
|
38
|
+
formData.append('to', params.to);
|
|
39
|
+
formData.append('subject', params.subject);
|
|
40
|
+
formData.append('html', params.html);
|
|
41
|
+
if (params.text)
|
|
42
|
+
formData.append('text', params.text);
|
|
43
|
+
if (params.replyTo)
|
|
44
|
+
formData.append('h:Reply-To', params.replyTo);
|
|
45
|
+
body = formData.toString();
|
|
46
|
+
contentType = 'application/x-www-form-urlencoded';
|
|
47
|
+
}
|
|
48
|
+
const headers = {
|
|
49
|
+
Authorization: 'Basic ' + Buffer.from(`api:${apiKey}`).toString('base64'),
|
|
50
|
+
};
|
|
51
|
+
if (contentType)
|
|
52
|
+
headers['Content-Type'] = contentType;
|
|
19
53
|
const resp = await fetch(`${baseUrl}/${domain}/messages`, {
|
|
20
54
|
method: 'POST',
|
|
21
|
-
headers
|
|
22
|
-
|
|
23
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
24
|
-
},
|
|
25
|
-
body: formData.toString(),
|
|
55
|
+
headers,
|
|
56
|
+
body,
|
|
26
57
|
});
|
|
27
58
|
if (!resp.ok) {
|
|
28
59
|
const text = (await resp.text()).slice(0, 500);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mailgun.js","sourceRoot":"","sources":["../../../src/shared/email/mailgun.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,mBAAmB,GAAG,+BAA+B,CAAC;AAE5D,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,
|
|
1
|
+
{"version":3,"file":"mailgun.js","sourceRoot":"","sources":["../../../src/shared/email/mailgun.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
* SMTP email helper — uses nodemailer for sending via SMTP (Gmail, Outlook, etc.).
|
|
3
3
|
* Graceful no-op when credentials are missing.
|
|
4
4
|
*/
|
|
5
|
-
import type { SmtpResponse } from './types.js';
|
|
5
|
+
import type { SmtpResponse, EmailAttachment } from './types.js';
|
|
6
6
|
export declare function sendSmtpEmail(params: {
|
|
7
7
|
from: string;
|
|
8
8
|
to: string;
|
|
9
9
|
subject: string;
|
|
10
10
|
html: string;
|
|
11
|
+
text?: string;
|
|
12
|
+
replyTo?: string;
|
|
13
|
+
attachments?: EmailAttachment[];
|
|
11
14
|
smtpHost: string;
|
|
12
15
|
smtpPort: number;
|
|
13
16
|
smtpUser: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"smtp.d.ts","sourceRoot":"","sources":["../../../src/shared/email/smtp.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"smtp.d.ts","sourceRoot":"","sources":["../../../src/shared/email/smtp.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEhE,wBAAsB,aAAa,CAAC,MAAM,EAAE;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,GAAG,OAAO,CAAC,YAAY,CAAC,CAoDxB"}
|
|
@@ -20,12 +20,24 @@ export async function sendSmtpEmail(params) {
|
|
|
20
20
|
pass: params.smtpPass,
|
|
21
21
|
},
|
|
22
22
|
});
|
|
23
|
-
const
|
|
23
|
+
const mailOpts = {
|
|
24
24
|
from: params.from,
|
|
25
25
|
to: params.to,
|
|
26
26
|
subject: params.subject,
|
|
27
27
|
html: params.html,
|
|
28
|
-
}
|
|
28
|
+
};
|
|
29
|
+
if (params.text)
|
|
30
|
+
mailOpts.text = params.text;
|
|
31
|
+
if (params.replyTo)
|
|
32
|
+
mailOpts.replyTo = params.replyTo;
|
|
33
|
+
if (params.attachments?.length) {
|
|
34
|
+
mailOpts.attachments = params.attachments.map(att => ({
|
|
35
|
+
filename: att.filename,
|
|
36
|
+
content: att.data,
|
|
37
|
+
contentType: att.contentType || 'application/octet-stream',
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
const info = await transporter.sendMail(mailOpts);
|
|
29
41
|
return {
|
|
30
42
|
success: true,
|
|
31
43
|
message: 'Email sent via SMTP',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"smtp.js","sourceRoot":"","sources":["../../../src/shared/email/smtp.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,
|
|
1
|
+
{"version":3,"file":"smtp.js","sourceRoot":"","sources":["../../../src/shared/email/smtp.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,MAanC;IACC,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC;IAEhD,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ,EAAE,CAAC;QACxC,OAAO,CAAC,IAAI,CAAC,mDAAmD,CAAC,CAAC;QAClE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,qBAAqB,EAAE,CAAC;IAC5D,CAAC;IAED,IAAI,CAAC;QACH,wDAAwD;QACxD,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC;QAE9C,MAAM,WAAW,GAAG,UAAU,CAAC,OAAO,CAAC,eAAe,CAAC;YACrD,IAAI,EAAE,MAAM,CAAC,QAAQ;YACrB,IAAI,EAAE,MAAM,CAAC,QAAQ;YACrB,MAAM,EAAE,MAAM,CAAC,UAAU,IAAI,MAAM,CAAC,QAAQ,KAAK,GAAG;YACpD,IAAI,EAAE;gBACJ,IAAI,EAAE,MAAM,CAAC,QAAQ;gBACrB,IAAI,EAAE,MAAM,CAAC,QAAQ;aACtB;SACF,CAAC,CAAC;QAEH,MAAM,QAAQ,GAA4B;YACxC,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,EAAE,EAAE,MAAM,CAAC,EAAE;YACb,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,IAAI,EAAE,MAAM,CAAC,IAAI;SAClB,CAAC;QAEF,IAAI,MAAM,CAAC,IAAI;YAAE,QAAQ,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;QAC7C,IAAI,MAAM,CAAC,OAAO;YAAE,QAAQ,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;QAEtD,IAAI,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,CAAC;YAC/B,QAAQ,CAAC,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gBACpD,QAAQ,EAAE,GAAG,CAAC,QAAQ;gBACtB,OAAO,EAAE,GAAG,CAAC,IAAI;gBACjB,WAAW,EAAE,GAAG,CAAC,WAAW,IAAI,0BAA0B;aAC3D,CAAC,CAAC,CAAC;QACN,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAElD,OAAO;YACL,OAAO,EAAE,IAAI;YACb,OAAO,EAAE,qBAAqB;YAC9B,SAAS,EAAE,IAAI,CAAC,SAAS;SAC1B,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC7D,OAAO,CAAC,KAAK,CAAC,2BAA2B,EAAE,GAAG,CAAC,CAAC;QAChD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,qBAAqB,GAAG,EAAE,EAAE,CAAC;IACjE,CAAC;AACH,CAAC"}
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module @soulbatical/tetra-core/email
|
|
5
5
|
*/
|
|
6
|
+
export interface EmailAttachment {
|
|
7
|
+
filename: string;
|
|
8
|
+
data: Buffer;
|
|
9
|
+
contentType?: string;
|
|
10
|
+
}
|
|
6
11
|
export interface EmailConfig {
|
|
7
12
|
/** From address, e.g. "VinciFox <noreply@vincifox.com>" */
|
|
8
13
|
fromAddress: string;
|
|
@@ -18,6 +23,8 @@ export interface EmailConfig {
|
|
|
18
23
|
mailgunDomain?: string;
|
|
19
24
|
/** Mailgun API endpoint — defaults to EU endpoint */
|
|
20
25
|
mailgunEndpoint?: string;
|
|
26
|
+
/** Mailgun webhook signing key — required for webhook signature verification */
|
|
27
|
+
mailgunWebhookSigningKey?: string;
|
|
21
28
|
/** SMTP host, e.g. smtp.gmail.com */
|
|
22
29
|
smtpHost?: string;
|
|
23
30
|
/** SMTP port — defaults to 587 */
|
|
@@ -40,12 +47,22 @@ export interface SendEmailOpts {
|
|
|
40
47
|
/** Language for template lookup, falls back to config.defaultLanguage */
|
|
41
48
|
language?: string;
|
|
42
49
|
organizationId?: string;
|
|
50
|
+
/** Reply-To address (e.g. for contact forms) */
|
|
51
|
+
replyTo?: string;
|
|
52
|
+
/** File attachments */
|
|
53
|
+
attachments?: EmailAttachment[];
|
|
54
|
+
/** Email type for logging/categorization (e.g. 'transactional', 'contact_admin') */
|
|
55
|
+
emailType?: string;
|
|
56
|
+
/** Arbitrary metadata stored in email_logs for tracking/debugging */
|
|
57
|
+
metadata?: Record<string, unknown>;
|
|
43
58
|
/** Optional FK — consuming project can use this for app-specific references */
|
|
44
59
|
feedbackId?: string;
|
|
45
60
|
/** Subject fallback when DB template is missing */
|
|
46
61
|
fallbackSubject?: string;
|
|
47
62
|
/** HTML fallback when DB template is missing */
|
|
48
63
|
fallbackHtml?: string;
|
|
64
|
+
/** Plain text fallback when DB template is missing */
|
|
65
|
+
fallbackText?: string;
|
|
49
66
|
}
|
|
50
67
|
export interface EmailTemplate {
|
|
51
68
|
id: string;
|
|
@@ -70,10 +87,15 @@ export interface EmailLogEntry {
|
|
|
70
87
|
subject: string;
|
|
71
88
|
template_slug: string | null;
|
|
72
89
|
variables_used: Record<string, string>;
|
|
73
|
-
|
|
90
|
+
email_type: string | null;
|
|
91
|
+
metadata: Record<string, unknown> | null;
|
|
92
|
+
status: 'pending' | 'sent' | 'failed' | 'delivered' | 'opened' | 'clicked' | 'bounced' | 'complained';
|
|
74
93
|
error_message: string | null;
|
|
75
94
|
mailgun_message_id: string | null;
|
|
76
95
|
sent_at: string | null;
|
|
96
|
+
delivered_at: string | null;
|
|
97
|
+
opened_at: string | null;
|
|
98
|
+
clicked_at: string | null;
|
|
77
99
|
created_at: string;
|
|
78
100
|
}
|
|
79
101
|
export interface MailgunResponse {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/shared/email/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,WAAW;IAC1B,2DAA2D;IAC3D,WAAW,EAAE,MAAM,CAAC;IACpB,oDAAoD;IACpD,eAAe,EAAE,IAAI,GAAG,IAAI,CAAC;IAC7B,8CAA8C;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,+FAA+F;IAC/F,SAAS,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;IACzC,2CAA2C;IAC3C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2CAA2C;IAC3C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,qDAAqD;IACrD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kCAAkC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gDAAgD;IAChD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,OAAO,EAAE,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,MAAM,EAAE,SAAS,GAAG,MAAM,GAAG,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/shared/email/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mailgun Webhook Routes — optional route factory for delivery tracking.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { addMailgunWebhookRoutes } from '@soulbatical/tetra-core';
|
|
6
|
+
* addMailgunWebhookRoutes(router, { signingKey: process.env.MAILGUN_WEBHOOK_SIGNING_KEY });
|
|
7
|
+
*
|
|
8
|
+
* @module @soulbatical/tetra-core/email
|
|
9
|
+
*/
|
|
10
|
+
import { Router } from 'express';
|
|
11
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
12
|
+
interface MailgunWebhookConfig {
|
|
13
|
+
/** Mailgun webhook signing key for signature verification */
|
|
14
|
+
signingKey?: string;
|
|
15
|
+
/** Supabase client (systemDB or service_role) for updating email_logs */
|
|
16
|
+
supabase: SupabaseClient;
|
|
17
|
+
/** Max age of webhook timestamp in seconds (default: 30) */
|
|
18
|
+
maxTimestampAge?: number;
|
|
19
|
+
/** Set true in development to skip signature verification */
|
|
20
|
+
skipVerification?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Add Mailgun webhook route to a router.
|
|
24
|
+
* Handles: delivered, opened, clicked, bounced, failed, complained events.
|
|
25
|
+
* Updates email_logs table with delivery tracking data.
|
|
26
|
+
*/
|
|
27
|
+
export declare function addMailgunWebhookRoutes(router: Router, config: MailgunWebhookConfig): void;
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=webhookRoutes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhookRoutes.d.ts","sourceRoot":"","sources":["../../../src/shared/email/webhookRoutes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,MAAM,EAAqB,MAAM,SAAS,CAAC;AAEpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,UAAU,oBAAoB;IAC5B,6DAA6D;IAC7D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yEAAyE;IACzE,QAAQ,EAAE,cAAc,CAAC;IACzB,4DAA4D;IAC5D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,6DAA6D;IAC7D,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AA2BD;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,oBAAoB,GAAG,IAAI,CA0G1F"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mailgun Webhook Routes — optional route factory for delivery tracking.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { addMailgunWebhookRoutes } from '@soulbatical/tetra-core';
|
|
6
|
+
* addMailgunWebhookRoutes(router, { signingKey: process.env.MAILGUN_WEBHOOK_SIGNING_KEY });
|
|
7
|
+
*
|
|
8
|
+
* @module @soulbatical/tetra-core/email
|
|
9
|
+
*/
|
|
10
|
+
import crypto from 'crypto';
|
|
11
|
+
function verifySignature(timestamp, token, signature, signingKey, maxAge) {
|
|
12
|
+
const currentTime = Date.now() / 1000;
|
|
13
|
+
const webhookTime = parseInt(timestamp, 10);
|
|
14
|
+
const timestampAge = currentTime - webhookTime;
|
|
15
|
+
// Reject timestamps too far in the future (clock skew tolerance: 5 seconds)
|
|
16
|
+
if (timestampAge < -5)
|
|
17
|
+
return false;
|
|
18
|
+
// Reject timestamps older than maxAge
|
|
19
|
+
if (timestampAge > maxAge)
|
|
20
|
+
return false;
|
|
21
|
+
// Verify HMAC signature using timing-safe comparison
|
|
22
|
+
const hmac = crypto.createHmac('sha256', signingKey);
|
|
23
|
+
const digest = hmac.update(timestamp + token).digest('hex');
|
|
24
|
+
if (digest.length !== signature.length)
|
|
25
|
+
return false;
|
|
26
|
+
return crypto.timingSafeEqual(Buffer.from(digest, 'hex'), Buffer.from(signature, 'hex'));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Add Mailgun webhook route to a router.
|
|
30
|
+
* Handles: delivered, opened, clicked, bounced, failed, complained events.
|
|
31
|
+
* Updates email_logs table with delivery tracking data.
|
|
32
|
+
*/
|
|
33
|
+
export function addMailgunWebhookRoutes(router, config) {
|
|
34
|
+
const maxAge = config.maxTimestampAge ?? 30;
|
|
35
|
+
router.post('/mailgun', async (req, res) => {
|
|
36
|
+
try {
|
|
37
|
+
const event = req.body;
|
|
38
|
+
if (!event?.signature || !event?.['event-data']) {
|
|
39
|
+
res.status(400).json({ error: 'Invalid webhook structure' });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// Verify signature
|
|
43
|
+
const { timestamp, token, signature } = event.signature;
|
|
44
|
+
if (!config.skipVerification) {
|
|
45
|
+
if (!config.signingKey) {
|
|
46
|
+
console.error('[email/webhook] MAILGUN_WEBHOOK_SIGNING_KEY not configured');
|
|
47
|
+
res.status(401).json({ error: 'Webhook signing key not configured' });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (!verifySignature(timestamp, token, signature, config.signingKey, maxAge)) {
|
|
51
|
+
console.warn('[email/webhook] Invalid Mailgun webhook signature');
|
|
52
|
+
res.status(401).json({ error: 'Invalid signature' });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const eventData = event['event-data'];
|
|
57
|
+
const eventType = eventData.event;
|
|
58
|
+
const recipient = eventData.recipient?.toLowerCase();
|
|
59
|
+
const eventTimestamp = new Date(eventData.timestamp * 1000).toISOString();
|
|
60
|
+
if (!recipient) {
|
|
61
|
+
res.status(200).json({ message: 'No recipient' });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Find most recent email log for this recipient
|
|
65
|
+
const { data: emailLog } = await config.supabase
|
|
66
|
+
.from('email_logs')
|
|
67
|
+
.select('id, status, opened_at, clicked_at')
|
|
68
|
+
.eq('to_email', recipient)
|
|
69
|
+
.order('created_at', { ascending: false })
|
|
70
|
+
.limit(1)
|
|
71
|
+
.single();
|
|
72
|
+
if (!emailLog) {
|
|
73
|
+
res.status(200).json({ message: 'No matching log' });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// Build updates based on event type
|
|
77
|
+
const updates = {};
|
|
78
|
+
switch (eventType) {
|
|
79
|
+
case 'delivered':
|
|
80
|
+
updates.status = 'delivered';
|
|
81
|
+
updates.delivered_at = eventTimestamp;
|
|
82
|
+
break;
|
|
83
|
+
case 'opened':
|
|
84
|
+
if (!emailLog.opened_at)
|
|
85
|
+
updates.opened_at = eventTimestamp;
|
|
86
|
+
if (emailLog.status === 'sent' || emailLog.status === 'delivered') {
|
|
87
|
+
updates.status = 'opened';
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
case 'clicked':
|
|
91
|
+
if (!emailLog.clicked_at)
|
|
92
|
+
updates.clicked_at = eventTimestamp;
|
|
93
|
+
updates.status = 'clicked';
|
|
94
|
+
break;
|
|
95
|
+
case 'bounced':
|
|
96
|
+
case 'failed':
|
|
97
|
+
updates.status = eventType;
|
|
98
|
+
updates.error_message =
|
|
99
|
+
eventData['delivery-status']?.message ||
|
|
100
|
+
eventData['delivery-status']?.description ||
|
|
101
|
+
`Email ${eventType}`;
|
|
102
|
+
break;
|
|
103
|
+
case 'complained':
|
|
104
|
+
updates.status = 'complained';
|
|
105
|
+
break;
|
|
106
|
+
default:
|
|
107
|
+
res.status(200).json({ message: 'ok' });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (Object.keys(updates).length > 0) {
|
|
111
|
+
await config.supabase
|
|
112
|
+
.from('email_logs')
|
|
113
|
+
.update(updates)
|
|
114
|
+
.eq('id', emailLog.id);
|
|
115
|
+
}
|
|
116
|
+
res.status(200).json({ message: 'ok' });
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
console.error('[email/webhook] Error processing webhook:', error);
|
|
120
|
+
// Return 200 to prevent Mailgun retries
|
|
121
|
+
res.status(200).json({ message: 'error logged' });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=webhookRoutes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhookRoutes.js","sourceRoot":"","sources":["../../../src/shared/email/webhookRoutes.ts"],"names":[],"mappings":"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"}
|