mirror of
https://github.com/standardnotes/server
synced 2026-01-16 20:04:32 -05:00
feat(domain-events-infra): add SES email bounce notifications handler (#569)
* feat(domain-events-infra): add SES email bounce notifications handler * fix(domain-events-infra): specs
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { DomainEventHandlerInterface } from '@standardnotes/domain-events'
|
||||
|
||||
import { startBackgroundTransaction } from 'newrelic'
|
||||
jest.mock('newrelic')
|
||||
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { SQSNewRelicBounceNotificiationHandler } from './SQSNewRelicBounceNotificiationHandler'
|
||||
|
||||
describe('SQSNewRelicBounceNotificiationHandler', () => {
|
||||
let handler: DomainEventHandlerInterface
|
||||
let handlers: Map<string, DomainEventHandlerInterface>
|
||||
let logger: Logger
|
||||
let mockedStartBackgroundTransaction: unknown
|
||||
|
||||
const createHandler = () => new SQSNewRelicBounceNotificiationHandler(handlers, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
handler = {} as jest.Mocked<DomainEventHandlerInterface>
|
||||
handler.handle = jest.fn()
|
||||
|
||||
handlers = new Map([['EMAIL_BOUNCED', handler]])
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.debug = jest.fn()
|
||||
logger.error = jest.fn()
|
||||
|
||||
mockedStartBackgroundTransaction = startBackgroundTransaction as jest.Mocked<unknown>
|
||||
})
|
||||
|
||||
it('should handle messages', async () => {
|
||||
const sqsMessage = `{
|
||||
"Message" : "{\\"notificationType\\":\\"Bounce\\",\\"bounce\\":{\\"feedbackId\\":\\"010001879d0a9def-d9882210-6467-48ed-8088-2193c66a349b-000000\\",\\"bounceType\\":\\"Transient\\",\\"bounceSubType\\":\\"General\\",\\"bouncedRecipients\\":[{\\"emailAddress\\":\\"test@test.te\\",\\"action\\":\\"failed\\",\\"status\\":\\"5.7.1\\",\\"diagnosticCode\\":\\"smtp; 550 5.7.1 <test@test.te>: Recipient address rejected: Recipient not found\\"}],\\"timestamp\\":\\"2023-04-20T05:02:11.000Z\\",\\"remoteMtaIp\\":\\"1.2.3.4\\",\\"reportingMTA\\":\\"dns; test.smtp-out.amazonses.com\\"},\\"mail\\":{\\"timestamp\\":\\"2023-04-20T05:02:08.589Z\\",\\"source\\":\\"Standard Notes <backups@standardnotes.org>\\",\\"sourceArn\\":\\"arn:aws:ses:us-east-1:336603415364:identity/backups@standardnotes.org\\",\\"sourceIp\\":\\"1.2.3.4\\",\\"callerIdentity\\":\\"test\\",\\"sendingAccountId\\":\\"123456\\",\\"messageId\\":\\"010001879d0a92cd-00ed31d1-bf9e-4ce4-abb9-8c6e95a30733-000000\\",\\"destination\\":[\\"test@test.te\\"]}}"
|
||||
}`
|
||||
|
||||
await createHandler().handleMessage(sqsMessage)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
expect((<any>mockedStartBackgroundTransaction).mock.calls[0][0]).toBe('EMAIL_BOUNCED')
|
||||
})
|
||||
|
||||
it('should not handle unsupported messages', async () => {
|
||||
const sqsMessage = `{
|
||||
"Message" : "{\\"notificationType\\":\\"TEST\\",\\"bounce\\":{\\"feedbackId\\":\\"010001879d0a9def-d9882210-6467-48ed-8088-2193c66a349b-000000\\",\\"bounceType\\":\\"Transient\\",\\"bounceSubType\\":\\"General\\",\\"bouncedRecipients\\":[{\\"emailAddress\\":\\"test@test.te\\",\\"action\\":\\"failed\\",\\"status\\":\\"5.7.1\\",\\"diagnosticCode\\":\\"smtp; 550 5.7.1 <test@test.te>: Recipient address rejected: Recipient not found\\"}],\\"timestamp\\":\\"2023-04-20T05:02:11.000Z\\",\\"remoteMtaIp\\":\\"1.2.3.4\\",\\"reportingMTA\\":\\"dns; test.smtp-out.amazonses.com\\"},\\"mail\\":{\\"timestamp\\":\\"2023-04-20T05:02:08.589Z\\",\\"source\\":\\"Standard Notes <backups@standardnotes.org>\\",\\"sourceArn\\":\\"arn:aws:ses:us-east-1:336603415364:identity/backups@standardnotes.org\\",\\"sourceIp\\":\\"1.2.3.4\\",\\"callerIdentity\\":\\"test\\",\\"sendingAccountId\\":\\"123456\\",\\"messageId\\":\\"010001879d0a92cd-00ed31d1-bf9e-4ce4-abb9-8c6e95a30733-000000\\",\\"destination\\":[\\"test@test.te\\"]}}"
|
||||
}`
|
||||
|
||||
await createHandler().handleMessage(sqsMessage)
|
||||
|
||||
expect(handler.handle).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle errors', async () => {
|
||||
await createHandler().handleError(new Error('test'))
|
||||
|
||||
expect(logger.error).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should tell if there is no handler for an event', async () => {
|
||||
const sqsMessage = `{
|
||||
"Message" : "{\\"notificationType\\":\\"Bounce\\",\\"bounce\\":{\\"feedbackId\\":\\"010001879d0a9def-d9882210-6467-48ed-8088-2193c66a349b-000000\\",\\"bounceType\\":\\"Transient\\",\\"bounceSubType\\":\\"General\\",\\"bouncedRecipients\\":[{\\"emailAddress\\":\\"test@test.te\\",\\"action\\":\\"failed\\",\\"status\\":\\"5.7.1\\",\\"diagnosticCode\\":\\"smtp; 550 5.7.1 <test@test.te>: Recipient address rejected: Recipient not found\\"}],\\"timestamp\\":\\"2023-04-20T05:02:11.000Z\\",\\"remoteMtaIp\\":\\"1.2.3.4\\",\\"reportingMTA\\":\\"dns; test.smtp-out.amazonses.com\\"},\\"mail\\":{\\"timestamp\\":\\"2023-04-20T05:02:08.589Z\\",\\"source\\":\\"Standard Notes <backups@standardnotes.org>\\",\\"sourceArn\\":\\"arn:aws:ses:us-east-1:336603415364:identity/backups@standardnotes.org\\",\\"sourceIp\\":\\"1.2.3.4\\",\\"callerIdentity\\":\\"test\\",\\"sendingAccountId\\":\\"123456\\",\\"messageId\\":\\"010001879d0a92cd-00ed31d1-bf9e-4ce4-abb9-8c6e95a30733-000000\\",\\"destination\\":[\\"test@test.te\\"]}}"
|
||||
}`
|
||||
|
||||
const bounceHandler = new SQSNewRelicBounceNotificiationHandler(new Map([]), logger)
|
||||
await bounceHandler.handleMessage(sqsMessage)
|
||||
|
||||
expect(logger.debug).toHaveBeenCalledWith('Event handler for event type EMAIL_BOUNCED does not exist')
|
||||
|
||||
expect(handler.handle).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Logger } from 'winston'
|
||||
import * as newrelic from 'newrelic'
|
||||
|
||||
import {
|
||||
DomainEventHandlerInterface,
|
||||
DomainEventMessageHandlerInterface,
|
||||
DomainEventService,
|
||||
EmailBouncedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
|
||||
export class SQSNewRelicBounceNotificiationHandler implements DomainEventMessageHandlerInterface {
|
||||
private readonly ALLOWED_NOTIFICATION_TYPES = ['Bounce']
|
||||
|
||||
constructor(private handlers: Map<string, DomainEventHandlerInterface>, private logger: Logger) {}
|
||||
|
||||
async handleMessage(message: string): Promise<void> {
|
||||
const messageParsed = JSON.parse(JSON.parse(message).Message)
|
||||
|
||||
if (!this.ALLOWED_NOTIFICATION_TYPES.includes(messageParsed.notificationType)) {
|
||||
this.logger.error(`Received notification of type ${messageParsed.notificationType} which is not allowed`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for (const bouncedRecipient of messageParsed.bounce.bouncedRecipients) {
|
||||
const domainEvent: EmailBouncedEvent = {
|
||||
type: 'EMAIL_BOUNCED',
|
||||
payload: {
|
||||
bounceType: messageParsed.bounce.bounceType,
|
||||
bounceSubType: messageParsed.bounce.bounceSubType,
|
||||
recipientEmail: bouncedRecipient.emailAddress,
|
||||
diagnosticCode: bouncedRecipient.diagnosticCode,
|
||||
},
|
||||
createdAt: new Date(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: bouncedRecipient.emailAddress,
|
||||
userIdentifierType: 'email',
|
||||
},
|
||||
origin: DomainEventService.SES,
|
||||
},
|
||||
}
|
||||
|
||||
const handler = this.handlers.get(domainEvent.type)
|
||||
if (!handler) {
|
||||
this.logger.debug(`Event handler for event type ${domainEvent.type} does not exist`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this.logger.debug(`Received event: ${domainEvent.type}`)
|
||||
|
||||
await newrelic.startBackgroundTransaction(
|
||||
domainEvent.type,
|
||||
/* istanbul ignore next */
|
||||
() => {
|
||||
newrelic.getTransaction()
|
||||
|
||||
return handler.handle(domainEvent)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async handleError(error: Error): Promise<void> {
|
||||
this.logger.error('Error occured while handling SQS message: %O', error)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ export * from './Redis/RedisEventMessageHandler'
|
||||
|
||||
export * from './SNS/SNSDomainEventPublisher'
|
||||
|
||||
export * from './SQS/SQSNewRelicBounceNotificiationHandler'
|
||||
export * from './SQS/SQSDomainEventSubscriberFactory'
|
||||
export * from './SQS/SQSEventMessageHandler'
|
||||
export * from './SQS/SQSNewRelicEventMessageHandler'
|
||||
|
||||
@@ -12,4 +12,5 @@ export enum DomainEventService {
|
||||
Revisions = 'revisions',
|
||||
Email = 'email',
|
||||
Settings = 'settings',
|
||||
SES = 'ses',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { EmailBouncedEventPayload } from './EmailBouncedEventPayload'
|
||||
|
||||
export interface EmailBouncedEvent extends DomainEventInterface {
|
||||
type: 'EMAIL_BOUNCED'
|
||||
payload: EmailBouncedEventPayload
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface EmailBouncedEventPayload {
|
||||
recipientEmail: string
|
||||
bounceType: string
|
||||
bounceSubType: string
|
||||
diagnosticCode?: string
|
||||
}
|
||||
@@ -12,6 +12,8 @@ export * from './Event/DuplicateItemSyncedEvent'
|
||||
export * from './Event/DuplicateItemSyncedEventPayload'
|
||||
export * from './Event/EmailBackupRequestedEvent'
|
||||
export * from './Event/EmailBackupRequestedEventPayload'
|
||||
export * from './Event/EmailBouncedEvent'
|
||||
export * from './Event/EmailBouncedEventPayload'
|
||||
export * from './Event/EmailRequestedEvent'
|
||||
export * from './Event/EmailRequestedEventPayload'
|
||||
export * from './Event/EmailSentEvent'
|
||||
|
||||
Reference in New Issue
Block a user