diff --git a/cmd/bounce.go b/cmd/bounce.go index 0c2231a5..6f418a41 100644 --- a/cmd/bounce.go +++ b/cmd/bounce.go @@ -191,6 +191,19 @@ func handleBounceWebhook(c echo.Context) error { } bounces = append(bounces, bs...) + // Postmark. + case service == "postmark" && app.constants.BouncePostmarkEnabled: + bs, err := app.bounce.Postmark.ProcessBounce(rawReq, c) + if err != nil { + app.log.Printf("error processing postmark notification: %v", err) + if _, ok := err.(*echo.HTTPError); ok { + return err + } + + return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidData")) + } + bounces = append(bounces, bs...) + default: return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("bounces.unknownService")) } diff --git a/cmd/init.go b/cmd/init.go index f5c481a0..25f05bc3 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -103,6 +103,7 @@ type constants struct { BounceWebhooksEnabled bool BounceSESEnabled bool BounceSendgridEnabled bool + BouncePostmarkEnabled bool } type notifTpls struct { @@ -400,6 +401,8 @@ func initConstants() *constants { c.BounceWebhooksEnabled = ko.Bool("bounce.webhooks_enabled") c.BounceSESEnabled = ko.Bool("bounce.ses_enabled") c.BounceSendgridEnabled = ko.Bool("bounce.sendgrid_enabled") + c.BouncePostmarkEnabled = ko.Bool("bounce.postmark.enabled") + return &c } @@ -668,7 +671,15 @@ func initBounceManager(app *App) *bounce.Manager { SESEnabled: ko.Bool("bounce.ses_enabled"), SendgridEnabled: ko.Bool("bounce.sendgrid_enabled"), SendgridKey: ko.String("bounce.sendgrid_key"), - + Postmark: struct { + Enabled bool + Username string + Password string + }{ + ko.Bool("bounce.postmark.enabled"), + ko.String("bounce.postmark.username"), + ko.String("bounce.postmark.password"), + }, RecordBounceCB: app.core.RecordBounce, } diff --git a/cmd/settings.go b/cmd/settings.go index cbef5325..f8411b33 100644 --- a/cmd/settings.go +++ b/cmd/settings.go @@ -69,6 +69,7 @@ func handleGetSettings(c echo.Context) error { s.UploadS3AwsSecretAccessKey = strings.Repeat(pwdMask, utf8.RuneCountInString(s.UploadS3AwsSecretAccessKey)) s.SendgridKey = strings.Repeat(pwdMask, utf8.RuneCountInString(s.SendgridKey)) s.SecurityCaptchaSecret = strings.Repeat(pwdMask, utf8.RuneCountInString(s.SecurityCaptchaSecret)) + s.BouncePostmark.Password = strings.Repeat(pwdMask, utf8.RuneCountInString(s.BouncePostmark.Password)) return c.JSON(http.StatusOK, okResp{s}) } @@ -183,6 +184,9 @@ func handleUpdateSettings(c echo.Context) error { if set.SendgridKey == "" { set.SendgridKey = cur.SendgridKey } + if set.BouncePostmark.Password == "" { + set.BouncePostmark.Password = cur.BouncePostmark.Password + } if set.SecurityCaptchaSecret == "" { set.SecurityCaptchaSecret = cur.SecurityCaptchaSecret } diff --git a/cmd/upgrade.go b/cmd/upgrade.go index bd0c1759..3eedb1a3 100644 --- a/cmd/upgrade.go +++ b/cmd/upgrade.go @@ -36,6 +36,7 @@ var migList = []migFunc{ {"v2.3.0", migrations.V2_3_0}, {"v2.4.0", migrations.V2_4_0}, {"v2.5.0", migrations.V2_5_0}, + {"v2.6.0", migrations.V2_6_0}, } // upgrade upgrades the database to the current version by running SQL migration files diff --git a/docs/docs/content/bounces.md b/docs/docs/content/bounces.md index 15332df7..c13c83aa 100644 --- a/docs/docs/content/bounces.md +++ b/docs/docs/content/bounces.md @@ -18,12 +18,12 @@ Some mail servers may also return the bounce to the `Reply-To` address, which ca The bounce webhook API can be used to record bounce events with custom scripting. This could be by reading a mailbox, a database, or mail server logs. | Method | Endpoint | Description | -|--------|------------------|------------------------| +| ------ | ---------------- | ---------------------- | | `POST` | /webhooks/bounce | Record a bounce event. | | Name | Data type | Required/Optional | Description | -|-------------------|-----------|-------------------|--------------------------------------------------------------------------------------| +| ----------------- | --------- | ----------------- | ------------------------------------------------------------------------------------ | | `subscriber_uuid` | String | Optional | The UUID of the subscriber. Either this or `email` is required. | | `email` | String | Optional | The e-mail of the subscriber. Either this or `subscriber_uuid` is required. | | `campaign_uuid` | String | Optional | UUID of the campaign for which the bounce happened. | @@ -46,7 +46,7 @@ listmonk supports receiving bounce webhook events from the following SMTP provid |-----------------------------|------------------|-----------| | `https://listmonk.yoursite.com/webhooks/service/ses` | Amazon (AWS) SES | You can use these [Mautic steps](https://docs.mautic.org/en/channels/emails/bounce-management#amazon-webhook) as a general guide, but use your listmonk's endpoint instead. | | `https://listmonk.yoursite.com/webhooks/service/sendgrid` | Sendgrid / Twilio Signed event webhook | [More info](https://docs.sendgrid.com/for-developers/tracking-events/getting-started-event-webhook-security-features) | - +| `https://listmonk.yoursite.com/webhooks/service/postmark` | Postmark webhook | [More info](https://postmarkapp.com/developer/webhooks/webhooks-overview) ## Verification diff --git a/docs/swagger/collections.yaml b/docs/swagger/collections.yaml index 7cb37c86..d68df248 100644 --- a/docs/swagger/collections.yaml +++ b/docs/swagger/collections.yaml @@ -2703,6 +2703,8 @@ components: type: string settings.bounces.enableSendgrid: type: string + settings.bounces.enablePostmark: + type: string settings.bounces.enableWebhooks: type: string settings.bounces.enabled: @@ -2721,6 +2723,12 @@ components: type: string settings.bounces.sendgridKey: type: string + settings.bounces.postmarkUsername: + type: string + settings.bounces.postmarkUsernameHelp: + type: string + settings.bounces.postmarkPassword: + type: string settings.bounces.type: type: string settings.bounces.username: @@ -3398,6 +3406,12 @@ components: type: boolean bounce.sendgrid_key: type: string + bounce.postmark_enabled: + type: boolean + bounce.postmark_username: + type: string + bounce.postmark_password: + type: string bounce.mailboxes: type: array items: diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue index e04e4bb9..6fd4e06d 100644 --- a/frontend/src/views/Settings.vue +++ b/frontend/src/views/Settings.vue @@ -155,6 +155,12 @@ export default Vue.extend({ hasDummy = 'captcha'; } + if (this.isDummy(form['bounce.postmark'].password)) { + form['bounce.postmark'].password = ''; + } else if (this.hasDummy(form['bounce.postmark'].password)) { + hasDummy = 'postmark'; + } + for (let i = 0; i < form.messengers.length; i += 1) { // If it's the dummy UI password placeholder, ignore it. if (this.isDummy(form.messengers[i].password)) { diff --git a/frontend/src/views/settings/bounces.vue b/frontend/src/views/settings/bounces.vue index c1714d4c..8ffbc8c3 100644 --- a/frontend/src/views/settings/bounces.vue +++ b/frontend/src/views/settings/bounces.vue @@ -73,6 +73,33 @@ +
+
+ + + +
+
+ + + +
+
+ + + +
+
diff --git a/i18n/ca.json b/i18n/ca.json index 15685177..557fed5f 100644 --- a/i18n/ca.json +++ b/i18n/ca.json @@ -365,6 +365,7 @@ "settings.bounces.enableSES": "Activa SES", "settings.bounces.enableSendgrid": "Activa SendGrid", "settings.bounces.enableWebhooks": "Activa els webhooks pels rebots", + "settings.bounces.enablePostmark": "Activa Postmark", "settings.bounces.enabled": "Activat", "settings.bounces.folder": "Carpeta", "settings.bounces.folderHelp": "Nom de la carpeta IMAP a escanejar. Ex: Safata d'entrada.", @@ -577,4 +578,4 @@ "templates.subject": "Assumpte", "users.login": "Inicia sessió", "users.logout": "Tanca sessió" -} +} \ No newline at end of file diff --git a/i18n/cs-cz.json b/i18n/cs-cz.json index 0ca61523..a3649ff4 100644 --- a/i18n/cs-cz.json +++ b/i18n/cs-cz.json @@ -365,6 +365,7 @@ "settings.bounces.enableSES": "Povolit SES", "settings.bounces.enableSendgrid": "Povolit SendGrid", "settings.bounces.enableWebhooks": "Povolit webhooky v případě nedoručitelnosti", + "settings.bounces.enablePostmark": "Povolit Postmark", "settings.bounces.enabled": "Povoleno", "settings.bounces.folder": "Složka", "settings.bounces.folderHelp": "Název složky IMAP ke skenování. Např.: Došlá pošta.", @@ -577,4 +578,4 @@ "templates.subject": "Předmět", "users.login": "Přihlásit", "users.logout": "Odhlásit" -} +} \ No newline at end of file diff --git a/i18n/cy.json b/i18n/cy.json index abeb3856..900449f2 100644 --- a/i18n/cy.json +++ b/i18n/cy.json @@ -365,6 +365,7 @@ "settings.bounces.enableSES": "Galluogi SES", "settings.bounces.enableSendgrid": "Galluogi SendGrid", "settings.bounces.enableWebhooks": "Galluogi bachau gwe sydd wedi sboncio'n ôl", + "settings.bounces.enablePostmark": "Galluogi Postmark", "settings.bounces.enabled": "Wedi galluogi", "settings.bounces.folder": "Ffolder", "settings.bounces.folderHelp": "Enw'r ffolder IMAP i'w sganio. ee: blwch derbyn.", @@ -577,4 +578,4 @@ "templates.subject": "Pwnc", "users.login": "Mewngofnodi", "users.logout": "Allgofnodi" -} +} \ No newline at end of file diff --git a/i18n/de.json b/i18n/de.json index 1206ec05..f04055cf 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -365,6 +365,7 @@ "settings.bounces.enableSES": "SES aktivieren", "settings.bounces.enableSendgrid": "SendGrid aktivieren", "settings.bounces.enableWebhooks": "Bounce-Webhooks aktivieren", + "settings.bounces.enablePostmark": "Postmark aktivieren", "settings.bounces.enabled": "Aktiviert", "settings.bounces.folder": "Ordner", "settings.bounces.folderHelp": "Name des zu scannenden IMAP-Ordners. z.B.: Inbox.", @@ -577,4 +578,4 @@ "templates.subject": "Betreff", "users.login": "Anmelden", "users.logout": "Abmelden" -} +} \ No newline at end of file diff --git a/i18n/en.json b/i18n/en.json index 339f1ba2..be947549 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -363,6 +363,7 @@ "settings.bounces.enableSES": "Enable SES", "settings.bounces.enableSendgrid": "Enable SendGrid", "settings.bounces.enableWebhooks": "Enable bounce webhooks", + "settings.bounces.enablePostmark": "Enable Postmark", "settings.bounces.enabled": "Enabled", "settings.bounces.folder": "Folder", "settings.bounces.folderHelp": "Name of the IMAP folder to scan. Eg: Inbox.", @@ -372,6 +373,9 @@ "settings.bounces.scanInterval": "Scan interval", "settings.bounces.scanIntervalHelp": "Interval at which the bounce mailbox should be scanned for bounces (s for second, m for minute).", "settings.bounces.sendgridKey": "SendGrid Key", + "settings.bounces.postmarkUsername": "Postmark Username", + "settings.bounces.postmarkUsernameHelp": "Postmark allows you to enable basic authorization for webhooks. Make sure to enter the same credentials here and in your Postmark webhook settings.", + "settings.bounces.postmarkPassword": "Postmark Password", "settings.bounces.type": "Type", "settings.bounces.username": "Username", "settings.confirmRestart": "Ensure running campaigns are paused. Restart?", @@ -573,4 +577,4 @@ "templates.subject": "Subject", "users.login": "Login", "users.logout": "Logout" -} +} \ No newline at end of file diff --git a/i18n/es.json b/i18n/es.json index 7b2f45c1..40eacf40 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -366,6 +366,7 @@ "settings.bounces.enableSES": "Activar SES", "settings.bounces.enableSendgrid": "Activar SendGrid", "settings.bounces.enableWebhooks": "Activar webhooks de rebotes", + "settings.bounces.enablePostmark": "Activar Postmark", "settings.bounces.enabled": "Activado", "settings.bounces.folder": "Carpeta", "settings.bounces.folderHelp": "Nombre de la carpeta IMAP a escanear, por ejemplo: Entrada.", @@ -578,4 +579,4 @@ "templates.subject": "Asunto", "users.login": "Ingresar", "users.logout": "Salir" -} +} \ No newline at end of file diff --git a/i18n/fi.json b/i18n/fi.json index 4b53d0ae..2693c438 100644 --- a/i18n/fi.json +++ b/i18n/fi.json @@ -365,6 +365,7 @@ "settings.bounces.enableMailbox": "Ota käyttöön bounce-postilaatikko", "settings.bounces.enableSES": "Ota käyttöön SES", "settings.bounces.enableSendgrid": "Ota käyttöön SendGrid", + "settings.bounces.enablePostmark": "Ota käyttöön Postmark", "settings.bounces.enableWebhooks": "Ota käyttöön palautusten webhookit", "settings.bounces.enabled": "Käytössä", "settings.bounces.folder": "Kansio", @@ -578,4 +579,4 @@ "templates.subject": "Aihe", "users.login": "Kirjaudu sisään", "users.logout": "Kirjaudu ulos" -} +} \ No newline at end of file diff --git a/i18n/fr.json b/i18n/fr.json index 05c9eb75..a8db607e 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -366,6 +366,7 @@ "settings.bounces.enableSES": "Activer SES", "settings.bounces.enableSendgrid": "Activer SendGrid", "settings.bounces.enableWebhooks": "Activez les 'webhooks' de rebond", + "settings.bounces.enablePostmark": "Activer Postmark", "settings.bounces.enabled": "Activer", "settings.bounces.folder": "Dossier", "settings.bounces.folderHelp": "Nom du dossier IMAP à scanner. Exple : InBox.", @@ -578,4 +579,4 @@ "templates.subject": "Objet", "users.login": "Connecter", "users.logout": "Déconnecter" -} +} \ No newline at end of file diff --git a/i18n/hu.json b/i18n/hu.json index 0ce03b37..ea27b068 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -365,6 +365,7 @@ "settings.bounces.enableSES": "SES", "settings.bounces.enableSendgrid": "SendGrid", "settings.bounces.enableWebhooks": "Visszapattanó webhook", + "settings.bounces.enablePostmark": "Postmark", "settings.bounces.enabled": "Engedélyezve", "settings.bounces.folder": "Mappa", "settings.bounces.folderHelp": "A vizsgálandó IMAP mappa neve. Például: 'Inbox'", @@ -577,4 +578,4 @@ "templates.subject": "Tárgy", "users.login": "Belépés", "users.logout": "Kijelentkezés" -} +} \ No newline at end of file diff --git a/i18n/it.json b/i18n/it.json index 42f90d02..0997e6cf 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -366,6 +366,7 @@ "settings.bounces.enableSES": "Attiva SES", "settings.bounces.enableSendgrid": "Attiva SendGrid", "settings.bounces.enableWebhooks": "Attiva bounce webhooks", + "settings.bounces.enablePostmark": "Attiva Postmark", "settings.bounces.enabled": "Attivato", "settings.bounces.folder": "Cartella", "settings.bounces.folderHelp": "Nome della cartella IMAP da analizzare. Ad esempio: Posta in arrivo.", @@ -578,4 +579,4 @@ "templates.subject": "Oggeto", "users.login": "Accesso", "users.logout": "Esci" -} +} \ No newline at end of file diff --git a/i18n/jp.json b/i18n/jp.json index 8765350a..ddba9539 100644 --- a/i18n/jp.json +++ b/i18n/jp.json @@ -366,6 +366,7 @@ "settings.bounces.enableSES": "SESを有効にする", "settings.bounces.enableSendgrid": "SendGridを有効にする", "settings.bounces.enableWebhooks": "バウンスウェブフックを有効にする", + "settings.bounces.enablePostmark": "Postmarkを有効にする", "settings.bounces.enabled": "有効", "settings.bounces.folder": "フォルダ", "settings.bounces.folderHelp": "スキャンするIMAPフォルダの名前。 例: Inbox.", @@ -578,4 +579,4 @@ "templates.subject": "件名", "users.login": "ログイン", "users.logout": "ログアウト" -} +} \ No newline at end of file diff --git a/i18n/ml.json b/i18n/ml.json index 39e24ff6..91b26554 100644 --- a/i18n/ml.json +++ b/i18n/ml.json @@ -365,6 +365,7 @@ "settings.bounces.enableSES": "SES പ്രവർത്തനക്ഷമമാക്കുക", "settings.bounces.enableSendgrid": "SendGrid പ്രവർത്തനക്ഷമമാക്കുക", "settings.bounces.enableWebhooks": "ബൗൺസ് വെബ്‌ഹുക്കുകൾ പ്രവർത്തനക്ഷമമാക്കുക", + "settings.bounces.enablePostmark": "Postmark പ്രവർത്തനക്ഷമമാക്കുക", "settings.bounces.enabled": "പ്രവർത്തനക്ഷമമാക്കി", "settings.bounces.folder": "ഫോൾഡർ", "settings.bounces.folderHelp": "സ്കാൻ ചെയ്യാനുള്ള IMAP ഫോൾഡറിന്റെ പേര്. ഉദാ: ഇൻബോക്സ്.", @@ -577,4 +578,4 @@ "templates.subject": "വിഷയം", "users.login": "പ്രവേശിക്കുക", "users.logout": "പുറത്തുകടക്കുക" -} +} \ No newline at end of file diff --git a/i18n/nl.json b/i18n/nl.json index 903959cb..188d08b5 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -365,6 +365,7 @@ "settings.bounces.enableSES": "SES inschakelen", "settings.bounces.enableSendgrid": "SendGrid inschakelen", "settings.bounces.enableWebhooks": "Bounce webhooks inschakelen", + "settings.bounces.enablePostmark": "Postmark inschakelen", "settings.bounces.enabled": "Ingeschakeld", "settings.bounces.folder": "Map", "settings.bounces.folderHelp": "Naam van de IMAP map om te scannen. Bv.: Inbox.", @@ -577,4 +578,4 @@ "templates.subject": "Onderwerp", "users.login": "Inloggen", "users.logout": "Uitloggen" -} +} \ No newline at end of file diff --git a/i18n/pl.json b/i18n/pl.json index f78d4667..2ad5d91e 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -365,6 +365,7 @@ "settings.bounces.enableSES": "Włącz SES", "settings.bounces.enableSendgrid": "Włącz SendGrid", "settings.bounces.enableWebhooks": "Włącz webhooki odbić", + "settings.bounces.enablePostmark": "Włącz Postmark", "settings.bounces.enabled": "Włączone", "settings.bounces.folder": "Folder", "settings.bounces.folderHelp": "Nazwa folderu IMAP do skanowania. Np: Inbox.", @@ -577,4 +578,4 @@ "templates.subject": "Temat", "users.login": "Zaloguj", "users.logout": "Wyloguj" -} +} \ No newline at end of file diff --git a/i18n/pt-BR.json b/i18n/pt-BR.json index 566c536e..aea98a42 100644 --- a/i18n/pt-BR.json +++ b/i18n/pt-BR.json @@ -365,6 +365,7 @@ "settings.bounces.enableSES": "Ativar SES", "settings.bounces.enableSendgrid": "Ativar SendGrid", "settings.bounces.enableWebhooks": "Ativar webhooks bounce", + "settings.bounces.enablePostmark": "Ativar Postmark", "settings.bounces.enabled": "Ativado", "settings.bounces.folder": "Pasta", "settings.bounces.folderHelp": "Noma da pasta IMAP para escanear. Ex: Inbox.", @@ -577,4 +578,4 @@ "templates.subject": "Assunto", "users.login": "Entrar", "users.logout": "Sair" -} +} \ No newline at end of file diff --git a/i18n/pt.json b/i18n/pt.json index 89985889..a519ffd8 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -365,6 +365,7 @@ "settings.bounces.enableSES": "Ligar SES", "settings.bounces.enableSendgrid": "Ligar SendGrid", "settings.bounces.enableWebhooks": "Ligar webhooks de bounces", + "settings.bounces.enablePostmark": "Ligar Postmark", "settings.bounces.enabled": "Ligado", "settings.bounces.folder": "Pasta", "settings.bounces.folderHelp": "Nome da pasta IMAP para procurar. E.g.: Inbox.", @@ -577,4 +578,4 @@ "templates.subject": "Assunto", "users.login": "Entrar", "users.logout": "Sair" -} +} \ No newline at end of file diff --git a/i18n/ro.json b/i18n/ro.json index 5048e8df..9cec93c2 100644 --- a/i18n/ro.json +++ b/i18n/ro.json @@ -366,6 +366,7 @@ "settings.bounces.enableSES": "Activați SES", "settings.bounces.enableSendgrid": "Activați SendGrid", "settings.bounces.enableWebhooks": "Activați webhooks bounce", + "settings.bounces.enablePostmark": "Activați Postmark", "settings.bounces.enabled": "Activat", "settings.bounces.folder": "Director", "settings.bounces.folderHelp": "Numele folderului IMAP pentru a scana. De exemplu: Inbox.", diff --git a/i18n/ru.json b/i18n/ru.json index 1a997626..3a91b25d 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -365,6 +365,7 @@ "settings.bounces.enableSES": "Включить SES", "settings.bounces.enableSendgrid": "Включить SendGrid", "settings.bounces.enableWebhooks": "Включить веб-крючки отскока", + "settings.bounces.enablePostmark": "Включить Postmark", "settings.bounces.enabled": "Включено", "settings.bounces.folder": "Папка", "settings.bounces.folderHelp": "Имя папки IMAP для сканирования. Например: Входящие.", @@ -577,4 +578,4 @@ "templates.subject": "Тема", "users.login": "Вход в систему", "users.logout": "Выход из системы" -} +} \ No newline at end of file diff --git a/i18n/se.json b/i18n/se.json index ef9d07b0..54950cd2 100644 --- a/i18n/se.json +++ b/i18n/se.json @@ -365,6 +365,7 @@ "settings.bounces.enableSES": "Enable SES", "settings.bounces.enableSendgrid": "Enable SendGrid", "settings.bounces.enableWebhooks": "Enable bounce webhooks", + "settings.bounces.enablePostmark": "Enable Postmark", "settings.bounces.enabled": "Enabled", "settings.bounces.folder": "Folder", "settings.bounces.folderHelp": "Name of the IMAP folder to scan. Eg: Inbox.", @@ -577,4 +578,4 @@ "templates.subject": "Subject", "users.login": "Login", "users.logout": "Logout" -} +} \ No newline at end of file diff --git a/i18n/sk.json b/i18n/sk.json index 316510ce..a4ddb768 100644 --- a/i18n/sk.json +++ b/i18n/sk.json @@ -365,6 +365,7 @@ "settings.bounces.enableSES": "Zapnúť SES", "settings.bounces.enableSendgrid": "Zapnúť SendGrid", "settings.bounces.enableWebhooks": "Zapnúť webhooky pre nedoručiteľné", + "settings.bounces.enablePostmark": "Zapnúť Postmark", "settings.bounces.enabled": "Zapnuté", "settings.bounces.folder": "Priečinok", "settings.bounces.folderHelp": "Názov kontrolovaného priečinku IMAP. Napríklad INBOX.", @@ -577,4 +578,4 @@ "templates.subject": "Predmet", "users.login": "Prihlásiť", "users.logout": "Odhlásiť" -} +} \ No newline at end of file diff --git a/i18n/tr.json b/i18n/tr.json index 7cfdadae..53f9c06a 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -366,6 +366,7 @@ "settings.bounces.enableSES": "SES'i etkinleştirin", "settings.bounces.enableSendgrid": "SendGrid'i etkinleştirin", "settings.bounces.enableWebhooks": "Sıçrama web kancalarını etkinleştirin", + "settings.bounces.enablePostmark": "Postmark'i etkinleştirin", "settings.bounces.enabled": "Etkinleştir", "settings.bounces.folder": "Dizin", "settings.bounces.folderHelp": "Taranacak IMAP klasörünün adı. Örn: Gelen Kutusu.", @@ -578,4 +579,4 @@ "templates.subject": "Konu", "users.login": "Giriş", "users.logout": "Çıkış" -} +} \ No newline at end of file diff --git a/i18n/vi.json b/i18n/vi.json index 0117947d..81178468 100644 --- a/i18n/vi.json +++ b/i18n/vi.json @@ -366,6 +366,7 @@ "settings.bounces.enableSES": "Bật SES", "settings.bounces.enableSendgrid": "Bật SendGrid", "settings.bounces.enableWebhooks": "Bật webhook bị trả lại", + "settings.bounces.enablePostmark": "Bật Postmark", "settings.bounces.enabled": "Đã bật", "settings.bounces.folder": "Thư mục", "settings.bounces.folderHelp": "Tên của thư mục IMAP để quét. Vd: Hộp thư đến.", @@ -578,4 +579,4 @@ "templates.subject": "Chủ đề", "users.login": "Đăng nhập", "users.logout": "Đăng xuất" -} +} \ No newline at end of file diff --git a/i18n/zh-CN.json b/i18n/zh-CN.json index d4979434..9ebac187 100644 --- a/i18n/zh-CN.json +++ b/i18n/zh-CN.json @@ -365,6 +365,7 @@ "settings.bounces.enableSES": "启用SES", "settings.bounces.enableSendgrid": "启用SendGrid", "settings.bounces.enableWebhooks": "启用反弹webhooks", + "settings.bounces.enablePostmark": "启用Postmark", "settings.bounces.enabled": "已启用", "settings.bounces.folder": "文件夹", "settings.bounces.folderHelp": "要扫描的 IMAP 文件夹的名称。例如:收件箱。", @@ -577,4 +578,4 @@ "templates.subject": "主题", "users.login": "登录", "users.logout": "登出" -} +} \ No newline at end of file diff --git a/i18n/zh-TW.json b/i18n/zh-TW.json index f5911ecd..9aca067f 100644 --- a/i18n/zh-TW.json +++ b/i18n/zh-TW.json @@ -366,6 +366,7 @@ "settings.bounces.enableSES": "啟用SES", "settings.bounces.enableSendgrid": "啟用SendGrid", "settings.bounces.enableWebhooks": "啟用反彈webhooks", + "settings.bounces.enablePostmark": "啟用Postmark", "settings.bounces.enabled": "已啟用", "settings.bounces.folder": "文件夾", "settings.bounces.folderHelp": "要掃描的IMAP 文件夾的名稱。例如:收件箱。", @@ -578,4 +579,4 @@ "templates.subject": "主題", "users.login": "登錄", "users.logout": "登出" -} +} \ No newline at end of file diff --git a/internal/bounce/bounce.go b/internal/bounce/bounce.go index a646730e..857a50a7 100644 --- a/internal/bounce/bounce.go +++ b/internal/bounce/bounce.go @@ -33,6 +33,11 @@ type Opt struct { SESEnabled bool `json:"ses_enabled"` SendgridEnabled bool `json:"sendgrid_enabled"` SendgridKey string `json:"sendgrid_key"` + Postmark struct { + Enabled bool + Username string + Password string + } RecordBounceCB func(models.Bounce) error } @@ -43,6 +48,7 @@ type Manager struct { mailbox Mailbox SES *webhooks.SES Sendgrid *webhooks.Sendgrid + Postmark *webhooks.Postmark queries *Queries opt Opt log *log.Logger @@ -77,6 +83,7 @@ func New(opt Opt, q *Queries, lo *log.Logger) (*Manager, error) { if opt.SESEnabled { m.SES = webhooks.NewSES() } + if opt.SendgridEnabled { sg, err := webhooks.NewSendgrid(opt.SendgridKey) if err != nil { @@ -85,6 +92,10 @@ func New(opt Opt, q *Queries, lo *log.Logger) (*Manager, error) { m.Sendgrid = sg } } + + if opt.Postmark.Enabled { + m.Postmark = webhooks.NewPostmark(opt.Postmark.Username, opt.Postmark.Password) + } } return m, nil diff --git a/internal/bounce/webhooks/postmark.go b/internal/bounce/webhooks/postmark.go new file mode 100644 index 00000000..3bed6961 --- /dev/null +++ b/internal/bounce/webhooks/postmark.go @@ -0,0 +1,118 @@ +package webhooks + +import ( + "crypto/subtle" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/knadh/listmonk/models" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" +) + +type postmarkNotif struct { + RecordType string `json:"RecordType"` + MessageStream string `json:"MessageStream"` + ID int `json:"ID"` + Type string `json:"Type"` + TypeCode int `json:"TypeCode"` + Name string `json:"Name"` + Tag string `json:"Tag"` + MessageID string `json:"MessageID"` + Metadata map[string]string `json:"Metadata"` + ServerID int `json:"ServerID"` + Description string `json:"Description"` + Details string `json:"Details"` + Email string `json:"Email"` + From string `json:"From"` + BouncedAt time.Time `json:"BouncedAt"` // "2019-11-05T16:33:54.9070259Z" + DumpAvailable bool `json:"DumpAvailable"` + Inactive bool `json:"Inactive"` + CanActivate bool `json:"CanActivate"` + Subject string `json:"Subject"` + Content string `json:"Content"` +} + +// Postmark handles webhook notifications (mainly bounce notifications). +type Postmark struct { + authHandler echo.HandlerFunc +} + +func NewPostmark(username, password string) *Postmark { + return &Postmark{ + authHandler: middleware.BasicAuth(makePostmarkAuthHandler(username, password))(func(c echo.Context) error { + return nil + }), + } +} + +// ProcessBounce processes Postmark bounce notifications and returns one object. +func (p *Postmark) ProcessBounce(b []byte, c echo.Context) ([]models.Bounce, error) { + // Do basicauth. + if err := p.authHandler(c); err != nil { + return nil, err + } + + var n postmarkNotif + if err := json.Unmarshal(b, &n); err != nil { + return nil, fmt.Errorf("error unmarshalling postmark notification: %v", err) + } + + // Ignore non-bounce messages. + if n.RecordType != "Bounce" { + return nil, nil + } + + supportedBounceType := true + typ := models.BounceTypeHard + switch n.Type { + case "HardBounce", "BadEmailAddress", "ManuallyDeactivated": + typ = models.BounceTypeHard + case "SoftBounce", "Transient", "DnsError", "SpamNotification", "VirusNotification", "DMARCPolicy": + typ = models.BounceTypeSoft + case "SpamComplaint": + typ = models.BounceTypeComplaint + default: + supportedBounceType = false + } + + if !supportedBounceType { + return nil, fmt.Errorf("unsupported bounce type: %v", n.Type) + } + + // Look for the campaign ID in headers. + campUUID := "" + if v, ok := n.Metadata["X-Listmonk-Campaign"]; ok { + campUUID = v + } + + return []models.Bounce{{ + Email: strings.ToLower(n.Email), + CampaignUUID: campUUID, + Type: typ, + Source: "postmark", + Meta: json.RawMessage(b), + CreatedAt: n.BouncedAt, + }}, nil +} + +func makePostmarkAuthHandler(cfgUser, cfgPassword string) func(username, password string, c echo.Context) (bool, error) { + var ( + u = []byte(cfgUser) + p = []byte(cfgPassword) + ) + + return func(username, password string, c echo.Context) (bool, error) { + if len(u) == 0 || len(p) == 0 { + return true, nil + } + + if subtle.ConstantTimeCompare([]byte(username), u) == 1 && subtle.ConstantTimeCompare([]byte(password), p) == 1 { + return true, nil + } + + return false, nil + } +} diff --git a/internal/migrations/v2.6.0.go b/internal/migrations/v2.6.0.go new file mode 100644 index 00000000..f7a11f08 --- /dev/null +++ b/internal/migrations/v2.6.0.go @@ -0,0 +1,17 @@ +package migrations + +import ( + "github.com/jmoiron/sqlx" + "github.com/knadh/koanf/v2" + "github.com/knadh/stuffbin" +) + +// V2_6_0 performs the DB migrations. +func V2_6_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error { + // Insert new preference settings. + if _, err := db.Exec(`INSERT INTO settings (key, value) VALUES ('bounce.postmark', '{"enabled": false, "username": "", "password": ""}') ON CONFLICT DO NOTHING;`); err != nil { + return err + } + + return nil +} diff --git a/models/settings.go b/models/settings.go index 7638844d..2350f82f 100644 --- a/models/settings.go +++ b/models/settings.go @@ -92,7 +92,12 @@ type Settings struct { SESEnabled bool `json:"bounce.ses_enabled"` SendgridEnabled bool `json:"bounce.sendgrid_enabled"` SendgridKey string `json:"bounce.sendgrid_key"` - BounceBoxes []struct { + BouncePostmark struct { + Enabled bool `json:"enabled"` + Username string `json:"username"` + Password string `json:"password"` + } `json:"bounce.postmark"` + BounceBoxes []struct { UUID string `json:"uuid"` Enabled bool `json:"enabled"` Type string `json:"type"` diff --git a/schema.sql b/schema.sql index f8e5b451..3f89f6dc 100644 --- a/schema.sql +++ b/schema.sql @@ -255,6 +255,7 @@ INSERT INTO settings (key, value) VALUES ('bounce.ses_enabled', 'false'), ('bounce.sendgrid_enabled', 'false'), ('bounce.sendgrid_key', '""'), + ('bounce.postmark', '{"enabled": false, "username": "", "password": ""}'), ('bounce.mailboxes', '[{"enabled":false, "type": "pop", "host":"pop.yoursite.com","port":995,"auth_protocol":"userpass","username":"username","password":"password","return_path": "bounce@listmonk.yoursite.com","scan_interval":"15m","tls_enabled":true,"tls_skip_verify":false}]'), ('appearance.admin.custom_css', '""'),