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.
- package/CHANGELOG.md +178 -0
- package/README.md +630 -1154
- package/dist/admin/App-Bi6mzEff.mjs +10973 -0
- package/dist/admin/App-aaEU9cpu.js +10976 -0
- package/dist/admin/PluginSettings-BR2-PVHx.mjs +705 -0
- package/dist/admin/PluginSettings-DbTkOzCh.js +707 -0
- package/dist/admin/analyticsLogs-BCY0Zv3Y.mjs +685 -0
- package/dist/admin/analyticsLogs-DNU09hoB.js +686 -0
- package/dist/admin/de-B5bPRK6l.js +717 -0
- package/dist/admin/de-BqYqMhOn.mjs +717 -0
- package/dist/admin/en-8xANOQdO.mjs +717 -0
- package/dist/admin/en-Bk5eVrtc.js +717 -0
- package/dist/admin/es-BfPsCsam.js +717 -0
- package/dist/admin/es-DgpTufJ_.mjs +717 -0
- package/dist/admin/fr-BuotP0Gc.mjs +717 -0
- package/dist/admin/fr-IQ3KPUoS.js +717 -0
- package/dist/admin/index-QGqbGw6k.js +102 -0
- package/dist/admin/index-qvFJ-a0Q.mjs +103 -0
- package/dist/admin/index.js +2 -99
- package/dist/admin/index.mjs +2 -99
- package/dist/admin/pt-BI63Lncs.js +717 -0
- package/dist/admin/pt-DBZu2cDY.mjs +717 -0
- package/dist/server/index.js +4623 -2214
- package/dist/server/index.mjs +4623 -2214
- package/package.json +30 -19
- package/strapi-admin.js +3 -2
- package/strapi-server.js +3 -2
- package/dist/admin/App-Bmav09Ol.mjs +0 -7156
- package/dist/admin/App-mX8QtU1Q.js +0 -7177
- package/dist/admin/PluginSettings-D5F5y7n2.mjs +0 -386
- package/dist/admin/PluginSettings-DE2PMxlS.js +0 -388
- package/dist/admin/StyledButtons-aq1EWrQ-.mjs +0 -433
- package/dist/admin/StyledButtons-jHF7TXal.js +0 -434
- package/dist/admin/de-BXyjQhJQ.mjs +0 -43
- package/dist/admin/de-VtWzTjWV.js +0 -43
- package/dist/admin/en-DL_aZYfg.js +0 -43
- package/dist/admin/en-DMdnprlB.mjs +0 -43
- package/dist/admin/es-BJ1KgkMA.js +0 -43
- package/dist/admin/es-g3CHX4iu.mjs +0 -43
- package/dist/admin/fr-Bmh5-45j.js +0 -43
- package/dist/admin/fr-CN1X3dM-.mjs +0 -43
- package/dist/admin/pt-DJ6GNCKb.js +0 -43
- package/dist/admin/pt-m8_Gnsqe.mjs +0 -43
- package/pics/EmailAccountDashboard.png +0 -0
- package/pics/Emailaddccount1.png +0 -0
- package/pics/RoutingEmailTemplates.png +0 -0
- package/pics/Templates.png +0 -0
- package/pics/codesnippet.png +0 -0
- package/pics/emailAnalytics.png +0 -0
- package/pics/emailDesigner.png +0 -0
- package/pics/emailTemplateSendTest.png +0 -0
- package/pics/emailVersionHistory.png +0 -0
- package/pics/emailaddaccount2.png +0 -0
- package/pics/emailaddaccount3.png +0 -0
package/README.md
CHANGED
|
@@ -1,1454 +1,930 @@
|
|
|
1
|
-
#
|
|
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
|
[](https://www.npmjs.com/package/strapi-plugin-magic-mail)
|
|
8
6
|
[](LICENSE)
|
|
9
|
-
[-blue.svg>)](https://strapi.io)
|
|
10
8
|
|
|
11
|
-
|
|
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
|
-
|
|
15
|
+
**At a glance**
|
|
16
16
|
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
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
|
-
##
|
|
44
|
+
## Screenshots
|
|
33
45
|
|
|
34
|
-
|
|
35
|
-

|
|
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
|
-
###
|
|
39
|
-

|
|
40
|
-
*Choose from 6 different email providers with OAuth 2.0 support*
|
|
48
|
+
### Email accounts
|
|
41
49
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
*Configure SMTP settings or connect with OAuth*
|
|
50
|
+

|
|
51
|
+
_Accounts with status, provider, priority, today's usage against the daily limit, and test/edit/delete actions._
|
|
45
52
|
|
|
46
|
-
###
|
|
47
|
-

|
|
48
|
-
*Complete setup with rate limits and test connection*
|
|
53
|
+
### Account wizard
|
|
49
54
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
*Create and manage email templates with the visual designer*
|
|
55
|
+

|
|
56
|
+
_Four steps: provider, details, credentials, settings. OAuth providers connect in a popup._
|
|
53
57
|
|
|
54
|
-
###
|
|
55
|
-

|
|
56
|
-
*Drag-and-drop email editor with real-time preview*
|
|
58
|
+
### Routing rules
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
*Track changes and restore previous template versions*
|
|
60
|
+

|
|
61
|
+
_Rules in evaluation order. Rules that point to a missing or inactive account are flagged._
|
|
61
62
|
|
|
62
|
-
###
|
|
63
|
-

|
|
64
|
-
*Test your templates before sending to production*
|
|
63
|
+
### Templates
|
|
65
64
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
*Define intelligent routing rules based on email type, recipient, subject*
|
|
65
|
+

|
|
66
|
+
_Templates with Reference ID, category and status; edit, download as HTML or JSON, duplicate, code snippets, test send and delete._
|
|
69
67
|
|
|
70
|
-
###
|
|
71
|
-

|
|
72
|
-
*Track email performance with detailed analytics and statistics*
|
|
68
|
+
### Template editor
|
|
73
69
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
*Simple API integration with your Strapi application*
|
|
70
|
+

|
|
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
|
-
|
|
75
|
+

|
|
76
|
+
_Totals, delivery log with server-side search, opens and clicks per email._
|
|
81
77
|
|
|
82
|
-
|
|
78
|
+
### Plugin settings
|
|
83
79
|
|
|
84
|
-
|
|
80
|
+

|
|
81
|
+
_Settings → MagicMail → Plugin Settings: link and open tracking, tracking base URL, unsubscribe header, default sender name._
|
|
85
82
|
|
|
86
|
-
|
|
83
|
+
### Dark mode and mobile
|
|
84
|
+
|
|
85
|
+

|
|
86
|
+
|
|
87
|
+
| Accounts at 390 px | Routing rules at 390 px |
|
|
88
|
+
| ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
89
|
+
|  |  |
|
|
87
90
|
|
|
88
91
|
---
|
|
89
92
|
|
|
90
|
-
## Quick
|
|
93
|
+
## Quick start
|
|
94
|
+
|
|
95
|
+
### Requirements
|
|
91
96
|
|
|
92
|
-
|
|
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
|
-
|
|
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
|
|
108
|
+
### 2. Enable
|
|
103
109
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
###
|
|
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
|
|
139
|
+
### 5. Add an account and send a test
|
|
122
140
|
|
|
123
|
-
1.
|
|
124
|
-
2.
|
|
125
|
-
3.
|
|
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
|
-
|
|
145
|
+
Every existing `strapi.plugin('email')` call now goes through MagicMail.
|
|
133
146
|
|
|
134
|
-
|
|
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
|
-
##
|
|
151
|
+
## Sending email
|
|
146
152
|
|
|
147
|
-
###
|
|
153
|
+
### Option 1: the Strapi email service (recommended)
|
|
148
154
|
|
|
149
|
-
MagicMail
|
|
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
|
|
156
|
-
text: '
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
###
|
|
170
|
+
### Option 2: the MagicMail router service
|
|
175
171
|
|
|
176
|
-
|
|
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
|
|
183
|
-
html: '<h1>Your
|
|
184
|
-
accountName: '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
192
|
+
### Send options
|
|
219
193
|
|
|
220
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
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
|
-
|
|
236
|
+
### Unsubscribe and bulk headers
|
|
281
237
|
|
|
282
|
-
|
|
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
|
-
|
|
243
|
+
---
|
|
285
244
|
|
|
286
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
259
|
+
**Reconnecting an account** (revoked consent, expired refresh token): edit the account and use **Reconnect with <provider>** 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
|
-
|
|
372
|
-
const templates = await strapi
|
|
373
|
-
.plugin('magic-mail')
|
|
374
|
-
.service('email-designer')
|
|
375
|
-
.exportTemplates();
|
|
261
|
+
### Gmail (OAuth 2.0)
|
|
376
262
|
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
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
|
-
|
|
384
|
-
const templates = JSON.parse(fs.readFileSync('templates.json'));
|
|
270
|
+
### Microsoft 365 (OAuth 2.0)
|
|
385
271
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
281
|
+
### Yahoo Mail (OAuth 2.0)
|
|
395
282
|
|
|
396
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
290
|
+
### SMTP
|
|
409
291
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
323
|
+
### Mailgun
|
|
466
324
|
|
|
467
|
-
|
|
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
|
-
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
337
|
+
### Provider comparison
|
|
495
338
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
|
|
348
|
+
All providers support attachments, inline images (`cid`) and custom headers.
|
|
501
349
|
|
|
502
350
|
---
|
|
503
351
|
|
|
504
|
-
##
|
|
352
|
+
## Routing rules and quotas
|
|
505
353
|
|
|
506
|
-
|
|
354
|
+
Routing rules send an email through a specific account when it matches a condition. Manage them under **MagicMail → Routing Rules**.
|
|
507
355
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
398
|
+
### Creating templates
|
|
594
399
|
|
|
595
|
-
**
|
|
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
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
413
|
+
### Sending a template
|
|
616
414
|
|
|
617
|
-
For custom domains, you can add DKIM:
|
|
618
415
|
```javascript
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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
|
-
###
|
|
754
|
-
|
|
755
|
-
Each account can have:
|
|
448
|
+
### Users & Permissions templates
|
|
756
449
|
|
|
757
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
777
|
-
-
|
|
778
|
-
-
|
|
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
|
-
|
|
783
|
-
|
|
784
|
-
### Send Email (Standard Method)
|
|
462
|
+
Optional overrides (`config/plugins.js`):
|
|
785
463
|
|
|
786
464
|
```javascript
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
834
|
-
|
|
835
|
-
'
|
|
483
|
+
module.exports = [
|
|
484
|
+
'strapi::logger',
|
|
485
|
+
'strapi::errors',
|
|
836
486
|
{
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
534
|
+
## Content API
|
|
936
535
|
|
|
937
|
-
|
|
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
|
-
|
|
940
|
-
- Emails sent today (all accounts)
|
|
941
|
-
- Total emails sent (lifetime)
|
|
942
|
-
- Active accounts count
|
|
543
|
+
### The API token rule
|
|
943
544
|
|
|
944
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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
|
-
|
|
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
|
-
|
|
562
|
+
### Example: send
|
|
1000
563
|
|
|
1001
|
-
---
|
|
1002
|
-
|
|
1003
|
-
## 🚨 Troubleshooting
|
|
1004
|
-
|
|
1005
|
-
### Email Not Received
|
|
1006
|
-
|
|
1007
|
-
**1. Check Logs**
|
|
1008
564
|
```bash
|
|
1009
|
-
|
|
1010
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1042
|
-
```
|
|
1043
|
-
v=DMARC1; p=quarantine; rua=mailto:dmarc@yourdomain.com
|
|
1044
|
-
```
|
|
596
|
+
### Errors
|
|
1045
597
|
|
|
1046
|
-
|
|
1047
|
-
- ✅ DKIM handled automatically
|
|
1048
|
-
- ✅ SPF handled automatically
|
|
1049
|
-
- ✅ DMARC should pass automatically
|
|
598
|
+
Errors use Strapi's envelope:
|
|
1050
599
|
|
|
1051
|
-
|
|
600
|
+
```json
|
|
601
|
+
{
|
|
602
|
+
"data": null,
|
|
603
|
+
"error": { "status": 400, "name": "ValidationError", "message": "…", "details": {} }
|
|
604
|
+
}
|
|
605
|
+
```
|
|
1052
606
|
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
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
|
-
|
|
615
|
+
---
|
|
1063
616
|
|
|
1064
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
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
|
-
|
|
641
|
+
### Example: create a routing rule
|
|
1082
642
|
|
|
1083
|
-
|
|
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
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
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
|
-
|
|
1149
|
-
// Match Type: Custom Field
|
|
1150
|
-
// Match Value: tenant-123
|
|
1151
|
-
// Target Account: Tenant 123 Gmail
|
|
674
|
+
### Example: list accounts
|
|
1152
675
|
|
|
1153
|
-
|
|
1154
|
-
|
|
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
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1181
|
-
|
|
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
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
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
|
-
##
|
|
730
|
+
## Permissions
|
|
1233
731
|
|
|
1234
|
-
|
|
732
|
+
MagicMail registers four admin actions under **Settings → Administration Panel → Roles → Plugins → MagicMail**:
|
|
1235
733
|
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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
|
-
|
|
741
|
+
Typical roles:
|
|
1257
742
|
|
|
1258
|
-
**
|
|
743
|
+
- **Read-only:** `access` + `read`
|
|
744
|
+
- **Editor:** `access` + `read` + `manage`
|
|
745
|
+
- **Tester:** `access` + `read` + `test-send`
|
|
1259
746
|
|
|
1260
|
-
|
|
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
|
-
|
|
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
|
-
|
|
751
|
+
## Configuration reference
|
|
1276
752
|
|
|
1277
|
-
|
|
753
|
+
### Environment variables
|
|
1278
754
|
|
|
1279
|
-
|
|
|
1280
|
-
|
|
1281
|
-
|
|
|
1282
|
-
|
|
|
1283
|
-
|
|
|
1284
|
-
|
|
|
1285
|
-
|
|
|
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
|
-
|
|
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
|
-
###
|
|
769
|
+
### Plugin config
|
|
1292
770
|
|
|
1293
|
-
|
|
771
|
+
All keys are optional and validated at startup (an invalid value stops Strapi with a clear message):
|
|
1294
772
|
|
|
1295
773
|
```javascript
|
|
1296
|
-
//
|
|
1297
|
-
{
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
806
|
+
**Settings → MagicMail → Plugin Settings**, stored in the database (`GET`/`PUT /magic-mail/settings`):
|
|
1329
807
|
|
|
1330
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
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
|
-
|
|
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
|
-
|
|
828
|
+
const accounts = await strapi.plugin('magic-mail').service('account-manager').getAllAccounts();
|
|
1360
829
|
|
|
1361
|
-
|
|
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
|
-
|
|
1364
|
-
|
|
1365
|
-
//
|
|
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
|
-
##
|
|
843
|
+
## Troubleshooting
|
|
1380
844
|
|
|
1381
|
-
|
|
845
|
+
**Email not received**
|
|
1382
846
|
|
|
1383
|
-
|
|
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
|
-
**
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
878
|
+
## Upgrading to 4.0.0
|
|
1413
879
|
|
|
1414
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1440
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
926
|
+
## License
|
|
927
|
+
|
|
928
|
+
MIT License, Copyright © 2025 Schero D. See [LICENSE](./LICENSE).
|
|
1453
929
|
|
|
1454
|
-
|
|
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).
|