Skip to content

Commit 17439b9

Browse files
committed
Option BASE64_EMBED_IMAGES (default false) in mail settings to inline image attachments
1 parent f528df9 commit 17439b9

File tree

3 files changed

+97
-0
lines changed

3 files changed

+97
-0
lines changed

custom/conf/app.example.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1704,6 +1704,9 @@ LEVEL = Info
17041704
;;
17051705
;; convert \r\n to \n for Sendmail
17061706
;SENDMAIL_CONVERT_CRLF = true
1707+
;;
1708+
;; convert links of attached images to inline images
1709+
;B64_EMBED_IMAGES = true
17071710

17081711
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
17091712
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

modules/setting/mailer.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type Mailer struct {
2828
SendAsPlainText bool `ini:"SEND_AS_PLAIN_TEXT"`
2929
SubjectPrefix string `ini:"SUBJECT_PREFIX"`
3030
OverrideHeader map[string][]string `ini:"-"`
31+
Base64EmbedImages bool `ini:"BASE64_EMBED_IMAGES"`
3132

3233
// SMTP sender
3334
Protocol string `ini:"PROTOCOL"`
@@ -150,6 +151,7 @@ func loadMailerFrom(rootCfg ConfigProvider) {
150151
sec.Key("SENDMAIL_TIMEOUT").MustDuration(5 * time.Minute)
151152
sec.Key("SENDMAIL_CONVERT_CRLF").MustBool(true)
152153
sec.Key("FROM").MustString(sec.Key("USER").String())
154+
sec.Key("BASE64_EMBED_IMAGES").MustBool(false)
153155

154156
// Now map the values on to the MailService
155157
MailService = &Mailer{}

services/mailer/mail.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ package mailer
77
import (
88
"bytes"
99
"context"
10+
"encoding/base64"
1011
"fmt"
1112
"html/template"
1213
"mime"
14+
"net/http"
1315
"regexp"
1416
"strconv"
1517
"strings"
@@ -26,11 +28,13 @@ import (
2628
"code.gitea.io/gitea/modules/markup"
2729
"code.gitea.io/gitea/modules/markup/markdown"
2830
"code.gitea.io/gitea/modules/setting"
31+
"code.gitea.io/gitea/modules/storage"
2932
"code.gitea.io/gitea/modules/timeutil"
3033
"code.gitea.io/gitea/modules/translation"
3134
incoming_payload "code.gitea.io/gitea/services/mailer/incoming/payload"
3235
"code.gitea.io/gitea/services/mailer/token"
3336

37+
"golang.org/x/net/html"
3438
"gopkg.in/gomail.v2"
3539
)
3640

@@ -232,6 +236,15 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
232236
return nil, err
233237
}
234238

239+
if setting.MailService.Base64EmbedImages {
240+
bodyStr := string(body)
241+
bodyStr, err = inlineImages(bodyStr, ctx)
242+
if err != nil {
243+
return nil, err
244+
}
245+
body = template.HTML(bodyStr)
246+
}
247+
235248
actType, actName, tplName := actionToTemplate(ctx.Issue, ctx.ActionType, commentType, reviewType)
236249

237250
if actName != "new" {
@@ -363,6 +376,85 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
363376
return msgs, nil
364377
}
365378

379+
func inlineImages(body string, ctx *mailCommentContext) (string, error) {
380+
doc, err := html.Parse(strings.NewReader(body))
381+
if err != nil {
382+
log.Error("Failed to parse HTML body: %v", err)
383+
return "", err
384+
}
385+
386+
var processNode func(*html.Node)
387+
processNode = func(n *html.Node) {
388+
if n.Type == html.ElementNode {
389+
if n.Data == "img" {
390+
// Process <img> tags
391+
for i, attr := range n.Attr {
392+
if attr.Key == "src" {
393+
attachmentPath := attr.Val
394+
// Read the attachment file and encode it as base64
395+
dataURI, err := attachmentSrcToDataURI(attachmentPath, ctx)
396+
if err != nil {
397+
log.Error("attachmentSrcToDataURI failed: %v", err)
398+
continue
399+
}
400+
log.Trace("Old value of src attribute: %s, new value (first 100 characters): %s", attr.Val, dataURI[:100])
401+
n.Attr[i].Val = dataURI
402+
}
403+
}
404+
}
405+
}
406+
407+
if n.FirstChild != nil {
408+
log.Trace("Processing child nodes of <%s>", n.Data)
409+
}
410+
for c := n.FirstChild; c != nil; c = c.NextSibling {
411+
processNode(c)
412+
}
413+
}
414+
415+
processNode(doc)
416+
417+
var buf bytes.Buffer
418+
err = html.Render(&buf, doc)
419+
if err != nil {
420+
log.Error("Failed to render modified HTML: %v", err)
421+
return "", err
422+
}
423+
return buf.String(), nil
424+
}
425+
426+
// Helper function to convert attachment source to data URI
427+
428+
func attachmentSrcToDataURI(attachmentPath string, ctx *mailCommentContext) (string, error) {
429+
parts := strings.Split(attachmentPath, "/attachments/")
430+
if len(parts) <= 1 {
431+
return "", fmt.Errorf("Invalid attachment path: %s", attachmentPath)
432+
}
433+
434+
attachmentUUID := parts[len(parts)-1]
435+
attachment, err := repo_model.GetAttachmentByUUID(ctx, attachmentUUID)
436+
if err != nil {
437+
return "", err
438+
}
439+
440+
fr, err := storage.Attachments.Open(attachment.RelativePath())
441+
if err != nil {
442+
return "", err
443+
}
444+
defer fr.Close()
445+
446+
content := make([]byte, attachment.Size)
447+
if _, err := fr.Read(content); err != nil {
448+
return "", err
449+
}
450+
451+
mimeType := http.DetectContentType(content)
452+
encoded := base64.StdEncoding.EncodeToString(content)
453+
dataURI := fmt.Sprintf("data:%s;base64,%s", mimeType, encoded)
454+
455+
return dataURI, nil
456+
}
457+
366458
func generateMessageIDForIssue(issue *issues_model.Issue, comment *issues_model.Comment, actionType activities_model.ActionType) string {
367459
var path string
368460
if issue.IsPull {

0 commit comments

Comments
 (0)