Привет, {{.UserName}}!
Благодарим за регистрацию в приложении "Бегущий Башкир". Для завершения регистрации подтвердите ваш email адрес.
Подтвердить EmailИли скопируйте эту ссылку в браузер:
Ссылка действительна в течение 24 часов.
// 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"}}
Подтверждение email адреса
Благодарим за регистрацию в приложении "Бегущий Башкир". Для завершения регистрации подтвердите ваш email адрес.
Подтвердить EmailИли скопируйте эту ссылку в браузер:
Ссылка действительна в течение 24 часов.
Восстановление пароля
Мы получили запрос на восстановление пароля для вашего аккаунта.
Восстановить парольИли скопируйте эту ссылку в браузер:
Если вы не запрашивали восстановление пароля, просто проигнорируйте это письмо.
Ссылка действительна в течение 1 часа.
Новости и обновления