strapi-plugin-magic-mail 2.2.3 → 2.2.5

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.
Files changed (70) hide show
  1. package/dist/server/index.js +1 -1
  2. package/dist/server/index.mjs +1 -1
  3. package/package.json +1 -3
  4. package/admin/jsconfig.json +0 -10
  5. package/admin/src/components/AddAccountModal.jsx +0 -1943
  6. package/admin/src/components/Initializer.jsx +0 -14
  7. package/admin/src/components/LicenseGuard.jsx +0 -475
  8. package/admin/src/components/PluginIcon.jsx +0 -5
  9. package/admin/src/hooks/useAuthRefresh.js +0 -44
  10. package/admin/src/hooks/useLicense.js +0 -158
  11. package/admin/src/index.js +0 -87
  12. package/admin/src/pages/Analytics.jsx +0 -762
  13. package/admin/src/pages/App.jsx +0 -111
  14. package/admin/src/pages/EmailDesigner/EditorPage.jsx +0 -1424
  15. package/admin/src/pages/EmailDesigner/TemplateList.jsx +0 -1807
  16. package/admin/src/pages/HomePage.jsx +0 -1170
  17. package/admin/src/pages/LicensePage.jsx +0 -430
  18. package/admin/src/pages/RoutingRules.jsx +0 -1141
  19. package/admin/src/pages/Settings.jsx +0 -603
  20. package/admin/src/pluginId.js +0 -3
  21. package/admin/src/translations/de.json +0 -71
  22. package/admin/src/translations/en.json +0 -70
  23. package/admin/src/translations/es.json +0 -71
  24. package/admin/src/translations/fr.json +0 -71
  25. package/admin/src/translations/pt.json +0 -71
  26. package/admin/src/utils/fetchWithRetry.js +0 -123
  27. package/admin/src/utils/getTranslation.js +0 -5
  28. package/admin/src/utils/theme.js +0 -85
  29. package/server/jsconfig.json +0 -10
  30. package/server/src/bootstrap.js +0 -157
  31. package/server/src/config/features.js +0 -260
  32. package/server/src/config/index.js +0 -9
  33. package/server/src/content-types/email-account/schema.json +0 -93
  34. package/server/src/content-types/email-event/index.js +0 -8
  35. package/server/src/content-types/email-event/schema.json +0 -57
  36. package/server/src/content-types/email-link/index.js +0 -8
  37. package/server/src/content-types/email-link/schema.json +0 -49
  38. package/server/src/content-types/email-log/index.js +0 -8
  39. package/server/src/content-types/email-log/schema.json +0 -106
  40. package/server/src/content-types/email-template/schema.json +0 -74
  41. package/server/src/content-types/email-template-version/schema.json +0 -60
  42. package/server/src/content-types/index.js +0 -33
  43. package/server/src/content-types/routing-rule/schema.json +0 -59
  44. package/server/src/controllers/accounts.js +0 -229
  45. package/server/src/controllers/analytics.js +0 -361
  46. package/server/src/controllers/controller.js +0 -26
  47. package/server/src/controllers/email-designer.js +0 -474
  48. package/server/src/controllers/index.js +0 -21
  49. package/server/src/controllers/license.js +0 -269
  50. package/server/src/controllers/oauth.js +0 -474
  51. package/server/src/controllers/routing-rules.js +0 -129
  52. package/server/src/controllers/test.js +0 -301
  53. package/server/src/destroy.js +0 -27
  54. package/server/src/index.js +0 -25
  55. package/server/src/middlewares/index.js +0 -3
  56. package/server/src/policies/index.js +0 -3
  57. package/server/src/register.js +0 -5
  58. package/server/src/routes/admin.js +0 -469
  59. package/server/src/routes/content-api.js +0 -37
  60. package/server/src/routes/index.js +0 -9
  61. package/server/src/services/account-manager.js +0 -329
  62. package/server/src/services/analytics.js +0 -512
  63. package/server/src/services/email-designer.js +0 -717
  64. package/server/src/services/email-router.js +0 -1446
  65. package/server/src/services/index.js +0 -17
  66. package/server/src/services/license-guard.js +0 -423
  67. package/server/src/services/oauth.js +0 -515
  68. package/server/src/services/service.js +0 -7
  69. package/server/src/utils/encryption.js +0 -81
  70. package/server/src/utils/logger.js +0 -84
