444 lines
12 KiB
Go
444 lines
12 KiB
Go
package services
|
||
|
||
import (
|
||
"bytes"
|
||
"crypto/tls"
|
||
"fmt"
|
||
"html/template"
|
||
"math"
|
||
"net/smtp"
|
||
"time"
|
||
|
||
"github.com/jordan-wright/email"
|
||
|
||
"goalfymax-admin/internal/config"
|
||
)
|
||
|
||
type EmailService struct {
|
||
host string
|
||
port int
|
||
username string
|
||
password string
|
||
sender string
|
||
inviteURLPrefix string
|
||
}
|
||
|
||
// NewEmailService 创建邮件服务实例
|
||
func NewEmailService() *EmailService {
|
||
cfg := config.GetConfig()
|
||
return &EmailService{
|
||
host: cfg.Email.Host,
|
||
port: cfg.Email.Port,
|
||
username: cfg.Email.Username,
|
||
password: cfg.Email.Password,
|
||
sender: cfg.Email.Sender,
|
||
inviteURLPrefix: cfg.Email.InviteURLPrefix,
|
||
}
|
||
}
|
||
|
||
// SendInviteCodeApprovalEmail 发送邀请码审批通过邮件
|
||
func (s *EmailService) SendInviteCodeApprovalEmail(toEmail, inviteCode, language string, expiresAt *time.Time) error {
|
||
var subject string
|
||
if language == "en" {
|
||
subject = "Your GoalfyAI Beta Access Invitation"
|
||
} else {
|
||
subject = "GoalfyAI 内测邀请函"
|
||
}
|
||
|
||
// 构造邮件内容
|
||
htmlContent := s.generateApprovalEmailHTML(inviteCode, language, expiresAt)
|
||
|
||
return s.sendEmail(toEmail, subject, htmlContent)
|
||
}
|
||
|
||
// SendInviteCodeRejectionEmail 发送邀请码申请拒绝邮件
|
||
func (s *EmailService) SendInviteCodeRejectionEmail(toEmail, rejectReason string) error {
|
||
subject := "关于您的 GoalfyAI 申请"
|
||
|
||
// 构造邮件内容
|
||
htmlContent := s.generateRejectionEmailHTML(rejectReason)
|
||
|
||
return s.sendEmail(toEmail, subject, htmlContent)
|
||
}
|
||
|
||
// generateApprovalEmailHTML 生成审批通过的邮件HTML
|
||
func (s *EmailService) generateApprovalEmailHTML(inviteCode, language string, expiresAt *time.Time) string {
|
||
if language == "en" {
|
||
return s.GenerateApprovalEmailEN(inviteCode, expiresAt)
|
||
}
|
||
return s.GenerateApprovalEmailZH(inviteCode, expiresAt)
|
||
}
|
||
|
||
// formatExpiryTimeEN 格式化过期时间为英文显示(全部显示为小时,向上取整)
|
||
func formatExpiryTimeEN(expiresAt *time.Time) string {
|
||
if expiresAt == nil {
|
||
return "until used"
|
||
}
|
||
|
||
now := time.Now()
|
||
if expiresAt.Before(now) {
|
||
return "expired"
|
||
}
|
||
|
||
duration := expiresAt.Sub(now)
|
||
hours := int(math.Ceil(duration.Hours())) // 向上取整
|
||
|
||
if hours <= 0 {
|
||
hours = 1 // 不足一小时算一小时
|
||
}
|
||
|
||
if hours == 1 {
|
||
return "1 hour"
|
||
}
|
||
return fmt.Sprintf("%d hours", hours)
|
||
}
|
||
|
||
// formatExpiryTimeZH 格式化过期时间为中文显示(全部显示为小时,向上取整)
|
||
func formatExpiryTimeZH(expiresAt *time.Time) string {
|
||
if expiresAt == nil {
|
||
return "永久有效"
|
||
}
|
||
|
||
now := time.Now()
|
||
if expiresAt.Before(now) {
|
||
return "已过期"
|
||
}
|
||
|
||
duration := expiresAt.Sub(now)
|
||
hours := int(math.Ceil(duration.Hours())) // 向上取整
|
||
|
||
if hours <= 0 {
|
||
hours = 1 // 不足一小时算一小时
|
||
}
|
||
|
||
return fmt.Sprintf("%d小时", hours)
|
||
}
|
||
|
||
// GenerateApprovalEmailEN 生成英文版审批通过邮件(导出用于测试)
|
||
func (s *EmailService) GenerateApprovalEmailEN(inviteCode string, expiresAt *time.Time) string {
|
||
expiryHours := formatExpiryTimeEN(expiresAt)
|
||
|
||
tmplStr := `
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
</head>
|
||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||
<p>Thank you again for your interest in GoalfyAI!</p>
|
||
|
||
<p>We're excited to let you know that your request for beta access has been approved.<br>
|
||
You can now activate your GoalfyAI account using the link below:</p>
|
||
|
||
<p>👉 <a href="{{.InviteURL}}">Activate Your Account</a><br>
|
||
<span style="color: #666; font-size: 14px;">(This link is valid for {{.ExpiryHours}})</span></p>
|
||
|
||
<p>With this invite, you'll be among the first to explore our intelligent task execution system—designed for long-range, professional workflows. We'd love to hear your feedback as we continue to refine the experience.</p>
|
||
|
||
<p>Need help getting started? Visit our website for tips, use cases, and product updates:<br>
|
||
🌐 <a href="https://www.goalfyai.com">GoalfyAI.com</a></p>
|
||
|
||
<p>Thanks again for joining us on this journey.<br>
|
||
Let's build the future of intelligent tasks—together.</p>
|
||
|
||
<p>Warm regards,<br>
|
||
The GoalfyAI Team</p>
|
||
|
||
<hr style="border: none; border-top: 1px solid #ddd; margin: 40px 0;">
|
||
|
||
<p style="text-align: center; color: #999; font-size: 12px; line-height: 1.5;">
|
||
This email is sent automatically. Please do not reply.<br>
|
||
For any questions, please contact <a href="mailto:hi@goalfyai.com" style="color: #999;">hi@goalfyai.com</a>
|
||
</p>
|
||
</body>
|
||
</html>
|
||
`
|
||
|
||
tmpl, _ := template.New("approval_en").Parse(tmplStr)
|
||
var buf bytes.Buffer
|
||
tmpl.Execute(&buf, map[string]string{
|
||
"InviteCode": inviteCode,
|
||
"ExpiryHours": expiryHours,
|
||
"InviteURL": s.inviteURLPrefix + inviteCode,
|
||
})
|
||
|
||
return buf.String()
|
||
}
|
||
|
||
// GenerateApprovalEmailZH 生成中文版审批通过邮件(导出用于测试)
|
||
func (s *EmailService) GenerateApprovalEmailZH(inviteCode string, expiresAt *time.Time) string {
|
||
expiryHours := formatExpiryTimeZH(expiresAt)
|
||
|
||
tmplStr := `
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
</head>
|
||
<body style="font-family: 'Microsoft YaHei', Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||
<p>感谢您对 GoalfyAI 的关注与支持!</p>
|
||
|
||
<p>我们很高兴通知您,您的内测申请已通过审核。<br>
|
||
请通过以下链接激活您的 GoalfyAI 账户:</p>
|
||
|
||
<p>👉 <a href="{{.InviteURL}}">点击激活账户</a><br>
|
||
<span style="color: #666; font-size: 14px;">(该链接在 {{.ExpiryHours}} 内有效)</span></p>
|
||
|
||
<p>通过本次邀请,您将率先体验我们为长周期专业任务打造的智能任务系统。我们也非常欢迎您在使用过程中给予反馈,帮助我们持续优化产品体验。</p>
|
||
|
||
<p>如需了解更多使用建议、典型场景或最新进展,欢迎访问官网:<br>
|
||
🌐 <a href="https://www.goalfyai.com">GoalfyAI.com</a></p>
|
||
|
||
<p>感谢您的加入,<br>
|
||
让我们一同开启智能任务的新篇章!</p>
|
||
|
||
<p>此致,<br>
|
||
GoalfyAI 团队</p>
|
||
|
||
|
||
<hr style="border: none; border-top: 1px solid #ddd; margin: 40px 0;">
|
||
|
||
<p style="text-align: center; color: #999; font-size: 12px; line-height: 1.5;">
|
||
本邮件为自动化发送,请勿回复。<br>
|
||
如有疑问请联系 <a href="mailto:hi@goalfyai.com" style="color: #999;">hi@goalfyai.com</a>
|
||
</p>
|
||
</body>
|
||
</html>
|
||
`
|
||
|
||
tmpl, _ := template.New("approval_zh").Parse(tmplStr)
|
||
var buf bytes.Buffer
|
||
tmpl.Execute(&buf, map[string]string{
|
||
"InviteCode": inviteCode,
|
||
"ExpiryHours": expiryHours,
|
||
"InviteURL": s.inviteURLPrefix + inviteCode,
|
||
})
|
||
|
||
return buf.String()
|
||
}
|
||
|
||
// generateRejectionEmailHTML 生成申请拒绝的邮件HTML
|
||
func (s *EmailService) generateRejectionEmailHTML(rejectReason string) string {
|
||
if rejectReason == "" {
|
||
rejectReason = "感谢您对 GoalfyAI 的关注。经过审核,您的账户申请暂未通过。"
|
||
}
|
||
|
||
tmplStr := `
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||
line-height: 1.6;
|
||
color: #1a1a1a;
|
||
background-color: #f5f5f5;
|
||
padding: 40px 20px;
|
||
}
|
||
.email-wrapper {
|
||
max-width: 600px;
|
||
margin: 0 auto;
|
||
background-color: #ffffff;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||
}
|
||
.header {
|
||
background-color: #000000;
|
||
padding: 32px 40px;
|
||
text-align: center;
|
||
}
|
||
.header-logo {
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
color: #ffffff;
|
||
letter-spacing: -0.5px;
|
||
}
|
||
.content {
|
||
padding: 40px;
|
||
}
|
||
.greeting {
|
||
font-size: 16px;
|
||
color: #1a1a1a;
|
||
margin-bottom: 24px;
|
||
}
|
||
.message {
|
||
font-size: 15px;
|
||
color: #4a4a4a;
|
||
margin-bottom: 24px;
|
||
line-height: 1.7;
|
||
}
|
||
.reason-box {
|
||
background-color: #fafafa;
|
||
border-left: 3px solid #4a4a4a;
|
||
padding: 20px 24px;
|
||
margin: 24px 0;
|
||
border-radius: 0 4px 4px 0;
|
||
}
|
||
.reason-box p {
|
||
font-size: 15px;
|
||
color: #1a1a1a;
|
||
line-height: 1.7;
|
||
}
|
||
.support-box {
|
||
background-color: #fafafa;
|
||
border-radius: 6px;
|
||
padding: 20px 24px;
|
||
margin-top: 32px;
|
||
text-align: center;
|
||
}
|
||
.support-box p {
|
||
font-size: 14px;
|
||
color: #4a4a4a;
|
||
margin: 0;
|
||
}
|
||
.support-box a {
|
||
color: #000000;
|
||
text-decoration: none;
|
||
font-weight: 600;
|
||
}
|
||
.footer {
|
||
background-color: #fafafa;
|
||
padding: 32px 40px;
|
||
text-align: center;
|
||
border-top: 1px solid #e5e5e5;
|
||
}
|
||
.footer-brand {
|
||
font-size: 14px;
|
||
color: #1a1a1a;
|
||
font-weight: 600;
|
||
margin-bottom: 8px;
|
||
}
|
||
.footer-contact {
|
||
font-size: 13px;
|
||
color: #666666;
|
||
margin-top: 8px;
|
||
}
|
||
.footer-contact a {
|
||
color: #000000;
|
||
text-decoration: none;
|
||
}
|
||
@media only screen and (max-width: 600px) {
|
||
.content {
|
||
padding: 24px 20px;
|
||
}
|
||
.header {
|
||
padding: 24px 20px;
|
||
}
|
||
.reason-box {
|
||
padding: 16px 20px;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="email-wrapper">
|
||
<div class="header">
|
||
<div class="header-logo">GoalfyAI</div>
|
||
</div>
|
||
|
||
<div class="content">
|
||
<div class="greeting">您好,</div>
|
||
|
||
<div class="message">
|
||
感谢您对 GoalfyAI 的关注和申请。
|
||
</div>
|
||
|
||
<div class="reason-box">
|
||
<p>{{.RejectReason}}</p>
|
||
</div>
|
||
|
||
<div class="message" style="margin-top: 24px;">
|
||
我们期待未来有机会为您提供服务。
|
||
</div>
|
||
|
||
<div class="support-box">
|
||
<p>如有任何疑问,欢迎联系我们</p>
|
||
<p style="margin-top: 8px;">
|
||
<a href="mailto:goalfymax@goalfyai.com">goalfymax@goalfyai.com</a>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="footer">
|
||
<div class="footer-brand">GoalfyAI 团队</div>
|
||
<div class="footer-contact">
|
||
© 2025 GoalfyAI. All rights reserved.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
`
|
||
|
||
tmpl, _ := template.New("rejection").Parse(tmplStr)
|
||
var buf bytes.Buffer
|
||
tmpl.Execute(&buf, map[string]string{
|
||
"RejectReason": rejectReason,
|
||
})
|
||
|
||
return buf.String()
|
||
}
|
||
|
||
// sendEmail 发送邮件的通用方法
|
||
func (s *EmailService) sendEmail(toEmail, subject, htmlContent string) error {
|
||
e := email.NewEmail()
|
||
e.From = s.sender
|
||
e.To = []string{toEmail}
|
||
e.Subject = subject
|
||
e.HTML = []byte(htmlContent)
|
||
|
||
// 创建SMTP认证
|
||
auth := smtp.PlainAuth("", s.username, s.password, s.host)
|
||
|
||
// 配置TLS
|
||
tlsConfig := &tls.Config{
|
||
ServerName: s.host,
|
||
}
|
||
|
||
// 发送邮件
|
||
addr := fmt.Sprintf("%s:%d", s.host, s.port)
|
||
|
||
// 如果是465端口,使用SSL
|
||
if s.port == 465 {
|
||
return e.SendWithTLS(addr, auth, tlsConfig)
|
||
}
|
||
|
||
// 否则使用STARTTLS
|
||
return e.SendWithStartTLS(addr, auth, tlsConfig)
|
||
}
|
||
|
||
// SendBatchEmails 批量发送邮件(异步)
|
||
func (s *EmailService) SendBatchEmails(emails []string, subject, htmlContent string) []error {
|
||
errors := make([]error, len(emails))
|
||
for i, email := range emails {
|
||
errors[i] = s.sendEmail(email, subject, htmlContent)
|
||
// 避免发送过快
|
||
time.Sleep(100 * time.Millisecond)
|
||
}
|
||
return errors
|
||
}
|
||
|
||
// TestConnection 测试邮件服务器连接
|
||
func (s *EmailService) TestConnection() error {
|
||
addr := fmt.Sprintf("%s:%d", s.host, s.port)
|
||
|
||
// 尝试连接SMTP服务器
|
||
client, err := smtp.Dial(addr)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to connect to SMTP server: %w", err)
|
||
}
|
||
defer client.Close()
|
||
|
||
return nil
|
||
}
|