Compare commits

...

3 commits

Author SHA1 Message Date
Shaun Warman 9b90fa144c
Merge 091af996d8 into 16f4dfd3e9 2024-09-19 12:26:20 +05:30
Bowrna 16f4dfd3e9
Fix incorrect bulk blocklisting behaviour (#2041). Fixes #1841 2024-09-19 10:56:56 +05:30
Shaun Warman 091af996d8 feat: add forwardemail as email option 2024-08-26 22:52:42 -05:00
19 changed files with 224 additions and 62 deletions

View file

@ -191,6 +191,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...)
// Postmark.
case service == "postmark" && app.constants.BouncePostmarkEnabled:
bs, err := app.bounce.Postmark.ProcessBounce(rawReq, c)

View file

@ -106,10 +106,11 @@ type constants struct {
Extensions []string
}
BounceWebhooksEnabled bool
BounceSESEnabled bool
BounceSendgridEnabled bool
BouncePostmarkEnabled bool
BounceWebhooksEnabled bool
BounceSESEnabled bool
BounceSendgridEnabled bool
BounceForwardemailEnabled bool
BouncePostmarkEnabled bool
}
type notifTpls struct {
@ -418,6 +419,7 @@ 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.BounceForwardemailEnabled = ko.Bool("bounce.forwardemail_enabled")
c.BouncePostmarkEnabled = ko.Bool("bounce.postmark.enabled")
b := md5.Sum([]byte(time.Now().String()))
@ -666,10 +668,12 @@ func initNotifTemplates(path string, fs stuffbin.FileSystem, i *i18n.I18n, cs *c
// for incoming bounce events.
func initBounceManager(app *App) *bounce.Manager {
opt := bounce.Opt{
WebhooksEnabled: ko.Bool("bounce.webhooks_enabled"),
SESEnabled: ko.Bool("bounce.ses_enabled"),
SendgridEnabled: ko.Bool("bounce.sendgrid_enabled"),
SendgridKey: ko.String("bounce.sendgrid_key"),
WebhooksEnabled: ko.Bool("bounce.webhooks_enabled"),
SESEnabled: ko.Bool("bounce.ses_enabled"),
SendgridEnabled: ko.Bool("bounce.sendgrid_enabled"),
SendgridKey: ko.String("bounce.sendgrid_key"),
ForwardemailEnabled: ko.Bool("bounce.forwardemail_enabled"),
ForwardemailKey: ko.String("bounce.forwardemail_key"),
Postmark: struct {
Enabled bool
Username string

View file

@ -67,6 +67,8 @@ 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.ForwardemailKey = strings.Repeat(pwdMask, utf8.RuneCountInString(s.ForwardemailKey))
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))
@ -195,6 +197,9 @@ func handleUpdateSettings(c echo.Context) error {
if set.SendgridKey == "" {
set.SendgridKey = cur.SendgridKey
}
if set.ForwardemailKey == "" {
set.ForwardemailKey = cur.ForwardemailKey
}
if set.BouncePostmark.Password == "" {
set.BouncePostmark.Password = cur.BouncePostmark.Password
}

View file

@ -22,12 +22,13 @@ const (
// subQueryReq is a "catch all" struct for reading various
// subscriber related requests.
type subQueryReq struct {
Query string `json:"query"`
ListIDs []int `json:"list_ids"`
TargetListIDs []int `json:"target_list_ids"`
SubscriberIDs []int `json:"ids"`
Action string `json:"action"`
Status string `json:"status"`
Query string `json:"query"`
ListIDs []int `json:"list_ids"`
TargetListIDs []int `json:"target_list_ids"`
SubscriberIDs []int `json:"ids"`
Action string `json:"action"`
Status string `json:"status"`
SubscriptionStatus string `json:"subscription_status"`
}
// subProfileData represents a subscriber's collated data in JSON
@ -415,7 +416,7 @@ func handleDeleteSubscribersByQuery(c echo.Context) error {
return err
}
if err := app.core.DeleteSubscribersByQuery(req.Query, req.ListIDs); err != nil {
if err := app.core.DeleteSubscribersByQuery(req.Query, req.ListIDs, req.SubscriptionStatus); err != nil {
return err
}
@ -434,7 +435,7 @@ func handleBlocklistSubscribersByQuery(c echo.Context) error {
return err
}
if err := app.core.BlocklistSubscribersByQuery(req.Query, req.ListIDs); err != nil {
if err := app.core.BlocklistSubscribersByQuery(req.Query, req.ListIDs, req.SubscriptionStatus); err != nil {
return err
}
@ -461,11 +462,11 @@ func handleManageSubscriberListsByQuery(c echo.Context) error {
var err error
switch req.Action {
case "add":
err = app.core.AddSubscriptionsByQuery(req.Query, req.ListIDs, req.TargetListIDs, req.Status)
err = app.core.AddSubscriptionsByQuery(req.Query, req.ListIDs, req.TargetListIDs, req.Status, req.SubscriptionStatus)
case "remove":
err = app.core.DeleteSubscriptionsByQuery(req.Query, req.ListIDs, req.TargetListIDs)
err = app.core.DeleteSubscriptionsByQuery(req.Query, req.ListIDs, req.TargetListIDs, req.SubscriptionStatus)
case "unsubscribe":
err = app.core.UnsubscribeListsByQuery(req.Query, req.ListIDs, req.TargetListIDs)
err = app.core.UnsubscribeListsByQuery(req.Query, req.ListIDs, req.TargetListIDs, req.SubscriptionStatus)
default:
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidAction"))
}

View file

@ -42,11 +42,12 @@ curl -u 'username:password' -X POST 'http://localhost:9000/webhooks/bounce' \
## 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)

View file

@ -2703,6 +2703,8 @@ components:
type: string
settings.bounces.enableSendgrid:
type: string
settings.bounces.enableForwardemail:
type: string
settings.bounces.enablePostmark:
type: string
settings.bounces.enableWebhooks:
@ -2723,6 +2725,8 @@ components:
type: string
settings.bounces.sendgridKey:
type: string
settings.bounces.forwardemailKey:
type: string
settings.bounces.postmarkUsername:
type: string
settings.bounces.postmarkUsernameHelp:
@ -3406,6 +3410,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:

View file

@ -166,6 +166,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)) {

View file

@ -375,6 +375,7 @@ export default Vue.extend({
this.$api.blocklistSubscribersByQuery({
query: this.queryParams.queryExp,
list_ids: this.queryParams.listID ? [this.queryParams.listID] : null,
subscription_status: this.queryParams.subStatus,
}).then(() => this.querySubscribers());
};
}
@ -426,6 +427,7 @@ export default Vue.extend({
this.$api.deleteSubscribersByQuery({
query: this.queryParams.queryExp,
list_ids: this.queryParams.listID ? [this.queryParams.listID] : null,
subscription_status: this.queryParams.subStatus,
}).then(() => {
this.querySubscribers();
@ -460,6 +462,7 @@ export default Vue.extend({
} else {
// 'All' is selected, perform by query.
data.query = this.queryParams.queryExp;
data.subscription_status = this.queryParams.subStatus;
fn = this.$api.addSubscribersToListsByQuery;
}
@ -503,7 +506,6 @@ export default Vue.extend({
// Get subscribers on load.
this.querySubscribers();
}
if (this.$route.query.subscription_status) {
this.queryParams.subStatus = this.$route.query.subscription_status;
}

View file

@ -59,6 +59,20 @@
</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 class="columns">
<div class="column is-3">
<b-field :label="$t('settings.bounces.enableSendgrid')">

View file

@ -67,6 +67,7 @@
</div>
</div><!-- auth -->
<div class="smtp-shortcuts is-size-7">
<a href="#" @click.prevent="() => fillSettings(n, 'forwardemail')">Forward Email</a>
<a href="#" @click.prevent="() => fillSettings(n, 'gmail')">Gmail</a>
<a href="#" @click.prevent="() => fillSettings(n, 'ses')">Amazon SES</a>
<a href="#" @click.prevent="() => fillSettings(n, 'mailgun')">Mailgun</a>
@ -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',
},

View file

@ -369,12 +369,14 @@
"settings.bounces.enable": "Enable bounce processing",
"settings.bounces.enableMailbox": "Enable bounce mailbox",
"settings.bounces.enablePostmark": "Enable Postmark",
"settings.bounces.enableForwardemail": "Enable Forward Email",
"settings.bounces.enableSES": "Enable SES",
"settings.bounces.enableSendgrid": "Enable SendGrid",
"settings.bounces.enableWebhooks": "Enable bounce webhooks",
"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",

View file

@ -26,14 +26,16 @@ type Mailbox interface {
// Opt represents bounce processing options.
type Opt struct {
MailboxEnabled bool `json:"mailbox_enabled"`
MailboxType string `json:"mailbox_type"`
Mailbox mailbox.Opt `json:"mailbox"`
WebhooksEnabled bool `json:"webhooks_enabled"`
SESEnabled bool `json:"ses_enabled"`
SendgridEnabled bool `json:"sendgrid_enabled"`
SendgridKey string `json:"sendgrid_key"`
Postmark struct {
MailboxEnabled bool `json:"mailbox_enabled"`
MailboxType string `json:"mailbox_type"`
Mailbox mailbox.Opt `json:"mailbox"`
WebhooksEnabled bool `json:"webhooks_enabled"`
SESEnabled bool `json:"ses_enabled"`
SendgridEnabled bool `json:"sendgrid_enabled"`
SendgridKey string `json:"sendgrid_key"`
ForwardemailEnabled bool `json:"forwardemail_enabled"`
ForwardemailKey string `json:"forwardemail_key"`
Postmark struct {
Enabled bool
Username string
Password string
@ -44,14 +46,15 @@ type Opt struct {
// Manager handles e-mail bounces.
type Manager struct {
queue chan models.Bounce
mailbox Mailbox
SES *webhooks.SES
Sendgrid *webhooks.Sendgrid
Postmark *webhooks.Postmark
queries *Queries
opt Opt
log *log.Logger
queue chan models.Bounce
mailbox Mailbox
SES *webhooks.SES
Sendgrid *webhooks.Sendgrid
Postmark *webhooks.Postmark
Forwardemail *webhooks.Forwardemail
queries *Queries
opt Opt
log *log.Logger
}
// Queries contains the queries.
@ -93,6 +96,11 @@ func New(opt Opt, q *Queries, lo *log.Logger) (*Manager, error) {
}
}
if opt.ForwardemailEnabled {
fe := webhooks.NewForwardemail([]byte(opt.ForwardemailKey))
m.Forwardemail = fe
}
if opt.Postmark.Enabled {
m.Postmark = webhooks.NewPostmark(opt.Postmark.Username, opt.Postmark.Password)
}

View file

@ -0,0 +1,87 @@
package webhooks
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/knadh/listmonk/models"
)
type BounceDetails struct {
Action string `json:"action"`
Message string `json:"message"`
Category string `json:"category"`
Code int `json:"code"`
Status string `json:"status"`
Line int `json:"line"`
}
type forwardemailNotif struct {
EmailID string `json:"email_id"`
ListID string `json:"list_id"`
ListUnsubscribe string `json:"list_unsubscribe"`
FeedbackID string `json:"feedback_id"`
Recipient string `json:"recipient"`
Message string `json:"message"`
Response string `json:"response"`
ResponseCode int `json:"response_code"`
TruthSource string `json:"truth_source"`
Headers map[string]string `json:"headers"`
Bounce BounceDetails `json:"bounce"`
BouncedAt time.Time `json:"bounced_at"`
}
// Forwardemail handles webhook notifications (mainly bounce notifications).
type Forwardemail struct {
hmacKey []byte
}
func NewForwardemail(key []byte) *Forwardemail {
return &Forwardemail{hmacKey: key}
}
// ProcessBounce processes Forward Email bounce notifications and returns one object.
func (p *Forwardemail) ProcessBounce(sig, b []byte) ([]models.Bounce, error) {
key := []byte(p.hmacKey)
mac := hmac.New(sha256.New, key)
mac.Write(b)
signature := mac.Sum(nil)
if subtle.ConstantTimeCompare(signature, []byte(sig)) != 1 {
return nil, fmt.Errorf("invalid signature")
}
var n forwardemailNotif
if err := json.Unmarshal(b, &n); err != nil {
return nil, fmt.Errorf("error unmarshalling Forwardemail notification: %v", err)
}
typ := models.BounceTypeSoft
// TODO: support `typ = models.BounceTypeComplaint` in future
switch n.Bounce.Category {
case "block", "recipient", "virus", "spam":
typ = models.BounceTypeHard
}
campUUID := ""
if v, ok := n.Headers["X-Listmonk-Campaign"]; ok {
campUUID = v
}
return []models.Bounce{{
Email: strings.ToLower(n.Recipient),
CampaignUUID: campUUID,
Type: typ,
Source: "forwardemail",
Meta: json.RawMessage(b),
CreatedAt: n.BouncedAt,
}}, nil
}

View file

@ -394,8 +394,8 @@ func (c *Core) BlocklistSubscribers(subIDs []int) error {
}
// BlocklistSubscribersByQuery blocklists the given list of subscribers.
func (c *Core) BlocklistSubscribersByQuery(query string, listIDs []int) error {
if err := c.q.ExecSubQueryTpl(sanitizeSQLExp(query), c.q.BlocklistSubscribersByQuery, listIDs, c.db); err != nil {
func (c *Core) BlocklistSubscribersByQuery(query string, listIDs []int, subStatus string) error {
if err := c.q.ExecSubQueryTpl(sanitizeSQLExp(query), c.q.BlocklistSubscribersByQuery, listIDs, c.db, subStatus); err != nil {
c.log.Printf("error blocklisting subscribers: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError,
c.i18n.Ts("subscribers.errorBlocklisting", "error", pqErrMsg(err)))
@ -423,8 +423,8 @@ func (c *Core) DeleteSubscribers(subIDs []int, subUUIDs []string) error {
}
// DeleteSubscribersByQuery deletes subscribers by a given arbitrary query expression.
func (c *Core) DeleteSubscribersByQuery(query string, listIDs []int) error {
err := c.q.ExecSubQueryTpl(sanitizeSQLExp(query), c.q.DeleteSubscribersByQuery, listIDs, c.db)
func (c *Core) DeleteSubscribersByQuery(query string, listIDs []int, subStatus string) error {
err := c.q.ExecSubQueryTpl(sanitizeSQLExp(query), c.q.DeleteSubscribersByQuery, listIDs, c.db, subStatus)
if err != nil {
c.log.Printf("error deleting subscribers: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError,

View file

@ -35,12 +35,12 @@ func (c *Core) AddSubscriptions(subIDs, listIDs []int, status string) error {
// AddSubscriptionsByQuery adds list subscriptions to subscribers by a given arbitrary query expression.
// sourceListIDs is the list of list IDs to filter the subscriber query with.
func (c *Core) AddSubscriptionsByQuery(query string, sourceListIDs, targetListIDs []int, status string) error {
func (c *Core) AddSubscriptionsByQuery(query string, sourceListIDs, targetListIDs []int, status string, subStatus string) error {
if sourceListIDs == nil {
sourceListIDs = []int{}
}
err := c.q.ExecSubQueryTpl(sanitizeSQLExp(query), c.q.AddSubscribersToListsByQuery, sourceListIDs, c.db, pq.Array(targetListIDs), status)
err := c.q.ExecSubQueryTpl(sanitizeSQLExp(query), c.q.AddSubscribersToListsByQuery, sourceListIDs, c.db, subStatus, pq.Array(targetListIDs), status)
if err != nil {
c.log.Printf("error adding subscriptions by query: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError,
@ -64,12 +64,12 @@ func (c *Core) DeleteSubscriptions(subIDs, listIDs []int) error {
// DeleteSubscriptionsByQuery deletes list subscriptions from subscribers by a given arbitrary query expression.
// sourceListIDs is the list of list IDs to filter the subscriber query with.
func (c *Core) DeleteSubscriptionsByQuery(query string, sourceListIDs, targetListIDs []int) error {
func (c *Core) DeleteSubscriptionsByQuery(query string, sourceListIDs, targetListIDs []int, subStatus string) error {
if sourceListIDs == nil {
sourceListIDs = []int{}
}
err := c.q.ExecSubQueryTpl(sanitizeSQLExp(query), c.q.DeleteSubscriptionsByQuery, sourceListIDs, c.db, pq.Array(targetListIDs))
err := c.q.ExecSubQueryTpl(sanitizeSQLExp(query), c.q.DeleteSubscriptionsByQuery, sourceListIDs, c.db, subStatus, pq.Array(targetListIDs))
if err != nil {
c.log.Printf("error deleting subscriptions by query: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError,
@ -92,12 +92,12 @@ func (c *Core) UnsubscribeLists(subIDs, listIDs []int, listUUIDs []string) error
// UnsubscribeListsByQuery sets list subscriptions to 'unsubscribed' by a given arbitrary query expression.
// sourceListIDs is the list of list IDs to filter the subscriber query with.
func (c *Core) UnsubscribeListsByQuery(query string, sourceListIDs, targetListIDs []int) error {
func (c *Core) UnsubscribeListsByQuery(query string, sourceListIDs, targetListIDs []int, subStatus string) error {
if sourceListIDs == nil {
sourceListIDs = []int{}
}
err := c.q.ExecSubQueryTpl(sanitizeSQLExp(query), c.q.UnsubscribeSubscribersFromListsByQuery, sourceListIDs, c.db, pq.Array(targetListIDs))
err := c.q.ExecSubQueryTpl(sanitizeSQLExp(query), c.q.UnsubscribeSubscribersFromListsByQuery, sourceListIDs, c.db, subStatus, pq.Array(targetListIDs))
if err != nil {
c.log.Printf("error unsubscribing from lists by query: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError,

View file

@ -114,7 +114,7 @@ type Queries struct {
// out of it using the raw `query-subscribers-template` query template.
// While doing this, a readonly transaction is created and the query is
// dry run on it to ensure that it is indeed readonly.
func (q *Queries) CompileSubscriberQueryTpl(exp string, db *sqlx.DB) (string, error) {
func (q *Queries) CompileSubscriberQueryTpl(exp string, db *sqlx.DB, subStatus string) (string, error) {
tx, err := db.BeginTxx(context.Background(), &sql.TxOptions{ReadOnly: true})
if err != nil {
return "", err
@ -126,19 +126,18 @@ func (q *Queries) CompileSubscriberQueryTpl(exp string, db *sqlx.DB) (string, er
exp = " AND " + exp
}
stmt := fmt.Sprintf(q.QuerySubscribersTpl, exp)
if _, err := tx.Exec(stmt, true, pq.Int64Array{}); err != nil {
if _, err := tx.Exec(stmt, true, pq.Int64Array{}, subStatus); err != nil {
return "", err
}
return stmt, nil
}
// compileSubscriberQueryTpl takes an arbitrary WHERE expressions and a subscriber
// query template that depends on the filter (eg: delete by query, blocklist by query etc.)
// combines and executes them.
func (q *Queries) ExecSubQueryTpl(exp, tpl string, listIDs []int, db *sqlx.DB, args ...interface{}) error {
func (q *Queries) ExecSubQueryTpl(exp, tpl string, listIDs []int, db *sqlx.DB, subStatus string, args ...interface{}) error {
// Perform a dry run.
filterExp, err := q.CompileSubscriberQueryTpl(exp, db)
filterExp, err := q.CompileSubscriberQueryTpl(exp, db, subStatus)
if err != nil {
return err
}
@ -148,10 +147,9 @@ func (q *Queries) ExecSubQueryTpl(exp, tpl string, listIDs []int, db *sqlx.DB, a
}
// First argument is the boolean indicating if the query is a dry run.
a := append([]interface{}{false, pq.Array(listIDs)}, args...)
a := append([]interface{}{false, pq.Array(listIDs), subStatus}, args...)
if _, err := db.Exec(fmt.Sprintf(tpl, filterExp), a...); err != nil {
return err
}
return nil
}

View file

@ -99,7 +99,9 @@ type Settings struct {
Username string `json:"username"`
Password string `json:"password"`
} `json:"bounce.postmark"`
BounceBoxes []struct {
ForwardemailEnabled bool `json:"bounce.forwardemail_enabled"`
ForwardemailKey string `json:"bounce.forwardemail_key"`
BounceBoxes []struct {
UUID string `json:"uuid"`
Enabled bool `json:"enabled"`
Type string `json:"type"`

View file

@ -370,6 +370,7 @@ ON (
-- Optional list filtering.
(CASE WHEN CARDINALITY($2::INT[]) > 0 THEN true ELSE false END)
AND subscriber_lists.subscriber_id = subscribers.id
AND ($3 = '' OR subscriber_lists.status = $3::subscription_status)
)
WHERE subscriber_lists.list_id = ALL($2::INT[]) %s
LIMIT (CASE WHEN $1 THEN 1 END)
@ -393,20 +394,20 @@ UPDATE subscriber_lists SET status='unsubscribed', updated_at=NOW()
-- raw: true
WITH subs AS (%s)
INSERT INTO subscriber_lists (subscriber_id, list_id, status)
(SELECT a, b, (CASE WHEN $4 != '' THEN $4::subscription_status ELSE 'unconfirmed' END) FROM UNNEST(ARRAY(SELECT id FROM subs)) a, UNNEST($3::INT[]) b)
(SELECT a, b, (CASE WHEN $5 != '' THEN $5::subscription_status ELSE 'unconfirmed' END) FROM UNNEST(ARRAY(SELECT id FROM subs)) a, UNNEST($4::INT[]) b)
ON CONFLICT (subscriber_id, list_id) DO NOTHING;
-- name: delete-subscriptions-by-query
-- raw: true
WITH subs AS (%s)
DELETE FROM subscriber_lists
WHERE (subscriber_id, list_id) = ANY(SELECT a, b FROM UNNEST(ARRAY(SELECT id FROM subs)) a, UNNEST($3::INT[]) b);
WHERE (subscriber_id, list_id) = ANY(SELECT a, b FROM UNNEST(ARRAY(SELECT id FROM subs)) a, UNNEST($4::INT[]) b);
-- name: unsubscribe-subscribers-from-lists-by-query
-- raw: true
WITH subs AS (%s)
UPDATE subscriber_lists SET status='unsubscribed', updated_at=NOW()
WHERE (subscriber_id, list_id) = ANY(SELECT a, b FROM UNNEST(ARRAY(SELECT id FROM subs)) a, UNNEST($3::INT[]) b);
WHERE (subscriber_id, list_id) = ANY(SELECT a, b FROM UNNEST(ARRAY(SELECT id FROM subs)) a, UNNEST($4::INT[]) b);
-- lists

View file

@ -268,6 +268,8 @@ INSERT INTO settings (key, value) VALUES
('bounce.enabled', 'false'),
('bounce.webhooks_enabled', 'false'),
('bounce.actions', '{"soft": {"count": 2, "action": "none"}, "hard": {"count": 1, "action": "blocklist"}, "complaint" : {"count": 1, "action": "blocklist"}}'),
('bounce.forwardemail_enabled', 'false'),
('bounce.forwardemail_key', '""'),
('bounce.ses_enabled', 'false'),
('bounce.sendgrid_enabled', 'false'),
('bounce.sendgrid_key', '""'),