mailpit-api
Version:
A TypeScript client for interacting with Mailpit's REST API.
744 lines (743 loc) • 25.9 kB
JavaScript
// src/index.ts
import axios, {
isAxiosError
} from "axios";
import { WebSocket as ReconnectingWebSocket } from "partysocket";
import WS from "ws";
var MailpitClient = class {
axiosInstance;
baseURL;
wsURL;
webSocket = null;
eventListeners = /* @__PURE__ */ new Map();
/**
* Creates an instance of {@link MailpitClient}.
* @param baseURL - The base URL of the Mailpit API.
* @param auth - Optional authentication credentials.
* @param auth.username - The username for basic authentication.
* @param auth.password - The password for basic authentication.
* @example No Auth
* ```typescript
* const mailpit = new MailpitClient("http://localhost:8025");
* ```
* @example Basic Auth
* ```typescript
* const mailpit = new MailpitClient("http://localhost:8025", {
* username: "admin",
* password: "supersecret"
* });
* ```
*/
constructor(baseURL, auth) {
if (!baseURL || !/^https?:\/\//.test(baseURL)) {
throw new Error(
"The value of the 'baseURL' parameter must start with http:// or https://"
);
}
this.baseURL = baseURL;
this.wsURL = `${baseURL.replace(/^http/, "ws")}/api/events`;
this.axiosInstance = axios.create({
baseURL,
auth,
validateStatus: function(status) {
return status === 200;
}
});
}
async handleRequest(request, options = { fullResponse: false }) {
try {
const response = await request();
return options.fullResponse ? response : response.data;
} catch (error) {
if (isAxiosError(error)) {
const url = error.config?.url || "UNKNOWN URL";
const method = error.config?.method?.toUpperCase() || "UNKNOWN METHOD";
if (error.response) {
throw new Error(
`Mailpit API Error: ${error.response.status.toString()} ${error.response.statusText} at ${method} ${url}: ${JSON.stringify(error.response.data)}`
);
} else if (error.request) {
throw new Error(
`Mailpit API Error: No response received from server at ${method} ${url}`
);
} else {
throw new Error(
`Mailpit API Error: ${error.toString()} at ${method} ${url}`
);
}
} else {
throw new Error(`Unexpected Error: ${error}`);
}
}
}
/**
* Retrieves information about the Mailpit instance.
*
* @returns Basic runtime information, message totals and latest release version.
* @example
* ```typescript
* const info = await mailpit.getInfo();
* ```
*/
async getInfo() {
return await this.handleRequest(
() => this.axiosInstance.get("/api/v1/info")
);
}
/**
* Retrieves the configuration of the Mailpit web UI.
* @remarks Intended for web UI only!
* @returns Configuration settings
* @example
* ```typescript
* const config = await mailpit.getConfiguration();
* ```
*/
async getConfiguration() {
return await this.handleRequest(
() => this.axiosInstance.get("/api/v1/webui")
);
}
/**
* Retrieves a summary of a specific message and marks it as read.
* @param id - The message database ID. Defaults to `latest` to return the latest message.
* @returns Message summary
* @example
* ```typescript
* const message = await mailpit.getMessageSummary();
* ```
*/
async getMessageSummary(id = "latest") {
return await this.handleRequest(
() => this.axiosInstance.get(
`/api/v1/message/${id}`
)
);
}
/**
* Retrieves the headers of a specific message.
* @remarks Header keys are returned alphabetically.
* @param id - The message database ID. Defaults to `latest` to return the latest message.
* @returns Message headers
* @example
* ```typescript
* const headers = await mailpit.getMessageHeaders();
* ```
*/
async getMessageHeaders(id = "latest") {
return await this.handleRequest(
() => this.axiosInstance.get(
`/api/v1/message/${id}/headers`
)
);
}
/**
* Retrieves a specific attachment from a message.
* @param id - Message database ID or "latest"
* @param partID - The attachment part ID
* @returns Attachment as binary data and the content type
* @example
* ```typescript
* const message = await mailpit.getMessageSummary();
* if (message.Attachments.length) {
* const attachment = await mailpit.getMessageAttachment(message.ID, message.Attachments[0].PartID);
* // Do something with the attachment data
* }
* ```
*/
async getMessageAttachment(id, partID) {
const response = await this.handleRequest(
() => this.axiosInstance.get(
`/api/v1/message/${id}/part/${partID}`,
{ responseType: "arraybuffer" }
),
{ fullResponse: true }
);
return {
data: response.data,
contentType: response.headers["content-type"]
};
}
/**
* Generates a cropped 180x120 JPEG thumbnail of an image attachment from a message.
* Only image attachments are supported.
* @remarks
* If the image is smaller than 180x120 then the image is padded.
* If the attachment is not an image then a blank image is returned.
* @param id - Message database ID or "latest"
* @param partID - The attachment part ID
* @returns Image attachment thumbnail as binary data and the content type
* @example
* ```typescript
* const message = await mailpit.getMessageSummary();
* if (message.Attachments.length) {
* const thumbnail = await mailpit.getAttachmentThumbnail(message.ID, message.Attachments[0].PartID);
* // Do something with the thumbnail data
* }
* ```
*/
async getAttachmentThumbnail(id, partID) {
const response = await this.handleRequest(
() => this.axiosInstance.get(
`/api/v1/message/${id}/part/${partID}/thumb`,
{
responseType: "arraybuffer"
}
),
{ fullResponse: true }
);
return {
data: response.data,
contentType: response.headers["content-type"]
};
}
/**
* Retrieves the full email message source as plain text.
* @param id - The message database ID. Defaults to `latest` to return the latest message.
* @returns Plain text message source
* @example
* ```typescript
* const messageSource = await mailpit.getMessageSource();
* ```
*/
async getMessageSource(id = "latest") {
return await this.handleRequest(
() => this.axiosInstance.get(`/api/v1/message/${id}/raw`)
);
}
/**
* Release a message via a pre-configured external SMTP server.
* @remarks This is only enabled if message relaying has been configured.
* @param id - The message database ID. Use `latest` to return the latest message.
* @param relayTo - Array of email addresses to relay the message to
* @returns Plain text "ok" response
* @example
* ```typescript
* const message = await mailpit.releaseMessage("latest", ["user1@example.test", "user2@example.test"]);
* ```
*/
async releaseMessage(id, relayTo) {
return await this.handleRequest(
() => this.axiosInstance.post(`/api/v1/message/${id}/release`, relayTo)
);
}
/**
* Sends a message
* @param sendReqest - The request containing the message details.
* @returns Response containing database messsage ID
* @example
* ```typescript
* await mailpit.sendMessage(
* From: { Email: "user@example.test", Name: "First LastName" },
* To: [{ Email: "rec@example.test", Name: "Recipient Name"}, {Email: "another@example.test"}],
* Subject: "Test Email",
* );
* ```
*/
async sendMessage(sendReqest) {
return await this.handleRequest(
() => this.axiosInstance.post(
`/api/v1/send`,
sendReqest
)
);
}
/**
* Retrieves a list of message summaries ordered from newest to oldest.
* @remarks Only contains the number of attachments and a snippet of the message body.
* @see {@link MailpitClient.getMessageSummary | getMessageSummary()} for more attachment and body details for a specific message.
* @param start - The pagination offset. Defaults to `0`.
* @param limit - The number of messages to retrieve. Defaults to `50`.
* @returns A list of message summaries
* @example
* ```typescript
* const messages = await.listMessages();
* ```
*/
async listMessages(start = 0, limit = 50) {
return await this.handleRequest(
() => this.axiosInstance.get(
`/api/v1/messages`,
{ params: { start, limit } }
)
);
}
/**
* Set the read status of messages.
* @remarks You can optionally provide an array of `IDs` **OR** a `Search` filter. If neither is set then all messages are updated.
* @param readStatus - The request containing the message database IDs/search string and the read status.
* @param readStatus.Read - The read status to set. Defaults to `false`.
* @param readStatus.IDs - The optional IDs of the messages to update.
* @param readStatus.Search - The optional search string to filter messages.
* @param params - Optional parameters for defining the time zone when using the `before:` and `after:` search filters.
* @see {@link https://mailpit.axllent.org/docs/usage/search-filters/ | Search filters}
* @returns Plain text "ok" response
* @example
* ```typescript
* // Set all messages as unread
* await mailpit.setReadStatus();
*
* // Set all messages as read
* await mailpit.setReadStatus({ Read: true });
*
* // Set specific messages as read using IDs
* await mailpit.setReadStatus({ IDs: ["1", "2", "3"], Read: true });
*
* // Set specific messages as read using search
* await mailpit.setReadStatus({ Search: "from:example.test", Read: true });
*
* // Set specific messages as read using after: search with time zone
* await mailpit.setReadStatus({ Search: "after:2025-04-30", Read: true }, { tz: "America/Chicago" });
* ```
*/
async setReadStatus(readStatus, params) {
return await this.handleRequest(
() => this.axiosInstance.put(`/api/v1/messages`, readStatus, {
params
})
);
}
/**
* Delete individual or all messages.
* @remarks If no `IDs` are provided then all messages are deleted.
* @param deleteRequest - The request containing the message database IDs to delete.
* @returns Plain text "ok" response
* @example
* ```typescript
* // Delete all messages
* await mailpit.deleteMessages();
*
* // Delete specific messages
* await mailpit.deleteMessages({ IDs: ["1", "2", "3"] });
* ```
*/
async deleteMessages(deleteRequest) {
return await this.handleRequest(
() => this.axiosInstance.delete(`/api/v1/messages`, {
data: deleteRequest
})
);
}
/**
* Retrieve messages matching a search, sorted by received date (descending).
* @see {@link https://mailpit.axllent.org/docs/usage/search-filters/ | Search filters}
* @remarks Only contains the number of attachments and a snippet of the message body.
* @see {@link MailpitClient.getMessageSummary | getMessageSummary()} for more attachment and body details for a specific message.
* @param search - The search request containing the query and optional parameters.
* @returns A list of message summaries matching the search criteria.
* @example
* ```typescript
* // Search for messages from a the domain example.test
* const messages = await mailpit.searchMessages({query: "from:example.test"});
* ```
*/
async searchMessages(search) {
return await this.handleRequest(
() => this.axiosInstance.get(`/api/v1/search`, {
params: search
})
);
}
/**
* Delete all messages matching a search.
* @see {@link https://mailpit.axllent.org/docs/usage/search-filters/ | Search filters}
* @param search - The search request containing the query.
* @returns Plain text "ok" response
* @example
* ```typescript
* // Delete all messages from the domain example.test
* await mailpit.deleteMessagesBySearch({query: "from:example.test"});
* ```
*/
async deleteMessagesBySearch(search) {
return await this.handleRequest(
() => this.axiosInstance.delete(`/api/v1/search`, { params: search })
);
}
/**
* Performs an HTML check on a specific message.
* @param id - The message database ID. Defaults to `latest` to return the latest message.
* @returns The summary of the message HTML checker
* @example
* ```typescript
* const htmlCheck = await mailpit.htmlCheck();
* ```
*/
async htmlCheck(id = "latest") {
return await this.handleRequest(
() => this.axiosInstance.get(
`/api/v1/message/${id}/html-check`
)
);
}
/**
* Performs a link check on a specific message.
* @param id - The message database ID. Defaults to `latest` to return the latest message.
* @param follow - Whether to follow links. Defaults to `false`.
* @returns The summary of the message Link checker.
* @example
* ```typescript
* const linkCheck = await mailpit.linkCheck();
* ```
*/
async linkCheck(id = "latest", follow = false) {
return await this.handleRequest(
() => this.axiosInstance.get(
`/api/v1/message/${id}/link-check`,
{ params: { follow } }
)
);
}
/**
* Performs a SpamAssassin check (if enabled) on a specific message.
* @param id - The message database ID. Defaults to `latest` to return the latest message.
* @returns The SpamAssassin summary (if enabled)
* @example
* ```typescript
* const spamAssassinCheck = await mailpit.spamAssassinCheck();
* ```
*/
async spamAssassinCheck(id = "latest") {
return await this.handleRequest(
() => this.axiosInstance.get(
`/api/v1/message/${id}/sa-check`
)
);
}
/**
* Retrieves a list of all the unique tags.
* @returns All unique message tags
* @example
* ```typescript
* const tags = await mailpit.getTags();
* ```
*/
async getTags() {
return await this.handleRequest(
() => this.axiosInstance.get(`/api/v1/tags`)
);
}
/**
* Sets and removes tag(s) on message(s). This will overwrite any existing tags for selected message database IDs.
* @param request - The request containing the message IDs and tags. To remove all tags from a message, pass an empty `Tags` array or exclude `Tags` entirely.
* @remarks
* Tags are limited to the following characters: `a-z`, `A-Z`, `0-9`, `-`, `.`, `spaces`, and `_`, and must be a minimum of 1 character.
* Other characters are silently stripped from the tag.
* @returns Plain text "ok" response
* @example
* ```typescript
* // Set tags on message(s)
* await mailpit.setTags({ IDs: ["1", "2", "3"], Tags: ["tag1", "tag2"] });
* // Remove tags from message(s)
* await mailpit.setTags({ IDs: ["1", "2", "3"]});
* ```
*/
async setTags(request) {
return await this.handleRequest(
() => this.axiosInstance.put(`/api/v1/tags`, request)
);
}
/**
* Renames an existing tag.
* @param tag - The current name of the tag.
* @param newTagName - A new name for the tag.
* @remarks
* Tags are limited to the following characters: `a-z`, `A-Z`, `0-9`, `-`, `.`, `spaces`, and `_`, and must be a minimum of 1 character.
* Other characters are silently stripped from the tag.
* @returns Plain text "ok" response
* @example
* ```typescript
* await mailpit.renameTag("Old Tag Name", "New Tag Name");
* ```
*/
async renameTag(tag, newTagName) {
const encodedTag = encodeURIComponent(tag);
return await this.handleRequest(
() => this.axiosInstance.put(`/api/v1/tags/${encodedTag}`, {
Name: newTagName
})
);
}
/**
* Deletes a tag from all messages.
* @param tag - The name of the tag to delete.
* @remarks This does NOT delete any messages
* @returns Plain text "ok" response
* ```typescript
* await mailpit.deleteTag("Tag 1");
* ```
*/
async deleteTag(tag) {
const encodedTag = encodeURIComponent(tag);
return await this.handleRequest(
() => this.axiosInstance.delete(`/api/v1/tags/${encodedTag}`)
);
}
/**
* Retrieves the current Chaos triggers configuration (if enabled).
* @remarks This will return an error if Chaos is not enabled at runtime.
* @returns The Chaos triggers configuration
* @example
* ```typescript
* const triggers = await mailpit.getChaosTriggers();
* ```
*/
async getChaosTriggers() {
return await this.handleRequest(
() => this.axiosInstance.get("/api/v1/chaos")
);
}
/**
* Sets and/or resets the Chaos triggers configuration (if enabled).
* @param triggers - The request containing the chaos triggers. Omitted triggers will reset to the default `0%` probabibility.
* @remarks This will return an error if Chaos is not enabled at runtime.
* @returns The updated Chaos triggers configuration
* @example
* ```typescript
* // Reset all triggers to `0%` probability
* const triggers = await mailpit.setChaosTriggers();
* // Set `Sender` and reset `Authentication` and `Recipient` triggers
* const triggers = await mailpit.setChaosTriggers({ Sender: { ErrorCode: 451, Probability: 5 } });
* ```
*/
async setChaosTriggers(triggers = {}) {
return await this.handleRequest(
() => this.axiosInstance.put(
"/api/v1/chaos",
triggers
)
);
}
/**
* Renders the HTML part of a specific message which can be used for UI integration testing.
* @remarks
* Attached inline images are modified to link to the API provided they exist.
* If the message does not contain an HTML part then a 404 error is returned.
*
*
* @param id - The message database ID. Defaults to `latest` to return the latest message.
* @param embed - Whether this route is to be embedded in an iframe. Defaults to `undefined`. Set to `1` to embed.
* The `embed` parameter will add `target="_blank"` and `rel="noreferrer noopener"` to all links.
* In addition, a small script will be added to the end of the document to post (postMessage()) the height of the document back to the parent window for optional iframe height resizing.
* Note that this will also transform the message into a full HTML document (if it isn't already), so this option is useful for viewing but not programmatic testing.
* @returns Rendered HTML
* @example
* ```typescript
* const html = await mailpit.renderMessageHTML();
* ```
*/
async renderMessageHTML(id = "latest", embed) {
return await this.handleRequest(
() => this.axiosInstance.get(`/view/${id}.html`, { params: { embed } })
);
}
/**
* Renders just the message's text part which can be used for UI integration testing.
* @param id - The message database ID. Defaults to `latest` to return the latest message.
* @returns Plain text
* @example
* ```typescript
* const html = await mailpit.renderMessageText();
* ```
*/
async renderMessageText(id = "latest") {
return await this.handleRequest(
() => this.axiosInstance.get(`/view/${id}.txt`)
);
}
/**
* @internal
* Connects to the WebSocket endpoint for receiving real-time events.
*/
connectWebSocket() {
if (this.webSocket && (this.webSocket.readyState === ReconnectingWebSocket.OPEN || this.webSocket.readyState === ReconnectingWebSocket.CONNECTING)) {
return;
}
const wsOptions = {};
if (this.axiosInstance.defaults.auth) {
wsOptions.headers = {
Authorization: `Basic ${Buffer.from(`${this.axiosInstance.defaults.auth.username}:${this.axiosInstance.defaults.auth.password}`).toString("base64")}`
};
}
class AuthenticatedWebSocket extends WS {
constructor(address, options) {
super(address, { ...wsOptions, ...options });
}
}
this.webSocket = new ReconnectingWebSocket(this.wsURL, void 0, {
WebSocket: AuthenticatedWebSocket
});
this.webSocket.addEventListener("message", (event) => {
let message;
try {
message = JSON.parse(event.data);
} catch {
return;
}
this.handleWebSocketMessage(message);
});
}
/**
* @internal
* Adds a listener to the event listeners map.
* @param eventType - The type of event to listen for
* @param listener - The listener function to add
*/
addListener(eventType, listener) {
if (!this.eventListeners.has(eventType)) {
this.eventListeners.set(eventType, /* @__PURE__ */ new Set());
}
this.eventListeners.get(eventType)?.add(listener);
}
/**
* @internal
* Removes a listener from the event listeners map.
* @param eventType - The type of event to remove the listener from
* @param listener - The listener function to remove
*/
removeListener(eventType, listener) {
const listeners = this.eventListeners.get(eventType);
if (listeners) {
listeners.delete(listener);
if (listeners.size === 0) {
this.eventListeners.delete(eventType);
}
}
}
/**
* @internal
* Dispatches a message to listeners of a specific event type.
* @param eventType - The event type to dispatch to
* @param message - The event message
*/
dispatchToListeners(eventType, message) {
const listeners = this.eventListeners.get(eventType);
if (listeners) {
listeners.forEach((listener) => {
listener(message);
});
}
}
/**
* @internal
* Handles incoming WebSocket messages and dispatches them to registered listeners.
* @param message - The event message
*/
handleWebSocketMessage(message) {
this.dispatchToListeners(message.Type, message);
this.dispatchToListeners("*", message);
}
/**
* Disconnects from the real-time event stream.
* @example
* ```typescript
* mailpit.disconnect();
* ```
*/
disconnect() {
if (this.webSocket) {
const ws = this.webSocket;
this.webSocket = null;
ws.close(1e3, "Client disconnect");
}
}
/**
* Registers a listener for real-time events of a specific type.
* @remarks
* Automatically connects to the event stream if not already connected.
* @param eventType - The type of event to listen for.
* Specific event types include: "new" (new messages), "stats", "update", "delete", "prune", "truncate", and "error".
* Use "*" to listen for all event types (Useful if processing all events uniformly (e.g., logging, debugging, metrics)).
* @param listener - The callback function to invoke when an event is received
* @returns A function to unregister the listener
* @example Listen for event type "new" messages (recommended)
* ```typescript
* const unsubscribe = mailpit.onEvent("new", (event) => {
* // event.Data is typed as MailpitMessageSummary with full type safety
* console.log("New message:", event.Data.Subject);
* });
*
* // Other code...
*
* // Unsubscribe listener when no longer needed
* unsubscribe();
* ```
* @example Listen for all events uniformly (for logging/debugging)
* ```typescript
* const unsubscribe = mailpit.onEvent("*", (event) => {
* // Generic processing for all event types
* console.log(`Event ${event.Type} received`);
* });
*
* // Other code...
*
* // Unsubscribe listener when no longer needed
* unsubscribe();
* ```
*/
onEvent(eventType, listener) {
if (!this.webSocket || this.webSocket.readyState === ReconnectingWebSocket.CLOSED) {
this.connectWebSocket();
}
this.addListener(eventType, listener);
return () => {
this.removeListener(eventType, listener);
};
}
/**
* Waits for the next event of a specific type.
* @remarks
* Automatically connects to the event stream if not already connected.
* Primarily intended for testing scenarios where you need to wait for a single specific event.
* The promise will reject if the timeout is reached before an event is received.
* @param eventType - The type of event to wait for.
* Specific event types include: "new" (new messages), "stats", "update", "delete", "prune", "truncate", and "error".
* @param timeout - Timeout in milliseconds (default: 5000ms). Pass `Infinity` to disable timeout.
* @returns A promise that resolves with the event when received, or rejects on timeout
* @example Basic usage
* ```typescript
* // Create the promise before triggering the event
* const eventPromise = mailpit.waitForEvent("new");
*
* // Do something that triggers an email to send
* await mailpit.sendMessage({
* From: { Email: "test@example.com" },
* To: [{ Email: "recipient@example.com" }],
* Subject: "Test",
* });
*
* // Wait for the event confirming the message was received
* const event = await eventPromise;
* // event.Data is fully typed as MailpitMessageSummary
* console.log("Message received:", event.Data.Subject);
* ```
*/
waitForEvent(eventType, timeout = 5e3) {
if (!this.webSocket || this.webSocket.readyState === ReconnectingWebSocket.CLOSED) {
this.connectWebSocket();
}
return new Promise((resolve, reject) => {
let timer = null;
const cleanup = () => {
if (timer) {
clearTimeout(timer);
}
this.removeListener(eventType, listener);
};
const listener = (event) => {
cleanup();
resolve(event);
};
this.addListener(eventType, listener);
if (isFinite(timeout)) {
timer = setTimeout(() => {
cleanup();
reject(new Error(`Timeout waiting for event of type "${eventType}"`));
}, timeout);
}
});
}
};
export {
MailpitClient
};