Add ability for subscribers to manage preferences on the unsub form.

- Ability to change name.
- Ability to unsubscribe from individual lists.
- Toggle option to enable this in Admin Settings -> Privacy.

Closes #455.
This commit is contained in:
Kailash Nadh 2022-10-18 21:44:57 +05:30
parent 372a144322
commit 3b0083190e
38 changed files with 488 additions and 60 deletions

View file

@ -49,6 +49,14 @@ func initHTTPHandlers(e *echo.Echo, app *App) {
g = e.Group("", middleware.BasicAuth(basicAuth))
}
e.HTTPErrorHandler = func(err error, c echo.Context) {
// Generic, non-echo error. Log it.
if _, ok := err.(*echo.HTTPError); !ok {
app.log.Println(err.Error())
}
e.DefaultHTTPErrorHandler(err, c)
}
// Admin JS app views.
// /admin/static/* file server is registered in initHTTPServer().
e.GET("/", func(c echo.Context) error {
@ -163,7 +171,7 @@ func initHTTPHandlers(e *echo.Echo, app *App) {
e.POST("/subscription/form", handleSubscriptionForm)
e.GET("/subscription/:campUUID/:subUUID", noIndex(validateUUID(subscriberExists(handleSubscriptionPage),
"campUUID", "subUUID")))
e.POST("/subscription/:campUUID/:subUUID", validateUUID(subscriberExists(handleSubscriptionPage),
e.POST("/subscription/:campUUID/:subUUID", validateUUID(subscriberExists(handleSubscriptionPrefs),
"campUUID", "subUUID"))
e.GET("/subscription/optin/:subUUID", noIndex(validateUUID(subscriberExists(handleOptinPage), "subUUID")))
e.POST("/subscription/optin/:subUUID", validateUUID(subscriberExists(handleOptinPage), "subUUID"))

View file

@ -60,6 +60,7 @@ type constants struct {
DBBatchSize int `koanf:"batch_size"`
Privacy struct {
IndividualTracking bool `koanf:"individual_tracking"`
AllowPreferences bool `koanf:"allow_preferences"`
AllowBlocklist bool `koanf:"allow_blocklist"`
AllowExport bool `koanf:"allow_export"`
AllowWipe bool `koanf:"allow_wipe"`

View file

@ -48,10 +48,14 @@ type publicTpl struct {
type unsubTpl struct {
publicTpl
SubUUID string
AllowBlocklist bool
AllowExport bool
AllowWipe bool
Subscriber models.Subscriber
Subscriptions []models.Subscription
SubUUID string
AllowBlocklist bool
AllowExport bool
AllowWipe bool
AllowPreferences bool
ShowManage bool
}
type optinTpl struct {
@ -175,36 +179,149 @@ func handleViewCampaignMessage(c echo.Context) error {
// campaigns link to.
func handleSubscriptionPage(c echo.Context) error {
var (
app = c.Get("app").(*App)
campUUID = c.Param("campUUID")
subUUID = c.Param("subUUID")
unsub = c.Request().Method == http.MethodPost
blocklist, _ = strconv.ParseBool(c.FormValue("blocklist"))
out = unsubTpl{}
app = c.Get("app").(*App)
subUUID = c.Param("subUUID")
showManage, _ = strconv.ParseBool(c.FormValue("manage"))
out = unsubTpl{}
)
out.SubUUID = subUUID
out.Title = app.i18n.T("public.unsubscribeTitle")
out.AllowBlocklist = app.constants.Privacy.AllowBlocklist
out.AllowExport = app.constants.Privacy.AllowExport
out.AllowWipe = app.constants.Privacy.AllowWipe
out.AllowPreferences = app.constants.Privacy.AllowPreferences
// Unsubscribe.
if unsub {
// Is blocklisting allowed?
if !app.constants.Privacy.AllowBlocklist {
blocklist = false
if app.constants.Privacy.AllowPreferences {
out.ShowManage = showManage
}
// Get the subscriber's lists.
subs, err := app.core.GetSubscriptions(0, subUUID, false)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("public.errorFetchingLists"))
}
s, err := app.core.GetSubscriber(0, subUUID, "")
if err != nil {
return c.Render(http.StatusInternalServerError, tplMessage,
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("public.errorProcessingRequest")))
}
out.Subscriber = s
if s.Status == models.SubscriberStatusBlockListed {
return c.Render(http.StatusOK, tplMessage,
makeMsgTpl(app.i18n.T("public.noSubTitle"), "", app.i18n.Ts("public.blocklisted")))
}
// Filter out unrelated private lists.
if showManage {
out.Subscriptions = make([]models.Subscription, 0, len(subs))
for _, s := range subs {
if s.Type == models.ListTypePrivate {
if s.SubscriptionStatus.IsZero() {
continue
}
s.Name = app.i18n.T("public.subPrivateList")
}
out.Subscriptions = append(out.Subscriptions, s)
}
}
return c.Render(http.StatusOK, "subscription", out)
}
// handleSubscriptionPage renders the subscription management page and
// handles unsubscriptions. This is the view that {{ UnsubscribeURL }} in
// campaigns link to.
func handleSubscriptionPrefs(c echo.Context) error {
var (
app = c.Get("app").(*App)
campUUID = c.Param("campUUID")
subUUID = c.Param("subUUID")
req struct {
Name string `form:"name" json:"name"`
ListUUIDs []string `form:"l" json:"list_uuids"`
Blocklist bool `form:"blocklist" json:"blocklist"`
Manage bool `form:"manage" json:"manage"`
}
)
// Read the form.
if err := c.Bind(&req); err != nil {
return c.Render(http.StatusBadRequest, tplMessage,
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.T("globals.messages.invalidData")))
}
// Simple unsubscribe.
blocklist := app.constants.Privacy.AllowBlocklist && req.Blocklist
if !req.Manage || blocklist {
if err := app.core.UnsubscribeByCampaign(subUUID, campUUID, blocklist); err != nil {
return c.Render(http.StatusInternalServerError, tplMessage,
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("public.errorProcessingRequest")))
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.T("public.errorProcessingRequest")))
}
return c.Render(http.StatusOK, tplMessage,
makeMsgTpl(app.i18n.T("public.unsubbedTitle"), "", app.i18n.T("public.unsubbedInfo")))
}
return c.Render(http.StatusOK, "subscription", out)
// Is preference management enabled?
if !app.constants.Privacy.AllowPreferences {
return c.Render(http.StatusBadRequest, tplMessage,
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.T("public.invalidFeature")))
}
// Manage preferences.
req.Name = strings.TrimSpace(req.Name)
if req.Name == "" || len(req.Name) > 256 {
return c.Render(http.StatusBadRequest, tplMessage,
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.T("subscribers.invalidName")))
}
// Get the subscriber from the DB.
sub, err := app.core.GetSubscriber(0, subUUID, "")
if err != nil {
return c.Render(http.StatusInternalServerError, tplMessage,
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("globals.messages.pFound",
"name", app.i18n.T("globals.terms.subscriber"))))
}
sub.Name = req.Name
// Update name.
if _, err := app.core.UpdateSubscriber(sub.ID, sub); err != nil {
return c.Render(http.StatusInternalServerError, tplMessage,
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.T("public.errorProcessingRequest")))
}
// Get the subscriber's lists and whatever is not sent in the request (unchecked),
// unsubscribe them.
subs, err := app.core.GetSubscriptions(0, subUUID, false)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("public.errorFetchingLists"))
}
reqUUIDs := make(map[string]struct{})
for _, u := range req.ListUUIDs {
reqUUIDs[u] = struct{}{}
}
unsubUUIDs := make([]string, 0, len(req.ListUUIDs))
for _, s := range subs {
if _, ok := reqUUIDs[s.UUID]; !ok {
unsubUUIDs = append(unsubUUIDs, s.UUID)
}
}
// Unsubscribe from lists.
if err := app.core.UnsubscribeLists([]int{sub.ID}, nil, unsubUUIDs); err != nil {
return c.Render(http.StatusInternalServerError, tplMessage,
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.T("public.errorProcessingRequest")))
}
return c.Render(http.StatusOK, tplMessage,
makeMsgTpl(app.i18n.T("globals.messages.done"), "", app.i18n.T("public.prefsSaved")))
}
// handleOptinPage renders the double opt-in confirmation page that subscribers
@ -306,7 +423,6 @@ func handleSubscriptionForm(c echo.Context) error {
// If there's a nonce value, a bot could've filled the form.
if c.FormValue("nonce") != "" {
return echo.NewHTTPError(http.StatusBadGateway, app.i18n.T("public.invalidFeature"))
}
hasOptin, err := processSubForm(c)
@ -547,7 +663,7 @@ func processSubForm(c echo.Context) (bool, error) {
return false, err
}
if _, err := app.core.UpdateSubscriber(sub.ID, sub, nil, listUUIDs, false); err != nil {
if _, err := app.core.UpdateSubscriberWithLists(sub.ID, sub, nil, listUUIDs, false, false); err != nil {
return false, err
}

View file

@ -251,7 +251,7 @@ func handleUpdateSubscriber(c echo.Context) error {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidName"))
}
out, err := app.core.UpdateSubscriber(id, req.Subscriber, req.Lists, nil, req.PreconfirmSubs)
out, err := app.core.UpdateSubscriberWithLists(id, req.Subscriber, req.Lists, nil, req.PreconfirmSubs, true)
if err != nil {
return err
}
@ -364,7 +364,7 @@ func handleManageSubscriberLists(c echo.Context) error {
case "remove":
err = app.core.DeleteSubscriptions(subIDs, req.TargetListIDs)
case "unsubscribe":
err = app.core.UnsubscribeLists(subIDs, req.TargetListIDs)
err = app.core.UnsubscribeLists(subIDs, req.TargetListIDs, nil)
default:
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidAction"))
}

View file

@ -18,6 +18,12 @@
name="privacy.allow_blocklist" />
</b-field>
<b-field :label="$t('settings.privacy.allowPrefs')"
:message="$t('settings.privacy.allowPrefsHelp')">
<b-switch v-model="data['privacy.allow_preferences']"
name="privacy.allow_blocklist" />
</b-field>
<b-field :label="$t('settings.privacy.allowExport')"
:message="$t('settings.privacy.allowExportHelp')">
<b-switch v-model="data['privacy.allow_export']"

View file

@ -280,6 +280,7 @@
"menu.media": "Mèdia",
"menu.newCampaign": "Crea nova",
"menu.settings": "Configuració",
"public.blocklisted": "Permanently unsubscribed.",
"public.campaignNotFound": "No s'ha trobat el missatge de correu electrònic.",
"public.confirmOptinSubTitle": "Confirmació de la subscripció",
"public.confirmSub": "Confirma la subscripció",
@ -296,11 +297,14 @@
"public.errorTitle": "Error",
"public.invalidFeature": "Aquesta funció no està disponible.",
"public.invalidLink": "Enllaç no vàlid",
"public.managePrefs": "Manage preferences",
"public.managePrefsUnsub": "Uncheck lists to unsubscribe from them.",
"public.noListsAvailable": "No hi ha llistes disponibles per subscriure's.",
"public.noListsSelected": "No s'han seleccionat llistes vàlides per subscriure's.",
"public.noSubInfo": "No hi ha subscripcions per confirmar.",
"public.noSubTitle": "No hi ha subscripcions ",
"public.notFoundTitle": "No trobat",
"public.prefsSaved": "Your preferences have been saved.",
"public.privacyConfirmWipe": "Estàs segur que vols suprimir totes les dades de la teva subscripció de manera permanent?",
"public.privacyExport": "Exporta les teves dades",
"public.privacyExportHelp": "Se t'enviarà per correu electrònic una còpia de les teves dades.",
@ -445,6 +449,8 @@
"settings.privacy.allowBlocklistHelp": "Vols permetre als subscriptors donar-se de baixa de totes les llistes de correu i marcar-se com a llista bloquejada?",
"settings.privacy.allowExport": "Permet l'exportació",
"settings.privacy.allowExportHelp": "Vols permetre als subscriptors exportar les dades recollides sobre ells?",
"settings.privacy.allowPrefs": "Allow preference changes",
"settings.privacy.allowPrefsHelp": "Allow subscribers to change preferences such as their names and multiple list subscriptions.",
"settings.privacy.allowWipe": "Permet l'esborrat permanent",
"settings.privacy.allowWipeHelp": "Permet als subscriptors esborrar-se, incloses les seves subscripcions i totes les altres dades de la base de dades. Les visualitzacions de campanya i els clics als enllaços també s'eliminen mentre es mantenen les visualitzacions i els recomptes de clics (sense subscriptors associats a ells) de manera que les estadístiques i els indicadors no es veuran afectats.",
"settings.privacy.domainBlocklist": "Llista de dominis bloquejats",

View file

@ -281,6 +281,7 @@
"menu.media": "Médium",
"menu.newCampaign": "Vytvořit nový",
"menu.settings": "Nastavení",
"public.blocklisted": "Permanently unsubscribed.",
"public.campaignNotFound": "E-mailová zpráva nebyla nalezena.",
"public.confirmOptinSubTitle": "Potvrdit odběr",
"public.confirmSub": "Potvrdit odběr",
@ -297,11 +298,14 @@
"public.errorTitle": "Chyba",
"public.invalidFeature": "Tato funkce není k dispozici.",
"public.invalidLink": "Neplatný odkaz",
"public.managePrefs": "Manage preferences",
"public.managePrefsUnsub": "Uncheck lists to unsubscribe from them.",
"public.noListsAvailable": "Nejsou k dispozici žádné seznamy k odběru.",
"public.noListsSelected": "Nebyly vybrány žádné platné seznamy k odběru.",
"public.noSubInfo": "Nejsou zde žádné odběry k potvrzení.",
"public.noSubTitle": "Žádné odběry",
"public.notFoundTitle": "Nebyl nalezen",
"public.prefsSaved": "Your preferences have been saved.",
"public.privacyConfirmWipe": "Opravdu chcete trvale odstranit všechna data svých odběrů?",
"public.privacyExport": "Exportovat data",
"public.privacyExportHelp": "Kopie dat vám bude odeslána e-mailem.",
@ -446,6 +450,8 @@
"settings.privacy.allowBlocklistHelp": "Povolit odběratelům zrušit odběr ze všech seznamů adresářů a označit sebe jako blokované?",
"settings.privacy.allowExport": "Umožnit export",
"settings.privacy.allowExportHelp": "Umožnit odběratelům exportovat shromážděná data?",
"settings.privacy.allowPrefs": "Allow preference changes",
"settings.privacy.allowPrefsHelp": "Allow subscribers to change preferences such as their names and multiple list subscriptions.",
"settings.privacy.allowWipe": "Umožnit vymazání",
"settings.privacy.allowWipeHelp": "Umožnit odběratelům odstranit sebe včetně svých odběrů a všech ostatních dat z databáze. Pohledy na kampaně a klepnutí na odkazy se rovněž odeberou, zatímco pohledy a počty klepnutí se zachovají (aniž by měly přidruženého odběratele), takže statistiky a analýzy nebudou ovlivněny.",
"settings.privacy.domainBlocklist": "Seznam blokovaných domén",

View file

@ -281,6 +281,7 @@
"menu.media": "Medien",
"menu.newCampaign": "Neu Anlegen",
"menu.settings": "Einstellungen",
"public.blocklisted": "Permanently unsubscribed.",
"public.campaignNotFound": "Die E-Mail wurde nicht gefunden.",
"public.confirmOptinSubTitle": "Abonnement bestätigen",
"public.confirmSub": "Abonnement bestätigen",
@ -297,11 +298,14 @@
"public.errorTitle": "Fehler",
"public.invalidFeature": "Dieses Feature ist nicht verfügbar",
"public.invalidLink": "Ungültiger Link",
"public.managePrefs": "Manage preferences",
"public.managePrefsUnsub": "Uncheck lists to unsubscribe from them.",
"public.noListsAvailable": "Keine Listen zum Abonnieren verfügbar.",
"public.noListsSelected": "Keine Liste zum Abonnieren ausgewählt.",
"public.noSubInfo": "Es gibt keine zu bestätigenden Abonnements",
"public.noSubTitle": "Keine Abonnements",
"public.notFoundTitle": "Nicht gefunden",
"public.prefsSaved": "Your preferences have been saved.",
"public.privacyConfirmWipe": "Bist du sicher, dass du alle Abonnements und Daten dauerhaft löschen möchtest?",
"public.privacyExport": "Daten exportieren",
"public.privacyExportHelp": "Eine Kopie der gespeicherten Daten wird an deine E-Mail-Adresse versendet.",
@ -446,6 +450,8 @@
"settings.privacy.allowBlocklistHelp": "Erlaube es Abonnenten ihre E-Mail-Adresse dauerhaft zu sperren.",
"settings.privacy.allowExport": "Export aktivieren",
"settings.privacy.allowExportHelp": "Erlaube Abonnenten alle ihre Daten zu exportieren?",
"settings.privacy.allowPrefs": "Allow preference changes",
"settings.privacy.allowPrefsHelp": "Allow subscribers to change preferences such as their names and multiple list subscriptions.",
"settings.privacy.allowWipe": "Löschen aktivieren",
"settings.privacy.allowWipeHelp": "Erlaube Abonnenten alle Daten, welche über sie gespeichert sind zu löschen. Dies beinhaltet auch Klicks und Anzeigen, verändert allerdings nicht die Gesamtzahl. Statistiken bleiben auch unverändert.",
"settings.privacy.domainBlocklist": "Domain-Sperrliste",

View file

@ -280,6 +280,7 @@
"menu.media": "Media",
"menu.newCampaign": "Create new",
"menu.settings": "Settings",
"public.blocklisted": "Permanently unsubscribed.",
"public.campaignNotFound": "The e-mail message was not found.",
"public.confirmOptinSubTitle": "Confirm subscription",
"public.confirmSub": "Confirm subscription",
@ -296,17 +297,20 @@
"public.errorTitle": "Error",
"public.invalidFeature": "That feature is not available.",
"public.invalidLink": "Invalid link",
"public.managePrefs": "Manage preferences",
"public.managePrefsUnsub": "Uncheck lists to unsubscribe from them.",
"public.noListsAvailable": "No lists available to subscribe.",
"public.noListsSelected": "No valid lists selected to subscribe.",
"public.noSubInfo": "There are no subscriptions to confirm.",
"public.noSubTitle": "No subscriptions",
"public.notFoundTitle": "Not found",
"public.prefsSaved": "Your preferences have been saved.",
"public.privacyConfirmWipe": "Are you sure you want to delete all your subscription data permanently?",
"public.privacyExport": "Export your data",
"public.privacyExportHelp": "A copy of your data will be e-mailed to you.",
"public.privacyTitle": "Privacy and data",
"public.privacyWipe": "Wipe your data",
"public.privacyWipeHelp": "Delete all your subscriptions and related data from the database permanently.",
"public.privacyWipeHelp": "Delete all your subscriptions and related data permanently.",
"public.sub": "Subscribe",
"public.subConfirmed": "Subscribed successfully.",
"public.subConfirmedTitle": "Confirmed",
@ -316,7 +320,7 @@
"public.subPrivateList": "Private list",
"public.subTitle": "Subscribe",
"public.unsub": "Unsubscribe",
"public.unsubFull": "Also unsubscribe from all future e-mails.",
"public.unsubFull": "Unsubscribe from all future e-mails.",
"public.unsubHelp": "Do you want to unsubscribe from this mailing list?",
"public.unsubTitle": "Unsubscribe",
"public.unsubbedInfo": "You have unsubscribed successfully.",
@ -445,6 +449,8 @@
"settings.privacy.allowBlocklistHelp": "Allow subscribers to unsubscribe from all mailing lists and mark themselves as blocklisted?",
"settings.privacy.allowExport": "Allow exporting",
"settings.privacy.allowExportHelp": "Allow subscribers to export data collected on them?",
"settings.privacy.allowPrefs": "Allow preference changes",
"settings.privacy.allowPrefsHelp": "Allow subscribers to change preferences such as their names and multiple list subscriptions.",
"settings.privacy.allowWipe": "Allow wiping",
"settings.privacy.allowWipeHelp": "Allow subscribers to delete themselves including their subscriptions and all other data from the database. 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 are not affected.",
"settings.privacy.domainBlocklist": "Domain blocklist",

View file

@ -281,6 +281,7 @@
"menu.media": "Multimedia",
"menu.newCampaign": "Crear nueva",
"menu.settings": "Configuraciones",
"public.blocklisted": "Permanently unsubscribed.",
"public.campaignNotFound": "El mensaje de correo electrónico no fue encontrado",
"public.confirmOptinSubTitle": "Confirmar subscripción",
"public.confirmSub": "Confirmar subscripción",
@ -297,11 +298,14 @@
"public.errorTitle": "Error",
"public.invalidFeature": "Esta función no está disponible",
"public.invalidLink": "Enlace inválido",
"public.managePrefs": "Manage preferences",
"public.managePrefsUnsub": "Uncheck lists to unsubscribe from them.",
"public.noListsAvailable": "No hay listas disponibles para subscribirse",
"public.noListsSelected": "No se seleccionaron listas válidas a las cuales subscribirse",
"public.noSubInfo": "No hay subscripciones para confirmar.",
"public.noSubTitle": "No hay subscripciones",
"public.notFoundTitle": "No encontrado",
"public.prefsSaved": "Your preferences have been saved.",
"public.privacyConfirmWipe": "¿Está seguro que quiere eliminar todos sus datos de subscripción permanentemente?",
"public.privacyExport": "Exportar sus datos",
"public.privacyExportHelp": "Una copia de sus datos le será enviada por correo electrónico.",
@ -446,6 +450,8 @@
"settings.privacy.allowBlocklistHelp": "¿Permitir a los subscriptores des-subscribirse de todas las listas de correo y marcarlas como \"blocklisted\"?",
"settings.privacy.allowExport": "Permitir exportar",
"settings.privacy.allowExportHelp": "¿Permitir a los subscriptores exportar los datos recabados de ellos?",
"settings.privacy.allowPrefs": "Allow preference changes",
"settings.privacy.allowPrefsHelp": "Allow subscribers to change preferences such as their names and multiple list subscriptions.",
"settings.privacy.allowWipe": "Permitir limpieza de datos",
"settings.privacy.allowWipeHelp": "Permitir a los subscriptores eliminarse incluyendo sus subscripciones y todos sus datos de la base de datos. Las vistas de las campañas y los vínculos cliqueados también son eliminados mientras que las vistas y el conteo de clics se mantienen. (sin subscriptores asociados a ellos) de manera que las estadísticas y el análisis no se vea afectado.",
"settings.privacy.domainBlocklist": "Listado de dominios bloqueados",

View file

@ -281,6 +281,7 @@
"menu.media": "Media",
"menu.newCampaign": "Create new",
"menu.settings": "Settings",
"public.blocklisted": "Permanently unsubscribed.",
"public.campaignNotFound": "Sähköpostiviestiä ei löytynyt",
"public.confirmOptinSubTitle": "Vahvista uutiskirjeen tilaus",
"public.confirmSub": "Vahvista tilaus",
@ -297,11 +298,14 @@
"public.errorTitle": "Tapahtui virhe",
"public.invalidFeature": "Tämä ominaisuus ei ole saatavilla.",
"public.invalidLink": "Virheellinen linkki",
"public.managePrefs": "Manage preferences",
"public.managePrefsUnsub": "Uncheck lists to unsubscribe from them.",
"public.noListsAvailable": "Postituslistoja ei ole saatavilla.",
"public.noListsSelected": "Et ole valinnut uutiskirjelistaa.",
"public.noSubInfo": "Sinulla ei ole vahvistettavia uutiskirjetilauksia.",
"public.noSubTitle": "Ei vahvistettavia uutiskirjetilauksia",
"public.notFoundTitle": "Ei löytynyt",
"public.prefsSaved": "Your preferences have been saved.",
"public.privacyConfirmWipe": "Oletko varma, että haluat poistaa kaikki uutiskirjetietosi pysyvästi?",
"public.privacyExport": "Vie tietosi",
"public.privacyExportHelp": "Kopio tiedoistasi lähetetään sinulle sähköpostitse.",
@ -446,6 +450,8 @@
"settings.privacy.allowBlocklistHelp": "Allow subscribers to unsubscribe from all mailing lists and mark themselves as blocklisted?",
"settings.privacy.allowExport": "Allow exporting",
"settings.privacy.allowExportHelp": "Allow subscribers to export data collected on them?",
"settings.privacy.allowPrefs": "Allow preference changes",
"settings.privacy.allowPrefsHelp": "Allow subscribers to change preferences such as their names and multiple list subscriptions.",
"settings.privacy.allowWipe": "Allow wiping",
"settings.privacy.allowWipeHelp": "Allow subscribers to delete themselves including their subscriptions and all other data from the database. 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 are not affected.",
"settings.privacy.domainBlocklist": "Domain blocklist",

View file

@ -281,6 +281,7 @@
"menu.media": "Fichiers",
"menu.newCampaign": "Nouvelle campagne",
"menu.settings": "Paramètres",
"public.blocklisted": "Permanently unsubscribed.",
"public.campaignNotFound": "La liste de diffusion est introuvable.",
"public.confirmOptinSubTitle": "Confirmer votre abonnement",
"public.confirmSub": "Confirmer votre abonnement",
@ -297,11 +298,14 @@
"public.errorTitle": "Erreur",
"public.invalidFeature": "Cette fonctionnalité n'est pas disponible.",
"public.invalidLink": "Lien invalide",
"public.managePrefs": "Manage preferences",
"public.managePrefsUnsub": "Uncheck lists to unsubscribe from them.",
"public.noListsAvailable": "Aucune liste n'est disponible pour vous abonner.",
"public.noListsSelected": "Aucune liste valide sélectionnée pour s'abonner.",
"public.noSubInfo": "Il n'y a pas d'abonnement à confirmer.",
"public.noSubTitle": "Aucun abonnement",
"public.notFoundTitle": "Non trouvé",
"public.prefsSaved": "Your preferences have been saved.",
"public.privacyConfirmWipe": "Voulez-vous vraiment supprimer définitivement toutes vos données d'abonnement ?",
"public.privacyExport": "Exportez vos données personnelles",
"public.privacyExportHelp": "Une copie de vos données vous sera envoyée par e-mail.",
@ -446,6 +450,8 @@
"settings.privacy.allowBlocklistHelp": "Autoriser les abonné·es à se désabonner de toutes les listes de diffusion et à se marquer comme étant bloqué·es ?",
"settings.privacy.allowExport": "Autoriser l'export des données par les abonné·es",
"settings.privacy.allowExportHelp": "Autoriser les abonné·es à exporter les données collectées à leur sujet ?",
"settings.privacy.allowPrefs": "Allow preference changes",
"settings.privacy.allowPrefsHelp": "Allow subscribers to change preferences such as their names and multiple list subscriptions.",
"settings.privacy.allowWipe": "Autoriser la suppression des données par les abonné·es",
"settings.privacy.allowWipeHelp": "Autoriser les abonné·es à supprimer leurs abonnements et toutes les autres données de la base de données. Les vues de campagne et les clics sur les liens sont également supprimés, tandis que le compteur de vues et de nombre de clics globaux restent inchangés (aucun·e abonné·e ne leur est associé) afin que les statistiques et les analyses ne soient pas affectées.",
"settings.privacy.domainBlocklist": "Domaine bloqué",

View file

@ -281,6 +281,7 @@
"menu.media": "Média",
"menu.newCampaign": "Új készítése",
"menu.settings": "Beállítások",
"public.blocklisted": "Permanently unsubscribed.",
"public.campaignNotFound": "Az e-mail üzenet nem található.",
"public.confirmOptinSubTitle": "Feliratkozás megerősítése",
"public.confirmSub": "Feliratkozás megerősítése",
@ -297,11 +298,14 @@
"public.errorTitle": "Hiba",
"public.invalidFeature": "Ez a funkció nem elérhető.",
"public.invalidLink": "A link érvénytelen",
"public.managePrefs": "Manage preferences",
"public.managePrefsUnsub": "Uncheck lists to unsubscribe from them.",
"public.noListsAvailable": "Nincsenek feliratkozható listák.",
"public.noListsSelected": "Nincsenek érvényes listák az feliratkozókhoz.",
"public.noSubInfo": "Nincsenek megerősítendő feliratkozások .",
"public.noSubTitle": "Nincs feliratkozó",
"public.notFoundTitle": "Nem található",
"public.prefsSaved": "Your preferences have been saved.",
"public.privacyConfirmWipe": "Biztos benne, hogy végleg törölni szeretné az összes feliratkozási adatot?",
"public.privacyExport": "Exportálja adatait",
"public.privacyExportHelp": "Az adatok másolatát e-mailben küldjük el.",
@ -446,6 +450,8 @@
"settings.privacy.allowBlocklistHelp": "Engedélyezi a feliratkozóknak, hogy leiratkozzanak az összes levelezőlistáról, és tiltólistán jelöljék meg magukat?",
"settings.privacy.allowExport": "Exportálás engedélyezése",
"settings.privacy.allowExportHelp": "Engedélyezze az előfizetőknek a róluk gyűjtött adatok exportálását?",
"settings.privacy.allowPrefs": "Allow preference changes",
"settings.privacy.allowPrefsHelp": "Allow subscribers to change preferences such as their names and multiple list subscriptions.",
"settings.privacy.allowWipe": "Törlés engedélyezése",
"settings.privacy.allowWipeHelp": "Lehetővé teszi az feliratkozóknak, hogy töröljék magukat az adatbázisból, beleértve az feliratkozásaikat és az összes többi adatot. A kampánynézeteket és a linkekre leadott kattintásokat szintén eltávolítjuk, miközben a megtekintések és kattintások száma megmarad (nem társított feliratkozókkal), így a statisztikák és az elemzések nem érintik.",
"settings.privacy.domainBlocklist": "Domain tiltólista",

View file

@ -281,6 +281,7 @@
"menu.media": "Media",
"menu.newCampaign": "Creare nuovo",
"menu.settings": "Impostazioni",
"public.blocklisted": "Permanently unsubscribed.",
"public.campaignNotFound": "Newsletter impossibile da trovare.",
"public.confirmOptinSubTitle": "Confermare l'iscrizione",
"public.confirmSub": "Confermare l'iscrizione",
@ -297,11 +298,14 @@
"public.errorTitle": "Errore",
"public.invalidFeature": "Questa funzione non è disponibile.",
"public.invalidLink": "Link non valido",
"public.managePrefs": "Manage preferences",
"public.managePrefsUnsub": "Uncheck lists to unsubscribe from them.",
"public.noListsAvailable": "Nessuna lista disponibile per l'iscrizione.",
"public.noListsSelected": "Nessuna lista valida selezionata per l'iscrizione.",
"public.noSubInfo": "Non ci sono iscrizioni da confermare.",
"public.noSubTitle": "Nessuna iscrizione",
"public.notFoundTitle": "Non trovato",
"public.prefsSaved": "Your preferences have been saved.",
"public.privacyConfirmWipe": "Sei sicuro di voler cancellare in modo permanente tutti i tuoi dati d'iscrizione?",
"public.privacyExport": "Esporta i tuoi dati",
"public.privacyExportHelp": "Una copia dei tuoi dati ti sarà trasmessa via mail.",
@ -446,6 +450,8 @@
"settings.privacy.allowBlocklistHelp": "Autorizza gli iscritti a cancellare l'iscrizione da tutte le newsletters e a segnalarsi come bloccati?",
"settings.privacy.allowExport": "Autorizza l'esportazione",
"settings.privacy.allowExportHelp": "Autorizzi gli iscritti a esportare i dati raccolti su di loro?",
"settings.privacy.allowPrefs": "Allow preference changes",
"settings.privacy.allowPrefsHelp": "Allow subscribers to change preferences such as their names and multiple list subscriptions.",
"settings.privacy.allowWipe": "Autorizza la cancellazione",
"settings.privacy.allowWipeHelp": "Autorizza gli iscritti a cancellare le loro iscrizioni e tutti gli altri dati dal database. Le visualizzazioni della campagna e i clic sui link verranno anch'essi cancellati, mentre i contatori globali delle visualizzazioni e del numero di clic restano invariati (nessun iscritto vi è associato) in modo che le statistiche non siano compromesse.",
"settings.privacy.domainBlocklist": "Domain blocklist",

View file

@ -281,6 +281,7 @@
"menu.media": "メディア",
"menu.newCampaign": "新規作成",
"menu.settings": "設定",
"public.blocklisted": "Permanently unsubscribed.",
"public.campaignNotFound": "メールのメッセージが見つかりませんでした。",
"public.confirmOptinSubTitle": "サブスクリプション確認",
"public.confirmSub": "サブスクリプション確認",
@ -297,11 +298,14 @@
"public.errorTitle": "エラー",
"public.invalidFeature": "その機能は使用できません。",
"public.invalidLink": "無効なリンク",
"public.managePrefs": "Manage preferences",
"public.managePrefsUnsub": "Uncheck lists to unsubscribe from them.",
"public.noListsAvailable": "加入できるリストはありません。",
"public.noListsSelected": "加入に有効なリストが選択されてません。",
"public.noSubInfo": "確認できるサブスクリプションはありません。",
"public.noSubTitle": "サブスクリプションはありません。",
"public.notFoundTitle": "見つかりません",
"public.prefsSaved": "Your preferences have been saved.",
"public.privacyConfirmWipe": "全ての加入データが永久に削除されますがよろしいでしょうか?",
"public.privacyExport": "データをエクスポート",
"public.privacyExportHelp": "データのコピーがメールにて送られます。",
@ -446,6 +450,8 @@
"settings.privacy.allowBlocklistHelp": "加入者自身が全てのメーリングリストの登録を解除し、ブロックリストに追加することを許可しますか?",
"settings.privacy.allowExport": "エクスポートを許可する",
"settings.privacy.allowExportHelp": "加入者が自分自身について収集されたデータをエクスポートすることを許可しますか?",
"settings.privacy.allowPrefs": "Allow preference changes",
"settings.privacy.allowPrefsHelp": "Allow subscribers to change preferences such as their names and multiple list subscriptions.",
"settings.privacy.allowWipe": "ワイプを許可する",
"settings.privacy.allowWipeHelp": "加入者サブスクリプション含むすべてのデータを含めて、データベースから自身を削除することを許可する。キャンペーンビューとリンククリックも削除されるが、統計と分析に影響が出ないよう、ビューとクリックカウントは残る (加入者を持たない状態)。",
"settings.privacy.domainBlocklist": "ドメインブロックリスト",

View file

@ -281,6 +281,7 @@
"menu.media": "മീഡിയ",
"menu.newCampaign": "പുതിയത് തുടങ്ങുക",
"menu.settings": "ക്രമീകരണങ്ങൾ",
"public.blocklisted": "Permanently unsubscribed.",
"public.campaignNotFound": "ഇ-മെയിൽ കണ്ടെത്താനായില്ല.",
"public.confirmOptinSubTitle": "വരിക്കാരനാകുന്നത് സ്ഥിരീകരിക്കുക",
"public.confirmSub": "വരിക്കാരനാകുന്നത് സ്ഥിരീകരിക്കുക",
@ -297,11 +298,14 @@
"public.errorTitle": "എറർ",
"public.invalidFeature": "ഈ ഫീച്ചർ ലഭ്യമല്ല",
"public.invalidLink": "കണ്ണി അസാധുവാണ്",
"public.managePrefs": "Manage preferences",
"public.managePrefsUnsub": "Uncheck lists to unsubscribe from them.",
"public.noListsAvailable": "No lists available to subscribe.",
"public.noListsSelected": "No valid lists selected to subscribe.",
"public.noSubInfo": "സ്ഥിരീകരിക്കാനായി വരിക്കാരനാകാനുള്ള അഭ്യർത്ഥനകളൊന്നുമില്ല",
"public.noSubTitle": "വരിക്കാരാരുമില്ല",
"public.notFoundTitle": "കണ്ടെത്തിയില്ല",
"public.prefsSaved": "Your preferences have been saved.",
"public.privacyConfirmWipe": "വരിക്കാരനായിരിക്കുന്നതിന്റെ എല്ലാ വിവരങ്ങളും എന്നത്തേയ്ക്കുമായി നീക്കം ചെയ്യണമെന്ന് നിങ്ങളുൾക്കുറപ്പാണോ?",
"public.privacyExport": "നിങ്ങളുടെ വിവരങ്ങൾ എക്സ്പോർട്ട് ചെയ്യുക",
"public.privacyExportHelp": "വിവരങ്ങളുടെ ഒരു പകർപ്പ് നിങ്ങൾക്ക് ഇ-മെയിലായി അയച്ചു തരുന്നതാണ്.",
@ -446,6 +450,8 @@
"settings.privacy.allowBlocklistHelp": "എല്ലാ മെയിലിങ് ലിസ്റ്റുകളിൽ നിന്നും വരിക്കാരല്ലാതാകാനും തടയുന്ന പട്ടികയിൽപ്പെടുത്താനും ഉപഭോക്താക്കളെ അനുവദിക്കണോ?",
"settings.privacy.allowExport": "എക്സ്പോർട്ട് ചെയ്യാനനുവദിക്കുക",
"settings.privacy.allowExportHelp": "ഉപഭോക്കാക്കളിൽ നിന്നും ശേഖരിച്ച വിവരങ്ങൾ എക്സ്പോർട്ട് ചെയ്യാൻ അനുവദിക്കണോ?",
"settings.privacy.allowPrefs": "Allow preference changes",
"settings.privacy.allowPrefsHelp": "Allow subscribers to change preferences such as their names and multiple list subscriptions.",
"settings.privacy.allowWipe": "വിവരങ്ങൾ എന്നന്നേയ്ക്കുമായി ഇല്ലാതാക്കുന്നത് അനുവദിക്കുക",
"settings.privacy.allowWipeHelp": "ഉപഭോക്താക്കളെ അവരുടെ വരിക്കാരായിട്ടുള്ള ലിസ്റ്റുകളും മറ്റു വിവരങ്ങളും ഡാറ്റാബേസിൽ നിന്നും ഇല്ലാതാക്കാൻ അനുവദിക്കുക.ക്യാമ്പെയ്ൻ കാഴ്ചകളും കണ്ണികളിന്മേലുള്ള ക്ലിക്കുകളുടെ വിവരങ്ങളും ഇല്ലാതാക്കുമെങ്കിലും കാഴ്ചകളുടെയും കണ്ണിയിലുള്ള ക്ലിക്കുകളുടെ (ഉപഭോക്തൃ വിവരങ്ങളില്ലാതെ) എണ്ണവും നിലനിൽക്കും. അതിനാൽ സ്ഥിതിവിവരക്കണക്കുകളെയും വിശകലനങ്ങളെയും ബാധിക്കില്ല.",
"settings.privacy.domainBlocklist": "Domain blocklist",

View file

@ -281,6 +281,7 @@
"menu.media": "Media",
"menu.newCampaign": "Nieuwe aanmaken",
"menu.settings": "Instellingen",
"public.blocklisted": "Permanently unsubscribed.",
"public.campaignNotFound": "Het e-mailbericht werd niet gevonden.",
"public.confirmOptinSubTitle": "Bevestig inschrijving",
"public.confirmSub": "Bevestig inschrijving",
@ -297,11 +298,14 @@
"public.errorTitle": "Fout",
"public.invalidFeature": "Deze functie is niet beschikbaar",
"public.invalidLink": "Ongeldige link",
"public.managePrefs": "Manage preferences",
"public.managePrefsUnsub": "Uncheck lists to unsubscribe from them.",
"public.noListsAvailable": "Geen lijsten beschikbaar om in te schrijven",
"public.noListsSelected": "Geen geldige lijsten geselecteerd om op in te schrijven",
"public.noSubInfo": "Er zijn geen inschrijvingen om te bevestigen.",
"public.noSubTitle": "Geen inschrijvingen",
"public.notFoundTitle": "Niet gevonden",
"public.prefsSaved": "Your preferences have been saved.",
"public.privacyConfirmWipe": "Ben je zeker dat je all je inschrijvingsdata permanent wil verwijderen?",
"public.privacyExport": "Exporteer je data",
"public.privacyExportHelp": "Een kopie van je data zal naar je ge-e-maild worden.",
@ -446,6 +450,8 @@
"settings.privacy.allowBlocklistHelp": "Abonnees toelaten zich voor alle mailinglijsten uit te schrijven en zichzelf te markeren als geblokkeerd?",
"settings.privacy.allowExport": "Exporteren toelaten",
"settings.privacy.allowExportHelp": "Abonnees toelaten om data die over hen is verzameld te exporteren?",
"settings.privacy.allowPrefs": "Allow preference changes",
"settings.privacy.allowPrefsHelp": "Allow subscribers to change preferences such as their names and multiple list subscriptions.",
"settings.privacy.allowWipe": "Data wipe toestaan",
"settings.privacy.allowWipeHelp": "Abonnees toelaten zichzelf, al hun inschrijvingen en alle andere data over hun te verwijderen uit de database. Views en klikken op links van campagnes worden verwijderd, maar het aantal views en kliks blijft hetzelfde zodat statistieken niet veranderen.",
"settings.privacy.domainBlocklist": "Domein blocklist",

View file

@ -281,6 +281,7 @@
"menu.media": "Media",
"menu.newCampaign": "Utwórz nową",
"menu.settings": "Ustawienia",
"public.blocklisted": "Permanently unsubscribed.",
"public.campaignNotFound": "Wiadomość email nie została znaleziona.",
"public.confirmOptinSubTitle": "Potwierdź subskrypcję",
"public.confirmSub": "Potwierdź subskrypcję",
@ -297,11 +298,14 @@
"public.errorTitle": "Błąd",
"public.invalidFeature": "Ta funkcjonalność jest niedostępna.",
"public.invalidLink": "Nieprawidłowy liny.",
"public.managePrefs": "Manage preferences",
"public.managePrefsUnsub": "Uncheck lists to unsubscribe from them.",
"public.noListsAvailable": "Brak list do subkskrybowania.",
"public.noListsSelected": "Brak prawidłowych list wybranych do subskrybowania.",
"public.noSubInfo": "Brak subskrypcji do potwierdzenia.",
"public.noSubTitle": "Brak subskrypcji ",
"public.notFoundTitle": "Nie znaleziono",
"public.prefsSaved": "Your preferences have been saved.",
"public.privacyConfirmWipe": "Czy jesteś pewny(a), że chcesz usunąć wszystkie swoje dane?",
"public.privacyExport": "Eksportuj swoje dane",
"public.privacyExportHelp": "Kopia twoich danych zostanie przesłana do ciebie mailem.",
@ -446,6 +450,8 @@
"settings.privacy.allowBlocklistHelp": "Czy zezwolić subskrybentom na wypisywanie się z wszystkich list mailowych i oznaczenie siebie jako zablokowanych?",
"settings.privacy.allowExport": "Zezwól na eksportowanie danych",
"settings.privacy.allowExportHelp": "Czy zezwolić subskrybentom na eksportowanie danych zebranych o nich?",
"settings.privacy.allowPrefs": "Allow preference changes",
"settings.privacy.allowPrefsHelp": "Allow subscribers to change preferences such as their names and multiple list subscriptions.",
"settings.privacy.allowWipe": "Zezwól na czyszczenie danych",
"settings.privacy.allowWipeHelp": "Czy zezwolić subskrybentom na usuwanie ich samych razem z wszystkimi ich danymi? Wyświetlenia i liczba kliknięć zostaną zachowane, ale zostaną z nich usunięte informacje kto wykonał tę akcję.",
"settings.privacy.domainBlocklist": "Lista zablokowanych domen",

View file

@ -281,6 +281,7 @@
"menu.media": "Mídia",
"menu.newCampaign": "Criar nova",
"menu.settings": "Configurações",
"public.blocklisted": "Permanently unsubscribed.",
"public.campaignNotFound": "A mensagem do e-mail não foi encontrada.",
"public.confirmOptinSubTitle": "Confirmar a assinatura",
"public.confirmSub": "Confirmar a assinatura",
@ -297,11 +298,14 @@
"public.errorTitle": "Erro",
"public.invalidFeature": "Este recurso não está disponível.",
"public.invalidLink": "Link inválido",
"public.managePrefs": "Manage preferences",
"public.managePrefsUnsub": "Uncheck lists to unsubscribe from them.",
"public.noListsAvailable": "Não há listas disponíveis para se inscrever.",
"public.noListsSelected": "Não foram selecionadas listas válidas para inscrever.",
"public.noSubInfo": "Não há nenhuma inscrição para confirmar.",
"public.noSubTitle": "Sem inscrições",
"public.notFoundTitle": "Não Encontrado",
"public.prefsSaved": "Your preferences have been saved.",
"public.privacyConfirmWipe": "Você tem certeza que deseja excluir todos os seus dados de assinatura permanentemente?",
"public.privacyExport": "Exportar seus dados",
"public.privacyExportHelp": "Uma cópia de seus dados será enviado por e-mail para você.",
@ -446,6 +450,8 @@
"settings.privacy.allowBlocklistHelp": "Permitir que os inscritos cancelem a inscrição de todas as listas de e-mails e se marquem como bloqueados?",
"settings.privacy.allowExport": "Permitir exportação",
"settings.privacy.allowExportHelp": "Permitir que os assinantes exportem os dados coletados neles?",
"settings.privacy.allowPrefs": "Allow preference changes",
"settings.privacy.allowPrefsHelp": "Allow subscribers to change preferences such as their names and multiple list subscriptions.",
"settings.privacy.allowWipe": "Permitir limpeza",
"settings.privacy.allowWipeHelp": "Permitir que os assinantes se excluam incluindo suas inscrições e todos os outros dados da base de dados. Visualizações da campanha e cliques de links também são removidos enquanto o total de visualizações e cliques permanecem (com nenhum inscrito associado a eles) para que as estatísticas e análises não sejam afetadas.",
"settings.privacy.domainBlocklist": "Blocklist de domínios",

View file

@ -281,6 +281,7 @@
"menu.media": "Mídia",
"menu.newCampaign": "Criar nova",
"menu.settings": "Definições",
"public.blocklisted": "Permanently unsubscribed.",
"public.campaignNotFound": "A mensagem de email não foi encontrada.",
"public.confirmOptinSubTitle": "Confirmar subscrição",
"public.confirmSub": "Confirmar subscrição",
@ -297,11 +298,14 @@
"public.errorTitle": "Erro",
"public.invalidFeature": "That feature is not available",
"public.invalidLink": "Link inválido",
"public.managePrefs": "Manage preferences",
"public.managePrefsUnsub": "Uncheck lists to unsubscribe from them.",
"public.noListsAvailable": "Não existem listas disponíveis para subscrever.",
"public.noListsSelected": "Não foram selecionadas listas válidas para subscrever.",
"public.noSubInfo": "There are no subscriptions to confirm",
"public.noSubTitle": "Sem subscrições",
"public.notFoundTitle": "Não encontrado",
"public.prefsSaved": "Your preferences have been saved.",
"public.privacyConfirmWipe": "Tem a certeza que deseja apagar permanentemente todos os seus dados de subscrições?",
"public.privacyExport": "Exportar os seus dados",
"public.privacyExportHelp": "Uma cópia dos seus dados ser-lhe-á enviada por email.",
@ -446,6 +450,8 @@
"settings.privacy.allowBlocklistHelp": "Permitir ao subscritores cancelar a subscrição de todas as listas de emails e marcar-se como bloqueados?",
"settings.privacy.allowExport": "Permitir exportação",
"settings.privacy.allowExportHelp": "Permitir aos subscritores exportar os dados coletados neles mesmos?",
"settings.privacy.allowPrefs": "Allow preference changes",
"settings.privacy.allowPrefsHelp": "Allow subscribers to change preferences such as their names and multiple list subscriptions.",
"settings.privacy.allowWipe": "Permitir eliminação de dados",
"settings.privacy.allowWipeHelp": "Permitir aos subscritores eliminar todos os seus dados, incluindo as suas subscrições, da base de dados. Visualizações de campanhas e cliques em links também são removidos enquanto visualizações e contagem de clicks permanecem (sem nenhum subscritor associado) para que as estatísticas não sejam afetadas.",
"settings.privacy.domainBlocklist": "Domain blocklist",

View file

@ -281,6 +281,7 @@
"menu.media": "Media",
"menu.newCampaign": "Creaza nou",
"menu.settings": "Setări",
"public.blocklisted": "Permanently unsubscribed.",
"public.campaignNotFound": "Mesajul emailului nu a fost găsit.",
"public.confirmOptinSubTitle": "Confirmă abonarea",
"public.confirmSub": "Confirmă abonarea",
@ -297,11 +298,14 @@
"public.errorTitle": "Eroare",
"public.invalidFeature": "Această functie nu este disponibilă",
"public.invalidLink": "Link invalid",
"public.managePrefs": "Manage preferences",
"public.managePrefsUnsub": "Uncheck lists to unsubscribe from them.",
"public.noListsAvailable": "Nu există liste disponibile pentru abonare.",
"public.noListsSelected": "Nu există liste valide pentru abonare.",
"public.noSubInfo": "Nu există abonamente de confirmat",
"public.noSubTitle": "Fără abonamente",
"public.notFoundTitle": "Nu a fost găsit",
"public.prefsSaved": "Your preferences have been saved.",
"public.privacyConfirmWipe": "Sigur vrei să ștergi definitiv toate datele legate de abonament?",
"public.privacyExport": "Exportă datele tale",
"public.privacyExportHelp": "O copie a datelor iti vor fi trimise prin email.",
@ -446,6 +450,8 @@
"settings.privacy.allowBlocklistHelp": "Permite abonaților să se dezaboneze de la toate listele de e-mail și să se marcheze ca listă de blocuri?",
"settings.privacy.allowExport": "Permite exportul",
"settings.privacy.allowExportHelp": "Permite abonaților să exporte datele colectate pe aceștia?",
"settings.privacy.allowPrefs": "Allow preference changes",
"settings.privacy.allowPrefsHelp": "Allow subscribers to change preferences such as their names and multiple list subscriptions.",
"settings.privacy.allowWipe": "Permite ștergerea",
"settings.privacy.allowWipeHelp": "Permite abonaților să se șteargă, inclusiv abonamentele lor și toate celelalte date din baza de date. Vizualizările campaniei și clicurile pe linkuri sunt, de asemenea, eliminate, în timp ce numărul de vizualizări și clicuri rămâne (fără niciun abonat asociat acestora), astfel încât statisticile și analizele să nu fie afectate.",
"settings.privacy.domainBlocklist": "Domain blocklist",

View file

@ -281,6 +281,7 @@
"menu.media": "Медиа",
"menu.newCampaign": "Создать новую",
"menu.settings": "Параметры",
"public.blocklisted": "Permanently unsubscribed.",
"public.campaignNotFound": "Письмо не было найдено.",
"public.confirmOptinSubTitle": "Подтверждение подписки",
"public.confirmSub": "Подтвердить подписку",
@ -297,11 +298,14 @@
"public.errorTitle": "Ошибка",
"public.invalidFeature": "Эта функция недоступна.",
"public.invalidLink": "Неверная ссылка",
"public.managePrefs": "Manage preferences",
"public.managePrefsUnsub": "Uncheck lists to unsubscribe from them.",
"public.noListsAvailable": "Нет доступных списков для подписки.",
"public.noListsSelected": "Для подписки не выбраны действительные списки.",
"public.noSubInfo": "Нет подписок для подтверждения.",
"public.noSubTitle": "Нет подписок",
"public.notFoundTitle": "Не найдено",
"public.prefsSaved": "Your preferences have been saved.",
"public.privacyConfirmWipe": "Вы уверены, что хотите навсегда удалить все данные о подписке?",
"public.privacyExport": "Экспортировать Ваши данные",
"public.privacyExportHelp": "Копия Ваших данных будет отправлена Вам письмом",
@ -446,6 +450,8 @@
"settings.privacy.allowBlocklistHelp": "Позволить подписчикам отписываться от всех списков рассылки и помечать себя заблокированными?",
"settings.privacy.allowExport": "Разрешить экспорт",
"settings.privacy.allowExportHelp": "Разрешить подписчикам экспортировать собранные на них данные?",
"settings.privacy.allowPrefs": "Allow preference changes",
"settings.privacy.allowPrefsHelp": "Allow subscribers to change preferences such as their names and multiple list subscriptions.",
"settings.privacy.allowWipe": "Разрешить удаление",
"settings.privacy.allowWipeHelp": "Разрешить подписчикам удалять себя (включая их подписки и иные данные) из базы данных. Просмотры кампании и клики по ссылкам также удаляются, в то время как просмотры и счетчики кликов остаются (без привязанного к ним подписчика), так что это не влияет на статистику и аналитику.",
"settings.privacy.domainBlocklist": "Блокирующий список доменов",

View file

@ -281,6 +281,7 @@
"menu.media": "Medya",
"menu.newCampaign": "Yeni oluştur",
"menu.settings": "Ayarlar",
"public.blocklisted": "Permanently unsubscribed.",
"public.campaignNotFound": "E-posta mesajı bulunamadı.",
"public.confirmOptinSubTitle": "Üyeliği doğrula",
"public.confirmSub": "Üyeliği doğrula",
@ -297,11 +298,14 @@
"public.errorTitle": "Hata",
"public.invalidFeature": "Bu özellik geçerli değil.",
"public.invalidLink": "Geçersiz link",
"public.managePrefs": "Manage preferences",
"public.managePrefsUnsub": "Uncheck lists to unsubscribe from them.",
"public.noListsAvailable": "Eklenecek liste yok.",
"public.noListsSelected": "Bağlanılacak geçerli bir liste seçilmedi.",
"public.noSubInfo": "Doğrulanacak üyelik bulunmuyor.",
"public.noSubTitle": "Üyelik yok",
"public.notFoundTitle": "Bulunamadı",
"public.prefsSaved": "Your preferences have been saved.",
"public.privacyConfirmWipe": "Tüm üyelik verilerinizin kalıcı olarak silinmesini istediğinize eminmisiniz?",
"public.privacyExport": "Verinizi dışarı aktarın",
"public.privacyExportHelp": "Size ait verilerin bir kopyası size e-posta ile gönderilecektir.",
@ -446,6 +450,8 @@
"settings.privacy.allowBlocklistHelp": "Abonelerin tüm posta listelerinden çıkmalarına ve kendilerini engellenmiş olarak işaretlemelerine izin verin?",
"settings.privacy.allowExport": "Dışa aktarım için izin ver",
"settings.privacy.allowExportHelp": "Abonelerin üzerlerinde toplanan verileri dışa aktarmalarına izin verin?",
"settings.privacy.allowPrefs": "Allow preference changes",
"settings.privacy.allowPrefsHelp": "Allow subscribers to change preferences such as their names and multiple list subscriptions.",
"settings.privacy.allowWipe": "Silmek için izin ver",
"settings.privacy.allowWipeHelp": "Abonelerin, abonelikleri ve veritabanındaki diğer tüm veriler dahil olmak üzere kendilerini silmesine izin verin. Kampanya görüntülemeleri ve bağlantı tıklamaları da, görünümler ve tıklama sayıları kalır (bunlarla ilişkilendirilmiş abone olmadan), böylece istatistikler ve analizler etkilenmez.",
"settings.privacy.domainBlocklist": "Domain blocklist",

View file

@ -281,6 +281,7 @@
"menu.media": "Dữ liệu truyền thông",
"menu.newCampaign": "Tạo mới",
"menu.settings": "Cài đặt",
"public.blocklisted": "Permanently unsubscribed.",
"public.campaignNotFound": "Tin nhắn e-mail không được tìm thấy.",
"public.confirmOptinSubTitle": "Xác nhận đăng ký",
"public.confirmSub": "Xác nhận đăng ký",
@ -297,11 +298,14 @@
"public.errorTitle": "Lỗi",
"public.invalidFeature": "Tính năng đó không khả dụng.",
"public.invalidLink": "Link không khả dụng",
"public.managePrefs": "Manage preferences",
"public.managePrefsUnsub": "Uncheck lists to unsubscribe from them.",
"public.noListsAvailable": "Không có danh sách nào để đăng ký.",
"public.noListsSelected": "Không có danh sách hợp lệ nào được chọn để đăng ký.",
"public.noSubInfo": "Không có đăng ký để xác nhận.",
"public.noSubTitle": "Không có đăng ký",
"public.notFoundTitle": "Không tìm thấy",
"public.prefsSaved": "Your preferences have been saved.",
"public.privacyConfirmWipe": "Bạn có chắc chắn muốn xóa vĩnh viễn tất cả dữ liệu đăng ký của mình không?",
"public.privacyExport": "Xuất dữ liệu của bạn",
"public.privacyExportHelp": "Một bản sao dữ liệu của bạn sẽ được gửi qua email cho bạn.",
@ -446,6 +450,8 @@
"settings.privacy.allowBlocklistHelp": "Cho phép người đăng ký hủy đăng ký khỏi tất cả các danh sách gửi thư và tự đánh dấu là đã bị chặn?",
"settings.privacy.allowExport": "Cho phép xuất",
"settings.privacy.allowExportHelp": "Cho phép người đăng ký xuất dữ liệu được thu thập trên chúng?",
"settings.privacy.allowPrefs": "Allow preference changes",
"settings.privacy.allowPrefsHelp": "Allow subscribers to change preferences such as their names and multiple list subscriptions.",
"settings.privacy.allowWipe": "Cho phép xóa",
"settings.privacy.allowWipeHelp": "Cho phép người đăng ký tự xóa bao gồm đăng ký của họ và tất cả dữ liệu khác khỏi cơ sở dữ liệu. Lượt xem chiến dịch và lượt nhấp vào liên kết cũng bị xóa trong khi lượt xem và số lượt nhấp vẫn còn (không có người đăng ký nào được liên kết với chúng) để số liệu thống kê và phân tích không bị ảnh hưởng.",
"settings.privacy.domainBlocklist": "Danh sách chặn tên miền",

View file

@ -281,6 +281,7 @@
"menu.media": "媒体",
"menu.newCampaign": "创建新的",
"menu.settings": "设置",
"public.blocklisted": "Permanently unsubscribed.",
"public.campaignNotFound": "未找到电子邮件。",
"public.confirmOptinSubTitle": "确认订阅",
"public.confirmSub": "确认订阅",
@ -297,11 +298,14 @@
"public.errorTitle": "错误",
"public.invalidFeature": "该功能不可用。",
"public.invalidLink": "无效的链接",
"public.managePrefs": "Manage preferences",
"public.managePrefsUnsub": "Uncheck lists to unsubscribe from them.",
"public.noListsAvailable": "没有可供订阅的列表。",
"public.noListsSelected": "没有可以选择订阅的有效列表",
"public.noSubInfo": "没有要确认的订阅。",
"public.noSubTitle": "没有订阅",
"public.notFoundTitle": "未找到",
"public.prefsSaved": "Your preferences have been saved.",
"public.privacyConfirmWipe": "您确定要永久删除所有订阅数据吗?",
"public.privacyExport": "导出您的数据",
"public.privacyExportHelp": "您的数据副本将通过电子邮件发送给您。",
@ -446,6 +450,8 @@
"settings.privacy.allowBlocklistHelp": "允许订阅者从所有邮件列表中退订并将自己标记为已列入黑名单?",
"settings.privacy.allowExport": "允许导出",
"settings.privacy.allowExportHelp": "允许订阅者导出收集到的数据?",
"settings.privacy.allowPrefs": "Allow preference changes",
"settings.privacy.allowPrefsHelp": "Allow subscribers to change preferences such as their names and multiple list subscriptions.",
"settings.privacy.allowWipe": "允许擦除",
"settings.privacy.allowWipeHelp": "允许订阅者删除自己,包括他们的订阅和数据库中的所有其他数据。广告系列浏览量和链接点击量也会被删除,而浏览量和点击量仍然存在(没有与之关联的订阅者),因此统计数据和分析不会受到影响。",
"settings.privacy.domainBlocklist": "域阻止列表",

View file

@ -155,6 +155,7 @@
"globals.messages.created": "“{name}”已創建",
"globals.messages.deleted": "“{name}”已刪除",
"globals.messages.deletedCount": "{name} ({num}) 個已刪除",
"globals.messages.done": "Done",
"globals.messages.emptyState": "這裡沒有什麼",
"globals.messages.errorCreating": "創建{name} 時出錯:{error}",
"globals.messages.errorDeleting": "刪除{name} 時出錯:{error}",
@ -183,6 +184,7 @@
"globals.months.8": "八月",
"globals.months.9": "九月",
"globals.states.off": "關閉",
"globals.terms.all": "All",
"globals.terms.analytics": "統計",
"globals.terms.bounce": "反彈| 多個反彈",
"globals.terms.bounces": "反彈",
@ -202,6 +204,7 @@
"globals.terms.settings": "設置",
"globals.terms.subscriber": "訂閱者| 多個訂閱者",
"globals.terms.subscribers": "訂閱者",
"globals.terms.subscriptions": "Subscription | Subscriptions",
"globals.terms.tag": "標籤| 多個標籤",
"globals.terms.tags": "標籤",
"globals.terms.template": "模板| 多個模板",
@ -252,6 +255,11 @@
"lists.types.private": "私人的",
"lists.types.public": "公開",
"logs.title": "日誌",
"maintenance.help": "Some actions may take a while to complete depending on the amount of data.",
"maintenance.maintenance.unconfirmedOptins": "Unconfirmed opt-in subscriptions",
"maintenance.olderThan": "Older than",
"maintenance.title": "Maintenance",
"maintenance.unconfirmedSubs": "Unconfirmed subscriptions older than {name} days.",
"media.errorReadingFile": "讀取文件時出錯:{error}",
"media.errorResizing": "調整圖像大小時出錯:{error}",
"media.errorSavingThumbnail": "保存縮略圖時出錯:{error}",
@ -269,9 +277,11 @@
"menu.forms": "表格",
"menu.import": "導入",
"menu.logs": "日誌",
"menu.maintenance": "Maintenance",
"menu.media": "媒體",
"menu.newCampaign": "創建新的",
"menu.settings": "設置",
"public.blocklisted": "Permanently unsubscribed.",
"public.campaignNotFound": "未找到電子郵件。",
"public.confirmOptinSubTitle": "確認訂閱",
"public.confirmSub": "確認訂閱",
@ -288,11 +298,14 @@
"public.errorTitle": "錯誤",
"public.invalidFeature": "該功能不可用。",
"public.invalidLink": "無效的鏈接",
"public.managePrefs": "Manage preferences",
"public.managePrefsUnsub": "Uncheck lists to unsubscribe from them.",
"public.noListsAvailable": "沒有可供訂閱的列表。",
"public.noListsSelected": "沒有可以選擇訂閱的有效列表",
"public.noSubInfo": "沒有要確認的訂閱。",
"public.noSubTitle": "沒有訂閱",
"public.notFoundTitle": "未找到",
"public.prefsSaved": "Your preferences have been saved.",
"public.privacyConfirmWipe": "您確定要永久刪除所有訂閱數據嗎?",
"public.privacyExport": "導出您的數據",
"public.privacyExportHelp": "您的數據副本將通過電子郵件發送給您。",
@ -437,6 +450,8 @@
"settings.privacy.allowBlocklistHelp": "允許訂閱者從所有郵件列表中退訂並將自己標記為已列入黑名單?",
"settings.privacy.allowExport": "允許導出",
"settings.privacy.allowExportHelp": "允許訂閱者導出收集到的數據?",
"settings.privacy.allowPrefs": "Allow preference changes",
"settings.privacy.allowPrefsHelp": "Allow subscribers to change preferences such as their names and multiple list subscriptions.",
"settings.privacy.allowWipe": "允許擦除",
"settings.privacy.allowWipeHelp": "允許訂閱者刪除自己,包括他們的訂閱和數據庫中的所有其他數據。廣告系列瀏覽量和鏈接點擊量也會被刪除,而瀏覽量和點擊量仍然存在(沒有與之關聯的訂閱者),因此統計數據和分析不會受到影響。",
"settings.privacy.domainBlocklist": "域阻止列表",

View file

@ -302,12 +302,7 @@ func (c *Core) InsertSubscriber(sub models.Subscriber, listIDs []int, listUUIDs
}
// UpdateSubscriber updates a subscriber's properties.
func (c *Core) UpdateSubscriber(id int, sub models.Subscriber, listIDs []int, listUUIDs []string, preconfirm bool) (models.Subscriber, error) {
subStatus := models.SubscriptionStatusUnconfirmed
if preconfirm {
subStatus = models.SubscriptionStatusConfirmed
}
func (c *Core) UpdateSubscriber(id int, sub models.Subscriber) (models.Subscriber, error) {
// Format raw JSON attributes.
attribs := []byte("{}")
if len(sub.Attribs) > 0 {
@ -325,14 +320,55 @@ func (c *Core) UpdateSubscriber(id int, sub models.Subscriber, listIDs []int, li
strings.TrimSpace(sub.Name),
sub.Status,
json.RawMessage(attribs),
pq.Array(listIDs),
pq.Array(listUUIDs),
subStatus)
)
if err != nil {
c.log.Printf("error updating subscriber: %v", err)
return models.Subscriber{}, echo.NewHTTPError(http.StatusInternalServerError,
c.i18n.Ts("globals.messages.errorUpdating",
"name", "{globals.terms.subscriber}", "error", pqErrMsg(err)))
c.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.subscriber}", "error", pqErrMsg(err)))
}
out, err := c.GetSubscriber(sub.ID, "", sub.Email)
if err != nil {
return models.Subscriber{}, err
}
return out, nil
}
// UpdateSubscriberWithLists updates a subscriber's properties.
// If deleteLists is set to true, all existing subscriptions are deleted and only
// the ones provided are added or retained.
func (c *Core) UpdateSubscriberWithLists(id int, sub models.Subscriber, listIDs []int, listUUIDs []string, preconfirm, deleteLists bool) (models.Subscriber, error) {
subStatus := models.SubscriptionStatusUnconfirmed
if preconfirm {
subStatus = models.SubscriptionStatusConfirmed
}
// Format raw JSON attributes.
attribs := []byte("{}")
if len(sub.Attribs) > 0 {
if b, err := json.Marshal(sub.Attribs); err != nil {
return models.Subscriber{}, echo.NewHTTPError(http.StatusInternalServerError,
c.i18n.Ts("globals.messages.errorUpdating",
"name", "{globals.terms.subscriber}", "error", err.Error()))
} else {
attribs = b
}
}
_, err := c.q.UpdateSubscriberWithLists.Exec(id,
sub.Email,
strings.TrimSpace(sub.Name),
sub.Status,
json.RawMessage(attribs),
pq.Array(listIDs),
pq.Array(listUUIDs),
subStatus,
deleteLists)
if err != nil {
c.log.Printf("error updating subscriber: %v", err)
return models.Subscriber{}, echo.NewHTTPError(http.StatusInternalServerError,
c.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.subscriber}", "error", pqErrMsg(err)))
}
out, err := c.GetSubscriber(sub.ID, "", sub.Email)

View file

@ -4,10 +4,24 @@ import (
"net/http"
"time"
"github.com/knadh/listmonk/models"
"github.com/labstack/echo/v4"
"github.com/lib/pq"
)
// GetSubscriptions retrieves the subscriptions for a subscriber.
func (c *Core) GetSubscriptions(subID int, subUUID string, allLists bool) ([]models.Subscription, error) {
var out []models.Subscription
err := c.q.GetSubscriptions.Select(&out, subID, subUUID, allLists)
if err != nil {
c.log.Printf("error getting subscriptions: %v", err)
return nil, echo.NewHTTPError(http.StatusInternalServerError,
c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.subscribers}", "error", err.Error()))
}
return out, err
}
// AddSubscriptions adds list subscriptions to subscribers.
func (c *Core) AddSubscriptions(subIDs, listIDs []int, status string) error {
if _, err := c.q.AddSubscribersToLists.Exec(pq.Array(subIDs), pq.Array(listIDs), status); err != nil {
@ -66,8 +80,8 @@ func (c *Core) DeleteSubscriptionsByQuery(query string, sourceListIDs, targetLis
}
// UnsubscribeLists sets list subscriptions to 'unsubscribed'.
func (c *Core) UnsubscribeLists(subIDs, listIDs []int) error {
if _, err := c.q.UnsubscribeSubscribersFromLists.Exec(pq.Array(subIDs), pq.Array(listIDs)); err != nil {
func (c *Core) UnsubscribeLists(subIDs, listIDs []int, listUUIDs []string) error {
if _, err := c.q.UnsubscribeSubscribersFromLists.Exec(pq.Array(subIDs), pq.Array(listIDs), pq.StringArray(listUUIDs)); err != nil {
c.log.Printf("error unsubscribing from lists: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError,
c.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.subscribers}", "error", err.Error()))

View file

@ -451,6 +451,9 @@ func (m *Manager) TemplateFuncs(c *models.Campaign) template.FuncMap {
"UnsubscribeURL": func(msg *CampaignMessage) string {
return msg.unsubURL
},
"ManageURL": func(msg *CampaignMessage) string {
return msg.unsubURL + "?manage=true"
},
"OptinURL": func(msg *CampaignMessage) string {
// Add list IDs.
// TODO: Show private lists list on optin e-mail

View file

@ -12,5 +12,14 @@ func V2_3_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
return err
}
// Insert appearance related settings.
if _, err := db.Exec(`
INSERT INTO settings (key, value) VALUES
('privacy.allow_preferences', 'false')
ON CONFLICT DO NOTHING;
`); err != nil {
return err
}
return nil
}

View file

@ -116,7 +116,7 @@ var regTplFuncs = []regTplFunc{
},
{
regExp: regexp.MustCompile(`{{(\s+)?(TrackView|UnsubscribeURL|OptinURL|MessageURL)(\s+)?}}`),
regExp: regexp.MustCompile(`{{(\s+)?(TrackView|UnsubscribeURL|ManageURL|OptinURL|MessageURL)(\s+)?}}`),
replace: `{{ $2 . }}`,
},
}
@ -169,6 +169,13 @@ type subLists struct {
Lists types.JSONText `db:"lists"`
}
// Subscription represents a list attached to a subscriber.
type Subscription struct {
List
SubscriptionStatus null.String `db:"subscription_status" json:"subscription_status"`
SubscriptionCreatedAt null.String `db:"subscription_created_at" json:"subscription_created_at"`
}
// SubscriberExportProfile represents a subscriber's collated data in JSON for export.
type SubscriberExportProfile struct {
Email string `db:"email" json:"-"`

View file

@ -20,8 +20,10 @@ type Queries struct {
GetSubscriber *sqlx.Stmt `query:"get-subscriber"`
GetSubscribersByEmails *sqlx.Stmt `query:"get-subscribers-by-emails"`
GetSubscriberLists *sqlx.Stmt `query:"get-subscriber-lists"`
GetSubscriptions *sqlx.Stmt `query:"get-subscriptions"`
GetSubscriberListsLazy *sqlx.Stmt `query:"get-subscriber-lists-lazy"`
UpdateSubscriber *sqlx.Stmt `query:"update-subscriber"`
UpdateSubscriberWithLists *sqlx.Stmt `query:"update-subscriber-with-lists"`
BlocklistSubscribers *sqlx.Stmt `query:"blocklist-subscribers"`
AddSubscribersToLists *sqlx.Stmt `query:"add-subscribers-to-lists"`
DeleteSubscriptions *sqlx.Stmt `query:"delete-subscriptions"`

View file

@ -24,6 +24,7 @@ type Settings struct {
PrivacyIndividualTracking bool `json:"privacy.individual_tracking"`
PrivacyUnsubHeader bool `json:"privacy.unsubscribe_header"`
PrivacyAllowBlocklist bool `json:"privacy.allow_blocklist"`
PrivacyAllowPreferences bool `json:"privacy.allow_preferences"`
PrivacyAllowExport bool `json:"privacy.allow_export"`
PrivacyAllowWipe bool `json:"privacy.allow_wipe"`
PrivacyExportable []string `json:"privacy.exportable"`

View file

@ -25,7 +25,7 @@ SELECT * FROM lists
WHEN CARDINALITY($4::UUID[]) > 0 THEN uuid = ANY($4::UUID[])
ELSE TRUE
END)
AND (CASE WHEN $5 != '' THEN subscriber_lists.status = $5::subscription_status END)
AND (CASE WHEN $5 != '' THEN subscriber_lists.status = $5::subscription_status ELSE TRUE END)
AND (CASE WHEN $6 != '' THEN lists.optin = $6::list_optin ELSE TRUE END)
ORDER BY id;
@ -51,6 +51,19 @@ SELECT id as subscriber_id,
LEFT JOIN subs AS s ON (s.subscriber_id = id)
ORDER BY ARRAY_POSITION($1, id);
-- name: get-subscriptions
-- Retrieves all lists a subscriber is attached to.
-- if $3 is set to true, all lists are fetched including the subscriber's subscriptions.
-- subscription_status, and subscription_created_at are null in that case.
WITH sub AS (
SELECT id FROM subscribers WHERE CASE WHEN $1 > 0 THEN id = $1 ELSE uuid = $2 END
)
SELECT lists.*, subscriber_lists.status as subscription_status, subscriber_lists.created_at as subscription_created_at
FROM lists LEFT JOIN subscriber_lists
ON (subscriber_lists.list_id = lists.id AND subscriber_lists.subscriber_id = (SELECT id FROM sub))
WHERE CASE WHEN $3 = TRUE THEN TRUE ELSE subscriber_lists.status IS NOT NULL END
ORDER BY subscriber_lists.status;
-- name: insert-subscriber
WITH sub AS (
INSERT INTO subscribers (uuid, email, name, status, attribs)
@ -115,6 +128,15 @@ UPDATE subscriber_lists SET status='unsubscribed', updated_at=NOW()
WHERE subscriber_id = (SELECT id FROM sub);
-- name: update-subscriber
UPDATE subscribers SET
email=(CASE WHEN $2 != '' THEN $2 ELSE email END),
name=(CASE WHEN $3 != '' THEN $3 ELSE name END),
status=(CASE WHEN $4 != '' THEN $4::subscriber_status ELSE status END),
attribs=(CASE WHEN $5 != '' THEN $5::JSONB ELSE attribs END),
updated_at=NOW()
WHERE id = $1;
-- name: update-subscriber-with-lists
-- Updates a subscriber's data, and given a list of list_ids, inserts subscriptions
-- for them while deleting existing subscriptions not in the list.
WITH s AS (
@ -126,13 +148,13 @@ WITH s AS (
updated_at=NOW()
WHERE id = $1 RETURNING id
),
d AS (
DELETE FROM subscriber_lists WHERE subscriber_id = $1 AND list_id != ALL($6)
),
listIDs AS (
SELECT id FROM lists WHERE
(CASE WHEN CARDINALITY($6::INT[]) > 0 THEN id=ANY($6)
ELSE uuid=ANY($7::UUID[]) END)
),
d AS (
DELETE FROM subscriber_lists WHERE $9 = TRUE AND subscriber_id = $1 AND list_id != ALL(SELECT id FROM listIDs)
)
INSERT INTO subscriber_lists (subscriber_id, list_id, status)
VALUES(
@ -182,8 +204,14 @@ UPDATE subscriber_lists SET status='confirmed', updated_at=NOW()
WHERE subscriber_id = (SELECT id FROM subID) AND list_id = ANY(SELECT id FROM listIDs);
-- name: unsubscribe-subscribers-from-lists
WITH listIDs AS (
SELECT ARRAY(
SELECT id FROM lists WHERE
(CASE WHEN CARDINALITY($2::INT[]) > 0 THEN id=ANY($2) ELSE uuid=ANY($3::UUID[]) END)
) id
)
UPDATE subscriber_lists SET status='unsubscribed', updated_at=NOW()
WHERE (subscriber_id, list_id) = ANY(SELECT a, b FROM UNNEST($1::INT[]) a, UNNEST($2::INT[]) b);
WHERE (subscriber_id, list_id) = ANY(SELECT a, b FROM UNNEST($1::INT[]) a, UNNEST((SELECT id FROM listIDs)) b);
-- name: unsubscribe-by-campaign
-- Unsubscribes a subscriber given a campaign UUID (from all the lists in the campaign) and the subscriber UUID.

View file

@ -197,6 +197,7 @@ INSERT INTO settings (key, value) VALUES
('privacy.allow_blocklist', 'true'),
('privacy.allow_export', 'true'),
('privacy.allow_wipe', 'true'),
('privacy.allow_preferences', 'true'),
('privacy.exportable', '["profile", "subscriptions", "campaign_views", "link_clicks"]'),
('privacy.domain_blocklist', '[]'),
('upload.provider', '"filesystem"'),

View file

@ -50,6 +50,10 @@ input:focus::placeholder {
color: transparent;
}
input[disabled] {
opacity: 0.5;
}
.center {
text-align: center;
}
@ -111,8 +115,7 @@ input:focus::placeholder {
.row {
margin-bottom: 20px;
}
.form .lists {
margin-top: 45px;
.lists {
list-style-type: none;
padding: 0;
}

View file

@ -15,6 +15,7 @@
<label>{{ L.T "public.subName" }}</label>
<input name="name" type="text" placeholder="{{ L.T "public.subName" }}" >
</p>
<br />
<ul class="lists">
<h2>{{ L.T "globals.terms.lists" }}</h2>
{{ range $i, $l := .Data.Lists }}

View file

@ -1,22 +1,63 @@
{{ define "subscription" }}
{{ template "header" .}}
<section class="section">
<h2>{{ L.T "public.unsubTitle" }}</h2>
<p>{{ L.T "public.unsubHelp" }}</p>
<form method="post">
<div>
{{ if .Data.AllowBlocklist }}
<p>
<input id="privacy-blocklist" type="checkbox" name="blocklist" value="true" />
<label for="privacy-blocklist">{{ L.T "public.unsubFull" }}</label>
</p>
{{ end }}
{{ if not .Data.ShowManage }}
<h2>{{ L.T "public.unsubTitle" }}</h2>
<form method="post">
<div>
{{ if .Data.AllowBlocklist }}
<p>{{ L.T "public.unsubHelp" }}</p>
<p>
<input id="privacy-blocklist" type="checkbox" name="blocklist" value="true" />
<label for="privacy-blocklist">{{ L.T "public.unsubFull" }}</label>
</p>
{{ end }}
<p>
<button type="submit" class="button" id="btn-unsub">{{ L.T "public.unsub" }}</button>
</p>
</div>
</form>
<p>
<button type="submit" class="button" id="btn-unsub">{{ L.T "public.unsub" }}</button>
</p>
{{ if .Data.AllowPreferences }}
<a href="?manage=true">Manage preferences</a>
{{ end }}
</div>
</form>
{{ else }}
<form method="post">
<div>
<input type="hidden" name="manage" value="true" />
<h2>{{ L.T "public.managePrefs" }}</h2>
<label>{{ L.T "globals.fields.name" }}</label>
<input type="text" name="name" value="{{ .Data.Subscriber.Name }}" maxlength="256" required />
{{ if .Data.Subscriptions }}
<p><label>{{ L.T "public.managePrefsUnsub" }}</label></p>
<ul class="lists">
{{ range $i, $l := .Data.Subscriptions }}
{{ if ne $l.SubscriptionStatus.Value "unsubscribed" }}
<li>
<input id="l-{{ $l.UUID}}" type="checkbox" name="l" value="{{ $l.UUID }}" checked />
<label for="l-{{ $l.UUID}}">{{ $l.Name }}</label>
</li>
{{ end }}
{{ end }}
</ul>
{{ end }}
{{ if .Data.AllowBlocklist }}
<p>
<input id="privacy-blocklist" type="checkbox" name="blocklist" value="true" onchange="unsubAll(event)" />
<label for="privacy-blocklist">{{ L.T "public.unsubFull" }}</label>
</p>
{{ end }}
<p>
<button type="submit" class="button" id="btn-unsub">{{ L.T "globals.buttons.save" }}</button>
</p>
</div>
</form>
{{ end }}
</section>
{{ if or .Data.AllowExport .Data.AllowWipe }}
@ -58,6 +99,22 @@
}
return false;
}
function unsubAll(e) {
if (e.target.checked) {
document.querySelector("input[name=name]").disabled = "disabled";
} else {
document.querySelector("input[name=name]").removeAttribute("disabled");
}
document.querySelectorAll('input[type=checkbox][name=l]').forEach(function(l) {
if (e.target.checked) {
l.disabled = "disabled";
} else {
l.removeAttribute("disabled");
}
});
}
</script>
{{ end }}