@jhits/plugin-newsletter 0.0.10 → 0.0.11
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/package.json +3 -2
- package/src/api/email-utils.ts +165 -0
- package/src/api/handler.ts +28 -0
- package/src/api/handlers/index.ts +44 -0
- package/src/api/handlers/newsletters.ts +332 -0
- package/src/api/handlers/send-newsletter.ts +288 -0
- package/src/api/handlers/settings.ts +403 -0
- package/src/api/handlers/subscribers.ts +152 -0
- package/src/api/handlers/upload.ts +47 -0
- package/src/api/handlers/welcome-email.ts +210 -0
- package/src/api/router.ts +166 -0
- package/src/index.server.ts +12 -0
- package/src/index.tsx +353 -0
- package/src/index.tsx.patch +98 -0
- package/src/init.tsx +72 -0
- package/src/lib/blocks/BlockRenderer.tsx +125 -0
- package/src/lib/email/EmailRenderer.tsx +420 -0
- package/src/lib/email/index.ts +6 -0
- package/src/lib/i18n.ts +82 -0
- package/src/lib/mappers/apiMapper.ts +57 -0
- package/src/lib/utils/blockHelpers.ts +71 -0
- package/src/lib/utils/slugify.ts +43 -0
- package/src/registry/BlockRegistry.ts +53 -0
- package/src/registry/index.ts +5 -0
- package/src/state/EditorContext.tsx +278 -0
- package/src/state/index.ts +10 -0
- package/src/state/reducer.ts +561 -0
- package/src/state/types.ts +154 -0
- package/src/types/block.ts +275 -0
- package/src/types/newsletter.ts +152 -0
- package/src/types/registry.ts +14 -0
- package/src/views/CanvasEditor/BlockWrapper.tsx +143 -0
- package/src/views/CanvasEditor/CanvasEditorView.tsx +343 -0
- package/src/views/CanvasEditor/EditorBody.tsx +95 -0
- package/src/views/CanvasEditor/EditorHeader.tsx +255 -0
- package/src/views/CanvasEditor/components/CustomBlockItem.tsx +83 -0
- package/src/views/CanvasEditor/components/EditorCanvas.tsx +674 -0
- package/src/views/CanvasEditor/components/EditorLibrary.tsx +120 -0
- package/src/views/CanvasEditor/components/EditorSidebar.tsx +139 -0
- package/src/views/CanvasEditor/components/ErrorBanner.tsx +31 -0
- package/src/views/CanvasEditor/components/LibraryItem.tsx +71 -0
- package/src/views/CanvasEditor/components/SlashCommandDetector.tsx +196 -0
- package/src/views/CanvasEditor/components/SlashCommandMenu.tsx +131 -0
- package/src/views/CanvasEditor/components/index.ts +16 -0
- package/src/views/CanvasEditor/hooks/index.ts +7 -0
- package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.ts +136 -0
- package/src/views/CanvasEditor/hooks/useNewsletterLoader.ts +73 -0
- package/src/views/CanvasEditor/hooks/useRegisteredBlocks.ts +54 -0
- package/src/views/CanvasEditor/hooks/useSlashCommand.ts +106 -0
- package/src/views/CanvasEditor/index.ts +12 -0
- package/src/views/NewsletterEditor.tsx +42 -0
- package/src/views/NewsletterManager.tsx +483 -0
- package/src/views/SettingsView.tsx +216 -0
- package/src/views/SubscribersView.tsx +269 -0
- package/src/views/components/SendNewsletterModal.tsx +322 -0
- package/src/views/components/SmtpSettingsModal.tsx +433 -0
- package/src/views/components/TestEmailModal.tsx +268 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Send Newsletter API Handler
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
'use server';
|
|
6
|
+
|
|
7
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
8
|
+
import { NewsletterApiConfig } from '../../types/newsletter';
|
|
9
|
+
import nodemailer from 'nodemailer';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
|
|
13
|
+
const ObjectId = require('mongodb').ObjectId;
|
|
14
|
+
|
|
15
|
+
interface SmtpConfig {
|
|
16
|
+
host: string;
|
|
17
|
+
port: number;
|
|
18
|
+
user: string;
|
|
19
|
+
password: string;
|
|
20
|
+
from: string;
|
|
21
|
+
fromName: string;
|
|
22
|
+
logoUrl?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function getSmtpConfig(config: NewsletterApiConfig): Promise<SmtpConfig | null> {
|
|
26
|
+
const dbConnection = await config.getDb();
|
|
27
|
+
const db = dbConnection.db();
|
|
28
|
+
const settings = db.collection('settings');
|
|
29
|
+
const smtpConfig = await settings.findOne({ key: 'smtp' });
|
|
30
|
+
|
|
31
|
+
if (smtpConfig && smtpConfig.host) {
|
|
32
|
+
return {
|
|
33
|
+
host: smtpConfig.host,
|
|
34
|
+
port: smtpConfig.port || 465,
|
|
35
|
+
user: smtpConfig.user,
|
|
36
|
+
password: smtpConfig.password,
|
|
37
|
+
from: smtpConfig.from,
|
|
38
|
+
fromName: smtpConfig.fromName || '',
|
|
39
|
+
logoUrl: smtpConfig.logoUrl || '',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getNewsletterFilter(idOrSlug: string) {
|
|
46
|
+
if (ObjectId.isValid(idOrSlug) && new ObjectId(idOrSlug).toString() === idOrSlug) {
|
|
47
|
+
return { _id: new ObjectId(idOrSlug) };
|
|
48
|
+
}
|
|
49
|
+
return { slug: idOrSlug };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function POST_SEND_NEWSLETTER(
|
|
53
|
+
req: NextRequest,
|
|
54
|
+
idOrSlug: string,
|
|
55
|
+
config: NewsletterApiConfig
|
|
56
|
+
): Promise<NextResponse> {
|
|
57
|
+
try {
|
|
58
|
+
const userId = await config.getUserId?.(req);
|
|
59
|
+
if (!userId) {
|
|
60
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const body = await req.json();
|
|
64
|
+
const { testEmail, language = 'en' } = body;
|
|
65
|
+
const isTest = !!testEmail;
|
|
66
|
+
|
|
67
|
+
const dbConnection = await config.getDb();
|
|
68
|
+
const db = dbConnection.db();
|
|
69
|
+
const newsletters = db.collection('newsletters');
|
|
70
|
+
const subscribers = db.collection('subscribers');
|
|
71
|
+
|
|
72
|
+
const filter = getNewsletterFilter(idOrSlug);
|
|
73
|
+
const newsletter = await newsletters.findOne(filter);
|
|
74
|
+
|
|
75
|
+
if (!newsletter) {
|
|
76
|
+
return NextResponse.json(
|
|
77
|
+
{ error: 'Newsletter not found' },
|
|
78
|
+
{ status: 404 }
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const blocks = newsletter.blocks || [];
|
|
83
|
+
const metadata = newsletter.metadata || {};
|
|
84
|
+
|
|
85
|
+
if (!blocks || blocks.length === 0) {
|
|
86
|
+
return NextResponse.json(
|
|
87
|
+
{ error: 'Newsletter has no content' },
|
|
88
|
+
{ status: 400 }
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let smtpConfig = await getSmtpConfig(config);
|
|
93
|
+
|
|
94
|
+
if (!smtpConfig || !smtpConfig.host || !smtpConfig.user || !smtpConfig.from) {
|
|
95
|
+
return NextResponse.json(
|
|
96
|
+
{ error: 'SMTP not configured. Please configure SMTP settings first.' },
|
|
97
|
+
{ status: 400 }
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const transporter = nodemailer.createTransport({
|
|
102
|
+
host: smtpConfig.host,
|
|
103
|
+
port: smtpConfig.port,
|
|
104
|
+
secure: smtpConfig.port === 465,
|
|
105
|
+
auth: {
|
|
106
|
+
user: smtpConfig.user,
|
|
107
|
+
pass: smtpConfig.password,
|
|
108
|
+
},
|
|
109
|
+
connectionTimeout: 30000,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const baseUrl = config.baseUrl || 'http://localhost:3001';
|
|
113
|
+
|
|
114
|
+
let logoAttachment: any = undefined;
|
|
115
|
+
let logoSrc = smtpConfig.logoUrl || `${baseUrl}/logo_black.svg`;
|
|
116
|
+
|
|
117
|
+
if (smtpConfig.logoUrl && smtpConfig.logoUrl.startsWith('data:')) {
|
|
118
|
+
const matches = smtpConfig.logoUrl.match(/^data:([^;]+);base64,(.+)$/);
|
|
119
|
+
if (matches) {
|
|
120
|
+
const mimeType = matches[1];
|
|
121
|
+
const base64Data = matches[2];
|
|
122
|
+
const ext = mimeType === 'image/png' ? 'png' : 'jpg';
|
|
123
|
+
logoAttachment = {
|
|
124
|
+
filename: `logo.${ext}`,
|
|
125
|
+
content: Buffer.from(base64Data, 'base64'),
|
|
126
|
+
cid: 'logo',
|
|
127
|
+
contentType: mimeType,
|
|
128
|
+
};
|
|
129
|
+
logoSrc = 'cid:logo';
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const { generateNewsletterEmailHtml } = await import('../../lib/email/EmailRenderer');
|
|
134
|
+
|
|
135
|
+
const slugs: Record<string, string> = {
|
|
136
|
+
sv: '/avmälla',
|
|
137
|
+
nl: '/afmelden',
|
|
138
|
+
en: '/unsubscribe',
|
|
139
|
+
};
|
|
140
|
+
const slug = slugs[language] || slugs.en;
|
|
141
|
+
|
|
142
|
+
let recipientEmails: string[] = [];
|
|
143
|
+
let recipientCount = 0;
|
|
144
|
+
|
|
145
|
+
if (isTest) {
|
|
146
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
147
|
+
if (!emailRegex.test(testEmail)) {
|
|
148
|
+
return NextResponse.json({ error: 'Invalid test email address' }, { status: 400 });
|
|
149
|
+
}
|
|
150
|
+
recipientEmails = [testEmail];
|
|
151
|
+
recipientCount = 1;
|
|
152
|
+
} else {
|
|
153
|
+
const subscriberList = await subscribers.find({ status: 'active' }).toArray();
|
|
154
|
+
recipientEmails = subscriberList.map((s: any) => s.email);
|
|
155
|
+
recipientCount = recipientEmails.length;
|
|
156
|
+
|
|
157
|
+
if (recipientCount === 0) {
|
|
158
|
+
return NextResponse.json(
|
|
159
|
+
{ error: 'No active subscribers found' },
|
|
160
|
+
{ status: 400 }
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const subject = metadata.subject || 'Newsletter';
|
|
166
|
+
|
|
167
|
+
const recipientsWithUrls = await Promise.all(
|
|
168
|
+
recipientEmails.map(async (email) => {
|
|
169
|
+
const subscriber = isTest ? null : await subscribers.findOne({ email });
|
|
170
|
+
const subscriberLang = subscriber?.language || language;
|
|
171
|
+
const subscriberSlug = slugs[subscriberLang] || slugs.en;
|
|
172
|
+
const unsubscribeUrl = `${baseUrl}${subscriberSlug}?email=${encodeURIComponent(email)}`;
|
|
173
|
+
|
|
174
|
+
return { email, unsubscribeUrl };
|
|
175
|
+
})
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
let successCount = 0;
|
|
179
|
+
let failedCount = 0;
|
|
180
|
+
|
|
181
|
+
for (const { email, unsubscribeUrl } of recipientsWithUrls) {
|
|
182
|
+
try {
|
|
183
|
+
const html = generateNewsletterEmailHtml(
|
|
184
|
+
blocks,
|
|
185
|
+
{ subject: metadata.subject || '', previewText: metadata.previewText || '' },
|
|
186
|
+
{
|
|
187
|
+
baseUrl,
|
|
188
|
+
locale: language,
|
|
189
|
+
logoUrl: logoSrc,
|
|
190
|
+
unsubscribeUrl,
|
|
191
|
+
footerText: `© ${new Date().getFullYear()} ${smtpConfig.fromName || 'Botanics & You'}`,
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
await transporter.sendMail({
|
|
196
|
+
from: smtpConfig.fromName
|
|
197
|
+
? `"${smtpConfig.fromName}" <${smtpConfig.from}>`
|
|
198
|
+
: smtpConfig.from,
|
|
199
|
+
to: email,
|
|
200
|
+
subject,
|
|
201
|
+
html,
|
|
202
|
+
...(logoAttachment && {
|
|
203
|
+
attachments: [logoAttachment],
|
|
204
|
+
}),
|
|
205
|
+
});
|
|
206
|
+
successCount++;
|
|
207
|
+
} catch (err) {
|
|
208
|
+
console.error(`Failed to send email to ${email}:`, err);
|
|
209
|
+
failedCount++;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!isTest && successCount > 0) {
|
|
214
|
+
await newsletters.updateOne(filter, {
|
|
215
|
+
$set: {
|
|
216
|
+
'publication.status': 'sent',
|
|
217
|
+
'publication.sentDate': new Date().toISOString(),
|
|
218
|
+
'publication.recipientCount': successCount,
|
|
219
|
+
updatedAt: new Date().toISOString(),
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return NextResponse.json({
|
|
225
|
+
success: true,
|
|
226
|
+
message: isTest
|
|
227
|
+
? `Test newsletter sent to ${testEmail}`
|
|
228
|
+
: `Newsletter sent to ${successCount} subscriber${successCount !== 1 ? 's' : ''}${failedCount > 0 ? ` (${failedCount} failed)` : ''}`,
|
|
229
|
+
details: {
|
|
230
|
+
successCount,
|
|
231
|
+
failedCount,
|
|
232
|
+
totalRecipients: recipientCount,
|
|
233
|
+
isTest,
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
} catch (error: any) {
|
|
237
|
+
console.error('[NewsletterAPI] POST_SEND_NEWSLETTER error:', error);
|
|
238
|
+
return NextResponse.json(
|
|
239
|
+
{ error: 'Failed to send newsletter', detail: error.message },
|
|
240
|
+
{ status: 500 }
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function GET_NEWSLETTER_FOR_SEND(
|
|
246
|
+
req: NextRequest,
|
|
247
|
+
idOrSlug: string,
|
|
248
|
+
config: NewsletterApiConfig
|
|
249
|
+
): Promise<NextResponse> {
|
|
250
|
+
try {
|
|
251
|
+
const userId = await config.getUserId?.(req);
|
|
252
|
+
if (!userId) {
|
|
253
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const dbConnection = await config.getDb();
|
|
257
|
+
const db = dbConnection.db();
|
|
258
|
+
const newsletters = db.collection('newsletters');
|
|
259
|
+
const subscribers = db.collection('subscribers');
|
|
260
|
+
|
|
261
|
+
const filter = getNewsletterFilter(idOrSlug);
|
|
262
|
+
const newsletter = await newsletters.findOne(filter);
|
|
263
|
+
|
|
264
|
+
if (!newsletter) {
|
|
265
|
+
return NextResponse.json(
|
|
266
|
+
{ error: 'Newsletter not found' },
|
|
267
|
+
{ status: 404 }
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const subscriberCount = await subscribers.countDocuments({ status: 'active' });
|
|
272
|
+
|
|
273
|
+
return NextResponse.json({
|
|
274
|
+
id: newsletter._id?.toString() || newsletter.id,
|
|
275
|
+
title: newsletter.title,
|
|
276
|
+
subject: newsletter.metadata?.subject || '',
|
|
277
|
+
status: newsletter.publication?.status || 'draft',
|
|
278
|
+
hasContent: (newsletter.blocks?.length || 0) > 0,
|
|
279
|
+
subscriberCount,
|
|
280
|
+
});
|
|
281
|
+
} catch (error: any) {
|
|
282
|
+
console.error('[NewsletterAPI] GET_NEWSLETTER_FOR_SEND error:', error);
|
|
283
|
+
return NextResponse.json(
|
|
284
|
+
{ error: 'Failed to fetch newsletter', detail: error.message },
|
|
285
|
+
{ status: 500 }
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings API Handler
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
'use server';
|
|
6
|
+
|
|
7
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
8
|
+
import { NewsletterApiConfig, NewsletterSettings } from '../../types/newsletter';
|
|
9
|
+
import nodemailer from 'nodemailer';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import handlebars from 'handlebars';
|
|
13
|
+
import { getTestEmailTranslations } from '../../lib/i18n';
|
|
14
|
+
|
|
15
|
+
async function getWelcomeEmail(config: NewsletterApiConfig) {
|
|
16
|
+
const dbConnection = await config.getDb();
|
|
17
|
+
const db = dbConnection.db();
|
|
18
|
+
const newsletters = db.collection('newsletters');
|
|
19
|
+
|
|
20
|
+
const welcomeEmail = await newsletters.findOne({
|
|
21
|
+
type: 'welcome_email',
|
|
22
|
+
id: 'welcome_automation'
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return welcomeEmail || null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function GET_SETTINGS(
|
|
29
|
+
req: NextRequest,
|
|
30
|
+
config: NewsletterApiConfig
|
|
31
|
+
): Promise<NextResponse> {
|
|
32
|
+
try {
|
|
33
|
+
const userId = await config.getUserId?.(req);
|
|
34
|
+
if (!userId) {
|
|
35
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const dbConnection = await config.getDb();
|
|
39
|
+
const db = dbConnection.db();
|
|
40
|
+
const newsletters = db.collection('newsletters');
|
|
41
|
+
|
|
42
|
+
const settings = await newsletters.findOne({ id: 'welcome_automation' });
|
|
43
|
+
return NextResponse.json(settings || {
|
|
44
|
+
id: 'welcome_automation',
|
|
45
|
+
languages: {
|
|
46
|
+
nl: { title: '', message: '' },
|
|
47
|
+
en: { title: '', message: '' },
|
|
48
|
+
sv: { title: '', message: '' },
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
} catch (error: any) {
|
|
52
|
+
console.error('[NewsletterAPI] GET_SETTINGS error:', error);
|
|
53
|
+
return NextResponse.json(
|
|
54
|
+
{ error: 'Failed to fetch settings', detail: error.message },
|
|
55
|
+
{ status: 500 }
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function POST_SETTINGS(
|
|
61
|
+
req: NextRequest,
|
|
62
|
+
config: NewsletterApiConfig
|
|
63
|
+
): Promise<NextResponse> {
|
|
64
|
+
try {
|
|
65
|
+
const userId = await config.getUserId?.(req);
|
|
66
|
+
if (!userId) {
|
|
67
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const body = await req.json();
|
|
71
|
+
const dbConnection = await config.getDb();
|
|
72
|
+
const db = dbConnection.db();
|
|
73
|
+
const newsletters = db.collection('newsletters');
|
|
74
|
+
|
|
75
|
+
await newsletters.updateOne(
|
|
76
|
+
{ id: 'welcome_automation' },
|
|
77
|
+
{
|
|
78
|
+
$set: {
|
|
79
|
+
id: 'welcome_automation',
|
|
80
|
+
languages: body.languages || {},
|
|
81
|
+
updatedAt: new Date(),
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{ upsert: true }
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return NextResponse.json({ success: true, message: 'Settings updated successfully' });
|
|
88
|
+
} catch (error: any) {
|
|
89
|
+
console.error('[NewsletterAPI] POST_SETTINGS error:', error);
|
|
90
|
+
return NextResponse.json(
|
|
91
|
+
{ error: 'Failed to update settings', detail: error.message },
|
|
92
|
+
{ status: 500 }
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function GET_SMTP_SETTINGS(
|
|
98
|
+
req: NextRequest,
|
|
99
|
+
config: NewsletterApiConfig
|
|
100
|
+
): Promise<NextResponse> {
|
|
101
|
+
try {
|
|
102
|
+
const userId = await config.getUserId?.(req);
|
|
103
|
+
if (!userId) {
|
|
104
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const dbConnection = await config.getDb();
|
|
108
|
+
const db = dbConnection.db();
|
|
109
|
+
const settings = db.collection('settings');
|
|
110
|
+
|
|
111
|
+
const smtpConfig = await settings.findOne({ key: 'smtp' });
|
|
112
|
+
|
|
113
|
+
if (!smtpConfig) {
|
|
114
|
+
return NextResponse.json({
|
|
115
|
+
host: '',
|
|
116
|
+
port: 465,
|
|
117
|
+
user: '',
|
|
118
|
+
password: '',
|
|
119
|
+
from: '',
|
|
120
|
+
fromName: '',
|
|
121
|
+
primaryLanguage: 'en',
|
|
122
|
+
logoUrl: '',
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return NextResponse.json({
|
|
127
|
+
host: smtpConfig.host || '',
|
|
128
|
+
port: smtpConfig.port || 465,
|
|
129
|
+
user: smtpConfig.user || '',
|
|
130
|
+
password: smtpConfig.password || '',
|
|
131
|
+
from: smtpConfig.from || '',
|
|
132
|
+
fromName: smtpConfig.fromName || '',
|
|
133
|
+
primaryLanguage: smtpConfig.primaryLanguage || 'en',
|
|
134
|
+
logoUrl: smtpConfig.logoUrl || '',
|
|
135
|
+
});
|
|
136
|
+
} catch (error: any) {
|
|
137
|
+
console.error('[NewsletterAPI] GET_SMTP_SETTINGS error:', error);
|
|
138
|
+
return NextResponse.json(
|
|
139
|
+
{ error: 'Failed to fetch SMTP settings', detail: error.message },
|
|
140
|
+
{ status: 500 }
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function POST_SMTP_SETTINGS(
|
|
146
|
+
req: NextRequest,
|
|
147
|
+
config: NewsletterApiConfig
|
|
148
|
+
): Promise<NextResponse> {
|
|
149
|
+
try {
|
|
150
|
+
const userId = await config.getUserId?.(req);
|
|
151
|
+
if (!userId) {
|
|
152
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const body = await req.json();
|
|
156
|
+
|
|
157
|
+
if (!body.host || !body.user || !body.from) {
|
|
158
|
+
return NextResponse.json(
|
|
159
|
+
{ error: 'Host, user, and from address are required' },
|
|
160
|
+
{ status: 400 }
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const dbConnection = await config.getDb();
|
|
165
|
+
const db = dbConnection.db();
|
|
166
|
+
const settings = db.collection('settings');
|
|
167
|
+
|
|
168
|
+
await settings.updateOne(
|
|
169
|
+
{ key: 'smtp' },
|
|
170
|
+
{
|
|
171
|
+
$set: {
|
|
172
|
+
key: 'smtp',
|
|
173
|
+
host: body.host,
|
|
174
|
+
port: body.port || 465,
|
|
175
|
+
user: body.user,
|
|
176
|
+
password: body.password,
|
|
177
|
+
from: body.from,
|
|
178
|
+
fromName: body.fromName || '',
|
|
179
|
+
primaryLanguage: body.primaryLanguage || 'en',
|
|
180
|
+
logoUrl: body.logoUrl || '',
|
|
181
|
+
updatedAt: new Date(),
|
|
182
|
+
updatedBy: userId,
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
{ upsert: true }
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
return NextResponse.json({ success: true, message: 'SMTP settings saved successfully' });
|
|
189
|
+
} catch (error: any) {
|
|
190
|
+
console.error('[NewsletterAPI] POST_SMTP_SETTINGS error:', error);
|
|
191
|
+
return NextResponse.json(
|
|
192
|
+
{ error: 'Failed to save SMTP settings', detail: error.message },
|
|
193
|
+
{ status: 500 }
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function getSmtpConfigFromDb(config: NewsletterApiConfig): Promise<{ host: string; port: number; user: string; password: string; from: string; fromName: string; logoUrl: string } | null> {
|
|
199
|
+
return (async () => {
|
|
200
|
+
const dbConnection = await config.getDb();
|
|
201
|
+
const db = dbConnection.db();
|
|
202
|
+
const settings = db.collection('settings');
|
|
203
|
+
const smtpConfig = await settings.findOne({ key: 'smtp' });
|
|
204
|
+
|
|
205
|
+
if (smtpConfig && smtpConfig.host) {
|
|
206
|
+
return {
|
|
207
|
+
host: smtpConfig.host,
|
|
208
|
+
port: smtpConfig.port || 465,
|
|
209
|
+
user: smtpConfig.user,
|
|
210
|
+
password: smtpConfig.password,
|
|
211
|
+
from: smtpConfig.from,
|
|
212
|
+
fromName: smtpConfig.fromName || '',
|
|
213
|
+
logoUrl: smtpConfig.logoUrl || '',
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
})();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function POST_TEST_EMAIL(
|
|
221
|
+
req: NextRequest,
|
|
222
|
+
config: NewsletterApiConfig
|
|
223
|
+
): Promise<NextResponse> {
|
|
224
|
+
try {
|
|
225
|
+
const userId = await config.getUserId?.(req);
|
|
226
|
+
if (!userId) {
|
|
227
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const body = await req.json();
|
|
231
|
+
const { email, language = 'en', emailType = 'test' } = body;
|
|
232
|
+
|
|
233
|
+
if (!email) {
|
|
234
|
+
return NextResponse.json({ error: 'Email address is required' }, { status: 400 });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
238
|
+
if (!emailRegex.test(email)) {
|
|
239
|
+
return NextResponse.json({ error: 'Invalid email address' }, { status: 400 });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let smtpConfig = await getSmtpConfigFromDb(config);
|
|
243
|
+
|
|
244
|
+
if (!smtpConfig || !smtpConfig.host || !smtpConfig.user || !smtpConfig.from) {
|
|
245
|
+
return NextResponse.json(
|
|
246
|
+
{ error: 'SMTP not configured. Please configure SMTP settings first.' },
|
|
247
|
+
{ status: 400 }
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const transporter = nodemailer.createTransport({
|
|
252
|
+
host: smtpConfig.host,
|
|
253
|
+
port: smtpConfig.port,
|
|
254
|
+
secure: smtpConfig.port === 465,
|
|
255
|
+
auth: {
|
|
256
|
+
user: smtpConfig.user,
|
|
257
|
+
pass: smtpConfig.password,
|
|
258
|
+
},
|
|
259
|
+
connectionTimeout: 10000,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const logoPath = path.join(process.cwd(), 'node_modules', '@jhits', 'plugin-newsletter', 'templates', 'logo.png');
|
|
263
|
+
let logoExists = fs.existsSync(logoPath);
|
|
264
|
+
|
|
265
|
+
if (!logoExists) {
|
|
266
|
+
const altPath = path.join(__dirname, '..', '..', '..', '..', 'templates', 'logo.png');
|
|
267
|
+
logoExists = fs.existsSync(altPath);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const baseUrl = config.baseUrl || 'http://localhost:3001';
|
|
271
|
+
|
|
272
|
+
let logoAttachment: any = undefined;
|
|
273
|
+
let logoSrc = smtpConfig.logoUrl || `${baseUrl}/logo_black.svg`;
|
|
274
|
+
|
|
275
|
+
if (smtpConfig.logoUrl && smtpConfig.logoUrl.startsWith('data:')) {
|
|
276
|
+
const matches = smtpConfig.logoUrl.match(/^data:([^;]+);base64,(.+)$/);
|
|
277
|
+
if (matches) {
|
|
278
|
+
const mimeType = matches[1];
|
|
279
|
+
const base64Data = matches[2];
|
|
280
|
+
const ext = mimeType === 'image/png' ? 'png' : 'jpg';
|
|
281
|
+
logoAttachment = {
|
|
282
|
+
filename: `logo.${ext}`,
|
|
283
|
+
content: Buffer.from(base64Data, 'base64'),
|
|
284
|
+
cid: 'logo',
|
|
285
|
+
contentType: mimeType,
|
|
286
|
+
};
|
|
287
|
+
logoSrc = 'cid:logo';
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
let html: string;
|
|
292
|
+
let subject: string;
|
|
293
|
+
|
|
294
|
+
if (emailType === 'welcome') {
|
|
295
|
+
const dbConnection = await config.getDb();
|
|
296
|
+
const db = dbConnection.db();
|
|
297
|
+
const newsletters = db.collection('newsletters');
|
|
298
|
+
|
|
299
|
+
const welcomeEmail = await newsletters.findOne({
|
|
300
|
+
type: 'welcome_email',
|
|
301
|
+
id: 'welcome_automation'
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const languages = welcomeEmail?.languages as Record<string, { blocks: any[]; metadata: any }> | undefined;
|
|
305
|
+
const langContent = languages?.[language];
|
|
306
|
+
const defaultContent = languages?.en || { blocks: [], metadata: { subject: '' } };
|
|
307
|
+
const content = langContent || defaultContent;
|
|
308
|
+
|
|
309
|
+
const blocks = content.blocks;
|
|
310
|
+
const metadata = content.metadata;
|
|
311
|
+
|
|
312
|
+
if (!blocks || blocks.length === 0) {
|
|
313
|
+
return NextResponse.json(
|
|
314
|
+
{ error: 'Welcome email not configured for this language. Please configure it first.' },
|
|
315
|
+
{ status: 400 }
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const { generateNewsletterEmailHtml } = await import('../../lib/email/EmailRenderer');
|
|
320
|
+
|
|
321
|
+
const isDutch = language === 'nl';
|
|
322
|
+
const isSwedish = language === 'sv';
|
|
323
|
+
|
|
324
|
+
const slugs: Record<string, string> = {
|
|
325
|
+
sv: '/avmälla',
|
|
326
|
+
nl: '/afmelden',
|
|
327
|
+
en: '/unsubscribe',
|
|
328
|
+
};
|
|
329
|
+
const slug = slugs[language] || slugs.en;
|
|
330
|
+
const unsubscribeUrl = `${baseUrl}${slug}?email=${encodeURIComponent(email)}`;
|
|
331
|
+
|
|
332
|
+
html = generateNewsletterEmailHtml(
|
|
333
|
+
blocks,
|
|
334
|
+
{ subject: metadata?.subject || '' },
|
|
335
|
+
{
|
|
336
|
+
baseUrl,
|
|
337
|
+
locale: language,
|
|
338
|
+
logoUrl: logoSrc,
|
|
339
|
+
unsubscribeUrl,
|
|
340
|
+
footerText: `© ${new Date().getFullYear()} ${smtpConfig.fromName || 'Botanics & You'}`,
|
|
341
|
+
}
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
subject = metadata?.subject || (isDutch ? 'Welkom!' : isSwedish ? 'Välkommen!' : 'Welcome!');
|
|
345
|
+
} else {
|
|
346
|
+
const t = getTestEmailTranslations(language);
|
|
347
|
+
|
|
348
|
+
const templatePath = path.join(process.cwd(), 'node_modules', '@jhits', 'plugin-newsletter', 'templates', 'test-email.hbs');
|
|
349
|
+
let templateContent: string;
|
|
350
|
+
|
|
351
|
+
if (fs.existsSync(templatePath)) {
|
|
352
|
+
templateContent = fs.readFileSync(templatePath, 'utf-8');
|
|
353
|
+
} else {
|
|
354
|
+
const altPath = path.join(__dirname, '..', '..', '..', '..', 'templates', 'test-email.hbs');
|
|
355
|
+
if (fs.existsSync(altPath)) {
|
|
356
|
+
templateContent = fs.readFileSync(altPath, 'utf-8');
|
|
357
|
+
} else {
|
|
358
|
+
return NextResponse.json({ error: 'Email template not found' }, { status: 500 });
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const template = handlebars.compile(templateContent);
|
|
363
|
+
|
|
364
|
+
html = template({
|
|
365
|
+
recipientEmail: email,
|
|
366
|
+
fromName: smtpConfig.fromName || 'JHITS Newsletter',
|
|
367
|
+
currentYear: new Date().getFullYear(),
|
|
368
|
+
...t,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
subject = language === 'nl' ? 'Test e-mail - SMTP configuratie' : language === 'sv' ? 'Test e-post - SMTP-konfiguration' : 'Test Email - SMTP Configuration';
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
await transporter.sendMail({
|
|
375
|
+
from: smtpConfig.fromName
|
|
376
|
+
? `"${smtpConfig.fromName}" <${smtpConfig.from}>`
|
|
377
|
+
: smtpConfig.from,
|
|
378
|
+
to: email,
|
|
379
|
+
subject,
|
|
380
|
+
html,
|
|
381
|
+
...(emailType === 'test' && logoExists && {
|
|
382
|
+
attachments: [
|
|
383
|
+
{
|
|
384
|
+
filename: 'logo.png',
|
|
385
|
+
path: logoPath,
|
|
386
|
+
cid: 'logo',
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
}),
|
|
390
|
+
...(emailType === 'welcome' && logoAttachment && {
|
|
391
|
+
attachments: [logoAttachment],
|
|
392
|
+
}),
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
return NextResponse.json({ success: true, message: `Test email sent to ${email}` });
|
|
396
|
+
} catch (error: any) {
|
|
397
|
+
console.error('[NewsletterAPI] POST_TEST_EMAIL error:', error);
|
|
398
|
+
return NextResponse.json(
|
|
399
|
+
{ error: 'Failed to send test email', detail: error.message },
|
|
400
|
+
{ status: 500 }
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
}
|