Skip to content

Commit

Permalink
Add ForwardEmail (provider) bounce integration (#2016)
Browse files Browse the repository at this point in the history
* Add ForwardEmail one-click SMTP form option.
* Add bounce webhook integration.

---------

Co-authored-by: Kailash Nadh <[email protected]>
  • Loading branch information
shaunwarman and knadh authored Nov 10, 2024
1 parent 0392582 commit cb8b54f
Show file tree
Hide file tree
Showing 44 changed files with 261 additions and 25 deletions.
17 changes: 17 additions & 0 deletions cmd/bounce.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,23 @@ func handleBounceWebhook(c echo.Context) error {
}
bounces = append(bounces, bs...)

// ForwardEmail.
case service == "forwardemail" && app.constants.BounceForwardemailEnabled:
var (
sig = c.Request().Header.Get("X-Webhook-Signature")
)

bs, err := app.bounce.Forwardemail.ProcessBounce([]byte(sig), rawReq)
if err != nil {
app.log.Printf("error processing forwardemail 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"))
}
Expand Down
19 changes: 15 additions & 4 deletions cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,11 @@ type constants struct {
Extensions []string
}

BounceWebhooksEnabled bool
BounceSESEnabled bool
BounceSendgridEnabled bool
BouncePostmarkEnabled bool
BounceWebhooksEnabled bool
BounceSESEnabled bool
BounceSendgridEnabled bool
BouncePostmarkEnabled bool
BounceForwardemailEnabled bool

PermissionsRaw json.RawMessage
Permissions map[string]struct{}
Expand Down Expand Up @@ -432,6 +433,9 @@ func initConstants() *constants {
c.BounceSESEnabled = ko.Bool("bounce.ses_enabled")
c.BounceSendgridEnabled = ko.Bool("bounce.sendgrid_enabled")
c.BouncePostmarkEnabled = ko.Bool("bounce.postmark.enabled")
c.BounceForwardemailEnabled = ko.Bool("bounce.forwardemail.enabled")

fmt.Println(c.BounceForwardemailEnabled)

c.HasLegacyUser = ko.Exists("app.admin_username") || ko.Exists("app.admin_password")

Expand Down Expand Up @@ -709,6 +713,13 @@ func initBounceManager(app *App) *bounce.Manager {
ko.String("bounce.postmark.username"),
ko.String("bounce.postmark.password"),
},
ForwardEmail: struct {
Enabled bool
Key string
}{
ko.Bool("bounce.forwardemail.enabled"),
ko.String("bounce.forwardemail.key"),
},
RecordBounceCB: app.core.RecordBounce,
}

