diff --git a/packages/central-server/src/apiV2/deleteAccount.js b/packages/central-server/src/apiV2/deleteAccount.js index 6b6b14cb1f..beec13a516 100644 --- a/packages/central-server/src/apiV2/deleteAccount.js +++ b/packages/central-server/src/apiV2/deleteAccount.js @@ -4,23 +4,24 @@ */ import { requireEnv, respond } from '@tupaia/utils'; import { sendEmail } from '@tupaia/server-utils'; -import { getUserInfoInString } from './utilities'; -const sendRequest = userInfo => { +const sendRequest = user => { const TUPAIA_ADMIN_EMAIL_ADDRESS = requireEnv('TUPAIA_ADMIN_EMAIL_ADDRESS'); - const emailText = `${userInfo} has requested to delete their account`; return sendEmail(TUPAIA_ADMIN_EMAIL_ADDRESS, { subject: 'Tupaia Account Deletion Request', - text: emailText, + templateName: 'deleteAccount', + templateContext: { + user, + }, }); }; export const deleteAccount = async (req, res) => { const { userId: requestUserId, params, models } = req; const userId = requestUserId || params.userId; - const userInfo = await getUserInfoInString(userId, models); - await sendRequest(userInfo); + const user = await models.user.findById(userId); + await sendRequest(user); respond(res, { message: 'Account deletion requested.' }, 200); }; diff --git a/packages/central-server/src/apiV2/import/importSurveyResponses/constructImportEmail.js b/packages/central-server/src/apiV2/import/importSurveyResponses/constructImportEmail.js index 1ce4d17d19..65f5dd796f 100644 --- a/packages/central-server/src/apiV2/import/importSurveyResponses/constructImportEmail.js +++ b/packages/central-server/src/apiV2/import/importSurveyResponses/constructImportEmail.js @@ -13,24 +13,37 @@ Any responses not listed here have been successfully imported, and can be remove return message; }; -const constructMessage = responseBody => { +const constructTemplateContextMessage = responseBody => { const { error, failures = [] } = responseBody; // global error, whole import has failed if (error) { - return `Unfortunately, your survey response import failed. + return { + message: `Unfortunately, your survey response import failed. -${error}`; +${error}`, + + title: 'Import Failed', + }; } // at least one response failed, but import finished processing if (failures.length > 0) { - return constructFailuresMessage(failures); + return { + message: constructFailuresMessage(failures), + title: 'Import Finished with Failures', + }; } - return 'Your survey responses have been successfully imported.'; + return { + message: 'Your survey responses have been successfully imported.', + title: 'Import Successful', + }; }; export const constructImportEmail = responseBody => { - return { subject: 'Tupaia Survey Response Import', message: constructMessage(responseBody) }; + return { + subject: 'Tupaia Survey Response Import', + templateContext: constructTemplateContextMessage(responseBody), + }; }; diff --git a/packages/central-server/src/apiV2/requestCountryAccess.js b/packages/central-server/src/apiV2/requestCountryAccess.js index d3bf7c158e..35fe0f88d5 100644 --- a/packages/central-server/src/apiV2/requestCountryAccess.js +++ b/packages/central-server/src/apiV2/requestCountryAccess.js @@ -6,7 +6,6 @@ import { requireEnv, respond, UnauthenticatedError, ValidationError } from '@tupaia/utils'; import { sendEmail } from '@tupaia/server-utils'; import { getTokenClaimsFromBearerAuth } from '@tupaia/auth'; -import { getUserInfoInString } from './utilities'; const checkUserPermission = (req, userId) => { const authHeader = req.headers.authorization; @@ -17,26 +16,28 @@ const checkUserPermission = (req, userId) => { } }; -const sendRequest = (userInfo, countryNames, message, project) => { +const sendRequest = async (userId, models, countries, message, project) => { + const user = await models.user.findById(userId); + const TUPAIA_ADMIN_EMAIL_ADDRESS = requireEnv('TUPAIA_ADMIN_EMAIL_ADDRESS'); - const emailText = ` -${userInfo} has requested access to countries: -${countryNames.map(n => ` - ${n}`).join('\n')} -${ - project - ? ` -For the project ${project.code} (linked to permission groups: ${project.permission_groups.join( - ', ', - )}) - ` - : '' -} -With the message: '${message}' -`; return sendEmail(TUPAIA_ADMIN_EMAIL_ADDRESS, { subject: 'Tupaia Country Access Request', - text: emailText, + templateName: 'requestCountryAccess', + templateContext: { + title: 'You have a new country request!', + cta: { + url: `${process.env.ADMIN_PANEL_URL}/users/access-requests/${userId}`, + text: 'Approve or deny request', + }, + countries, + message, + project: { + code: project.code, + permissionGroups: project.permission_groups.join(', '), + }, + user, + }, }); }; @@ -79,13 +80,12 @@ export const requestCountryAccess = async (req, res) => { } catch (error) { throw new UnauthenticatedError(error.message); } - const userInfo = await getUserInfoInString(userId, models); const project = projectCode && (await models.project.findOne({ code: projectCode })); await createAccessRequests(models, userId, entities, message, project); const countryNames = entities.map(e => e.name); - await sendRequest(userInfo, countryNames, message, project); + await sendRequest(userId, models, countryNames, message, project); respond(res, { message: 'Country access requested' }, 200); }; diff --git a/packages/central-server/src/apiV2/requestPasswordReset.js b/packages/central-server/src/apiV2/requestPasswordReset.js index 2366ef1399..52c805e2e9 100644 --- a/packages/central-server/src/apiV2/requestPasswordReset.js +++ b/packages/central-server/src/apiV2/requestPasswordReset.js @@ -33,17 +33,18 @@ export const requestPasswordReset = async (req, res) => { resetPasswordUrl || process.env.TUPAIA_FRONT_END_URL }/reset-password?passwordResetToken={token}`; const resetUrl = passwordResetUrl.replace('{token}', token); - const emailText = `Dear ${user.fullName}, -You are receiving this email because someone requested a password reset for -this user account on Tupaia.org. To reset your password follow the link below. - -${resetUrl} - -If you believe this email was sent to you in error, please contact us immediately at -admin@tupaia.org.`; - - sendEmail(user.email, { subject: 'Password reset on Tupaia.org', text: emailText }); + sendEmail(user.email, { + subject: 'Password reset on Tupaia.org', + templateName: 'passwordReset', + templateContext: { + userName: user.fullName, + cta: { + text: 'Reset your password', + url: resetUrl, + }, + }, + }); respond(res, { success: true, diff --git a/packages/central-server/src/apiV2/utilities/emailVerification.js b/packages/central-server/src/apiV2/utilities/emailVerification.js index 96080eba74..3f499712a7 100644 --- a/packages/central-server/src/apiV2/utilities/emailVerification.js +++ b/packages/central-server/src/apiV2/utilities/emailVerification.js @@ -10,35 +10,23 @@ import { requireEnv } from '@tupaia/utils'; const EMAILS = { tupaia: { subject: 'Tupaia email verification', - body: (token, url) => - 'Thank you for registering with tupaia.org.\n' + - 'Please click on the following link to register your email address.\n\n' + - `${url}/verify-email?verifyEmailToken=${token}\n\n` + - 'If you believe this email was sent to you in error, please contact us immediately at admin@tupaia.org.\n', + platformName: 'tupaia.org', }, datatrak: { subject: 'Tupaia Datatrak email verification', - body: (token, url) => - 'Thank you for registering with datatrak.tupaia.org.\n' + - 'Please click on the following link to register your email address.\n\n' + - `${url}/verify-email?verifyEmailToken=${token}\n\n` + - 'If you believe this email was sent to you in error, please contact us immediately at admin@tupaia.org.\n', + platformName: 'datatrak.tupaia.org', }, lesmis: { subject: 'LESMIS email verification', - body: (token, url) => - 'Thank you for registering with lesmis.la.\n' + - 'Please click on the following link to register your email address.\n\n' + - `${url}/en/verify-email?verifyEmailToken=${token}\n\n` + - 'If you believe this email was sent to you in error, please contact us immediately at admin@tupaia.org.\n', signOff: 'Best regards,\nThe LESMIS Team', + platformName: 'lesmis.la', }, }; export const sendEmailVerification = async user => { const token = encryptPassword(user.email + user.password_hash, user.password_salt); const platform = user.primary_platform ? user.primary_platform : 'tupaia'; - const { subject, body, signOff } = EMAILS[platform]; + const { subject, signOff, platformName } = EMAILS[platform]; const TUPAIA_FRONT_END_URL = requireEnv('TUPAIA_FRONT_END_URL'); const LESMIS_FRONT_END_URL = requireEnv('LESMIS_FRONT_END_URL'); const DATATRAK_FRONT_END_URL = requireEnv('DATATRAK_FRONT_END_URL'); @@ -46,10 +34,24 @@ export const sendEmailVerification = async user => { const url = { tupaia: TUPAIA_FRONT_END_URL, datatrak: DATATRAK_FRONT_END_URL, - lesmis: LESMIS_FRONT_END_URL, + lesmis: `${LESMIS_FRONT_END_URL}/en`, }[platform]; - return sendEmail(user.email, { subject, text: body(token, url), signOff }); + const fullUrl = `${url}/verify-email?verifyEmailToken=${token}`; + + return sendEmail(user.email, { + subject, + signOff, + templateName: 'verifyEmail', + templateContext: { + title: 'Verify your email address', + platform: platformName, + cta: { + text: 'Verify email', + url: fullUrl, + }, + }, + }); }; export const verifyEmailHelper = async (models, searchCondition, token) => { diff --git a/packages/central-server/src/database/models/UserEntityPermission.js b/packages/central-server/src/database/models/UserEntityPermission.js index 29f7fa60c3..541f3ff3b2 100644 --- a/packages/central-server/src/database/models/UserEntityPermission.js +++ b/packages/central-server/src/database/models/UserEntityPermission.js @@ -13,21 +13,13 @@ export class UserEntityPermissionModel extends CommonUserEntityPermissionModel { const EMAILS = { tupaia: { subject: 'Tupaia Permission Granted', - body: (userName, permissionGroupName, entityName) => - `Hi ${userName},\n\n` + - `This is just to let you know that you've been added to the ${permissionGroupName} access group for ${entityName}. ` + - 'This allows you to collect surveys through the Tupaia data collection app, and to see reports and map overlays on Tupaia.org.\n\n' + - "Please note that you'll need to log out and then log back in to get access to the new permissions.\n\n" + - 'Have fun exploring Tupaia, and feel free to get in touch if you have any questions.\n', + description: + 'This allows you to collect surveys through the Tupaia data collection app, and to see reports and map overlays on Tupaia.org.', }, lesmis: { subject: 'LESMIS Permission Granted', - body: (userName, permissionGroupName, entityName) => - `Hi ${userName},\n\n` + - `This is just to let you know that you've been added to the ${permissionGroupName} access group for ${entityName}. ` + - 'This allows you to see reports and map overlays on lesmis.la.\n\n' + - "Please note that you'll need to log out and then log back in to get access to the new permissions.\n\n" + - 'Feel free to get in touch if you have any questions.\n', + description: + 'This allows you to see reports and map overlays on lesmis.la.', signOff: 'Best regards,\nThe LESMIS Team', }, }; @@ -51,12 +43,19 @@ async function onUpsertSendPermissionGrantEmail( const permissionGroup = await models.permissionGroup.findById(newRecord.permission_group_id); const platform = user.primary_platform ? user.primary_platform : 'tupaia'; - const { subject, body, signOff } = EMAILS[platform]; + const { subject, description, signOff } = EMAILS[platform]; sendEmail(user.email, { subject, - text: body(user.first_name, permissionGroup.name, entity.name), signOff, + templateName: 'permissionGranted', + templateContext: { + title: 'Permission Granted', + description, + userName: user.first_name, + entityName: entity.name, + permissionGroupName: permissionGroup.name, + }, }); } diff --git a/packages/server-boilerplate/src/utils/emailAfterTimeout.ts b/packages/server-boilerplate/src/utils/emailAfterTimeout.ts index ec2eda5fed..b37f1f3e68 100644 --- a/packages/server-boilerplate/src/utils/emailAfterTimeout.ts +++ b/packages/server-boilerplate/src/utils/emailAfterTimeout.ts @@ -7,26 +7,39 @@ import { sendEmail } from '@tupaia/server-utils'; import { UserAccount } from '@tupaia/types'; import { respond } from '@tupaia/utils'; +type TemplateContext = { + title: string; + message: string; + cta?: { + text: string; + url: string; + }; +}; + type ConstructEmailFromResponseT = ( responseBody: any, req: any, ) => Promise<{ subject: string; - message: string; attachments?: { filename: string; content: Buffer }[]; + templateContext: TemplateContext; }>; const sendResponseAsEmail = ( user: UserAccount, subject: string, - message: string, + templateContext: TemplateContext, attachments?: { filename: string; content: Buffer }[], ) => { - const text = `Hi ${user.first_name}, - -${message} - `; - sendEmail(user.email, { subject, text, attachments }); + sendEmail(user.email, { + subject, + attachments, + templateName: 'emailAfterTimeout', + templateContext: { + ...templateContext, + userName: user.first_name, + }, + }); }; const setupEmailResponse = async ( @@ -54,8 +67,11 @@ const setupEmailResponse = async ( // override the respond function so that when the endpoint handler finishes (or throws an error), // the response is sent via email res.overrideRespond = async (responseBody: any) => { - const { subject, message, attachments } = await constructEmailFromResponse(responseBody, req); - sendResponseAsEmail(user, subject, message, attachments); + const { subject, attachments, templateContext } = await constructEmailFromResponse( + responseBody, + req, + ); + sendResponseAsEmail(user, subject, templateContext, attachments); }; }; diff --git a/packages/server-utils/package.json b/packages/server-utils/package.json index cd2cce4483..37949b625f 100644 --- a/packages/server-utils/package.json +++ b/packages/server-utils/package.json @@ -12,7 +12,8 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { - "build": "rm -rf dist && npm run --prefix ../../ package:build:ts", + "copy-templates": "copyfiles -u 1 src/email/templates/**/**/* ./dist", + "build": "rm -rf dist && npm run --prefix ../../ package:build:ts && npm run copy-templates", "build-dev": "npm run build", "lint": "yarn package:lint", "lint:fix": "yarn lint --fix", @@ -25,13 +26,15 @@ "@aws-sdk/lib-storage": "^3.348.0", "@tupaia/utils": "workspace:*", "cookie": "^0.5.0", + "copyfiles": "^2.4.1", "dotenv": "^16.4.5", + "handlebars": "^4.7.8", "nodemailer": "^6.9.12", "puppeteer": "^15.4.0", "sha256": "^0.2.0" }, "devDependencies": { - "@types/nodemailer": "^6.4.13", + "@types/nodemailer": "^6.4.15", "@types/sha256": "^0.2.2" } } diff --git a/packages/server-utils/src/constructExportEmail.ts b/packages/server-utils/src/constructExportEmail.ts index 2733c81510..0b76182e36 100644 --- a/packages/server-utils/src/constructExportEmail.ts +++ b/packages/server-utils/src/constructExportEmail.ts @@ -39,8 +39,11 @@ export const constructExportEmail = async (responseBody: ResponseBody, req: Req) if (error) { return { subject, - message: `Unfortunately, your export failed. -${error}`, + templateContext: { + title: 'Export failed', + message: `Unfortunately, your export failed. + ${error}`, + }, }; } @@ -51,15 +54,25 @@ ${error}`, if (emailExportFileMode === EmailExportFileModes.ATTACHMENT) { return { subject, - message: 'Please find your requested export attached to this email.', attachments: await generateAttachments(filePath), + templateContext: { + title: 'Your export is ready', + message: 'Please find your requested export attached to this email.', + }, }; } const downloadLink = createDownloadLink(filePath); return { subject, - message: `Please click this one-time link to download your requested export: ${downloadLink} -Note that you need to be logged in to the admin panel for it to work, and after clicking it once, you won't be able to download the file again.`, + templateContext: { + title: 'Your export is ready', + message: + "Here is your one time link to access your requested export.\nNote that you need to be logged in to the admin panel for it to work, and after clicking it once, you won't be able to download the file again.", + cta: { + url: downloadLink, + text: 'Download export', + }, + }, }; }; diff --git a/packages/server-utils/src/email/index.ts b/packages/server-utils/src/email/index.ts new file mode 100644 index 0000000000..7a2169c9e9 --- /dev/null +++ b/packages/server-utils/src/email/index.ts @@ -0,0 +1,6 @@ +/** + * Tupaia + * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd + */ + +export { sendEmail } from './sendEmail'; diff --git a/packages/server-utils/src/sendEmail.ts b/packages/server-utils/src/email/sendEmail.ts similarity index 52% rename from packages/server-utils/src/sendEmail.ts rename to packages/server-utils/src/email/sendEmail.ts index 79e0b775cb..de8750a232 100644 --- a/packages/server-utils/src/sendEmail.ts +++ b/packages/server-utils/src/email/sendEmail.ts @@ -3,38 +3,57 @@ * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd */ +import fs from 'fs'; +import path from 'path'; import nodemailer from 'nodemailer'; import { getEnvVarOrDefault, getIsProductionEnvironment, requireEnv } from '@tupaia/utils'; import Mail from 'nodemailer/lib/mailer'; +import handlebars from 'handlebars'; -const TEXT_SIGN_OFF = 'Cheers,\n\nThe Tupaia Team'; -const HTML_SIGN_OFF = '

Cheers,

The Tupaia Team

'; +type CTA = { + text: string; + url: string; +}; +type TemplateContext = { + signOff?: string; + templateName: string; + templateContext: Record & { + cta?: CTA; + title: string; + }; +}; -type MailOptions = { +type MailOptions = TemplateContext & { subject?: string; - text?: string; - html?: string; attachments?: Mail.Attachment[]; signOff?: string; }; -export const sendEmail = async (to: string | string[], mailOptions: MailOptions = {}) => { - const { - subject, - text, - html, - attachments, - signOff = html ? HTML_SIGN_OFF : TEXT_SIGN_OFF, - } = mailOptions; +const compileHtml = (context: TemplateContext) => { + const { templateName, templateContext } = context; + const templatePath = path.resolve(__dirname, './templates/wrapper.html'); + const mainTemplate = fs.readFileSync(templatePath); + const compiledTemplate = handlebars.compile(mainTemplate.toString()); + let content = ''; + if (templateName) { + const innerContentTemplate = fs.readFileSync( + path.resolve(__dirname, `./templates/content/${templateName}.html`), + ); + content = handlebars.compile(innerContentTemplate.toString())(templateContext); + } + return compiledTemplate({ + ...templateContext, + content, + }).toString(); +}; + +export const sendEmail = async (to: string | string[], mailOptions: MailOptions) => { + const { subject, templateName, templateContext, attachments, signOff } = mailOptions || {}; const SMTP_HOST = getEnvVarOrDefault('SMTP_HOST', undefined); const SMTP_USER = getEnvVarOrDefault('SMTP_USER', undefined); const SMTP_PASSWORD = getEnvVarOrDefault('SMTP_PASSWORD', undefined); const SITE_EMAIL_ADDRESS = getEnvVarOrDefault('SITE_EMAIL_ADDRESS', undefined); - if (text && html) { - throw new Error('Only text or HTML can be sent in an email, not both'); - } - if (!SMTP_HOST || !SMTP_USER || !SMTP_PASSWORD || !SITE_EMAIL_ADDRESS) { return {}; } @@ -52,8 +71,7 @@ export const sendEmail = async (to: string | string[], mailOptions: MailOptions // Make sure it doesn't send real users mail from the dev server const sendTo = getIsProductionEnvironment() ? to : (requireEnv('DEV_EMAIL_ADDRESS') as string); - const fullText = text ? `${text}\n${signOff}` : undefined; - const fullHtml = html ? `${html}
${signOff}` : undefined; + const fullHtml = compileHtml({ templateName, templateContext, signOff }); return transporter.sendMail({ from: `Tupaia <${SITE_EMAIL_ADDRESS}>`, @@ -61,7 +79,6 @@ export const sendEmail = async (to: string | string[], mailOptions: MailOptions to: sendTo, subject, attachments, - text: fullText, html: fullHtml, }); }; diff --git a/packages/server-utils/src/email/templates/content/dashboardSubscription.html b/packages/server-utils/src/email/templates/content/dashboardSubscription.html new file mode 100644 index 0000000000..2011de78f9 --- /dev/null +++ b/packages/server-utils/src/email/templates/content/dashboardSubscription.html @@ -0,0 +1,3 @@ +
+

The latest data for the {{dashboardName}} dashboard in {{entityName}} is ready to view.

+
diff --git a/packages/server-utils/src/email/templates/content/deleteAccount.html b/packages/server-utils/src/email/templates/content/deleteAccount.html new file mode 100644 index 0000000000..4ed54a4d60 --- /dev/null +++ b/packages/server-utils/src/email/templates/content/deleteAccount.html @@ -0,0 +1,6 @@ +
+

+ {{user.first_name}} {{user.last_name}} ({{user.email}} - {{user.id}}, {{user.position}} at + {{user.employer}}) has requested to delete their account. +

+
diff --git a/packages/server-utils/src/email/templates/content/emailAfterTimeout.html b/packages/server-utils/src/email/templates/content/emailAfterTimeout.html new file mode 100644 index 0000000000..d9c2b24310 --- /dev/null +++ b/packages/server-utils/src/email/templates/content/emailAfterTimeout.html @@ -0,0 +1,4 @@ +
+

Hi {{userName}},

+

{{message}}

+
diff --git a/packages/server-utils/src/email/templates/content/passwordReset.html b/packages/server-utils/src/email/templates/content/passwordReset.html new file mode 100644 index 0000000000..e044eab6ef --- /dev/null +++ b/packages/server-utils/src/email/templates/content/passwordReset.html @@ -0,0 +1,11 @@ +
+

Hi {{userName}}

+

+ You are receiving this email because someone requested a password reset for this user account on + Tupaia.org. To reset your password follow the link below. +

+

+ If you believe this email was sent to you in error, please contact us immediately at + admin@tupaia.org. +

+
diff --git a/packages/server-utils/src/email/templates/content/permissionGranted.html b/packages/server-utils/src/email/templates/content/permissionGranted.html new file mode 100644 index 0000000000..3bb14d6860 --- /dev/null +++ b/packages/server-utils/src/email/templates/content/permissionGranted.html @@ -0,0 +1,13 @@ +
+

Hi {{userName}}

+

+ This is just to let you know that you've been added to the {{permissionGroupName}} access group + for for {{entityName}}. +

+

{{{description}}}

+

+ Please note that you'll need to log out and then log back in to get access to the new + permissions. +

+

Feel free to get in touch if you have any questions.

+
diff --git a/packages/server-utils/src/email/templates/content/requestCountryAccess.html b/packages/server-utils/src/email/templates/content/requestCountryAccess.html new file mode 100644 index 0000000000..8dff954cf6 --- /dev/null +++ b/packages/server-utils/src/email/templates/content/requestCountryAccess.html @@ -0,0 +1,17 @@ +
+

+ {{user.first_name}} {{user.last_name}} ({{user.email}} - {{user.id}}, {{user.position}} at + {{user.employer}}) has requested access to countries: +

+ + {{#if project}} +

+ For the project {{project.code}} (linked to permission groups: {{project.permissionGroups}}) +

+ {{/if}} +

With the message: "{{message}}"

+
diff --git a/packages/server-utils/src/email/templates/content/verifyEmail.html b/packages/server-utils/src/email/templates/content/verifyEmail.html new file mode 100644 index 0000000000..a1ba2e92f1 --- /dev/null +++ b/packages/server-utils/src/email/templates/content/verifyEmail.html @@ -0,0 +1,8 @@ +
+

Thank you for registering with {{platform}}

+

Please click below to register your email address.

+

+ If you believe this email was sent to you in error, please contact us immediately at + admin@tupaia.org. +

+
diff --git a/packages/server-utils/src/email/templates/wrapper.html b/packages/server-utils/src/email/templates/wrapper.html new file mode 100644 index 0000000000..2bf0b86fe5 --- /dev/null +++ b/packages/server-utils/src/email/templates/wrapper.html @@ -0,0 +1,120 @@ + + + + + + + +
+
+ + + + + + + + + + + + + {{#if cta}} + + + + {{/if}} + + + + + + + + {{#if unsubscribeUrl}} + + + + {{/if}} +
+ Tupaia logo +
+

{{title}}

+
{{{content}}}
+ {{cta.text}} +
+ {{#if signoff}} +

{{signoff}}

+ {{else}} +

+ Cheers, +
+ The Tupaia Team +

+ {{/if}} + Tupaia logo +
+ tupaia.org + bes.au +

+ Beyond Essential Systems +
+ 89 Nicholson St, Brunswick East VIC 3057 Australia +

+
+

+ If you wish to unsubscribe from these emails please click + here +

+
+
+
+ + diff --git a/packages/server-utils/src/index.ts b/packages/server-utils/src/index.ts index 5c78cfa9e7..3d6fcc3cc1 100644 --- a/packages/server-utils/src/index.ts +++ b/packages/server-utils/src/index.ts @@ -1,6 +1,6 @@ export { downloadPageAsPDF } from './downloadPageAsPDF'; export * from './s3'; -export { sendEmail } from './sendEmail'; +export { sendEmail } from './email'; export { generateUnsubscribeToken, verifyUnsubscribeToken } from './unsubscribeToken'; export { configureDotEnv } from './configureDotEnv'; export { constructExportEmail } from './constructExportEmail'; diff --git a/packages/tupaia-web-server/src/routes/EmailDashboardRoute.ts b/packages/tupaia-web-server/src/routes/EmailDashboardRoute.ts index 27f67b7a74..fc90fc2217 100644 --- a/packages/tupaia-web-server/src/routes/EmailDashboardRoute.ts +++ b/packages/tupaia-web-server/src/routes/EmailDashboardRoute.ts @@ -112,7 +112,6 @@ export class EmailDashboardRoute extends Route { const emails = mailingListEntries.map(({ email }) => email); const subject = `Tupaia Dashboard: ${projectEntity.name} ${entity.name} ${dashboard.name}`; - const html = `

Latest data for the ${dashboard.name} dashboard in ${entity.name}.

`; const filename = `${projectEntity.name}-${entity.name}-${dashboard.name}-export.pdf`; emails.forEach(email => { @@ -122,13 +121,16 @@ export class EmailDashboardRoute extends Route { token: unsubscribeToken, mailingListId: mailingList.id, }); - const unsubscribeHtml = `If you wish to unsubscribe from these emails please click here`; - const signOff = `

Cheers,

The Tupaia Team


${unsubscribeHtml}

`; return sendEmail(email, { subject, - html, - signOff, attachments: [{ filename, content: buffer }], + templateName: 'dashboardSubscription', + templateContext: { + title: 'Your Tupaia Dashboard Export is ready', + dashboardName: dashboard.name, + entityName: entity.name, + unsubscribeUrl, + }, }); }); diff --git a/yarn.lock b/yarn.lock index b8227055d6..4d22eea9ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12494,10 +12494,12 @@ __metadata: "@aws-sdk/credential-providers": ^3.341.0 "@aws-sdk/lib-storage": ^3.348.0 "@tupaia/utils": "workspace:*" - "@types/nodemailer": ^6.4.13 + "@types/nodemailer": ^6.4.15 "@types/sha256": ^0.2.2 cookie: ^0.5.0 + copyfiles: ^2.4.1 dotenv: ^16.4.5 + handlebars: ^4.7.8 nodemailer: ^6.9.12 puppeteer: ^15.4.0 sha256: ^0.2.0 @@ -13753,12 +13755,12 @@ __metadata: languageName: node linkType: hard -"@types/nodemailer@npm:^6.4.13": - version: 6.4.13 - resolution: "@types/nodemailer@npm:6.4.13" +"@types/nodemailer@npm:^6.4.15": + version: 6.4.15 + resolution: "@types/nodemailer@npm:6.4.15" dependencies: "@types/node": "*" - checksum: fd27d57d5801aaa7594d3bab748aedb1addc0c3a8ff9a21ea7675eec0e7e99cc477d05264b757b97691612fcfab37e52ded6c0725a700a90ad24fc7829dcc641 + checksum: f6f9a2f8a669703ecc3ca6359c12345b16f6b2e5691b93c406b9af7de639c02092ec00133526e6fecd8c60d884890a7cd0f967d8e64bedab46d5c3d8be0882d7 languageName: node linkType: hard @@ -19410,6 +19412,24 @@ __metadata: languageName: node linkType: hard +"copyfiles@npm:^2.4.1": + version: 2.4.1 + resolution: "copyfiles@npm:2.4.1" + dependencies: + glob: ^7.0.5 + minimatch: ^3.0.3 + mkdirp: ^1.0.4 + noms: 0.0.0 + through2: ^2.0.1 + untildify: ^4.0.0 + yargs: ^16.1.0 + bin: + copyfiles: copyfiles + copyup: copyfiles + checksum: aea69873bb99cc5f553967660cbfb70e4eeda198f572a36fb0f748b36877ff2c90fd906c58b1d540adbad8afa8ee82820172f1c18e69736f7ab52792c12745a7 + languageName: node + linkType: hard + "core-js-compat@npm:^3.14.0, core-js-compat@npm:^3.16.0": version: 3.17.3 resolution: "core-js-compat@npm:3.17.3" @@ -25377,6 +25397,24 @@ __metadata: languageName: node linkType: hard +"handlebars@npm:^4.7.8": + version: 4.7.8 + resolution: "handlebars@npm:4.7.8" + dependencies: + minimist: ^1.2.5 + neo-async: ^2.6.2 + source-map: ^0.6.1 + uglify-js: ^3.1.4 + wordwrap: ^1.0.0 + dependenciesMeta: + uglify-js: + optional: true + bin: + handlebars: bin/handlebars + checksum: 00e68bb5c183fd7b8b63322e6234b5ac8fbb960d712cb3f25587d559c2951d9642df83c04a1172c918c41bcfc81bfbd7a7718bbce93b893e0135fc99edea93ff + languageName: node + linkType: hard + "har-schema@npm:^2.0.0": version: 2.0.0 resolution: "har-schema@npm:2.0.0" @@ -31574,7 +31612,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": +"minimatch@npm:^3.0.3, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" dependencies: @@ -32277,7 +32315,7 @@ __metadata: languageName: node linkType: hard -"neo-async@npm:^2.6.0": +"neo-async@npm:^2.6.0, neo-async@npm:^2.6.2": version: 2.6.2 resolution: "neo-async@npm:2.6.2" checksum: deac9f8d00eda7b2e5cd1b2549e26e10a0faa70adaa6fdadca701cc55f49ee9018e427f424bac0c790b7c7e2d3068db97f3093f1093975f2acb8f8818b936ed9 @@ -32667,6 +32705,16 @@ __metadata: languageName: node linkType: hard +"noms@npm:0.0.0": + version: 0.0.0 + resolution: "noms@npm:0.0.0" + dependencies: + inherits: ^2.0.1 + readable-stream: ~1.0.31 + checksum: a05f056dabf764c86472b6b5aad10455f3adcb6971f366cdf36a72b559b29310a940e316bca30802f2804fdd41707941366224f4cba80c4f53071512245bf200 + languageName: node + linkType: hard + "nopt@npm:^4.0.3, nopt@npm:~4.0.1": version: 4.0.3 resolution: "nopt@npm:4.0.3" @@ -37265,6 +37313,18 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:~1.0.31": + version: 1.0.34 + resolution: "readable-stream@npm:1.0.34" + dependencies: + core-util-is: ~1.0.0 + inherits: ~2.0.1 + isarray: 0.0.1 + string_decoder: ~0.10.x + checksum: 85042c537e4f067daa1448a7e257a201070bfec3dd2706abdbd8ebc7f3418eb4d3ed4b8e5af63e2544d69f88ab09c28d5da3c0b77dc76185fddd189a59863b60 + languageName: node + linkType: hard + "readdirp@npm:^2.2.1": version: 2.2.1 resolution: "readdirp@npm:2.2.1" @@ -43818,7 +43878,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:16, yargs@npm:16.2.0, yargs@npm:^16.1.1, yargs@npm:^16.2.0": +"yargs@npm:16, yargs@npm:16.2.0, yargs@npm:^16.1.0, yargs@npm:^16.1.1, yargs@npm:^16.2.0": version: 16.2.0 resolution: "yargs@npm:16.2.0" dependencies: