-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[#172380989] Add html body to DPO's UserDataProcessing email #65
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,20 +1,23 @@ | ||
import * as t from "io-ts"; | ||
|
||
import { isLeft, isRight } from "fp-ts/lib/Either"; | ||
|
||
import * as NodeMailer from "nodemailer"; | ||
|
||
import { Context } from "@azure/functions"; | ||
|
||
import { readableReport } from "italia-ts-commons/lib/reporters"; | ||
|
||
import { sendMail } from "io-functions-commons/dist/src/utils/email"; | ||
|
||
import { isSome } from "fp-ts/lib/Option"; | ||
import { left, right } from "fp-ts/lib/Either"; | ||
import { fromEither } from "fp-ts/lib/TaskEither"; | ||
import { FiscalCode } from "io-functions-commons/dist/generated/definitions/FiscalCode"; | ||
import { UserDataProcessingChoice } from "io-functions-commons/dist/generated/definitions/UserDataProcessingChoice"; | ||
import { ProfileModel } from "io-functions-commons/dist/src/models/profile"; | ||
import { EmailDefaults } from "."; | ||
import { | ||
UserDataProcessingChoice, | ||
UserDataProcessingChoiceEnum | ||
} from "io-functions-commons/dist/generated/definitions/UserDataProcessingChoice"; | ||
import { EmailString } from "italia-ts-commons/lib/strings"; | ||
import Mail = require("nodemailer/lib/mailer"); | ||
import { | ||
EmailDefaults, | ||
findOneProfileByFiscalCodeTaskT, | ||
sendMailTaskT | ||
} from "."; | ||
|
||
// Activity input | ||
export const ActivityInput = t.interface({ | ||
|
@@ -29,60 +32,33 @@ const ActivityResultSuccess = t.interface({ | |
kind: t.literal("SUCCESS") | ||
}); | ||
|
||
type ActivityResultSuccess = t.TypeOf<typeof ActivityResultSuccess>; | ||
|
||
const ActivityResultFailure = t.interface({ | ||
kind: t.literal("FAILURE"), | ||
reason: t.string | ||
}); | ||
|
||
type ActivityResultFailure = t.TypeOf<typeof ActivityResultFailure>; | ||
|
||
export const ActivityResult = t.taggedUnion("kind", [ | ||
ActivityResultSuccess, | ||
ActivityResultFailure | ||
]); | ||
|
||
export type ActivityResult = t.TypeOf<typeof ActivityResult>; | ||
|
||
export const getSendUserDataProcessingEmailActivityHandler = ( | ||
mailerTransporter: NodeMailer.Transporter, | ||
emailDefaults: EmailDefaults, | ||
profileModel: ProfileModel | ||
) => async (context: Context, input: unknown) => { | ||
const logPrefix = "SendUserDataProcessingEmail"; | ||
|
||
const errorOrActivityInput = ActivityInput.decode(input); | ||
|
||
if (isLeft(errorOrActivityInput)) { | ||
context.log.error( | ||
`${logPrefix}|Error decoding SendUserDataProcessingActivity input` | ||
); | ||
context.log.verbose( | ||
`${logPrefix}|Error decoding input|ERROR=${readableReport( | ||
errorOrActivityInput.value | ||
)}` | ||
); | ||
return ActivityResultFailure.encode({ | ||
kind: "FAILURE", | ||
reason: "Error decoding input" | ||
}); | ||
} | ||
|
||
const activityInput = errorOrActivityInput.value; | ||
const { choice, fiscalCode } = activityInput; | ||
|
||
const errorOrMaybeRetrievedProfile = await profileModel.findOneProfileByFiscalCode( | ||
fiscalCode | ||
); | ||
if (isRight(errorOrMaybeRetrievedProfile)) { | ||
const maybeRetrievedProfile = errorOrMaybeRetrievedProfile.value; | ||
if (isSome(maybeRetrievedProfile)) { | ||
const { from, to } = emailDefaults; | ||
const subject = `IO - Richiesta di tipo ${choice} da parte di ${fiscalCode}`; | ||
const userEmailAddress = maybeRetrievedProfile.value.email; | ||
// prepare the text version of the message | ||
const emailText = `Un utente di IO ha inoltrato una nuova richiesta: | ||
tipo richiesta: ${choice} | ||
codice fiscale: ${fiscalCode} | ||
indirizzo email: ${userEmailAddress}.`; | ||
const documentHtml = ` | ||
export const getDpoEmailText = ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. useful in tests to avoid to type mock text |
||
choice: UserDataProcessingChoiceEnum, | ||
fiscalCode: FiscalCode, | ||
userEmailAddress: EmailString | ||
) => | ||
`Un utente di IO ha inoltrato una nuova richiesta: | ||
tipo richiesta: ${choice} | ||
codice fiscale: ${fiscalCode} | ||
indirizzo email: ${userEmailAddress}.`; | ||
|
||
export const getDpoEmailHtml = (subject: string, emailText: string) => ` | ||
<!doctype html> | ||
<html> | ||
<head> | ||
|
@@ -91,40 +67,75 @@ export const getSendUserDataProcessingEmailActivityHandler = ( | |
<title>${subject}</title> | ||
</head> | ||
<body> | ||
<p>${emailText}</p> | ||
<p>${emailText.replace("\n", "<br>\n")}</p> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. handle newlines in html |
||
</body> | ||
</html>`; | ||
// Send an email to the DPO containing the information about the IO user's | ||
// choice to download or delete his own private data stored by the platform | ||
const errorOrSentMessageInfo = await sendMail(mailerTransporter, { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. prefer working with taskeithers than promise |
||
from, | ||
html: documentHtml, | ||
|
||
export const getDpoEmailSubject = ( | ||
choice: UserDataProcessingChoiceEnum, | ||
fiscalCode: FiscalCode | ||
) => `IO - Richiesta di tipo ${choice} da parte di ${fiscalCode}`; | ||
|
||
const failActivity = (context: Context, logPrefix: string) => ( | ||
errorMessage: string, | ||
errorDetails?: string | ||
) => { | ||
const details = errorDetails ? `ERROR_DETAILS=${errorDetails}` : ``; | ||
context.log.error(`${logPrefix}|${errorMessage}|${details}`); | ||
return ActivityResultFailure.encode({ | ||
kind: "FAILURE", | ||
reason: errorMessage | ||
}); | ||
}; | ||
|
||
const success = () => | ||
ActivityResultSuccess.encode({ | ||
kind: "SUCCESS" | ||
}); | ||
|
||
/** | ||
* For each user data procesing request send an email to the DPO | ||
* containing the information about the user's choice | ||
* to download or delete his own private data stored by the platform | ||
*/ | ||
export const getSendUserDataProcessingEmailActivityHandler = ( | ||
emailDefaults: EmailDefaults, | ||
sendMail: ReturnType<sendMailTaskT>, | ||
findOneProfileByFiscalCode: ReturnType<findOneProfileByFiscalCodeTaskT>, | ||
logPrefix = "SendUserDataProcessingEmail" | ||
) => async (context: Context, input: unknown) => { | ||
const failure = failActivity(context, logPrefix); | ||
return fromEither(ActivityInput.decode(input)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. when possible avoid assignement to |
||
.mapLeft(errs => | ||
failure("Error decoding activity input", readableReport(errs)) | ||
) | ||
.chain(({ choice, fiscalCode }) => | ||
findOneProfileByFiscalCode(fiscalCode).foldTaskEither( | ||
err => | ||
fromEither( | ||
left(failure("Error retrieving user's profile", err.message)) | ||
), | ||
maybeRetrievedProfile => | ||
maybeRetrievedProfile.fold( | ||
fromEither(left(failure("No user's profile found"))), | ||
profile => fromEither(right({ choice, fiscalCode, profile })) | ||
) | ||
) | ||
) | ||
.chain(({ choice, fiscalCode, profile }) => { | ||
const subject = getDpoEmailSubject(choice, fiscalCode); | ||
const emailText = getDpoEmailText(choice, fiscalCode, profile.email); | ||
const emailHtml = getDpoEmailHtml(subject, emailText); | ||
return sendMail({ | ||
from: emailDefaults.from, | ||
html: emailHtml, | ||
subject, | ||
text: emailText, | ||
to | ||
}); | ||
|
||
if (isLeft(errorOrSentMessageInfo)) { | ||
context.log.error( | ||
`${logPrefix}|Error sending userDataProcessing email|ERROR=${errorOrSentMessageInfo.value.message}` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there's not need to repeat userDataProcessing since w have the log prefix |
||
); | ||
return ActivityResultFailure.encode({ | ||
kind: "FAILURE", | ||
reason: "Error while sending mail" | ||
}); | ||
} | ||
|
||
return ActivityResultSuccess.encode({ | ||
kind: "SUCCESS" | ||
}); | ||
} | ||
} else { | ||
context.log.error( | ||
`${logPrefix}|Error retrieving user's profile|ERROR=${errorOrMaybeRetrievedProfile.value}` | ||
); | ||
return ActivityResultFailure.encode({ | ||
kind: "FAILURE", | ||
reason: "Error while retrieving user's profile" | ||
}); | ||
} | ||
to: emailDefaults.to | ||
}).foldTaskEither<ActivityResultFailure, ActivityResultSuccess>( | ||
err => fromEither(left(failure("Error sending email", err.message))), | ||
_ => fromEither(right(success())) | ||
); | ||
}) | ||
.run(); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,20 @@ | ||
import * as NodeMailer from "nodemailer"; | ||
|
||
import { getRequiredStringEnv } from "io-functions-commons/dist/src/utils/env"; | ||
import Mail = require("nodemailer/lib/mailer"); | ||
|
||
import { DocumentClient as DocumentDBClient } from "documentdb"; | ||
import { toError } from "fp-ts/lib/Either"; | ||
import { Option } from "fp-ts/lib/Option"; | ||
import { Task } from "fp-ts/lib/Task"; | ||
import * as TE from "fp-ts/lib/TaskEither"; | ||
import { | ||
PROFILE_COLLECTION_NAME, | ||
ProfileModel | ||
ProfileModel, | ||
RetrievedProfile | ||
} from "io-functions-commons/dist/src/models/profile"; | ||
import * as documentDbUtils from "io-functions-commons/dist/src/utils/documentdb"; | ||
import { getRequiredStringEnv } from "io-functions-commons/dist/src/utils/env"; | ||
import { MailUpTransport } from "io-functions-commons/dist/src/utils/mailup"; | ||
import { FiscalCode } from "italia-ts-commons/lib/strings"; | ||
import { getSendUserDataProcessingEmailActivityHandler } from "./handler"; | ||
|
||
const cosmosDbUri = getRequiredStringEnv("COSMOSDB_URI"); | ||
|
@@ -28,6 +34,23 @@ const documentClient = new DocumentDBClient(cosmosDbUri, { | |
|
||
const profileModel = new ProfileModel(documentClient, profilesCollectionUrl); | ||
|
||
const findOneProfileByFiscalCodeTask = (pm: ProfileModel) => ( | ||
fiscalCode: FiscalCode | ||
) => | ||
TE.tryCatch( | ||
() => pm.findOneProfileByFiscalCode(fiscalCode), | ||
toError | ||
).foldTaskEither<Error, Option<RetrievedProfile>>( | ||
err => TE.left(new Task(async () => err)), | ||
queryErrorOrMaybeProfile => | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. all this mess is just to convert |
||
queryErrorOrMaybeProfile.fold( | ||
queryError => TE.left(new Task(async () => new Error(queryError.body))), | ||
maybeProfile => TE.right(new Task(async () => maybeProfile)) | ||
) | ||
); | ||
|
||
export type findOneProfileByFiscalCodeTaskT = typeof findOneProfileByFiscalCodeTask; | ||
|
||
// Whether we're in a production environment | ||
const isProduction = process.env.NODE_ENV === "production"; | ||
|
||
|
@@ -63,10 +86,16 @@ const mailerTransporter = isProduction | |
secure: false | ||
}); | ||
|
||
const sendMailTask = (mt: Mail) => ( | ||
options: Mail.Options & { html: Mail.Options["html"] } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. while the proposed changes fix the error, they don't prevent the error (missing html param) to occur again in the future so I've added a required html parameter here |
||
) => TE.tryCatch(() => mt.sendMail(options), toError); | ||
|
||
export type sendMailTaskT = typeof sendMailTask; | ||
|
||
const activityFunctionHandler = getSendUserDataProcessingEmailActivityHandler( | ||
mailerTransporter, | ||
emailDefaults, | ||
profileModel | ||
sendMailTask(mailerTransporter), | ||
findOneProfileByFiscalCodeTask(profileModel) | ||
); | ||
|
||
export default activityFunctionHandler; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isNone branch was missing