Init V4 community edition (#2265)

* Init V4 community edition

* Init V4 community edition
This commit is contained in:
AaronLiu
2025-04-20 17:31:25 +08:00
committed by GitHub
parent da4e44b77a
commit 21d158db07
597 changed files with 119415 additions and 41692 deletions

View File

@@ -1,52 +0,0 @@
package email
import (
"sync"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
)
// Client 默认的邮件发送客户端
var Client Driver
// Lock 读写锁
var Lock sync.RWMutex
// Init 初始化
func Init() {
util.Log().Debug("Initializing email sending queue...")
Lock.Lock()
defer Lock.Unlock()
if Client != nil {
Client.Close()
}
// 读取SMTP设置
options := model.GetSettingByNames(
"fromName",
"fromAdress",
"smtpHost",
"replyTo",
"smtpUser",
"smtpPass",
"smtpEncryption",
)
port := model.GetIntSetting("smtpPort", 25)
keepAlive := model.GetIntSetting("mail_keepalive", 30)
client := NewSMTPClient(SMTPConfig{
Name: options["fromName"],
Address: options["fromAdress"],
ReplyTo: options["replyTo"],
Host: options["smtpHost"],
Port: port,
User: options["smtpUser"],
Password: options["smtpPass"],
Keepalive: keepAlive,
Encryption: model.IsTrueVal(options["smtpEncryption"]),
})
Client = client
}

View File

@@ -1,8 +1,8 @@
package email
import (
"context"
"errors"
"strings"
)
// Driver 邮件发送驱动
@@ -10,7 +10,7 @@ type Driver interface {
// Close 关闭驱动
Close()
// Send 发送邮件
Send(to, title, body string) error
Send(ctx context.Context, to, title, body string) error
}
var (
@@ -19,20 +19,3 @@ var (
// ErrNoActiveDriver 无可用邮件发送服务
ErrNoActiveDriver = errors.New("no avaliable email provider")
)
// Send 发送邮件
func Send(to, title, body string) error {
// 忽略通过QQ登录的邮箱
if strings.HasSuffix(to, "@login.qq.com") {
return nil
}
Lock.RLock()
defer Lock.RUnlock()
if Client == nil {
return ErrNoActiveDriver
}
return Client.Send(to, title, body)
}

View File

@@ -1,19 +1,27 @@
package email
import (
"context"
"fmt"
"github.com/google/uuid"
"strings"
"time"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/cloudreve/Cloudreve/v4/inventory"
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
"github.com/go-mail/mail"
"github.com/gofrs/uuid"
)
// SMTP SMTP协议发送邮件
type SMTP struct {
// SMTPPool SMTP协议发送邮件
type SMTPPool struct {
// Deprecated
Config SMTPConfig
ch chan *mail.Message
config *setting.SMTP
ch chan *message
chOpen bool
l logging.Logger
}
// SMTPConfig SMTP发送配置
@@ -26,14 +34,34 @@ type SMTPConfig struct {
User string // 用户名
Password string // 密码
Encryption bool // 是否启用加密
Keepalive int // SMTP 连接保留时长
Keepalive int // SMTPPool 连接保留时长
}
type message struct {
msg *mail.Message
cid string
userID int
}
// NewSMTPPool initializes a new SMTP based email sending queue.
func NewSMTPPool(config setting.Provider, logger logging.Logger) *SMTPPool {
client := &SMTPPool{
config: config.SMTP(context.Background()),
ch: make(chan *message, 30),
chOpen: false,
l: logger,
}
client.Init()
return client
}
// NewSMTPClient 新建SMTP发送队列
func NewSMTPClient(config SMTPConfig) *SMTP {
client := &SMTP{
// Deprecated
func NewSMTPClient(config SMTPConfig) *SMTPPool {
client := &SMTPPool{
Config: config,
ch: make(chan *mail.Message, 30),
ch: make(chan *message, 30),
chOpen: false,
}
@@ -43,46 +71,57 @@ func NewSMTPClient(config SMTPConfig) *SMTP {
}
// Send 发送邮件
func (client *SMTP) Send(to, title, body string) error {
func (client *SMTPPool) Send(ctx context.Context, to, title, body string) error {
if !client.chOpen {
return ErrChanNotOpen
return fmt.Errorf("SMTP pool is closed")
}
// 忽略通过QQ登录的邮箱
if strings.HasSuffix(to, "@login.qq.com") {
return nil
}
m := mail.NewMessage()
m.SetAddressHeader("From", client.Config.Address, client.Config.Name)
m.SetAddressHeader("Reply-To", client.Config.ReplyTo, client.Config.Name)
m.SetAddressHeader("From", client.config.From, client.config.FromName)
m.SetAddressHeader("Reply-To", client.config.ReplyTo, client.config.FromName)
m.SetHeader("To", to)
m.SetHeader("Subject", title)
m.SetHeader("Message-ID", fmt.Sprintf("<%s@%s>", uuid.NewString(), "cloudreve"))
m.SetHeader("Message-ID", fmt.Sprintf("<%s@%s>", uuid.Must(uuid.NewV4()).String(), "cloudreve"))
m.SetBody("text/html", body)
client.ch <- m
client.ch <- &message{
msg: m,
cid: logging.CorrelationID(ctx).String(),
userID: inventory.UserIDFromContext(ctx),
}
return nil
}
// Close 关闭发送队列
func (client *SMTP) Close() {
func (client *SMTPPool) Close() {
if client.ch != nil {
close(client.ch)
}
}
// Init 初始化发送队列
func (client *SMTP) Init() {
func (client *SMTPPool) Init() {
go func() {
client.l.Info("Initializing and starting SMTP email pool...")
defer func() {
if err := recover(); err != nil {
client.chOpen = false
util.Log().Error("Exception while sending email: %s, queue will be reset in 10 seconds.", err)
client.l.Error("Exception while sending email: %s, queue will be reset in 10 seconds.", err)
time.Sleep(time.Duration(10) * time.Second)
client.Init()
}
}()
d := mail.NewDialer(client.Config.Host, client.Config.Port, client.Config.User, client.Config.Password)
d.Timeout = time.Duration(client.Config.Keepalive+5) * time.Second
d := mail.NewDialer(client.config.Host, client.config.Port, client.config.User, client.config.Password)
d.Timeout = time.Duration(client.config.Keepalive+5) * time.Second
client.chOpen = true
// 是否启用 SSL
d.SSL = false
if client.Config.Encryption {
if client.config.ForceEncryption {
d.SSL = true
}
d.StartTLSPolicy = mail.OpportunisticStartTLS
@@ -94,26 +133,29 @@ func (client *SMTP) Init() {
select {
case m, ok := <-client.ch:
if !ok {
util.Log().Debug("Email queue closing...")
client.l.Info("Email queue closing...")
client.chOpen = false
return
}
if !open {
if s, err = d.Dial(); err != nil {
panic(err)
}
open = true
}
if err := mail.Send(s, m); err != nil {
util.Log().Warning("Failed to send email: %s", err)
l := client.l.CopyWithPrefix(fmt.Sprintf("[Cid: %s]", m.cid))
if err := mail.Send(s, m.msg); err != nil {
l.Warning("Failed to send email: %s, Cid=%s", err, m.cid)
} else {
util.Log().Debug("Email sent.")
l.Info("Email sent to %q, title: %q.", m.msg.GetHeader("To"), m.msg.GetHeader("Subject"))
}
// 长时间没有新邮件则关闭SMTP连接
case <-time.After(time.Duration(client.Config.Keepalive) * time.Second):
case <-time.After(time.Duration(client.config.Keepalive) * time.Second):
if open {
if err := s.Close(); err != nil {
util.Log().Warning("Failed to close SMTP connection: %s", err)
client.l.Warning("Failed to close SMTP connection: %s", err)
}
open = false
}

View File

@@ -1,36 +1,125 @@
package email
import (
"context"
"fmt"
"html/template"
"net/url"
"strings"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/cloudreve/Cloudreve/v4/ent"
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
)
// NewActivationEmail 新建激活邮件
func NewActivationEmail(userName, activateURL string) (string, string) {
options := model.GetSettingByNames("siteName", "siteURL", "siteTitle", "mail_activation_template")
replace := map[string]string{
"{siteTitle}": options["siteName"],
"{userName}": userName,
"{activationUrl}": activateURL,
"{siteUrl}": options["siteURL"],
"{siteSecTitle}": options["siteTitle"],
}
return fmt.Sprintf("【%s】注册激活", options["siteName"]),
util.Replace(replace, options["mail_activation_template"])
type CommonContext struct {
SiteBasic *setting.SiteBasic
Logo *setting.Logo
SiteUrl string
}
// NewResetEmail 新建重设密码邮件
func NewResetEmail(userName, resetURL string) (string, string) {
options := model.GetSettingByNames("siteName", "siteURL", "siteTitle", "mail_reset_pwd_template")
replace := map[string]string{
"{siteTitle}": options["siteName"],
"{userName}": userName,
"{resetUrl}": resetURL,
"{siteUrl}": options["siteURL"],
"{siteSecTitle}": options["siteTitle"],
}
return fmt.Sprintf("【%s】密码重置", options["siteName"]),
util.Replace(replace, options["mail_reset_pwd_template"])
// ResetContext used for variables in reset email
type ResetContext struct {
*CommonContext
User *ent.User
Url string
}
// NewResetEmail generates reset email from template
func NewResetEmail(ctx context.Context, settings setting.Provider, user *ent.User, url string) (string, string, error) {
templates := settings.ResetEmailTemplate(ctx)
if len(templates) == 0 {
return "", "", fmt.Errorf("reset email template not configured")
}
selected := selectTemplate(templates, user)
resetCtx := ResetContext{
CommonContext: commonContext(ctx, settings),
User: user,
Url: url,
}
tmpl, err := template.New("reset").Parse(selected.Body)
if err != nil {
return "", "", fmt.Errorf("failed to parse email template: %w", err)
}
var res strings.Builder
err = tmpl.Execute(&res, resetCtx)
if err != nil {
return "", "", fmt.Errorf("failed to execute email template: %w", err)
}
return fmt.Sprintf("[%s] %s", resetCtx.SiteBasic.Name, selected.Title), res.String(), nil
}
// ActivationContext used for variables in activation email
type ActivationContext struct {
*CommonContext
User *ent.User
Url string
}
// NewActivationEmail generates activation email from template
func NewActivationEmail(ctx context.Context, settings setting.Provider, user *ent.User, url string) (string, string, error) {
templates := settings.ActivationEmailTemplate(ctx)
if len(templates) == 0 {
return "", "", fmt.Errorf("activation email template not configured")
}
selected := selectTemplate(templates, user)
activationCtx := ActivationContext{
CommonContext: commonContext(ctx, settings),
User: user,
Url: url,
}
tmpl, err := template.New("activation").Parse(selected.Body)
if err != nil {
return "", "", fmt.Errorf("failed to parse email template: %w", err)
}
var res strings.Builder
err = tmpl.Execute(&res, activationCtx)
if err != nil {
return "", "", fmt.Errorf("failed to execute email template: %w", err)
}
return fmt.Sprintf("[%s] %s", activationCtx.SiteBasic.Name, selected.Title), res.String(), nil
}
func commonContext(ctx context.Context, settings setting.Provider) *CommonContext {
logo := settings.Logo(ctx)
siteUrl := settings.SiteURL(ctx)
res := &CommonContext{
SiteBasic: settings.SiteBasic(ctx),
Logo: settings.Logo(ctx),
SiteUrl: siteUrl.String(),
}
// Add site url if logo is not an url
if !strings.HasPrefix(logo.Light, "http") {
logoPath, _ := url.Parse(logo.Light)
res.Logo.Light = siteUrl.ResolveReference(logoPath).String()
}
if !strings.HasPrefix(logo.Normal, "http") {
logoPath, _ := url.Parse(logo.Normal)
res.Logo.Normal = siteUrl.ResolveReference(logoPath).String()
}
return res
}
func selectTemplate(templates []setting.EmailTemplate, u *ent.User) setting.EmailTemplate {
selected := templates[0]
if u != nil {
for _, t := range templates {
if strings.EqualFold(t.Language, u.Settings.Language) {
selected = t
break
}
}
}
return selected
}