Skip to content

Commit 151c1ab

Browse files
committed
add: send mail to stmp server
1 parent 4a7ee56 commit 151c1ab

File tree

11 files changed

+308
-40
lines changed

11 files changed

+308
-40
lines changed

README.md

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# Golang CRUD Service
22

3-
This is a sample CRUD service built with **Golang**, demonstrating a layered architecture that separates concerns into handler, service, repository, and model layers. The project uses **TiDB** as the relational database, **Redis** for `caching` and `pub/sub` in-memory operations, and **JWT** for authentication.
3+
This is a sample CRUD service built with **Golang**, demonstrating a layered architecture that separates concerns into handler, service, repository, and model layers. The project uses **TiDB** as the relational database, **Redis** for `caching` and `async tasks` in-memory operations, and **JWT** for authentication.
44

5-
## 🚀 Project Purpose
5+
## Project Purpose
66

77
Build a sample CRUD service API with Golang to showcase code structure and common best practices in web API development.
88

9-
## 🛠️ Tech Stack
9+
## Tech Stack
1010

1111
- **[Golang](https://golang.org)** — Core programming language
1212
- **[Fiber](https://gofiber.io)** — Fast HTTP router
@@ -18,7 +18,7 @@ Build a sample CRUD service API with Golang to showcase code structure and commo
1818
- **[Redis Stream](https://redis.io/docs/latest/develop/data-types/streams)** — Background task queue using consumer group
1919
- **[JWT](https://github.com/golang-jwt/jwt)** — JSON Web Token implementation for authentication
2020

21-
## 📁 Project Structure
21+
## Project Structure
2222

2323
```
2424
├── cmd/
@@ -47,22 +47,26 @@ Build a sample CRUD service API with Golang to showcase code structure and commo
4747
│ └── dto/ # DTOs for transforming request/response data
4848
```
4949

50-
## 📌 Key Concepts
50+
## Key Concepts
5151

5252
- **Layered Architecture**: Divides the project into clear layers for maintainability and scalability.
53-
- **Goroutines**: Background task workers use `goroutines` to run concurrently and process `redis stream` jobs efficiently.
53+
- **Goroutines**: Background task workers use `goroutines` to run concurrent `redis stream` jobs efficiently.
5454
- **Rate Limit**: Failed attempts and request limited using `redis` to prevent abuse `429 Too Many Requests`.
5555
- **DTO Pattern**: Uses `copier` to map between internal models and request/response structures.
5656
- **Validation**: Ensures request payloads are validated with `go-playground/validator`.
5757
- **ORM**: Leverages `GORM` to interact with `TiDB` in a concise and type-safe way.
58+
- **SMTP**: Standard `net/smtp` package for email sending to SMTP server.
5859

59-
## 🧪 Running the Project
60+
> 💡 Tip: If you're using Gmail SMTP: allow **"Less secure apps"** (not recommended)
61+
62+
## Running the Project
6063

6164
1. **Configure Environment**
6265
Create a `.env` file. find in a `.env.example` file for environment:
63-
- JWT Secret key
64-
- TiDB connection
6566
- Redis connection
67+
- TiDB connection
68+
- Email configure
69+
- JWT Secret key
6670

6771
2. **Run the App**
6872
```bash
@@ -79,11 +83,12 @@ Build a sample CRUD service API with Golang to showcase code structure and commo
7983
docker compose up -d
8084
```
8185

82-
## 🔧 Future Enhancements
86+
87+
## Future Enhancements
8388

8489
- Add unit tests and integration tests
8590
- Add Swagger/OpenAPI documentation
8691

87-
## 📄 License
92+
## License
8893

8994
This project is open-source and available under the [MIT License]().

config/config.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type (
2121
App `mapstructure:",squash"`
2222
Log `mapstructure:",squash"`
2323
DB `mapstructure:",squash"`
24+
Email `mapstructure:",squash"`
2425
Redis `mapstructure:",squash"`
2526
PermLogs `mapstructure:",squash"`
2627
JWT `mapstructure:",squash"`
@@ -47,6 +48,13 @@ type (
4748
AutoMigrate bool `mapstructure:"db_auto_migrate"`
4849
}
4950

51+
Email struct {
52+
Host string `mapstructure:"email_host"`
53+
Port string `mapstructure:"email_port"`
54+
Username string `mapstructure:"email_username"`
55+
Password string `mapstructure:"email_password"`
56+
}
57+
5058
Redis struct {
5159
Addr string `mapstructure:"redis_addr"`
5260
Port string `mapstructure:"redis_port"`
@@ -113,6 +121,11 @@ func Init() error {
113121
pflag.String("db_use_ssl", "", "database use ssl")
114122
pflag.Bool("db_auto_migrate", false, "database auto migrate")
115123

124+
pflag.String("email_host", "", "database host")
125+
pflag.String("email_port", "", "database port")
126+
pflag.String("email_username", "", "database username")
127+
pflag.String("email_password", "", "database password")
128+
116129
pflag.String("redis_addr", "", "redis address")
117130
pflag.String("redis_port", "6379", "redis port")
118131
pflag.String("redis_pwd", "", "redis password")
@@ -152,6 +165,12 @@ func Init() error {
152165
UseSSL: viper.GetString("db_use_ssl"),
153166
AutoMigrate: viper.GetBool("db_auto_migrate"),
154167
},
168+
Email: Email{
169+
Host: viper.GetString("email_host"),
170+
Port: viper.GetString("email_port"),
171+
Username: viper.GetString("email_username"),
172+
Password: viper.GetString("email_password"),
173+
},
155174
Redis: Redis{
156175
Addr: viper.GetString("redis_addr"),
157176
Port: viper.GetString("redis_port"),

internal/repository/user.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
)
88

99
type User interface {
10+
GetUsername(email string) (string, error)
1011
GetUser(username string) (model.User, error)
1112
UpdatePass(username, password string) error
1213
}
@@ -19,6 +20,13 @@ func NewUser(db *gorm.DB) User {
1920
return &user{db: db}
2021
}
2122

23+
func (h *user) GetUsername(email string) (username string, err error) {
24+
err = h.db.Model(&model.User{}).Select("username").
25+
Where("email = ?", email).
26+
Scan(&username).Error
27+
return username, err
28+
}
29+
2230
func (h *user) GetUser(username string) (entity model.User, err error) {
2331
return entity, h.db.First(&entity, "username = ?", username).Error
2432
}

internal/service/auth.go

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"learn-fiber/internal/dto"
99
"learn-fiber/internal/model"
1010
"learn-fiber/internal/repository"
11+
"learn-fiber/internal/task/consumer"
1112
"learn-fiber/internal/task/producer"
1213
"learn-fiber/pkg/crypto"
1314
"learn-fiber/pkg/redis"
@@ -46,14 +47,6 @@ func (h *auth) Info(username string) (model.User, error) {
4647

4748
func (h *auth) Login(ip string, req dto.LoginRequest) (token model.Token, err error) {
4849
key := "attempt:" + req.Username + ":" + ip
49-
_ = producer.Send(map[string]interface{}{
50-
"cmd": "send_email",
51-
"data": map[string]interface{}{
52-
"title": "Change account password",
53-
"body": "Your account password has been changed successfully",
54-
"to": "user.Email",
55-
},
56-
})
5750
user, err := h.repo.GetUser(req.Username)
5851
if err != nil {
5952
if err = attempts(key); err != nil {
@@ -112,14 +105,14 @@ func (h *auth) RenewToken(ip string, req dto.RenewTokenRequest) (token model.Tok
112105
if err != nil {
113106
return token, errors.New("refresh token invalid")
114107
}
115-
token.RefreshToken = uuid.NewString()
116108
token.Expired = time.Now().Add(time.Hour).Unix()
117109
claims := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
118110
"username": user.Username,
119111
"role": user.Role,
120112
"exp": token.Expired,
121113
})
122114
token.Ip = ip
115+
token.RefreshToken = uuid.NewString()
123116
token.AccessToken, err = claims.SignedString([]byte(config.Cfg.JWT.Secret))
124117
if err != nil {
125118
return token, err
@@ -150,13 +143,16 @@ func (h *auth) ChangePassword(username string, req dto.ChangePassRequest) error
150143
if err != nil {
151144
return err
152145
}
153-
_ = producer.Send(map[string]interface{}{
154-
"cmd": "send_email",
155-
"data": map[string]interface{}{
156-
"title": "Change account password",
157-
"body": "Your account password has been changed successfully",
158-
"to": user.Email,
159-
},
160-
})
146+
data := consumer.EmailData{
147+
Body: "Your account password has been changed successfully",
148+
Title: "Change Account Password",
149+
To: user.Email,
150+
}
151+
sendMail(data)
161152
return h.repo.UpdatePass(username, hashPass)
162153
}
154+
155+
func sendMail(data consumer.EmailData) {
156+
message := consumer.EmailCommand{Cmd: "send_email", Data: data}
157+
producer.Send(message)
158+
}

internal/task/consumer/consumer.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,39 @@
11
package consumer
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"gorm.io/gorm"
67
)
78

9+
type EmailData struct {
10+
Title string `json:"title"`
11+
Body string `json:"body"`
12+
To string `json:"to"`
13+
}
14+
15+
type EmailCommand struct {
16+
Cmd string `json:"cmd"`
17+
Data EmailData `json:"data"`
18+
}
19+
820
func Listener(db *gorm.DB, data map[string]interface{}) {
9-
if cmd, ok := data["cmd"]; ok {
10-
switch cmd {
21+
if cmd, err := covert(data); err == nil {
22+
switch cmd.Cmd {
1123
case "send_email":
12-
SendEmail(db, data)
24+
SendEmail(db, cmd.Data)
1325
default:
14-
fmt.Printf("command `%s` not found", cmd)
26+
fmt.Printf("command `%s` not found", cmd.Cmd)
1527
}
1628
}
1729
}
30+
31+
func covert(data map[string]interface{}) (cmd EmailCommand, err error) {
32+
bytes, err := json.Marshal(data)
33+
if err != nil {
34+
return cmd, err
35+
}
36+
_ = json.Unmarshal([]byte(data["data"].(string)), &cmd.Data)
37+
_ = json.Unmarshal(bytes, &cmd)
38+
return cmd, nil
39+
}

internal/task/consumer/handler.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@ package consumer
22

33
import (
44
"fmt"
5+
"learn-fiber/internal/repository"
6+
"learn-fiber/pkg/mail"
7+
58
"gorm.io/gorm"
69
)
710

8-
func SendEmail(db *gorm.DB, data interface{}) {
11+
func SendEmail(db *gorm.DB, data EmailData) {
912
fmt.Println("Processing sending email", data)
13+
if username, err := repository.NewUser(db).GetUsername(data.To); err == nil {
14+
r := mail.NewRequest(data.To, data.Title, data.Body, "", username)
15+
r.Send("templates/change-pass.html")
16+
}
1017
}

internal/task/producer/producer.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@ package producer
22

33
import (
44
"encoding/json"
5+
"learn-fiber/internal/task/consumer"
56
"learn-fiber/pkg/redis"
67
)
78

8-
func Send(payload map[string]interface{}) error {
9-
if data, ok := payload["data"]; ok {
10-
body, err := json.Marshal(data)
11-
if err != nil {
12-
return err
9+
func Send(payload consumer.EmailCommand) {
10+
if result := toMap(payload); result != nil {
11+
if body, err := json.Marshal(payload.Data); err == nil {
12+
result["data"] = string(body)
13+
redis.EnqueueTask(result)
1314
}
14-
payload["data"] = string(body)
1515
}
16-
return redis.EnqueueTask(payload).Err()
16+
}
17+
18+
func toMap(v interface{}) (result map[string]interface{}) {
19+
b, _ := json.Marshal(v)
20+
_ = json.Unmarshal(b, &result)
21+
return result
1722
}

pkg/mail/mailer.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package mail
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"html/template"
7+
"learn-fiber/config"
8+
"log"
9+
"net/smtp"
10+
)
11+
12+
type Request struct {
13+
Username string
14+
Subject string
15+
Link string
16+
Body string
17+
to string
18+
}
19+
20+
func NewRequest(to, subject, body, link, username string) *Request {
21+
return &Request{
22+
Username: username,
23+
Subject: subject,
24+
Link: link,
25+
Body: body,
26+
to: to,
27+
}
28+
}
29+
30+
func (h *Request) parseTemplate(fileName string) error {
31+
temp, err := template.ParseFiles(fileName)
32+
if err != nil {
33+
return err
34+
}
35+
var buffer bytes.Buffer
36+
if err = temp.Execute(&buffer, h); err != nil {
37+
return err
38+
}
39+
h.Body = buffer.String()
40+
return nil
41+
}
42+
43+
func (h *Request) sendMail() error {
44+
addr := fmt.Sprintf("%s:%s", config.Cfg.Email.Host, config.Cfg.Email.Port)
45+
msg := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n"
46+
msg += "From: " + config.Cfg.Email.Username + "\n"
47+
msg += "Subject: " + h.Subject + "\n"
48+
msg += "To: " + h.to + "\n\n"
49+
msg += h.Body
50+
51+
auth := smtp.PlainAuth("", config.Cfg.Email.Username, config.Cfg.Email.Password, config.Cfg.Email.Host)
52+
err := smtp.SendMail(addr, auth, config.Cfg.Email.Username, []string{h.to}, []byte(msg))
53+
return err
54+
}
55+
56+
func (h *Request) Send(template string) {
57+
if err := h.parseTemplate(template); err != nil {
58+
log.Fatal(err)
59+
}
60+
if err := h.sendMail(); err != nil {
61+
log.Printf("Failed to send to %s\n", h.to)
62+
log.Print(err)
63+
return
64+
}
65+
log.Printf("Email sent to %s\n", h.to)
66+
}

0 commit comments

Comments
 (0)