chore: 初始化 Cloudreve 源码仓库

This commit is contained in:
2026-09-13 11:28:59 +08:00
commit 2a7b03ab1d
489 changed files with 163867 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
package email
import (
"context"
"errors"
)
// Driver 邮件发送驱动
type Driver interface {
// Close 关闭驱动
Close()
// Send 发送邮件
Send(ctx context.Context, to, title, body string) error
}
var (
// ErrChanNotOpen 邮件队列未开启
ErrChanNotOpen = errors.New("email queue is not started")
// ErrNoActiveDriver 无可用邮件发送服务
ErrNoActiveDriver = errors.New("no avaliable email provider")
)
+188
View File
@@ -0,0 +1,188 @@
package email
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/cloudreve/Cloudreve/v4/inventory"
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
"github.com/wneessen/go-mail"
)
// SMTPPool SMTP协议发送邮件
type SMTPPool struct {
// Deprecated
Config SMTPConfig
config *setting.SMTP
ch chan *message
chOpen bool
l logging.Logger
}
// SMTPConfig SMTP发送配置
type SMTPConfig struct {
Name string // 发送者名
Address string // 发送者地址
ReplyTo string // 回复地址
Host string // 服务器主机名
Port int // 服务器端口
User string // 用户名
Password string // 密码
Encryption bool // 是否启用加密
Keepalive int // SMTPPool 连接保留时长
}
type message struct {
msg *mail.Msg
to string
subject string
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发送队列
// Deprecated
func NewSMTPClient(config SMTPConfig) *SMTPPool {
client := &SMTPPool{
Config: config,
ch: make(chan *message, 30),
chOpen: false,
}
client.Init()
return client
}
// Send 发送邮件
func (client *SMTPPool) Send(ctx context.Context, to, title, body string) error {
if !client.chOpen {
return fmt.Errorf("SMTP pool is closed")
}
// 忽略通过QQ登录的邮箱
if strings.HasSuffix(to, "@login.qq.com") {
return nil
}
m := mail.NewMsg()
if err := m.FromFormat(client.config.FromName, client.config.From); err != nil {
return err
}
m.ReplyToFormat(client.config.FromName, client.config.ReplyTo)
m.To(to)
m.Subject(title)
m.SetMessageID()
m.SetBodyString(mail.TypeTextHTML, body)
client.ch <- &message{
msg: m,
subject: title,
to: to,
cid: logging.CorrelationID(ctx).String(),
userID: inventory.UserIDFromContext(ctx),
}
return nil
}
// Close 关闭发送队列
func (client *SMTPPool) Close() {
if client.ch != nil {
close(client.ch)
}
}
// 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
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()
}
}()
opts := []mail.Option{
mail.WithPort(client.config.Port),
mail.WithTimeout(time.Duration(client.config.Keepalive+5) * time.Second),
mail.WithSMTPAuth(mail.SMTPAuthAutoDiscover), mail.WithTLSPortPolicy(mail.TLSOpportunistic),
mail.WithUsername(client.config.User), mail.WithPassword(client.config.Password),
}
if client.config.ForceEncryption {
opts = append(opts, mail.WithSSL())
}
d, diaErr := mail.NewClient(client.config.Host, opts...)
if diaErr != nil {
client.l.Panic("Failed to create SMTP client: %s", diaErr)
return
}
client.chOpen = true
var err error
open := false
for {
select {
case m, ok := <-client.ch:
if !ok {
client.l.Info("Email queue closing...")
client.chOpen = false
return
}
if !open {
if err = d.DialWithContext(context.Background()); err != nil {
panic(err)
}
open = true
}
l := client.l.CopyWithPrefix(fmt.Sprintf("[Cid: %s]", m.cid))
if err := d.Send(m.msg); err != nil {
// Check if this is an SMTP RESET error after successful delivery
var sendErr *mail.SendError
var errParsed = errors.As(err, &sendErr)
if errParsed && sendErr.Reason == mail.ErrSMTPReset {
open = false
l.Debug("SMTP RESET error, closing connection...")
// https://github.com/wneessen/go-mail/issues/463
continue // Don't treat this as a delivery failure since mail was sent
}
l.Warning("Failed to send email: %s, Cid=%s", err, m.cid)
} else {
l.Info("Email sent to %q, title: %q.", m.to, m.subject)
}
// 长时间没有新邮件,则关闭SMTP连接
case <-time.After(time.Duration(client.config.Keepalive) * time.Second):
if open {
if err := d.Close(); err != nil {
client.l.Warning("Failed to close SMTP connection: %s", err)
}
open = false
}
}
}
}()
}
+147
View File
@@ -0,0 +1,147 @@
package email
import (
"context"
"fmt"
"html/template"
"net/url"
"strings"
"github.com/cloudreve/Cloudreve/v4/ent"
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
)
type CommonContext struct {
SiteBasic *setting.SiteBasic
Logo *setting.Logo
SiteUrl string
}
// 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,
}
tmplTitle, err := template.New("resetTitle").Parse(selected.Title)
if err != nil {
return "", "", fmt.Errorf("failed to parse email title: %w", err)
}
var resTitle strings.Builder
err = tmplTitle.Execute(&resTitle, resetCtx)
if err != nil {
return "", "", fmt.Errorf("failed to execute email title: %w", err)
}
tmplBody, err := template.New("resetBody").Parse(selected.Body)
if err != nil {
return "", "", fmt.Errorf("failed to parse email template: %w", err)
}
var resBody strings.Builder
err = tmplBody.Execute(&resBody, resetCtx)
if err != nil {
return "", "", fmt.Errorf("failed to execute email template: %w", err)
}
return resTitle.String(), resBody.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,
}
tmplTitle, err := template.New("activationTitle").Parse(selected.Title)
if err != nil {
return "", "", fmt.Errorf("failed to parse email title: %w", err)
}
var resTitle strings.Builder
err = tmplTitle.Execute(&resTitle, activationCtx)
if err != nil {
return "", "", fmt.Errorf("failed to execute email title: %w", err)
}
tmplBody, err := template.New("activationBody").Parse(selected.Body)
if err != nil {
return "", "", fmt.Errorf("failed to parse email template: %w", err)
}
var resBody strings.Builder
err = tmplBody.Execute(&resBody, activationCtx)
if err != nil {
return "", "", fmt.Errorf("failed to execute email template: %w", err)
}
return resTitle.String(), resBody.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
}