mailisk 2.0.1 → 2.1.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/.github/workflows/test.yml +31 -0
- package/README.md +64 -16
- package/dist/index.d.ts +47 -2
- package/dist/index.js +16 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +16 -4
- package/dist/index.mjs.map +1 -1
- package/jest.config.js +13 -0
- package/package.json +10 -2
- package/src/mailisk.interfaces.ts +32 -0
- package/src/mailisk.ts +32 -5
- package/tests/mocks/axios-mocks.ts +49 -0
- package/tests/setup.ts +21 -0
- package/tests/unit/mailisk.test.ts +344 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Build and Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [master]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
name: Test
|
|
12
|
+
needs: build
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v3
|
|
17
|
+
|
|
18
|
+
- name: Use Node.js 18.x
|
|
19
|
+
uses: actions/setup-node@v3
|
|
20
|
+
with:
|
|
21
|
+
node-version: 18.x
|
|
22
|
+
cache: "npm"
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: npm ci
|
|
26
|
+
|
|
27
|
+
- name: Build
|
|
28
|
+
run: npm run build
|
|
29
|
+
|
|
30
|
+
- name: Run tests
|
|
31
|
+
run: npm test
|
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ const { MailiskClient } = require("mailisk");
|
|
|
35
35
|
const mailisk = new MailiskClient({ apiKey: "YOUR_API_KEY" });
|
|
36
36
|
|
|
37
37
|
// send email (using virtual SMTP)
|
|
38
|
-
await
|
|
38
|
+
await mailisk.sendVirtualEmail(namespace, {
|
|
39
39
|
from: "test@example.com",
|
|
40
40
|
to: `john@${namespace}.mailisk.net`,
|
|
41
41
|
subject: "Testing",
|
|
@@ -43,7 +43,7 @@ await client.sendVirtualEmail(namespace, {
|
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
// receive email
|
|
46
|
-
const result = await
|
|
46
|
+
const result = await mailisk.searchInbox(namespace);
|
|
47
47
|
|
|
48
48
|
console.log(result);
|
|
49
49
|
```
|
|
@@ -54,35 +54,42 @@ This library wraps the REST API endpoints. Find out more in the [API Reference](
|
|
|
54
54
|
|
|
55
55
|
## Client functions
|
|
56
56
|
|
|
57
|
-
### searchInbox(namespace, params?)
|
|
57
|
+
### `searchInbox(namespace, params?)`
|
|
58
|
+
|
|
59
|
+
Use `searchInbox` to fetch messages that arrived in a given namespace, optionally waiting until the first new mail shows up.
|
|
60
|
+
|
|
61
|
+
For the full parameter options see the [endpoint reference](https://docs.mailisk.com/api-reference/search-inbox.html#request-1).
|
|
62
|
+
|
|
63
|
+
Default behaviour:
|
|
58
64
|
|
|
59
|
-
|
|
65
|
+
- Waits until at least one new email arrives (override with `wait: false`).
|
|
66
|
+
- Times out after 5 minutes if nothing shows up (adjust via `requestOptions.timeout`).
|
|
67
|
+
- Ignores messages older than 15 minutes to avoid picking up leftovers from previous tests (change via `from_timestamp`).
|
|
60
68
|
|
|
61
|
-
|
|
62
|
-
- The request timeout is adjustable by passing `timeout` in the request options. By default it uses a timeout of 5 minutes.
|
|
63
|
-
- By default `from_timestamp` is set to **current timestamp - 5 seconds**. This ensures that only new emails are returned. Without this, older emails would also be returned, potentially disrupting you if you were waiting for a specific email. This can be overriden by passing the `from_timestamp` parameter (`from_timestmap: 0` will disable filtering by email age).
|
|
69
|
+
#### Quick examples
|
|
64
70
|
|
|
65
71
|
```js
|
|
66
|
-
//
|
|
67
|
-
await mailisk.searchInbox(namespace);
|
|
68
|
-
|
|
72
|
+
// wait up to the default 5 min for *any* new mail
|
|
73
|
+
const { data: emails } = await mailisk.searchInbox(namespace);
|
|
74
|
+
|
|
75
|
+
// custom 60-second timeout
|
|
69
76
|
await mailisk.searchInbox(namespace, {}, { timeout: 1000 * 60 });
|
|
70
|
-
|
|
77
|
+
|
|
78
|
+
// polling pattern — return immediately, even if inbox is empty
|
|
71
79
|
await mailisk.searchInbox(namespace, { wait: false });
|
|
72
80
|
```
|
|
73
81
|
|
|
74
82
|
#### Filter by destination address
|
|
75
83
|
|
|
76
|
-
A common
|
|
84
|
+
A common pattern is to wait for the email your UI just triggered (e.g. password-reset).
|
|
85
|
+
Pass `to_addr_prefix` so you don’t pick up stale messages:
|
|
77
86
|
|
|
78
87
|
```js
|
|
79
88
|
const { data: emails } = await mailisk.searchInbox(namespace, {
|
|
80
|
-
to_addr_prefix:
|
|
89
|
+
to_addr_prefix: `john@${namespace}.mailisk.net`,
|
|
81
90
|
});
|
|
82
91
|
```
|
|
83
92
|
|
|
84
|
-
For more parameter options see the [endpoint reference](https://docs.mailisk.com/api-reference/search-inbox.html#request-1).
|
|
85
|
-
|
|
86
93
|
### sendVirtualEmail(namespace, params)
|
|
87
94
|
|
|
88
95
|
Send an email using [Virtual SMTP](https://docs.mailisk.com/smtp.html). This will fetch the SMTP settings for the selected namespace and send an email. These emails can only be sent to an address that ends in `@{namespace}.mailisk.net`.
|
|
@@ -100,7 +107,7 @@ await mailisk.sendVirtualEmail(namespace, {
|
|
|
100
107
|
|
|
101
108
|
This does not call an API endpoint but rather uses nodemailer to send an email using SMTP.
|
|
102
109
|
|
|
103
|
-
### listNamespaces()
|
|
110
|
+
### `listNamespaces()`
|
|
104
111
|
|
|
105
112
|
List all namespaces associated with the current API Key.
|
|
106
113
|
|
|
@@ -110,3 +117,44 @@ const namespacesResponse = await mailisk.listNamespaces();
|
|
|
110
117
|
// will be ['namespace1', 'namespace2']
|
|
111
118
|
const namespaces = namespacesResponse.map((nr) => nr.namespace);
|
|
112
119
|
```
|
|
120
|
+
|
|
121
|
+
### `getAttachment(attachmentId)`
|
|
122
|
+
|
|
123
|
+
Get information about an attachment.
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
const attachment = await mailisk.getAttachment(attachmentId);
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### `downloadAttachment(attachmentId)`
|
|
130
|
+
|
|
131
|
+
Retrieve the raw bytes of a file attached to an email message.
|
|
132
|
+
Typically you call this after `searchInbox` → iterate over `email.attachments[]` → pass the desired `attachment.id`.
|
|
133
|
+
|
|
134
|
+
#### Quick examples
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
import fs from "node:fs";
|
|
138
|
+
import path from "node:path";
|
|
139
|
+
|
|
140
|
+
// assume 'email' was fetched via searchInbox()
|
|
141
|
+
const { id, filename } = email.attachments[0];
|
|
142
|
+
|
|
143
|
+
// download the attachment
|
|
144
|
+
const buffer = await mailisk.downloadAttachment(id);
|
|
145
|
+
|
|
146
|
+
// save to disk (preserve original filename)
|
|
147
|
+
fs.writeFileSync(filename, buffer);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Streaming large files
|
|
151
|
+
|
|
152
|
+
downloadAttachment returns the entire file as a single Buffer.
|
|
153
|
+
If you expect very large attachments and want to avoid holding them fully in memory, use getAttachment(attachmentId).download_url and stream with fetch / axios instead:
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
const meta = await mailisk.getAttachment(id);
|
|
157
|
+
const res = await fetch(meta.download_url);
|
|
158
|
+
const fileStream = fs.createWriteStream(filename);
|
|
159
|
+
await new Promise((ok, err) => res.body.pipe(fileStream).on("finish", ok).on("error", err));
|
|
160
|
+
```
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { AxiosRequestConfig } from 'axios';
|
|
1
|
+
import { AxiosBasicCredentials, AxiosRequestConfig } from 'axios';
|
|
2
|
+
import { Attachment } from 'nodemailer/lib/mailer';
|
|
2
3
|
|
|
3
4
|
interface EmailAddress {
|
|
4
5
|
/** Email address */
|
|
@@ -6,6 +7,16 @@ interface EmailAddress {
|
|
|
6
7
|
/** Display name, if one is specified */
|
|
7
8
|
name?: string;
|
|
8
9
|
}
|
|
10
|
+
interface EmailAttachment {
|
|
11
|
+
/** Unique identifier for the attachment */
|
|
12
|
+
id: string;
|
|
13
|
+
/** Filename of the attachment */
|
|
14
|
+
filename: string;
|
|
15
|
+
/** Content type of the attachment */
|
|
16
|
+
content_type: string;
|
|
17
|
+
/** Size in bytes of the attachment */
|
|
18
|
+
size: number;
|
|
19
|
+
}
|
|
9
20
|
interface Email {
|
|
10
21
|
/** Namespace scoped ID */
|
|
11
22
|
id: string;
|
|
@@ -31,6 +42,10 @@ interface Email {
|
|
|
31
42
|
expires_timestamp: number;
|
|
32
43
|
/** The spam score as reported by SpamAssassin */
|
|
33
44
|
spam_score?: number;
|
|
45
|
+
/** The headers of the email */
|
|
46
|
+
headers?: Record<string, string>;
|
|
47
|
+
/** The attachments of the email */
|
|
48
|
+
attachments?: EmailAttachment[];
|
|
34
49
|
}
|
|
35
50
|
interface SearchInboxParams {
|
|
36
51
|
/**
|
|
@@ -96,6 +111,16 @@ interface SmtpSettings {
|
|
|
96
111
|
password: string;
|
|
97
112
|
};
|
|
98
113
|
}
|
|
114
|
+
interface GetAttachmentResponse {
|
|
115
|
+
data: {
|
|
116
|
+
id: string;
|
|
117
|
+
filename: string;
|
|
118
|
+
content_type: string;
|
|
119
|
+
size: number;
|
|
120
|
+
expires_at: string | null;
|
|
121
|
+
download_url: string;
|
|
122
|
+
};
|
|
123
|
+
}
|
|
99
124
|
interface ListNamespacesResponse {
|
|
100
125
|
data: [
|
|
101
126
|
{
|
|
@@ -119,12 +144,17 @@ interface SendVirtualEmailParams {
|
|
|
119
144
|
text?: string | undefined;
|
|
120
145
|
/** The HTML version of the message */
|
|
121
146
|
html?: string | undefined;
|
|
147
|
+
/** Custom headers for the email */
|
|
148
|
+
headers?: Record<string, string>;
|
|
149
|
+
/** Attachments to the email */
|
|
150
|
+
attachments?: Attachment[];
|
|
122
151
|
}
|
|
123
152
|
|
|
124
153
|
declare class MailiskClient {
|
|
125
|
-
constructor({ apiKey, baseUrl }: {
|
|
154
|
+
constructor({ apiKey, baseUrl, auth }: {
|
|
126
155
|
apiKey: string;
|
|
127
156
|
baseUrl?: string;
|
|
157
|
+
auth?: AxiosBasicCredentials;
|
|
128
158
|
});
|
|
129
159
|
private readonly axiosInstance;
|
|
130
160
|
/**
|
|
@@ -175,6 +205,21 @@ declare class MailiskClient {
|
|
|
175
205
|
* Get the SMTP settings for a namespace.
|
|
176
206
|
*/
|
|
177
207
|
getSmtpSettings(namespace: string): Promise<SmtpSettings>;
|
|
208
|
+
getAttachment(attachmentId: string): Promise<GetAttachmentResponse>;
|
|
209
|
+
/**
|
|
210
|
+
* Download an attachment from an attachment ID.
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* Download an attachment from an email
|
|
214
|
+
* ```typescript
|
|
215
|
+
* const attachment = email.attachments[0];
|
|
216
|
+
* const attachmentBuffer = await client.downloadAttachment(attachment.id);
|
|
217
|
+
*
|
|
218
|
+
* // save to file
|
|
219
|
+
* fs.writeFileSync(attachment.filename, attachmentBuffer);
|
|
220
|
+
* ```
|
|
221
|
+
*/
|
|
222
|
+
downloadAttachment(attachmentId: string): Promise<Buffer>;
|
|
178
223
|
}
|
|
179
224
|
|
|
180
225
|
export { MailiskClient };
|
package/dist/index.js
CHANGED
|
@@ -35,12 +35,13 @@ module.exports = __toCommonJS(src_exports);
|
|
|
35
35
|
var import_axios = __toESM(require("axios"));
|
|
36
36
|
var import_nodemailer = __toESM(require("nodemailer"));
|
|
37
37
|
var MailiskClient = class {
|
|
38
|
-
constructor({ apiKey, baseUrl }) {
|
|
38
|
+
constructor({ apiKey, baseUrl, auth }) {
|
|
39
39
|
this.axiosInstance = import_axios.default.create({
|
|
40
40
|
headers: {
|
|
41
41
|
"X-Api-Key": apiKey
|
|
42
42
|
},
|
|
43
|
-
baseURL: baseUrl || "https://api.mailisk.com/"
|
|
43
|
+
baseURL: baseUrl || "https://api.mailisk.com/",
|
|
44
|
+
auth
|
|
44
45
|
});
|
|
45
46
|
}
|
|
46
47
|
axiosInstance;
|
|
@@ -58,13 +59,15 @@ var MailiskClient = class {
|
|
|
58
59
|
pass: smtpSettings.data.password
|
|
59
60
|
}
|
|
60
61
|
});
|
|
61
|
-
const { from, to, subject, text, html } = params;
|
|
62
|
+
const { from, to, subject, text, html, headers, attachments } = params;
|
|
62
63
|
await transport.sendMail({
|
|
63
64
|
from,
|
|
64
65
|
to,
|
|
65
66
|
subject,
|
|
66
67
|
text,
|
|
67
|
-
html
|
|
68
|
+
html,
|
|
69
|
+
headers,
|
|
70
|
+
attachments
|
|
68
71
|
});
|
|
69
72
|
transport.close();
|
|
70
73
|
}
|
|
@@ -92,6 +95,15 @@ var MailiskClient = class {
|
|
|
92
95
|
const result = await this.axiosInstance.get(`api/smtp/${namespace}`);
|
|
93
96
|
return result.data;
|
|
94
97
|
}
|
|
98
|
+
async getAttachment(attachmentId) {
|
|
99
|
+
const result = await this.axiosInstance.get(`api/attachments/${attachmentId}`);
|
|
100
|
+
return result.data;
|
|
101
|
+
}
|
|
102
|
+
async downloadAttachment(attachmentId) {
|
|
103
|
+
const result = await this.getAttachment(attachmentId);
|
|
104
|
+
const response = await import_axios.default.get(result.data.download_url, { responseType: "arraybuffer" });
|
|
105
|
+
return Buffer.from(response.data);
|
|
106
|
+
}
|
|
95
107
|
};
|
|
96
108
|
__name(MailiskClient, "MailiskClient");
|
|
97
109
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/mailisk.ts"],"sourcesContent":["export * from \"./mailisk\";\n","import axios, { AxiosRequestConfig } from \"axios\";\nimport {\n ListNamespacesResponse,\n SearchInboxParams,\n SearchInboxResponse,\n SendVirtualEmailParams,\n SmtpSettings,\n} from \"./mailisk.interfaces\";\nimport nodemailer from \"nodemailer\";\n\nexport class MailiskClient {\n constructor({ apiKey, baseUrl }: { apiKey: string; baseUrl?: string }) {\n this.axiosInstance = axios.create({\n headers: {\n \"X-Api-Key\": apiKey,\n },\n baseURL: baseUrl || \"https://api.mailisk.com/\",\n });\n }\n\n private readonly axiosInstance;\n\n /**\n * List all namespaces that belong to the current account (API key).\n */\n async listNamespaces(): Promise<ListNamespacesResponse> {\n return (await this.axiosInstance.get(\"api/namespaces\")).data;\n }\n\n /**\n * Send an email using the Virtual SMTP.\n *\n * These emails can only be sent to valid Mailisk namespaces, i.e. emails that end in @mynamespace.mailisk.net\n *\n * @example\n * For example, sending a test email:\n * ```typescript\n * client.sendVirtualEmail(namespace, {\n * from: \"test@example.com\",\n * to: `john@${namespace}.mailisk.net`,\n * subject: \"This is a test\",\n * text: \"Testing\",\n * });\n * ```\n */\n async sendVirtualEmail(namespace: string, params: SendVirtualEmailParams): Promise<void> {\n const smtpSettings = await this.getSmtpSettings(namespace);\n\n
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/mailisk.ts"],"sourcesContent":["export * from \"./mailisk\";\n","import axios, { AxiosBasicCredentials, AxiosRequestConfig } from \"axios\";\nimport {\n GetAttachmentResponse,\n ListNamespacesResponse,\n SearchInboxParams,\n SearchInboxResponse,\n SendVirtualEmailParams,\n SmtpSettings,\n} from \"./mailisk.interfaces\";\nimport nodemailer from \"nodemailer\";\n\nexport class MailiskClient {\n constructor({ apiKey, baseUrl, auth }: { apiKey: string; baseUrl?: string; auth?: AxiosBasicCredentials }) {\n this.axiosInstance = axios.create({\n headers: {\n \"X-Api-Key\": apiKey,\n },\n baseURL: baseUrl || \"https://api.mailisk.com/\",\n auth,\n });\n }\n\n private readonly axiosInstance;\n\n /**\n * List all namespaces that belong to the current account (API key).\n */\n async listNamespaces(): Promise<ListNamespacesResponse> {\n return (await this.axiosInstance.get(\"api/namespaces\")).data;\n }\n\n /**\n * Send an email using the Virtual SMTP.\n *\n * These emails can only be sent to valid Mailisk namespaces, i.e. emails that end in @mynamespace.mailisk.net\n *\n * @example\n * For example, sending a test email:\n * ```typescript\n * client.sendVirtualEmail(namespace, {\n * from: \"test@example.com\",\n * to: `john@${namespace}.mailisk.net`,\n * subject: \"This is a test\",\n * text: \"Testing\",\n * });\n * ```\n */\n async sendVirtualEmail(namespace: string, params: SendVirtualEmailParams): Promise<void> {\n const smtpSettings = await this.getSmtpSettings(namespace);\n\n const transport = nodemailer.createTransport({\n host: smtpSettings.data.host,\n port: smtpSettings.data.port,\n secure: false,\n auth: {\n user: smtpSettings.data.username,\n pass: smtpSettings.data.password,\n },\n });\n\n const { from, to, subject, text, html, headers, attachments } = params;\n\n await transport.sendMail({\n from,\n to,\n subject,\n text,\n html,\n headers,\n attachments,\n });\n\n transport.close();\n }\n\n /**\n * Search inbox of a namespace.\n *\n * By default, this calls the api using the `wait` flag. This means the call won't timeout until at least one email is received or 5 minutes pass.\n * It also uses a default `from_timestamp` of **current timestamp - 5 seconds**. This means that older emails will be ignored.\n *\n * Both of these settings can be overriden by passing them in the `params` object.\n *\n * @example\n * Get the latest emails in the namespace\n * ```typescript\n * const { data: emails } = await client.searchInbox(namespace);\n * ```\n *\n * @example\n * Get the latest emails for a specific email address\n * ```typescript\n * const { data: emails } = await client.searchInbox(namespace, {\n * to_addr_prefix: 'john@mynamespace.mailisk.net'\n * });\n * ```\n */\n async searchInbox(\n namespace: string,\n params?: SearchInboxParams,\n config?: AxiosRequestConfig\n ): Promise<SearchInboxResponse> {\n let _params = { ...params };\n\n // default from timestamp, 15 minutes before starting this request\n if (params?.from_timestamp === undefined || params?.from_timestamp === null) {\n _params.from_timestamp = Math.floor(new Date().getTime() / 1000) - 15 * 60;\n }\n\n // by default wait for email\n if (params?.wait !== false) {\n _params.wait = true;\n }\n\n let _config = { ...config };\n\n if (!config?.maxRedirects) {\n _config.maxRedirects = 99999;\n }\n\n // by default, wait 5 minutes for emails before timing out\n if (_params.wait && !config?.timeout) {\n _config.timeout = 1000 * 60 * 5;\n }\n\n return (\n await this.axiosInstance.get(`api/emails/${namespace}/inbox`, {\n ..._config,\n params: _params,\n })\n ).data;\n }\n\n /**\n * Get the SMTP settings for a namespace.\n */\n async getSmtpSettings(namespace: string): Promise<SmtpSettings> {\n const result = await this.axiosInstance.get(`api/smtp/${namespace}`);\n return result.data;\n }\n\n async getAttachment(attachmentId: string): Promise<GetAttachmentResponse> {\n const result = await this.axiosInstance.get(`api/attachments/${attachmentId}`);\n return result.data;\n }\n\n /**\n * Download an attachment from an attachment ID.\n *\n * @example\n * Download an attachment from an email\n * ```typescript\n * const attachment = email.attachments[0];\n * const attachmentBuffer = await client.downloadAttachment(attachment.id);\n *\n * // save to file\n * fs.writeFileSync(attachment.filename, attachmentBuffer);\n * ```\n */\n async downloadAttachment(attachmentId: string): Promise<Buffer> {\n const result = await this.getAttachment(attachmentId);\n\n const response = await axios.get(result.data.download_url, { responseType: \"arraybuffer\" });\n return Buffer.from(response.data);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAAiE;AASjE,wBAAuB;AAEhB,IAAM,gBAAN,MAAoB;AAAA,EACzB,YAAY,EAAE,QAAQ,SAAS,KAAK,GAAuE;AACzG,SAAK,gBAAgB,aAAAA,QAAM,OAAO;AAAA,MAChC,SAAS;AAAA,QACP,aAAa;AAAA,MACf;AAAA,MACA,SAAS,WAAW;AAAA,MACpB;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEiB;AAAA,EAKjB,MAAM,iBAAkD;AACtD,YAAQ,MAAM,KAAK,cAAc,IAAI,gBAAgB,GAAG;AAAA,EAC1D;AAAA,EAkBA,MAAM,iBAAiB,WAAmB,QAA+C;AACvF,UAAM,eAAe,MAAM,KAAK,gBAAgB,SAAS;AAEzD,UAAM,YAAY,kBAAAC,QAAW,gBAAgB;AAAA,MAC3C,MAAM,aAAa,KAAK;AAAA,MACxB,MAAM,aAAa,KAAK;AAAA,MACxB,QAAQ;AAAA,MACR,MAAM;AAAA,QACJ,MAAM,aAAa,KAAK;AAAA,QACxB,MAAM,aAAa,KAAK;AAAA,MAC1B;AAAA,IACF,CAAC;AAED,UAAM,EAAE,MAAM,IAAI,SAAS,MAAM,MAAM,SAAS,YAAY,IAAI;AAEhE,UAAM,UAAU,SAAS;AAAA,MACvB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAED,cAAU,MAAM;AAAA,EAClB;AAAA,EAwBA,MAAM,YACJ,WACA,QACA,QAC8B;AAC9B,QAAI,UAAU,EAAE,GAAG,OAAO;AAG1B,QAAI,QAAQ,mBAAmB,UAAa,QAAQ,mBAAmB,MAAM;AAC3E,cAAQ,iBAAiB,KAAK,MAAM,IAAI,KAAK,EAAE,QAAQ,IAAI,GAAI,IAAI,KAAK;AAAA,IAC1E;AAGA,QAAI,QAAQ,SAAS,OAAO;AAC1B,cAAQ,OAAO;AAAA,IACjB;AAEA,QAAI,UAAU,EAAE,GAAG,OAAO;AAE1B,QAAI,CAAC,QAAQ,cAAc;AACzB,cAAQ,eAAe;AAAA,IACzB;AAGA,QAAI,QAAQ,QAAQ,CAAC,QAAQ,SAAS;AACpC,cAAQ,UAAU,MAAO,KAAK;AAAA,IAChC;AAEA,YACE,MAAM,KAAK,cAAc,IAAI,cAAc,mBAAmB;AAAA,MAC5D,GAAG;AAAA,MACH,QAAQ;AAAA,IACV,CAAC,GACD;AAAA,EACJ;AAAA,EAKA,MAAM,gBAAgB,WAA0C;AAC9D,UAAM,SAAS,MAAM,KAAK,cAAc,IAAI,YAAY,WAAW;AACnE,WAAO,OAAO;AAAA,EAChB;AAAA,EAEA,MAAM,cAAc,cAAsD;AACxE,UAAM,SAAS,MAAM,KAAK,cAAc,IAAI,mBAAmB,cAAc;AAC7E,WAAO,OAAO;AAAA,EAChB;AAAA,EAeA,MAAM,mBAAmB,cAAuC;AAC9D,UAAM,SAAS,MAAM,KAAK,cAAc,YAAY;AAEpD,UAAM,WAAW,MAAM,aAAAD,QAAM,IAAI,OAAO,KAAK,cAAc,EAAE,cAAc,cAAc,CAAC;AAC1F,WAAO,OAAO,KAAK,SAAS,IAAI;AAAA,EAClC;AACF;AA1Ja;","names":["axios","nodemailer"]}
|
package/dist/index.mjs
CHANGED
|
@@ -5,12 +5,13 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
|
|
|
5
5
|
import axios from "axios";
|
|
6
6
|
import nodemailer from "nodemailer";
|
|
7
7
|
var MailiskClient = class {
|
|
8
|
-
constructor({ apiKey, baseUrl }) {
|
|
8
|
+
constructor({ apiKey, baseUrl, auth }) {
|
|
9
9
|
this.axiosInstance = axios.create({
|
|
10
10
|
headers: {
|
|
11
11
|
"X-Api-Key": apiKey
|
|
12
12
|
},
|
|
13
|
-
baseURL: baseUrl || "https://api.mailisk.com/"
|
|
13
|
+
baseURL: baseUrl || "https://api.mailisk.com/",
|
|
14
|
+
auth
|
|
14
15
|
});
|
|
15
16
|
}
|
|
16
17
|
axiosInstance;
|
|
@@ -28,13 +29,15 @@ var MailiskClient = class {
|
|
|
28
29
|
pass: smtpSettings.data.password
|
|
29
30
|
}
|
|
30
31
|
});
|
|
31
|
-
const { from, to, subject, text, html } = params;
|
|
32
|
+
const { from, to, subject, text, html, headers, attachments } = params;
|
|
32
33
|
await transport.sendMail({
|
|
33
34
|
from,
|
|
34
35
|
to,
|
|
35
36
|
subject,
|
|
36
37
|
text,
|
|
37
|
-
html
|
|
38
|
+
html,
|
|
39
|
+
headers,
|
|
40
|
+
attachments
|
|
38
41
|
});
|
|
39
42
|
transport.close();
|
|
40
43
|
}
|
|
@@ -62,6 +65,15 @@ var MailiskClient = class {
|
|
|
62
65
|
const result = await this.axiosInstance.get(`api/smtp/${namespace}`);
|
|
63
66
|
return result.data;
|
|
64
67
|
}
|
|
68
|
+
async getAttachment(attachmentId) {
|
|
69
|
+
const result = await this.axiosInstance.get(`api/attachments/${attachmentId}`);
|
|
70
|
+
return result.data;
|
|
71
|
+
}
|
|
72
|
+
async downloadAttachment(attachmentId) {
|
|
73
|
+
const result = await this.getAttachment(attachmentId);
|
|
74
|
+
const response = await axios.get(result.data.download_url, { responseType: "arraybuffer" });
|
|
75
|
+
return Buffer.from(response.data);
|
|
76
|
+
}
|
|
65
77
|
};
|
|
66
78
|
__name(MailiskClient, "MailiskClient");
|
|
67
79
|
export {
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/mailisk.ts"],"sourcesContent":["import axios, { AxiosRequestConfig } from \"axios\";\nimport {\n ListNamespacesResponse,\n SearchInboxParams,\n SearchInboxResponse,\n SendVirtualEmailParams,\n SmtpSettings,\n} from \"./mailisk.interfaces\";\nimport nodemailer from \"nodemailer\";\n\nexport class MailiskClient {\n constructor({ apiKey, baseUrl }: { apiKey: string; baseUrl?: string }) {\n this.axiosInstance = axios.create({\n headers: {\n \"X-Api-Key\": apiKey,\n },\n baseURL: baseUrl || \"https://api.mailisk.com/\",\n });\n }\n\n private readonly axiosInstance;\n\n /**\n * List all namespaces that belong to the current account (API key).\n */\n async listNamespaces(): Promise<ListNamespacesResponse> {\n return (await this.axiosInstance.get(\"api/namespaces\")).data;\n }\n\n /**\n * Send an email using the Virtual SMTP.\n *\n * These emails can only be sent to valid Mailisk namespaces, i.e. emails that end in @mynamespace.mailisk.net\n *\n * @example\n * For example, sending a test email:\n * ```typescript\n * client.sendVirtualEmail(namespace, {\n * from: \"test@example.com\",\n * to: `john@${namespace}.mailisk.net`,\n * subject: \"This is a test\",\n * text: \"Testing\",\n * });\n * ```\n */\n async sendVirtualEmail(namespace: string, params: SendVirtualEmailParams): Promise<void> {\n const smtpSettings = await this.getSmtpSettings(namespace);\n\n
|
|
1
|
+
{"version":3,"sources":["../src/mailisk.ts"],"sourcesContent":["import axios, { AxiosBasicCredentials, AxiosRequestConfig } from \"axios\";\nimport {\n GetAttachmentResponse,\n ListNamespacesResponse,\n SearchInboxParams,\n SearchInboxResponse,\n SendVirtualEmailParams,\n SmtpSettings,\n} from \"./mailisk.interfaces\";\nimport nodemailer from \"nodemailer\";\n\nexport class MailiskClient {\n constructor({ apiKey, baseUrl, auth }: { apiKey: string; baseUrl?: string; auth?: AxiosBasicCredentials }) {\n this.axiosInstance = axios.create({\n headers: {\n \"X-Api-Key\": apiKey,\n },\n baseURL: baseUrl || \"https://api.mailisk.com/\",\n auth,\n });\n }\n\n private readonly axiosInstance;\n\n /**\n * List all namespaces that belong to the current account (API key).\n */\n async listNamespaces(): Promise<ListNamespacesResponse> {\n return (await this.axiosInstance.get(\"api/namespaces\")).data;\n }\n\n /**\n * Send an email using the Virtual SMTP.\n *\n * These emails can only be sent to valid Mailisk namespaces, i.e. emails that end in @mynamespace.mailisk.net\n *\n * @example\n * For example, sending a test email:\n * ```typescript\n * client.sendVirtualEmail(namespace, {\n * from: \"test@example.com\",\n * to: `john@${namespace}.mailisk.net`,\n * subject: \"This is a test\",\n * text: \"Testing\",\n * });\n * ```\n */\n async sendVirtualEmail(namespace: string, params: SendVirtualEmailParams): Promise<void> {\n const smtpSettings = await this.getSmtpSettings(namespace);\n\n const transport = nodemailer.createTransport({\n host: smtpSettings.data.host,\n port: smtpSettings.data.port,\n secure: false,\n auth: {\n user: smtpSettings.data.username,\n pass: smtpSettings.data.password,\n },\n });\n\n const { from, to, subject, text, html, headers, attachments } = params;\n\n await transport.sendMail({\n from,\n to,\n subject,\n text,\n html,\n headers,\n attachments,\n });\n\n transport.close();\n }\n\n /**\n * Search inbox of a namespace.\n *\n * By default, this calls the api using the `wait` flag. This means the call won't timeout until at least one email is received or 5 minutes pass.\n * It also uses a default `from_timestamp` of **current timestamp - 5 seconds**. This means that older emails will be ignored.\n *\n * Both of these settings can be overriden by passing them in the `params` object.\n *\n * @example\n * Get the latest emails in the namespace\n * ```typescript\n * const { data: emails } = await client.searchInbox(namespace);\n * ```\n *\n * @example\n * Get the latest emails for a specific email address\n * ```typescript\n * const { data: emails } = await client.searchInbox(namespace, {\n * to_addr_prefix: 'john@mynamespace.mailisk.net'\n * });\n * ```\n */\n async searchInbox(\n namespace: string,\n params?: SearchInboxParams,\n config?: AxiosRequestConfig\n ): Promise<SearchInboxResponse> {\n let _params = { ...params };\n\n // default from timestamp, 15 minutes before starting this request\n if (params?.from_timestamp === undefined || params?.from_timestamp === null) {\n _params.from_timestamp = Math.floor(new Date().getTime() / 1000) - 15 * 60;\n }\n\n // by default wait for email\n if (params?.wait !== false) {\n _params.wait = true;\n }\n\n let _config = { ...config };\n\n if (!config?.maxRedirects) {\n _config.maxRedirects = 99999;\n }\n\n // by default, wait 5 minutes for emails before timing out\n if (_params.wait && !config?.timeout) {\n _config.timeout = 1000 * 60 * 5;\n }\n\n return (\n await this.axiosInstance.get(`api/emails/${namespace}/inbox`, {\n ..._config,\n params: _params,\n })\n ).data;\n }\n\n /**\n * Get the SMTP settings for a namespace.\n */\n async getSmtpSettings(namespace: string): Promise<SmtpSettings> {\n const result = await this.axiosInstance.get(`api/smtp/${namespace}`);\n return result.data;\n }\n\n async getAttachment(attachmentId: string): Promise<GetAttachmentResponse> {\n const result = await this.axiosInstance.get(`api/attachments/${attachmentId}`);\n return result.data;\n }\n\n /**\n * Download an attachment from an attachment ID.\n *\n * @example\n * Download an attachment from an email\n * ```typescript\n * const attachment = email.attachments[0];\n * const attachmentBuffer = await client.downloadAttachment(attachment.id);\n *\n * // save to file\n * fs.writeFileSync(attachment.filename, attachmentBuffer);\n * ```\n */\n async downloadAttachment(attachmentId: string): Promise<Buffer> {\n const result = await this.getAttachment(attachmentId);\n\n const response = await axios.get(result.data.download_url, { responseType: \"arraybuffer\" });\n return Buffer.from(response.data);\n }\n}\n"],"mappings":";;;;AAAA,OAAO,WAA0D;AASjE,OAAO,gBAAgB;AAEhB,IAAM,gBAAN,MAAoB;AAAA,EACzB,YAAY,EAAE,QAAQ,SAAS,KAAK,GAAuE;AACzG,SAAK,gBAAgB,MAAM,OAAO;AAAA,MAChC,SAAS;AAAA,QACP,aAAa;AAAA,MACf;AAAA,MACA,SAAS,WAAW;AAAA,MACpB;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEiB;AAAA,EAKjB,MAAM,iBAAkD;AACtD,YAAQ,MAAM,KAAK,cAAc,IAAI,gBAAgB,GAAG;AAAA,EAC1D;AAAA,EAkBA,MAAM,iBAAiB,WAAmB,QAA+C;AACvF,UAAM,eAAe,MAAM,KAAK,gBAAgB,SAAS;AAEzD,UAAM,YAAY,WAAW,gBAAgB;AAAA,MAC3C,MAAM,aAAa,KAAK;AAAA,MACxB,MAAM,aAAa,KAAK;AAAA,MACxB,QAAQ;AAAA,MACR,MAAM;AAAA,QACJ,MAAM,aAAa,KAAK;AAAA,QACxB,MAAM,aAAa,KAAK;AAAA,MAC1B;AAAA,IACF,CAAC;AAED,UAAM,EAAE,MAAM,IAAI,SAAS,MAAM,MAAM,SAAS,YAAY,IAAI;AAEhE,UAAM,UAAU,SAAS;AAAA,MACvB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAED,cAAU,MAAM;AAAA,EAClB;AAAA,EAwBA,MAAM,YACJ,WACA,QACA,QAC8B;AAC9B,QAAI,UAAU,EAAE,GAAG,OAAO;AAG1B,QAAI,QAAQ,mBAAmB,UAAa,QAAQ,mBAAmB,MAAM;AAC3E,cAAQ,iBAAiB,KAAK,MAAM,IAAI,KAAK,EAAE,QAAQ,IAAI,GAAI,IAAI,KAAK;AAAA,IAC1E;AAGA,QAAI,QAAQ,SAAS,OAAO;AAC1B,cAAQ,OAAO;AAAA,IACjB;AAEA,QAAI,UAAU,EAAE,GAAG,OAAO;AAE1B,QAAI,CAAC,QAAQ,cAAc;AACzB,cAAQ,eAAe;AAAA,IACzB;AAGA,QAAI,QAAQ,QAAQ,CAAC,QAAQ,SAAS;AACpC,cAAQ,UAAU,MAAO,KAAK;AAAA,IAChC;AAEA,YACE,MAAM,KAAK,cAAc,IAAI,cAAc,mBAAmB;AAAA,MAC5D,GAAG;AAAA,MACH,QAAQ;AAAA,IACV,CAAC,GACD;AAAA,EACJ;AAAA,EAKA,MAAM,gBAAgB,WAA0C;AAC9D,UAAM,SAAS,MAAM,KAAK,cAAc,IAAI,YAAY,WAAW;AACnE,WAAO,OAAO;AAAA,EAChB;AAAA,EAEA,MAAM,cAAc,cAAsD;AACxE,UAAM,SAAS,MAAM,KAAK,cAAc,IAAI,mBAAmB,cAAc;AAC7E,WAAO,OAAO;AAAA,EAChB;AAAA,EAeA,MAAM,mBAAmB,cAAuC;AAC9D,UAAM,SAAS,MAAM,KAAK,cAAc,YAAY;AAEpD,UAAM,WAAW,MAAM,MAAM,IAAI,OAAO,KAAK,cAAc,EAAE,cAAc,cAAc,CAAC;AAC1F,WAAO,OAAO,KAAK,SAAS,IAAI;AAAA,EAClC;AACF;AA1Ja;","names":[]}
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
preset: "ts-jest",
|
|
3
|
+
testEnvironment: "node",
|
|
4
|
+
roots: ["<rootDir>/tests"],
|
|
5
|
+
transform: {
|
|
6
|
+
"^.+\\.tsx?$": "ts-jest",
|
|
7
|
+
},
|
|
8
|
+
testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$",
|
|
9
|
+
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
|
|
10
|
+
collectCoverage: true,
|
|
11
|
+
collectCoverageFrom: ["src/**/*.ts"],
|
|
12
|
+
setupFilesAfterEnv: ["<rootDir>/tests/setup.ts"],
|
|
13
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mailisk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Mailisk library for NodeJS",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mailisk",
|
|
@@ -11,12 +11,20 @@
|
|
|
11
11
|
"main": "dist/index.js",
|
|
12
12
|
"types": "dist/index.d.ts",
|
|
13
13
|
"scripts": {
|
|
14
|
-
"build": "tsup"
|
|
14
|
+
"build": "tsup",
|
|
15
|
+
"test": "jest",
|
|
16
|
+
"test:watch": "jest --watch",
|
|
17
|
+
"test:coverage": "jest --coverage"
|
|
15
18
|
},
|
|
16
19
|
"author": "",
|
|
17
20
|
"license": "ISC",
|
|
18
21
|
"devDependencies": {
|
|
22
|
+
"@types/jest": "^29.5.14",
|
|
19
23
|
"@types/nodemailer": "^6.4.6",
|
|
24
|
+
"dotenv": "^16.4.7",
|
|
25
|
+
"jest": "^29.7.0",
|
|
26
|
+
"jest-mock-axios": "^4.8.0",
|
|
27
|
+
"ts-jest": "^29.2.6",
|
|
20
28
|
"tsup": "^6.2.3",
|
|
21
29
|
"typescript": "^4.8.4"
|
|
22
30
|
},
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Attachment } from "nodemailer/lib/mailer";
|
|
2
|
+
|
|
1
3
|
export interface EmailAddress {
|
|
2
4
|
/** Email address */
|
|
3
5
|
address: string;
|
|
@@ -5,6 +7,17 @@ export interface EmailAddress {
|
|
|
5
7
|
name?: string;
|
|
6
8
|
}
|
|
7
9
|
|
|
10
|
+
export interface EmailAttachment {
|
|
11
|
+
/** Unique identifier for the attachment */
|
|
12
|
+
id: string;
|
|
13
|
+
/** Filename of the attachment */
|
|
14
|
+
filename: string;
|
|
15
|
+
/** Content type of the attachment */
|
|
16
|
+
content_type: string;
|
|
17
|
+
/** Size in bytes of the attachment */
|
|
18
|
+
size: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
8
21
|
export interface Email {
|
|
9
22
|
/** Namespace scoped ID */
|
|
10
23
|
id: string;
|
|
@@ -30,6 +43,10 @@ export interface Email {
|
|
|
30
43
|
expires_timestamp: number;
|
|
31
44
|
/** The spam score as reported by SpamAssassin */
|
|
32
45
|
spam_score?: number;
|
|
46
|
+
/** The headers of the email */
|
|
47
|
+
headers?: Record<string, string>;
|
|
48
|
+
/** The attachments of the email */
|
|
49
|
+
attachments?: EmailAttachment[];
|
|
33
50
|
}
|
|
34
51
|
|
|
35
52
|
export interface SearchInboxParams {
|
|
@@ -99,6 +116,17 @@ export interface SmtpSettings {
|
|
|
99
116
|
};
|
|
100
117
|
}
|
|
101
118
|
|
|
119
|
+
export interface GetAttachmentResponse {
|
|
120
|
+
data: {
|
|
121
|
+
id: string;
|
|
122
|
+
filename: string;
|
|
123
|
+
content_type: string;
|
|
124
|
+
size: number;
|
|
125
|
+
expires_at: string | null;
|
|
126
|
+
download_url: string;
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
102
130
|
export interface ListNamespacesResponse {
|
|
103
131
|
data: [
|
|
104
132
|
{
|
|
@@ -123,4 +151,8 @@ export interface SendVirtualEmailParams {
|
|
|
123
151
|
text?: string | undefined;
|
|
124
152
|
/** The HTML version of the message */
|
|
125
153
|
html?: string | undefined;
|
|
154
|
+
/** Custom headers for the email */
|
|
155
|
+
headers?: Record<string, string>;
|
|
156
|
+
/** Attachments to the email */
|
|
157
|
+
attachments?: Attachment[];
|
|
126
158
|
}
|
package/src/mailisk.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import axios, { AxiosRequestConfig } from "axios";
|
|
1
|
+
import axios, { AxiosBasicCredentials, AxiosRequestConfig } from "axios";
|
|
2
2
|
import {
|
|
3
|
+
GetAttachmentResponse,
|
|
3
4
|
ListNamespacesResponse,
|
|
4
5
|
SearchInboxParams,
|
|
5
6
|
SearchInboxResponse,
|
|
@@ -9,12 +10,13 @@ import {
|
|
|
9
10
|
import nodemailer from "nodemailer";
|
|
10
11
|
|
|
11
12
|
export class MailiskClient {
|
|
12
|
-
constructor({ apiKey, baseUrl }: { apiKey: string; baseUrl?: string }) {
|
|
13
|
+
constructor({ apiKey, baseUrl, auth }: { apiKey: string; baseUrl?: string; auth?: AxiosBasicCredentials }) {
|
|
13
14
|
this.axiosInstance = axios.create({
|
|
14
15
|
headers: {
|
|
15
16
|
"X-Api-Key": apiKey,
|
|
16
17
|
},
|
|
17
18
|
baseURL: baseUrl || "https://api.mailisk.com/",
|
|
19
|
+
auth,
|
|
18
20
|
});
|
|
19
21
|
}
|
|
20
22
|
|
|
@@ -46,8 +48,6 @@ export class MailiskClient {
|
|
|
46
48
|
async sendVirtualEmail(namespace: string, params: SendVirtualEmailParams): Promise<void> {
|
|
47
49
|
const smtpSettings = await this.getSmtpSettings(namespace);
|
|
48
50
|
|
|
49
|
-
// TODO: to should match namespace
|
|
50
|
-
|
|
51
51
|
const transport = nodemailer.createTransport({
|
|
52
52
|
host: smtpSettings.data.host,
|
|
53
53
|
port: smtpSettings.data.port,
|
|
@@ -58,7 +58,7 @@ export class MailiskClient {
|
|
|
58
58
|
},
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
const { from, to, subject, text, html } = params;
|
|
61
|
+
const { from, to, subject, text, html, headers, attachments } = params;
|
|
62
62
|
|
|
63
63
|
await transport.sendMail({
|
|
64
64
|
from,
|
|
@@ -66,6 +66,8 @@ export class MailiskClient {
|
|
|
66
66
|
subject,
|
|
67
67
|
text,
|
|
68
68
|
html,
|
|
69
|
+
headers,
|
|
70
|
+
attachments,
|
|
69
71
|
});
|
|
70
72
|
|
|
71
73
|
transport.close();
|
|
@@ -136,4 +138,29 @@ export class MailiskClient {
|
|
|
136
138
|
const result = await this.axiosInstance.get(`api/smtp/${namespace}`);
|
|
137
139
|
return result.data;
|
|
138
140
|
}
|
|
141
|
+
|
|
142
|
+
async getAttachment(attachmentId: string): Promise<GetAttachmentResponse> {
|
|
143
|
+
const result = await this.axiosInstance.get(`api/attachments/${attachmentId}`);
|
|
144
|
+
return result.data;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Download an attachment from an attachment ID.
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* Download an attachment from an email
|
|
152
|
+
* ```typescript
|
|
153
|
+
* const attachment = email.attachments[0];
|
|
154
|
+
* const attachmentBuffer = await client.downloadAttachment(attachment.id);
|
|
155
|
+
*
|
|
156
|
+
* // save to file
|
|
157
|
+
* fs.writeFileSync(attachment.filename, attachmentBuffer);
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
async downloadAttachment(attachmentId: string): Promise<Buffer> {
|
|
161
|
+
const result = await this.getAttachment(attachmentId);
|
|
162
|
+
|
|
163
|
+
const response = await axios.get(result.data.download_url, { responseType: "arraybuffer" });
|
|
164
|
+
return Buffer.from(response.data);
|
|
165
|
+
}
|
|
139
166
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Mock namespace response data
|
|
2
|
+
export const mockNamespacesResponse = {
|
|
3
|
+
namespaces: [
|
|
4
|
+
{
|
|
5
|
+
name: "test-namespace",
|
|
6
|
+
created_at: new Date().toISOString(),
|
|
7
|
+
expires_at: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
8
|
+
},
|
|
9
|
+
],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Mock email response data
|
|
13
|
+
export const mockEmailsResponse = {
|
|
14
|
+
emails: [
|
|
15
|
+
{
|
|
16
|
+
id: "email-123",
|
|
17
|
+
from: { address: "sender@example.com", name: "Sender" },
|
|
18
|
+
to: [{ address: "recipient@test-namespace.mailisk.net", name: "Recipient" }],
|
|
19
|
+
subject: "Test Email",
|
|
20
|
+
html: "<p>Test content</p>",
|
|
21
|
+
text: "Test content",
|
|
22
|
+
received_date: new Date().toISOString(),
|
|
23
|
+
received_timestamp: Math.floor(Date.now() / 1000),
|
|
24
|
+
expires_timestamp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
count: 1,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Mock attachment response data
|
|
31
|
+
export const mockAttachmentResponse = {
|
|
32
|
+
id: "attachment-123",
|
|
33
|
+
filename: "test.txt",
|
|
34
|
+
content_type: "text/plain",
|
|
35
|
+
size: 100,
|
|
36
|
+
expires_at: "Fri, 23 Jun 2025 02:29:13 GMT",
|
|
37
|
+
download_url: "https://example.com/attachment-123.txt",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Mock SMTP settings response
|
|
41
|
+
export const mockSmtpSettingsResponse = {
|
|
42
|
+
host: "smtp.mailisk.com",
|
|
43
|
+
port: 25,
|
|
44
|
+
secure: false,
|
|
45
|
+
auth: {
|
|
46
|
+
user: "test-namespace",
|
|
47
|
+
pass: "mock-password",
|
|
48
|
+
},
|
|
49
|
+
};
|
package/tests/setup.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import dotenv from "dotenv";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
dotenv.config({ path: path.resolve(process.cwd(), ".env.test") });
|
|
5
|
+
|
|
6
|
+
jest.setTimeout(10000);
|
|
7
|
+
|
|
8
|
+
process.env.TEST_MODE = "true";
|
|
9
|
+
|
|
10
|
+
export const createTestClient = (mockResponses = {}) => {
|
|
11
|
+
const { MailiskClient } = require("../src/mailisk");
|
|
12
|
+
|
|
13
|
+
return new MailiskClient({
|
|
14
|
+
apiKey: "test-api-key",
|
|
15
|
+
baseUrl: "https://test-api.mailisk.com/",
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
jest.resetAllMocks();
|
|
21
|
+
});
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
jest.mock("axios");
|
|
2
|
+
jest.mock("nodemailer");
|
|
3
|
+
|
|
4
|
+
import axios from "axios";
|
|
5
|
+
import nodemailer from "nodemailer";
|
|
6
|
+
import { MailiskClient } from "../../src/mailisk";
|
|
7
|
+
import {
|
|
8
|
+
mockNamespacesResponse,
|
|
9
|
+
mockEmailsResponse,
|
|
10
|
+
mockSmtpSettingsResponse,
|
|
11
|
+
mockAttachmentResponse,
|
|
12
|
+
} from "../mocks/axios-mocks";
|
|
13
|
+
|
|
14
|
+
const setupMockAxios = () => {
|
|
15
|
+
jest.clearAllMocks();
|
|
16
|
+
|
|
17
|
+
const mockGet = jest.fn();
|
|
18
|
+
const mockPost = jest.fn();
|
|
19
|
+
const mockPut = jest.fn();
|
|
20
|
+
const mockDelete = jest.fn();
|
|
21
|
+
|
|
22
|
+
const mockInstance = {
|
|
23
|
+
get: mockGet,
|
|
24
|
+
post: mockPost,
|
|
25
|
+
put: mockPut,
|
|
26
|
+
delete: mockDelete,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
(axios.create as jest.Mock).mockReturnValue(mockInstance);
|
|
30
|
+
(axios.get as jest.Mock).mockImplementation(jest.fn());
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
mockInstance,
|
|
34
|
+
mockGet,
|
|
35
|
+
mockPost,
|
|
36
|
+
mockPut,
|
|
37
|
+
mockDelete,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
describe("MailiskClient", () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
jest.clearAllMocks();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("constructor", () => {
|
|
47
|
+
it("should initialize with default baseURL when not provided", () => {
|
|
48
|
+
setupMockAxios();
|
|
49
|
+
const client = new MailiskClient({ apiKey: "test-key" });
|
|
50
|
+
expect(axios.create).toHaveBeenCalledWith({
|
|
51
|
+
headers: {
|
|
52
|
+
"X-Api-Key": "test-key",
|
|
53
|
+
},
|
|
54
|
+
baseURL: "https://api.mailisk.com/",
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should initialize with custom baseURL when provided", () => {
|
|
59
|
+
setupMockAxios();
|
|
60
|
+
const client = new MailiskClient({
|
|
61
|
+
apiKey: "test-key",
|
|
62
|
+
baseUrl: "https://custom-api.mailisk.com/",
|
|
63
|
+
});
|
|
64
|
+
expect(axios.create).toHaveBeenCalledWith({
|
|
65
|
+
headers: {
|
|
66
|
+
"X-Api-Key": "test-key",
|
|
67
|
+
},
|
|
68
|
+
baseURL: "https://custom-api.mailisk.com/",
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("listNamespaces", () => {
|
|
74
|
+
it("should fetch and return namespaces", async () => {
|
|
75
|
+
const { mockGet } = setupMockAxios();
|
|
76
|
+
mockGet.mockResolvedValueOnce({ data: mockNamespacesResponse });
|
|
77
|
+
|
|
78
|
+
const client = new MailiskClient({ apiKey: "test-key" });
|
|
79
|
+
const result = await client.listNamespaces();
|
|
80
|
+
|
|
81
|
+
expect(mockGet).toHaveBeenCalledWith("api/namespaces");
|
|
82
|
+
expect(result).toEqual(mockNamespacesResponse);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should handle errors correctly", async () => {
|
|
86
|
+
const { mockGet } = setupMockAxios();
|
|
87
|
+
const error = new Error("API Error");
|
|
88
|
+
mockGet.mockRejectedValueOnce(error);
|
|
89
|
+
|
|
90
|
+
const client = new MailiskClient({ apiKey: "test-key" });
|
|
91
|
+
|
|
92
|
+
await expect(client.listNamespaces()).rejects.toThrow("API Error");
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("getSmtpSettings", () => {
|
|
97
|
+
it("should fetch and return SMTP settings for a namespace", async () => {
|
|
98
|
+
const { mockGet } = setupMockAxios();
|
|
99
|
+
mockGet.mockResolvedValueOnce({ data: mockSmtpSettingsResponse });
|
|
100
|
+
|
|
101
|
+
const client = new MailiskClient({ apiKey: "test-key" });
|
|
102
|
+
const result = await client.getSmtpSettings("test-namespace");
|
|
103
|
+
|
|
104
|
+
expect(mockGet).toHaveBeenCalledWith("api/smtp/test-namespace");
|
|
105
|
+
expect(result).toEqual(mockSmtpSettingsResponse);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should handle errors correctly", async () => {
|
|
109
|
+
const { mockGet } = setupMockAxios();
|
|
110
|
+
const error = new Error("SMTP Settings Error");
|
|
111
|
+
mockGet.mockRejectedValueOnce(error);
|
|
112
|
+
|
|
113
|
+
const client = new MailiskClient({ apiKey: "test-key" });
|
|
114
|
+
|
|
115
|
+
await expect(client.getSmtpSettings("test-namespace")).rejects.toThrow("SMTP Settings Error");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("getAttachment", () => {
|
|
120
|
+
it("should fetch and return attachment data", async () => {
|
|
121
|
+
const { mockGet } = setupMockAxios();
|
|
122
|
+
mockGet.mockResolvedValueOnce({ data: mockAttachmentResponse });
|
|
123
|
+
|
|
124
|
+
const client = new MailiskClient({ apiKey: "test-key" });
|
|
125
|
+
const result = await client.getAttachment("attachment-123");
|
|
126
|
+
|
|
127
|
+
expect(mockGet).toHaveBeenCalledWith("api/attachments/attachment-123");
|
|
128
|
+
expect(result).toEqual(mockAttachmentResponse);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("downloadAttachment", () => {
|
|
133
|
+
it("should download and return attachment data", async () => {
|
|
134
|
+
const getAttachmentSpy = jest.spyOn(MailiskClient.prototype, "getAttachment");
|
|
135
|
+
getAttachmentSpy.mockResolvedValueOnce({ data: mockAttachmentResponse });
|
|
136
|
+
|
|
137
|
+
const mockBuffer = Buffer.from("test content");
|
|
138
|
+
(axios.get as jest.Mock).mockResolvedValueOnce({ data: mockBuffer });
|
|
139
|
+
|
|
140
|
+
const client = new MailiskClient({ apiKey: "test-key" });
|
|
141
|
+
const result = await client.downloadAttachment("attachment-123");
|
|
142
|
+
|
|
143
|
+
expect(axios.get).toHaveBeenCalledWith(mockAttachmentResponse.download_url, { responseType: "arraybuffer" });
|
|
144
|
+
expect(result).toEqual(mockBuffer);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("searchInbox", () => {
|
|
149
|
+
it("should fetch and return emails with default parameters", async () => {
|
|
150
|
+
const { mockGet } = setupMockAxios();
|
|
151
|
+
mockGet.mockResolvedValueOnce({ data: mockEmailsResponse });
|
|
152
|
+
|
|
153
|
+
const client = new MailiskClient({ apiKey: "test-key" });
|
|
154
|
+
const result = await client.searchInbox("test-namespace");
|
|
155
|
+
|
|
156
|
+
expect(mockGet).toHaveBeenCalledWith("api/emails/test-namespace/inbox", {
|
|
157
|
+
maxRedirects: 99999,
|
|
158
|
+
timeout: 1000 * 60 * 5,
|
|
159
|
+
params: {
|
|
160
|
+
from_timestamp: expect.any(Number),
|
|
161
|
+
wait: true,
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
expect(result).toEqual(mockEmailsResponse);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("should use custom parameters if provided", async () => {
|
|
168
|
+
const { mockGet } = setupMockAxios();
|
|
169
|
+
mockGet.mockResolvedValueOnce({ data: mockEmailsResponse });
|
|
170
|
+
|
|
171
|
+
const customParams = {
|
|
172
|
+
limit: 10,
|
|
173
|
+
offset: 5,
|
|
174
|
+
from_timestamp: 1234567890,
|
|
175
|
+
to_timestamp: 1234567899,
|
|
176
|
+
to_addr_prefix: "john",
|
|
177
|
+
from_addr_includes: "@example.com",
|
|
178
|
+
subject_includes: "test",
|
|
179
|
+
wait: false,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const client = new MailiskClient({ apiKey: "test-key" });
|
|
183
|
+
const result = await client.searchInbox("test-namespace", customParams);
|
|
184
|
+
|
|
185
|
+
expect(mockGet).toHaveBeenCalledWith("api/emails/test-namespace/inbox", {
|
|
186
|
+
maxRedirects: 99999,
|
|
187
|
+
params: customParams,
|
|
188
|
+
});
|
|
189
|
+
expect(result).toEqual(mockEmailsResponse);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("should use custom axios config if provided", async () => {
|
|
193
|
+
const { mockGet } = setupMockAxios();
|
|
194
|
+
mockGet.mockResolvedValueOnce({ data: mockEmailsResponse });
|
|
195
|
+
|
|
196
|
+
const customParams = {
|
|
197
|
+
wait: true,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const customConfig = {
|
|
201
|
+
timeout: 10000,
|
|
202
|
+
maxRedirects: 5,
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const client = new MailiskClient({ apiKey: "test-key" });
|
|
206
|
+
const result = await client.searchInbox("test-namespace", customParams, customConfig);
|
|
207
|
+
|
|
208
|
+
expect(mockGet).toHaveBeenCalledWith("api/emails/test-namespace/inbox", {
|
|
209
|
+
...customConfig,
|
|
210
|
+
params: {
|
|
211
|
+
from_timestamp: expect.any(Number),
|
|
212
|
+
wait: true,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
expect(result).toEqual(mockEmailsResponse);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("should handle errors correctly", async () => {
|
|
219
|
+
const { mockGet } = setupMockAxios();
|
|
220
|
+
const error = new Error("Search Inbox Error");
|
|
221
|
+
mockGet.mockRejectedValueOnce(error);
|
|
222
|
+
|
|
223
|
+
const client = new MailiskClient({ apiKey: "test-key" });
|
|
224
|
+
|
|
225
|
+
await expect(client.searchInbox("test-namespace")).rejects.toThrow("Search Inbox Error");
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe("sendVirtualEmail", () => {
|
|
230
|
+
// Setup for nodemailer mock
|
|
231
|
+
const mockSendMail = jest.fn();
|
|
232
|
+
const mockClose = jest.fn();
|
|
233
|
+
const mockTransport = {
|
|
234
|
+
sendMail: mockSendMail,
|
|
235
|
+
close: mockClose,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
beforeEach(() => {
|
|
239
|
+
(nodemailer.createTransport as jest.Mock).mockReturnValue(mockTransport);
|
|
240
|
+
mockSendMail.mockReset().mockResolvedValue({});
|
|
241
|
+
mockClose.mockReset();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("should send an email using nodemailer", async () => {
|
|
245
|
+
const { mockGet } = setupMockAxios();
|
|
246
|
+
mockGet.mockResolvedValueOnce({
|
|
247
|
+
data: {
|
|
248
|
+
data: {
|
|
249
|
+
host: "smtp.mailisk.com",
|
|
250
|
+
port: 25,
|
|
251
|
+
username: "test-namespace",
|
|
252
|
+
password: "mock-password",
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const emailParams = {
|
|
258
|
+
from: "sender@example.com",
|
|
259
|
+
to: "recipient@test-namespace.mailisk.net",
|
|
260
|
+
subject: "Test Subject",
|
|
261
|
+
text: "Test Content",
|
|
262
|
+
html: "<p>Test HTML Content</p>",
|
|
263
|
+
headers: { "X-Custom-Header": "Custom Value" },
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const client = new MailiskClient({ apiKey: "test-key" });
|
|
267
|
+
await client.sendVirtualEmail("test-namespace", emailParams);
|
|
268
|
+
|
|
269
|
+
expect(mockGet).toHaveBeenCalledWith("api/smtp/test-namespace");
|
|
270
|
+
|
|
271
|
+
expect(nodemailer.createTransport).toHaveBeenCalledWith({
|
|
272
|
+
host: "smtp.mailisk.com",
|
|
273
|
+
port: 25,
|
|
274
|
+
secure: false,
|
|
275
|
+
auth: {
|
|
276
|
+
user: "test-namespace",
|
|
277
|
+
pass: "mock-password",
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
expect(mockSendMail).toHaveBeenCalledWith({
|
|
282
|
+
from: "sender@example.com",
|
|
283
|
+
to: "recipient@test-namespace.mailisk.net",
|
|
284
|
+
subject: "Test Subject",
|
|
285
|
+
text: "Test Content",
|
|
286
|
+
html: "<p>Test HTML Content</p>",
|
|
287
|
+
headers: { "X-Custom-Header": "Custom Value" },
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
expect(mockClose).toHaveBeenCalled();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("should handle errors from SMTP settings fetch", async () => {
|
|
294
|
+
const { mockGet } = setupMockAxios();
|
|
295
|
+
const error = new Error("SMTP Settings Error");
|
|
296
|
+
mockGet.mockRejectedValueOnce(error);
|
|
297
|
+
|
|
298
|
+
const emailParams = {
|
|
299
|
+
from: "sender@example.com",
|
|
300
|
+
to: "recipient@test-namespace.mailisk.net",
|
|
301
|
+
subject: "Test Subject",
|
|
302
|
+
text: "Test Content",
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const client = new MailiskClient({ apiKey: "test-key" });
|
|
306
|
+
|
|
307
|
+
await expect(client.sendVirtualEmail("test-namespace", emailParams)).rejects.toThrow("SMTP Settings Error");
|
|
308
|
+
|
|
309
|
+
expect(nodemailer.createTransport).not.toHaveBeenCalled();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("should handle errors from nodemailer sendMail", async () => {
|
|
313
|
+
const { mockGet } = setupMockAxios();
|
|
314
|
+
mockGet.mockResolvedValueOnce({
|
|
315
|
+
data: {
|
|
316
|
+
data: {
|
|
317
|
+
host: "smtp.mailisk.com",
|
|
318
|
+
port: 25,
|
|
319
|
+
username: "test-namespace",
|
|
320
|
+
password: "mock-password",
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const sendMailError = new Error("Failed to send email");
|
|
326
|
+
mockSendMail.mockRejectedValueOnce(sendMailError);
|
|
327
|
+
|
|
328
|
+
const emailParams = {
|
|
329
|
+
from: "sender@example.com",
|
|
330
|
+
to: "recipient@test-namespace.mailisk.net",
|
|
331
|
+
subject: "Test Subject",
|
|
332
|
+
text: "Test Content",
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const client = new MailiskClient({ apiKey: "test-key" });
|
|
336
|
+
|
|
337
|
+
await expect(client.sendVirtualEmail("test-namespace", emailParams)).rejects.toThrow("Failed to send email");
|
|
338
|
+
|
|
339
|
+
// In the actual implementation, if sendMail throws an error, close won't be called
|
|
340
|
+
// We just verify that sendMail was called
|
|
341
|
+
expect(mockSendMail).toHaveBeenCalled();
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
});
|