From 0baf2778896d1f27df91cd4366f89bece1f3cf36 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Thu, 30 Jul 2026 02:35:08 -0400 Subject: [PATCH] fix: ensure relative CID images load in web views Previously, a relative-link CID image in a template would not render properly in the web view or archive of the message; it would use a link relative to the actual campaign URL, which did not contain the media file. This corrects that by properly resolving CID images to real media URLs during rendering, displaying these images properly. --- cmd/archive.go | 4 ++++ cmd/manager_store.go | 10 ++++++++ cmd/public.go | 3 +++ internal/manager/manager.go | 46 +++++++++++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+) diff --git a/cmd/archive.go b/cmd/archive.go index c6282790b..9f5be8d7d 100644 --- a/cmd/archive.go +++ b/cmd/archive.go @@ -244,6 +244,10 @@ func (a *App) compileArchiveCampaigns(camps []models.Campaign) ([]manager.Campai ) for _, c := range camps { camp := c + + // Resolve data-embed images to media URLs as cid: cannot resolve on a web page. + a.manager.ApplyArchiveImages(&camp) + if err := camp.CompileTemplate(a.manager.TemplateFuncs(&camp)); err != nil { a.log.Printf("error compiling template: %v", err) return nil, echo.NewHTTPError(http.StatusInternalServerError, a.i18n.T("public.errorFetchingCampaign")) diff --git a/cmd/manager_store.go b/cmd/manager_store.go index d2972939b..c704737e2 100644 --- a/cmd/manager_store.go +++ b/cmd/manager_store.go @@ -128,6 +128,16 @@ func (s *store) GetInlineAttachmentByFilename(filename string) (models.Attachmen }, cid, nil } +// GetMediaURLByFilename returns the public URL of a media item by filename. +func (s *store) GetMediaURLByFilename(filename string) (string, error) { + m, err := s.core.GetMedia(0, "", filename, s.media) + if err != nil { + return "", err + } + + return m.URL, nil +} + // CreateLink registers a URL with a UUID for tracking clicks and returns the UUID. func (s *store) CreateLink(url string) (string, error) { // Create a new UUID for the URL. If the URL already exists in the DB diff --git a/cmd/public.go b/cmd/public.go index b81e51ba7..803ef1052 100644 --- a/cmd/public.go +++ b/cmd/public.go @@ -174,6 +174,9 @@ func (a *App) ViewCampaignMessage(c echo.Context) error { makeMsgTpl(a.i18n.T("public.errorTitle"), "", a.i18n.Ts("public.errorFetchingCampaign"))) } + // Resolve data-embed images to media URLs as cid: cannot resolve on a web page. + a.manager.ApplyArchiveImages(&camp) + // Compile the template. if err := camp.CompileTemplate(a.manager.TemplateFuncs(&camp)); err != nil { a.log.Printf("error compiling template: %v", err) diff --git a/internal/manager/manager.go b/internal/manager/manager.go index 643187b15..ae08f6443 100644 --- a/internal/manager/manager.go +++ b/internal/manager/manager.go @@ -52,6 +52,7 @@ type Store interface { GetCampaign(campID int) (*models.Campaign, error) GetAttachment(mediaID int) (models.Attachment, error) GetInlineAttachmentByFilename(filename string) (models.Attachment, string, error) + GetMediaURLByFilename(filename string) (string, error) UpdateCampaignStatus(campID int, status string) error UpdateCampaignCounts(campID int, toSend int, sent int, lastSubID int) error CreateLink(url string) (string, error) @@ -766,6 +767,51 @@ func (m *Manager) applyInlineImages(body string, cache map[string]string) (strin return out, atts } +// ApplyArchiveImages resolves any tags in the campaign +// body and template body to public media URLs. The archive is rendered as a +// standalone HTML page and not a MIME message, so cid: cannot resolve there. +func (m *Manager) ApplyArchiveImages(c *models.Campaign) { + if c.ContentType == models.CampaignContentTypePlain { + return + } + + cache := make(map[string]string) + c.Body = m.applyArchiveImages(c.Body, cache) + c.TemplateBody = m.applyArchiveImages(c.TemplateBody, cache) +} + +func (m *Manager) applyArchiveImages(body string, cache map[string]string) string { + if !strings.Contains(body, attribInlineEmbed) { + return body + } + + return reInlineImage.ReplaceAllStringFunc(body, func(tag string) string { + src := extractSrc(tag) + if src == "" || strings.HasPrefix(strings.ToLower(src), "cid:") { + return tag + } + + fname := filenameFromSrc(src) + if fname == "" { + return tag + } + + u, ok := cache[src] + if !ok { + s, err := m.store.GetMediaURLByFilename(fname) + if err != nil { + m.log.Printf("archive image %q not resolved: %v", src, err) + } + u = s + cache[src] = u + } + if u == "" { + return tag + } + return reImgSrc.ReplaceAllString(tag, `${1}src="`+u+`"`) + }) +} + // MakeContentID returns a standard `Content-ID` value (without the angle brackets). func MakeContentID(key string) string { sum := sha1.Sum([]byte(key))