From e8fd12bddf032fe7863af427bcf9e59b82dffb1c Mon Sep 17 00:00:00 2001 From: Kailash Nadh Date: Sun, 19 Jan 2025 11:41:17 +0530 Subject: [PATCH] Add `List-Unsubscribe` header to opt-in confirmation mails. Closes #2224. --- cmd/init.go | 5 +++-- cmd/notifications.go | 4 +++- cmd/subscribers.go | 14 +++++++++++++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/cmd/init.go b/cmd/init.go index 8b5eb899..e046c8fb 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -76,6 +76,7 @@ type constants struct { AllowExport bool `koanf:"allow_export"` AllowWipe bool `koanf:"allow_wipe"` RecordOptinIP bool `koanf:"record_optin_ip"` + UnsubHeader bool `koanf:"unsubscribe_header"` Exportable map[string]bool `koanf:"-"` DomainBlocklist []string `koanf:"-"` } `koanf:"privacy"` @@ -483,7 +484,7 @@ func initI18n(lang string, fs stuffbin.FileSystem) *i18n.I18n { // initCampaignManager initializes the campaign manager. func initCampaignManager(q *models.Queries, cs *constants, app *App) *manager.Manager { campNotifCB := func(subject string, data interface{}) error { - return app.sendNotification(cs.NotifyEmails, subject, notifTplCampaign, data) + return app.sendNotification(cs.NotifyEmails, subject, notifTplCampaign, data, nil) } if ko.Bool("passive") { @@ -541,7 +542,7 @@ func initImporter(q *models.Queries, db *sqlx.DB, core *core.Core, app *App) *su // Refresh cached subscriber counts and stats. core.RefreshMatViews(true) - app.sendNotification(app.constants.NotifyEmails, subject, notifTplImport, data) + app.sendNotification(app.constants.NotifyEmails, subject, notifTplImport, data, nil) return nil }, }, db.DB, app.i18n) diff --git a/cmd/notifications.go b/cmd/notifications.go index d2415fe8..eece80ed 100644 --- a/cmd/notifications.go +++ b/cmd/notifications.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "net/textproto" "regexp" "strings" @@ -27,7 +28,7 @@ type notifData struct { } // sendNotification sends out an e-mail notification to admins. -func (app *App) sendNotification(toEmails []string, subject, tplName string, data interface{}) error { +func (app *App) sendNotification(toEmails []string, subject, tplName string, data interface{}, headers textproto.MIMEHeader) error { if len(toEmails) == 0 { return nil } @@ -48,6 +49,7 @@ func (app *App) sendNotification(toEmails []string, subject, tplName string, dat m.Subject = subject m.Body = body m.Messenger = emailMsgr + m.Headers = headers if err := app.manager.PushMessage(m); err != nil { app.log.Printf("error sending admin notification (%s): %v", subject, err) return err diff --git a/cmd/subscribers.go b/cmd/subscribers.go index 1f65b38e..d15dd2b7 100644 --- a/cmd/subscribers.go +++ b/cmd/subscribers.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/http" + "net/textproto" "net/url" "strconv" "strings" @@ -663,8 +664,19 @@ func sendOptinConfirmationHook(app *App) func(sub models.Subscriber, listIDs []i out.OptinURL = fmt.Sprintf(app.constants.OptinURL, sub.UUID, qListIDs.Encode()) out.UnsubURL = fmt.Sprintf(app.constants.UnsubURL, dummyUUID, sub.UUID) + // Unsub headers. + h := textproto.MIMEHeader{} + h.Set(models.EmailHeaderSubscriberUUID, sub.UUID) + + // Attach List-Unsubscribe headers? + if app.constants.Privacy.UnsubHeader { + unsubURL := fmt.Sprintf(app.constants.UnsubURL, dummyUUID, sub.UUID) + h.Set("List-Unsubscribe-Post", "List-Unsubscribe=One-Click") + h.Set("List-Unsubscribe", `<`+unsubURL+`>`) + } + // Send the e-mail. - if err := app.sendNotification([]string{sub.Email}, app.i18n.T("subscribers.optinSubject"), notifSubscriberOptin, out); err != nil { + if err := app.sendNotification([]string{sub.Email}, app.i18n.T("subscribers.optinSubject"), notifSubscriberOptin, out, h); err != nil { app.log.Printf("error sending opt-in e-mail for subscriber %d (%s): %s", sub.ID, sub.UUID, err) return 0, err }