Skip to content
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

feat: disable tracking in transactional email #2241

Merged
merged 6 commits into from
Jan 25, 2024
Merged
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -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

6 changes: 6 additions & 0 deletions backend/src/core/config.ts
Original file line number Diff line number Diff line change
@@ -83,6 +83,7 @@ interface ConfigSchema {
}
mailFrom: string
mailConfigurationSet: string
noTrackingMailConfigurationSet: string
mailVia: string
mailDefaultRate: number
transactionalEmail: {
@@ -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',
3 changes: 2 additions & 1 deletion backend/src/core/services/mail.service.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Original file line number Diff line number Diff line change
@@ -67,6 +67,7 @@ export const InitEmailTransactionalMiddleware = (
tag?: string
cc?: string[]
bcc?: string[]
disable_tracking?: boolean
}
type ReqBodyWithId = ReqBody & { emailMessageTransactionalId: string }

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

try {
@@ -275,6 +277,7 @@ export const InitEmailTransactionalMiddleware = (
? bcc.filter((c) => !blacklistedRecipients.includes(c))
: undefined,
emailMessageTransactionalId,
disableTracking,
})
emailMessageTransactional.set(
'status',
1 change: 1 addition & 0 deletions backend/src/email/routes/email-transactional.routes.ts
Original file line number Diff line number Diff line change
@@ -62,6 +62,7 @@ export const InitEmailTransactionalRoute = (
.items(
Joi.string().trim().email().options({ convert: true }).lowercase()
),
disable_tracking: Joi.boolean().default(false),
}),
}
const getByIdValidator = {
3 changes: 3 additions & 0 deletions backend/src/email/services/email-transactional.service.ts
Original file line number Diff line number Diff line change
@@ -41,6 +41,7 @@ async function sendMessage({
cc,
bcc,
emailMessageTransactionalId,
disableTracking,
}: {
subject: string
body: string
@@ -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
@@ -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')
43 changes: 32 additions & 11 deletions shared/src/clients/mail-client.class/index.ts
Original file line number Diff line number Diff line change
@@ -10,19 +10,22 @@ 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
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
@@ -35,7 +38,8 @@ export default class MailClient {
pass: auth.pass,
},
})
this.configSet = configSet
this.defaultConfigSet = defaultConfigSet
this.noTrackingConfigSet = noTrackingConfigSet
}

public sendMail(
@@ -61,14 +65,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,
@@ -96,4 +93,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
@@ -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


Unchanged files with check annotations Beta

void fetchGovsgMessages(search, selectedPage).finally(() => {
setLoading(false)
})
}, [])

Check warning on line 112 in frontend/src/components/dashboard/create/govsg/GovsgMessages.tsx

GitHub Actions / test-frontend

React Hook useEffect has missing dependencies: 'fetchGovsgMessages', 'search', and 'selectedPage'. Either include them or remove the dependency array
const handlePageChange = (index: number) => {
void fetchGovsgMessages(search, index)
default:
break
}
}, [])

Check warning on line 123 in frontend/src/components/login/login-input/LoginInput.tsx

GitHub Actions / test-frontend

React Hook useEffect has a missing dependency: 'modalContext'. Either include it or remove the dependency array
async function sendOtp() {
resetButton()
campaignId: number
}): (data: CSVParams[]) => Promise<void> {
return async function (data: CSVParams[]): Promise<void> {
const paramsWithoutPasscode = template.params!.filter(

Check warning on line 112 in backend/src/govsg/services/govsg.service.ts

GitHub Actions / test-backend

Forbidden non-null assertion
(param) => param !== 'passcode'
) // TODO: Un-hardcode this
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion