[go: up one dir, main page]

Skip to content

Commit

Permalink
Option BASE64_EMBED_IMAGES (default false) in mail settings to inline…
Browse files Browse the repository at this point in the history
… image attachments
  • Loading branch information
sommerf-lf committed Sep 18, 2024
1 parent f528df9 commit 37428d3
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 0 deletions.
3 changes: 3 additions & 0 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1704,6 +1704,9 @@ LEVEL = Info
;;
;; convert \r\n to \n for Sendmail
;SENDMAIL_CONVERT_CRLF = true
;;
;; convert links of attached images to inline images
;B64_EMBED_IMAGES = true

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Expand Down
2 changes: 2 additions & 0 deletions modules/setting/mailer.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type Mailer struct {
SendAsPlainText bool `ini:"SEND_AS_PLAIN_TEXT"`
SubjectPrefix string `ini:"SUBJECT_PREFIX"`
OverrideHeader map[string][]string `ini:"-"`
Base64EmbedImages bool `ini:"BASE64_EMBED_IMAGES"`

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

// Now map the values on to the MailService
MailService = &Mailer{}
Expand Down
85 changes: 85 additions & 0 deletions services/mailer/mail.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ package mailer
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"html/template"
"mime"
"net/http"
"regexp"
"strconv"
"strings"
Expand All @@ -26,11 +28,13 @@ import (
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/translation"
incoming_payload "code.gitea.io/gitea/services/mailer/incoming/payload"
"code.gitea.io/gitea/services/mailer/token"

"golang.org/x/net/html"
"gopkg.in/gomail.v2"
)

Expand Down Expand Up @@ -232,6 +236,15 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
return nil, err
}

if setting.MailService.Base64EmbedImages {
bodyStr := string(body)
bodyStr, err = inlineImages(bodyStr, ctx)
if err != nil {
return nil, err
}
body = template.HTML(bodyStr)
}

actType, actName, tplName := actionToTemplate(ctx.Issue, ctx.ActionType, commentType, reviewType)

if actName != "new" {
Expand Down Expand Up @@ -363,6 +376,78 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
return msgs, nil
}

func inlineImages(body string, ctx *mailCommentContext) (string, error) {
doc, err := html.Parse(strings.NewReader(body))
if err != nil {
log.Error("Failed to parse HTML body: %v", err)
return "", err
}

var processNode func(*html.Node)
processNode = func(n *html.Node) {
if n.Type == html.ElementNode {
if n.Data == "img" {
for i, attr := range n.Attr {
if attr.Key == "src" {
attachmentPath := attr.Val
dataURI, err := attachmentSrcToDataURI(attachmentPath, ctx)
if err != nil {
log.Trace("attachmentSrcToDataURI not possible: %v", err) // Not an error, just skip. This is probably an image from outside the gitea instance.
continue
}
log.Trace("Old value of src attribute: %s, new value (first 100 characters): %s", attr.Val, dataURI[:100])
n.Attr[i].Val = dataURI
}
}
}
}

for c := n.FirstChild; c != nil; c = c.NextSibling {
processNode(c)
}
}

processNode(doc)

var buf bytes.Buffer
err = html.Render(&buf, doc)
if err != nil {
log.Error("Failed to render modified HTML: %v", err)
return "", err
}
return buf.String(), nil
}

func attachmentSrcToDataURI(attachmentPath string, ctx *mailCommentContext) (string, error) {
parts := strings.Split(attachmentPath, "/attachments/")
if len(parts) <= 1 {
return "", fmt.Errorf("invalid attachment path: %s", attachmentPath)
}

attachmentUUID := parts[len(parts)-1]
attachment, err := repo_model.GetAttachmentByUUID(ctx, attachmentUUID)
if err != nil {
return "", err
}

fr, err := storage.Attachments.Open(attachment.RelativePath())
if err != nil {
return "", err
}
defer fr.Close()

content := make([]byte, attachment.Size)
if _, err := fr.Read(content); err != nil {
return "", err
}

mimeType := http.DetectContentType(content)
encoded := base64.StdEncoding.EncodeToString(content)
dataURI := fmt.Sprintf("data:%s;base64,%s", mimeType, encoded)

return dataURI, nil
}

func generateMessageIDForIssue(issue *issues_model.Issue, comment *issues_model.Comment, actionType activities_model.ActionType) string {
var path string
if issue.IsPull {
Expand Down

0 comments on commit 37428d3

Please sign in to comment.