4 changed files with 261 additions and 2 deletions
@ -0,0 +1,160 @@ |
|||
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.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
|
|||
// m := comments.LoadMailingList()
|
|||
// for _, to := range m.List(c.ThreadID) {
|
|||
// if to == c.Email {
|
|||
// continue // don't email yourself
|
|||
// }
|
|||
// email.To = to
|
|||
// email.UnsubscribeURL = fmt.Sprintf("%s/comments/subscription?t=%s&e=%s",
|
|||
// strings.Trim(s.Site.URL, "/"),
|
|||
// url.QueryEscape(c.ThreadID),
|
|||
// url.QueryEscape(to),
|
|||
// )
|
|||
// console.Info("Mail subscriber '%s' about comment notification on '%s'", email.To, c.ThreadID)
|
|||
// SendEmail(email)
|
|||
// }
|
|||
} |
|||
|
|||
// ParseAddress parses an email address.
|
|||
func ParseAddress(addr string) (*mail.Address, error) { |
|||
return mail.ParseAddress(addr) |
|||
} |
@ -0,0 +1,61 @@ |
|||
<!DOCTYPE html> |
|||
<html> |
|||
<head> |
|||
<meta charset="utf-8"> |
|||
<meta name="viewport" content="width=device-width"> |
|||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> |
|||
<meta name="x-apple-disable-message-reformatting"><!-- Disable auto-scale in iOS 10 Mail --> |
|||
<title>{{ .Subject }}</title> |
|||
</head> |
|||
<body width="100%" bgcolor="#FFFFFF" color="#000000" style="margin: 0; mso-line-height-rule: exactly;"> |
|||
|
|||
<center> |
|||
<table width="90%" cellspacing="0" cellpadding="8" style="border: 1px solid #000000"> |
|||
<tr> |
|||
<td align="left" valign="top" bgcolor="#C0C0C0"> |
|||
<font face="Helvetica,Arial,Verdana-sans-serif" size="6" color="#000000"> |
|||
<b>{{ .Subject }}</b> |
|||
</font> |
|||
</td> |
|||
</tr> |
|||
<tr> |
|||
<td align="left" valign="top" bgcolor="#FEFEFE"> |
|||
<font face="Helvetica,Arial,Verdana-sans-serif" size="3" color="#000000"> |
|||
{{ if not .Admin }} |
|||
Hello,<br><br> |
|||
{{ end }} |
|||
|
|||
{{ or .Data.Name "Anonymous" }} has left a comment on: {{ .Data.Subject }} |
|||
<br><br> |
|||
|
|||
{{ .Data.Body }} |
|||
<br><br> |
|||
|
|||
<hr> |
|||
|
|||
To view this comment, please go to <a href="{{ .Data.URL }}" target="_blank">{{ .Data.URL }}</a>. |
|||
|
|||
{{ if .Admin }} |
|||
<br><br> |
|||
Was this comment spam? <a href="{{ .Data.QuickDelete }}" target="_blank">Delete it</a>. |
|||
{{ end }} |
|||
|
|||
{{ if .UnsubscribeURL }} |
|||
<br><br> |
|||
To unsubscribe from this comment thread, visit <a href="{{ .UnsubscribeURL }}" target="_blank">{{ .UnsubscribeURL }}</a> |
|||
{{ end }} |
|||
</font> |
|||
</td> |
|||
</tr> |
|||
<tr> |
|||
<td align="left" valign="top" bgcolor="#C0C0C0"> |
|||
<font face="Helvetica,Arial,Verdana-sans-serif" size="3" color="#000000"> |
|||
This e-mail was automatically generated; do not reply to it. |
|||
</font> |
|||
</td> |
|||
</tr> |
|||
</table> |
|||
</center> |
|||
|
|||
</body> |
|||
</html> |
Loading…
Reference in new issue