@email-service/email-service
Version:
email-service is a versatile npm package designed to simplify the integration and standardization of email communications across multiple Email Service Providers (ESPs).
283 lines (280 loc) • 12.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.PostMarkEmailService = void 0;
const error_js_1 = require("../../utils/error.js");
const esp_js_1 = require("../esp.js");
const postMark_status_js_1 = require("./postMark.status.js");
const postMark_errors_js_1 = require("./postMark.errors.js");
class PostMarkEmailService extends esp_js_1.ESP {
constructor(service, opts) {
super(service, opts);
this.mailOutbound = [];
this.getSuppressionInfos = async (address) => {
const extractAddressFrom = (destination) => destination.match(/<.+@.+>/)?.[0].replace(/[<>]/g, "") || destination;
try {
const response = await fetch(`https://api.postmarkapp.com/message-streams/${this.transporter.stream}/suppressions/dump`, {
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"X-Postmark-Server-Token": this.transporter.apiKey
}
});
const result = await response.json();
return result.Suppressions.find((r) => r.EmailAddress === extractAddressFrom(address));
}
catch (error) {
console.log(error);
}
};
this.getBounceDump = async (id) => {
try {
const response = await fetch(`https://api.postmarkapp.com/bounces/${id}/dump`, {
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"X-Postmark-Server-Token": this.transporter.apiKey
}
});
const result = await response.json();
return result.Body;
}
catch (error) {
return 'No dump available';
}
};
this.mailMultiple = true; // Postmark does not support sending multiple emails in one request
// TODO HENRI
/*
Recherche dans Postmark la liste des suppressions pour chaque email en erreur 406
Les mettre dans mailOutbound = [ { email, reason } ]
*/
}
async doSendMail(options) {
if (this.transporter.stream === undefined) {
if (this.transporter.logger)
console.log('******** ES-SendMail Postmark ******** Stream for ', this.transporter.esp, ' is not defined in the configuration');
throw new Error('Stream is not defined in the configuration');
}
// TODO HENRI : Tester si l'email est dans la liste des suppressions
// Si oui, ne pas envoyer et retourner une erreur immédiate
// this.mailOutbound.find(m => m[0] === (options.to as Recipient[])[0].email)
try {
const body = {
MessageStream: this.transporter.stream,
From: formatFromForPostMark(options.from),
To: formatForPostMark(options.to),
Cc: options.cc ? formatForPostMark(options.cc) : undefined,
Bcc: options.bcc ? formatForPostMark(options.bcc) : undefined,
Subject: options.subject,
HtmlBody: options.html,
TextBody: options.text,
Tag: options.tag,
ReplyTo: formatFromForPostMark(options.from),
Metadata: options.metaData,
TrackOpens: options.trackOpens,
TrackLinks: options.trackLinks,
Headers: options.headers
};
const opts = {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-Postmark-Server-Token': this.transporter.apiKey
},
body: JSON.stringify(body)
};
if (this.transporter.logger)
console.log('******** ES-SendMail Postmark ******** to ', body.To);
const response = await fetch('https://api.postmarkapp.com/email', opts);
if (this.transporter.logger)
console.log('******** ES-SendMail Postmark ******** response from fetch', response.status, response.statusText);
const retour = await response.json();
if (this.transporter.logger)
console.log('******** ES-SendMail Postmark ******** json', retour.ErrorCode, retour.Message);
const returnedValue = await this.sendMailResultManagement(retour, response, options);
if (this.transporter.logger)
console.log('******** ES-SendMail Postmark ******** returneValue', returnedValue);
return returnedValue;
}
catch (error) {
return { success: false, status: 500, error: (0, error_js_1.errorManagement)(error) };
}
}
async sendMailMultiple(emails) {
const messageStream = this.transporter.stream;
const apiKey = this.transporter.apiKey;
const logger = this.transporter.logger;
const myClass = this;
// Fonction principale qui traite par batchs et agrège les résultats
async function processInBatches(data, processBatch, batchSize = 499) {
const results = [];
const totalBatches = Math.ceil(data.length / batchSize);
for (let i = 0; i < totalBatches; i++) {
const start = i * batchSize;
const end = start + batchSize;
const batch = data.slice(start, end);
console.log(`Processing batch ${batch.length} emails...`);
const batchResult = await processBatch(batch, i);
results.push(...batchResult);
}
return results;
}
async function processBatch(batch, index) {
console.log(`Processing batch ${index + 1}...`);
const emailsToSend = [];
for (const email of batch) {
const body = {
MessageStream: messageStream,
From: formatFromForPostMark(email.from),
To: formatForPostMark(email.to),
Cc: email.cc ? formatForPostMark(email.cc) : undefined,
Bcc: email.bcc ? formatForPostMark(email.bcc) : undefined,
Subject: email.subject,
HtmlBody: email.html,
TextBody: email.text,
Tag: email.tag,
ReplyTo: formatFromForPostMark(email.from),
Metadata: email.metaData,
TrackOpens: email.trackOpens,
TrackLinks: email.trackLinks,
Headers: email.headers
};
emailsToSend.push(body);
}
const opts = {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-Postmark-Server-Token': apiKey
},
body: JSON.stringify(emailsToSend)
};
if (logger)
console.log('******** ES-SendMail Postmark ******** send batch #', index);
try {
const response = await fetch('https://api.postmarkapp.com/email/batch', opts);
if (logger)
console.log('******** ES-SendMail Postmark ******** response from fetch', response.status, response.statusText);
const retours = await response.json();
if (logger)
console.log('******** ES-SendMail Postmark ******** json', retours.ErrorCode, retours.Message);
let i = 0;
const returnedValues = [];
for (const retour of retours) {
const returnedValue = await myClass.sendMailResultManagement(retour, response, emails[i]);
returnedValues.push(returnedValue);
i++;
}
return returnedValues;
}
catch (error) {
return [{ success: false, status: 500, error: (0, error_js_1.errorManagement)(error) }];
}
}
if (this.transporter.stream === undefined) {
if (this.transporter.logger)
console.log('******** ES-SendMail Postmark ******** Stream for ', this.transporter.esp, ' is not defined in the configuration');
throw new Error('Stream is not defined in the configuration');
}
const resultats = processInBatches(emails, async (batch, index) => {
return await processBatch(batch, index);
}, 499);
return resultats;
}
async sendMailResultManagement(retour, response, options) {
if (retour.ErrorCode === 0) {
return {
success: true,
status: response.status,
data: {
to: retour.To,
cc: retour.Cc,
bcc: retour.Bcc,
submittedAt: retour.SubmittedAt, //Pour acceepter les dates sous forme de string
messageId: retour.MessageID
}
};
}
const errorResult = postMark_errors_js_1.errorCode[retour.ErrorCode] || { name: 'UNKNOWN', category: 'ACCOUNT_INVALID' };
errorResult.cause = { code: retour.ErrorCode, message: retour.Message };
// Traitement du cas particlier de l'erreur 406
if (retour.ErrorCode === 406) {
const suppressionInfos = await this.getSuppressionInfos(formatForPostMark(options.to));
const errorResult406 = postMark_errors_js_1.supressionListStatus[suppressionInfos?.SuppressionReason] || { name: 'UNKNOWN', category: 'ACCOUNT_INVALID' };
return {
success: false,
status: response.status,
error: errorResult406
};
}
return {
success: false, status: response.status,
error: errorResult
};
}
async webHookManagement(req) {
if (this.transporter.logger) {
console.log('******** ES-WebHook Postmark ******** transporter', this.transporter);
console.log('******** ES-WebHook Postmark ******** req', req);
}
let result = postMark_status_js_1.webHookStatus[req.RecordType];
let dump;
if (req.RecordType === 'Bounce' && req.TypeCode) {
// @ts-ignore
const errorValue = postMark_status_js_1.bouncesTypes[req.TypeCode];
console.log('******** ES-WebHook Postmark ******** errorValue', errorValue);
if (errorValue)
result = errorValue.webHookEventType;
// Aller chercher le dump s'il existe
if (req?.DumpAvailable && req?.ID) {
dump = await this.getBounceDump(req.ID);
}
}
const data = {
webHookType: result,
message: req?.Description || req?.Details || req?.FirstOpen || req?.Plateform || req?.SuppressionReason,
messageId: req.MessageID,
to: req?.Recipient ? req.Recipient : req.Email,
subject: req?.Subject ? req.Subject : undefined,
from: req?.From ? req.From : undefined,
dump: dump
};
// Manage the metaData
if (req.Metadata)
data.metaData = req.Metadata;
if (this.transporter.logger)
console.log('******** ES-WebHook Postmark ******** result', data);
if (result) {
return {
success: true, status: 200, data, espData: {
...req,
espType: req?.Type ? req.Type : 'Default',
espRecordType: req.RecordType,
}
};
}
else
return { success: false, status: 500, error: { name: 'NO_STATUS_FOR_WEBHOOK', message: 'No status aviable for webhook' } };
}
}
exports.PostMarkEmailService = PostMarkEmailService;
/**
* Converts recipients to PostMark format: "John Doe <john@example.com>, Jane Doe <jane@example.com>"
*
* @param recipients - Array of `{ name, email }` objects.
* @returns A string formatted for PostMark.
*/
function formatForPostMark(recipients) {
return recipients.map(r => r.name ? `${r.name} <${r.email}>` : r.email).join(", ");
}
/**
* Converts recipients to PostMark format: "John Doe <john@example.com>, Jane Doe <jane@example.com>"
*
* @param recipients - Array of `{ name, email }` objects.
* @returns A string formatted for PostMark.
*/
function formatFromForPostMark(from) {
return (from.name ? `${from.name} <${from.email}>` : from.email);
}