From aca5ac16fb45660fbe1a6e3cef187345ce3df2d6 Mon Sep 17 00:00:00 2001 From: Gergely Nyiri Date: Fri, 8 May 2026 14:18:05 +0200 Subject: [PATCH 01/11] Implement MS Graph Mail service --- .../backend/src/config/dependencyConfigELI.ts | 4 +- .../MailService/MSGraph/MSGraphMailService.ts | 191 ++++++++++++ .../MailService/SMTP/SMTPMailService.ts | 243 +--------------- .../MailService/TemplateMailService.ts | 263 +++++++++++++++++ package-lock.json | 274 ++++++++++++++++++ package.json | 1 + 6 files changed, 737 insertions(+), 239 deletions(-) create mode 100644 apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.ts create mode 100644 apps/backend/src/eventHandlers/MailService/TemplateMailService.ts diff --git a/apps/backend/src/config/dependencyConfigELI.ts b/apps/backend/src/config/dependencyConfigELI.ts index cc79b28b05..a1dea66f40 100644 --- a/apps/backend/src/config/dependencyConfigELI.ts +++ b/apps/backend/src/config/dependencyConfigELI.ts @@ -48,7 +48,7 @@ import PostgresVisitRegistrationClaimDataSource from '../datasources/postgres/Vi import PostgresWorkflowDataSource from '../datasources/postgres/WorkflowDataSource'; import { eliEmailHandler } from '../eventHandlers/email/eliEmailHandler'; import createLoggingHandler from '../eventHandlers/logging'; -import { SMTPMailService } from '../eventHandlers/MailService/SMTP/SMTPMailService'; +import { MSGraphMailService } from '../eventHandlers/MailService/MSGraph/MSGraphMailService'; import { createListenToRabbitMQHandler, createPostToRabbitMQHandler, @@ -127,7 +127,7 @@ mapClass(Tokens.DataAccessUsersAuthorization, DataAccessUsersAuthorization); mapClass(Tokens.AssetRegistrar, EAMAssetRegistrar); -mapClass(Tokens.MailService, SMTPMailService); +mapClass(Tokens.MailService, MSGraphMailService); mapValue(Tokens.FapDataColumns, FapDataColumns); mapValue(Tokens.FapDataRow, getDataRow); diff --git a/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.ts b/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.ts new file mode 100644 index 0000000000..9cdf888ada --- /dev/null +++ b/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.ts @@ -0,0 +1,191 @@ +import * as msal from '@azure/msal-node'; +import { logger } from '@user-office-software/duo-logger'; +import { NodeMailerTransportOptions } from 'email-templates'; +import * as nodemailer from 'nodemailer'; +import { SentMessageInfo, Transport, TransportOptions } from 'nodemailer'; +import MailMessage from 'nodemailer/lib/mailer/mail-message'; + +import { TemplateMailService } from '../TemplateMailService'; + +interface MSGraphTransportOptions extends TransportOptions { + usePool: boolean; + maxConnections: number; + authority: string | undefined; + apiUrl: string | undefined; + clientId: string | undefined; + clientSecret: string | undefined; + tenantId: string | undefined; +} + +class MSGraphTransport implements Transport { + name: string; + version: string; + + private authToken: msal.AuthenticationResult | null = null; + private msalClient: msal.ConfidentialClientApplication; + + constructor(private config: MSGraphTransportOptions) { + if ( + !this.config.authority || + !this.config.apiUrl || + !this.config.clientId || + !this.config.clientSecret || + !this.config.tenantId + ) { + throw new Error( + 'authority, api url, client id, client secret and tenant id must be defined to be able to use the MS Graph API client' + ); + } + + this.name = 'MSGraphTransport'; + this.version = '1.0.0'; + + this.msalClient = new msal.ConfidentialClientApplication({ + auth: { + clientId: this.config.clientId, + clientSecret: this.config.clientSecret, + authority: `${this.config.authority}/${this.config.tenantId}`, + }, + }); + } + + protected isTokenExpired(): boolean { + if (!this.authToken?.expiresOn) { + return false; + } + + return Date.now() >= this.authToken.expiresOn.getTime(); + } + + private async getAccessToken(): Promise { + if (this.authToken && !this.isTokenExpired()) { + return this.authToken.accessToken; + } + + try { + this.authToken = await this.msalClient.acquireTokenByClientCredential({ + scopes: [`${this.config.apiUrl}/.default`], + }); + + if (!this.authToken || !this.authToken.accessToken) { + throw new Error('Could not acquire access token'); + } + } catch (error) { + throw new Error('Could not retrieve an access token.'); + } + + return this.authToken.accessToken; + } + + public async send( + message: MailMessage, + callback: (err: Error | null, info: any) => void + ) { + try { + const accessToken = await this.getAccessToken(); + const { + subject, + from, + to, + text, + html, + attachments = [], + } = message.data || {}; + + const mail = { + message: { + subject, + body: { + contentType: html ? 'HTML' : 'TEXT', + content: html || text || '', + }, + toRecipients: [ + { + emailAddress: { + address: to, + }, + }, + ], + from: { + emailAddress: { + address: from, + }, + }, + attachments: attachments?.map((item) => ({ + '@odata.type': '#microsoft.graph.fileAttachment', + name: item.filename, + contentType: item.contentType, + contentBytes: item.content, + })), + }, + saveToSentItems: true, + }; + + const response = await fetch( + `${this.config.apiUrl}/v1.0/users/${from}/sendMail`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(mail), + } + ); + + if (!response.ok) { + logger.logError('Unable to send email to user', { + error: response.statusText + ' : ' + response.status, + }); + + throw new Error( + `Failed to send email. Status: ${response.status} - ${response.statusText}` + ); + } + + const responseData = await response.text(); + callback(null, { + envelope: { + from: from, + to: [to], + }, + messageId: '', + accepted: [], + rejected: [], + pending: [], + response: responseData, + }); + } catch (error: any) { + logger.logError('Unable to send email to user', { + error: error, + }); + + callback(error, null); + } + } +} + +export class MSGraphMailService extends TemplateMailService { + private msalClient: msal.ConfidentialClientApplication; + private tokenInfo: msal.AuthenticationResult | null = null; + + constructor() { + super(); + } + + protected createTransport(): NodeMailerTransportOptions { + return nodemailer.createTransport( + new MSGraphTransport({ + usePool: process.env.MS_GRAPH_API_USE_POOL ? true : false, + maxConnections: parseInt( + process.env.MS_GRAPH_API_MAX_CONNECTIONS || '5' + ), + authority: process.env.MS_GRAPH_API_AUTHORITY, + apiUrl: process.env.MS_GRAPH_API_URL, + clientId: process.env.MS_GRAPH_API_CLIENT_ID, + clientSecret: process.env.MS_GRAPH_API_CLIENT_SECRET, + tenantId: process.env.MS_GRAPH_API_TENANT_ID, + }) + ); + } +} diff --git a/apps/backend/src/eventHandlers/MailService/SMTP/SMTPMailService.ts b/apps/backend/src/eventHandlers/MailService/SMTP/SMTPMailService.ts index 12933b258a..fa447d9438 100644 --- a/apps/backend/src/eventHandlers/MailService/SMTP/SMTPMailService.ts +++ b/apps/backend/src/eventHandlers/MailService/SMTP/SMTPMailService.ts @@ -1,51 +1,20 @@ -import { existsSync, readFileSync } from 'node:fs'; -import path from 'path'; - import { logger } from '@user-office-software/duo-logger'; -import EmailTemplates from 'email-templates'; +import { NodeMailerTransportOptions } from 'email-templates'; import * as nodemailer from 'nodemailer'; import { Transporter } from 'nodemailer'; import SMTPPool from 'nodemailer/lib/smtp-pool'; import SMTPTransport from 'nodemailer/lib/smtp-transport'; -import pug from 'pug'; -import { container } from 'tsyringe'; - -import { Tokens } from '../../../config/Tokens'; -import { AdminDataSource } from '../../../datasources/AdminDataSource'; -import { EmailTemplateDataSource } from '../../../datasources/EmailTemplateDataSource'; -import { SettingsId } from '../../../models/Settings'; -import { isProduction } from '../../../utils/helperFunctions'; -import SendMailOptions, { MailService, SendMailResults } from '../MailService'; -export class SMTPMailService extends MailService { - private emailTemplate: EmailTemplates; - private emailTemplateDataSource: EmailTemplateDataSource; +import { TemplateMailService } from '../TemplateMailService'; +export class SMTPMailService extends TemplateMailService { constructor() { super(); logger.logInfo('Initializing SMTPMailService', {}); + } - this.emailTemplateDataSource = container.resolve( - Tokens.EmailTemplateDataSource - ); - - const attachments = []; - - if (process.env.EMAIL_FOOTER_IMAGE_PATH !== undefined) { - if (existsSync(process.env.EMAIL_FOOTER_IMAGE_PATH)) { - attachments.push({ - filename: 'logo.png', - path: process.env.EMAIL_FOOTER_IMAGE_PATH, - cid: 'logo1', - }); - } else { - logger.logWarn('Email footer image path does not exist', { - path: process.env.EMAIL_FOOTER_IMAGE_PATH, - }); - } - } - + protected createTransport(): NodeMailerTransportOptions | any { let smtpTransport: | Transporter | Transporter; @@ -66,109 +35,7 @@ export class SMTPMailService extends MailService { }); } - this.emailTemplate = new EmailTemplates({ - message: { - from: process.env.EMAIL_SENDER, - attachments, - }, - send: true, - transport: smtpTransport, - juice: true, - juiceResources: { - webResources: { - relativeTo: path.resolve(process.env.EMAIL_TEMPLATE_PATH || ''), - }, - }, - render: (view: string) => { - return new Promise((resolve, reject) => { - const lastSlashIndex = view.lastIndexOf('/'); - const templateBody = - lastSlashIndex !== -1 ? view.substring(0, lastSlashIndex) : view; - - this.emailTemplate - .juiceResources(templateBody) - .then((html) => { - resolve(html); - }) - .catch((err) => { - reject(err); - }); - }); - }, - }); - } - - private getEmailTemplatePath(type: string, template: string): string { - return path.join( - process.env.EMAIL_TEMPLATE_PATH || '', - `${template}.${type}` - ); - } - - private async compileEmailTemplate(options: SendMailOptions): Promise<{ - subject: string; - body: string; - } | null> { - if (process.env.NODE_ENV === 'test') { - return { subject: '= ``', body: '' }; - } - - const emailTemplate = await this.emailTemplateDataSource.getEmailTemplate( - +options.content.template - ); - - if (!emailTemplate) { - logger.logError('Email template not found', { - template: options.content.template, - }); - - return null; - } - - let templateBody = ''; - let templateSubject = ''; - - if (emailTemplate.useTemplateFile) { - const templateBodyPath = - this.getEmailTemplatePath('html', emailTemplate.name) + '.pug'; - const templateSubjectPath = - this.getEmailTemplatePath('subject', emailTemplate.name) + '.pug'; - - try { - templateBody = readFileSync(templateBodyPath, 'utf-8'); - templateSubject = readFileSync(templateSubjectPath, 'utf-8'); - } catch (error) { - logger.logError('Email template file not found', { - error: error, - }); - - return null; - } - } else { - templateBody = emailTemplate.body || ''; - templateSubject = emailTemplate.subject || ''; - } - - try { - let compiledSubject = ''; - let compiledBody = ''; - compiledSubject = pug.render( - templateSubject, - options.substitution_data || {} - ); - compiledBody = pug.render(templateBody, options.substitution_data || {}); - - return { - subject: compiledSubject, - body: compiledBody, - }; - } catch (error) { - logger.logError('Error compiling email template', { - error: error, - }); - - return null; - } + return smtpTransport; } private getSmtpAuthOptions() { @@ -188,102 +55,4 @@ export class SMTPMailService extends MailService { }, }; } - - async sendMail(options: SendMailOptions) { - const adminDataSource = container.resolve( - Tokens.AdminDataSource - ); - - const bccAddress = ( - await adminDataSource.getSetting(SettingsId.SMTP_BCC_EMAIL) - )?.settingsValue; - - const emailPromises: Promise[] = []; - - const sendMailResults: SendMailResults = { - total_rejected_recipients: 0, - total_accepted_recipients: 0, - id: Math.random().toString(36).substring(7), - }; - - if (process.env.NODE_ENV === 'test') { - sendMailResults.id = 'test'; - } - - const template = await this.compileEmailTemplate(options); - - if (!template) { - logger.logError('Email template not found', { - template: options.content.template, - }); - - return { results: sendMailResults }; - } - - if (process.env.SKIP_SMTP_EMAIL_SENDING === 'true') { - logger.logInfo('Skipping email sending', { - template: options.content.template, - }); - - return { results: sendMailResults }; - } - - options.recipients.forEach((participant) => { - emailPromises.push( - this.emailTemplate.send({ - message: { - ...(participant.header_to - ? { - to: { - address: isProduction - ? participant.address - : process.env.SINK_EMAIL, - name: participant.header_to, - }, - bcc: bccAddress, - subject: template.subject, - html: template.body, - } - : { - to: isProduction - ? participant.address - : process.env.SINK_EMAIL, - bcc: bccAddress, - subject: template.subject, - html: template.body, - }), - }, - }) - ); - }); - - return Promise.allSettled(emailPromises).then((results) => { - results.forEach((result) => { - if (result.status === 'rejected') { - logger.logError('Unable to send email to user', { - error: result.reason, - }); - sendMailResults.total_rejected_recipients++; - } else { - sendMailResults.total_accepted_recipients++; - } - }); - - return sendMailResults.total_rejected_recipients > 0 - ? Promise.reject({ results: sendMailResults }) - : Promise.resolve({ results: sendMailResults }); - }); - } - - async getEmailTemplates() { - const emailTemplates = - await this.emailTemplateDataSource.getEmailTemplates(); - - return { - results: emailTemplates.emailTemplates.map((template) => ({ - id: template.id.toString(), - name: template.name || '', - })), - }; - } } diff --git a/apps/backend/src/eventHandlers/MailService/TemplateMailService.ts b/apps/backend/src/eventHandlers/MailService/TemplateMailService.ts new file mode 100644 index 0000000000..03963d6e28 --- /dev/null +++ b/apps/backend/src/eventHandlers/MailService/TemplateMailService.ts @@ -0,0 +1,263 @@ +import { existsSync, readFileSync } from 'node:fs'; +import path from 'path'; + +import { logger } from '@user-office-software/duo-logger'; +import EmailTemplates, { NodeMailerTransportOptions } from 'email-templates'; +import pug from 'pug'; +import { container } from 'tsyringe'; + +import SendMailOptions, { MailService, SendMailResults } from './MailService'; +import { Tokens } from '../../config/Tokens'; +import { AdminDataSource } from '../../datasources/AdminDataSource'; +import { EmailTemplateDataSource } from '../../datasources/EmailTemplateDataSource'; +import { SettingsId } from '../../models/Settings'; +import { isProduction } from '../../utils/helperFunctions'; + +export abstract class TemplateMailService extends MailService { + protected emailTemplate: EmailTemplates; + protected emailTemplateDataSource: EmailTemplateDataSource; + + constructor() { + super(); + + logger.logInfo('Initializing TemplateMailService', {}); + + this.emailTemplateDataSource = container.resolve( + Tokens.EmailTemplateDataSource + ); + } + + protected createAttachments(): any[] { + const attachments = []; + + if (process.env.EMAIL_FOOTER_IMAGE_PATH !== undefined) { + if (existsSync(process.env.EMAIL_FOOTER_IMAGE_PATH)) { + attachments.push({ + filename: 'logo.png', + path: process.env.EMAIL_FOOTER_IMAGE_PATH, + cid: 'logo1', + }); + } else { + logger.logWarn('Email footer image path does not exist', { + path: process.env.EMAIL_FOOTER_IMAGE_PATH, + }); + } + } + + return attachments; + } + + protected createTransport(): NodeMailerTransportOptions | undefined { + return undefined; + } + + protected createEmailTemplate() { + if (this.emailTemplate) { + return; + } + + this.emailTemplate = new EmailTemplates({ + message: { + from: process.env.EMAIL_SENDER, + attachments: this.createAttachments(), + }, + send: true, + transport: this.createTransport(), + juice: true, + juiceResources: { + webResources: { + relativeTo: path.resolve(process.env.EMAIL_TEMPLATE_PATH || ''), + }, + }, + render: (view: string) => { + return new Promise((resolve, reject) => { + const lastSlashIndex = view.lastIndexOf('/'); + const templateBody = + lastSlashIndex !== -1 ? view.substring(0, lastSlashIndex) : view; + + this.emailTemplate + .juiceResources(templateBody) + .then((html) => { + resolve(html); + }) + .catch((err) => { + reject(err); + }); + }); + }, + }); + } + + protected getEmailTemplatePath(type: string, template: string): string { + return path.join( + process.env.EMAIL_TEMPLATE_PATH || '', + `${template}.${type}` + ); + } + + protected async compileEmailTemplate(options: SendMailOptions): Promise<{ + subject: string; + body: string; + } | null> { + if (process.env.NODE_ENV === 'test') { + return { subject: '= ``', body: '' }; + } + + const emailTemplate = await this.emailTemplateDataSource.getEmailTemplate( + +options.content.template + ); + + if (!emailTemplate) { + logger.logError('Email template not found', { + template: options.content.template, + }); + + return null; + } + + let templateBody = ''; + let templateSubject = ''; + + if (emailTemplate.useTemplateFile) { + const templateBodyPath = + this.getEmailTemplatePath('html', emailTemplate.name) + '.pug'; + const templateSubjectPath = + this.getEmailTemplatePath('subject', emailTemplate.name) + '.pug'; + + try { + templateBody = readFileSync(templateBodyPath, 'utf-8'); + templateSubject = readFileSync(templateSubjectPath, 'utf-8'); + } catch (error) { + logger.logError('Email template file not found', { + error: error, + }); + + return null; + } + } else { + templateBody = emailTemplate.body || ''; + templateSubject = emailTemplate.subject || ''; + } + + try { + let compiledSubject = ''; + let compiledBody = ''; + compiledSubject = pug.render( + templateSubject, + options.substitution_data || {} + ); + compiledBody = pug.render(templateBody, options.substitution_data || {}); + + return { + subject: compiledSubject, + body: compiledBody, + }; + } catch (error) { + logger.logError('Error compiling email template', { + error: error, + }); + + return null; + } + } + + async sendMail(options: SendMailOptions) { + const adminDataSource = container.resolve( + Tokens.AdminDataSource + ); + + const bccAddress = ( + await adminDataSource.getSetting(SettingsId.SMTP_BCC_EMAIL) + )?.settingsValue; + + const emailPromises: Promise[] = []; + + const sendMailResults: SendMailResults = { + total_rejected_recipients: 0, + total_accepted_recipients: 0, + id: Math.random().toString(36).substring(7), + }; + + if (process.env.NODE_ENV === 'test') { + sendMailResults.id = 'test'; + } + + this.createEmailTemplate(); + + const template = await this.compileEmailTemplate(options); + + if (!template) { + logger.logError('Email template not found', { + template: options.content.template, + }); + + return { results: sendMailResults }; + } + + if (process.env.SKIP_SMTP_EMAIL_SENDING === 'true') { + logger.logInfo('Skipping email sending', { + template: options.content.template, + }); + + return { results: sendMailResults }; + } + + options.recipients.forEach((participant) => { + emailPromises.push( + this.emailTemplate.send({ + message: { + ...(participant.header_to + ? { + to: { + address: isProduction + ? participant.address + : process.env.SINK_EMAIL, + name: participant.header_to, + }, + bcc: bccAddress, + subject: template.subject, + html: template.body, + } + : { + to: isProduction + ? participant.address + : process.env.SINK_EMAIL, + bcc: bccAddress, + subject: template.subject, + html: template.body, + }), + }, + }) + ); + }); + + return Promise.allSettled(emailPromises).then((results) => { + results.forEach((result) => { + if (result.status === 'rejected') { + logger.logError('Unable to send email to user', { + error: result.reason, + }); + sendMailResults.total_rejected_recipients++; + } else { + sendMailResults.total_accepted_recipients++; + } + }); + + return sendMailResults.total_rejected_recipients > 0 + ? Promise.reject({ results: sendMailResults }) + : Promise.resolve({ results: sendMailResults }); + }); + } + + async getEmailTemplates() { + const emailTemplates = + await this.emailTemplateDataSource.getEmailTemplates(); + + return { + results: emailTemplates.emailTemplates.map((template) => ({ + id: template.id.toString(), + name: template.name || '', + })), + }; + } +} diff --git a/package-lock.json b/package-lock.json index 266d9b00d4..4e90cfb9b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "license": "ISC", "dependencies": { + "@azure/msal-node": "^5.2.0", "concurrently": "^9.0.1", "wait-on": "^9.0.1" }, @@ -18,6 +19,28 @@ "lint-staged": "^16.0.0" } }, + "node_modules/@azure/msal-common": { + "version": "16.6.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.6.0.tgz", + "integrity": "sha512-FemGljX0csPlBMUE5GUan7BfRn1emeMRUhHSARhqzLN6LA9nt+MgzmAQ1xVqdLm+6plVoxsq9mS5eoyKtpPSgA==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.2.0.tgz", + "integrity": "sha512-b/ak8XAqpnGk1N1nsyTVV0Remp48BP3QrGQZ1uCMcvg2S8X1eSXzhHQZEae2oX276Q4KFAqCUswanDtcvIKLrw==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.6.0", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@hapi/address": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", @@ -117,6 +140,12 @@ "proxy-from-env": "^2.1.0" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -328,6 +357,15 @@ "node": ">= 0.4" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -592,6 +630,49 @@ "node": ">= 20" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/lint-staged": { "version": "16.4.0", "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.4.0.tgz", @@ -716,6 +797,48 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -861,6 +984,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/onetime": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", @@ -935,6 +1064,38 @@ "tslib": "^2.1.0" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shell-quote": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", @@ -1154,6 +1315,20 @@ } }, "dependencies": { + "@azure/msal-common": { + "version": "16.6.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.6.0.tgz", + "integrity": "sha512-FemGljX0csPlBMUE5GUan7BfRn1emeMRUhHSARhqzLN6LA9nt+MgzmAQ1xVqdLm+6plVoxsq9mS5eoyKtpPSgA==" + }, + "@azure/msal-node": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.2.0.tgz", + "integrity": "sha512-b/ak8XAqpnGk1N1nsyTVV0Remp48BP3QrGQZ1uCMcvg2S8X1eSXzhHQZEae2oX276Q4KFAqCUswanDtcvIKLrw==", + "requires": { + "@azure/msal-common": "16.6.0", + "jsonwebtoken": "^9.0.0" + } + }, "@hapi/address": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", @@ -1232,6 +1407,11 @@ "proxy-from-env": "^2.1.0" } }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -1377,6 +1557,14 @@ "gopd": "^1.2.0" } }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -1543,6 +1731,42 @@ "@standard-schema/spec": "^1.1.0" } }, + "jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "requires": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + } + }, + "jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "requires": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "requires": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "lint-staged": { "version": "16.4.0", "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.4.0.tgz", @@ -1627,6 +1851,41 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -1720,6 +1979,11 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, "onetime": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", @@ -1769,6 +2033,16 @@ "tslib": "^2.1.0" } }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==" + }, "shell-quote": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", diff --git a/package.json b/package.json index 947a1eba84..f2348f971a 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "monorepo" ], "dependencies": { + "@azure/msal-node": "^5.2.0", "concurrently": "^9.0.1", "wait-on": "^9.0.1" }, From 8e3f0c9825af796ae7ea7315d665034bd4f927d5 Mon Sep 17 00:00:00 2001 From: Gergely Nyiri Date: Mon, 11 May 2026 10:45:43 +0200 Subject: [PATCH 02/11] feat: complete ms graph api mail service --- .../MSGraph/MSGraphMailService.spec.ts | 105 ++++++++++++++++++ .../MailService/MSGraph/MSGraphMailService.ts | 12 +- .../MailService/SMTP/SMTPMailService.spec.ts | 2 +- .../MailService/TemplateMailService.ts | 32 +++--- 4 files changed, 126 insertions(+), 25 deletions(-) create mode 100644 apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.spec.ts diff --git a/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.spec.ts b/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.spec.ts new file mode 100644 index 0000000000..b87fb9328f --- /dev/null +++ b/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.spec.ts @@ -0,0 +1,105 @@ +import EmailTemplates from 'email-templates'; +import { container } from 'tsyringe'; + +import { MSGraphMailService } from './MSGraphMailService'; +import { Tokens } from '../../../config/Tokens'; +import { AdminDataSource } from '../../../datasources/AdminDataSource'; +import { SettingsId } from '../../../models/Settings'; +import SendMailOptions from '../MailService'; + +jest.mock('email-templates'); +const mockAdminDataSource = container.resolve( + Tokens.AdminDataSource +) as AdminDataSource; + +const mockGetSetting = jest.spyOn(mockAdminDataSource, 'getSetting'); + +test('Return result should indicate all emails were successfully sent', async () => { + mockGetSetting.mockResolvedValue(null); + + const options: SendMailOptions = { + content: { + template: '1', + }, + substitution_data: { + piPreferredname: 'John', + piLastname: 'Doe', + proposalNumber: '1', + proposalTitle: 'Title', + coProposers: ['Jane Doe'], + call: '', + }, + recipients: [ + { + address: 'john.doe@email.com', + }, + ], + }; + + const msgGraphMailService: MSGraphMailService = new MSGraphMailService(); + const result = await msgGraphMailService.sendMail(options); + + return expect(result).toStrictEqual({ + results: { + id: 'test', + total_accepted_recipients: 1, + total_rejected_recipients: 0, + }, + }); +}); + +test('All emails with bcc were successfully sent', async () => { + mockGetSetting.mockClear(); + const emailInfo = jest.spyOn(EmailTemplates.prototype, 'send'); + + const bccEmail = 'testmail@test.co'; + + const substitutionData = { + piPreferredname: 'John', + piLastname: 'Doe', + proposalNumber: '1', + proposalTitle: 'Title', + coProposers: ['Jane Doe'], + call: '', + }; + + mockGetSetting.mockImplementation(() => + Promise.resolve({ + settingsValue: bccEmail, + description: 'bcc mail', + id: SettingsId.SMTP_BCC_EMAIL, + }) + ); + + const options: SendMailOptions = { + content: { + template: '1', + }, + substitution_data: substitutionData, + recipients: [ + { + address: 'john.doe@email.com', + }, + ], + }; + + const msgGraphMailService: MSGraphMailService = new MSGraphMailService(); + const result = await msgGraphMailService.sendMail(options); + + expect(emailInfo).toHaveBeenCalledWith({ + message: { + to: process.env.SINK_EMAIL, + bcc: bccEmail, + subject: '= ``', + html: '', + }, + }); + + return expect(result).toStrictEqual({ + results: { + id: 'test', + total_accepted_recipients: 1, + total_rejected_recipients: 0, + }, + }); +}); diff --git a/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.ts b/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.ts index 9cdf888ada..45f45a9f1b 100644 --- a/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.ts +++ b/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.ts @@ -20,6 +20,7 @@ interface MSGraphTransportOptions extends TransportOptions { class MSGraphTransport implements Transport { name: string; version: string; + apiUrl: string | undefined; private authToken: msal.AuthenticationResult | null = null; private msalClient: msal.ConfidentialClientApplication; @@ -92,9 +93,9 @@ class MSGraphTransport implements Transport { attachments = [], } = message.data || {}; - const mail = { + const payload = { message: { - subject, + subject: subject || '', body: { contentType: html ? 'HTML' : 'TEXT', content: html || text || '', @@ -106,11 +107,6 @@ class MSGraphTransport implements Transport { }, }, ], - from: { - emailAddress: { - address: from, - }, - }, attachments: attachments?.map((item) => ({ '@odata.type': '#microsoft.graph.fileAttachment', name: item.filename, @@ -129,7 +125,7 @@ class MSGraphTransport implements Transport { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, - body: JSON.stringify(mail), + body: JSON.stringify(payload), } ); diff --git a/apps/backend/src/eventHandlers/MailService/SMTP/SMTPMailService.spec.ts b/apps/backend/src/eventHandlers/MailService/SMTP/SMTPMailService.spec.ts index a5a1d5e707..5d6f1ae6b6 100644 --- a/apps/backend/src/eventHandlers/MailService/SMTP/SMTPMailService.spec.ts +++ b/apps/backend/src/eventHandlers/MailService/SMTP/SMTPMailService.spec.ts @@ -1,11 +1,11 @@ import EmailTemplates from 'email-templates'; import { container } from 'tsyringe'; +import { SMTPMailService } from './SMTPMailService'; import { Tokens } from '../../../config/Tokens'; import { AdminDataSource } from '../../../datasources/AdminDataSource'; import { SettingsId } from '../../../models/Settings'; import SendMailOptions from '../MailService'; -import { SMTPMailService } from './SMTPMailService'; jest.mock('email-templates'); const mockAdminDataSource = container.resolve( diff --git a/apps/backend/src/eventHandlers/MailService/TemplateMailService.ts b/apps/backend/src/eventHandlers/MailService/TemplateMailService.ts index 03963d6e28..ca6f69e30c 100644 --- a/apps/backend/src/eventHandlers/MailService/TemplateMailService.ts +++ b/apps/backend/src/eventHandlers/MailService/TemplateMailService.ts @@ -14,7 +14,7 @@ import { SettingsId } from '../../models/Settings'; import { isProduction } from '../../utils/helperFunctions'; export abstract class TemplateMailService extends MailService { - protected emailTemplate: EmailTemplates; + protected emailTemplates: EmailTemplates; protected emailTemplateDataSource: EmailTemplateDataSource; constructor() { @@ -51,12 +51,12 @@ export abstract class TemplateMailService extends MailService { return undefined; } - protected createEmailTemplate() { - if (this.emailTemplate) { + protected createEmailTemplates() { + if (this.emailTemplates) { return; } - this.emailTemplate = new EmailTemplates({ + this.emailTemplates = new EmailTemplates({ message: { from: process.env.EMAIL_SENDER, attachments: this.createAttachments(), @@ -75,7 +75,7 @@ export abstract class TemplateMailService extends MailService { const templateBody = lastSlashIndex !== -1 ? view.substring(0, lastSlashIndex) : view; - this.emailTemplate + this.emailTemplates .juiceResources(templateBody) .then((html) => { resolve(html); @@ -95,7 +95,7 @@ export abstract class TemplateMailService extends MailService { ); } - protected async compileEmailTemplate(options: SendMailOptions): Promise<{ + protected async compileTemplate(options: SendMailOptions): Promise<{ subject: string; body: string; } | null> { @@ -103,11 +103,11 @@ export abstract class TemplateMailService extends MailService { return { subject: '= ``', body: '' }; } - const emailTemplate = await this.emailTemplateDataSource.getEmailTemplate( + const template = await this.emailTemplateDataSource.getEmailTemplate( +options.content.template ); - if (!emailTemplate) { + if (!template) { logger.logError('Email template not found', { template: options.content.template, }); @@ -118,11 +118,11 @@ export abstract class TemplateMailService extends MailService { let templateBody = ''; let templateSubject = ''; - if (emailTemplate.useTemplateFile) { + if (template.useTemplateFile) { const templateBodyPath = - this.getEmailTemplatePath('html', emailTemplate.name) + '.pug'; + this.getEmailTemplatePath('html', template.name) + '.pug'; const templateSubjectPath = - this.getEmailTemplatePath('subject', emailTemplate.name) + '.pug'; + this.getEmailTemplatePath('subject', template.name) + '.pug'; try { templateBody = readFileSync(templateBodyPath, 'utf-8'); @@ -135,8 +135,8 @@ export abstract class TemplateMailService extends MailService { return null; } } else { - templateBody = emailTemplate.body || ''; - templateSubject = emailTemplate.subject || ''; + templateBody = template.body || ''; + templateSubject = template.subject || ''; } try { @@ -182,9 +182,9 @@ export abstract class TemplateMailService extends MailService { sendMailResults.id = 'test'; } - this.createEmailTemplate(); + this.createEmailTemplates(); - const template = await this.compileEmailTemplate(options); + const template = await this.compileTemplate(options); if (!template) { logger.logError('Email template not found', { @@ -204,7 +204,7 @@ export abstract class TemplateMailService extends MailService { options.recipients.forEach((participant) => { emailPromises.push( - this.emailTemplate.send({ + this.emailTemplates.send({ message: { ...(participant.header_to ? { From cac976274f6fa0b95ddf3ea0682e0e7e593b6b93 Mon Sep 17 00:00:00 2001 From: Gergely Nyiri Date: Mon, 11 May 2026 11:06:16 +0200 Subject: [PATCH 03/11] fix: package files --- apps/backend/package-lock.json | 29 +++- apps/backend/package.json | 3 +- package-lock.json | 274 --------------------------------- package.json | 3 +- 4 files changed, 26 insertions(+), 283 deletions(-) diff --git a/apps/backend/package-lock.json b/apps/backend/package-lock.json index 6679ba38cb..ebbca4ea3a 100644 --- a/apps/backend/package-lock.json +++ b/apps/backend/package-lock.json @@ -13,6 +13,7 @@ "@apollo/server": "^5.5.0", "@apollo/subgraph": "^2.13.2", "@as-integrations/express4": "^1.1.2", + "@azure/msal-node": "^5.2.0", "@graphql-tools/utils": "^10.1.0", "@isaacs/ttlcache": "^1.4.1", "@opentelemetry/exporter-trace-otlp-http": "^0.200.0", @@ -89,7 +90,6 @@ "@types/sanitize-html": "^2.6.2", "@types/simple-oauth2": "^4.1.1", "@types/sinon": "^9.0.11", - "@types/yup": "^0.29.13", "@typescript-eslint/eslint-plugin": "^8.46.3", "@typescript-eslint/parser": "^8.46.2", "@user-office-software/uows_client_generator": "^3.1.0", @@ -4172,6 +4172,28 @@ "express": "^4.0.0" } }, + "node_modules/@azure/msal-common": { + "version": "16.6.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.6.0.tgz", + "integrity": "sha512-FemGljX0csPlBMUE5GUan7BfRn1emeMRUhHSARhqzLN6LA9nt+MgzmAQ1xVqdLm+6plVoxsq9mS5eoyKtpPSgA==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.2.0.tgz", + "integrity": "sha512-b/ak8XAqpnGk1N1nsyTVV0Remp48BP3QrGQZ1uCMcvg2S8X1eSXzhHQZEae2oX276Q4KFAqCUswanDtcvIKLrw==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.6.0", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@babel/code-frame": { "version": "7.22.13", "devOptional": true, @@ -7103,11 +7125,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/yup": { - "version": "0.29.13", - "dev": true, - "license": "MIT" - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.56.1", "dev": true, diff --git a/apps/backend/package.json b/apps/backend/package.json index 4c7a26298a..29b9784f82 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -36,6 +36,7 @@ "@apollo/server": "^5.5.0", "@apollo/subgraph": "^2.13.2", "@as-integrations/express4": "^1.1.2", + "@azure/msal-node": "^5.2.0", "@graphql-tools/utils": "^10.1.0", "@isaacs/ttlcache": "^1.4.1", "@opentelemetry/exporter-trace-otlp-http": "^0.200.0", @@ -64,8 +65,8 @@ "email-templates": "^13.0.1", "express": "^4.22.1", "express-jwt": "^6.1.2", - "graphql-request": "^7.4.0", "graphql": "^16.13.2", + "graphql-request": "^7.4.0", "graphql-tag": "^2.12.6", "jsonpath": "^1.2.1", "jsonwebtoken": "^9.0.1", diff --git a/package-lock.json b/package-lock.json index 4e90cfb9b2..266d9b00d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "hasInstallScript": true, "license": "ISC", "dependencies": { - "@azure/msal-node": "^5.2.0", "concurrently": "^9.0.1", "wait-on": "^9.0.1" }, @@ -19,28 +18,6 @@ "lint-staged": "^16.0.0" } }, - "node_modules/@azure/msal-common": { - "version": "16.6.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.6.0.tgz", - "integrity": "sha512-FemGljX0csPlBMUE5GUan7BfRn1emeMRUhHSARhqzLN6LA9nt+MgzmAQ1xVqdLm+6plVoxsq9mS5eoyKtpPSgA==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@azure/msal-node": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.2.0.tgz", - "integrity": "sha512-b/ak8XAqpnGk1N1nsyTVV0Remp48BP3QrGQZ1uCMcvg2S8X1eSXzhHQZEae2oX276Q4KFAqCUswanDtcvIKLrw==", - "license": "MIT", - "dependencies": { - "@azure/msal-common": "16.6.0", - "jsonwebtoken": "^9.0.0" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/@hapi/address": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", @@ -140,12 +117,6 @@ "proxy-from-env": "^2.1.0" } }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" - }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -357,15 +328,6 @@ "node": ">= 0.4" } }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -630,49 +592,6 @@ "node": ">= 20" } }, - "node_modules/jsonwebtoken": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", - "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", - "license": "MIT", - "dependencies": { - "jws": "^4.0.1", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "license": "MIT", - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, "node_modules/lint-staged": { "version": "16.4.0", "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.4.0.tgz", @@ -797,48 +716,6 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", - "license": "MIT" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", - "license": "MIT" - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", - "license": "MIT" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", - "license": "MIT" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "license": "MIT" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", - "license": "MIT" - }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "license": "MIT" - }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -984,12 +861,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/onetime": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", @@ -1064,38 +935,6 @@ "tslib": "^2.1.0" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/shell-quote": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", @@ -1315,20 +1154,6 @@ } }, "dependencies": { - "@azure/msal-common": { - "version": "16.6.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.6.0.tgz", - "integrity": "sha512-FemGljX0csPlBMUE5GUan7BfRn1emeMRUhHSARhqzLN6LA9nt+MgzmAQ1xVqdLm+6plVoxsq9mS5eoyKtpPSgA==" - }, - "@azure/msal-node": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.2.0.tgz", - "integrity": "sha512-b/ak8XAqpnGk1N1nsyTVV0Remp48BP3QrGQZ1uCMcvg2S8X1eSXzhHQZEae2oX276Q4KFAqCUswanDtcvIKLrw==", - "requires": { - "@azure/msal-common": "16.6.0", - "jsonwebtoken": "^9.0.0" - } - }, "@hapi/address": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", @@ -1407,11 +1232,6 @@ "proxy-from-env": "^2.1.0" } }, - "buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, "call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -1557,14 +1377,6 @@ "gopd": "^1.2.0" } }, - "ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -1731,42 +1543,6 @@ "@standard-schema/spec": "^1.1.0" } }, - "jsonwebtoken": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", - "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", - "requires": { - "jws": "^4.0.1", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^7.5.4" - } - }, - "jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "requires": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "requires": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, "lint-staged": { "version": "16.4.0", "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.4.0.tgz", @@ -1851,41 +1627,6 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" }, - "lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" - }, - "lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" - }, - "lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" - }, - "lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" - }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" - }, - "lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" - }, - "lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" - }, "log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -1979,11 +1720,6 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, "onetime": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", @@ -2033,16 +1769,6 @@ "tslib": "^2.1.0" } }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==" - }, "shell-quote": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", diff --git a/package.json b/package.json index f2348f971a..5a59ffc98e 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "monorepo" ], "dependencies": { - "@azure/msal-node": "^5.2.0", "concurrently": "^9.0.1", "wait-on": "^9.0.1" }, @@ -57,4 +56,4 @@ "husky": "^9.0.10", "lint-staged": "^16.0.0" } -} +} \ No newline at end of file From 177cb996a82fb4d3bb82583ef3fcde23b7046e7d Mon Sep 17 00:00:00 2001 From: Gergely Nyiri Date: Mon, 11 May 2026 11:23:47 +0200 Subject: [PATCH 04/11] fix: lazy creation of client --- .../MailService/MSGraph/MSGraphMailService.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.ts b/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.ts index 45f45a9f1b..da488a41f6 100644 --- a/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.ts +++ b/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.ts @@ -40,10 +40,16 @@ class MSGraphTransport implements Transport { this.name = 'MSGraphTransport'; this.version = '1.0.0'; + } + + private createClient() { + if (this.msalClient) { + return; + } this.msalClient = new msal.ConfidentialClientApplication({ auth: { - clientId: this.config.clientId, + clientId: this.config.clientId!, clientSecret: this.config.clientSecret, authority: `${this.config.authority}/${this.config.tenantId}`, }, @@ -83,6 +89,7 @@ class MSGraphTransport implements Transport { callback: (err: Error | null, info: any) => void ) { try { + this.createClient(); const accessToken = await this.getAccessToken(); const { subject, From 0382853200de250c0edf1844e0b9dd02ae389805 Mon Sep 17 00:00:00 2001 From: Gergely Nyiri Date: Mon, 11 May 2026 13:43:12 +0200 Subject: [PATCH 05/11] feat: simplify ms graph mail service --- .../MailService/MSGraph/MSGraphMailService.ts | 69 +++++++------------ 1 file changed, 23 insertions(+), 46 deletions(-) diff --git a/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.ts b/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.ts index da488a41f6..ea944b2b10 100644 --- a/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.ts +++ b/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.ts @@ -2,21 +2,11 @@ import * as msal from '@azure/msal-node'; import { logger } from '@user-office-software/duo-logger'; import { NodeMailerTransportOptions } from 'email-templates'; import * as nodemailer from 'nodemailer'; -import { SentMessageInfo, Transport, TransportOptions } from 'nodemailer'; +import { SentMessageInfo, Transport } from 'nodemailer'; import MailMessage from 'nodemailer/lib/mailer/mail-message'; import { TemplateMailService } from '../TemplateMailService'; -interface MSGraphTransportOptions extends TransportOptions { - usePool: boolean; - maxConnections: number; - authority: string | undefined; - apiUrl: string | undefined; - clientId: string | undefined; - clientSecret: string | undefined; - tenantId: string | undefined; -} - class MSGraphTransport implements Transport { name: string; version: string; @@ -25,21 +15,10 @@ class MSGraphTransport implements Transport { private authToken: msal.AuthenticationResult | null = null; private msalClient: msal.ConfidentialClientApplication; - constructor(private config: MSGraphTransportOptions) { - if ( - !this.config.authority || - !this.config.apiUrl || - !this.config.clientId || - !this.config.clientSecret || - !this.config.tenantId - ) { - throw new Error( - 'authority, api url, client id, client secret and tenant id must be defined to be able to use the MS Graph API client' - ); - } - + constructor() { this.name = 'MSGraphTransport'; this.version = '1.0.0'; + this.apiUrl = process.env.MS_GRAPH_API_URL; } private createClient() { @@ -49,9 +28,9 @@ class MSGraphTransport implements Transport { this.msalClient = new msal.ConfidentialClientApplication({ auth: { - clientId: this.config.clientId!, - clientSecret: this.config.clientSecret, - authority: `${this.config.authority}/${this.config.tenantId}`, + clientId: process.env.MS_GRAPH_API_CLIENT_ID!, + clientSecret: process.env.MS_GRAPH_API_CLIENT_SECRET!, + authority: `${process.env.MS_GRAPH_API_AUTHORITY}/${process.env.MS_GRAPH_API_TENANT_ID}`, }, }); } @@ -71,7 +50,7 @@ class MSGraphTransport implements Transport { try { this.authToken = await this.msalClient.acquireTokenByClientCredential({ - scopes: [`${this.config.apiUrl}/.default`], + scopes: [`${this.apiUrl}/.default`], }); if (!this.authToken || !this.authToken.accessToken) { @@ -93,10 +72,10 @@ class MSGraphTransport implements Transport { const accessToken = await this.getAccessToken(); const { subject, - from, to, - text, + from, html, + bcc, attachments = [], } = message.data || {}; @@ -104,8 +83,8 @@ class MSGraphTransport implements Transport { message: { subject: subject || '', body: { - contentType: html ? 'HTML' : 'TEXT', - content: html || text || '', + contentType: 'HTML', + content: html || '', }, toRecipients: [ { @@ -114,6 +93,16 @@ class MSGraphTransport implements Transport { }, }, ], + bccRecipients: bcc + ? [ + { + emailAddress: { + address: bcc, + }, + }, + ] + : undefined, + //bccRecipients: bcc, attachments: attachments?.map((item) => ({ '@odata.type': '#microsoft.graph.fileAttachment', name: item.filename, @@ -125,7 +114,7 @@ class MSGraphTransport implements Transport { }; const response = await fetch( - `${this.config.apiUrl}/v1.0/users/${from}/sendMail`, + `${this.apiUrl}/v1.0/users/${from}/sendMail`, { method: 'POST', headers: { @@ -177,18 +166,6 @@ export class MSGraphMailService extends TemplateMailService { } protected createTransport(): NodeMailerTransportOptions { - return nodemailer.createTransport( - new MSGraphTransport({ - usePool: process.env.MS_GRAPH_API_USE_POOL ? true : false, - maxConnections: parseInt( - process.env.MS_GRAPH_API_MAX_CONNECTIONS || '5' - ), - authority: process.env.MS_GRAPH_API_AUTHORITY, - apiUrl: process.env.MS_GRAPH_API_URL, - clientId: process.env.MS_GRAPH_API_CLIENT_ID, - clientSecret: process.env.MS_GRAPH_API_CLIENT_SECRET, - tenantId: process.env.MS_GRAPH_API_TENANT_ID, - }) - ); + return nodemailer.createTransport(new MSGraphTransport()); } } From 4e27cf54affd3510c6fd69cbe945a77813f776cb Mon Sep 17 00:00:00 2001 From: Gergely Nyiri Date: Mon, 11 May 2026 14:31:01 +0200 Subject: [PATCH 06/11] feat: simplify ms graph api mail service --- .github/workflows/test-build.yml | 2 +- .../MailService/MSGraph/MSGraphMailService.ts | 27 +++++-------------- .../MailService/TemplateMailService.ts | 6 +++-- 3 files changed, 12 insertions(+), 23 deletions(-) diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index d505cf6ef7..5a894c69b4 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -477,7 +477,7 @@ jobs: # EMAIL_AUTH_HOST: exchsmtp.stfc.ac.uk # EMAIL_TEMPLATE_PATH: /config/emails/ # EMAIL_FOOTER_IMAGE_PATH: /config/logos/STFC-Logo-small.png - # SKIP_SMTP_EMAIL_SENDING: true + # SKIP_EMAIL_SENDING: true # EXTERNAL_AUTH_LOGIN_URL: http://localhost:9003/auth/Login.aspx # EXTERNAL_AUTH_LOGOUT_URL: http://localhost:9003/auth/Login.aspx # EXTERNAL_UOWS_API_URL: http://localhost:1080/users-service diff --git a/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.ts b/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.ts index ea944b2b10..d6d57dd788 100644 --- a/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.ts +++ b/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.ts @@ -19,12 +19,6 @@ class MSGraphTransport implements Transport { this.name = 'MSGraphTransport'; this.version = '1.0.0'; this.apiUrl = process.env.MS_GRAPH_API_URL; - } - - private createClient() { - if (this.msalClient) { - return; - } this.msalClient = new msal.ConfidentialClientApplication({ auth: { @@ -48,19 +42,15 @@ class MSGraphTransport implements Transport { return this.authToken.accessToken; } - try { - this.authToken = await this.msalClient.acquireTokenByClientCredential({ - scopes: [`${this.apiUrl}/.default`], - }); + this.authToken = await this.msalClient.acquireTokenByClientCredential({ + scopes: [`${this.apiUrl}/.default`], + }); - if (!this.authToken || !this.authToken.accessToken) { - throw new Error('Could not acquire access token'); - } - } catch (error) { - throw new Error('Could not retrieve an access token.'); + if (!this.authToken) { + throw new Error('Failed to acquire access token for Microsoft Graph API'); } - return this.authToken.accessToken; + return this.authToken?.accessToken; } public async send( @@ -68,8 +58,8 @@ class MSGraphTransport implements Transport { callback: (err: Error | null, info: any) => void ) { try { - this.createClient(); const accessToken = await this.getAccessToken(); + const { subject, to, @@ -158,9 +148,6 @@ class MSGraphTransport implements Transport { } export class MSGraphMailService extends TemplateMailService { - private msalClient: msal.ConfidentialClientApplication; - private tokenInfo: msal.AuthenticationResult | null = null; - constructor() { super(); } diff --git a/apps/backend/src/eventHandlers/MailService/TemplateMailService.ts b/apps/backend/src/eventHandlers/MailService/TemplateMailService.ts index ca6f69e30c..dc0a93df86 100644 --- a/apps/backend/src/eventHandlers/MailService/TemplateMailService.ts +++ b/apps/backend/src/eventHandlers/MailService/TemplateMailService.ts @@ -161,7 +161,9 @@ export abstract class TemplateMailService extends MailService { } } - async sendMail(options: SendMailOptions) { + async sendMail( + options: SendMailOptions + ): Promise<{ results: SendMailResults }> { const adminDataSource = container.resolve( Tokens.AdminDataSource ); @@ -194,7 +196,7 @@ export abstract class TemplateMailService extends MailService { return { results: sendMailResults }; } - if (process.env.SKIP_SMTP_EMAIL_SENDING === 'true') { + if (process.env.SKIP_EMAIL_SENDING === 'true') { logger.logInfo('Skipping email sending', { template: options.content.template, }); From 5474a491f00664b53856e0f93c3688aa68da64ce Mon Sep 17 00:00:00 2001 From: Gergely Nyiri Date: Tue, 12 May 2026 09:53:09 +0200 Subject: [PATCH 07/11] fix: add further logs --- .../src/eventHandlers/MailService/SMTP/SMTPMailService.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/eventHandlers/MailService/SMTP/SMTPMailService.ts b/apps/backend/src/eventHandlers/MailService/SMTP/SMTPMailService.ts index fa447d9438..872f17d777 100644 --- a/apps/backend/src/eventHandlers/MailService/SMTP/SMTPMailService.ts +++ b/apps/backend/src/eventHandlers/MailService/SMTP/SMTPMailService.ts @@ -11,7 +11,12 @@ export class SMTPMailService extends TemplateMailService { constructor() { super(); - logger.logInfo('Initializing SMTPMailService', {}); + logger.logInfo('Initializing SMTPMailService', { + host: process.env.EMAIL_AUTH_HOST, + port: process.env.EMAIL_AUTH_PORT, + user: process.env.EMAIL_AUTH_USERNAME, + pass: process.env.EMAIL_AUTH_PASSWORD, + }); } protected createTransport(): NodeMailerTransportOptions | any { From 0b7b056212a463c9f2e88d449c31b004fea2324c Mon Sep 17 00:00:00 2001 From: Gergely Nyiri Date: Tue, 12 May 2026 10:22:40 +0200 Subject: [PATCH 08/11] feat: delete ms graph api test --- .../MSGraph/MSGraphMailService.spec.ts | 105 ------------------ 1 file changed, 105 deletions(-) delete mode 100644 apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.spec.ts diff --git a/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.spec.ts b/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.spec.ts deleted file mode 100644 index b87fb9328f..0000000000 --- a/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.spec.ts +++ /dev/null @@ -1,105 +0,0 @@ -import EmailTemplates from 'email-templates'; -import { container } from 'tsyringe'; - -import { MSGraphMailService } from './MSGraphMailService'; -import { Tokens } from '../../../config/Tokens'; -import { AdminDataSource } from '../../../datasources/AdminDataSource'; -import { SettingsId } from '../../../models/Settings'; -import SendMailOptions from '../MailService'; - -jest.mock('email-templates'); -const mockAdminDataSource = container.resolve( - Tokens.AdminDataSource -) as AdminDataSource; - -const mockGetSetting = jest.spyOn(mockAdminDataSource, 'getSetting'); - -test('Return result should indicate all emails were successfully sent', async () => { - mockGetSetting.mockResolvedValue(null); - - const options: SendMailOptions = { - content: { - template: '1', - }, - substitution_data: { - piPreferredname: 'John', - piLastname: 'Doe', - proposalNumber: '1', - proposalTitle: 'Title', - coProposers: ['Jane Doe'], - call: '', - }, - recipients: [ - { - address: 'john.doe@email.com', - }, - ], - }; - - const msgGraphMailService: MSGraphMailService = new MSGraphMailService(); - const result = await msgGraphMailService.sendMail(options); - - return expect(result).toStrictEqual({ - results: { - id: 'test', - total_accepted_recipients: 1, - total_rejected_recipients: 0, - }, - }); -}); - -test('All emails with bcc were successfully sent', async () => { - mockGetSetting.mockClear(); - const emailInfo = jest.spyOn(EmailTemplates.prototype, 'send'); - - const bccEmail = 'testmail@test.co'; - - const substitutionData = { - piPreferredname: 'John', - piLastname: 'Doe', - proposalNumber: '1', - proposalTitle: 'Title', - coProposers: ['Jane Doe'], - call: '', - }; - - mockGetSetting.mockImplementation(() => - Promise.resolve({ - settingsValue: bccEmail, - description: 'bcc mail', - id: SettingsId.SMTP_BCC_EMAIL, - }) - ); - - const options: SendMailOptions = { - content: { - template: '1', - }, - substitution_data: substitutionData, - recipients: [ - { - address: 'john.doe@email.com', - }, - ], - }; - - const msgGraphMailService: MSGraphMailService = new MSGraphMailService(); - const result = await msgGraphMailService.sendMail(options); - - expect(emailInfo).toHaveBeenCalledWith({ - message: { - to: process.env.SINK_EMAIL, - bcc: bccEmail, - subject: '= ``', - html: '', - }, - }); - - return expect(result).toStrictEqual({ - results: { - id: 'test', - total_accepted_recipients: 1, - total_rejected_recipients: 0, - }, - }); -}); From 700a505fba18fb597cbb95802dca45c85dacbc76 Mon Sep 17 00:00:00 2001 From: Gergely Nyiri Date: Tue, 12 May 2026 10:23:31 +0200 Subject: [PATCH 09/11] fix: remove password log output --- .../src/eventHandlers/MailService/SMTP/SMTPMailService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/backend/src/eventHandlers/MailService/SMTP/SMTPMailService.ts b/apps/backend/src/eventHandlers/MailService/SMTP/SMTPMailService.ts index 872f17d777..2c5567b058 100644 --- a/apps/backend/src/eventHandlers/MailService/SMTP/SMTPMailService.ts +++ b/apps/backend/src/eventHandlers/MailService/SMTP/SMTPMailService.ts @@ -15,7 +15,6 @@ export class SMTPMailService extends TemplateMailService { host: process.env.EMAIL_AUTH_HOST, port: process.env.EMAIL_AUTH_PORT, user: process.env.EMAIL_AUTH_USERNAME, - pass: process.env.EMAIL_AUTH_PASSWORD, }); } From d609c8a635a9bf3be91c9c661decb257709e87a4 Mon Sep 17 00:00:00 2001 From: Gergely Nyiri Date: Tue, 12 May 2026 11:35:43 +0200 Subject: [PATCH 10/11] fix: make createAttachments abstract and fix ms graph implementation --- .../MailService/MSGraph/MSGraphMailService.ts | 39 ++++++++++++++----- .../MailService/SMTP/SMTPMailService.ts | 22 +++++++++++ .../MailService/TemplateMailService.ts | 27 ++----------- 3 files changed, 54 insertions(+), 34 deletions(-) diff --git a/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.ts b/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.ts index d6d57dd788..d331dd1355 100644 --- a/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.ts +++ b/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.ts @@ -1,3 +1,5 @@ +import { existsSync, readFileSync } from 'node:fs'; + import * as msal from '@azure/msal-node'; import { logger } from '@user-office-software/duo-logger'; import { NodeMailerTransportOptions } from 'email-templates'; @@ -10,15 +12,15 @@ import { TemplateMailService } from '../TemplateMailService'; class MSGraphTransport implements Transport { name: string; version: string; - apiUrl: string | undefined; + private apiUrl: string; private authToken: msal.AuthenticationResult | null = null; private msalClient: msal.ConfidentialClientApplication; constructor() { this.name = 'MSGraphTransport'; this.version = '1.0.0'; - this.apiUrl = process.env.MS_GRAPH_API_URL; + this.apiUrl = process.env.MS_GRAPH_API_URL || ''; this.msalClient = new msal.ConfidentialClientApplication({ auth: { @@ -55,7 +57,7 @@ class MSGraphTransport implements Transport { public async send( message: MailMessage, - callback: (err: Error | null, info: any) => void + callback: (err: Error | null, info: unknown) => void ) { try { const accessToken = await this.getAccessToken(); @@ -92,13 +94,7 @@ class MSGraphTransport implements Transport { }, ] : undefined, - //bccRecipients: bcc, - attachments: attachments?.map((item) => ({ - '@odata.type': '#microsoft.graph.fileAttachment', - name: item.filename, - contentType: item.contentType, - contentBytes: item.content, - })), + attachments: attachments, }, saveToSentItems: true, }; @@ -155,4 +151,27 @@ export class MSGraphMailService extends TemplateMailService { protected createTransport(): NodeMailerTransportOptions { return nodemailer.createTransport(new MSGraphTransport()); } + + protected createAttachments() { + const attachments = []; + + if (process.env.EMAIL_FOOTER_IMAGE_PATH !== undefined) { + if (existsSync(process.env.EMAIL_FOOTER_IMAGE_PATH)) { + attachments.push({ + '@odata.type': '#microsoft.graph.fileAttachment', + name: 'logo.png', + contentType: 'image/png', + contentBytes: readFileSync( + process.env.EMAIL_FOOTER_IMAGE_PATH + ).toString('base64'), + }); + } else { + logger.logWarn('Email footer image path does not exist', { + path: process.env.EMAIL_FOOTER_IMAGE_PATH, + }); + } + } + + return attachments; + } } diff --git a/apps/backend/src/eventHandlers/MailService/SMTP/SMTPMailService.ts b/apps/backend/src/eventHandlers/MailService/SMTP/SMTPMailService.ts index 2c5567b058..b46b068dd7 100644 --- a/apps/backend/src/eventHandlers/MailService/SMTP/SMTPMailService.ts +++ b/apps/backend/src/eventHandlers/MailService/SMTP/SMTPMailService.ts @@ -1,3 +1,5 @@ +import { existsSync } from 'node:fs'; + import { logger } from '@user-office-software/duo-logger'; import { NodeMailerTransportOptions } from 'email-templates'; import * as nodemailer from 'nodemailer'; @@ -59,4 +61,24 @@ export class SMTPMailService extends TemplateMailService { }, }; } + + protected createAttachments(): any[] { + const attachments = []; + + if (process.env.EMAIL_FOOTER_IMAGE_PATH !== undefined) { + if (existsSync(process.env.EMAIL_FOOTER_IMAGE_PATH)) { + attachments.push({ + filename: 'logo.png', + path: process.env.EMAIL_FOOTER_IMAGE_PATH, + cid: 'logo1', + }); + } else { + logger.logWarn('Email footer image path does not exist', { + path: process.env.EMAIL_FOOTER_IMAGE_PATH, + }); + } + } + + return attachments; + } } diff --git a/apps/backend/src/eventHandlers/MailService/TemplateMailService.ts b/apps/backend/src/eventHandlers/MailService/TemplateMailService.ts index dc0a93df86..05f0638faf 100644 --- a/apps/backend/src/eventHandlers/MailService/TemplateMailService.ts +++ b/apps/backend/src/eventHandlers/MailService/TemplateMailService.ts @@ -1,4 +1,4 @@ -import { existsSync, readFileSync } from 'node:fs'; +import { readFileSync } from 'node:fs'; import path from 'path'; import { logger } from '@user-office-software/duo-logger'; @@ -27,29 +27,8 @@ export abstract class TemplateMailService extends MailService { ); } - protected createAttachments(): any[] { - const attachments = []; - - if (process.env.EMAIL_FOOTER_IMAGE_PATH !== undefined) { - if (existsSync(process.env.EMAIL_FOOTER_IMAGE_PATH)) { - attachments.push({ - filename: 'logo.png', - path: process.env.EMAIL_FOOTER_IMAGE_PATH, - cid: 'logo1', - }); - } else { - logger.logWarn('Email footer image path does not exist', { - path: process.env.EMAIL_FOOTER_IMAGE_PATH, - }); - } - } - - return attachments; - } - - protected createTransport(): NodeMailerTransportOptions | undefined { - return undefined; - } + protected abstract createAttachments(): any[]; + protected abstract createTransport(): NodeMailerTransportOptions | undefined; protected createEmailTemplates() { if (this.emailTemplates) { From f14e5ac1827d85fc42fd977a5ac0748044d7d337 Mon Sep 17 00:00:00 2001 From: Gergely Nyiri Date: Tue, 2 Jun 2026 14:18:30 +0200 Subject: [PATCH 11/11] fix: review findings --- .../MailService/MSGraph/MSGraphMailService.ts | 43 +++++++++++++------ .../MailService/SMTP/SMTPMailService.ts | 2 +- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.ts b/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.ts index d331dd1355..3211f132b4 100644 --- a/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.ts +++ b/apps/backend/src/eventHandlers/MailService/MSGraph/MSGraphMailService.ts @@ -16,6 +16,7 @@ class MSGraphTransport implements Transport { private apiUrl: string; private authToken: msal.AuthenticationResult | null = null; private msalClient: msal.ConfidentialClientApplication; + private tokenPromise: Promise | null = null; constructor() { this.name = 'MSGraphTransport'; @@ -33,26 +34,38 @@ class MSGraphTransport implements Transport { protected isTokenExpired(): boolean { if (!this.authToken?.expiresOn) { - return false; + throw new Error('Invalid token: Missing expiresOn property'); } return Date.now() >= this.authToken.expiresOn.getTime(); } - private async getAccessToken(): Promise { + private async getAuthToken(): Promise { + // if we already have a valid token, return it if (this.authToken && !this.isTokenExpired()) { - return this.authToken.accessToken; + return this.authToken; } - this.authToken = await this.msalClient.acquireTokenByClientCredential({ - scopes: [`${this.apiUrl}/.default`], - }); - - if (!this.authToken) { - throw new Error('Failed to acquire access token for Microsoft Graph API'); + // cache the promise to prevent multiple simultaneous token requests + if (!this.tokenPromise) { + this.tokenPromise = (async () => { + try { + this.authToken = await this.msalClient.acquireTokenByClientCredential( + { + scopes: [`${this.apiUrl}/.default`], + } + ); + + logger.logInfo('Acquired new access token for Microsoft Graph API', { + expiresOn: this.authToken?.expiresOn, + }); + } finally { + this.tokenPromise = null; + } + })(); } - return this.authToken?.accessToken; + return this.tokenPromise; } public async send( @@ -60,7 +73,13 @@ class MSGraphTransport implements Transport { callback: (err: Error | null, info: unknown) => void ) { try { - const accessToken = await this.getAccessToken(); + const token = await this.getAuthToken(); + + if (!token?.accessToken) { + throw new Error( + 'Failed to acquire access token for Microsoft Graph API' + ); + } const { subject, @@ -104,7 +123,7 @@ class MSGraphTransport implements Transport { { method: 'POST', headers: { - Authorization: `Bearer ${accessToken}`, + Authorization: `Bearer ${token.accessToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify(payload), diff --git a/apps/backend/src/eventHandlers/MailService/SMTP/SMTPMailService.ts b/apps/backend/src/eventHandlers/MailService/SMTP/SMTPMailService.ts index b46b068dd7..7f02af9f66 100644 --- a/apps/backend/src/eventHandlers/MailService/SMTP/SMTPMailService.ts +++ b/apps/backend/src/eventHandlers/MailService/SMTP/SMTPMailService.ts @@ -20,7 +20,7 @@ export class SMTPMailService extends TemplateMailService { }); } - protected createTransport(): NodeMailerTransportOptions | any { + protected createTransport(): NodeMailerTransportOptions { let smtpTransport: | Transporter | Transporter;