@@ -6,16 +6,26 @@ package mailer
6
6
7
7
import (
8
8
"bytes"
9
+ "context"
10
+ "encoding/base64"
11
+ "fmt"
9
12
"html/template"
13
+ "io"
10
14
"mime"
11
15
"regexp"
12
16
"strings"
13
17
texttmpl "text/template"
14
18
19
+ repo_model "code.gitea.io/gitea/models/repo"
15
20
user_model "code.gitea.io/gitea/models/user"
21
+ "code.gitea.io/gitea/modules/httplib"
16
22
"code.gitea.io/gitea/modules/log"
17
23
"code.gitea.io/gitea/modules/setting"
24
+ "code.gitea.io/gitea/modules/storage"
25
+ "code.gitea.io/gitea/modules/typesniffer"
18
26
sender_service "code.gitea.io/gitea/services/mailer/sender"
27
+
28
+ "golang.org/x/net/html"
19
29
)
20
30
21
31
const mailMaxSubjectRunes = 256 // There's no actual limit for subject in RFC 5322
@@ -44,6 +54,107 @@ func sanitizeSubject(subject string) string {
44
54
return mime .QEncoding .Encode ("utf-8" , string (runes ))
45
55
}
46
56
57
+ type mailAttachmentBase64Embedder struct {
58
+ doer * user_model.User
59
+ repo * repo_model.Repository
60
+ maxSize int64
61
+ estimateSize int64
62
+ }
63
+
64
+ func newMailAttachmentBase64Embedder (doer * user_model.User , repo * repo_model.Repository , maxSize int64 ) * mailAttachmentBase64Embedder {
65
+ return & mailAttachmentBase64Embedder {doer : doer , repo : repo , maxSize : maxSize }
66
+ }
67
+
68
+ func (b64embedder * mailAttachmentBase64Embedder ) Base64InlineImages (ctx context.Context , body template.HTML ) (template.HTML , error ) {
69
+ doc , err := html .Parse (strings .NewReader (string (body )))
70
+ if err != nil {
71
+ return "" , fmt .Errorf ("html.Parse failed: %w" , err )
72
+ }
73
+
74
+ b64embedder .estimateSize = int64 (len (string (body )))
75
+
76
+ var processNode func (* html.Node )
77
+ processNode = func (n * html.Node ) {
78
+ if n .Type == html .ElementNode {
79
+ if n .Data == "img" {
80
+ for i , attr := range n .Attr {
81
+ if attr .Key == "src" {
82
+ attachmentSrc := attr .Val
83
+ dataURI , err := b64embedder .AttachmentSrcToBase64DataURI (ctx , attachmentSrc )
84
+ if err != nil {
85
+ // Not an error, just skip. This is probably an image from outside the gitea instance.
86
+ log .Trace ("Unable to embed attachment %q to mail body: %v" , attachmentSrc , err )
87
+ } else {
88
+ n .Attr [i ].Val = dataURI
89
+ }
90
+ break
91
+ }
92
+ }
93
+ }
94
+ }
95
+ for c := n .FirstChild ; c != nil ; c = c .NextSibling {
96
+ processNode (c )
97
+ }
98
+ }
99
+
100
+ processNode (doc )
101
+
102
+ var buf bytes.Buffer
103
+ err = html .Render (& buf , doc )
104
+ if err != nil {
105
+ return "" , fmt .Errorf ("html.Render failed: %w" , err )
106
+ }
107
+ return template .HTML (buf .String ()), nil
108
+ }
109
+
110
+ func (b64embedder * mailAttachmentBase64Embedder ) AttachmentSrcToBase64DataURI (ctx context.Context , attachmentSrc string ) (string , error ) {
111
+ parsedSrc := httplib .ParseGiteaSiteURL (ctx , attachmentSrc )
112
+ var attachmentUUID string
113
+ if parsedSrc != nil {
114
+ var ok bool
115
+ attachmentUUID , ok = strings .CutPrefix (parsedSrc .RoutePath , "/attachments/" )
116
+ if ! ok {
117
+ attachmentUUID , ok = strings .CutPrefix (parsedSrc .RepoSubPath , "/attachments/" )
118
+ }
119
+ if ! ok {
120
+ return "" , fmt .Errorf ("not an attachment" )
121
+ }
122
+ }
123
+ attachment , err := repo_model .GetAttachmentByUUID (ctx , attachmentUUID )
124
+ if err != nil {
125
+ return "" , err
126
+ }
127
+
128
+ if attachment .RepoID != b64embedder .repo .ID {
129
+ return "" , fmt .Errorf ("attachment does not belong to the repository" )
130
+ }
131
+ if attachment .Size + b64embedder .estimateSize > b64embedder .maxSize {
132
+ return "" , fmt .Errorf ("total embedded images exceed max limit" )
133
+ }
134
+
135
+ fr , err := storage .Attachments .Open (attachment .RelativePath ())
136
+ if err != nil {
137
+ return "" , err
138
+ }
139
+ defer fr .Close ()
140
+
141
+ lr := & io.LimitedReader {R : fr , N : b64embedder .maxSize + 1 }
142
+ content , err := io .ReadAll (lr )
143
+ if err != nil {
144
+ return "" , fmt .Errorf ("LimitedReader ReadAll: %w" , err )
145
+ }
146
+
147
+ mimeType := typesniffer .DetectContentType (content )
148
+ if ! mimeType .IsImage () {
149
+ return "" , fmt .Errorf ("not an image" )
150
+ }
151
+
152
+ encoded := base64 .StdEncoding .EncodeToString (content )
153
+ dataURI := fmt .Sprintf ("data:%s;base64,%s" , mimeType .GetMimeType (), encoded )
154
+ b64embedder .estimateSize += int64 (len (dataURI ))
155
+ return dataURI , nil
156
+ }
157
+
47
158
func fromDisplayName (u * user_model.User ) string {
48
159
if setting .MailService .FromDisplayNameFormatTemplate != nil {
49
160
var ctx bytes.Buffer
0 commit comments