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] [app]
# Interface and port where the app will run its webserver. # Interface and port where the app will run its webserver.
address = "0.0.0.0:9000" 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
# Database. # Database.
[db] [db]
host = "demo-db" host = "demo-db"
port = 5432 port = 5432
user = "listmonk" user = "listmonk"
password = "listmonk" password = "listmonk"
database = "listmonk" database = "listmonk"
ssl_mode = "disable" 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"

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', export const addSubscribersToListsByQuery = (data) => http.put('/api/subscribers/query/lists',
data, { loading: models.subscribers }); 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 }); { 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 }); { loading: models.subscribers });
export const deleteSubscribers = (params) => http.delete('/api/subscribers', export const deleteSubscribers = (params) => http.delete('/api/subscribers',

View File

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

View File

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

View File

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

View File

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

View File

@ -22,10 +22,10 @@
</b-field> </b-field>
<b-field label="Status" label-position="on-border" <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> <b-select v-model="form.status" placeholder="Status" required>
<option value="enabled">Enabled</option> <option value="enabled">Enabled</option>
<option value="blacklisted">Blacklisted</option> <option value="blocklisted">Blocklisted</option>
</b-select> </b-select>
</b-field> </b-field>

View File

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

View File

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

View File

@ -38,7 +38,7 @@ func handleImportSubscribers(c echo.Context) error {
fmt.Sprintf("Invalid `params` field: %v", err)) 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`") return echo.NewHTTPError(http.StatusBadRequest, "Invalid `mode`")
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,14 +16,14 @@ type Queries struct {
InsertSubscriber *sqlx.Stmt `query:"insert-subscriber"` InsertSubscriber *sqlx.Stmt `query:"insert-subscriber"`
UpsertSubscriber *sqlx.Stmt `query:"upsert-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"` GetSubscriber *sqlx.Stmt `query:"get-subscriber"`
GetSubscribersByEmails *sqlx.Stmt `query:"get-subscribers-by-emails"` GetSubscribersByEmails *sqlx.Stmt `query:"get-subscribers-by-emails"`
GetSubscriberLists *sqlx.Stmt `query:"get-subscriber-lists"` GetSubscriberLists *sqlx.Stmt `query:"get-subscriber-lists"`
GetSubscriberListsLazy *sqlx.Stmt `query:"get-subscriber-lists-lazy"` GetSubscriberListsLazy *sqlx.Stmt `query:"get-subscriber-lists-lazy"`
SubscriberExists *sqlx.Stmt `query:"subscriber-exists"` SubscriberExists *sqlx.Stmt `query:"subscriber-exists"`
UpdateSubscriber *sqlx.Stmt `query:"update-subscriber"` 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"` AddSubscribersToLists *sqlx.Stmt `query:"add-subscribers-to-lists"`
DeleteSubscriptions *sqlx.Stmt `query:"delete-subscriptions"` DeleteSubscriptions *sqlx.Stmt `query:"delete-subscriptions"`
ConfirmSubscriptionOptin *sqlx.Stmt `query:"confirm-subscription-optin"` ConfirmSubscriptionOptin *sqlx.Stmt `query:"confirm-subscription-optin"`
@ -37,7 +37,7 @@ type Queries struct {
QuerySubscribersTpl string `query:"query-subscribers-template"` QuerySubscribersTpl string `query:"query-subscribers-template"`
DeleteSubscribersByQuery string `query:"delete-subscribers-by-query"` DeleteSubscribersByQuery string `query:"delete-subscribers-by-query"`
AddSubscribersToListsByQuery string `query:"add-subscribers-to-lists-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"` DeleteSubscriptionsByQuery string `query:"delete-subscriptions-by-query"`
UnsubscribeSubscribersFromListsByQuery string `query:"unsubscribe-subscribers-from-lists-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 // 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. // combines and executes them.
func (q *Queries) execSubscriberQueryTpl(exp, tpl string, listIDs []int64, db *sqlx.DB, args ...interface{}) error { func (q *Queries) execSubscriberQueryTpl(exp, tpl string, listIDs []int64, db *sqlx.DB, args ...interface{}) error {
// Perform a dry run. // Perform a dry run.

View File

@ -64,7 +64,7 @@ subs AS (
VALUES( VALUES(
(SELECT id FROM sub), (SELECT id FROM sub),
UNNEST(ARRAY(SELECT id FROM listIDs)), 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 ON CONFLICT (subscriber_id, list_id) DO UPDATE
SET updated_at=NOW() SET updated_at=NOW()
@ -92,15 +92,15 @@ subs AS (
) )
SELECT uuid, id from sub; SELECT uuid, id from sub;
-- name: upsert-blacklist-subscriber -- name: upsert-blocklist-subscriber
-- Upserts a subscriber where the update will only set the status to blacklisted -- 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 -- unlike upsert-subscribers where name and attributes are updated. In addition, all
-- existing subscriptions are marked as 'unsubscribed'. -- existing subscriptions are marked as 'unsubscribed'.
-- This is used in the bulk importer. -- This is used in the bulk importer.
WITH sub AS ( WITH sub AS (
INSERT INTO subscribers (uuid, email, name, attribs, status) INSERT INTO subscribers (uuid, email, name, attribs, status)
VALUES($1, $2, $3, $4, 'blacklisted') VALUES($1, $2, $3, $4, 'blocklisted')
ON CONFLICT (email) DO UPDATE SET status='blacklisted', updated_at=NOW() ON CONFLICT (email) DO UPDATE SET status='blocklisted', updated_at=NOW()
RETURNING id RETURNING id
) )
UPDATE subscriber_lists SET status='unsubscribed', updated_at=NOW() UPDATE subscriber_lists SET status='unsubscribed', updated_at=NOW()
@ -125,18 +125,18 @@ INSERT INTO subscriber_lists (subscriber_id, list_id, status)
VALUES( VALUES(
(SELECT id FROM s), (SELECT id FROM s),
UNNEST($6), 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 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 -- name: delete-subscribers
-- Delete one or more subscribers by ID or UUID. -- 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; 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 ( WITH b AS (
UPDATE subscribers SET status='blacklisted', updated_at=NOW() UPDATE subscribers SET status='blocklisted', updated_at=NOW()
WHERE id = ANY($1::INT[]) WHERE id = ANY($1::INT[])
) )
UPDATE subscriber_lists SET status='unsubscribed', updated_at=NOW() UPDATE subscriber_lists SET status='unsubscribed', updated_at=NOW()
@ -167,7 +167,7 @@ UPDATE subscriber_lists SET status='unsubscribed', updated_at=NOW()
-- name: unsubscribe -- name: unsubscribe
-- Unsubscribes a subscriber given a campaign UUID (from all the lists in the campaign) and the subscriber UUID. -- 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. -- and all existing subscriptions, irrespective of lists, unsubscribed.
WITH lists AS ( WITH lists AS (
SELECT list_id FROM campaign_lists SELECT list_id FROM campaign_lists
@ -175,7 +175,7 @@ WITH lists AS (
WHERE campaigns.uuid = $1 WHERE campaigns.uuid = $1
), ),
sub AS ( 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 WHERE uuid = $2 RETURNING id
) )
UPDATE subscriber_lists SET status = 'unsubscribed' WHERE UPDATE subscriber_lists SET status = 'unsubscribed' WHERE
@ -239,7 +239,7 @@ SELECT COUNT(*) OVER () AS total, subscribers.* FROM subscribers
-- name: query-subscribers-template -- name: query-subscribers-template
-- raw: true -- 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, -- 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. -- 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) WITH subs AS (%s)
DELETE FROM subscribers WHERE id=ANY(SELECT id FROM subs); DELETE FROM subscribers WHERE id=ANY(SELECT id FROM subs);
-- name: blacklist-subscribers-by-query -- name: blocklist-subscribers-by-query
-- raw: true -- raw: true
WITH subs AS (%s), WITH subs AS (%s),
b AS ( 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) WHERE id = ANY(SELECT id FROM subs)
) )
UPDATE subscriber_lists SET status='unsubscribed', updated_at=NOW() UPDATE subscriber_lists SET status='unsubscribed', updated_at=NOW()
@ -513,7 +513,7 @@ subs AS (
campLists.list_id = subscriber_lists.list_id campLists.list_id = subscriber_lists.list_id
) )
INNER JOIN subscribers ON ( INNER JOIN subscribers ON (
subscribers.status != 'blacklisted' AND subscribers.status != 'blocklisted' AND
subscribers.id = subscriber_lists.subscriber_id AND subscribers.id = subscriber_lists.subscriber_id AND
(CASE (CASE
@ -702,7 +702,7 @@ SELECT JSON_BUILD_OBJECT('link_clicks', COALESCE((SELECT * FROM clicks), '[]'),
-- name: get-dashboard-counts -- name: get-dashboard-counts
SELECT JSON_BUILD_OBJECT('subscribers', JSON_BUILD_OBJECT( SELECT JSON_BUILD_OBJECT('subscribers', JSON_BUILD_OBJECT(
'total', (SELECT COUNT(*) FROM subscribers), 'total', (SELECT COUNT(*) FROM subscribers),
'blacklisted', (SELECT COUNT(*) FROM subscribers WHERE status='blacklisted'), 'blocklisted', (SELECT COUNT(*) FROM subscribers WHERE status='blocklisted'),
'orphans', ( 'orphans', (
SELECT COUNT(id) FROM subscribers SELECT COUNT(id) FROM subscribers
LEFT JOIN subscriber_lists ON (subscribers.id = subscriber_lists.subscriber_id) 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_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 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 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_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'); 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.batch_size', '1000'),
('app.max_send_errors', '1000'), ('app.max_send_errors', '1000'),
('app.notify_emails', '["admin1@mysite.com", "admin2@mysite.com"]'), ('app.notify_emails', '["admin1@mysite.com", "admin2@mysite.com"]'),
('privacy.allow_blacklist', 'true'), ('privacy.allow_blocklist', 'true'),
('privacy.allow_export', 'true'), ('privacy.allow_export', 'true'),
('privacy.allow_wipe', 'true'), ('privacy.allow_wipe', 'true'),
('privacy.exportable', '["profile", "subscriptions", "campaign_views", "link_clicks"]'), ('privacy.exportable', '["profile", "subscriptions", "campaign_views", "link_clicks"]'),

View File

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

View File

@ -7,9 +7,9 @@
<div> <div>
<input type="hidden" name="unsubscribe" value="true" /> <input type="hidden" name="unsubscribe" value="true" />
{{ if .Data.AllowBlacklist }} {{ if .Data.AllowBlocklist }}
<p> <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> </p>
{{ end }} {{ end }}

View File

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