Skip to content

Commit f29adba

Browse files
committed
blocklist the bounces option
1 parent 4d8c8cd commit f29adba

9 files changed

Lines changed: 150 additions & 14 deletions

File tree

cmd/bounce.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,3 +248,38 @@ func (a *App) validateBounceFields(b models.Bounce) (models.Bounce, error) {
248248

249249
return b, nil
250250
}
251+
252+
func (a *App) BlocklistSubscriberBounces(c echo.Context) error {
253+
var (
254+
app = c.Get("app").(*App)
255+
pID = c.Param("id")
256+
all, _ = strconv.ParseBool(c.QueryParam("all"))
257+
IDs = []int{}
258+
)
259+
// Is it an /:id call?
260+
if pID != "" {
261+
id, _ := strconv.Atoi(pID)
262+
if id < 1 {
263+
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
264+
}
265+
IDs = append(IDs, id)
266+
} else if !all {
267+
// Multiple IDs.
268+
i, err := parseStringIDs(c.Request().URL.Query()["id"])
269+
if err != nil {
270+
return echo.NewHTTPError(http.StatusBadRequest,
271+
app.i18n.Ts("globals.messages.invalidID", "error", err.Error()))
272+
}
273+
274+
if len(i) == 0 {
275+
return echo.NewHTTPError(http.StatusBadRequest,
276+
app.i18n.Ts("globals.messages.invalidID"))
277+
}
278+
IDs = i
279+
}
280+
281+
if err := app.core.BlocklistSubscriberBounces(IDs); err != nil {
282+
return err
283+
}
284+
return c.JSON(http.StatusOK, okResp{true})
285+
}

