Skip to content

Commit

Permalink
[#175205379] Added mailing component from io-functions-commons (#82)
Browse files Browse the repository at this point in the history
  • Loading branch information
infantesimone authored Oct 28, 2020
1 parent bf3a7d8 commit 8b034e1
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 350 deletions.
167 changes: 167 additions & 0 deletions EmailNotificationActivity/__tests__/handler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/* tslint:disable:no-any */
/* tslint:disable:no-duplicate-string */
/* tslint:disable:no-big-function */
/* tslint:disable: no-identical-functions */

import {
NonEmptyString,
OrganizationFiscalCode
} from "italia-ts-commons/lib/strings";

import {
EmailNotificationActivityInput,
getEmailNotificationActivityHandler
} from "../handler";

import { some } from "fp-ts/lib/Option";
import { fromLeft, taskEither } from "fp-ts/lib/TaskEither";

import { EmailAddress } from "io-functions-commons/dist/generated/definitions/EmailAddress";
import { MessageBodyMarkdown } from "io-functions-commons/dist/generated/definitions/MessageBodyMarkdown";
import { MessageContent } from "io-functions-commons/dist/generated/definitions/MessageContent";
import { MessageSubject } from "io-functions-commons/dist/generated/definitions/MessageSubject";
import { TimeToLiveSeconds } from "io-functions-commons/dist/generated/definitions/TimeToLiveSeconds";

import { NotificationChannelEnum } from "io-functions-commons/dist/generated/definitions/NotificationChannel";
import * as mail from "io-functions-commons/dist/src/mailer";
import { CreatedMessageEventSenderMetadata } from "io-functions-commons/dist/src/models/created_message_sender_metadata";
import {
NewNotification,
NotificationAddressSourceEnum,
NotificationModel,
RetrievedNotification
} from "io-functions-commons/dist/src/models/notification";

beforeEach(() => jest.clearAllMocks());

const mockContext = {
log: {
// tslint:disable-next-line: no-console
error: console.error,
// tslint:disable-next-line: no-console
info: console.log,
// tslint:disable-next-line: no-console
verbose: console.log,
// tslint:disable-next-line: no-console
warn: console.warn
}
} as any;

const aMessageId = "A_MESSAGE_ID" as NonEmptyString;

const aNewEmailNotification: NewNotification = {
channels: {
[NotificationChannelEnum.EMAIL]: {
addressSource: NotificationAddressSourceEnum.DEFAULT_ADDRESS,
toAddress: "to@example.com" as EmailAddress
}
},
fiscalCode: "FRLFRC74E04B157I" as any,
id: "A_NOTIFICATION_ID" as NonEmptyString,
kind: "INewNotification",
messageId: aMessageId
};

const aRetrievedNotification: RetrievedNotification = {
_etag: "_etag",
_rid: "_rid",
_self: "_self",
_ts: 1,
...aNewEmailNotification,
kind: "IRetrievedNotification"
};

const notificationModelMock = ({
find: jest.fn(() => taskEither.of(some(aRetrievedNotification)))
} as unknown) as NotificationModel;

const aNotificationId = "A_NOTIFICATION_ID" as NonEmptyString;
const anOrganizationFiscalCode = "00000000000" as OrganizationFiscalCode;

const aMessageBodyMarkdown = "test".repeat(80) as MessageBodyMarkdown;

const aMessageContent: MessageContent = {
markdown: aMessageBodyMarkdown,
subject: "test".repeat(10) as MessageSubject
};

const aMessage = {
createdAt: new Date(),
fiscalCode: "FRLFRC74E04B157I" as any,
id: aMessageId,
indexedId: aMessageId,
kind: "INewMessageWithoutContent" as "INewMessageWithoutContent",
senderServiceId: "s123" as NonEmptyString,
senderUserId: "u123" as NonEmptyString,
timeToLiveSeconds: 3600 as TimeToLiveSeconds
};

const aSenderMetadata: CreatedMessageEventSenderMetadata = {
departmentName: "IT" as NonEmptyString,
organizationFiscalCode: anOrganizationFiscalCode,
organizationName: "AgID" as NonEmptyString,
requireSecureChannels: false,
serviceName: "Test" as NonEmptyString,
serviceUserEmail: "email@example.com" as EmailAddress
};

const HTML_TO_TEXT_OPTIONS: HtmlToTextOptions = {
ignoreImage: true, // ignore all document images
tables: true
};

const MAIL_FROM = "IO - l’app dei servizi pubblici <no-reply@io.italia.it>" as NonEmptyString;
const defaultNotificationParams = {
HTML_TO_TEXT_OPTIONS,
MAIL_FROM
};

const input: EmailNotificationActivityInput = {
notificationEvent: {
content: aMessageContent,
message: aMessage,
notificationId: aNotificationId,
senderMetadata: aSenderMetadata
}
};

const lMailerTransporterMock = ({} as unknown) as mail.MailerTransporter;

describe("getEmailNotificationActivityHandler", () => {
it("should respond with 'SUCCESS' if the mail is sent", async () => {
jest.spyOn(mail, "sendMail").mockReturnValueOnce(taskEither.of("SUCCESS"));

const GetEmailNotificationActivityHandler = getEmailNotificationActivityHandler(
lMailerTransporterMock,
notificationModelMock,
defaultNotificationParams
);

const result = await GetEmailNotificationActivityHandler(
mockContext,
input
);

expect(result.kind).toBe("SUCCESS");
});

it("should respond with 'ERROR' if the mail is not sent", async () => {
const errorMessage: string = "Test Error";

jest
.spyOn(mail, "sendMail")
.mockReturnValueOnce(fromLeft(new Error(errorMessage)));

const GetEmailNotificationActivityHandler = getEmailNotificationActivityHandler(
lMailerTransporterMock,
notificationModelMock,
defaultNotificationParams
);

try {
await GetEmailNotificationActivityHandler(mockContext, input);
} catch (e) {
expect(e.message).toBe("Error while sending email: " + errorMessage);
}
});
});
30 changes: 17 additions & 13 deletions EmailNotificationActivity/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import {
} from "io-functions-commons/dist/src/models/notification";
import { NotificationEvent } from "io-functions-commons/dist/src/models/notification_event";

import { generateDocumentHtml, sendMail } from "./utils";
import { generateDocumentHtml } from "./utils";

import { sendMail } from "io-functions-commons/dist/src/mailer";

export interface INotificationDefaults {
readonly HTML_TO_TEXT_OPTIONS: HtmlToTextOptions;
Expand Down Expand Up @@ -52,7 +54,10 @@ export const getEmailNotificationActivityHandler = (
lMailerTransporter: NodeMailer.Transporter,
lNotificationModel: NotificationModel,
notificationDefaultParams: INotificationDefaults
) => async (context: Context, input: unknown): Promise<unknown> => {
) => async (
context: Context,
input: unknown
): Promise<EmailNotificationActivityResult> => {
const inputOrError = EmailNotificationActivityInput.decode(input);

if (inputOrError.isLeft()) {
Expand Down Expand Up @@ -146,7 +151,7 @@ export const getEmailNotificationActivityHandler = (

// trigger email delivery
// see https://nodemailer.com/message/
const sendResult = await sendMail(lMailerTransporter, {
await sendMail(lMailerTransporter, {
from: notificationDefaultParams.MAIL_FROM,
headers: {
"X-Italia-Messages-MessageId": message.id,
Expand All @@ -160,16 +165,15 @@ export const getEmailNotificationActivityHandler = (
// priority: "high", // TODO: set based on kind of notification
// disableFileAccess: true,
// disableUrlAccess: true,
});

if (sendResult.isLeft()) {
const error = sendResult.value;
// track the event of failed delivery
context.log.error(`${logPrefix}|ERROR=${error.message}`);
throw new Error(`Error while sending email: ${error.message}`);
}

context.log.verbose(`${logPrefix}|RESULT=SUCCESS`);
})
.bimap(
error => {
context.log.error(`${logPrefix}|ERROR=${error.message}`);
throw new Error(`Error while sending email: ${error.message}`);
},
() => context.log.verbose(`${logPrefix}|RESULT=SUCCESS`)
)
.run();

// TODO: handling bounces and delivery updates
// see https://nodemailer.com/usage/#sending-mail
Expand Down
61 changes: 3 additions & 58 deletions EmailNotificationActivity/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,42 +11,18 @@
import { AzureFunction } from "@azure/functions";
import { cosmosdbInstance } from "../utils/cosmosdb";

import * as NodeMailer from "nodemailer";

import { agent } from "italia-ts-commons";

import {
NOTIFICATION_COLLECTION_NAME,
NotificationModel
} from "io-functions-commons/dist/src/models/notification";
import { MailUpTransport } from "io-functions-commons/dist/src/utils/mailup";

import { getEmailNotificationActivityHandler } from "./handler";

import {
AbortableFetch,
setFetchTimeout,
toFetch
} from "italia-ts-commons/lib/fetch";
import { NonEmptyString } from "italia-ts-commons/lib/strings";
import { Millisecond } from "italia-ts-commons/lib/units";
import nodemailerSendgrid = require("nodemailer-sendgrid");

import { getMailerTransporter } from "io-functions-commons/dist/src/mailer";
import { getConfigOrThrow } from "../utils/config";

const config = getConfigOrThrow();

//
// setup SendGrid
//
const SendgridTransport = NonEmptyString.decode(config.SENDGRID_API_KEY)
.map(sendgridApiKey =>
nodemailerSendgrid({
apiKey: sendgridApiKey
})
)
.getOrElse(undefined);

const notificationModel = new NotificationModel(
cosmosdbInstance.container(NOTIFICATION_COLLECTION_NAME)
);
Expand All @@ -55,46 +31,15 @@ const notificationModel = new NotificationModel(
// options used when converting an HTML message to pure text
// see https://www.npmjs.com/package/html-to-text#options
//

const HTML_TO_TEXT_OPTIONS: HtmlToTextOptions = {
ignoreImage: true, // ignore all document images
tables: true
};

// default sender for email
const MAIL_FROM = config.MAIL_FROM_DEFAULT;

// 5 seconds timeout by default
const DEFAULT_EMAIL_REQUEST_TIMEOUT_MS = 5000;

// Must be an https endpoint so we use an https agent
const abortableFetch = AbortableFetch(agent.getHttpsFetch(process.env));
const fetchWithTimeout = setFetchTimeout(
DEFAULT_EMAIL_REQUEST_TIMEOUT_MS as Millisecond,
abortableFetch
);

// Whether we're in a production environment
const mailhogHostname: string = config.MAILHOG_HOSTNAME || "localhost";
const MAIL_FROM = config.MAIL_FROM;

const mailerTransporter = NodeMailer.createTransport(
config.isProduction
? SendgridTransport !== undefined
? SendgridTransport
: MailUpTransport({
creds: {
Secret: config.MAILUP_SECRET,
Username: config.MAILUP_USERNAME
},
fetchAgent: toFetch(fetchWithTimeout)
})
: // For development we use mailhog to intercept emails
{
host: mailhogHostname,
port: 1025,
secure: false
}
);
const mailerTransporter = getMailerTransporter(config);

const activityFunction: AzureFunction = getEmailNotificationActivityHandler(
mailerTransporter,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"express": "^4.15.3",
"fp-ts": "1.17.4",
"html-to-text": "^4.0.0",
"io-functions-commons": "^15.0.0",
"io-functions-commons": "^16.0.0",
"io-functions-express": "^0.1.1",
"io-ts": "1.8.5",
"italia-ts-commons": "^8.1.0",
Expand Down
Loading

0 comments on commit 8b034e1

Please sign in to comment.