From 56a4aa9ffe61eb5bf83fe511067e789123d9464d Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 21 Apr 2025 12:53:37 -0400 Subject: [PATCH] refactor: email repository (#17746) --- .../notification-admin.controller.ts | 2 +- server/src/emails/album-invite.email.tsx | 2 +- server/src/emails/album-update.email.tsx | 2 +- server/src/emails/test.email.tsx | 2 +- server/src/emails/welcome.email.tsx | 2 +- ...itory.spec.ts => email.repository.spec.ts} | 8 +- ...tion.repository.ts => email.repository.ts} | 4 +- server/src/repositories/index.ts | 4 +- server/src/services/base.service.ts | 4 +- .../src/services/notification.service.spec.ts | 88 +++++++++---------- server/src/services/notification.service.ts | 24 ++--- server/test/medium.factory.ts | 2 +- server/test/utils.ts | 8 +- 13 files changed, 74 insertions(+), 78 deletions(-) rename server/src/repositories/{notification.repository.spec.ts => email.repository.spec.ts} (87%) rename server/src/repositories/{notification.repository.ts => email.repository.ts} (97%) diff --git a/server/src/controllers/notification-admin.controller.ts b/server/src/controllers/notification-admin.controller.ts index f3ce4cdac94..937244fc564 100644 --- a/server/src/controllers/notification-admin.controller.ts +++ b/server/src/controllers/notification-admin.controller.ts @@ -4,7 +4,7 @@ import { AuthDto } from 'src/dtos/auth.dto'; import { TemplateDto, TemplateResponseDto, TestEmailResponseDto } from 'src/dtos/notification.dto'; import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; -import { EmailTemplate } from 'src/repositories/notification.repository'; +import { EmailTemplate } from 'src/repositories/email.repository'; import { NotificationService } from 'src/services/notification.service'; @ApiTags('Notifications (Admin)') diff --git a/server/src/emails/album-invite.email.tsx b/server/src/emails/album-invite.email.tsx index 4bd7abc3057..fdc189af972 100644 --- a/server/src/emails/album-invite.email.tsx +++ b/server/src/emails/album-invite.email.tsx @@ -2,7 +2,7 @@ import { Img, Link, Section, Text } from '@react-email/components'; import * as React from 'react'; import { ImmichButton } from 'src/emails/components/button.component'; import ImmichLayout from 'src/emails/components/immich.layout'; -import { AlbumInviteEmailProps } from 'src/repositories/notification.repository'; +import { AlbumInviteEmailProps } from 'src/repositories/email.repository'; import { replaceTemplateTags } from 'src/utils/replace-template-tags'; export const AlbumInviteEmail = ({ diff --git a/server/src/emails/album-update.email.tsx b/server/src/emails/album-update.email.tsx index 2311e896e1e..3bed3a5b367 100644 --- a/server/src/emails/album-update.email.tsx +++ b/server/src/emails/album-update.email.tsx @@ -2,7 +2,7 @@ import { Img, Link, Section, Text } from '@react-email/components'; import * as React from 'react'; import { ImmichButton } from 'src/emails/components/button.component'; import ImmichLayout from 'src/emails/components/immich.layout'; -import { AlbumUpdateEmailProps } from 'src/repositories/notification.repository'; +import { AlbumUpdateEmailProps } from 'src/repositories/email.repository'; import { replaceTemplateTags } from 'src/utils/replace-template-tags'; export const AlbumUpdateEmail = ({ diff --git a/server/src/emails/test.email.tsx b/server/src/emails/test.email.tsx index ac9bdbe0eab..0d873070805 100644 --- a/server/src/emails/test.email.tsx +++ b/server/src/emails/test.email.tsx @@ -1,7 +1,7 @@ import { Link, Row, Text } from '@react-email/components'; import * as React from 'react'; import ImmichLayout from 'src/emails/components/immich.layout'; -import { TestEmailProps } from 'src/repositories/notification.repository'; +import { TestEmailProps } from 'src/repositories/email.repository'; export const TestEmail = ({ baseUrl, displayName }: TestEmailProps) => ( diff --git a/server/src/emails/welcome.email.tsx b/server/src/emails/welcome.email.tsx index 11a66027113..57e86ab2523 100644 --- a/server/src/emails/welcome.email.tsx +++ b/server/src/emails/welcome.email.tsx @@ -2,7 +2,7 @@ import { Link, Section, Text } from '@react-email/components'; import * as React from 'react'; import { ImmichButton } from 'src/emails/components/button.component'; import ImmichLayout from 'src/emails/components/immich.layout'; -import { WelcomeEmailProps } from 'src/repositories/notification.repository'; +import { WelcomeEmailProps } from 'src/repositories/email.repository'; import { replaceTemplateTags } from 'src/utils/replace-template-tags'; export const WelcomeEmail = ({ baseUrl, displayName, username, password, customTemplate }: WelcomeEmailProps) => { diff --git a/server/src/repositories/notification.repository.spec.ts b/server/src/repositories/email.repository.spec.ts similarity index 87% rename from server/src/repositories/notification.repository.spec.ts rename to server/src/repositories/email.repository.spec.ts index 1d0770af6b8..5640b26bf60 100644 --- a/server/src/repositories/notification.repository.spec.ts +++ b/server/src/repositories/email.repository.spec.ts @@ -1,13 +1,13 @@ +import { EmailRenderRequest, EmailRepository, EmailTemplate } from 'src/repositories/email.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; -import { EmailRenderRequest, EmailTemplate, NotificationRepository } from 'src/repositories/notification.repository'; import { automock } from 'test/utils'; -describe(NotificationRepository.name, () => { - let sut: NotificationRepository; +describe(EmailRepository.name, () => { + let sut: EmailRepository; beforeEach(() => { // eslint-disable-next-line no-sparse-arrays - sut = new NotificationRepository(automock(LoggingRepository, { args: [, { getEnv: () => ({}) }], strict: false })); + sut = new EmailRepository(automock(LoggingRepository, { args: [, { getEnv: () => ({}) }], strict: false })); }); describe('renderEmail', () => { diff --git a/server/src/repositories/notification.repository.ts b/server/src/repositories/email.repository.ts similarity index 97% rename from server/src/repositories/notification.repository.ts rename to server/src/repositories/email.repository.ts index 91f03b928bb..78c89b4a9d9 100644 --- a/server/src/repositories/notification.repository.ts +++ b/server/src/repositories/email.repository.ts @@ -98,9 +98,9 @@ export type SendEmailResponse = { }; @Injectable() -export class NotificationRepository { +export class EmailRepository { constructor(private logger: LoggingRepository) { - this.logger.setContext(NotificationRepository.name); + this.logger.setContext(EmailRepository.name); } verifySmtp(options: SmtpOptions): Promise { diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index ef36a2b3f86..bd2e5c6774c 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -11,6 +11,7 @@ import { CronRepository } from 'src/repositories/cron.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository'; import { DatabaseRepository } from 'src/repositories/database.repository'; import { DownloadRepository } from 'src/repositories/download.repository'; +import { EmailRepository } from 'src/repositories/email.repository'; import { EventRepository } from 'src/repositories/event.repository'; import { JobRepository } from 'src/repositories/job.repository'; import { LibraryRepository } from 'src/repositories/library.repository'; @@ -21,7 +22,6 @@ import { MediaRepository } from 'src/repositories/media.repository'; import { MemoryRepository } from 'src/repositories/memory.repository'; import { MetadataRepository } from 'src/repositories/metadata.repository'; import { MoveRepository } from 'src/repositories/move.repository'; -import { NotificationRepository } from 'src/repositories/notification.repository'; import { OAuthRepository } from 'src/repositories/oauth.repository'; import { PartnerRepository } from 'src/repositories/partner.repository'; import { PersonRepository } from 'src/repositories/person.repository'; @@ -65,7 +65,7 @@ export const repositories = [ MemoryRepository, MetadataRepository, MoveRepository, - NotificationRepository, + EmailRepository, OAuthRepository, PartnerRepository, PersonRepository, diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts index 2fbdd6e4c0e..23ddb1b63eb 100644 --- a/server/src/services/base.service.ts +++ b/server/src/services/base.service.ts @@ -18,6 +18,7 @@ import { CronRepository } from 'src/repositories/cron.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository'; import { DatabaseRepository } from 'src/repositories/database.repository'; import { DownloadRepository } from 'src/repositories/download.repository'; +import { EmailRepository } from 'src/repositories/email.repository'; import { EventRepository } from 'src/repositories/event.repository'; import { JobRepository } from 'src/repositories/job.repository'; import { LibraryRepository } from 'src/repositories/library.repository'; @@ -28,7 +29,6 @@ import { MediaRepository } from 'src/repositories/media.repository'; import { MemoryRepository } from 'src/repositories/memory.repository'; import { MetadataRepository } from 'src/repositories/metadata.repository'; import { MoveRepository } from 'src/repositories/move.repository'; -import { NotificationRepository } from 'src/repositories/notification.repository'; import { OAuthRepository } from 'src/repositories/oauth.repository'; import { PartnerRepository } from 'src/repositories/partner.repository'; import { PersonRepository } from 'src/repositories/person.repository'; @@ -70,6 +70,7 @@ export class BaseService { protected cryptoRepository: CryptoRepository, protected databaseRepository: DatabaseRepository, protected downloadRepository: DownloadRepository, + protected emailRepository: EmailRepository, protected eventRepository: EventRepository, protected jobRepository: JobRepository, protected libraryRepository: LibraryRepository, @@ -79,7 +80,6 @@ export class BaseService { protected memoryRepository: MemoryRepository, protected metadataRepository: MetadataRepository, protected moveRepository: MoveRepository, - protected notificationRepository: NotificationRepository, protected oauthRepository: OAuthRepository, protected partnerRepository: PartnerRepository, protected personRepository: PersonRepository, diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index 85e425b11f5..5830260753c 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -3,7 +3,7 @@ import { defaults, SystemConfig } from 'src/config'; import { AlbumUser } from 'src/database'; import { SystemConfigDto } from 'src/dtos/system-config.dto'; import { AssetFileType, JobName, JobStatus, UserMetadataKey } from 'src/enum'; -import { EmailTemplate } from 'src/repositories/notification.repository'; +import { EmailTemplate } from 'src/repositories/email.repository'; import { NotificationService } from 'src/services/notification.service'; import { INotifyAlbumUpdateJob } from 'src/types'; import { albumStub } from 'test/fixtures/album.stub'; @@ -74,18 +74,18 @@ describe(NotificationService.name, () => { const oldConfig = configs.smtpDisabled; const newConfig = configs.smtpEnabled; - mocks.notification.verifySmtp.mockResolvedValue(true); + mocks.email.verifySmtp.mockResolvedValue(true); await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow(); - expect(mocks.notification.verifySmtp).toHaveBeenCalledWith(newConfig.notifications.smtp.transport); + expect(mocks.email.verifySmtp).toHaveBeenCalledWith(newConfig.notifications.smtp.transport); }); it('validates smtp config when transport changes', async () => { const oldConfig = configs.smtpEnabled; const newConfig = configs.smtpTransport; - mocks.notification.verifySmtp.mockResolvedValue(true); + mocks.email.verifySmtp.mockResolvedValue(true); await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow(); - expect(mocks.notification.verifySmtp).toHaveBeenCalledWith(newConfig.notifications.smtp.transport); + expect(mocks.email.verifySmtp).toHaveBeenCalledWith(newConfig.notifications.smtp.transport); }); it('skips smtp validation when there are no changes', async () => { @@ -93,7 +93,7 @@ describe(NotificationService.name, () => { const newConfig = { ...configs.smtpEnabled }; await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow(); - expect(mocks.notification.verifySmtp).not.toHaveBeenCalled(); + expect(mocks.email.verifySmtp).not.toHaveBeenCalled(); }); it('skips smtp validation with DTO when there are no changes', async () => { @@ -101,7 +101,7 @@ describe(NotificationService.name, () => { const newConfig = plainToInstance(SystemConfigDto, configs.smtpEnabled); await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow(); - expect(mocks.notification.verifySmtp).not.toHaveBeenCalled(); + expect(mocks.email.verifySmtp).not.toHaveBeenCalled(); }); it('skips smtp validation when smtp is disabled', async () => { @@ -109,14 +109,14 @@ describe(NotificationService.name, () => { const newConfig = { ...configs.smtpDisabled }; await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow(); - expect(mocks.notification.verifySmtp).not.toHaveBeenCalled(); + expect(mocks.email.verifySmtp).not.toHaveBeenCalled(); }); it('should fail if smtp configuration is invalid', async () => { const oldConfig = configs.smtpDisabled; const newConfig = configs.smtpEnabled; - mocks.notification.verifySmtp.mockRejectedValue(new Error('Failed validating smtp')); + mocks.email.verifySmtp.mockRejectedValue(new Error('Failed validating smtp')); await expect(sut.onConfigValidate({ oldConfig, newConfig })).rejects.toBeInstanceOf(Error); }); }); @@ -248,7 +248,7 @@ describe(NotificationService.name, () => { it('should throw error if smtp validation fails', async () => { mocks.user.get.mockResolvedValue(userStub.admin); - mocks.notification.verifySmtp.mockRejectedValue(''); + mocks.email.verifySmtp.mockRejectedValue(''); await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).rejects.toThrow( 'Failed to verify SMTP configuration', @@ -257,16 +257,16 @@ describe(NotificationService.name, () => { it('should send email to default domain', async () => { mocks.user.get.mockResolvedValue(userStub.admin); - mocks.notification.verifySmtp.mockResolvedValue(true); - mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); - mocks.notification.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' }); + mocks.email.verifySmtp.mockResolvedValue(true); + mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' }); await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).resolves.not.toThrow(); - expect(mocks.notification.renderEmail).toHaveBeenCalledWith({ + expect(mocks.email.renderEmail).toHaveBeenCalledWith({ template: EmailTemplate.TEST_EMAIL, data: { baseUrl: 'https://my.immich.app', displayName: userStub.admin.name }, }); - expect(mocks.notification.sendEmail).toHaveBeenCalledWith( + expect(mocks.email.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ subject: 'Test email from Immich', smtp: configs.smtpTransport.notifications.smtp.transport, @@ -276,17 +276,17 @@ describe(NotificationService.name, () => { it('should send email to external domain', async () => { mocks.user.get.mockResolvedValue(userStub.admin); - mocks.notification.verifySmtp.mockResolvedValue(true); - mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.email.verifySmtp.mockResolvedValue(true); + mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.systemMetadata.get.mockResolvedValue({ server: { externalDomain: 'https://demo.immich.app' } }); - mocks.notification.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' }); + mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' }); await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).resolves.not.toThrow(); - expect(mocks.notification.renderEmail).toHaveBeenCalledWith({ + expect(mocks.email.renderEmail).toHaveBeenCalledWith({ template: EmailTemplate.TEST_EMAIL, data: { baseUrl: 'https://demo.immich.app', displayName: userStub.admin.name }, }); - expect(mocks.notification.sendEmail).toHaveBeenCalledWith( + expect(mocks.email.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ subject: 'Test email from Immich', smtp: configs.smtpTransport.notifications.smtp.transport, @@ -296,18 +296,18 @@ describe(NotificationService.name, () => { it('should send email with replyTo', async () => { mocks.user.get.mockResolvedValue(userStub.admin); - mocks.notification.verifySmtp.mockResolvedValue(true); - mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); - mocks.notification.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' }); + mocks.email.verifySmtp.mockResolvedValue(true); + mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' }); await expect( sut.sendTestEmail('', { ...configs.smtpTransport.notifications.smtp, replyTo: 'demo@immich.app' }), ).resolves.not.toThrow(); - expect(mocks.notification.renderEmail).toHaveBeenCalledWith({ + expect(mocks.email.renderEmail).toHaveBeenCalledWith({ template: EmailTemplate.TEST_EMAIL, data: { baseUrl: 'https://my.immich.app', displayName: userStub.admin.name }, }); - expect(mocks.notification.sendEmail).toHaveBeenCalledWith( + expect(mocks.email.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ subject: 'Test email from Immich', smtp: configs.smtpTransport.notifications.smtp.transport, @@ -325,7 +325,7 @@ describe(NotificationService.name, () => { it('should be successful', async () => { mocks.user.get.mockResolvedValue(userStub.admin); mocks.systemMetadata.get.mockResolvedValue({ server: {} }); - mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); await expect(sut.handleUserSignup({ id: '' })).resolves.toBe(JobStatus.SUCCESS); expect(mocks.job.queue).toHaveBeenCalledWith({ @@ -390,7 +390,7 @@ describe(NotificationService.name, () => { ], }); mocks.systemMetadata.get.mockResolvedValue({ server: {} }); - mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS); expect(mocks.job.queue).toHaveBeenCalledWith({ @@ -411,7 +411,7 @@ describe(NotificationService.name, () => { ], }); mocks.systemMetadata.get.mockResolvedValue({ server: {} }); - mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS); @@ -440,7 +440,7 @@ describe(NotificationService.name, () => { ], }); mocks.systemMetadata.get.mockResolvedValue({ server: {} }); - mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([ { id: '1', type: AssetFileType.THUMBNAIL, path: 'path-to-thumb.jpg' }, ]); @@ -471,7 +471,7 @@ describe(NotificationService.name, () => { ], }); mocks.systemMetadata.get.mockResolvedValue({ server: {} }); - mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([assetStub.image.files[2]]); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS); @@ -508,12 +508,12 @@ describe(NotificationService.name, () => { albumUsers: [{ user: { id: userStub.user1.id } } as AlbumUser], }); mocks.user.get.mockResolvedValueOnce(userStub.user1); - mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]); await sut.handleAlbumUpdate({ id: '', recipientIds: [userStub.user1.id] }); expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false }); - expect(mocks.notification.renderEmail).not.toHaveBeenCalled(); + expect(mocks.email.renderEmail).not.toHaveBeenCalled(); }); it('should skip recipient with disabled email notifications', async () => { @@ -530,12 +530,12 @@ describe(NotificationService.name, () => { }, ], }); - mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]); await sut.handleAlbumUpdate({ id: '', recipientIds: [userStub.user1.id] }); expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false }); - expect(mocks.notification.renderEmail).not.toHaveBeenCalled(); + expect(mocks.email.renderEmail).not.toHaveBeenCalled(); }); it('should skip recipient with disabled email notifications for the album update event', async () => { @@ -552,12 +552,12 @@ describe(NotificationService.name, () => { }, ], }); - mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]); await sut.handleAlbumUpdate({ id: '', recipientIds: [userStub.user1.id] }); expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false }); - expect(mocks.notification.renderEmail).not.toHaveBeenCalled(); + expect(mocks.email.renderEmail).not.toHaveBeenCalled(); }); it('should send email', async () => { @@ -566,12 +566,12 @@ describe(NotificationService.name, () => { albumUsers: [{ user: { id: userStub.user1.id } } as AlbumUser], }); mocks.user.get.mockResolvedValue(userStub.user1); - mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]); await sut.handleAlbumUpdate({ id: '', recipientIds: [userStub.user1.id] }); expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false }); - expect(mocks.notification.renderEmail).toHaveBeenCalled(); + expect(mocks.email.renderEmail).toHaveBeenCalled(); expect(mocks.job.queue).toHaveBeenCalled(); }); @@ -599,24 +599,20 @@ describe(NotificationService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ notifications: { smtp: { enabled: true, from: 'test@immich.app' } }, }); - mocks.notification.sendEmail.mockResolvedValue({ messageId: '', response: '' }); + mocks.email.sendEmail.mockResolvedValue({ messageId: '', response: '' }); await expect(sut.handleSendEmail({ html: '', subject: '', text: '', to: '' })).resolves.toBe(JobStatus.SUCCESS); - expect(mocks.notification.sendEmail).toHaveBeenCalledWith( - expect.objectContaining({ replyTo: 'test@immich.app' }), - ); + expect(mocks.email.sendEmail).toHaveBeenCalledWith(expect.objectContaining({ replyTo: 'test@immich.app' })); }); it('should send mail with replyTo successfully', async () => { mocks.systemMetadata.get.mockResolvedValue({ notifications: { smtp: { enabled: true, from: 'test@immich.app', replyTo: 'demo@immich.app' } }, }); - mocks.notification.sendEmail.mockResolvedValue({ messageId: '', response: '' }); + mocks.email.sendEmail.mockResolvedValue({ messageId: '', response: '' }); await expect(sut.handleSendEmail({ html: '', subject: '', text: '', to: '' })).resolves.toBe(JobStatus.SUCCESS); - expect(mocks.notification.sendEmail).toHaveBeenCalledWith( - expect.objectContaining({ replyTo: 'demo@immich.app' }), - ); + expect(mocks.email.sendEmail).toHaveBeenCalledWith(expect.objectContaining({ replyTo: 'demo@immich.app' })); }); }); }); diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index 2c4cc767568..2e456718ca4 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -2,8 +2,8 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { OnEvent, OnJob } from 'src/decorators'; import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; import { AssetFileType, JobName, JobStatus, QueueName } from 'src/enum'; +import { EmailTemplate } from 'src/repositories/email.repository'; import { ArgOf } from 'src/repositories/event.repository'; -import { EmailTemplate } from 'src/repositories/notification.repository'; import { BaseService } from 'src/services/base.service'; import { EmailImageAttachment, IEntityJob, INotifyAlbumUpdateJob, JobItem, JobOf } from 'src/types'; import { getFilenameExtension } from 'src/utils/file'; @@ -28,7 +28,7 @@ export class NotificationService extends BaseService { newConfig.notifications.smtp.enabled && !isEqualObject(oldConfig.notifications.smtp, newConfig.notifications.smtp) ) { - await this.notificationRepository.verifySmtp(newConfig.notifications.smtp.transport); + await this.emailRepository.verifySmtp(newConfig.notifications.smtp.transport); } } catch (error: Error | any) { this.logger.error(`Failed to validate SMTP configuration: ${error}`, error?.stack); @@ -138,13 +138,13 @@ export class NotificationService extends BaseService { } try { - await this.notificationRepository.verifySmtp(dto.transport); + await this.emailRepository.verifySmtp(dto.transport); } catch (error) { throw new BadRequestException('Failed to verify SMTP configuration', { cause: error }); } const { server } = await this.getConfig({ withCache: false }); - const { html, text } = await this.notificationRepository.renderEmail({ + const { html, text } = await this.emailRepository.renderEmail({ template: EmailTemplate.TEST_EMAIL, data: { baseUrl: getExternalDomain(server), @@ -152,7 +152,7 @@ export class NotificationService extends BaseService { }, customTemplate: tempTemplate!, }); - const { messageId } = await this.notificationRepository.sendEmail({ + const { messageId } = await this.emailRepository.sendEmail({ to: user.email, subject: 'Test email from Immich', html, @@ -172,7 +172,7 @@ export class NotificationService extends BaseService { switch (name) { case EmailTemplate.WELCOME: { - const { html: _welcomeHtml } = await this.notificationRepository.renderEmail({ + const { html: _welcomeHtml } = await this.emailRepository.renderEmail({ template: EmailTemplate.WELCOME, data: { baseUrl: getExternalDomain(server), @@ -187,7 +187,7 @@ export class NotificationService extends BaseService { break; } case EmailTemplate.ALBUM_UPDATE: { - const { html: _updateAlbumHtml } = await this.notificationRepository.renderEmail({ + const { html: _updateAlbumHtml } = await this.emailRepository.renderEmail({ template: EmailTemplate.ALBUM_UPDATE, data: { baseUrl: getExternalDomain(server), @@ -203,7 +203,7 @@ export class NotificationService extends BaseService { } case EmailTemplate.ALBUM_INVITE: { - const { html } = await this.notificationRepository.renderEmail({ + const { html } = await this.emailRepository.renderEmail({ template: EmailTemplate.ALBUM_INVITE, data: { baseUrl: getExternalDomain(server), @@ -235,7 +235,7 @@ export class NotificationService extends BaseService { } const { server, templates } = await this.getConfig({ withCache: true }); - const { html, text } = await this.notificationRepository.renderEmail({ + const { html, text } = await this.emailRepository.renderEmail({ template: EmailTemplate.WELCOME, data: { baseUrl: getExternalDomain(server), @@ -280,7 +280,7 @@ export class NotificationService extends BaseService { const attachment = await this.getAlbumThumbnailAttachment(album); const { server, templates } = await this.getConfig({ withCache: false }); - const { html, text } = await this.notificationRepository.renderEmail({ + const { html, text } = await this.emailRepository.renderEmail({ template: EmailTemplate.ALBUM_INVITE, data: { baseUrl: getExternalDomain(server), @@ -339,7 +339,7 @@ export class NotificationService extends BaseService { continue; } - const { html, text } = await this.notificationRepository.renderEmail({ + const { html, text } = await this.emailRepository.renderEmail({ template: EmailTemplate.ALBUM_UPDATE, data: { baseUrl: getExternalDomain(server), @@ -374,7 +374,7 @@ export class NotificationService extends BaseService { } const { to, subject, html, text: plain } = data; - const response = await this.notificationRepository.sendEmail({ + const response = await this.emailRepository.sendEmail({ to, subject, html, diff --git a/server/test/medium.factory.ts b/server/test/medium.factory.ts index d3ab876e07c..671a8a50cab 100644 --- a/server/test/medium.factory.ts +++ b/server/test/medium.factory.ts @@ -284,6 +284,7 @@ export const asDeps = (repositories: ServiceOverrides) => { repositories.crypto || getRepositoryMock('crypto'), repositories.database || getRepositoryMock('database'), repositories.downloadRepository, + repositories.email, repositories.event, repositories.job || getRepositoryMock('job'), repositories.library, @@ -293,7 +294,6 @@ export const asDeps = (repositories: ServiceOverrides) => { repositories.memory || getRepositoryMock('memory'), repositories.metadata, repositories.move, - repositories.notification, repositories.oauth, repositories.partner || getRepositoryMock('partner'), repositories.person || getRepositoryMock('person'), diff --git a/server/test/utils.ts b/server/test/utils.ts index 52984d97a28..e1d979fbfe1 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -18,6 +18,7 @@ import { CronRepository } from 'src/repositories/cron.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository'; import { DatabaseRepository } from 'src/repositories/database.repository'; import { DownloadRepository } from 'src/repositories/download.repository'; +import { EmailRepository } from 'src/repositories/email.repository'; import { EventRepository } from 'src/repositories/event.repository'; import { JobRepository } from 'src/repositories/job.repository'; import { LibraryRepository } from 'src/repositories/library.repository'; @@ -28,7 +29,6 @@ import { MediaRepository } from 'src/repositories/media.repository'; import { MemoryRepository } from 'src/repositories/memory.repository'; import { MetadataRepository } from 'src/repositories/metadata.repository'; import { MoveRepository } from 'src/repositories/move.repository'; -import { NotificationRepository } from 'src/repositories/notification.repository'; import { OAuthRepository } from 'src/repositories/oauth.repository'; import { PartnerRepository } from 'src/repositories/partner.repository'; import { PersonRepository } from 'src/repositories/person.repository'; @@ -124,6 +124,7 @@ export type ServiceOverrides = { crypto: CryptoRepository; database: DatabaseRepository; downloadRepository: DownloadRepository; + email: EmailRepository; event: EventRepository; job: JobRepository; library: LibraryRepository; @@ -134,7 +135,6 @@ export type ServiceOverrides = { memory: MemoryRepository; metadata: MetadataRepository; move: MoveRepository; - notification: NotificationRepository; oauth: OAuthRepository; partner: PartnerRepository; person: PersonRepository; @@ -190,6 +190,7 @@ export const newTestService = ( config: newConfigRepositoryMock(), database: newDatabaseRepositoryMock(), downloadRepository: automock(DownloadRepository, { strict: false }), + email: automock(EmailRepository, { args: [loggerMock] }), // eslint-disable-next-line no-sparse-arrays event: automock(EventRepository, { args: [, , loggerMock], strict: false }), job: newJobRepositoryMock(), @@ -201,7 +202,6 @@ export const newTestService = ( memory: automock(MemoryRepository), metadata: newMetadataRepositoryMock(), move: automock(MoveRepository, { strict: false }), - notification: automock(NotificationRepository, { args: [loggerMock] }), oauth: automock(OAuthRepository, { args: [loggerMock] }), partner: automock(PartnerRepository, { strict: false }), person: newPersonRepositoryMock(), @@ -240,6 +240,7 @@ export const newTestService = ( overrides.crypto || (mocks.crypto as As), overrides.database || (mocks.database as As), overrides.downloadRepository || (mocks.downloadRepository as As), + overrides.email || (mocks.email as As), overrides.event || (mocks.event as As), overrides.job || (mocks.job as As), overrides.library || (mocks.library as As), @@ -249,7 +250,6 @@ export const newTestService = ( overrides.memory || (mocks.memory as As), overrides.metadata || (mocks.metadata as As), overrides.move || (mocks.move as As), - overrides.notification || (mocks.notification as As), overrides.oauth || (mocks.oauth as As), overrides.partner || (mocks.partner as As), overrides.person || (mocks.person as As),