From ec097909dbe84e8317f4def41806626c636512c2 Mon Sep 17 00:00:00 2001 From: Kailash Nadh Date: Sat, 1 Aug 2020 17:54:51 +0530 Subject: [PATCH] Add support for `List-Unsubscribe` header. - Added as a setting in the settings UI. - Refactor Messenger.Push() method to accept messenger.Message{} instead of a growing number of positional arguments. --- campaigns.go | 11 +++++--- frontend/src/views/Settings.vue | 13 +++++++--- init.go | 1 + internal/manager/manager.go | 31 +++++++++++++++++++---- internal/messenger/emailer.go | 30 +++++++++++++--------- internal/messenger/messenger.go | 12 ++++++++- public.go | 17 +++++++------ schema.sql | 3 ++- settings.go | 1 + static/public/templates/subscription.html | 2 -- subscribers.go | 2 +- 11 files changed, 86 insertions(+), 37 deletions(-) diff --git a/campaigns.go b/campaigns.go index 351c037..af1967c 100644 --- a/campaigns.go +++ b/campaigns.go @@ -14,6 +14,7 @@ import ( "time" "github.com/gofrs/uuid" + "github.com/knadh/listmonk/internal/messenger" "github.com/knadh/listmonk/internal/subimporter" "github.com/knadh/listmonk/models" "github.com/labstack/echo" @@ -558,10 +559,12 @@ func sendTestMessage(sub models.Subscriber, camp *models.Campaign, app *App) err fmt.Sprintf("Error rendering message: %v", err)) } - if err := app.messenger.Push(camp.FromEmail, - []string{sub.Email}, - m.Subject(), - m.Body(), nil); err != nil { + if err := app.messenger.Push(messenger.Message{ + From: camp.FromEmail, + To: []string{sub.Email}, + Subject: m.Subject(), + Body: m.Body(), + }); err != nil { return err } diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue index 0416212..56598ab 100644 --- a/frontend/src/views/Settings.vue +++ b/frontend/src/views/Settings.vue @@ -104,6 +104,13 @@
+ + + + @@ -118,9 +125,9 @@ `) + out.Headers = h + } + + if err := m.messengers[msg.Campaign.MessengerID].Push(out); err != nil { m.logger.Printf("error sending message in campaign %s: %v", msg.Campaign.Name, err) select { @@ -265,8 +281,13 @@ func (m *Manager) messageWorker() { if !ok { return } - err := m.messengers[msg.Messenger].Push( - msg.From, msg.To, msg.Subject, msg.Body, nil) + + err := m.messengers[msg.Messenger].Push(messenger.Message{ + From: msg.From, + To: msg.To, + Subject: msg.Subject, + Body: msg.Body, + }) if err != nil { m.logger.Printf("error sending message '%s': %v", msg.Subject, err) } diff --git a/internal/messenger/emailer.go b/internal/messenger/emailer.go index 6c73600..c4e41e0 100644 --- a/internal/messenger/emailer.go +++ b/internal/messenger/emailer.go @@ -86,7 +86,7 @@ func (e *Emailer) Name() string { } // Push pushes a message to the server. -func (e *Emailer) Push(fromAddr string, toAddr []string, subject string, m []byte, atts []Attachment) error { +func (e *Emailer) Push(m Message) error { // If there are more than one SMTP servers, send to a random // one from the list. var ( @@ -101,9 +101,9 @@ func (e *Emailer) Push(fromAddr string, toAddr []string, subject string, m []byt // Are there attachments? var files []smtppool.Attachment - if atts != nil { - files = make([]smtppool.Attachment, 0, len(atts)) - for _, f := range atts { + if m.Attachments != nil { + files = make([]smtppool.Attachment, 0, len(m.Attachments)) + for _, f := range m.Attachments { a := smtppool.Attachment{ Filename: f.Name, Header: f.Header, @@ -114,21 +114,27 @@ func (e *Emailer) Push(fromAddr string, toAddr []string, subject string, m []byt } } - mtext, err := html2text.FromString(string(m), html2text.Options{PrettyTables: true}) + mtext, err := html2text.FromString(string(m.Body), + html2text.Options{PrettyTables: true}) if err != nil { return err } em := smtppool.Email{ - From: fromAddr, - To: toAddr, - Subject: subject, + From: m.From, + To: m.To, + Subject: m.Subject, Attachments: files, } - // If there are custom e-mail headers, attach them. + em.Headers = textproto.MIMEHeader{} + // Attach e-mail level headers. + if len(m.Headers) > 0 { + em.Headers = m.Headers + } + + // Attach SMTP level headers. if len(srv.EmailHeaders) > 0 { - em.Headers = textproto.MIMEHeader{} for k, v := range srv.EmailHeaders { em.Headers.Set(k, v) } @@ -136,11 +142,11 @@ func (e *Emailer) Push(fromAddr string, toAddr []string, subject string, m []byt switch srv.EmailFormat { case "html": - em.HTML = m + em.HTML = m.Body case "plain": em.Text = []byte(mtext) default: - em.HTML = m + em.HTML = m.Body em.Text = []byte(mtext) } diff --git a/internal/messenger/messenger.go b/internal/messenger/messenger.go index 67b8e32..bd182fb 100644 --- a/internal/messenger/messenger.go +++ b/internal/messenger/messenger.go @@ -6,11 +6,21 @@ import "net/textproto" // for instance, e-mail, SMS etc. type Messenger interface { Name() string - Push(fromAddr string, toAddr []string, subject string, message []byte, atts []Attachment) error + Push(Message) error Flush() error Close() error } +// Message is the message pushed to a Messenger. +type Message struct { + From string + To []string + Subject string + Body []byte + Headers textproto.MIMEHeader + Attachments []Attachment +} + // Attachment represents a file or blob attachment that can be // sent along with a message by a Messenger. type Attachment struct { diff --git a/public.go b/public.go index 0fd8c6e..6c753fa 100644 --- a/public.go +++ b/public.go @@ -146,7 +146,7 @@ func handleSubscriptionPage(c echo.Context) error { app = c.Get("app").(*App) campUUID = c.Param("campUUID") subUUID = c.Param("subUUID") - unsub, _ = strconv.ParseBool(c.FormValue("unsubscribe")) + unsub = c.Request().Method == http.MethodPost blocklist, _ = strconv.ParseBool(c.FormValue("blocklist")) out = unsubTpl{} ) @@ -366,19 +366,20 @@ func handleSelfExportSubscriberData(c echo.Context) error { } // Send the data as a JSON attachment to the subscriber. - const fname = "profile.json" - if err := app.messenger.Push(app.constants.FromEmail, - []string{data.Email}, - "Your profile data", - msg.Bytes(), - []messenger.Attachment{ + const fname = "data.json" + if err := app.messenger.Push(messenger.Message{ + From: app.constants.FromEmail, + To: []string{data.Email}, + Subject: "Your data", + Body: msg.Bytes(), + Attachments: []messenger.Attachment{ { Name: fname, Content: b, Header: messenger.MakeAttachmentHeader(fname, "base64"), }, }, - ); err != nil { + }); err != nil { app.log.Printf("error e-mailing subscriber profile: %s", err) return c.Render(http.StatusInternalServerError, tplMessage, makeMsgTpl("Error e-mailing data", "", diff --git a/schema.sql b/schema.sql index a6f4779..6377941 100644 --- a/schema.sql +++ b/schema.sql @@ -165,7 +165,7 @@ CREATE TABLE settings ( ); DROP INDEX IF EXISTS idx_settings_key; CREATE INDEX idx_settings_key ON settings(key); INSERT INTO settings (key, value) VALUES - ('app.root_url', '"https://localhost:9000"'), + ('app.root_url', '"http://localhost:9000"'), ('app.favicon_url', '""'), ('app.from_email', '"listmonk "'), ('app.logo_url', '"http://localhost:9000/public/static/logo.png"'), @@ -174,6 +174,7 @@ INSERT INTO settings (key, value) VALUES ('app.batch_size', '1000'), ('app.max_send_errors', '1000'), ('app.notify_emails', '["admin1@mysite.com", "admin2@mysite.com"]'), + ('privacy.unsubscribe_header', 'true'), ('privacy.allow_blocklist', 'true'), ('privacy.allow_export', 'true'), ('privacy.allow_wipe', 'true'), diff --git a/settings.go b/settings.go index 5920e89..1666661 100644 --- a/settings.go +++ b/settings.go @@ -24,6 +24,7 @@ type settings struct { Messengers []interface{} `json:"messengers"` + PrivacyUnsubHeader bool `json:"privacy.unsubscribe_header"` PrivacyAllowBlocklist bool `json:"privacy.allow_blocklist"` PrivacyAllowExport bool `json:"privacy.allow_export"` PrivacyAllowWipe bool `json:"privacy.allow_wipe"` diff --git a/static/public/templates/subscription.html b/static/public/templates/subscription.html index 87198a5..509dc89 100644 --- a/static/public/templates/subscription.html +++ b/static/public/templates/subscription.html @@ -5,8 +5,6 @@

Do you wish to unsubscribe from this mailing list?

- - {{ if .Data.AllowBlocklist }}

diff --git a/subscribers.go b/subscribers.go index 157e384..ed5683d 100644 --- a/subscribers.go +++ b/subscribers.go @@ -495,7 +495,7 @@ func handleExportSubscriberData(c echo.Context) error { } c.Response().Header().Set("Cache-Control", "no-cache") - c.Response().Header().Set("Content-Disposition", `attachment; filename="profile.json"`) + c.Response().Header().Set("Content-Disposition", `attachment; filename="data.json"`) return c.Blob(http.StatusOK, "application/json", b) }