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.
Files changed (91) hide show
  1. package/COPYRIGHT_NOTICE.txt +13 -0
  2. package/LICENSE +22 -0
  3. package/README.md +1420 -0
  4. package/admin/jsconfig.json +10 -0
  5. package/admin/src/components/AddAccountModal.jsx +1943 -0
  6. package/admin/src/components/Initializer.jsx +14 -0
  7. package/admin/src/components/LicenseGuard.jsx +475 -0
  8. package/admin/src/components/PluginIcon.jsx +5 -0
  9. package/admin/src/hooks/useAuthRefresh.js +44 -0
  10. package/admin/src/hooks/useLicense.js +158 -0
  11. package/admin/src/index.js +86 -0
  12. package/admin/src/pages/Analytics.jsx +762 -0
  13. package/admin/src/pages/App.jsx +111 -0
  14. package/admin/src/pages/EmailDesigner/EditorPage.jsx +1405 -0
  15. package/admin/src/pages/EmailDesigner/TemplateList.jsx +1807 -0
  16. package/admin/src/pages/HomePage.jsx +1233 -0
  17. package/admin/src/pages/LicensePage.jsx +424 -0
  18. package/admin/src/pages/RoutingRules.jsx +1141 -0
  19. package/admin/src/pages/Settings.jsx +603 -0
  20. package/admin/src/pluginId.js +3 -0
  21. package/admin/src/translations/de.json +71 -0
  22. package/admin/src/translations/en.json +70 -0
  23. package/admin/src/translations/es.json +71 -0
  24. package/admin/src/translations/fr.json +71 -0
  25. package/admin/src/translations/pt.json +71 -0
  26. package/admin/src/utils/fetchWithRetry.js +123 -0
  27. package/admin/src/utils/getTranslation.js +5 -0
  28. package/dist/_chunks/App-B-Gp4Vbr.js +7568 -0
  29. package/dist/_chunks/App-BymMjoGM.mjs +7543 -0
  30. package/dist/_chunks/LicensePage-Bl02myMx.mjs +342 -0
  31. package/dist/_chunks/LicensePage-CJXwPnEe.js +344 -0
  32. package/dist/_chunks/Settings-C_TmKwcz.mjs +400 -0
  33. package/dist/_chunks/Settings-zuFQ3pnn.js +402 -0
  34. package/dist/_chunks/de-CN-G9j1S.js +64 -0
  35. package/dist/_chunks/de-DS04rP54.mjs +64 -0
  36. package/dist/_chunks/en-BDc7Jk8u.js +64 -0
  37. package/dist/_chunks/en-BEFQJXvR.mjs +64 -0
  38. package/dist/_chunks/es-BpV1MIdm.js +64 -0
  39. package/dist/_chunks/es-DQHwzPpP.mjs +64 -0
  40. package/dist/_chunks/fr-BG1WfEVm.mjs +64 -0
  41. package/dist/_chunks/fr-vpziIpRp.js +64 -0
  42. package/dist/_chunks/pt-CMoGrOib.mjs +64 -0
  43. package/dist/_chunks/pt-ODpAhDNa.js +64 -0
  44. package/dist/admin/index.js +89 -0
  45. package/dist/admin/index.mjs +90 -0
  46. package/dist/server/index.js +6214 -0
  47. package/dist/server/index.mjs +6208 -0
  48. package/package.json +113 -0
  49. package/server/jsconfig.json +10 -0
  50. package/server/src/bootstrap.js +153 -0
  51. package/server/src/config/features.js +260 -0
  52. package/server/src/config/index.js +6 -0
  53. package/server/src/content-types/email-account/schema.json +93 -0
  54. package/server/src/content-types/email-event/index.js +8 -0
  55. package/server/src/content-types/email-event/schema.json +57 -0
  56. package/server/src/content-types/email-link/index.js +8 -0
  57. package/server/src/content-types/email-link/schema.json +49 -0
  58. package/server/src/content-types/email-log/index.js +8 -0
  59. package/server/src/content-types/email-log/schema.json +106 -0
  60. package/server/src/content-types/email-template/schema.json +74 -0
  61. package/server/src/content-types/email-template-version/schema.json +60 -0
  62. package/server/src/content-types/index.js +33 -0
  63. package/server/src/content-types/routing-rule/schema.json +59 -0
  64. package/server/src/controllers/accounts.js +220 -0
  65. package/server/src/controllers/analytics.js +347 -0
  66. package/server/src/controllers/controller.js +26 -0
  67. package/server/src/controllers/email-designer.js +474 -0
  68. package/server/src/controllers/index.js +21 -0
  69. package/server/src/controllers/license.js +267 -0
  70. package/server/src/controllers/oauth.js +474 -0
  71. package/server/src/controllers/routing-rules.js +122 -0
  72. package/server/src/controllers/test.js +383 -0
  73. package/server/src/destroy.js +23 -0
  74. package/server/src/index.js +25 -0
  75. package/server/src/middlewares/index.js +3 -0
  76. package/server/src/policies/index.js +3 -0
  77. package/server/src/register.js +5 -0
  78. package/server/src/routes/admin.js +469 -0
  79. package/server/src/routes/content-api.js +37 -0
  80. package/server/src/routes/index.js +9 -0
  81. package/server/src/services/account-manager.js +277 -0
  82. package/server/src/services/analytics.js +496 -0
  83. package/server/src/services/email-designer.js +870 -0
  84. package/server/src/services/email-router.js +1420 -0
  85. package/server/src/services/index.js +17 -0
  86. package/server/src/services/license-guard.js +418 -0
  87. package/server/src/services/oauth.js +515 -0
  88. package/server/src/services/service.js +7 -0
  89. package/server/src/utils/encryption.js +81 -0
  90. package/strapi-admin.js +4 -0
  91. package/strapi-server.js +4 -0
