Skip to content

Commit 14dff38

Browse files
committed
feat: insert image in mail body
1 parent 8a37abb commit 14dff38

File tree

6 files changed

+197
-54
lines changed

6 files changed

+197
-54
lines changed

cmd/server/main.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type Config struct {
4040
} `yaml:"smtp"`
4141
Templates struct {
4242
Email string `yaml:"email"`
43+
Image string `yaml:"image"`
4344
} `yaml:"templates"`
4445
}
4546

@@ -99,7 +100,7 @@ func main() {
99100
})
100101
log.Println("SMTP client initialized.")
101102

102-
tmplManager := email.NewTemplateManager(cfg.Templates.Email)
103+
tmplManager := email.NewTemplateManager(cfg.Templates.Email, cfg.Templates.Image)
103104
files, err := os.ReadDir(cfg.Templates.Email)
104105
if err != nil {
105106
log.Fatalf("템플릿 디렉터리 읽기 실패: %v", err)
@@ -113,8 +114,7 @@ func main() {
113114
}
114115
}
115116

116-
apiHandler := web.NewAPIHandler(oidcSvc, tmplManager, smtpClient, authClient)
117-
117+
apiHandler := web.NewAPIHandler(oidcSvc, tmplManager, smtpClient, authClient, cfg.Templates.Image)
118118
mux := http.NewServeMux()
119119

120120
mux.HandleFunc("/login", oidcSvc.LoginHandler)
@@ -126,6 +126,18 @@ func main() {
126126
mux.Handle("/api/users", oidcSvc.AuthMiddleware(http.HandlerFunc(apiHandler.UsersHandler)))
127127
mux.Handle("/api/email", oidcSvc.AuthMiddleware(http.HandlerFunc(apiHandler.EmailHandler)))
128128
mux.Handle("/api/me", oidcSvc.AuthMiddleware(http.HandlerFunc(apiHandler.MeHandler)))
129+
mux.Handle("/api/images/upload", oidcSvc.AuthMiddleware(http.HandlerFunc(apiHandler.ImageUploadHandler)))
130+
mux.Handle("/api/images/", oidcSvc.AuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
131+
if r.Method == http.MethodDelete {
132+
apiHandler.ImageDeleteHandler(w, r)
133+
} else if r.Method == http.MethodGet && r.URL.Path == "/api/images/" {
134+
apiHandler.ImageListHandler(w, r)
135+
} else if r.Method == http.MethodGet {
136+
apiHandler.ImageServeHandler(w, r)
137+
} else {
138+
http.Error(w, "", http.StatusMethodNotAllowed)
139+
}
140+
})))
129141
mux.Handle("/logout", http.HandlerFunc(apiHandler.LogoutHandler))
130142

131143
// 전체 핸들러에 로깅 및 복구 미들웨어 적용

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/coreos/go-oidc/v3 v3.12.0
77
github.com/gorilla/sessions v1.4.0
88
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
9+
github.com/vanng822/go-premailer v1.23.0
910
goauthentik.io/api/v3 v3.2024123.4
1011
golang.org/x/oauth2 v0.26.0
1112
gopkg.in/yaml.v3 v3.0.1
@@ -18,7 +19,6 @@ require (
1819
github.com/gorilla/css v1.0.1 // indirect
1920
github.com/gorilla/securecookie v1.1.2 // indirect
2021
github.com/vanng822/css v1.0.1 // indirect
21-
github.com/vanng822/go-premailer v1.23.0 // indirect
2222
golang.org/x/crypto v0.33.0 // indirect
2323
golang.org/x/net v0.34.0 // indirect
2424
)

go.sum

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,6 @@ github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbS
5656
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
5757
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
5858
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
59-
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
60-
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
6159
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
6260
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
6361
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -93,8 +91,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
9391
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
9492
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
9593
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
96-
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
97-
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
94+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
9895
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
9996
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
10097
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -140,9 +137,8 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/
140137
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
141138
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
142139
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
143-
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
144-
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
145140
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
141+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
146142
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
147143
github.com/unrolled/render v1.7.0/go.mod h1:LwQSeDhjml8NLjIO9GJO1/1qpFJxtfVIpzxXKjfVkoI=
148144
github.com/vanng822/css v1.0.1 h1:10yiXc4e8NI8ldU6mSrWmSWMuyWgPr9DZ63RSlsgDw8=
@@ -171,8 +167,6 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
171167
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
172168
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
173169
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
174-
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
175-
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
176170
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
177171
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
178172
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=

internal/email/smtp.go

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,48 @@ package email
33
import (
44
"fmt"
55
"net/smtp"
6+
"time"
67

78
"github.com/jordan-wright/email"
89
)
910

1011
// SMTPConfig holds the configuration for the SMTP server.
1112
type SMTPConfig struct {
12-
Host string // SMTP server host, e.g., "smtp.example.com"
13-
Port int // SMTP server port, e.g., 587 (STARTTLS) 또는 465 (TLS)
14-
Username string // SMTP 계정의 사용자 이름
15-
Password string // SMTP 계정의 비밀번호
16-
From string // 기본 발신자 이메일 주소 (예: "Your Name <[email protected]>")
13+
Host string
14+
Port int
15+
Username string
16+
Password string
17+
From string
1718
}
1819

1920
// SMTPClient is a client for sending emails via SMTP using a third-party library.
2021
type SMTPClient struct {
2122
Config SMTPConfig
23+
Pool *email.Pool
2224
}
2325

