浏览代码

feat(syncing-server): replace email backup attachment created with email requested

Karol Sójko 2 年之前
父节点
当前提交
32601f34f1

+ 0 - 7
packages/domain-events/src/Domain/Event/EmailBackupAttachmentCreatedEvent.ts

@@ -1,7 +0,0 @@
-import { DomainEventInterface } from './DomainEventInterface'
-import { EmailBackupAttachmentCreatedEventPayload } from './EmailBackupAttachmentCreatedEventPayload'
-
-export interface EmailBackupAttachmentCreatedEvent extends DomainEventInterface {
-  type: 'EMAIL_BACKUP_ATTACHMENT_CREATED'
-  payload: EmailBackupAttachmentCreatedEventPayload
-}

+ 0 - 6
packages/domain-events/src/Domain/Event/EmailBackupAttachmentCreatedEventPayload.ts

@@ -1,6 +0,0 @@
-export interface EmailBackupAttachmentCreatedEventPayload {
-  backupFileName: string
-  backupFileIndex: number
-  backupFilesTotal: number
-  email: string
-}

+ 1 - 0
packages/domain-events/src/Domain/Event/EmailRequestedEventPayload.ts

@@ -4,6 +4,7 @@ export interface EmailRequestedEventPayload {
   level: string
   subject: string
   body: string
+  sender?: string
   attachments?: Array<{
     filePath: string
     fileName: string

+ 0 - 2
packages/domain-events/src/Domain/index.ts

@@ -14,8 +14,6 @@ export * from './Event/DuplicateItemSyncedEvent'
 export * from './Event/DuplicateItemSyncedEventPayload'
 export * from './Event/EmailArchiveExtensionSyncedEvent'
 export * from './Event/EmailArchiveExtensionSyncedEventPayload'
-export * from './Event/EmailBackupAttachmentCreatedEvent'
-export * from './Event/EmailBackupAttachmentCreatedEventPayload'
 export * from './Event/EmailBackupRequestedEvent'
 export * from './Event/EmailBackupRequestedEventPayload'
 export * from './Event/EmailRequestedEvent'

+ 14 - 0
packages/syncing-server/src/Domain/Email/EmailBackupAttachmentCreated.ts

@@ -0,0 +1,14 @@
+import { html } from './email-backup-attachment-created.html'
+
+export function getSubject(fileIndex: number, numberOfFiles: number, date: string): string {
+  let subject = `Data Backup for ${date}`
+  if (numberOfFiles > 1) {
+    subject = `Data Backup for ${date} - Part ${fileIndex} Of ${numberOfFiles}`
+  }
+
+  return subject
+}
+
+export function getBody(email: string): string {
+  return html(email)
+}

+ 33 - 0
packages/syncing-server/src/Domain/Email/email-backup-attachment-created.html.ts

@@ -0,0 +1,33 @@
+export const html = (email: string) => `
+<p>
+  Your encrypted data backup is attached for ${email}. You can import this file using
+  the Standard Notes web or desktop app, or by using the offline decryption script available at
+  <a style="text-decoration:none !important; text-decoration:none;">standardnotes.org/offline</a>.
+</p>
+
+<p>
+  <strong>Please note:</strong>
+  <ol>
+    <li>
+      We will never send anything other than a <code>txt</code> file
+      as part of your daily backups. To protect yourself against phishing attacks, never open
+      any other kind of file, and always open the <code>txt</code> file with a text editor to
+      verify its contents before decrypting.
+    </li>
+
+    <li>
+      We will never include clickable links in this email. Instead, manually verify
+      and copy/paste the offline link above in your browser.
+    </li>
+  </ol>
+</p>
+<hr />
+<p>
+  <i>
+    Want to disable daily backups? Uninstall 'Daily Email Backups' from your Extensions
+    menu in Standard Notes to immediately disable backups.
+    Otherwise, reply to this email with "Stop". Note that it may
+    take up to 72 hours or more to perform manual removal via the "Stop" method.
+  </i>
+</p>
+`

+ 7 - 21
packages/syncing-server/src/Domain/Event/DomainEventFactory.ts

@@ -3,7 +3,6 @@ import {
   DomainEventService,
   DuplicateItemSyncedEvent,
   EmailArchiveExtensionSyncedEvent,
-  EmailBackupAttachmentCreatedEvent,
   EmailRequestedEvent,
   ItemDumpedEvent,
   ItemRevisionCreationRequestedEvent,
@@ -135,6 +134,13 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
     level: string
     body: string
     subject: string
+    sender?: string
+    attachments?: Array<{
+      filePath: string
+      fileName: string
+      attachmentFileName: string
+      attachmentContentType: string
+    }>
   }): EmailRequestedEvent {
     return {
       type: 'EMAIL_REQUESTED',
@@ -190,24 +196,4 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
       },
     }
   }
