From df65e20792d40f79c73f08079634b95b276efcef Mon Sep 17 00:00:00 2001 From: Dmitry Verkhoturov Date: Mon, 6 Apr 2020 02:06:41 +0200 Subject: [PATCH] add admin email notifications on new comments --- README.md | 2 + backend/app/cmd/server.go | 17 +++-- backend/app/notify/email.go | 108 ++++++++++++++++++++++++------- backend/app/notify/email_test.go | 47 +++++++++++++- compose-dev-backend.yml | 3 + docs/email.md | 5 ++ 6 files changed, 152 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 67a7c85da5..eebf641067 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,8 @@ _this is the recommended way to run remark42_ | notify.telegram.timeout | NOTIFY_TELEGRAM_TIMEOUT | `5s` | telegram timeout | | notify.email.fromAddress | NOTIFY_EMAIL_FROM | | from email address | | notify.email.verification_subj | NOTIFY_EMAIL_VERIFICATION_SUBJ | `Email verification` | verification message subject | +| notify.email.notify_admin | NOTIFY_EMAIL_ADMIN | `false` | notify admin on new comments via ADMIN_SHARED_EMAIL | +| notify.email.notify_admin_on_replies | NOTIFY_EMAIL_ADMIN_ON_REPLIES | `false` | notify admin on replies to comments as well (default is only to top-level comments) | | smtp.host | SMTP_HOST | | SMTP host | | smtp.port | SMTP_PORT | | SMTP port | | smtp.username | SMTP_USERNAME | | SMTP user name | diff --git a/backend/app/cmd/server.go b/backend/app/cmd/server.go index de5599e94d..6b332d316d 100644 --- a/backend/app/cmd/server.go +++ b/backend/app/cmd/server.go @@ -198,8 +198,10 @@ type NotifyGroup struct { API string `long:"api" env:"API" default:"https://api.telegram.org/bot" description:"telegram api prefix"` } `group:"telegram" namespace:"telegram" env-namespace:"TELEGRAM"` Email struct { - From string `long:"fromAddress" env:"FROM" description:"from email address"` - VerificationSubject string `long:"verification_subj" env:"VERIFICATION_SUBJ" description:"verification message subject"` + From string `long:"fromAddress" env:"FROM" description:"from email address"` + VerificationSubject string `long:"verification_subj" env:"VERIFICATION_SUBJ" description:"verification message subject"` + AdminNotifications bool `long:"notify_admin" env:"ADMIN" description:"notify admin on new comments via ADMIN_SHARED_EMAIL"` + AdminNotifyOnReplies bool `long:"notify_admin_on_replies" env:"ADMIN_ON_REPLIES" description:"notify admin on replies to comments as well (default is only to top-level comments)"` } `group:"email" namespace:"email" env-namespace:"EMAIL"` } @@ -764,9 +766,10 @@ func (s *ServerCommand) makeNotify(dataStore *service.DataStore, authenticator * destinations = append(destinations, tg) case "email": emailParams := notify.EmailParams{ - From: s.Notify.Email.From, - VerificationSubject: s.Notify.Email.VerificationSubject, - UnsubscribeURL: s.RemarkURL + "/email/unsubscribe.html", + From: s.Notify.Email.From, + VerificationSubject: s.Notify.Email.VerificationSubject, + UnsubscribeURL: s.RemarkURL + "/email/unsubscribe.html", + AdminNotifyOnReplies: s.Notify.Email.AdminNotifyOnReplies, // TODO: uncomment after #560 frontend part is ready and URL is known // SubscribeURL: s.RemarkURL + "/subscribe.html?token=", TokenGenFn: func(userID, email, site string) (string, error) { @@ -786,6 +789,10 @@ func (s *ServerCommand) makeNotify(dataStore *service.DataStore, authenticator * return tkn, nil }, } + // enable admin notifications only if admin email is set + if s.Notify.Email.AdminNotifications && s.Admin.Shared.Email != "" { + emailParams.AdminEmail = s.Admin.Shared.Email + } smtpParams := notify.SmtpParams{ Host: s.SMTP.Host, Port: s.SMTP.Port, diff --git a/backend/app/notify/email.go b/backend/app/notify/email.go index a2c3798d6e..d51c8b7983 100644 --- a/backend/app/notify/email.go +++ b/backend/app/notify/email.go @@ -26,6 +26,8 @@ type EmailParams struct { VerificationTemplate string // verification message template SubscribeURL string // full subscribe handler URL UnsubscribeURL string // full unsubscribe handler URL + AdminEmail string // admin email for sending notifications about new messages + AdminNotifyOnReplies bool // if we should send all messages to admin or only first-level comments TokenGenFn func(userID, email, site string) (string, error) // Unsubscribe token generation function } @@ -89,6 +91,7 @@ type msgTmplData struct { PostTitle string Email string UnsubscribeLink string + ForAdmin bool } // verifyTmplData store data for verification message template execution @@ -134,8 +137,13 @@ const (

Remark42

+ {{- if .ForAdmin}} +
New comment from {{.UserName}} on your site {{if .PostTitle}} to «{{.PostTitle}}»{{ end }}
+ {{- else }}
New reply from {{.UserName}} on your comment{{if .PostTitle}} to «{{.PostTitle}}»{{ end }}
+ {{- end }}
+ {{- if .ParentCommentText}}
{{.ParentUserName}} @@ -145,6 +153,7 @@ const (
{{.ParentCommentText}}
+ {{- end }}
@@ -156,9 +165,11 @@ const (
- Sent to {{.Email}} for {{.ParentUserName}} + Sent to {{.Email}}{{if not .ForAdmin}} for {{.ParentUserName}}{{ end }}
+ {{- if .UnsubscribeLink}} Unsubscribe + {{- end }}
[{{.CommentDate.Format "02.01.2006 at 15:04"}}]
@@ -177,10 +188,10 @@ const (

Remark42

Confirmation for {{.User}} on site {{.Site}}

- {{if .SubscribeURL}} + {{- if .SubscribeURL}}

Click here to subscribe to email notifications

Alternatively, you can use code below for subscription.

- {{ end }} + {{- end }}

TOKEN

Copy and paste this text into “token” field on comments page

@@ -248,6 +259,14 @@ func (e *Email) Send(ctx context.Context, req Request) (err error) { emails = append(emails, *email) } + email, err = e.createAdminEmail(req) + if err != nil { + return err + } + if email != nil { + emails = append(emails, *email) + } + errs := new(multierror.Error) for _, email := range emails { errs = multierror.Append(errs, repeater.NewDefault(5, time.Millisecond*250).Do( @@ -285,7 +304,7 @@ func (e *Email) createUserEmail(req Request) (*emailMessage, error) { return nil, nil } log.Printf("[DEBUG] send notification via %s, comment id %s", e, req.Comment.ID) - msg, err = e.buildMessageFromRequest(req) + msg, err = e.buildMessageFromRequest(req, false) if err != nil { return nil, err } @@ -294,6 +313,33 @@ func (e *Email) createUserEmail(req Request) (*emailMessage, error) { return &emailMessage{from: e.From, to: req.Email, message: msg}, nil } +// construct email for admin if it's enabled +func (e *Email) createAdminEmail(req Request) (*emailMessage, error) { + switch { + case req.Verification.Token != "": + // don't notify admin on verification request + return nil, nil + case e.AdminEmail == "": + // don't send anything if notifications to admin are disabled + return nil, nil + case req.Comment.ParentID != "" && !e.AdminNotifyOnReplies: + // don't send anything if it's a reply to someone's comment, + // and notifications for replies are disabled + return nil, nil + } + + var msg string + var err error + + log.Printf("[DEBUG] send admin notification via %s, comment id %s", e, req.Comment.ID) + msg, err = e.buildMessageFromRequest(req, true) + if err != nil { + return nil, err + } + + return &emailMessage{from: e.From, to: e.AdminEmail, message: msg}, nil +} + // buildVerificationMessage generates verification email message based on given input func (e *Email) buildVerificationMessage(user, email, token, site string) (string, error) { subject := e.VerificationSubject @@ -312,37 +358,55 @@ func (e *Email) buildVerificationMessage(user, email, token, site string) (strin } // buildMessageFromRequest generates email message based on Request using e.MsgTemplate -func (e *Email) buildMessageFromRequest(req Request) (string, error) { +func (e *Email) buildMessageFromRequest(req Request, forAdmin bool) (string, error) { subject := "New reply to your comment" + if forAdmin { + subject = "New comment to your site" + } if req.Comment.PostTitle != "" { subject += fmt.Sprintf(" for \"%s\"", req.Comment.PostTitle) } + token, err := e.TokenGenFn(req.parent.User.ID, req.Email, req.Comment.Locator.SiteID) - unsubscribeLink := e.UnsubscribeURL + "?site=" + req.Comment.Locator.SiteID + "&tkn=" + token if err != nil { return "", errors.Wrapf(err, "error creating token for unsubscribe link") } + unsubscribeLink := e.UnsubscribeURL + "?site=" + req.Comment.Locator.SiteID + "&tkn=" + token + if forAdmin { + unsubscribeLink = "" + } + + email := req.Email + if forAdmin { + email = e.AdminEmail + } + commentUrlPrefix := req.Comment.Locator.URL + uiNav msg := bytes.Buffer{} - err = e.msgTmpl.Execute(&msg, msgTmplData{ - UserName: req.Comment.User.Name, - UserPicture: req.Comment.User.Picture, - CommentText: req.Comment.Text, - CommentLink: commentUrlPrefix + req.Comment.ID, - CommentDate: req.Comment.Timestamp, - ParentUserName: req.parent.User.Name, - ParentUserPicture: req.parent.User.Picture, - ParentCommentText: req.parent.Text, - ParentCommentLink: commentUrlPrefix + req.parent.ID, - ParentCommentDate: req.parent.Timestamp, - PostTitle: req.Comment.PostTitle, - Email: req.Email, - UnsubscribeLink: unsubscribeLink, - }) + tmplData := msgTmplData{ + UserName: req.Comment.User.Name, + UserPicture: req.Comment.User.Picture, + CommentText: req.Comment.Text, + CommentLink: commentUrlPrefix + req.Comment.ID, + CommentDate: req.Comment.Timestamp, + PostTitle: req.Comment.PostTitle, + Email: email, + UnsubscribeLink: unsubscribeLink, + ForAdmin: forAdmin, + } + // in case of message to admin, parent message might be empty + if req.Comment.ParentID != "" { + tmplData.ParentUserName = req.parent.User.Name + tmplData.ParentUserPicture = req.parent.User.Picture + tmplData.ParentCommentText = req.parent.Text + tmplData.ParentCommentLink = commentUrlPrefix + req.parent.ID + tmplData.ParentCommentDate = req.parent.Timestamp + } + err = e.msgTmpl.Execute(&msg, tmplData) if err != nil { return "", errors.Wrapf(err, "error executing template to build comment reply message") } - return e.buildMessage(subject, msg.String(), req.Email, "text/html", unsubscribeLink) + return e.buildMessage(subject, msg.String(), email, "text/html", unsubscribeLink) } // buildMessage generates email message to send using net/smtp.Data() diff --git a/backend/app/notify/email_test.go b/backend/app/notify/email_test.go index f05784a61e..6dae231a19 100644 --- a/backend/app/notify/email_test.go +++ b/backend/app/notify/email_test.go @@ -179,7 +179,7 @@ func TestEmailSendClientError(t *testing.T) { } func TestEmail_Send(t *testing.T) { - email, err := NewEmail(EmailParams{From: "from@example.org"}, SmtpParams{}) + email, err := NewEmail(EmailParams{From: "from@example.org", AdminEmail: "admin@example.org"}, SmtpParams{}) assert.NoError(t, err) assert.NotNil(t, email) fakeSmtp := fakeTestSMTP{} @@ -187,7 +187,7 @@ func TestEmail_Send(t *testing.T) { email.TokenGenFn = TokenGenFn email.UnsubscribeURL = "https://remark42.com/api/v1/email/unsubscribe" req := Request{ - Comment: store.Comment{ID: "999", User: store.User{ID: "1", Name: "test_user"}, PostTitle: "test_title"}, + Comment: store.Comment{ID: "999", User: store.User{ID: "1", Name: "test_user"}, ParentID: "1", PostTitle: "test_title"}, parent: store.Comment{ID: "1", User: store.User{ID: "999", Name: "parent_user"}}, Email: "test@example.org", } @@ -196,7 +196,7 @@ func TestEmail_Send(t *testing.T) { assert.Equal(t, 1, fakeSmtp.readQuitCount()) assert.Equal(t, "test@example.org", fakeSmtp.readRcpt()) // test buildMessageFromRequest separately for message text - res, err := email.buildMessageFromRequest(req) + res, err := email.buildMessageFromRequest(req, false) assert.NoError(t, err) assert.Contains(t, res, `From: from@example.org To: test@example.org @@ -209,6 +209,47 @@ List-Unsubscribe: