organify-email 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/brevo-client.d.ts +72 -0
- package/dist/brevo-client.d.ts.map +1 -0
- package/dist/brevo-client.js +167 -0
- package/dist/brevo-client.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/templates.d.ts +5 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +426 -0
- package/dist/templates.js.map +1 -0
- package/package.json +15 -0
- package/src/brevo-client.ts +242 -0
- package/src/index.ts +17 -0
- package/src/templates.ts +524 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────
|
|
2
|
+
// Brevo Email Client — Transactional API
|
|
3
|
+
// ─────────────────────────────────────────────
|
|
4
|
+
// Shared email service using Brevo API for all
|
|
5
|
+
// Organify microservices.
|
|
6
|
+
//
|
|
7
|
+
// Limits:
|
|
8
|
+
// - 300 emails/day (Brevo free tier)
|
|
9
|
+
// - ~30 estimated for beta
|
|
10
|
+
// - Built-in daily counter + priority system
|
|
11
|
+
//
|
|
12
|
+
// Usage:
|
|
13
|
+
// const client = new BrevoEmailClient({ apiKey, senderName, senderEmail });
|
|
14
|
+
// await client.send({ to, subject, template: 'report-ready', params });
|
|
15
|
+
// ─────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
import { emailTemplates, type EmailTemplateName } from './templates';
|
|
18
|
+
|
|
19
|
+
// ─── Types ──────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export interface BrevoEmailConfig {
|
|
22
|
+
apiKey: string;
|
|
23
|
+
senderName?: string;
|
|
24
|
+
senderEmail?: string;
|
|
25
|
+
/** Daily send limit (default: 300) */
|
|
26
|
+
dailyLimit?: number;
|
|
27
|
+
/** Enable sending (set false for dev/test to just log) */
|
|
28
|
+
enabled?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface EmailRecipient {
|
|
32
|
+
email: string;
|
|
33
|
+
name?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SendEmailOptions {
|
|
37
|
+
to: EmailRecipient | EmailRecipient[];
|
|
38
|
+
subject: string;
|
|
39
|
+
/** Use a named template with dynamic params */
|
|
40
|
+
template?: EmailTemplateName;
|
|
41
|
+
/** Template parameters (dynamic variables) */
|
|
42
|
+
params?: Record<string, string | number | boolean>;
|
|
43
|
+
/** Raw HTML content (overrides template) */
|
|
44
|
+
htmlContent?: string;
|
|
45
|
+
/** Plain text fallback */
|
|
46
|
+
textContent?: string;
|
|
47
|
+
/** Priority: 'critical' always sends, 'normal' respects rate limits, 'low' may be skipped */
|
|
48
|
+
priority?: 'critical' | 'normal' | 'low';
|
|
49
|
+
/** Tags for tracking */
|
|
50
|
+
tags?: string[];
|
|
51
|
+
/** CC recipients */
|
|
52
|
+
cc?: EmailRecipient[];
|
|
53
|
+
/** BCC recipients */
|
|
54
|
+
bcc?: EmailRecipient[];
|
|
55
|
+
/** Reply-to address */
|
|
56
|
+
replyTo?: EmailRecipient;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface SendResult {
|
|
60
|
+
success: boolean;
|
|
61
|
+
messageId?: string;
|
|
62
|
+
error?: string;
|
|
63
|
+
skipped?: boolean;
|
|
64
|
+
reason?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface DailyStats {
|
|
68
|
+
sent: number;
|
|
69
|
+
limit: number;
|
|
70
|
+
remaining: number;
|
|
71
|
+
date: string;
|
|
72
|
+
byPriority: { critical: number; normal: number; low: number };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── Client ─────────────────────────────────
|
|
76
|
+
|
|
77
|
+
export class BrevoEmailClient {
|
|
78
|
+
private readonly apiKey: string;
|
|
79
|
+
private readonly senderName: string;
|
|
80
|
+
private readonly senderEmail: string;
|
|
81
|
+
private readonly dailyLimit: number;
|
|
82
|
+
private readonly enabled: boolean;
|
|
83
|
+
|
|
84
|
+
// Daily counter (in-memory, resets on restart)
|
|
85
|
+
private dailySent = 0;
|
|
86
|
+
private dailyDate = '';
|
|
87
|
+
private dailyByPriority = { critical: 0, normal: 0, low: 0 };
|
|
88
|
+
|
|
89
|
+
private static readonly API_URL = 'https://api.brevo.com/v3/smtp/email';
|
|
90
|
+
|
|
91
|
+
constructor(config: BrevoEmailConfig) {
|
|
92
|
+
this.apiKey = config.apiKey;
|
|
93
|
+
this.senderName = config.senderName || 'Organify Team';
|
|
94
|
+
this.senderEmail = config.senderEmail || 'noreply@organify.studio';
|
|
95
|
+
this.dailyLimit = config.dailyLimit || 300;
|
|
96
|
+
this.enabled = config.enabled !== false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ─── Main Send ────────────────────────────
|
|
100
|
+
|
|
101
|
+
async send(options: SendEmailOptions): Promise<SendResult> {
|
|
102
|
+
const priority = options.priority || 'normal';
|
|
103
|
+
|
|
104
|
+
// Reset counter if new day
|
|
105
|
+
this.checkDayReset();
|
|
106
|
+
|
|
107
|
+
// Rate limit check (critical bypasses)
|
|
108
|
+
if (priority !== 'critical') {
|
|
109
|
+
if (this.dailySent >= this.dailyLimit) {
|
|
110
|
+
return {
|
|
111
|
+
success: false,
|
|
112
|
+
skipped: true,
|
|
113
|
+
reason: `Daily limit reached (${this.dailyLimit}/day). Email queued for tomorrow.`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Reserve 10% for critical emails
|
|
118
|
+
const criticalReserve = Math.floor(this.dailyLimit * 0.1);
|
|
119
|
+
if (priority === 'low' && this.dailySent >= this.dailyLimit - criticalReserve) {
|
|
120
|
+
return {
|
|
121
|
+
success: false,
|
|
122
|
+
skipped: true,
|
|
123
|
+
reason: `Low-priority email skipped (remaining quota reserved for critical/normal).`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Build HTML content
|
|
129
|
+
let htmlContent = options.htmlContent;
|
|
130
|
+
if (!htmlContent && options.template) {
|
|
131
|
+
const templateFn = emailTemplates[options.template];
|
|
132
|
+
if (templateFn) {
|
|
133
|
+
htmlContent = templateFn(options.params || {});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!htmlContent && !options.textContent) {
|
|
138
|
+
return { success: false, error: 'No content provided (template, htmlContent, or textContent required)' };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Disabled mode (dev/test)
|
|
142
|
+
if (!this.enabled) {
|
|
143
|
+
const recipients = Array.isArray(options.to) ? options.to : [options.to];
|
|
144
|
+
console.log(`[BrevoEmail] DRY RUN → ${recipients.map((r) => r.email).join(', ')} | Subject: ${options.subject}`);
|
|
145
|
+
this.incrementCounter(priority);
|
|
146
|
+
return { success: true, messageId: `dry-run-${Date.now()}`, skipped: false };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Build Brevo payload
|
|
150
|
+
const recipients = Array.isArray(options.to) ? options.to : [options.to];
|
|
151
|
+
const payload: Record<string, any> = {
|
|
152
|
+
sender: { name: this.senderName, email: this.senderEmail },
|
|
153
|
+
to: recipients.map((r) => ({ email: r.email, name: r.name })),
|
|
154
|
+
subject: options.subject,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
if (htmlContent) payload.htmlContent = htmlContent;
|
|
158
|
+
else if (options.textContent) payload.textContent = options.textContent;
|
|
159
|
+
|
|
160
|
+
if (options.params) payload.params = options.params;
|
|
161
|
+
if (options.tags) payload.tags = options.tags;
|
|
162
|
+
if (options.cc) payload.cc = options.cc;
|
|
163
|
+
if (options.bcc) payload.bcc = options.bcc;
|
|
164
|
+
if (options.replyTo) payload.replyTo = options.replyTo;
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const response = await fetch(BrevoEmailClient.API_URL, {
|
|
168
|
+
method: 'POST',
|
|
169
|
+
headers: {
|
|
170
|
+
'accept': 'application/json',
|
|
171
|
+
'api-key': this.apiKey,
|
|
172
|
+
'content-type': 'application/json',
|
|
173
|
+
},
|
|
174
|
+
body: JSON.stringify(payload),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (!response.ok) {
|
|
178
|
+
const body = await response.text();
|
|
179
|
+
console.error(`[BrevoEmail] API error ${response.status}: ${body}`);
|
|
180
|
+
return { success: false, error: `Brevo API error: ${response.status} — ${body}` };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const data = await response.json() as { messageId?: string };
|
|
184
|
+
this.incrementCounter(priority);
|
|
185
|
+
|
|
186
|
+
return { success: true, messageId: data.messageId };
|
|
187
|
+
} catch (err: any) {
|
|
188
|
+
console.error(`[BrevoEmail] Network error: ${err.message}`);
|
|
189
|
+
return { success: false, error: `Network error: ${err.message}` };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ─── Batch Send (same template, multiple recipients) ──
|
|
194
|
+
|
|
195
|
+
async sendBatch(
|
|
196
|
+
recipients: EmailRecipient[],
|
|
197
|
+
options: Omit<SendEmailOptions, 'to'>,
|
|
198
|
+
): Promise<SendResult[]> {
|
|
199
|
+
// Brevo supports multiple "to" in a single request
|
|
200
|
+
// But for individual params, send separately
|
|
201
|
+
if (options.params) {
|
|
202
|
+
// Each recipient gets their own email
|
|
203
|
+
const results: SendResult[] = [];
|
|
204
|
+
for (const recipient of recipients) {
|
|
205
|
+
results.push(await this.send({ ...options, to: recipient }));
|
|
206
|
+
}
|
|
207
|
+
return results;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Single request with multiple recipients
|
|
211
|
+
return [await this.send({ ...options, to: recipients })];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ─── Stats ────────────────────────────────
|
|
215
|
+
|
|
216
|
+
getStats(): DailyStats {
|
|
217
|
+
this.checkDayReset();
|
|
218
|
+
return {
|
|
219
|
+
sent: this.dailySent,
|
|
220
|
+
limit: this.dailyLimit,
|
|
221
|
+
remaining: Math.max(0, this.dailyLimit - this.dailySent),
|
|
222
|
+
date: this.dailyDate,
|
|
223
|
+
byPriority: { ...this.dailyByPriority },
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ─── Internal ─────────────────────────────
|
|
228
|
+
|
|
229
|
+
private checkDayReset() {
|
|
230
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
231
|
+
if (this.dailyDate !== today) {
|
|
232
|
+
this.dailySent = 0;
|
|
233
|
+
this.dailyDate = today;
|
|
234
|
+
this.dailyByPriority = { critical: 0, normal: 0, low: 0 };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private incrementCounter(priority: 'critical' | 'normal' | 'low') {
|
|
239
|
+
this.dailySent++;
|
|
240
|
+
this.dailyByPriority[priority]++;
|
|
241
|
+
}
|
|
242
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────
|
|
2
|
+
// organify-email — Public API
|
|
3
|
+
// ─────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
BrevoEmailClient,
|
|
7
|
+
type BrevoEmailConfig,
|
|
8
|
+
type EmailRecipient,
|
|
9
|
+
type SendEmailOptions,
|
|
10
|
+
type SendResult,
|
|
11
|
+
type DailyStats,
|
|
12
|
+
} from './brevo-client';
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
emailTemplates,
|
|
16
|
+
type EmailTemplateName,
|
|
17
|
+
} from './templates';
|