Admin email on new comments + Quick Delete
parent
ceb42aa4d0
commit
87fbdea68b
|
@ -9,6 +9,7 @@ import (
|
||||||
|
|
||||||
"git.kirsle.net/apps/gophertype/pkg/authentication"
|
"git.kirsle.net/apps/gophertype/pkg/authentication"
|
||||||
"git.kirsle.net/apps/gophertype/pkg/glue"
|
"git.kirsle.net/apps/gophertype/pkg/glue"
|
||||||
|
"git.kirsle.net/apps/gophertype/pkg/mail"
|
||||||
"git.kirsle.net/apps/gophertype/pkg/models"
|
"git.kirsle.net/apps/gophertype/pkg/models"
|
||||||
"git.kirsle.net/apps/gophertype/pkg/responses"
|
"git.kirsle.net/apps/gophertype/pkg/responses"
|
||||||
"git.kirsle.net/apps/gophertype/pkg/session"
|
"git.kirsle.net/apps/gophertype/pkg/session"
|
||||||
|
@ -197,6 +198,9 @@ func PostComment(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notify site admins and subscribers by email.
|
||||||
|
go mail.NotifyComment(v.V["Subject"].(string), v.V["OriginURL"].(string), comment)
|
||||||
|
|
||||||
session.Flash(w, r, "Your comment has been added!")
|
session.Flash(w, r, "Your comment has been added!")
|
||||||
responses.Redirect(w, r, form.Get("origin"))
|
responses.Redirect(w, r, form.Get("origin"))
|
||||||
return
|
return
|
||||||
|
@ -211,8 +215,27 @@ func PostComment(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// CommentQuickDelete handles quick-delete links to remove spam comments.
|
// CommentQuickDelete handles quick-delete links to remove spam comments.
|
||||||
func CommentQuickDelete(w http.ResponseWriter, r *http.Request) {
|
func CommentQuickDelete(w http.ResponseWriter, r *http.Request) {
|
||||||
v := responses.NewTemplateVars(w, r)
|
// Query parameters.
|
||||||
responses.RenderTemplate(w, r, "_builtin/blog/view-post.gohtml", v)
|
var (
|
||||||
|
deleteToken = r.URL.Query().Get("d")
|
||||||
|
nextURL = r.URL.Query().Get("next")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Look up the comment by thread and quick-delete token.
|
||||||
|
comment, err := models.Comments.LoadByDeleteToken(deleteToken)
|
||||||
|
if err != nil {
|
||||||
|
responses.Forbidden(w, r, "Comment by Delete Token not found.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
comment.Delete()
|
||||||
|
|
||||||
|
session.Flash(w, r, "Comment has been quick-deleted!")
|
||||||
|
|
||||||
|
if nextURL == "" {
|
||||||
|
nextURL = "/"
|
||||||
|
}
|
||||||
|
responses.Redirect(w, r, nextURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getEditToken gets the edit token from the user's session.
|
// getEditToken gets the edit token from the user's session.
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -64,6 +64,21 @@ func GetUserByEmail(email string) (User, error) {
|
||||||
return user, r.Error
|
return user, r.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListAdminEmails returns the array of email addresses of all admin users.
|
||||||
|
func ListAdminEmails() ([]string, error) {
|
||||||
|
var (
|
||||||
|
users []User
|
||||||
|
emails []string
|
||||||
|
)
|
||||||
|
r := DB.Where("is_admin=true AND email IS NOT NULL").Find(&users)
|
||||||
|
for _, user := range users {
|
||||||
|
if len(user.Email) > 0 {
|
||||||
|
emails = append(emails, user.Email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return emails, r.Error
|
||||||
|
}
|
||||||
|
|
||||||
// SetPassword stores the hashed password for a user.
|
// SetPassword stores the hashed password for a user.
|
||||||
func (u *User) SetPassword(password string) error {
|
func (u *User) SetPassword(password string) error {
|
||||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), constants.BcryptCost)
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), constants.BcryptCost)
|
||||||
|
|
|
@ -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