2426
// NewSMTPClient creates a new SMTPClient with the provided configuration.
2527
func NewSMTPClient(cfg SMTPConfig) *SMTPClient {
26-
return &SMTPClient{
28+
client := SMTPClient{
2729
Config: cfg,
2830
}
31+
pool, err := email.NewPool(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port), 10, smtp.PlainAuth("", cfg.Username, cfg.Password, cfg.Host))
32+
if err != nil {
33+
panic(fmt.Errorf("failed to create email pool: %w", err))
34+
}
35+
client.Pool = pool
36+
return &client
2937
}
3038

3139
// SendEmail sends an email with the given recipients, subject, and HTML body using the "github.com/jordan-wright/email" library.
32-
func (c *SMTPClient) SendEmail(to []string, subject, body string) error {
40+
func (c *SMTPClient) SendEmail(to []string, subject, body string, attachments []email.Attachment) error {
3341
e := email.NewEmail()
3442
e.From = c.Config.From
3543
e.To = to
3644
e.Subject = subject
3745
e.HTML = []byte(body)
38-
39-
addr := fmt.Sprintf("%s:%d", c.Config.Host, c.Config.Port)
40-
auth := smtp.PlainAuth("", c.Config.Username, c.Config.Password, c.Config.Host)
41-
return e.Send(addr, auth)
46+
for _, attachment := range attachments {
47+
e.Attachments = append(e.Attachments, &attachment)
48+
}
49+
return c.Pool.Send(e, 10*time.Second)
4250
}

internal/email/template.go

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ package email
33
import (
44
"bytes"
55
"fmt"
6+
"github.com/jordan-wright/email"
67
"html/template"
8+
"net/textproto"
9+
"os"
710
"path/filepath"
811
"sync"
912
)
@@ -14,14 +17,16 @@ import (
1417
type TemplateManager struct {
1518
templates map[string]*template.Template
1619
baseDir string
20+
imageDir string
1721
mu sync.RWMutex
1822
}
1923

2024
// NewTemplateManager initializes and returns a new TemplateManager with the provided base directory.
21-
func NewTemplateManager(baseDir string) *TemplateManager {
25+
func NewTemplateManager(baseDir string, imageDir string) *TemplateManager {
2226
return &TemplateManager{
2327
templates: make(map[string]*template.Template),
2428
baseDir: baseDir,
29+
imageDir: imageDir,
2530
}
2631
}
2732

@@ -33,7 +38,12 @@ func (tm *TemplateManager) BaseDir() string {
3338
// 예: 이름 "default"로 "default.html" 파일을 로드하여 캐시에 저장합니다.
3439
func (tm *TemplateManager) LoadTemplate(name, filename string) error {
3540
fullPath := filepath.Join(tm.baseDir, filename)
36-
tmpl, err := template.ParseFiles(fullPath)
41+
tmpl, err := template.New(filename).Funcs(template.FuncMap{
42+
// Dummy image function that does nothing
43+
"image": func(imageSrc string) string {
44+
return ""
45+
},
46+
}).ParseFiles(fullPath)
3747
if err != nil {
3848
return fmt.Errorf("failed to parse template file %s: %v", fullPath, err)
3949
}
@@ -45,18 +55,42 @@ func (tm *TemplateManager) LoadTemplate(name, filename string) error {
4555

4656
// RenderTemplate executes the cached template identified by name using the provided data.
4757
// 데이터는 예를 들어 map[string]interface{} 형태로 전달할 수 있습니다.
48-
func (tm *TemplateManager) RenderTemplate(name string, data interface{}) (string, error) {
58+
func (tm *TemplateManager) RenderTemplate(name string, data interface{}) (string, []email.Attachment, error) {
4959
tm.mu.RLock()
5060
tmpl, exists := tm.templates[name]
5161
tm.mu.RUnlock()
5262
if !exists {
53-
return "", fmt.Errorf("template %s not found", name)
63+
return "", nil, fmt.Errorf("template %s not found", name)
5464
}
5565
var buf bytes.Buffer
66+
var attachments []email.Attachment
67+
tmpl.Funcs(template.FuncMap{
68+
"image": func(imageSrc string) template.HTML {
69+
imagePath := filepath.Join(tm.imageDir, imageSrc)
70+
if _, err := os.Stat(imagePath); err != nil {
71+
return template.HTML(fmt.Sprintf("Image not found: %s", imageSrc))
72+
}
73+
content, err := os.ReadFile(imagePath)
74+
if err != nil {
75+
return template.HTML(fmt.Sprintf("Failed to read image: %s", imageSrc))
76+
}
77+
header := textproto.MIMEHeader{
78+
"Content-ID": {fmt.Sprintf("<%s>", imageSrc)},
79+
}
80+
attachments = append(attachments, email.Attachment{
81+
Filename: imageSrc,
82+
Content: content,
83+
HTMLRelated: true,
84+
ContentType: fmt.Sprintf("image/%s", filepath.Ext(imageSrc)[1:]),
85+
Header: header,
86+
})
87+
return template.HTML(fmt.Sprintf("<img src='cid:%s' alt='%s'>", imageSrc, imageSrc))
88+
},
89+
})
5690
if err := tmpl.Execute(&buf, data); err != nil {
57-
return "", fmt.Errorf("failed to execute template %s: %v", name, err)
91+
return "", nil, fmt.Errorf("failed to execute template %s: %v", name, err)
5892
}
59-
return buf.String(), nil
93+
return buf.String(), attachments, nil
6094
}
6195

6296
// ListTemplates returns a slice of the names of the currently loaded templates.

0 commit comments

Comments
 (0)