hazo_notify 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/.cursor/rules/db_schema.mdc +0 -0
- package/.cursor/rules/design.mdc +16 -0
- package/.cursor/rules/general.mdc +49 -0
- package/README.md +765 -0
- package/components/emailer-html-editor.tsx +94 -0
- package/components/ui/button.tsx +53 -0
- package/components/ui/card.tsx +78 -0
- package/components/ui/input.tsx +24 -0
- package/components/ui/label.tsx +21 -0
- package/components/ui/sidebar.tsx +121 -0
- package/components/ui/spinner.tsx +54 -0
- package/components/ui/textarea.tsx +23 -0
- package/components/ui/tooltip.tsx +30 -0
- package/components.json +20 -0
- package/hazo_notify_config.ini +153 -0
- package/jest.config.js +27 -0
- package/jest.setup.js +1 -0
- package/next.config.js +22 -0
- package/package.json +72 -0
- package/postcss.config.js +6 -0
- package/src/app/api/hazo_notify/emailer/send/__tests__/route.test.ts +227 -0
- package/src/app/api/hazo_notify/emailer/send/route.ts +537 -0
- package/src/app/editor-00/page.tsx +47 -0
- package/src/app/globals.css +69 -0
- package/src/app/hazo_notify/emailer_test/layout.tsx +53 -0
- package/src/app/hazo_notify/emailer_test/page.tsx +369 -0
- package/src/app/hazo_notify/layout.tsx +77 -0
- package/src/app/hazo_notify/page.tsx +12 -0
- package/src/app/layout.tsx +26 -0
- package/src/app/page.tsx +14 -0
- package/src/components/blocks/editor-00/editor.tsx +61 -0
- package/src/components/blocks/editor-00/nodes.ts +11 -0
- package/src/components/blocks/editor-00/plugins.tsx +36 -0
- package/src/components/editor/editor-ui/content-editable.tsx +34 -0
- package/src/components/editor/themes/editor-theme.css +91 -0
- package/src/components/editor/themes/editor-theme.ts +130 -0
- package/src/components/ui/button.tsx +53 -0
- package/src/components/ui/card.tsx +78 -0
- package/src/components/ui/input.tsx +24 -0
- package/src/components/ui/label.tsx +21 -0
- package/src/components/ui/sidebar.tsx +121 -0
- package/src/components/ui/spinner.tsx +54 -0
- package/src/components/ui/textarea.tsx +23 -0
- package/src/components/ui/tooltip.tsx +30 -0
- package/src/lib/emailer/__tests__/emailer.test.ts +200 -0
- package/src/lib/emailer/emailer.ts +263 -0
- package/src/lib/emailer/index.ts +11 -0
- package/src/lib/emailer/providers/__tests__/zeptomail_provider.test.ts +196 -0
- package/src/lib/emailer/providers/index.ts +33 -0
- package/src/lib/emailer/providers/pop3_provider.ts +30 -0
- package/src/lib/emailer/providers/smtp_provider.ts +30 -0
- package/src/lib/emailer/providers/zeptomail_provider.ts +299 -0
- package/src/lib/emailer/types.ts +119 -0
- package/src/lib/emailer/utils/constants.ts +24 -0
- package/src/lib/emailer/utils/index.ts +9 -0
- package/src/lib/emailer/utils/logger.ts +71 -0
- package/src/lib/emailer/utils/validation.ts +84 -0
- package/src/lib/index.ts +6 -0
- package/src/lib/utils.ts +6 -0
- package/tailwind.config.ts +65 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POP3 provider implementation (placeholder)
|
|
3
|
+
* This provider is not yet implemented and will return an error
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { EmailProvider, SendEmailOptions, EmailerConfig, EmailSendResponse } from '../types';
|
|
7
|
+
|
|
8
|
+
export class Pop3Provider implements EmailProvider {
|
|
9
|
+
/**
|
|
10
|
+
* Send email using POP3 (not implemented)
|
|
11
|
+
* @param options - Email send options
|
|
12
|
+
* @param config - Emailer configuration
|
|
13
|
+
* @returns Promise with error response
|
|
14
|
+
*/
|
|
15
|
+
async send_email(
|
|
16
|
+
options: SendEmailOptions,
|
|
17
|
+
config: EmailerConfig
|
|
18
|
+
): Promise<EmailSendResponse> {
|
|
19
|
+
return {
|
|
20
|
+
success: false,
|
|
21
|
+
error: 'POP3 provider is not yet implemented',
|
|
22
|
+
message: 'POP3 provider is not yet implemented. Please use API method instead.',
|
|
23
|
+
raw_response: {
|
|
24
|
+
provider: 'pop3',
|
|
25
|
+
status: 'not_implemented',
|
|
26
|
+
message: 'POP3 provider is a placeholder and will be implemented in a future version',
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMTP provider implementation (placeholder)
|
|
3
|
+
* This provider is not yet implemented and will return an error
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { EmailProvider, SendEmailOptions, EmailerConfig, EmailSendResponse } from '../types';
|
|
7
|
+
|
|
8
|
+
export class SmtpProvider implements EmailProvider {
|
|
9
|
+
/**
|
|
10
|
+
* Send email using SMTP (not implemented)
|
|
11
|
+
* @param options - Email send options
|
|
12
|
+
* @param config - Emailer configuration
|
|
13
|
+
* @returns Promise with error response
|
|
14
|
+
*/
|
|
15
|
+
async send_email(
|
|
16
|
+
options: SendEmailOptions,
|
|
17
|
+
config: EmailerConfig
|
|
18
|
+
): Promise<EmailSendResponse> {
|
|
19
|
+
return {
|
|
20
|
+
success: false,
|
|
21
|
+
error: 'SMTP provider is not yet implemented',
|
|
22
|
+
message: 'SMTP provider is not yet implemented. Please use API method instead.',
|
|
23
|
+
raw_response: {
|
|
24
|
+
provider: 'smtp',
|
|
25
|
+
status: 'not_implemented',
|
|
26
|
+
message: 'SMTP provider is a placeholder and will be implemented in a future version',
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zeptomail API provider implementation
|
|
3
|
+
* Sends emails using the Zeptomail API
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { EmailProvider, SendEmailOptions, EmailerConfig, EmailSendResponse, ZeptomailEmailData } from '../types';
|
|
7
|
+
import { sanitize_email_header } from '../utils/validation';
|
|
8
|
+
import { log_error, log_info, create_log_entry } from '../utils/logger';
|
|
9
|
+
import DOMPurify from 'isomorphic-dompurify';
|
|
10
|
+
import { DEFAULT_REQUEST_TIMEOUT } from '../utils/constants';
|
|
11
|
+
|
|
12
|
+
export class ZeptomailProvider implements EmailProvider {
|
|
13
|
+
/**
|
|
14
|
+
* Send email using Zeptomail API
|
|
15
|
+
* @param options - Email send options
|
|
16
|
+
* @param config - Emailer configuration
|
|
17
|
+
* @returns Promise with email send response
|
|
18
|
+
*/
|
|
19
|
+
async send_email(
|
|
20
|
+
options: SendEmailOptions,
|
|
21
|
+
config: EmailerConfig
|
|
22
|
+
): Promise<EmailSendResponse> {
|
|
23
|
+
const filename = 'zeptomail_provider.ts';
|
|
24
|
+
const function_name = 'send_email';
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// Validate required configuration
|
|
28
|
+
if (!config.zeptomail_api_endpoint) {
|
|
29
|
+
throw new Error('Zeptomail API endpoint is required');
|
|
30
|
+
}
|
|
31
|
+
if (!config.zeptomail_api_key) {
|
|
32
|
+
throw new Error('Zeptomail API key is required');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Prepare recipient array
|
|
36
|
+
const to_emails = Array.isArray(options.to) ? options.to : [options.to];
|
|
37
|
+
const cc_emails = options.cc ? (Array.isArray(options.cc) ? options.cc : [options.cc]) : [];
|
|
38
|
+
const bcc_emails = options.bcc ? (Array.isArray(options.bcc) ? options.bcc : [options.bcc]) : [];
|
|
39
|
+
|
|
40
|
+
// Prepare email data according to Zeptomail API format
|
|
41
|
+
const email_data: ZeptomailEmailData = {
|
|
42
|
+
from: {
|
|
43
|
+
address: options.from || config.from_email,
|
|
44
|
+
name: sanitize_email_header(options.from_name || config.from_name || 'Hazo Notify'),
|
|
45
|
+
},
|
|
46
|
+
to: to_emails.map((email) => ({
|
|
47
|
+
email_address: {
|
|
48
|
+
address: email,
|
|
49
|
+
},
|
|
50
|
+
})),
|
|
51
|
+
subject: sanitize_email_header(options.subject),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Add content (text and/or html)
|
|
55
|
+
if (options.content.text) {
|
|
56
|
+
email_data.textbody = options.content.text;
|
|
57
|
+
}
|
|
58
|
+
if (options.content.html) {
|
|
59
|
+
// Sanitize HTML content to prevent XSS attacks
|
|
60
|
+
email_data.htmlbody = DOMPurify.sanitize(options.content.html);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Add reply-to if specified
|
|
64
|
+
if (options.reply_to || config.reply_to_email) {
|
|
65
|
+
email_data.reply_to = [
|
|
66
|
+
{
|
|
67
|
+
address: sanitize_email_header(options.reply_to || config.reply_to_email || ''),
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Add CC if specified
|
|
73
|
+
if (cc_emails.length > 0) {
|
|
74
|
+
email_data.cc = cc_emails.map((email) => ({
|
|
75
|
+
email_address: {
|
|
76
|
+
address: email,
|
|
77
|
+
},
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Add BCC if specified
|
|
82
|
+
if (bcc_emails.length > 0) {
|
|
83
|
+
email_data.bcc = bcc_emails.map((email) => ({
|
|
84
|
+
email_address: {
|
|
85
|
+
address: email,
|
|
86
|
+
},
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Add attachments if specified
|
|
91
|
+
if (options.attachments && options.attachments.length > 0) {
|
|
92
|
+
email_data.attachments = options.attachments.map((attachment) => ({
|
|
93
|
+
name: attachment.filename,
|
|
94
|
+
content: attachment.content,
|
|
95
|
+
mime_type: attachment.mime_type,
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Prepare request headers
|
|
100
|
+
// Handle API key - it may already include the "Zoho-enczapikey" prefix or just be the key
|
|
101
|
+
const api_key = config.zeptomail_api_key.trim();
|
|
102
|
+
const auth_header = api_key.startsWith('Zoho-enczapikey')
|
|
103
|
+
? api_key
|
|
104
|
+
: `Zoho-enczapikey ${api_key}`;
|
|
105
|
+
|
|
106
|
+
const headers: Record<string, string> = {
|
|
107
|
+
'Accept': 'application/json',
|
|
108
|
+
'Content-Type': 'application/json',
|
|
109
|
+
'Authorization': auth_header,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Log request for debugging (mask API key in logs)
|
|
113
|
+
log_info(create_log_entry(
|
|
114
|
+
filename,
|
|
115
|
+
function_name,
|
|
116
|
+
'Sending email via Zeptomail API',
|
|
117
|
+
{
|
|
118
|
+
endpoint: config.zeptomail_api_endpoint,
|
|
119
|
+
method: 'POST',
|
|
120
|
+
headers: {
|
|
121
|
+
...headers,
|
|
122
|
+
Authorization: headers.Authorization ? 'Zoho-enczapikey ***' : undefined, // Mask API key
|
|
123
|
+
},
|
|
124
|
+
body: {
|
|
125
|
+
from: email_data.from,
|
|
126
|
+
to_count: email_data.to.length,
|
|
127
|
+
subject: email_data.subject,
|
|
128
|
+
has_text: !!email_data.textbody,
|
|
129
|
+
has_html: !!email_data.htmlbody,
|
|
130
|
+
attachments_count: email_data.attachments?.length || 0,
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
));
|
|
134
|
+
|
|
135
|
+
// Set up request timeout
|
|
136
|
+
const timeout = config.request_timeout || DEFAULT_REQUEST_TIMEOUT;
|
|
137
|
+
const controller = new AbortController();
|
|
138
|
+
const timeout_id = setTimeout(() => controller.abort(), timeout);
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
// Make API request
|
|
142
|
+
const response = await fetch(config.zeptomail_api_endpoint, {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
headers: headers,
|
|
145
|
+
body: JSON.stringify(email_data),
|
|
146
|
+
signal: controller.signal,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
clearTimeout(timeout_id);
|
|
150
|
+
|
|
151
|
+
// Parse response (handle both JSON and text responses)
|
|
152
|
+
let response_data: Record<string, unknown> | string;
|
|
153
|
+
const content_type = response.headers.get('content-type') || '';
|
|
154
|
+
|
|
155
|
+
// Get response text first for better error handling
|
|
156
|
+
const response_text = await response.text();
|
|
157
|
+
|
|
158
|
+
if (content_type.includes('application/json')) {
|
|
159
|
+
try {
|
|
160
|
+
response_data = JSON.parse(response_text) as Record<string, unknown>;
|
|
161
|
+
} catch (e) {
|
|
162
|
+
response_data = { error: 'Failed to parse JSON response', raw: response_text };
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
// For HTML/text responses, try to extract error information
|
|
166
|
+
response_data = {
|
|
167
|
+
text: response_text,
|
|
168
|
+
error: response_text.trim() || 'Unknown error from server'
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// Log the full response for debugging
|
|
172
|
+
log_error(create_log_entry(
|
|
173
|
+
filename,
|
|
174
|
+
function_name,
|
|
175
|
+
'Zeptomail API returned non-JSON response',
|
|
176
|
+
{
|
|
177
|
+
status: response.status,
|
|
178
|
+
statusText: response.statusText,
|
|
179
|
+
contentType: content_type,
|
|
180
|
+
body: response_text,
|
|
181
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
182
|
+
}
|
|
183
|
+
));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const raw_response: Record<string, unknown> = {
|
|
187
|
+
status: response.status,
|
|
188
|
+
status_text: response.statusText,
|
|
189
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
190
|
+
body: response_data,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Check if request was successful
|
|
194
|
+
if (!response.ok) {
|
|
195
|
+
const response_obj = typeof response_data === 'object' ? response_data : {};
|
|
196
|
+
const error_message = (response_obj.message as string) ||
|
|
197
|
+
(response_obj.error as string) ||
|
|
198
|
+
(typeof response_data === 'string' ? response_data : '') ||
|
|
199
|
+
`HTTP ${response.status}: ${response.statusText}`;
|
|
200
|
+
|
|
201
|
+
// Enhanced error logging
|
|
202
|
+
log_error(create_log_entry(
|
|
203
|
+
filename,
|
|
204
|
+
function_name,
|
|
205
|
+
'Zeptomail API error',
|
|
206
|
+
{
|
|
207
|
+
status: response.status,
|
|
208
|
+
error_message,
|
|
209
|
+
response_body: response_data,
|
|
210
|
+
}
|
|
211
|
+
));
|
|
212
|
+
|
|
213
|
+
const is_production = process.env.NODE_ENV === 'production';
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
success: false,
|
|
217
|
+
error: error_message,
|
|
218
|
+
raw_response: is_production ? { status: response.status, status_text: response.statusText } : raw_response,
|
|
219
|
+
message: error_message,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Return successful response
|
|
224
|
+
const response_obj = typeof response_data === 'object' ? response_data : {};
|
|
225
|
+
const data = response_obj.data as Record<string, unknown> | undefined;
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
success: true,
|
|
229
|
+
message_id: (data?.message_id as string) || (response_obj.message_id as string) || undefined,
|
|
230
|
+
message: 'Email sent successfully',
|
|
231
|
+
raw_response: raw_response,
|
|
232
|
+
};
|
|
233
|
+
} catch (fetch_error: unknown) {
|
|
234
|
+
clearTimeout(timeout_id);
|
|
235
|
+
|
|
236
|
+
// Handle timeout errors
|
|
237
|
+
if (fetch_error instanceof Error && fetch_error.name === 'AbortError') {
|
|
238
|
+
const timeout_error = `Request timeout after ${timeout}ms`;
|
|
239
|
+
log_error(create_log_entry(
|
|
240
|
+
filename,
|
|
241
|
+
function_name,
|
|
242
|
+
timeout_error,
|
|
243
|
+
{
|
|
244
|
+
options: {
|
|
245
|
+
to: options.to,
|
|
246
|
+
subject: options.subject,
|
|
247
|
+
has_text: !!options.content.text,
|
|
248
|
+
has_html: !!options.content.html,
|
|
249
|
+
attachments_count: options.attachments?.length || 0,
|
|
250
|
+
},
|
|
251
|
+
}
|
|
252
|
+
));
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
success: false,
|
|
256
|
+
error: timeout_error,
|
|
257
|
+
message: timeout_error,
|
|
258
|
+
raw_response: undefined,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Re-throw to be caught by outer catch
|
|
263
|
+
throw fetch_error;
|
|
264
|
+
}
|
|
265
|
+
} catch (error: unknown) {
|
|
266
|
+
const error_message = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
267
|
+
const error_string = error instanceof Error ? error.toString() : String(error);
|
|
268
|
+
const stack = error instanceof Error ? error.stack : undefined;
|
|
269
|
+
const is_production = process.env.NODE_ENV === 'production';
|
|
270
|
+
|
|
271
|
+
log_error(create_log_entry(
|
|
272
|
+
filename,
|
|
273
|
+
function_name,
|
|
274
|
+
error_message,
|
|
275
|
+
{
|
|
276
|
+
line_number: stack || 'unknown',
|
|
277
|
+
error: error_string,
|
|
278
|
+
options: {
|
|
279
|
+
to: options.to,
|
|
280
|
+
subject: options.subject,
|
|
281
|
+
has_text: !!options.content.text,
|
|
282
|
+
has_html: !!options.content.html,
|
|
283
|
+
attachments_count: options.attachments?.length || 0,
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
));
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
success: false,
|
|
290
|
+
error: error_message,
|
|
291
|
+
message: error_message,
|
|
292
|
+
raw_response: is_production ? undefined : {
|
|
293
|
+
error: error_string,
|
|
294
|
+
stack: stack,
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for the emailer service
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Emailer module types
|
|
6
|
+
export type EmailerModule = 'zeptoemail_api' | 'smtp' | 'pop3';
|
|
7
|
+
|
|
8
|
+
// Email content types
|
|
9
|
+
export interface EmailContent {
|
|
10
|
+
text?: string;
|
|
11
|
+
html?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Attachment interface
|
|
15
|
+
export interface EmailAttachment {
|
|
16
|
+
filename: string;
|
|
17
|
+
content: string; // Base64 encoded content
|
|
18
|
+
mime_type: string; // e.g., 'text/plain', 'application/pdf', 'image/png'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Email send options
|
|
22
|
+
export interface SendEmailOptions {
|
|
23
|
+
to: string | string[]; // Recipient email address(es)
|
|
24
|
+
subject: string; // Email subject
|
|
25
|
+
content: EmailContent; // Email content (text, html, or both)
|
|
26
|
+
attachments?: EmailAttachment[]; // Optional attachments
|
|
27
|
+
from?: string; // Override default from email
|
|
28
|
+
from_name?: string; // Override default from name
|
|
29
|
+
reply_to?: string; // Reply-to email address
|
|
30
|
+
cc?: string | string[]; // CC recipient(s)
|
|
31
|
+
bcc?: string | string[]; // BCC recipient(s)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Email configuration interface
|
|
35
|
+
export interface EmailerConfig {
|
|
36
|
+
emailer_module: EmailerModule;
|
|
37
|
+
// Zeptomail API configuration
|
|
38
|
+
zeptomail_api_endpoint?: string;
|
|
39
|
+
zeptomail_api_key?: string;
|
|
40
|
+
// Common configuration
|
|
41
|
+
from_email: string;
|
|
42
|
+
from_name: string;
|
|
43
|
+
reply_to_email?: string;
|
|
44
|
+
bounce_email?: string;
|
|
45
|
+
return_path_email?: string;
|
|
46
|
+
// Rate limiting configuration
|
|
47
|
+
rate_limit_requests?: number;
|
|
48
|
+
rate_limit_window?: number;
|
|
49
|
+
// Attachment limits
|
|
50
|
+
max_attachment_size?: number;
|
|
51
|
+
max_attachments?: number;
|
|
52
|
+
// Request timeout
|
|
53
|
+
request_timeout?: number;
|
|
54
|
+
// Input length limits
|
|
55
|
+
max_subject_length?: number;
|
|
56
|
+
max_body_length?: number;
|
|
57
|
+
// CORS configuration
|
|
58
|
+
cors_allowed_origins?: string;
|
|
59
|
+
// SMTP configuration (placeholder)
|
|
60
|
+
smtp_host?: string;
|
|
61
|
+
smtp_port?: number;
|
|
62
|
+
smtp_secure?: boolean;
|
|
63
|
+
smtp_auth_user?: string;
|
|
64
|
+
smtp_auth_pass?: string;
|
|
65
|
+
smtp_timeout?: number;
|
|
66
|
+
smtp_pool?: boolean;
|
|
67
|
+
// POP3 configuration (placeholder)
|
|
68
|
+
pop3_host?: string;
|
|
69
|
+
pop3_port?: number;
|
|
70
|
+
pop3_secure?: boolean;
|
|
71
|
+
pop3_auth_user?: string;
|
|
72
|
+
pop3_auth_pass?: string;
|
|
73
|
+
pop3_timeout?: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Email send response
|
|
77
|
+
export interface EmailSendResponse {
|
|
78
|
+
success: boolean;
|
|
79
|
+
message_id?: string;
|
|
80
|
+
message?: string;
|
|
81
|
+
raw_response?: Record<string, unknown> | string;
|
|
82
|
+
error?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Zeptomail API request data structure
|
|
86
|
+
export interface ZeptomailEmailAddress {
|
|
87
|
+
address: string;
|
|
88
|
+
name?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ZeptomailEmailAddressObject {
|
|
92
|
+
email_address: ZeptomailEmailAddress;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface ZeptomailAttachment {
|
|
96
|
+
name: string;
|
|
97
|
+
content: string;
|
|
98
|
+
mime_type: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface ZeptomailEmailData {
|
|
102
|
+
from: {
|
|
103
|
+
address: string;
|
|
104
|
+
name: string;
|
|
105
|
+
};
|
|
106
|
+
to: ZeptomailEmailAddressObject[];
|
|
107
|
+
subject: string;
|
|
108
|
+
textbody?: string;
|
|
109
|
+
htmlbody?: string;
|
|
110
|
+
reply_to?: Array<{ address: string }>;
|
|
111
|
+
cc?: ZeptomailEmailAddressObject[];
|
|
112
|
+
bcc?: ZeptomailEmailAddressObject[];
|
|
113
|
+
attachments?: ZeptomailAttachment[];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Provider interface
|
|
117
|
+
export interface EmailProvider {
|
|
118
|
+
send_email(options: SendEmailOptions, config: EmailerConfig): Promise<EmailSendResponse>;
|
|
119
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants for emailer service
|
|
3
|
+
* Centralized constants to avoid magic numbers and strings
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Rate limiting constants
|
|
7
|
+
export const DEFAULT_RATE_LIMIT_REQUESTS = 10; // requests per minute
|
|
8
|
+
export const DEFAULT_RATE_LIMIT_WINDOW = 60; // seconds
|
|
9
|
+
|
|
10
|
+
// Attachment limits
|
|
11
|
+
export const DEFAULT_MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024; // 10MB in bytes
|
|
12
|
+
export const DEFAULT_MAX_ATTACHMENTS = 10;
|
|
13
|
+
|
|
14
|
+
// Request timeout
|
|
15
|
+
export const DEFAULT_REQUEST_TIMEOUT = 30000; // 30 seconds in milliseconds
|
|
16
|
+
|
|
17
|
+
// Input length limits
|
|
18
|
+
export const MAX_SUBJECT_LENGTH = 255; // characters
|
|
19
|
+
export const MAX_BODY_LENGTH = 1024 * 1024; // 1MB in bytes
|
|
20
|
+
export const MAX_EMAIL_LENGTH = 254; // characters (RFC 5321)
|
|
21
|
+
|
|
22
|
+
// Valid emailer modules
|
|
23
|
+
export const VALID_EMAILER_MODULES = ['zeptoemail_api', 'smtp', 'pop3'] as const;
|
|
24
|
+
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized logging utility for emailer service
|
|
3
|
+
* Provides consistent JSON logging format across all components
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
interface LogEntry {
|
|
7
|
+
filename: string;
|
|
8
|
+
function_name: string;
|
|
9
|
+
line_number?: string | number;
|
|
10
|
+
message: string;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Log an error message
|
|
16
|
+
* @param entry - Log entry object
|
|
17
|
+
*/
|
|
18
|
+
export function log_error(entry: LogEntry): void {
|
|
19
|
+
console.error(JSON.stringify({
|
|
20
|
+
level: 'error',
|
|
21
|
+
timestamp: new Date().toISOString(),
|
|
22
|
+
...entry,
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Log an info message
|
|
28
|
+
* @param entry - Log entry object
|
|
29
|
+
*/
|
|
30
|
+
export function log_info(entry: LogEntry): void {
|
|
31
|
+
console.log(JSON.stringify({
|
|
32
|
+
level: 'info',
|
|
33
|
+
timestamp: new Date().toISOString(),
|
|
34
|
+
...entry,
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Log a warning message
|
|
40
|
+
* @param entry - Log entry object
|
|
41
|
+
*/
|
|
42
|
+
export function log_warning(entry: LogEntry): void {
|
|
43
|
+
console.warn(JSON.stringify({
|
|
44
|
+
level: 'warning',
|
|
45
|
+
timestamp: new Date().toISOString(),
|
|
46
|
+
...entry,
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create a log entry with standard fields
|
|
52
|
+
* @param filename - Name of the file
|
|
53
|
+
* @param function_name - Name of the function
|
|
54
|
+
* @param message - Log message
|
|
55
|
+
* @param additional_data - Additional data to include
|
|
56
|
+
* @returns Log entry object
|
|
57
|
+
*/
|
|
58
|
+
export function create_log_entry(
|
|
59
|
+
filename: string,
|
|
60
|
+
function_name: string,
|
|
61
|
+
message: string,
|
|
62
|
+
additional_data?: Record<string, unknown>
|
|
63
|
+
): LogEntry {
|
|
64
|
+
return {
|
|
65
|
+
filename,
|
|
66
|
+
function_name,
|
|
67
|
+
message,
|
|
68
|
+
...(additional_data || {}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation utilities for emailer service
|
|
3
|
+
* Provides email validation, sanitization, and input validation functions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { MAX_SUBJECT_LENGTH, MAX_BODY_LENGTH, MAX_EMAIL_LENGTH } from './constants';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Validate email address format
|
|
10
|
+
* @param email - Email address to validate
|
|
11
|
+
* @returns boolean indicating if email is valid
|
|
12
|
+
*/
|
|
13
|
+
export function validate_email_address(email: string): boolean {
|
|
14
|
+
if (!email || typeof email !== 'string') {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// RFC 5322 compliant regex (simplified)
|
|
19
|
+
const email_regex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
20
|
+
|
|
21
|
+
return email_regex.test(email) && email.length <= MAX_EMAIL_LENGTH;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Sanitize email header to prevent injection attacks
|
|
26
|
+
* Removes newlines, carriage returns, and other control characters
|
|
27
|
+
* @param value - Header value to sanitize
|
|
28
|
+
* @returns Sanitized header value
|
|
29
|
+
*/
|
|
30
|
+
export function sanitize_email_header(value: string): string {
|
|
31
|
+
if (!value || typeof value !== 'string') {
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Remove newlines, carriage returns, and other control characters
|
|
36
|
+
return value.replace(/[\r\n\t\0\x08\x0B\x0C\x1F]/g, '').trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Validate subject length
|
|
41
|
+
* @param subject - Email subject to validate
|
|
42
|
+
* @returns boolean indicating if subject is valid
|
|
43
|
+
*/
|
|
44
|
+
export function validate_subject_length(subject: string): boolean {
|
|
45
|
+
if (!subject || typeof subject !== 'string') {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return subject.length <= MAX_SUBJECT_LENGTH;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Validate body length
|
|
53
|
+
* @param body - Email body to validate
|
|
54
|
+
* @returns boolean indicating if body is valid
|
|
55
|
+
*/
|
|
56
|
+
export function validate_body_length(body: string): boolean {
|
|
57
|
+
if (!body || typeof body !== 'string') {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Calculate byte length (UTF-8 encoding)
|
|
62
|
+
const byte_length = Buffer.from(body, 'utf8').length;
|
|
63
|
+
return byte_length <= MAX_BODY_LENGTH;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Validate attachment size
|
|
68
|
+
* @param content - Base64 encoded attachment content
|
|
69
|
+
* @param max_size - Maximum size in bytes
|
|
70
|
+
* @returns boolean indicating if attachment size is valid
|
|
71
|
+
*/
|
|
72
|
+
export function validate_attachment_size(content: string, max_size: number): boolean {
|
|
73
|
+
if (!content || typeof content !== 'string') {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const size = Buffer.from(content, 'base64').length;
|
|
79
|
+
return size <= max_size;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
package/src/lib/index.ts
ADDED