strapi-plugin-magic-mail 3.0.4 → 4.0.0

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 (54) hide show
  1. package/CHANGELOG.md +192 -0
  2. package/README.md +625 -1336
  3. package/dist/admin/App-Bi6mzEff.mjs +10973 -0
  4. package/dist/admin/App-aaEU9cpu.js +10976 -0
  5. package/dist/admin/PluginSettings-BR2-PVHx.mjs +705 -0
  6. package/dist/admin/PluginSettings-DbTkOzCh.js +707 -0
  7. package/dist/admin/analyticsLogs-BCY0Zv3Y.mjs +685 -0
  8. package/dist/admin/analyticsLogs-DNU09hoB.js +686 -0
  9. package/dist/admin/de-B5bPRK6l.js +717 -0
  10. package/dist/admin/de-BqYqMhOn.mjs +717 -0
  11. package/dist/admin/en-8xANOQdO.mjs +717 -0
  12. package/dist/admin/en-Bk5eVrtc.js +717 -0
  13. package/dist/admin/es-BfPsCsam.js +717 -0
  14. package/dist/admin/es-DgpTufJ_.mjs +717 -0
  15. package/dist/admin/fr-BuotP0Gc.mjs +717 -0
  16. package/dist/admin/fr-IQ3KPUoS.js +717 -0
  17. package/dist/admin/index-QGqbGw6k.js +102 -0
  18. package/dist/admin/index-qvFJ-a0Q.mjs +103 -0
  19. package/dist/admin/index.js +2 -99
  20. package/dist/admin/index.mjs +2 -99
  21. package/dist/admin/pt-BI63Lncs.js +717 -0
  22. package/dist/admin/pt-DBZu2cDY.mjs +717 -0
  23. package/dist/server/index.js +4655 -3374
  24. package/dist/server/index.mjs +4655 -3374
  25. package/package.json +54 -48
  26. package/strapi-admin.js +3 -2
  27. package/strapi-server.js +3 -2
  28. package/dist/admin/App-D3TchJYB.js +0 -8028
  29. package/dist/admin/App-DkyWmEgv.mjs +0 -8007
  30. package/dist/admin/PluginSettings-B6TdA1qd.js +0 -388
  31. package/dist/admin/PluginSettings-DTqdZ-nW.mjs +0 -386
  32. package/dist/admin/StyledButtons-aq1EWrQ-.mjs +0 -433
  33. package/dist/admin/StyledButtons-jHF7TXal.js +0 -434
  34. package/dist/admin/de-DKEFnBGC.js +0 -63
  35. package/dist/admin/de-SKsLY2de.mjs +0 -63
  36. package/dist/admin/en-CvtZE1HH.js +0 -63
  37. package/dist/admin/en-Ds2Q7fue.mjs +0 -63
  38. package/dist/admin/es-BEvzLtTg.mjs +0 -63
  39. package/dist/admin/es-BbLLes3D.js +0 -63
  40. package/dist/admin/fr-CRpwImX6.mjs +0 -63
  41. package/dist/admin/fr-nH9uN6s9.js +0 -63
  42. package/dist/admin/pt-CjOe2N_n.mjs +0 -63
  43. package/dist/admin/pt-qt_48NxL.js +0 -63
  44. package/pics/EmailAccountDashboard.png +0 -0
  45. package/pics/Emailaddccount1.png +0 -0
  46. package/pics/RoutingEmailTemplates.png +0 -0
  47. package/pics/Templates.png +0 -0
  48. package/pics/codesnippet.png +0 -0
  49. package/pics/emailAnalytics.png +0 -0
  50. package/pics/emailDesigner.png +0 -0
  51. package/pics/emailTemplateSendTest.png +0 -0
  52. package/pics/emailVersionHistory.png +0 -0
  53. package/pics/emailaddaccount2.png +0 -0
  54. package/pics/emailaddaccount3.png +0 -0
package/README.md CHANGED
@@ -1,1641 +1,930 @@
1
- # 📧 MagicMail - Email Business Suite for Strapi v5
1
+ # MagicMail: email accounts, routing and templates for Strapi 5
2
2
 
3
- > **Professional-grade multi-account email management with smart routing, OAuth 2.0 support, and complete security compliance — 100% free and open source**
3
+ Project site: https://schero94.github.io/Magic-Mail/
4
4
 
