strapi-plugin-magic-mail 3.1.0 → 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 +178 -0
  2. package/README.md +630 -1154
  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 +4623 -2214
  24. package/dist/server/index.mjs +4623 -2214
  25. package/package.json +30 -19
  26. package/strapi-admin.js +3 -2
  27. package/strapi-server.js +3 -2
  28. package/dist/admin/App-Bmav09Ol.mjs +0 -7156
  29. package/dist/admin/App-mX8QtU1Q.js +0 -7177
  30. package/dist/admin/PluginSettings-D5F5y7n2.mjs +0 -386
  31. package/dist/admin/PluginSettings-DE2PMxlS.js +0 -388
  32. package/dist/admin/StyledButtons-aq1EWrQ-.mjs +0 -433
  33. package/dist/admin/StyledButtons-jHF7TXal.js +0 -434
  34. package/dist/admin/de-BXyjQhJQ.mjs +0 -43
  35. package/dist/admin/de-VtWzTjWV.js +0 -43
  36. package/dist/admin/en-DL_aZYfg.js +0 -43
  37. package/dist/admin/en-DMdnprlB.mjs +0 -43
  38. package/dist/admin/es-BJ1KgkMA.js +0 -43
  39. package/dist/admin/es-g3CHX4iu.mjs +0 -43
  40. package/dist/admin/fr-Bmh5-45j.js +0 -43
  41. package/dist/admin/fr-CN1X3dM-.mjs +0 -43
  42. package/dist/admin/pt-DJ6GNCKb.js +0 -43
  43. package/dist/admin/pt-m8_Gnsqe.mjs +0 -43
  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,1454 +1,930 @@
1
- # 📧 MagicMail - Email Business Suite for Strapi v5
1
+ # MagicMail: email accounts, routing and templates for Strapi 5
2
2
 
3
3
  Project site: https://schero94.github.io/Magic-Mail/
4
4
 
