Ver código fonte

Add Postmark bounce webhook support (refactor #1385) (#1485)

Co-authored-by: Thomas Siebers <tom@tsiebers.de>
Kailash Nadh 1 ano atrás
pai
commit
2b95c88188

+ 13 - 0
cmd/bounce.go

@@ -191,6 +191,19 @@ func handleBounceWebhook(c echo.Context) error {
 		}
 		}
 		bounces = append(bounces, bs...)
 		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:
 	default:
 		return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("bounces.unknownService"))
 		return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("bounces.unknownService"))
 	}
 	}

+ 12 - 1
cmd/init.go

@@ -103,6 +103,7 @@ type constants struct {
 	BounceWebhooksEnabled bool
 	BounceWebhooksEnabled bool
 	BounceSESEnabled      bool
 	BounceSESEnabled      bool
 	BounceSendgridEnabled bool
 	BounceSendgridEnabled bool
+	BouncePostmarkEnabled bool
 }
 }
 
 
 type notifTpls struct {
 type notifTpls struct {
@@ -400,6 +401,8 @@ func initConstants() *constants {
 	c.BounceWebhooksEnabled = ko.Bool("bounce.webhooks_enabled")
 	c.BounceWebhooksEnabled = ko.Bool("bounce.webhooks_enabled")
 	c.BounceSESEnabled = ko.Bool("bounce.ses_enabled")
 	c.BounceSESEnabled = ko.Bool("bounce.ses_enabled")
 	c.BounceSendgridEnabled = ko.Bool("bounce.sendgrid_enabled")
 	c.BounceSendgridEnabled = ko.Bool("bounce.sendgrid_enabled")
+	c.BouncePostmarkEnabled = ko.Bool("bounce.postmark.enabled")
+
 	return &c
 	return &c
 }
 }
 
 
@@ -668,7 +671,15 @@ func initBounceManager(app *App) *bounce.Manager {
 		SESEnabled:      ko.Bool("bounce.ses_enabled"),
 		SESEnabled:      ko.Bool("bounce.ses_enabled"),
 		SendgridEnabled: ko.Bool("bounce.sendgrid_enabled"),
 		SendgridEnabled: ko.Bool("bounce.sendgrid_enabled"),
 		SendgridKey:     ko.String("bounce.sendgrid_key"),
 		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,
 		RecordBounceCB: app.core.RecordBounce,
 	}
 	}
 
 

+ 4 - 0
cmd/settings.go

@@ -69,6 +69,7 @@ func handleGetSettings(c echo.Context) error {
 	s.UploadS3AwsSecretAccessKey = strings.Repeat(pwdMask, utf8.RuneCountInString(s.UploadS3AwsSecretAccessKey))
 	s.UploadS3AwsSecretAccessKey = strings.Repeat(pwdMask, utf8.RuneCountInString(s.UploadS3AwsSecretAccessKey))
 	s.SendgridKey = strings.Repeat(pwdMask, utf8.RuneCountInString(s.SendgridKey))
 	s.SendgridKey = strings.Repeat(pwdMask, utf8.RuneCountInString(s.SendgridKey))
 	s.SecurityCaptchaSecret = strings.Repeat(pwdMask, utf8.RuneCountInString(s.SecurityCaptchaSecret))
 	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})
 	return c.JSON(http.StatusOK, okResp{s})
 }
 }
