Skip to content

Commit 37428d3

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

File tree

3 files changed

+90
-0
lines changed

3 files changed

+90
-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: 85 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,78 @@ 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+
for i, attr := range n.Attr {
391+
if attr.Key == "src" {
392+
attachmentPath := attr.Val
393+
dataURI, err := attachmentSrcToDataURI(attachmentPath, ctx)
394+
if err != nil {
395+
log.Trace("attachmentSrcToDataURI not possible: %v", err) // Not an error, just skip. This is probably an image from outside the gitea instance.
396+
continue
397+
}
398+
log.Trace("Old value of src attribute: %s, new value (first 100 characters): %s", attr.Val, dataURI[:100])
399+
n.Attr[i].Val = dataURI
400+
}
401+
}
402+
}
403+
}
404+
405+
for c := n.FirstChild; c != nil; c = c.NextSibling {
406+
processNode(c)
407+
}
408+
}
409+
410+
processNode(doc)
411+
412+
var buf bytes.Buffer
413+
err = html.Render(&buf, doc)
414+
if err != nil {
415+
log.Error("Failed to render modified HTML: %v", err)
416+
return "", err
417+
}
418+
return buf.String(), nil
419+
}
420+
421+
func attachmentSrcToDataURI(attachmentPath string, ctx *mailCommentContext) (string, error) {
422+
parts := strings.Split(attachmentPath, "/attachments/")
423+
if len(parts) <= 1 {
424+
return "", fmt.Errorf("invalid attachment path: %s", attachmentPath)
425+
}
426+
427+
attachmentUUID := parts[len(parts)-1]
428+
attachment, err := repo_model.GetAttachmentByUUID(ctx, attachmentUUID)
429+
if err != nil {
430+
return "", err
431+
}
432+
433+
fr, err := storage.Attachments.Open(attachment.RelativePath())
434+
if err != nil {
435+
return "", err
436+
}
437+
defer fr.Close()
438+
439+
content := make([]byte, attachment.Size)
440+
if _, err := fr.Read(content); err != nil {
441+
return "", err
442+
}
443+
444+
mimeType := http.DetectContentType(content)
445+
encoded := base64.StdEncoding.EncodeToString(content)
446+
dataURI := fmt.Sprintf("data:%s;base64,%s", mimeType, encoded)
447+
448+
return dataURI, nil
449+
}
450+
366451
func generateMessageIDForIssue(issue *issues_model.Issue, comment *issues_model.Comment, actionType activities_model.ActionType) string {
367452
var path string
368453
if issue.IsPull {

0 commit comments

Comments
 (0)