thormail-adapter-generic-rest 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -0
- package/index.js +330 -0
- package/package.json +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# ThorMail Generic REST API Adapter
|
|
2
|
+
|
|
3
|
+
A highly configurable adapter for [ThorMail](https://thormail.io) that allows sending messages to any generic REST API.
|
|
4
|
+
|
|
5
|
+
## 📖 Overview
|
|
6
|
+
|
|
7
|
+
This adapter provides a flexible bridge between ThorMail and any external service that accepts HTTP requests (POST, PUT, PATCH, GET). It is ideal for:
|
|
8
|
+
|
|
9
|
+
- Integrating with bespoke internal APIs.
|
|
10
|
+
- Connecting to small providers without dedicated adapters.
|
|
11
|
+
- Prototyping new integrations quickly.
|
|
12
|
+
- Webhook-based messaging.
|
|
13
|
+
|
|
14
|
+
## 🛠️ Configuration
|
|
15
|
+
|
|
16
|
+
When creating a new Connection in ThorMail, select **Generic REST API** and configure the following fields:
|
|
17
|
+
|
|
18
|
+
| Field | Type | Description |
|
|
19
|
+
|-------|------|-------------|
|
|
20
|
+
| **Base URL** | `text` | The full HTTP(S) endpoint of the external API. |
|
|
21
|
+
| **HTTP Method**| `select` | The method to use (POST, PUT, PATCH, GET). |
|
|
22
|
+
| **Custom Headers** | `customHeaders` | Headers required for authentication or metadata (e.g., `Authorization`). |
|
|
23
|
+
| **Payload Template** | `json-template` | A JSON template with `{{to}}`, `{{subject}}`, and `{{body}}` placeholders. |
|
|
24
|
+
| **Success Status Code(s)** | `text` | Comma-separated list of successful HTTP codes (default: `200,201,202`). |
|
|
25
|
+
|
|
26
|
+
### Payload Replacements
|
|
27
|
+
|
|
28
|
+
You can use the following variables in your **Payload Template**:
|
|
29
|
+
|
|
30
|
+
- `{{to}}`: The recipient's identifier (address, phone, ID).
|
|
31
|
+
- `{{subject}}`: The message subject or title.
|
|
32
|
+
- `{{body}}`: The main content of the message.
|
|
33
|
+
- `{{any_data_key}}`: Any key passed in the `data` object during the send request.
|
|
34
|
+
|
|
35
|
+
### Example Configuration: Simple Webhook
|
|
36
|
+
|
|
37
|
+
- **Base URL**: `https://hooks.example.com/services/T0000/B0000/XXXX`
|
|
38
|
+
- **Method**: `POST`
|
|
39
|
+
- **Payload Template**:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"text": "New Mail to {{to}}: {{subject}}",
|
|
44
|
+
"details": "{{body}}"
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Example Configuration: authenticated API
|
|
49
|
+
|
|
50
|
+
- **Base URL**: `https://api.provider.com/v1/messages`
|
|
51
|
+
- **Method**: `POST`
|
|
52
|
+
- **Custom Headers**:
|
|
53
|
+
- `X-API-Key`: `your-secret-key`
|
|
54
|
+
- `Content-Type`: `application/json`
|
|
55
|
+
- **Payload Template**:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"recipient": "{{to}}",
|
|
60
|
+
"title": "{{subject}}",
|
|
61
|
+
"html_body": "{{body}}",
|
|
62
|
+
"metadata": {
|
|
63
|
+
"source": "thormail"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 🚀 Features
|
|
69
|
+
|
|
70
|
+
- **Standard Idempotency**: Automatically sends `Idempotency-Key` or `X-Idempotency-Key` headers if available.
|
|
71
|
+
- **Smart Retries**: Correctly identifies 429 and 5xx errors as temporary for ThorMail's retry logic.
|
|
72
|
+
- **Custom Success Logic**: Configurable success status codes to match legacy or non-standard APIs.
|
|
73
|
+
- **Dynamic Headers**: Merges headers provided in the specific send request with the global configuration.
|
|
74
|
+
|
|
75
|
+
## 📜 License
|
|
76
|
+
|
|
77
|
+
This adapter is licensed under the MIT License. See the main ThorMail repository for details.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
*Powered by [ThorMail](https://thormail.io) - The God of Deliveries.*
|
package/index.js
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic REST API Adapter for ThorMail
|
|
3
|
+
* Allows sending messages via any RESTful API with configurable endpoints and payloads.
|
|
4
|
+
*/
|
|
5
|
+
export default class GenericRestAdapter {
|
|
6
|
+
/**
|
|
7
|
+
* Defines the configuration form schema for the frontend.
|
|
8
|
+
*/
|
|
9
|
+
static getConfigSchema() {
|
|
10
|
+
return [
|
|
11
|
+
{
|
|
12
|
+
name: 'custom_name',
|
|
13
|
+
label: 'Name',
|
|
14
|
+
type: 'text',
|
|
15
|
+
required: false,
|
|
16
|
+
placeholder: 'My Generic API',
|
|
17
|
+
hint: 'Internal name for this adapter instance.',
|
|
18
|
+
group: 'settings'
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'baseUrl',
|
|
22
|
+
label: 'Base URL',
|
|
23
|
+
type: 'text',
|
|
24
|
+
required: true,
|
|
25
|
+
placeholder: 'https://api.myservice.com/v1/send',
|
|
26
|
+
hint: 'The full URL endpoint where the request will be sent.',
|
|
27
|
+
group: 'connection'
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'method',
|
|
31
|
+
label: 'HTTP Method',
|
|
32
|
+
type: 'select',
|
|
33
|
+
required: true,
|
|
34
|
+
options: ['POST', 'PUT', 'PATCH', 'GET'],
|
|
35
|
+
placeholder: 'POST',
|
|
36
|
+
hint: 'The HTTP method to use for sending the message.',
|
|
37
|
+
group: 'connection'
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'customHeaders',
|
|
41
|
+
label: 'Custom Headers',
|
|
42
|
+
type: 'customHeaders',
|
|
43
|
+
required: false,
|
|
44
|
+
hint: 'Optional HTTP headers to include in the request (e.g., Authorization, X-API-Key).',
|
|
45
|
+
group: 'authentication'
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'payloadTemplate',
|
|
49
|
+
label: 'Payload Template',
|
|
50
|
+
type: 'json-template',
|
|
51
|
+
required: false,
|
|
52
|
+
placeholder: '{\n "recipient": "{{to}}",\n "content": "{{body}}"\n}',
|
|
53
|
+
hint: 'A JSON template for the request body. Values like {{to}}, {{subject}}, and {{body}} will be automatically replaced.',
|
|
54
|
+
group: 'payload'
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'successStatus',
|
|
58
|
+
label: 'Success Status Code(s)',
|
|
59
|
+
type: 'text',
|
|
60
|
+
required: false,
|
|
61
|
+
placeholder: '200,201,202',
|
|
62
|
+
hint: 'Comma-separated list of HTTP status codes to treat as success. Defaults to 200, 201, 202.',
|
|
63
|
+
group: 'validation'
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'from_email',
|
|
67
|
+
label: 'From Email',
|
|
68
|
+
type: 'text',
|
|
69
|
+
required: false,
|
|
70
|
+
placeholder: 'noreply@yourdomain.com',
|
|
71
|
+
hint: 'Default sender email address if required by the destination API.',
|
|
72
|
+
group: 'defaults'
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'from_name',
|
|
76
|
+
label: 'From Name',
|
|
77
|
+
type: 'text',
|
|
78
|
+
required: false,
|
|
79
|
+
placeholder: 'My App',
|
|
80
|
+
hint: 'Default sender name.',
|
|
81
|
+
group: 'defaults'
|
|
82
|
+
}
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Defines adapter metadata for the registry.
|
|
88
|
+
*/
|
|
89
|
+
static getMetadata() {
|
|
90
|
+
return {
|
|
91
|
+
name: 'Generic REST API',
|
|
92
|
+
description: 'Send messages via any generic REST API with custom payloads and headers.',
|
|
93
|
+
group: 'generic'
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {Object} config - The configuration object.
|
|
99
|
+
*/
|
|
100
|
+
constructor(config) {
|
|
101
|
+
this.config = config;
|
|
102
|
+
this.successCodes = (config.successStatus || '200,201,202')
|
|
103
|
+
.split(',')
|
|
104
|
+
.map(code => parseInt(code.trim(), 10));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Internal helper to make HTTP requests.
|
|
109
|
+
*/
|
|
110
|
+
async _request(method, url, body = null, headers = {}) {
|
|
111
|
+
const fetchHeaders = { ...headers };
|
|
112
|
+
|
|
113
|
+
// Default Content-Type if body is provided and not already set
|
|
114
|
+
if (body && !Object.keys(fetchHeaders).some(h => h.toLowerCase() === 'content-type')) {
|
|
115
|
+
fetchHeaders['Content-Type'] = 'application/json';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const fetchOptions = {
|
|
119
|
+
method,
|
|
120
|
+
headers: fetchHeaders
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (body && method !== 'GET' && method !== 'HEAD') {
|
|
124
|
+
fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const response = await fetch(url, fetchOptions);
|
|
129
|
+
const status = response.status;
|
|
130
|
+
let responseData;
|
|
131
|
+
const text = await response.text();
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
responseData = text ? JSON.parse(text) : {};
|
|
135
|
+
} catch (e) {
|
|
136
|
+
responseData = { raw: text };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const success = this.successCodes.includes(status);
|
|
140
|
+
|
|
141
|
+
if (!success) {
|
|
142
|
+
// Determine if temporary error
|
|
143
|
+
// 429 (Rate Limit) and 5xx (Server Error) are usually temporary
|
|
144
|
+
const isTemporary = status === 429 || (status >= 500 && status < 600);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
success: false,
|
|
148
|
+
error: `HTTP ${status}: ${text.substring(0, 500)}`,
|
|
149
|
+
isTemporary,
|
|
150
|
+
status,
|
|
151
|
+
response: responseData
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return { success: true, data: responseData, status };
|
|
156
|
+
} catch (error) {
|
|
157
|
+
return {
|
|
158
|
+
success: false,
|
|
159
|
+
error: `Network/Fetch Error: ${error.message}`,
|
|
160
|
+
isTemporary: true
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Validates configuration by attempting a request to the base URL.
|
|
167
|
+
*/
|
|
168
|
+
async validateConfig() {
|
|
169
|
+
try {
|
|
170
|
+
// For validation, we use the method configured, but if it's POST/PUT/PATCH we don't send a body.
|
|
171
|
+
// Some APIs might reject this, so we try a simple request.
|
|
172
|
+
const validationHeaders = this._parseHeaders(this.config.customHeaders);
|
|
173
|
+
|
|
174
|
+
// If the user provided a success status, we check if the base URL responds with one of them.
|
|
175
|
+
// Note: This is an optimistic check.
|
|
176
|
+
const result = await this._request(this.config.method || 'GET', this.config.baseUrl, null, validationHeaders);
|
|
177
|
+
|
|
178
|
+
if (result.success) {
|
|
179
|
+
return {
|
|
180
|
+
success: true,
|
|
181
|
+
message: `Successfully reached the endpoint (Status ${result.status}).`,
|
|
182
|
+
canValidate: true
|
|
183
|
+
};
|
|
184
|
+
} else {
|
|
185
|
+
return {
|
|
186
|
+
success: false,
|
|
187
|
+
message: `Endpoint returned error: ${result.error}`,
|
|
188
|
+
canValidate: true
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
} catch (error) {
|
|
192
|
+
return {
|
|
193
|
+
success: false,
|
|
194
|
+
message: `Validation failed: ${error.message}`,
|
|
195
|
+
canValidate: true
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Checks if the external service is reachable.
|
|
202
|
+
*/
|
|
203
|
+
async healthCheck() {
|
|
204
|
+
const result = await this.validateConfig();
|
|
205
|
+
return result.success ? 'HEALTHY' : 'UNHEALTHY';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Dispatches a message via the configured REST API.
|
|
210
|
+
* @param {Object} params - { to, subject, body, data, idempotencyKey }
|
|
211
|
+
*/
|
|
212
|
+
async sendMail({ to, subject, body, data, idempotencyKey }) {
|
|
213
|
+
try {
|
|
214
|
+
const method = this.config.method || 'POST';
|
|
215
|
+
const url = this.config.baseUrl;
|
|
216
|
+
const headers = this._parseHeaders(this.config.customHeaders);
|
|
217
|
+
|
|
218
|
+
// Merge headers from the send request if any
|
|
219
|
+
if (data && data.headers) {
|
|
220
|
+
Object.assign(headers, data.headers);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Handle Idempotency Key
|
|
224
|
+
if (idempotencyKey) {
|
|
225
|
+
// Common headers for idempotency, can be overridden by customHeaders
|
|
226
|
+
const idHeaders = ['Idempotency-Key', 'X-Idempotency-Key', 'X-Request-Id'];
|
|
227
|
+
for (const h of idHeaders) {
|
|
228
|
+
if (!Object.keys(headers).some(key => key.toLowerCase() === h.toLowerCase())) {
|
|
229
|
+
headers[h] = idempotencyKey;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let payload;
|
|
235
|
+
|
|
236
|
+
if (this.config.payloadTemplate) {
|
|
237
|
+
// If a template is provided, we replace variables.
|
|
238
|
+
// ThorMail core usually provides data with replacements,
|
|
239
|
+
// but we also have to, subject, body specifically.
|
|
240
|
+
let templateStr = this.config.payloadTemplate;
|
|
241
|
+
const replacements = {
|
|
242
|
+
...data,
|
|
243
|
+
to,
|
|
244
|
+
subject,
|
|
245
|
+
body,
|
|
246
|
+
from_email: this.config.from_email || (data && data.from_email),
|
|
247
|
+
from_name: this.config.from_name || (data && data.from_name)
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// Simple mustache-like replacement: {{variable}}
|
|
251
|
+
payload = templateStr.replace(/\{\{([^}]+)\}\}/g, (match, p1) => {
|
|
252
|
+
const key = p1.trim();
|
|
253
|
+
return replacements[key] !== undefined ? replacements[key] : match;
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Attempt to parse as JSON if it looks like one
|
|
257
|
+
try {
|
|
258
|
+
payload = JSON.parse(payload);
|
|
259
|
+
} catch (e) {
|
|
260
|
+
// Keep as string if not valid JSON
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
// Default payload if no template is provided
|
|
264
|
+
payload = {
|
|
265
|
+
to,
|
|
266
|
+
subject,
|
|
267
|
+
content: body,
|
|
268
|
+
data
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const result = await this._request(method, url, payload, headers);
|
|
273
|
+
|
|
274
|
+
if (!result.success) {
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Extract ID if possible from response
|
|
279
|
+
const responseId = result.data?.id || result.data?.messageId || result.data?.result?.id || `rest-${Date.now()}`;
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
success: true,
|
|
283
|
+
id: String(responseId),
|
|
284
|
+
response: result.data,
|
|
285
|
+
isTemporary: false
|
|
286
|
+
};
|
|
287
|
+
} catch (error) {
|
|
288
|
+
return {
|
|
289
|
+
success: false,
|
|
290
|
+
error: `Unexpected Error: ${error.message}`,
|
|
291
|
+
isTemporary: true
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Processes incoming webhooks (optional for generic REST, as it depends on the destination service).
|
|
298
|
+
*/
|
|
299
|
+
async webhook(event, headers) {
|
|
300
|
+
// Generic adapter cannot know the format of webhooks from unknown services.
|
|
301
|
+
// User should use a specialized adapter or the core's generic webhook handling if available.
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Helper to parse customHeaders field.
|
|
307
|
+
* ThorMail customHeaders type usually comes as an array of objects { key, value } or a JSON object.
|
|
308
|
+
*/
|
|
309
|
+
_parseHeaders(headersConfig) {
|
|
310
|
+
const headers = {};
|
|
311
|
+
if (!headersConfig) return headers;
|
|
312
|
+
|
|
313
|
+
if (Array.isArray(headersConfig)) {
|
|
314
|
+
headersConfig.forEach(h => {
|
|
315
|
+
if (h.key && h.value) {
|
|
316
|
+
headers[h.key] = h.value;
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
} else if (typeof headersConfig === 'object') {
|
|
320
|
+
Object.assign(headers, headersConfig);
|
|
321
|
+
} else if (typeof headersConfig === 'string') {
|
|
322
|
+
try {
|
|
323
|
+
Object.assign(headers, JSON.parse(headersConfig));
|
|
324
|
+
} catch (e) {
|
|
325
|
+
// Ignore invalid JSON
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return headers;
|
|
329
|
+
}
|
|
330
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "thormail-adapter-generic-rest",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Standard REST API adapter for ThorMail. Send messages via any RESTful service with configurable URL, method, and headers.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"thormail",
|
|
9
|
+
"thormail-adapter",
|
|
10
|
+
"rest",
|
|
11
|
+
"api",
|
|
12
|
+
"generic"
|
|
13
|
+
],
|
|
14
|
+
"author": "ThorMail Team",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"registry": "http://lab.lan:4873/"
|
|
18
|
+
}
|
|
19
|
+
}
|