Files
tp/main_dc/BB/api_bb/pkg/email/email.go
T
valitovgaziz 15357fd3c0 create and moove into new directories for BegushiyBashkir and
yalarbacreate and moove into new directories for BegushiyBashkir and
yalarbacreate and moove into new directories for BegushiyBashkir and
yalarbacreate and moove into new directories for BegushiyBashkir and
yalarbacreate and moove into new directories for BegushiyBashkir and
yalarbacreate and moove into new directories for BegushiyBashkir and
yalarbacreate and moove into new directories for BegushiyBashkir and
yalarbacreate and moove into new directories for BegushiyBashkir and
yalarbacreate and moove into new directories for BegushiyBashkir and
yalarba
2025-10-24 05:22:44 +05:00

442 lines
14 KiB
Go

// pkg/email/email.go
package email
import (
"bytes"
"fmt"
"html/template"
"time"
"api_bb/internal/config"
"api_bb/pkg/logger"
"github.com/wneessen/go-mail"
"go.uber.org/zap"
)
// Service представляет сервис для отправки email
type Service struct {
client *mail.Client
config *config.Config
logger *zap.Logger
tmpl *template.Template
fromAddr string
isActive bool
}
// NewService создает новый экземпляр email сервиса
func NewService(cfg *config.Config) (*Service, error) {
log := logger.Get()
log.Info("Initializing email service")
// Проверяем обязательные параметры конфигурации
if err := validateConfig(cfg); err != nil {
log.Warn("Email service configuration is invalid, service will be disabled", zap.Error(err))
return &Service{
logger: log,
isActive: false,
}, nil
}
// Создаем SMTP клиент с правильными настройками
client, err := createSMTPClient(cfg)
if err != nil {
log.Warn("Failed to create SMTP client, email service will be disabled", zap.Error(err))
return &Service{
logger: log,
isActive: false,
}, nil
}
// Загружаем шаблоны писем
tmpl, err := loadTemplates()
if err != nil {
log.Warn("Failed to load email templates, email service will be disabled", zap.Error(err))
return &Service{
logger: log,
isActive: false,
}, nil
}
service := &Service{
client: client,
config: cfg,
logger: log,
tmpl: tmpl,
fromAddr: cfg.FromEmail,
isActive: true,
}
log.Info("Email service initialized successfully",
zap.String("host", cfg.SMTPHost),
zap.Int("port", cfg.SMTPPort),
zap.String("from", cfg.FromEmail))
return service, nil
}
// validateConfig проверяет корректность конфигурации email
func validateConfig(cfg *config.Config) error {
if cfg.SMTPHost == "" {
return fmt.Errorf("SMTP host is required")
}
if cfg.SMTPPort <= 0 || cfg.SMTPPort > 65535 {
return fmt.Errorf("invalid SMTP port: %d", cfg.SMTPPort)
}
if cfg.SMTPUsername == "" {
return fmt.Errorf("SMTP username is required")
}
if cfg.SMTPPassword == "" {
return fmt.Errorf("SMTP password is required")
}
if cfg.FromEmail == "" {
return fmt.Errorf("from email is required")
}
if cfg.FrontendURL == "" {
return fmt.Errorf("frontend URL is required")
}
return nil
}
// createSMTPClient создает SMTP клиент с правильными настройками
func createSMTPClient(cfg *config.Config) (*mail.Client, error) {
opts := []mail.Option{
mail.WithPort(cfg.SMTPPort),
mail.WithSMTPAuth(mail.SMTPAuthPlain),
mail.WithUsername(cfg.SMTPUsername),
mail.WithPassword(cfg.SMTPPassword),
}
// Настраиваем TLS в зависимости от порта
switch cfg.SMTPPort {
case 587:
// STARTTLS для порта 587
opts = append(opts, mail.WithTLSPolicy(mail.TLSMandatory))
case 465:
// SSL/TLS для порта 465
opts = append(opts, mail.WithSSL())
default:
// Opportunistic TLS для других портов
opts = append(opts, mail.WithTLSPolicy(mail.TLSOpportunistic))
}
return mail.NewClient(cfg.SMTPHost, opts...)
}
// loadTemplates загружает HTML шаблоны для писем
func loadTemplates() (*template.Template, error) {
tmpl := template.New("email")
templates := map[string]string{
"verification": verificationTemplate,
"password_reset": passwordResetTemplate,
"newsletter": newsletterTemplate,
}
for name, content := range templates {
var err error
tmpl, err = tmpl.New(name).Parse(content)
if err != nil {
return nil, fmt.Errorf("failed to parse template %s: %w", name, err)
}
}
return tmpl, nil
}
// IsActive возвращает статус сервиса
func (s *Service) IsActive() bool {
return s.isActive
}
// EmailData содержит данные для шаблонов писем
type EmailData struct {
UserName string
AppName string
FrontendURL string
Token string
Subject string
Content string
Year int
}
// SendVerificationEmail отправляет email для подтверждения адреса
func (s *Service) SendVerificationEmail(to, userName, token string) error {
if !s.isActive {
s.logger.Warn("Email service is disabled, skipping verification email",
zap.String("to", to), zap.String("user", userName))
return nil
}
s.logger.Info("Sending verification email", zap.String("to", to), zap.String("user", userName))
data := EmailData{
UserName: userName,
AppName: "Бегущий Башкир",
FrontendURL: s.config.FrontendURL,
Token: token,
Subject: "Подтверждение email",
Year: time.Now().Year(),
}
return s.sendEmail(to, "Подтверждение email - Бегущий Башкир", "verification", data)
}
// SendPasswordResetEmail отправляет email для сброса пароля
func (s *Service) SendPasswordResetEmail(to, userName, token string) error {
if !s.isActive {
s.logger.Warn("Email service is disabled, skipping password reset email",
zap.String("to", to), zap.String("user", userName))
return nil
}
s.logger.Info("Sending password reset email", zap.String("to", to), zap.String("user", userName))
data := EmailData{
UserName: userName,
AppName: "Бегущий Башкир",
FrontendURL: s.config.FrontendURL,
Token: token,
Subject: "Восстановление пароля",
Year: time.Now().Year(),
}
return s.sendEmail(to, "Восстановление пароля - Бегущий Башкир", "password_reset", data)
}
// SendNewsletterEmail отправляет email рассылку
func (s *Service) SendNewsletterEmail(to, userName, subject, content string) error {
if !s.isActive {
s.logger.Warn("Email service is disabled, skipping newsletter",
zap.String("to", to), zap.String("user", userName), zap.String("subject", subject))
return nil
}
s.logger.Info("Sending newsletter email",
zap.String("to", to),
zap.String("user", userName),
zap.String("subject", subject))
data := EmailData{
UserName: userName,
AppName: "Бегущий Башкир",
FrontendURL: s.config.FrontendURL,
Subject: subject,
Content: content,
Year: time.Now().Year(),
}
// Для новостей используем специальный шаблон
var body bytes.Buffer
if err := s.tmpl.ExecuteTemplate(&body, "newsletter", data); err != nil {
s.logger.Error("Failed to execute newsletter template", zap.Error(err))
return fmt.Errorf("failed to execute newsletter template: %w", err)
}
msg := mail.NewMsg()
if err := msg.From(s.fromAddr); err != nil {
return fmt.Errorf("failed to set from address: %w", err)
}
if err := msg.To(to); err != nil {
return fmt.Errorf("failed to set to address: %w", err)
}
msg.Subject(subject)
msg.SetBodyString(mail.TypeTextHTML, body.String())
if err := s.client.DialAndSend(msg); err != nil {
s.logger.Error("Failed to send newsletter email", zap.Error(err))
return fmt.Errorf("failed to send newsletter email: %w", err)
}
s.logger.Info("Newsletter email sent successfully", zap.String("to", to))
return nil
}
// sendEmail общий метод для отправки email
func (s *Service) sendEmail(to, subject, templateName string, data EmailData) error {
var body bytes.Buffer
if err := s.tmpl.ExecuteTemplate(&body, templateName, data); err != nil {
s.logger.Error("Failed to execute email template",
zap.String("template", templateName),
zap.Error(err))
return fmt.Errorf("failed to execute template %s: %w", templateName, err)
}
msg := mail.NewMsg()
if err := msg.From(s.fromAddr); err != nil {
return fmt.Errorf("failed to set from address: %w", err)
}
if err := msg.To(to); err != nil {
return fmt.Errorf("failed to set to address: %w", err)
}
msg.Subject(subject)
msg.SetBodyString(mail.TypeTextHTML, body.String())
if err := s.client.DialAndSend(msg); err != nil {
s.logger.Error("Failed to send email",
zap.String("type", templateName),
zap.String("to", to),
zap.Error(err))
return fmt.Errorf("failed to send email: %w", err)
}
s.logger.Info("Email sent successfully",
zap.String("type", templateName),
zap.String("to", to))
return nil
}
// TestConnection тестирует подключение к SMTP серверу
func (s *Service) TestConnection() error {
if !s.isActive {
return fmt.Errorf("email service is disabled")
}
// Создаем тестовое сообщение
msg := mail.NewMsg()
if err := msg.From(s.fromAddr); err != nil {
return fmt.Errorf("failed to set from address: %w", err)
}
if err := msg.To(s.fromAddr); err != nil {
return fmt.Errorf("failed to set to address: %w", err)
}
msg.Subject("Тестовое письмо - Бегущий Башкир")
msg.SetBodyString(mail.TypeTextPlain, "Это тестовое письмо для проверки подключения.")
// Пытаемся отправить тестовое письмо
if err := s.client.DialAndSend(msg); err != nil {
return fmt.Errorf("failed to send test email: %w", err)
}
s.logger.Info("SMTP connection test successful")
return nil
}
// Шаблоны писем остаются без изменений...
const verificationTemplate = `
{{define "verification"}}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #2e8b57, #3cb371); color: white; padding: 2rem; text-align: center; border-radius: 10px 10px 0 0; }
.content { padding: 2rem; background: #f8f9fa; }
.footer { padding: 1rem; text-align: center; color: #666; font-size: 0.9rem; }
.cta-button { display: inline-block; padding: 12px 24px; background: #2e8b57; color: white; text-decoration: none; border-radius: 5px; margin: 1rem 0; }
.token { background: #e9ecef; padding: 10px; border-radius: 5px; font-family: monospace; margin: 1rem 0; }
</style>
</head>
<body>
<div class="header">
<h1>🏃 Бегущий Башкир</h1>
<p>Подтверждение email адреса</p>
</div>
<div class="content">
<h2>Привет, {{.UserName}}!</h2>
<p>Благодарим за регистрацию в приложении "Бегущий Башкир". Для завершения регистрации подтвердите ваш email адрес.</p>
<a href="{{.FrontendURL}}/verify-email?token={{.Token}}" class="cta-button">
Подтвердить Email
</a>
<p>Или скопируйте эту ссылку в браузер:</p>
<div class="token">{{.FrontendURL}}/verify-email?token={{.Token}}</div>
<p>Ссылка действительна в течение 24 часов.</p>
</div>
<div class="footer">
<p>© {{.Year}} Бегущий Башкир. Все права защищены.</p>
</div>
</body>
</html>
{{end}}
`
const passwordResetTemplate = `
{{define "password_reset"}}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #dc3545, #e35d6a); color: white; padding: 2rem; text-align: center; border-radius: 10px 10px 0 0; }
.content { padding: 2rem; background: #f8f9fa; }
.footer { padding: 1rem; text-align: center; color: #666; font-size: 0.9rem; }
.cta-button { display: inline-block; padding: 12px 24px; background: #dc3545; color: white; text-decoration: none; border-radius: 5px; margin: 1rem 0; }
.token { background: #e9ecef; padding: 10px; border-radius: 5px; font-family: monospace; margin: 1rem 0; }
</style>
</head>
<body>
<div class="header">
<h1>🏃 Бегущий Башкир</h1>
<p>Восстановление пароля</p>
</div>
<div class="content">
<h2>Привет, {{.UserName}}!</h2>
<p>Мы получили запрос на восстановление пароля для вашего аккаунта.</p>
<a href="{{.FrontendURL}}/reset-password?token={{.Token}}" class="cta-button">
Восстановить пароль
</a>
<p>Или скопируйте эту ссылку в браузер:</p>
<div class="token">{{.FrontendURL}}/reset-password?token={{.Token}}</div>
<p>Если вы не запрашивали восстановление пароля, просто проигнорируйте это письмо.</p>
<p>Ссылка действительна в течение 1 часа.</p>
</div>
<div class="footer">
<p>© {{.Year}} Бегущий Башкир. Все права защищены.</p>
</div>
</body>
</html>
{{end}}
`
const newsletterTemplate = `
{{define "newsletter"}}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #2e8b57, #3cb371); color: white; padding: 2rem; text-align: center; border-radius: 10px 10px 0 0; }
.content { padding: 2rem; background: #f8f9fa; }
.footer { padding: 1rem; text-align: center; color: #666; font-size: 0.9rem; }
.newsletter-content { line-height: 1.8; }
.cta-button { display: inline-block; padding: 12px 24px; background: #2e8b57; color: white; text-decoration: none; border-radius: 5px; margin: 1rem 0; }
</style>
</head>
<body>
<div class="header">
<h1>🏃 Бегущий Башкир</h1>
<p>Новости и обновления</p>
</div>
<div class="content">
<h2>Привет, {{.UserName}}!</h2>
<div class="newsletter-content">
{{.Content}}
</div>
</div>
<div class="footer">
<p>© {{.Year}} Бегущий Башкир. Все права защищены.</p>
<p><a href="{{.FrontendURL}}/unsubscribe" style="color: #666;">Отписаться от рассылки</a></p>
</div>
</body>
</html>
{{end}}
`