strapi-plugin-magic-mail 2.2.4 → 2.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/server/index.js +1 -1
  2. package/dist/server/index.mjs +1 -1
  3. package/package.json +1 -3
  4. package/admin/jsconfig.json +0 -10
  5. package/admin/src/components/AddAccountModal.jsx +0 -1943
  6. package/admin/src/components/Initializer.jsx +0 -14
  7. package/admin/src/components/LicenseGuard.jsx +0 -475
  8. package/admin/src/components/PluginIcon.jsx +0 -5
  9. package/admin/src/hooks/useAuthRefresh.js +0 -44
  10. package/admin/src/hooks/useLicense.js +0 -158
  11. package/admin/src/index.js +0 -87
  12. package/admin/src/pages/Analytics.jsx +0 -762
  13. package/admin/src/pages/App.jsx +0 -111
  14. package/admin/src/pages/EmailDesigner/EditorPage.jsx +0 -1424
  15. package/admin/src/pages/EmailDesigner/TemplateList.jsx +0 -1807
  16. package/admin/src/pages/HomePage.jsx +0 -1170
  17. package/admin/src/pages/LicensePage.jsx +0 -430
  18. package/admin/src/pages/RoutingRules.jsx +0 -1141
  19. package/admin/src/pages/Settings.jsx +0 -603
  20. package/admin/src/pluginId.js +0 -3
  21. package/admin/src/translations/de.json +0 -71
  22. package/admin/src/translations/en.json +0 -70
  23. package/admin/src/translations/es.json +0 -71
  24. package/admin/src/translations/fr.json +0 -71
  25. package/admin/src/translations/pt.json +0 -71
  26. package/admin/src/utils/fetchWithRetry.js +0 -123
  27. package/admin/src/utils/getTranslation.js +0 -5
  28. package/admin/src/utils/theme.js +0 -85
  29. package/server/jsconfig.json +0 -10
  30. package/server/src/bootstrap.js +0 -157
  31. package/server/src/config/features.js +0 -260
  32. package/server/src/config/index.js +0 -9
  33. package/server/src/content-types/email-account/schema.json +0 -93
  34. package/server/src/content-types/email-event/index.js +0 -8
  35. package/server/src/content-types/email-event/schema.json +0 -57
  36. package/server/src/content-types/email-link/index.js +0 -8
  37. package/server/src/content-types/email-link/schema.json +0 -49
  38. package/server/src/content-types/email-log/index.js +0 -8
  39. package/server/src/content-types/email-log/schema.json +0 -106
  40. package/server/src/content-types/email-template/schema.json +0 -74
  41. package/server/src/content-types/email-template-version/schema.json +0 -60
  42. package/server/src/content-types/index.js +0 -33
  43. package/server/src/content-types/routing-rule/schema.json +0 -59
  44. package/server/src/controllers/accounts.js +0 -229
  45. package/server/src/controllers/analytics.js +0 -361
  46. package/server/src/controllers/controller.js +0 -26
  47. package/server/src/controllers/email-designer.js +0 -474
  48. package/server/src/controllers/index.js +0 -21
  49. package/server/src/controllers/license.js +0 -269
  50. package/server/src/controllers/oauth.js +0 -474
  51. package/server/src/controllers/routing-rules.js +0 -129
  52. package/server/src/controllers/test.js +0 -301
  53. package/server/src/destroy.js +0 -27
  54. package/server/src/index.js +0 -25
  55. package/server/src/middlewares/index.js +0 -3
  56. package/server/src/policies/index.js +0 -3
  57. package/server/src/register.js +0 -5
  58. package/server/src/routes/admin.js +0 -469
  59. package/server/src/routes/content-api.js +0 -37
  60. package/server/src/routes/index.js +0 -9
  61. package/server/src/services/account-manager.js +0 -329
  62. package/server/src/services/analytics.js +0 -512
  63. package/server/src/services/email-designer.js +0 -717
  64. package/server/src/services/email-router.js +0 -1446
  65. package/server/src/services/index.js +0 -17
  66. package/server/src/services/license-guard.js +0 -423
  67. package/server/src/services/oauth.js +0 -515
  68. package/server/src/services/service.js +0 -7
  69. package/server/src/utils/encryption.js +0 -81
  70. package/server/src/utils/logger.js +0 -84
