package mail import ( "bytes" "fmt" "html/template" "net/mail" "net/url" "strings" "git.kirsle.net/apps/gophertype/pkg/console" "git.kirsle.net/apps/gophertype/pkg/markdown" "git.kirsle.net/apps/gophertype/pkg/models" "git.kirsle.net/apps/gophertype/pkg/responses" "git.kirsle.net/apps/gophertype/pkg/settings" "github.com/microcosm-cc/bluemonday" gomail "gopkg.in/gomail.v2" ) // Email configuration. type Email struct { To string ReplyTo string Admin bool /* admin view of the email */ Subject string UnsubscribeURL string Data map[string]interface{} Template string } // SendEmail sends an email. func SendEmail(email Email) { s := settings.Current // Suppress sending any mail when no mail settings are configured, but go // through the motions -- great for local dev. var doNotMail bool if !s.MailEnabled || s.MailHost == "" || s.MailPort == 0 || s.MailSender == "" { console.Info("Suppressing email: not completely configured") doNotMail = true } // Resolve the template. tmpl, err := responses.GetFile(email.Template) if err != nil { console.Error("SendEmail: %s", err.Error()) return } // Render the template to HTML. var html bytes.Buffer t := template.New(email.Template) t, err = t.Parse(string(tmpl)) if err != nil { console.Error("SendEmail: template parsing error: %s", err.Error()) } // Execute the template. err = t.ExecuteTemplate(&html, email.Template, email) if err != nil { console.Error("SendEmail: template execution error: %s", err.Error()) } // Condense the body down to plain text, lazily. Who even has a plain text // email client anymore? rawLines := strings.Split( bluemonday.StrictPolicy().Sanitize(html.String()), "\n", ) var lines []string for _, line := range rawLines { line = strings.TrimSpace(line) if len(line) == 0 { continue } lines = append(lines, line) } plaintext := strings.Join(lines, "\n\n") // If we're not actually going to send the mail, this is a good place to stop. if doNotMail { console.Info("Not going to send an email.") console.Debug("The message was going to be:\n%s", plaintext) return } m := gomail.NewMessage() m.SetHeader("From", fmt.Sprintf("%s <%s>", s.Title, s.MailSender)) m.SetHeader("To", email.To) if email.ReplyTo != "" { m.SetHeader("Reply-To", email.ReplyTo) } m.SetHeader("Subject", email.Subject) m.SetBody("text/plain", plaintext) m.AddAlternative("text/html", html.String()) d := gomail.NewDialer(s.MailHost, s.MailPort, s.MailUsername, s.MailPassword) console.Info("SendEmail: %s (%s) to %s", email.Subject, email.Template, email.To) if err := d.DialAndSend(m); err != nil { console.Error("SendEmail: %s", err.Error()) } } // NotifyComment sends notification emails about comments. func NotifyComment(subject string, originURL string, c models.Comment) { s := settings.Current if s.BaseURL == "" { console.Error("Can't send comment notification because the site URL is not configured") return } // Prepare the email payload. email := Email{ Template: "_builtin/email/comment.gohtml", Subject: "Comment Added: " + subject, Data: map[string]interface{}{ "Name": c.Name, "Subject": subject, "Body": template.HTML(markdown.RenderMarkdown(c.Body)), "URL": strings.Trim(s.BaseURL, "/") + originURL, "QuickDelete": fmt.Sprintf("%s/comments/quick-delete?d=%s&next=%s", strings.Trim(s.BaseURL, "/"), url.QueryEscape(c.DeleteToken), url.QueryEscape(strings.Trim(s.BaseURL, "/")+originURL), ), }, } // Email the site admins. if adminEmails, err := models.Users.ListAdminEmails(); err == nil { email.To = strings.Join(adminEmails, ", ") email.Admin = true console.Info("Mail site admin '%s' about comment notification on '%s'", email.To, c.Thread) SendEmail(email) } // // Email the subscribers. email.Admin = false if subscribers, err := models.Comments.GetSubscribers(c.Thread); err == nil { email.Subject = "A new comment has been added to: " + subject for _, to := range subscribers { // Don't email to the writer of the comment. if to == c.Email { continue } email.To = to email.UnsubscribeURL = fmt.Sprintf("%s/comments/subscription?t=%s&e=%s", strings.Trim(s.BaseURL, "/"), url.QueryEscape(c.Thread), url.QueryEscape(to), ) console.Info("Mail subscriber '%s' about comment notification on '%s'", email.To, c.Thread) SendEmail(email) } } } // ParseAddress parses an email address. func ParseAddress(addr string) (*mail.Address, error) { return mail.ParseAddress(addr) }