@mediarithmics/plugins-nodejs-sdk
Version:
This is the mediarithmics nodejs to help plugin developers bootstrapping their plugin without having to deal with most of the plugin boilerplate
442 lines (406 loc) • 12.3 kB
text/typescript
import { core } from "@mediarithmics/plugins-nodejs-sdk";
import { MyInstanceContext } from "./MyInstanceContext";
import * as rp from "request-promise-native";
import * as retry from "retry";
export interface MailjetSentResponse {
Sent: {
Email: string;
MessageID: number;
}[];
}
export interface MailjetPayload {
datamartId: string;
campaignId: string;
creativeId: string;
emailHash: string;
routerId: string;
ts: string;
}
// See https://dev.mailjet.com/guides/#events
export interface MailjetEvent {
event: "open" | "click" | "bounce" | "blocked" | "spam" | "unsub" | "sent";
time: number;
email: string;
mj_campaign_id: number;
mj_contact_id: number;
customcampaign: string;
CustomID: string;
Payload: MailjetPayload;
MessageID: number;
blocked?: boolean;
hard_bounce?: boolean;
error_related_to?: string;
error?: string;
ip?: string;
geo?: string;
agent?: string;
url?: string;
source?: string;
mj_list_id: number;
}
export class MySimpleEmailRouter extends core.EmailRouterPlugin {
/**
* Helpers
*/
buildMailjetPayload(
datamartId: string,
campaignId: string,
creativeId: string,
emailHash: string,
routerId: string
): MailjetPayload {
return {
datamartId: datamartId,
campaignId: campaignId,
creativeId: creativeId,
emailHash: emailHash,
routerId: routerId,
ts: new Date().toString()
};
}
/**
* Mailjet Send Email
*/
async sendEmail(
request: core.EmailRoutingRequest,
identifier: core.UserEmailIdentifierInfo,
payload: MailjetPayload
): Promise<MailjetSentResponse> {
this.logger.debug(
`Sending email to: request: ${JSON.stringify(
request
)} - identifier: ${JSON.stringify(
identifier
)} - payload: ${JSON.stringify(payload)}`
);
const emailHeaders = {
"Reply-To": request.meta.reply_to
};
const emailData = {
FromEmail: request.meta.from_email,
FromName: request.meta.from_name,
Headers: emailHeaders,
Subject: request.meta.subject_line,
"Text-part": request.content.text,
"Html-part": request.content.html,
Recipients: [
{
Email: identifier.email || request.meta.to_email
}
],
"Mj-EventPayLoad": JSON.stringify(payload),
"Mj-campaign": request.campaign_id
};
const mailjetResponse: MailjetSentResponse = await this.requestGatewayHelper(
"POST",
`${
this.outboundPlatformUrl
}/v1/external_services/technical_name=mailjet/call`,
emailData
);
if (mailjetResponse.Sent.length === 0) {
this.logger.error("Mailjet sent an empty response, will retry");
throw new Error("Mailjet sent an empty response, will retry");
}
return mailjetResponse;
}
createEmailTrackingActivity(
event: MailjetEvent,
eventName: string,
payload: MailjetPayload
): core.UserActivity {
const now = Date.now();
return ({
$type: "EMAIL",
$source: "API",
$ts: now,
$email_hash: {
$hash: payload.emailHash
},
$datamart_id: payload.datamartId,
$events: [
{
$ts: now,
$event_name: eventName,
$properties: {
$delivery_id: "" + event.MessageID
}
}
],
$origin: {
$ts: now,
$campaign_id: payload.campaignId,
$creative_id: payload.creativeId
}
} as any) as core.UserActivity;
}
processMailjetEventToMicsTrackingActivity(
mailjetEvent: MailjetEvent
): core.UserActivity {
switch (mailjetEvent.event) {
case "open":
return this.createEmailTrackingActivity(
mailjetEvent,
"$email_view",
mailjetEvent.Payload
);
case "click":
return this.createEmailTrackingActivity(
mailjetEvent,
"$email_click",
mailjetEvent.Payload
);
case "bounce":
if (
mailjetEvent.blocked === true ||
mailjetEvent.hard_bounce === true
) {
return this.createEmailTrackingActivity(
mailjetEvent,
"$email_hard_bounce",
mailjetEvent.Payload
);
} else {
return this.createEmailTrackingActivity(
mailjetEvent,
"$email_soft_bounce",
mailjetEvent.Payload
);
}
case "blocked":
return this.createEmailTrackingActivity(
mailjetEvent,
"$email_hard_bounce",
mailjetEvent.Payload
);
case "spam":
return this.createEmailTrackingActivity(
mailjetEvent,
"$email_complaint",
mailjetEvent.Payload
);
case "unsub":
return this.createEmailTrackingActivity(
mailjetEvent,
"$email_unsubscribe",
mailjetEvent.Payload
);
default:
throw new Error(
`POST /v1/email_events: Received ${
mailjetEvent.event
} - We're not handling this event YET. With Payload ${JSON.stringify(
mailjetEvent.Payload
)}`
);
}
}
sendUserActivityEvent(
datamartId: string,
emailUserActivity: core.UserActivity,
authenticationToken: string
) {
const uri =
(process.env.OUTBOUND_PLATFORM_URL || "https://api.mediarithmics.com") +
"/v1/datamarts/" +
datamartId +
"/user_activities";
const options = {
method: "POST",
uri: uri,
body: emailUserActivity,
json: true,
headers: {
Authorization: authenticationToken
}
};
this.logger.debug(
`Sending email user activity to the timeline: ${JSON.stringify(
emailUserActivity
)}`
);
return rp(options).catch(function(e) {
if (e.name === "StatusCodeError") {
throw new Error(
`Error while calling ${options.method} '${
options.uri
}' with the request body '${JSON.stringify(options.body) ||
""}': got a ${e.response.statusCode} ${
e.response.statusMessage
} with the response body ${JSON.stringify(e.response.body)}`
);
} else {
throw e;
}
});
}
initMailjetNotificationRoute() {
// Return an emailRoutingResponse
// Mailjet notify entry point for events such as sent, open, click, bounce, spam, blocked etc...
this.app.post(
"/r/external_token_to_be_provided_by_your_account_manager", // This will be listening on : https://plugins.mediarithmics.io/r/external_token_to_be_provided_by_your_account_manager
async (req, res) => {
const emailEvent = req.body;
this.logger.debug(
"POST /r/external_token_to_be_provided_by_your_account_manager",
JSON.stringify(emailEvent)
);
try {
if (!emailEvent) {
this.logger.error(
"POST /r/external_token_to_be_provided_by_your_account_manager: Missing email event"
);
return res.status(400).json({
Result: "Missing email event"
});
}
if (!emailEvent.Payload || !emailEvent.MessageID) {
this.logger.error(
"POST /r/external_token_to_be_provided_by_your_account_manager: Missing Payload or MessageID"
);
return res.status(400).json({
Result: "Missing Payload or MessageID"
});
}
if (
JSON.stringify(emailEvent.Payload) === "" &&
emailEvent.MessageID === 0
) {
// If test sent with mailjet
return res.status(200).json({
Result: "Considered as test. Success."
});
} else {
const mailjetEvent = JSON.parse(emailEvent) as MailjetEvent;
const payload = mailjetEvent.Payload;
const props = (await this.getInstanceContext(
payload.routerId
)) as MyInstanceContext;
const micsActivity = this.processMailjetEventToMicsTrackingActivity(
mailjetEvent
);
try {
await this.sendUserActivityEvent(
payload.datamartId,
micsActivity,
props.authenticationToken
);
return res.status(200).json({
Result: `Event ${emailEvent.event} successfully sent`
});
} catch (err) {
this.logger.error(
`POST /r/external_token_to_be_provided_by_your_account_manager: Failed to integrate event ${
emailEvent.event
} Error: ${err.message} - ${err.stack}`
);
return res.status(400).json({
Result: `Failed to integrate event ${emailEvent.event} Error: ${
err.message
} - ${err.stack}`
});
}
}
} catch (err) {
this.logger.error(
`POST /r/external_token_to_be_provided_by_your_account_manager: Failed because of Error: ${
err.message
} - ${err.stack}`
);
return res.status(500).json({
Result: `${err.message} - ${err.stack}`
});
}
}
);
}
protected async instanceContextBuilder(
routerId: string
): Promise<MyInstanceContext> {
const defaultInstanceContext = await super.instanceContextBuilder(routerId);
const authenticationToken = defaultInstanceContext.routerProperties.find(
prop => {
return prop.technical_name === "authentication_token";
}
);
if (authenticationToken && authenticationToken.value.value) {
return {
...defaultInstanceContext,
authenticationToken: authenticationToken.value.value as string
};
} else {
this.logger.error(
`There is no authentification_token configured for routerId: ${
routerId
}`
);
throw Error(
`There is no authentification_token configured for routerId: ${
routerId
}`
);
}
}
protected async onEmailCheck(
request: core.CheckEmailsRequest,
instanceContext: core.EmailRouterBaseInstanceContext
): Promise<core.CheckEmailsPluginResponse> {
// Mailjet can send emails to every email adress.
// Trust me, I've seen it.
return Promise.resolve({ result: true });
}
protected async onEmailRouting(
request: core.EmailRoutingRequest,
instanceContext: core.EmailRouterBaseInstanceContext
): Promise<core.EmailRoutingPluginResponse> {
const identifier = request.user_identifiers.find(identifier => {
return (
identifier.type === "USER_EMAIL" &&
!!(identifier as core.UserEmailIdentifierInfo).email
);
}) as core.UserEmailIdentifierInfo;
if (!identifier) {
throw Error("No clear email identifiers were provided");
}
const payload = this.buildMailjetPayload(
request.datamart_id,
request.datamart_id,
request.creative_id,
identifier.hash,
request.email_router_id
);
// As Mailjet can be a little difficult when shotting an email, we'll try a lot of time
const mailjetResponse: MailjetSentResponse = await this.retryPromiseHelper(
this.sendEmail,
[request, identifier, payload]
);
if (mailjetResponse.Sent.length > 0) {
return { result: true };
} else {
return { result: false };
}
}
retryPromiseHelper(mainFn: Function, args: any[]): Promise<any> {
return new Promise((resolve, reject) => {
const operation = retry.operation();
operation.attempt(async attempt => {
try {
this.logger.debug(
`Trying to call for the ${attempt}th time ${mainFn.name}`
);
const response = await mainFn.apply(this, args);
resolve(response);
} catch (err) {
if (operation.retry(err)) {
return;
}
reject(operation.mainError());
}
});
});
}
constructor() {
super();
this.initMailjetNotificationRoute();
}
}