@@ -1,1170 +0,0 @@
1
- import { useState, useEffect } from 'react';
2
- import { useFetchClient, useNotification } from '@strapi/strapi/admin';
3
- import { useAuthRefresh } from '../hooks/useAuthRefresh';
4
- import styled, { keyframes, css } from 'styled-components';
5
- import { theme } from '../utils/theme';
6
- import {
7
- Box,
8
- Button,
9
- Flex,
10
- Typography,
11
- Loader,
12
- Badge,
13
- SingleSelect,
14
- SingleSelectOption,
15
- Modal,
16
- Field,
17
- TextInput,
18
- } from '@strapi/design-system';
19
- import { Table, Thead, Tbody, Tr, Th, Td } from '@strapi/design-system';
20
- import {
21
- CheckIcon,
22
- EnvelopeIcon,
23
- ServerIcon,
24
- SparklesIcon,
25
- TrashIcon,
26
- PlayIcon,
27
- PlusIcon,
28
- MagnifyingGlassIcon,
29
- PencilIcon,
30
- } from '@heroicons/react/24/outline';
31
- import AddAccountModal from '../components/AddAccountModal';
32
-
33
- // ================ ANIMATIONS ================
34
- const fadeIn = keyframes`
35
- from { opacity: 0; transform: translateY(10px); }
36
- to { opacity: 1; transform: translateY(0); }
37
- `;
38
-
39
- const shimmer = keyframes`
40
- 0% { background-position: -200% 0; }
41
- 100% { background-position: 200% 0; }
42
- `;
43
-
44
- const float = keyframes`
45
- 0%, 100% { transform: translateY(0px); }
46
- 50% { transform: translateY(-5px); }
47
- `;
48
-
49
- const pulse = keyframes`
50
- 0%, 100% { opacity: 1; }
51
- 50% { opacity: 0.5; }
52
- `;
53
-
54
- const FloatingEmoji = styled.div`
55
- position: absolute;
56
- bottom: 40px;
57
- right: 40px;
58
- font-size: 72px;
59
- opacity: 0.08;
60
- ${css`animation: ${float} 4s ease-in-out infinite;`}
61
- `;
62
-
63
- // ================ RESPONSIVE BREAKPOINTS ================
64
- const breakpoints = {
65
- mobile: '768px',
66
- tablet: '1024px',
67
- };
68
-
69
- // ================ STYLED COMPONENTS ================
70
- const Container = styled(Box)`
71
- ${css`animation: ${fadeIn} ${theme.transitions.slow};`}
72
- min-height: 100vh;
73
- max-width: 1440px;
74
- margin: 0 auto;
75
- padding: ${theme.spacing.xl} ${theme.spacing.lg} 0;
76
-
77
- @media screen and (max-width: ${breakpoints.mobile}) {
78
- padding: 16px 12px 0;
79
- }
80
- `;
81
-
82
- const Header = styled(Box)`
83
- background: linear-gradient(135deg,
84
- ${theme.colors.primary[600]} 0%,
85
- ${theme.colors.secondary[600]} 100%
86
- );
87
- border-radius: ${theme.borderRadius.xl};
88
- padding: ${theme.spacing.xl} ${theme.spacing['2xl']};
89
- margin-bottom: ${theme.spacing.xl};
90
- position: relative;
91
- overflow: hidden;
92
- box-shadow: ${theme.shadows.xl};
93
-
94
- @media screen and (max-width: ${breakpoints.mobile}) {
95
- padding: 24px 20px;
96
- border-radius: 12px;
97
- }
98
-
99
- &::before {
100
- content: '';
101
- position: absolute;
102
- top: 0;
103
- left: -100%;
104
- width: 200%;
105
- height: 100%;
106
- background: linear-gradient(
107
- 90deg,
108
- transparent,
109
- rgba(255, 255, 255, 0.15),
110
- transparent
111
- );
112
- ${css`animation: ${shimmer} 3s infinite;`}
113
- }
114
-
115
- &::after {
116
- content: '';
117
- position: absolute;
118
- top: 0;
119
- right: 0;
120
- width: 100%;
121
- height: 100%;
122
- background-image: radial-gradient(circle at 20% 80%, transparent 50%, rgba(255, 255, 255, 0.1) 50%);
123
- background-size: 15px 15px;
124
- opacity: 0.3;
125
- }
126
- `;
127
-
128
- const HeaderContent = styled(Flex)`
129
- position: relative;
130
- z-index: 1;
131
- `;
132
-
133
- const Title = styled(Typography)`
134
- color: white;
135
- font-size: 2rem;
136
- font-weight: 700;
137
- letter-spacing: -0.025em;
138
- text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
139
- display: flex;
140
- align-items: center;
141
- gap: ${theme.spacing.sm};
142
-
143
- svg {
144
- width: 28px;
145
- height: 28px;
146
- ${css`animation: ${float} 3s ease-in-out infinite;`}
147
- }
148
-
149
- @media screen and (max-width: ${breakpoints.mobile}) {
150
- font-size: 1.5rem;
151
-
152
- svg {
153
- width: 22px;
154
- height: 22px;
155
- }
156
- }
157
- `;
158
-
159
- const Subtitle = styled(Typography)`
160
- color: rgba(255, 255, 255, 0.95);
161
- font-size: 0.95rem;
162
- font-weight: 400;
163
- margin-top: ${theme.spacing.xs};
164
- letter-spacing: 0.01em;
165
-
166
- @media screen and (max-width: ${breakpoints.mobile}) {
167
- font-size: 0.85rem;
168
- }
169
- `;
170
-
171
- const StatsGrid = styled.div`
172
- margin-bottom: ${theme.spacing.xl};
173
- display: grid;
174
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
175
- gap: ${theme.spacing.lg};
176
- justify-content: center;
177
- max-width: 1200px;
178
- margin-left: auto;
179
- margin-right: auto;
180
-
181
- @media screen and (max-width: ${breakpoints.mobile}) {
182
- grid-template-columns: repeat(2, 1fr);
183
- gap: 12px;
184
- margin-bottom: 24px;
185
- }
186
- `;
187
-
188
- const StatCard = styled(Box)`
189
- background: ${props => props.theme.colors.neutral0};
190
- border-radius: ${theme.borderRadius.lg};
191
- padding: 28px ${theme.spacing.lg};
192
- position: relative;
193
- overflow: hidden;
194
- transition: all ${theme.transitions.normal};
195
- ${css`animation: ${fadeIn} ${theme.transitions.slow} backwards;`}
196
- animation-delay: ${props => props.$delay || '0s'};
197
- box-shadow: ${theme.shadows.sm};
198
- border: 1px solid ${props => props.theme.colors.neutral200};
199
- min-width: 200px;
200
- flex: 1;
201
- text-align: center;
202
- display: flex;
203
- flex-direction: column;
204
- align-items: center;
205
- justify-content: center;
206
-
207
- @media screen and (max-width: ${breakpoints.mobile}) {
208
- min-width: unset;
209
- padding: 20px 12px;
210
-
211
- &:hover {
212
- transform: none;
213
- }
214
- }
215
-
216
- &:hover {
217
- transform: translateY(-6px);
218
- box-shadow: ${theme.shadows.xl};
219
- border-color: ${props => props.$color || theme.colors.primary[500]};
220
-
221
- .stat-icon {
222
- transform: scale(1.15) rotate(5deg);
223
- }
224
-
225
- .stat-value {
226
- transform: scale(1.08);
227
- color: ${props => props.$color || theme.colors.primary[600]};
228
- }
229
- }
230
- `;
231
-
232
- const StatIcon = styled(Box)`
233
- width: 68px;
234
- height: 68px;
235
- border-radius: ${theme.borderRadius.lg};
236
- display: flex;
237
- align-items: center;
238
- justify-content: center;
239
- background: ${props => props.$bg || theme.colors.primary[100]};
240
- transition: all ${theme.transitions.normal};
241
- margin: 0 auto 20px;
242
- box-shadow: ${theme.shadows.sm};
243
-
244
- svg {
245
- width: 34px;
246
- height: 34px;
247
- color: ${props => props.$color || theme.colors.primary[600]};
248
- }
249
-
250
- @media screen and (max-width: ${breakpoints.mobile}) {
251
- width: 48px;
252
- height: 48px;
253
- margin-bottom: 12px;
254
-
255
- svg {
256
- width: 24px;
257
- height: 24px;
258
- }
259
- }
260
- `;
261
-
262
- const StatValue = styled(Typography)`
263
- font-size: 2.75rem;
264
- font-weight: 700;
265
- color: ${props => props.theme.colors.neutral800};
266
- line-height: 1;
267
- margin-bottom: 10px;
268
- transition: all ${theme.transitions.normal};
269
- text-align: center;
270
-
271
- @media screen and (max-width: ${breakpoints.mobile}) {
272
- font-size: 2rem;
273
- margin-bottom: 6px;
274
- }
275
- `;
276
-
277
- const StatLabel = styled(Typography)`
278
- font-size: 0.95rem;
279
- color: ${props => props.theme.colors.neutral600};
280
- font-weight: 500;
281
- letter-spacing: 0.025em;
282
- text-align: center;
283
-
284
- @media screen and (max-width: ${breakpoints.mobile}) {
285
- font-size: 0.8rem;
286
- }
287
- `;
288
-
289
- const AccountsContainer = styled(Box)`
290
- margin-top: ${theme.spacing.xl};
291
- `;
292
-
293
- const EmptyState = styled(Box)`
294
- background: ${props => props.theme.colors.neutral0};
295
- border-radius: ${theme.borderRadius.xl};
296
- border: 2px dashed ${props => props.theme.colors.neutral300};
297
- padding: 80px 32px;
298
- text-align: center;
299
- position: relative;
300
- overflow: hidden;
301
- min-height: 400px;
302
- display: flex;
303
- align-items: center;
304
- justify-content: center;
305
-
306
- /* Background Gradient */
307
- &::before {
308
- content: '';
309
- position: absolute;
310
- top: 0;
311
- left: 0;
312
- right: 0;
313
- bottom: 0;
314
- background: linear-gradient(135deg, ${theme.colors.primary[50]} 0%, ${theme.colors.secondary[50]} 100%);
315
- opacity: 0.3;
316
- z-index: 0;
317
- }
318
- `;
319
-
320
- const OnlineBadge = styled.div`
321
- width: 12px;
322
- height: 12px;
323
- border-radius: 50%;
324
- background: ${props => props.$active ? theme.colors.success[500] : props.theme.colors.neutral400};
325
- display: inline-block;
326
- margin-right: 8px;
327
- ${css`animation: ${props => props.$active ? pulse : 'none'} 2s ease-in-out infinite;`}
328
- `;
329
-
330
- const StyledTable = styled(Table)`
331
- thead {
332
- background: ${props => props.theme.colors.neutral100};
333
- border-bottom: 2px solid ${props => props.theme.colors.neutral200};
334
-
335
- th {
336
- font-weight: 600;
337
- color: ${props => props.theme.colors.neutral800};
338
- font-size: 0.875rem;
339
- text-transform: uppercase;
340
- letter-spacing: 0.025em;
341
- padding: ${theme.spacing.lg} ${theme.spacing.lg};
342
- }
343
- }
344
-
345
- tbody tr {
346
- transition: all ${theme.transitions.fast};
347
- border-bottom: 1px solid ${props => props.theme.colors.neutral150};
348
-
349
- &:last-child {
350
- border-bottom: none;
351
- }
352
-
353
- &:hover {
354
- background: ${props => props.theme.colors.primary100};
355
- }
356
-
357
- td {
358
- padding: ${theme.spacing.lg} ${theme.spacing.lg};
359
- color: ${props => props.theme.colors.neutral800};
360
- vertical-align: middle;
361
- }
362
- }
363
- `;
364
-
365
- const FilterBar = styled(Flex)`
366
- background: ${props => props.theme.colors.neutral0};
367
- padding: ${theme.spacing.md} ${theme.spacing.lg};
368
- border-radius: ${theme.borderRadius.lg};
369
- margin-bottom: ${theme.spacing.lg};
370
- box-shadow: ${theme.shadows.sm};
371
- border: 1px solid ${props => props.theme.colors.neutral200};
372
- gap: ${theme.spacing.md};
373
- align-items: center;
374
- `;
375
-
376
- const SearchInputWrapper = styled.div`
377
- position: relative;
378
- flex: 1;
379
- display: flex;
380
- align-items: center;
381
- `;
382
-
383
- const SearchIcon = styled(MagnifyingGlassIcon)`
384
- position: absolute;
385
- left: 12px;
386
- width: 16px;
387
- height: 16px;
388
- color: ${props => props.theme.colors.neutral600};
389
- pointer-events: none;
390
- `;
391
-
392
- const StyledSearchInput = styled.input`
393
- width: 100%;
394
- padding: 10px 12px 10px 40px;
395
- border: 1px solid ${props => props.theme.colors.neutral200};
396
- border-radius: ${theme.borderRadius.md};
397
- font-size: 0.875rem;
398
- transition: all ${theme.transitions.fast};
399
- background: ${props => props.theme.colors.neutral0};
400
- color: ${props => props.theme.colors.neutral800};
401
-
402
- &:focus {
403
- outline: none;
404
- border-color: ${theme.colors.primary[500]};
405
- box-shadow: 0 0 0 2px ${theme.colors.primary[100]};
406
- }
407
-
408
- &::placeholder {
409
- color: ${props => props.theme.colors.neutral500};
410
- }
411
- `;
412
-
413
- const HomePage = () => {
414
- useAuthRefresh(); // Initialize token auto-refresh
415
- const { get, post, del } = useFetchClient();
416
- const { toggleNotification } = useNotification();
417
- const [loading, setLoading] = useState(true);
418
- const [accounts, setAccounts] = useState([]);
419
- const [showAddModal, setShowAddModal] = useState(false);
420
- const [editingAccount, setEditingAccount] = useState(null);
421
- const [testingAccount, setTestingAccount] = useState(null);
422
- const [searchQuery, setSearchQuery] = useState('');
423
- const [filterStatus, setFilterStatus] = useState('all');
424
- const [filterProvider, setFilterProvider] = useState('all');
425
-
426
- useEffect(() => {
427
- fetchAccounts();
428
- }, []);
429
-
430
- const fetchAccounts = async () => {
431
- setLoading(true);
432
- try {
433
- const { data } = await get('/magic-mail/accounts');
434
- setAccounts(data.data || []);
435
- } catch (err) {
436
- console.error('[magic-mail] Error fetching accounts:', err);
437
- toggleNotification({
438
- type: 'danger',
439
- message: 'Failed to load email accounts',
440
- });
441
- } finally {
442
- setLoading(false);
443
- }
444
- };
445
-
446
- const testAccount = async (accountId, accountName, testEmail, testOptions = {}) => {
447
- toggleNotification({
448
- type: 'info',
449
- message: `Testing ${accountName}...`,
450
- });
451
-
452
- try {
453
- const { data } = await post(`/magic-mail/accounts/${accountId}/test`, {
454
- testEmail: testEmail,
455
- priority: testOptions.priority || 'normal',
456
- type: testOptions.type || 'transactional',
457
- unsubscribeUrl: testOptions.unsubscribeUrl || null,
458
- });
459
-
460
- toggleNotification({
461
- type: data.success ? 'success' : 'danger',
462
- message: data.message,
463
- });
464
- } catch (err) {
465
- toggleNotification({
466
- type: 'danger',
467
- message: 'Test email failed',
468
- });
469
- }
470
- };
471
-
472
- const deleteAccount = async (accountId, accountName) => {
473
- if (!confirm(`Delete "${accountName}"?`)) return;
474
-
475
- try {
476
- await del(`/magic-mail/accounts/${accountId}`);
477
- toggleNotification({
478
- type: 'success',
479
- message: 'Account deleted successfully',
480
- });
481
- fetchAccounts();
482
- } catch (err) {
483
- toggleNotification({
484
- type: 'danger',
485
- message: 'Failed to delete account',
486
- });
487
- }
488
- };
489
-
490
- if (loading) {
491
- return (
492
- <Flex justifyContent="center" alignItems="center" style={{ minHeight: '400px' }}>
493
- <Loader>Loading MagicMail...</Loader>
494
- </Flex>
495
- );
496
- }
497
-
498
- const totalSentToday = accounts.reduce((sum, acc) => sum + (acc.emailsSentToday || 0), 0);
499
- const totalSent = accounts.reduce((sum, acc) => sum + (acc.totalEmailsSent || 0), 0);
500
- const activeAccounts = accounts.filter(a => a.isActive).length;
501
-
502
- // Filter and search logic
503
- const filteredAccounts = accounts.filter(account => {
504
- const matchesSearch =
505
- account.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
506
- account.fromEmail.toLowerCase().includes(searchQuery.toLowerCase()) ||
507
- (account.provider || '').toLowerCase().includes(searchQuery.toLowerCase());
508
-
509
- const matchesStatus =
510
- filterStatus === 'all' ||
511
- (filterStatus === 'active' && account.isActive) ||
512
- (filterStatus === 'inactive' && !account.isActive) ||
513
- (filterStatus === 'primary' && account.isPrimary);
514
-
515
- const matchesProvider =
516
- filterProvider === 'all' ||
517
- account.provider === filterProvider;
518
-
519
- return matchesSearch && matchesStatus && matchesProvider;
520
- });
521
-
522
- const uniqueProviders = [...new Set(accounts.map(a => a.provider))].filter(Boolean);
523
-
524
- return (
525
- <Container>
526
- {/* Hero Header */}
527
- <Header>
528
- <HeaderContent justifyContent="space-between" alignItems="center">
529
- <Flex direction="column" alignItems="flex-start" gap={2}>
530
- <Title>
531
- <EnvelopeIcon />
532
- MagicMail - Email Business Suite
533
- </Title>
534
- <Subtitle>
535
- Multi-account email management with smart routing and OAuth support
536
- </Subtitle>
537
- </Flex>
538
- </HeaderContent>
539
- </Header>
540
-
541
- {/* Quick Stats */}
542
- <StatsGrid>
543
- <StatCard $delay="0.1s" $color={theme.colors.primary[600]}>
544
- <StatIcon className="stat-icon" $bg={theme.colors.primary[100]} $color={theme.colors.primary[600]}>
545
- <EnvelopeIcon />
546
- </StatIcon>
547
- <StatValue className="stat-value">{totalSentToday}</StatValue>
548
- <StatLabel>Emails Today</StatLabel>
549
- </StatCard>
550
-
551
- <StatCard $delay="0.2s" $color={theme.colors.success[600]}>
552
- <StatIcon className="stat-icon" $bg={theme.colors.success[100]} $color={theme.colors.success[600]}>
553
- <ServerIcon />
554
- </StatIcon>
555
- <StatValue className="stat-value">{totalSent}</StatValue>
556
- <StatLabel>Total Sent</StatLabel>
557
- </StatCard>
558
-
559
- <StatCard $delay="0.3s" $color={theme.colors.warning[600]}>
560
- <StatIcon className="stat-icon" $bg={theme.colors.warning[100]} $color={theme.colors.warning[600]}>
561
- <SparklesIcon />
562
- </StatIcon>
563
- <StatValue className="stat-value">{activeAccounts} / {accounts.length}</StatValue>
564
- <StatLabel>Active Accounts</StatLabel>
565
- </StatCard>
566
- </StatsGrid>
567
-
568
- {/* Account List or Empty State */}
569
- {accounts.length === 0 ? (
570
- <EmptyState>
571
- {/* Floating Emoji */}
572
- <FloatingEmoji>
573
- ✉️
574
- </FloatingEmoji>
575
-
576
- <Flex direction="column" alignItems="center" gap={6} style={{ position: 'relative', zIndex: 1 }}>
577
- <Box
578
- style={{
579
- width: '120px',
580
- height: '120px',
581
- borderRadius: '50%',
582
- background: `linear-gradient(135deg, ${theme.colors.primary[100]} 0%, ${theme.colors.secondary[100]} 100%)`,
583
- display: 'flex',
584
- alignItems: 'center',
585
- justifyContent: 'center',
586
- boxShadow: theme.shadows.xl,
587
- }}
588
- >
589
- <EnvelopeIcon style={{ width: '60px', height: '60px', color: theme.colors.primary[600] }} />
590
- </Box>
591
-
592
- <Typography
593
- variant="alpha"
594
- textColor="neutral800"
595
- style={{
596
- fontSize: '1.75rem',
597
- fontWeight: '700',
598
- marginBottom: '8px',
599
- }}
600
- >
601
- No Email Accounts Yet
602
- </Typography>
603
-
604
- <Typography
605
- variant="omega"
606
- textColor="neutral600"
607
- style={{
608
- fontSize: '1rem',
609
- maxWidth: '500px',
610
- lineHeight: '1.6',
611
- }}
612
- >
613
- Add your first email account to start sending emails through MagicMail's multi-account routing system
614
- </Typography>
615
-
616
- <Button
617
- startIcon={<PlusIcon style={{ width: 20, height: 20 }} />}
618
- onClick={() => setShowAddModal(true)}
619
- size="L"
620
- >
621
- Add First Account
622
- </Button>
623
- </Flex>
624
- </EmptyState>
625
- ) : (
626
- <AccountsContainer>
627
- <Box style={{ marginBottom: theme.spacing.md }}>
628
- <Flex justifyContent="space-between" alignItems="center" marginBottom={4}>
629
- <Typography variant="delta" textColor="neutral700" style={{ fontSize: '1.5rem', fontWeight: 600 }}>
630
- 📧 Email Accounts
631
- </Typography>
632
- <Button startIcon={<PlusIcon style={{ width: 16, height: 16 }} />} onClick={() => setShowAddModal(true)}>
633
- Add Account
634
- </Button>
635
- </Flex>
636
- </Box>
637
-
638
- {/* Filter Bar */}
639
- <FilterBar>
640
- {/* Search Input */}
641
- <SearchInputWrapper>
642
- <SearchIcon />
643
- <StyledSearchInput
644
- value={searchQuery}
645
- onChange={(e) => setSearchQuery(e.target.value)}
646
- placeholder="Search by name, email, or provider..."
647
- type="text"
648
- />
649
- </SearchInputWrapper>
650
-
651
- {/* Status Filter */}
652
- <Box style={{ minWidth: '160px' }}>
653
- <SingleSelect
654
- value={filterStatus}
655
- onChange={setFilterStatus}
656
- placeholder="Status"
657
- size="S"
658
- >
659
- <SingleSelectOption value="all">All Accounts</SingleSelectOption>
660
- <SingleSelectOption value="active">✅ Active</SingleSelectOption>
661
- <SingleSelectOption value="inactive">❌ Inactive</SingleSelectOption>
662
- <SingleSelectOption value="primary">⭐ Primary</SingleSelectOption>
663
- </SingleSelect>
664
- </Box>
665
-
666
- {/* Provider Filter */}
667
- <Box style={{ minWidth: '160px' }}>
668
- <SingleSelect
669
- value={filterProvider}
670
- onChange={setFilterProvider}
671
- placeholder="Provider"
672
- size="S"
673
- >
674
- <SingleSelectOption value="all">All Providers</SingleSelectOption>
675
- {uniqueProviders.map(provider => (
676
- <SingleSelectOption key={provider} value={provider}>
677
- {provider}
678
- </SingleSelectOption>
679
- ))}
680
- </SingleSelect>
681
- </Box>
682
- </FilterBar>
683
-
684
- {/* Accounts Table */}
685
- {filteredAccounts.length > 0 ? (
686
- <Box>
687
- <StyledTable>
688
- <Thead>
689
- <Tr>
690
- <Th>Status</Th>
691
- <Th>Account</Th>
692
- <Th>Provider</Th>
693
- <Th title="Routing Priority (higher = preferred)">Priority</Th>
694
- <Th>Usage Today</Th>
695
- <Th>Total Sent</Th>
696
- <Th>Last Used</Th>
697
- <Th>Actions</Th>
698
- </Tr>
699
- </Thead>
700
- <Tbody>
701
- {filteredAccounts.map((account) => {
702
- const usagePercent = account.dailyLimit > 0
703
- ? Math.round((account.emailsSentToday / account.dailyLimit) * 100)
704
- : 0;
705
- const isNearLimit = usagePercent > 80;
706
-
707
- return (
708
- <Tr key={account.id}>
709
- {/* Status */}
710
- <Td>
711
- <Flex alignItems="center" gap={2}>
712
- <OnlineBadge $active={account.isActive} />
713
- {account.isActive ? (
714
- <Badge backgroundColor="success600" textColor="neutral0" size="S">
715
- Active
716
- </Badge>
717
- ) : (
718
- <Badge backgroundColor="neutral600" textColor="neutral0" size="S">
719
- Inactive
720
- </Badge>
721
- )}
722
- </Flex>
723
- </Td>
724
-
725
- {/* Account */}
726
- <Td>
727
- <Flex direction="column" alignItems="flex-start" gap={1}>
728
- <Flex alignItems="center" gap={2}>
729
- <Typography fontWeight="semiBold">
730
- {account.name}
731
- </Typography>
732
- {account.isPrimary && (
733
- <Badge backgroundColor="warning600" textColor="neutral0" size="S">
734
- ⭐ Primary
735
- </Badge>
736
- )}
737
- </Flex>
738
- <Typography variant="pi" textColor="neutral600">
739
- {account.fromEmail}
740
- </Typography>
741
- </Flex>
742
- </Td>
743
-
744
- {/* Provider */}
745
- <Td>
746
- <Badge size="S">
747
- <ServerIcon style={{ width: 12, height: 12, marginRight: 4 }} />
748
- {account.provider}
749
- </Badge>
750
- </Td>
751
-
752
- {/* Priority */}
753
- <Td>
754
- <Badge size="S" variant="secondary">
755
- {account.priority}/10
756
- </Badge>
757
- </Td>
758
-
759
- {/* Usage Today */}
760
- <Td>
761
- <Flex direction="column" alignItems="flex-start" gap={1}>
762
- <Typography fontWeight="semiBold">
763
- {account.emailsSentToday || 0}
764
- {account.dailyLimit > 0 && (
765
- <Typography variant="pi" textColor="neutral500" as="span">
766
- {' '}/ {account.dailyLimit}
767
- </Typography>
768
- )}
769
- </Typography>
770
- {account.dailyLimit > 0 && (
771
- <Box style={{ width: '100%', minWidth: '80px' }}>
772
- <Box
773
- background="neutral100"
774
- style={{
775
- width: '100%',
776
- height: '6px',
777
- borderRadius: '999px',
778
- overflow: 'hidden',
779
- }}
780
- >
781
- <Box
782
- style={{
783
- width: `${Math.min(usagePercent, 100)}%`,
784
- height: '100%',
785
- background: isNearLimit
786
- ? theme.colors.danger[600]
787
- : theme.colors.success[600],
788
- borderRadius: '999px',
789
- }}
790
- />
791
- </Box>
792
- </Box>
793
- )}
794
- </Flex>
795
- </Td>
796
-
797
- {/* Total Sent */}
798
- <Td>
799
- <Typography fontWeight="semiBold">
800
- {(account.totalEmailsSent || 0).toLocaleString()}
801
- </Typography>
802
- </Td>
803
-
804
- {/* Last Used */}
805
- <Td>
806
- {account.lastUsed ? (
807
- <Typography variant="pi" textColor="neutral600">
808
- {new Date(account.lastUsed).toLocaleString('de-DE', {
809
- day: '2-digit',
810
- month: '2-digit',
811
- year: 'numeric',
812
- hour: '2-digit',
813
- minute: '2-digit'
814
- })}
815
- </Typography>
816
- ) : (
817
- <Typography variant="pi" textColor="neutral500">
818
- Never
819
- </Typography>
820
- )}
821
- </Td>
822
-
823
- {/* Actions */}
824
- <Td>
825
- <Flex gap={2}>
826
- <Button
827
- variant="secondary"
828
- onClick={(e) => {
829
- e.stopPropagation();
830
- setEditingAccount(account);
831
- }}
832
- size="S"
833
- aria-label="Edit Account"
834
- >
835
- <PencilIcon style={{ width: 16, height: 16 }} />
836
- </Button>
837
- <Button
838
- variant="secondary"
839
- onClick={(e) => {
840
- e.stopPropagation();
841
- setTestingAccount(account);
842
- }}
843
- size="S"
844
- aria-label="Test Account"
845
- >
846
- <PlayIcon style={{ width: 16, height: 16 }} />
847
- </Button>
848
- <Button
849
- variant="danger-light"
850
- onClick={(e) => {
851
- e.stopPropagation();
852
- deleteAccount(account.id, account.name);
853
- }}
854
- size="S"
855
- aria-label="Delete Account"
856
- >
857
- <TrashIcon style={{ width: 16, height: 16 }} />
858
- </Button>
859
- </Flex>
860
- </Td>
861
- </Tr>
862
- );
863
- })}
864
- </Tbody>
865
- </StyledTable>
866
- </Box>
867
- ) : (
868
- <Box padding={8} style={{ textAlign: 'center' }}>
869
- <Typography variant="beta" textColor="neutral600">
870
- No accounts found matching your filters
871
- </Typography>
872
- </Box>
873
- )}
874
- </AccountsContainer>
875
- )}
876
-
877
- {/* Add Account Modal */}
878
- <AddAccountModal
879
- isOpen={showAddModal}
880
- onClose={() => setShowAddModal(false)}
881
- onAccountAdded={fetchAccounts}
882
- />
883
-
884
- {/* Edit Account Modal */}
885
- <AddAccountModal
886
- isOpen={!!editingAccount}
887
- onClose={() => setEditingAccount(null)}
888
- onAccountAdded={() => {
889
- fetchAccounts();
890
- setEditingAccount(null);
891
- }}
892
- editAccount={editingAccount}
893
- />
894
-
895
- {/* Test Email Modal */}
896
- {testingAccount && (
897
- <TestEmailModal
898
- account={testingAccount}
899
- onClose={() => setTestingAccount(null)}
900
- onTest={(email, testOptions) => {
901
- testAccount(testingAccount.id, testingAccount.name, email, testOptions);
902
- setTestingAccount(null);
903
- }}
904
- />
905
- )}
906
- </Container>
907
- );
908
- };
909
-
910
- // Test Email Modal Component
911
- const TestEmailModal = ({ account, onClose, onTest }) => {
912
- const { post } = useFetchClient();
913
- const { toggleNotification } = useNotification();
914
- const [testEmail, setTestEmail] = useState('');
915
- const [priority, setPriority] = useState('normal');
916
- const [emailType, setEmailType] = useState('transactional');
917
- const [unsubscribeUrl, setUnsubscribeUrl] = useState('');
918
- const [testingStrapiService, setTestingStrapiService] = useState(false);
919
-
920
- const testStrapiService = async () => {
921
- setTestingStrapiService(true);
922
- try {
923
- const { data } = await post('/magic-mail/test-strapi-service', {
924
- testEmail,
925
- accountName: account.name, // Force this specific account!
926
- });
927
-
928
- if (data.success) {
929
- toggleNotification({
930
- type: 'success',
931
- message: `✅ Strapi Email Service Test: Email sent via ${account.name}!`,
932
- });
933
- onClose();
934
- } else {
935
- toggleNotification({
936
- type: 'warning',
937
- message: data.message || 'Test completed with warnings',
938
- });
939
- }
940
- } catch (err) {
941
- toggleNotification({
942
- type: 'danger',
943
- message: 'Strapi Email Service test failed',
944
- });
945
- } finally {
946
- setTestingStrapiService(false);
947
- }
948
- };
949
-
950
- // Prevent event bubbling to avoid triggering dashboard search
951
- const handleInputChange = (e) => {
952
- e.stopPropagation();
953
- setTestEmail(e.target.value);
954
- };
955
-
956
- const handleKeyDown = (e) => {
957
- e.stopPropagation();
958
- };
959
-
960
- return (
961
- <Modal.Root open={true} onOpenChange={onClose}>
962
- <Modal.Content size="L">
963
- <Modal.Header>
964
- <Typography variant="beta">
965
- <PlayIcon style={{ marginRight: 8, width: 20, height: 20 }} />
966
- Test Email Account
967
- </Typography>
968
- </Modal.Header>
969
-
970
- <Modal.Body>
971
- <Flex direction="column" gap={6} style={{ width: '100%' }}>
972
- {/* Account Info */}
973
- <Box
974
- padding={4}
975
- background="neutral100"
976
- hasRadius
977
- style={{
978
- borderRadius: '8px',
979
- width: '100%',
980
- }}
981
- >
982
- <Flex direction="column" gap={2} style={{ width: '100%' }}>
983
- <Typography fontWeight="semiBold" style={{ fontSize: '14px', color: '#4B5563' }}>
984
- Testing Account
985
- </Typography>
986
- <Typography variant="beta" style={{ fontSize: '18px', fontWeight: 600 }}>
987
- {account.name}
988
- </Typography>
989
- <Typography variant="pi" textColor="neutral600" style={{ fontSize: '14px' }}>
990
- {account.fromEmail}
991
- </Typography>
992
- </Flex>
993
- </Box>
994
-
995
- {/* Email Input */}
996
- <Field.Root required style={{ width: '100%' }}>
997
- <Field.Label style={{ fontSize: '14px' }}>Recipient Email Address</Field.Label>
998
- <TextInput
999
- placeholder="recipient@example.com"
1000
- value={testEmail}
1001
- onChange={handleInputChange}
1002
- onKeyDown={handleKeyDown}
1003
- onClick={(e) => e.stopPropagation()}
1004
- onFocus={(e) => e.stopPropagation()}
1005
- onBlur={(e) => e.stopPropagation()}
1006
- type="email"
1007
- autoFocus
1008
- autoComplete="off"
1009
- name="test-email-recipient"
1010
- style={{ width: '100%', fontSize: '14px' }}
1011
- />
1012
- <Field.Hint style={{ fontSize: '13px' }}>
1013
- Enter the email address where you want to receive the test email
1014
- </Field.Hint>
1015
- </Field.Root>
1016
-
1017
- {/* Test Configuration */}
1018
- <Box style={{ width: '100%' }}>
1019
- <Typography fontWeight="semiBold" marginBottom={3} style={{ fontSize: '14px', color: '#4B5563' }}>
1020
- Email Configuration
1021
- </Typography>
1022
-
1023
- <Flex direction="column" gap={3} style={{ width: '100%' }}>
1024
- {/* Priority */}
1025
- <Field.Root style={{ width: '100%' }}>
1026
- <Field.Label style={{ fontSize: '14px' }}>Priority</Field.Label>
1027
- <SingleSelect
1028
- value={priority}
1029
- onChange={setPriority}
1030
- style={{ width: '100%' }}
1031
- >
1032
- <SingleSelectOption value="normal">Normal Priority</SingleSelectOption>
1033
- <SingleSelectOption value="high">High Priority</SingleSelectOption>
1034
- </SingleSelect>
1035
- <Field.Hint style={{ fontSize: '13px' }}>
1036
- High priority adds X-Priority and Importance headers
1037
- </Field.Hint>
1038
- </Field.Root>
1039
-
1040
- {/* Email Type */}
1041
- <Field.Root style={{ width: '100%' }}>
1042
- <Field.Label style={{ fontSize: '14px' }}>Email Type</Field.Label>
1043
- <SingleSelect
1044
- value={emailType}
1045
- onChange={setEmailType}
1046
- style={{ width: '100%' }}
1047
- >
1048
- <SingleSelectOption value="transactional">Transactional</SingleSelectOption>
1049
- <SingleSelectOption value="marketing">Marketing</SingleSelectOption>
1050
- <SingleSelectOption value="notification">Notification</SingleSelectOption>
1051
- </SingleSelect>
1052
- <Field.Hint style={{ fontSize: '13px' }}>
1053
- Marketing emails automatically include List-Unsubscribe headers
1054
- </Field.Hint>
1055
- </Field.Root>
1056
-
1057
- {/* Unsubscribe URL (nur für Marketing) */}
1058
- {emailType === 'marketing' && (
1059
- <Field.Root style={{ width: '100%' }}>
1060
- <Field.Label style={{ fontSize: '14px' }}>Unsubscribe URL (Required for Marketing)</Field.Label>
1061
- <TextInput
1062
- placeholder="https://yoursite.com/unsubscribe"
1063
- value={unsubscribeUrl}
1064
- onChange={(e) => {
1065
- e.stopPropagation();
1066
- setUnsubscribeUrl(e.target.value);
1067
- }}
1068
- style={{ width: '100%', fontSize: '14px' }}
1069
- />
1070
- <Field.Hint style={{ fontSize: '13px' }}>
1071
- Required for GDPR/CAN-SPAM compliance. Adds List-Unsubscribe header.
1072
- </Field.Hint>
1073
- </Field.Root>
1074
- )}
1075
- </Flex>
1076
- </Box>
1077
-
1078
- {/* Test Options */}
1079
- <Box style={{ width: '100%' }}>
1080
- <Typography fontWeight="semiBold" marginBottom={3} style={{ fontSize: '14px', color: '#4B5563' }}>
1081
- Test Options
1082
- </Typography>
1083
-
1084
- <Flex direction="column" gap={3} style={{ width: '100%' }}>
1085
- {/* Direct Test */}
1086
- <Box
1087
- padding={4}
1088
- background="neutral0"
1089
- hasRadius
1090
- style={{
1091
- border: '2px solid #E5E7EB',
1092
- borderRadius: '8px',
1093
- width: '100%',
1094
- }}
1095
- >
1096
- <Flex direction="column" gap={2}>
1097
- <Flex alignItems="center" gap={2}>
1098
- <PlayIcon style={{ width: 18, height: 18, color: '#0EA5E9', flexShrink: 0 }} />
1099
- <Typography fontWeight="semiBold" style={{ fontSize: '14px' }}>
1100
- Direct Test
1101
- </Typography>
1102
- </Flex>
1103
- <Typography variant="pi" textColor="neutral600" style={{ fontSize: '13px', lineHeight: '1.5' }}>
1104
- Send test email directly through this specific account
1105
- </Typography>
1106
- </Flex>
1107
- </Box>
1108
-
1109
- {/* Strapi Service Test */}
1110
- <Box
1111
- padding={4}
1112
- background="primary50"
1113
- hasRadius
1114
- style={{
1115
- border: '2px solid #0EA5E9',
1116
- borderRadius: '8px',
1117
- width: '100%',
1118
- }}
1119
- >
1120
- <Flex direction="column" gap={2}>
1121
- <Flex alignItems="center" gap={2}>
1122
- <SparklesIcon style={{ width: 18, height: 18, color: '#0369A1', flexShrink: 0 }} />
1123
- <Typography fontWeight="semiBold" style={{ fontSize: '14px', color: '#0369A1' }}>
1124
- Strapi Email Service Test
1125
- </Typography>
1126
- </Flex>
1127
- <Typography variant="pi" textColor="neutral600" style={{ fontSize: '13px', lineHeight: '1.5' }}>
1128
- Test if MagicMail intercepts Strapi's native email service via THIS account ({account.name})
1129
- </Typography>
1130
- <Typography variant="pi" textColor="neutral600" style={{ fontSize: '13px', lineHeight: '1.5' }}>
1131
- <strong style={{ color: '#0369A1' }}>Use this to verify Email Designer compatibility</strong>
1132
- </Typography>
1133
- </Flex>
1134
- </Box>
1135
- </Flex>
1136
- </Box>
1137
- </Flex>
1138
- </Modal.Body>
1139
-
1140
- <Modal.Footer>
1141
- <Flex justifyContent="space-between" gap={2} style={{ width: '100%' }}>
1142
- <Button onClick={onClose} variant="tertiary">
1143
- Cancel
1144
- </Button>
1145
- <Flex gap={2}>
1146
- <Button
1147
- onClick={() => onTest(testEmail, { priority, type: emailType, unsubscribeUrl })}
1148
- disabled={!testEmail || !testEmail.includes('@') || (emailType === 'marketing' && !unsubscribeUrl)}
1149
- startIcon={<PlayIcon style={{ width: 16, height: 16 }} />}
1150
- variant="secondary"
1151
- >
1152
- Test Direct
1153
- </Button>
1154
- <Button
1155
- onClick={testStrapiService}
1156
- disabled={!testEmail || !testEmail.includes('@')}
1157
- loading={testingStrapiService}
1158
- startIcon={<SparklesIcon style={{ width: 16, height: 16 }} />}
1159
- >
1160
- Test Strapi Service
1161
- </Button>
1162
- </Flex>
1163
- </Flex>
1164
- </Modal.Footer>
1165
- </Modal.Content>
1166
- </Modal.Root>
1167
- );
1168
- };
1169
-
1170
- export default HomePage;