mailisk 2.0.0 → 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.
@@ -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 client.sendVirtualEmail(namespace, {
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 client.searchInbox(namespace);
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
- The `searchInbox` function takes a namespace and call parameters.
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
- - By default it uses the `wait` flag. This means the call won't return until at least one email is received. Disabling this flag via `wait: false` can cause it to return an empty response immediately.
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
- // timeout of 5 minutes
67
- await mailisk.searchInbox(namespace);
68
- // timeout of 1 minute
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
- // returns immediately, even if the result would be empty
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 use case is filtering the returned emails by the destination address, this is done using the `to_addr_prefix` parameter.
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: "john@mynamespace.mailisk.net",
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
  /**
@@ -52,9 +67,21 @@ interface SearchInboxParams {
52
67
  /**
53
68
  * Filter emails by 'to' address. Address must start with this.
54
69
  *
55
- * 'foo' would return 'foobar@namespace.mailisk.net' but not 'barfoo@namespace.mailisk.net'
70
+ * 'foo' would return for 'foobar@namespace.mailisk.net' but not 'barfoo@namespace.mailisk.net'
56
71
  */
57
72
  to_addr_prefix?: string;
73
+ /**
74
+ * Filter emails by 'from' address. Address must include this.
75
+ *
76
+ * '@foo' would return for 'a@foo.com', 'b@foo.net'
77
+ */
78
+ from_addr_includes?: string;
79
+ /**
80
+ * Filter emails by subject. This is case insensitive. Subject must include this.
81
+ *
82
+ * 'password' would return for 'Password reset', 'Reset password notification' but not 'Reset'
83
+ */
84
+ subject_includes?: string;
58
85
  /**
59
86
  * Will keep the request going till at least one email would be returned.
60
87
  *
@@ -84,6 +111,16 @@ interface SmtpSettings {
84
111
  password: string;
85
112
  };
86
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
+ }
87
124
  interface ListNamespacesResponse {
88
125
  data: [
89
126
  {
@@ -107,12 +144,17 @@ interface SendVirtualEmailParams {
107
144
  text?: string | undefined;
108
145
  /** The HTML version of the message */
109
146
  html?: string | undefined;
147
+ /** Custom headers for the email */
148
+ headers?: Record<string, string>;
149
+ /** Attachments to the email */
150
+ attachments?: Attachment[];
110
151
  }
111
152
 
112
153
  declare class MailiskClient {
113
- constructor({ apiKey, baseUrl }: {
154
+ constructor({ apiKey, baseUrl, auth }: {
114
155
  apiKey: string;
115
156
  baseUrl?: string;
157
+ auth?: AxiosBasicCredentials;
116
158
  });
117
159
  private readonly axiosInstance;
118
160
  /**
@@ -163,6 +205,21 @@ declare class MailiskClient {
163
205
  * Get the SMTP settings for a namespace.
164
206
  */
165
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>;
166
223
  }
167
224
 
168
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,30 +59,35 @@ 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
  }
71
74
  async searchInbox(namespace, params, config) {
72
75
  let _params = { ...params };
73
76
  if (params?.from_timestamp === void 0 || params?.from_timestamp === null) {
74
- _params.from_timestamp = Math.floor(new Date().getTime() / 1e3) - 5;
77
+ _params.from_timestamp = Math.floor(new Date().getTime() / 1e3) - 15 * 60;
75
78
  }
76
79
  if (params?.wait !== false) {
77
80
  _params.wait = true;
78
81
  }
79
82
  let _config = { ...config };
83
+ if (!config?.maxRedirects) {
84
+ _config.maxRedirects = 99999;
85
+ }
80
86
  if (_params.wait && !config?.timeout) {
81
87
  _config.timeout = 1e3 * 60 * 5;
82
88
  }
83
89
  return (await this.axiosInstance.get(`api/emails/${namespace}/inbox`, {
84
- ...config,
90
+ ..._config,
85
91
  params: _params
86
92
  })).data;
87
93
  }
@@ -89,6 +95,15 @@ var MailiskClient = class {
89
95
  const result = await this.axiosInstance.get(`api/smtp/${namespace}`);
90
96
  return result.data;
91
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
+ }
92
107
  };
93
108
  __name(MailiskClient, "MailiskClient");
94
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 // TODO: to should match 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 } = params;\n\n await transport.sendMail({\n from,\n to,\n subject,\n text,\n html,\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, 5 seconds before starting this request\n if (params?.from_timestamp === undefined || params?.from_timestamp === null) {\n _params.from_timestamp = Math.floor(new Date().getTime() / 1000) - 5;\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 // 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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAA0C;AAQ1C,wBAAuB;AAEhB,IAAM,gBAAN,MAAoB;AAAA,EACzB,YAAY,EAAE,QAAQ,QAAQ,GAAyC;AACrE,SAAK,gBAAgB,aAAAA,QAAM,OAAO;AAAA,MAChC,SAAS;AAAA,QACP,aAAa;AAAA,MACf;AAAA,MACA,SAAS,WAAW;AAAA,IACtB,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;AAIzD,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,KAAK,IAAI;AAE1C,UAAM,UAAU,SAAS;AAAA,MACvB;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;AAAA,IACrE;AAGA,QAAI,QAAQ,SAAS,OAAO;AAC1B,cAAQ,OAAO;AAAA,IACjB;AAEA,QAAI,UAAU,EAAE,GAAG,OAAO;AAG1B,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;AACF;AA5Ha;","names":["axios","nodemailer"]}
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,30 +29,35 @@ 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
  }
