strapi-plugin-magic-mail 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/COPYRIGHT_NOTICE.txt +13 -0
- package/LICENSE +22 -0
- package/README.md +1420 -0
- package/admin/jsconfig.json +10 -0
- package/admin/src/components/AddAccountModal.jsx +1943 -0
- package/admin/src/components/Initializer.jsx +14 -0
- package/admin/src/components/LicenseGuard.jsx +475 -0
- package/admin/src/components/PluginIcon.jsx +5 -0
- package/admin/src/hooks/useAuthRefresh.js +44 -0
- package/admin/src/hooks/useLicense.js +158 -0
- package/admin/src/index.js +86 -0
- package/admin/src/pages/Analytics.jsx +762 -0
- package/admin/src/pages/App.jsx +111 -0
- package/admin/src/pages/EmailDesigner/EditorPage.jsx +1405 -0
- package/admin/src/pages/EmailDesigner/TemplateList.jsx +1807 -0
- package/admin/src/pages/HomePage.jsx +1233 -0
- package/admin/src/pages/LicensePage.jsx +424 -0
- package/admin/src/pages/RoutingRules.jsx +1141 -0
- package/admin/src/pages/Settings.jsx +603 -0
- package/admin/src/pluginId.js +3 -0
- package/admin/src/translations/de.json +71 -0
- package/admin/src/translations/en.json +70 -0
- package/admin/src/translations/es.json +71 -0
- package/admin/src/translations/fr.json +71 -0
- package/admin/src/translations/pt.json +71 -0
- package/admin/src/utils/fetchWithRetry.js +123 -0
- package/admin/src/utils/getTranslation.js +5 -0
- package/dist/_chunks/App-B-Gp4Vbr.js +7568 -0
- package/dist/_chunks/App-BymMjoGM.mjs +7543 -0
- package/dist/_chunks/LicensePage-Bl02myMx.mjs +342 -0
- package/dist/_chunks/LicensePage-CJXwPnEe.js +344 -0
- package/dist/_chunks/Settings-C_TmKwcz.mjs +400 -0
- package/dist/_chunks/Settings-zuFQ3pnn.js +402 -0
- package/dist/_chunks/de-CN-G9j1S.js +64 -0
- package/dist/_chunks/de-DS04rP54.mjs +64 -0
- package/dist/_chunks/en-BDc7Jk8u.js +64 -0
- package/dist/_chunks/en-BEFQJXvR.mjs +64 -0
- package/dist/_chunks/es-BpV1MIdm.js +64 -0
- package/dist/_chunks/es-DQHwzPpP.mjs +64 -0
- package/dist/_chunks/fr-BG1WfEVm.mjs +64 -0
- package/dist/_chunks/fr-vpziIpRp.js +64 -0
- package/dist/_chunks/pt-CMoGrOib.mjs +64 -0
- package/dist/_chunks/pt-ODpAhDNa.js +64 -0
- package/dist/admin/index.js +89 -0
- package/dist/admin/index.mjs +90 -0
- package/dist/server/index.js +6214 -0
- package/dist/server/index.mjs +6208 -0
- package/package.json +113 -0
- package/server/jsconfig.json +10 -0
- package/server/src/bootstrap.js +153 -0
- package/server/src/config/features.js +260 -0
- package/server/src/config/index.js +6 -0
- package/server/src/content-types/email-account/schema.json +93 -0
- package/server/src/content-types/email-event/index.js +8 -0
- package/server/src/content-types/email-event/schema.json +57 -0
- package/server/src/content-types/email-link/index.js +8 -0
- package/server/src/content-types/email-link/schema.json +49 -0
- package/server/src/content-types/email-log/index.js +8 -0
- package/server/src/content-types/email-log/schema.json +106 -0
- package/server/src/content-types/email-template/schema.json +74 -0
- package/server/src/content-types/email-template-version/schema.json +60 -0
- package/server/src/content-types/index.js +33 -0
- package/server/src/content-types/routing-rule/schema.json +59 -0
- package/server/src/controllers/accounts.js +220 -0
- package/server/src/controllers/analytics.js +347 -0
- package/server/src/controllers/controller.js +26 -0
- package/server/src/controllers/email-designer.js +474 -0
- package/server/src/controllers/index.js +21 -0
- package/server/src/controllers/license.js +267 -0
- package/server/src/controllers/oauth.js +474 -0
- package/server/src/controllers/routing-rules.js +122 -0
- package/server/src/controllers/test.js +383 -0
- package/server/src/destroy.js +23 -0
- package/server/src/index.js +25 -0
- package/server/src/middlewares/index.js +3 -0
- package/server/src/policies/index.js +3 -0
- package/server/src/register.js +5 -0
- package/server/src/routes/admin.js +469 -0
- package/server/src/routes/content-api.js +37 -0
- package/server/src/routes/index.js +9 -0
- package/server/src/services/account-manager.js +277 -0
- package/server/src/services/analytics.js +496 -0
- package/server/src/services/email-designer.js +870 -0
- package/server/src/services/email-router.js +1420 -0
- package/server/src/services/index.js +17 -0
- package/server/src/services/license-guard.js +418 -0
- package/server/src/services/oauth.js +515 -0
- package/server/src/services/service.js +7 -0
- package/server/src/utils/encryption.js +81 -0
- package/strapi-admin.js +4 -0
- package/strapi-server.js +4 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics Controller
|
|
3
|
+
* Handles tracking endpoints and analytics API
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
module.exports = ({ strapi }) => ({
|
|
9
|
+
/**
|
|
10
|
+
* Tracking pixel endpoint
|
|
11
|
+
* GET /magic-mail/track/open/:emailId/:recipientHash
|
|
12
|
+
*/
|
|
13
|
+
async trackOpen(ctx) {
|
|
14
|
+
const { emailId, recipientHash } = ctx.params;
|
|
15
|
+
|
|
16
|
+
// Record the open event
|
|
17
|
+
await strapi.plugin('magic-mail').service('analytics').recordOpen(emailId, recipientHash, ctx.request);
|
|
18
|
+
|
|
19
|
+
// Return 1x1 transparent GIF
|
|
20
|
+
const pixel = Buffer.from(
|
|
21
|
+
'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
|
|
22
|
+
'base64'
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
ctx.type = 'image/gif';
|
|
26
|
+
ctx.body = pixel;
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Click tracking endpoint
|
|
31
|
+
* GET /magic-mail/track/click/:emailId/:linkHash/:recipientHash
|
|
32
|
+
*/
|
|
33
|
+
async trackClick(ctx) {
|
|
34
|
+
const { emailId, linkHash, recipientHash } = ctx.params;
|
|
35
|
+
let { url } = ctx.query;
|
|
36
|
+
|
|
37
|
+
// Try to get URL from database if not in query string
|
|
38
|
+
if (!url) {
|
|
39
|
+
const analyticsService = strapi.plugin('magic-mail').service('analytics');
|
|
40
|
+
url = await analyticsService.getOriginalUrlFromHash(emailId, linkHash);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!url) {
|
|
44
|
+
return ctx.badRequest('Missing target URL');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Record the click event
|
|
48
|
+
await strapi
|
|
49
|
+
.plugin('magic-mail')
|
|
50
|
+
.service('analytics')
|
|
51
|
+
.recordClick(emailId, linkHash, recipientHash, url, ctx.request);
|
|
52
|
+
|
|
53
|
+
// Redirect to the original URL
|
|
54
|
+
ctx.redirect(url);
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get analytics statistics
|
|
59
|
+
* GET /magic-mail/analytics/stats
|
|
60
|
+
*/
|
|
61
|
+
async getStats(ctx) {
|
|
62
|
+
try {
|
|
63
|
+
const filters = {
|
|
64
|
+
userId: ctx.query.userId ? parseInt(ctx.query.userId) : null,
|
|
65
|
+
templateId: ctx.query.templateId ? parseInt(ctx.query.templateId) : null,
|
|
66
|
+
accountId: ctx.query.accountId ? parseInt(ctx.query.accountId) : null,
|
|
67
|
+
dateFrom: ctx.query.dateFrom || null,
|
|
68
|
+
dateTo: ctx.query.dateTo || null,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Remove null values
|
|
72
|
+
Object.keys(filters).forEach(key => filters[key] === null && delete filters[key]);
|
|
73
|
+
|
|
74
|
+
const stats = await strapi.plugin('magic-mail').service('analytics').getStats(filters);
|
|
75
|
+
|
|
76
|
+
return ctx.send({
|
|
77
|
+
success: true,
|
|
78
|
+
data: stats,
|
|
79
|
+
});
|
|
80
|
+
} catch (error) {
|
|
81
|
+
ctx.throw(500, error);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get email logs
|
|
87
|
+
* GET /magic-mail/analytics/emails
|
|
88
|
+
*/
|
|
89
|
+
async getEmailLogs(ctx) {
|
|
90
|
+
try {
|
|
91
|
+
const filters = {
|
|
92
|
+
userId: ctx.query.userId ? parseInt(ctx.query.userId) : null,
|
|
93
|
+
templateId: ctx.query.templateId ? parseInt(ctx.query.templateId) : null,
|
|
94
|
+
search: ctx.query.search || null,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const pagination = {
|
|
98
|
+
page: ctx.query.page ? parseInt(ctx.query.page) : 1,
|
|
99
|
+
pageSize: ctx.query.pageSize ? parseInt(ctx.query.pageSize) : 25,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Remove null values
|
|
103
|
+
Object.keys(filters).forEach(key => filters[key] === null && delete filters[key]);
|
|
104
|
+
|
|
105
|
+
const result = await strapi
|
|
106
|
+
.plugin('magic-mail')
|
|
107
|
+
.service('analytics')
|
|
108
|
+
.getEmailLogs(filters, pagination);
|
|
109
|
+
|
|
110
|
+
return ctx.send({
|
|
111
|
+
success: true,
|
|
112
|
+
...result,
|
|
113
|
+
});
|
|
114
|
+
} catch (error) {
|
|
115
|
+
ctx.throw(500, error);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get email log details
|
|
121
|
+
* GET /magic-mail/analytics/emails/:emailId
|
|
122
|
+
*/
|
|
123
|
+
async getEmailDetails(ctx) {
|
|
124
|
+
try {
|
|
125
|
+
const { emailId } = ctx.params;
|
|
126
|
+
|
|
127
|
+
const emailLog = await strapi
|
|
128
|
+
.plugin('magic-mail')
|
|
129
|
+
.service('analytics')
|
|
130
|
+
.getEmailLogDetails(emailId);
|
|
131
|
+
|
|
132
|
+
if (!emailLog) {
|
|
133
|
+
return ctx.notFound('Email log not found');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return ctx.send({
|
|
137
|
+
success: true,
|
|
138
|
+
data: emailLog,
|
|
139
|
+
});
|
|
140
|
+
} catch (error) {
|
|
141
|
+
ctx.throw(500, error);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get user email activity
|
|
147
|
+
* GET /magic-mail/analytics/users/:userId
|
|
148
|
+
*/
|
|
149
|
+
async getUserActivity(ctx) {
|
|
150
|
+
try {
|
|
151
|
+
const { userId } = ctx.params;
|
|
152
|
+
|
|
153
|
+
const activity = await strapi
|
|
154
|
+
.plugin('magic-mail')
|
|
155
|
+
.service('analytics')
|
|
156
|
+
.getUserActivity(parseInt(userId));
|
|
157
|
+
|
|
158
|
+
return ctx.send({
|
|
159
|
+
success: true,
|
|
160
|
+
data: activity,
|
|
161
|
+
});
|
|
162
|
+
} catch (error) {
|
|
163
|
+
ctx.throw(500, error);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Debug Analytics - Check database state
|
|
169
|
+
* GET /magic-mail/analytics/debug
|
|
170
|
+
*/
|
|
171
|
+
async debug(ctx) {
|
|
172
|
+
try {
|
|
173
|
+
strapi.log.info('[magic-mail] 🔍 Running Analytics Debug...');
|
|
174
|
+
|
|
175
|
+
// Get email logs
|
|
176
|
+
const emailLogs = await strapi.db.query('plugin::magic-mail.email-log').findMany({
|
|
177
|
+
limit: 10,
|
|
178
|
+
orderBy: { sentAt: 'DESC' },
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Get email events
|
|
182
|
+
const emailEvents = await strapi.db.query('plugin::magic-mail.email-event').findMany({
|
|
183
|
+
limit: 20,
|
|
184
|
+
orderBy: { timestamp: 'DESC' },
|
|
185
|
+
populate: ['emailLog'],
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Get stats
|
|
189
|
+
const analyticsService = strapi.plugin('magic-mail').service('analytics');
|
|
190
|
+
const stats = await analyticsService.getStats();
|
|
191
|
+
|
|
192
|
+
// Get active accounts
|
|
193
|
+
const accounts = await strapi.entityService.findMany('plugin::magic-mail.email-account', {
|
|
194
|
+
filters: { isActive: true },
|
|
195
|
+
fields: ['id', 'name', 'provider', 'fromEmail', 'emailsSentToday', 'totalEmailsSent'],
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Generate sample tracking URLs
|
|
199
|
+
let sampleTrackingUrls = null;
|
|
200
|
+
if (emailLogs.length > 0) {
|
|
201
|
+
const testLog = emailLogs[0];
|
|
202
|
+
const testHash = analyticsService.generateRecipientHash(testLog.emailId, testLog.recipient);
|
|
203
|
+
|
|
204
|
+
const baseUrl = strapi.config.get('server.url') || 'http://localhost:1337';
|
|
205
|
+
sampleTrackingUrls = {
|
|
206
|
+
trackingPixel: `${baseUrl}/api/magic-mail/track/open/${testLog.emailId}/${testHash}`,
|
|
207
|
+
clickTracking: `${baseUrl}/api/magic-mail/track/click/${testLog.emailId}/test/${testHash}?url=https://example.com`,
|
|
208
|
+
emailId: testLog.emailId,
|
|
209
|
+
recipient: testLog.recipient,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return ctx.send({
|
|
214
|
+
success: true,
|
|
215
|
+
debug: {
|
|
216
|
+
timestamp: new Date().toISOString(),
|
|
217
|
+
stats,
|
|
218
|
+
emailLogsCount: emailLogs.length,
|
|
219
|
+
emailEventsCount: emailEvents.length,
|
|
220
|
+
activeAccountsCount: accounts.length,
|
|
221
|
+
recentEmailLogs: emailLogs.map(log => ({
|
|
222
|
+
emailId: log.emailId,
|
|
223
|
+
recipient: log.recipient,
|
|
224
|
+
subject: log.subject,
|
|
225
|
+
sentAt: log.sentAt,
|
|
226
|
+
openCount: log.openCount,
|
|
227
|
+
clickCount: log.clickCount,
|
|
228
|
+
firstOpenedAt: log.firstOpenedAt,
|
|
229
|
+
accountName: log.accountName,
|
|
230
|
+
templateName: log.templateName,
|
|
231
|
+
})),
|
|
232
|
+
recentEvents: emailEvents.map(event => ({
|
|
233
|
+
type: event.type,
|
|
234
|
+
timestamp: event.timestamp,
|
|
235
|
+
emailId: event.emailLog?.emailId,
|
|
236
|
+
ipAddress: event.ipAddress,
|
|
237
|
+
linkUrl: event.linkUrl,
|
|
238
|
+
})),
|
|
239
|
+
accounts,
|
|
240
|
+
sampleTrackingUrls,
|
|
241
|
+
notes: [
|
|
242
|
+
'If emailLogsCount is 0: Emails are not being tracked (check if enableTracking=true)',
|
|
243
|
+
'If openCount is 0: Tracking pixel not being loaded (check email HTML source)',
|
|
244
|
+
'Test tracking URLs should be publicly accessible without authentication',
|
|
245
|
+
'Check Strapi console logs for tracking events when opening emails',
|
|
246
|
+
],
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
} catch (error) {
|
|
250
|
+
strapi.log.error('[magic-mail] Debug error:', error);
|
|
251
|
+
ctx.throw(500, error);
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Delete single email log
|
|
257
|
+
* DELETE /magic-mail/analytics/emails/:emailId
|
|
258
|
+
*/
|
|
259
|
+
async deleteEmailLog(ctx) {
|
|
260
|
+
try {
|
|
261
|
+
const { emailId } = ctx.params;
|
|
262
|
+
|
|
263
|
+
// Find email log
|
|
264
|
+
const emailLog = await strapi.db.query('plugin::magic-mail.email-log').findOne({
|
|
265
|
+
where: { emailId },
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
if (!emailLog) {
|
|
269
|
+
return ctx.notFound('Email log not found');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Delete associated events first
|
|
273
|
+
await strapi.db.query('plugin::magic-mail.email-event').deleteMany({
|
|
274
|
+
where: { emailLog: emailLog.id },
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Delete email log
|
|
278
|
+
await strapi.db.query('plugin::magic-mail.email-log').delete({
|
|
279
|
+
where: { id: emailLog.id },
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
strapi.log.info(`[magic-mail] 🗑️ Deleted email log: ${emailId}`);
|
|
283
|
+
|
|
284
|
+
return ctx.send({
|
|
285
|
+
success: true,
|
|
286
|
+
message: 'Email log deleted successfully',
|
|
287
|
+
});
|
|
288
|
+
} catch (error) {
|
|
289
|
+
strapi.log.error('[magic-mail] Error deleting email log:', error);
|
|
290
|
+
ctx.throw(500, error);
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Clear all email logs
|
|
296
|
+
* DELETE /magic-mail/analytics/emails
|
|
297
|
+
*/
|
|
298
|
+
async clearAllEmailLogs(ctx) {
|
|
299
|
+
try {
|
|
300
|
+
// Optional: Add query params for filtered deletion
|
|
301
|
+
const { olderThan } = ctx.query; // e.g., ?olderThan=2024-01-01
|
|
302
|
+
|
|
303
|
+
const where = {};
|
|
304
|
+
if (olderThan) {
|
|
305
|
+
where.sentAt = { $lt: new Date(olderThan) };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Get all email logs to delete
|
|
309
|
+
const emailLogs = await strapi.db.query('plugin::magic-mail.email-log').findMany({
|
|
310
|
+
where,
|
|
311
|
+
select: ['id'],
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const emailLogIds = emailLogs.map(log => log.id);
|
|
315
|
+
|
|
316
|
+
if (emailLogIds.length === 0) {
|
|
317
|
+
return ctx.send({
|
|
318
|
+
success: true,
|
|
319
|
+
message: 'No email logs to delete',
|
|
320
|
+
deletedCount: 0,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Delete all associated events
|
|
325
|
+
await strapi.db.query('plugin::magic-mail.email-event').deleteMany({
|
|
326
|
+
where: { emailLog: { $in: emailLogIds } },
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Delete all email logs
|
|
330
|
+
await strapi.db.query('plugin::magic-mail.email-log').deleteMany({
|
|
331
|
+
where: { id: { $in: emailLogIds } },
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
strapi.log.info(`[magic-mail] 🗑️ Cleared ${emailLogIds.length} email logs`);
|
|
335
|
+
|
|
336
|
+
return ctx.send({
|
|
337
|
+
success: true,
|
|
338
|
+
message: `Successfully deleted ${emailLogIds.length} email log(s)`,
|
|
339
|
+
deletedCount: emailLogIds.length,
|
|
340
|
+
});
|
|
341
|
+
} catch (error) {
|
|
342
|
+
strapi.log.error('[magic-mail] Error clearing email logs:', error);
|
|
343
|
+
ctx.throw(500, error);
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Main Controller
|
|
5
|
+
* Handles email sending requests
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
/**
|
|
10
|
+
* Send email via MagicMail router
|
|
11
|
+
*/
|
|
12
|
+
async send(ctx) {
|
|
13
|
+
try {
|
|
14
|
+
const emailRouter = strapi.plugin('magic-mail').service('email-router');
|
|
15
|
+
const result = await emailRouter.send(ctx.request.body);
|
|
16
|
+
|
|
17
|
+
ctx.body = {
|
|
18
|
+
success: true,
|
|
19
|
+
...result,
|
|
20
|
+
};
|
|
21
|
+
} catch (err) {
|
|
22
|
+
strapi.log.error('[magic-mail] Error sending email:', err);
|
|
23
|
+
ctx.throw(500, err.message || 'Failed to send email');
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
};
|