fix: build CID attachment URLs against the API route and base path
replaceCidReferences rewrote cid: references to /email/:id/attachment/:file,
which matches no route: attachments are served from the API server, mounted
at ${basePath}/api. When MailDev sits under a subpath the prefix was missing
as well, which is the setup described in #439.
Emit ${basePath}/api/email/:id/attachment/:file, root-relative by default, and
thread the API server's basePath through getEmailHtml. baseUrl still prefixes
//host for a caller that passes it.
On the current code this does not change what the web UI shows for a typical
message: mailparser already inlines related parts as data: URIs, and a cid:
that survives parsing is one that matches no attachment. replaceCidReferences
is exported from @maildev/smtp, though, so the URL it builds should resolve.
Refs #439
This commit is contained in:
1 parent
1ce2ff856b
commit
8dceaec54a
6 files changed
+53
-13
No files matched your search
@@ -457,11 +457,11 @@ export class APIServer extends EventEmitter {
|
||||
`${apiPath}/email/:id/html`,
|
||||
async (request, reply) => {
|
||||
const { id } = request.params
|
||||
const baseUrl = request.headers.host ?? ''
|
||||
|
||||
try {
|
||||
if (this.smtp) {
|
||||
const html = await this.smtp.getEmailHtml(id, { baseUrl })
|
||||
// No host: the HTML is rendered for a browser already on this origin
|
||||
const html = await this.smtp.getEmailHtml(id, { basePath })
|
||||
if (!html) {
|
||||
return reply.status(404).send({ error: 'Email has no HTML content' })
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ describe('replaceCidReferences', () => {
|
||||
|
||||
const result = replaceCidReferences(html, 'email1', attachments)
|
||||
|
||||
expect(result).toContain('/email/email1/attachment/abc.png')
|
||||
expect(result).toContain('/api/email/email1/attachment/abc.png')
|
||||
expect(result).not.toContain('cid:')
|
||||
})
|
||||
|
||||
@@ -85,7 +85,7 @@ describe('replaceCidReferences', () => {
|
||||
|
||||
const result = replaceCidReferences(html, 'email1', attachments)
|
||||
|
||||
expect(result).toContain('/email/email1/attachment/abc.png')
|
||||
expect(result).toContain('/api/email/email1/attachment/abc.png')
|
||||
})
|
||||
|
||||
it('should include baseUrl when provided', () => {
|
||||
@@ -96,7 +96,30 @@ describe('replaceCidReferences', () => {
|
||||
|
||||
const result = replaceCidReferences(html, 'email1', attachments, 'example.com')
|
||||
|
||||
expect(result).toContain('//example.com/email/email1/attachment/abc.png')
|
||||
expect(result).toContain('//example.com/api/email/email1/attachment/abc.png')
|
||||
})
|
||||
|
||||
it('should include basePath when MailDev is mounted under a subpath', () => {
|
||||
const html = '<img src="cid:image123">'
|
||||
const attachments = [
|
||||
{ contentId: 'image123', generatedFileName: 'abc.png' },
|
||||
]
|
||||
|
||||
const result = replaceCidReferences(html, 'email1', attachments, undefined, '/mail')
|
||||
|
||||
expect(result).toContain('src="/mail/api/email/email1/attachment/abc.png"')
|
||||
})
|
||||
|
||||
it('should normalize a basePath given with or without slashes', () => {
|
||||
const html = '<img src="cid:image123">'
|
||||
const attachments = [
|
||||
{ contentId: 'image123', generatedFileName: 'abc.png' },
|
||||
]
|
||||
|
||||
for (const basePath of ['mail', '/mail', '/mail/']) {
|
||||
const result = replaceCidReferences(html, 'email1', attachments, undefined, basePath)
|
||||
expect(result).toContain('src="/mail/api/email/email1/attachment/abc.png"')
|
||||
}
|
||||
})
|
||||
|
||||
it('should replace multiple cid references', () => {
|
||||
|
||||
@@ -45,14 +45,16 @@ export function sanitizeHtml(html: string | undefined): string | undefined {
|
||||
* @param html - HTML content with cid: references
|
||||
* @param emailId - ID of the email
|
||||
* @param attachments - Array of attachments with contentId
|
||||
* @param baseUrl - Optional base URL for attachment URLs
|
||||
* @param baseUrl - Optional host to prefix as `//host/...`; unset gives a root-relative URL
|
||||
* @param basePath - Optional pathname the API is mounted under, e.g. `/mail`
|
||||
* @returns HTML with cid: references replaced with actual URLs
|
||||
*/
|
||||
export function replaceCidReferences(
|
||||
html: string,
|
||||
emailId: string,
|
||||
attachments: Array<{ contentId?: string; generatedFileName: string }>,
|
||||
baseUrl?: string
|
||||
baseUrl?: string,
|
||||
basePath?: string
|
||||
): string {
|
||||
if (!attachments || attachments.length === 0) {
|
||||
return html
|
||||
@@ -72,7 +74,7 @@ export function replaceCidReferences(
|
||||
'g'
|
||||
)
|
||||
|
||||
const url = buildAttachmentUrl(emailId, attachment.generatedFileName, baseUrl)
|
||||
const url = buildAttachmentUrl(emailId, attachment.generatedFileName, baseUrl, basePath)
|
||||
const replacement = `src="${url}"`
|
||||
|
||||
result = result.replace(regex, replacement)
|
||||
@@ -82,15 +84,18 @@ export function replaceCidReferences(
|
||||
}
|
||||
|
||||
/**
|
||||
* Build URL for an attachment
|
||||
* Build URL for an attachment. The API serves these from `${basePath}/api/email/...`,
|
||||
* so both parts belong here or the src 404s.
|
||||
*/
|
||||
function buildAttachmentUrl(
|
||||
emailId: string,
|
||||
filename: string,
|
||||
baseUrl?: string
|
||||
baseUrl?: string,
|
||||
basePath?: string
|
||||
): string {
|
||||
const base = baseUrl ? `//${baseUrl}` : ''
|
||||
return `${base}/email/${emailId}/attachment/${encodeURIComponent(filename)}`
|
||||
const prefix = basePath ? `/${basePath.replace(/^\/|\/$/g, '')}` : ''
|
||||
return `${base}${prefix}/api/email/${emailId}/attachment/${encodeURIComponent(filename)}`
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -258,7 +258,7 @@ export class SMTPServer extends EventEmitter {
|
||||
|
||||
// Replace CID references with actual URLs
|
||||
if (email.attachments && email.attachments.length > 0) {
|
||||
html = replaceCidReferences(html, id, email.attachments, options.baseUrl)
|
||||
html = replaceCidReferences(html, id, email.attachments, options.baseUrl, options.basePath)
|
||||
}
|
||||
|
||||
return html
|
||||
|
||||
@@ -218,8 +218,10 @@ export interface RelayResult {
|
||||
* HTML rendering options for email
|
||||
*/
|
||||
export interface HTMLRenderOptions {
|
||||
/** Base URL for attachment URLs */
|
||||
/** Host to prefix attachment URLs with; unset gives a root-relative URL */
|
||||
baseUrl?: string
|
||||
/** Pathname the API is mounted under, e.g. `/mail` */
|
||||
basePath?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user