A web blog and personal homepage engine written in Go.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

175 lines
4.8 KiB

  1. package mail
  2. import (
  3. "bytes"
  4. "fmt"
  5. "html/template"
  6. "net/mail"
  7. "net/url"
  8. "strings"
  9. "git.kirsle.net/apps/gophertype/pkg/console"
  10. "git.kirsle.net/apps/gophertype/pkg/markdown"
  11. "git.kirsle.net/apps/gophertype/pkg/models"
  12. "git.kirsle.net/apps/gophertype/pkg/responses"
  13. "git.kirsle.net/apps/gophertype/pkg/settings"
  14. "github.com/microcosm-cc/bluemonday"
  15. gomail "gopkg.in/gomail.v2"
  16. )
  17. // Email configuration.
  18. type Email struct {
  19. To string
  20. ReplyTo string
  21. Admin bool /* admin view of the email */
  22. Subject string
  23. UnsubscribeURL string
  24. Data map[string]interface{}
  25. Template string
  26. }
  27. // SendEmail sends an email.
  28. func SendEmail(email Email) {
  29. s := settings.Current
  30. // Suppress sending any mail when no mail settings are configured, but go
  31. // through the motions -- great for local dev.
  32. var doNotMail bool
  33. if !s.MailEnabled || s.MailHost == "" || s.MailPort == 0 || s.MailSender == "" {
  34. console.Info("Suppressing email: not completely configured")
  35. doNotMail = true
  36. }
  37. // Resolve the template.
  38. tmpl, err := responses.GetFile(email.Template)
  39. if err != nil {
  40. console.Error("SendEmail: %s", err.Error())
  41. return
  42. }
  43. // Render the template to HTML.
  44. var html bytes.Buffer
  45. t := template.New(email.Template)
  46. t, err = t.Parse(string(tmpl))
  47. if err != nil {
  48. console.Error("SendEmail: template parsing error: %s", err.Error())
  49. }
  50. // Execute the template.
  51. err = t.ExecuteTemplate(&html, email.Template, email)
  52. if err != nil {
  53. console.Error("SendEmail: template execution error: %s", err.Error())
  54. }
  55. // Condense the body down to plain text, lazily. Who even has a plain text
  56. // email client anymore?
  57. rawLines := strings.Split(
  58. bluemonday.StrictPolicy().Sanitize(html.String()),
  59. "\n",
  60. )
  61. var lines []string
  62. for _, line := range rawLines {
  63. line = strings.TrimSpace(line)
  64. if len(line) == 0 {
  65. continue
  66. }
  67. lines = append(lines, line)
  68. }
  69. plaintext := strings.Join(lines, "\n\n")
  70. // If we're not actually going to send the mail, this is a good place to stop.
  71. if doNotMail {
  72. console.Info("Not going to send an email.")
  73. console.Debug("The message was going to be:\n%s", plaintext)
  74. return
  75. }
  76. m := gomail.NewMessage()
  77. m.SetHeader("From", fmt.Sprintf("%s <%s>", s.Title, s.MailSender))
  78. m.SetHeader("To", email.To)
  79. if email.ReplyTo != "" {
  80. m.SetHeader("Reply-To", email.ReplyTo)
  81. }
  82. m.SetHeader("Subject", email.Subject)
  83. m.SetBody("text/plain", plaintext)
  84. m.AddAlternative("text/html", html.String())
  85. d := gomail.NewDialer(s.MailHost, s.MailPort, s.MailUsername, s.MailPassword)
  86. console.Info("SendEmail: %s (%s) to %s", email.Subject, email.Template, email.To)
  87. if err := d.DialAndSend(m); err != nil {
  88. console.Error("SendEmail: %s", err.Error())
  89. }
  90. }
  91. // EmailAdmins sends an e-mail to all admin user email addresses.
  92. func EmailAdmins(email Email) {
  93. if adminEmails, err := models.Users.ListAdminEmails(); err == nil {
  94. email.To = strings.Join(adminEmails, ", ")
  95. email.Admin = true
  96. console.Info("Mail site admin '%s' about email '%s'", email.To, email.Subject)
  97. SendEmail(email)
  98. }
  99. }
  100. // NotifyComment sends notification emails about comments.
  101. func NotifyComment(subject string, originURL string, c models.Comment) {
  102. s := settings.Current
  103. if s.BaseURL == "" {
  104. console.Error("Can't send comment notification because the site URL is not configured")
  105. return
  106. }
  107. // Prepare the email payload.
  108. email := Email{
  109. Template: "_builtin/email/comment.gohtml",
  110. Subject: "Comment Added: " + subject,
  111. Data: map[string]interface{}{
  112. "Name": c.Name,
  113. "Subject": subject,
  114. "Body": template.HTML(markdown.RenderMarkdown(c.Body)),
  115. "URL": strings.Trim(s.BaseURL, "/") + originURL,
  116. "QuickDelete": fmt.Sprintf("%s/comments/quick-delete?d=%s&next=%s",
  117. strings.Trim(s.BaseURL, "/"),
  118. url.QueryEscape(c.DeleteToken),
  119. url.QueryEscape(strings.Trim(s.BaseURL, "/")+originURL),
  120. ),
  121. },
  122. }
  123. // Email the site admins.
  124. if adminEmails, err := models.Users.ListAdminEmails(); err == nil {
  125. email.To = strings.Join(adminEmails, ", ")
  126. email.Admin = true
  127. console.Info("Mail site admin '%s' about comment notification on '%s'", email.To, c.Thread)
  128. SendEmail(email)
  129. }
  130. // // Email the subscribers.
  131. email.Admin = false
  132. if subscribers, err := models.Comments.GetSubscribers(c.Thread); err == nil {
  133. email.Subject = "A new comment has been added to: " + subject
  134. for _, to := range subscribers {
  135. // Don't email to the writer of the comment.
  136. if to == c.Email {
  137. continue
  138. }
  139. email.To = to
  140. email.UnsubscribeURL = fmt.Sprintf("%s/comments/subscription?t=%s&e=%s",
  141. strings.Trim(s.BaseURL, "/"),
  142. url.QueryEscape(c.Thread),
  143. url.QueryEscape(to),
  144. )
  145. console.Info("Mail subscriber '%s' about comment notification on '%s'", email.To, c.Thread)
  146. SendEmail(email)
  147. }
  148. }
  149. }
  150. // ParseAddress parses an email address.
  151. func ParseAddress(addr string) (*mail.Address, error) {
  152. return mail.ParseAddress(addr)
  153. }