Refactor `blacklist` to `blocklist`

This commit is contained in:
Kailash Nadh 2020-08-01 16:45:29 +05:30
parent 2143def136
commit 8c0804ba9f
22 changed files with 84 additions and 273 deletions

View File

@ -1,199 +1,12 @@
[app]
# Interface and port where the app will run its webserver.
address = "0.0.0.0:9000"
# Public root URL of the listmonk installation that'll be used
# in the messages for linking to images, unsubscribe page etc.
root = "https://listmonk.mysite.com"
# (Optional) full URL to the static logo to be displayed on
# user facing view such as the unsubscription page.
# eg: https://mysite.com/images/logo.svg
logo_url = "https://listmonk.mysite.com/public/static/logo.png"
# (Optional) full URL to the static favicon to be displayed on
# user facing view such as the unsubscription page.
# eg: https://mysite.com/images/favicon.png
favicon_url = "https://listmonk.mysite.com/public/static/favicon.png"
# The default 'from' e-mail for outgoing e-mail campaigns.
from_email = "listmonk <from@mail.com>"
# List of e-mail addresses to which admin notifications such as
# import updates, campaign completion, failure etc. should be sent.
# To disable notifications, set an empty list, eg: notify_emails = []
notify_emails = ["admin1@mysite.com", "admin2@mysite.com"]
# Maximum concurrent workers that will attempt to send messages
# simultaneously. This should ideally depend on the number of CPUs
# available, and should be based on the maximum number of messages
# a target SMTP server will accept.
concurrency = 5
# Maximum number of messages to be sent out per second per worker.
# If concurrency = 10 and message_rate = 10, then up to 10x10=100 messages
# may be pushed out every second. This, along with concurrency, should be
# tweaked to keep the net messages going out per second under the target
# SMTP's rate limits, if any.
message_rate = 5
# The number of errors (eg: SMTP timeouts while e-mailing) a running
# campaign should tolerate before it is paused for manual
# investigation or intervention. Set to 0 to never pause.
max_send_errors = 1000
# The number of subscribers to pull from the databse in a single iteration.
# Each iteration pulls subscribers from the database, sends messages to them,
# and then moves on to the next iteration to pull the next batch.
# This should ideally be higher than the maximum achievable throughput (concurrency * message_rate)
batch_size = 1000
[privacy]
# Allow subscribers to unsubscribe from all mailing lists and mark themselves
# as blacklisted?
allow_blacklist = false
# Allow subscribers to export data recorded on them?
allow_export = false
# Items to include in the data export.
# profile Subscriber's profile including custom attributes
# subscriptions Subscriber's subscription lists (private list names are masked)
# campaign_views Campaigns the subscriber has viewed and the view counts
# link_clicks Links that the subscriber has clicked and the click counts
exportable = ["profile", "subscriptions", "campaign_views", "link_clicks"]
# Allow subscribers to delete themselves from the database?
# This deletes the subscriber and all their subscriptions.
# Their association to campaign views and link clicks are also
# removed while views and click counts remain (with no subscriber
# associated to them) so that stats and analytics aren't affected.
allow_wipe = false
# Interface and port where the app will run its webserver.
address = "0.0.0.0:9000"
# Database.
[db]
host = "demo-db"
port = 5432
user = "listmonk"
password = "listmonk"
database = "listmonk"
ssl_mode = "disable"
# Maximum active and idle connections to pool.
max_open = 50
max_idle = 10
# SMTP servers.
[smtp]
[smtp.my0]
enabled = true
host = "my.smtp.server"
port = 25
# "cram", "plain", or "login". Empty string for no auth.
auth_protocol = "cram"
username = "xxxxx"
password = ""
# Format to send e-mails in: html|plain|both.
email_format = "both"
# Optional. Some SMTP servers require a FQDN in the hostname.
# By default, HELLOs go with "localhost". Set this if a custom
# hostname should be used.
hello_hostname = ""
# Maximum concurrent connections to the SMTP server.
max_conns = 10
# Time to wait for new activity on a connection before closing
# it and removing it from the pool.
idle_timeout = "15s"
# Message send / wait timeout.
wait_timeout = "5s"
# The number of times a message should be retried if sending fails.
max_msg_retries = 2
# Enable STARTTLS.
tls_enabled = true
tls_skip_verify = false
# One or more optional custom headers to be attached to all e-mails
# sent from this SMTP server. Uncomment the line to enable.
# email_headers = { "X-Sender" = "listmonk", "X-Custom-Header" = "listmonk" }
[smtp.postal]
enabled = false
host = "my.smtp.server2"
port = 25
# cram or plain.
auth_protocol = "plain"
username = "xxxxx"
password = ""
# Format to send e-mails in: html|plain|both.
email_format = "both"
# Optional. Some SMTP servers require a FQDN in the hostname.
# By default, HELLOs go with "localhost". Set this if a custom
# hostname should be used.
hello_hostname = ""
# Maximum concurrent connections to the SMTP server.
max_conns = 10
# Time to wait for new activity on a connection before closing
# it and removing it from the pool.
idle_timeout = "15s"
# Message send / wait timeout.
wait_timeout = "5s"
# The number of times a message should be retried if sending fails.
max_msg_retries = 2
# Enable STARTTLS.
tls_enabled = true
tls_skip_verify = false
[upload]
# File storage backend. "filesystem" or "s3".
provider = "filesystem"
[upload.s3]
# (Optional). AWS Access Key and Secret Key for the user to access the bucket.
# Leaving it empty would default to use instance IAM role.
aws_access_key_id = ""
aws_secret_access_key = ""
# AWS Region where S3 bucket is hosted.
aws_default_region = "ap-south-1"
# Bucket name.
bucket = ""
# Path where the files will be stored inside bucket. Default is "/".
bucket_path = "/"
# Optional full URL to the bucket. eg: https://files.mycustom.com
bucket_url = ""
# "private" or "public".
bucket_type = "public"
# (Optional) Specify TTL (in seconds) for the generated presigned URL.
# Expiry value is used only if the bucket is private.
expiry = 86400
[upload.filesystem]
# Path to the uploads directory where media will be uploaded.
upload_path="./uploads"
# Upload URI that's visible to the outside world.
# The media uploaded to upload_path will be made available publicly
# under this URI, for instance, list.yoursite.com/uploads.
upload_uri = "/uploads"
host = "demo-db"
port = 5432
user = "listmonk"
password = "listmonk"
database = "listmonk"
ssl_mode = "disable"

