mirror of
https://github.com/hcengineering/platform.git
synced 2025-06-09 09:20:54 +00:00
UBERF-11239: Fix multipart content in mta-hook (#9110)
Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
parent
86601457da
commit
26cbba64f9
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -394,6 +394,10 @@ describe('handleMtaHook', () => {
|
||||
it('should process HTML email correctly', async () => {
|
||||
// Mock request with HTML content
|
||||
const htmlContent = '<html><body><h1>Hello</h1><p>This is an <b>HTML</b> test email</p></body></html>'
|
||||
const expectedContent = `Hello
|
||||
=====
|
||||
|
||||
This is an **HTML** test email`
|
||||
mockReq = {
|
||||
headers: { 'x-hook-token': 'test-hook-token' },
|
||||
body: createValidMtaMessage('sender@example.com', ['recipient@example.com'], {
|
||||
@ -423,7 +427,7 @@ describe('handleMtaHook', () => {
|
||||
from: { email: 'sender@example.com', firstName: 'sender', lastName: 'example.com' },
|
||||
to: [{ email: 'recipient@example.com', firstName: 'recipient', lastName: 'example.com' }],
|
||||
subject: 'HTML Test Subject',
|
||||
content: htmlContent,
|
||||
content: expectedContent,
|
||||
incoming: true
|
||||
}),
|
||||
[] // attachments
|
||||
|
115
services/mail/pod-inbound-mail/src/__tests__/parseMail.test.ts
Normal file
115
services/mail/pod-inbound-mail/src/__tests__/parseMail.test.ts
Normal file
@ -0,0 +1,115 @@
|
||||
//
|
||||
// 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 fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { MeasureContext } from '@hcengineering/core'
|
||||
import { parseContent } from '../utils'
|
||||
import { type MtaMessage } from '../types'
|
||||
|
||||
// Mock config to ensure storage is available for tests
|
||||
jest.mock('../config', () => ({
|
||||
storageConfig: {
|
||||
// Mock minimal storage config to ensure attachments are processed
|
||||
type: 'fs',
|
||||
url: '/tmp'
|
||||
}
|
||||
}))
|
||||
|
||||
// Create a mock logger context
|
||||
const mockContext = {
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn()
|
||||
} as unknown as MeasureContext
|
||||
|
||||
describe('parseContent', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
test('should parse email with attachment', async () => {
|
||||
const attachmentEml = await fs.readFile(path.join(__dirname, '__mocks__/attachment.txt'), 'utf-8')
|
||||
// Create MTA message from the sample email file
|
||||
const mtaMessage: MtaMessage = {
|
||||
envelope: {
|
||||
from: { address: 'test1@example.com' },
|
||||
to: [{ address: 'test2@example.com' }]
|
||||
},
|
||||
message: {
|
||||
headers: [['Content-Type', 'multipart/mixed; boundary="000000000000384e1705e9d0930b"']],
|
||||
contents: attachmentEml
|
||||
}
|
||||
}
|
||||
|
||||
const result = await parseContent(mockContext, mtaMessage)
|
||||
|
||||
// Verify content was parsed
|
||||
expect(result.content).toContain('Attached huly.png')
|
||||
|
||||
// Verify attachment was extracted
|
||||
expect(result.attachments.length).toBe(1)
|
||||
|
||||
// Check attachment properties
|
||||
const attachment = result.attachments[0]
|
||||
expect(attachment).toHaveProperty('id')
|
||||
expect(attachment).toHaveProperty('name')
|
||||
expect(attachment).toHaveProperty('data')
|
||||
expect(attachment).toHaveProperty('contentType')
|
||||
|
||||
// Verify attachment data is a Buffer
|
||||
expect(Buffer.isBuffer(attachment.data)).toBe(true)
|
||||
expect(attachment.name).toBe('huly.png')
|
||||
})
|
||||
|
||||
test('should parse email with 2 attachments', async () => {
|
||||
const attachmentEml = await fs.readFile(path.join(__dirname, '__mocks__/2attachments.txt'), 'utf-8')
|
||||
// Create MTA message from the sample email file
|
||||
const mtaMessage: MtaMessage = {
|
||||
envelope: {
|
||||
from: { address: 'test1@example.com' },
|
||||
to: [{ address: 'test2@example.com' }]
|
||||
},
|
||||
message: {
|
||||
headers: [['Content-Type', 'multipart/mixed; boundary="00000000000016290e0636150598"']],
|
||||
contents: attachmentEml
|
||||
}
|
||||
}
|
||||
|
||||
const result = await parseContent(mockContext, mtaMessage)
|
||||
|
||||
// Verify content was parsed
|
||||
expect(result.content).toContain('Send huly.png and cat.png')
|
||||
|
||||
// Verify attachment was extracted
|
||||
expect(result.attachments.length).toBe(2)
|
||||
|
||||
// Check attachment properties
|
||||
const attachment = result.attachments[0]
|
||||
expect(attachment).toHaveProperty('id')
|
||||
expect(attachment).toHaveProperty('name')
|
||||
expect(attachment).toHaveProperty('data')
|
||||
expect(attachment).toHaveProperty('contentType')
|
||||
|
||||
// Verify attachment data is a Buffer
|
||||
expect(Buffer.isBuffer(attachment.data)).toBe(true)
|
||||
expect(attachment.name).toBe('huly.png')
|
||||
|
||||
const catAttachment = result.attachments[1]
|
||||
// Verify attachment data is a Buffer
|
||||
expect(Buffer.isBuffer(catAttachment.data)).toBe(true)
|
||||
expect(catAttachment.name).toBe('cat.png')
|
||||
})
|
||||
})
|
@ -27,7 +27,7 @@ export async function parseContent (
|
||||
mta: MtaMessage
|
||||
): Promise<{ content: string, attachments: Attachment[] }> {
|
||||
// TODO: UBERF-11029 - remove this logging after testing
|
||||
ctx.info('Parsing email content', { content: mta.message.contents })
|
||||
ctx.info('Parsing email content', { mta })
|
||||
const contentType = getHeader(mta, 'Content-Type')
|
||||
if (contentType === undefined) {
|
||||
throw new Error('Content-Type header not found')
|
||||
@ -37,7 +37,7 @@ export async function parseContent (
|
||||
return { content: mta.message.contents, attachments: [] }
|
||||
}
|
||||
|
||||
const email = await getEmailContent(mta.message.contents, contentType)
|
||||
const email = await getEmailContent(mta)
|
||||
|
||||
let content = email.text ?? ''
|
||||
let isMarkdown = false
|
||||
@ -83,23 +83,34 @@ export async function parseContent (
|
||||
return { content, attachments }
|
||||
}
|
||||
|
||||
export function convertMtaToEml (mta: MtaMessage): string {
|
||||
return `MIME-Version: 1.0
|
||||
Date: ${new Date().toUTCString()}
|
||||
From: ${mta.envelope.from.address}
|
||||
To: ${mta.envelope.to.map((to) => to.address).join(', ')}
|
||||
Content-Type: ${getHeader(mta, 'Content-Type') ?? 'text/plain; charset=utf-8'}
|
||||
|
||||
${unescapeString(mta.message.contents)}`
|
||||
}
|
||||
|
||||
function unescapeString (str: string): string {
|
||||
return str
|
||||
.replace(/\\n/g, '\n')
|
||||
.replace(/\\r/g, '\r')
|
||||
.replace(/\\t/g, '\t')
|
||||
.replace(/\\"/g, '"')
|
||||
.replace(/\\\\/g, '\\')
|
||||
}
|
||||
|
||||
export function getHeader (mta: MtaMessage, header: string): string | undefined {
|
||||
const h = header.toLowerCase()
|
||||
return mta.message.headers.find((header) => header[0].toLowerCase() === h)?.[1]?.trim()
|
||||
}
|
||||
|
||||
async function getEmailContent (mtaContent: string, contentType: string): Promise<ReadedEmlJson> {
|
||||
if (mtaContent == null) {
|
||||
return {
|
||||
text: '',
|
||||
html: '',
|
||||
attachments: []
|
||||
} as any
|
||||
}
|
||||
const contentRegex = /Content-Type/i
|
||||
const content = contentRegex.test(mtaContent) ? mtaContent : `Content-Type: ${contentType}\r\n${mtaContent}`
|
||||
async function getEmailContent (mta: MtaMessage): Promise<ReadedEmlJson> {
|
||||
const eml = convertMtaToEml(mta)
|
||||
const email = await new Promise<ReadedEmlJson>((resolve, reject) => {
|
||||
readEml(content, (err, json) => {
|
||||
readEml(eml, (err, json) => {
|
||||
if (err !== undefined && err !== null) {
|
||||
reject(new Error(`Email parsing error: ${err.message}`))
|
||||
} else if (json === undefined) {
|
||||
@ -112,7 +123,7 @@ async function getEmailContent (mtaContent: string, contentType: string): Promis
|
||||
if (isEmptyString(email.text) && isEmptyString(email.html)) {
|
||||
return {
|
||||
...email,
|
||||
text: removeContentTypeHeader(mtaContent)
|
||||
text: removeContentTypeHeader(mta.message.contents)
|
||||
}
|
||||
}
|
||||
return email
|
||||
|
Loading…
Reference in New Issue
Block a user