Build CID attachment URLs against the API route and base path (#582)

* 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

* fix: omit the base path prefix when it resolves to the root

A caller passing basePath '/' produced //api/email/..., which a browser reads as a protocol-relative URL rather than a path. Strip the slashes first and only add a prefix when something is left.
This commit is contained in:
dualfroz committed 2026-09-22 14:00:23 -04:00
1 parent 77bb28b844
commit ed77334df2
6 files changed
+69 -13

No files matched your search

+40 -3
View File
@@ -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,44 @@ 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 treat basePath "/" as no prefix', () => {
const html = '<img src="cid:image123">'
const attachments = [{ contentId: 'image123', generatedFileName: 'abc.png' }]
const result = replaceCidReferences(html, 'email1', attachments, undefined, '/')
expect(result).toContain('src="/api/email/email1/attachment/abc.png"')
})
it('should combine baseUrl and basePath', () => {
const html = '<img src="cid:image123">'
const attachments = [{ contentId: 'image123', generatedFileName: 'abc.png' }]
const result = replaceCidReferences(html, 'email1', attachments, 'example.com', '/mail')
expect(result).toContain('src="//example.com/mail/api/email/email1/attachment/abc.png"')
})
it('should replace multiple cid references', () => {