@@ -0,0 +1,515 @@
1
+ 'use strict';
2
+
3
+ const { encryptCredentials } = require('../utils/encryption');
4
+
5
+ /**
6
+ * OAuth Service
7
+ * Handles OAuth flows for Gmail, Microsoft, etc.
8
+ */
9
+
10
+ module.exports = ({ strapi }) => ({
11
+ /**
12
+ * Get Gmail OAuth URL
13
+ * @param {string} clientId - OAuth Client ID (from UI, not .env!)
14
+ * @param {string} state - State parameter for security
15
+ */
16
+ getGmailAuthUrl(clientId, state) {
17
+ const redirectUri = `${process.env.URL || 'http://localhost:1337'}/magic-mail/oauth/gmail/callback`;
18
+
19
+ if (!clientId) {
20
+ throw new Error('Client ID is required for OAuth');
21
+ }
22
+
23
+ const scopes = [
24
+ 'https://www.googleapis.com/auth/gmail.send',
25
+ 'https://www.googleapis.com/auth/userinfo.email',
26
+ 'openid',
27
+ ].join(' ');
28
+
29
+ const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?` +
30
+ `client_id=${encodeURIComponent(clientId)}&` +
31
+ `redirect_uri=${encodeURIComponent(redirectUri)}&` +
32
+ `response_type=code&` +
33
+ `scope=${encodeURIComponent(scopes)}&` +
34
+ `access_type=offline&` +
35
+ `prompt=consent&` +
36
+ `state=${state}`;
37
+
38
+ return authUrl;
39
+ },
40
+
41
+ /**
42
+ * Exchange Google OAuth code for tokens
43
+ * @param {string} code - OAuth authorization code
44
+ * @param {string} clientId - OAuth Client ID (from UI!)
45
+ * @param {string} clientSecret - OAuth Client Secret (from UI!)
46
+ */
47
+ async exchangeGoogleCode(code, clientId, clientSecret) {
48
+ const redirectUri = `${process.env.URL || 'http://localhost:1337'}/magic-mail/oauth/gmail/callback`;
49
+
50
+ strapi.log.info('[magic-mail] Exchanging OAuth code for tokens...');
51
+ strapi.log.info(`[magic-mail] Client ID: ${clientId.substring(0, 20)}...`);
52
+ strapi.log.info(`[magic-mail] Redirect URI: ${redirectUri}`);
53
+
54
+ const response = await fetch('https://oauth2.googleapis.com/token', {
55
+ method: 'POST',
56
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
57
+ body: new URLSearchParams({
58
+ code,
59
+ client_id: clientId,
60
+ client_secret: clientSecret,
61
+ redirect_uri: redirectUri,
62
+ grant_type: 'authorization_code',
63
+ }),
64
+ });
65
+
66
+ if (!response.ok) {
67
+ const errorData = await response.text();
68
+ strapi.log.error('[magic-mail] Token exchange failed:', errorData);
69
+ throw new Error(`Failed to exchange code for tokens: ${response.status}`);
70
+ }
71
+
72
+ const tokens = await response.json();
73
+ strapi.log.info('[magic-mail] ✅ Tokens received from Google');
74
+
75
+ if (!tokens.access_token) {
76
+ throw new Error('No access token received from Google');
77
+ }
78
+
79
+ // Get user email from Google
80
+ strapi.log.info('[magic-mail] Fetching user info from Google...');
81
+ const userInfoResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
82
+ headers: { Authorization: `Bearer ${tokens.access_token}` },
83
+ });
84
+
85
+ if (!userInfoResponse.ok) {
86
+ const errorData = await userInfoResponse.text();
87
+ strapi.log.error('[magic-mail] User info fetch failed:', errorData);
88
+ throw new Error('Failed to get user email from Google');
89
+ }
90
+
91
+ const userInfo = await userInfoResponse.json();
92
+ strapi.log.info(`[magic-mail] ✅ Got user email from Google: ${userInfo.email}`);
93
+
94
+ if (!userInfo.email) {
95
+ strapi.log.error('[magic-mail] userInfo:', userInfo);
96
+ throw new Error('Google did not provide email address');
97
+ }
98
+
99
+ return {
100
+ email: userInfo.email,
101
+ accessToken: tokens.access_token,
102
+ refreshToken: tokens.refresh_token,
103
+ expiresAt: new Date(Date.now() + (tokens.expires_in || 3600) * 1000),
104
+ };
105
+ },
106
+
107
+ /**
108
+ * Refresh Gmail OAuth tokens
109
+ * @param {string} refreshToken - Refresh token
110
+ * @param {string} clientId - OAuth Client ID (from DB!)
111
+ * @param {string} clientSecret - OAuth Client Secret (from DB!)
112
+ */
113
+ async refreshGmailTokens(refreshToken, clientId, clientSecret) {
114
+
115
+ const response = await fetch('https://oauth2.googleapis.com/token', {
116
+ method: 'POST',
117
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
118
+ body: new URLSearchParams({
119
+ refresh_token: refreshToken,
120
+ client_id: clientId,
121
+ client_secret: clientSecret,
122
+ grant_type: 'refresh_token',
123
+ }),
124
+ });
125
+
126
+ if (!response.ok) {
127
+ throw new Error('Failed to refresh Gmail tokens');
128
+ }
129
+
130
+ const tokens = await response.json();
131
+
132
+ return {
133
+ accessToken: tokens.access_token,
134
+ expiresAt: new Date(Date.now() + tokens.expires_in * 1000),
135
+ };
136
+ },
137
+
138
+ /**
139
+ * Get Microsoft OAuth URL
140
+ * @param {string} clientId - Application (Client) ID
141
+ * @param {string} tenantId - Tenant (Directory) ID
142
+ * @param {string} state - State parameter for security
143
+ */
144
+ getMicrosoftAuthUrl(clientId, tenantId, state) {
145
+ const redirectUri = `${process.env.URL || 'http://localhost:1337'}/magic-mail/oauth/microsoft/callback`;
146
+
147
+ if (!clientId) {
148
+ throw new Error('Client ID is required for Microsoft OAuth');
149
+ }
150
+
151
+ if (!tenantId) {
152
+ throw new Error('Tenant ID is required for Microsoft OAuth');
153
+ }
154
+
155
+ // Microsoft Graph API Scopes (official format)
156
+ const scopes = [
157
+ 'https://graph.microsoft.com/Mail.Send', // Send emails
158
+ 'https://graph.microsoft.com/User.Read', // Read user profile
159
+ 'offline_access', // Refresh tokens
160
+ 'openid', // OpenID Connect
161
+ 'email', // Email address
162
+ ].join(' ');
163
+
164
+ // Microsoft Identity Platform v2.0 endpoint with tenant-specific URL
165
+ // Using tenantId instead of /common to support single-tenant apps
166
+ const authUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?` +
167
+ `client_id=${encodeURIComponent(clientId)}&` +
168
+ `redirect_uri=${encodeURIComponent(redirectUri)}&` +
169
+ `response_type=code&` +
170
+ `scope=${encodeURIComponent(scopes)}&` +
171
+ `response_mode=query&` +
172
+ `prompt=consent&` +
173
+ `state=${state}`;
174
+
175
+ strapi.log.info(`[magic-mail] Microsoft OAuth URL: Using tenant ${tenantId}`);
176
+
177
+ return authUrl;
178
+ },
179
+
180
+ /**
181
+ * Exchange Microsoft OAuth code for tokens
182
+ * @param {string} code - OAuth authorization code
183
+ * @param {string} clientId - Application (Client) ID
184
+ * @param {string} clientSecret - Client Secret Value
185
+ * @param {string} tenantId - Tenant (Directory) ID
186
+ */
187
+ async exchangeMicrosoftCode(code, clientId, clientSecret, tenantId) {
188
+ const redirectUri = `${process.env.URL || 'http://localhost:1337'}/magic-mail/oauth/microsoft/callback`;
189
+
190
+ if (!tenantId) {
191
+ throw new Error('Tenant ID is required for Microsoft OAuth token exchange');
192
+ }
193
+
194
+ strapi.log.info('[magic-mail] Exchanging Microsoft OAuth code for tokens...');
195
+ strapi.log.info(`[magic-mail] Tenant ID: ${tenantId.substring(0, 20)}...`);
196
+ strapi.log.info(`[magic-mail] Client ID: ${clientId.substring(0, 20)}...`);
197
+ strapi.log.info(`[magic-mail] Redirect URI: ${redirectUri}`);
198
+
199
+ // Microsoft Identity Platform v2.0 token endpoint (tenant-specific!)
200
+ const tokenEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
201
+ strapi.log.info(`[magic-mail] Token endpoint: ${tokenEndpoint}`);
202
+
203
+ const response = await fetch(tokenEndpoint, {
204
+ method: 'POST',
205
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
206
+ body: new URLSearchParams({
207
+ code,
208
+ client_id: clientId,
209
+ client_secret: clientSecret,
210
+ redirect_uri: redirectUri,
211
+ grant_type: 'authorization_code',
212
+ scope: 'https://graph.microsoft.com/Mail.Send https://graph.microsoft.com/User.Read offline_access',
213
+ }),
214
+ });
215
+
216
+ if (!response.ok) {
217
+ const errorData = await response.text();
218
+ strapi.log.error('[magic-mail] Microsoft token exchange failed:', errorData);
219
+ throw new Error(`Failed to exchange code for tokens: ${response.status} - ${errorData}`);
220
+ }
221
+
222
+ const tokens = await response.json();
223
+ strapi.log.info('[magic-mail] ✅ Tokens received from Microsoft');
224
+ strapi.log.info('[magic-mail] Has access_token:', !!tokens.access_token);
225
+ strapi.log.info('[magic-mail] Has refresh_token:', !!tokens.refresh_token);
226
+ strapi.log.info('[magic-mail] Has id_token:', !!tokens.id_token);
227
+
228
+ if (!tokens.access_token) {
229
+ throw new Error('No access token received from Microsoft');
230
+ }
231
+
232
+ // Try to get email from ID token first
233
+ let email = null;
234
+ if (tokens.id_token) {
235
+ try {
236
+ // JWT format: header.payload.signature
237
+ const payloadBase64 = tokens.id_token.split('.')[1];
238
+ const payload = JSON.parse(Buffer.from(payloadBase64, 'base64').toString());
239
+ email = payload.email || payload.preferred_username || payload.upn;
240
+ strapi.log.info(`[magic-mail] ✅ Got email from Microsoft ID token: ${email}`);
241
+ } catch (jwtErr) {
242
+ strapi.log.warn('[magic-mail] Could not decode ID token:', jwtErr.message);
243
+ }
244
+ }
245
+
246
+ // Fallback: Get email from Microsoft Graph API /me endpoint
247
+ if (!email) {
248
+ strapi.log.info('[magic-mail] Fetching user info from Microsoft Graph API /me endpoint...');
249
+ const userInfoResponse = await fetch('https://graph.microsoft.com/v1.0/me', {
250
+ headers: {
251
+ 'Authorization': `Bearer ${tokens.access_token}`,
252
+ 'Content-Type': 'application/json',
253
+ },
254
+ });
255
+
256
+ if (!userInfoResponse.ok) {
257
+ const errorData = await userInfoResponse.text();
258
+ strapi.log.error('[magic-mail] User info fetch failed:', errorData);
259
+ strapi.log.error('[magic-mail] Status:', userInfoResponse.status);
260
+ throw new Error(`Failed to get user email from Microsoft Graph: ${userInfoResponse.status}`);
261
+ }
262
+
263
+ const userInfo = await userInfoResponse.json();
264
+ strapi.log.info('[magic-mail] User info from Graph:', JSON.stringify(userInfo, null, 2));
265
+
266
+ email = userInfo.mail || userInfo.userPrincipalName;
267
+ strapi.log.info(`[magic-mail] ✅ Got email from Microsoft Graph: ${email}`);
268
+ }
269
+
270
+ if (!email) {
271
+ strapi.log.error('[magic-mail] Microsoft did not provide email - ID token and Graph API both failed');
272
+ throw new Error('Microsoft did not provide email address');
273
+ }
274
+
275
+ return {
276
+ email,
277
+ accessToken: tokens.access_token,
278
+ refreshToken: tokens.refresh_token,
279
+ expiresAt: new Date(Date.now() + (tokens.expires_in || 3600) * 1000),
280
+ };
281
+ },
282
+
283
+ /**
284
+ * Refresh Microsoft OAuth tokens
285
+ * @param {string} refreshToken - Refresh token
286
+ * @param {string} clientId - Application (Client) ID
287
+ * @param {string} clientSecret - Client Secret Value
288
+ * @param {string} tenantId - Tenant (Directory) ID
289
+ */
290
+ async refreshMicrosoftTokens(refreshToken, clientId, clientSecret, tenantId) {
291
+ if (!tenantId) {
292
+ throw new Error('Tenant ID is required for Microsoft OAuth token refresh');
293
+ }
294
+
295
+ strapi.log.info('[magic-mail] Refreshing Microsoft OAuth tokens...');
296
+ strapi.log.info(`[magic-mail] Tenant ID: ${tenantId.substring(0, 20)}...`);
297
+
298
+ // Tenant-specific token endpoint
299
+ const tokenEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
300
+
301
+ const response = await fetch(tokenEndpoint, {
302
+ method: 'POST',
303
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
304
+ body: new URLSearchParams({
305
+ refresh_token: refreshToken,
306
+ client_id: clientId,
307
+ client_secret: clientSecret,
308
+ grant_type: 'refresh_token',
309
+ scope: 'https://graph.microsoft.com/Mail.Send https://graph.microsoft.com/User.Read offline_access',
310
+ }),
311
+ });
312
+
313
+ if (!response.ok) {
314
+ const errorData = await response.text();
315
+ strapi.log.error('[magic-mail] Microsoft token refresh failed:', errorData);
316
+ throw new Error(`Failed to refresh Microsoft tokens: ${response.status}`);
317
+ }
318
+
319
+ const tokens = await response.json();
320
+ strapi.log.info('[magic-mail] ✅ Microsoft tokens refreshed successfully');
321
+
322
+ return {
323
+ accessToken: tokens.access_token,
324
+ expiresAt: new Date(Date.now() + (tokens.expires_in || 3600) * 1000),
325
+ };
326
+ },
327
+
328
+ /**
329
+ * Get Yahoo OAuth URL
330
+ * @param {string} clientId - Yahoo Client ID
331
+ * @param {string} state - State parameter for security
332
+ */
333
+ getYahooAuthUrl(clientId, state) {
334
+ const redirectUri = `${process.env.URL || 'http://localhost:1337'}/magic-mail/oauth/yahoo/callback`;
335
+
336
+ if (!clientId) {
337
+ throw new Error('Client ID is required for Yahoo OAuth');
338
+ }
339
+
340
+ const scopes = [
341
+ 'mail-w', // Write/send emails
342
+ 'sdps-r', // Read profile
343
+ ].join(' ');
344
+
345
+ const authUrl = `https://api.login.yahoo.com/oauth2/request_auth?` +
346
+ `client_id=${encodeURIComponent(clientId)}&` +
347
+ `redirect_uri=${encodeURIComponent(redirectUri)}&` +
348
+ `response_type=code&` +
349
+ `scope=${encodeURIComponent(scopes)}&` +
350
+ `state=${state}`;
351
+
352
+ return authUrl;
353
+ },
354
+
355
+ /**
356
+ * Exchange Yahoo OAuth code for tokens
357
+ * @param {string} code - OAuth authorization code
358
+ * @param {string} clientId - Yahoo Client ID
359
+ * @param {string} clientSecret - Yahoo Client Secret
360
+ */
361
+ async exchangeYahooCode(code, clientId, clientSecret) {
362
+ const redirectUri = `${process.env.URL || 'http://localhost:1337'}/magic-mail/oauth/yahoo/callback`;
363
+
364
+ strapi.log.info('[magic-mail] Exchanging Yahoo OAuth code for tokens...');
365
+ strapi.log.info(`[magic-mail] Client ID: ${clientId.substring(0, 20)}...`);
366
+ strapi.log.info(`[magic-mail] Redirect URI: ${redirectUri}`);
367
+
368
+ // Create Basic Auth header
369
+ const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
370
+
371
+ const response = await fetch('https://api.login.yahoo.com/oauth2/get_token', {
372
+ method: 'POST',
373
+ headers: {
374
+ 'Content-Type': 'application/x-www-form-urlencoded',
375
+ 'Authorization': `Basic ${basicAuth}`,
376
+ },
377
+ body: new URLSearchParams({
378
+ code,
379
+ redirect_uri: redirectUri,
380
+ grant_type: 'authorization_code',
381
+ }),
382
+ });
383
+
384
+ if (!response.ok) {
385
+ const errorData = await response.text();
386
+ strapi.log.error('[magic-mail] Yahoo token exchange failed:', errorData);
387
+ throw new Error(`Failed to exchange code for tokens: ${response.status}`);
388
+ }
389
+
390
+ const tokens = await response.json();
391
+ strapi.log.info('[magic-mail] ✅ Tokens received from Yahoo');
392
+
393
+ if (!tokens.access_token) {
394
+ throw new Error('No access token received from Yahoo');
395
+ }
396
+
397
+ // Get user email from Yahoo profile API
398
+ strapi.log.info('[magic-mail] Fetching user info from Yahoo API...');
399
+ const userInfoResponse = await fetch('https://api.login.yahoo.com/openid/v1/userinfo', {
400
+ headers: { Authorization: `Bearer ${tokens.access_token}` },
401
+ });
402
+
403
+ if (!userInfoResponse.ok) {
404
+ const errorData = await userInfoResponse.text();
405
+ strapi.log.error('[magic-mail] User info fetch failed:', errorData);
406
+ throw new Error('Failed to get user email from Yahoo');
407
+ }
408
+
409
+ const userInfo = await userInfoResponse.json();
410
+ const email = userInfo.email;
411
+ strapi.log.info(`[magic-mail] ✅ Got email from Yahoo: ${email}`);
412
+
413
+ if (!email) {
414
+ throw new Error('Yahoo did not provide email address');
415
+ }
416
+
417
+ return {
418
+ email,
419
+ accessToken: tokens.access_token,
420
+ refreshToken: tokens.refresh_token,
421
+ expiresAt: new Date(Date.now() + (tokens.expires_in || 3600) * 1000),
422
+ };
423
+ },
424
+
425
+ /**
426
+ * Refresh Yahoo OAuth tokens
427
+ * @param {string} refreshToken - Refresh token
428
+ * @param {string} clientId - Yahoo Client ID
429
+ * @param {string} clientSecret - Yahoo Client Secret
430
+ */
431
+ async refreshYahooTokens(refreshToken, clientId, clientSecret) {
432
+ const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
433
+
434
+ const response = await fetch('https://api.login.yahoo.com/oauth2/get_token', {
435
+ method: 'POST',
436
+ headers: {
437
+ 'Content-Type': 'application/x-www-form-urlencoded',
438
+ 'Authorization': `Basic ${basicAuth}`,
439
+ },
440
+ body: new URLSearchParams({
441
+ refresh_token: refreshToken,
442
+ grant_type: 'refresh_token',
443
+ }),
444
+ });
445
+
446
+ if (!response.ok) {
447
+ throw new Error('Failed to refresh Yahoo tokens');
448
+ }
449
+
450
+ const tokens = await response.json();
451
+
452
+ return {
453
+ accessToken: tokens.access_token,
454
+ expiresAt: new Date(Date.now() + tokens.expires_in * 1000),
455
+ };
456
+ },
457
+
458
+ /**
459
+ * Store OAuth account
460
+ */
461
+ async storeOAuthAccount(provider, tokenData, accountDetails, oauthCredentials) {
462
+ // Separate config (OAuth app credentials) and oauth (tokens)
463
+ // Store ALL config fields (including tenantId for Microsoft)
464
+ const configToStore = {
465
+ clientId: oauthCredentials.clientId,
466
+ clientSecret: oauthCredentials.clientSecret,
467
+ };
468
+
469
+ // Add tenantId for Microsoft OAuth
470
+ if (oauthCredentials.tenantId) {
471
+ configToStore.tenantId = oauthCredentials.tenantId;
472
+ strapi.log.info(`[magic-mail] Storing tenantId: ${oauthCredentials.tenantId.substring(0, 20)}...`);
473
+ }
474
+
475
+ // Add domain for Mailgun
476
+ if (oauthCredentials.domain) {
477
+ configToStore.domain = oauthCredentials.domain;
478
+ }
479
+
480
+ const encryptedConfig = encryptCredentials(configToStore);
481
+
482
+ const encryptedOAuth = encryptCredentials({
483
+ email: tokenData.email,
484
+ accessToken: tokenData.accessToken,
485
+ refreshToken: tokenData.refreshToken,
486
+ expiresAt: tokenData.expiresAt,
487
+ });
488
+
489
+ const account = await strapi.entityService.create('plugin::magic-mail.email-account', {
490
+ data: {
491
+ name: accountDetails.name,
492
+ description: accountDetails.description || '',
493
+ provider: `${provider}-oauth`,
494
+ config: encryptedConfig, // OAuth app credentials
495
+ oauth: encryptedOAuth, // OAuth tokens
496
+ fromEmail: tokenData.email, // ✅ Use email from Google, not from accountDetails
497
+ fromName: accountDetails.fromName || tokenData.email.split('@')[0],
498
+ replyTo: accountDetails.replyTo || tokenData.email,
499
+ isActive: true,
500
+ isPrimary: accountDetails.isPrimary || false,
501
+ priority: accountDetails.priority || 1,
502
+ dailyLimit: accountDetails.dailyLimit || 0,
503
+ hourlyLimit: accountDetails.hourlyLimit || 0,
504
+ emailsSentToday: 0,
505
+ emailsSentThisHour: 0,
506
+ totalEmailsSent: 0,
507
+ },
508
+ });
509
+
510
+ strapi.log.info(`[magic-mail] ✅ OAuth account created: ${accountDetails.name} (${tokenData.email})`);
511
+
512
+ return account;
513
+ },
514
+ });
515
+
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ module.exports = ({ strapi }) => ({
4
+ getWelcomeMessage() {
5
+ return 'Welcome to Strapi 🚀';
6
+ },
7
+ });
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ const ALGORITHM = 'aes-256-gcm';
6
+ const IV_LENGTH = 16;
7
+
8
+ /**
9
+ * Get encryption key from environment or generate one
10
+ */
11
+ function getEncryptionKey() {
12
+ const envKey = process.env.MAGIC_MAIL_ENCRYPTION_KEY || process.env.APP_KEYS;
13
+
14
+ if (envKey) {
15
+ return crypto.createHash('sha256').update(envKey).digest();
16
+ }
17
+
18
+ console.warn('[magic-mail] ⚠️ No MAGIC_MAIL_ENCRYPTION_KEY found. Using fallback.');
19
+ return crypto.createHash('sha256').update('magic-mail-default-key').digest();
20
+ }
21
+
22
+ /**
23
+ * Encrypt credentials
24
+ */
25
+ function encryptCredentials(data) {
26
+ if (!data) return null;
27
+
28
+ try {
29
+ const key = getEncryptionKey();
30
+ const iv = crypto.randomBytes(IV_LENGTH);
31
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
32
+
33
+ const jsonData = JSON.stringify(data);
34
+ let encrypted = cipher.update(jsonData, 'utf8', 'hex');
35
+ encrypted += cipher.final('hex');
36
+
37
+ const authTag = cipher.getAuthTag();
38
+
39
+ return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
40
+ } catch (err) {
41
+ console.error('[magic-mail] Encryption failed:', err);
42
+ throw new Error('Failed to encrypt credentials');
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Decrypt credentials
48
+ */
49
+ function decryptCredentials(encryptedData) {
50
+ if (!encryptedData) return null;
51
+
52
+ try {
53
+ const key = getEncryptionKey();
54
+ const parts = encryptedData.split(':');
55
+
56
+ if (parts.length !== 3) {
57
+ throw new Error('Invalid encrypted data format');
58
+ }
59
+
60
+ const iv = Buffer.from(parts[0], 'hex');
61
+ const authTag = Buffer.from(parts[1], 'hex');
62
+ const encrypted = parts[2];
63
+
64
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
65
+ decipher.setAuthTag(authTag);
66
+
67
+ let decrypted = decipher.update(encrypted, 'hex', 'utf8');
68
+ decrypted += decipher.final('utf8');
69
+
70
+ return JSON.parse(decrypted);
71
+ } catch (err) {
72
+ console.error('[magic-mail] Decryption failed:', err);
73
+ return null;
74
+ }
75
+ }
76
+
77
+ module.exports = {
78
+ encryptCredentials,
79
+ decryptCredentials,
80
+ };
81
+
@@ -0,0 +1,4 @@
1
+ 'use strict';
2
+
3
+ module.exports = require('./admin/src/index.js');
4
+
@@ -0,0 +1,4 @@
1
+ 'use strict';
2
+
3
+ module.exports = require('./server/src/index.js');
4
+