Init V4 community edition (#2265)
* Init V4 community edition * Init V4 community edition
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user