fastmail-masked-email
Version:
A library for creating, deleting, and updating Fastmail masked emails
340 lines • 14.5 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MaskedEmailService = void 0;
const axios_1 = __importDefault(require("axios"));
const debug_1 = __importDefault(require("debug"));
const constants_1 = require("./constants");
const invalidArgumentError_1 = require("./error/invalidArgumentError");
/**
* MaskedEmailService - A comprehensive service for managing Fastmail masked emails
* Provides methods for creating, retrieving, updating, and deleting masked email addresses
*/
class MaskedEmailService {
/**
* Creates a new MaskedEmailService instance
* @param token - Optional Fastmail API token for authentication
* @param hostname - Optional Fastmail API hostname; defaults to api.fastmail.com
*/
constructor(token, hostname) {
this.token = token;
this.hostname = hostname;
this.session = null;
this.debugLogger = (0, debug_1.default)('MaskedEmailService:debug');
this.errorLogger = (0, debug_1.default)('MaskedEmailService:error');
this.token = token || process.env.JMAP_TOKEN;
this.hostname = hostname || process.env.JMAP_HOSTNAME || constants_1.API_HOSTNAME;
}
/**
* Initialize the service by getting a session from the JMAP server
* @returns Promise that resolves when the session is established
* @throws Error if no auth token is provided
*/
async initialize() {
if (!this.token) {
throw new Error('No auth token provided and JMAP_TOKEN environment variable is not set. Please provide a token.');
}
const authUrl = `https://${this.hostname}/jmap/session`;
const headers = this.buildHeaders(this.token);
try {
const response = await axios_1.default.get(authUrl, { headers });
this.session = response.data;
this.debugLogger('Session initialized: %o', JSON.stringify(this.session));
}
catch (error) {
return this.handleAxiosError(error, constants_1.Action.SESSION);
}
}
getSession() {
this.ensureInitialized();
return this.session;
}
/**
* Creates a new masked email address
* @param options - The {@link CreateOptions|options} for creating the masked email
* @throws {@link InvalidArgumentError} if no session is provided
*/
async createEmail(options = {}) {
this.ensureInitialized();
const { apiUrl, accountId } = this.parseSession();
const headers = this.buildHeaders(this.token);
const state = options.state || 'enabled';
const requestBody = {
using: [constants_1.JMAP.CORE, constants_1.MASKED_EMAIL_CAPABILITY],
methodCalls: [
[
constants_1.MASKED_EMAIL_CALLS.set,
{
accountId,
create: {
['0']: Object.assign(Object.assign({}, options), { state })
}
},
'a'
]
]
};
this.debugLogger('createEmail() request body: %o', JSON.stringify(requestBody));
try {
const response = await axios_1.default.post(apiUrl, requestBody, { headers });
this.debugLogger('createEmail() response: %o', JSON.stringify(response.data));
const { data } = response;
return Object.assign(Object.assign({}, data.methodResponses[0][1].created['0']), { state });
}
catch (error) {
return this.handleAxiosError(error, constants_1.Action.CREATE);
}
}
/**
* Retrieves all masked emails
* @throws {@link InvalidArgumentError} if no session is provided
* @returns A list of all {@link MaskedEmail} objects
*/
async getAllEmails() {
this.ensureInitialized();
const { apiUrl, accountId } = this.parseSession();
const headers = this.buildHeaders(this.token);
const body = {
using: [constants_1.JMAP.CORE, constants_1.MASKED_EMAIL_CAPABILITY],
methodCalls: [[constants_1.MASKED_EMAIL_CALLS.get, { accountId, ids: null }, 'a']]
};
this.debugLogger('getAllEmails() body: %o', JSON.stringify(body));
try {
const response = await axios_1.default.post(apiUrl, body, {
headers
});
this.debugLogger('getAllEmails() response: %o', JSON.stringify(response.data));
const jmapResponse = response.data;
const methodResponse = jmapResponse.methodResponses[0][1];
return methodResponse.list;
}
catch (error) {
return this.handleAxiosError(error, constants_1.Action.LIST);
}
}
/**
* Get a masked email by id
* @param id - The id of the masked email address.
* @returns A {@link MaskedEmail} object
* @throws {@link InvalidArgumentError} if no session is provided or no id is provided
*/
async getEmailById(id) {
this.ensureInitialized();
if (!id) {
return Promise.reject(new invalidArgumentError_1.InvalidArgumentError('No id provided'));
}
const { apiUrl, accountId } = this.parseSession();
const headers = this.buildHeaders(this.token);
const body = {
using: [constants_1.JMAP.CORE, constants_1.MASKED_EMAIL_CAPABILITY],
methodCalls: [[constants_1.MASKED_EMAIL_CALLS.get, { accountId, ids: [id] }, 'a']]
};
try {
const response = await axios_1.default.post(apiUrl, body, {
headers
});
this.debugLogger('getEmailById() body: %o', JSON.stringify(body));
const responseData = response.data;
this.debugLogger('getEmailById() response %o', JSON.stringify(response.data));
if (this.maskedEmailNotFound(id, responseData)) {
return Promise.reject(new Error(`No masked email found with id ${id}`));
}
return responseData.methodResponses[0][1].list[0];
}
catch (error) {
return this.handleAxiosError(error, constants_1.Action.GET_BY_ID);
}
}
/**
* Get a masked email by address
* @param address - The address to retrieve
* @returns A {@link MaskedEmail} object
*/
async getEmailByAddress(address) {
try {
const maskedEmails = await this.getAllEmails();
return this.filterByAddress(address, maskedEmails);
}
catch (error) {
return Promise.reject(error);
}
}
/**
* Updates a masked email
* @param id - The id of the masked email to update
* @param options - The {@link Options} containing the fields to update
* @throws {@link InvalidArgumentError} if no id is provided, no session is provided, or the {@link Options} are empty
*/
async updateEmail(id, options) {
this.ensureInitialized();
if (!id) {
return Promise.reject(new invalidArgumentError_1.InvalidArgumentError('No id provided'));
}
if (Object.keys(options).length === 0) {
return Promise.reject(new invalidArgumentError_1.InvalidArgumentError('No options provided. Please provide at least one option to updateEmail.'));
}
const validOptions = ['description', 'forDomain', 'state'];
const invalidOptions = Object.keys(options).filter((option) => !validOptions.includes(option));
if (invalidOptions.length > 0) {
return Promise.reject(new invalidArgumentError_1.InvalidArgumentError(`Invalid options provided: ${invalidOptions.join(', ')}`));
}
const { apiUrl, accountId } = this.parseSession();
const headers = this.buildHeaders(this.token);
const body = {
using: [constants_1.JMAP.CORE, constants_1.MASKED_EMAIL_CAPABILITY],
methodCalls: [
[
constants_1.MASKED_EMAIL_CALLS.set,
{ accountId, update: { [id]: Object.assign({}, options) } },
'a'
]
]
};
this.debugLogger('updateEmail() body: %o', JSON.stringify(body));
try {
const response = await axios_1.default.post(apiUrl, body, { headers });
this.debugLogger('updateEmail() response: %o', JSON.stringify(response.data));
const data = await response.data;
return data.methodResponses[0][1].updated;
}
catch (error) {
return this.handleAxiosError(error, constants_1.Action.UPDATE);
}
}
/**
* Deletes a masked email by setting the state to deleted
* @param id - The id of the masked email to deleteEmail
*/
async deleteEmail(id) {
return await this.updateEmail(id, { state: 'deleted' });
}
/**
* Disables a masked email by setting the state to disabled
* @param id - The id of the masked email to disableEmail
*/
async disableEmail(id) {
return await this.updateEmail(id, { state: 'disabled' });
}
/**
* Enables a masked email by setting the state to enabled
* @param id - The id of the masked email to enableEmail
*/
async enableEmail(id) {
return await this.updateEmail(id, { state: 'enabled' });
}
/**
* Permanently deletes a masked email
* @param id - The id of the masked email to permanently deleteEmail
* @throws {@link InvalidArgumentError} if no id is provided or no session is provided
*/
async permanentlyDeleteEmail(id) {
this.ensureInitialized();
if (!id) {
return Promise.reject(new invalidArgumentError_1.InvalidArgumentError('No id provided'));
}
const { apiUrl, accountId } = this.parseSession();
const headers = this.buildHeaders(this.token);
const body = {
using: [constants_1.JMAP.CORE, constants_1.MASKED_EMAIL_CAPABILITY],
methodCalls: [[constants_1.MASKED_EMAIL_CALLS.set, { accountId, destroy: [id] }, 'a']]
};
this.debugLogger('permanentlyDeleteEmail() body: %o', JSON.stringify(body));
try {
const response = await axios_1.default.post(apiUrl, body, { headers });
this.debugLogger('permanentlyDeleteEmail() response: %o', JSON.stringify(response.data));
const data = await response.data;
// Check if the email was not destroyed
if (data.methodResponses[0][1].notDestroyed) {
const notDestroyedObj = data.methodResponses[0][1].notDestroyed[id];
this.errorLogger('permanentlyDeleteEmail() error: %o', JSON.stringify(notDestroyedObj));
return Promise.reject(new Error(notDestroyedObj.description));
}
else {
return data.methodResponses[0][1].destroyed;
}
}
catch (error) {
return this.handleAxiosError(error, constants_1.Action.DELETE);
}
}
/**
* Filter masked emails by state
* @param state - The state to filter by
* @param list - The list of masked emails (optional, will fetch all if not provided)
* @returns Promise that resolves to a filtered {@link MaskedEmail} array
*/
async filterByState(state, list) {
const emails = list || (await this.getAllEmails());
return emails.filter((me) => me.state === state);
}
/**
* Filter masked emails by domain
* @param domain - The domain to filter by
* @param list - The list of masked emails (optional, will fetch all if not provided)
* @returns Promise that resolves to a filtered {@link MaskedEmail} array
*/
async filterByForDomain(domain, list) {
const emails = list || (await this.getAllEmails());
return emails.filter((me) => me.forDomain === domain);
}
// Private utility methods
ensureInitialized() {
if (!this.session) {
throw new invalidArgumentError_1.InvalidArgumentError('Service not initialized. Call initialize() first.');
}
}
parseSession() {
const accountId = this.session.primaryAccounts[constants_1.JMAP.CORE];
const { apiUrl } = this.session;
return {
accountId,
apiUrl
};
}
/**
* Builds headers for requests using the JMAP token
* @param authToken - The JMAP authentication token
*/
buildHeaders(authToken) {
return {
'Content-Type': 'application/json',
Authorization: `Bearer ${authToken}`
};
}
maskedEmailNotFound(id, response) {
const notFoundIds = response.methodResponses[0][1].notFound;
if (notFoundIds && notFoundIds.length > 0) {
return notFoundIds.includes(id);
}
return false;
}
filterByAddress(address, list) {
return list.filter((me) => me.email === address);
}
/**
* Handles an axios error and returns a rejected promise with a formatted error message based on the type of error and action attempted.
* @param error - The axios error
* @param action - The action that was being performed when the error occurred
*/
async handleAxiosError(error, action) {
if (error.response) {
const errorMessage = `${action} failed with status code ${error.response.status}: ${error.response.statusText}. ${error.response.data}`;
this.errorLogger('Error response from axios: %o', error.response);
return Promise.reject(new Error(errorMessage));
}
else if (error.request) {
const errorMessage = `${action} request was made, but no response was received. Error message: ${error.message}`;
this.errorLogger('Error request: %o', error.request);
return Promise.reject(new Error(errorMessage));
}
else {
const errorMessage = `An error occurred while ${action.toLowerCase()}. Error message: ${error.message}`;
this.errorLogger('Error: %o', error);
return Promise.reject(new Error(errorMessage));
}
}
}
exports.MaskedEmailService = MaskedEmailService;
//# sourceMappingURL=MaskedEmailService.js.map