View File

@ -122,10 +122,10 @@ export const addSubscribersToLists = (data) => http.put('/api/subscribers/lists'
export const addSubscribersToListsByQuery = (data) => http.put('/api/subscribers/query/lists',
data, { loading: models.subscribers });
export const blacklistSubscribers = (data) => http.put('/api/subscribers/blacklist', data,
export const blocklistSubscribers = (data) => http.put('/api/subscribers/blocklist', data,
{ loading: models.subscribers });
export const blacklistSubscribersByQuery = (data) => http.put('/api/subscribers/query/blacklist', data,
export const blocklistSubscribersByQuery = (data) => http.put('/api/subscribers/query/blocklist', data,
{ loading: models.subscribers });
export const deleteSubscribers = (params) => http.delete('/api/subscribers',

View File

@ -294,7 +294,7 @@ section {
border: 1px solid lighten($color, 45%);
box-shadow: 1px 1px 0 lighten($color, 45%);
}
&.blacklisted, &.cancelled {
&.blocklisted, &.cancelled {
$color: #f5222d;
color: $color;
background: #fff1f0;

View File

@ -67,8 +67,8 @@
<div class="column is-6">
<ul class="no is-size-7 has-text-grey">
<li>
<label>{{ $utils.niceNumber(counts.subscribers.blacklisted) }}</label>
blacklisted
<label>{{ $utils.niceNumber(counts.subscribers.blocklisted) }}</label>
blocklisted
</li>
<li>
<label>{{ $utils.niceNumber(counts.subscribers.orphans) }}</label>

View File

@ -14,7 +14,7 @@
<b-radio v-model="form.mode" name="mode"
native-value="subscribe">Subscribe</b-radio>
<b-radio v-model="form.mode" name="mode"
native-value="blacklist">Blacklist</b-radio>
native-value="blocklist">Blocklist</b-radio>
</div>
</b-field>
</div>

View File

@ -98,11 +98,11 @@
<b-tab-item label="Privacy">
<div class="items">
<b-field label="Allow blacklisting"
<b-field label="Allow blocklisting"
message="Allow subscribers to unsubscribe from all mailing lists and mark
themselves as blacklisted?">
<b-switch v-model="form['privacy.allow_blacklist']"
name="privacy.allow_blacklist" />
themselves as blocklisted?">
<b-switch v-model="form['privacy.allow_blocklist']"
name="privacy.allow_blocklist" />
</b-field>
<b-field label="Allow exporting"

View File

@ -22,10 +22,10 @@
</b-field>
<b-field label="Status" label-position="on-border"
message="Blacklisted subscribers will never receive any e-mails.">
message="Blocklisted subscribers will never receive any e-mails.">
<b-select v-model="form.status" placeholder="Status" required>
<option value="enabled">Enabled</option>
<option value="blacklisted">Blacklisted</option>
<option value="blocklisted">Blocklisted</option>
</b-select>
</b-field>

View File

@ -37,7 +37,7 @@
<b-input v-model="queryParams.queryExp"
@keydown.native.enter="onAdvancedQueryEnter"
type="textarea" ref="queryExp"
placeholder="subscribers.name LIKE '%user%' or subscribers.status='blacklisted'">
placeholder="subscribers.name LIKE '%user%' or subscribers.status='blocklisted'">
</b-input>
</b-field>
<b-field>
@ -80,8 +80,8 @@
<b-icon icon="trash-can-outline" size="is-small" /> Delete
</a>
<a href='' @click.prevent="blacklistSubscribers">
<b-icon icon="account-off-outline" size="is-small" /> Blacklist
<a href='' @click.prevent="blocklistSubscribers">
<b-icon icon="account-off-outline" size="is-small" /> Blocklist
</a>
</p><!-- selection actions //-->
</div>
@ -324,19 +324,19 @@ export default Vue.extend({
);
},
blacklistSubscribers() {
blocklistSubscribers() {
let fn = null;
if (!this.bulk.all && this.bulk.checked.length > 0) {
// If 'all' is not selected, blacklist subscribers by IDs.
// If 'all' is not selected, blocklist subscribers by IDs.
fn = () => {
const ids = this.bulk.checked.map((s) => s.id);
this.$api.blacklistSubscribers({ ids })
this.$api.blocklistSubscribers({ ids })
.then(() => this.querySubscribers());
};
} else {
// 'All' is selected, blacklist by query.
// 'All' is selected, blocklist by query.
fn = () => {
this.$api.blacklistSubscribersByQuery({
this.$api.blocklistSubscribersByQuery({
query: this.queryParams.queryExp,
list_ids: [],
}).then(() => this.querySubscribers());
@ -344,7 +344,7 @@ export default Vue.extend({
}
this.$utils.confirm(
`Blacklist ${this.numSelectedSubscribers} subscriber(s)?`,
`Blocklist ${this.numSelectedSubscribers} subscriber(s)?`,
fn,
);
},

View File

@ -51,8 +51,8 @@ func registerHTTPHandlers(e *echo.Echo) {
e.POST("/api/subscribers", handleCreateSubscriber)
e.PUT("/api/subscribers/:id", handleUpdateSubscriber)
e.POST("/api/subscribers/:id/optin", handleSubscriberSendOptin)
e.PUT("/api/subscribers/blacklist", handleBlacklistSubscribers)
e.PUT("/api/subscribers/:id/blacklist", handleBlacklistSubscribers)
e.PUT("/api/subscribers/blocklist", handleBlocklistSubscribers)
e.PUT("/api/subscribers/:id/blocklist", handleBlocklistSubscribers)
e.PUT("/api/subscribers/lists/:id", handleManageSubscriberLists)
e.PUT("/api/subscribers/lists", handleManageSubscriberLists)
e.DELETE("/api/subscribers/:id", handleDeleteSubscribers)
@ -61,7 +61,7 @@ func registerHTTPHandlers(e *echo.Echo) {
// Subscriber operations based on arbitrary SQL queries.
// These aren't very REST-like.
e.POST("/api/subscribers/query/delete", handleDeleteSubscribersByQuery)
e.PUT("/api/subscribers/query/blacklist", handleBlacklistSubscribersByQuery)
e.PUT("/api/subscribers/query/blocklist", handleBlocklistSubscribersByQuery)
e.PUT("/api/subscribers/query/lists", handleManageSubscriberListsByQuery)
e.GET("/api/subscribers", handleQuerySubscribers)

View File

@ -38,7 +38,7 @@ func handleImportSubscribers(c echo.Context) error {
fmt.Sprintf("Invalid `params` field: %v", err))
}
if r.Mode != subimporter.ModeSubscribe && r.Mode != subimporter.ModeBlacklist {
if r.Mode != subimporter.ModeSubscribe && r.Mode != subimporter.ModeBlocklist {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid `mode`")
}

View File

@ -43,7 +43,7 @@ type constants struct {
FromEmail string `koanf:"from_email"`
NotifyEmails []string `koanf:"notify_emails"`
Privacy struct {
AllowBlacklist bool `koanf:"allow_blacklist"`
AllowBlocklist bool `koanf:"allow_blocklist"`
AllowExport bool `koanf:"allow_export"`
AllowWipe bool `koanf:"allow_wipe"`
Exportable map[string]bool `koanf:"-"`
@ -278,7 +278,7 @@ func initImporter(q *Queries, db *sqlx.DB, app *App) *subimporter.Importer {
return subimporter.New(
subimporter.Options{
UpsertStmt: q.UpsertSubscriber.Stmt,
BlacklistStmt: q.UpsertBlacklistSubscriber.Stmt,
BlocklistStmt: q.UpsertBlocklistSubscriber.Stmt,
UpdateListDateStmt: q.UpdateListsDate.Stmt,
NotifCB: func(subject string, data interface{}) error {
app.sendNotification(app.constants.NotifyEmails, subject, notifTplImport, data)

View File

@ -6,7 +6,6 @@ import (
"io/ioutil"
"os"
"strings"
"time"
"github.com/gofrs/uuid"
"github.com/jmoiron/sqlx"
@ -113,7 +112,6 @@ func install(db *sqlx.DB, fs stuffbin.FileSystem, prompt bool) {
}
// Sample campaign.
sendAt := time.Now().Add(time.Hour * 24)
if _, err := q.CreateCampaign.Exec(uuid.Must(uuid.NewV4()),
models.CampaignTypeRegular,
"Test campaign",
@ -122,7 +120,7 @@ func install(db *sqlx.DB, fs stuffbin.FileSystem, prompt bool) {
`<h3>Hi {{ .Subscriber.FirstName }}!</h3>
This is a test e-mail campaign. Your second name is {{ .Subscriber.LastName }} and you are from {{ .Subscriber.Attribs.city }}.`,
"richtext",
sendAt,
nil,
pq.StringArray{"test-campaign"},
"email",
1,

View File

@ -52,7 +52,7 @@ func NewEmailer(servers ...Server) (*Emailer, error) {
auth = smtp.PlainAuth("", s.Username, s.Password, s.Host)
case "login":
auth = &smtppool.LoginAuth{Username: s.Username, Password: s.Password}
case "":
case "", "none":
default:
return nil, fmt.Errorf("unknown SMTP auth type '%s'", s.AuthProtocol)
}

View File

@ -44,7 +44,7 @@ const (
StatusFailed = "failed"
ModeSubscribe = "subscribe"
ModeBlacklist = "blacklist"
ModeBlocklist = "blocklist"
)
// Importer represents the bulk CSV subscriber import system.
@ -60,7 +60,7 @@ type Importer struct {
// Options represents inport options.
type Options struct {
UpsertStmt *sql.Stmt
BlacklistStmt *sql.Stmt
BlocklistStmt *sql.Stmt
UpdateListDateStmt *sql.Stmt
NotifCB models.AdminNotifCallback
}
@ -255,7 +255,7 @@ func (s *Session) Start() {
if s.mode == ModeSubscribe {
stmt = tx.Stmt(s.im.opt.UpsertStmt)
} else {
stmt = tx.Stmt(s.im.opt.BlacklistStmt)
stmt = tx.Stmt(s.im.opt.BlocklistStmt)
}
}
@ -268,7 +268,7 @@ func (s *Session) Start() {
if s.mode == ModeSubscribe {
_, err = stmt.Exec(uu, sub.Email, sub.Name, sub.Attribs, listIDs, s.overwrite)
} else if s.mode == ModeBlacklist {
} else if s.mode == ModeBlocklist {
_, err = stmt.Exec(uu, sub.Email, sub.Name, sub.Attribs)
}
if err != nil {

View File

@ -21,7 +21,7 @@ const (
// Subscriber.
SubscriberStatusEnabled = "enabled"
SubscriberStatusDisabled = "disabled"
SubscriberStatusBlackListed = "blacklisted"
SubscriberStatusBlockListed = "blocklisted"
// Subscription.
SubscriptionStatusUnconfirmed = "unconfirmed"

View File

@ -48,7 +48,7 @@ type publicTpl struct {
type unsubTpl struct {
publicTpl
SubUUID string
AllowBlacklist bool
AllowBlocklist bool
AllowExport bool
AllowWipe bool
}
@ -147,23 +147,23 @@ func handleSubscriptionPage(c echo.Context) error {
campUUID = c.Param("campUUID")
subUUID = c.Param("subUUID")
unsub, _ = strconv.ParseBool(c.FormValue("unsubscribe"))
blacklist, _ = strconv.ParseBool(c.FormValue("blacklist"))
blocklist, _ = strconv.ParseBool(c.FormValue("blocklist"))
out = unsubTpl{}
)
out.SubUUID = subUUID
out.Title = "Unsubscribe from mailing list"
out.AllowBlacklist = app.constants.Privacy.AllowBlacklist
out.AllowBlocklist = app.constants.Privacy.AllowBlocklist
out.AllowExport = app.constants.Privacy.AllowExport
out.AllowWipe = app.constants.Privacy.AllowWipe
// Unsubscribe.
if unsub {
// Is blacklisting allowed?
if !app.constants.Privacy.AllowBlacklist {
blacklist = false
// Is blocklisting allowed?
if !app.constants.Privacy.AllowBlocklist {
blocklist = false
}
if _, err := app.queries.Unsubscribe.Exec(campUUID, subUUID, blacklist); err != nil {
if _, err := app.queries.Unsubscribe.Exec(campUUID, subUUID, blocklist); err != nil {
app.log.Printf("error unsubscribing: %v", err)
return c.Render(http.StatusInternalServerError, tplMessage,
makeMsgTpl("Error", "",

View File

@ -16,14 +16,14 @@ type Queries struct {
InsertSubscriber *sqlx.Stmt `query:"insert-subscriber"`
UpsertSubscriber *sqlx.Stmt `query:"upsert-subscriber"`
UpsertBlacklistSubscriber *sqlx.Stmt `query:"upsert-blacklist-subscriber"`
UpsertBlocklistSubscriber *sqlx.Stmt `query:"upsert-blocklist-subscriber"`
GetSubscriber *sqlx.Stmt `query:"get-subscriber"`
GetSubscribersByEmails *sqlx.Stmt `query:"get-subscribers-by-emails"`
GetSubscriberLists *sqlx.Stmt `query:"get-subscriber-lists"`
GetSubscriberListsLazy *sqlx.Stmt `query:"get-subscriber-lists-lazy"`
SubscriberExists *sqlx.Stmt `query:"subscriber-exists"`
UpdateSubscriber *sqlx.Stmt `query:"update-subscriber"`
BlacklistSubscribers *sqlx.Stmt `query:"blacklist-subscribers"`
BlocklistSubscribers *sqlx.Stmt `query:"blocklist-subscribers"`
AddSubscribersToLists *sqlx.Stmt `query:"add-subscribers-to-lists"`
DeleteSubscriptions *sqlx.Stmt `query:"delete-subscriptions"`
ConfirmSubscriptionOptin *sqlx.Stmt `query:"confirm-subscription-optin"`
@ -37,7 +37,7 @@ type Queries struct {
QuerySubscribersTpl string `query:"query-subscribers-template"`
DeleteSubscribersByQuery string `query:"delete-subscribers-by-query"`
AddSubscribersToListsByQuery string `query:"add-subscribers-to-lists-by-query"`
BlacklistSubscribersByQuery string `query:"blacklist-subscribers-by-query"`
BlocklistSubscribersByQuery string `query:"blocklist-subscribers-by-query"`
DeleteSubscriptionsByQuery string `query:"delete-subscriptions-by-query"`
UnsubscribeSubscribersFromListsByQuery string `query:"unsubscribe-subscribers-from-lists-by-query"`
@ -132,7 +132,7 @@ func (q *Queries) compileSubscriberQueryTpl(exp string, db *sqlx.DB) (string, er
}
// compileSubscriberQueryTpl takes a arbitrary WHERE expressions and a subscriber
// query template that depends on the filter (eg: delete by query, blacklist by query etc.)
// query template that depends on the filter (eg: delete by query, blocklist by query etc.)
// combines and executes them.
func (q *Queries) execSubscriberQueryTpl(exp, tpl string, listIDs []int64, db *sqlx.DB, args ...interface{}) error {
// Perform a dry run.

View File

@ -64,7 +64,7 @@ subs AS (
VALUES(
(SELECT id FROM sub),
UNNEST(ARRAY(SELECT id FROM listIDs)),
(CASE WHEN $4='blacklisted' THEN 'unsubscribed'::subscription_status ELSE 'unconfirmed' END)
(CASE WHEN $4='blocklisted' THEN 'unsubscribed'::subscription_status ELSE 'unconfirmed' END)
)
ON CONFLICT (subscriber_id, list_id) DO UPDATE
SET updated_at=NOW()
@ -92,15 +92,15 @@ subs AS (
)
SELECT uuid, id from sub;
-- name: upsert-blacklist-subscriber
-- Upserts a subscriber where the update will only set the status to blacklisted
-- name: upsert-blocklist-subscriber
-- Upserts a subscriber where the update will only set the status to blocklisted
-- unlike upsert-subscribers where name and attributes are updated. In addition, all
-- existing subscriptions are marked as 'unsubscribed'.
-- This is used in the bulk importer.
WITH sub AS (
INSERT INTO subscribers (uuid, email, name, attribs, status)
VALUES($1, $2, $3, $4, 'blacklisted')
ON CONFLICT (email) DO UPDATE SET status='blacklisted', updated_at=NOW()
VALUES($1, $2, $3, $4, 'blocklisted')
ON CONFLICT (email) DO UPDATE SET status='blocklisted', updated_at=NOW()
RETURNING id
)
UPDATE subscriber_lists SET status='unsubscribed', updated_at=NOW()
@ -125,18 +125,18 @@ INSERT INTO subscriber_lists (subscriber_id, list_id, status)
VALUES(
(SELECT id FROM s),
UNNEST($6),
(CASE WHEN $4='blacklisted' THEN 'unsubscribed'::subscription_status ELSE 'unconfirmed' END)
(CASE WHEN $4='blocklisted' THEN 'unsubscribed'::subscription_status ELSE 'unconfirmed' END)
)
ON CONFLICT (subscriber_id, list_id) DO UPDATE
SET status = (CASE WHEN $4='blacklisted' THEN 'unsubscribed'::subscription_status ELSE subscriber_lists.status END);
SET status = (CASE WHEN $4='blocklisted' THEN 'unsubscribed'::subscription_status ELSE subscriber_lists.status END);
-- name: delete-subscribers
-- Delete one or more subscribers by ID or UUID.
DELETE FROM subscribers WHERE CASE WHEN ARRAY_LENGTH($1::INT[], 1) > 0 THEN id = ANY($1) ELSE uuid = ANY($2::UUID[]) END;
-- name: blacklist-subscribers
-- name: blocklist-subscribers
WITH b AS (
UPDATE subscribers SET status='blacklisted', updated_at=NOW()
UPDATE subscribers SET status='blocklisted', updated_at=NOW()
WHERE id = ANY($1::INT[])
)
UPDATE subscriber_lists SET status='unsubscribed', updated_at=NOW()
@ -167,7 +167,7 @@ UPDATE subscriber_lists SET status='unsubscribed', updated_at=NOW()
-- name: unsubscribe
-- Unsubscribes a subscriber given a campaign UUID (from all the lists in the campaign) and the subscriber UUID.
-- If $3 is TRUE, then all subscriptions of the subscriber is blacklisted
-- If $3 is TRUE, then all subscriptions of the subscriber is blocklisted
-- and all existing subscriptions, irrespective of lists, unsubscribed.
WITH lists AS (
SELECT list_id FROM campaign_lists
@ -175,7 +175,7 @@ WITH lists AS (
WHERE campaigns.uuid = $1
),
sub AS (
UPDATE subscribers SET status = (CASE WHEN $3 IS TRUE THEN 'blacklisted' ELSE status END)
UPDATE subscribers SET status = (CASE WHEN $3 IS TRUE THEN 'blocklisted' ELSE status END)
WHERE uuid = $2 RETURNING id
)
UPDATE subscriber_lists SET status = 'unsubscribed' WHERE
@ -239,7 +239,7 @@ SELECT COUNT(*) OVER () AS total, subscribers.* FROM subscribers
-- name: query-subscribers-template
-- raw: true
-- This raw query is reused in multiple queries (blacklist, add to list, delete)
-- This raw query is reused in multiple queries (blocklist, add to list, delete)
-- etc., so it's kept has a raw template to be injected into other raw queries,
-- and for the same reason, it is not terminated with a semicolon.
--
@ -261,11 +261,11 @@ LIMIT (CASE WHEN $1 THEN 1 END)
WITH subs AS (%s)
DELETE FROM subscribers WHERE id=ANY(SELECT id FROM subs);
-- name: blacklist-subscribers-by-query
-- name: blocklist-subscribers-by-query
-- raw: true
WITH subs AS (%s),
b AS (
UPDATE subscribers SET status='blacklisted', updated_at=NOW()
UPDATE subscribers SET status='blocklisted', updated_at=NOW()
WHERE id = ANY(SELECT id FROM subs)
)
UPDATE subscriber_lists SET status='unsubscribed', updated_at=NOW()
@ -513,7 +513,7 @@ subs AS (
campLists.list_id = subscriber_lists.list_id
)
INNER JOIN subscribers ON (
subscribers.status != 'blacklisted' AND
subscribers.status != 'blocklisted' AND
subscribers.id = subscriber_lists.subscriber_id AND
(CASE
@ -702,7 +702,7 @@ SELECT JSON_BUILD_OBJECT('link_clicks', COALESCE((SELECT * FROM clicks), '[]'),
-- name: get-dashboard-counts
SELECT JSON_BUILD_OBJECT('subscribers', JSON_BUILD_OBJECT(
'total', (SELECT COUNT(*) FROM subscribers),
'blacklisted', (SELECT COUNT(*) FROM subscribers WHERE status='blacklisted'),
'blocklisted', (SELECT COUNT(*) FROM subscribers WHERE status='blocklisted'),
'orphans', (
SELECT COUNT(id) FROM subscribers
LEFT JOIN subscriber_lists ON (subscribers.id = subscriber_lists.subscriber_id)

View File

@ -1,6 +1,6 @@
DROP TYPE IF EXISTS list_type CASCADE; CREATE TYPE list_type AS ENUM ('public', 'private', 'temporary');
DROP TYPE IF EXISTS list_optin CASCADE; CREATE TYPE list_optin AS ENUM ('single', 'double');
DROP TYPE IF EXISTS subscriber_status CASCADE; CREATE TYPE subscriber_status AS ENUM ('enabled', 'disabled', 'blacklisted');
DROP TYPE IF EXISTS subscriber_status CASCADE; CREATE TYPE subscriber_status AS ENUM ('enabled', 'disabled', 'blocklisted');
DROP TYPE IF EXISTS subscription_status CASCADE; CREATE TYPE subscription_status AS ENUM ('unconfirmed', 'confirmed', 'unsubscribed');
DROP TYPE IF EXISTS campaign_status CASCADE; CREATE TYPE campaign_status AS ENUM ('draft', 'running', 'scheduled', 'paused', 'cancelled', 'finished');
DROP TYPE IF EXISTS campaign_type CASCADE; CREATE TYPE campaign_type AS ENUM ('regular', 'optin');
@ -173,7 +173,7 @@ INSERT INTO settings (key, value) VALUES
('app.batch_size', '1000'),
('app.max_send_errors', '1000'),
('app.notify_emails', '["admin1@mysite.com", "admin2@mysite.com"]'),
('privacy.allow_blacklist', 'true'),
('privacy.allow_blocklist', 'true'),
('privacy.allow_export', 'true'),
('privacy.allow_wipe', 'true'),
('privacy.exportable', '["profile", "subscriptions", "campaign_views", "link_clicks"]'),

View File

@ -24,7 +24,7 @@ type settings struct {
Messengers []interface{} `json:"messengers"`
PrivacyAllowBlacklist bool `json:"privacy.allow_blacklist"`
PrivacyAllowBlocklist bool `json:"privacy.allow_blocklist"`
PrivacyAllowExport bool `json:"privacy.allow_export"`
PrivacyAllowWipe bool `json:"privacy.allow_wipe"`
PrivacyExportable []string `json:"privacy.exportable"`

View File

@ -7,9 +7,9 @@
<div>
<input type="hidden" name="unsubscribe" value="true" />
{{ if .Data.AllowBlacklist }}
{{ if .Data.AllowBlocklist }}
<p>
<input id="privacy-blacklist" type="checkbox" name="blacklist" value="true" /> <label for="privacy-blacklist">Also unsubscribe from all future e-mails.</label>
<input id="privacy-blocklist" type="checkbox" name="blocklist" value="true" /> <label for="privacy-blocklist">Also unsubscribe from all future e-mails.</label>
</p>
{{ end }}

View File

@ -248,9 +248,9 @@ func handleSubscriberSendOptin(c echo.Context) error {
return c.JSON(http.StatusOK, okResp{true})
}
// handleBlacklistSubscribers handles the blacklisting of one or more subscribers.
// handleBlocklistSubscribers handles the blocklisting of one or more subscribers.
// It takes either an ID in the URI, or a list of IDs in the request body.
func handleBlacklistSubscribers(c echo.Context) error {
func handleBlocklistSubscribers(c echo.Context) error {
var (
app = c.Get("app").(*App)
pID = c.Param("id")
@ -278,10 +278,10 @@ func handleBlacklistSubscribers(c echo.Context) error {
IDs = req.SubscriberIDs
}
if _, err := app.queries.BlacklistSubscribers.Exec(IDs); err != nil {
app.log.Printf("error blacklisting subscribers: %v", err)
if _, err := app.queries.BlocklistSubscribers.Exec(IDs); err != nil {
app.log.Printf("error blocklisting subscribers: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError,
fmt.Sprintf("Error blacklisting: %v", err))
fmt.Sprintf("Error blocklisting: %v", err))
}
return c.JSON(http.StatusOK, okResp{true})
@ -407,9 +407,9 @@ func handleDeleteSubscribersByQuery(c echo.Context) error {
return c.JSON(http.StatusOK, okResp{true})
}
// handleBlacklistSubscribersByQuery bulk blacklists subscribers
// handleBlocklistSubscribersByQuery bulk blocklists subscribers
// based on an arbitrary SQL expression.
func handleBlacklistSubscribersByQuery(c echo.Context) error {
func handleBlocklistSubscribersByQuery(c echo.Context) error {
var (
app = c.Get("app").(*App)
req subQueryReq
@ -420,10 +420,10 @@ func handleBlacklistSubscribersByQuery(c echo.Context) error {
}
err := app.queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query),
app.queries.BlacklistSubscribersByQuery,
app.queries.BlocklistSubscribersByQuery,
req.ListIDs, app.db)
if err != nil {
app.log.Printf("error blacklisting subscribers: %v", err)
app.log.Printf("error blocklisting subscribers: %v", err)
return echo.NewHTTPError(http.StatusBadRequest,
fmt.Sprintf("Error: %v", err))
}
@ -431,7 +431,7 @@ func handleBlacklistSubscribersByQuery(c echo.Context) error {
return c.JSON(http.StatusOK, okResp{true})
}
// handleBlacklistSubscribersByQuery bulk adds/removes/unsubscribers subscribers
// handleManageSubscriberListsByQuery bulk adds/removes/unsubscribers subscribers
// from one or more lists based on an arbitrary SQL expression.
func handleManageSubscriberListsByQuery(c echo.Context) error {
var (