41
44
  async searchInbox(namespace, params, config) {
42
45
  let _params = { ...params };
43
46
  if (params?.from_timestamp === void 0 || params?.from_timestamp === null) {
44
- _params.from_timestamp = Math.floor(new Date().getTime() / 1e3) - 5;
47
+ _params.from_timestamp = Math.floor(new Date().getTime() / 1e3) - 15 * 60;
45
48
  }
46
49
  if (params?.wait !== false) {
47
50
  _params.wait = true;
48
51
  }
49
52
  let _config = { ...config };
53
+ if (!config?.maxRedirects) {
54
+ _config.maxRedirects = 99999;
55
+ }
50
56
  if (_params.wait && !config?.timeout) {
51
57
  _config.timeout = 1e3 * 60 * 5;
52
58
  }
53
59
  return (await this.axiosInstance.get(`api/emails/${namespace}/inbox`, {
54
- ...config,
60
+ ..._config,
55
61
  params: _params
56
62
  })).data;
57
63
  }
@@ -59,6 +65,15 @@ var MailiskClient = class {
59
65
  const result = await this.axiosInstance.get(`api/smtp/${namespace}`);
60
66
  return result.data;
61
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
+ }
62
77
  };
63
78
  __name(MailiskClient, "MailiskClient");
64
79
  export {
@@ -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 // TODO: to should match 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 } = params;\n\n await transport.sendMail({\n from,\n to,\n subject,\n text,\n html,\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, 5 seconds before starting this request\n if (params?.from_timestamp === undefined || params?.from_timestamp === null) {\n _params.from_timestamp = Math.floor(new Date().getTime() / 1000) - 5;\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 // 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"],"mappings":";;;;AAAA,OAAO,WAAmC;AAQ1C,OAAO,gBAAgB;AAEhB,IAAM,gBAAN,MAAoB;AAAA,EACzB,YAAY,EAAE,QAAQ,QAAQ,GAAyC;AACrE,SAAK,gBAAgB,MAAM,OAAO;AAAA,MAChC,SAAS;AAAA,QACP,aAAa;AAAA,MACf;AAAA,MACA,SAAS,WAAW;AAAA,IACtB,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;AAIzD,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,KAAK,IAAI;AAE1C,UAAM,UAAU,SAAS;AAAA,MACvB;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;AAAA,IACrE;AAGA,QAAI,QAAQ,SAAS,OAAO;AAC1B,cAAQ,OAAO;AAAA,IACjB;AAEA,QAAI,UAAU,EAAE,GAAG,OAAO;AAG1B,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;AACF;AA5Ha;","names":[]}
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.0.0",
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();
@@ -112,6 +114,10 @@ export class MailiskClient {
112
114
 
113
115
  let _config = { ...config };
114
116
 
117
+ if (!config?.maxRedirects) {
118
+ _config.maxRedirects = 99999;
119
+ }
120
+
115
121
  // by default, wait 5 minutes for emails before timing out
116
122
  if (_params.wait && !config?.timeout) {
117
123
  _config.timeout = 1000 * 60 * 5;
@@ -119,7 +125,7 @@ export class MailiskClient {
119
125
 
120
126
  return (
121
127
  await this.axiosInstance.get(`api/emails/${namespace}/inbox`, {
122
- ...config,
128
+ ..._config,
123
129
  params: _params,
124
130
  })
125
131
  ).data;
@@ -132,4 +138,29 @@ export class MailiskClient {
132
138
  const result = await this.axiosInstance.get(`api/smtp/${namespace}`);
133
139
  return result.data;
134
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
+ }
135
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
+ });