Files
tp/serv_nginx/api_bb/pkg/email/email.go
T
valitovgaziz 1e678c4b7e modified: serv_nginx/api_bb/.env
modified:   serv_nginx/api_bb/go.mod
	modified:   serv_nginx/api_bb/go.sum
	modified:   serv_nginx/api_bb/internal/database/migrate.go
	modified:   serv_nginx/api_bb/internal/handlers/auth.go
	new file:   serv_nginx/api_bb/internal/handlers/email_handler.go
	modified:   serv_nginx/api_bb/internal/handlers/handlers.go
	modified:   serv_nginx/api_bb/internal/models/user.go
	new file:   serv_nginx/api_bb/internal/repository/email_repository.go
	modified:   serv_nginx/api_bb/internal/repository/user_repository.go
	modified:   serv_nginx/api_bb/internal/routes/routes.go
	new file:   serv_nginx/api_bb/internal/service/email_service.go
	modified:   serv_nginx/api_bb/internal/service/user_service.go
	new file:   serv_nginx/api_bb/pkg/email/email.go
add email sender, vrificator and reset password
2025-10-22 05:16:30 +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}}
`