Skip to content

Commit

Permalink
feat: disable tracking in transactional email (#2241)
Browse files Browse the repository at this point in the history
* feat: disable tracking for transactional emails

* chore: update dockerfile

* chore: set default value of disable_tracking to false

* chore: add comments to Dockerfile

* chore: add comment to explain use of noTrackingConfigSet

* test: update failing tests

---------

Co-authored-by: KishenKumarrrrr <kishen@open.gov.sg>
  • Loading branch information
KishenKumarrrrr and KishenKumarrrrr committed Jan 25, 2024
1 parent dd3522a commit 1c9bcee
Show file tree
Hide file tree
Showing 9 changed files with 66 additions and 15 deletions.
5 changes: 4 additions & 1 deletion backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ RUN apk update && apk upgrade && apk add --no-cache --virtual builds-deps build-

RUN apk add jq

RUN python3 -m pip install awscli
# There was a breaking change in the base image used that prevents us from installing via pip
# Instead of activating a virtual env, this is a simpler workaround
# https://github.com/python/cpython/issues/102134
RUN apk add --no-cache aws-cli

RUN aws configure set default.region ap-southeast-1

Expand Down
6 changes: 6 additions & 0 deletions backend/src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ interface ConfigSchema {
}
mailFrom: string
mailConfigurationSet: string
noTrackingMailConfigurationSet: string
mailVia: string
mailDefaultRate: number
transactionalEmail: {
Expand Down Expand Up @@ -468,6 +469,11 @@ const config: Config<ConfigSchema> = convict({
default: 'postman-email-open',
env: 'BACKEND_SES_CONFIGURATION_SET',
},
noTrackingMailConfigurationSet: {
doc: 'AWS SES Configuration set that does not include open and read tracking',
default: 'postman-email-no-tracking',
env: 'BACKEND_SES_NO_TRACKING_CONFIGURATION_SET',
},
mailVia: {
doc: 'Text to appended to custom sender name',
default: 'via Postman',
Expand Down
3 changes: 2 additions & 1 deletion backend/src/core/services/mail.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ const mailClient = new MailClient(
config.get('mailOptions'),
config.get('emailCallback.hashSecret'),
config.get('emailFallback.activate') ? config.get('mailFrom') : undefined,
config.get('mailConfigurationSet')
config.get('mailConfigurationSet'),
config.get('noTrackingMailConfigurationSet')
)

export const MailService = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export const InitEmailTransactionalMiddleware = (
tag?: string
cc?: string[]
bcc?: string[]
disable_tracking?: boolean
}
type ReqBodyWithId = ReqBody & { emailMessageTransactionalId: string }

Expand Down Expand Up @@ -210,6 +211,7 @@ export const InitEmailTransactionalMiddleware = (
cc,
bcc,
emailMessageTransactionalId, // added by saveMessage middleware
disable_tracking: disableTracking,
} = req.body

try {
Expand Down Expand Up @@ -275,6 +277,7 @@ export const InitEmailTransactionalMiddleware = (
? bcc.filter((c) => !blacklistedRecipients.includes(c))
: undefined,
emailMessageTransactionalId,
disableTracking,
})
emailMessageTransactional.set(
'status',
Expand Down
1 change: 1 addition & 0 deletions backend/src/email/routes/email-transactional.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export const InitEmailTransactionalRoute = (
.items(
Joi.string().trim().email().options({ convert: true }).lowercase()
),
disable_tracking: Joi.boolean().default(false),
}),
}
const getByIdValidator = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ describe(`${emailTransactionalRoute}/send`, () => {
).id.toString(),
attachments: undefined,
},
{ extraSmtpHeaders: { isTransactional: true } }
{ disableTracking: false, extraSmtpHeaders: { isTransactional: true } }
)
})
test('Should throw a 400 error if the body size is too large (JSON payload)', async () => {
Expand Down Expand Up @@ -616,6 +616,7 @@ describe(`${emailTransactionalRoute}/send`, () => {
],
},
{
disableTracking: false,
extraSmtpHeaders: { isTransactional: true },
}
)
Expand Down Expand Up @@ -692,6 +693,7 @@ describe(`${emailTransactionalRoute}/send`, () => {
],
},
{
disableTracking: false,
extraSmtpHeaders: { isTransactional: true },
}
)
Expand Down Expand Up @@ -825,6 +827,7 @@ describe(`${emailTransactionalRoute}/send`, () => {
],
},
{
disableTracking: false,
extraSmtpHeaders: { isTransactional: true },
}
)
Expand Down
3 changes: 3 additions & 0 deletions backend/src/email/services/email-transactional.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ async function sendMessage({
cc,
bcc,
emailMessageTransactionalId,
disableTracking,
}: {
subject: string
body: string
Expand All @@ -51,6 +52,7 @@ async function sendMessage({
cc?: string[]
bcc?: string[]
emailMessageTransactionalId: string
disableTracking?: boolean
}): Promise<void> {
// TODO: flagging this coupling for future refactoring:
// currently, we are using EmailTemplateService to sanitize both tx emails and campaign emails
Expand Down Expand Up @@ -99,6 +101,7 @@ async function sendMessage({
// receive from SES, but not saving to DB
const isEmailSent = await EmailService.sendEmail(mailToSend, {
extraSmtpHeaders: { isTransactional: true },
disableTracking,
})
if (!isEmailSent) {
throw new Error('Failed to send transactional email')
Expand Down
50 changes: 39 additions & 11 deletions shared/src/clients/mail-client.class/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,29 @@ export * from './interfaces'

export type SendEmailOpts = {
extraSmtpHeaders: Record<string, any>
disableTracking?: boolean
}

export default class MailClient {
private mailer: nodemailer.Transporter
private email: string
private hashSecret: string
private configSet: string | undefined
private defaultConfigSet: string | undefined
/*
The AWS SES events to be tracked are defined in configuration sets within the AWS console.
When an email is sent, we specify the configuration set to be used by setting "X-SES-CONFIGURATION-SET" in the API call header.
There is no option to turn off tracking via parameters in the API call, it can only be configured through a configuration set.
Thus, we need multiple configuration sets to toggle the tracking feature for read and open receipts.
*/
private noTrackingConfigSet: string | undefined

constructor(
credentials: MailCredentials,
hashSecret: string,
email?: string,
configSet?: string
defaultConfigSet?: string,
noTrackingConfigSet?: string
) {
const { host, port, auth } = credentials
this.hashSecret = hashSecret
Expand All @@ -35,7 +45,8 @@ export default class MailClient {
pass: auth.pass,
},
})
this.configSet = configSet
this.defaultConfigSet = defaultConfigSet
this.noTrackingConfigSet = noTrackingConfigSet
}

public sendMail(
Expand All @@ -61,14 +72,7 @@ export default class MailClient {
let headers: any = {
[REFERENCE_ID_HEADER]: JSON.stringify(xSmtpHeader),
}
if (this.configSet) {
headers = {
...headers,
// Specify this to configure callback endpoint for notifications other
// than delivery and bounce through SES configuration set
[CONFIGURATION_SET_HEADER]: this.configSet,
}
}
headers = this.setSesConfigurationHeader(headers, option?.disableTracking)
if (input.unsubLink) {
headers = {
...headers,
Expand Down Expand Up @@ -96,4 +100,28 @@ export default class MailClient {
})
})
}

private setSesConfigurationHeader(
headers: object,
disableTracking: boolean | undefined
): object {
// 1. If there is no default config set, we will not set any configuration header
if (!this.defaultConfigSet) {
return headers
}
// 2. If the user wants to disable tracking and there is a no tracking configuration, we set it
if (disableTracking && this.noTrackingConfigSet) {
return {
...headers,
// Configuration header does not include open and read notification
[CONFIGURATION_SET_HEADER]: this.noTrackingConfigSet,
}
}
// 3. Otherwise, we will use the default tracking SES configuration set
return {
...headers,
// Configuration header includes open and read notification
[CONFIGURATION_SET_HEADER]: this.defaultConfigSet,
}
}
}
5 changes: 4 additions & 1 deletion worker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ RUN apk update && apk upgrade && apk add --no-cache --virtual builds-deps build-

RUN apk add jq

RUN python3 -m pip install awscli
# There was a breaking change in the base image used that prevents us from installing via pip
# Instead of activating a virtual env, this is a simpler workaround
# https://github.com/python/cpython/issues/102134
RUN apk add --no-cache aws-cli

RUN aws configure set default.region ap-southeast-1

Expand Down

0 comments on commit 1c9bcee

Please sign in to comment.