diff --git a/.changeset/fix-cid-attachment-url.md b/.changeset/fix-cid-attachment-url.md
new file mode 100644
index 0000000..4a525f0
--- /dev/null
+++ b/.changeset/fix-cid-attachment-url.md
@@ -0,0 +1,10 @@
+---
+'@maildev/smtp': patch
+'@maildev/api': patch
+---
+
+Fix the URL that `replaceCidReferences` rewrites inline (CID) attachments to. It
+built `/email/:id/attachment/:file`, which matches no route: the attachment is
+served from `/api/email/:id/attachment/:file`, under the configured base path.
+The URL is now root-relative and includes both parts, so it resolves at the root
+and under a subpath such as `/mail`.
diff --git a/packages/api/src/server.ts b/packages/api/src/server.ts
index b5887ad..671c616 100644
--- a/packages/api/src/server.ts
+++ b/packages/api/src/server.ts
@@ -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' })
}
diff --git a/packages/smtp/src/__tests__/sanitize.test.ts b/packages/smtp/src/__tests__/sanitize.test.ts
index 3d94460..52a4d98 100644
--- a/packages/smtp/src/__tests__/sanitize.test.ts
+++ b/packages/smtp/src/__tests__/sanitize.test.ts
@@ -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 = '
'
+ 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 = '
'
+ 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', () => {
diff --git a/packages/smtp/src/sanitize.ts b/packages/smtp/src/sanitize.ts
index fa54a29..16ae54a 100644
--- a/packages/smtp/src/sanitize.ts
+++ b/packages/smtp/src/sanitize.ts
@@ -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)}`
}
/**
diff --git a/packages/smtp/src/server.ts b/packages/smtp/src/server.ts
index 15f0cb5..c933860 100644
--- a/packages/smtp/src/server.ts
+++ b/packages/smtp/src/server.ts
@@ -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
diff --git a/packages/smtp/src/types.ts b/packages/smtp/src/types.ts
index 427e956..1b81d6b 100644
--- a/packages/smtp/src/types.ts
+++ b/packages/smtp/src/types.ts
@@ -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
}
/**