cmd/handlers.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ func initHTTPHandlers(e *echo.Echo, a *App) {
124124
g.GET("/api/bounces/:id", pm(hasID(a.GetBounce), "bounces:get"))
125125
g.DELETE("/api/bounces", pm(a.DeleteBounces, "bounces:manage"))
126126
g.DELETE("/api/bounces/:id", pm(hasID(a.DeleteBounce), "bounces:manage"))
127+
g.PUT("/api/bounces/blocklist", pm(a.BlocklistSubscriberBounces, "bounces:manage"))
128+
g.PUT("/api/bounces/:id/blocklist", pm(hasID(a.BlocklistSubscriberBounces), "bounces:manage"))
127129

128130
// Subscriber operations based on arbitrary SQL queries.
129131
// These aren't very REST-like.

frontend/src/api/index.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,18 @@ export const deleteSubscriberBounces = async (id) => http.delete(
179179
{ loading: models.bounces },
180180
);
181181

182+
export const blocklistSubscriberBounce = async (id) => http.put(
183+
`/api/bounces/${id}/blocklist`,
184+
null,
185+
{ loading: models.bounces },
186+
);
187+
188+
export const blocklistSubscriberBounces = async (params) => http.put(
189+
'/api/bounces/blocklist',
190+
null,
191+
{ params, loading: models.bounces },
192+
);
193+
182194
export const deleteBounce = async (id) => http.delete(
183195
`/api/bounces/${id}`,
184196
{ loading: models.bounces },

frontend/src/assets/style.scss

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -812,7 +812,12 @@ section.lists {
812812
max-width: 100%;
813813
}
814814
}
815-
815+
.bulk-dropdown-wrapper {
816+
position: absolute;
817+
top: 50%;
818+
transform: translate(-110%, -50%); // shift left and vertically center
819+
z-index: 10;
820+
}
816821
/* Import page */
817822
section.import {
818823
.delimiter input {

frontend/src/views/Bounces.vue

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,59 @@
77
<span v-if="bounces.total > 0">({{ bounces.total }})</span>
88
</h1>
99
</div>
10-
<div class="column has-text-right buttons">
11-
<b-button v-if="bulk.checked.length > 0 || bulk.all" type="is-primary" icon-left="trash-can-outline"
12-
data-cy="btn-delete" @click.prevent="$utils.confirm(null, () => deleteBounces())">
13-
{{ $t('globals.buttons.clear') }}
10+
<div class="column has-text-right buttons is-flex is-align-items-center is-justify-content-flex-end is-flex-wrap-nowrap">
11+
<div class="bulk-actions-wrapper" style="position: relative; display: flex; align-items: center;">
12+
13+
<!-- Dropdown for selected bulk actions -->
14+
<div class="bulk-dropdown-wrapper">
15+
<b-dropdown
16+
v-if="bulk.checked.length > 0 || bulk.all"
17+
aria-role="list"
18+
class="bulk-dropdown-wrapper"
19+
>
20+
<template #trigger="{ active }">
21+
<b-button
22+
label="Selected Actions"
23+
type="is-primary"
24+
:icon-right="active ? 'arrow-up' : 'arrow-down'"
25+
/>
26+
</template>
27+
<b-dropdown-item
28+
aria-role="listitem"
29+
@click="$utils.confirm(null, () => blocklistBounces())"
30+
>
31+
{{ $t('settings.bounces.blocklist') }}
32+
</b-dropdown-item>
33+
<b-dropdown-item
34+
aria-role="listitem"
35+
@click="$utils.confirm(null, () => deleteBounces())"
36+
>
37+
{{ $t('globals.buttons.clear') }}
38+
</b-dropdown-item>
39+
</b-dropdown>
40+
</div>
41+
42+
<!-- Actions on all bounces -->
43+
<b-button
44+
v-if="bounces.total"
45+
icon-left="account-off-outline"
46+
data-cy="btn-bulk-blocklist"
47+
@click.prevent="$utils.confirm(null, () => blocklistBounces(true))"
48+
>
49+
{{ $t('settings.bounces.blocklist') + ' ' + $t('globals.terms.all') }}
1450
</b-button>
15-
<b-button v-if="bounces.total" icon-left="trash-can-outline" data-cy="btn-delete"
16-
@click.prevent="$utils.confirm(null, () => deleteBounces(true))">
51+
52+
<b-button
53+
v-if="bounces.total"
54+
icon-left="trash-can-outline"
55+
data-cy="btn-delete"
56+
@click.prevent="$utils.confirm(null, () => deleteBounces(true))"
57+
>
1758
{{ $t('globals.buttons.clearAll') }}
1859
</b-button>
60+
1961
</div>
62+
</div>
2063
</header>
2164

2265
<b-table :data="bounces.results" :hoverable="true" :loading="loading.bounces" default-sort="createdAt" checkable
@@ -166,18 +209,32 @@ export default Vue.extend({
166209
const ids = this.bulk.checked.map((s) => s.id);
167210
this.$api.deleteBounces({ id: ids }).then(fnSuccess);
168211
},
212+
blocklistBounces(all) {
213+
const count = all? this.bounces.total : this.bulk.checked.length;
214+
const fnSuccess = () => {
215+
this.getBounces();
216+
this.$utils.toast(this.$t(
217+
'globals.messages.blocklistedCount',
218+
{ name: this.$tc('globals.terms.bounces'), num: count},
219+
));
220+
};
221+
if (all) {
222+
this.$api.blocklistSubscriberBounces({all: true}). then(fnSuccess);
223+
return;
224+
}
225+
const ids = this.bulk.checked.map((s) => s.id);
226+
this.$api.blocklistSubscriberBounces({id: ids }).then(fnSuccess);
227+
},
169228
},
170229
171230
computed: {
172231
...mapState(['templates', 'loading']),
173-
174232
selectedBounces() {
175233
if (this.bulk.all) {
176234
return this.bounces.total;
177235
}
178236
return this.bulk.checked.length;
179237
},
180-
181238
},
182239
183240
mounted() {

i18n/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@
132132
"globals.buttons.add": "Add",
133133
"globals.buttons.addNew": "Add new",
134134
"globals.buttons.back": "Back",
135+
"globals.buttons.blocklist": "Blocklist",
136+
"globals.buttons.blocklistAll": "Blocklist all",
135137
"globals.buttons.cancel": "Cancel",
136138
"globals.buttons.clear": "Clear",
137139
"globals.buttons.clearAll": "Clear all",
@@ -170,6 +172,7 @@
170172
"globals.fields.type": "Type",
171173
"globals.fields.updatedAt": "Updated",
172174
"globals.fields.uuid": "UUID",
175+
"globals.messages.blocklistedCount": "{name} ({num}) blocklisted",
173176
"globals.messages.confirm": "Are you sure?",
174177
"globals.messages.confirmDiscard": "Discard changes?",
175178
"globals.messages.copied": "Copied",

internal/core/bounces.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,13 @@ func (c *Core) DeleteBounces(ids []int) error {
100100
}
101101
return nil
102102
}
103+
104+
// BlocklistSubscriberBounces blocklists the subscribers whose message bounces.
105+
func (c *Core) BlocklistSubscriberBounces(ids []int) error {
106+
if _, err := c.q.BlocklistSubscribersByBounces.Exec(pq.Array(ids)); err != nil {
107+
c.log.Printf("error blocklisting subscribers by bounces: %v", err)
108+
return echo.NewHTTPError(http.StatusInternalServerError,
109+
c.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.bounce}", "error", pqErrMsg(err)))
110+
}
111+
return nil
112+
}

models/queries.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,12 @@ type Queries struct {
106106
UpdateSettings *sqlx.Stmt `query:"update-settings"`
107107

108108
// GetStats *sqlx.Stmt `query:"get-stats"`
109-
RecordBounce *sqlx.Stmt `query:"record-bounce"`
110-
QueryBounces string `query:"query-bounces"`
111-
DeleteBounces *sqlx.Stmt `query:"delete-bounces"`
112-
DeleteBouncesBySubscriber *sqlx.Stmt `query:"delete-bounces-by-subscriber"`
113-
GetDBInfo string `query:"get-db-info"`
109+
RecordBounce *sqlx.Stmt `query:"record-bounce"`
110+
QueryBounces string `query:"query-bounces"`
111+
DeleteBounces *sqlx.Stmt `query:"delete-bounces"`
112+
DeleteBouncesBySubscriber *sqlx.Stmt `query:"delete-bounces-by-subscriber"`
113+
BlocklistSubscribersByBounces *sqlx.Stmt `query:"blocklist-subscribers-by-bounces"`
114+
GetDBInfo string `query:"get-db-info"`
114115

115116
CreateUser *sqlx.Stmt `query:"create-user"`
116117
UpdateUser *sqlx.Stmt `query:"update-user"`

queries.sql

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,6 +1070,17 @@ ORDER BY %order% OFFSET $5 LIMIT $6;
10701070
-- name: delete-bounces
10711071
DELETE FROM bounces WHERE CARDINALITY($1::INT[]) = 0 OR id = ANY($1);
10721072

1073+
--name: blocklist-subscribers-by-bounces
1074+
WITH subscriber_ids_to_blocklist AS (
1075+
SELECT DISTINCT b.subscriber_id
1076+
FROM bounces b
1077+
WHERE CARDINALITY($1::INT[]) = 0 OR b.id = ANY($1)
1078+
)
1079+
UPDATE subscribers
1080+
SET status = 'blocklisted'
1081+
WHERE id IN (SELECT subscriber_id FROM subscriber_ids_to_blocklist)
1082+
RETURNING id, email, status;
1083+
10731084
-- name: delete-bounces-by-subscriber
10741085
WITH sub AS (
10751086
SELECT id FROM subscribers WHERE CASE WHEN $1 > 0 THEN id = $1 ELSE uuid = $2 END

0 commit comments

Comments
 (0)