diff --git a/package.json b/package.json index 9906e9b7d7..4c84dd2b0c 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.440.0", "@aws-sdk/s3-request-presigner": "^3.440.0", + "@faire/mjml-react": "^3.5.0", "@faker-js/faker": "^9.7.0", "@fastify/compress": "^8.0.1", "@fastify/cookie": "^11.0.1", @@ -51,7 +52,7 @@ "@seedcompany/common": ">=0.19.1 <1", "@seedcompany/data-loader": "^2.0.1", "@seedcompany/nest": "^1.8.0", - "@seedcompany/nestjs-email": "^4.3.0", + "@seedcompany/nestjs-email": "^5.0.0-alpha.5", "@seedcompany/scripture": ">=0.8.0", "argon2": "^0.43.0", "aws-xray-sdk-core": "^3.5.3", @@ -91,13 +92,14 @@ "lru-cache": "^11.0.1", "luxon": "^3.5.0", "mime": "^4.0.7", + "mjml": "^4.15.3", "nanoid": "^5.1.5", "neo4j-driver": "^5.28.1", "p-retry": "^6.2.1", "plur": "^5.1.0", "prismjs-terminal": "^1.2.3", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", "read-package-up": "^11.0.0", "reflect-metadata": "^0.2.2", "rimraf": "^6.0.1", @@ -129,7 +131,7 @@ "@types/luxon": "^3.6.2", "@types/node": "^22.7.8", "@types/prismjs": "^1.26.2", - "@types/react": "^18.2.33", + "@types/react": "^19.1.8", "@types/stack-trace": "^0.0.33", "@types/triple-beam": "^1.3.4", "@types/validator": "^13.11.5", diff --git a/src/components/dbl-upload-notification/templates/dbl-upload-email.template.tsx b/src/components/dbl-upload-notification/emails/dbl-upload.email.tsx similarity index 55% rename from src/components/dbl-upload-notification/templates/dbl-upload-email.template.tsx rename to src/components/dbl-upload-notification/emails/dbl-upload.email.tsx index 0a987eb12c..192fc6c537 100644 --- a/src/components/dbl-upload-notification/templates/dbl-upload-email.template.tsx +++ b/src/components/dbl-upload-notification/emails/dbl-upload.email.tsx @@ -1,169 +1,176 @@ import { type NonEmptyArray } from '@seedcompany/common'; -import { - Column, - Text as Head, - Section, - Text, -} from '@seedcompany/nestjs-email/templates'; import type { Verse } from '@seedcompany/scripture'; import type { Range } from '~/common'; -import { EmailTemplate } from '~/core/email/templates/base'; -import { useFrontendUrl } from '~/core/email/templates/frontend-url'; -import { LanguageRef } from '~/core/email/templates/user-ref'; -import { type Engagement } from '../../../components/engagement/dto'; -import { type Language } from '../../../components/language/dto'; -import { type Project } from '../../../components/project/dto'; -import { type User } from '../../../components/user/dto'; +import { + EmailTemplate, + Headers, + LanguageRef, + Mjml, + useConfig, + useFrontendUrl, + useResources, +} from '~/core/email'; +import { type LanguageEngagement } from '../../engagement/dto'; interface Props { - recipient: User; - project: Pick; - engagement: Pick; - language: Pick; + engagement: LanguageEngagement; completedBooks: NonEmptyArray>; - dblFormUrl: string; } -export function DBLUpload(props: Props) { - const { language, project, completedBooks, engagement, dblFormUrl } = props; +export async function DBLUpload(props: Props) { + const { engagement, completedBooks } = props; + + const resources = useResources(); + const [language, project] = await Promise.all([ + resources.load('Language', props.engagement.language.value!.id), + resources.load('Project', props.engagement.project.id), + ]); + + const config = useConfig().email.notifyDblUpload!; + const languageName = language.name.value; return ( -
- - + {config.replyTo && } + + + has recently indicated reaching some All Access goals via{' '} {project.name.value ?? 'Some Project'} . - - + + Books:{' '} {completedBooks.map((range) => range.start.book.name).join(', ')} - - -
-
- - + + + + + + Our records identify you as the Field Project Manager (FPM), and we’d like to confirm the next steps for uploading the text to the Digital Bible Library (DBL). - - + + To move forward, we need a few details from you. Please have your field partner complete this short form to provide the necessary information indicated below: - - -
-
- - ✅ First Step: Who will upload the Scripture to the DBL? - + + + + + + + ✅ First Step: Who will upload the Scripture to the DBL? + + 1. If someone is already responsible for uploading to the DBL, please let us know on the form so we can update our records and avoid unnecessary follow-ups. - - + + 2. If you need Seed Company to upload it to the DBL, we will need additional information. - - -
-
- - ✅ If Seed Company uploads to the DBL, please provide: - + + + + + + + ✅ If Seed Company uploads to the DBL, please provide: + + 🔹 Copyright Holder & Licensing – Who will hold the copyright for this text in DBL? - - + + The copyright holder can be the field partner or Seed Company if needed. - - + + We also need to confirm the licensing options you prefer for distribution. More details on these options are included in the attached information sheet. - - -
-
- - + + + + + + 🔹 Error-Free Text in Paratext – The text must pass Basic Checks in Paratext without errors. - - + + A quick way to verify is by printing the text to PDF format using PTXPrint ( learn here). Besides, our Investors love to see your progress and this is a great way to share it with them! 😊 - - + + If errors appear, they must be fixed before we can proceed. - - -
-
- - + + + + + + 🔹 Paratext Project Access – We need access to the project in Paratext. - - + + Please add SC DBL Admin to the project with the Consultant/Archivist role. - - + + This permission level is required for us to complete the upload. - - -
-
- - + + + + + + 🔹 Books Ready for Upload – Please confirm which books are ready for DBL. - - + + We can upload an entire testament or individual books that have completed consultant checking. - - -
-
- - + + + + + + 🔗{' '} - + Seed Company DBL Publication Request Form - - + + All of this information can be entered in the form linked above (with yellow highlight). - - -
-
- - + + + + + + 🔗{' '} Seed Company DBL Information Sheet - - + + Please review the linked information sheet for additional details about the DBL, the process, and licensing options. - - -
+ + +
); } diff --git a/src/components/dbl-upload-notification/handlers/dbl-upload-notification.handler.ts b/src/components/dbl-upload-notification/handlers/dbl-upload-notification.handler.tsx similarity index 73% rename from src/components/dbl-upload-notification/handlers/dbl-upload-notification.handler.ts rename to src/components/dbl-upload-notification/handlers/dbl-upload-notification.handler.tsx index 21402c9917..028fb6e1ab 100644 --- a/src/components/dbl-upload-notification/handlers/dbl-upload-notification.handler.ts +++ b/src/components/dbl-upload-notification/handlers/dbl-upload-notification.handler.tsx @@ -1,18 +1,17 @@ import { ModuleRef } from '@nestjs/core'; import { asNonEmptyArray, + asyncPool, groupBy, type NonEmptyArray, setOf, } from '@seedcompany/common'; -import { EmailService } from '@seedcompany/nestjs-email'; import { Book, mergeVerseRanges, splitRangeByBook, type Verse, } from '@seedcompany/scripture'; -import { type ComponentProps as PropsOf } from 'react'; import { type ID, type Range } from '~/common'; import { ConfigService, @@ -23,6 +22,7 @@ import { ResourceLoader, } from '~/core'; import { Identity } from '~/core/authentication'; +import { MailerService } from '~/core/email'; import { type ProgressReport, ProgressReportStatus as Status, @@ -38,7 +38,7 @@ import { resolveProductType, } from '../../product/dto'; import { ProjectMemberRepository } from '../../project/project-member/project-member.repository'; -import { DBLUpload } from '../templates/dbl-upload-email.template'; +import { DBLUpload } from '../emails/dbl-upload.email'; @EventsHandler(WorkflowUpdatedEvent) export class DBLUploadNotificationHandler @@ -49,7 +49,7 @@ export class DBLUploadNotificationHandler private readonly moduleRef: ModuleRef, private readonly resources: ResourceLoader, private readonly config: ConfigService, - private readonly mailer: EmailService, + private readonly mailer: MailerService, @Logger('progress-report:dbl-upload-notifier') private readonly logger: ILogger, ) {} @@ -96,42 +96,23 @@ export class DBLUploadNotificationHandler .get(ProjectMemberRepository, { strict: false }) .listAsNotifiers(engagement.project.id, ['ProjectManager']); - const notifyeesProps = await Promise.all( - notifyees - .filter((n) => n.email) - .map(({ id: userId }) => - this.gatherTemplateProps( - userId, - engagement.id, - engagement.language.value!.id, - engagement.project.id, - completedBooks, - ), - ), - ); - this.logger.info('Notifying', { - engagement: notifyeesProps[0]?.engagement.id ?? undefined, - reportId: report.id, - reportDate: report.start, + language: engagement.language.value!.id, books: completedBooks.map((r) => r.start.book.name), - emails: notifyeesProps.map((r) => r.recipient.email.value), + emails: notifyees.flatMap((r) => r.email ?? []), }); - for (const props of notifyeesProps) { - // members without an email address are already omitted - const to = props.recipient.email.value!; - await this.mailer - .withOptions({ send: !!this.config.email.notifyDblUpload }) - .render(DBLUpload, props) - .with({ - to, - ...(this.config.email.notifyDblUpload?.replyTo && { - 'reply-to': this.config.email.notifyDblUpload.replyTo, - }), - }) - .send(); - } + await asyncPool(Infinity, notifyees, async ({ id: user, email }) => { + if (!email) { + return; + } + const msg = await this.identity.asUser(user, async () => + this.mailer + .withOptions({ send: !!this.config.email.notifyDblUpload }) + .compose(email, [DBLUpload, { engagement, completedBooks }]), + ); + await msg.send(); + }); } private async determineCompletedProducts(report: ProgressReport) { @@ -213,30 +194,4 @@ export class DBLUploadNotificationHandler }); return asNonEmptyArray(completedBooks); } - - private async gatherTemplateProps( - recipientId: ID<'User'>, - engagementId: ID, - languageId: ID, - projectId: ID, - completedBooks: NonEmptyArray>, - ) { - return await this.identity.asUser(recipientId, async () => { - const [recipient, language, engagement, project] = await Promise.all([ - this.resources.load('User', recipientId), - this.resources.load('Language', languageId), - this.resources.load('Engagement', engagementId), - this.resources.load('Project', projectId), - ]); - - return { - recipient, - language, - project, - engagement, - completedBooks, - dblFormUrl: this.config.email.notifyDblUpload?.formUrl ?? '', - } satisfies PropsOf; - }); - } } diff --git a/src/core/email/templates/progress-report-status-changed.template.tsx b/src/components/progress-report/workflow/emails/progress-report-status-changed.email.tsx similarity index 72% rename from src/core/email/templates/progress-report-status-changed.template.tsx rename to src/components/progress-report/workflow/emails/progress-report-status-changed.email.tsx index 414e673359..a3a31fa20d 100644 --- a/src/core/email/templates/progress-report-status-changed.template.tsx +++ b/src/components/progress-report/workflow/emails/progress-report-status-changed.email.tsx @@ -1,20 +1,19 @@ -import { - Button, - Column, - Section, - Text, -} from '@seedcompany/nestjs-email/templates'; import { fiscalQuarter, fiscalYear } from '~/common'; -import { type Language } from '../../../components/language/dto'; -import { type PeriodicReport } from '../../../components/periodic-report/dto'; -import { ProgressReportStatus } from '../../../components/progress-report/dto'; -import { type ProgressReportWorkflowEvent } from '../../../components/progress-report/workflow/dto/workflow-event.dto'; -import { type Project } from '../../../components/project/dto'; -import { type User } from '../../../components/user/dto'; -import { EmailTemplate, Heading } from './base'; -import { FormattedDateTime } from './formatted-date-time'; -import { useFrontendUrl } from './frontend-url'; -import { UserRef, type UserRefProps } from './user-ref'; +import { + EmailTemplate, + FormattedDateTime, + Heading, + Mjml, + useFrontendUrl, + UserRef, + type UserRefProps, +} from '~/core/email'; +import { type Language } from '../../../language/dto'; +import { type PeriodicReport } from '../../../periodic-report/dto'; +import { type Project } from '../../../project/dto'; +import { type User } from '../../../user/dto'; +import { ProgressReportStatus } from '../../dto'; +import { type ProgressReportWorkflowEvent } from '../dto/workflow-event.dto'; export interface ProgressReportStatusChangedProps { changedBy: UserRefProps; @@ -73,9 +72,9 @@ export function ProgressReportStatusChanged({ )} -
- - + + + has changed{' '} {reportLabel}{' '} {newStatus ? ( @@ -95,12 +94,12 @@ export function ProgressReportStatusChanged({ timezone={recipient.timezone.value} /> - - - -
+ + + ); } diff --git a/src/components/progress-report/workflow/handlers/progress-report-workflow-notification.handler.ts b/src/components/progress-report/workflow/handlers/progress-report-workflow-notification.handler.tsx similarity index 94% rename from src/components/progress-report/workflow/handlers/progress-report-workflow-notification.handler.ts rename to src/components/progress-report/workflow/handlers/progress-report-workflow-notification.handler.tsx index 24a0a0ce3e..d976b89104 100644 --- a/src/components/progress-report/workflow/handlers/progress-report-workflow-notification.handler.ts +++ b/src/components/progress-report/workflow/handlers/progress-report-workflow-notification.handler.tsx @@ -1,5 +1,4 @@ import { entries, mapEntries } from '@seedcompany/common'; -import { EmailService } from '@seedcompany/nestjs-email'; import { type RequireExactlyOne } from 'type-fest'; import { type ID, Role, type UnsecuredDto } from '~/common'; import { @@ -10,16 +9,17 @@ import { Logger, } from '~/core'; import { Identity } from '~/core/authentication'; -import { - type ProgressReportStatusChangedProps as EmailReportStatusNotification, - ProgressReportStatusChanged, -} from '~/core/email/templates/progress-report-status-changed.template'; +import { MailerService } from '~/core/email'; import { LanguageService } from '../../../language'; import { PeriodicReportService } from '../../../periodic-report'; import { ProjectService } from '../../../project'; import { UserService } from '../../../user'; import { type ProgressReportStatus as Status } from '../../dto'; import { type ProgressReportWorkflowEvent } from '../dto/workflow-event.dto'; +import { + type ProgressReportStatusChangedProps as EmailReportStatusNotification, + ProgressReportStatusChanged, +} from '../emails/progress-report-status-changed.email'; import { WorkflowUpdatedEvent } from '../events/workflow-updated.event'; import { ProgressReportWorkflowRepository } from '../progress-report-workflow.repository'; import { ProgressReportWorkflowService } from '../progress-report-workflow.service'; @@ -43,7 +43,7 @@ export class ProgressReportWorkflowNotificationHandler private readonly projectService: ProjectService, private readonly languageService: LanguageService, private readonly reportService: PeriodicReportService, - private readonly emailService: EmailService, + private readonly mailer: MailerService, private readonly workflowService: ProgressReportWorkflowService, @Logger('progress-report:status-change-notifier') private readonly logger: ILogger, @@ -95,11 +95,12 @@ export class ProgressReportWorkflowNotificationHandler for (const notification of notifications) { if (notification.recipient.email.value) { - await this.emailService.send( - notification.recipient.email.value, - ProgressReportStatusChanged, - notification, - ); + await this.mailer + .compose( + notification.recipient.email.value, + , + ) + .send(); } } } diff --git a/src/core/email/templates/project-step-changed.template.tsx b/src/components/project/workflow/emails/project-step-changed.email.tsx similarity index 77% rename from src/core/email/templates/project-step-changed.template.tsx rename to src/components/project/workflow/emails/project-step-changed.email.tsx index 91108fdd90..8e988bc991 100644 --- a/src/core/email/templates/project-step-changed.template.tsx +++ b/src/components/project/workflow/emails/project-step-changed.email.tsx @@ -1,21 +1,19 @@ import { cleanJoin } from '@seedcompany/common'; import { - Button, - Column, - HideInText, - Section, - Text, -} from '@seedcompany/nestjs-email/templates'; + EmailTemplate, + FormattedDateTime, + Heading, + InHtml, + Mjml, + useFrontendUrl, + UserRef, +} from '~/core/email'; +import { type User } from '../../../user/dto'; import { type Project, ProjectStep as Step, ProjectType as Type, -} from '../../../components/project/dto'; -import { type User } from '../../../components/user/dto'; -import { EmailTemplate, Heading } from './base'; -import { FormattedDateTime } from './formatted-date-time'; -import { useFrontendUrl } from './frontend-url'; -import { UserRef } from './user-ref'; +} from '../../dto'; export interface ProjectStepChangedProps { recipient: Pick< @@ -63,9 +61,9 @@ export function ProjectStepChanged({ )} -
- - + + + has changed{' '} {projectName ? 'project ' : ''} {projectName ?? 'a project'}{' '} @@ -84,14 +82,14 @@ export function ProjectStepChanged({ value={project.modifiedAt} timezone={recipient.timezone.value} /> - - - - - -
+ + + + ); } diff --git a/src/components/project/workflow/handlers/project-workflow-notification.handler.ts b/src/components/project/workflow/handlers/project-workflow-notification.handler.tsx similarity index 94% rename from src/components/project/workflow/handlers/project-workflow-notification.handler.ts rename to src/components/project/workflow/handlers/project-workflow-notification.handler.tsx index 3553cef8f2..a4e3982e9d 100644 --- a/src/components/project/workflow/handlers/project-workflow-notification.handler.ts +++ b/src/components/project/workflow/handlers/project-workflow-notification.handler.tsx @@ -1,6 +1,5 @@ import { ModuleRef } from '@nestjs/core'; import { asyncPool } from '@seedcompany/common'; -import { EmailService } from '@seedcompany/nestjs-email'; import { type UnsecuredDto } from '~/common'; import { ConfigService, @@ -10,15 +9,16 @@ import { Logger, } from '~/core'; import { Identity } from '~/core/authentication'; -import { - ProjectStepChanged, - type ProjectStepChangedProps, -} from '~/core/email/templates/project-step-changed.template'; +import { MailerService } from '~/core/email'; import { ProjectService } from '../../../project'; import { UserService } from '../../../user'; import { type User } from '../../../user/dto'; import { type Notifier } from '../../../workflow/transitions/notifiers'; import { type Project, type ProjectStep } from '../../dto'; +import { + ProjectStepChanged, + type ProjectStepChangedProps, +} from '../emails/project-step-changed.email'; import { ProjectTransitionedEvent } from '../events/project-transitioned.event'; @EventsHandler(ProjectTransitionedEvent) @@ -30,7 +30,7 @@ export class ProjectWorkflowNotificationHandler private readonly config: ConfigService, private readonly users: UserService, private readonly projects: ProjectService, - private readonly emailService: EmailService, + private readonly mailer: MailerService, private readonly moduleRef: ModuleRef, @Logger('progress-report:status-change-notifier') private readonly logger: ILogger, @@ -95,7 +95,9 @@ export class ProjectWorkflowNotificationHandler previousStep, primaryPartnerName, ); - await this.emailService.send(notifier.email, ProjectStepChanged, props); + await this.mailer + .compose(notifier.email, ) + .send(); }); } diff --git a/src/core/authentication/authentication.service.ts b/src/core/authentication/authentication.service.ts index 9334381a40..0f948bd127 100644 --- a/src/core/authentication/authentication.service.ts +++ b/src/core/authentication/authentication.service.ts @@ -1,6 +1,5 @@ import { Injectable } from '@nestjs/common'; import { ModuleRef } from '@nestjs/core'; -import { EmailService } from '@seedcompany/nestjs-email'; import { AuthenticationException, DuplicateException, @@ -9,12 +8,13 @@ import { ServerException, UnauthenticatedException, } from '~/common'; +import { MailerService } from '~/core/email'; import { ILogger, Logger } from '~/core/logger'; -import { ForgotPassword } from '../email/templates'; import { disableAccessPolicies, Gel } from '../gel'; import { AuthenticationRepository } from './authentication.repository'; import { CryptoService } from './crypto.service'; import type { LoginInput, RegisterInput, ResetPasswordInput } from './dto'; +import { ForgotPassword } from './emails/forgot-password.email'; import { JwtService } from './jwt.service'; import { SessionHost } from './session/session.host'; import { SessionManager } from './session/session.manager'; @@ -26,7 +26,7 @@ import { SessionManager } from './session/session.manager'; export class AuthenticationService { constructor( private readonly crypto: CryptoService, - private readonly email: EmailService, + private readonly mailer: MailerService, @Logger('authentication:service') private readonly logger: ILogger, private readonly repo: AuthenticationRepository, private readonly gel: Gel, @@ -135,9 +135,7 @@ export class AuthenticationService { const token = this.jwt.encode(); await this.repo.saveEmailToken(email, token); - await this.email.send(email, ForgotPassword, { - token, - }); + await this.mailer.compose(email, [ForgotPassword, { token }]).send(); } async resetPassword({ token, password }: ResetPasswordInput): Promise { diff --git a/src/core/email/templates/forgot-password.template.tsx b/src/core/authentication/emails/forgot-password.email.tsx similarity index 50% rename from src/core/email/templates/forgot-password.template.tsx rename to src/core/authentication/emails/forgot-password.email.tsx index b2ac1d4de5..e4ad126fd0 100644 --- a/src/core/email/templates/forgot-password.template.tsx +++ b/src/core/authentication/emails/forgot-password.email.tsx @@ -1,13 +1,13 @@ import { - Button, - Column, - HideInText, + EmailTemplate, + Heading, + InHtml, InText, - Section, - Text, -} from '@seedcompany/nestjs-email/templates'; -import { EmailTemplate, Heading, Link, ReplyInfoFooter } from './base'; -import { useFrontendUrl } from './frontend-url'; + Link, + Mjml, + ReplyInfoFooter, + useFrontendUrl, +} from '../../email'; export interface ForgotPasswordProps { token: string; @@ -19,17 +19,19 @@ export function ForgotPassword({ token }: ForgotPasswordProps) { We received your password reset request -
- - If it was you, create a new password here - - - + + + + If it was you, create a new password here + + + CONFIRM + - -
+ +
diff --git a/src/core/config/config.service.ts b/src/core/config/config.service.ts index f05087f2a9..0913b5f570 100644 --- a/src/core/config/config.service.ts +++ b/src/core/config/config.service.ts @@ -1,8 +1,5 @@ import { csv } from '@seedcompany/common'; -import type { - EmailModuleOptions, - EmailOptionsFactory, -} from '@seedcompany/nestjs-email'; +import type { EmailModuleOptions as EmailOptions } from '@seedcompany/nestjs-email'; import type { Server as HttpServer } from 'http'; import { type LRUCache } from 'lru-cache'; import { DateTime, Duration, type DurationLike } from 'luxon'; @@ -14,8 +11,6 @@ import { type ID } from '~/common'; import { parseUri } from '../../components/file/bucket/parse-uri'; import { ProgressReportStatus } from '../../components/progress-report/dto/progress-report-status.enum'; import { TransitionName as ProgressReportTransitionName } from '../../components/progress-report/workflow/transitions'; -import { DefaultTimezoneWrapper } from '../email/templates/formatted-date-time'; -import { FrontendUrlWrapper } from '../email/templates/frontend-url'; import type { CookieOptions, CorsOptions, IRequest } from '../http'; import { LogLevel } from '../logger/logger.interface'; import { type EnvironmentService } from './environment.service'; @@ -30,7 +25,7 @@ type HttpTimeoutOptions = AppConfig['httpTimeouts']; const isDev = process.env.NODE_ENV === 'development'; export const makeConfig = (env: EnvironmentService) => - class ConfigService implements EmailOptionsFactory { + class ConfigService { port = env.number('port').optional(3000); // The port where the app is being hosted. i.e. a docker bound port publicPort = env.number('public_port').optional(this.port); @@ -95,13 +90,17 @@ export const makeConfig = (env: EnvironmentService) => jwtKey = env.string('JWT_AUTH_KEY').optional('cord-field'); - createEmailOptions = () => { + emailDriver = (() => { const send = env.boolean('EMAIL_SEND').optional(false); + const from = env + .string('EMAIL_FROM') + .optional('CORD Field '); + const replyTo = env.string('EMAIL_REPLY_TO').optional() || undefined; // falsy -> undefined return { - from: env - .string('EMAIL_FROM') - .optional('CORD Field '), - replyTo: env.string('EMAIL_REPLY_TO').optional() || undefined, // falsy -> undefined + defaultHeaders: { + from, + ...(replyTo && { replyTo }), + }, send, open: this.jest ? false @@ -109,12 +108,8 @@ export const makeConfig = (env: EnvironmentService) => ses: { region: env.string('SES_REGION').optional(), }, - wrappers: [ - FrontendUrlWrapper(this.frontendUrl), - DefaultTimezoneWrapper(this.defaultTimeZone), - ], - } satisfies EmailModuleOptions; - }; + } satisfies EmailOptions; + })(); email = { notifyDistributionLists: env @@ -134,7 +129,7 @@ export const makeConfig = (env: EnvironmentService) => progressReportStatusChange = { enabled: env .boolean('NOTIFY_PROGRESS_REPORT_STATUS_CHANGES') - .optional(this.createEmailOptions().send), + .optional(this.emailDriver.send), notifyExtraEmails: { forTransitions: env .map('PROGRESS_REPORT_EMAILS_FOR_TRANSITIONS', { diff --git a/src/core/core.module.ts b/src/core/core.module.ts index 6349cc86ac..ea95e16c50 100644 --- a/src/core/core.module.ts +++ b/src/core/core.module.ts @@ -7,10 +7,10 @@ import { AwsS3Factory } from './aws-s3.factory'; import { CacheModule } from './cache/cache.module'; import { CliModule } from './cli/cli.module'; import { ConfigModule } from './config/config.module'; -import { ConfigService } from './config/config.service'; import { CoreController } from './core.controller'; import { DataLoaderConfig } from './data-loader/data-loader.config'; import { DatabaseModule } from './database/database.module'; +import { EmailConfig } from './email/email.config'; import { EventsModule } from './events'; import { ExceptionFilter } from './exception/exception.filter'; import { ExceptionNormalizer } from './exception/exception.normalizer'; @@ -35,7 +35,7 @@ import { WaitResolver } from './wait.resolver'; DatabaseModule, DataLoaderModule.registerAsync({ useClass: DataLoaderConfig }), GelModule, - EmailModule.forRootAsync({ useExisting: ConfigService }), + EmailModule.registerAsync({ useClass: EmailConfig }), GraphqlModule, EventsModule, TracingModule, diff --git a/src/core/email/email.config.ts b/src/core/email/email.config.ts new file mode 100644 index 0000000000..1c91bacc8b --- /dev/null +++ b/src/core/email/email.config.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@nestjs/common'; +import { type EmailModuleOptions } from '@seedcompany/nestjs-email'; +import { ConfigService } from '~/core/config/config.service'; + +@Injectable() +export class EmailConfig { + constructor(private readonly config: ConfigService) {} + + create(): EmailModuleOptions { + const options = this.config.emailDriver; + return options; + } +} diff --git a/src/core/email/index.ts b/src/core/email/index.ts new file mode 100644 index 0000000000..218bd381d1 --- /dev/null +++ b/src/core/email/index.ts @@ -0,0 +1,9 @@ +export { MailerService, EmailMessage } from '@seedcompany/nestjs-email'; +export * from '@seedcompany/nestjs-email/templates'; +export * as Mjml from '@seedcompany/nestjs-email/templates/mjml'; +export * from './templates/base'; +export * from './templates/formatted-date-time'; +export * from './templates/frontend-url'; +export * from './templates/user-ref'; +export * from './templates/useConfig'; +export * from './templates/useResources'; diff --git a/src/core/email/templates/base.tsx b/src/core/email/templates/base.tsx index 73ae51f85d..9e28071b3c 100644 --- a/src/core/email/templates/base.tsx +++ b/src/core/email/templates/base.tsx @@ -1,24 +1,5 @@ -import { - All, - Attributes, - Body, - Button, - Column, - Divider, - Font, - Head, - HideInText, - Image, - InText, - Mjml, - Preview, - Raw, - Section, - Style, - Text, - Title, - Wrapper, -} from '@seedcompany/nestjs-email/templates'; +import * as Meta from '@seedcompany/nestjs-email/templates'; +import * as Mjml from '@seedcompany/nestjs-email/templates/mjml'; import { type ComponentProps, Fragment, @@ -36,22 +17,23 @@ export const EmailTemplate = ({ preview?: ReactNode; children: ReactNode; }) => ( - - - {title} + + + + {title} {preview != null && ( - - + + {preview} {/* Fill the remaining space with nothing-ness so the email context is avoided */} {[...Array(140).keys()].map((i) => ( ͏‌  ))} - - + + )} -