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

+12 -6
View File
@@ -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,19 @@ 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 segments = basePath ? basePath.replace(/^\/+|\/+$/g, '') : ''
const prefix = segments ? `/${segments}` : ''
return `${base}${prefix}/api/email/${emailId}/attachment/${encodeURIComponent(filename)}`
}
/**