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,1943 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import styled, { keyframes, css } from 'styled-components';
|
|
3
|
+
import {
|
|
4
|
+
Modal,
|
|
5
|
+
Typography,
|
|
6
|
+
Box,
|
|
7
|
+
Flex,
|
|
8
|
+
Button,
|
|
9
|
+
Field,
|
|
10
|
+
TextInput,
|
|
11
|
+
Textarea,
|
|
12
|
+
NumberInput,
|
|
13
|
+
Toggle,
|
|
14
|
+
Alert,
|
|
15
|
+
Divider,
|
|
16
|
+
Badge,
|
|
17
|
+
} from '@strapi/design-system';
|
|
18
|
+
import { useFetchClient, useNotification } from '@strapi/strapi/admin';
|
|
19
|
+
import { Mail, Server, Key, ArrowRight, ArrowLeft, Check, Lock, Cloud, Cog, Star } from '@strapi/icons';
|
|
20
|
+
|
|
21
|
+
// ============= ANIMATIONS =============
|
|
22
|
+
const fadeIn = keyframes`
|
|
23
|
+
from {
|
|
24
|
+
opacity: 0;
|
|
25
|
+
transform: translateY(10px);
|
|
26
|
+
}
|
|
27
|
+
to {
|
|
28
|
+
opacity: 1;
|
|
29
|
+
transform: translateY(0);
|
|
30
|
+
}
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
const pulse = keyframes`
|
|
34
|
+
0%, 100% {
|
|
35
|
+
transform: scale(1);
|
|
36
|
+
}
|
|
37
|
+
50% {
|
|
38
|
+
transform: scale(1.05);
|
|
39
|
+
}
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
const slideIn = keyframes`
|
|
43
|
+
from {
|
|
44
|
+
transform: translateX(-20px);
|
|
45
|
+
opacity: 0;
|
|
46
|
+
}
|
|
47
|
+
to {
|
|
48
|
+
transform: translateX(0);
|
|
49
|
+
opacity: 1;
|
|
50
|
+
}
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
// ============= COLORS =============
|
|
54
|
+
const colors = {
|
|
55
|
+
primary: '#4945ff', // Strapi Primary Blue
|
|
56
|
+
primaryLight: '#f0f0ff', // Light Blue Background
|
|
57
|
+
success: '#5cb176', // Green for completed
|
|
58
|
+
successLight: '#eafaf1', // Light green background
|
|
59
|
+
neutral: '#8e8ea9', // Gray
|
|
60
|
+
neutralLight: '#f6f6f9', // Light gray
|
|
61
|
+
white: '#ffffff',
|
|
62
|
+
border: '#dcdce4',
|
|
63
|
+
text: '#32324d',
|
|
64
|
+
textLight: '#666687',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// ============= STYLED COMPONENTS =============
|
|
68
|
+
const StepHeader = styled(Box)`
|
|
69
|
+
padding-bottom: 24px;
|
|
70
|
+
margin-bottom: 32px;
|
|
71
|
+
position: relative;
|
|
72
|
+
animation: ${fadeIn} 0.4s ease;
|
|
73
|
+
|
|
74
|
+
&::after {
|
|
75
|
+
content: '';
|
|
76
|
+
position: absolute;
|
|
77
|
+
bottom: 0;
|
|
78
|
+
left: -24px;
|
|
79
|
+
right: -24px;
|
|
80
|
+
height: 1px;
|
|
81
|
+
background: linear-gradient(90deg, transparent, ${colors.border}, transparent);
|
|
82
|
+
}
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
const StepTitle = styled(Typography)`
|
|
86
|
+
color: ${colors.text};
|
|
87
|
+
font-size: 24px;
|
|
88
|
+
font-weight: 600;
|
|
89
|
+
margin-bottom: 8px;
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
gap: 12px;
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
const StepSubtitle = styled(Typography)`
|
|
96
|
+
color: ${colors.textLight};
|
|
97
|
+
font-size: 14px;
|
|
98
|
+
line-height: 1.5;
|
|
99
|
+
`;
|
|
100
|
+
|
|
101
|
+
const StepperContainer = styled(Box)`
|
|
102
|
+
display: flex;
|
|
103
|
+
align-items: flex-start;
|
|
104
|
+
justify-content: center;
|
|
105
|
+
gap: 0;
|
|
106
|
+
margin-bottom: 48px;
|
|
107
|
+
margin-top: 8px;
|
|
108
|
+
position: relative;
|
|
109
|
+
padding: 0 40px;
|
|
110
|
+
`;
|
|
111
|
+
|
|
112
|
+
const StepWrapper = styled.div`
|
|
113
|
+
flex: 1;
|
|
114
|
+
display: flex;
|
|
115
|
+
flex-direction: column;
|
|
116
|
+
align-items: center;
|
|
117
|
+
position: relative;
|
|
118
|
+
|
|
119
|
+
&:not(:last-child)::after {
|
|
120
|
+
content: '';
|
|
121
|
+
position: absolute;
|
|
122
|
+
top: 28px;
|
|
123
|
+
left: 50%;
|
|
124
|
+
width: 100%;
|
|
125
|
+
height: 3px;
|
|
126
|
+
background: ${props => props.$completed ? colors.success : colors.neutralLight};
|
|
127
|
+
transition: all 0.4s ease;
|
|
128
|
+
z-index: 0;
|
|
129
|
+
}
|
|
130
|
+
`;
|
|
131
|
+
|
|
132
|
+
const StepDot = styled.div`
|
|
133
|
+
width: 56px;
|
|
134
|
+
height: 56px;
|
|
135
|
+
border-radius: 50%;
|
|
136
|
+
background: ${props =>
|
|
137
|
+
props.$active ? colors.primary :
|
|
138
|
+
props.$completed ? colors.success :
|
|
139
|
+
colors.white
|
|
140
|
+
};
|
|
141
|
+
color: ${props =>
|
|
142
|
+
props.$active || props.$completed ? colors.white : colors.textLight
|
|
143
|
+
};
|
|
144
|
+
border: 4px solid ${props =>
|
|
145
|
+
props.$active ? colors.primary :
|
|
146
|
+
props.$completed ? colors.success :
|
|
147
|
+
colors.border
|
|
148
|
+
};
|
|
149
|
+
display: flex;
|
|
150
|
+
align-items: center;
|
|
151
|
+
justify-content: center;
|
|
152
|
+
font-weight: 700;
|
|
153
|
+
font-size: 18px;
|
|
154
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
155
|
+
position: relative;
|
|
156
|
+
z-index: 1;
|
|
157
|
+
cursor: ${props => props.$completed ? 'pointer' : 'default'};
|
|
158
|
+
box-shadow: ${props =>
|
|
159
|
+
props.$active ? `0 4px 16px ${colors.primary}40, 0 0 0 8px ${colors.primaryLight}` :
|
|
160
|
+
props.$completed ? `0 4px 12px ${colors.success}30` :
|
|
161
|
+
'0 2px 8px rgba(0,0,0,0.08)'
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
${props => props.$active && css`
|
|
165
|
+
animation: ${pulse} 2s infinite;
|
|
166
|
+
`}
|
|
167
|
+
|
|
168
|
+
&:hover {
|
|
169
|
+
transform: ${props => props.$completed ? 'scale(1.1)' : props.$active ? 'scale(1.05)' : 'scale(1)'};
|
|
170
|
+
}
|
|
171
|
+
`;
|
|
172
|
+
|
|
173
|
+
const StepLabel = styled(Typography)`
|
|
174
|
+
margin-top: 12px;
|
|
175
|
+
font-size: 13px;
|
|
176
|
+
color: ${props => props.$active ? colors.primary : props.$completed ? colors.success : colors.textLight};
|
|
177
|
+
white-space: nowrap;
|
|
178
|
+
font-weight: ${props => props.$active ? 600 : 500};
|
|
179
|
+
text-align: center;
|
|
180
|
+
transition: all 0.3s ease;
|
|
181
|
+
`;
|
|
182
|
+
|
|
183
|
+
const ProvidersGrid = styled(Box)`
|
|
184
|
+
display: grid;
|
|
185
|
+
grid-template-columns: repeat(2, 1fr);
|
|
186
|
+
gap: 20px;
|
|
187
|
+
margin-bottom: 24px;
|
|
188
|
+
animation: ${slideIn} 0.5s ease;
|
|
189
|
+
max-width: 800px;
|
|
190
|
+
margin-left: auto;
|
|
191
|
+
margin-right: auto;
|
|
192
|
+
`;
|
|
193
|
+
|
|
194
|
+
const ProviderCard = styled(Box)`
|
|
195
|
+
background: ${props => props.$selected ? colors.successLight : colors.white};
|
|
196
|
+
border: 2px solid ${props => props.$selected ? colors.success : colors.border};
|
|
197
|
+
border-radius: 12px;
|
|
198
|
+
padding: 24px;
|
|
199
|
+
cursor: pointer;
|
|
200
|
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
201
|
+
text-align: center;
|
|
202
|
+
aspect-ratio: 1;
|
|
203
|
+
min-height: 180px;
|
|
204
|
+
display: flex;
|
|
205
|
+
flex-direction: column;
|
|
206
|
+
align-items: center;
|
|
207
|
+
justify-content: center;
|
|
208
|
+
gap: 12px;
|
|
209
|
+
position: relative;
|
|
210
|
+
overflow: hidden;
|
|
211
|
+
|
|
212
|
+
&::before {
|
|
213
|
+
content: '';
|
|
214
|
+
position: absolute;
|
|
215
|
+
top: 0;
|
|
216
|
+
left: 0;
|
|
217
|
+
right: 0;
|
|
218
|
+
bottom: 0;
|
|
219
|
+
background: linear-gradient(135deg, transparent, rgba(73, 69, 255, 0.05));
|
|
220
|
+
opacity: 0;
|
|
221
|
+
transition: opacity 0.3s;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
&:hover {
|
|
225
|
+
transform: translateY(-4px);
|
|
226
|
+
box-shadow: 0 8px 24px rgba(73, 69, 255, 0.12);
|
|
227
|
+
border-color: ${props => props.$selected ? colors.success : colors.primary};
|
|
228
|
+
|
|
229
|
+
&::before {
|
|
230
|
+
opacity: 1;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
${props => props.$selected && `
|
|
235
|
+
&::after {
|
|
236
|
+
content: '✓';
|
|
237
|
+
position: absolute;
|
|
238
|
+
top: 8px;
|
|
239
|
+
right: 8px;
|
|
240
|
+
width: 24px;
|
|
241
|
+
height: 24px;
|
|
242
|
+
background: ${colors.success};
|
|
243
|
+
color: white;
|
|
244
|
+
border-radius: 50%;
|
|
245
|
+
display: flex;
|
|
246
|
+
align-items: center;
|
|
247
|
+
justify-content: center;
|
|
248
|
+
font-size: 14px;
|
|
249
|
+
font-weight: bold;
|
|
250
|
+
}
|
|
251
|
+
`}
|
|
252
|
+
`;
|
|
253
|
+
|
|
254
|
+
const ProviderIcon = styled.div`
|
|
255
|
+
width: 56px;
|
|
256
|
+
height: 56px;
|
|
257
|
+
border-radius: ${props => props.$round ? '50%' : '12px'};
|
|
258
|
+
background: ${props => props.$bgColor || colors.primaryLight};
|
|
259
|
+
display: flex;
|
|
260
|
+
align-items: center;
|
|
261
|
+
justify-content: center;
|
|
262
|
+
font-size: ${props => props.$fontSize || '24px'};
|
|
263
|
+
font-weight: bold;
|
|
264
|
+
color: ${props => props.$color || colors.primary};
|
|
265
|
+
box-shadow: 0 4px 12px ${props => props.$shadowColor || 'rgba(73, 69, 255, 0.15)'};
|
|
266
|
+
`;
|
|
267
|
+
|
|
268
|
+
const ProviderName = styled(Typography)`
|
|
269
|
+
font-weight: 600;
|
|
270
|
+
font-size: 15px;
|
|
271
|
+
color: ${colors.text};
|
|
272
|
+
margin: 0;
|
|
273
|
+
`;
|
|
274
|
+
|
|
275
|
+
const ProviderTagline = styled(Typography)`
|
|
276
|
+
font-size: 12px;
|
|
277
|
+
color: ${colors.textLight};
|
|
278
|
+
margin: 0;
|
|
279
|
+
`;
|
|
280
|
+
|
|
281
|
+
const InfoAlert = styled(Alert)`
|
|
282
|
+
background: ${colors.primaryLight};
|
|
283
|
+
border: 1px solid ${colors.primary}33;
|
|
284
|
+
animation: ${fadeIn} 0.4s ease;
|
|
285
|
+
|
|
286
|
+
svg {
|
|
287
|
+
color: ${colors.primary};
|
|
288
|
+
}
|
|
289
|
+
`;
|
|
290
|
+
|
|
291
|
+
const FormSection = styled(Box)`
|
|
292
|
+
animation: ${slideIn} 0.4s ease;
|
|
293
|
+
width: 100%;
|
|
294
|
+
max-width: 100%;
|
|
295
|
+
`;
|
|
296
|
+
|
|
297
|
+
const FullWidthField = styled(Box)`
|
|
298
|
+
width: 100%;
|
|
299
|
+
|
|
300
|
+
& > * {
|
|
301
|
+
width: 100%;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
input, textarea {
|
|
305
|
+
width: 100% !important;
|
|
306
|
+
}
|
|
307
|
+
`;
|
|
308
|
+
|
|
309
|
+
const SectionTitle = styled(Typography)`
|
|
310
|
+
color: ${colors.text};
|
|
311
|
+
font-weight: 600;
|
|
312
|
+
font-size: 16px;
|
|
313
|
+
margin-bottom: 16px;
|
|
314
|
+
display: flex;
|
|
315
|
+
align-items: center;
|
|
316
|
+
gap: 8px;
|
|
317
|
+
`;
|
|
318
|
+
|
|
319
|
+
const PrimaryToggleBox = styled(Box)`
|
|
320
|
+
background: linear-gradient(135deg, ${colors.primaryLight}, ${colors.successLight});
|
|
321
|
+
border: 2px solid ${colors.primary}33;
|
|
322
|
+
border-radius: 12px;
|
|
323
|
+
padding: 20px;
|
|
324
|
+
transition: all 0.3s;
|
|
325
|
+
|
|
326
|
+
&:hover {
|
|
327
|
+
border-color: ${colors.primary}66;
|
|
328
|
+
box-shadow: 0 4px 12px rgba(73, 69, 255, 0.1);
|
|
329
|
+
}
|
|
330
|
+
`;
|
|
331
|
+
|
|
332
|
+
// ============= COMPONENT =============
|
|
333
|
+
const AddAccountModal = ({ isOpen, onClose, onAccountAdded, editAccount = null }) => {
|
|
334
|
+
const { post, get, put } = useFetchClient();
|
|
335
|
+
const { toggleNotification } = useNotification();
|
|
336
|
+
const [loading, setLoading] = useState(false);
|
|
337
|
+
const [currentStep, setCurrentStep] = useState(1);
|
|
338
|
+
const [provider, setProvider] = useState('');
|
|
339
|
+
const [oauthCode, setOauthCode] = useState(null);
|
|
340
|
+
const [oauthState, setOauthState] = useState(null);
|
|
341
|
+
const isEditMode = !!editAccount;
|
|
342
|
+
|
|
343
|
+
const [formData, setFormData] = useState({
|
|
344
|
+
name: '',
|
|
345
|
+
description: '',
|
|
346
|
+
fromEmail: '',
|
|
347
|
+
fromName: '',
|
|
348
|
+
replyTo: '',
|
|
349
|
+
isActive: true,
|
|
350
|
+
isPrimary: false,
|
|
351
|
+
priority: 5,
|
|
352
|
+
dailyLimit: 500,
|
|
353
|
+
hourlyLimit: 50,
|
|
354
|
+
host: '',
|
|
355
|
+
port: 587,
|
|
356
|
+
user: '',
|
|
357
|
+
pass: '',
|
|
358
|
+
secure: false,
|
|
359
|
+
apiKey: '',
|
|
360
|
+
oauthClientId: '',
|
|
361
|
+
oauthClientSecret: '',
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const handleChange = (field, value) => {
|
|
365
|
+
setFormData(prev => ({ ...prev, [field]: value }));
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// Populate form when editing - fetch decrypted data
|
|
369
|
+
React.useEffect(() => {
|
|
370
|
+
const loadAccountData = async () => {
|
|
371
|
+
if (isEditMode && editAccount && isOpen) {
|
|
372
|
+
try {
|
|
373
|
+
// Fetch account with decrypted config
|
|
374
|
+
const { data } = await get(`/magic-mail/accounts/${editAccount.id}`);
|
|
375
|
+
const accountData = data.data;
|
|
376
|
+
|
|
377
|
+
setProvider(accountData.provider);
|
|
378
|
+
setCurrentStep(2); // Skip provider selection in edit mode
|
|
379
|
+
setFormData({
|
|
380
|
+
name: accountData.name || '',
|
|
381
|
+
description: accountData.description || '',
|
|
382
|
+
fromEmail: accountData.fromEmail || '',
|
|
383
|
+
fromName: accountData.fromName || '',
|
|
384
|
+
replyTo: accountData.replyTo || '',
|
|
385
|
+
isActive: accountData.isActive !== undefined ? accountData.isActive : true,
|
|
386
|
+
isPrimary: accountData.isPrimary || false,
|
|
387
|
+
priority: accountData.priority || 5,
|
|
388
|
+
dailyLimit: accountData.dailyLimit || 500,
|
|
389
|
+
hourlyLimit: accountData.hourlyLimit || 50,
|
|
390
|
+
host: accountData.config?.host || '',
|
|
391
|
+
port: accountData.config?.port || 587,
|
|
392
|
+
user: accountData.config?.user || '',
|
|
393
|
+
pass: accountData.config?.pass || '', // Now populated from decrypted data
|
|
394
|
+
secure: accountData.config?.secure || false,
|
|
395
|
+
apiKey: accountData.config?.apiKey || '', // Now populated from decrypted data
|
|
396
|
+
mailgunDomain: accountData.config?.domain || '',
|
|
397
|
+
microsoftTenantId: accountData.config?.tenantId || '',
|
|
398
|
+
oauthClientId: accountData.config?.clientId || '',
|
|
399
|
+
oauthClientSecret: accountData.config?.clientSecret || '', // Now populated from decrypted data
|
|
400
|
+
});
|
|
401
|
+
} catch (err) {
|
|
402
|
+
console.error('[magic-mail] Error loading account:', err);
|
|
403
|
+
toggleNotification({
|
|
404
|
+
type: 'danger',
|
|
405
|
+
message: 'Failed to load account data',
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
} else if (!isEditMode) {
|
|
409
|
+
setCurrentStep(1);
|
|
410
|
+
setProvider('');
|
|
411
|
+
setFormData({
|
|
412
|
+
name: '',
|
|
413
|
+
description: '',
|
|
414
|
+
fromEmail: '',
|
|
415
|
+
fromName: '',
|
|
416
|
+
replyTo: '',
|
|
417
|
+
isActive: true,
|
|
418
|
+
isPrimary: false,
|
|
419
|
+
priority: 5,
|
|
420
|
+
dailyLimit: 500,
|
|
421
|
+
hourlyLimit: 50,
|
|
422
|
+
host: '',
|
|
423
|
+
port: 587,
|
|
424
|
+
user: '',
|
|
425
|
+
pass: '',
|
|
426
|
+
secure: false,
|
|
427
|
+
apiKey: '',
|
|
428
|
+
mailgunDomain: '',
|
|
429
|
+
microsoftTenantId: '',
|
|
430
|
+
oauthClientId: '',
|
|
431
|
+
oauthClientSecret: '',
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
loadAccountData();
|
|
437
|
+
}, [isEditMode, editAccount, isOpen]);
|
|
438
|
+
|
|
439
|
+
// Check for OAuth callback parameters (from URL or postMessage)
|
|
440
|
+
React.useEffect(() => {
|
|
441
|
+
// Check URL params (fallback)
|
|
442
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
443
|
+
const code = urlParams.get('oauth_code');
|
|
444
|
+
const state = urlParams.get('oauth_state');
|
|
445
|
+
|
|
446
|
+
if (code && state) {
|
|
447
|
+
setOauthCode(code);
|
|
448
|
+
setOauthState(state);
|
|
449
|
+
|
|
450
|
+
// Clean URL
|
|
451
|
+
window.history.replaceState({}, document.title, window.location.pathname);
|
|
452
|
+
|
|
453
|
+
toggleNotification({
|
|
454
|
+
type: 'success',
|
|
455
|
+
message: '✅ Gmail OAuth authorized! Please complete the account setup.',
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Listen for postMessage from OAuth popup
|
|
460
|
+
const handleMessage = (event) => {
|
|
461
|
+
if (event.origin !== window.location.origin) return;
|
|
462
|
+
|
|
463
|
+
if (event.data.type === 'gmail-oauth-success') {
|
|
464
|
+
setOauthCode(event.data.code);
|
|
465
|
+
setOauthState(event.data.state);
|
|
466
|
+
|
|
467
|
+
toggleNotification({
|
|
468
|
+
type: 'success',
|
|
469
|
+
message: '✅ Gmail OAuth authorized! Please complete the account setup.',
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (event.data.type === 'microsoft-oauth-success') {
|
|
474
|
+
setOauthCode(event.data.code);
|
|
475
|
+
setOauthState(event.data.state);
|
|
476
|
+
|
|
477
|
+
toggleNotification({
|
|
478
|
+
type: 'success',
|
|
479
|
+
message: '✅ Microsoft OAuth authorized! Please complete the account setup.',
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (event.data.type === 'yahoo-oauth-success') {
|
|
484
|
+
setOauthCode(event.data.code);
|
|
485
|
+
setOauthState(event.data.state);
|
|
486
|
+
|
|
487
|
+
toggleNotification({
|
|
488
|
+
type: 'success',
|
|
489
|
+
message: '✅ Yahoo Mail OAuth authorized! Please complete the account setup.',
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
window.addEventListener('message', handleMessage);
|
|
495
|
+
|
|
496
|
+
return () => {
|
|
497
|
+
window.removeEventListener('message', handleMessage);
|
|
498
|
+
};
|
|
499
|
+
}, []);
|
|
500
|
+
|
|
501
|
+
const startGmailOAuth = async () => {
|
|
502
|
+
if (!formData.oauthClientId) {
|
|
503
|
+
toggleNotification({
|
|
504
|
+
type: 'warning',
|
|
505
|
+
message: 'Please enter your OAuth Client ID first',
|
|
506
|
+
});
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
try {
|
|
511
|
+
const { data } = await get(`/magic-mail/oauth/gmail/auth?clientId=${encodeURIComponent(formData.oauthClientId)}`);
|
|
512
|
+
|
|
513
|
+
if (data.authUrl) {
|
|
514
|
+
// Open OAuth in popup window
|
|
515
|
+
const width = 600;
|
|
516
|
+
const height = 700;
|
|
517
|
+
const left = (window.screen.width - width) / 2;
|
|
518
|
+
const top = (window.screen.height - height) / 2;
|
|
519
|
+
|
|
520
|
+
const popup = window.open(
|
|
521
|
+
data.authUrl,
|
|
522
|
+
'gmail-oauth',
|
|
523
|
+
`width=${width},height=${height},left=${left},top=${top},toolbar=no,menubar=no,location=no,status=no`
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
// Listen for OAuth callback
|
|
527
|
+
const checkPopup = setInterval(() => {
|
|
528
|
+
try {
|
|
529
|
+
if (popup.closed) {
|
|
530
|
+
clearInterval(checkPopup);
|
|
531
|
+
|
|
532
|
+
// Check if we got OAuth code from URL
|
|
533
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
534
|
+
const code = urlParams.get('oauth_code');
|
|
535
|
+
|
|
536
|
+
if (!code) {
|
|
537
|
+
toggleNotification({
|
|
538
|
+
type: 'info',
|
|
539
|
+
message: 'OAuth window closed. Please try again if not completed.',
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
} catch (err) {
|
|
544
|
+
// Cross-origin error is expected
|
|
545
|
+
}
|
|
546
|
+
}, 500);
|
|
547
|
+
|
|
548
|
+
toggleNotification({
|
|
549
|
+
type: 'info',
|
|
550
|
+
message: '🔐 Please authorize in the popup window...',
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
} catch (err) {
|
|
554
|
+
toggleNotification({
|
|
555
|
+
type: 'danger',
|
|
556
|
+
message: 'Failed to start OAuth flow',
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
const startMicrosoftOAuth = async () => {
|
|
562
|
+
if (!formData.microsoftTenantId) {
|
|
563
|
+
toggleNotification({
|
|
564
|
+
type: 'warning',
|
|
565
|
+
message: 'Please enter your Tenant (Directory) ID first',
|
|
566
|
+
});
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (!formData.oauthClientId) {
|
|
571
|
+
toggleNotification({
|
|
572
|
+
type: 'warning',
|
|
573
|
+
message: 'Please enter your Application (Client) ID',
|
|
574
|
+
});
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
try {
|
|
579
|
+
const { data } = await get(`/magic-mail/oauth/microsoft/auth?clientId=${encodeURIComponent(formData.oauthClientId)}&tenantId=${encodeURIComponent(formData.microsoftTenantId)}`);
|
|
580
|
+
|
|
581
|
+
if (data.authUrl) {
|
|
582
|
+
// Open OAuth in popup window
|
|
583
|
+
const width = 600;
|
|
584
|
+
const height = 700;
|
|
585
|
+
const left = (window.screen.width - width) / 2;
|
|
586
|
+
const top = (window.screen.height - height) / 2;
|
|
587
|
+
|
|
588
|
+
const popup = window.open(
|
|
589
|
+
data.authUrl,
|
|
590
|
+
'microsoft-oauth',
|
|
591
|
+
`width=${width},height=${height},left=${left},top=${top},toolbar=no,menubar=no,location=no,status=no`
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
// Listen for OAuth callback
|
|
595
|
+
const checkPopup = setInterval(() => {
|
|
596
|
+
try {
|
|
597
|
+
if (popup.closed) {
|
|
598
|
+
clearInterval(checkPopup);
|
|
599
|
+
|
|
600
|
+
// Check if we got OAuth code from URL
|
|
601
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
602
|
+
const code = urlParams.get('oauth_code');
|
|
603
|
+
|
|
604
|
+
if (!code) {
|
|
605
|
+
toggleNotification({
|
|
606
|
+
type: 'info',
|
|
607
|
+
message: 'OAuth window closed. Please try again if not completed.',
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
} catch (err) {
|
|
612
|
+
// Cross-origin error is expected
|
|
613
|
+
}
|
|
614
|
+
}, 500);
|
|
615
|
+
|
|
616
|
+
toggleNotification({
|
|
617
|
+
type: 'info',
|
|
618
|
+
message: '🔐 Please authorize in the popup window...',
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
} catch (err) {
|
|
622
|
+
toggleNotification({
|
|
623
|
+
type: 'danger',
|
|
624
|
+
message: 'Failed to start Microsoft OAuth flow',
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
const startYahooOAuth = async () => {
|
|
630
|
+
if (!formData.oauthClientId) {
|
|
631
|
+
toggleNotification({
|
|
632
|
+
type: 'warning',
|
|
633
|
+
message: 'Please enter your Yahoo Client ID first',
|
|
634
|
+
});
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
try {
|
|
639
|
+
const { data } = await get(`/magic-mail/oauth/yahoo/auth?clientId=${encodeURIComponent(formData.oauthClientId)}`);
|
|
640
|
+
|
|
641
|
+
if (data.authUrl) {
|
|
642
|
+
// Open OAuth in popup window
|
|
643
|
+
const width = 600;
|
|
644
|
+
const height = 700;
|
|
645
|
+
const left = (window.screen.width - width) / 2;
|
|
646
|
+
const top = (window.screen.height - height) / 2;
|
|
647
|
+
|
|
648
|
+
const popup = window.open(
|
|
649
|
+
data.authUrl,
|
|
650
|
+
'yahoo-oauth',
|
|
651
|
+
`width=${width},height=${height},left=${left},top=${top},toolbar=no,menubar=no,location=no,status=no`
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
// Listen for OAuth callback
|
|
655
|
+
const checkPopup = setInterval(() => {
|
|
656
|
+
try {
|
|
657
|
+
if (popup.closed) {
|
|
658
|
+
clearInterval(checkPopup);
|
|
659
|
+
|
|
660
|
+
// Check if we got OAuth code from URL
|
|
661
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
662
|
+
const code = urlParams.get('oauth_code');
|
|
663
|
+
|
|
664
|
+
if (!code) {
|
|
665
|
+
toggleNotification({
|
|
666
|
+
type: 'info',
|
|
667
|
+
message: 'OAuth window closed. Please try again if not completed.',
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
} catch (err) {
|
|
672
|
+
// Cross-origin error is expected
|
|
673
|
+
}
|
|
674
|
+
}, 500);
|
|
675
|
+
|
|
676
|
+
toggleNotification({
|
|
677
|
+
type: 'info',
|
|
678
|
+
message: '🔐 Please authorize in the popup window...',
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
} catch (err) {
|
|
682
|
+
toggleNotification({
|
|
683
|
+
type: 'danger',
|
|
684
|
+
message: 'Failed to start Yahoo OAuth flow',
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
const canProceed = () => {
|
|
690
|
+
if (currentStep === 1) return provider !== '';
|
|
691
|
+
if (currentStep === 2) {
|
|
692
|
+
// For OAuth providers, fromEmail comes from provider, so only name is required
|
|
693
|
+
if (provider === 'gmail-oauth' || provider === 'microsoft-oauth' || provider === 'yahoo-oauth') return formData.name;
|
|
694
|
+
return formData.name && formData.fromEmail;
|
|
695
|
+
}
|
|
696
|
+
if (currentStep === 3) {
|
|
697
|
+
if (provider === 'smtp') {
|
|
698
|
+
// Password is required (we show the decrypted one in edit mode)
|
|
699
|
+
return formData.host && formData.user && formData.pass;
|
|
700
|
+
}
|
|
701
|
+
if (provider === 'gmail-oauth' || provider === 'yahoo-oauth') {
|
|
702
|
+
// In edit mode with existing credentials, they're pre-filled
|
|
703
|
+
if (isEditMode && formData.oauthClientId && formData.oauthClientSecret) {
|
|
704
|
+
return true;
|
|
705
|
+
}
|
|
706
|
+
// Only allow proceed if successfully connected (has oauthCode)
|
|
707
|
+
return !!oauthCode;
|
|
708
|
+
}
|
|
709
|
+
if (provider === 'microsoft-oauth') {
|
|
710
|
+
// In edit mode with existing credentials, they're pre-filled
|
|
711
|
+
if (isEditMode && formData.oauthClientId && formData.oauthClientSecret && formData.microsoftTenantId) {
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
// Only allow proceed if successfully connected (has oauthCode)
|
|
715
|
+
return !!oauthCode;
|
|
716
|
+
}
|
|
717
|
+
if (provider === 'sendgrid') {
|
|
718
|
+
// API key is required
|
|
719
|
+
return !!formData.apiKey;
|
|
720
|
+
}
|
|
721
|
+
if (provider === 'mailgun') {
|
|
722
|
+
// API key AND domain are required
|
|
723
|
+
return !!formData.apiKey && !!formData.mailgunDomain;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return true;
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
const handleSubmit = async () => {
|
|
730
|
+
setLoading(true);
|
|
731
|
+
try {
|
|
732
|
+
let config = {};
|
|
733
|
+
|
|
734
|
+
// OAuth providers with full OAuth flow (only for creation, not edit)
|
|
735
|
+
if (!isEditMode && (provider === 'gmail-oauth' || provider === 'microsoft-oauth' || provider === 'yahoo-oauth') && oauthCode && oauthState) {
|
|
736
|
+
// Use OAuth endpoint to exchange code for tokens and create account
|
|
737
|
+
const accountDetails = {
|
|
738
|
+
name: formData.name,
|
|
739
|
+
description: formData.description,
|
|
740
|
+
fromEmail: 'oauth@placeholder.com', // Will be replaced by provider email
|
|
741
|
+
fromName: formData.fromName,
|
|
742
|
+
replyTo: formData.replyTo,
|
|
743
|
+
isPrimary: formData.isPrimary,
|
|
744
|
+
priority: formData.priority,
|
|
745
|
+
dailyLimit: formData.dailyLimit,
|
|
746
|
+
hourlyLimit: formData.hourlyLimit,
|
|
747
|
+
config: provider === 'microsoft-oauth' ? {
|
|
748
|
+
clientId: formData.oauthClientId,
|
|
749
|
+
clientSecret: formData.oauthClientSecret,
|
|
750
|
+
tenantId: formData.microsoftTenantId,
|
|
751
|
+
} : {
|
|
752
|
+
clientId: formData.oauthClientId,
|
|
753
|
+
clientSecret: formData.oauthClientSecret,
|
|
754
|
+
},
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
const providerMap = {
|
|
758
|
+
'gmail-oauth': 'gmail',
|
|
759
|
+
'microsoft-oauth': 'microsoft',
|
|
760
|
+
'yahoo-oauth': 'yahoo',
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
await post('/magic-mail/oauth/create-account', {
|
|
764
|
+
provider: providerMap[provider],
|
|
765
|
+
code: oauthCode,
|
|
766
|
+
state: oauthState,
|
|
767
|
+
accountDetails,
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
const providerNames = {
|
|
771
|
+
'gmail-oauth': 'Gmail',
|
|
772
|
+
'microsoft-oauth': 'Microsoft',
|
|
773
|
+
'yahoo-oauth': 'Yahoo Mail',
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
toggleNotification({
|
|
777
|
+
type: 'success',
|
|
778
|
+
message: `✅ ${formData.name} created successfully with ${providerNames[provider]} OAuth!`,
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
onAccountAdded();
|
|
782
|
+
onClose();
|
|
783
|
+
setCurrentStep(1);
|
|
784
|
+
setOauthCode(null);
|
|
785
|
+
setOauthState(null);
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Prepare config based on provider
|
|
790
|
+
if (provider === 'smtp') {
|
|
791
|
+
config = {
|
|
792
|
+
host: formData.host,
|
|
793
|
+
port: formData.port,
|
|
794
|
+
user: formData.user,
|
|
795
|
+
pass: formData.pass, // Now always available (decrypted in edit mode)
|
|
796
|
+
secure: formData.secure,
|
|
797
|
+
};
|
|
798
|
+
} else if (provider === 'sendgrid') {
|
|
799
|
+
config = {
|
|
800
|
+
apiKey: formData.apiKey // Now always available (decrypted in edit mode)
|
|
801
|
+
};
|
|
802
|
+
} else if (provider === 'mailgun') {
|
|
803
|
+
config = {
|
|
804
|
+
apiKey: formData.apiKey, // Now always available (decrypted in edit mode)
|
|
805
|
+
domain: formData.mailgunDomain,
|
|
806
|
+
};
|
|
807
|
+
} else if (provider === 'gmail-oauth' || provider === 'yahoo-oauth') {
|
|
808
|
+
config = {
|
|
809
|
+
clientId: formData.oauthClientId,
|
|
810
|
+
clientSecret: formData.oauthClientSecret, // Now always available (decrypted in edit mode)
|
|
811
|
+
};
|
|
812
|
+
} else if (provider === 'microsoft-oauth') {
|
|
813
|
+
config = {
|
|
814
|
+
clientId: formData.oauthClientId,
|
|
815
|
+
clientSecret: formData.oauthClientSecret, // Now always available (decrypted in edit mode)
|
|
816
|
+
tenantId: formData.microsoftTenantId,
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const payload = {
|
|
821
|
+
name: formData.name,
|
|
822
|
+
description: formData.description,
|
|
823
|
+
provider,
|
|
824
|
+
config,
|
|
825
|
+
fromEmail: formData.fromEmail,
|
|
826
|
+
fromName: formData.fromName,
|
|
827
|
+
replyTo: formData.replyTo,
|
|
828
|
+
isActive: formData.isActive,
|
|
829
|
+
isPrimary: formData.isPrimary,
|
|
830
|
+
priority: formData.priority,
|
|
831
|
+
dailyLimit: formData.dailyLimit,
|
|
832
|
+
hourlyLimit: formData.hourlyLimit,
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
if (isEditMode) {
|
|
836
|
+
// Update existing account
|
|
837
|
+
await put(`/magic-mail/accounts/${editAccount.id}`, payload);
|
|
838
|
+
toggleNotification({
|
|
839
|
+
type: 'success',
|
|
840
|
+
message: `✅ ${formData.name} updated successfully!`,
|
|
841
|
+
});
|
|
842
|
+
} else {
|
|
843
|
+
// Create new account
|
|
844
|
+
await post('/magic-mail/accounts', payload);
|
|
845
|
+
toggleNotification({
|
|
846
|
+
type: 'success',
|
|
847
|
+
message: `✅ ${formData.name} created successfully!`,
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
onAccountAdded();
|
|
852
|
+
onClose();
|
|
853
|
+
setCurrentStep(1);
|
|
854
|
+
} catch (err) {
|
|
855
|
+
toggleNotification({
|
|
856
|
+
type: 'danger',
|
|
857
|
+
message: err.response?.data?.error?.message || `Failed to ${isEditMode ? 'update' : 'create'} account`,
|
|
858
|
+
});
|
|
859
|
+
} finally {
|
|
860
|
+
setLoading(false);
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
const getProviderLabel = () => {
|
|
865
|
+
switch(provider) {
|
|
866
|
+
case 'gmail-oauth': return 'Gmail OAuth';
|
|
867
|
+
case 'microsoft-oauth': return 'Microsoft OAuth';
|
|
868
|
+
case 'yahoo-oauth': return 'Yahoo Mail OAuth';
|
|
869
|
+
case 'smtp': return 'SMTP';
|
|
870
|
+
case 'sendgrid': return 'SendGrid';
|
|
871
|
+
case 'mailgun': return 'Mailgun';
|
|
872
|
+
default: return '';
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
const stepTitles = ['Provider', 'Details', 'Credentials', 'Settings'];
|
|
877
|
+
|
|
878
|
+
return (
|
|
879
|
+
<Modal.Root open={isOpen} onOpenChange={onClose}>
|
|
880
|
+
<Modal.Content size="XL">
|
|
881
|
+
<Modal.Header>
|
|
882
|
+
<Typography variant="beta">
|
|
883
|
+
<Star style={{ marginRight: 8 }} />
|
|
884
|
+
{isEditMode ? 'Edit Email Account' : 'Add Email Account'}
|
|
885
|
+
</Typography>
|
|
886
|
+
</Modal.Header>
|
|
887
|
+
|
|
888
|
+
<Modal.Body>
|
|
889
|
+
<Flex direction="column" gap={0}>
|
|
890
|
+
|
|
891
|
+
{/* Header */}
|
|
892
|
+
<StepHeader>
|
|
893
|
+
<StepTitle>
|
|
894
|
+
{currentStep === 1 && <Mail />}
|
|
895
|
+
{currentStep === 2 && <Server />}
|
|
896
|
+
{currentStep === 3 && <Lock />}
|
|
897
|
+
{currentStep === 4 && <Cog />}
|
|
898
|
+
{currentStep === 1 && 'Choose Email Provider'}
|
|
899
|
+
{currentStep === 2 && 'Account Details'}
|
|
900
|
+
{currentStep === 3 && 'Authentication'}
|
|
901
|
+
{currentStep === 4 && 'Configuration'}
|
|
902
|
+
</StepTitle>
|
|
903
|
+
<StepSubtitle>
|
|
904
|
+
{currentStep === 1 && 'Select your preferred email service provider'}
|
|
905
|
+
{currentStep === 2 && 'Configure how emails will appear to recipients'}
|
|
906
|
+
{currentStep === 3 && `Enter your ${getProviderLabel()} credentials securely`}
|
|
907
|
+
{currentStep === 4 && 'Set rate limits and priority for this account'}
|
|
908
|
+
</StepSubtitle>
|
|
909
|
+
</StepHeader>
|
|
910
|
+
|
|
911
|
+
{/* Stepper */}
|
|
912
|
+
<StepperContainer>
|
|
913
|
+
{[1, 2, 3, 4].map((step) => (
|
|
914
|
+
<StepWrapper
|
|
915
|
+
key={step}
|
|
916
|
+
$completed={currentStep > step}
|
|
917
|
+
>
|
|
918
|
+
<StepDot
|
|
919
|
+
$active={currentStep === step}
|
|
920
|
+
$completed={currentStep > step}
|
|
921
|
+
onClick={() => currentStep > step && setCurrentStep(step)}
|
|
922
|
+
>
|
|
923
|
+
{currentStep > step ? <Check /> : step}
|
|
924
|
+
</StepDot>
|
|
925
|
+
<StepLabel
|
|
926
|
+
$active={currentStep === step}
|
|
927
|
+
$completed={currentStep > step}
|
|
928
|
+
>
|
|
929
|
+
{stepTitles[step - 1]}
|
|
930
|
+
</StepLabel>
|
|
931
|
+
</StepWrapper>
|
|
932
|
+
))}
|
|
933
|
+
</StepperContainer>
|
|
934
|
+
|
|
935
|
+
{/* Step 1: Choose Provider */}
|
|
936
|
+
{currentStep === 1 && (
|
|
937
|
+
<Box>
|
|
938
|
+
<ProvidersGrid>
|
|
939
|
+
<ProviderCard
|
|
940
|
+
$selected={provider === 'gmail-oauth'}
|
|
941
|
+
onClick={() => setProvider('gmail-oauth')}
|
|
942
|
+
>
|
|
943
|
+
<ProviderIcon
|
|
944
|
+
$round
|
|
945
|
+
$bgColor="#4285F433"
|
|
946
|
+
$color="#4285F4"
|
|
947
|
+
$shadowColor="rgba(66, 133, 244, 0.2)"
|
|
948
|
+
>
|
|
949
|
+
G
|
|
950
|
+
</ProviderIcon>
|
|
951
|
+
<ProviderName>Gmail OAuth</ProviderName>
|
|
952
|
+
</ProviderCard>
|
|
953
|
+
|
|
954
|
+
<ProviderCard
|
|
955
|
+
$selected={provider === 'microsoft-oauth'}
|
|
956
|
+
onClick={() => setProvider('microsoft-oauth')}
|
|
957
|
+
>
|
|
958
|
+
<ProviderIcon
|
|
959
|
+
$round
|
|
960
|
+
$bgColor="#00A4EF33"
|
|
961
|
+
$color="#00A4EF"
|
|
962
|
+
$shadowColor="rgba(0, 164, 239, 0.2)"
|
|
963
|
+
>
|
|
964
|
+
M
|
|
965
|
+
</ProviderIcon>
|
|
966
|
+
<ProviderName>Microsoft OAuth</ProviderName>
|
|
967
|
+
</ProviderCard>
|
|
968
|
+
|
|
969
|
+
<ProviderCard
|
|
970
|
+
$selected={provider === 'yahoo-oauth'}
|
|
971
|
+
onClick={() => setProvider('yahoo-oauth')}
|
|
972
|
+
>
|
|
973
|
+
<ProviderIcon
|
|
974
|
+
$round
|
|
975
|
+
$bgColor="#6001D233"
|
|
976
|
+
$color="#6001D2"
|
|
977
|
+
$shadowColor="rgba(96, 1, 210, 0.2)"
|
|
978
|
+
>
|
|
979
|
+
Y
|
|
980
|
+
</ProviderIcon>
|
|
981
|
+
<ProviderName>Yahoo Mail OAuth</ProviderName>
|
|
982
|
+
</ProviderCard>
|
|
983
|
+
|
|
984
|
+
<ProviderCard
|
|
985
|
+
$selected={provider === 'smtp'}
|
|
986
|
+
onClick={() => setProvider('smtp')}
|
|
987
|
+
>
|
|
988
|
+
<ProviderIcon>
|
|
989
|
+
<Server style={{ width: 28, height: 28 }} />
|
|
990
|
+
</ProviderIcon>
|
|
991
|
+
<ProviderName>SMTP</ProviderName>
|
|
992
|
+
</ProviderCard>
|
|
993
|
+
|
|
994
|
+
<ProviderCard
|
|
995
|
+
$selected={provider === 'sendgrid'}
|
|
996
|
+
onClick={() => setProvider('sendgrid')}
|
|
997
|
+
>
|
|
998
|
+
<ProviderIcon $bgColor="#1E90FF22" $color="#1E90FF" $shadowColor="rgba(30, 144, 255, 0.2)">
|
|
999
|
+
<Cloud style={{ width: 28, height: 28 }} />
|
|
1000
|
+
</ProviderIcon>
|
|
1001
|
+
<ProviderName>SendGrid</ProviderName>
|
|
1002
|
+
</ProviderCard>
|
|
1003
|
+
|
|
1004
|
+
<ProviderCard
|
|
1005
|
+
$selected={provider === 'mailgun'}
|
|
1006
|
+
onClick={() => setProvider('mailgun')}
|
|
1007
|
+
>
|
|
1008
|
+
<ProviderIcon $bgColor="#FF6B6B22" $color="#FF6B6B" $shadowColor="rgba(255, 107, 107, 0.2)">
|
|
1009
|
+
<Mail style={{ width: 28, height: 28 }} />
|
|
1010
|
+
</ProviderIcon>
|
|
1011
|
+
<ProviderName>Mailgun</ProviderName>
|
|
1012
|
+
</ProviderCard>
|
|
1013
|
+
</ProvidersGrid>
|
|
1014
|
+
</Box>
|
|
1015
|
+
)}
|
|
1016
|
+
|
|
1017
|
+
{/* Step 2: Name Your Account */}
|
|
1018
|
+
{currentStep === 2 && (
|
|
1019
|
+
<FormSection>
|
|
1020
|
+
<Flex direction="column" gap={4} style={{ width: '100%' }}>
|
|
1021
|
+
<FullWidthField>
|
|
1022
|
+
<Field.Root required>
|
|
1023
|
+
<Field.Label>Account Name</Field.Label>
|
|
1024
|
+
<TextInput
|
|
1025
|
+
placeholder="e.g., Company Gmail, Marketing SendGrid, Transactional Emails"
|
|
1026
|
+
value={formData.name}
|
|
1027
|
+
onChange={(e) => handleChange('name', e.target.value)}
|
|
1028
|
+
/>
|
|
1029
|
+
<Field.Hint>Give this email account a unique, descriptive name so you can easily identify it later</Field.Hint>
|
|
1030
|
+
</Field.Root>
|
|
1031
|
+
</FullWidthField>
|
|
1032
|
+
|
|
1033
|
+
{provider === 'gmail-oauth' ? (
|
|
1034
|
+
<Alert variant="default" title="📧 Email Address">
|
|
1035
|
+
<Typography variant="pi">
|
|
1036
|
+
Your Gmail address will be automatically retrieved from Google after OAuth authorization.
|
|
1037
|
+
You don't need to enter it manually.
|
|
1038
|
+
</Typography>
|
|
1039
|
+
</Alert>
|
|
1040
|
+
) : (
|
|
1041
|
+
<FullWidthField>
|
|
1042
|
+
<Field.Root required>
|
|
1043
|
+
<Field.Label>From Email Address</Field.Label>
|
|
1044
|
+
<TextInput
|
|
1045
|
+
placeholder="noreply@company.com"
|
|
1046
|
+
type="email"
|
|
1047
|
+
value={formData.fromEmail}
|
|
1048
|
+
onChange={(e) => handleChange('fromEmail', e.target.value)}
|
|
1049
|
+
/>
|
|
1050
|
+
<Field.Hint>The email address that will appear as the sender. Recipients will see this in their inbox</Field.Hint>
|
|
1051
|
+
</Field.Root>
|
|
1052
|
+
</FullWidthField>
|
|
1053
|
+
)}
|
|
1054
|
+
|
|
1055
|
+
<FullWidthField>
|
|
1056
|
+
<Field.Root>
|
|
1057
|
+
<Field.Label>From Display Name</Field.Label>
|
|
1058
|
+
<TextInput
|
|
1059
|
+
placeholder="Company Name"
|
|
1060
|
+
value={formData.fromName}
|
|
1061
|
+
onChange={(e) => handleChange('fromName', e.target.value)}
|
|
1062
|
+
/>
|
|
1063
|
+
<Field.Hint>The friendly name shown next to the email address (e.g., 'ACME Corp' instead of just 'noreply@acme.com')</Field.Hint>
|
|
1064
|
+
</Field.Root>
|
|
1065
|
+
</FullWidthField>
|
|
1066
|
+
|
|
1067
|
+
<FullWidthField>
|
|
1068
|
+
<Field.Root>
|
|
1069
|
+
<Field.Label>Reply-To Email Address</Field.Label>
|
|
1070
|
+
<TextInput
|
|
1071
|
+
placeholder="support@company.com"
|
|
1072
|
+
type="email"
|
|
1073
|
+
value={formData.replyTo}
|
|
1074
|
+
onChange={(e) => handleChange('replyTo', e.target.value)}
|
|
1075
|
+
/>
|
|
1076
|
+
<Field.Hint>When recipients hit 'Reply', their response will go to this address. Leave empty to use the From Email</Field.Hint>
|
|
1077
|
+
</Field.Root>
|
|
1078
|
+
</FullWidthField>
|
|
1079
|
+
|
|
1080
|
+
<FullWidthField>
|
|
1081
|
+
<Field.Root>
|
|
1082
|
+
<Field.Label>Account Description</Field.Label>
|
|
1083
|
+
<Textarea
|
|
1084
|
+
placeholder="What is this account used for? (e.g., 'Marketing campaigns', 'Order confirmations', 'Password resets')"
|
|
1085
|
+
value={formData.description}
|
|
1086
|
+
onChange={(e) => handleChange('description', e.target.value)}
|
|
1087
|
+
/>
|
|
1088
|
+
<Field.Hint>Add notes about this account's purpose, usage limits, or any special configuration. Only visible to admins</Field.Hint>
|
|
1089
|
+
</Field.Root>
|
|
1090
|
+
</FullWidthField>
|
|
1091
|
+
</Flex>
|
|
1092
|
+
</FormSection>
|
|
1093
|
+
)}
|
|
1094
|
+
|
|
1095
|
+
{/* Step 3: Credentials */}
|
|
1096
|
+
{currentStep === 3 && (
|
|
1097
|
+
<FormSection>
|
|
1098
|
+
{provider === 'smtp' && (
|
|
1099
|
+
<>
|
|
1100
|
+
<InfoAlert variant="success" title="🔒 Secure Storage" marginBottom={4}>
|
|
1101
|
+
All credentials are encrypted with AES-256-GCM before storage. No plain text passwords in the database.
|
|
1102
|
+
</InfoAlert>
|
|
1103
|
+
|
|
1104
|
+
<Flex direction="column" gap={4} style={{ width: '100%' }}>
|
|
1105
|
+
<SectionTitle>
|
|
1106
|
+
<Server />
|
|
1107
|
+
Server Connection
|
|
1108
|
+
</SectionTitle>
|
|
1109
|
+
|
|
1110
|
+
<FullWidthField>
|
|
1111
|
+
<Field.Root required>
|
|
1112
|
+
<Field.Label>SMTP Host Server</Field.Label>
|
|
1113
|
+
<TextInput
|
|
1114
|
+
placeholder="smtp.gmail.com"
|
|
1115
|
+
value={formData.host}
|
|
1116
|
+
onChange={(e) => handleChange('host', e.target.value)}
|
|
1117
|
+
/>
|
|
1118
|
+
<Field.Hint>The address of your email server. Common examples: smtp.gmail.com (Gmail), smtp-mail.outlook.com (Outlook), smtp.sendgrid.net (SendGrid)</Field.Hint>
|
|
1119
|
+
</Field.Root>
|
|
1120
|
+
</FullWidthField>
|
|
1121
|
+
|
|
1122
|
+
<FullWidthField>
|
|
1123
|
+
<Field.Root required>
|
|
1124
|
+
<Field.Label>Port Number</Field.Label>
|
|
1125
|
+
<NumberInput
|
|
1126
|
+
value={formData.port}
|
|
1127
|
+
onValueChange={(value) => handleChange('port', value)}
|
|
1128
|
+
/>
|
|
1129
|
+
<Field.Hint>Standard ports: 587 (recommended - STARTTLS), 465 (SSL/TLS), or 25 (unencrypted - not recommended)</Field.Hint>
|
|
1130
|
+
</Field.Root>
|
|
1131
|
+
</FullWidthField>
|
|
1132
|
+
|
|
1133
|
+
<Divider />
|
|
1134
|
+
|
|
1135
|
+
<SectionTitle>
|
|
1136
|
+
<Lock />
|
|
1137
|
+
Authentication Credentials
|
|
1138
|
+
</SectionTitle>
|
|
1139
|
+
|
|
1140
|
+
<FullWidthField>
|
|
1141
|
+
<Field.Root required>
|
|
1142
|
+
<Field.Label>Username / Email</Field.Label>
|
|
1143
|
+
<TextInput
|
|
1144
|
+
placeholder="your-email@gmail.com"
|
|
1145
|
+
value={formData.user}
|
|
1146
|
+
onChange={(e) => handleChange('user', e.target.value)}
|
|
1147
|
+
/>
|
|
1148
|
+
<Field.Hint>Usually your full email address. Some providers may use just the username part before the @</Field.Hint>
|
|
1149
|
+
</Field.Root>
|
|
1150
|
+
</FullWidthField>
|
|
1151
|
+
|
|
1152
|
+
<FullWidthField>
|
|
1153
|
+
<Field.Root required>
|
|
1154
|
+
<Field.Label>Password / App Password</Field.Label>
|
|
1155
|
+
<TextInput
|
|
1156
|
+
type="password"
|
|
1157
|
+
placeholder="Enter your password"
|
|
1158
|
+
value={formData.pass}
|
|
1159
|
+
onChange={(e) => handleChange('pass', e.target.value)}
|
|
1160
|
+
/>
|
|
1161
|
+
<Field.Hint>For Gmail: Create an App Password in Google Account → Security → 2-Step Verification → App passwords. Regular passwords won't work with 2FA enabled</Field.Hint>
|
|
1162
|
+
</Field.Root>
|
|
1163
|
+
</FullWidthField>
|
|
1164
|
+
|
|
1165
|
+
<Box
|
|
1166
|
+
padding={4}
|
|
1167
|
+
background={formData.secure ? '#DCFCE7' : '#FEF3C7'}
|
|
1168
|
+
hasRadius
|
|
1169
|
+
style={{
|
|
1170
|
+
border: formData.secure ? '2px solid #22C55E' : '2px solid #F59E0B',
|
|
1171
|
+
borderRadius: '8px',
|
|
1172
|
+
transition: 'all 0.2s ease',
|
|
1173
|
+
}}
|
|
1174
|
+
>
|
|
1175
|
+
<Flex gap={3} alignItems="center">
|
|
1176
|
+
<Toggle
|
|
1177
|
+
checked={formData.secure}
|
|
1178
|
+
onChange={() => handleChange('secure', !formData.secure)}
|
|
1179
|
+
/>
|
|
1180
|
+
<Flex direction="column" gap={1} style={{ flex: 1 }}>
|
|
1181
|
+
<Flex alignItems="center" gap={2}>
|
|
1182
|
+
<Typography fontWeight="semiBold" style={{ fontSize: '14px' }}>
|
|
1183
|
+
{formData.secure ? '🔒' : '⚠️'} Use SSL/TLS Encryption
|
|
1184
|
+
</Typography>
|
|
1185
|
+
<Badge
|
|
1186
|
+
backgroundColor={formData.secure ? 'success600' : 'warning600'}
|
|
1187
|
+
textColor="neutral0"
|
|
1188
|
+
size="S"
|
|
1189
|
+
>
|
|
1190
|
+
{formData.secure ? 'ENABLED' : 'DISABLED'}
|
|
1191
|
+
</Badge>
|
|
1192
|
+
</Flex>
|
|
1193
|
+
<Typography variant="pi" textColor="neutral600" style={{ fontSize: '13px', lineHeight: '1.5' }}>
|
|
1194
|
+
{formData.secure
|
|
1195
|
+
? 'SSL/TLS enabled - Use this for port 465'
|
|
1196
|
+
: 'SSL/TLS disabled - Port 587 will use STARTTLS instead'
|
|
1197
|
+
}
|
|
1198
|
+
</Typography>
|
|
1199
|
+
</Flex>
|
|
1200
|
+
</Flex>
|
|
1201
|
+
</Box>
|
|
1202
|
+
</Flex>
|
|
1203
|
+
</>
|
|
1204
|
+
)}
|
|
1205
|
+
|
|
1206
|
+
{provider === 'gmail-oauth' && (
|
|
1207
|
+
<>
|
|
1208
|
+
<InfoAlert variant="success" title="🔒 OAuth 2.0 Security" marginBottom={4}>
|
|
1209
|
+
No passwords stored. Users authenticate directly with Google for maximum security.
|
|
1210
|
+
</InfoAlert>
|
|
1211
|
+
|
|
1212
|
+
<Flex direction="column" gap={4} style={{ width: '100%' }}>
|
|
1213
|
+
<SectionTitle>
|
|
1214
|
+
<Lock />
|
|
1215
|
+
Google OAuth Application
|
|
1216
|
+
</SectionTitle>
|
|
1217
|
+
|
|
1218
|
+
<FullWidthField>
|
|
1219
|
+
<Field.Root required>
|
|
1220
|
+
<Field.Label>OAuth Client ID</Field.Label>
|
|
1221
|
+
<TextInput
|
|
1222
|
+
placeholder="123456789-abc123xyz.apps.googleusercontent.com"
|
|
1223
|
+
value={formData.oauthClientId}
|
|
1224
|
+
onChange={(e) => handleChange('oauthClientId', e.target.value)}
|
|
1225
|
+
/>
|
|
1226
|
+
<Field.Hint>Found in Google Cloud Console → APIs & Services → Credentials. Looks like a long string ending in .apps.googleusercontent.com</Field.Hint>
|
|
1227
|
+
</Field.Root>
|
|
1228
|
+
</FullWidthField>
|
|
1229
|
+
|
|
1230
|
+
<FullWidthField>
|
|
1231
|
+
<Field.Root required>
|
|
1232
|
+
<Field.Label>OAuth Client Secret</Field.Label>
|
|
1233
|
+
<TextInput
|
|
1234
|
+
type="password"
|
|
1235
|
+
placeholder="GOCSPX-abcdefghijklmnop"
|
|
1236
|
+
value={formData.oauthClientSecret}
|
|
1237
|
+
onChange={(e) => handleChange('oauthClientSecret', e.target.value)}
|
|
1238
|
+
/>
|
|
1239
|
+
<Field.Hint>Keep this secret! Found in the same OAuth 2.0 Client ID settings. Never share or commit to git</Field.Hint>
|
|
1240
|
+
</Field.Root>
|
|
1241
|
+
</FullWidthField>
|
|
1242
|
+
|
|
1243
|
+
<Divider />
|
|
1244
|
+
|
|
1245
|
+
{oauthCode ? (
|
|
1246
|
+
<Alert variant="success" title="✅ OAuth Authorized!">
|
|
1247
|
+
<Typography variant="pi">
|
|
1248
|
+
You've successfully authorized with Google! Click "Continue" to proceed to settings.
|
|
1249
|
+
</Typography>
|
|
1250
|
+
</Alert>
|
|
1251
|
+
) : (
|
|
1252
|
+
<Box>
|
|
1253
|
+
<Typography variant="omega" textColor="neutral600" marginBottom={3}>
|
|
1254
|
+
After entering your credentials above, click the button below to connect with Gmail:
|
|
1255
|
+
</Typography>
|
|
1256
|
+
<Button
|
|
1257
|
+
onClick={startGmailOAuth}
|
|
1258
|
+
variant="secondary"
|
|
1259
|
+
size="L"
|
|
1260
|
+
disabled={!formData.oauthClientId}
|
|
1261
|
+
style={{
|
|
1262
|
+
width: '100%',
|
|
1263
|
+
background: '#4285F4',
|
|
1264
|
+
color: 'white',
|
|
1265
|
+
fontWeight: 600,
|
|
1266
|
+
}}
|
|
1267
|
+
>
|
|
1268
|
+
🔐 Connect with Google
|
|
1269
|
+
</Button>
|
|
1270
|
+
</Box>
|
|
1271
|
+
)}
|
|
1272
|
+
|
|
1273
|
+
<Box
|
|
1274
|
+
padding={4}
|
|
1275
|
+
background="neutral100"
|
|
1276
|
+
hasRadius
|
|
1277
|
+
style={{
|
|
1278
|
+
border: `1px solid ${colors.border}`,
|
|
1279
|
+
borderRadius: '8px'
|
|
1280
|
+
}}
|
|
1281
|
+
>
|
|
1282
|
+
<Typography
|
|
1283
|
+
fontWeight="semiBold"
|
|
1284
|
+
marginBottom={3}
|
|
1285
|
+
style={{ fontSize: '15px' }}
|
|
1286
|
+
>
|
|
1287
|
+
📋 Setup Guide
|
|
1288
|
+
</Typography>
|
|
1289
|
+
|
|
1290
|
+
<Flex direction="column" gap={2} style={{ fontSize: '14px', lineHeight: '1.6' }}>
|
|
1291
|
+
<Box>
|
|
1292
|
+
<strong>1.</strong> Go to <a href="https://console.cloud.google.com" target="_blank" rel="noopener noreferrer" style={{ color: colors.primary, textDecoration: 'underline' }}>console.cloud.google.com</a>
|
|
1293
|
+
</Box>
|
|
1294
|
+
|
|
1295
|
+
<Box>
|
|
1296
|
+
<strong>2.</strong> Enable Gmail API (search and click Enable)
|
|
1297
|
+
</Box>
|
|
1298
|
+
|
|
1299
|
+
<Box>
|
|
1300
|
+
<strong>3.</strong> Create Credentials → OAuth Client ID
|
|
1301
|
+
</Box>
|
|
1302
|
+
|
|
1303
|
+
<Box>
|
|
1304
|
+
<strong>4.</strong> Add this redirect URI:
|
|
1305
|
+
</Box>
|
|
1306
|
+
|
|
1307
|
+
<Flex gap={2} alignItems="center">
|
|
1308
|
+
<Box
|
|
1309
|
+
padding={2}
|
|
1310
|
+
background="neutral0"
|
|
1311
|
+
hasRadius
|
|
1312
|
+
style={{
|
|
1313
|
+
flex: 1,
|
|
1314
|
+
fontFamily: 'monospace',
|
|
1315
|
+
fontSize: '13px',
|
|
1316
|
+
wordBreak: 'break-all',
|
|
1317
|
+
border: `1px solid ${colors.border}`
|
|
1318
|
+
}}
|
|
1319
|
+
>
|
|
1320
|
+
{window.location.origin}/magic-mail/oauth/gmail/callback
|
|
1321
|
+
</Box>
|
|
1322
|
+
<Button
|
|
1323
|
+
variant="secondary"
|
|
1324
|
+
size="S"
|
|
1325
|
+
onClick={() => {
|
|
1326
|
+
navigator.clipboard.writeText(`${window.location.origin}/magic-mail/oauth/gmail/callback`);
|
|
1327
|
+
toggleNotification({
|
|
1328
|
+
type: 'success',
|
|
1329
|
+
message: 'Redirect URI copied to clipboard!',
|
|
1330
|
+
});
|
|
1331
|
+
}}
|
|
1332
|
+
>
|
|
1333
|
+
Copy
|
|
1334
|
+
</Button>
|
|
1335
|
+
</Flex>
|
|
1336
|
+
</Flex>
|
|
1337
|
+
</Box>
|
|
1338
|
+
</Flex>
|
|
1339
|
+
</>
|
|
1340
|
+
)}
|
|
1341
|
+
|
|
1342
|
+
{provider === 'microsoft-oauth' && (
|
|
1343
|
+
<>
|
|
1344
|
+
<InfoAlert variant="success" title="🔒 OAuth 2.0 Security" marginBottom={4}>
|
|
1345
|
+
No passwords stored. Users authenticate directly with Microsoft for maximum security.
|
|
1346
|
+
</InfoAlert>
|
|
1347
|
+
|
|
1348
|
+
<Flex direction="column" gap={4} style={{ width: '100%' }}>
|
|
1349
|
+
<SectionTitle>
|
|
1350
|
+
<Lock />
|
|
1351
|
+
Microsoft Azure Application
|
|
1352
|
+
</SectionTitle>
|
|
1353
|
+
|
|
1354
|
+
<FullWidthField>
|
|
1355
|
+
<Field.Root required>
|
|
1356
|
+
<Field.Label>Tenant (Directory) ID</Field.Label>
|
|
1357
|
+
<TextInput
|
|
1358
|
+
placeholder="87654321-4321-4321-4321-987654321abc"
|
|
1359
|
+
value={formData.microsoftTenantId}
|
|
1360
|
+
onChange={(e) => handleChange('microsoftTenantId', e.target.value)}
|
|
1361
|
+
/>
|
|
1362
|
+
<Field.Hint>Found in Azure Portal → App Registrations → Your App → Overview (next to Application ID). Also a GUID format. Required for OAuth!</Field.Hint>
|
|
1363
|
+
</Field.Root>
|
|
1364
|
+
</FullWidthField>
|
|
1365
|
+
|
|
1366
|
+
<FullWidthField>
|
|
1367
|
+
<Field.Root required>
|
|
1368
|
+
<Field.Label>Application (Client) ID</Field.Label>
|
|
1369
|
+
<TextInput
|
|
1370
|
+
placeholder="12345678-1234-1234-1234-123456789abc"
|
|
1371
|
+
value={formData.oauthClientId}
|
|
1372
|
+
onChange={(e) => handleChange('oauthClientId', e.target.value)}
|
|
1373
|
+
/>
|
|
1374
|
+
<Field.Hint>Found in Azure Portal → App Registrations → Your App → Overview. It's a GUID format.</Field.Hint>
|
|
1375
|
+
</Field.Root>
|
|
1376
|
+
</FullWidthField>
|
|
1377
|
+
|
|
1378
|
+
<FullWidthField>
|
|
1379
|
+
<Field.Root required>
|
|
1380
|
+
<Field.Label>Client Secret Value</Field.Label>
|
|
1381
|
+
<TextInput
|
|
1382
|
+
type="password"
|
|
1383
|
+
placeholder="abc~123XYZ..."
|
|
1384
|
+
value={formData.oauthClientSecret}
|
|
1385
|
+
onChange={(e) => handleChange('oauthClientSecret', e.target.value)}
|
|
1386
|
+
/>
|
|
1387
|
+
<Field.Hint>From Azure Portal → Certificates & secrets → Client secrets. Copy the VALUE, not the Secret ID. Keep this secret!</Field.Hint>
|
|
1388
|
+
</Field.Root>
|
|
1389
|
+
</FullWidthField>
|
|
1390
|
+
|
|
1391
|
+
<Divider />
|
|
1392
|
+
|
|
1393
|
+
{oauthCode ? (
|
|
1394
|
+
<Alert variant="success" title="✅ OAuth Authorized!">
|
|
1395
|
+
<Typography variant="pi">
|
|
1396
|
+
You've successfully authorized with Microsoft! Click "Continue" to proceed to settings.
|
|
1397
|
+
</Typography>
|
|
1398
|
+
</Alert>
|
|
1399
|
+
) : (
|
|
1400
|
+
<Box>
|
|
1401
|
+
<Typography variant="omega" textColor="neutral600" marginBottom={3}>
|
|
1402
|
+
After entering your credentials above, click the button below to connect with Microsoft:
|
|
1403
|
+
</Typography>
|
|
1404
|
+
<Button
|
|
1405
|
+
onClick={startMicrosoftOAuth}
|
|
1406
|
+
variant="secondary"
|
|
1407
|
+
size="L"
|
|
1408
|
+
disabled={!formData.oauthClientId}
|
|
1409
|
+
style={{
|
|
1410
|
+
width: '100%',
|
|
1411
|
+
background: '#00A4EF',
|
|
1412
|
+
color: 'white',
|
|
1413
|
+
fontWeight: 600,
|
|
1414
|
+
}}
|
|
1415
|
+
>
|
|
1416
|
+
🔐 Connect with Microsoft
|
|
1417
|
+
</Button>
|
|
1418
|
+
</Box>
|
|
1419
|
+
)}
|
|
1420
|
+
|
|
1421
|
+
<Box
|
|
1422
|
+
padding={4}
|
|
1423
|
+
background="neutral100"
|
|
1424
|
+
hasRadius
|
|
1425
|
+
style={{
|
|
1426
|
+
border: `1px solid ${colors.border}`,
|
|
1427
|
+
borderRadius: '8px'
|
|
1428
|
+
}}
|
|
1429
|
+
>
|
|
1430
|
+
<Typography
|
|
1431
|
+
fontWeight="semiBold"
|
|
1432
|
+
marginBottom={3}
|
|
1433
|
+
style={{ fontSize: '15px' }}
|
|
1434
|
+
>
|
|
1435
|
+
📋 Setup Guide
|
|
1436
|
+
</Typography>
|
|
1437
|
+
|
|
1438
|
+
<Flex direction="column" gap={2} style={{ fontSize: '14px', lineHeight: '1.6' }}>
|
|
1439
|
+
<Box>
|
|
1440
|
+
<strong>1.</strong> Go to <a href="https://portal.azure.com" target="_blank" rel="noopener noreferrer" style={{ color: colors.primary, textDecoration: 'underline' }}>portal.azure.com</a>
|
|
1441
|
+
</Box>
|
|
1442
|
+
|
|
1443
|
+
<Box>
|
|
1444
|
+
<strong>2.</strong> Navigate to Azure Active Directory → App registrations → New registration
|
|
1445
|
+
</Box>
|
|
1446
|
+
|
|
1447
|
+
<Box>
|
|
1448
|
+
<strong>3.</strong> Name your app (e.g., "MagicMail") and select "Accounts in this organizational directory only"
|
|
1449
|
+
</Box>
|
|
1450
|
+
|
|
1451
|
+
<Box>
|
|
1452
|
+
<strong>Important:</strong> Copy both <strong>Application (client) ID</strong> AND <strong>Directory (tenant) ID</strong> from the Overview page!
|
|
1453
|
+
</Box>
|
|
1454
|
+
|
|
1455
|
+
<Box>
|
|
1456
|
+
<strong>4.</strong> Add this redirect URI:
|
|
1457
|
+
</Box>
|
|
1458
|
+
|
|
1459
|
+
<Flex gap={2} alignItems="center">
|
|
1460
|
+
<Box
|
|
1461
|
+
padding={2}
|
|
1462
|
+
background="neutral0"
|
|
1463
|
+
hasRadius
|
|
1464
|
+
style={{
|
|
1465
|
+
flex: 1,
|
|
1466
|
+
fontFamily: 'monospace',
|
|
1467
|
+
fontSize: '13px',
|
|
1468
|
+
wordBreak: 'break-all',
|
|
1469
|
+
color: colors.textSecondary,
|
|
1470
|
+
border: `1px solid ${colors.border}`,
|
|
1471
|
+
}}
|
|
1472
|
+
>
|
|
1473
|
+
{`${window.location.origin}/magic-mail/oauth/microsoft/callback`}
|
|
1474
|
+
</Box>
|
|
1475
|
+
<Button
|
|
1476
|
+
variant="secondary"
|
|
1477
|
+
size="S"
|
|
1478
|
+
onClick={() => {
|
|
1479
|
+
navigator.clipboard.writeText(`${window.location.origin}/magic-mail/oauth/microsoft/callback`);
|
|
1480
|
+
toggleNotification({
|
|
1481
|
+
type: 'success',
|
|
1482
|
+
message: 'Redirect URI copied to clipboard!',
|
|
1483
|
+
});
|
|
1484
|
+
}}
|
|
1485
|
+
>
|
|
1486
|
+
Copy
|
|
1487
|
+
</Button>
|
|
1488
|
+
</Flex>
|
|
1489
|
+
|
|
1490
|
+
<Box>
|
|
1491
|
+
<strong>5.</strong> Under API permissions → Add a permission → Microsoft Graph → Delegated permissions:
|
|
1492
|
+
</Box>
|
|
1493
|
+
|
|
1494
|
+
<Box style={{ marginLeft: '20px', marginTop: '8px' }}>
|
|
1495
|
+
• <code>Mail.Send</code> - Send emails as the signed-in user<br/>
|
|
1496
|
+
• <code>User.Read</code> - Read user profile (email address)<br/>
|
|
1497
|
+
• <code>offline_access</code> - Maintain access to data (refresh tokens)<br/>
|
|
1498
|
+
• <code>openid</code> - Sign users in<br/>
|
|
1499
|
+
• <code>email</code> - View users' email address
|
|
1500
|
+
</Box>
|
|
1501
|
+
|
|
1502
|
+
<Box>
|
|
1503
|
+
<strong>6.</strong> Click "Grant admin consent" for your organization (Required!)
|
|
1504
|
+
</Box>
|
|
1505
|
+
|
|
1506
|
+
<Box>
|
|
1507
|
+
<strong>7.</strong> Under Certificates & secrets → Client secrets → New client secret
|
|
1508
|
+
</Box>
|
|
1509
|
+
|
|
1510
|
+
<Box>
|
|
1511
|
+
<strong>8.</strong> Copy the <strong>Value</strong> (not Secret ID) immediately - it won't be shown again
|
|
1512
|
+
</Box>
|
|
1513
|
+
</Flex>
|
|
1514
|
+
</Box>
|
|
1515
|
+
</Flex>
|
|
1516
|
+
</>
|
|
1517
|
+
)}
|
|
1518
|
+
|
|
1519
|
+
{provider === 'yahoo-oauth' && (
|
|
1520
|
+
<>
|
|
1521
|
+
<InfoAlert variant="success" title="🔒 OAuth 2.0 Security" marginBottom={4}>
|
|
1522
|
+
No passwords stored. Users authenticate directly with Yahoo for maximum security.
|
|
1523
|
+
</InfoAlert>
|
|
1524
|
+
|
|
1525
|
+
<Flex direction="column" gap={4} style={{ width: '100%' }}>
|
|
1526
|
+
<SectionTitle>
|
|
1527
|
+
<Lock />
|
|
1528
|
+
Yahoo Developer Application
|
|
1529
|
+
</SectionTitle>
|
|
1530
|
+
|
|
1531
|
+
<FullWidthField>
|
|
1532
|
+
<Field.Root required>
|
|
1533
|
+
<Field.Label>Yahoo Client ID</Field.Label>
|
|
1534
|
+
<TextInput
|
|
1535
|
+
placeholder="dj0yJmk9..."
|
|
1536
|
+
value={formData.oauthClientId}
|
|
1537
|
+
onChange={(e) => handleChange('oauthClientId', e.target.value)}
|
|
1538
|
+
/>
|
|
1539
|
+
<Field.Hint>Found in Yahoo Developer Console → Your App → App Information. Starts with "dj0y..."</Field.Hint>
|
|
1540
|
+
</Field.Root>
|
|
1541
|
+
</FullWidthField>
|
|
1542
|
+
|
|
1543
|
+
<FullWidthField>
|
|
1544
|
+
<Field.Root required>
|
|
1545
|
+
<Field.Label>Yahoo Client Secret</Field.Label>
|
|
1546
|
+
<TextInput
|
|
1547
|
+
type="password"
|
|
1548
|
+
placeholder="abc123def456..."
|
|
1549
|
+
value={formData.oauthClientSecret}
|
|
1550
|
+
onChange={(e) => handleChange('oauthClientSecret', e.target.value)}
|
|
1551
|
+
/>
|
|
1552
|
+
<Field.Hint>Keep this secret! Found in the same App Information section. Never share or commit to git.</Field.Hint>
|
|
1553
|
+
</Field.Root>
|
|
1554
|
+
</FullWidthField>
|
|
1555
|
+
|
|
1556
|
+
<Divider />
|
|
1557
|
+
|
|
1558
|
+
{oauthCode ? (
|
|
1559
|
+
<Alert variant="success" title="✅ OAuth Authorized!">
|
|
1560
|
+
<Typography variant="pi">
|
|
1561
|
+
You've successfully authorized with Yahoo Mail! Click "Continue" to proceed to settings.
|
|
1562
|
+
</Typography>
|
|
1563
|
+
</Alert>
|
|
1564
|
+
) : (
|
|
1565
|
+
<Box>
|
|
1566
|
+
<Typography variant="omega" textColor="neutral600" marginBottom={3}>
|
|
1567
|
+
After entering your credentials above, click the button below to connect with Yahoo:
|
|
1568
|
+
</Typography>
|
|
1569
|
+
<Button
|
|
1570
|
+
onClick={startYahooOAuth}
|
|
1571
|
+
variant="secondary"
|
|
1572
|
+
size="L"
|
|
1573
|
+
disabled={!formData.oauthClientId}
|
|
1574
|
+
style={{
|
|
1575
|
+
width: '100%',
|
|
1576
|
+
background: '#6001D2',
|
|
1577
|
+
color: 'white',
|
|
1578
|
+
fontWeight: 600,
|
|
1579
|
+
}}
|
|
1580
|
+
>
|
|
1581
|
+
🔐 Connect with Yahoo
|
|
1582
|
+
</Button>
|
|
1583
|
+
</Box>
|
|
1584
|
+
)}
|
|
1585
|
+
|
|
1586
|
+
<Box
|
|
1587
|
+
padding={4}
|
|
1588
|
+
background="neutral100"
|
|
1589
|
+
hasRadius
|
|
1590
|
+
style={{
|
|
1591
|
+
border: `1px solid ${colors.border}`,
|
|
1592
|
+
borderRadius: '8px'
|
|
1593
|
+
}}
|
|
1594
|
+
>
|
|
1595
|
+
<Typography
|
|
1596
|
+
fontWeight="semiBold"
|
|
1597
|
+
marginBottom={3}
|
|
1598
|
+
style={{ fontSize: '15px' }}
|
|
1599
|
+
>
|
|
1600
|
+
📋 Setup Guide
|
|
1601
|
+
</Typography>
|
|
1602
|
+
|
|
1603
|
+
<Flex direction="column" gap={2} style={{ fontSize: '14px', lineHeight: '1.6' }}>
|
|
1604
|
+
<Box>
|
|
1605
|
+
<strong>1.</strong> Go to <a href="https://developer.yahoo.com/apps/" target="_blank" rel="noopener noreferrer" style={{ color: colors.primary, textDecoration: 'underline' }}>developer.yahoo.com/apps</a>
|
|
1606
|
+
</Box>
|
|
1607
|
+
|
|
1608
|
+
<Box>
|
|
1609
|
+
<strong>2.</strong> Click "Create an App"
|
|
1610
|
+
</Box>
|
|
1611
|
+
|
|
1612
|
+
<Box>
|
|
1613
|
+
<strong>3.</strong> Fill in app details (name, description)
|
|
1614
|
+
</Box>
|
|
1615
|
+
|
|
1616
|
+
<Box>
|
|
1617
|
+
<strong>4.</strong> Add this redirect URI:
|
|
1618
|
+
</Box>
|
|
1619
|
+
|
|
1620
|
+
<Flex gap={2} alignItems="center">
|
|
1621
|
+
<Box
|
|
1622
|
+
padding={2}
|
|
1623
|
+
background="neutral0"
|
|
1624
|
+
hasRadius
|
|
1625
|
+
style={{
|
|
1626
|
+
flex: 1,
|
|
1627
|
+
fontFamily: 'monospace',
|
|
1628
|
+
fontSize: '13px',
|
|
1629
|
+
wordBreak: 'break-all',
|
|
1630
|
+
color: colors.textSecondary,
|
|
1631
|
+
border: `1px solid ${colors.border}`,
|
|
1632
|
+
}}
|
|
1633
|
+
>
|
|
1634
|
+
{`${window.location.origin}/magic-mail/oauth/yahoo/callback`}
|
|
1635
|
+
</Box>
|
|
1636
|
+
<Button
|
|
1637
|
+
variant="secondary"
|
|
1638
|
+
size="S"
|
|
1639
|
+
onClick={() => {
|
|
1640
|
+
navigator.clipboard.writeText(`${window.location.origin}/magic-mail/oauth/yahoo/callback`);
|
|
1641
|
+
toggleNotification({
|
|
1642
|
+
type: 'success',
|
|
1643
|
+
message: 'Redirect URI copied to clipboard!',
|
|
1644
|
+
});
|
|
1645
|
+
}}
|
|
1646
|
+
>
|
|
1647
|
+
Copy
|
|
1648
|
+
</Button>
|
|
1649
|
+
</Flex>
|
|
1650
|
+
|
|
1651
|
+
<Box>
|
|
1652
|
+
<strong>5.</strong> Under API Permissions, enable:
|
|
1653
|
+
</Box>
|
|
1654
|
+
|
|
1655
|
+
<Box style={{ marginLeft: '20px' }}>
|
|
1656
|
+
• <code>Mail</code> - Send and manage emails<br/>
|
|
1657
|
+
• <code>OpenID Connect</code> - User authentication
|
|
1658
|
+
</Box>
|
|
1659
|
+
|
|
1660
|
+
<Box>
|
|
1661
|
+
<strong>6.</strong> Note your Client ID and Client Secret from the app settings
|
|
1662
|
+
</Box>
|
|
1663
|
+
</Flex>
|
|
1664
|
+
</Box>
|
|
1665
|
+
</Flex>
|
|
1666
|
+
</>
|
|
1667
|
+
)}
|
|
1668
|
+
|
|
1669
|
+
{provider === 'sendgrid' && (
|
|
1670
|
+
<>
|
|
1671
|
+
<InfoAlert variant="success" title="🔒 API Key Security" marginBottom={4}>
|
|
1672
|
+
Your API key will be encrypted with AES-256-GCM before storage.
|
|
1673
|
+
</InfoAlert>
|
|
1674
|
+
|
|
1675
|
+
<Flex direction="column" gap={4} style={{ width: '100%' }}>
|
|
1676
|
+
<SectionTitle>
|
|
1677
|
+
<Key />
|
|
1678
|
+
SendGrid API Configuration
|
|
1679
|
+
</SectionTitle>
|
|
1680
|
+
|
|
1681
|
+
<FullWidthField>
|
|
1682
|
+
<Field.Root required>
|
|
1683
|
+
<Field.Label>SendGrid API Key</Field.Label>
|
|
1684
|
+
<TextInput
|
|
1685
|
+
type="password"
|
|
1686
|
+
placeholder="SG.xxxxxxxxxxxxxxxxxxxxxx"
|
|
1687
|
+
value={formData.apiKey}
|
|
1688
|
+
onChange={(e) => handleChange('apiKey', e.target.value)}
|
|
1689
|
+
/>
|
|
1690
|
+
<Field.Hint>
|
|
1691
|
+
Found in SendGrid Dashboard → Settings → API Keys. Create a new key with "Mail Send" permission
|
|
1692
|
+
</Field.Hint>
|
|
1693
|
+
</Field.Root>
|
|
1694
|
+
</FullWidthField>
|
|
1695
|
+
|
|
1696
|
+
<Alert variant="default" title="📖 SendGrid Resources">
|
|
1697
|
+
<Typography variant="pi">
|
|
1698
|
+
<strong>Dashboard:</strong> <a href="https://app.sendgrid.com" target="_blank" rel="noopener noreferrer" style={{color: '#0284c7'}}>app.sendgrid.com</a><br/>
|
|
1699
|
+
<strong>API Keys:</strong> Settings → API Keys → Create API Key<br/>
|
|
1700
|
+
<strong>Required Scope:</strong> Mail Send (Full Access)<br/>
|
|
1701
|
+
<strong>Docs:</strong> <a href="https://docs.sendgrid.com" target="_blank" rel="noopener noreferrer" style={{color: '#0284c7'}}>docs.sendgrid.com</a>
|
|
1702
|
+
</Typography>
|
|
1703
|
+
</Alert>
|
|
1704
|
+
</Flex>
|
|
1705
|
+
</>
|
|
1706
|
+
)}
|
|
1707
|
+
|
|
1708
|
+
{provider === 'mailgun' && (
|
|
1709
|
+
<>
|
|
1710
|
+
<InfoAlert variant="success" title="🔒 API Key Security" marginBottom={4}>
|
|
1711
|
+
Your API key will be encrypted with AES-256-GCM before storage.
|
|
1712
|
+
</InfoAlert>
|
|
1713
|
+
|
|
1714
|
+
<Flex direction="column" gap={4} style={{ width: '100%' }}>
|
|
1715
|
+
<SectionTitle>
|
|
1716
|
+
<Key />
|
|
1717
|
+
Mailgun API Configuration
|
|
1718
|
+
</SectionTitle>
|
|
1719
|
+
|
|
1720
|
+
<FullWidthField>
|
|
1721
|
+
<Field.Root required>
|
|
1722
|
+
<Field.Label>Mailgun Domain</Field.Label>
|
|
1723
|
+
<TextInput
|
|
1724
|
+
placeholder="mg.yourdomain.com or sandbox-xxx.mailgun.org"
|
|
1725
|
+
value={formData.mailgunDomain}
|
|
1726
|
+
onChange={(e) => handleChange('mailgunDomain', e.target.value)}
|
|
1727
|
+
/>
|
|
1728
|
+
<Field.Hint>
|
|
1729
|
+
Your verified Mailgun domain (e.g., mg.yourdomain.com) or sandbox domain for testing
|
|
1730
|
+
</Field.Hint>
|
|
1731
|
+
</Field.Root>
|
|
1732
|
+
</FullWidthField>
|
|
1733
|
+
|
|
1734
|
+
<FullWidthField>
|
|
1735
|
+
<Field.Root required>
|
|
1736
|
+
<Field.Label>Mailgun API Key</Field.Label>
|
|
1737
|
+
<TextInput
|
|
1738
|
+
type="password"
|
|
1739
|
+
placeholder="key-xxxxxxxxxxxxxxxxxxxxxxxx"
|
|
1740
|
+
value={formData.apiKey}
|
|
1741
|
+
onChange={(e) => handleChange('apiKey', e.target.value)}
|
|
1742
|
+
/>
|
|
1743
|
+
<Field.Hint>
|
|
1744
|
+
Found in Mailgun Dashboard → Settings → API Keys. Use your Private API key, not the Public Validation key
|
|
1745
|
+
</Field.Hint>
|
|
1746
|
+
</Field.Root>
|
|
1747
|
+
</FullWidthField>
|
|
1748
|
+
|
|
1749
|
+
<Alert variant="default" title="📖 Mailgun Resources">
|
|
1750
|
+
<Typography variant="pi">
|
|
1751
|
+
<strong>Dashboard:</strong> <a href="https://app.mailgun.com" target="_blank" rel="noopener noreferrer" style={{color: '#0284c7'}}>app.mailgun.com</a><br/>
|
|
1752
|
+
<strong>API Keys:</strong> Settings → API Security → Private API Key<br/>
|
|
1753
|
+
<strong>Domains:</strong> Sending → Domains (verify your domain or use sandbox)<br/>
|
|
1754
|
+
<strong>Docs:</strong> <a href="https://documentation.mailgun.com" target="_blank" rel="noopener noreferrer" style={{color: '#0284c7'}}>documentation.mailgun.com</a>
|
|
1755
|
+
</Typography>
|
|
1756
|
+
</Alert>
|
|
1757
|
+
</Flex>
|
|
1758
|
+
</>
|
|
1759
|
+
)}
|
|
1760
|
+
</FormSection>
|
|
1761
|
+
)}
|
|
1762
|
+
|
|
1763
|
+
{/* Step 4: Limits & Finalize */}
|
|
1764
|
+
{currentStep === 4 && (
|
|
1765
|
+
<FormSection>
|
|
1766
|
+
<Flex direction="column" gap={5} style={{ width: '100%' }}>
|
|
1767
|
+
|
|
1768
|
+
{/* Daily Limit */}
|
|
1769
|
+
<Box>
|
|
1770
|
+
<Typography fontWeight="semiBold" marginBottom={2} style={{ fontSize: '15px' }}>
|
|
1771
|
+
Daily Email Limit
|
|
1772
|
+
</Typography>
|
|
1773
|
+
<NumberInput
|
|
1774
|
+
value={formData.dailyLimit}
|
|
1775
|
+
onValueChange={(value) => handleChange('dailyLimit', value)}
|
|
1776
|
+
/>
|
|
1777
|
+
<Typography variant="pi" textColor="neutral600" marginTop={2} style={{ fontSize: '13px', lineHeight: '1.5' }}>
|
|
1778
|
+
Maximum number of emails this account can send per day. Set to 0 for unlimited.
|
|
1779
|
+
</Typography>
|
|
1780
|
+
</Box>
|
|
1781
|
+
|
|
1782
|
+
{/* Hourly Limit */}
|
|
1783
|
+
<Box>
|
|
1784
|
+
<Typography fontWeight="semiBold" marginBottom={2} style={{ fontSize: '15px' }}>
|
|
1785
|
+
Hourly Email Limit
|
|
1786
|
+
</Typography>
|
|
1787
|
+
<NumberInput
|
|
1788
|
+
value={formData.hourlyLimit}
|
|
1789
|
+
onValueChange={(value) => handleChange('hourlyLimit', value)}
|
|
1790
|
+
/>
|
|
1791
|
+
<Typography variant="pi" textColor="neutral600" marginTop={2} style={{ fontSize: '13px', lineHeight: '1.5' }}>
|
|
1792
|
+
Maximum number of emails this account can send per hour. Set to 0 for unlimited.
|
|
1793
|
+
</Typography>
|
|
1794
|
+
</Box>
|
|
1795
|
+
|
|
1796
|
+
{/* Priority */}
|
|
1797
|
+
<Box>
|
|
1798
|
+
<Typography fontWeight="semiBold" marginBottom={2} style={{ fontSize: '15px' }}>
|
|
1799
|
+
Account Priority
|
|
1800
|
+
</Typography>
|
|
1801
|
+
<NumberInput
|
|
1802
|
+
value={formData.priority}
|
|
1803
|
+
onValueChange={(value) => handleChange('priority', value)}
|
|
1804
|
+
min={1}
|
|
1805
|
+
max={10}
|
|
1806
|
+
/>
|
|
1807
|
+
<Typography variant="pi" textColor="neutral600" marginTop={2} style={{ fontSize: '13px', lineHeight: '1.5' }}>
|
|
1808
|
+
When routing emails, accounts with higher priority (1-10) are preferred. Use 10 for your most reliable account.
|
|
1809
|
+
</Typography>
|
|
1810
|
+
</Box>
|
|
1811
|
+
|
|
1812
|
+
<Divider />
|
|
1813
|
+
|
|
1814
|
+
{/* Account Active Toggle */}
|
|
1815
|
+
<Box
|
|
1816
|
+
padding={4}
|
|
1817
|
+
background={formData.isActive ? '#DCFCE7' : '#FEE2E2'}
|
|
1818
|
+
hasRadius
|
|
1819
|
+
style={{
|
|
1820
|
+
border: formData.isActive ? `2px solid #22C55E` : `2px solid #EF4444`,
|
|
1821
|
+
borderRadius: '8px',
|
|
1822
|
+
transition: 'all 0.2s ease'
|
|
1823
|
+
}}
|
|
1824
|
+
>
|
|
1825
|
+
<Flex gap={3} alignItems="flex-start">
|
|
1826
|
+
<Box style={{ paddingTop: '2px' }}>
|
|
1827
|
+
<Toggle
|
|
1828
|
+
checked={formData.isActive}
|
|
1829
|
+
onChange={() => handleChange('isActive', !formData.isActive)}
|
|
1830
|
+
/>
|
|
1831
|
+
</Box>
|
|
1832
|
+
<Box style={{ flex: 1 }}>
|
|
1833
|
+
<Flex alignItems="center" gap={2} marginBottom={1}>
|
|
1834
|
+
<Typography fontWeight="semiBold" style={{ fontSize: '15px' }}>
|
|
1835
|
+
{formData.isActive ? '✅' : '❌'} Account Active
|
|
1836
|
+
</Typography>
|
|
1837
|
+
{formData.isActive ? (
|
|
1838
|
+
<Badge backgroundColor="success600" textColor="neutral0" size="S">
|
|
1839
|
+
ENABLED
|
|
1840
|
+
</Badge>
|
|
1841
|
+
) : (
|
|
1842
|
+
<Badge backgroundColor="danger600" textColor="neutral0" size="S">
|
|
1843
|
+
DISABLED
|
|
1844
|
+
</Badge>
|
|
1845
|
+
)}
|
|
1846
|
+
</Flex>
|
|
1847
|
+
<Typography variant="pi" textColor="neutral600" style={{ lineHeight: '1.6' }}>
|
|
1848
|
+
{formData.isActive
|
|
1849
|
+
? 'This account is enabled and can send emails. Disable it to prevent sending without deleting the account.'
|
|
1850
|
+
: 'This account is disabled and will not send any emails. Enable it to start sending again.'
|
|
1851
|
+
}
|
|
1852
|
+
</Typography>
|
|
1853
|
+
</Box>
|
|
1854
|
+
</Flex>
|
|
1855
|
+
</Box>
|
|
1856
|
+
|
|
1857
|
+
{/* Primary Account Toggle */}
|
|
1858
|
+
<Box
|
|
1859
|
+
padding={4}
|
|
1860
|
+
background={formData.isPrimary ? '#FEF3C7' : 'neutral100'}
|
|
1861
|
+
hasRadius
|
|
1862
|
+
style={{
|
|
1863
|
+
border: formData.isPrimary ? `2px solid #F59E0B` : `1px solid ${colors.border}`,
|
|
1864
|
+
borderRadius: '8px',
|
|
1865
|
+
transition: 'all 0.2s ease'
|
|
1866
|
+
}}
|
|
1867
|
+
>
|
|
1868
|
+
<Flex gap={3} alignItems="flex-start">
|
|
1869
|
+
<Box style={{ paddingTop: '2px' }}>
|
|
1870
|
+
<Toggle
|
|
1871
|
+
checked={formData.isPrimary}
|
|
1872
|
+
onChange={() => handleChange('isPrimary', !formData.isPrimary)}
|
|
1873
|
+
/>
|
|
1874
|
+
</Box>
|
|
1875
|
+
<Box style={{ flex: 1 }}>
|
|
1876
|
+
<Flex alignItems="center" gap={2} marginBottom={1}>
|
|
1877
|
+
<Typography fontWeight="semiBold" style={{ fontSize: '15px' }}>
|
|
1878
|
+
⭐ Set as Primary Account
|
|
1879
|
+
</Typography>
|
|
1880
|
+
{formData.isPrimary && (
|
|
1881
|
+
<Badge backgroundColor="warning600" textColor="neutral0" size="S">
|
|
1882
|
+
PRIMARY
|
|
1883
|
+
</Badge>
|
|
1884
|
+
)}
|
|
1885
|
+
</Flex>
|
|
1886
|
+
<Typography variant="pi" textColor="neutral600" style={{ lineHeight: '1.6' }}>
|
|
1887
|
+
This account will be used by default when sending emails if no specific account is selected. Only one account can be primary at a time.
|
|
1888
|
+
</Typography>
|
|
1889
|
+
</Box>
|
|
1890
|
+
</Flex>
|
|
1891
|
+
</Box>
|
|
1892
|
+
|
|
1893
|
+
</Flex>
|
|
1894
|
+
</FormSection>
|
|
1895
|
+
)}
|
|
1896
|
+
|
|
1897
|
+
</Flex>
|
|
1898
|
+
</Modal.Body>
|
|
1899
|
+
|
|
1900
|
+
<Modal.Footer>
|
|
1901
|
+
<Flex justifyContent="space-between" style={{ width: '100%' }}>
|
|
1902
|
+
<div>
|
|
1903
|
+
{currentStep > 1 && (
|
|
1904
|
+
<Button
|
|
1905
|
+
variant="tertiary"
|
|
1906
|
+
startIcon={<ArrowLeft />}
|
|
1907
|
+
onClick={() => setCurrentStep(currentStep - 1)}
|
|
1908
|
+
>
|
|
1909
|
+
Back
|
|
1910
|
+
</Button>
|
|
1911
|
+
)}
|
|
1912
|
+
</div>
|
|
1913
|
+
<Flex gap={2}>
|
|
1914
|
+
<Button onClick={onClose} variant="tertiary">
|
|
1915
|
+
Cancel
|
|
1916
|
+
</Button>
|
|
1917
|
+
{currentStep < 4 ? (
|
|
1918
|
+
<Button
|
|
1919
|
+
endIcon={<ArrowRight />}
|
|
1920
|
+
onClick={() => setCurrentStep(currentStep + 1)}
|
|
1921
|
+
disabled={!canProceed()}
|
|
1922
|
+
>
|
|
1923
|
+
Continue
|
|
1924
|
+
</Button>
|
|
1925
|
+
) : (
|
|
1926
|
+
<Button
|
|
1927
|
+
onClick={handleSubmit}
|
|
1928
|
+
loading={loading}
|
|
1929
|
+
disabled={!canProceed()}
|
|
1930
|
+
startIcon={<Check />}
|
|
1931
|
+
>
|
|
1932
|
+
{isEditMode ? 'Update Account' : 'Create Account'}
|
|
1933
|
+
</Button>
|
|
1934
|
+
)}
|
|
1935
|
+
</Flex>
|
|
1936
|
+
</Flex>
|
|
1937
|
+
</Modal.Footer>
|
|
1938
|
+
</Modal.Content>
|
|
1939
|
+
</Modal.Root>
|
|
1940
|
+
);
|
|
1941
|
+
};
|
|
1942
|
+
|
|
1943
|
+
export default AddAccountModal;
|