strapi-plugin-magic-mail 2.2.4 → 2.2.6

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 (71) hide show
  1. package/README.md +0 -2
  2. package/dist/server/index.js +1 -1
  3. package/dist/server/index.mjs +1 -1
  4. package/package.json +1 -3
  5. package/admin/jsconfig.json +0 -10
  6. package/admin/src/components/AddAccountModal.jsx +0 -1943
  7. package/admin/src/components/Initializer.jsx +0 -14
  8. package/admin/src/components/LicenseGuard.jsx +0 -475
  9. package/admin/src/components/PluginIcon.jsx +0 -5
  10. package/admin/src/hooks/useAuthRefresh.js +0 -44
  11. package/admin/src/hooks/useLicense.js +0 -158
  12. package/admin/src/index.js +0 -87
  13. package/admin/src/pages/Analytics.jsx +0 -762
  14. package/admin/src/pages/App.jsx +0 -111
  15. package/admin/src/pages/EmailDesigner/EditorPage.jsx +0 -1424
  16. package/admin/src/pages/EmailDesigner/TemplateList.jsx +0 -1807
  17. package/admin/src/pages/HomePage.jsx +0 -1170
  18. package/admin/src/pages/LicensePage.jsx +0 -430
  19. package/admin/src/pages/RoutingRules.jsx +0 -1141
  20. package/admin/src/pages/Settings.jsx +0 -603
  21. package/admin/src/pluginId.js +0 -3
  22. package/admin/src/translations/de.json +0 -71
  23. package/admin/src/translations/en.json +0 -70
  24. package/admin/src/translations/es.json +0 -71
  25. package/admin/src/translations/fr.json +0 -71
  26. package/admin/src/translations/pt.json +0 -71
  27. package/admin/src/utils/fetchWithRetry.js +0 -123
  28. package/admin/src/utils/getTranslation.js +0 -5
  29. package/admin/src/utils/theme.js +0 -85
  30. package/server/jsconfig.json +0 -10
  31. package/server/src/bootstrap.js +0 -157
  32. package/server/src/config/features.js +0 -260
  33. package/server/src/config/index.js +0 -9
  34. package/server/src/content-types/email-account/schema.json +0 -93
  35. package/server/src/content-types/email-event/index.js +0 -8
  36. package/server/src/content-types/email-event/schema.json +0 -57
  37. package/server/src/content-types/email-link/index.js +0 -8
  38. package/server/src/content-types/email-link/schema.json +0 -49
  39. package/server/src/content-types/email-log/index.js +0 -8
  40. package/server/src/content-types/email-log/schema.json +0 -106
  41. package/server/src/content-types/email-template/schema.json +0 -74
  42. package/server/src/content-types/email-template-version/schema.json +0 -60
  43. package/server/src/content-types/index.js +0 -33
  44. package/server/src/content-types/routing-rule/schema.json +0 -59
  45. package/server/src/controllers/accounts.js +0 -229
  46. package/server/src/controllers/analytics.js +0 -361
  47. package/server/src/controllers/controller.js +0 -26
  48. package/server/src/controllers/email-designer.js +0 -474
  49. package/server/src/controllers/index.js +0 -21
  50. package/server/src/controllers/license.js +0 -269
  51. package/server/src/controllers/oauth.js +0 -474
  52. package/server/src/controllers/routing-rules.js +0 -129
  53. package/server/src/controllers/test.js +0 -301
  54. package/server/src/destroy.js +0 -27
  55. package/server/src/index.js +0 -25
  56. package/server/src/middlewares/index.js +0 -3
  57. package/server/src/policies/index.js +0 -3
  58. package/server/src/register.js +0 -5
  59. package/server/src/routes/admin.js +0 -469
  60. package/server/src/routes/content-api.js +0 -37
  61. package/server/src/routes/index.js +0 -9
  62. package/server/src/services/account-manager.js +0 -329
  63. package/server/src/services/analytics.js +0 -512
  64. package/server/src/services/email-designer.js +0 -717
  65. package/server/src/services/email-router.js +0 -1446
  66. package/server/src/services/index.js +0 -17
  67. package/server/src/services/license-guard.js +0 -423
  68. package/server/src/services/oauth.js +0 -515
  69. package/server/src/services/service.js +0 -7
  70. package/server/src/utils/encryption.js +0 -81
  71. 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
- });