@@ -183,6 +184,9 @@ func handleUpdateSettings(c echo.Context) error {
 	if set.SendgridKey == "" {
 	if set.SendgridKey == "" {
 		set.SendgridKey = cur.SendgridKey
 		set.SendgridKey = cur.SendgridKey
 	}
 	}
+	if set.BouncePostmark.Password == "" {
+		set.BouncePostmark.Password = cur.BouncePostmark.Password
+	}
 	if set.SecurityCaptchaSecret == "" {
 	if set.SecurityCaptchaSecret == "" {
 		set.SecurityCaptchaSecret = cur.SecurityCaptchaSecret
 		set.SecurityCaptchaSecret = cur.SecurityCaptchaSecret
 	}
 	}

+ 1 - 0
cmd/upgrade.go

@@ -36,6 +36,7 @@ var migList = []migFunc{
 	{"v2.3.0", migrations.V2_3_0},
 	{"v2.3.0", migrations.V2_3_0},
 	{"v2.4.0", migrations.V2_4_0},
 	{"v2.4.0", migrations.V2_4_0},
 	{"v2.5.0", migrations.V2_5_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
 // upgrade upgrades the database to the current version by running SQL migration files

+ 3 - 3
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.
 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            |
 | Method | Endpoint         | Description            |
-|--------|------------------|------------------------|
+| ------ | ---------------- | ---------------------- |
 | `POST` | /webhooks/bounce | Record a bounce event. |
 | `POST` | /webhooks/bounce | Record a bounce event. |
 
 
 
 
 | Name              | Data type | Required/Optional | Description                                                                          |
 | Name              | Data type | Required/Optional | Description                                                                          |
-|-------------------|-----------|-------------------|--------------------------------------------------------------------------------------|
+| ----------------- | --------- | ----------------- | ------------------------------------------------------------------------------------ |
 | `subscriber_uuid` | String    | Optional          | The UUID of the subscriber. Either this or `email` is required.                      |
 | `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.          |
 | `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.                                  |
 | `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. <ul>  <li>When creating the *topic* select "standard" instead of the preselected "FIFO". You can put a name and leave everything else at default.</li>  <li>When creating a *subscription* choose HTTPS for "Protocol", and leave *"Enable raw message delivery"* UNCHECKED.</li>  <li>On the _"SES -> verified identities"_ page, make sure to check **"[include original headers](https://github.com/knadh/listmonk/issues/720#issuecomment-1046877192)"**.</li>  <li>The Mautic screenshot suggests you should turn off _email feedback forwarding_, but that's completely optional depending on whether you want want email notifications.</li></ul>   |
 | `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. <ul>  <li>When creating the *topic* select "standard" instead of the preselected "FIFO". You can put a name and leave everything else at default.</li>  <li>When creating a *subscription* choose HTTPS for "Protocol", and leave *"Enable raw message delivery"* UNCHECKED.</li>  <li>On the _"SES -> verified identities"_ page, make sure to check **"[include original headers](https://github.com/knadh/listmonk/issues/720#issuecomment-1046877192)"**.</li>  <li>The Mautic screenshot suggests you should turn off _email feedback forwarding_, but that's completely optional depending on whether you want want email notifications.</li></ul>   |
 | `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/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
 ## Verification

+ 14 - 0
docs/swagger/collections.yaml

@@ -2703,6 +2703,8 @@ components:
               type: string
               type: string
             settings.bounces.enableSendgrid:
             settings.bounces.enableSendgrid:
               type: string
               type: string
+            settings.bounces.enablePostmark:
+              type: string
             settings.bounces.enableWebhooks:
             settings.bounces.enableWebhooks:
               type: string
               type: string
             settings.bounces.enabled:
             settings.bounces.enabled:
@@ -2721,6 +2723,12 @@ components:
               type: string
               type: string
             settings.bounces.sendgridKey:
             settings.bounces.sendgridKey:
               type: string
               type: string
+            settings.bounces.postmarkUsername:
+              type: string
+            settings.bounces.postmarkUsernameHelp:
+              type: string
+            settings.bounces.postmarkPassword:
+              type: string
             settings.bounces.type:
             settings.bounces.type:
               type: string
               type: string
             settings.bounces.username:
             settings.bounces.username:
@@ -3398,6 +3406,12 @@ components:
           type: boolean
           type: boolean
         bounce.sendgrid_key:
         bounce.sendgrid_key:
           type: string
           type: string
+        bounce.postmark_enabled:
+          type: boolean
+        bounce.postmark_username:
+          type: string
+        bounce.postmark_password:
+          type: string
         bounce.mailboxes:
         bounce.mailboxes:
           type: array
           type: array
           items:
           items:

+ 6 - 0
frontend/src/views/Settings.vue

@@ -155,6 +155,12 @@ export default Vue.extend({
         hasDummy = 'captcha';
         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) {
       for (let i = 0; i < form.messengers.length; i += 1) {
         // If it's the dummy UI password placeholder, ignore it.
         // If it's the dummy UI password placeholder, ignore it.
         if (this.isDummy(form.messengers[i].password)) {
         if (this.isDummy(form.messengers[i].password)) {

+ 27 - 0
frontend/src/views/settings/bounces.vue

@@ -73,6 +73,33 @@
               </b-field>
               </b-field>
             </div>
             </div>
           </div>
           </div>
+          <div class="columns">
+            <div class="column is-3">
+              <b-field :label="$t('settings.bounces.enablePostmark')">
+                <b-switch v-model="data['bounce.postmark'].enabled"
+                  name="postmark_enabled" :native-value="true"
+                  data-cy="btn-enable-bounce-postmark" />
+              </b-field>
+            </div>
+            <div class="column">
+              <b-field :label="$t('settings.bounces.postmarkUsername')"
+                :message="$t('settings.bounces.postmarkUsernameHelp')">
+                <b-input v-model="data['bounce.postmark'].username" type="text"
+                  :disabled="!data['bounce.postmark'].enabled"
+                  name="postmark_username"
+                  data-cy="btn-enable-bounce-postmark" />
+              </b-field>
+            </div>
+            <div class="column">
+              <b-field :label="$t('settings.bounces.postmarkPassword')"
+                :message="$t('globals.messages.passwordChange')">
+                <b-input v-model="data['bounce.postmark'].password" type="password"
+                  :disabled="!data['bounce.postmark'].enabled"
+                  name="postmark_password"
+                  data-cy="btn-enable-bounce-postmark" />
+              </b-field>
+            </div>
+          </div>
       </div>
       </div>
     </div>
     </div>
 
 

+ 2 - 1
i18n/ca.json

@@ -365,6 +365,7 @@
     "settings.bounces.enableSES": "Activa SES",
     "settings.bounces.enableSES": "Activa SES",
     "settings.bounces.enableSendgrid": "Activa SendGrid",
     "settings.bounces.enableSendgrid": "Activa SendGrid",
     "settings.bounces.enableWebhooks": "Activa els webhooks pels rebots",
     "settings.bounces.enableWebhooks": "Activa els webhooks pels rebots",
+    "settings.bounces.enablePostmark": "Activa Postmark",
     "settings.bounces.enabled": "Activat",
     "settings.bounces.enabled": "Activat",
     "settings.bounces.folder": "Carpeta",
     "settings.bounces.folder": "Carpeta",
     "settings.bounces.folderHelp": "Nom de la carpeta IMAP a escanejar. Ex: Safata d'entrada.",
     "settings.bounces.folderHelp": "Nom de la carpeta IMAP a escanejar. Ex: Safata d'entrada.",
@@ -577,4 +578,4 @@
     "templates.subject": "Assumpte",
     "templates.subject": "Assumpte",
     "users.login": "Inicia sessió",
     "users.login": "Inicia sessió",
     "users.logout": "Tanca sessió"
     "users.logout": "Tanca sessió"
-}
+}

+ 2 - 1
i18n/cs-cz.json

@@ -365,6 +365,7 @@
     "settings.bounces.enableSES": "Povolit SES",
     "settings.bounces.enableSES": "Povolit SES",
     "settings.bounces.enableSendgrid": "Povolit SendGrid",
     "settings.bounces.enableSendgrid": "Povolit SendGrid",
     "settings.bounces.enableWebhooks": "Povolit webhooky v případě nedoručitelnosti",
     "settings.bounces.enableWebhooks": "Povolit webhooky v případě nedoručitelnosti",
+    "settings.bounces.enablePostmark": "Povolit Postmark",
     "settings.bounces.enabled": "Povoleno",
     "settings.bounces.enabled": "Povoleno",
     "settings.bounces.folder": "Složka",
     "settings.bounces.folder": "Složka",
     "settings.bounces.folderHelp": "Název složky IMAP ke skenování. Např.: Došlá pošta.",
     "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",
     "templates.subject": "Předmět",
     "users.login": "Přihlásit",
     "users.login": "Přihlásit",
     "users.logout": "Odhlásit"
     "users.logout": "Odhlásit"
-}
+}

+ 2 - 1
i18n/cy.json

@@ -365,6 +365,7 @@
     "settings.bounces.enableSES": "Galluogi SES",
     "settings.bounces.enableSES": "Galluogi SES",
     "settings.bounces.enableSendgrid": "Galluogi SendGrid",
     "settings.bounces.enableSendgrid": "Galluogi SendGrid",
     "settings.bounces.enableWebhooks": "Galluogi bachau gwe sydd wedi sboncio'n ôl",
     "settings.bounces.enableWebhooks": "Galluogi bachau gwe sydd wedi sboncio'n ôl",
+    "settings.bounces.enablePostmark": "Galluogi Postmark",
     "settings.bounces.enabled": "Wedi galluogi",
     "settings.bounces.enabled": "Wedi galluogi",
     "settings.bounces.folder": "Ffolder",
     "settings.bounces.folder": "Ffolder",
     "settings.bounces.folderHelp": "Enw'r ffolder IMAP i'w sganio. ee: blwch derbyn.",
     "settings.bounces.folderHelp": "Enw'r ffolder IMAP i'w sganio. ee: blwch derbyn.",
@@ -577,4 +578,4 @@
     "templates.subject": "Pwnc",
     "templates.subject": "Pwnc",
     "users.login": "Mewngofnodi",
     "users.login": "Mewngofnodi",
     "users.logout": "Allgofnodi"
     "users.logout": "Allgofnodi"
-}
+}

+ 2 - 1
i18n/de.json

@@ -365,6 +365,7 @@
     "settings.bounces.enableSES": "SES aktivieren",
     "settings.bounces.enableSES": "SES aktivieren",
     "settings.bounces.enableSendgrid": "SendGrid aktivieren",
     "settings.bounces.enableSendgrid": "SendGrid aktivieren",
     "settings.bounces.enableWebhooks": "Bounce-Webhooks aktivieren",
     "settings.bounces.enableWebhooks": "Bounce-Webhooks aktivieren",
+    "settings.bounces.enablePostmark": "Postmark aktivieren",
     "settings.bounces.enabled": "Aktiviert",
     "settings.bounces.enabled": "Aktiviert",
     "settings.bounces.folder": "Ordner",
     "settings.bounces.folder": "Ordner",
     "settings.bounces.folderHelp": "Name des zu scannenden IMAP-Ordners. z.B.: Inbox.",
     "settings.bounces.folderHelp": "Name des zu scannenden IMAP-Ordners. z.B.: Inbox.",
@@ -577,4 +578,4 @@
     "templates.subject": "Betreff",
     "templates.subject": "Betreff",
     "users.login": "Anmelden",
     "users.login": "Anmelden",
     "users.logout": "Abmelden"
     "users.logout": "Abmelden"
-}
+}

+ 5 - 1
i18n/en.json

@@ -363,6 +363,7 @@
     "settings.bounces.enableSES": "Enable SES",
     "settings.bounces.enableSES": "Enable SES",
     "settings.bounces.enableSendgrid": "Enable SendGrid",
     "settings.bounces.enableSendgrid": "Enable SendGrid",
     "settings.bounces.enableWebhooks": "Enable bounce webhooks",
     "settings.bounces.enableWebhooks": "Enable bounce webhooks",
+    "settings.bounces.enablePostmark": "Enable Postmark",
     "settings.bounces.enabled": "Enabled",
     "settings.bounces.enabled": "Enabled",
     "settings.bounces.folder": "Folder",
     "settings.bounces.folder": "Folder",
     "settings.bounces.folderHelp": "Name of the IMAP folder to scan. Eg: Inbox.",
     "settings.bounces.folderHelp": "Name of the IMAP folder to scan. Eg: Inbox.",
@@ -372,6 +373,9 @@
     "settings.bounces.scanInterval": "Scan interval",
     "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.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.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.type": "Type",
     "settings.bounces.username": "Username",
     "settings.bounces.username": "Username",
     "settings.confirmRestart": "Ensure running campaigns are paused. Restart?",
     "settings.confirmRestart": "Ensure running campaigns are paused. Restart?",
@@ -573,4 +577,4 @@
     "templates.subject": "Subject",
     "templates.subject": "Subject",
     "users.login": "Login",
     "users.login": "Login",
     "users.logout": "Logout"
     "users.logout": "Logout"
-}
+}

+ 2 - 1
i18n/es.json

@@ -366,6 +366,7 @@
     "settings.bounces.enableSES": "Activar SES",
     "settings.bounces.enableSES": "Activar SES",
     "settings.bounces.enableSendgrid": "Activar SendGrid",
     "settings.bounces.enableSendgrid": "Activar SendGrid",
     "settings.bounces.enableWebhooks": "Activar webhooks de rebotes",
     "settings.bounces.enableWebhooks": "Activar webhooks de rebotes",
+    "settings.bounces.enablePostmark": "Activar Postmark",
     "settings.bounces.enabled": "Activado",
     "settings.bounces.enabled": "Activado",
     "settings.bounces.folder": "Carpeta",
     "settings.bounces.folder": "Carpeta",
     "settings.bounces.folderHelp": "Nombre de la carpeta IMAP a escanear, por ejemplo: Entrada.",
     "settings.bounces.folderHelp": "Nombre de la carpeta IMAP a escanear, por ejemplo: Entrada.",
@@ -578,4 +579,4 @@
     "templates.subject": "Asunto",
     "templates.subject": "Asunto",
     "users.login": "Ingresar",
     "users.login": "Ingresar",
     "users.logout": "Salir"
     "users.logout": "Salir"
-}
+}

+ 2 - 1
i18n/fi.json

@@ -365,6 +365,7 @@
     "settings.bounces.enableMailbox": "Ota käyttöön bounce-postilaatikko",
     "settings.bounces.enableMailbox": "Ota käyttöön bounce-postilaatikko",
     "settings.bounces.enableSES": "Ota käyttöön SES",
     "settings.bounces.enableSES": "Ota käyttöön SES",
     "settings.bounces.enableSendgrid": "Ota käyttöön SendGrid",
     "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.enableWebhooks": "Ota käyttöön palautusten webhookit",
     "settings.bounces.enabled": "Käytössä",
     "settings.bounces.enabled": "Käytössä",
     "settings.bounces.folder": "Kansio",
     "settings.bounces.folder": "Kansio",
@@ -578,4 +579,4 @@
     "templates.subject": "Aihe",
     "templates.subject": "Aihe",
     "users.login": "Kirjaudu sisään",
     "users.login": "Kirjaudu sisään",
     "users.logout": "Kirjaudu ulos"
     "users.logout": "Kirjaudu ulos"
-}
+}

+ 2 - 1
i18n/fr.json

@@ -366,6 +366,7 @@
     "settings.bounces.enableSES": "Activer SES",
     "settings.bounces.enableSES": "Activer SES",
     "settings.bounces.enableSendgrid": "Activer SendGrid",
     "settings.bounces.enableSendgrid": "Activer SendGrid",
     "settings.bounces.enableWebhooks": "Activez les 'webhooks' de rebond",
     "settings.bounces.enableWebhooks": "Activez les 'webhooks' de rebond",
+    "settings.bounces.enablePostmark": "Activer Postmark",
     "settings.bounces.enabled": "Activer",
     "settings.bounces.enabled": "Activer",
     "settings.bounces.folder": "Dossier",
     "settings.bounces.folder": "Dossier",
     "settings.bounces.folderHelp": "Nom du dossier IMAP à scanner. Exple : InBox.",
     "settings.bounces.folderHelp": "Nom du dossier IMAP à scanner. Exple : InBox.",
@@ -578,4 +579,4 @@
     "templates.subject": "Objet",
     "templates.subject": "Objet",
     "users.login": "Connecter",
     "users.login": "Connecter",
     "users.logout": "Déconnecter"
     "users.logout": "Déconnecter"
-}
+}

+ 2 - 1
i18n/hu.json

@@ -365,6 +365,7 @@
     "settings.bounces.enableSES": "SES",
     "settings.bounces.enableSES": "SES",
     "settings.bounces.enableSendgrid": "SendGrid",
     "settings.bounces.enableSendgrid": "SendGrid",
     "settings.bounces.enableWebhooks": "Visszapattanó webhook",
     "settings.bounces.enableWebhooks": "Visszapattanó webhook",
+    "settings.bounces.enablePostmark": "Postmark",
     "settings.bounces.enabled": "Engedélyezve",
     "settings.bounces.enabled": "Engedélyezve",
     "settings.bounces.folder": "Mappa",
     "settings.bounces.folder": "Mappa",
     "settings.bounces.folderHelp": "A vizsgálandó IMAP mappa neve. Például: 'Inbox'",
     "settings.bounces.folderHelp": "A vizsgálandó IMAP mappa neve. Például: 'Inbox'",
@@ -577,4 +578,4 @@
     "templates.subject": "Tárgy",
     "templates.subject": "Tárgy",
     "users.login": "Belépés",
     "users.login": "Belépés",
     "users.logout": "Kijelentkezés"
     "users.logout": "Kijelentkezés"
-}
+}

+ 2 - 1
i18n/it.json

@@ -366,6 +366,7 @@
     "settings.bounces.enableSES": "Attiva SES",
     "settings.bounces.enableSES": "Attiva SES",
     "settings.bounces.enableSendgrid": "Attiva SendGrid",
     "settings.bounces.enableSendgrid": "Attiva SendGrid",
     "settings.bounces.enableWebhooks": "Attiva bounce webhooks",
     "settings.bounces.enableWebhooks": "Attiva bounce webhooks",
+    "settings.bounces.enablePostmark": "Attiva Postmark",
     "settings.bounces.enabled": "Attivato",
     "settings.bounces.enabled": "Attivato",
     "settings.bounces.folder": "Cartella",
     "settings.bounces.folder": "Cartella",
     "settings.bounces.folderHelp": "Nome della cartella IMAP da analizzare. Ad esempio: Posta in arrivo.",
     "settings.bounces.folderHelp": "Nome della cartella IMAP da analizzare. Ad esempio: Posta in arrivo.",
@@ -578,4 +579,4 @@
     "templates.subject": "Oggeto",
     "templates.subject": "Oggeto",
     "users.login": "Accesso",
     "users.login": "Accesso",
     "users.logout": "Esci"
     "users.logout": "Esci"
-}
+}

+ 2 - 1
i18n/jp.json

@@ -366,6 +366,7 @@
     "settings.bounces.enableSES": "SESを有効にする",
     "settings.bounces.enableSES": "SESを有効にする",
     "settings.bounces.enableSendgrid": "SendGridを有効にする",
     "settings.bounces.enableSendgrid": "SendGridを有効にする",
     "settings.bounces.enableWebhooks": "バウンスウェブフックを有効にする",
     "settings.bounces.enableWebhooks": "バウンスウェブフックを有効にする",
+    "settings.bounces.enablePostmark": "Postmarkを有効にする",
     "settings.bounces.enabled": "有効",
     "settings.bounces.enabled": "有効",
     "settings.bounces.folder": "フォルダ",
     "settings.bounces.folder": "フォルダ",
     "settings.bounces.folderHelp": "スキャンするIMAPフォルダの名前。 例: Inbox.",
     "settings.bounces.folderHelp": "スキャンするIMAPフォルダの名前。 例: Inbox.",
@@ -578,4 +579,4 @@
     "templates.subject": "件名",
     "templates.subject": "件名",
     "users.login": "ログイン",
     "users.login": "ログイン",
     "users.logout": "ログアウト"
     "users.logout": "ログアウト"
-}
+}

+ 2 - 1
i18n/ml.json

@@ -365,6 +365,7 @@
     "settings.bounces.enableSES": "SES പ്രവർത്തനക്ഷമമാക്കുക",
     "settings.bounces.enableSES": "SES പ്രവർത്തനക്ഷമമാക്കുക",
     "settings.bounces.enableSendgrid": "SendGrid പ്രവർത്തനക്ഷമമാക്കുക",
     "settings.bounces.enableSendgrid": "SendGrid പ്രവർത്തനക്ഷമമാക്കുക",
     "settings.bounces.enableWebhooks": "ബൗൺസ് വെബ്‌ഹുക്കുകൾ പ്രവർത്തനക്ഷമമാക്കുക",
     "settings.bounces.enableWebhooks": "ബൗൺസ് വെബ്‌ഹുക്കുകൾ പ്രവർത്തനക്ഷമമാക്കുക",
+    "settings.bounces.enablePostmark": "Postmark പ്രവർത്തനക്ഷമമാക്കുക",
     "settings.bounces.enabled": "പ്രവർത്തനക്ഷമമാക്കി",
     "settings.bounces.enabled": "പ്രവർത്തനക്ഷമമാക്കി",
     "settings.bounces.folder": "ഫോൾഡർ",
     "settings.bounces.folder": "ഫോൾഡർ",
     "settings.bounces.folderHelp": "സ്കാൻ ചെയ്യാനുള്ള IMAP ഫോൾഡറിന്റെ പേര്. ഉദാ: ഇൻബോക്സ്.",
     "settings.bounces.folderHelp": "സ്കാൻ ചെയ്യാനുള്ള IMAP ഫോൾഡറിന്റെ പേര്. ഉദാ: ഇൻബോക്സ്.",
@@ -577,4 +578,4 @@
     "templates.subject": "വിഷയം",
     "templates.subject": "വിഷയം",
     "users.login": "പ്രവേശിക്കുക",
     "users.login": "പ്രവേശിക്കുക",
     "users.logout": "പുറത്തുകടക്കുക"
     "users.logout": "പുറത്തുകടക്കുക"
-}
+}

+ 2 - 1
i18n/nl.json

@@ -365,6 +365,7 @@
     "settings.bounces.enableSES": "SES inschakelen",
     "settings.bounces.enableSES": "SES inschakelen",
     "settings.bounces.enableSendgrid": "SendGrid inschakelen",
     "settings.bounces.enableSendgrid": "SendGrid inschakelen",
     "settings.bounces.enableWebhooks": "Bounce webhooks inschakelen",
     "settings.bounces.enableWebhooks": "Bounce webhooks inschakelen",
+    "settings.bounces.enablePostmark": "Postmark inschakelen",
     "settings.bounces.enabled": "Ingeschakeld",
     "settings.bounces.enabled": "Ingeschakeld",
     "settings.bounces.folder": "Map",
     "settings.bounces.folder": "Map",
     "settings.bounces.folderHelp": "Naam van de IMAP map om te scannen. Bv.: Inbox.",
     "settings.bounces.folderHelp": "Naam van de IMAP map om te scannen. Bv.: Inbox.",
@@ -577,4 +578,4 @@
     "templates.subject": "Onderwerp",
     "templates.subject": "Onderwerp",
     "users.login": "Inloggen",
     "users.login": "Inloggen",
     "users.logout": "Uitloggen"
     "users.logout": "Uitloggen"
-}
+}

+ 2 - 1
i18n/pl.json

@@ -365,6 +365,7 @@
     "settings.bounces.enableSES": "Włącz SES",
     "settings.bounces.enableSES": "Włącz SES",
     "settings.bounces.enableSendgrid": "Włącz SendGrid",
     "settings.bounces.enableSendgrid": "Włącz SendGrid",
     "settings.bounces.enableWebhooks": "Włącz webhooki odbić",
     "settings.bounces.enableWebhooks": "Włącz webhooki odbić",
+    "settings.bounces.enablePostmark": "Włącz Postmark",
     "settings.bounces.enabled": "Włączone",
     "settings.bounces.enabled": "Włączone",
     "settings.bounces.folder": "Folder",
     "settings.bounces.folder": "Folder",
     "settings.bounces.folderHelp": "Nazwa folderu IMAP do skanowania. Np: Inbox.",
     "settings.bounces.folderHelp": "Nazwa folderu IMAP do skanowania. Np: Inbox.",
@@ -577,4 +578,4 @@
     "templates.subject": "Temat",
     "templates.subject": "Temat",
     "users.login": "Zaloguj",
     "users.login": "Zaloguj",
     "users.logout": "Wyloguj"
     "users.logout": "Wyloguj"
-}
+}

+ 2 - 1
i18n/pt-BR.json

@@ -365,6 +365,7 @@
     "settings.bounces.enableSES": "Ativar SES",
     "settings.bounces.enableSES": "Ativar SES",
     "settings.bounces.enableSendgrid": "Ativar SendGrid",
     "settings.bounces.enableSendgrid": "Ativar SendGrid",
     "settings.bounces.enableWebhooks": "Ativar webhooks bounce",
     "settings.bounces.enableWebhooks": "Ativar webhooks bounce",
+    "settings.bounces.enablePostmark": "Ativar Postmark",
     "settings.bounces.enabled": "Ativado",
     "settings.bounces.enabled": "Ativado",
     "settings.bounces.folder": "Pasta",
     "settings.bounces.folder": "Pasta",
     "settings.bounces.folderHelp": "Noma da pasta IMAP para escanear. Ex: Inbox.",
     "settings.bounces.folderHelp": "Noma da pasta IMAP para escanear. Ex: Inbox.",
@@ -577,4 +578,4 @@
     "templates.subject": "Assunto",
     "templates.subject": "Assunto",
     "users.login": "Entrar",
     "users.login": "Entrar",
     "users.logout": "Sair"
     "users.logout": "Sair"
-}
+}

+ 2 - 1
i18n/pt.json

@@ -365,6 +365,7 @@
     "settings.bounces.enableSES": "Ligar SES",
     "settings.bounces.enableSES": "Ligar SES",
     "settings.bounces.enableSendgrid": "Ligar SendGrid",
     "settings.bounces.enableSendgrid": "Ligar SendGrid",
     "settings.bounces.enableWebhooks": "Ligar webhooks de bounces",
     "settings.bounces.enableWebhooks": "Ligar webhooks de bounces",
+    "settings.bounces.enablePostmark": "Ligar Postmark",
     "settings.bounces.enabled": "Ligado",
     "settings.bounces.enabled": "Ligado",
     "settings.bounces.folder": "Pasta",
     "settings.bounces.folder": "Pasta",
     "settings.bounces.folderHelp": "Nome da pasta IMAP para procurar. E.g.: Inbox.",
     "settings.bounces.folderHelp": "Nome da pasta IMAP para procurar. E.g.: Inbox.",
@@ -577,4 +578,4 @@
     "templates.subject": "Assunto",
     "templates.subject": "Assunto",
     "users.login": "Entrar",
     "users.login": "Entrar",
     "users.logout": "Sair"
     "users.logout": "Sair"
-}
+}

+ 1 - 0
i18n/ro.json

@@ -366,6 +366,7 @@
     "settings.bounces.enableSES": "Activați SES",
     "settings.bounces.enableSES": "Activați SES",
     "settings.bounces.enableSendgrid": "Activați SendGrid",
     "settings.bounces.enableSendgrid": "Activați SendGrid",
     "settings.bounces.enableWebhooks": "Activați webhooks bounce",
     "settings.bounces.enableWebhooks": "Activați webhooks bounce",
+    "settings.bounces.enablePostmark": "Activați Postmark",
     "settings.bounces.enabled": "Activat",
     "settings.bounces.enabled": "Activat",
     "settings.bounces.folder": "Director",
     "settings.bounces.folder": "Director",
     "settings.bounces.folderHelp": "Numele folderului IMAP pentru a scana. De exemplu: Inbox.",
     "settings.bounces.folderHelp": "Numele folderului IMAP pentru a scana. De exemplu: Inbox.",

+ 2 - 1
i18n/ru.json

@@ -365,6 +365,7 @@
     "settings.bounces.enableSES": "Включить SES",
     "settings.bounces.enableSES": "Включить SES",
     "settings.bounces.enableSendgrid": "Включить SendGrid",
     "settings.bounces.enableSendgrid": "Включить SendGrid",
     "settings.bounces.enableWebhooks": "Включить веб-крючки отскока",
     "settings.bounces.enableWebhooks": "Включить веб-крючки отскока",
+    "settings.bounces.enablePostmark": "Включить Postmark",
     "settings.bounces.enabled": "Включено",
     "settings.bounces.enabled": "Включено",
     "settings.bounces.folder": "Папка",
     "settings.bounces.folder": "Папка",
     "settings.bounces.folderHelp": "Имя папки IMAP для сканирования. Например: Входящие.",
     "settings.bounces.folderHelp": "Имя папки IMAP для сканирования. Например: Входящие.",
@@ -577,4 +578,4 @@
     "templates.subject": "Тема",
     "templates.subject": "Тема",
     "users.login": "Вход в систему",
     "users.login": "Вход в систему",
     "users.logout": "Выход из системы"
     "users.logout": "Выход из системы"
-}
+}

+ 2 - 1
i18n/se.json

@@ -365,6 +365,7 @@
     "settings.bounces.enableSES": "Enable SES",
     "settings.bounces.enableSES": "Enable SES",
     "settings.bounces.enableSendgrid": "Enable SendGrid",
     "settings.bounces.enableSendgrid": "Enable SendGrid",
     "settings.bounces.enableWebhooks": "Enable bounce webhooks",
     "settings.bounces.enableWebhooks": "Enable bounce webhooks",
+    "settings.bounces.enablePostmark": "Enable Postmark",
     "settings.bounces.enabled": "Enabled",
     "settings.bounces.enabled": "Enabled",
     "settings.bounces.folder": "Folder",
     "settings.bounces.folder": "Folder",
     "settings.bounces.folderHelp": "Name of the IMAP folder to scan. Eg: Inbox.",
     "settings.bounces.folderHelp": "Name of the IMAP folder to scan. Eg: Inbox.",
@@ -577,4 +578,4 @@
     "templates.subject": "Subject",
     "templates.subject": "Subject",
     "users.login": "Login",
     "users.login": "Login",
     "users.logout": "Logout"
     "users.logout": "Logout"
-}
+}

+ 2 - 1
i18n/sk.json

@@ -365,6 +365,7 @@
     "settings.bounces.enableSES": "Zapnúť SES",
     "settings.bounces.enableSES": "Zapnúť SES",
     "settings.bounces.enableSendgrid": "Zapnúť SendGrid",
     "settings.bounces.enableSendgrid": "Zapnúť SendGrid",
     "settings.bounces.enableWebhooks": "Zapnúť webhooky pre nedoručiteľné",
     "settings.bounces.enableWebhooks": "Zapnúť webhooky pre nedoručiteľné",
+    "settings.bounces.enablePostmark": "Zapnúť Postmark",
     "settings.bounces.enabled": "Zapnuté",
     "settings.bounces.enabled": "Zapnuté",
     "settings.bounces.folder": "Priečinok",
     "settings.bounces.folder": "Priečinok",
     "settings.bounces.folderHelp": "Názov kontrolovaného priečinku IMAP. Napríklad INBOX.",
     "settings.bounces.folderHelp": "Názov kontrolovaného priečinku IMAP. Napríklad INBOX.",
@@ -577,4 +578,4 @@
     "templates.subject": "Predmet",
     "templates.subject": "Predmet",
     "users.login": "Prihlásiť",
     "users.login": "Prihlásiť",
     "users.logout": "Odhlásiť"
     "users.logout": "Odhlásiť"
-}
+}

+ 2 - 1
i18n/tr.json

@@ -366,6 +366,7 @@
     "settings.bounces.enableSES": "SES'i etkinleştirin",
     "settings.bounces.enableSES": "SES'i etkinleştirin",
     "settings.bounces.enableSendgrid": "SendGrid'i etkinleştirin",
     "settings.bounces.enableSendgrid": "SendGrid'i etkinleştirin",
     "settings.bounces.enableWebhooks": "Sıçrama web kancalarını 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.enabled": "Etkinleştir",
     "settings.bounces.folder": "Dizin",
     "settings.bounces.folder": "Dizin",
     "settings.bounces.folderHelp": "Taranacak IMAP klasörünün adı. Örn: Gelen Kutusu.",
     "settings.bounces.folderHelp": "Taranacak IMAP klasörünün adı. Örn: Gelen Kutusu.",
@@ -578,4 +579,4 @@
     "templates.subject": "Konu",
     "templates.subject": "Konu",
     "users.login": "Giriş",
     "users.login": "Giriş",
     "users.logout": "Çıkış"
     "users.logout": "Çıkış"
-}
+}

+ 2 - 1
i18n/vi.json

@@ -366,6 +366,7 @@
     "settings.bounces.enableSES": "Bật SES",
     "settings.bounces.enableSES": "Bật SES",
     "settings.bounces.enableSendgrid": "Bật SendGrid",
     "settings.bounces.enableSendgrid": "Bật SendGrid",
     "settings.bounces.enableWebhooks": "Bật webhook bị trả lại",
     "settings.bounces.enableWebhooks": "Bật webhook bị trả lại",
+    "settings.bounces.enablePostmark": "Bật Postmark",
     "settings.bounces.enabled": "Đã bật",
     "settings.bounces.enabled": "Đã bật",
     "settings.bounces.folder": "Thư mục",
     "settings.bounces.folder": "Thư mục",
     "settings.bounces.folderHelp": "Tên của thư mục IMAP để quét. Vd: Hộp thư đến.",
     "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ủ đề",
     "templates.subject": "Chủ đề",
     "users.login": "Đăng nhập",
     "users.login": "Đăng nhập",
     "users.logout": "Đăng xuất"
     "users.logout": "Đăng xuất"
-}
+}

+ 2 - 1
i18n/zh-CN.json

@@ -365,6 +365,7 @@
     "settings.bounces.enableSES": "启用SES",
     "settings.bounces.enableSES": "启用SES",
     "settings.bounces.enableSendgrid": "启用SendGrid",
     "settings.bounces.enableSendgrid": "启用SendGrid",
     "settings.bounces.enableWebhooks": "启用反弹webhooks",
     "settings.bounces.enableWebhooks": "启用反弹webhooks",
+    "settings.bounces.enablePostmark": "启用Postmark",
     "settings.bounces.enabled": "已启用",
     "settings.bounces.enabled": "已启用",
     "settings.bounces.folder": "文件夹",
     "settings.bounces.folder": "文件夹",
     "settings.bounces.folderHelp": "要扫描的 IMAP 文件夹的名称。例如:收件箱。",
     "settings.bounces.folderHelp": "要扫描的 IMAP 文件夹的名称。例如:收件箱。",
@@ -577,4 +578,4 @@
     "templates.subject": "主题",
     "templates.subject": "主题",
     "users.login": "登录",
     "users.login": "登录",
     "users.logout": "登出"
     "users.logout": "登出"
-}
+}

+ 2 - 1
i18n/zh-TW.json

@@ -366,6 +366,7 @@
     "settings.bounces.enableSES": "啟用SES",
     "settings.bounces.enableSES": "啟用SES",
     "settings.bounces.enableSendgrid": "啟用SendGrid",
     "settings.bounces.enableSendgrid": "啟用SendGrid",
     "settings.bounces.enableWebhooks": "啟用反彈webhooks",
     "settings.bounces.enableWebhooks": "啟用反彈webhooks",
+    "settings.bounces.enablePostmark": "啟用Postmark",
     "settings.bounces.enabled": "已啟用",
     "settings.bounces.enabled": "已啟用",
     "settings.bounces.folder": "文件夾",
     "settings.bounces.folder": "文件夾",
     "settings.bounces.folderHelp": "要掃描的IMAP 文件夾的名稱。例如:收件箱。",
     "settings.bounces.folderHelp": "要掃描的IMAP 文件夾的名稱。例如:收件箱。",
@@ -578,4 +579,4 @@
     "templates.subject": "主題",
     "templates.subject": "主題",
     "users.login": "登錄",
     "users.login": "登錄",
     "users.logout": "登出"
     "users.logout": "登出"
-}
+}

+ 11 - 0
internal/bounce/bounce.go

@@ -33,6 +33,11 @@ type Opt struct {
 	SESEnabled      bool        `json:"ses_enabled"`
 	SESEnabled      bool        `json:"ses_enabled"`
 	SendgridEnabled bool        `json:"sendgrid_enabled"`
 	SendgridEnabled bool        `json:"sendgrid_enabled"`
 	SendgridKey     string      `json:"sendgrid_key"`
 	SendgridKey     string      `json:"sendgrid_key"`
+	Postmark        struct {
+		Enabled  bool
+		Username string
+		Password string
+	}
 
 
 	RecordBounceCB func(models.Bounce) error
 	RecordBounceCB func(models.Bounce) error
 }
 }
@@ -43,6 +48,7 @@ type Manager struct {
 	mailbox  Mailbox
 	mailbox  Mailbox
 	SES      *webhooks.SES
 	SES      *webhooks.SES
 	Sendgrid *webhooks.Sendgrid
 	Sendgrid *webhooks.Sendgrid
+	Postmark *webhooks.Postmark
 	queries  *Queries
 	queries  *Queries
 	opt      Opt
 	opt      Opt
 	log      *log.Logger
 	log      *log.Logger
@@ -77,6 +83,7 @@ func New(opt Opt, q *Queries, lo *log.Logger) (*Manager, error) {
 		if opt.SESEnabled {
 		if opt.SESEnabled {
 			m.SES = webhooks.NewSES()
 			m.SES = webhooks.NewSES()
 		}
 		}
+
 		if opt.SendgridEnabled {
 		if opt.SendgridEnabled {
 			sg, err := webhooks.NewSendgrid(opt.SendgridKey)
 			sg, err := webhooks.NewSendgrid(opt.SendgridKey)
 			if err != nil {
 			if err != nil {
@@ -85,6 +92,10 @@ func New(opt Opt, q *Queries, lo *log.Logger) (*Manager, error) {
 				m.Sendgrid = sg
 				m.Sendgrid = sg
 			}
 			}
 		}
 		}
+
+		if opt.Postmark.Enabled {
+			m.Postmark = webhooks.NewPostmark(opt.Postmark.Username, opt.Postmark.Password)
+		}
 	}
 	}
 
 
 	return m, nil
 	return m, nil

+ 118 - 0
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
+	}
+}

+ 17 - 0
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
+}

+ 6 - 1
models/settings.go

@@ -92,7 +92,12 @@ type Settings struct {
 	SESEnabled      bool   `json:"bounce.ses_enabled"`
 	SESEnabled      bool   `json:"bounce.ses_enabled"`
 	SendgridEnabled bool   `json:"bounce.sendgrid_enabled"`
 	SendgridEnabled bool   `json:"bounce.sendgrid_enabled"`
 	SendgridKey     string `json:"bounce.sendgrid_key"`
 	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"`
 		UUID          string `json:"uuid"`
 		Enabled       bool   `json:"enabled"`
 		Enabled       bool   `json:"enabled"`
 		Type          string `json:"type"`
 		Type          string `json:"type"`

+ 1 - 0
schema.sql

@@ -255,6 +255,7 @@ INSERT INTO settings (key, value) VALUES
     ('bounce.ses_enabled', 'false'),
     ('bounce.ses_enabled', 'false'),
     ('bounce.sendgrid_enabled', 'false'),
     ('bounce.sendgrid_enabled', 'false'),
     ('bounce.sendgrid_key', '""'),
     ('bounce.sendgrid_key', '""'),
+    ('bounce.postmark', '{"enabled": false, "username": "", "password": ""}'),
     ('bounce.mailboxes',
     ('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}]'),
         '[{"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', '""'),
     ('appearance.admin.custom_css', '""'),