diff --git a/services/mail/pod-mail/src/__tests__/config.test.ts b/services/mail/pod-mail/src/__tests__/config.test.ts index 7d9ad2891a..380fb8ec3e 100644 --- a/services/mail/pod-mail/src/__tests__/config.test.ts +++ b/services/mail/pod-mail/src/__tests__/config.test.ts @@ -35,6 +35,7 @@ describe('Config', () => { process.env.SMTP_HOST = 'smtp.example.com' process.env.SMTP_PORT = '587' process.env.SMTP_USERNAME = 'user' + process.env.SMTP_PASSWORD = undefined // eslint-disable-next-line @typescript-eslint/no-var-requires const loadedConfig = require('../config').default diff --git a/services/mail/pod-mail/src/__tests__/main.test.ts b/services/mail/pod-mail/src/__tests__/main.test.ts index 9020308b36..ea740f3b80 100644 --- a/services/mail/pod-mail/src/__tests__/main.test.ts +++ b/services/mail/pod-mail/src/__tests__/main.test.ts @@ -22,12 +22,15 @@ jest.mock('../mail', () => ({ sendMessage: jest.fn() })) })) -jest.mock('../config', () => ({})) +jest.mock('../config', () => ({ + source: 'noreply@example.com' +})) describe('handleSendMail', () => { let req: Request let res: Response let sendMailMock: jest.Mock + let mailClient: MailClient beforeEach(() => { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions @@ -44,7 +47,8 @@ describe('handleSendMail', () => { send: jest.fn() } as unknown as Response - sendMailMock = (new MailClient().sendMessage as jest.Mock).mockResolvedValue({}) + mailClient = new MailClient() + sendMailMock = (mailClient.sendMessage as jest.Mock).mockResolvedValue({}) }) it('should return 400 if text is missing', async () => { @@ -84,4 +88,45 @@ describe('handleSendMail', () => { expect(res.send).toHaveBeenCalled() // Check that a response is still sent }) + + it('should use source from config if from is not provided', async () => { + await handleSendMail(mailClient, req, res) + + expect(sendMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + from: 'noreply@example.com', // Verify that the default source from config is used + to: 'test@example.com', + subject: 'Test Subject', + text: 'Hello, world!' + }) + ) + }) + + it('should use from if it is provided', async () => { + req.body.from = 'test.from@example.com' + await handleSendMail(mailClient, req, res) + + expect(sendMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + from: 'test.from@example.com', // Verify that the from is used + to: 'test@example.com', + subject: 'Test Subject', + text: 'Hello, world!' + }) + ) + }) + + it('should send to multiple addresses', async () => { + req.body.to = ['test1@example.com', 'test2@example.com'] + await handleSendMail(mailClient, req, res) + + expect(sendMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + from: 'noreply@example.com', + to: ['test1@example.com', 'test2@example.com'], // Verify that multiple addresses are passed + subject: 'Test Subject', + text: 'Hello, world!' + }) + ) + }) }) diff --git a/services/mail/pod-mail/src/mail.ts b/services/mail/pod-mail/src/mail.ts index 36bc581020..ca7d312020 100644 --- a/services/mail/pod-mail/src/mail.ts +++ b/services/mail/pod-mail/src/mail.ts @@ -26,10 +26,12 @@ export class MailClient { async sendMessage (message: SendMailOptions): Promise { this.transporter.sendMail(message, (err, info) => { + const messageInfo = `(from: ${message.from as string}, to: ${message.to as string})` if (err !== null) { - console.error('Failed to send email: ', err.message) + console.error(`Failed to send email ${messageInfo}: `, err.message) + console.log('Failed message details: ', message) } else { - console.log(`Email request ${info?.messageId} sent: ${info?.response}`) + console.log(`Email request ${messageInfo} sent: ${info?.response}`) } }) } diff --git a/services/mail/pod-mail/src/main.ts b/services/mail/pod-mail/src/main.ts index 306b29b3b7..cb3e4f9d2a 100644 --- a/services/mail/pod-mail/src/main.ts +++ b/services/mail/pod-mail/src/main.ts @@ -15,6 +15,7 @@ import { type SendMailOptions } from 'nodemailer' import { Request, Response } from 'express' +import Mail from 'nodemailer/lib/mailer' import config from './config' import { createServer, listen } from './server' @@ -55,19 +56,36 @@ export const main = async (): Promise => { export async function handleSendMail (client: MailClient, req: Request, res: Response): Promise { // Skip auth check, since service should be internal - const message: SendMailOptions = req.body - if (message?.text === undefined) { + const { from, to, subject, text, html, attachments } = req.body + const fromAddress = from ?? config.source + if (text === undefined) { res.status(400).send({ err: "'text' is missing" }) return } - if (message?.subject === undefined) { + if (subject === undefined) { res.status(400).send({ err: "'subject' is missing" }) return } - if (message?.to === undefined) { + if (to === undefined) { res.status(400).send({ err: "'to' is missing" }) return } + if (fromAddress === undefined) { + res.status(400).send({ err: "'from' is missing" }) + return + } + const message: SendMailOptions = { + from: fromAddress, + to, + subject, + text + } + if (html !== undefined) { + message.html = html + } + if (attachments !== undefined) { + message.attachments = getAttachments(attachments) + } try { await client.sendMessage(message) } catch (err) { @@ -76,3 +94,27 @@ export async function handleSendMail (client: MailClient, req: Request, res: Res res.send() } + +function getAttachments (attachments: any): Mail.Attachment[] | undefined { + if (attachments === undefined || attachments === null) { + return undefined + } + if (!Array.isArray(attachments)) { + console.error('attachments is not array') + return undefined + } + return attachments.map((a) => { + const attachment: Mail.Attachment = { + content: a.content, + contentType: a.contentType, + path: a.path, + filename: a.filename, + cid: a.cid, + encoding: a.encoding, + contentTransferEncoding: a.contentTransferEncoding, + headers: a.headers, + raw: a.raw + } + return attachment + }) +}