5
- > Manage email accounts, routing rules, OAuth setup, and delivery in Strapi. The plugin is MIT-licensed and open source.
6
-
7
5
  [![NPM Version](https://img.shields.io/npm/v/strapi-plugin-magic-mail.svg)](https://www.npmjs.com/package/strapi-plugin-magic-mail)
8
6
  [![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
9
- [![Strapi Version](https://img.shields.io/badge/Strapi-v5.57.0-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)
10
8
 
11
- > The plugin does not require a license key or paid plan.
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.
12
10
 
13
- ---
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.
12
+
13
+ MagicMail sends **email only**. It is MIT-licensed, has no license key, tiers or usage limits.
14
14
 
15
- ## 🌟 Why MagicMail?
15
+ **At a glance**
16
16
 
17
- **Stop fighting with .env files and email configuration!** MagicMail brings professional email management to Strapi v5 with:
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
18
24
 
19
- - **100% Free & Open Source** - Every feature included, no license key, tiers, or limits (MIT)
20
- - **7 Email Providers** - Gmail, Microsoft 365, Yahoo, SMTP, SendGrid, Mailgun, Nodemailer
21
- - **OAuth 2.0 Authentication** - No passwords needed for Gmail, Microsoft, Yahoo
22
- - **Smart Routing Rules** - Route emails by type, recipient, subject, or custom conditions
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
25
+ ## Contents
26
+
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)
29
41
 
30
42
  ---
31
43
 
32
- ## 📸 Screenshots
44
+ ## Screenshots
33
45
 
34
- ### Email Accounts Dashboard
35
- ![Email Accounts Dashboard](pics/EmailAccountDashboard.png)
36
- *Manage unlimited email accounts with live stats and real-time monitoring*
46
+ The screenshots show synthetic demo data (`example.test` addresses) on Strapi 5.61.
37
47
 
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*
48
+ ### Email accounts
41
49
 
42
- ### Add Email Account - Step 2
43
- ![Add Account Step 2](pics/emailaddaccount2.png)
44
- *Configure SMTP settings or connect with OAuth*
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._
45
52
 
46
- ### Add Email Account - Step 3
47
- ![Add Account Step 3](pics/emailaddaccount3.png)
48
- *Complete setup with rate limits and test connection*
53
+ ### Account wizard
49
54
 
50
- ### Email Templates
51
- ![Email Templates](pics/Templates.png)
52
- *Create and manage email templates with the visual designer*
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._
53
57
 
54
- ### Email Designer
55
- ![Email Designer](pics/emailDesigner.png)
56
- *Drag-and-drop email editor with real-time preview*
58
+ ### Routing rules
57
59
 
58
- ### Template Version History
59
- ![Version History](pics/emailVersionHistory.png)
60
- *Track changes and restore previous template versions*
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._
61
62
 
62
- ### Template Send Test
63
- ![Send Test](pics/emailTemplateSendTest.png)
64
- *Test your templates before sending to production*
63
+ ### Templates
65
64
 
66
- ### Routing Rules & Templates
67
- ![Routing Rules](pics/RoutingEmailTemplates.png)
68
- *Define intelligent routing rules based on email type, recipient, subject*
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._
69
67
 
70
- ### Email Analytics
71
- ![Email Analytics](pics/emailAnalytics.png)
72
- *Track email performance with detailed analytics and statistics*
68
+ ### Template editor
73
69
 
74
- ### Code Integration
75
- ![Code Snippet](pics/codesnippet.png)
76
- *Simple API integration with your Strapi application*
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._
77
72
 
78
- ---
73
+ ### Analytics
79
74
 
80
- ## Messaging migration
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._
81
77
 
82
- Magic-Mail now handles email only. Install and configure the separate Magic-WhatsApp plugin for WhatsApp transport, connection state, templates, and message actions. Magic-Mail does not proxy its old routes to another plugin.
78
+ ### Plugin settings
83
79
 
84
- The `/api/magic-mail/send` endpoint is unchanged. `/api/magic-mail/send-message` still sends email and accepts `channel: "email"` or `channel: "auto"` when `to` contains the recipient address. The old `/send-whatsapp` and `/whatsapp/*` routes are removed. The `email-router` service no longer exposes WhatsApp methods, and email routing rules no longer offer WhatsApp fallback.
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._
85
82
 
86
- Magic-Mail does not delete or import old local session files, stored templates, or settings. The two legacy routing-rule columns remain in Strapi's schema so schema synchronization does not drop their saved values. The plugin no longer reads or updates those fields. Back up existing session data securely and migrate it only by following the separate Magic-WhatsApp plugin's instructions.
83
+ ### Dark mode and mobile
84
+
85
+ ![Analytics in dark mode](https://raw.githubusercontent.com/Schero94/Magic-Mail/main/pics/emailAnalyticsDark.png)
86
+
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) |
87
90
 
88
91
  ---
89
92
 
90
- ## Quick Start
93
+ ## Quick start
94
+
95
+ ### Requirements
91
96
 
92
- ### Installation
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))
93
101
 
94
- Requires Node.js `>=20.19.0 <=26.x.x` and Strapi `^5.57.0`.
102
+ ### 1. Install
95
103
 
96
104
  ```bash
97
105
  npm install strapi-plugin-magic-mail
98
- # or
99
- yarn add strapi-plugin-magic-mail
100
106
  ```
101
107
 
102
- ### Enable Plugin
108
+ ### 2. Enable
103
109
 
104
- Create or update `config/plugins.ts`:
105
-
106
- ```typescript
107
- export default () => ({
110
+ ```javascript
111
+ // config/plugins.js (or config/plugins.ts with export default)
112
+ module.exports = () => ({
108
113
  'magic-mail': {
109
114
  enabled: true,
110
115
  },
111
116
  });
112
117
  ```
113
118
 
114
- ### Build & Start
119
+ ### 3. Set the encryption key
120
+
121
+ ```bash
122
+ # Generate once and keep it with your other secrets
123
+ node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
124
+ ```
125
+
126
+ ```env
127
+ MAGIC_MAIL_ENCRYPTION_KEY=<the generated value>
128
+ ```
129
+
130
+ Keep the key stable. Losing it makes the stored account credentials unreadable.
131
+
132
+ ### 4. Build and start
115
133
 
116
134
  ```bash
117
135
  npm run build
118
136
  npm run develop
119
137
  ```
120
138
 
121
- ### Add Your First Email Account
139
+ ### 5. Add an account and send a test
122
140
 
123
- 1. Navigate to **Admin Panel → MagicMail → Email Accounts**
124
- 2. Click **"Add Account"**
125
- 3. Choose your provider (Gmail OAuth, SMTP, etc.)
126
- 4. Fill in credentials
127
- 5. Click **"Test"** to verify
128
- 6. Done! 🎉
129
-
130
- ---
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**.
131
144
 
132
- ## 📧 Supported Email Providers
145
+ Every existing `strapi.plugin('email')` call now goes through MagicMail.
133
146
 
134
- | Provider | Type | Authentication | Features |
135
- |----------|------|----------------|----------|
136
- | **Gmail** | OAuth 2.0 | Google OAuth | Gmail API, Attachments, Auto DKIM |
137
- | **Microsoft 365** | OAuth 2.0 | Azure AD | Graph API, Attachments, Tenant Support |
138
- | **Yahoo Mail** | OAuth 2.0 | Yahoo OAuth | SMTP OAuth2, Attachments |
139
- | **SMTP** | Credentials | Username/Password | Universal, DKIM Optional, Custom Servers |
140
- | **SendGrid** | API Key | SendGrid API | Transactional, Marketing, Templates |
141
- | **Mailgun** | API Key | Mailgun API | Bulk Sending, Analytics |
147
+ Upgrading from 3.x? Read [Upgrading to 4.0.0](#upgrading-to-400) first.
142
148
 
143
149
  ---
144
150
 
145
- ## 💡 How to Send Emails
151
+ ## Sending email
146
152
 
147
- ### Method 1: Strapi Native Email Service (Recommended!)
153
+ ### Option 1: the Strapi email service (recommended)
148
154
 
149
- 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:
150
156
 
151
157
  ```javascript
152
- // This works in ANY Strapi plugin or controller:
153
158
  await strapi.plugin('email').service('email').send({
154
159
  to: 'user@example.com',
155
- subject: 'Welcome to Our Platform!',
156
- text: 'Plain text version',
160
+ subject: 'Welcome to Example Shop',
161
+ text: 'Thanks for signing up!',
157
162
  html: '<h1>Welcome!</h1><p>Thanks for signing up!</p>',
158
163
  });
159
-
160
- // ✅ MagicMail automatically:
161
- // - Selects best account (via routing rules or priority)
162
- // - Checks rate limits
163
- // - Handles failover if needed
164
- // - Logs the email
165
- // - Updates statistics
166
164
  ```
167
165
 
168
- **Benefits:**
169
- - ✅ **Zero code changes** - Existing plugins work automatically
170
- - ✅ **Email Designer compatible** - Works with strapi-plugin-email-designer-5
171
- - ✅ **Fallback safe** - Falls back to original email service if MagicMail fails
172
- - ✅ **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.
173
169
 
174
- ### Method 2: Direct MagicMail API
170
+ ### Option 2: the MagicMail router service
175
171
 
176
- For explicit control over account selection:
172
+ Use the router directly when you need its options, for example to force an account:
177
173
 
178
174
  ```javascript
179
- // Force a specific account
180
175
  await strapi.plugin('magic-mail').service('email-router').send({
181
176
  to: 'customer@example.com',
182
- subject: 'Order Confirmation',
183
- html: '<h1>Your Order #12345</h1>',
184
- 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
185
180
  type: 'transactional',
186
181
  priority: 'high',
187
182
  });
183
+ // → { success: true, accountUsed: 'Transactional SMTP', messageId: '<…>', emailId: '…' }
188
184
  ```
189
185
 
190
- ### Sending with Attachments
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.
191
187
 
192
- ```javascript
193
- await strapi.plugin('email').service('email').send({
194
- to: 'customer@example.com',
195
- subject: 'Invoice #12345',
196
- html: '<h1>Invoice Attached</h1>',
197
- attachments: [
198
- {
199
- filename: 'invoice.pdf',
200
- path: './uploads/invoice-12345.pdf',
201
- },
202
- {
203
- filename: 'receipt.pdf',
204
- content: Buffer.from(pdfData),
205
- contentType: 'application/pdf',
206
- },
207
- ],
208
- });
209
- ```
188
+ ### Option 3: HTTP
210
189
 
211
- **Supported attachment formats:**
212
- - ✅ PDF, Word, Excel, PowerPoint
213
- - ✅ Images (PNG, JPG, GIF, WebP)
214
- - ✅ Text files (TXT, CSV, JSON)
215
- - ✅ Archives (ZIP, RAR)
216
- - ✅ Any file type!
190
+ See [Content API](#content-api) for `POST /api/magic-mail/send` and `/send-message`.
217
191
 
218
- ### Advanced: Priority & Marketing Emails
192
+ ### Send options
219
193
 
220
- ```javascript
221
- // High priority email
222
- await strapi.plugin('magic-mail').service('email-router').send({
223
- to: 'vip@example.com',
224
- subject: 'Urgent: Action Required',
225
- html: '<h1>Important Update</h1>',
226
- priority: 'high', // Adds X-Priority and Importance headers
227
- });
194
+ The same fields work for the router, the Strapi email service and `POST /api/magic-mail/send`.
228
195
 
229
- // Marketing email with unsubscribe (GDPR/CAN-SPAM compliant)
230
- await strapi.plugin('magic-mail').service('email-router').send({
231
- to: 'subscriber@example.com',
232
- subject: 'Weekly Newsletter',
233
- html: '<h1>This Week's Updates</h1>',
234
- type: 'marketing',
235
- unsubscribeUrl: 'https://yoursite.com/unsubscribe?id=123',
236
- // Automatically adds List-Unsubscribe header!
237
- });
238
- ```
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 |
239
212
 
240
- ---
213
+ ### Attachments
241
214
 
242
- ## 🎨 Email Designer Suite (NEW!)
243
-
244
- **Design beautiful emails visually with the integrated email designer!**
245
-
246
- ### ✨ Features
247
-
248
- Every Email Designer feature is included for free:
249
-
250
- | Feature | Included |
251
- |---------|----------|
252
- | **Visual Designer** | ✅ |
253
- | **Unlimited Templates** | ✅ |
254
- | **Drag & Drop + Mustache** | ✅ |
255
- | **Import / Export** | ✅ |
256
- | **Template Versioning** | ✅ |
257
- | **Email Analytics** | ✅ |
258
-
259
- ### 📧 Creating Email Templates
260
-
261
- **1. Navigate to Email Templates Tab**
262
-
263
- Go to **MagicMail → Email Templates** in the admin panel.
264
-
265
- **2. Create New Template**
266
-
267
- Click **"Create Template"** and use the visual designer to build your email.
268
-
269
- **3. Add Variables with Mustache**
270
-
271
- Use Mustache syntax for dynamic content:
272
-
273
- ```html
274
- <h1>Welcome {{user.firstName}}!</h1>
275
- <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
+ });
276
232
  ```
277
233
 
278
- **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`.
279
235
 
280
- Each template gets a unique **Reference ID** (e.g., `100`) that you use when sending.
236
+ ### Unsubscribe and bulk headers
281
237
 
282
- ### 💌 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`.
283
242
 
284
- **Method 1: Using Template ID**
243
+ ---
285
244
 
286
- ```javascript
287
- // Send email using template
288
- await strapi.plugin('magic-mail').service('email-router').send({
289
- to: 'user@example.com',
290
- templateId: 100, // Your template reference ID
291
- templateData: {
292
- user: {
293
- firstName: 'John',
294
- lastName: 'Doe',
295
- email: 'john@example.com',
296
- },
297
- company: {
298
- name: 'ACME Corp',
299
- url: 'https://acme.com',
300
- },
301
- orderNumber: '12345',
302
- orderTotal: '$199.99',
303
- },
304
- });
245
+ ## Provider setup
305
246
 
306
- // ✅ MagicMail will:
307
- // - Load template #100
308
- // - Render it with your data (Mustache)
309
- // - Route via smart routing
310
- // - Send via best account
311
- ```
247
+ ### OAuth redirect URI
312
248
 
313
- **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`):
314
250
 
315
- ```javascript
316
- // Also works with standard Strapi email service!
317
- await strapi.plugin('email').service('email').send({
318
- to: 'user@example.com',
319
- templateId: 100,
320
- templateData: {
321
- user: { firstName: 'Jane' },
322
- resetUrl: 'https://yoursite.com/reset?token=xyz',
323
- },
324
- });
325
251
  ```
326
-
327
- ### 🎯 Template Categories
328
-
329
- Templates can be categorized for automatic routing:
330
-
331
- - **transactional** - Order confirmations, receipts
332
- - **marketing** - Newsletters, promotions
333
- - **notification** - System alerts, updates
334
- - **custom** - Anything else
335
-
336
- **Automatic Routing by Category:**
337
-
338
- ```javascript
339
- // Create routing rule: All marketing templates → SendGrid
340
- // Then just send:
341
- await strapi.plugin('magic-mail').service('email-router').send({
342
- to: 'subscriber@example.com',
343
- templateId: 200, // Marketing category template
344
- templateData: { offer: '50% OFF' },
345
- });
346
- // ✅ Automatically routes via SendGrid (based on category)
252
+ <public URL>/magic-mail/oauth/<gmail|microsoft|yahoo>/callback
347
253
  ```
348
254
 
349
- ### 🔄 Template Versioning
350
-
351
- Every time you save a template, a new version is created automatically:
352
-
353
- ```javascript
354
- // Get all versions
355
- const versions = await strapi
356
- .plugin('magic-mail')
357
- .service('email-designer')
358
- .getVersions(templateId);
359
-
360
- // Restore old version
361
- await strapi
362
- .plugin('magic-mail')
363
- .service('email-designer')
364
- .restoreVersion(templateId, versionId);
365
- ```
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.
366
256
 
367
- ### 📦 Import/Export Templates
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.
368
258
 
369
- **Export all templates:**
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.
370
260
 
371
- ```javascript
372
- const templates = await strapi
373
- .plugin('magic-mail')
374
- .service('email-designer')
375
- .exportTemplates();
261
+ ### Gmail (OAuth 2.0)
376
262
 
377
- // Save to file
378
- fs.writeFileSync('templates.json', JSON.stringify(templates, null, 2));
379
- ```
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.
380
267
 
381
- **Import templates:**
268
+ Scopes: `https://www.googleapis.com/auth/gmail.send`, `https://www.googleapis.com/auth/userinfo.email`, `openid`. Mail is sent through the Gmail API.
382
269
 
383
- ```javascript
384
- const templates = JSON.parse(fs.readFileSync('templates.json'));
270
+ ### Microsoft 365 (OAuth 2.0)
385
271
 
386
- const results = await strapi
387
- .plugin('magic-mail')
388
- .service('email-designer')
389
- .importTemplates(templates);
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**.
390
278
 
391
- console.log(`Imported ${results.filter(r => r.success).length} templates`);
392
- ```
279
+ The tenant ID must be a GUID, a domain, `common`, `organizations` or `consumers`. Mail is sent through Microsoft Graph `sendMail`.
393
280
 
394
- ### 🎨 Visual Designer Features
281
+ ### Yahoo Mail (OAuth 2.0)
395
282
 
396
- **Powered by Unlayer Email Editor:**
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**.
397
287
 
398
- - ✅ Drag & Drop Interface
399
- - ✅ Pre-built Content Blocks
400
- - ✅ Image Upload & Management
401
- - ✅ Custom Fonts & Colors
402
- - ✅ Responsive Design
403
- - ✅ Mobile Preview
404
- - ✅ HTML/Text Editor
405
- - ✅ Merge Tags Support
406
- - ✅ Save/Load Designs
288
+ Mail is sent over SMTP with XOAUTH2.
407
289
 
408
- ### 📊 Template Statistics
290
+ ### SMTP
409
291
 
410
- ```javascript
411
- const stats = await strapi
412
- .plugin('magic-mail')
413
- .service('email-designer')
414
- .getStats();
415
-
416
- console.log(stats);
417
- // {
418
- // total: 15,
419
- // active: 12,
420
- // inactive: 3,
421
- // byCategory: [
422
- // { category: 'transactional', count: 8 },
423
- // { category: 'marketing', count: 4 },
424
- // { category: 'notification', count: 3 }
425
- // ],
426
- // maxTemplates: -1, // unlimited (all features are free)
427
- // remaining: -1 // unlimited
428
- // }
429
- ```
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 |
430
297
 
431
- ### 🚀 Best Practices
298
+ Common hosts: Gmail `smtp.gmail.com:587` (app password), Outlook.com `smtp-mail.outlook.com:587`, Microsoft 365 `smtp.office365.com:587`.
432
299
 
433
- **1. Use Template Categories**
434
- ```javascript
435
- // Set category when creating template
436
- category: 'marketing' // Auto-routes to marketing account
437
- ```
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.
438
301
 
439
- **2. Test Templates Before Using**
440
- ```javascript
441
- // Test render before sending
442
- const rendered = await strapi
443
- .plugin('magic-mail')
444
- .service('email-designer')
445
- .renderTemplate(100, { user: { firstName: 'Test' } });
302
+ Optional DKIM signing (account config):
446
303
 
447
- 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
+ }
448
312
  ```
449
313
 
450
- **3. Version Control**
451
- ```javascript
452
- // Template version history is always enabled (free for everyone).
453
- ```
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`.
454
315
 
455
- **4. Organize with Tags**
456
- ```javascript
457
- // Add tags to templates
458
- tags: ['onboarding', 'welcome-series', 'v2']
459
- ```
316
+ The provider value `nodemailer` is an alias of `smtp`.
460
317
 
461
- ---
318
+ ### SendGrid
462
319
 
463
- ## 🎯 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.
464
322
 
465
- Create intelligent routing rules to send emails through specific accounts based on conditions.
323
+ ### Mailgun
466
324
 
467
- ### 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.
468
327
 
469
- **1. Route Marketing Emails via SendGrid:**
470
- ```
471
- Match Type: Email Type
472
- Match Value: marketing
473
- Target Account: SendGrid Marketing
474
- Priority: 10
475
- ```
328
+ A Mailgun domain can only be used through the API host of its region. MagicMail stores the region as `config.region` and calls:
476
329
 
477
- **2. Route VIP Customers via Priority SMTP:**
478
- ```
479
- Match Type: Recipient
480
- Match Value: @vip-customers.com
481
- Target Account: Priority SMTP
482
- Fallback Account: Gmail OAuth
483
- Priority: 9
484
- ```
330
+ | `config.region` | API host |
331
+ | --------------- | ---------------------------- |
332
+ | `us` (default) | `https://api.mailgun.net` |
333
+ | `eu` | `https://api.eu.mailgun.net` |
485
334
 
486
- **3. Route Password Reset Emails:**
487
- ```
488
- Match Type: Subject
489
- Match Value: Password Reset
490
- Target Account: Transactional SMTP
491
- Priority: 8
492
- ```
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.
493
336
 
494
- ### Routing Priority
337
+ ### Provider comparison
495
338
 
496
- Rules are evaluated from **highest to lowest priority** (1-10):
497
- - Priority 10 = Evaluated first (most specific rules)
498
- - 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 |
499
347
 
500
- If no rule matches, the **Primary Account** is used.
348
+ All providers support attachments, inline images (`cid`) and custom headers.
501
349
 
502
350
  ---
503
351
 
504
- ## 🔐 Provider Setup Guides
352
+ ## Routing rules and quotas
505
353
 
506
- ### Gmail OAuth Setup
354
+ Routing rules send an email through a specific account when it matches a condition. Manage them under **MagicMail → Routing Rules**.
507
355
 
508
- **1. Google Cloud Console**
509
- 1. Go to [console.cloud.google.com](https://console.cloud.google.com)
510
- 2. Create a new project or select existing
511
- 3. Enable Gmail API
512
- 4. Create OAuth 2.0 Client ID
513
- 5. Add redirect URI: `http://localhost:1337/magic-mail/oauth/gmail/callback`
514
- 6. Copy Client ID and Client Secret
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 |
515
363
 
516
- **2. In MagicMail**
517
- 1. Choose "Gmail OAuth" provider
518
- 2. Paste Client ID and Client Secret
519
- 3. Click "Connect with Google"
520
- 4. Authorize in popup
521
- 5. Done! ✅
364
+ Examples:
522
365
 
523
- **Required Scopes:**
524
- - `https://www.googleapis.com/auth/gmail.send`
525
- - `https://www.googleapis.com/auth/userinfo.email`
526
-
527
- ---
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 |
528
371
 
529
- ### Microsoft 365 OAuth Setup
530
-
531
- **1. Azure Portal**
532
- 1. Go to [portal.azure.com](https://portal.azure.com)
533
- 2. Navigate to **Azure Active Directory → App registrations**
534
- 3. Click **"New registration"**
535
- 4. Name: "MagicMail"
536
- 5. Account type: **"Accounts in this organizational directory only"**
537
- 6. Copy **Application (client) ID** AND **Directory (tenant) ID**
538
-
539
- **2. Add Redirect URI**
540
- ```
541
- http://localhost:1337/magic-mail/oauth/microsoft/callback
542
- ```
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.
543
373
 
544
- **3. API Permissions**
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.
545
375
 
546
- Add these **Delegated permissions** under Microsoft Graph:
547
- - ✅ `Mail.Send` - Send emails as the signed-in user
548
- - ✅ `User.Read` - Read user profile
549
- - ✅ `offline_access` - Maintain access (refresh tokens)
550
- - ✅ `openid` - Sign users in
551
- - ✅ `email` - View users' email address
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.
552
377
 
553
- **4. Grant Admin Consent** (Required!)
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.
554
379
 
555
- **5. Create Client Secret**
556
- 1. **Certificates & secrets** → **New client secret**
557
- 2. Copy the **VALUE** (not the Secret ID)
558
- 3. Store it securely!
559
-
560
- **6. In MagicMail**
561
- 1. Choose "Microsoft OAuth" provider
562
- 2. Paste **Tenant ID**, **Client ID**, and **Client Secret**
563
- 3. Click "Connect with Microsoft"
564
- 4. Authorize
565
- 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.
566
381
 
567
382
  ---
568
383
 
569
- ### Yahoo Mail OAuth Setup
570
-
571
- **1. Yahoo Developer Console**
572
- 1. Go to [developer.yahoo.com/apps](https://developer.yahoo.com/apps)
573
- 2. Click **"Create an App"**
574
- 3. Fill in app details
575
-
576
- **2. Add Redirect URI**
577
- ```
578
- http://localhost:1337/magic-mail/oauth/yahoo/callback
579
- ```
580
-
581
- **3. API Permissions**
582
- - ✅ `Mail` - Send and manage emails
583
- - ✅ `OpenID Connect` - User authentication
384
+ ## Templates and the visual editor
584
385
 
585
- **4. In MagicMail**
586
- 1. Choose "Yahoo Mail OAuth" provider
587
- 2. Paste Client ID and Client Secret
588
- 3. Click "Connect with Yahoo"
589
- 4. Done! ✅
386
+ Everything in the designer is included, with no tiers:
590
387
 
591
- ---
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** | ✅ |
592
397
 
593
- ### SMTP Setup (Universal)
398
+ ### Creating templates
594
399
 
595
- **Works with ANY SMTP server:**
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:
596
403
 
597
- ```
598
- Host: smtp.gmail.com (or your SMTP server)
599
- Port: 587 (STARTTLS) or 465 (SSL/TLS)
600
- Username: your-email@domain.com
601
- Password: your-password (or App Password)
602
- ```
404
+ ```html
405
+ <h1>Welcome {{user.firstName}}!</h1>
406
+ <p>Thanks for joining {{company.name}}</p>
407
+ ```
603
408
 
604
- **Popular SMTP Servers:**
605
- - **Gmail:** `smtp.gmail.com:587` (use App Password!)
606
- - **Outlook:** `smtp-mail.outlook.com:587`
607
- - **Office365:** `smtp.office365.com:587`
608
- - **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.
609
410
 
610
- **Gmail App Password:**
611
- 1. Google Account → Security → 2-Step Verification
612
- 2. App passwords → Generate
613
- 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.
614
412
 
615
- **Optional: DKIM Signing**
413
+ ### Sending a template
616
414
 
617
- For custom domains, you can add DKIM:
618
415
  ```javascript
619
- // In config field (advanced users):
620
- {
621
- dkim: {
622
- domainName: 'yourdomain.com',
623
- keySelector: 'default',
624
- privateKey: '-----BEGIN PRIVATE KEY-----\n...'
625
- }
626
- }
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
+ });
627
427
  ```
628
428
 
629
- ---
630
-
631
- ### SendGrid Setup
632
-
633
- **1. Get API Key**
634
- 1. Go to [app.sendgrid.com](https://app.sendgrid.com)
635
- 2. Settings → API Keys → Create API Key
636
- 3. Scope: **Mail Send** (Full Access)
637
- 4. Copy the API key
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.
638
430
 
639
- **2. In MagicMail**
640
- 1. Choose "SendGrid" provider
641
- 2. Paste API Key
642
- 3. Done! ✅
431
+ ### Categories
643
432
 
644
- ---
645
-
646
- ### Mailgun Setup
647
-
648
- **1. Get API Key & Domain**
649
- 1. Go to [app.mailgun.com](https://app.mailgun.com)
650
- 2. Settings → API Security → Copy **Private API Key**
651
- 3. Sending → Domains → Copy your verified domain
652
-
653
- **2. In MagicMail**
654
- 1. Choose "Mailgun" provider
655
- 2. Enter **Domain** (e.g., `mg.yourdomain.com`)
656
- 3. Paste **Private API Key**
657
- 4. Done! ✅
658
-
659
- ---
660
-
661
- ## 🛡️ Security Features
662
-
663
- ### Encryption
664
- - ✅ **AES-256-GCM** encryption for all credentials
665
- - ✅ **No plain text passwords** in database
666
- - ✅ **OAuth tokens encrypted** at rest
667
- - ✅ **Secure token refresh** mechanism
668
-
669
- ### Transport Security
670
- - ✅ **TLS 1.2+ enforced** (SMTP)
671
- - ✅ **Certificate verification** enabled
672
- - ✅ **HTTPS only** for API providers
673
-
674
- ### Email Authentication
675
- - ✅ **DKIM signing** (optional for SMTP, automatic for OAuth)
676
- - ✅ **SPF validation** support
677
- - ✅ **DMARC compliance** - All providers pass DMARC checks
678
- - ✅ **Proper Message-ID** generation (RFC 5322)
679
-
680
- ### Content Security
681
- - ✅ **XSS prevention** - No `<script>` tags allowed
682
- - ✅ **Input validation** - Email format, subject length
683
- - ✅ **Spam trigger detection** - Warns about suspicious patterns
684
- - ✅ **MIME type validation**
685
-
686
- ### Compliance Headers
687
- - ✅ **List-Unsubscribe** (RFC 8058) - GDPR/CAN-SPAM compliant
688
- - ✅ **Auto-Submitted** (RFC 3834) - Prevents auto-responder loops
689
- - ✅ **Precedence: bulk** for marketing emails
690
- - ✅ **X-Priority** and **Importance** headers
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.
691
434
 
692
- ---
435
+ ### Versions, import and export
693
436
 
694
- ## 📊 Features Overview
695
-
696
- ### Multi-Account Management
697
- - Unlimited email accounts
698
- - Provider-specific configuration
699
- - Active/Inactive status
700
- - Primary account designation
701
- - Per-account priority (1-10)
702
- - Daily and hourly rate limits
703
-
704
- ### Smart Routing Engine
705
- - **Email Type Routing** - transactional, marketing, notification
706
- - **Recipient Matching** - Route by email domain or address
707
- - **Subject Matching** - Route by subject keywords
708
- - **Template Matching** - Route by template name
709
- - **Custom Field Matching** - Flexible custom conditions
710
- - **Priority-based evaluation** - Higher priority rules match first
711
- - **Fallback accounts** - Automatic failover per rule
712
-
713
- ### Rate Limiting & Failover
714
- - Per-account daily limits
715
- - Per-account hourly limits
716
- - Automatic counter reset (hourly/daily)
717
- - Smart failover to backup accounts
718
- - Rate limit hit detection
719
- - Account health monitoring
720
-
721
- ### Analytics & Monitoring
722
- - Real-time statistics dashboard
723
- - Emails sent today (per account and total)
724
- - Total emails sent (lifetime)
725
- - Active/Inactive account count
726
- - Success/Failure tracking
727
- - Last used timestamps
728
- - Visual progress bars for rate limits
729
-
730
- ### Testing Features
731
- - **Test Direct** - Test specific account directly
732
- - **Test Strapi Service** - Verify Email Designer compatibility
733
- - **Priority Testing** - Test high/normal priority headers
734
- - **Marketing Email Testing** - Test List-Unsubscribe headers
735
- - **Detailed test results** - Shows all security features applied
736
-
737
- ---
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.
738
439
 
739
- ## 🔧 Configuration
740
-
741
- ### Basic Setup (No .env Required!)
742
-
743
- All configuration is done via the Admin Panel. However, for production deployments, you can set:
744
-
745
- ```env
746
- # Optional - Base URL for OAuth callbacks
747
- URL=https://yourdomain.com
748
-
749
- # Optional - Encryption key (auto-generated if not provided)
750
- 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);
751
446
  ```
752
447
 
753
- ### Account Configuration
754
-
755
- Each account can have:
448
+ ### Users & Permissions templates
756
449
 
757
- **Basic Settings:**
758
- - Name (unique identifier)
759
- - Description
760
- - Provider type
761
- - From Email
762
- - From Name
763
- - Reply-To Email
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.
764
451
 
765
- **Security:**
766
- - OAuth credentials (encrypted)
767
- - SMTP credentials (encrypted)
768
- - API keys (encrypted)
769
- - DKIM configuration (optional)
452
+ ### The visual editor (Unlayer) and privacy
770
453
 
771
- **Rate Limiting:**
772
- - Daily email limit (0 = unlimited)
773
- - Hourly email limit (0 = unlimited)
774
- - Priority (1-10, higher = preferred)
454
+ The visual designer embeds the [Unlayer](https://unlayer.com) editor through `react-email-editor`. What that means for you:
775
455
 
776
- **Status:**
777
- - Active/Inactive
778
- - Primary account designation
779
-
780
- ---
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.
781
461
 
782
- ## 🎨 API Reference
783
-
784
- ### Send Email (Standard Method)
462
+ Optional overrides (`config/plugins.js`):
785
463
 
786
464
  ```javascript
787
- await strapi.plugin('email').service('email').send({
788
- to: 'recipient@example.com',
789
- from: 'sender@example.com', // Optional - uses account default
790
- subject: 'Email Subject',
791
- text: 'Plain text content',
792
- html: '<h1>HTML content</h1>',
793
-
794
- // Optional advanced options:
795
- replyTo: 'reply@example.com',
796
- type: 'transactional', // or 'marketing', 'notification'
797
- priority: 'normal', // or 'high'
798
- accountName: 'Specific Account', // Force specific account
799
- unsubscribeUrl: 'https://site.com/unsubscribe', // For marketing emails
800
-
801
- attachments: [
802
- {
803
- filename: 'document.pdf',
804
- path: './path/to/file.pdf',
805
- 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
+ },
806
473
  },
807
- ],
808
- });
809
- ```
810
-
811
- ### Send Email (Direct MagicMail API)
812
-
813
- ```javascript
814
- await strapi.plugin('magic-mail').service('email-router').send({
815
- to: 'user@example.com',
816
- subject: 'Test Email',
817
- html: '<p>Content</p>',
818
- accountName: 'Gmail OAuth', // Optional - force specific account
819
- type: 'transactional',
820
- priority: 'normal',
474
+ },
821
475
  });
822
476
  ```
823
477
 
824
- ### Get All Accounts
825
-
826
- ```javascript
827
- const accounts = await strapi.plugin('magic-mail').service('account-manager').getAllAccounts();
828
- ```
478
+ ### Content Security Policy for the visual editor
829
479
 
830
- ### Test Account
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'`.
831
481
 
832
482
  ```javascript
833
- const result = await strapi.plugin('magic-mail').service('account-manager').testAccount(
834
- accountId,
835
- 'test@example.com',
483
+ module.exports = [
484
+ 'strapi::logger',
485
+ 'strapi::errors',
836
486
  {
837
- priority: 'high',
838
- type: 'marketing',
839
- unsubscribeUrl: 'https://example.com/unsubscribe',
840
- }
841
- );
842
- ```
843
-
844
- ---
845
-
846
- ## 🧪 Testing Your Setup
847
-
848
- ### Test via Admin UI
849
-
850
- 1. Go to **Email Accounts**
851
- 2. Click **Test** button on any account
852
- 3. Enter recipient email
853
- 4. Select test options:
854
- - **Priority:** Normal / High
855
- - **Email Type:** Transactional / Marketing / Notification
856
- - **Unsubscribe URL:** (for marketing emails)
857
- 5. Choose test method:
858
- - **Test Direct** - Direct send via account
859
- - **Test Strapi Service** - Via Strapi email service (tests Email Designer compatibility)
860
-
861
- ### Test via Code
862
-
863
- ```javascript
864
- // Test email with all security features
865
- await strapi.plugin('magic-mail').service('account-manager').testAccount(
866
- accountId,
867
- 'your-email@example.com',
868
- {
869
- priority: 'high',
870
- type: 'marketing',
871
- unsubscribeUrl: 'https://yoursite.com/unsubscribe',
872
- }
873
- );
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
+ ];
874
508
  ```
875
509
 
876
- ### Verify Headers
877
-
878
- **To verify security headers are applied:**
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.
879
511
 
880
- **In Gmail:**
881
- 1. Open email → Click ⋮ (three dots)
882
- 2. "Show original"
883
- 3. Look for:
884
- ```
885
- X-Mailer: MagicMail/1.0
886
- X-Priority: 1 (Highest)
887
- Importance: high
888
- List-Unsubscribe: <https://...>
889
- Message-ID: <...>
890
- DKIM-Signature: ...
891
- ```
512
+ ---
892
513
 
893
- **In Outlook/Microsoft:**
894
- 1. Email → File → Properties
895
- 2. View "Internet headers"
896
- 3. Should show "High Importance" with red exclamation mark
514
+ ## Tracking, privacy and retention
897
515
 
898
- ---
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`.
899
517
 
900
- ## 🔒 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.
901
519
 
902
- ### 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).
903
521
 
904
- | Standard | Status | Description |
905
- |----------|--------|-------------|
906
- | **DKIM** | ✅ | DomainKeys Identified Mail - Signature validation |
907
- | **SPF** | ✅ | Sender Policy Framework - Sender verification |
908
- | **DMARC** | ✅ | Domain-based Authentication - Policy enforcement |
909
- | **TLS 1.2+** | ✅ | Transport Layer Security - Encrypted transmission |
910
- | **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.
911
523
 
912
- ### 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>`.
913
525
 
914
- | Regulation | Status | Implementation |
915
- |------------|--------|----------------|
916
- | **GDPR** | ✅ | List-Unsubscribe header for marketing emails |
917
- | **CAN-SPAM** | ✅ | One-click unsubscribe mechanism (RFC 8058) |
918
- | **RFC 5322** | ✅ | Proper email format and headers |
919
- | **RFC 3834** | ✅ | Auto-Submitted header prevents loops |
920
- | **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.
921
527
 
922
- ### 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.
923
529
 
924
- **Every email is validated for:**
925
- - ✅ Valid recipient email format
926
- - ✅ Non-empty subject line
927
- - ✅ Subject length (warns if > 200 chars)
928
- - ✅ Content presence (text or html required)
929
- - ✅ No `<script>` tags in HTML
930
- - ✅ No `javascript:` protocol in links
931
- - ✅ Spam trigger detection (warns only)
530
+ Mention open and click tracking in your privacy notice.
932
531
 
933
532
  ---
934
533
 
935
- ## 📈 Analytics & Monitoring
534
+ ## Content API
936
535
 
937
- ### Dashboard Stats
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 |
938
542
 
939
- **Global Stats:**
940
- - Emails sent today (all accounts)
941
- - Total emails sent (lifetime)
942
- - Active accounts count
543
+ ### The API token rule
943
544
 
944
- **Per-Account Stats:**
945
- - Emails sent today
946
- - Total emails sent
947
- - Rate limit usage (visual progress bars)
948
- - Last used timestamp
949
- - Success/failure rate
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.
950
546
 
951
- ### Email Logging
547
+ To let users-permissions roles send as well (the pre-4.0 behaviour):
952
548
 
953
- Every email is logged with:
954
- - Recipient
955
- - Sender
956
- - Subject
957
- - Account used
958
- - Status (sent/failed)
959
- - Timestamp
960
- - Email type
961
- - Error message (if failed)
962
-
963
- Access logs via:
964
549
  ```javascript
965
- const logs = await strapi.entityService.findMany('plugin::magic-mail.email-log', {
966
- filters: { status: 'sent' },
967
- sort: { createdAt: 'desc' },
968
- limit: 100,
550
+ // config/plugins.js
551
+ module.exports = () => ({
552
+ 'magic-mail': {
553
+ config: { contentApi: { allowUserRoles: true } },
554
+ },
969
555
  });
970
556
  ```
971
557
 
972
- ---
973
-
974
- ## 🔄 Integration with Email Designer
975
-
976
- **MagicMail is fully compatible with [strapi-plugin-email-designer-5](https://www.npmjs.com/package/strapi-plugin-email-designer-5)!**
977
-
978
- ### How It Works
979
-
980
- 1. Install Email Designer:
981
- ```bash
982
- npm install strapi-plugin-email-designer-5
983
- ```
984
-
985
- 2. Create email templates in Email Designer
986
-
987
- 3. Send via template:
988
- ```javascript
989
- // Email Designer internally uses strapi.plugin('email').service('email').send()
990
- // MagicMail automatically intercepts it!
991
- ```
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).
992
559
 
993
- 4. Your email is automatically:
994
- - ✅ Routed through MagicMail's smart routing
995
- - ✅ Sent via the best account (based on rules)
996
- - ✅ Rate limited and failover protected
997
- - ✅ Logged and tracked
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).
998
561
 
999
- **No configuration needed - it just works!** 🎉
562
+ ### Example: send
1000
563
 
1001
- ---
1002
-
1003
- ## 🚨 Troubleshooting
1004
-
1005
- ### Email Not Received
1006
-
1007
- **1. Check Logs**
1008
564
  ```bash
1009
- # Look for errors in Strapi console
1010
- [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
+ }
1011
583
  ```
1012
584
 
1013
- **2. Check Account Status**
1014
- - Is the account marked as "Active"?
1015
- - Test the account using the Test button
1016
- - Check rate limits haven't been exceeded
1017
-
1018
- **3. Check Spam Folder**
1019
- - First emails from new accounts may land in spam
1020
- - Check spam/junk folders
1021
- - Mark as "Not Spam" to train filters
1022
-
1023
- **4. Verify Credentials**
1024
- - SMTP: Username and password correct?
1025
- - Gmail: Using App Password (not regular password)?
1026
- - OAuth: Tokens may have expired - reconnect
1027
-
1028
- ### DMARC Failures
585
+ ### Example: send-message
1029
586
 
1030
- **For Custom Domains:**
1031
- 1. Set up SPF record:
1032
- ```
1033
- v=spf1 include:_spf.google.com ~all
1034
- ```
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
+ ```
1035
593
 
1036
- 2. Set up DKIM:
1037
- - Generate DKIM key pair
1038
- - Add public key to DNS
1039
- - Add private key to MagicMail SMTP config
594
+ The response has the same shape as `/send`.
1040
595
 
1041
- 3. Set up DMARC:
1042
- ```
1043
- v=DMARC1; p=quarantine; rua=mailto:dmarc@yourdomain.com
1044
- ```
596
+ ### Errors
1045
597
 
1046
- **For OAuth Providers (Gmail, Microsoft, Yahoo):**
1047
- - ✅ DKIM handled automatically
1048
- - ✅ SPF handled automatically
1049
- - ✅ DMARC should pass automatically
598
+ Errors use Strapi's envelope:
1050
599
 
1051
- ### Rate Limit Errors
600
+ ```json
601
+ {
602
+ "data": null,
603
+ "error": { "status": 400, "name": "ValidationError", "message": "…", "details": {} }
604
+ }
605
+ ```
1052
606
 
1053
- **If you hit rate limits:**
1054
- - Increase daily/hourly limits in account settings
1055
- - Add more accounts for load balancing
1056
- - Set up routing rules to distribute load
1057
- - Check provider-specific limits:
1058
- - Gmail: 500/day (free), 2000/day (workspace)
1059
- - SendGrid: Varies by plan
1060
- - 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) |
1061
614
 
1062
- ### OAuth Connection Issues
615
+ ---
1063
616
 
1064
- **Gmail OAuth:**
1065
- - Ensure Gmail API is enabled
1066
- - Check redirect URI matches exactly
1067
- - Clear browser cookies and try again
617
+ ## Admin API
1068
618
 
1069
- **Microsoft OAuth:**
1070
- - Verify all 5 permissions are granted
1071
- - **Grant admin consent** (required!)
1072
- - Use correct Tenant ID
1073
- - 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.
1074
620
 
1075
- **Yahoo OAuth:**
1076
- - Verify app is approved by Yahoo
1077
- - 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` |
1078
638
 
1079
- ---
639
+ Test sends (`/accounts/:accountId/test`, `/test-strapi-service`, `/designer/templates/:id/test-send`) are limited to 10 per minute per admin.
1080
640
 
1081
- ## 🏗️ Architecture
641
+ ### Example: create a routing rule
1082
642
 
1083
- ### Plugin Structure
643
+ ```http
644
+ POST /magic-mail/routing-rules
645
+ Authorization: Bearer <admin JWT>
646
+ Content-Type: application/json
1084
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
+ }
1085
657
  ```
1086
- magic-mail/
1087
- ├── admin/ # React Admin UI
1088
- │ └── src/
1089
- │ ├── components/
1090
- │ │ ├── AddAccountModal.jsx # Multi-step account setup
1091
- │ │ └── PluginIcon.jsx # Sidebar icon
1092
- │ └── pages/
1093
- │ ├── App.jsx # Main app with tabs
1094
- │ ├── HomePage.jsx # Email accounts page
1095
- │ └── RoutingRules.jsx # Routing rules page
1096
- ├── server/ # Backend
1097
- │ └── src/
1098
- │ ├── bootstrap.js # Overrides Strapi email service
1099
- │ ├── controllers/
1100
- │ │ ├── accounts.js # CRUD for accounts
1101
- │ │ ├── oauth.js # OAuth flows
1102
- │ │ └── routing-rules.js # CRUD for routing rules
1103
- │ ├── services/
1104
- │ │ ├── email-router.js # Core routing logic
1105
- │ │ ├── account-manager.js # Account management
1106
- │ │ └── oauth.js # OAuth token exchange
1107
- │ ├── content-types/
1108
- │ │ ├── email-account/ # Account schema
1109
- │ │ ├── email-log/ # Email log schema
1110
- │ │ └── routing-rule/ # Routing rule schema
1111
- │ └── utils/
1112
- │ └── encryption.js # AES-256-GCM encryption
1113
- └── package.json
1114
- ```
1115
-
1116
- ### Data Flow
1117
658
 
1118
- ```
1119
- Email Request
1120
- ↓
1121
- Strapi Email Service (intercepted!)
1122
- ↓
1123
- MagicMail Email Router
1124
- ↓
1125
- Routing Rules Engine
1126
- ↓
1127
- Account Selection (priority/rules)
1128
- ↓
1129
- Rate Limit Check
1130
- ↓
1131
- Provider-Specific Sender
1132
- ↓
1133
- Email Log & Stats Update
1134
- ↓
1135
- Response
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
+ }
1136
670
  ```
1137
671
 
1138
- ---
1139
-
1140
- ## 🎯 Use Cases
1141
-
1142
- ### Multi-Tenant SaaS
1143
-
1144
- ```javascript
1145
- // Each tenant gets their own email account
1146
- // Route by custom field:
672
+ `matchType` is one of `emailType`, `recipient`, `subject`, `template`, `custom`. An unknown `accountName` or `fallbackAccountName` returns 400.
1147
673
 
1148
- // Routing Rule:
1149
- // Match Type: Custom Field
1150
- // Match Value: tenant-123
1151
- // Target Account: Tenant 123 Gmail
674
+ ### Example: list accounts
1152
675
 
1153
- await strapi.plugin('email').service('email').send({
1154
- to: 'customer@example.com',
1155
- subject: 'Order Confirmation',
1156
- html: '<h1>Order #456</h1>',
1157
- customField: 'tenant-123', // Matches routing rule
1158
- });
676
+ ```http
677
+ GET /magic-mail/accounts
1159
678
  ```
1160
679
 
1161
- ### High-Volume Email Sending
1162
-
1163
- ```javascript
1164
- // Distribute load across multiple accounts
1165
- // Account 1: Priority 10, Limit 500/day
1166
- // Account 2: Priority 9, Limit 500/day
1167
- // Account 3: Priority 8, Limit 500/day
1168
- // → 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
+ }
1169
699
  ```
1170
700
 
1171
- ### 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`).
1172
702
 
1173
- ```javascript
1174
- // Route marketing emails via SendGrid
1175
- // Routing Rule:
1176
- // Match Type: Email Type
1177
- // Match Value: marketing
1178
- // Target Account: SendGrid Marketing
703
+ ### Example: delivery log
1179
704
 
1180
- await strapi.plugin('email').service('email').send({
1181
- to: 'subscriber@example.com',
1182
- subject: 'Weekly Newsletter',
1183
- html: template,
1184
- type: 'marketing',
1185
- unsubscribeUrl: 'https://yoursite.com/unsubscribe?id=123',
1186
- });
1187
- // ✅ Automatically routes via SendGrid
1188
- // ✅ Adds List-Unsubscribe header
1189
- // ✅ GDPR/CAN-SPAM compliant
705
+ ```http
706
+ GET /magic-mail/analytics/emails?search=order&deliveryStatus=sent&page=1&pageSize=25
1190
707
  ```
1191
708
 
1192
- ### VIP Customer Emails
1193
-
1194
- ```javascript
1195
- // Route VIP customers via a priority account
1196
- // Routing Rule:
1197
- // Match Type: Recipient
1198
- // Match Value: @vip-domain.com
1199
- // Target Account: Priority SMTP
1200
- // Fallback Account: Gmail OAuth
1201
-
1202
- await strapi.plugin('email').service('email').send({
1203
- to: 'ceo@vip-domain.com',
1204
- subject: 'Exclusive Offer',
1205
- html: content,
1206
- priority: 'high',
1207
- });
1208
- // ✅ Routes via Priority SMTP
1209
- // ✅ High priority headers added
1210
- // ✅ 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
+ }
1211
724
  ```
1212
725
 
1213
- ---
1214
-
1215
- ## 🔌 Email Provider Comparison
1216
-
1217
- | Feature | Gmail OAuth | Microsoft OAuth | Yahoo OAuth | SMTP | SendGrid | Mailgun |
1218
- |---------|-------------|-----------------|-------------|------|----------|---------|
1219
- | **Auth Method** | OAuth 2.0 | OAuth 2.0 | OAuth 2.0 | Password | API Key | API Key |
1220
- | **Free Tier** | 500/day | Varies | Limited | Varies | 100/day | 5000/month |
1221
- | **Attachments** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
1222
- | **HTML Emails** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
1223
- | **DKIM Auto** | ✅ | ✅ | ✅ | Optional | ✅ | ✅ |
1224
- | **Custom Headers** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
1225
- | **Priority Headers** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
1226
- | **Security** | Excellent | Excellent | Good | Good | Excellent | Excellent |
1227
- | **Deliverability** | Excellent | Excellent | Good | Varies | Excellent | Excellent |
1228
- | **Best For** | Personal/Small | Enterprise | Personal | Custom | Marketing | Bulk/Dev |
726
+ `search` looks in recipient, subject and template name; `pageSize` is capped at 100.
1229
727
 
1230
728
  ---
1231
729
 
1232
- ## 🌍 Production Deployment
730
+ ## Permissions
1233
731
 
1234
- ### Environment Variables
732
+ MagicMail registers four admin actions under **Settings → Administration Panel → Roles → Plugins → MagicMail**:
1235
733
 
1236
- ```env
1237
- # Base URL (required for OAuth callbacks in production)
1238
- URL=https://yourdomain.com
1239
-
1240
- # Database (Strapi default)
1241
- DATABASE_CLIENT=postgres
1242
- DATABASE_HOST=127.0.0.1
1243
- DATABASE_PORT=5432
1244
- DATABASE_NAME=strapi
1245
- DATABASE_USERNAME=strapi
1246
- DATABASE_PASSWORD=password
1247
-
1248
- # Required in production. Keep this stable across deploys and backups.
1249
- # Generate: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
1250
- MAGIC_MAIL_ENCRYPTION_KEY=your-generated-secret-with-at-least-32-bytes
1251
-
1252
- # Optional dedicated OAuth-state signing secret. APP_KEYS is used otherwise.
1253
- MAGIC_MAIL_OAUTH_STATE_SECRET=your-generated-secret-with-at-least-32-bytes
1254
- ```
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 |
1255
740
 
1256
- ### OAuth Callback URLs
741
+ Typical roles:
1257
742
 
1258
- **Update redirect URIs in your OAuth apps:**
743
+ - **Read-only:** `access` + `read`
744
+ - **Editor:** `access` + `read` + `manage`
745
+ - **Tester:** `access` + `read` + `test-send`
1259
746
 
1260
- **Gmail:**
1261
- ```
1262
- https://yourdomain.com/magic-mail/oauth/gmail/callback
1263
- ```
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).
1264
748
 
1265
- **Microsoft:**
1266
- ```
1267
- https://yourdomain.com/magic-mail/oauth/microsoft/callback
1268
- ```
1269
-
1270
- **Yahoo:**
1271
- ```
1272
- https://yourdomain.com/magic-mail/oauth/yahoo/callback
1273
- ```
749
+ ---
1274
750
 
1275
- ### Recommended Provider Settings
751
+ ## Configuration reference
1276
752
 
1277
- **For Production Apps:**
753
+ ### Environment variables
1278
754
 
1279
- | Use Case | Recommended Provider | Configuration |
1280
- |----------|---------------------|---------------|
1281
- | **Transactional** | SendGrid or Mailgun | High daily limit, dedicated IP |
1282
- | **Marketing** | SendGrid | List management, analytics |
1283
- | **Notifications** | SMTP or Gmail OAuth | Medium limits, reliable |
1284
- | **Internal** | Microsoft 365 | Company domain, OAuth |
1285
- | **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 |
1286
764
 
1287
- ---
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.
1288
766
 
1289
- ## 📚 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.
1290
768
 
1291
- ### Custom Routing Logic
769
+ ### Plugin config
1292
770
 
1293
- Create complex routing rules:
771
+ All keys are optional and validated at startup (an invalid value stops Strapi with a clear message):
1294
772
 
1295
773
  ```javascript
1296
- // Rule 1: VIP customers via a priority account
1297
- {
1298
- name: "VIP Customer Emails",
1299
- matchType: "recipient",
1300
- matchValue: "@vip-customers.com",
1301
- accountName: "Priority SMTP",
1302
- fallbackAccountName: "Gmail OAuth",
1303
- priority: 10,
1304
- isActive: true
1305
- }
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
+ });
800
+ ```
1306
801
 
1307
- // Rule 2: Password resets via fast account
1308
- {
1309
- name: "Password Reset Emails",
1310
- matchType: "subject",
1311
- matchValue: "Password Reset",
1312
- accountName: "Fast Gmail",
1313
- priority: 9,
1314
- isActive: true
1315
- }
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.
1316
803
 
1317
- // Rule 3: Marketing via SendGrid
1318
- {
1319
- name: "Marketing Campaigns",
1320
- matchType: "emailType",
1321
- matchValue: "marketing",
1322
- accountName: "SendGrid Marketing",
1323
- priority: 8,
1324
- isActive: true
1325
- }
1326
- ```
804
+ ### Admin settings
1327
805
 
1328
- ### DKIM Configuration (Advanced)
806
+ **Settings → MagicMail → Plugin Settings**, stored in the database (`GET`/`PUT /magic-mail/settings`):
1329
807
 
1330
- For custom domains with SMTP:
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 |
1331
817
 
1332
- **1. Generate DKIM Keys:**
1333
- ```bash
1334
- openssl genrsa -out dkim_private.pem 1024
1335
- openssl rsa -in dkim_private.pem -pubout -out dkim_public.pem
1336
- ```
818
+ ### Account fields
1337
819
 
1338
- **2. Add to DNS:**
1339
- ```
1340
- default._domainkey.yourdomain.com TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA..."
1341
- ```
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.
821
+
822
+ ### Service API
1342
823
 
1343
- **3. Add to MagicMail SMTP Account:**
1344
824
  ```javascript
1345
- // In account config (edit account → credentials):
1346
- {
1347
- host: "smtp.yourdomain.com",
1348
- port: 587,
1349
- user: "...",
1350
- pass: "...",
1351
- dkim: {
1352
- domainName: "yourdomain.com",
1353
- keySelector: "default",
1354
- privateKey: "-----BEGIN PRIVATE KEY-----\n..."
1355
- }
1356
- }
1357
- ```
825
+ const router = strapi.plugin('magic-mail').service('email-router');
826
+ await router.send({ to, subject, html });
1358
827
 
1359
- ### Rate Limit Strategy
828
+ const accounts = await strapi.plugin('magic-mail').service('account-manager').getAllAccounts();
1360
829
 
1361
- **Best practices for high-volume sending:**
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' });
1362
835
 
1363
- ```javascript
1364
- // Account Setup:
1365
- // Account A: Priority 10, Daily Limit: 500
1366
- // Account B: Priority 9, Daily Limit: 500
1367
- // Account C: Priority 8, Daily Limit: 500
1368
- // → Total capacity: 1500 emails/day
1369
-
1370
- // MagicMail automatically:
1371
- // 1. Uses Account A until limit hit
1372
- // 2. Switches to Account B
1373
- // 3. Switches to Account C
1374
- // 4. All transparent to your code!
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, ... }
1375
839
  ```
1376
840
 
1377
841
  ---
1378
842
 
1379
- ## 🤝 Contributing
843
+ ## Troubleshooting
1380
844
 
1381
- We welcome contributions!
845
+ **Email not received**
1382
846
 
1383
- **Found a bug?** [Open an issue](https://github.com/Schero94/MagicMail/issues)
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.
1384
851
 
1385
- **Have a feature idea?** [Start a discussion](https://github.com/Schero94/MagicMail/discussions)
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.
1386
853
 
1387
- **Want to contribute code?**
1388
- 1. Fork the repo
1389
- 2. Create a feature branch
1390
- 3. Make your changes
1391
- 4. Submit a pull request
854
+ **OAuth popup fails or reports a redirect mismatch**
1392
855
 
1393
- ---
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.
1394
861
 
1395
- ## License
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.
1396
863
 
1397
- **MIT License**
864
+ **Content API returns 403**: use an API token with the `send`/`sendMessage` action; users-permissions roles are rejected unless `contentApi.allowUserRoles` is true.
1398
865
 
1399
- Copyright © 2025 Schero D.
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.
1400
867
 
1401
- 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:
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.
1402
869
 
1403
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
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.
1404
871
 
1405
- MagicMail is fully free and open source. Every feature is available to everyone
1406
- with no license key, tier, activation, or usage limit.
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.
1407
873
 
1408
- 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.
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).
1409
875
 
1410
876
  ---
1411
877
 
1412
- ## 🙏 Credits
878
+ ## Upgrading to 4.0.0
1413
879
 
1414
- **Built with:**
1415
- - [Strapi v5](https://strapi.io) - Headless CMS
1416
- - [Nodemailer](https://nodemailer.com) - SMTP client
1417
- - [Heroicons](https://heroicons.com) - Beautiful icons
1418
- - [styled-components](https://styled-components.com) - CSS-in-JS
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).
1419
881
 
1420
- **Inspired by:**
1421
- - Professional email requirements
1422
- - Multi-tenant application needs
1423
- - Developer experience first
882
+ Breaking changes and what to do:
1424
883
 
1425
- ---
1426
-
1427
- ## 💬 Support
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.
1428
898
 
1429
- **Need help?**
1430
- - 📖 [Documentation](https://github.com/Schero94/Magic-Mail#readme)
1431
- - 🐛 [Report Issues](https://github.com/Schero94/Magic-Mail/issues)
1432
- - 💡 [Feature Requests](https://github.com/Schero94/Magic-Mail/discussions)
899
+ ---
1433
900
 
901
+ ## Development
1434
902
 
1435
- ---
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
911
+ ```
1436
912
 
1437
- ## ⭐ Show Your Support
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.
1438
914
 
1439
- If MagicMail helps your project, please:
1440
- - ⭐ Star the repo on [GitHub](https://github.com/Schero94/Magic-Mail)
1441
- - 📢 Share with your team
1442
- - 🐦 Tweet about it
915
+ - **Bug?** [Open an issue](https://github.com/Schero94/Magic-Mail/issues)
916
+ - **Idea?** [Start a discussion](https://github.com/Schero94/Magic-Mail/discussions)
1443
917
 
1444
- ---
918
+ ### Dependency audits
1445
919
 
1446
- ## Dependency audits
920
+ Run `npm audit` from your Strapi project root to check the complete installed tree; npm applies overrides from the root project only.
1447
921
 
1448
- Run `npm audit` from the Strapi project root to check the complete installed dependency tree. npm applies dependency overrides from the root project manifest, not from this plugin's manifest.
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.
1449
923
 
1450
924
  ---
1451
925
 
1452
- **Made for the Strapi Community**
926
+ ## License
927
+
928
+ MIT License, Copyright © 2025 Schero D. See [LICENSE](./LICENSE).
1453
929
 
1454
- **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).