5
5
  [![NPM Version](https://img.shields.io/npm/v/strapi-plugin-magic-mail.svg)](https://www.npmjs.com/package/strapi-plugin-magic-mail)
6
6
  [![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
- [![Strapi Version](https://img.shields.io/badge/Strapi-v5.30.1-blue.svg)](https://strapi.io)
7
+ [![Strapi Version](<https://img.shields.io/badge/Strapi-5.57%2B%20(tested%205.61)-blue.svg>)](https://strapi.io)
8
8
 
9
- > 💚 **Fully free & open source (MIT).** Every feature is included for everyone — no license key, no activation, no paid tiers, and no usage limits.
9
+ MagicMail adds a mail desk to the Strapi admin. You connect one or more sending accounts (Gmail, Microsoft 365, Yahoo, any SMTP server, SendGrid or Mailgun), decide with routing rules which account sends which email, design templates, and see what was delivered, opened and clicked.
10
10
 
11
- ---
12
-
13
- ## 🌟 Why MagicMail?
14
-
15
- **Stop fighting with .env files and email configuration!** MagicMail brings professional email management to Strapi v5 with:
16
-
17
- - **100% Free & Open Source** - Every feature included, no license key, tiers, or limits (MIT)
18
- - **6 Email Providers** - Gmail, Microsoft 365, Yahoo, SMTP, SendGrid, Mailgun
19
- - **WhatsApp Messaging** - Send messages via WhatsApp (FREE!)
20
- - **OAuth 2.0 Authentication** - No passwords needed for Gmail, Microsoft, Yahoo
21
- - **Smart Routing Rules** - Route emails by type, recipient, subject, or custom conditions
22
- - **WhatsApp Fallback** - Auto-send via WhatsApp if email fails
23
- - **Automatic Failover** - Never lose an email when rate limits hit
24
- - **Beautiful Admin UI** - Manage everything from Strapi Admin Panel
25
- - **Zero Configuration** - No .env files, everything in the database
26
- - **Email Designer Compatible** - Works seamlessly with strapi-plugin-email-designer-5
27
- - **GDPR/CAN-SPAM Compliant** - Built-in List-Unsubscribe headers
28
- - **Professional Security** - TLS 1.2+, DKIM, SPF, DMARC validation
29
-
30
- ---
31
-
32
- ## 📸 Screenshots
33
-
34
- ### Email Accounts Dashboard
35
- ![Email Accounts Dashboard](pics/EmailAccountDashboard.png)
36
- *Manage unlimited email accounts with live stats and real-time monitoring*
37
-
38
- ### Add Email Account - Step 1
39
- ![Add Account Step 1](pics/Emailaddccount1.png)
40
- *Choose from 6 different email providers with OAuth 2.0 support*
41
-
42
- ### Add Email Account - Step 2
43
- ![Add Account Step 2](pics/emailaddaccount2.png)
44
- *Configure SMTP settings or connect with OAuth*
45
-
46
- ### Add Email Account - Step 3
47
- ![Add Account Step 3](pics/emailaddaccount3.png)
48
- *Complete setup with rate limits and test connection*
49
-
50
- ### Email Templates
51
- ![Email Templates](pics/Templates.png)
52
- *Create and manage email templates with the visual designer*
11
+ It plugs into the Strapi email service you already use. Code that calls `strapi.plugin('email').service('email').send()`, Strapi's own password-reset mail and every Users & Permissions mail go through MagicMail without changes.
53
12
 
54
- ### Email Designer
55
- ![Email Designer](pics/emailDesigner.png)
56
- *Drag-and-drop email editor with real-time preview*
13
+ MagicMail sends **email only**. It is MIT-licensed, has no license key, tiers or usage limits.
57
14
 
58
- ### Template Version History
59
- ![Version History](pics/emailVersionHistory.png)
60
- *Track changes and restore previous template versions*
15
+ **At a glance**
61
16
 
62
- ### Template Send Test
63
- ![Send Test](pics/emailTemplateSendTest.png)
64
- *Test your templates before sending to production*
17
+ - 6 providers: Gmail, Microsoft 365 and Yahoo with OAuth 2.0, SMTP (optional DKIM), SendGrid, Mailgun
18
+ - Routing rules by email type, recipient, subject, template or a custom value, with a fallback account
19
+ - Hourly and daily quotas per account, reserved atomically, with safe failover
20
+ - Template designer: visual editor (Unlayer) or plain HTML/text, Mustache variables, versions, import/export
21
+ - Delivery log with privacy-friendly open and click tracking and configurable retention
22
+ - Credentials encrypted with AES-256-GCM; secrets never reach the browser
23
+ - Admin UI in English, German, French, Spanish and Portuguese, light and dark mode
65
24
 
66
- ### Routing Rules & Templates
67
- ![Routing Rules](pics/RoutingEmailTemplates.png)
68
- *Define intelligent routing rules based on email type, recipient, subject*
25
+ ## Contents
69
26
 
70
- ### Email Analytics
71
- ![Email Analytics](pics/emailAnalytics.png)
72
- *Track email performance with detailed analytics and statistics*
73
-
74
- ### Code Integration
75
- ![Code Snippet](pics/codesnippet.png)
76
- *Simple API integration with your Strapi application*
27
+ - [Screenshots](#screenshots)
28
+ - [Quick start](#quick-start)
29
+ - [Sending email](#sending-email)
30
+ - [Provider setup](#provider-setup)
31
+ - [Routing rules and quotas](#routing-rules-and-quotas)
32
+ - [Templates and the visual editor](#templates-and-the-visual-editor)
33
+ - [Tracking, privacy and retention](#tracking-privacy-and-retention)
34
+ - [Content API](#content-api)
35
+ - [Admin API](#admin-api)
36
+ - [Permissions](#permissions)
37
+ - [Configuration reference](#configuration-reference)
38
+ - [Troubleshooting](#troubleshooting)
39
+ - [Upgrading to 4.0.0](#upgrading-to-400)
40
+ - [Development](#development)
77
41
 
78
42
  ---
79
43
 
80
- ## WhatsApp Messaging (NEW!)
44
+ ## Screenshots
81
45
 
82
- MagicMail now includes **FREE WhatsApp messaging** - send messages directly from your Strapi application!
46
+ The screenshots show synthetic demo data (`example.test` addresses) on Strapi 5.61.
83
47
 
84
- ### Why WhatsApp?
48
+ ### Email accounts
85
49
 
86
- | Feature | Email | WhatsApp |
87
- |---------|-------|----------|
88
- | **Open Rate** | ~20% | ~98% |
89
- | **Delivery Speed** | Minutes | Instant |
90
- | **Spam Folder Risk** | High | None |
91
- | **User Preference** | Declining | Growing |
92
- | **Cost** | Varies | FREE |
50
+ ![Email accounts list with usage, quotas and actions](https://raw.githubusercontent.com/Schero94/Magic-Mail/main/pics/EmailAccountDashboard.png)
51
+ _Accounts with status, provider, priority, today's usage against the daily limit, and test/edit/delete actions._
93
52
 
94
- ### Quick Setup (4 Steps)
53
+ ### Account wizard
95
54
 
96
- 1. **Navigate** to Admin Panel -> MagicMail -> WhatsApp
97
- 2. **Connect** - Scan QR code with WhatsApp mobile app
98
- 3. **Verify** - Check connection status shows "Connected"
99
- 4. **Send** - Start sending messages programmatically!
55
+ ![Add email account wizard, provider step](https://raw.githubusercontent.com/Schero94/Magic-Mail/main/pics/Emailaddccount1.png)
56
+ _Four steps: provider, details, credentials, settings. OAuth providers connect in a popup._
100
57
 
101
- ### Sending WhatsApp Messages
58
+ ### Routing rules
102
59
 
103
- **Method 1: Direct WhatsApp Send**
60
+ ![Routing rules list in evaluation order](https://raw.githubusercontent.com/Schero94/Magic-Mail/main/pics/RoutingEmailTemplates.png)
61
+ _Rules in evaluation order. Rules that point to a missing or inactive account are flagged._
104
62
 
105
- ```javascript
106
- // Send WhatsApp message directly
107
- await strapi.plugin('magic-mail').service('whatsapp').sendMessage(
108
- '+491234567890', // Phone number with country code
109
- 'Hello! This is a test message from MagicMail.'
110
- );
111
- ```
63
+ ### Templates
112
64
 
113
- **Method 2: Unified Messaging API**
65
+ ![Template list with categories, status and actions](https://raw.githubusercontent.com/Schero94/Magic-Mail/main/pics/Templates.png)
66
+ _Templates with Reference ID, category and status; edit, download as HTML or JSON, duplicate, code snippets, test send and delete._
114
67
 
115
- ```javascript
116
- // Send via unified API - auto-detects channel
117
- await strapi.plugin('magic-mail').service('email-router').sendMessage({
118
- channel: 'whatsapp', // or 'email' or 'auto'
119
- phoneNumber: '+491234567890',
120
- message: 'Your order #12345 has been shipped!'
121
- });
68
+ ### Template editor
122
69
 
123
- // Auto-detect: sends WhatsApp if phoneNumber provided, email if to provided
124
- await strapi.plugin('magic-mail').service('email-router').sendMessage({
125
- channel: 'auto',
126
- phoneNumber: '+491234567890', // Has phone -> WhatsApp
127
- message: 'Shipping notification'
128
- });
70
+ ![Template editor with the visual designer](https://raw.githubusercontent.com/Schero94/Magic-Mail/main/pics/emailDesigner.png)
71
+ _Template editor. For this screenshot the Unlayer canvas was replaced by a local stand-in, so no request went to Unlayer; the real editor looks different inside the canvas._
129
72
 
130
- await strapi.plugin('magic-mail').service('email-router').sendMessage({
131
- channel: 'auto',
132
- to: 'user@example.com', // Has email -> Email
133
- subject: 'Shipping Notification',
134
- message: 'Your order has been shipped!'
135
- });
136
- ```
73
+ ### Analytics
137
74
 
138
- **Method 3: REST API**
75
+ ![Analytics with totals and the delivery log](https://raw.githubusercontent.com/Schero94/Magic-Mail/main/pics/emailAnalytics.png)
76
+ _Totals, delivery log with server-side search, opens and clicks per email._
139
77
 
140
- ```javascript
141
- // POST /api/magic-mail/send-whatsapp
142
- await fetch('/api/magic-mail/send-whatsapp', {
143
- method: 'POST',
144
- headers: {
145
- 'Content-Type': 'application/json',
146
- 'Authorization': 'Bearer YOUR_STRAPI_API_TOKEN'
147
- },
148
- body: JSON.stringify({
149
- phoneNumber: '+491234567890',
150
- message: 'Hello from MagicMail!'
151
- })
152
- });
78
+ ### Plugin settings
153
79
 
154
- // POST /api/magic-mail/send-message (unified)
155
- await fetch('/api/magic-mail/send-message', {
156
- method: 'POST',
157
- headers: {
158
- 'Content-Type': 'application/json',
159
- 'Authorization': 'Bearer YOUR_STRAPI_API_TOKEN'
160
- },
161
- body: JSON.stringify({
162
- channel: 'whatsapp',
163
- phoneNumber: '+491234567890',
164
- message: 'Order confirmed!'
165
- })
166
- });
167
- ```
80
+ ![Plugin settings for tracking and unsubscribe headers](https://raw.githubusercontent.com/Schero94/Magic-Mail/main/pics/pluginSettings.png)
81
+ _Settings → MagicMail → Plugin Settings: link and open tracking, tracking base URL, unsubscribe header, default sender name._
168
82
 
169
- ### WhatsApp Use Cases
83
+ ### Dark mode and mobile
170
84
 
171
- **1. Email Fallback**
172
- If email delivery fails, automatically send via WhatsApp:
85
+ ![Analytics in dark mode](https://raw.githubusercontent.com/Schero94/Magic-Mail/main/pics/emailAnalyticsDark.png)
173
86
 
174
- ```javascript
175
- // In Routing Rules, enable "WhatsApp Fallback"
176
- // Set "WhatsApp Phone Field" to 'phoneNumber'
87
+ | Accounts at 390 px | Routing rules at 390 px |
88
+ | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
89
+ | ![Accounts on a phone](https://raw.githubusercontent.com/Schero94/Magic-Mail/main/pics/mobileAccounts.png) | ![Routing rules on a phone](https://raw.githubusercontent.com/Schero94/Magic-Mail/main/pics/mobileRoutingRules.png) |
177
90
 
178
- await strapi.plugin('magic-mail').service('email-router').send({
179
- to: 'user@example.com',
180
- phoneNumber: '+491234567890', // Fallback phone
181
- subject: 'Important Update',
182
- html: '<h1>Update</h1>',
183
- type: 'transactional'
184
- });
185
-
186
- // If email fails -> auto-sends via WhatsApp!
187
- ```
91
+ ---
188
92
 
189
- **2. Admin Notifications**
190
- Get instant WhatsApp alerts for critical events:
93
+ ## Quick start
191
94
 
192
- ```javascript
193
- // Example: Notify admin when email quota exceeded
194
- const adminPhone = '+4917612345678';
95
+ ### Requirements
195
96
 
196
- await strapi.plugin('magic-mail').service('whatsapp').sendMessage(
197
- adminPhone,
198
- '[ALERT] Email quota exceeded for SendGrid account!'
199
- );
200
- ```
97
+ - Node.js `>=22.12.0 <=26.x.x`
98
+ - Strapi `^5.57.0` (developed and tested against 5.61)
99
+ - `@strapi/plugin-users-permissions` `^5.57.0` (part of every default Strapi app; the email log links to its users)
100
+ - Outside development and test: the environment variable `MAGIC_MAIL_ENCRYPTION_KEY` (see [Environment variables](#environment-variables))
201
101
 
202
- **3. Urgent Transactional Messages**
203
- Send time-sensitive messages instantly:
102
+ ### 1. Install
204
103
 
205
- ```javascript
206
- // OTP/Verification codes
207
- await strapi.plugin('magic-mail').service('whatsapp').sendMessage(
208
- userPhone,
209
- `Your verification code: 123456\n\nValid for 5 minutes.`
210
- );
211
-
212
- // Order updates
213
- await strapi.plugin('magic-mail').service('whatsapp').sendMessage(
214
- customerPhone,
215
- `Order #${orderId} is out for delivery!\nTrack: ${trackingUrl}`
216
- );
104
+ ```bash
105
+ npm install strapi-plugin-magic-mail
217
106
  ```
218
107
 
219
- ### WhatsApp Status API
108
+ ### 2. Enable
220
109
 
221
110
  ```javascript
222
- // Check connection status
223
- const status = await strapi.plugin('magic-mail').service('whatsapp').getStatus();
224
- // { isConnected: true, phoneNumber: '+49...', name: 'Business Account' }
225
-
226
- // Check if phone number is on WhatsApp
227
- const exists = await strapi.plugin('magic-mail').service('whatsapp').checkNumber('+491234567890');
228
- // { exists: true, jid: '491234567890@s.whatsapp.net' }
111
+ // config/plugins.js (or config/plugins.ts with export default)
112
+ module.exports = () => ({
113
+ 'magic-mail': {
114
+ enabled: true,
115
+ },
116
+ });
229
117
  ```
230
118
 
231
- ### Integration with Magic-Link Plugin
232
-
233
- When both **MagicMail** and **Magic-Link** are installed:
234
-
235
- - Magic-Link automatically uses MagicMail's WhatsApp service
236
- - Only ONE WhatsApp connection needed (managed here in MagicMail)
237
- - Send magic links via WhatsApp from Magic-Link settings
238
- - Unified WhatsApp management for all plugins
239
-
240
- ---
241
-
242
- ## Quick Start
243
-
244
- ### Installation
245
-
246
- Requires Node.js 20.19 or newer and Strapi 5.50 or newer.
119
+ ### 3. Set the encryption key
247
120
 
248
121
  ```bash
249
- npm install strapi-plugin-magic-mail
250
- # or
251
- yarn add strapi-plugin-magic-mail
122
+ # Generate once and keep it with your other secrets
123
+ node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
252
124
  ```
253
125
 
254
- ### Enable Plugin
255
-
256
- Create or update `config/plugins.ts`:
257
-
258
- ```typescript
259
- export default () => ({
260
- 'magic-mail': {
261
- enabled: true,
262
- },
263
- });
126
+ ```env
127
+ MAGIC_MAIL_ENCRYPTION_KEY=<the generated value>
264
128
  ```
265
129
 
266
- ### Build & Start
130
+ Keep the key stable. Losing it makes the stored account credentials unreadable.
131
+
132
+ ### 4. Build and start
267
133
 
268
134
  ```bash
269
135
  npm run build
270
136
  npm run develop
271
137
  ```
272
138
 
273
- ### Add Your First Email Account
274
-
275
- 1. Navigate to **Admin Panel → MagicMail → Email Accounts**
276
- 2. Click **"Add Account"**
277
- 3. Choose your provider (Gmail OAuth, SMTP, etc.)
278
- 4. Fill in credentials
279
- 5. Click **"Test"** to verify
280
- 6. Done! 🎉
139
+ ### 5. Add an account and send a test
281
140
 
282
- ---
141
+ 1. Open **MagicMail → Email Accounts** in the admin and click **Add account**.
142
+ 2. Pick a provider and enter the details (see [Provider setup](#provider-setup)).
143
+ 3. Click the **Test** action on the account, enter your own address and choose **Direct test**.
283
144
 
284
- ## 📧 Supported Email Providers
145
+ Every existing `strapi.plugin('email')` call now goes through MagicMail.
285
146
 
286
- | Provider | Type | Authentication | Features |
287
- |----------|------|----------------|----------|
288
- | **Gmail** | OAuth 2.0 | Google OAuth | Gmail API, Attachments, Auto DKIM |
289
- | **Microsoft 365** | OAuth 2.0 | Azure AD | Graph API, Attachments, Tenant Support |
290
- | **Yahoo Mail** | OAuth 2.0 | Yahoo OAuth | SMTP OAuth2, Attachments |
291
- | **SMTP** | Credentials | Username/Password | Universal, DKIM Optional, Custom Servers |
292
- | **SendGrid** | API Key | SendGrid API | Transactional, Marketing, Templates |
293
- | **Mailgun** | API Key | Mailgun API | Bulk Sending, Analytics |
147
+ Upgrading from 3.x? Read [Upgrading to 4.0.0](#upgrading-to-400) first.
294
148
 
295
149
  ---
296
150
 
297
- ## 💡 How to Send Emails
151
+ ## Sending email
298
152
 
299
- ### Method 1: Strapi Native Email Service (Recommended!)
153
+ ### Option 1: the Strapi email service (recommended)
300
154
 
301
- MagicMail **automatically intercepts** Strapi's email service - no code changes needed!
155
+ MagicMail wraps the provider of `@strapi/email` at startup, so existing code keeps working:
302
156
 
303
157
  ```javascript
304
- // This works in ANY Strapi plugin or controller:
305
158
  await strapi.plugin('email').service('email').send({
306
159
  to: 'user@example.com',
307
- subject: 'Welcome to Our Platform!',
308
- text: 'Plain text version',
160
+ subject: 'Welcome to Example Shop',
161
+ text: 'Thanks for signing up!',
309
162
  html: '<h1>Welcome!</h1><p>Thanks for signing up!</p>',
310
163
  });
311
-
312
- // ✅ MagicMail automatically:
313
- // - Selects best account (via routing rules or priority)
314
- // - Checks rate limits
315
- // - Handles failover if needed
316
- // - Logs the email
317
- // - Updates statistics
318
164
  ```
319
165
 
320
- **Benefits:**
321
- - ✅ **Zero code changes** - Existing plugins work automatically
322
- - ✅ **Email Designer compatible** - Works with strapi-plugin-email-designer-5
323
- - ✅ **Fallback safe** - Falls back to original email service if MagicMail fails
324
- - ✅ **Drop-in replacement** - Just enable and go!
166
+ Both `send()` and `sendTemplatedEmail()` are routed, including Strapi's admin password-reset email and the Users & Permissions emails. MagicMail picks the account (routing rules, then priority), reserves quota, fails over when that is safe, logs the email and updates the statistics.
167
+
168
+ **Fallback to the original provider.** When no MagicMail account is active, or every eligible account is over its quota, the email is sent through the Strapi email provider you configured before (logged with the account name `strapi-email-provider`). Once a MagicMail account has attempted delivery, an error is returned to the caller and the email is **not** sent again through the original provider, so nothing goes out twice.
325
169
 
326
- ### Method 2: Direct MagicMail API
170
+ ### Option 2: the MagicMail router service
327
171
 
328
- For explicit control over account selection:
172
+ Use the router directly when you need its options, for example to force an account:
329
173
 
330
174
  ```javascript
331
- // Force a specific account
332
175
  await strapi.plugin('magic-mail').service('email-router').send({
333
176
  to: 'customer@example.com',
334
- subject: 'Order Confirmation',
335
- html: '<h1>Your Order #12345</h1>',
336
- accountName: 'SendGrid Marketing', // Force this account
177
+ subject: 'Order confirmation',
178
+ html: '<h1>Your order #12345</h1>',
179
+ accountName: 'Transactional SMTP', // case-insensitive; never replaced by another account
337
180
  type: 'transactional',
338
181
  priority: 'high',
339
182
  });
183
+ // → { success: true, accountUsed: 'Transactional SMTP', messageId: '<…>', emailId: '…' }
340
184
  ```
341
185
 
342
- ### Sending with Attachments
343
-
344
- ```javascript
345
- await strapi.plugin('email').service('email').send({
346
- to: 'customer@example.com',
347
- subject: 'Invoice #12345',
348
- html: '<h1>Invoice Attached</h1>',
349
- attachments: [
350
- {
351
- filename: 'invoice.pdf',
352
- path: './uploads/invoice-12345.pdf',
353
- },
354
- {
355
- filename: 'receipt.pdf',
356
- content: Buffer.from(pdfData),
357
- contentType: 'application/pdf',
358
- },
359
- ],
360
- });
361
- ```
362
-
363
- **Supported attachment formats:**
364
- - ✅ PDF, Word, Excel, PowerPoint
365
- - ✅ Images (PNG, JPG, GIF, WebP)
366
- - ✅ Text files (TXT, CSV, JSON)
367
- - ✅ Archives (ZIP, RAR)
368
- - ✅ Any file type!
369
-
370
- ### Advanced: Priority & Marketing Emails
371
-
372
- ```javascript
373
- // High priority email
374
- await strapi.plugin('magic-mail').service('email-router').send({
375
- to: 'vip@example.com',
376
- subject: 'Urgent: Action Required',
377
- html: '<h1>Important Update</h1>',
378
- priority: 'high', // Adds X-Priority and Importance headers
379
- });
380
-
381
- // Marketing email with unsubscribe (GDPR/CAN-SPAM compliant)
382
- await strapi.plugin('magic-mail').service('email-router').send({
383
- to: 'subscriber@example.com',
384
- subject: 'Weekly Newsletter',
385
- html: '<h1>This Week's Updates</h1>',
386
- type: 'marketing',
387
- unsubscribeUrl: 'https://yoursite.com/unsubscribe?id=123',
388
- // Automatically adds List-Unsubscribe header!
389
- });
390
- ```
391
-
392
- ---
393
-
394
- ## 🎨 Email Designer Suite (NEW!)
395
-
396
- **Design beautiful emails visually with the integrated email designer!**
397
-
398
- ### ✨ Features
399
-
400
- Every Email Designer feature is included for free:
186
+ An explicit `accountName` is never swapped for another account: if that account is over quota or fails, the send fails. Router calls never use the Strapi-provider fallback; they get the error.
401
187
 
402
- | Feature | Included |
403
- |---------|----------|
404
- | **Visual Designer** | ✅ |
405
- | **Unlimited Templates** | ✅ |
406
- | **Drag & Drop + Mustache** | ✅ |
407
- | **Import / Export** | ✅ |
408
- | **Template Versioning** | ✅ |
409
- | **Email Analytics** | ✅ |
188
+ ### Option 3: HTTP
410
189
 
411
- ### 📧 Creating Email Templates
190
+ See [Content API](#content-api) for `POST /api/magic-mail/send` and `/send-message`.
412
191
 
413
- **1. Navigate to Email Templates Tab**
192
+ ### Send options
414
193
 
415
- Go to **MagicMail → Email Templates** in the admin panel.
194
+ The same fields work for the router, the Strapi email service and `POST /api/magic-mail/send`.
416
195
 
417
- **2. Create New Template**
196
+ | Field | Description |
197
+ | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
198
+ | `to`, `cc`, `bcc` | Address, array or comma-separated list (max. 100 `to`, 50 `cc`/`bcc`). Display names allowed |
199
+ | `subject`, `text`, `html` | `subject` is required unless a template provides it; `text` or `html` is required. HTML is sanitised (no scripts, event handlers or `javascript:` URLs; email layout is kept) |
200
+ | `from` | Optional. Must be the sending account's address; only its display name is used |
201
+ | `replyTo` | A valid address |
202
+ | `type` | `transactional` (default), `marketing` or `notification`. Without `type`, a template's category is used |
203
+ | `priority` | `normal` (default), `high` or `low`. `high` adds `X-Priority` and `Importance` headers |
204
+ | `accountName` | Force one account |
205
+ | `templateId` / `templateReferenceId`, `templateData` | Render a MagicMail template by its Reference ID with Mustache data (`data` is accepted as an alias of `templateData`) |
206
+ | `unsubscribeUrl` | Per-email unsubscribe URL |
207
+ | `enableTracking: false`, `skipLinkTracking: true` | Disable open and click tracking, or only click tracking, for this email |
208
+ | `customField` | Value matched by routing rules of type _Custom_ |
209
+ | `headers` | Up to 20 custom headers: `X-*` (except MagicMail's own) plus `List-Id`, `Precedence`, `Auto-Submitted`, `In-Reply-To`, `References`. Others are dropped |
210
+ | `attachments` | Up to 20, 25 MB each, with `filename`, `content`, `contentType`, `encoding`, `cid` |
211
+ | `userId`, `recipientName` | Stored on the email log |
418
212
 
419
- Click **"Create Template"** and use the visual designer to build your email.
213
+ ### Attachments
420
214
 
421
- **3. Add Variables with Mustache**
422
-
423
- Use Mustache syntax for dynamic content:
424
-
425
- ```html
426
- <h1>Welcome {{user.firstName}}!</h1>
427
- <p>Thanks for joining {{company.name}}</p>
215
+ ```javascript
216
+ await strapi
217
+ .plugin('email')
218
+ .service('email')
219
+ .send({
220
+ to: 'customer@example.com',
221
+ subject: 'Invoice 12345',
222
+ html: '<h1>Your invoice is attached</h1>',
223
+ attachments: [
224
+ {
225
+ filename: 'invoice.pdf',
226
+ content: await fs.promises.readFile(invoicePath), // read the file yourself
227
+ contentType: 'application/pdf',
228
+ },
229
+ { filename: 'receipt.pdf', content: pdfBase64, encoding: 'base64' },
230
+ ],
231
+ });
428
232
  ```
429
233
 
430
- **4. Save & Use**
234
+ Attachments must carry their `content` (a Buffer, or a string with `encoding`). Attachments with `path` or `href` are rejected for every provider, so MagicMail never reads server files or fetches URLs for a caller. Inline images use `cid`.
431
235
 
432
- Each template gets a unique **Reference ID** (e.g., `100`) that you use when sending.
236
+ ### Unsubscribe and bulk headers
433
237
 
434
- ### 💌 Sending Templated Emails
238
+ - `List-Unsubscribe` is sent for marketing emails that have an unsubscribe URL, and for every email type while **Settings → Unsubscribe header** is on and a URL is available.
239
+ - A per-email `unsubscribeUrl` wins. The plugin-wide URL from the settings is used only while the toggle is on; `{{email}}` in it is replaced with the URL-encoded recipient (single-recipient emails only).
240
+ - `List-Unsubscribe-Post: List-Unsubscribe=One-Click` (RFC 8058) is only added for `https` URLs.
241
+ - Marketing emails get `Precedence: bulk`; notification emails get `Auto-Submitted: auto-generated`.
435
242
 
436
- **Method 1: Using Template ID**
243
+ ---
437
244
 
438
- ```javascript
439
- // Send email using template
440
- await strapi.plugin('magic-mail').service('email-router').send({
441
- to: 'user@example.com',
442
- templateId: 100, // Your template reference ID
443
- templateData: {
444
- user: {
445
- firstName: 'John',
446
- lastName: 'Doe',
447
- email: 'john@example.com',
448
- },
449
- company: {
450
- name: 'ACME Corp',
451
- url: 'https://acme.com',
452
- },
453
- orderNumber: '12345',
454
- orderTotal: '$199.99',
455
- },
456
- });
245
+ ## Provider setup
457
246
 
458
- // ✅ MagicMail will:
459
- // - Load template #100
460
- // - Render it with your data (Mustache)
461
- // - Route via smart routing
462
- // - Send via best account
463
- ```
247
+ ### OAuth redirect URI
464
248
 
465
- **Method 2: Via Strapi Email Service**
249
+ MagicMail builds the redirect URI from the `URL` environment variable if it is set (legacy override), otherwise from Strapi's `server.url` (or `server.absoluteUrl`):
466
250
 
467
- ```javascript
468
- // Also works with standard Strapi email service!
469
- await strapi.plugin('email').service('email').send({
470
- to: 'user@example.com',
471
- templateId: 100,
472
- templateData: {
473
- user: { firstName: 'Jane' },
474
- resetUrl: 'https://yoursite.com/reset?token=xyz',
475
- },
476
- });
477
251
  ```
478
-
479
- ### 🎯 Template Categories
480
-
481
- Templates can be categorized for automatic routing:
482
-
483
- - **transactional** - Order confirmations, receipts
484
- - **marketing** - Newsletters, promotions
485
- - **notification** - System alerts, updates
486
- - **custom** - Anything else
487
-
488
- **Automatic Routing by Category:**
489
-
490
- ```javascript
491
- // Create routing rule: All marketing templates → SendGrid
492
- // Then just send:
493
- await strapi.plugin('magic-mail').service('email-router').send({
494
- to: 'subscriber@example.com',
495
- templateId: 200, // Marketing category template
496
- templateData: { offer: '50% OFF' },
497
- });
498
- // ✅ Automatically routes via SendGrid (based on category)
252
+ <public URL>/magic-mail/oauth/<gmail|microsoft|yahoo>/callback
499
253
  ```
500
254
 
501
- ### 🔄 Template Versioning
255
+ The account wizard shows the exact value the server uses; register exactly that value with the provider. In production the URL must be public `https`; there is no `localhost` fallback. The `localhost:1337` URIs below are for local development.
502
256
 
503
- Every time you save a template, a new version is created automatically:
257
+ The authorization popup reports back to the admin origin (`admin.url`). If the admin runs on its own host, set `admin.url` to that absolute URL. OAuth flows are bound to the admin who started them and kept in process memory, so several Strapi instances behind a load balancer need sticky admin sessions.
504
258
 
505
- ```javascript
506
- // Get all versions
507
- const versions = await strapi
508
- .plugin('magic-mail')
509
- .service('email-designer')
510
- .getVersions(templateId);
259
+ **Reconnecting an account** (revoked consent, expired refresh token): edit the account and use **Reconnect with &lt;provider&gt;** in the wizard. The provider must return the account's own mailbox. Access tokens are refreshed 60 seconds before they expire and once more if the provider rejects them; rotated refresh tokens are stored.
511
260
 
512
- // Restore old version
513
- await strapi
514
- .plugin('magic-mail')
515
- .service('email-designer')
516
- .restoreVersion(templateId, versionId);
517
- ```
518
-
519
- ### 📦 Import/Export Templates
261
+ ### Gmail (OAuth 2.0)
520
262
 
521
- **Export all templates:**
522
-
523
- ```javascript
524
- const templates = await strapi
525
- .plugin('magic-mail')
526
- .service('email-designer')
527
- .exportTemplates();
263
+ 1. In the [Google Cloud Console](https://console.cloud.google.com) create or select a project and enable the **Gmail API**.
264
+ 2. Create an **OAuth client ID** of type _Web application_.
265
+ 3. Add the redirect URI, for example `http://localhost:1337/magic-mail/oauth/gmail/callback`.
266
+ 4. In MagicMail choose **Gmail**, paste the client ID and secret, click **Connect with Google** and sign in.
528
267
 
529
- // Save to file
530
- fs.writeFileSync('templates.json', JSON.stringify(templates, null, 2));
531
- ```
268
+ Scopes: `https://www.googleapis.com/auth/gmail.send`, `https://www.googleapis.com/auth/userinfo.email`, `openid`. Mail is sent through the Gmail API.
532
269
 
533
- **Import templates:**
270
+ ### Microsoft 365 (OAuth 2.0)
534
271
 
535
- ```javascript
536
- const templates = JSON.parse(fs.readFileSync('templates.json'));
272
+ 1. In the [Azure portal](https://portal.azure.com) open **Microsoft Entra ID → App registrations → New registration**.
273
+ 2. Copy the **Application (client) ID** and the **Directory (tenant) ID**.
274
+ 3. Add a _Web_ redirect URI, for example `http://localhost:1337/magic-mail/oauth/microsoft/callback`.
275
+ 4. API permissions (Microsoft Graph, _Delegated_): `Mail.Send`, `User.Read`, `offline_access`, `openid`, `email`. Grant admin consent.
276
+ 5. Under **Certificates & secrets** create a client secret and copy its **Value** (not the secret ID).
277
+ 6. In MagicMail choose **Microsoft 365**, enter tenant ID, client ID and secret, click **Connect with Microsoft**.
537
278
 
538
- const results = await strapi
539
- .plugin('magic-mail')
540
- .service('email-designer')
541
- .importTemplates(templates);
279
+ The tenant ID must be a GUID, a domain, `common`, `organizations` or `consumers`. Mail is sent through Microsoft Graph `sendMail`.
542
280
 
543
- console.log(`Imported ${results.filter(r => r.success).length} templates`);
544
- ```
281
+ ### Yahoo Mail (OAuth 2.0)
545
282
 
546
- ### 🎨 Visual Designer Features
283
+ 1. Create an app at [developer.yahoo.com/apps](https://developer.yahoo.com/apps).
284
+ 2. Redirect URI, for example `http://localhost:1337/magic-mail/oauth/yahoo/callback`.
285
+ 3. Permissions: **Mail (read/write)** and **OpenID Connect** (scopes `openid email mail-w`).
286
+ 4. In MagicMail choose **Yahoo Mail**, paste client ID and secret and click **Connect with Yahoo**.
547
287
 
548
- **Powered by Unlayer Email Editor:**
288
+ Mail is sent over SMTP with XOAUTH2.
549
289
 
550
- - ✅ Drag & Drop Interface
551
- - ✅ Pre-built Content Blocks
552
- - ✅ Image Upload & Management
553
- - ✅ Custom Fonts & Colors
554
- - ✅ Responsive Design
555
- - ✅ Mobile Preview
556
- - ✅ HTML/Text Editor
557
- - ✅ Merge Tags Support
558
- - ✅ Save/Load Designs
290
+ ### SMTP
559
291
 
560
- ### 📊 Template Statistics
292
+ | Field | Example |
293
+ | ----------------- | ------------------------------------------------------------ |
294
+ | Host | `smtp.example.com` |
295
+ | Port | `587` (STARTTLS) or `465` (turn on _Use SSL/TLS encryption_) |
296
+ | Username/password | Mailbox credentials or an app password |
561
297
 
562
- ```javascript
563
- const stats = await strapi
564
- .plugin('magic-mail')
565
- .service('email-designer')
566
- .getStats();
567
-
568
- console.log(stats);
569
- // {
570
- // total: 15,
571
- // active: 12,
572
- // inactive: 3,
573
- // byCategory: [
574
- // { category: 'transactional', count: 8 },
575
- // { category: 'marketing', count: 4 },
576
- // { category: 'notification', count: 3 }
577
- // ],
578
- // maxTemplates: -1, // unlimited (all features are free)
579
- // remaining: -1 // unlimited
580
- // }
581
- ```
298
+ Common hosts: Gmail `smtp.gmail.com:587` (app password), Outlook.com `smtp-mail.outlook.com:587`, Microsoft 365 `smtp.office365.com:587`.
582
299
 
583
- ### 🚀 Best Practices
300
+ TLS 1.2 or newer is required and certificates are verified. For a local mail catcher (Mailpit, MailHog) set `"allowInsecure": true` in the account config; it is honoured outside `NODE_ENV=production`, or for loopback hosts.
584
301
 
585
- **1. Use Template Categories**
586
- ```javascript
587
- // Set category when creating template
588
- category: 'marketing' // Auto-routes to marketing account
589
- ```
302
+ Optional DKIM signing (account config):
590
303
 
591
- **2. Test Templates Before Using**
592
- ```javascript
593
- // Test render before sending
594
- const rendered = await strapi
595
- .plugin('magic-mail')
596
- .service('email-designer')
597
- .renderTemplate(100, { user: { firstName: 'Test' } });
598
-
599
- console.log(rendered.html); // Preview HTML
304
+ ```json
305
+ {
306
+ "dkim": {
307
+ "domainName": "example.com",
308
+ "keySelector": "default",
309
+ "privateKey": "-----BEGIN PRIVATE KEY-----\n..."
310
+ }
311
+ }
600
312
  ```
601
313
 
602
- **3. Version Control**
603
- ```javascript
604
- // Template version history is always enabled (free for everyone).
605
- ```
314
+ Generate a key with `openssl genrsa -out dkim_private.pem 2048` and publish the public key as a TXT record at `default._domainkey.example.com`.
606
315
 
607
- **4. Organize with Tags**
608
- ```javascript
609
- // Add tags to templates
610
- tags: ['onboarding', 'welcome-series', 'v2']
611
- ```
316
+ The provider value `nodemailer` is an alias of `smtp`.
612
317
 
613
- ---
318
+ ### SendGrid
614
319
 
615
- ## 🎯 Smart Routing Rules
320
+ 1. In SendGrid go to **Settings → API Keys** and create a key with the **Mail Send** permission.
321
+ 2. In MagicMail choose **SendGrid** and paste the key. The sender address must be verified in SendGrid.
616
322
 
617
- Create intelligent routing rules to send emails through specific accounts based on conditions.
323
+ ### Mailgun
618
324
 
619
- ### Example Use Cases
325
+ 1. In Mailgun copy your **Private API key** and your verified sending domain. Note the region the domain was created in (US or EU).
326
+ 2. In MagicMail choose **Mailgun**, enter the domain (for example `mg.example.com`), pick the region and paste the key.
620
327
 
621
- **1. Route Marketing Emails via SendGrid:**
622
- ```
623
- Match Type: Email Type
624
- Match Value: marketing
625
- Target Account: SendGrid Marketing
626
- Priority: 10
627
- ```
328
+ A Mailgun domain can only be used through the API host of its region. MagicMail stores the region as `config.region` and calls:
628
329
 
629
- **2. Route VIP Customers via Priority SMTP:**
630
- ```
631
- Match Type: Recipient
632
- Match Value: @vip-customers.com
633
- Target Account: Priority SMTP
634
- Fallback Account: Gmail OAuth
635
- Priority: 9
636
- ```
330
+ | `config.region` | API host |
331
+ | --------------- | ---------------------------- |
332
+ | `us` (default) | `https://api.mailgun.net` |
333
+ | `eu` | `https://api.eu.mailgun.net` |
637
334
 
638
- **3. Route Password Reset Emails:**
639
- ```
640
- Match Type: Subject
641
- Match Value: Password Reset
642
- Target Account: Transactional SMTP
643
- Priority: 8
644
- ```
335
+ Any other value is rejected with a validation error when the account is created or updated. Accounts saved without a region keep using the US host.
645
336
 
646
- ### Routing Priority
337
+ ### Provider comparison
647
338
 
648
- Rules are evaluated from **highest to lowest priority** (1-10):
649
- - Priority 10 = Evaluated first (most specific rules)
650
- - Priority 1 = Evaluated last (catch-all rules)
339
+ | Provider | Authentication | Transport | DKIM |
340
+ | ------------- | --------------------- | --------------------------- | -------------------- |
341
+ | Gmail | OAuth 2.0 | Gmail API | by Google |
342
+ | Microsoft 365 | OAuth 2.0 (Entra ID) | Microsoft Graph `sendMail` | by Microsoft |
343
+ | Yahoo Mail | OAuth 2.0 | SMTP with XOAUTH2 | by Yahoo |
344
+ | SMTP | Username and password | Nodemailer, any SMTP server | optional (see above) |
345
+ | SendGrid | API key | HTTPS API (v3) | by SendGrid |
346
+ | Mailgun | API key + domain | HTTPS API (US or EU region) | by Mailgun |
651
347
 
652
- If no rule matches, the **Primary Account** is used.
348
+ All providers support attachments, inline images (`cid`) and custom headers.
653
349
 
654
350
  ---
655
351
 
656
- ## 🔐 Provider Setup Guides
657
-
658
- ### Gmail OAuth Setup
352
+ ## Routing rules and quotas
659
353
 
660
- **1. Google Cloud Console**
661
- 1. Go to [console.cloud.google.com](https://console.cloud.google.com)
662
- 2. Create a new project or select existing
663
- 3. Enable Gmail API
664
- 4. Create OAuth 2.0 Client ID
665
- 5. Add redirect URI: `http://localhost:1337/magic-mail/oauth/gmail/callback`
666
- 6. Copy Client ID and Client Secret
354
+ Routing rules send an email through a specific account when it matches a condition. Manage them under **MagicMail → Routing Rules**.
667
355
 
668
- **2. In MagicMail**
669
- 1. Choose "Gmail OAuth" provider
670
- 2. Paste Client ID and Client Secret
671
- 3. Click "Connect with Google"
672
- 4. Authorize in popup
673
- 5. Done! ✅
356
+ | Match type | Matches when |
357
+ | -------------- | ----------------------------------------------------------------------------------------------------------- |
358
+ | **Email Type** | the email type equals the value (`transactional`, `marketing`, `notification`) |
359
+ | **Recipient** | any recipient matches: `@domain.com` = domain, `user@domain.com` = exact address, anything else = substring |
360
+ | **Subject** | the subject contains the value (case-insensitive) |
361
+ | **Template** | the template name or Reference ID equals the value |
362
+ | **Custom** | the email's `customField` equals the value |
674
363
 
675
- **Required Scopes:**
676
- - `https://www.googleapis.com/auth/gmail.send`
677
- - `https://www.googleapis.com/auth/userinfo.email`
364
+ Examples:
678
365
 
679
- ---
680
-
681
- ### Microsoft 365 OAuth Setup
682
-
683
- **1. Azure Portal**
684
- 1. Go to [portal.azure.com](https://portal.azure.com)
685
- 2. Navigate to **Azure Active Directory → App registrations**
686
- 3. Click **"New registration"**
687
- 4. Name: "MagicMail"
688
- 5. Account type: **"Accounts in this organizational directory only"**
689
- 6. Copy **Application (client) ID** AND **Directory (tenant) ID**
690
-
691
- **2. Add Redirect URI**
692
- ```
693
- http://localhost:1337/magic-mail/oauth/microsoft/callback
694
- ```
366
+ | Rule | Match | Account | Fallback | Priority |
367
+ | --------------------- | ---------------------------------- | ------------------ | ------------------ | -------- |
368
+ | Password resets first | Subject contains `Password Reset` | Transactional SMTP | Gmail Support | 1000000 |
369
+ | Marketing campaigns | Email type `marketing` | Marketing SendGrid | | 50 |
370
+ | VIP customers | Recipient `@vip-customers.example` | Gmail Support | Transactional SMTP | 10 |
695
371
 
696
- **3. API Permissions**
372
+ **Order.** Rules are checked from the highest to the lowest priority (1 to 1,000,000); rules with the same priority are checked oldest first. The first match wins. The admin list and `GET /magic-mail/routing-rules` show them in this order.
697
373
 
698
- Add these **Delegated permissions** under Microsoft Graph:
699
- - ✅ `Mail.Send` - Send emails as the signed-in user
700
- - ✅ `User.Read` - Read user profile
701
- - ✅ `offline_access` - Maintain access (refresh tokens)
702
- - ✅ `openid` - Sign users in
703
- - ✅ `email` - View users' email address
374
+ **Account order for one email.** The account of the first matching rule, then that rule's fallback account, then the accounts (and fallbacks) of the next matching rules, then the primary account, then the remaining active accounts by account priority (1 to 10, higher first). Accounts over their quota are skipped.
704
375
 
705
- **4. Grant Admin Consent** (Required!)
376
+ **Failover.** Errors that guarantee the provider did not accept the message (connection or authentication failures, missing credentials, HTTP 401/403/429/500/502/503, SMTP envelope rejection) move on to the next account. Ambiguous errors such as timeouts are returned to the caller instead, so an email is never delivered twice.
706
377
 
707
- **5. Create Client Secret**
708
- 1. **Certificates & secrets** → **New client secret**
709
- 2. Copy the **VALUE** (not the Secret ID)
710
- 3. Store it securely!
378
+ **Account references.** Rules reference accounts by name. Creating or editing a rule with an unknown account name returns 400. Renaming an account updates its rules; deleting an account deactivates the rules that target it and clears it where it is the fallback.
711
379
 
712
- **6. In MagicMail**
713
- 1. Choose "Microsoft OAuth" provider
714
- 2. Paste **Tenant ID**, **Client ID**, and **Client Secret**
715
- 3. Click "Connect with Microsoft"
716
- 4. Authorize
717
- 5. Done! ✅
380
+ **Quotas.** Each account can have an hourly and a daily limit (0 = unlimited). Quota is reserved atomically before dispatch, so concurrent sends cannot exceed it. Windows follow the clock hour and the calendar day in UTC; set `quotaTimezone` to use another IANA time zone.
718
381
 
719
382
  ---
720
383
 
721
- ### Yahoo Mail OAuth Setup
722
-
723
- **1. Yahoo Developer Console**
724
- 1. Go to [developer.yahoo.com/apps](https://developer.yahoo.com/apps)
725
- 2. Click **"Create an App"**
726
- 3. Fill in app details
727
-
728
- **2. Add Redirect URI**
729
- ```
730
- http://localhost:1337/magic-mail/oauth/yahoo/callback
731
- ```
384
+ ## Templates and the visual editor
732
385
 
733
- **3. API Permissions**
734
- - ✅ `Mail` - Send and manage emails
735
- - ✅ `OpenID Connect` - User authentication
386
+ Everything in the designer is included, with no tiers:
736
387
 
737
- **4. In MagicMail**
738
- 1. Choose "Yahoo Mail OAuth" provider
739
- 2. Paste Client ID and Client Secret
740
- 3. Click "Connect with Yahoo"
741
- 4. Done! ✅
388
+ | Feature | Included |
389
+ | --------------------------------- | -------- |
390
+ | **Visual Designer (Unlayer)** | ✅ |
391
+ | **Plain HTML/Text editing** | ✅ |
392
+ | **Unlimited Templates** | ✅ |
393
+ | **Mustache Variables** | ✅ |
394
+ | **Import / Export** | ✅ |
395
+ | **Template Versioning** | ✅ |
396
+ | **Users & Permissions templates** | ✅ |
742
397
 
743
- ---
398
+ ### Creating templates
744
399
 
745
- ### SMTP Setup (Universal)
400
+ 1. Go to **MagicMail → Email Templates** and click **Create template**.
401
+ 2. Build the email in the **Visual Designer** tab, or write HTML and text in the **Plain Text** tab.
402
+ 3. Use Mustache variables for dynamic content:
746
403
 
747
- **Works with ANY SMTP server:**
748
-
749
- ```
750
- Host: smtp.gmail.com (or your SMTP server)
751
- Port: 587 (STARTTLS) or 465 (SSL/TLS)
752
- Username: your-email@domain.com
753
- Password: your-password (or App Password)
754
- ```
404
+ ```html
405
+ <h1>Welcome {{user.firstName}}!</h1>
406
+ <p>Thanks for joining {{company.name}}</p>
407
+ ```
755
408
 
756
- **Popular SMTP Servers:**
757
- - **Gmail:** `smtp.gmail.com:587` (use App Password!)
758
- - **Outlook:** `smtp-mail.outlook.com:587`
759
- - **Office365:** `smtp.office365.com:587`
760
- - **Custom:** Your own mail server
409
+ 4. Save. Each template has a numeric **Reference ID** (for example `100`) that you use when sending. It cannot be changed after creation.
761
410
 
762
- **Gmail App Password:**
763
- 1. Google Account → Security → 2-Step Verification
764
- 2. App passwords → Generate
765
- 3. Use the 16-character password in MagicMail
411
+ The server fills `{{system.date}}`, `{{system.year}}` and `{{system.unsubscribe}}` (the email's unsubscribe URL); callers cannot override them.
766
412
 
767
- **Optional: DKIM Signing**
413
+ ### Sending a template
768
414
 
769
- For custom domains, you can add DKIM:
770
415
  ```javascript
771
- // In config field (advanced users):
772
- {
773
- dkim: {
774
- domainName: 'yourdomain.com',
775
- keySelector: 'default',
776
- privateKey: '-----BEGIN PRIVATE KEY-----\n...'
777
- }
778
- }
416
+ await strapi
417
+ .plugin('magic-mail')
418
+ .service('email-router')
419
+ .send({
420
+ to: 'user@example.com',
421
+ templateId: 100, // Reference ID
422
+ templateData: {
423
+ user: { firstName: 'Ada' },
424
+ company: { name: 'Example Shop' },
425
+ },
426
+ });
779
427
  ```
780
428
 
781
- ---
429
+ `strapi.plugin('email').service('email').send()` accepts the same `templateId` and `templateData` fields. The template list also shows a ready-made code snippet per template.
782
430
 
783
- ### SendGrid Setup
431
+ ### Categories
784
432
 
785
- **1. Get API Key**
786
- 1. Go to [app.sendgrid.com](https://app.sendgrid.com)
787
- 2. Settings → API Keys → Create API Key
788
- 3. Scope: **Mail Send** (Full Access)
789
- 4. Copy the API key
433
+ Templates have a category: `transactional`, `marketing`, `notification` or `custom`. The first three become the email `type` when the caller passes none, so a rule _Email Type = marketing_ also catches marketing templates.
790
434
 
791
- **2. In MagicMail**
792
- 1. Choose "SendGrid" provider
793
- 2. Paste API Key
794
- 3. Done! ✅
435
+ ### Versions, import and export
795
436
 
796
- ---
797
-
798
- ### Mailgun Setup
799
-
800
- **1. Get API Key & Domain**
801
- 1. Go to [app.mailgun.com](https://app.mailgun.com)
802
- 2. Settings → API Security → Copy **Private API Key**
803
- 3. Sending → Domains → Copy your verified domain
437
+ - Version 1 is the created template. Every save that changes the content (name, subject, design, HTML, text or tags) adds a version with the saved content. Saves that only toggle `isActive` or the category add none. Restoring a version saves its content as a new version.
438
+ - Export writes JSON (all templates, or the ones you select); import accepts MagicMail exports and exports of `strapi-plugin-email-designer-5`. The `isActive` state is kept.
804
439
 
805
- **2. In MagicMail**
806
- 1. Choose "Mailgun" provider
807
- 2. Enter **Domain** (e.g., `mg.yourdomain.com`)
808
- 3. Paste **Private API Key**
809
- 4. Done! ✅
810
-
811
- ---
812
-
813
- ## 🛡️ Security Features
814
-
815
- ### Encryption
816
- - ✅ **AES-256-GCM** encryption for all credentials
817
- - ✅ **No plain text passwords** in database
818
- - ✅ **OAuth tokens encrypted** at rest
819
- - ✅ **Secure token refresh** mechanism
820
-
821
- ### Transport Security
822
- - ✅ **TLS 1.2+ enforced** (SMTP)
823
- - ✅ **Certificate verification** enabled
824
- - ✅ **HTTPS only** for API providers
825
-
826
- ### Email Authentication
827
- - ✅ **DKIM signing** (optional for SMTP, automatic for OAuth)
828
- - ✅ **SPF validation** support
829
- - ✅ **DMARC compliance** - All providers pass DMARC checks
830
- - ✅ **Proper Message-ID** generation (RFC 5322)
831
-
832
- ### Content Security
833
- - ✅ **XSS prevention** - No `<script>` tags allowed
834
- - ✅ **Input validation** - Email format, subject length
835
- - ✅ **Spam trigger detection** - Warns about suspicious patterns
836
- - ✅ **MIME type validation**
837
-
838
- ### Compliance Headers
839
- - ✅ **List-Unsubscribe** (RFC 8058) - GDPR/CAN-SPAM compliant
840
- - ✅ **Auto-Submitted** (RFC 3834) - Prevents auto-responder loops
841
- - ✅ **Precedence: bulk** for marketing emails
842
- - ✅ **X-Priority** and **Importance** headers
843
-
844
- ---
845
-
846
- ## 📊 Features Overview
847
-
848
- ### Multi-Account Management
849
- - Unlimited email accounts
850
- - Provider-specific configuration
851
- - Active/Inactive status
852
- - Primary account designation
853
- - Per-account priority (1-10)
854
- - Daily and hourly rate limits
855
-
856
- ### Smart Routing Engine
857
- - **Email Type Routing** - transactional, marketing, notification
858
- - **Recipient Matching** - Route by email domain or address
859
- - **Subject Matching** - Route by subject keywords
860
- - **Template Matching** - Route by template name
861
- - **Custom Field Matching** - Flexible custom conditions
862
- - **Priority-based evaluation** - Higher priority rules match first
863
- - **Fallback accounts** - Automatic failover per rule
864
-
865
- ### Rate Limiting & Failover
866
- - Per-account daily limits
867
- - Per-account hourly limits
868
- - Automatic counter reset (hourly/daily)
869
- - Smart failover to backup accounts
870
- - Rate limit hit detection
871
- - Account health monitoring
872
-
873
- ### Analytics & Monitoring
874
- - Real-time statistics dashboard
875
- - Emails sent today (per account and total)
876
- - Total emails sent (lifetime)
877
- - Active/Inactive account count
878
- - Success/Failure tracking
879
- - Last used timestamps
880
- - Visual progress bars for rate limits
881
-
882
- ### Testing Features
883
- - **Test Direct** - Test specific account directly
884
- - **Test Strapi Service** - Verify Email Designer compatibility
885
- - **Priority Testing** - Test high/normal priority headers
886
- - **Marketing Email Testing** - Test List-Unsubscribe headers
887
- - **Detailed test results** - Shows all security features applied
888
-
889
- ---
890
-
891
- ## 🔧 Configuration
892
-
893
- ### Basic Setup (No .env Required!)
894
-
895
- All configuration is done via the Admin Panel. However, for production deployments, you can set:
896
-
897
- ```env
898
- # Optional - Base URL for OAuth callbacks
899
- URL=https://yourdomain.com
900
-
901
- # Optional - Encryption key (auto-generated if not provided)
902
- ENCRYPTION_KEY=your-32-character-secret-key
440
+ ```javascript
441
+ const designer = strapi.plugin('magic-mail').service('email-designer');
442
+ const versions = await designer.getVersions(templateDocumentId);
443
+ await designer.restoreVersion(templateDocumentId, versionDocumentId);
444
+ const exported = await designer.exportTemplates(); // or exportTemplates([documentId, ...])
445
+ const results = await designer.importTemplates(exported);
903
446
  ```
904
447
 
905
- ### Account Configuration
906
-
907
- Each account can have:
908
-
909
- **Basic Settings:**
910
- - Name (unique identifier)
911
- - Description
912
- - Provider type
913
- - From Email
914
- - From Name
915
- - Reply-To Email
448
+ ### Users & Permissions templates
916
449
 
917
- **Security:**
918
- - OAuth credentials (encrypted)
919
- - SMTP credentials (encrypted)
920
- - API keys (encrypted)
921
- - DKIM configuration (optional)
450
+ The **Core emails** tab edits the _Reset password_ and _Email address confirmation_ templates of Users & Permissions. This needs the _Users & Permissions → Email templates_ read/update permissions in addition to MagicMail's. Only `{{ URL }}`, `{{ SERVER_URL }}`, `{{ ADMIN_URL }}`, `{{ CODE }}`, `{{ TOKEN }}`, `{{ USER }}`, `{{ USER.email }}` and `{{ USER.username }}` are accepted; they are stored as `<%= KEY %>`, the syntax Users & Permissions fills in.
922
451
 
923
- **Rate Limiting:**
924
- - Daily email limit (0 = unlimited)
925
- - Hourly email limit (0 = unlimited)
926
- - Priority (1-10, higher = preferred)
452
+ ### The visual editor (Unlayer) and privacy
927
453
 
928
- **Status:**
929
- - Active/Inactive
930
- - Primary account designation
454
+ The visual designer embeds the [Unlayer](https://unlayer.com) editor through `react-email-editor`. What that means for you:
931
455
 
932
- ---
933
-
934
- ## 🎨 API Reference
456
+ - The editor script loads from `https://editor.unlayer.com/embed.js?2` and the editor build is pinned (`1.524.0`). Unlayer serves the editor, so the browser of each editor user contacts Unlayer's servers.
457
+ - No Unlayer `projectId` is sent unless you configure one.
458
+ - Disabled: Unlayer image hosting, stock images, the AI assistant, the hosted image editor, "preview in browser" and third-party placeholder images. New image blocks use an inline placeholder.
459
+ - Images you add are uploaded to the **Strapi Media Library** (`POST /upload`), so editors also need _Media Library → Create (upload)_.
460
+ - If the editor cannot load (CSP, offline, blocked), the page explains why and the **Plain Text** tab keeps working. You never need Unlayer to send templates.
935
461
 
936
- ### Send Email (Standard Method)
462
+ Optional overrides (`config/plugins.js`):
937
463
 
938
464
  ```javascript
939
- await strapi.plugin('email').service('email').send({
940
- to: 'recipient@example.com',
941
- from: 'sender@example.com', // Optional - uses account default
942
- subject: 'Email Subject',
943
- text: 'Plain text content',
944
- html: '<h1>HTML content</h1>',
945
-
946
- // Optional advanced options:
947
- replyTo: 'reply@example.com',
948
- type: 'transactional', // or 'marketing', 'notification'
949
- priority: 'normal', // or 'high'
950
- accountName: 'Specific Account', // Force specific account
951
- unsubscribeUrl: 'https://site.com/unsubscribe', // For marketing emails
952
-
953
- attachments: [
954
- {
955
- filename: 'document.pdf',
956
- path: './path/to/file.pdf',
957
- contentType: 'application/pdf',
465
+ module.exports = () => ({
466
+ 'magic-mail': {
467
+ config: {
468
+ designer: {
469
+ projectId: null, // your own Unlayer project id (positive integer)
470
+ scriptUrl: null, // https URL of a vetted, self-hosted copy of embed.js
471
+ version: null, // exact Unlayer build, e.g. '1.524.0'
472
+ },
958
473
  },
959
- ],
960
- });
961
- ```
962
-
963
- ### Send Email (Direct MagicMail API)
964
-
965
- ```javascript
966
- await strapi.plugin('magic-mail').service('email-router').send({
967
- to: 'user@example.com',
968
- subject: 'Test Email',
969
- html: '<p>Content</p>',
970
- accountName: 'Gmail OAuth', // Optional - force specific account
971
- type: 'transactional',
972
- priority: 'normal',
474
+ },
973
475
  });
974
476
  ```
975
477
 
976
- ### Get All Accounts
977
-
978
- ```javascript
979
- const accounts = await strapi.plugin('magic-mail').service('account-manager').getAllAccounts();
980
- ```
981
-
982
- ### Test Account
983
-
984
- ```javascript
985
- const result = await strapi.plugin('magic-mail').service('account-manager').testAccount(
986
- accountId,
987
- 'test@example.com',
988
- {
989
- priority: 'high',
990
- type: 'marketing',
991
- unsubscribeUrl: 'https://example.com/unsubscribe',
992
- }
993
- );
994
- ```
995
-
996
- ---
997
-
998
- ## 🧪 Testing Your Setup
999
-
1000
- ### Test via Admin UI
1001
-
1002
- 1. Go to **Email Accounts**
1003
- 2. Click **Test** button on any account
1004
- 3. Enter recipient email
1005
- 4. Select test options:
1006
- - **Priority:** Normal / High
1007
- - **Email Type:** Transactional / Marketing / Notification
1008
- - **Unsubscribe URL:** (for marketing emails)
1009
- 5. Choose test method:
1010
- - **Test Direct** - Direct send via account
1011
- - **Test Strapi Service** - Via Strapi email service (tests Email Designer compatibility)
478
+ ### Content Security Policy for the visual editor
1012
479
 
1013
- ### Test via Code
480
+ Strapi's `strapi::security` middleware blocks third-party scripts and frames. To use the visual designer, allow the Unlayer origin in `config/middlewares.js`. Do **not** add `'unsafe-inline'` or `'unsafe-eval'`.
1014
481
 
1015
482
  ```javascript
1016
- // Test email with all security features
1017
- await strapi.plugin('magic-mail').service('account-manager').testAccount(
1018
- accountId,
1019
- 'your-email@example.com',
483
+ module.exports = [
484
+ 'strapi::logger',
485
+ 'strapi::errors',
1020
486
  {
1021
- priority: 'high',
1022
- type: 'marketing',
1023
- unsubscribeUrl: 'https://yoursite.com/unsubscribe',
1024
- }
1025
- );
487
+ name: 'strapi::security',
488
+ config: {
489
+ contentSecurityPolicy: {
490
+ useDefaults: true,
491
+ directives: {
492
+ 'script-src': ["'self'", 'https://editor.unlayer.com'],
493
+ 'frame-src': ["'self'", 'https://editor.unlayer.com'],
494
+ // Media Library images shown in the editor and the preview:
495
+ 'img-src': ["'self'", 'data:', 'blob:', 'https://your-media-host.example'],
496
+ },
497
+ },
498
+ },
499
+ },
500
+ 'strapi::cors',
501
+ 'strapi::poweredBy',
502
+ 'strapi::query',
503
+ 'strapi::body',
504
+ 'strapi::session',
505
+ 'strapi::favicon',
506
+ 'strapi::public',
507
+ ];
1026
508
  ```
1027
509
 
1028
- ### Verify Headers
510
+ With `designer.scriptUrl` (self-hosted `embed.js`) put that origin into `script-src` instead; the editor iframe still loads from `editor.unlayer.com`, so keep it in `frame-src`. The Unlayer script runs in the admin origin. Only allow it if you accept that trust.
1029
511
 
1030
- **To verify security headers are applied:**
1031
-
1032
- **In Gmail:**
1033
- 1. Open email → Click ⋮ (three dots)
1034
- 2. "Show original"
1035
- 3. Look for:
1036
- ```
1037
- X-Mailer: MagicMail/1.0
1038
- X-Priority: 1 (Highest)
1039
- Importance: high
1040
- List-Unsubscribe: <https://...>
1041
- Message-ID: <...>
1042
- DKIM-Signature: ...
1043
- ```
512
+ ---
1044
513
 
1045
- **In Outlook/Microsoft:**
1046
- 1. Email → File → Properties
1047
- 2. View "Internet headers"
1048
- 3. Should show "High Importance" with red exclamation mark
514
+ ## Tracking, privacy and retention
1049
515
 
1050
- ---
516
+ Open tracking adds a 1×1 pixel; click tracking rewrites `<a href>` links to a redirect. Both are on by default (**Settings → MagicMail → Plugin Settings**) and can be turned off per email with `enableTracking: false` or `skipLinkTracking: true`.
1051
517
 
1052
- ## 🔒 Security Compliance
518
+ **Public URL.** Tracking URLs use the _Tracking base URL_ setting, then `server.url`, then `server.absoluteUrl`, plus your REST prefix (`api.rest.prefix`, default `/api`), for example `https://cms.example.com/api/magic-mail/track/click/...`. In production a loopback host (`localhost`, `127.0.0.1`, `0.0.0.0`) is never used: without a public URL, emails are sent **untracked** and one warning is logged.
1053
519
 
1054
- ### Email Authentication
520
+ **Never tracked.** Authentication and one-time-token links (password reset, confirmation, magic links, OAuth, parameters such as `token`, `code`, `key`, `signature`, `session`, long random values), `mailto:` and `tel:` links, and links beyond `maxLinksPerEmail` (100). Emails sent through the Content API are click-tracked only for hosts in `tracking.allowedLinkHosts` (none by default).
1055
521
 
1056
- | Standard | Status | Description |
1057
- |----------|--------|-------------|
1058
- | **DKIM** | ✅ | DomainKeys Identified Mail - Signature validation |
1059
- | **SPF** | ✅ | Sender Policy Framework - Sender verification |
1060
- | **DMARC** | ✅ | Domain-based Authentication - Policy enforcement |
1061
- | **TLS 1.2+** | ✅ | Transport Layer Security - Encrypted transmission |
1062
- | **AES-256-GCM** | ✅ | Credential encryption at rest |
522
+ **What is stored.** Open and click events store the IP truncated to /24 (IPv4) or /48 (IPv6) and at most 512 characters of user agent; no location. Only Koa's `request.ip` is used, so behind a reverse proxy enable [`server.proxy`](https://docs.strapi.io/cms/configurations/server), otherwise every recipient shares the proxy's address.
1063
523
 
1064
- ### Regulatory Compliance
524
+ **Retention.** Every 6 hours MagicMail deletes events older than `tracking.eventRetentionDays` (90) and tracked links older than `tracking.linkTtlDays` (180; expired links then show the fallback page). `tracking.logRetentionDays` optionally deletes old email logs with their events and links. Logs contain personal data (recipient addresses); you can also delete them with **Analytics → Clear All** or with `DELETE /magic-mail/analytics/emails?olderThan=<ISO date>`.
1065
525
 
1066
- | Regulation | Status | Implementation |
1067
- |------------|--------|----------------|
1068
- | **GDPR** | ✅ | List-Unsubscribe header for marketing emails |
1069
- | **CAN-SPAM** | ✅ | One-click unsubscribe mechanism (RFC 8058) |
1070
- | **RFC 5322** | ✅ | Proper email format and headers |
1071
- | **RFC 3834** | ✅ | Auto-Submitted header prevents loops |
1072
- | **RFC 2156** | ✅ | Priority and Importance headers |
526
+ **Rate limit.** The tracking routes allow 300 requests per minute and IP. Above that, opens and clicks are no longer recorded but recipients still get the pixel and the redirect; only above 3,000 per minute is a plain-text 429 returned.
1073
527
 
1074
- ### Security Validation
528
+ **Signing.** Tracking hashes are signed with `MAGIC_MAIL_TRACKING_SECRET`, falling back to `MAGIC_MAIL_ENCRYPTION_KEY`. To rotate, keep the old values in `MAGIC_MAIL_TRACKING_SECRET_PREVIOUS` (comma-separated) while old emails matter.
1075
529
 
1076
- **Every email is validated for:**
1077
- - ✅ Valid recipient email format
1078
- - ✅ Non-empty subject line
1079
- - ✅ Subject length (warns if > 200 chars)
1080
- - ✅ Content presence (text or html required)
1081
- - ✅ No `<script>` tags in HTML
1082
- - ✅ No `javascript:` protocol in links
1083
- - ✅ Spam trigger detection (warns only)
530
+ Mention open and click tracking in your privacy notice.
1084
531
 
1085
532
  ---
1086
533
 
1087
- ## 📈 Analytics & Monitoring
1088
-
1089
- ### Dashboard Stats
534
+ ## Content API
1090
535
 
1091
- **Global Stats:**
1092
- - Emails sent today (all accounts)
1093
- - Total emails sent (lifetime)
1094
- - Active accounts count
536
+ | Method | Path | Auth | Purpose |
537
+ | ------ | --------------------------------------------------------------- | --------- | ----------------------------------------------------------------------------- |
538
+ | `POST` | `/api/magic-mail/send` | API token | Send through the router with all [send options](#send-options) |
539
+ | `POST` | `/api/magic-mail/send-message` | API token | Simple email: `to`, `cc`, `bcc`, `subject`, `message`/`text`/`html`, template |
540
+ | `GET` | `/api/magic-mail/track/open/:emailId/:recipientHash` | public | Open-tracking pixel |
541
+ | `GET` | `/api/magic-mail/track/click/:emailId/:linkHash/:recipientHash` | public | Click redirect |
1095
542
 
1096
- **Per-Account Stats:**
1097
- - Emails sent today
1098
- - Total emails sent
1099
- - Rate limit usage (visual progress bars)
1100
- - Last used timestamp
1101
- - Success/failure rate
543
+ ### The API token rule
1102
544
 
1103
- ### Email Logging
545
+ The send routes accept **API tokens only**. Create a _Custom_ token under **Settings → API Tokens** that allows only **Magic-mail → controller → `send`** (and `sendMessage` if you use it). Never enable these actions for the _Public_ or _Authenticated_ role: a users-permissions request is rejected with 403 even if the role has the permission.
1104
546
 
1105
- Every email is logged with:
1106
- - Recipient
1107
- - Sender
1108
- - Subject
1109
- - Account used
1110
- - Status (sent/failed)
1111
- - Timestamp
1112
- - Email type
1113
- - Error message (if failed)
547
+ To let users-permissions roles send as well (the pre-4.0 behaviour):
1114
548
 
1115
- Access logs via:
1116
549
  ```javascript
1117
- const logs = await strapi.entityService.findMany('plugin::magic-mail.email-log', {
1118
- filters: { status: 'sent' },
1119
- sort: { createdAt: 'desc' },
1120
- limit: 100,
550
+ // config/plugins.js
551
+ module.exports = () => ({
552
+ 'magic-mail': {
553
+ config: { contentApi: { allowUserRoles: true } },
554
+ },
1121
555
  });
1122
556
  ```
1123
557
 
1124
- ---
1125
-
1126
- ## 🔄 Integration with Email Designer
1127
-
1128
- **MagicMail is fully compatible with [strapi-plugin-email-designer-5](https://www.npmjs.com/package/strapi-plugin-email-designer-5)!**
1129
-
1130
- ### How It Works
1131
-
1132
- 1. Install Email Designer:
1133
- ```bash
1134
- npm install strapi-plugin-email-designer-5
1135
- ```
1136
-
1137
- 2. Create email templates in Email Designer
1138
-
1139
- 3. Send via template:
1140
- ```javascript
1141
- // Email Designer internally uses strapi.plugin('email').service('email').send()
1142
- // MagicMail automatically intercepts it!
1143
- ```
1144
-
1145
- 4. Your email is automatically:
1146
- - ✅ Routed through MagicMail's smart routing
1147
- - ✅ Sent via the best account (based on rules)
1148
- - ✅ Rate limited and failover protected
1149
- - ✅ Logged and tracked
1150
-
1151
- **No configuration needed - it just works!** 🎉
558
+ With user roles allowed, the email log is always attributed to the authenticated user and a body `userId` is ignored. With an API token, `userId` must be the id or documentId of an existing users-permissions user (else 400).
1152
559
 
1153
- ---
1154
-
1155
- ## 🚨 Troubleshooting
560
+ The body is validated strictly: unknown fields, `path`/`href` attachments and invalid addresses return 400. Both send routes share a budget of 60 requests per minute per token (in process memory, so each Strapi instance counts separately).
1156
561
 
1157
- ### Email Not Received
562
+ ### Example: send
1158
563
 
1159
- **1. Check Logs**
1160
564
  ```bash
1161
- # Look for errors in Strapi console
1162
- [magic-mail] ❌ Email send failed: ...
565
+ curl -X POST https://cms.example.com/api/magic-mail/send \
566
+ -H "Authorization: Bearer $MAGIC_MAIL_API_TOKEN" \
567
+ -H "Content-Type: application/json" \
568
+ -d '{
569
+ "to": "customer@example.com",
570
+ "templateId": 100,
571
+ "templateData": { "user": { "firstName": "Ada" } },
572
+ "type": "transactional"
573
+ }'
574
+ ```
575
+
576
+ ```json
577
+ {
578
+ "success": true,
579
+ "accountUsed": "Transactional SMTP",
580
+ "messageId": "<…@example.com>",
581
+ "emailId": "…"
582
+ }
1163
583
  ```
1164
584
 
1165
- **2. Check Account Status**
1166
- - Is the account marked as "Active"?
1167
- - Test the account using the Test button
1168
- - Check rate limits haven't been exceeded
1169
-
1170
- **3. Check Spam Folder**
1171
- - First emails from new accounts may land in spam
1172
- - Check spam/junk folders
1173
- - Mark as "Not Spam" to train filters
1174
-
1175
- **4. Verify Credentials**
1176
- - SMTP: Username and password correct?
1177
- - Gmail: Using App Password (not regular password)?
1178
- - OAuth: Tokens may have expired - reconnect
585
+ ### Example: send-message
1179
586
 
1180
- ### DMARC Failures
1181
-
1182
- **For Custom Domains:**
1183
- 1. Set up SPF record:
1184
- ```
1185
- v=spf1 include:_spf.google.com ~all
1186
- ```
587
+ ```bash
588
+ curl -X POST https://cms.example.com/api/magic-mail/send-message \
589
+ -H "Authorization: Bearer $MAGIC_MAIL_API_TOKEN" \
590
+ -H "Content-Type: application/json" \
591
+ -d '{ "to": "customer@example.com", "subject": "Your code", "message": "Your code is 123456." }'
592
+ ```
1187
593
 
1188
- 2. Set up DKIM:
1189
- - Generate DKIM key pair
1190
- - Add public key to DNS
1191
- - Add private key to MagicMail SMTP config
594
+ The response has the same shape as `/send`.
1192
595
 
1193
- 3. Set up DMARC:
1194
- ```
1195
- v=DMARC1; p=quarantine; rua=mailto:dmarc@yourdomain.com
1196
- ```
596
+ ### Errors
1197
597
 
1198
- **For OAuth Providers (Gmail, Microsoft, Yahoo):**
1199
- - ✅ DKIM handled automatically
1200
- - ✅ SPF handled automatically
1201
- - ✅ DMARC should pass automatically
598
+ Errors use Strapi's envelope:
1202
599
 
1203
- ### Rate Limit Errors
600
+ ```json
601
+ {
602
+ "data": null,
603
+ "error": { "status": 400, "name": "ValidationError", "message": "…", "details": {} }
604
+ }
605
+ ```
1204
606
 
1205
- **If you hit rate limits:**
1206
- - Increase daily/hourly limits in account settings
1207
- - Add more accounts for load balancing
1208
- - Set up routing rules to distribute load
1209
- - Check provider-specific limits:
1210
- - Gmail: 500/day (free), 2000/day (workspace)
1211
- - SendGrid: Varies by plan
1212
- - Mailgun: Varies by plan
607
+ | Status | When |
608
+ | ------ | --------------------------------------------------------------------------------------------------- |
609
+ | 400 | Invalid or unknown fields, invalid addresses, `userId` of a missing user |
610
+ | 401 | Invalid or expired token |
611
+ | 403 | No token, a token without the action, or a users-permissions caller while `allowUserRoles` is false |
612
+ | 429 | More than 60 send requests per minute (`TooManyRequestsError`, `Retry-After` header) |
613
+ | 500 | Delivery failed (no account could send, or the provider rejected the email) |
1213
614
 
1214
- ### OAuth Connection Issues
615
+ ---
1215
616
 
1216
- **Gmail OAuth:**
1217
- - Ensure Gmail API is enabled
1218
- - Check redirect URI matches exactly
1219
- - Clear browser cookies and try again
617
+ ## Admin API
1220
618
 
1221
- **Microsoft OAuth:**
1222
- - Verify all 5 permissions are granted
1223
- - **Grant admin consent** (required!)
1224
- - Use correct Tenant ID
1225
- - Check Client Secret hasn't expired
619
+ All admin routes live under `/magic-mail` on the Strapi server (not `/api`), require an authenticated administrator (admin JWT) and the capability shown. The admin UI uses them; you can call them with an admin token for automation.
1226
620
 
1227
- **Yahoo OAuth:**
1228
- - Verify app is approved by Yahoo
1229
- - Check API permissions are enabled
621
+ | Area | Routes | Capability |
622
+ | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
623
+ | Accounts | `GET /accounts`, `GET /accounts/:accountId` | `read` |
624
+ | | `POST /accounts`, `PUT /accounts/:accountId`, `DELETE /accounts/:accountId` | `manage` |
625
+ | | `POST /accounts/:accountId/test`, `POST /test-strapi-service` | `test-send` |
626
+ | Routing rules | `GET /routing-rules`, `GET /routing-rules/:ruleId` | `read` |
627
+ | | `POST /routing-rules`, `PUT /routing-rules/:ruleId`, `DELETE /routing-rules/:ruleId` | `manage` |
628
+ | OAuth | `GET /oauth/<gmail\|microsoft\|yahoo>/auth`, `POST /oauth/create-account`, `POST /oauth/reconnect/:accountId` | `manage` |
629
+ | | `GET /oauth/<provider>/callback` | public, signed single-use state |
630
+ | Templates | `GET /designer/templates`, `GET /designer/templates/:id`, `GET /designer/templates/:id/versions`, `GET /designer/templates/:id/download`, `POST /designer/render/:templateReferenceId`, `POST /designer/export`, `GET /designer/stats`, `GET /designer/editor-config` | `read` |
631
+ | | `POST /designer/templates`, `PUT`/`DELETE /designer/templates/:id`, `POST /designer/templates/:id/duplicate`, `POST /designer/import`, version restore/delete routes | `manage` |
632
+ | | `POST /designer/templates/:id/test-send` | `test-send` |
633
+ | | `GET`/`PUT /designer/core/:coreEmailType` | `read`/`manage` + users-permissions email-template permission |
634
+ | Analytics | `GET /analytics/stats`, `GET /analytics/emails`, `GET /analytics/emails/:emailId`, `GET /analytics/users/:userId` | `read` |
635
+ | | `DELETE /analytics/emails/:emailId`, `DELETE /analytics/emails` | `manage` |
636
+ | | `GET /analytics/debug` (only with `MAGIC_MAIL_ENABLE_DEBUG_ENDPOINT=true`, never in production) | `read` |
637
+ | Settings | `GET /settings` / `PUT /settings` | `read` / `manage` |
1230
638
 
1231
- ---
639
+ Test sends (`/accounts/:accountId/test`, `/test-strapi-service`, `/designer/templates/:id/test-send`) are limited to 10 per minute per admin.
1232
640
 
1233
- ## 🏗️ Architecture
641
+ ### Example: create a routing rule
1234
642
 
1235
- ### Plugin Structure
643
+ ```http
644
+ POST /magic-mail/routing-rules
645
+ Authorization: Bearer <admin JWT>
646
+ Content-Type: application/json
1236
647
 
648
+ {
649
+ "name": "Marketing campaigns",
650
+ "matchType": "emailType",
651
+ "matchValue": "marketing",
652
+ "accountName": "Marketing SendGrid",
653
+ "fallbackAccountName": "Transactional SMTP",
654
+ "priority": 50,
655
+ "isActive": true
656
+ }
1237
657
  ```
1238
- magic-mail/
1239
- ├── admin/ # React Admin UI
1240
- │ └── src/
1241
- │ ├── components/
1242
- │ │ ├── AddAccountModal.jsx # Multi-step account setup
1243
- │ │ └── PluginIcon.jsx # Sidebar icon
1244
- │ └── pages/
1245
- │ ├── App.jsx # Main app with tabs
1246
- │ ├── HomePage.jsx # Email accounts page
1247
- │ └── RoutingRules.jsx # Routing rules page
1248
- ├── server/ # Backend
1249
- │ └── src/
1250
- │ ├── bootstrap.js # Overrides Strapi email service
1251
- │ ├── controllers/
1252
- │ │ ├── accounts.js # CRUD for accounts
1253
- │ │ ├── oauth.js # OAuth flows
1254
- │ │ └── routing-rules.js # CRUD for routing rules
1255
- │ ├── services/
1256
- │ │ ├── email-router.js # Core routing logic
1257
- │ │ ├── account-manager.js # Account management
1258
- │ │ └── oauth.js # OAuth token exchange
1259
- │ ├── content-types/
1260
- │ │ ├── email-account/ # Account schema
1261
- │ │ ├── email-log/ # Email log schema
1262
- │ │ └── routing-rule/ # Routing rule schema
1263
- │ └── utils/
1264
- │ └── encryption.js # AES-256-GCM encryption
1265
- └── package.json
1266
- ```
1267
-
1268
- ### Data Flow
1269
658
 
659
+ ```json
660
+ {
661
+ "data": {
662
+ "documentId": "…",
663
+ "name": "Marketing campaigns",
664
+ "matchType": "emailType",
665
+ "priority": 50,
666
+ "…": "…"
667
+ },
668
+ "message": "Routing rule created successfully"
669
+ }
1270
670
  ```
1271
- Email Request
1272
- ↓
1273
- Strapi Email Service (intercepted!)
1274
- ↓
1275
- MagicMail Email Router
1276
- ↓
1277
- Routing Rules Engine
1278
- ↓
1279
- Account Selection (priority/rules)
1280
- ↓
1281
- Rate Limit Check
1282
- ↓
1283
- Provider-Specific Sender
1284
- ↓
1285
- Email Log & Stats Update
1286
- ↓
1287
- Response
1288
- ```
1289
-
1290
- ---
1291
671
 
1292
- ## 🎯 Use Cases
672
+ `matchType` is one of `emailType`, `recipient`, `subject`, `template`, `custom`. An unknown `accountName` or `fallbackAccountName` returns 400.
1293
673
 
1294
- ### Multi-Tenant SaaS
674
+ ### Example: list accounts
1295
675
 
1296
- ```javascript
1297
- // Each tenant gets their own email account
1298
- // Route by custom field:
1299
-
1300
- // Routing Rule:
1301
- // Match Type: Custom Field
1302
- // Match Value: tenant-123
1303
- // Target Account: Tenant 123 Gmail
1304
-
1305
- await strapi.plugin('email').service('email').send({
1306
- to: 'customer@example.com',
1307
- subject: 'Order Confirmation',
1308
- html: '<h1>Order #456</h1>',
1309
- customField: 'tenant-123', // Matches routing rule
1310
- });
676
+ ```http
677
+ GET /magic-mail/accounts
1311
678
  ```
1312
679
 
1313
- ### High-Volume Email Sending
1314
-
1315
- ```javascript
1316
- // Distribute load across multiple accounts
1317
- // Account 1: Priority 10, Limit 500/day
1318
- // Account 2: Priority 9, Limit 500/day
1319
- // Account 3: Priority 8, Limit 500/day
1320
- // → Total: 1500 emails/day with automatic load balancing!
680
+ ```json
681
+ {
682
+ "data": [
683
+ {
684
+ "documentId": "…",
685
+ "name": "Transactional SMTP",
686
+ "provider": "smtp",
687
+ "fromEmail": "noreply@example.com",
688
+ "isActive": true,
689
+ "isPrimary": true,
690
+ "priority": 9,
691
+ "dailyLimit": 500,
692
+ "hourlyLimit": 0,
693
+ "emailsSentToday": 128,
694
+ "totalEmailsSent": 18342
695
+ }
696
+ ],
697
+ "meta": { "count": 1 }
698
+ }
1321
699
  ```
1322
700
 
1323
- ### Marketing Campaigns
701
+ Credentials are never returned. `GET /accounts/:accountId` returns `********` for each stored secret; send that exact value back in `PUT` to keep a secret. Changing provider, `host`, `port`, `user`, `clientId`, `tenantId`, `domain` or `allowInsecure` requires the secrets again (400 otherwise, with the fields in `details.config`).
1324
702
 
1325
- ```javascript
1326
- // Route marketing emails via SendGrid
1327
- // Routing Rule:
1328
- // Match Type: Email Type
1329
- // Match Value: marketing
1330
- // Target Account: SendGrid Marketing
703
+ ### Example: delivery log
1331
704
 
1332
- await strapi.plugin('email').service('email').send({
1333
- to: 'subscriber@example.com',
1334
- subject: 'Weekly Newsletter',
1335
- html: template,
1336
- type: 'marketing',
1337
- unsubscribeUrl: 'https://yoursite.com/unsubscribe?id=123',
1338
- });
1339
- // ✅ Automatically routes via SendGrid
1340
- // ✅ Adds List-Unsubscribe header
1341
- // ✅ GDPR/CAN-SPAM compliant
705
+ ```http
706
+ GET /magic-mail/analytics/emails?search=order&deliveryStatus=sent&page=1&pageSize=25
1342
707
  ```
1343
708
 
1344
- ### VIP Customer Emails
1345
-
1346
- ```javascript
1347
- // Route VIP customers via a priority account
1348
- // Routing Rule:
1349
- // Match Type: Recipient
1350
- // Match Value: @vip-domain.com
1351
- // Target Account: Priority SMTP
1352
- // Fallback Account: Gmail OAuth
1353
-
1354
- await strapi.plugin('email').service('email').send({
1355
- to: 'ceo@vip-domain.com',
1356
- subject: 'Exclusive Offer',
1357
- html: content,
1358
- priority: 'high',
1359
- });
1360
- // ✅ Routes via Priority SMTP
1361
- // ✅ High priority headers added
1362
- // ✅ Automatic fallback if the priority account is unavailable
709
+ ```json
710
+ {
711
+ "success": true,
712
+ "data": [
713
+ {
714
+ "emailId": "…",
715
+ "recipient": "user01@example.com",
716
+ "subject": "Your order #1001 has shipped",
717
+ "deliveryStatus": "sent",
718
+ "openCount": 0,
719
+ "clickCount": 0
720
+ }
721
+ ],
722
+ "pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 1 }
723
+ }
1363
724
  ```
1364
725
 
1365
- ---
1366
-
1367
- ## 🔌 Email Provider Comparison
1368
-
1369
- | Feature | Gmail OAuth | Microsoft OAuth | Yahoo OAuth | SMTP | SendGrid | Mailgun |
1370
- |---------|-------------|-----------------|-------------|------|----------|---------|
1371
- | **Auth Method** | OAuth 2.0 | OAuth 2.0 | OAuth 2.0 | Password | API Key | API Key |
1372
- | **Free Tier** | 500/day | Varies | Limited | Varies | 100/day | 5000/month |
1373
- | **Attachments** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
1374
- | **HTML Emails** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
1375
- | **DKIM Auto** | ✅ | ✅ | ✅ | Optional | ✅ | ✅ |
1376
- | **Custom Headers** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
1377
- | **Priority Headers** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
1378
- | **Security** | Excellent | Excellent | Good | Good | Excellent | Excellent |
1379
- | **Deliverability** | Excellent | Excellent | Good | Varies | Excellent | Excellent |
1380
- | **Best For** | Personal/Small | Enterprise | Personal | Custom | Marketing | Bulk/Dev |
726
+ `search` looks in recipient, subject and template name; `pageSize` is capped at 100.
1381
727
 
1382
728
  ---
1383
729
 
1384
- ## 🌍 Production Deployment
1385
-
1386
- ### Environment Variables
730
+ ## Permissions
1387
731
 
1388
- ```env
1389
- # Base URL (required for OAuth callbacks in production)
1390
- URL=https://yourdomain.com
1391
-
1392
- # Database (Strapi default)
1393
- DATABASE_CLIENT=postgres
1394
- DATABASE_HOST=127.0.0.1
1395
- DATABASE_PORT=5432
1396
- DATABASE_NAME=strapi
1397
- DATABASE_USERNAME=strapi
1398
- DATABASE_PASSWORD=password
1399
-
1400
- # Required in production. Keep this stable across deploys and backups.
1401
- # Generate: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
1402
- MAGIC_MAIL_ENCRYPTION_KEY=your-generated-secret-with-at-least-32-bytes
1403
-
1404
- # Optional dedicated OAuth-state signing secret. APP_KEYS is used otherwise.
1405
- MAGIC_MAIL_OAUTH_STATE_SECRET=your-generated-secret-with-at-least-32-bytes
1406
- ```
732
+ MagicMail registers four admin actions under **Settings → Administration Panel → Roles → Plugins → MagicMail**:
1407
733
 
1408
- ### OAuth Callback URLs
734
+ | Action | Grants |
735
+ | ------------------------------ | --------------------------------------------------------------------------------------------------------------- |
736
+ | `plugin::magic-mail.access` | The MagicMail menu entry and page. No API access by itself |
737
+ | `plugin::magic-mail.read` | Read accounts, routing rules, templates, analytics and settings; render and export templates; the settings link |
738
+ | `plugin::magic-mail.manage` | Create, update and delete accounts, rules, templates, logs and settings; OAuth setup |
739
+ | `plugin::magic-mail.test-send` | Send test emails (account test, Strapi-service test, template test); 10 per minute per admin |
1409
740
 
1410
- **Update redirect URIs in your OAuth apps:**
741
+ Typical roles:
1411
742
 
1412
- **Gmail:**
1413
- ```
1414
- https://yourdomain.com/magic-mail/oauth/gmail/callback
1415
- ```
743
+ - **Read-only:** `access` + `read`
744
+ - **Editor:** `access` + `read` + `manage`
745
+ - **Tester:** `access` + `read` + `test-send`
1416
746
 
1417
- **Microsoft:**
1418
- ```
1419
- https://yourdomain.com/magic-mail/oauth/microsoft/callback
1420
- ```
747
+ Editing the Users & Permissions templates also needs the users-permissions email-template permissions; uploading images in the visual editor needs _Media Library → Create (upload)_. Details: [docs/permission-migration.md](./docs/permission-migration.md).
1421
748
 
1422
- **Yahoo:**
1423
- ```
1424
- https://yourdomain.com/magic-mail/oauth/yahoo/callback
1425
- ```
749
+ ---
1426
750
 
1427
- ### Recommended Provider Settings
751
+ ## Configuration reference
1428
752
 
1429
- **For Production Apps:**
753
+ ### Environment variables
1430
754
 
1431
- | Use Case | Recommended Provider | Configuration |
1432
- |----------|---------------------|---------------|
1433
- | **Transactional** | SendGrid or Mailgun | High daily limit, dedicated IP |
1434
- | **Marketing** | SendGrid | List management, analytics |
1435
- | **Notifications** | SMTP or Gmail OAuth | Medium limits, reliable |
1436
- | **Internal** | Microsoft 365 | Company domain, OAuth |
1437
- | **Development** | Mailgun Sandbox | Free testing |
755
+ | Variable | Required | Purpose |
756
+ | ------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------- |
757
+ | `MAGIC_MAIL_ENCRYPTION_KEY` | yes, unless `NODE_ENV` is unset, `development` or `test` | Credential encryption key (32+ bytes). Keep it stable and backed up |
758
+ | `MAGIC_MAIL_ENCRYPTION_KEY_PREVIOUS` | no | Previous key during a rotation (decrypt only; saving an account re-encrypts it) |
759
+ | `MAGIC_MAIL_OAUTH_STATE_SECRET` | no | OAuth state signing secret (default: derived from the encryption key) |
760
+ | `MAGIC_MAIL_TRACKING_SECRET` | no | Tracking hash secret (default: derived from the encryption key) |
761
+ | `MAGIC_MAIL_TRACKING_SECRET_PREVIOUS` | no | Comma-separated old tracking secrets that stay valid |
762
+ | `URL` | no | Legacy override of the OAuth callback base URL (default: `server.url`) |
763
+ | `MAGIC_MAIL_ENABLE_DEBUG_ENDPOINT` | no | `true` enables `GET /magic-mail/analytics/debug` outside production |
1438
764
 
1439
- ---
765
+ Without `MAGIC_MAIL_ENCRYPTION_KEY`, a development key derived from `ADMIN_JWT_SECRET`/`APP_KEYS` is used only when `NODE_ENV` is unset, `development` or `test`; rotating those secrets then makes stored credentials unreadable. Any other environment (for example `staging`) refuses to start without the key.
1440
766
 
1441
- ## 📚 Advanced Topics
767
+ Also set Strapi's `server.url` to your public URL (OAuth redirects, tracking links) and `server.proxy: true` behind a reverse proxy.
1442
768
 
1443
- ### Custom Routing Logic
769
+ ### Plugin config
1444
770
 
1445
- Create complex routing rules:
771
+ All keys are optional and validated at startup (an invalid value stops Strapi with a clear message):
1446
772
 
1447
773
  ```javascript
1448
- // Rule 1: VIP customers via a priority account
1449
- {
1450
- name: "VIP Customer Emails",
1451
- matchType: "recipient",
1452
- matchValue: "@vip-customers.com",
1453
- accountName: "Priority SMTP",
1454
- fallbackAccountName: "Gmail OAuth",
1455
- priority: 10,
1456
- isActive: true
1457
- }
1458
-
1459
- // Rule 2: Password resets via fast account
1460
- {
1461
- name: "Password Reset Emails",
1462
- matchType: "subject",
1463
- matchValue: "Password Reset",
1464
- accountName: "Fast Gmail",
1465
- priority: 9,
1466
- isActive: true
1467
- }
1468
-
1469
- // Rule 3: Marketing via SendGrid
1470
- {
1471
- name: "Marketing Campaigns",
1472
- matchType: "emailType",
1473
- matchValue: "marketing",
1474
- accountName: "SendGrid Marketing",
1475
- priority: 8,
1476
- isActive: true
1477
- }
774
+ // config/plugins.js
775
+ module.exports = () => ({
776
+ 'magic-mail': {
777
+ enabled: true,
778
+ config: {
779
+ debug: false, // info/debug logs; warnings and errors are always logged
780
+ contentApi: {
781
+ allowUserRoles: false, // true = users-permissions roles may use the send routes
782
+ },
783
+ quotaTimezone: 'UTC', // IANA zone for hourly/daily quota windows
784
+ tracking: {
785
+ eventRetentionDays: 90, // raw open/click events; 0 = keep forever
786
+ linkTtlDays: 180, // tracked links redirect this long; 0 = forever
787
+ logRetentionDays: 0, // email logs (with events and links); 0 = keep forever
788
+ maxLinksPerEmail: 100, // further links stay untracked
789
+ allowedLinkHosts: [], // e.g. ['example.com'] (subdomains included)
790
+ anonymizeIp: true, // store /24 (IPv4) or /48 (IPv6) only
791
+ },
792
+ designer: {
793
+ projectId: null, // optional Unlayer project id
794
+ scriptUrl: null, // optional https URL of the Unlayer loader
795
+ version: null, // optional exact Unlayer build
796
+ },
797
+ },
798
+ },
799
+ });
1478
800
  ```
1479
801
 
1480
- ### DKIM Configuration (Advanced)
802
+ `allowedLinkHosts` empty means: every host may be click-tracked for admin, service and native sends, and **no** host for Content-API sends. When the list is set, it applies to all sends.
1481
803
 
1482
- For custom domains with SMTP:
804
+ ### Admin settings
1483
805
 
1484
- **1. Generate DKIM Keys:**
1485
- ```bash
1486
- openssl genrsa -out dkim_private.pem 1024
1487
- openssl rsa -in dkim_private.pem -pubout -out dkim_public.pem
1488
- ```
806
+ **Settings → MagicMail → Plugin Settings**, stored in the database (`GET`/`PUT /magic-mail/settings`):
1489
807
 
1490
- **2. Add to DNS:**
1491
- ```
1492
- default._domainkey.yourdomain.com TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA..."
1493
- ```
808
+ | Setting | Default | Effect |
809
+ | ----------------------------- | ------- | --------------------------------------------------------------------------- |
810
+ | Link tracking / Open tracking | on | Rewrite links / add the open pixel |
811
+ | Tracking base URL | empty | Public base for tracking URLs (else `server.url`) |
812
+ | Default from name | empty | Display name when neither the account nor the caller sets one |
813
+ | Default from email | empty | Reference only; emails always go out from the selected account's address |
814
+ | Unsubscribe header | on | Send `List-Unsubscribe` for every email type and use the plugin-wide URL |
815
+ | Unsubscribe URL | empty | Plugin-wide unsubscribe URL; `{{email}}` is replaced per recipient |
816
+ | `trackingFallbackUrl` (API) | empty | Where expired or unknown tracking links redirect; empty shows a static page |
1494
817
 
1495
- **3. Add to MagicMail SMTP Account:**
1496
- ```javascript
1497
- // In account config (edit account → credentials):
1498
- {
1499
- host: "smtp.yourdomain.com",
1500
- port: 587,
1501
- user: "...",
1502
- pass: "...",
1503
- dkim: {
1504
- domainName: "yourdomain.com",
1505
- keySelector: "default",
1506
- privateKey: "-----BEGIN PRIVATE KEY-----\n..."
1507
- }
1508
- }
1509
- ```
818
+ ### Account fields
1510
819
 
1511
- ### Rate Limit Strategy
820
+ Name (unique), description, provider, from email and name, reply-to, encrypted credentials (SMTP password, API key, OAuth tokens, optional DKIM key), daily and hourly limit (0 = unlimited), priority (1 to 10, higher preferred), active and primary flags.
1512
821
 
1513
- **Best practices for high-volume sending:**
822
+ ### Service API
1514
823
 
1515
824
  ```javascript
1516
- // Account Setup:
1517
- // Account A: Priority 10, Daily Limit: 500
1518
- // Account B: Priority 9, Daily Limit: 500
1519
- // Account C: Priority 8, Daily Limit: 500
1520
- // → Total capacity: 1500 emails/day
1521
-
1522
- // MagicMail automatically:
1523
- // 1. Uses Account A until limit hit
1524
- // 2. Switches to Account B
1525
- // 3. Switches to Account C
1526
- // 4. All transparent to your code!
1527
- ```
825
+ const router = strapi.plugin('magic-mail').service('email-router');
826
+ await router.send({ to, subject, html });
1528
827
 
1529
- ---
1530
-
1531
- ## 🤝 Contributing
1532
-
1533
- We welcome contributions!
1534
-
1535
- **Found a bug?** [Open an issue](https://github.com/Schero94/MagicMail/issues)
828
+ const accounts = await strapi.plugin('magic-mail').service('account-manager').getAllAccounts();
1536
829
 
1537
- **Have a feature idea?** [Start a discussion](https://github.com/Schero94/MagicMail/discussions)
830
+ // Test one account (also works for inactive accounts)
831
+ await strapi
832
+ .plugin('magic-mail')
833
+ .service('account-manager')
834
+ .testAccount(accountDocumentId, 'you@example.com', { priority: 'high', type: 'marketing' });
1538
835
 
1539
- **Want to contribute code?**
1540
- 1. Fork the repo
1541
- 2. Create a feature branch
1542
- 3. Make your changes
1543
- 4. Submit a pull request
836
+ const designer = strapi.plugin('magic-mail').service('email-designer');
837
+ const rendered = await designer.renderTemplate(100, { user: { firstName: 'Ada' } });
838
+ const stats = await designer.getStats(); // { total, active, inactive, byCategory, ... }
839
+ ```
1544
840
 
1545
841
  ---
1546
842
 
1547
- ## License
843
+ ## Troubleshooting
1548
844
 
1549
- **MIT License**
845
+ **Email not received**
1550
846
 
1551
- Copyright © 2025 Schero D.
847
+ 1. Look for `[magic-mail]` warnings and errors in the Strapi log (always logged; set `debug: true` for details).
848
+ 2. Check the account: active, the **Test** action succeeds, today's usage is below the limit.
849
+ 3. Check **Analytics**: the log shows status, the account used and, for failures, the error.
850
+ 4. Check the spam folder; new senders often land there.
1552
851
 
1553
- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
852
+ **"Could not connect to the mail server"** on an SMTP test: check host, port and TLS (`587` STARTTLS, `465` with _Use SSL/TLS encryption_). Self-signed certificates are rejected unless `allowInsecure` is set on a non-production or loopback host.
1554
853
 
1555
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
854
+ **OAuth popup fails or reports a redirect mismatch**
1556
855
 
1557
- MagicMail is fully free and open source. Every feature is available to everyone
1558
- with no license key, tier, activation, or usage limit.
856
+ - Register exactly the redirect URI the wizard shows.
857
+ - Production: `server.url` must be public `https`.
858
+ - Admin on its own host: set `admin.url`.
859
+ - Microsoft: all five delegated permissions with admin consent, correct tenant ID, client secret not expired.
860
+ - Several Strapi instances: use sticky admin sessions.
1559
861
 
1560
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
862
+ **Edit wizard says "Stored credentials cannot be decrypted"**: the encryption key changed. Set the old key in `MAGIC_MAIL_ENCRYPTION_KEY_PREVIOUS`, or enter the secrets again.
1561
863
 
1562
- ---
864
+ **Content API returns 403**: use an API token with the `send`/`sendMessage` action; users-permissions roles are rejected unless `contentApi.allowUserRoles` is true.
1563
865
 
1564
- ## 🙏 Credits
866
+ **Content API returns 400**: the response `details` name the field. Common causes: unknown fields (for example `phoneNumber` or `channel` from 3.x), `path` attachments, a `from` that is not the account's address.
1565
867
 
1566
- **Built with:**
1567
- - [Strapi v5](https://strapi.io) - Headless CMS
1568
- - [Nodemailer](https://nodemailer.com) - SMTP client
1569
- - [Heroicons](https://heroicons.com) - Beautiful icons
1570
- - [styled-components](https://styled-components.com) - CSS-in-JS
868
+ **Links are not tracked**: set _Tracking base URL_ or `server.url` to a public URL; for Content-API sends add the link hosts to `tracking.allowedLinkHosts`. Authentication links are never tracked.
1571
869
 
1572
- **Inspired by:**
1573
- - Professional email requirements
1574
- - Multi-tenant application needs
1575
- - Developer experience first
870
+ **Visual designer does not load**: add `https://editor.unlayer.com` to `script-src` and `frame-src` (see [Content Security Policy](#content-security-policy-for-the-visual-editor)). The **Plain Text** tab works without it.
1576
871
 
1577
- ---
872
+ **DMARC failures with a custom domain**: publish SPF (for example `v=spf1 include:_spf.google.com ~all`), add DKIM (provider or SMTP DKIM config) and a DMARC record. MagicMail always sends from the account's address, which keeps alignment.
1578
873
 
1579
- ## 💬 Support
874
+ **"Too many requests"**: the send routes allow 60 requests per minute per token, test sends 10 per minute per admin. Also check provider limits (for example Gmail 500/day for free accounts, 2,000/day for Workspace).
1580
875
 
1581
- **Need help?**
1582
- - 📖 [Documentation](https://github.com/Schero94/Magic-Mail#readme)
1583
- - 🐛 [Report Issues](https://github.com/Schero94/Magic-Mail/issues)
1584
- - 💡 [Feature Requests](https://github.com/Schero94/Magic-Mail/discussions)
876
+ ---
1585
877
 
878
+ ## Upgrading to 4.0.0
1586
879
 
1587
- ---
880
+ 4.0.0 is a major release with security and correctness changes. **Back up the database first**: credentials are re-encrypted in a format 3.x cannot read, and two legacy routing-rule columns are dropped. The full list with migration notes is in [CHANGELOG.md](./CHANGELOG.md).
1588
881
 
1589
- ## ⭐ Show Your Support
882
+ Breaking changes and what to do:
1590
883
 
1591
- If MagicMail helps your project, please:
1592
- - ⭐ Star the repo on [GitHub](https://github.com/Schero94/Magic-Mail)
1593
- - 📢 Share with your team
1594
- - 🐦 Tweet about it
884
+ 1. **Node.js 22.12+** and **`@strapi/plugin-users-permissions`** are required. Peer ranges are `^5.57.0`.
885
+ 2. **Encryption key everywhere but development/test.** Set `MAGIC_MAIL_ENCRYPTION_KEY` for staging too. The constant fallback key is gone.
886
+ 3. **Email only.** The non-email messaging routes and service methods are removed and `/send` no longer accepts `phoneNumber` (both already in 3.1.0); `/send-message` no longer accepts `channel` (400 for unknown fields).
887
+ 4. **Content API needs API tokens.** Switch callers to an API token, or set `contentApi.allowUserRoles: true`. `userId` must reference an existing user.
888
+ 5. **Admin capabilities.** Grant `read`, `manage` and `test-send` in addition to `access` ([permission guide](./docs/permission-migration.md)).
889
+ 6. **Attachments:** replace `path`/`href` with `content`.
890
+ 7. **`from` and headers:** `from` must be the sending account's address; custom headers must be `X-*` or one of the allowed names.
891
+ 8. **Explicit `accountName` is never replaced**; a template's category now sets the email type when `type` is omitted.
892
+ 9. **Tracking needs a public URL.** Set _Tracking base URL_ or `server.url`; Content-API emails are click-tracked only for `tracking.allowedLinkHosts`; links expire after 180 days, events after 90 days, IPs are truncated.
893
+ 10. **Secrets:** the placeholder is `********`; changing host, user, provider or OAuth client requires the secret again.
894
+ 11. **OAuth:** in production the redirect URI comes from `server.url` (public `https`). Authorizations in progress during the upgrade must be restarted.
895
+ 12. **Users & Permissions templates** need the users-permissions email-template permissions and only accept the listed placeholders.
896
+ 13. **Visual designer:** add the CSP directives and the Media Library upload permission.
897
+ 14. **Removed:** provider `ses` (never had an adapter), two legacy messaging columns of the routing-rule table (names in the changelog), the admin keep-alive polling.
1595
898
 
1596
899
  ---
1597
900
 
1598
- ## 🔒 Security Advisories & Dependency Overrides
1599
-
1600
- MagicMail's direct dependencies are audited on every release, but two
1601
- transitive paths are governed by upstream packages we do not control.
1602
- If your own `npm audit` flags either of them, add the matching entry
1603
- to your **Strapi project's** `package.json` (not this plugin's — npm
1604
- `overrides` only take effect on the root project):
901
+ ## Development
1605
902
 
1606
- ```jsonc
1607
- {
1608
- "overrides": {
1609
- // GHSA-xq3m-2v4x-88gg — protobufjs <7.5.5 arbitrary code execution.
1610
- // Reaches us via baileys → libsignal → protobufjs. We already ship
1611
- // this override for local plugin development; apply it in your
1612
- // Strapi project so the fix propagates to the installed tree.
1613
- "protobufjs": "^7.5.5"
1614
- }
1615
- }
903
+ ```bash
904
+ npm ci --ignore-scripts
905
+ npm run lint
906
+ npm run format:check
907
+ npm test
908
+ npm run build
909
+ npm run verify
910
+ npm run verify:runtime
1616
911
  ```
1617
912
 
1618
- After editing `package.json`, run:
913
+ `npm run watch` (or `npm run watch:link` with yalc) rebuilds the admin and the server bundle on change. Pull requests run the same checks on Node 22 and 24.
1619
914
 
1620
- ```bash
1621
- rm -rf node_modules package-lock.json
1622
- npm install
1623
- npm audit
1624
- ```
915
+ - **Bug?** [Open an issue](https://github.com/Schero94/Magic-Mail/issues)
916
+ - **Idea?** [Start a discussion](https://github.com/Schero94/Magic-Mail/discussions)
1625
917
 
1626
- The critical `protobufjs` advisory disappears once the override is in
1627
- place. Remaining `@strapi/design-system → lodash` advisories are
1628
- Strapi-core issues and will clear up automatically when you upgrade
1629
- `@strapi/strapi` to the latest patch release.
918
+ ### Dependency audits
1630
919
 
1631
- If you don't use the WhatsApp delivery feature at all, you can also
1632
- install without the Baileys optional paths via
1633
- `npm install strapi-plugin-magic-mail --ignore-scripts` — the plugin
1634
- already guards every Baileys import with a try/catch and will log
1635
- "WhatsApp features disabled" rather than crash.
920
+ Run `npm audit` from your Strapi project root to check the complete installed tree; npm applies overrides from the root project only.
921
+
922
+ Known host-side advisory: `npm audit --omit=dev` in this repository reports GHSA-mrhx-c7xc-m3w5 (zod < 4.6.6) inside `@strapi/utils`, which the Strapi host provides. MagicMail validates every request with its own patched zod (`^4.6.7`) and bounds all collections, so the advisory is not reachable through MagicMail inputs. It disappears once your Strapi version ships a fixed zod.
1636
923
 
1637
924
  ---
1638
925
 
1639
- **Made for the Strapi Community**
926
+ ## License
927
+
928
+ MIT License, Copyright © 2025 Schero D. See [LICENSE](./LICENSE).
1640
929
 
1641
- **MagicMail - Because email management should be magical, not painful.**
930
+ Built with [Strapi](https://strapi.io), [Nodemailer](https://nodemailer.com), [Unlayer](https://unlayer.com) / [react-email-editor](https://github.com/unlayer/react-email-editor), [Heroicons](https://heroicons.com) and [styled-components](https://styled-components.com).