Refactor `blacklist` to `blocklist`
This commit is contained in:
parent
2143def136
commit
8c0804ba9f
203
config-demo.toml
203
config-demo.toml
|
@ -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"
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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`")
|
||||
}
|
||||
|
||||
|
|
4
init.go
4
init.go
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -21,7 +21,7 @@ const (
|
|||
// Subscriber.
|
||||
SubscriberStatusEnabled = "enabled"
|
||||
SubscriberStatusDisabled = "disabled"
|
||||
SubscriberStatusBlackListed = "blacklisted"
|
||||
SubscriberStatusBlockListed = "blocklisted"
|
||||
|
||||
// Subscription.
|
||||
SubscriptionStatusUnconfirmed = "unconfirmed"
|
||||
|
|
14
public.go
14
public.go
|
@ -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", "",
|
||||
|
|
|
@ -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.
|
||||
|
|
32
queries.sql
32
queries.sql
|
@ -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)
|
||||
|
|
|
@ -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"]'),
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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 }}
|
||||
|
||||
|
|
|
@ -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 (
|
||||
|
|
Loading…
Reference in New Issue