hazo_notify 1.0.0 → 1.0.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/dist/lib/emailer/emailer.d.ts +19 -0
- package/dist/lib/emailer/emailer.d.ts.map +1 -0
- package/dist/lib/emailer/emailer.js +220 -0
- package/dist/lib/emailer/emailer.js.map +1 -0
- package/{src/lib/emailer/index.ts → dist/lib/emailer/index.d.ts} +1 -1
- package/dist/lib/emailer/index.d.ts.map +1 -0
- package/dist/lib/emailer/index.js +34 -0
- package/dist/lib/emailer/index.js.map +1 -0
- package/dist/lib/emailer/providers/index.d.ts +15 -0
- package/dist/lib/emailer/providers/index.d.ts.map +1 -0
- package/dist/lib/emailer/providers/index.js +36 -0
- package/dist/lib/emailer/providers/index.js.map +1 -0
- package/dist/lib/emailer/providers/pop3_provider.d.ts +15 -0
- package/dist/lib/emailer/providers/pop3_provider.d.ts.map +1 -0
- package/dist/lib/emailer/providers/pop3_provider.js +29 -0
- package/dist/lib/emailer/providers/pop3_provider.js.map +1 -0
- package/dist/lib/emailer/providers/smtp_provider.d.ts +15 -0
- package/dist/lib/emailer/providers/smtp_provider.d.ts.map +1 -0
- package/dist/lib/emailer/providers/smtp_provider.js +29 -0
- package/dist/lib/emailer/providers/smtp_provider.js.map +1 -0
- package/dist/lib/emailer/providers/zeptomail_provider.d.ts +15 -0
- package/dist/lib/emailer/providers/zeptomail_provider.d.ts.map +1 -0
- package/dist/lib/emailer/providers/zeptomail_provider.js +250 -0
- package/dist/lib/emailer/providers/zeptomail_provider.js.map +1 -0
- package/dist/lib/emailer/types.d.ts +94 -0
- package/dist/lib/emailer/types.d.ts.map +1 -0
- package/dist/lib/emailer/types.js +6 -0
- package/dist/lib/emailer/types.js.map +1 -0
- package/dist/lib/emailer/utils/constants.d.ts +14 -0
- package/dist/lib/emailer/utils/constants.d.ts.map +1 -0
- package/dist/lib/emailer/utils/constants.js +22 -0
- package/dist/lib/emailer/utils/constants.js.map +1 -0
- package/{src/lib/emailer/utils/index.ts → dist/lib/emailer/utils/index.d.ts} +1 -2
- package/dist/lib/emailer/utils/index.d.ts.map +1 -0
- package/dist/lib/emailer/utils/index.js +24 -0
- package/dist/lib/emailer/utils/index.js.map +1 -0
- package/dist/lib/emailer/utils/logger.d.ts +37 -0
- package/dist/lib/emailer/utils/logger.d.ts.map +1 -0
- package/dist/lib/emailer/utils/logger.js +60 -0
- package/dist/lib/emailer/utils/logger.js.map +1 -0
- package/dist/lib/emailer/utils/validation.d.ts +37 -0
- package/dist/lib/emailer/utils/validation.d.ts.map +1 -0
- package/dist/lib/emailer/utils/validation.js +81 -0
- package/dist/lib/emailer/utils/validation.js.map +1 -0
- package/{src/lib/index.ts → dist/lib/index.d.ts} +1 -1
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +22 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +9 -0
- package/dist/lib/utils.js.map +1 -0
- package/package.json +21 -7
- package/.cursor/rules/db_schema.mdc +0 -0
- package/.cursor/rules/design.mdc +0 -16
- package/.cursor/rules/general.mdc +0 -49
- package/components/emailer-html-editor.tsx +0 -94
- package/components/ui/button.tsx +0 -53
- package/components/ui/card.tsx +0 -78
- package/components/ui/input.tsx +0 -24
- package/components/ui/label.tsx +0 -21
- package/components/ui/sidebar.tsx +0 -121
- package/components/ui/spinner.tsx +0 -54
- package/components/ui/textarea.tsx +0 -23
- package/components/ui/tooltip.tsx +0 -30
- package/components.json +0 -20
- package/jest.config.js +0 -27
- package/jest.setup.js +0 -1
- package/next.config.js +0 -22
- package/postcss.config.js +0 -6
- package/src/app/api/hazo_notify/emailer/send/__tests__/route.test.ts +0 -227
- package/src/app/api/hazo_notify/emailer/send/route.ts +0 -537
- package/src/app/editor-00/page.tsx +0 -47
- package/src/app/globals.css +0 -69
- package/src/app/hazo_notify/emailer_test/layout.tsx +0 -53
- package/src/app/hazo_notify/emailer_test/page.tsx +0 -369
- package/src/app/hazo_notify/layout.tsx +0 -77
- package/src/app/hazo_notify/page.tsx +0 -12
- package/src/app/layout.tsx +0 -26
- package/src/app/page.tsx +0 -14
- package/src/components/blocks/editor-00/editor.tsx +0 -61
- package/src/components/blocks/editor-00/nodes.ts +0 -11
- package/src/components/blocks/editor-00/plugins.tsx +0 -36
- package/src/components/editor/editor-ui/content-editable.tsx +0 -34
- package/src/components/editor/themes/editor-theme.css +0 -91
- package/src/components/editor/themes/editor-theme.ts +0 -130
- package/src/components/ui/button.tsx +0 -53
- package/src/components/ui/card.tsx +0 -78
- package/src/components/ui/input.tsx +0 -24
- package/src/components/ui/label.tsx +0 -21
- package/src/components/ui/sidebar.tsx +0 -121
- package/src/components/ui/spinner.tsx +0 -54
- package/src/components/ui/textarea.tsx +0 -23
- package/src/components/ui/tooltip.tsx +0 -30
- package/src/lib/emailer/__tests__/emailer.test.ts +0 -200
- package/src/lib/emailer/emailer.ts +0 -263
- package/src/lib/emailer/providers/__tests__/zeptomail_provider.test.ts +0 -196
- package/src/lib/emailer/providers/index.ts +0 -33
- package/src/lib/emailer/providers/pop3_provider.ts +0 -30
- package/src/lib/emailer/providers/smtp_provider.ts +0 -30
- package/src/lib/emailer/providers/zeptomail_provider.ts +0 -299
- package/src/lib/emailer/types.ts +0 -119
- package/src/lib/emailer/utils/constants.ts +0 -24
- package/src/lib/emailer/utils/logger.ts +0 -71
- package/src/lib/emailer/utils/validation.ts +0 -84
- package/src/lib/utils.ts +0 -6
- package/tailwind.config.ts +0 -65
- package/tsconfig.json +0 -27
|
@@ -1,537 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* API route for sending emails
|
|
3
|
-
* Handles POST requests to send emails via the emailer service
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
7
|
-
import { send_email, SendEmailOptions, load_emailer_config } from '@/lib/emailer';
|
|
8
|
-
import { HazoConfig } from 'hazo_config';
|
|
9
|
-
import { join } from 'path';
|
|
10
|
-
import { log_error, log_info, create_log_entry } from '@/lib/emailer/utils/logger';
|
|
11
|
-
import { validate_email_address, validate_subject_length, validate_body_length, validate_attachment_size } from '@/lib/emailer/utils/validation';
|
|
12
|
-
import { DEFAULT_RATE_LIMIT_REQUESTS, DEFAULT_RATE_LIMIT_WINDOW, DEFAULT_MAX_ATTACHMENT_SIZE, DEFAULT_MAX_ATTACHMENTS } from '@/lib/emailer/utils/constants';
|
|
13
|
-
|
|
14
|
-
// In-memory rate limiting store
|
|
15
|
-
// In production, consider using Redis or a proper rate limiting service
|
|
16
|
-
interface RateLimitEntry {
|
|
17
|
-
count: number;
|
|
18
|
-
reset_time: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const rate_limit_store = new Map<string, RateLimitEntry>();
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Check rate limit for an IP address
|
|
25
|
-
* @param ip - IP address to check
|
|
26
|
-
* @param max_requests - Maximum requests allowed
|
|
27
|
-
* @param window_seconds - Time window in seconds
|
|
28
|
-
* @returns true if within limit, false if rate limited
|
|
29
|
-
*/
|
|
30
|
-
function check_rate_limit(ip: string, max_requests: number, window_seconds: number): boolean {
|
|
31
|
-
const now = Date.now();
|
|
32
|
-
const entry = rate_limit_store.get(ip);
|
|
33
|
-
|
|
34
|
-
if (!entry || now > entry.reset_time) {
|
|
35
|
-
// Create new entry or reset expired entry
|
|
36
|
-
rate_limit_store.set(ip, {
|
|
37
|
-
count: 1,
|
|
38
|
-
reset_time: now + (window_seconds * 1000),
|
|
39
|
-
});
|
|
40
|
-
return true;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (entry.count >= max_requests) {
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
entry.count++;
|
|
48
|
-
return true;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Get client IP address from request
|
|
53
|
-
* @param request - Next.js request object
|
|
54
|
-
* @returns IP address string
|
|
55
|
-
*/
|
|
56
|
-
function get_client_ip(request: NextRequest): string {
|
|
57
|
-
// Try various headers for IP address
|
|
58
|
-
const forwarded = request.headers.get('x-forwarded-for');
|
|
59
|
-
if (forwarded) {
|
|
60
|
-
return forwarded.split(',')[0].trim();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const real_ip = request.headers.get('x-real-ip');
|
|
64
|
-
if (real_ip) {
|
|
65
|
-
return real_ip;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Fallback to a default if IP cannot be determined
|
|
69
|
-
return 'unknown';
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Get CORS headers based on configuration
|
|
74
|
-
* @param allowed_origins - Comma-separated list of allowed origins
|
|
75
|
-
* @param request_origin - Origin from request header
|
|
76
|
-
* @returns CORS headers object
|
|
77
|
-
*/
|
|
78
|
-
function get_cors_headers(allowed_origins: string, request_origin: string | null): Record<string, string> {
|
|
79
|
-
const headers: Record<string, string> = {};
|
|
80
|
-
|
|
81
|
-
if (!allowed_origins || allowed_origins.trim() === '') {
|
|
82
|
-
// Default: same-origin only (no CORS headers)
|
|
83
|
-
return headers;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const origins = allowed_origins.split(',').map(o => o.trim());
|
|
87
|
-
|
|
88
|
-
if (origins.includes('*')) {
|
|
89
|
-
// Allow all origins (not recommended for production)
|
|
90
|
-
headers['Access-Control-Allow-Origin'] = '*';
|
|
91
|
-
} else if (request_origin && origins.includes(request_origin)) {
|
|
92
|
-
// Allow specific origin
|
|
93
|
-
headers['Access-Control-Allow-Origin'] = request_origin;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (headers['Access-Control-Allow-Origin']) {
|
|
97
|
-
headers['Access-Control-Allow-Methods'] = 'POST, OPTIONS';
|
|
98
|
-
headers['Access-Control-Allow-Headers'] = 'Content-Type';
|
|
99
|
-
headers['Access-Control-Max-Age'] = '86400'; // 24 hours
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return headers;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Check if UI component is enabled
|
|
107
|
-
* @returns boolean indicating if UI is enabled
|
|
108
|
-
*/
|
|
109
|
-
function is_ui_enabled(): boolean {
|
|
110
|
-
try {
|
|
111
|
-
const config_file_path = join(process.cwd(), 'hazo_notify_config.ini');
|
|
112
|
-
const hazo_config = new HazoConfig({ filePath: config_file_path });
|
|
113
|
-
const ui_section = hazo_config.getSection('ui') || {};
|
|
114
|
-
const value: unknown = ui_section?.enable_ui;
|
|
115
|
-
|
|
116
|
-
// Check for various truthy values
|
|
117
|
-
return value === 'true' || value === true || value === '1' || value === 1;
|
|
118
|
-
} catch (error) {
|
|
119
|
-
// If config cannot be read, default to disabled for security
|
|
120
|
-
console.error('Error checking UI enabled status:', error);
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* OPTIONS handler for CORS preflight requests
|
|
127
|
-
*/
|
|
128
|
-
export async function OPTIONS(request: NextRequest) {
|
|
129
|
-
try {
|
|
130
|
-
const config = load_emailer_config();
|
|
131
|
-
const request_origin = request.headers.get('origin');
|
|
132
|
-
const cors_headers = get_cors_headers(config.cors_allowed_origins || '', request_origin);
|
|
133
|
-
|
|
134
|
-
return new NextResponse(null, {
|
|
135
|
-
status: 204,
|
|
136
|
-
headers: cors_headers,
|
|
137
|
-
});
|
|
138
|
-
} catch {
|
|
139
|
-
return new NextResponse(null, { status: 204 });
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* POST handler for sending emails
|
|
145
|
-
* @param request - Next.js request object
|
|
146
|
-
* @returns Next.js response with email send result
|
|
147
|
-
*/
|
|
148
|
-
export async function POST(request: NextRequest) {
|
|
149
|
-
const filename = 'route.ts';
|
|
150
|
-
const function_name = 'POST';
|
|
151
|
-
const route_path = '/api/hazo_notify/emailer/send';
|
|
152
|
-
const is_production = process.env.NODE_ENV === 'production';
|
|
153
|
-
const request_origin = request.headers.get('origin');
|
|
154
|
-
|
|
155
|
-
// Check if UI is enabled - if not, disable API route too
|
|
156
|
-
if (!is_ui_enabled()) {
|
|
157
|
-
// Try to get CORS headers even for disabled API
|
|
158
|
-
let cors_headers: Record<string, string> = {};
|
|
159
|
-
try {
|
|
160
|
-
const config = load_emailer_config();
|
|
161
|
-
cors_headers = get_cors_headers(config.cors_allowed_origins || '', request_origin);
|
|
162
|
-
} catch {
|
|
163
|
-
// Ignore config errors for disabled API
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return NextResponse.json(
|
|
167
|
-
{
|
|
168
|
-
success: false,
|
|
169
|
-
error: 'Emailer API is disabled',
|
|
170
|
-
message: 'Emailer API is disabled. Set enable_ui=true in hazo_notify_config.ini to enable.',
|
|
171
|
-
},
|
|
172
|
-
{
|
|
173
|
-
status: 403,
|
|
174
|
-
headers: cors_headers,
|
|
175
|
-
}
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Load configuration for rate limiting and CORS
|
|
180
|
-
let config;
|
|
181
|
-
try {
|
|
182
|
-
config = load_emailer_config();
|
|
183
|
-
} catch (error: unknown) {
|
|
184
|
-
const error_message = error instanceof Error ? error.message : 'Failed to load configuration';
|
|
185
|
-
log_error(create_log_entry(filename, function_name, error_message, { route_path }));
|
|
186
|
-
|
|
187
|
-
return NextResponse.json(
|
|
188
|
-
{
|
|
189
|
-
success: false,
|
|
190
|
-
error: is_production ? 'Internal server error' : error_message,
|
|
191
|
-
message: is_production ? 'Internal server error' : error_message,
|
|
192
|
-
},
|
|
193
|
-
{ status: 500 }
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Check rate limiting
|
|
198
|
-
const client_ip = get_client_ip(request);
|
|
199
|
-
const max_requests = config.rate_limit_requests || DEFAULT_RATE_LIMIT_REQUESTS;
|
|
200
|
-
const window_seconds = config.rate_limit_window || DEFAULT_RATE_LIMIT_WINDOW;
|
|
201
|
-
|
|
202
|
-
if (!check_rate_limit(client_ip, max_requests, window_seconds)) {
|
|
203
|
-
log_error(create_log_entry(
|
|
204
|
-
filename,
|
|
205
|
-
function_name,
|
|
206
|
-
'Rate limit exceeded',
|
|
207
|
-
{ route_path, client_ip, max_requests, window_seconds }
|
|
208
|
-
));
|
|
209
|
-
|
|
210
|
-
// Get CORS headers for rate limit response
|
|
211
|
-
const cors_headers = get_cors_headers(config.cors_allowed_origins || '', request_origin);
|
|
212
|
-
|
|
213
|
-
return NextResponse.json(
|
|
214
|
-
{
|
|
215
|
-
success: false,
|
|
216
|
-
error: 'Rate limit exceeded',
|
|
217
|
-
message: `Too many requests. Maximum ${max_requests} requests per ${window_seconds} seconds.`,
|
|
218
|
-
},
|
|
219
|
-
{
|
|
220
|
-
status: 429,
|
|
221
|
-
headers: cors_headers,
|
|
222
|
-
}
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Get CORS headers
|
|
227
|
-
const cors_headers = get_cors_headers(config.cors_allowed_origins || '', request_origin);
|
|
228
|
-
|
|
229
|
-
try {
|
|
230
|
-
// Parse request body with better error handling
|
|
231
|
-
let body: Record<string, unknown>;
|
|
232
|
-
try {
|
|
233
|
-
body = await request.json() as Record<string, unknown>;
|
|
234
|
-
} catch (json_error: unknown) {
|
|
235
|
-
const error_message = json_error instanceof Error ? json_error.message : 'Invalid JSON in request body';
|
|
236
|
-
log_error(create_log_entry(filename, function_name, error_message, { route_path }));
|
|
237
|
-
|
|
238
|
-
return NextResponse.json(
|
|
239
|
-
{
|
|
240
|
-
success: false,
|
|
241
|
-
error: 'Invalid JSON in request body',
|
|
242
|
-
message: 'Invalid JSON in request body',
|
|
243
|
-
},
|
|
244
|
-
{
|
|
245
|
-
status: 400,
|
|
246
|
-
headers: cors_headers,
|
|
247
|
-
}
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Validate request body
|
|
252
|
-
if (!body.to) {
|
|
253
|
-
return NextResponse.json(
|
|
254
|
-
{
|
|
255
|
-
success: false,
|
|
256
|
-
error: 'Recipient email address(es) are required',
|
|
257
|
-
message: 'Recipient email address(es) are required',
|
|
258
|
-
},
|
|
259
|
-
{
|
|
260
|
-
status: 400,
|
|
261
|
-
headers: cors_headers,
|
|
262
|
-
}
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Validate email addresses
|
|
267
|
-
const to_emails = Array.isArray(body.to) ? body.to : [body.to];
|
|
268
|
-
for (const email of to_emails) {
|
|
269
|
-
if (typeof email !== 'string' || !validate_email_address(email)) {
|
|
270
|
-
return NextResponse.json(
|
|
271
|
-
{
|
|
272
|
-
success: false,
|
|
273
|
-
error: `Invalid recipient email address: ${email}`,
|
|
274
|
-
message: `Invalid recipient email address: ${email}`,
|
|
275
|
-
},
|
|
276
|
-
{
|
|
277
|
-
status: 400,
|
|
278
|
-
headers: cors_headers,
|
|
279
|
-
}
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (!body.subject || typeof body.subject !== 'string') {
|
|
285
|
-
return NextResponse.json(
|
|
286
|
-
{
|
|
287
|
-
success: false,
|
|
288
|
-
error: 'Email subject is required',
|
|
289
|
-
message: 'Email subject is required',
|
|
290
|
-
},
|
|
291
|
-
{
|
|
292
|
-
status: 400,
|
|
293
|
-
headers: cors_headers,
|
|
294
|
-
}
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Validate subject length
|
|
299
|
-
if (!validate_subject_length(body.subject)) {
|
|
300
|
-
const max_length = config.max_subject_length || 255;
|
|
301
|
-
return NextResponse.json(
|
|
302
|
-
{
|
|
303
|
-
success: false,
|
|
304
|
-
error: `Email subject exceeds maximum length of ${max_length} characters`,
|
|
305
|
-
message: `Email subject exceeds maximum length of ${max_length} characters`,
|
|
306
|
-
},
|
|
307
|
-
{
|
|
308
|
-
status: 400,
|
|
309
|
-
headers: cors_headers,
|
|
310
|
-
}
|
|
311
|
-
);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (!body.content || (typeof body.content !== 'object')) {
|
|
315
|
-
return NextResponse.json(
|
|
316
|
-
{
|
|
317
|
-
success: false,
|
|
318
|
-
error: 'Email content (text or html) is required',
|
|
319
|
-
message: 'Email content (text or html) is required',
|
|
320
|
-
},
|
|
321
|
-
{
|
|
322
|
-
status: 400,
|
|
323
|
-
headers: cors_headers,
|
|
324
|
-
}
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const content = body.content as { text?: string; html?: string };
|
|
329
|
-
|
|
330
|
-
if (!content.text && !content.html) {
|
|
331
|
-
return NextResponse.json(
|
|
332
|
-
{
|
|
333
|
-
success: false,
|
|
334
|
-
error: 'Email content (text or html) is required',
|
|
335
|
-
message: 'Email content (text or html) is required',
|
|
336
|
-
},
|
|
337
|
-
{
|
|
338
|
-
status: 400,
|
|
339
|
-
headers: cors_headers,
|
|
340
|
-
}
|
|
341
|
-
);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// Validate body length
|
|
345
|
-
if (content.text && typeof content.text === 'string' && !validate_body_length(content.text)) {
|
|
346
|
-
const max_length = config.max_body_length || 1048576;
|
|
347
|
-
return NextResponse.json(
|
|
348
|
-
{
|
|
349
|
-
success: false,
|
|
350
|
-
error: `Email text body exceeds maximum size of ${max_length} bytes`,
|
|
351
|
-
message: `Email text body exceeds maximum size of ${max_length} bytes`,
|
|
352
|
-
},
|
|
353
|
-
{
|
|
354
|
-
status: 400,
|
|
355
|
-
headers: cors_headers,
|
|
356
|
-
}
|
|
357
|
-
);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (content.html && typeof content.html === 'string' && !validate_body_length(content.html)) {
|
|
361
|
-
const max_length = config.max_body_length || 1048576;
|
|
362
|
-
return NextResponse.json(
|
|
363
|
-
{
|
|
364
|
-
success: false,
|
|
365
|
-
error: `Email HTML body exceeds maximum size of ${max_length} bytes`,
|
|
366
|
-
message: `Email HTML body exceeds maximum size of ${max_length} bytes`,
|
|
367
|
-
},
|
|
368
|
-
{
|
|
369
|
-
status: 400,
|
|
370
|
-
headers: cors_headers,
|
|
371
|
-
}
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Validate attachments
|
|
376
|
-
if (body.attachments) {
|
|
377
|
-
if (!Array.isArray(body.attachments)) {
|
|
378
|
-
return NextResponse.json(
|
|
379
|
-
{
|
|
380
|
-
success: false,
|
|
381
|
-
error: 'Attachments must be an array',
|
|
382
|
-
message: 'Attachments must be an array',
|
|
383
|
-
},
|
|
384
|
-
{
|
|
385
|
-
status: 400,
|
|
386
|
-
headers: cors_headers,
|
|
387
|
-
}
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const max_attachments = config.max_attachments || DEFAULT_MAX_ATTACHMENTS;
|
|
392
|
-
if (body.attachments.length > max_attachments) {
|
|
393
|
-
return NextResponse.json(
|
|
394
|
-
{
|
|
395
|
-
success: false,
|
|
396
|
-
error: `Maximum ${max_attachments} attachments allowed`,
|
|
397
|
-
message: `Maximum ${max_attachments} attachments allowed`,
|
|
398
|
-
},
|
|
399
|
-
{
|
|
400
|
-
status: 400,
|
|
401
|
-
headers: cors_headers,
|
|
402
|
-
}
|
|
403
|
-
);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const max_size = config.max_attachment_size || DEFAULT_MAX_ATTACHMENT_SIZE;
|
|
407
|
-
for (const attachment of body.attachments) {
|
|
408
|
-
if (typeof attachment !== 'object' || !attachment || !('content' in attachment)) {
|
|
409
|
-
return NextResponse.json(
|
|
410
|
-
{
|
|
411
|
-
success: false,
|
|
412
|
-
error: 'Invalid attachment format',
|
|
413
|
-
message: 'Invalid attachment format',
|
|
414
|
-
},
|
|
415
|
-
{
|
|
416
|
-
status: 400,
|
|
417
|
-
headers: cors_headers,
|
|
418
|
-
}
|
|
419
|
-
);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const att = attachment as { content: string; filename?: string };
|
|
423
|
-
if (typeof att.content !== 'string' || !validate_attachment_size(att.content, max_size)) {
|
|
424
|
-
return NextResponse.json(
|
|
425
|
-
{
|
|
426
|
-
success: false,
|
|
427
|
-
error: `Attachment "${att.filename || 'unknown'}" exceeds maximum size of ${max_size} bytes`,
|
|
428
|
-
message: `Attachment "${att.filename || 'unknown'}" exceeds maximum size of ${max_size} bytes`,
|
|
429
|
-
},
|
|
430
|
-
{
|
|
431
|
-
status: 400,
|
|
432
|
-
headers: cors_headers,
|
|
433
|
-
}
|
|
434
|
-
);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// Prepare email options
|
|
440
|
-
const email_options: SendEmailOptions = {
|
|
441
|
-
to: body.to as string | string[],
|
|
442
|
-
subject: body.subject as string,
|
|
443
|
-
content: {
|
|
444
|
-
text: typeof content.text === 'string' ? content.text : undefined,
|
|
445
|
-
html: typeof content.html === 'string' ? content.html : undefined,
|
|
446
|
-
},
|
|
447
|
-
attachments: body.attachments as SendEmailOptions['attachments'],
|
|
448
|
-
from: typeof body.from === 'string' ? body.from : undefined,
|
|
449
|
-
from_name: typeof body.from_name === 'string' ? body.from_name : undefined,
|
|
450
|
-
reply_to: typeof body.reply_to === 'string' ? body.reply_to : undefined,
|
|
451
|
-
cc: body.cc as string | string[] | undefined,
|
|
452
|
-
bcc: body.bcc as string | string[] | undefined,
|
|
453
|
-
};
|
|
454
|
-
|
|
455
|
-
// Send email
|
|
456
|
-
const result = await send_email(email_options);
|
|
457
|
-
|
|
458
|
-
// Return response
|
|
459
|
-
if (result.success) {
|
|
460
|
-
return NextResponse.json(
|
|
461
|
-
{
|
|
462
|
-
success: true,
|
|
463
|
-
message_id: result.message_id,
|
|
464
|
-
message: result.message,
|
|
465
|
-
raw_response: result.raw_response,
|
|
466
|
-
},
|
|
467
|
-
{
|
|
468
|
-
status: 200,
|
|
469
|
-
headers: cors_headers,
|
|
470
|
-
}
|
|
471
|
-
);
|
|
472
|
-
} else {
|
|
473
|
-
// Log error
|
|
474
|
-
log_error(create_log_entry(
|
|
475
|
-
filename,
|
|
476
|
-
function_name,
|
|
477
|
-
result.error || 'Failed to send email',
|
|
478
|
-
{
|
|
479
|
-
route_path,
|
|
480
|
-
email_options: {
|
|
481
|
-
to: email_options.to,
|
|
482
|
-
subject: email_options.subject,
|
|
483
|
-
has_text: !!email_options.content.text,
|
|
484
|
-
has_html: !!email_options.content.html,
|
|
485
|
-
attachments_count: email_options.attachments?.length || 0,
|
|
486
|
-
},
|
|
487
|
-
response: result.raw_response,
|
|
488
|
-
}
|
|
489
|
-
));
|
|
490
|
-
|
|
491
|
-
return NextResponse.json(
|
|
492
|
-
{
|
|
493
|
-
success: false,
|
|
494
|
-
error: result.error || 'Failed to send email',
|
|
495
|
-
message: result.message,
|
|
496
|
-
raw_response: is_production ? undefined : result.raw_response,
|
|
497
|
-
},
|
|
498
|
-
{
|
|
499
|
-
status: 500,
|
|
500
|
-
headers: cors_headers,
|
|
501
|
-
}
|
|
502
|
-
);
|
|
503
|
-
}
|
|
504
|
-
} catch (error: unknown) {
|
|
505
|
-
// Log error
|
|
506
|
-
const error_message = error instanceof Error ? error.message : 'Internal server error';
|
|
507
|
-
const error_string = error instanceof Error ? error.toString() : String(error);
|
|
508
|
-
const stack = error instanceof Error ? error.stack : undefined;
|
|
509
|
-
|
|
510
|
-
log_error(create_log_entry(
|
|
511
|
-
filename,
|
|
512
|
-
function_name,
|
|
513
|
-
error_message,
|
|
514
|
-
{
|
|
515
|
-
line_number: stack || 'unknown',
|
|
516
|
-
route_path,
|
|
517
|
-
error: error_string,
|
|
518
|
-
}
|
|
519
|
-
));
|
|
520
|
-
|
|
521
|
-
return NextResponse.json(
|
|
522
|
-
{
|
|
523
|
-
success: false,
|
|
524
|
-
error: is_production ? 'Internal server error' : error_message,
|
|
525
|
-
message: is_production ? 'Internal server error' : error_message,
|
|
526
|
-
raw_response: is_production ? undefined : {
|
|
527
|
-
error: error_string,
|
|
528
|
-
stack: stack,
|
|
529
|
-
},
|
|
530
|
-
},
|
|
531
|
-
{
|
|
532
|
-
status: 500,
|
|
533
|
-
headers: cors_headers,
|
|
534
|
-
}
|
|
535
|
-
);
|
|
536
|
-
}
|
|
537
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { useState } from "react"
|
|
4
|
-
import { SerializedEditorState } from "lexical"
|
|
5
|
-
|
|
6
|
-
import { Editor } from "@/components/blocks/editor-00/editor"
|
|
7
|
-
|
|
8
|
-
const initialValue = {
|
|
9
|
-
root: {
|
|
10
|
-
children: [
|
|
11
|
-
{
|
|
12
|
-
children: [
|
|
13
|
-
{
|
|
14
|
-
detail: 0,
|
|
15
|
-
format: 0,
|
|
16
|
-
mode: "normal",
|
|
17
|
-
style: "",
|
|
18
|
-
text: "Hello World 🚀",
|
|
19
|
-
type: "text",
|
|
20
|
-
version: 1,
|
|
21
|
-
},
|
|
22
|
-
],
|
|
23
|
-
direction: "ltr",
|
|
24
|
-
format: "",
|
|
25
|
-
indent: 0,
|
|
26
|
-
type: "paragraph",
|
|
27
|
-
version: 1,
|
|
28
|
-
},
|
|
29
|
-
],
|
|
30
|
-
direction: "ltr",
|
|
31
|
-
format: "",
|
|
32
|
-
indent: 0,
|
|
33
|
-
type: "root",
|
|
34
|
-
version: 1,
|
|
35
|
-
},
|
|
36
|
-
} as unknown as SerializedEditorState
|
|
37
|
-
|
|
38
|
-
export default function EditorPage() {
|
|
39
|
-
const [editorState, setEditorState] =
|
|
40
|
-
useState<SerializedEditorState>(initialValue)
|
|
41
|
-
return (
|
|
42
|
-
<Editor
|
|
43
|
-
editorSerializedState={editorState}
|
|
44
|
-
onSerializedChange={(value) => setEditorState(value)}
|
|
45
|
-
/>
|
|
46
|
-
)
|
|
47
|
-
}
|
package/src/app/globals.css
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
@tailwind base;
|
|
2
|
-
@tailwind components;
|
|
3
|
-
@tailwind utilities;
|
|
4
|
-
|
|
5
|
-
@layer base {
|
|
6
|
-
:root {
|
|
7
|
-
--background: 0 0% 100%;
|
|
8
|
-
--foreground: 222.2 84% 4.9%;
|
|
9
|
-
--card: 0 0% 100%;
|
|
10
|
-
--card-foreground: 222.2 84% 4.9%;
|
|
11
|
-
--popover: 0 0% 100%;
|
|
12
|
-
--popover-foreground: 222.2 84% 4.9%;
|
|
13
|
-
--primary: 222.2 47.4% 11.2%;
|
|
14
|
-
--primary-foreground: 210 40% 98%;
|
|
15
|
-
--secondary: 210 40% 96.1%;
|
|
16
|
-
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
17
|
-
--muted: 210 40% 96.1%;
|
|
18
|
-
--muted-foreground: 215.4 16.3% 46.9%;
|
|
19
|
-
--accent: 210 40% 96.1%;
|
|
20
|
-
--accent-foreground: 222.2 47.4% 11.2%;
|
|
21
|
-
--destructive: 0 84.2% 60.2%;
|
|
22
|
-
--destructive-foreground: 210 40% 98%;
|
|
23
|
-
--border: 214.3 31.8% 91.4%;
|
|
24
|
-
--input: 214.3 31.8% 91.4%;
|
|
25
|
-
--ring: 222.2 84% 4.9%;
|
|
26
|
-
--chart-1: 12 76% 61%;
|
|
27
|
-
--chart-2: 173 58% 39%;
|
|
28
|
-
--chart-3: 197 37% 24%;
|
|
29
|
-
--chart-4: 43 74% 66%;
|
|
30
|
-
--chart-5: 27 87% 67%;
|
|
31
|
-
--radius: 0.5rem;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
.dark {
|
|
35
|
-
--background: 222.2 84% 4.9%;
|
|
36
|
-
--foreground: 210 40% 98%;
|
|
37
|
-
--card: 222.2 84% 4.9%;
|
|
38
|
-
--card-foreground: 210 40% 98%;
|
|
39
|
-
--popover: 222.2 84% 4.9%;
|
|
40
|
-
--popover-foreground: 210 40% 98%;
|
|
41
|
-
--primary: 210 40% 98%;
|
|
42
|
-
--primary-foreground: 222.2 47.4% 11.2%;
|
|
43
|
-
--secondary: 217.2 32.6% 17.5%;
|
|
44
|
-
--secondary-foreground: 210 40% 98%;
|
|
45
|
-
--muted: 217.2 32.6% 17.5%;
|
|
46
|
-
--muted-foreground: 215 20.2% 65.1%;
|
|
47
|
-
--accent: 217.2 32.6% 17.5%;
|
|
48
|
-
--accent-foreground: 210 40% 98%;
|
|
49
|
-
--destructive: 0 62.8% 30.6%;
|
|
50
|
-
--destructive-foreground: 210 40% 98%;
|
|
51
|
-
--border: 217.2 32.6% 17.5%;
|
|
52
|
-
--input: 217.2 32.6% 17.5%;
|
|
53
|
-
--ring: 212.7 26.8% 83.9%;
|
|
54
|
-
--chart-1: 220 70% 50%;
|
|
55
|
-
--chart-2: 160 60% 45%;
|
|
56
|
-
--chart-3: 30 80% 55%;
|
|
57
|
-
--chart-4: 280 65% 60%;
|
|
58
|
-
--chart-5: 340 75% 55%;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
@layer base {
|
|
63
|
-
* {
|
|
64
|
-
@apply border-border;
|
|
65
|
-
}
|
|
66
|
-
body {
|
|
67
|
-
@apply bg-background text-foreground;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Layout for emailer test page
|
|
3
|
-
* Checks if UI component is enabled in config
|
|
4
|
-
* If disabled, shows a message instead of the page content
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { HazoConfig } from 'hazo_config';
|
|
8
|
-
import { join } from 'path';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Check if UI component is enabled
|
|
12
|
-
* @returns boolean indicating if UI is enabled
|
|
13
|
-
*/
|
|
14
|
-
function is_ui_enabled(): boolean {
|
|
15
|
-
try {
|
|
16
|
-
const config_file_path = join(process.cwd(), 'hazo_notify_config.ini');
|
|
17
|
-
const hazo_config = new HazoConfig({ filePath: config_file_path });
|
|
18
|
-
const ui_section = hazo_config.getSection('ui') || {};
|
|
19
|
-
const value: unknown = ui_section?.enable_ui;
|
|
20
|
-
|
|
21
|
-
// Check for various truthy values
|
|
22
|
-
return value === 'true' || value === true || value === '1' || value === 1;
|
|
23
|
-
} catch (error) {
|
|
24
|
-
// If config cannot be read, default to disabled for security
|
|
25
|
-
console.error('Error checking UI enabled status:', error);
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export default function EmailerTestLayout({
|
|
31
|
-
children,
|
|
32
|
-
}: {
|
|
33
|
-
children: React.ReactNode;
|
|
34
|
-
}) {
|
|
35
|
-
const is_enabled = is_ui_enabled();
|
|
36
|
-
|
|
37
|
-
if (!is_enabled) {
|
|
38
|
-
return (
|
|
39
|
-
<div className="cls_emailer_test_disabled flex items-center justify-center h-full p-6">
|
|
40
|
-
<div className="cls_emailer_test_disabled_message text-center max-w-2xl">
|
|
41
|
-
<h1 className="cls_emailer_test_disabled_title text-2xl font-bold mb-4">
|
|
42
|
-
UI Component is Disabled
|
|
43
|
-
</h1>
|
|
44
|
-
<p className="cls_emailer_test_disabled_description text-muted-foreground">
|
|
45
|
-
To enable the UI component and all routes, set <code className="cls_emailer_test_disabled_code bg-muted px-2 py-1 rounded">enable_ui=true</code> in the <code className="cls_emailer_test_disabled_code bg-muted px-2 py-1 rounded">[ui]</code> section of <code className="cls_emailer_test_disabled_code bg-muted px-2 py-1 rounded">hazo_notify_config.ini</code>.
|
|
46
|
-
</p>
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return <>{children}</>;
|
|
53
|
-
}
|