1
1
mirror of https://github.com/go-gitea/gitea synced 2025-07-22 18:28:37 +00:00

Email option to embed images as base64 instead of link (#32061)

ref: #15081
ref: #14037

Documentation: https://gitea.com/gitea/docs/pulls/69

# Example
Content:

![image](https://github.com/user-attachments/assets/e73ebfbe-e329-40f6-9c4a-f73832bbb181)
Result in Email:

![image](https://github.com/user-attachments/assets/55b7019f-e17a-46c3-a374-3b4769d5c2d6)
Result with source code:
(first image is external image, 2nd is now embedded)

![image](https://github.com/user-attachments/assets/8e2804a1-580f-4a69-adcb-cc5d16f7da81)

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
sommerf-lf
2025-03-05 17:29:29 +01:00
committed by GitHub
parent f0f10413ae
commit 7cdde20c73
7 changed files with 328 additions and 28 deletions

View File

@@ -102,25 +102,77 @@ func MakeAbsoluteURL(ctx context.Context, link string) string {
return GuessCurrentHostURL(ctx) + "/" + strings.TrimPrefix(link, "/")
}
func IsCurrentGiteaSiteURL(ctx context.Context, s string) bool {
type urlType int
const (
urlTypeGiteaAbsolute urlType = iota + 1 // "http://gitea/subpath"
urlTypeGiteaPageRelative // "/subpath"
urlTypeGiteaSiteRelative // "?key=val"
urlTypeUnknown // "http://other"
)
func detectURLRoutePath(ctx context.Context, s string) (routePath string, ut urlType) {
u, err := url.Parse(s)
if err != nil {
return false
return "", urlTypeUnknown
}
cleanedPath := ""
if u.Path != "" {
cleanedPath := util.PathJoinRelX(u.Path)
if cleanedPath == "" || cleanedPath == "." {
u.Path = "/"
} else {
u.Path = "/" + cleanedPath + "/"
}
cleanedPath = util.PathJoinRelX(u.Path)
cleanedPath = util.Iif(cleanedPath == ".", "", "/"+cleanedPath)
}
if urlIsRelative(s, u) {
return u.Path == "" || strings.HasPrefix(strings.ToLower(u.Path), strings.ToLower(setting.AppSubURL+"/"))
}
if u.Path == "" {
u.Path = "/"
if u.Path == "" {
return "", urlTypeGiteaPageRelative
}
if strings.HasPrefix(strings.ToLower(cleanedPath+"/"), strings.ToLower(setting.AppSubURL+"/")) {
return cleanedPath[len(setting.AppSubURL):], urlTypeGiteaSiteRelative
}
return "", urlTypeUnknown
}
u.Path = cleanedPath + "/"
urlLower := strings.ToLower(u.String())
return strings.HasPrefix(urlLower, strings.ToLower(setting.AppURL)) || strings.HasPrefix(urlLower, strings.ToLower(GuessCurrentAppURL(ctx)))
if strings.HasPrefix(urlLower, strings.ToLower(setting.AppURL)) {
return cleanedPath[len(setting.AppSubURL):], urlTypeGiteaAbsolute
}
guessedCurURL := GuessCurrentAppURL(ctx)
if strings.HasPrefix(urlLower, strings.ToLower(guessedCurURL)) {
return cleanedPath[len(setting.AppSubURL):], urlTypeGiteaAbsolute
}
return "", urlTypeUnknown
}
func IsCurrentGiteaSiteURL(ctx context.Context, s string) bool {
_, ut := detectURLRoutePath(ctx, s)
return ut != urlTypeUnknown
}
type GiteaSiteURL struct {
RoutePath string
OwnerName string
RepoName string
RepoSubPath string
}
func ParseGiteaSiteURL(ctx context.Context, s string) *GiteaSiteURL {
routePath, ut := detectURLRoutePath(ctx, s)
if ut == urlTypeUnknown || ut == urlTypeGiteaPageRelative {
return nil
}
ret := &GiteaSiteURL{RoutePath: routePath}
fields := strings.SplitN(strings.TrimPrefix(ret.RoutePath, "/"), "/", 3)
// TODO: now it only does a quick check for some known reserved paths, should do more strict checks in the future
if fields[0] == "attachments" {
return ret
}
if len(fields) < 2 {
return ret
}
ret.OwnerName = fields[0]
ret.RepoName = fields[1]
if len(fields) == 3 {
ret.RepoSubPath = "/" + fields[2]
}
return ret
}

View File

@@ -122,3 +122,26 @@ func TestIsCurrentGiteaSiteURL(t *testing.T) {
assert.True(t, IsCurrentGiteaSiteURL(ctx, "https://user-host"))
assert.False(t, IsCurrentGiteaSiteURL(ctx, "https://forwarded-host"))
}
func TestParseGiteaSiteURL(t *testing.T) {
defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/sub/")()
defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
ctx := t.Context()
tests := []struct {
url string
exp *GiteaSiteURL
}{
{"http://localhost:3000/sub?k=v", &GiteaSiteURL{RoutePath: ""}},
{"http://localhost:3000/sub/", &GiteaSiteURL{RoutePath: ""}},
{"http://localhost:3000/sub/foo", &GiteaSiteURL{RoutePath: "/foo"}},
{"http://localhost:3000/sub/foo/bar", &GiteaSiteURL{RoutePath: "/foo/bar", OwnerName: "foo", RepoName: "bar"}},
{"http://localhost:3000/sub/foo/bar/", &GiteaSiteURL{RoutePath: "/foo/bar", OwnerName: "foo", RepoName: "bar"}},
{"http://localhost:3000/sub/attachments/bar", &GiteaSiteURL{RoutePath: "/attachments/bar"}},
{"http://localhost:3000/other", nil},
{"http://other/", nil},
}
for _, test := range tests {
su := ParseGiteaSiteURL(ctx, test.url)
assert.Equal(t, test.exp, su, "URL = %s", test.url)
}
}

View File

@@ -13,7 +13,7 @@ import (
"code.gitea.io/gitea/modules/log"
shellquote "github.com/kballard/go-shellquote"
"github.com/kballard/go-shellquote"
)
// Mailer represents mail service.
@@ -29,6 +29,9 @@ type Mailer struct {
SubjectPrefix string `ini:"SUBJECT_PREFIX"`
OverrideHeader map[string][]string `ini:"-"`
// Embed attachment images as inline base64 img src attribute
EmbedAttachmentImages bool
// SMTP sender
Protocol string `ini:"PROTOCOL"`
SMTPAddr string `ini:"SMTP_ADDR"`