Expand Down
7 changes: 6 additions & 1 deletion cmd/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,13 @@ func handleGetSettings(c echo.Context) error {
for i := 0; i < len(s.Messengers); i++ {
s.Messengers[i].Password = strings.Repeat(pwdMask, utf8.RuneCountInString(s.Messengers[i].Password))
}

s.UploadS3AwsSecretAccessKey = strings.Repeat(pwdMask, utf8.RuneCountInString(s.UploadS3AwsSecretAccessKey))
s.SendgridKey = strings.Repeat(pwdMask, utf8.RuneCountInString(s.SendgridKey))
s.BouncePostmark.Password = strings.Repeat(pwdMask, utf8.RuneCountInString(s.BouncePostmark.Password))
s.BounceForwardEmail.Key = strings.Repeat(pwdMask, utf8.RuneCountInString(s.BounceForwardEmail.Key))
s.SecurityCaptchaSecret = strings.Repeat(pwdMask, utf8.RuneCountInString(s.SecurityCaptchaSecret))
s.OIDC.ClientSecret = strings.Repeat(pwdMask, utf8.RuneCountInString(s.OIDC.ClientSecret))
s.BouncePostmark.Password = strings.Repeat(pwdMask, utf8.RuneCountInString(s.BouncePostmark.Password))

return c.JSON(http.StatusOK, okResp{s})
}
Expand Down Expand Up @@ -199,6 +201,9 @@ func handleUpdateSettings(c echo.Context) error {
if set.BouncePostmark.Password == "" {
set.BouncePostmark.Password = cur.BouncePostmark.Password
}
if set.BounceForwardEmail.Key == "" {
set.BounceForwardEmail.Key = cur.BounceForwardEmail.Key
}
if set.SecurityCaptchaSecret == "" {
set.SecurityCaptchaSecret = cur.SecurityCaptchaSecret
}
Expand Down
1 change: 1 addition & 0 deletions cmd/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ var migList = []migFunc{
{"v2.5.0", migrations.V2_5_0},
{"v3.0.0", migrations.V3_0_0},
{"v4.0.0", migrations.V4_0_0},
{"v4.1.0", migrations.V4_1_0},
}

// upgrade upgrades the database to the current version by running SQL migration files
Expand Down
11 changes: 6 additions & 5 deletions docs/docs/content/bounces.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ curl -u curl -u 'api_username:access_token' -X POST 'http://localhost:9000/webho
## External webhooks
listmonk supports receiving bounce webhook events from the following SMTP providers.

| Endpoint | Description | More info |
|:----------------------------------------------------------|:---------------------------------------|:----------------------------------------------------------------------------------------------------------------------|
| `https://listmonk.yoursite.com/webhooks/service/ses` | Amazon (AWS) SES | See below |
| `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) |
| Endpoint | Description | More info |
|:--------------------------------------------------------------|:---------------------------------------|:----------------------------------------------------------------------------------------------------------------------|
| `https://listmonk.yoursite.com/webhooks/service/ses` | Amazon (AWS) SES | See below |
| `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) |
| `https://listmonk.yoursite.com/webhooks/service/forwardemail` | Forward Email webhook | [More info](https://forwardemail.net/en/faq#do-you-support-bounce-webhooks) |

## Amazon Simple Email Service (SES)

Expand Down
8 changes: 8 additions & 0 deletions docs/swagger/collections.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2705,6 +2705,8 @@ components:
type: string
settings.bounces.enableSendgrid:
type: string
settings.bounces.enableForwardemail:
type: string
settings.bounces.enablePostmark:
type: string
settings.bounces.enableWebhooks:
Expand All @@ -2725,6 +2727,8 @@ components:
type: string
settings.bounces.sendgridKey:
type: string
settings.bounces.forwardemailKey:
type: string
settings.bounces.postmarkUsername:
type: string
settings.bounces.postmarkUsernameHelp:
Expand Down Expand Up @@ -3408,6 +3412,10 @@ components:
type: boolean
bounce.sendgrid_key:
type: string
bounce.forwardemail_enabled:
type: boolean
bounce.forwardemail_key:
type: string
bounce.postmark_enabled:
type: boolean
bounce.postmark_username:
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/views/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,12 @@ export default Vue.extend({
hasDummy = 'postmark';
}
if (this.isDummy(form['bounce.forwardemail'].key)) {
form['bounce.forwardemail'].key = '';
} else if (this.hasDummy(form['bounce.forwardemail'].key)) {
hasDummy = 'forwardemail';
}
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)) {
Expand Down
28 changes: 23 additions & 5 deletions frontend/src/views/settings/bounces.vue
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,9 @@
</div>
<div class="column">
<b-field :label="$t('settings.bounces.sendgridKey')" :message="$t('globals.messages.passwordChange')">
<b-input v-model="data['bounce.sendgrid_key']" type="password" :disabled="!data['bounce.sendgrid_enabled']"
name="sendgrid_enabled" :native-value="true" data-cy="btn-enable-bounce-sendgrid" />
<b-input v-model="data['bounce.sendgrid_key']" type="password"
:disabled="!data['bounce.sendgrid_enabled']" name="sendgrid_enabled" :native-value="true"
data-cy="btn-enable-bounce-sendgrid" />
</b-field>
</div>
</div>
Expand All @@ -83,8 +84,9 @@
<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-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">
Expand All @@ -95,6 +97,21 @@
</b-field>
</div>
</div>
<div class="columns">
<div class="column is-3">
<b-field :label="$t('settings.bounces.enableForwardemail')">
<b-switch v-model="data['bounce.forwardemail'].enabled" name="forwardemail_enabled" :native-value="true"
data-cy="btn-enable-bounce-forwardemail" />
</b-field>
</div>
<div class="column">
<b-field :label="$t('settings.bounces.forwardemailKey')" :message="$t('globals.messages.passwordChange')">
<b-input v-model="data['bounce.forwardemail'].key" type="password"
:disabled="!data['bounce.forwardemail'].enabled" name="forwardemail_enabled" :native-value="true"
data-cy="btn-enable-bounce-forwardemail" />
</b-field>
</div>
</div>
</div>
</div>

Expand Down Expand Up @@ -180,7 +197,8 @@
</b-field>
<b-field :label="$t('settings.mailserver.skipTLS')" expanded
:message="$t('settings.mailserver.skipTLSHelp')">
<b-switch v-model="item.tls_skip_verify" :disabled="!item.tls_enabled" name="item.tls_skip_verify" />
<b-switch v-model="item.tls_skip_verify" :disabled="!item.tls_enabled"
name="item.tls_skip_verify" />
</b-field>
</b-field>
</div>
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/views/settings/smtp.vue
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
<a href="#" @click.prevent="() => fillSettings(n, 'mailjet')">Mailjet</a>
<a href="#" @click.prevent="() => fillSettings(n, 'sendgrid')">Sendgrid</a>
<a href="#" @click.prevent="() => fillSettings(n, 'postmark')">Postmark</a>
<a href="#" @click.prevent="() => fillSettings(n, 'forwardemail')">Forward Email</a>
</div>
<hr />

Expand Down Expand Up @@ -165,8 +166,8 @@
</div>
<div class="column is-4">
<b-field :label="$t('settings.smtp.toEmail')" label-position="on-border">
<b-input type="email" required v-model="testEmail" :ref="'testEmailTo'" placeholder="[email protected]"
:custom-class="`test-email-${n}`" />
<b-input type="email" required v-model="testEmail" :ref="'testEmailTo'"
placeholder="[email protected]" :custom-class="`test-email-${n}`" />
</b-field>
</div>
</template>
Expand Down Expand Up @@ -220,6 +221,9 @@ const smtpTemplates = {
sendgrid: {
host: 'smtp.sendgrid.net', port: 465, auth_protocol: 'login', tls_type: 'TLS',
},
forwardemail: {
host: 'smtp.forwardemail.net', port: 465, auth_protocol: 'login', tls_type: 'TLS',
},
postmark: {
host: 'smtp.postmarkapp.com', port: 587, auth_protocol: 'cram', tls_type: 'STARTTLS',
},
Expand Down
2 changes: 2 additions & 0 deletions i18n/ca.json
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@
"settings.bounces.countHelp": "Nombre de rebots per subscriptor",
"settings.bounces.delete": "Esborra",
"settings.bounces.enable": "Activa el processament de rebots",
"settings.bounces.enableForwardemail": "Enable Forward Email",
"settings.bounces.enableMailbox": "Activa la bústia de rebots",
"settings.bounces.enablePostmark": "Activa Postmark",
"settings.bounces.enableSES": "Activa SES",
Expand All @@ -383,6 +384,7 @@
"settings.bounces.enabled": "Activat",
"settings.bounces.folder": "Carpeta",
"settings.bounces.folderHelp": "Nom de la carpeta IMAP a escanejar. Ex: Safata d'entrada.",
"settings.bounces.forwardemailKey": "Forward Email Key",
"settings.bounces.hard": "Hard",
"settings.bounces.invalidScanInterval": "L'interval d'escaneig ha de ser com a mínim d'1 minut.",
"settings.bounces.name": "Rebots",
Expand Down
2 changes: 2 additions & 0 deletions i18n/cs-cz.json
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@
"settings.bounces.countHelp": "Počet případů nedoručitelnosti na odběratele",
"settings.bounces.delete": "Odstranit",
"settings.bounces.enable": "Povolit zpracování nedoručitelnosti",
"settings.bounces.enableForwardemail": "Enable Forward Email",
"settings.bounces.enableMailbox": "Povolit poštovní schránku v případě nedoručitelnosti",
"settings.bounces.enablePostmark": "Povolit Postmark",
"settings.bounces.enableSES": "Povolit SES",
Expand All @@ -383,6 +384,7 @@
"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.",
"settings.bounces.forwardemailKey": "Forward Email Key",
"settings.bounces.hard": "Hard",
"settings.bounces.invalidScanInterval": "Interval skenování v případě nedoručitelnosti by měl být minimálně 1 minuta.",
"settings.bounces.name": "Případy nedoručitelnosti",
Expand Down
2 changes: 2 additions & 0 deletions i18n/cy.json
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@
"settings.bounces.countHelp": "Nifer y pethau sydd wedi sboncio'n ôl fesul tanysgrifiwr",
"settings.bounces.delete": "Dileu",
"settings.bounces.enable": "Galluogi proses sboncio'n ôl",
"settings.bounces.enableForwardemail": "Enable Forward Email",
"settings.bounces.enableMailbox": "Galluogi blwch post negeseuon sydd wedi sboncio'n ôl",
"settings.bounces.enablePostmark": "Galluogi Postmark",
"settings.bounces.enableSES": "Galluogi SES",
Expand All @@ -383,6 +384,7 @@
"settings.bounces.enabled": "Wedi galluogi",
"settings.bounces.folder": "Ffolder",
"settings.bounces.folderHelp": "Enw'r ffolder IMAP i'w sganio. ee: blwch derbyn.",
"settings.bounces.forwardemailKey": "Forward Email Key",
"settings.bounces.hard": "Hard",
"settings.bounces.invalidScanInterval": "Dylai'r cyfnod sganio ar gyfer negeseuon sydd wedi sboncio'n ôl bara o leiaf 1 munud",
"settings.bounces.name": "Wedi sboncio'n ôl",
Expand Down
2 changes: 2 additions & 0 deletions i18n/da.json
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@
"settings.bounces.count": "Antal afvisninger",
"settings.bounces.countHelp": "Antal afvisninger pr. abonnent",
"settings.bounces.enable": "Aktivér bounce behandling",
"settings.bounces.enableForwardemail": "Enable Forward Email",
"settings.bounces.enableMailbox": "Aktivér bounce-postkasse",
"settings.bounces.enablePostmark": "Aktivér poststempel",
"settings.bounces.enableSES": "Aktiver SES",
Expand All @@ -381,6 +382,7 @@
"settings.bounces.enabled": "Aktiveret",
"settings.bounces.folder": "Mappe",
"settings.bounces.folderHelp": "Navnet på den IMAP-mappe, der skal scannes. F.eks.: Indbakke.",
"settings.bounces.forwardemailKey": "Forward Email Key",
"settings.bounces.invalidScanInterval": "Bounce skanningsinterval skal være mindst 1 minut.",
"settings.bounces.name": "Fejlsendt",
"settings.bounces.none": "Ingen",
Expand Down
2 changes: 2 additions & 0 deletions i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@
"settings.bounces.count": "Bounce Anzahl",
"settings.bounces.countHelp": "Anzahl von Bounces pro Abonnent",
"settings.bounces.enable": "Verarbeiten von Bounces aktivieren",
"settings.bounces.enableForwardemail": "Enable Forward Email",
"settings.bounces.enableMailbox": "Bounce-Postfach aktivieren",
"settings.bounces.enablePostmark": "Postmark aktivieren",
"settings.bounces.enableSES": "SES aktivieren",
Expand All @@ -381,6 +382,7 @@
"settings.bounces.enabled": "Aktiviert",
"settings.bounces.folder": "Ordner",
"settings.bounces.folderHelp": "Name des zu scannenden IMAP-Ordners. z.B.: Inbox.",
"settings.bounces.forwardemailKey": "Forward Email Key",
"settings.bounces.invalidScanInterval": "Der Bounce Scan-Interval sollte mindestens 1 Minute betragen.",
"settings.bounces.name": "Bounces",
"settings.bounces.none": "Keine",
Expand Down
2 changes: 2 additions & 0 deletions i18n/el.json
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@
"settings.bounces.count": "Πλήθος bounce",
"settings.bounces.countHelp": "Αριθμός bounce ανά συνδρομητή",
"settings.bounces.enable": "Ενεργοποίηση επεξεργασίας bounce",
"settings.bounces.enableForwardemail": "Enable Forward Email",
"settings.bounces.enableMailbox": "Ενεργοποίηση γραμματοκιβωτίου για τα bounce",
"settings.bounces.enablePostmark": "Ενεργοποίηση Postmark",
"settings.bounces.enableSES": "Ενεργοποίηση SES",
Expand All @@ -381,6 +382,7 @@
"settings.bounces.enabled": "Ενεργοποιημένο",
"settings.bounces.folder": "Φάκελος",
"settings.bounces.folderHelp": "Όνομα του φακέλου IMAP προς περιοδική σάρωση. Π.χ.: Εισερχόμενα.",
"settings.bounces.forwardemailKey": "Forward Email Key",
"settings.bounces.invalidScanInterval": "Το διάστημα σάρωσης για αναγνώριση των bounce πρέπει να είναι τουλάχιστον 1 λεπτό.",
"settings.bounces.name": "Bounce",
"settings.bounces.none": "Κανένα",
Expand Down
2 changes: 2 additions & 0 deletions i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@
"settings.bounces.count": "Bounce count",
"settings.bounces.countHelp": "Number of bounces per subscriber",
"settings.bounces.enable": "Enable bounce processing",
"settings.bounces.enableForwardemail": "Enable Forward Email",
"settings.bounces.enableMailbox": "Enable bounce mailbox",
"settings.bounces.enablePostmark": "Enable Postmark",
"settings.bounces.enableSES": "Enable SES",
Expand All @@ -381,6 +382,7 @@
"settings.bounces.enabled": "Enabled",
"settings.bounces.folder": "Folder",
"settings.bounces.folderHelp": "Name of the IMAP folder to scan. Eg: Inbox.",
"settings.bounces.forwardemailKey": "Forward Email Key",
"settings.bounces.invalidScanInterval": "Bounce scan interval should be minimum 1 minute.",
"settings.bounces.name": "Bounces",
"settings.bounces.none": "None",
Expand Down
2 changes: 2 additions & 0 deletions i18n/eo.json
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@
"settings.bounces.countHelp": "Nombre de rebots per subscriptor",
"settings.bounces.delete": "Esborra",
"settings.bounces.enable": "Activa el processament de rebots",
"settings.bounces.enableForwardemail": "Enable Forward Email",
"settings.bounces.enableMailbox": "Activa la bústia de rebots",
"settings.bounces.enablePostmark": "Activa Postmark",
"settings.bounces.enableSES": "Activa SES",
Expand All @@ -383,6 +384,7 @@
"settings.bounces.enabled": "Activat",
"settings.bounces.folder": "Carpeta",
"settings.bounces.folderHelp": "Nom de la carpeta IMAP a escanejar. Ex: Safata d'entrada.",
"settings.bounces.forwardemailKey": "Forward Email Key",
"settings.bounces.hard": "Hard",
"settings.bounces.invalidScanInterval": "L'interval d'escaneig ha de ser com a mínim d'1 minut.",
"settings.bounces.name": "Rebots",
Expand Down
Loading

0 comments on commit cb8b54f

Please sign in to comment.