Files
goalfylearning-admin/internal/services/email_service.go

444 lines
12 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}