UNPKG

@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
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(); } }