diff --git a/cmd/init.go b/cmd/init.go
index 0cb4e14..99b7929 100644
--- a/cmd/init.go
+++ b/cmd/init.go
@@ -24,6 +24,7 @@ import (
"github.com/knadh/koanf/providers/posflag"
"github.com/knadh/listmonk/internal/bounce"
"github.com/knadh/listmonk/internal/bounce/mailbox"
+ "github.com/knadh/listmonk/internal/captcha"
"github.com/knadh/listmonk/internal/i18n"
"github.com/knadh/listmonk/internal/manager"
"github.com/knadh/listmonk/internal/media"
@@ -69,6 +70,11 @@ type constants struct {
Exportable map[string]bool `koanf:"-"`
DomainBlocklist map[string]bool `koanf:"-"`
} `koanf:"privacy"`
+ Security struct {
+ EnableCaptcha bool `koanf:"enable_captcha"`
+ CaptchaKey string `koanf:"captcha_key"`
+ CaptchaSecret string `koanf:"captcha_secret"`
+ } `koanf:"security"`
AdminUsername []byte `koanf:"admin_username"`
AdminPassword []byte `koanf:"admin_password"`
@@ -351,6 +357,9 @@ func initConstants() *constants {
if err := ko.Unmarshal("privacy", &c.Privacy); err != nil {
lo.Fatalf("error loading app.privacy config: %v", err)
}
+ if err := ko.Unmarshal("security", &c.Security); err != nil {
+ lo.Fatalf("error loading app.security config: %v", err)
+ }
if err := ko.UnmarshalWithConf("appearance", &c.Appearance, koanf.UnmarshalConf{FlatPaths: true}); err != nil {
lo.Fatalf("error loading app.appearance config: %v", err)
}
@@ -735,6 +744,12 @@ func initHTTPServer(app *App) *echo.Echo {
return srv
}
+func initCaptcha() *captcha.Captcha {
+ return captcha.New(captcha.Opt{
+ CaptchaSecret: ko.String("security.captcha_secret"),
+ })
+}
+
func awaitReload(sigChan chan os.Signal, closerWait chan bool, closer func()) chan bool {
// The blocking signal handler that main() waits on.
out := make(chan bool)
diff --git a/cmd/main.go b/cmd/main.go
index 801a195..e6b2930 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -17,6 +17,7 @@ import (
"github.com/knadh/koanf/providers/env"
"github.com/knadh/listmonk/internal/bounce"
"github.com/knadh/listmonk/internal/buflog"
+ "github.com/knadh/listmonk/internal/captcha"
"github.com/knadh/listmonk/internal/core"
"github.com/knadh/listmonk/internal/i18n"
"github.com/knadh/listmonk/internal/manager"
@@ -47,6 +48,7 @@ type App struct {
i18n *i18n.I18n
bounce *bounce.Manager
paginator *paginator.Paginator
+ captcha *captcha.Captcha
notifTpls *notifTpls
log *log.Logger
bufLog *buflog.BufLog
@@ -168,6 +170,7 @@ func main() {
messengers: make(map[string]messenger.Messenger),
log: lo,
bufLog: bufLog,
+ captcha: initCaptcha(),
paginator: paginator.New(paginator.Opt{
DefaultPerPage: 20,
diff --git a/cmd/public.go b/cmd/public.go
index 3931a7b..9c15917 100644
--- a/cmd/public.go
+++ b/cmd/public.go
@@ -79,7 +79,8 @@ type msgTpl struct {
type subFormTpl struct {
publicTpl
- Lists []models.List
+ Lists []models.List
+ CaptchaKey string
}
var (
@@ -418,6 +419,10 @@ func handleSubscriptionFormPage(c echo.Context) error {
out.Title = app.i18n.T("public.sub")
out.Lists = lists
+ if app.constants.Security.EnableCaptcha {
+ out.CaptchaKey = app.constants.Security.CaptchaKey
+ }
+
return c.Render(http.StatusOK, "subscription-form", out)
}
@@ -433,6 +438,19 @@ func handleSubscriptionForm(c echo.Context) error {
return echo.NewHTTPError(http.StatusBadGateway, app.i18n.T("public.invalidFeature"))
}
+ // Process CAPTCHA.
+ if app.constants.Security.EnableCaptcha {
+ err, ok := app.captcha.Verify(c.FormValue("h-captcha-response"))
+ if err != nil {
+ app.log.Printf("Captcha request failed: %v", err)
+ }
+
+ if !ok {
+ return c.Render(http.StatusBadRequest, tplMessage,
+ makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.T("public.invalidCaptcha")))
+ }
+ }
+
hasOptin, err := processSubForm(c)
if err != nil {
e, ok := err.(*echo.HTTPError)
diff --git a/cmd/settings.go b/cmd/settings.go
index 5f86a29..06435f3 100644
--- a/cmd/settings.go
+++ b/cmd/settings.go
@@ -44,6 +44,7 @@ func handleGetSettings(c echo.Context) error {
}
s.UploadS3AwsSecretAccessKey = ""
s.SendgridKey = ""
+ s.SecurityCaptchaSecret = ""
return c.JSON(http.StatusOK, okResp{s})
}
@@ -158,6 +159,9 @@ func handleUpdateSettings(c echo.Context) error {
if set.SendgridKey == "" {
set.SendgridKey = cur.SendgridKey
}
+ if set.SecurityCaptchaSecret == "" {
+ set.SecurityCaptchaSecret = cur.SecurityCaptchaSecret
+ }
// Domain blocklist.
doms := make([]string, 0)
diff --git a/cmd/upgrade.go b/cmd/upgrade.go
index a65bd0b..48f2bd3 100644
--- a/cmd/upgrade.go
+++ b/cmd/upgrade.go
@@ -34,6 +34,7 @@ var migList = []migFunc{
{"v2.1.0", migrations.V2_1_0},
{"v2.2.0", migrations.V2_2_0},
{"v2.3.0", migrations.V2_3_0},
+ {"v2.4.0", migrations.V2_4_0},
}
// upgrade upgrades the database to the current version by running SQL migration files
diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue
index b395a5f..8dad6d9 100644
--- a/frontend/src/views/Settings.vue
+++ b/frontend/src/views/Settings.vue
@@ -34,6 +34,10 @@
+
+
+
+
@@ -66,6 +70,7 @@ import { mapState } from 'vuex';
import GeneralSettings from './settings/general.vue';
import PerformanceSettings from './settings/performance.vue';
import PrivacySettings from './settings/privacy.vue';
+import SecuritySettings from './settings/security.vue';
import MediaSettings from './settings/media.vue';
import SmtpSettings from './settings/smtp.vue';
import BounceSettings from './settings/bounces.vue';
@@ -79,6 +84,7 @@ export default Vue.extend({
GeneralSettings,
PerformanceSettings,
PrivacySettings,
+ SecuritySettings,
MediaSettings,
SmtpSettings,
BounceSettings,
@@ -136,6 +142,10 @@ export default Vue.extend({
form['bounce.sendgrid_key'] = '';
}
+ if (form['security.captcha_secret'] === dummyPassword) {
+ form['security.captcha_secret'] = '';
+ }
+
for (let i = 0; i < form.messengers.length; i += 1) {
// If it's the dummy UI password placeholder, ignore it.
if (form.messengers[i].password === dummyPassword) {
@@ -203,6 +213,7 @@ export default Vue.extend({
d['upload.s3.aws_secret_access_key'] = dummyPassword;
}
d['bounce.sendgrid_key'] = dummyPassword;
+ d['security.captcha_secret'] = dummyPassword;
// Domain blocklist array to multi-line string.
d['privacy.domain_blocklist'] = d['privacy.domain_blocklist'].join('\n');
diff --git a/frontend/src/views/settings/security.vue b/frontend/src/views/settings/security.vue
new file mode 100644
index 0000000..0faf837
--- /dev/null
+++ b/frontend/src/views/settings/security.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/i18n/ca.json b/i18n/ca.json
index 2031da0..98b2fbd 100644
--- a/i18n/ca.json
+++ b/i18n/ca.json
@@ -304,6 +304,7 @@
"public.errorFetchingLists": "S'ha produït un error en obtenir les llistes. Si us plau, torna-ho a provar.",
"public.errorProcessingRequest": "S'ha produït un error en processar la sol·licitud. Si us plau, torna-ho a provar.",
"public.errorTitle": "Error",
+ "public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Aquesta funció no està disponible.",
"public.invalidLink": "Enllaç no vàlid",
"public.managePrefs": "Gestiona les preferències",
@@ -473,6 +474,12 @@
"settings.privacy.listUnsubHeaderHelp": "Inclou capçaleres de cancel·lació de subscripció que permetin als clients de correu electrònic permetre als usuaris donar-se de baixa amb un sol clic.",
"settings.privacy.name": "Privadesa",
"settings.restart": "Reinicia",
+ "settings.security.captchaKey": "hCaptcha.com SiteKey",
+ "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
+ "settings.security.captchaSecret": "hCaptcha.com secret",
+ "settings.security.enableCaptcha": "Enable CAPTCHA",
+ "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
+ "settings.security.name": "Security",
"settings.smtp.customHeaders": "Capçaleres personalitzades",
"settings.smtp.customHeadersHelp": "Matriu opcional de capçaleres de correu electrònic per incloure en tots els missatges enviats des d'aquest servidor. p. ex.: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Habilitat",
diff --git a/i18n/cs-cz.json b/i18n/cs-cz.json
index 4564bf9..5fd6f0e 100644
--- a/i18n/cs-cz.json
+++ b/i18n/cs-cz.json
@@ -304,6 +304,7 @@
"public.errorFetchingLists": "Chyba při načítání seznamů. Zopakujte pokus.",
"public.errorProcessingRequest": "Chyba při zpracování požadavku. Zopakujte pokus.",
"public.errorTitle": "Chyba",
+ "public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Tato funkce není k dispozici.",
"public.invalidLink": "Neplatný odkaz",
"public.managePrefs": "Zpráva předvoleb",
@@ -473,6 +474,12 @@
"settings.privacy.listUnsubHeaderHelp": "Zahrnout záhlaví zrušení odběrů, která umožňují e-mailovým klientům, aby povolili uživatelům zrušit odběr jediným klepnutím.",
"settings.privacy.name": "Soukromí",
"settings.restart": "Restart",
+ "settings.security.captchaKey": "hCaptcha.com SiteKey",
+ "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
+ "settings.security.captchaSecret": "hCaptcha.com secret",
+ "settings.security.enableCaptcha": "Enable CAPTCHA",
+ "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
+ "settings.security.name": "Security",
"settings.smtp.customHeaders": "Vlastní záhlaví",
"settings.smtp.customHeadersHelp": "Volitelné pole e-mailových záhlaví, která se mají zahrnout do všech zpráv odeslaných z tohoto serveru. Např.: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Povoleno",
diff --git a/i18n/cy.json b/i18n/cy.json
index b219128..7a7ab4c 100644
--- a/i18n/cy.json
+++ b/i18n/cy.json
@@ -304,6 +304,7 @@
"public.errorFetchingLists": "Gwall wrth chwilio am y rhestrau. Rhowch gynnig arall arni.",
"public.errorProcessingRequest": "Gwall wrth brosesu'r cais. Rhowch gynnig arall arni.",
"public.errorTitle": "Gwall",
+ "public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Nid yw'r nodwedd ar gael.",
"public.invalidLink": "Dolen annilys",
"public.managePrefs": "Rheoli dewisiadau",
@@ -473,6 +474,12 @@
"settings.privacy.listUnsubHeaderHelp": "Cynnwys penynnau dad-danysgrifio sy'n caniatáu i ddefnyddwyr dad-danysgrifio drwy glicio un botwm.",
"settings.privacy.name": "Preifatrwydd",
"settings.restart": "Ailgychwyn",
+ "settings.security.captchaKey": "hCaptcha.com SiteKey",
+ "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
+ "settings.security.captchaSecret": "hCaptcha.com secret",
+ "settings.security.enableCaptcha": "Enable CAPTCHA",
+ "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
+ "settings.security.name": "Security",
"settings.smtp.customHeaders": "Penynnau personol",
"settings.smtp.customHeadersHelp": "Ystod eang o bennynau e-bost i'w cynnwys mewn negeseuon a anfonir gan y gweinydd hwn. ee: [{\"\"X-Custom\"\": \"\"gwerth\"\"}",
"settings.smtp.enabled": "Wedi galluogi",
diff --git a/i18n/de.json b/i18n/de.json
index 83d29f4..1b1988b 100644
--- a/i18n/de.json
+++ b/i18n/de.json
@@ -304,6 +304,7 @@
"public.errorFetchingLists": "Fehler beim Abrufen der Listen. Bitte probiere es nochmal.",
"public.errorProcessingRequest": "Fehler bei der Anfrage. Bitte probiere es nochmal.",
"public.errorTitle": "Fehler",
+ "public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Dieses Feature ist nicht verfügbar",
"public.invalidLink": "Ungültiger Link",
"public.managePrefs": "Einstellungen verwalten",
@@ -473,6 +474,12 @@
"settings.privacy.listUnsubHeaderHelp": "Inkludiere Header zum einfachen Abmelden in den E-Mails. Erlaubt es, den E-Mail Clients der Nutzer eine \",Ein Klick\"-Abmeldung anzubieten.",
"settings.privacy.name": "Privatsphäre",
"settings.restart": "Neustarten",
+ "settings.security.captchaKey": "hCaptcha.com SiteKey",
+ "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
+ "settings.security.captchaSecret": "hCaptcha.com secret",
+ "settings.security.enableCaptcha": "Enable CAPTCHA",
+ "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
+ "settings.security.name": "Security",
"settings.smtp.customHeaders": "Benutzerdefinierte Header",
"settings.smtp.customHeadersHelp": "(Optional) Array von benutzerdefinierten E-Mail Headern, welche in die Nachricht eingefügt werden sollen. Z.B.: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Aktiviert",
diff --git a/i18n/en.json b/i18n/en.json
index 2d8a3ae..140bbfb 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -304,6 +304,7 @@
"public.errorFetchingLists": "Error fetching lists. Please retry.",
"public.errorProcessingRequest": "Error processing request. Please retry.",
"public.errorTitle": "Error",
+ "public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "That feature is not available.",
"public.invalidLink": "Invalid link",
"public.managePrefs": "Manage preferences",
@@ -473,6 +474,12 @@
"settings.privacy.listUnsubHeaderHelp": "Include unsubscription headers that allow e-mail clients to allow users to unsubscribe in a single click.",
"settings.privacy.name": "Privacy",
"settings.restart": "Restart",
+ "settings.security.captchaKey": "hCaptcha.com SiteKey",
+ "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
+ "settings.security.captchaSecret": "hCaptcha.com secret",
+ "settings.security.enableCaptcha": "Enable CAPTCHA",
+ "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
+ "settings.security.name": "Security",
"settings.smtp.customHeaders": "Custom headers",
"settings.smtp.customHeadersHelp": "Optional array of e-mail headers to include in all messages sent from this server. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Enabled",
diff --git a/i18n/es.json b/i18n/es.json
index 9901b9c..98a6c9c 100644
--- a/i18n/es.json
+++ b/i18n/es.json
@@ -305,6 +305,7 @@
"public.errorFetchingLists": "Error obteniendo listas. Por favor intente nuevamente.",
"public.errorProcessingRequest": "Error al procesar la petición. Por favor intente nuevamente.",
"public.errorTitle": "Error",
+ "public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Esta función no está disponible",
"public.invalidLink": "Enlace inválido",
"public.managePrefs": "Gestionar las preferencias",
@@ -474,6 +475,12 @@
"settings.privacy.listUnsubHeaderHelp": "Incluye los encabezados de darse de baja para habilitar a los clientes de correo para permitir a los usuarios darse de baja con un solo clic.",
"settings.privacy.name": "Privacidad",
"settings.restart": "Reiniciar",
+ "settings.security.captchaKey": "hCaptcha.com SiteKey",
+ "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
+ "settings.security.captchaSecret": "hCaptcha.com secret",
+ "settings.security.enableCaptcha": "Enable CAPTCHA",
+ "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
+ "settings.security.name": "Security",
"settings.smtp.customHeaders": "Encabezados personalizados",
"settings.smtp.customHeadersHelp": "Lista de encabezados opcionales a incluir en todos los mensajes enviados desde este servidor. Por ejemplo {{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Habilitado",
diff --git a/i18n/fi.json b/i18n/fi.json
index 8eb1d53..30b9953 100644
--- a/i18n/fi.json
+++ b/i18n/fi.json
@@ -305,6 +305,7 @@
"public.errorFetchingLists": "Virhe noutaessa postituslistoja. Ole hyvä ja yritä uudestaan.",
"public.errorProcessingRequest": "Virhe käsitellessä pyyntöäsi. Ole hyvä ja yritä uudestaan.",
"public.errorTitle": "Tapahtui virhe",
+ "public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Tämä ominaisuus ei ole saatavilla.",
"public.invalidLink": "Virheellinen linkki",
"public.managePrefs": "Manage preferences",
@@ -474,6 +475,12 @@
"settings.privacy.listUnsubHeaderHelp": "Include unsubscription headers that allow e-mail clients to allow users to unsubscribe in a single click.",
"settings.privacy.name": "Yksityisyys",
"settings.restart": "Restart",
+ "settings.security.captchaKey": "hCaptcha.com SiteKey",
+ "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
+ "settings.security.captchaSecret": "hCaptcha.com secret",
+ "settings.security.enableCaptcha": "Enable CAPTCHA",
+ "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
+ "settings.security.name": "Security",
"settings.smtp.customHeaders": "Custom headers",
"settings.smtp.customHeadersHelp": "Optional array of e-mail headers to include in all messages sent from this server. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Enabled",
diff --git a/i18n/fr.json b/i18n/fr.json
index e604e08..41dbb05 100644
--- a/i18n/fr.json
+++ b/i18n/fr.json
@@ -305,6 +305,7 @@
"public.errorFetchingLists": "Erreur lors de la récupération des listes. Veuillez réessayer.",
"public.errorProcessingRequest": "Erreur lors du traitement de la demande. Veuillez réessayer.",
"public.errorTitle": "Erreur",
+ "public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Cette fonctionnalité n'est pas disponible.",
"public.invalidLink": "Lien invalide",
"public.managePrefs": "Gérer les préférences",
@@ -474,6 +475,12 @@
"settings.privacy.listUnsubHeaderHelp": "Inclure des en-têtes de désabonnement qui permettent aux utilisateurs de se désabonner en un seul clic depuis leur client de messagerie.",
"settings.privacy.name": "Vie privée",
"settings.restart": "Redémarrer",
+ "settings.security.captchaKey": "hCaptcha.com SiteKey",
+ "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
+ "settings.security.captchaSecret": "hCaptcha.com secret",
+ "settings.security.enableCaptcha": "Enable CAPTCHA",
+ "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
+ "settings.security.name": "Security",
"settings.smtp.customHeaders": "En-têtes personnalisées",
"settings.smtp.customHeadersHelp": "Tableau facultatif d'en-têtes à inclure dans tous les e-mails envoyés depuis ce serveur. Par exemple : [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Activé",
diff --git a/i18n/hu.json b/i18n/hu.json
index 1d1a179..52a1d78 100644
--- a/i18n/hu.json
+++ b/i18n/hu.json
@@ -305,6 +305,7 @@
"public.errorFetchingLists": "Hiba a listák lekérésekor. Kérjük, próbálja újra.",
"public.errorProcessingRequest": "Hiba a kérelem feldolgozásakor. Kérjük, próbálja újra.",
"public.errorTitle": "Hiba",
+ "public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Ez a funkció nem elérhető.",
"public.invalidLink": "A link érvénytelen",
"public.managePrefs": "Manage preferences",
@@ -474,6 +475,12 @@
"settings.privacy.listUnsubHeaderHelp": "Tartalmazzon leiratkozási fejléceket, amelyek lehetővé teszik az e-mail kliensek számára, hogy a felhasználók egyetlen kattintással leiratkozhassanak.",
"settings.privacy.name": "Magánélet",
"settings.restart": "Újraindítás",
+ "settings.security.captchaKey": "hCaptcha.com SiteKey",
+ "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
+ "settings.security.captchaSecret": "hCaptcha.com secret",
+ "settings.security.enableCaptcha": "Enable CAPTCHA",
+ "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
+ "settings.security.name": "Security",
"settings.smtp.customHeaders": "Egyéni fejlécek",
"settings.smtp.customHeadersHelp": "Az e-mail fejlécek opcionális tömbje a szerverről küldött összes üzenetben. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Engedélyez",
diff --git a/i18n/it.json b/i18n/it.json
index e2d5eae..e9259df 100644
--- a/i18n/it.json
+++ b/i18n/it.json
@@ -305,6 +305,7 @@
"public.errorFetchingLists": "Errore durante il recupero delle liste. Per favore, riprova.",
"public.errorProcessingRequest": "Errore durante la gestione della richiesta. Per favore, riprova.",
"public.errorTitle": "Errore",
+ "public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Questa funzione non è disponibile.",
"public.invalidLink": "Link non valido",
"public.managePrefs": "Gestiona l'impostazinoni",
@@ -474,6 +475,12 @@
"settings.privacy.listUnsubHeaderHelp": "Includere intestazioni di annullamento dell'iscrizione che consentono agli utenti di annullare l'iscrizione con un clic dal proprio client di posta elettronica.",
"settings.privacy.name": "Privacy",
"settings.restart": "Riavviare",
+ "settings.security.captchaKey": "hCaptcha.com SiteKey",
+ "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
+ "settings.security.captchaSecret": "hCaptcha.com secret",
+ "settings.security.enableCaptcha": "Enable CAPTCHA",
+ "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
+ "settings.security.name": "Security",
"settings.smtp.customHeaders": "Intestazioni personalizzate",
"settings.smtp.customHeadersHelp": "Matrice facoltativa di intestazioni di posta elettronica da includere in tutti i messaggi inviati da questo server. Ad esempio: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Attivata",
diff --git a/i18n/jp.json b/i18n/jp.json
index cbc2e7b..d251d64 100644
--- a/i18n/jp.json
+++ b/i18n/jp.json
@@ -305,6 +305,7 @@
"public.errorFetchingLists": "リストの取得にエラーがありました。再試行してください。",
"public.errorProcessingRequest": "リクエスト中にエラーがありました。再試行してください。",
"public.errorTitle": "エラー",
+ "public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "その機能は使用できません。",
"public.invalidLink": "無効なリンク",
"public.managePrefs": "設定変更",
@@ -474,6 +475,12 @@
"settings.privacy.listUnsubHeaderHelp": "メールクライアントがワンクリックで登録解除をできるように登録解除用のヘッダーを含める。",
"settings.privacy.name": "プライバシー",
"settings.restart": "再起動",
+ "settings.security.captchaKey": "hCaptcha.com SiteKey",
+ "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
+ "settings.security.captchaSecret": "hCaptcha.com secret",
+ "settings.security.enableCaptcha": "Enable CAPTCHA",
+ "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
+ "settings.security.name": "Security",
"settings.smtp.customHeaders": "カスタムヘッダー",
"settings.smtp.customHeadersHelp": "このサーバーから送信する全てのメッセージに含まれる任意のメールヘッダーの配列。 例: [{\"X-カスタム\": \"バリュー\"}, {\"X-カスタム2\": \"バリュー\"}]",
"settings.smtp.enabled": "有効",
diff --git a/i18n/ml.json b/i18n/ml.json
index 794affa..da636f0 100644
--- a/i18n/ml.json
+++ b/i18n/ml.json
@@ -304,6 +304,7 @@
"public.errorFetchingLists": "ലിസ്റ്റുകൾ വീണ്ടെടുക്കുന്നതിൽ തടസം നേരിട്ടു. വീണ്ടും ശ്രമിക്കുക.",
"public.errorProcessingRequest": "അഭ്യർത്ഥനയിന്മേൽ നടപടിയെടുക്കുന്നതിൽ തടസം നേരിട്ടു. വീണ്ടും ശ്രമിക്കുക.",
"public.errorTitle": "എറർ",
+ "public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "ഈ ഫീച്ചർ ലഭ്യമല്ല",
"public.invalidLink": "കണ്ണി അസാധുവാണ്",
"public.managePrefs": "മുൻഗണനകളിൽ മാറ്റം വരുത്തുക",
@@ -473,6 +474,12 @@
"settings.privacy.listUnsubHeaderHelp": "ഒറ്റ ക്ലിക്കിലൂടെ വരിക്കാനല്ലാതാക്കാൻ ഇ-മെയിൽ ക്ലൈന്റിൽ വരിക്കാരനല്ലാതാക്കാനുള്ള തലക്കെട്ട് കൂട്ടിച്ചേർക്കുക.",
"settings.privacy.name": "സ്വകാര്യത",
"settings.restart": "പുനരാരംഭിയ്ക്കുക",
+ "settings.security.captchaKey": "hCaptcha.com SiteKey",
+ "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
+ "settings.security.captchaSecret": "hCaptcha.com secret",
+ "settings.security.enableCaptcha": "Enable CAPTCHA",
+ "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
+ "settings.security.name": "Security",
"settings.smtp.customHeaders": "ഇഷ്ടാനുസൃത തലക്കെട്ടുകൾ",
"settings.smtp.customHeadersHelp": "ഈ സേർവറിൽ നിന്നും അയക്കുന്ന എല്ലാ ഈ-മെയിലിലും ഉണ്ടാകേണ്ട ഇഷ്ടാനുസൃത തലക്കെട്ടുകൾ. ഉദാഹരണം: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "പ്രവർത്തനക്ഷമമാക്കി",
diff --git a/i18n/nl.json b/i18n/nl.json
index 1578f58..5cda4c2 100644
--- a/i18n/nl.json
+++ b/i18n/nl.json
@@ -305,6 +305,7 @@
"public.errorFetchingLists": "Fout bij ophalen lijsten. Probeer opnieuw.",
"public.errorProcessingRequest": "Fout bij behandelen verzoek. Probeer opnieuw.",
"public.errorTitle": "Fout",
+ "public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Deze functie is niet beschikbaar",
"public.invalidLink": "Ongeldige link",
"public.managePrefs": "Manage preferences",
@@ -474,6 +475,12 @@
"settings.privacy.listUnsubHeaderHelp": "Voeg header toe zodat e-mailprogramma's gebruikers zich kunnen laten uitschrijven in een klik.",
"settings.privacy.name": "Privacy",
"settings.restart": "Herstarten",
+ "settings.security.captchaKey": "hCaptcha.com SiteKey",
+ "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
+ "settings.security.captchaSecret": "hCaptcha.com secret",
+ "settings.security.enableCaptcha": "Enable CAPTCHA",
+ "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
+ "settings.security.name": "Security",
"settings.smtp.customHeaders": "Aangepaste headers",
"settings.smtp.customHeadersHelp": "Optionele lijst met e-mail headers om toe te voegen aan alle berichten van deze server. Bv.: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Ingeschakeld",
diff --git a/i18n/pl.json b/i18n/pl.json
index 628893e..d1a2369 100644
--- a/i18n/pl.json
+++ b/i18n/pl.json
@@ -305,6 +305,7 @@
"public.errorFetchingLists": "Błąd pobierania list. Spróbuj ponownie.",
"public.errorProcessingRequest": "Błąd przetwarzania żądania. Spróbuj ponownie.",
"public.errorTitle": "Błąd",
+ "public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Ta funkcjonalność jest niedostępna.",
"public.invalidLink": "Nieprawidłowy liny.",
"public.managePrefs": "Manage preferences",
@@ -474,6 +475,12 @@
"settings.privacy.listUnsubHeaderHelp": "Dodaj nagłówki do wypisania się z subskrypcji. Niektóre programy pocztowe umożliwiają wypisanie się jednym kliknięciem.",
"settings.privacy.name": "Prywatność",
"settings.restart": "Restart",
+ "settings.security.captchaKey": "hCaptcha.com SiteKey",
+ "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
+ "settings.security.captchaSecret": "hCaptcha.com secret",
+ "settings.security.enableCaptcha": "Enable CAPTCHA",
+ "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
+ "settings.security.name": "Security",
"settings.smtp.customHeaders": "Niestandardowe nagłówki",
"settings.smtp.customHeadersHelp": "Opcjonalna lista nagłówków do zamieszczania w wiadomościach we wszystkich wiadomościach wysłanych z tego serwera. np: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Włączone",
diff --git a/i18n/pt-BR.json b/i18n/pt-BR.json
index 61c77b3..b966f25 100644
--- a/i18n/pt-BR.json
+++ b/i18n/pt-BR.json
@@ -304,6 +304,7 @@
"public.errorFetchingLists": "Erro ao obter as listas. Por favor, tente novamente.",
"public.errorProcessingRequest": "Erro ao processar a solicitação. Por favor, tente novamente.",
"public.errorTitle": "Erro",
+ "public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Este recurso não está disponível.",
"public.invalidLink": "Link inválido",
"public.managePrefs": "Gerenciar preferências",
@@ -473,6 +474,12 @@
"settings.privacy.listUnsubHeaderHelp": "Incluir cabeçalhos de desinscrição que permitem aos clientes de e-mail cancelem a inscrição em um único clique.",
"settings.privacy.name": "Privacidade",
"settings.restart": "Reiniciar",
+ "settings.security.captchaKey": "hCaptcha.com SiteKey",
+ "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
+ "settings.security.captchaSecret": "hCaptcha.com secret",
+ "settings.security.enableCaptcha": "Enable CAPTCHA",
+ "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
+ "settings.security.name": "Security",
"settings.smtp.customHeaders": "Cabeçalhos personalizados",
"settings.smtp.customHeadersHelp": "Array opcional de cabeçalhos de e-mail para incluir em todas as mensagens enviadas a partir deste servidor. por exemplo: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Habilitado",
diff --git a/i18n/pt.json b/i18n/pt.json
index d55fe2d..5a5ba93 100644
--- a/i18n/pt.json
+++ b/i18n/pt.json
@@ -304,6 +304,7 @@
"public.errorFetchingLists": "Erro ao carregar listas. Por favor tente novamente.",
"public.errorProcessingRequest": "Erro ao processar pedido. Por favor tente novamente.",
"public.errorTitle": "Erro",
+ "public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "That feature is not available",
"public.invalidLink": "Link inválido",
"public.managePrefs": "Gerir preferências",
@@ -473,6 +474,12 @@
"settings.privacy.listUnsubHeaderHelp": "Incluir headers de cancelamento de subscrição que permite aos clientes de email permitir ao utilizadores cancelar a subscrição num único clique.",
"settings.privacy.name": "Privacidade",
"settings.restart": "Reiniciar",
+ "settings.security.captchaKey": "hCaptcha.com SiteKey",
+ "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
+ "settings.security.captchaSecret": "hCaptcha.com secret",
+ "settings.security.enableCaptcha": "Enable CAPTCHA",
+ "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
+ "settings.security.name": "Security",
"settings.smtp.customHeaders": "Headers customizados",
"settings.smtp.customHeadersHelp": "Array opcional de headers de email a incluir em todas as mensagens enviadas deste servidor. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Ativo",
diff --git a/i18n/ro.json b/i18n/ro.json
index 6c7a529..4f96a19 100644
--- a/i18n/ro.json
+++ b/i18n/ro.json
@@ -305,6 +305,7 @@
"public.errorFetchingLists": "Eroare la preluarea listelor. Încearcă din nou.",
"public.errorProcessingRequest": "Eroare la procesarea cererii. Încearcă din nou.",
"public.errorTitle": "Eroare",
+ "public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Această functie nu este disponibilă",
"public.invalidLink": "Link invalid",
"public.managePrefs": "Manage preferences",
@@ -474,6 +475,12 @@
"settings.privacy.listUnsubHeaderHelp": "Include anteturi de dezabonare care permit clienților de e-mail să permită utilizatorilor să se dezaboneze printr-un singur clic.",
"settings.privacy.name": "Confidențialitate",
"settings.restart": "Restart",
+ "settings.security.captchaKey": "hCaptcha.com SiteKey",
+ "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
+ "settings.security.captchaSecret": "hCaptcha.com secret",
+ "settings.security.enableCaptcha": "Enable CAPTCHA",
+ "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
+ "settings.security.name": "Security",
"settings.smtp.customHeaders": "Anteturi personalizate",
"settings.smtp.customHeadersHelp": "Matrice opțională de antete de e-mail pentru a include în toate mesajele trimise de pe acest server. de exemplu: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Activat",
diff --git a/i18n/ru.json b/i18n/ru.json
index a3ed731..0d4c918 100644
--- a/i18n/ru.json
+++ b/i18n/ru.json
@@ -304,6 +304,7 @@
"public.errorFetchingLists": "Ошибка получения списков. Пожалуйста, повторите.",
"public.errorProcessingRequest": "Ошибка обработки запроса. Пожалуйста, повторите.",
"public.errorTitle": "Ошибка",
+ "public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Эта функция недоступна.",
"public.invalidLink": "Неверная ссылка",
"public.managePrefs": "Параметры письма",
@@ -473,6 +474,12 @@
"settings.privacy.listUnsubHeaderHelp": "Включать заголовок отписки",
"settings.privacy.name": "Конфиденциальност",
"settings.restart": "Перезапустить",
+ "settings.security.captchaKey": "hCaptcha.com SiteKey",
+ "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
+ "settings.security.captchaSecret": "hCaptcha.com secret",
+ "settings.security.enableCaptcha": "Enable CAPTCHA",
+ "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
+ "settings.security.name": "Security",
"settings.smtp.customHeaders": "Настраиваемые заголовки",
"settings.smtp.customHeadersHelp": "Необязательный массив заголовков e-mail, которые будут включены во все письма, отправляемые с этого сервера. Например: [{\"X-Custom\": \"значение\"}, {\"X-Custom2\": \"значение\"}]",
"settings.smtp.enabled": "Включено",
diff --git a/i18n/sk.json b/i18n/sk.json
index c45ed63..4c1b650 100644
--- a/i18n/sk.json
+++ b/i18n/sk.json
@@ -304,6 +304,7 @@
"public.errorFetchingLists": "Chyba pri načítání zoznamov. Zopakujte pokus.",
"public.errorProcessingRequest": "Chyba pri spracovaní požiadavky. Zopakujte pokus.",
"public.errorTitle": "Chyba",
+ "public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Táto funkcia nie je k dispozícii.",
"public.invalidLink": "Neplatný odkaz",
"public.managePrefs": "Správa predvolieb",
@@ -473,6 +474,12 @@
"settings.privacy.listUnsubHeaderHelp": "Nastaví hlavičku zrušenia odberov, ktorá umožňuje e-mailovým klientom, aby povolili používateľom zrušiť odber jedným kliknutím.",
"settings.privacy.name": "Súkromie",
"settings.restart": "Restarť",
+ "settings.security.captchaKey": "hCaptcha.com SiteKey",
+ "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
+ "settings.security.captchaSecret": "hCaptcha.com secret",
+ "settings.security.enableCaptcha": "Enable CAPTCHA",
+ "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
+ "settings.security.name": "Security",
"settings.smtp.customHeaders": "Vlastné hlavičky",
"settings.smtp.customHeadersHelp": "Voliteľné polia e-mailových hlavičiek, ktorá sa majú nastaviť do všetkých správ odoslaných z tohoto servera. Napr.: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Zapnuté",
diff --git a/i18n/tr.json b/i18n/tr.json
index f6d7550..ec42798 100644
--- a/i18n/tr.json
+++ b/i18n/tr.json
@@ -305,6 +305,7 @@
"public.errorFetchingLists": "Listeleri getirme hatası. Lütfen tekrarla.",
"public.errorProcessingRequest": "İstek işleme hatası. Lütfen tekrarla.",
"public.errorTitle": "Hata",
+ "public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Bu özellik geçerli değil.",
"public.invalidLink": "Geçersiz link",
"public.managePrefs": "Manage preferences",
@@ -474,6 +475,12 @@
"settings.privacy.listUnsubHeaderHelp": "E-posta istemcilerinin kullanıcıların tek bir tıklamayla abonelikten çıkmalarına olanak tanıyan abonelik iptal başlıklarını ekleyin.",
"settings.privacy.name": "Gizlilik",
"settings.restart": "Yeniden başlat",
+ "settings.security.captchaKey": "hCaptcha.com SiteKey",
+ "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
+ "settings.security.captchaSecret": "hCaptcha.com secret",
+ "settings.security.enableCaptcha": "Enable CAPTCHA",
+ "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
+ "settings.security.name": "Security",
"settings.smtp.customHeaders": "Özel başlık bilgisi",
"settings.smtp.customHeadersHelp": "Bu sunucudan gönderilen tüm iletilere eklenecek isteğe bağlı e-posta başlıkları dizisi. Örnek: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Etkinleştirildi",
diff --git a/i18n/vi.json b/i18n/vi.json
index 6f5754b..a407243 100644
--- a/i18n/vi.json
+++ b/i18n/vi.json
@@ -305,6 +305,7 @@
"public.errorFetchingLists": "Lỗi khi tìm nạp danh sách. Xin hãy thử lại.",
"public.errorProcessingRequest": "Lỗi khi xử lý yêu cầu. Xin hãy thử lại.",
"public.errorTitle": "Lỗi",
+ "public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "Tính năng đó không khả dụng.",
"public.invalidLink": "Link không khả dụng",
"public.managePrefs": "Manage preferences",
@@ -474,6 +475,12 @@
"settings.privacy.listUnsubHeaderHelp": "Bao gồm các tiêu đề hủy đăng ký cho phép ứng dụng e-mail cho phép người dùng hủy đăng ký chỉ bằng một cú nhấp chuột.",
"settings.privacy.name": "Sự riêng tư",
"settings.restart": "Khởi động lại",
+ "settings.security.captchaKey": "hCaptcha.com SiteKey",
+ "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
+ "settings.security.captchaSecret": "hCaptcha.com secret",
+ "settings.security.enableCaptcha": "Enable CAPTCHA",
+ "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
+ "settings.security.name": "Security",
"settings.smtp.customHeaders": "Tiêu đề tùy chỉnh",
"settings.smtp.customHeadersHelp": "Mảng tiêu đề e-mail tùy chọn để bao gồm trong tất cả các thư được gửi từ máy chủ này. ví dụ: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Đã bật",
diff --git a/i18n/zh-CN.json b/i18n/zh-CN.json
index 23fd11f..8ae253f 100644
--- a/i18n/zh-CN.json
+++ b/i18n/zh-CN.json
@@ -304,6 +304,7 @@
"public.errorFetchingLists": "获取列表时出错。请重试。",
"public.errorProcessingRequest": "处理请求时出错。请重试。",
"public.errorTitle": "错误",
+ "public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "该功能不可用。",
"public.invalidLink": "无效的链接",
"public.managePrefs": "管理偏好设置",
@@ -473,6 +474,12 @@
"settings.privacy.listUnsubHeaderHelp": "包括允许电子邮件客户端允许用户通过单击取消订阅的取消订阅标题",
"settings.privacy.name": "隐私",
"settings.restart": "重新开始",
+ "settings.security.captchaKey": "hCaptcha.com SiteKey",
+ "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
+ "settings.security.captchaSecret": "hCaptcha.com secret",
+ "settings.security.enableCaptcha": "Enable CAPTCHA",
+ "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
+ "settings.security.name": "Security",
"settings.smtp.customHeaders": "自定义标头",
"settings.smtp.customHeadersHelp": "要包含在从此服务器发送的所有消息中的可选电子邮件标头数组。例如: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "已启用",
diff --git a/i18n/zh-TW.json b/i18n/zh-TW.json
index cdfac47..fa25946 100644
--- a/i18n/zh-TW.json
+++ b/i18n/zh-TW.json
@@ -305,6 +305,7 @@
"public.errorFetchingLists": "獲取列表時出錯。請重試。",
"public.errorProcessingRequest": "處理請求時出錯。請重試。",
"public.errorTitle": "錯誤",
+ "public.invalidCaptcha": "Invalid CAPTCHA.",
"public.invalidFeature": "該功能不可用。",
"public.invalidLink": "無效的鏈接",
"public.managePrefs": "Manage preferences",
@@ -474,6 +475,12 @@
"settings.privacy.listUnsubHeaderHelp": "包括允許電子郵件客戶端允許用戶通過單擊取消訂閱的取消訂閱標題",
"settings.privacy.name": "隱私",
"settings.restart": "重新開始",
+ "settings.security.captchaKey": "hCaptcha.com SiteKey",
+ "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
+ "settings.security.captchaSecret": "hCaptcha.com secret",
+ "settings.security.enableCaptcha": "Enable CAPTCHA",
+ "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
+ "settings.security.name": "Security",
"settings.smtp.customHeaders": "自定義標頭",
"settings.smtp.customHeadersHelp": "要包含在從此服務器發送的所有消息中的可選電子郵件標頭數組。例如: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "已啟用",
diff --git a/internal/captcha/captcha.go b/internal/captcha/captcha.go
new file mode 100644
index 0000000..92d7c9b
--- /dev/null
+++ b/internal/captcha/captcha.go
@@ -0,0 +1,76 @@
+package captcha
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+)
+
+const (
+ rootURL = "https://hcaptcha.com/siteverify"
+)
+
+type captchaResp struct {
+ Success bool `json:"success"`
+ ErrorCodes []string `json:"error_codes"`
+}
+
+// Captcha is a simple Captcha client.
+// It currently implements hcaptcha.com
+type Captcha struct {
+ o Opt
+ client *http.Client
+}
+
+type Opt struct {
+ CaptchaSecret string `json:"captcha_secret"`
+}
+
+// New returns a new instance of the HTTP CAPTCHA client.
+func New(o Opt) *Captcha {
+ timeout := time.Second * 5
+
+ return &Captcha{
+ o: o,
+ client: &http.Client{
+ Timeout: timeout,
+ Transport: &http.Transport{
+ MaxIdleConnsPerHost: 10,
+ MaxConnsPerHost: 100,
+ ResponseHeaderTimeout: timeout,
+ IdleConnTimeout: timeout,
+ },
+ }}
+}
+
+// Verify veries a CAPTCHA request.
+func (c *Captcha) Verify(token string) (error, bool) {
+ resp, err := c.client.PostForm(rootURL, url.Values{
+ "secret": {c.o.CaptchaSecret},
+ "response": {token},
+ })
+ if err != nil {
+ return err, false
+ }
+
+ defer resp.Body.Close()
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return err, false
+ }
+
+ var r captchaResp
+ if json.Unmarshal(body, &r); err != nil {
+ return err, true
+ }
+
+ if r.Success != true {
+ return fmt.Errorf("captcha failed: %s", strings.Join(r.ErrorCodes, ",")), false
+ }
+
+ return nil, true
+}
diff --git a/internal/migrations/v2.4.0.go b/internal/migrations/v2.4.0.go
new file mode 100644
index 0000000..85e76e3
--- /dev/null
+++ b/internal/migrations/v2.4.0.go
@@ -0,0 +1,23 @@
+package migrations
+
+import (
+ "github.com/jmoiron/sqlx"
+ "github.com/knadh/koanf"
+ "github.com/knadh/stuffbin"
+)
+
+// V2_4_0 performs the DB migrations.
+func V2_4_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
+ // Insert new preference settings.
+ if _, err := db.Exec(`
+ INSERT INTO settings (key, value) VALUES
+ ('security.enable_captcha', 'false'),
+ ('security.captcha_key', '""'),
+ ('security.captcha_secret', '""')
+ ON CONFLICT DO NOTHING;
+ `); err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/models/settings.go b/models/settings.go
index 3b8d75d..22ad4ef 100644
--- a/models/settings.go
+++ b/models/settings.go
@@ -32,6 +32,10 @@ type Settings struct {
PrivacyExportable []string `json:"privacy.exportable"`
DomainBlocklist []string `json:"privacy.domain_blocklist"`
+ SecurityEnableCaptcha bool `json:"security.enable_captcha"`
+ SecurityCaptchaKey string `json:"security.captcha_key"`
+ SecurityCaptchaSecret string `json:"security.captcha_secret"`
+
UploadProvider string `json:"upload.provider"`
UploadFilesystemUploadPath string `json:"upload.filesystem.upload_path"`
UploadFilesystemUploadURI string `json:"upload.filesystem.upload_uri"`
diff --git a/schema.sql b/schema.sql
index 35e0778..18214fa 100644
--- a/schema.sql
+++ b/schema.sql
@@ -208,6 +208,9 @@ INSERT INTO settings (key, value) VALUES
('privacy.allow_preferences', 'true'),
('privacy.exportable', '["profile", "subscriptions", "campaign_views", "link_clicks"]'),
('privacy.domain_blocklist', '[]'),
+ ('security.enable_captcha', 'false'),
+ ('security.captcha_key', '""'),
+ ('security.captcha_secret', '""'),
('upload.provider', '"filesystem"'),
('upload.filesystem.upload_path', '"uploads"'),
('upload.filesystem.upload_uri', '"/uploads"'),
diff --git a/static/public/static/style.css b/static/public/static/style.css
index 5075320..87c617c 100644
--- a/static/public/static/style.css
+++ b/static/public/static/style.css
@@ -122,7 +122,7 @@ input[disabled] {
.lists {
list-style-type: none;
padding: 0;
- margin-bottom: 30px;
+ margin: 40px 0;
}
.lists li {
margin: 0 0 5px 0;
@@ -137,6 +137,9 @@ input[disabled] {
.form .nonce {
display: none;
}
+ .form .captcha {
+ margin-top: 30px;
+ }
.archive {
list-style-type: none;
diff --git a/static/public/templates/subscription-form.html b/static/public/templates/subscription-form.html
index 22669be..a99e034 100644
--- a/static/public/templates/subscription-form.html
+++ b/static/public/templates/subscription-form.html
@@ -15,7 +15,7 @@
-
+
{{ L.T "globals.terms.lists" }}
{{ range $i, $l := .Data.Lists }}
@@ -28,6 +28,13 @@
{{ end }}
+
+ {{ if .Data.CaptchaKey }}
+
+ {{ end }}