-
-  createEmailBackupAttachmentCreatedEvent(dto: {
-    backupFileName: string
-    backupFileIndex: number
-    backupFilesTotal: number
-    email: string
-  }): EmailBackupAttachmentCreatedEvent {
-    return {
-      type: 'EMAIL_BACKUP_ATTACHMENT_CREATED',
-      createdAt: this.timer.getUTCDate(),
-      meta: {
-        correlation: {
-          userIdentifier: dto.email,
-          userIdentifierType: 'email',
-        },
-        origin: DomainEventService.SyncingServer,
-      },
-      payload: dto,
-    }
-  }
 }

+ 7 - 7
packages/syncing-server/src/Domain/Event/DomainEventFactoryInterface.ts

@@ -1,7 +1,6 @@
 import {
   DuplicateItemSyncedEvent,
   EmailArchiveExtensionSyncedEvent,
-  EmailBackupAttachmentCreatedEvent,
   EmailRequestedEvent,
   ItemDumpedEvent,
   ItemRevisionCreationRequestedEvent,
@@ -19,6 +18,13 @@ export interface DomainEventFactoryInterface {
     level: string
     body: string
     subject: string
+    sender?: string
+    attachments?: Array<{
+      filePath: string
+      fileName: string
+      attachmentFileName: string
+      attachmentContentType: string
+    }>
   }): EmailRequestedEvent
   createItemsSyncedEvent(dto: {
     userUuid: string
@@ -30,12 +36,6 @@ export interface DomainEventFactoryInterface {
     source: 'account-deletion' | 'realtime-extensions-sync'
   }): ItemsSyncedEvent
   createEmailArchiveExtensionSyncedEvent(userUuid: string, extensionId: string): EmailArchiveExtensionSyncedEvent
-  createEmailBackupAttachmentCreatedEvent(dto: {
-    backupFileName: string
-    backupFileIndex: number
-    backupFilesTotal: number
-    email: string
-  }): EmailBackupAttachmentCreatedEvent
   createDuplicateItemSyncedEvent(itemUuid: string, userUuid: string): DuplicateItemSyncedEvent
   createItemRevisionCreationRequested(itemUuid: string, userUuid: string): ItemRevisionCreationRequestedEvent
   createItemDumpedEvent(fileDumpPath: string, userUuid: string): ItemDumpedEvent

+ 7 - 24
packages/syncing-server/src/Domain/Handler/EmailArchiveExtensionSyncedEventHandler.spec.ts

@@ -3,7 +3,7 @@ import 'reflect-metadata'
 import {
   DomainEventPublisherInterface,
   EmailArchiveExtensionSyncedEvent,
-  EmailBackupAttachmentCreatedEvent,
+  EmailRequestedEvent,
 } from '@standardnotes/domain-events'
 import { Logger } from 'winston'
 import { AuthHttpServiceInterface } from '../Auth/AuthHttpServiceInterface'
@@ -35,6 +35,7 @@ describe('EmailArchiveExtensionSyncedEventHandler', () => {
       domainEventFactory,
       emailAttachmentMaxByteSize,
       itemTransferCalculator,
+      's3-backup-bucket-name',
       logger,
     )
 
@@ -62,9 +63,7 @@ describe('EmailArchiveExtensionSyncedEventHandler', () => {
     domainEventPublisher.publish = jest.fn()
 
     domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
-    domainEventFactory.createEmailBackupAttachmentCreatedEvent = jest
-      .fn()
-      .mockReturnValue({} as jest.Mocked<EmailBackupAttachmentCreatedEvent>)
+    domainEventFactory.createEmailRequestedEvent = jest.fn().mockReturnValue({} as jest.Mocked<EmailRequestedEvent>)
 
     itemTransferCalculator = {} as jest.Mocked<ItemTransferCalculatorInterface>
     itemTransferCalculator.computeItemUuidBundlesToFetch = jest.fn().mockReturnValue([['1-2-3']])
@@ -78,12 +77,7 @@ describe('EmailArchiveExtensionSyncedEventHandler', () => {
     await createHandler().handle(event)
 
     expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
-    expect(domainEventFactory.createEmailBackupAttachmentCreatedEvent).toHaveBeenCalledWith({
-      backupFileIndex: 1,
-      backupFileName: 'backup-file-name',
-      backupFilesTotal: 1,
-      email: 'test@test.com',
-    })
+    expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
   })
 
   it('should inform that multipart backup attachment for email was created', async () => {
@@ -96,18 +90,7 @@ describe('EmailArchiveExtensionSyncedEventHandler', () => {
     await createHandler().handle(event)
 
     expect(domainEventPublisher.publish).toHaveBeenCalledTimes(2)
-    expect(domainEventFactory.createEmailBackupAttachmentCreatedEvent).toHaveBeenNthCalledWith(1, {
-      backupFileIndex: 1,
-      backupFileName: 'backup-file-name-1',
-      backupFilesTotal: 2,
-      email: 'test@test.com',
-    })
-    expect(domainEventFactory.createEmailBackupAttachmentCreatedEvent).toHaveBeenNthCalledWith(2, {
-      backupFileIndex: 2,
-      backupFileName: 'backup-file-name-2',
-      backupFilesTotal: 2,
-      email: 'test@test.com',
-    })
+    expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalledTimes(2)
   })
 
   it('should not inform that backup attachment for email was created if user key params cannot be obtained', async () => {
@@ -118,7 +101,7 @@ describe('EmailArchiveExtensionSyncedEventHandler', () => {
     await createHandler().handle(event)
 
     expect(domainEventPublisher.publish).not.toHaveBeenCalled()
-    expect(domainEventFactory.createEmailBackupAttachmentCreatedEvent).not.toHaveBeenCalled()
+    expect(domainEventFactory.createEmailRequestedEvent).not.toHaveBeenCalled()
   })
 
   it('should not inform that backup attachment for email was created if backup file name is empty', async () => {
@@ -127,6 +110,6 @@ describe('EmailArchiveExtensionSyncedEventHandler', () => {
     await createHandler().handle(event)
 
     expect(domainEventPublisher.publish).not.toHaveBeenCalled()
-    expect(domainEventFactory.createEmailBackupAttachmentCreatedEvent).not.toHaveBeenCalled()
+    expect(domainEventFactory.createEmailRequestedEvent).not.toHaveBeenCalled()
   })
 })

+ 19 - 6
packages/syncing-server/src/Domain/Handler/EmailArchiveExtensionSyncedEventHandler.ts

@@ -4,6 +4,7 @@ import {
   DomainEventPublisherInterface,
   EmailArchiveExtensionSyncedEvent,
 } from '@standardnotes/domain-events'
+import { EmailLevel } from '@standardnotes/domain-core'
 import { inject, injectable } from 'inversify'
 import { Logger } from 'winston'
 import TYPES from '../../Bootstrap/Types'
@@ -13,6 +14,7 @@ import { ItemBackupServiceInterface } from '../Item/ItemBackupServiceInterface'
 import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
 import { ItemQuery } from '../Item/ItemQuery'
 import { ItemTransferCalculatorInterface } from '../Item/ItemTransferCalculatorInterface'
+import { getBody, getSubject } from '../Email/EmailBackupAttachmentCreated'
 
 @injectable()
 export class EmailArchiveExtensionSyncedEventHandler implements DomainEventHandlerInterface {
@@ -24,6 +26,7 @@ export class EmailArchiveExtensionSyncedEventHandler implements DomainEventHandl
     @inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
     @inject(TYPES.EMAIL_ATTACHMENT_MAX_BYTE_SIZE) private emailAttachmentMaxByteSize: number,
     @inject(TYPES.ItemTransferCalculator) private itemTransferCalculator: ItemTransferCalculatorInterface,
+    @inject(TYPES.S3_BACKUP_BUCKET_NAME) private s3BackupBucketName: string,
     @inject(TYPES.Logger) private logger: Logger,
   ) {}
 
@@ -64,14 +67,24 @@ export class EmailArchiveExtensionSyncedEventHandler implements DomainEventHandl
       this.logger.debug(`Data backed up into: ${backupFileName}`)
 
       if (backupFileName.length !== 0) {
-        this.logger.debug('Publishing EMAIL_BACKUP_ATTACHMENT_CREATED event')
+        const dateOnly = new Date().toISOString().substring(0, 10)
 
         await this.domainEventPublisher.publish(
-          this.domainEventFactory.createEmailBackupAttachmentCreatedEvent({
-            backupFileName,
-            backupFileIndex: bundleIndex++,
-            backupFilesTotal: itemUuidBundles.length,
-            email: authParams.identifier as string,
+          this.domainEventFactory.createEmailRequestedEvent({
+            body: getBody(authParams.identifier as string),
+            level: EmailLevel.LEVELS.System,
+            messageIdentifier: 'DATA_BACKUP',
+            subject: getSubject(bundleIndex++, itemUuidBundles.length, dateOnly),
+            userEmail: authParams.identifier as string,
+            sender: 'backups@standardnotes.org',
+            attachments: [
+              {
+                fileName: backupFileName,
+                filePath: this.s3BackupBucketName,
+                attachmentFileName: `SN-Data-${dateOnly}.txt`,
+                attachmentContentType: 'application/json',
+              },
+            ],
           }),
         )
       }

+ 7 - 24
packages/syncing-server/src/Domain/Handler/EmailBackupRequestedEventHandler.spec.ts

@@ -3,7 +3,7 @@ import 'reflect-metadata'
 import {
   DomainEventPublisherInterface,
   EmailBackupRequestedEvent,
-  EmailBackupAttachmentCreatedEvent,
+  EmailRequestedEvent,
 } from '@standardnotes/domain-events'
 import { Logger } from 'winston'
 import { AuthHttpServiceInterface } from '../Auth/AuthHttpServiceInterface'
@@ -35,6 +35,7 @@ describe('EmailBackupRequestedEventHandler', () => {
       domainEventFactory,
       emailAttachmentMaxByteSize,
       itemTransferCalculator,
+      's3-backup-bucket-name',
       logger,
     )
 
@@ -62,9 +63,7 @@ describe('EmailBackupRequestedEventHandler', () => {
     domainEventPublisher.publish = jest.fn()
 
     domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
-    domainEventFactory.createEmailBackupAttachmentCreatedEvent = jest
-      .fn()
-      .mockReturnValue({} as jest.Mocked<EmailBackupAttachmentCreatedEvent>)
+    domainEventFactory.createEmailRequestedEvent = jest.fn().mockReturnValue({} as jest.Mocked<EmailRequestedEvent>)
 
     itemTransferCalculator = {} as jest.Mocked<ItemTransferCalculatorInterface>
     itemTransferCalculator.computeItemUuidBundlesToFetch = jest.fn().mockReturnValue([['1-2-3']])
@@ -79,12 +78,7 @@ describe('EmailBackupRequestedEventHandler', () => {
     await createHandler().handle(event)
 
     expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
-    expect(domainEventFactory.createEmailBackupAttachmentCreatedEvent).toHaveBeenCalledWith({
-      backupFileIndex: 1,
-      backupFileName: 'backup-file-name',
-      backupFilesTotal: 1,
-      email: 'test@test.com',
-    })
+    expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
   })
 
   it('should inform that multipart backup attachment for email was created', async () => {
@@ -97,18 +91,7 @@ describe('EmailBackupRequestedEventHandler', () => {
     await createHandler().handle(event)
 
     expect(domainEventPublisher.publish).toHaveBeenCalledTimes(2)
-    expect(domainEventFactory.createEmailBackupAttachmentCreatedEvent).toHaveBeenNthCalledWith(1, {
-      backupFileIndex: 1,
-      backupFileName: 'backup-file-name-1',
-      backupFilesTotal: 2,
-      email: 'test@test.com',
-    })
-    expect(domainEventFactory.createEmailBackupAttachmentCreatedEvent).toHaveBeenNthCalledWith(2, {
-      backupFileIndex: 2,
-      backupFileName: 'backup-file-name-2',
-      backupFilesTotal: 2,
-      email: 'test@test.com',
-    })
+    expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalledTimes(2)
   })
 
   it('should not inform that backup attachment for email was created if user key params cannot be obtained', async () => {
@@ -119,7 +102,7 @@ describe('EmailBackupRequestedEventHandler', () => {
     await createHandler().handle(event)
 
     expect(domainEventPublisher.publish).not.toHaveBeenCalled()
-    expect(domainEventFactory.createEmailBackupAttachmentCreatedEvent).not.toHaveBeenCalled()
+    expect(domainEventFactory.createEmailRequestedEvent).not.toHaveBeenCalled()
   })
 
   it('should not inform that backup attachment for email was created if backup file name is empty', async () => {
@@ -128,6 +111,6 @@ describe('EmailBackupRequestedEventHandler', () => {
     await createHandler().handle(event)
 
     expect(domainEventPublisher.publish).not.toHaveBeenCalled()
-    expect(domainEventFactory.createEmailBackupAttachmentCreatedEvent).not.toHaveBeenCalled()
+    expect(domainEventFactory.createEmailRequestedEvent).not.toHaveBeenCalled()
   })
 })

+ 19 - 7
packages/syncing-server/src/Domain/Handler/EmailBackupRequestedEventHandler.ts

@@ -4,6 +4,7 @@ import {
   DomainEventPublisherInterface,
   EmailBackupRequestedEvent,
 } from '@standardnotes/domain-events'
+import { EmailLevel } from '@standardnotes/domain-core'
 import { inject, injectable } from 'inversify'
 import { Logger } from 'winston'
 import TYPES from '../../Bootstrap/Types'
@@ -13,6 +14,7 @@ import { ItemBackupServiceInterface } from '../Item/ItemBackupServiceInterface'
 import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
 import { ItemTransferCalculatorInterface } from '../Item/ItemTransferCalculatorInterface'
 import { ItemQuery } from '../Item/ItemQuery'
+import { getBody, getSubject } from '../Email/EmailBackupAttachmentCreated'
 
 @injectable()
 export class EmailBackupRequestedEventHandler implements DomainEventHandlerInterface {
@@ -24,6 +26,7 @@ export class EmailBackupRequestedEventHandler implements DomainEventHandlerInter
     @inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
     @inject(TYPES.EMAIL_ATTACHMENT_MAX_BYTE_SIZE) private emailAttachmentMaxByteSize: number,
     @inject(TYPES.ItemTransferCalculator) private itemTransferCalculator: ItemTransferCalculatorInterface,
+    @inject(TYPES.S3_BACKUP_BUCKET_NAME) private s3BackupBucketName: string,
     @inject(TYPES.Logger) private logger: Logger,
   ) {}
 
@@ -68,15 +71,24 @@ export class EmailBackupRequestedEventHandler implements DomainEventHandlerInter
 
         return
       }
-
-      this.logger.debug('Publishing EMAIL_BACKUP_ATTACHMENT_CREATED event')
+      const dateOnly = new Date().toISOString().substring(0, 10)
 
       await this.domainEventPublisher.publish(
-        this.domainEventFactory.createEmailBackupAttachmentCreatedEvent({
-          backupFileName,
-          backupFileIndex: bundleIndex++,
-          backupFilesTotal: itemUuidBundles.length,
-          email: authParams.identifier as string,
+        this.domainEventFactory.createEmailRequestedEvent({
+          body: getBody(authParams.identifier as string),
+          level: EmailLevel.LEVELS.System,
+          messageIdentifier: 'DATA_BACKUP',
+          subject: getSubject(bundleIndex++, itemUuidBundles.length, dateOnly),
+          userEmail: authParams.identifier as string,
+          sender: 'backups@standardnotes.org',
+          attachments: [
+            {
+              fileName: backupFileName,
+              filePath: this.s3BackupBucketName,
+              attachmentFileName: `SN-Data-${dateOnly}.txt`,
+              attachmentContentType: 'application/json',
+            },
+          ],
         }),
       )
     }