mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-07 18:27:44 +02:00
UBERF-9557: Support attachments in mail service (#8139)
* UBERF-9557: Support attachments in mail service Signed-off-by: Artem Savchenko <armisav@gmail.com> * UBERF-9557: Fix formatting Signed-off-by: Artem Savchenko <armisav@gmail.com> * UBERF-9557: Fix tests Signed-off-by: Artem Savchenko <armisav@gmail.com> --------- Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
@@ -53,6 +53,32 @@ Send an email message.
|
||||
- `subject`: Required. String containing the email subject.
|
||||
- `html`: Optional. String containing HTML message body.
|
||||
- `from`: Optional. Sender's email address.
|
||||
- `attachments`: Optional. Array of objects, each object can have the following fields:
|
||||
- `filename`: Filename to be reported as the name of the attached file. Use of unicode is allowed.
|
||||
- `contentType`: Optional. Content type for the attachment, if not set will be derived from the filename property.
|
||||
- `content`: String, Buffer, or a Stream contents for the attachment.
|
||||
- `href`: Optional. An URL to the file (data URIs are allowed as well).
|
||||
- `contentDisposition`: Optional. Content disposition type for the attachment, defaults to ‘attachment’.
|
||||
- `cid`: Optional. Content id for using inline images in HTML message source.
|
||||
- `encoding`: Optional. If set and content is a string, then encodes the content to a Buffer using the specified encoding. Example values: ‘base64’, ‘hex’, ‘binary’ etc. Useful if you want to use binary attachments in a JSON formatted email object.
|
||||
- `raw`: An optional special value that overrides the entire contents of the current MIME node, including MIME headers. Useful if you want to prepare node contents yourself.
|
||||
|
||||
Request body example:
|
||||
```
|
||||
{
|
||||
"subject": "Test SMTP",
|
||||
"text": "My text",
|
||||
"from": "test1@example.com",
|
||||
"to": "test2@example.com",
|
||||
"attachments": [
|
||||
{
|
||||
"filename": "test.txt",
|
||||
"content": "Hello world",
|
||||
"contentType": "text/plain"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Request, Response } from 'express'
|
||||
import { MailClient } from '../mail'
|
||||
import { handleSendMail } from '../main'
|
||||
|
||||
jest.mock('../mail', () => ({
|
||||
MailClient: jest.fn().mockImplementation(() => ({
|
||||
sendMessage: jest.fn()
|
||||
}))
|
||||
}))
|
||||
jest.mock('../config', () => ({}))
|
||||
|
||||
describe('handleSendMail', () => {
|
||||
let req: Request
|
||||
let res: Response
|
||||
let sendMailMock: jest.Mock
|
||||
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
req = {
|
||||
body: {
|
||||
text: 'Hello, world!',
|
||||
subject: 'Test Subject',
|
||||
to: 'test@example.com'
|
||||
}
|
||||
} as Request
|
||||
|
||||
res = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
send: jest.fn()
|
||||
} as unknown as Response
|
||||
|
||||
sendMailMock = (new MailClient().sendMessage as jest.Mock).mockResolvedValue({})
|
||||
})
|
||||
|
||||
it('should return 400 if text is missing', async () => {
|
||||
req.body.text = undefined
|
||||
|
||||
await handleSendMail(new MailClient(), req, res)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
expect(res.status).toHaveBeenCalledWith(400)
|
||||
expect(res.send).toHaveBeenCalledWith({ err: "'text' is missing" })
|
||||
})
|
||||
|
||||
it('should return 400 if subject is missing', async () => {
|
||||
req.body.subject = undefined
|
||||
|
||||
await handleSendMail(new MailClient(), req, res)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
expect(res.status).toHaveBeenCalledWith(400)
|
||||
expect(res.send).toHaveBeenCalledWith({ err: "'subject' is missing" })
|
||||
})
|
||||
|
||||
it('should return 400 if to is missing', async () => {
|
||||
req.body.to = undefined
|
||||
|
||||
await handleSendMail(new MailClient(), req, res)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
expect(res.status).toHaveBeenCalledWith(400)
|
||||
expect(res.send).toHaveBeenCalledWith({ err: "'to' is missing" })
|
||||
})
|
||||
|
||||
it('handles errors thrown by MailClient', async () => {
|
||||
sendMailMock.mockRejectedValue(new Error('Email service error'))
|
||||
|
||||
await handleSendMail(new MailClient(), req, res)
|
||||
|
||||
expect(res.send).toHaveBeenCalled() // Check that a response is still sent
|
||||
})
|
||||
})
|
||||
@@ -15,7 +15,6 @@
|
||||
import { type SendMailOptions, type Transporter } from 'nodemailer'
|
||||
|
||||
import config from './config'
|
||||
import { Message, Receivers } from './types'
|
||||
import { getTransport } from './transport'
|
||||
|
||||
export class MailClient {
|
||||
@@ -25,24 +24,8 @@ export class MailClient {
|
||||
this.transporter = getTransport(config)
|
||||
}
|
||||
|
||||
async sendMessage (message: Message, receivers: Receivers, from?: string): Promise<void> {
|
||||
const mailOptions: SendMailOptions = {
|
||||
from: from ?? config.source,
|
||||
to: receivers.to,
|
||||
text: message.text,
|
||||
subject: message.subject
|
||||
}
|
||||
if (receivers.cc !== undefined) {
|
||||
mailOptions.cc = receivers.cc
|
||||
}
|
||||
if (receivers.bcc !== undefined) {
|
||||
mailOptions.bcc = receivers.bcc
|
||||
}
|
||||
if (message.html !== undefined) {
|
||||
mailOptions.html = message.html
|
||||
}
|
||||
|
||||
this.transporter.sendMail(mailOptions, (err, info) => {
|
||||
async sendMessage (message: SendMailOptions): Promise<void> {
|
||||
this.transporter.sendMail(message, (err, info) => {
|
||||
if (err !== null) {
|
||||
console.error('Failed to send email: ', err.message)
|
||||
} else {
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type SendMailOptions } from 'nodemailer'
|
||||
import { Request, Response } from 'express'
|
||||
|
||||
import config from './config'
|
||||
import { createServer, listen } from './server'
|
||||
import { MailClient } from './mail'
|
||||
@@ -27,34 +30,7 @@ export const main = async (): Promise<void> => {
|
||||
endpoint: '/send',
|
||||
type: 'post',
|
||||
handler: async (req, res) => {
|
||||
// Skip auth check, since service should be internal
|
||||
const text = req.body?.text
|
||||
if (text === undefined) {
|
||||
res.status(400).send({ err: "'text' is missing" })
|
||||
return
|
||||
}
|
||||
const subject = req.body?.subject
|
||||
if (subject === undefined) {
|
||||
res.status(400).send({ err: "'subject' is missing" })
|
||||
return
|
||||
}
|
||||
const html = req.body?.html
|
||||
const to = req.body?.to
|
||||
if (to === undefined) {
|
||||
res.status(400).send({ err: "'to' is missing" })
|
||||
return
|
||||
}
|
||||
const receivers = {
|
||||
to: Array.isArray(to) ? to : [to]
|
||||
}
|
||||
const from = req.body?.from
|
||||
try {
|
||||
await client.sendMessage({ text, subject, html }, receivers, from)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
|
||||
res.send()
|
||||
await handleSendMail(client, req, res)
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -76,3 +52,27 @@ export const main = async (): Promise<void> => {
|
||||
console.error(e)
|
||||
})
|
||||
}
|
||||
|
||||
export async function handleSendMail (client: MailClient, req: Request, res: Response): Promise<void> {
|
||||
// Skip auth check, since service should be internal
|
||||
const message: SendMailOptions = req.body
|
||||
if (message?.text === undefined) {
|
||||
res.status(400).send({ err: "'text' is missing" })
|
||||
return
|
||||
}
|
||||
if (message?.subject === undefined) {
|
||||
res.status(400).send({ err: "'subject' is missing" })
|
||||
return
|
||||
}
|
||||
if (message?.to === undefined) {
|
||||
res.status(400).send({ err: "'to' is missing" })
|
||||
return
|
||||
}
|
||||
try {
|
||||
await client.sendMessage(message)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
|
||||
res.send()
|
||||
}
|
||||
|
||||
@@ -15,18 +15,6 @@
|
||||
|
||||
import { NextFunction, Request, Response } from 'express'
|
||||
|
||||
export interface Receivers {
|
||||
to: string[]
|
||||
cc?: string[]
|
||||
bcc?: string[]
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
text: string
|
||||
subject: string
|
||||
html?: string
|
||||
}
|
||||
|
||||
export type RequestType = 'get' | 'post'
|
||||
|
||||
export type RequestHandler = (req: Request, res: Response, next?: NextFunction) => Promise<void>
|
||||
|
||||
Reference in New Issue
Block a user