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,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,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
|
+
|
package/strapi-admin.js
ADDED
package/strapi-server.js
ADDED