setting_handler_email.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. package admin
  2. import (
  3. "html"
  4. "strings"
  5. "github.com/Wei-Shaw/sub2api/internal/handler/dto"
  6. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  7. "github.com/Wei-Shaw/sub2api/internal/service"
  8. "github.com/gin-gonic/gin"
  9. )
  10. // TestSMTPRequest 测试SMTP连接请求
  11. type TestSMTPRequest struct {
  12. SMTPHost string `json:"smtp_host"`
  13. SMTPPort int `json:"smtp_port"`
  14. SMTPUsername string `json:"smtp_username"`
  15. SMTPPassword string `json:"smtp_password"`
  16. SMTPUseTLS *bool `json:"smtp_use_tls"`
  17. }
  18. func resolveSMTPUseTLS(requested *bool, savedConfig *service.SMTPConfig) bool {
  19. if requested != nil {
  20. return *requested
  21. }
  22. return savedConfig != nil && savedConfig.UseTLS
  23. }
  24. // TestSMTPConnection 测试SMTP连接
  25. // POST /api/v1/admin/settings/test-smtp
  26. func (h *SettingHandler) TestSMTPConnection(c *gin.Context) {
  27. var req TestSMTPRequest
  28. if err := c.ShouldBindJSON(&req); err != nil {
  29. response.BadRequest(c, "Invalid request: "+err.Error())
  30. return
  31. }
  32. req.SMTPHost = strings.TrimSpace(req.SMTPHost)
  33. req.SMTPUsername = strings.TrimSpace(req.SMTPUsername)
  34. var savedConfig *service.SMTPConfig
  35. if cfg, err := h.emailService.GetSMTPConfig(c.Request.Context()); err == nil && cfg != nil {
  36. savedConfig = cfg
  37. }
  38. if req.SMTPHost == "" && savedConfig != nil {
  39. req.SMTPHost = savedConfig.Host
  40. }
  41. if req.SMTPPort <= 0 {
  42. if savedConfig != nil && savedConfig.Port > 0 {
  43. req.SMTPPort = savedConfig.Port
  44. } else {
  45. req.SMTPPort = 587
  46. }
  47. }
  48. if req.SMTPUsername == "" && savedConfig != nil {
  49. req.SMTPUsername = savedConfig.Username
  50. }
  51. password := strings.TrimSpace(req.SMTPPassword)
  52. if password == "" && savedConfig != nil {
  53. password = savedConfig.Password
  54. }
  55. if req.SMTPHost == "" {
  56. response.BadRequest(c, "SMTP host is required")
  57. return
  58. }
  59. config := &service.SMTPConfig{
  60. Host: req.SMTPHost,
  61. Port: req.SMTPPort,
  62. Username: req.SMTPUsername,
  63. Password: password,
  64. UseTLS: resolveSMTPUseTLS(req.SMTPUseTLS, savedConfig),
  65. }
  66. err := h.emailService.TestSMTPConnectionWithConfig(config)
  67. if err != nil {
  68. response.BadRequest(c, "SMTP connection test failed: "+err.Error())
  69. return
  70. }
  71. response.Success(c, gin.H{"message": "SMTP connection successful"})
  72. }
  73. // SendTestEmailRequest 发送测试邮件请求
  74. type SendTestEmailRequest struct {
  75. Email string `json:"email" binding:"required,email"`
  76. SMTPHost string `json:"smtp_host"`
  77. SMTPPort int `json:"smtp_port"`
  78. SMTPUsername string `json:"smtp_username"`
  79. SMTPPassword string `json:"smtp_password"`
  80. SMTPFrom string `json:"smtp_from_email"`
  81. SMTPFromName string `json:"smtp_from_name"`
  82. SMTPUseTLS *bool `json:"smtp_use_tls"`
  83. }
  84. // SendTestEmail 发送测试邮件
  85. // POST /api/v1/admin/settings/send-test-email
  86. func (h *SettingHandler) SendTestEmail(c *gin.Context) {
  87. var req SendTestEmailRequest
  88. if err := c.ShouldBindJSON(&req); err != nil {
  89. response.BadRequest(c, "Invalid request: "+err.Error())
  90. return
  91. }
  92. req.SMTPHost = strings.TrimSpace(req.SMTPHost)
  93. req.SMTPUsername = strings.TrimSpace(req.SMTPUsername)
  94. req.SMTPFrom = strings.TrimSpace(req.SMTPFrom)
  95. req.SMTPFromName = strings.TrimSpace(req.SMTPFromName)
  96. var savedConfig *service.SMTPConfig
  97. if cfg, err := h.emailService.GetSMTPConfig(c.Request.Context()); err == nil && cfg != nil {
  98. savedConfig = cfg
  99. }
  100. if req.SMTPHost == "" && savedConfig != nil {
  101. req.SMTPHost = savedConfig.Host
  102. }
  103. if req.SMTPPort <= 0 {
  104. if savedConfig != nil && savedConfig.Port > 0 {
  105. req.SMTPPort = savedConfig.Port
  106. } else {
  107. req.SMTPPort = 587
  108. }
  109. }
  110. if req.SMTPUsername == "" && savedConfig != nil {
  111. req.SMTPUsername = savedConfig.Username
  112. }
  113. password := strings.TrimSpace(req.SMTPPassword)
  114. if password == "" && savedConfig != nil {
  115. password = savedConfig.Password
  116. }
  117. if req.SMTPFrom == "" && savedConfig != nil {
  118. req.SMTPFrom = savedConfig.From
  119. }
  120. if req.SMTPFromName == "" && savedConfig != nil {
  121. req.SMTPFromName = savedConfig.FromName
  122. }
  123. if req.SMTPHost == "" {
  124. response.BadRequest(c, "SMTP host is required")
  125. return
  126. }
  127. config := &service.SMTPConfig{
  128. Host: req.SMTPHost,
  129. Port: req.SMTPPort,
  130. Username: req.SMTPUsername,
  131. Password: password,
  132. From: req.SMTPFrom,
  133. FromName: req.SMTPFromName,
  134. UseTLS: resolveSMTPUseTLS(req.SMTPUseTLS, savedConfig),
  135. }
  136. siteName := h.settingService.GetSiteName(c.Request.Context())
  137. subject := "[" + siteName + "] Test Email"
  138. body := `
  139. <!DOCTYPE html>
  140. <html>
  141. <head>
  142. <meta charset="UTF-8">
  143. <style>
  144. body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background-color: #f5f5f5; margin: 0; padding: 20px; }
  145. .container { max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
  146. .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; }
  147. .content { padding: 40px 30px; text-align: center; }
  148. .success { color: #10b981; font-size: 48px; margin-bottom: 20px; }
  149. .footer { background-color: #f8f9fa; padding: 20px; text-align: center; color: #999; font-size: 12px; }
  150. </style>
  151. </head>
  152. <body>
  153. <div class="container">
  154. <div class="header">
  155. <h1>` + html.EscapeString(siteName) + `</h1>
  156. </div>
  157. <div class="content">
  158. <div class="success">✓</div>
  159. <h2>Email Configuration Successful!</h2>
  160. <p>This is a test email to verify your SMTP settings are working correctly.</p>
  161. </div>
  162. <div class="footer">
  163. <p>This is an automated test message.</p>
  164. </div>
  165. </div>
  166. </body>
  167. </html>
  168. `
  169. if err := h.emailService.SendEmailWithConfig(config, req.Email, subject, body); err != nil {
  170. response.BadRequest(c, "Failed to send test email: "+err.Error())
  171. return
  172. }
  173. response.Success(c, gin.H{"message": "Test email sent successfully"})
  174. }
  175. // ListEmailTemplates returns all editable notification email templates.
  176. // GET /api/v1/admin/settings/email-templates
  177. func (h *SettingHandler) ListEmailTemplates(c *gin.Context) {
  178. if h.notificationEmailService == nil {
  179. response.InternalError(c, "notification email service is not configured")
  180. return
  181. }
  182. events := h.notificationEmailService.ListEventInfos()
  183. templates, err := h.notificationEmailService.ListTemplates(c.Request.Context())
  184. if err != nil {
  185. response.ErrorFrom(c, err)
  186. return
  187. }
  188. response.Success(c, dto.EmailTemplateListResponse{
  189. Events: emailTemplateEventOptionsToDTO(events),
  190. Locales: h.notificationEmailService.SupportedLocales(),
  191. Templates: emailTemplateSummariesToDTO(templates),
  192. Placeholders: emailTemplatePlaceholderUnion(events),
  193. })
  194. }
  195. // GetEmailTemplate returns one editable notification email template.
  196. // GET /api/v1/admin/settings/email-templates/:event/:locale
  197. func (h *SettingHandler) GetEmailTemplate(c *gin.Context) {
  198. if h.notificationEmailService == nil {
  199. response.InternalError(c, "notification email service is not configured")
  200. return
  201. }
  202. tmpl, err := h.notificationEmailService.GetTemplate(c.Request.Context(), c.Param("event"), c.Param("locale"))
  203. if err != nil {
  204. response.BadRequest(c, err.Error())
  205. return
  206. }
  207. response.Success(c, emailTemplateDetailToDTO(tmpl))
  208. }
  209. // UpdateEmailTemplate saves an override for one event/locale template.
  210. // PUT /api/v1/admin/settings/email-templates/:event/:locale
  211. func (h *SettingHandler) UpdateEmailTemplate(c *gin.Context) {
  212. if h.notificationEmailService == nil {
  213. response.InternalError(c, "notification email service is not configured")
  214. return
  215. }
  216. var req dto.UpdateEmailTemplateRequest
  217. if err := c.ShouldBindJSON(&req); err != nil {
  218. response.BadRequest(c, "Invalid request: "+err.Error())
  219. return
  220. }
  221. tmpl, err := h.notificationEmailService.UpdateTemplate(c.Request.Context(), c.Param("event"), c.Param("locale"), req.Subject, req.HTML)
  222. if err != nil {
  223. response.BadRequest(c, err.Error())
  224. return
  225. }
  226. response.Success(c, emailTemplateDetailToDTO(tmpl))
  227. }
  228. // RestoreOfficialEmailTemplate removes an override and returns the built-in template.
  229. // POST /api/v1/admin/settings/email-templates/:event/:locale/restore-official
  230. func (h *SettingHandler) RestoreOfficialEmailTemplate(c *gin.Context) {
  231. if h.notificationEmailService == nil {
  232. response.InternalError(c, "notification email service is not configured")
  233. return
  234. }
  235. tmpl, err := h.notificationEmailService.RestoreOfficialTemplate(c.Request.Context(), c.Param("event"), c.Param("locale"))
  236. if err != nil {
  237. response.BadRequest(c, err.Error())
  238. return
  239. }
  240. response.Success(c, emailTemplateDetailToDTO(tmpl))
  241. }
  242. // PreviewEmailTemplate renders a template with safe sample variables without saving it.
  243. // POST /api/v1/admin/settings/email-templates/preview
  244. func (h *SettingHandler) PreviewEmailTemplate(c *gin.Context) {
  245. if h.notificationEmailService == nil {
  246. response.InternalError(c, "notification email service is not configured")
  247. return
  248. }
  249. var req dto.PreviewEmailTemplateRequest
  250. if err := c.ShouldBindJSON(&req); err != nil {
  251. response.BadRequest(c, "Invalid request: "+err.Error())
  252. return
  253. }
  254. preview, err := h.notificationEmailService.PreviewTemplate(c.Request.Context(), service.NotificationEmailPreviewInput{
  255. Event: req.Event,
  256. Locale: req.Locale,
  257. Subject: req.Subject,
  258. HTML: req.HTML,
  259. Variables: req.Variables,
  260. })
  261. if err != nil {
  262. response.BadRequest(c, err.Error())
  263. return
  264. }
  265. response.Success(c, dto.EmailTemplatePreviewResponse{Subject: preview.Subject, HTML: preview.HTML})
  266. }
  267. func emailTemplateEventOptionsToDTO(events []service.NotificationEmailEventInfo) []dto.EmailTemplateEventOption {
  268. items := make([]dto.EmailTemplateEventOption, 0, len(events))
  269. for _, event := range events {
  270. items = append(items, dto.EmailTemplateEventOption{
  271. Value: event.Event,
  272. Label: event.Label,
  273. Description: event.Description,
  274. Category: event.Category,
  275. Optional: event.Optional,
  276. })
  277. }
  278. return items
  279. }
  280. func emailTemplateSummariesToDTO(templates []service.NotificationEmailTemplate) []dto.EmailTemplateSummary {
  281. items := make([]dto.EmailTemplateSummary, 0, len(templates))
  282. for _, tmpl := range templates {
  283. items = append(items, dto.EmailTemplateSummary{
  284. Event: tmpl.Event,
  285. Locale: tmpl.Locale,
  286. Subject: tmpl.Subject,
  287. IsCustom: tmpl.IsCustom,
  288. UpdatedAt: emailTemplateUpdatedAt(tmpl),
  289. })
  290. }
  291. return items
  292. }
  293. func emailTemplateDetailToDTO(tmpl service.NotificationEmailTemplate) dto.EmailTemplateDetail {
  294. return dto.EmailTemplateDetail{
  295. Event: tmpl.Event,
  296. Locale: tmpl.Locale,
  297. Subject: tmpl.Subject,
  298. HTML: tmpl.HTML,
  299. IsCustom: tmpl.IsCustom,
  300. UpdatedAt: emailTemplateUpdatedAt(tmpl),
  301. Placeholders: tmpl.Placeholders,
  302. }
  303. }
  304. func emailTemplateUpdatedAt(tmpl service.NotificationEmailTemplate) string {
  305. if tmpl.UpdatedAt == nil {
  306. return ""
  307. }
  308. return tmpl.UpdatedAt.Format("2006-01-02T15:04:05Z07:00")
  309. }
  310. func emailTemplatePlaceholderUnion(events []service.NotificationEmailEventInfo) []string {
  311. seen := make(map[string]struct{})
  312. placeholders := make([]string, 0)
  313. for _, event := range events {
  314. for _, placeholder := range event.Placeholders {
  315. if _, ok := seen[placeholder]; ok {
  316. continue
  317. }
  318. seen[placeholder] = struct{}{}
  319. placeholders = append(placeholders, placeholder)
  320. }
  321. }
  322. return placeholders
  323. }