@@ -1,512 +0,0 @@
1
- /**
2
- * Analytics Service
3
- * Handles email tracking, statistics, and user activity
4
- *
5
- * [SUCCESS] Migrated to strapi.documents() API (Strapi v5 Best Practice)
6
- */
7
-
8
- 'use strict';
9
-
10
- const crypto = require('crypto');
11
-
12
- // Content Type UIDs
13
- const EMAIL_LOG_UID = 'plugin::magic-mail.email-log';
14
- const EMAIL_EVENT_UID = 'plugin::magic-mail.email-event';
15
- const EMAIL_LINK_UID = 'plugin::magic-mail.email-link';
16
-
17
- module.exports = ({ strapi }) => ({
18
- /**
19
- * Generate unique email ID for tracking
20
- */
21
- generateEmailId() {
22
- return crypto.randomBytes(16).toString('hex');
23
- },
24
-
25
- /**
26
- * Generate secure hash for recipient (for tracking URLs)
27
- */
28
- generateRecipientHash(emailId, recipient) {
29
- return crypto
30
- .createHash('sha256')
31
- .update(`${emailId}-${recipient}-${process.env.APP_KEYS || 'secret'}`)
32
- .digest('hex')
33
- .substring(0, 16);
34
- },
35
-
36
- /**
37
- * Create email log entry
38
- */
39
- async createEmailLog(data) {
40
- const emailId = this.generateEmailId();
41
-
42
- const logEntry = await strapi.documents(EMAIL_LOG_UID).create({
43
- data: {
44
- emailId,
45
- user: data.userId || null,
46
- recipient: data.to,
47
- recipientName: data.recipientName || null,
48
- subject: data.subject,
49
- templateId: data.templateId || null,
50
- templateName: data.templateName || null,
51
- accountId: data.accountId || null,
52
- accountName: data.accountName || null,
53
- sentAt: new Date(),
54
- metadata: data.metadata || {},
55
- },
56
- });
57
-
58
- strapi.log.info(`[magic-mail] [SUCCESS] Email log created: ${emailId}`);
59
- if (data.templateId) {
60
- strapi.log.info(`[magic-mail] [INFO] Template tracked: ${data.templateName || 'Unknown'} (ID: ${data.templateId})`);
61
- }
62
- return logEntry;
63
- },
64
-
65
- /**
66
- * Record email open event
67
- */
68
- async recordOpen(emailId, recipientHash, req) {
69
- try {
70
- // Find email log using Document Service
71
- const emailLog = await strapi.documents(EMAIL_LOG_UID).findFirst({
72
- filters: { emailId },
73
- });
74
-
75
- if (!emailLog) {
76
- strapi.log.warn(`[magic-mail] Email log not found: ${emailId}`);
77
- return null;
78
- }
79
-
80
- // Verify recipient hash
81
- const validHash = this.generateRecipientHash(emailId, emailLog.recipient);
82
- if (recipientHash !== validHash) {
83
- strapi.log.warn(`[magic-mail] Invalid recipient hash for: ${emailId}`);
84
- return null;
85
- }
86
-
87
- const now = new Date();
88
-
89
- // Update email log counters using Document Service
90
- await strapi.documents(EMAIL_LOG_UID).update({
91
- documentId: emailLog.documentId,
92
- data: {
93
- openCount: (emailLog.openCount || 0) + 1,
94
- firstOpenedAt: emailLog.firstOpenedAt || now,
95
- lastOpenedAt: now,
96
- },
97
- });
98
-
99
- // Create event record
100
- const event = await strapi.documents(EMAIL_EVENT_UID).create({
101
- data: {
102
- emailLog: emailLog.documentId,
103
- type: 'open',
104
- timestamp: now,
105
- ipAddress: req.ip || req.headers['x-forwarded-for'] || null,
106
- userAgent: req.headers['user-agent'] || null,
107
- location: this.parseLocation(req),
108
- },
109
- });
110
-
111
- strapi.log.info(`[magic-mail] [EMAIL] Email opened: ${emailId} (count: ${(emailLog.openCount || 0) + 1})`);
112
- return event;
113
- } catch (error) {
114
- strapi.log.error('[magic-mail] Error recording open:', error);
115
- return null;
116
- }
117
- },
118
-
119
- /**
120
- * Record email click event
121
- */
122
- async recordClick(emailId, linkHash, recipientHash, targetUrl, req) {
123
- try {
124
- // Find email log using Document Service
125
- const emailLog = await strapi.documents(EMAIL_LOG_UID).findFirst({
126
- filters: { emailId },
127
- });
128
-
129
- if (!emailLog) {
130
- return null;
131
- }
132
-
133
- // Verify recipient hash
134
- const validHash = this.generateRecipientHash(emailId, emailLog.recipient);
135
- if (recipientHash !== validHash) {
136
- return null;
137
- }
138
-
139
- const now = new Date();
140
-
141
- // Update click count
142
- await strapi.documents(EMAIL_LOG_UID).update({
143
- documentId: emailLog.documentId,
144
- data: {
145
- clickCount: (emailLog.clickCount || 0) + 1,
146
- },
147
- });
148
-
149
- // Create event record
150
- const event = await strapi.documents(EMAIL_EVENT_UID).create({
151
- data: {
152
- emailLog: emailLog.documentId,
153
- type: 'click',
154
- timestamp: now,
155
- ipAddress: req.ip || req.headers['x-forwarded-for'] || null,
156
- userAgent: req.headers['user-agent'] || null,
157
- location: this.parseLocation(req),
158
- linkUrl: targetUrl,
159
- },
160
- });
161
-
162
- strapi.log.info(`[magic-mail] [CLICK] Link clicked: ${emailId} -> ${targetUrl}`);
163
- return event;
164
- } catch (error) {
165
- strapi.log.error('[magic-mail] Error recording click:', error);
166
- return null;
167
- }
168
- },
169
-
170
- /**
171
- * Get analytics statistics
172
- * Note: Document Service doesn't have count() - using findMany for counting
173
- */
174
- async getStats(filters = {}) {
175
- const baseFilters = {};
176
-
177
- // Filter by user relation - use documentId for Strapi v5
178
- if (filters.userId) {
179
- baseFilters.user = { documentId: filters.userId };
180
- }
181
- if (filters.templateId) {
182
- baseFilters.templateId = filters.templateId;
183
- }
184
- if (filters.accountId) {
185
- baseFilters.accountId = filters.accountId;
186
- }
187
- if (filters.dateFrom) {
188
- baseFilters.sentAt = { $gte: new Date(filters.dateFrom) };
189
- }
190
- if (filters.dateTo) {
191
- baseFilters.sentAt = { ...baseFilters.sentAt, $lte: new Date(filters.dateTo) };
192
- }
193
-
194
- // Use native count() method for efficient counting with filters
195
- const [totalSent, totalOpened, totalClicked, totalBounced] = await Promise.all([
196
- strapi.documents(EMAIL_LOG_UID).count({
197
- filters: baseFilters,
198
- }),
199
- strapi.documents(EMAIL_LOG_UID).count({
200
- filters: { ...baseFilters, openCount: { $gt: 0 } },
201
- }),
202
- strapi.documents(EMAIL_LOG_UID).count({
203
- filters: { ...baseFilters, clickCount: { $gt: 0 } },
204
- }),
205
- strapi.documents(EMAIL_LOG_UID).count({
206
- filters: { ...baseFilters, bounced: true },
207
- }),
208
- ]);
209
-
210
- const openRate = totalSent > 0 ? (totalOpened / totalSent) * 100 : 0;
211
- const clickRate = totalOpened > 0 ? (totalClicked / totalOpened) * 100 : 0;
212
- const bounceRate = totalSent > 0 ? (totalBounced / totalSent) * 100 : 0;
213
-
214
- return {
215
- totalSent,
216
- totalOpened,
217
- totalClicked,
218
- totalBounced,
219
- openRate: Math.round(openRate * 10) / 10,
220
- clickRate: Math.round(clickRate * 10) / 10,
221
- bounceRate: Math.round(bounceRate * 10) / 10,
222
- };
223
- },
224
-
225
- /**
226
- * Get email logs with pagination
227
- */
228
- async getEmailLogs(filters = {}, pagination = {}) {
229
- const where = {};
230
-
231
- // Filter by user relation - use documentId for Strapi v5
232
- if (filters.userId) {
233
- where.user = { documentId: filters.userId };
234
- }
235
- if (filters.templateId) {
236
- where.templateId = filters.templateId;
237
- }
238
- if (filters.search) {
239
- where.$or = [
240
- { recipient: { $containsi: filters.search } },
241
- { subject: { $containsi: filters.search } },
242
- { recipientName: { $containsi: filters.search } },
243
- ];
244
- }
245
-
246
- const page = pagination.page || 1;
247
- const pageSize = pagination.pageSize || 25;
248
-
249
- const [logs, total] = await Promise.all([
250
- strapi.documents(EMAIL_LOG_UID).findMany({
251
- filters: where,
252
- sort: [{ sentAt: 'desc' }],
253
- limit: pageSize,
254
- offset: (page - 1) * pageSize,
255
- populate: ['user'],
256
- }),
257
- // Get total count using native count() method
258
- strapi.documents(EMAIL_LOG_UID).count({
259
- filters: where,
260
- }),
261
- ]);
262
-
263
- return {
264
- data: logs,
265
- pagination: {
266
- page,
267
- pageSize,
268
- pageCount: Math.ceil(total / pageSize),
269
- total,
270
- },
271
- };
272
- },
273
-
274
- /**
275
- * Get email log details with events
276
- */
277
- async getEmailLogDetails(emailId) {
278
- const emailLog = await strapi.documents(EMAIL_LOG_UID).findFirst({
279
- filters: { emailId },
280
- populate: ['user', 'events'],
281
- });
282
-
283
- return emailLog;
284
- },
285
-
286
- /**
287
- * Get user email activity
288
- */
289
- async getUserActivity(userId) {
290
- // Filter by user relation - use documentId for Strapi v5
291
- const emailLogs = await strapi.documents(EMAIL_LOG_UID).findMany({
292
- filters: { user: { documentId: userId } },
293
- sort: [{ sentAt: 'desc' }],
294
- limit: 50,
295
- });
296
-
297
- const stats = await this.getStats({ userId });
298
-
299
- return {
300
- stats,
301
- recentEmails: emailLogs,
302
- };
303
- },
304
-
305
- /**
306
- * Parse location from request (basic implementation)
307
- */
308
- parseLocation(req) {
309
- // You can integrate with a GeoIP service here
310
- return {
311
- ip: req.ip || req.headers['x-forwarded-for'] || null,
312
- // country: null,
313
- // city: null,
314
- };
315
- },
316
-
317
- /**
318
- * Inject tracking pixel into HTML
319
- */
320
- injectTrackingPixel(html, emailId, recipientHash) {
321
- // Use /api/ path for content-api routes (publicly accessible)
322
- const baseUrl = strapi.config.get('server.url') || 'http://localhost:1337';
323
-
324
- // Add random parameter to prevent email client caching
325
- // This ensures each email open loads the pixel fresh
326
- const randomToken = crypto.randomBytes(8).toString('hex');
327
- const trackingUrl = `${baseUrl}/api/magic-mail/track/open/${emailId}/${recipientHash}?r=${randomToken}`;
328
- const trackingPixel = `<img src="${trackingUrl}" width="1" height="1" style="display:none;" alt="" />`;
329
-
330
- strapi.log.info(`[magic-mail] 📍 Tracking pixel URL: ${trackingUrl}`);
331
-
332
- // Try to inject before </body>, otherwise append at the end
333
- if (html.includes('</body>')) {
334
- return html.replace('</body>', `${trackingPixel}</body>`);
335
- }
336
- return `${html}${trackingPixel}`;
337
- },
338
-
339
- /**
340
- * Rewrite links for click tracking
341
- */
342
- async rewriteLinksForTracking(html, emailId, recipientHash) {
343
- const baseUrl = strapi.config.get('server.url') || 'http://localhost:1337';
344
-
345
- // Get the email log for storing link associations
346
- const emailLog = await strapi.documents(EMAIL_LOG_UID).findFirst({
347
- filters: { emailId },
348
- });
349
-
350
- if (!emailLog) {
351
- strapi.log.error(`[magic-mail] Cannot rewrite links: Email log not found for ${emailId}`);
352
- return html;
353
- }
354
-
355
- // More flexible regex to find links, including those with newlines/whitespace in attributes
356
- const linkRegex = /<a\s+[^>]*href\s*=\s*["']([^"']+)["'][^>]*>/gis;
357
-
358
- // Collect all link mappings to store
359
- const linkMappings = [];
360
- const replacements = [];
361
-
362
- let linkCount = 0;
363
- let match;
364
-
365
- // First pass: collect all links and their replacements
366
- while ((match = linkRegex.exec(html)) !== null) {
367
- const fullMatch = match[0];
368
- const originalUrl = match[1];
369
-
370
- // Debug: Log what we found
371
- strapi.log.debug(`[magic-mail] [CHECK] Found link: ${originalUrl.substring(0, 100)}${originalUrl.length > 100 ? '...' : ''}`);
372
-
373
- // Skip if already a tracking link or anchor
374
- if (originalUrl.startsWith('#') || originalUrl.includes('/track/click/')) {
375
- strapi.log.debug(`[magic-mail] [SKIP] Skipping (anchor or already tracked)`);
376
- continue;
377
- }
378
-
379
- // Skip relative URLs without protocol (internal anchors, relative paths)
380
- if (!originalUrl.match(/^https?:\/\//i) && !originalUrl.startsWith('/')) {
381
- strapi.log.debug(`[magic-mail] [SKIP] Skipping relative URL: ${originalUrl}`);
382
- continue;
383
- }
384
-
385
- // Create link hash - hash the full URL including any query params
386
- const linkHash = crypto.createHash('md5').update(originalUrl).digest('hex').substring(0, 8);
387
-
388
- // Store for database insert
389
- linkMappings.push({
390
- linkHash,
391
- originalUrl,
392
- });
393
-
394
- // Create tracking URL WITHOUT the url query parameter (we'll look it up in the DB instead)
395
- const trackingUrl = `${baseUrl}/api/magic-mail/track/click/${emailId}/${linkHash}/${recipientHash}`;
396
-
397
- linkCount++;
398
- strapi.log.info(`[magic-mail] [LINK] Link ${linkCount}: ${originalUrl} → ${trackingUrl}`);
399
-
400
- // Store replacement info
401
- replacements.push({
402
- from: originalUrl,
403
- to: trackingUrl,
404
- });
405
- }
406
-
407
- // Store all link mappings in database
408
- for (const mapping of linkMappings) {
409
- try {
410
- await this.storeLinkMapping(emailLog.documentId, mapping.linkHash, mapping.originalUrl);
411
- } catch (err) {
412
- strapi.log.error('[magic-mail] Error storing link mapping:', err);
413
- }
414
- }
415
-
416
- // Apply all replacements
417
- let result = html;
418
- for (const replacement of replacements) {
419
- result = result.replace(replacement.from, replacement.to);
420
- }
421
-
422
- if (linkCount > 0) {
423
- strapi.log.info(`[magic-mail] [SUCCESS] Rewrote ${linkCount} links for click tracking`);
424
- } else {
425
- strapi.log.warn(`[magic-mail] [WARNING] No links found in email HTML for tracking!`);
426
- }
427
-
428
- return result;
429
- },
430
-
431
- /**
432
- * Store link mapping in database
433
- */
434
- async storeLinkMapping(emailLogDocId, linkHash, originalUrl) {
435
- try {
436
- // Check if link already exists - filter relation with documentId object
437
- const existing = await strapi.documents(EMAIL_LINK_UID).findFirst({
438
- filters: {
439
- emailLog: { documentId: emailLogDocId },
440
- linkHash,
441
- },
442
- });
443
-
444
- if (existing) {
445
- strapi.log.debug(`[magic-mail] Link mapping already exists for ${linkHash}`);
446
- return existing;
447
- }
448
-
449
- // Create new link mapping
450
- const linkMapping = await strapi.documents(EMAIL_LINK_UID).create({
451
- data: {
452
- emailLog: emailLogDocId,
453
- linkHash,
454
- originalUrl,
455
- clickCount: 0,
456
- },
457
- });
458
-
459
- strapi.log.debug(`[magic-mail] [SAVE] Stored link mapping: ${linkHash} → ${originalUrl}`);
460
- return linkMapping;
461
- } catch (error) {
462
- strapi.log.error('[magic-mail] Error storing link mapping:', error);
463
- throw error;
464
- }
465
- },
466
-
467
- /**
468
- * Get original URL from link hash
469
- */
470
- async getOriginalUrlFromHash(emailId, linkHash) {
471
- try {
472
- // Find the email log
473
- const emailLog = await strapi.documents(EMAIL_LOG_UID).findFirst({
474
- filters: { emailId },
475
- });
476
-
477
- if (!emailLog) {
478
- strapi.log.warn(`[magic-mail] Email log not found: ${emailId}`);
479
- return null;
480
- }
481
-
482
- // Find the link mapping - filter relation with documentId object (Strapi v5)
483
- const linkMapping = await strapi.documents(EMAIL_LINK_UID).findFirst({
484
- filters: {
485
- emailLog: { documentId: emailLog.documentId },
486
- linkHash,
487
- },
488
- });
489
-
490
- if (!linkMapping) {
491
- strapi.log.warn(`[magic-mail] Link mapping not found: ${emailId}/${linkHash}`);
492
- return null;
493
- }
494
-
495
- // Update click tracking on the link itself
496
- const now = new Date();
497
- await strapi.documents(EMAIL_LINK_UID).update({
498
- documentId: linkMapping.documentId,
499
- data: {
500
- clickCount: (linkMapping.clickCount || 0) + 1,
501
- firstClickedAt: linkMapping.firstClickedAt || now,
502
- lastClickedAt: now,
503
- },
504
- });
505
-
506
- return linkMapping.originalUrl;
507
- } catch (error) {
508
- strapi.log.error('[magic-mail] Error getting original URL:', error);
509
- return null;
510
- }
511
- },
512
- });