{ "name": "Newsletter Antispam", "nodes": [ { "parameters": { "httpMethod": "POST", "path": "0c776b8d-dd1b-40d2-92cf-25a96d580a3c", "responseMode": "responseNode", "options": { "ignoreBots": true } }, "type": "n8n-nodes-base.webhook", "typeVersion": 2.1, "position": [ 0, 0 ], "id": "808e2cc0-e813-43a6-ac93-dcf79a882597", "name": "Webhook", "webhookId": "0c776b8d-dd1b-40d2-92cf-25a96d580a3c" }, { "parameters": { "jsCode": " /* ===================== Konfiguration ===================== */\nconst MIN_DELAY_SEC = 3; // min. Zeit zwischen Render & Submit\nconst MAX_AGE_SEC = 3600; // max. Alter des ts (1h)\nconst REQUIRE_CONSENT = true; // Consent-Checkbox Pflicht?\n\n// Optional: nur Requests von eigenen Origins/Referern akzeptieren\nconst REQUIRE_ORIGIN_HOSTS = [\n 'https://deinedomain.de',\n];\n\n// Optional: Shared-Secret erzwingen (Query/Body)\nconst REQUIRE_SECRET = false;\nconst SECRET_KEY = 'k';\nconst SECRET_VALUE = 'YOUR_SHARED_SECRET';\n\n// Domain-Blockliste\nconst BAD_DOMAINS = [\n 'mail.ru','yopmail.com','qq.com','outlook.cn','163.com','126.com',\n 'tempmail.','.ru','.cn','.xyz','.top'\n];\n\n// Freitext-Felder, die Bots gern füllen (falls vorhanden → Spam)\nconst COMMENT_LIKE_FIELDS = ['comment','message','text','content'];\n\n// NEU: Name-Checks (Bad-Words + Wortgrenzen-Pattern)\nconst BAD_NAME_WORDS = [\n 'bitcoin','crypto','forex','loan','sex','porn',\n 'seo','viagra','casino','telegram','t.me','whatsapp'\n];\nconst BAD_NAME_REGEX = /\\b(bitcoin|crypto|forex|loan|sex|porn|seo|viagra|casino|telegram|whatsapp)\\b/i;\n\n/* ===================== Helpers ===================== */\nconst norm = (s='') => String(s).trim();\nconst lower = (s='') => norm(s).toLowerCase();\n\nfunction toArray(x) {\n if (x == null) return [];\n if (Array.isArray(x)) return x.map(v => norm(String(v)));\n const s = String(x).trim();\n if (!s) return [];\n if (s.includes(',')) return s.split(',').map(v => v.trim()).filter(Boolean);\n return [s];\n}\n\nfunction isValidEmail(s='') {\n const re = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n return re.test(s);\n}\n\n/* ===================== Inputs ===================== */\nconst headers = $json.headers || {};\nconst body = $json.body || {};\nconst query = $json.query || {};\n\nconst ua = headers['user-agent'] || headers['User-Agent'] || '';\nconst ip = headers['x-real-ip'] ||\n (headers['x-forwarded-for'] || '').split(',')[0]?.trim() ||\n '0.0.0.0';\n\nconst origin = headers['origin'] || headers['Origin'] || '';\nconst referer = headers['referer'] || headers['Referer'] || '';\n\n/* ===================== Feldmapping ===================== */\n// Unterstützt hidden \"lists\" (UUID oder CSV) ODER klassisches mehrfaches \"l\"\nconst email = lower(body.email || '');\nconst name = norm(body.name || '');\nconst nameLower = lower(name);\nconst listsRaw = body.l ?? body.lists ?? '';\nconst listsArr = toArray(listsRaw);\nconst consent = lower(body.consent || '') === 'true';\nconst ts = parseInt(body.ts || '0', 10);\n\n// Honeypots\nconst honey1 = norm(body.confirm_email || '');\nconst honey2 = norm(body.website || '');\n\n// Optionales Secret (Query oder Body)\nconst providedSecret = norm(query[SECRET_KEY] ?? body[SECRET_KEY] ?? '');\n\n/* ===================== Prüfungen ===================== */\nconst reasons = [];\n\n// Origins/Referer einschränken\nif (REQUIRE_ORIGIN_HOSTS.length) {\n const okOrigin = REQUIRE_ORIGIN_HOSTS.some(host =>\n origin.startsWith(host) || referer.startsWith(host)\n );\n if (!okOrigin) reasons.push('bad_origin');\n}\n\n// Secret prüfen (optional)\nif (REQUIRE_SECRET && providedSecret !== SECRET_VALUE) {\n reasons.push('bad_secret');\n}\n\n// Honeypot\nif (honey1 !== '' || honey2 !== '') reasons.push('honeypot');\n\n// Zeitcheck\nconst now = Math.floor(Date.now()/1000);\nif (!ts || (now - ts) < MIN_DELAY_SEC) reasons.push('too_fast');\nif (ts && (now - ts) > MAX_AGE_SEC) reasons.push('too_old');\n\n// User-Agent grob plausibel\nif (!ua || ua.length < 8) reasons.push('ua_sus');\n\n// E-Mail Pflicht + Format\nif (!email) reasons.push('email_missing');\nif (email && !isValidEmail(email)) reasons.push('email_invalid');\n\n// Domain-Block\nconst domain = email.split('@')[1] || '';\nif (domain && BAD_DOMAINS.some(d => domain.includes(d))) reasons.push('domain_blocked');\n\n// Consent Pflicht\nif (REQUIRE_CONSENT && !consent) reasons.push('no_consent');\n\n// Listen-Auswahl Pflicht\nif (!listsArr.length) reasons.push('no_list');\n\n// Kommentar-ähnliche Felder gefüllt?\nfor (const f of COMMENT_LIKE_FIELDS) {\n if (norm(body[f] || '').length > 0) {\n reasons.push(`comment_like_${f}`);\n }\n}\n\n// **NEU: Name-Spam-Checks**\nif (name) {\n // 1) einfache Teilstring-Liste (robust gegen Variationen wie \"Best Bitcoin Deals\")\n if (BAD_NAME_WORDS.some(w => nameLower.includes(w))) {\n reasons.push('bad_name_word');\n }\n // 2) Wortgrenzen-Pattern (verhindert False-Positives durch Teilstrings)\n if (BAD_NAME_REGEX.test(name)) {\n reasons.push('bad_name_pattern');\n }\n}\n// Optional: extrem lange oder nur Sonderzeichen/Zahlen blocken\nif (name && (name.length > 80 || /^[^A-Za-zÄÖÜäöüß\\s\\-'.]+$/.test(name))) {\n reasons.push('bad_name_shape');\n}\n\n/* ===================== Ergebnis & Payload ===================== */\nconst spam = reasons.length > 0;\n\n// Payload für Listmonk Public API: POST /api/public/subscription\nconst data = {\n email,\n name,\n lists: listsArr, // intern\n consent, // intern\n listmonk: {\n email,\n name,\n list_uuids: listsArr\n }\n};\n\n/* ===================== Debug ===================== */\nconst debug = {\n origin, referer, ua, ip,\n ts, now,\n listsRaw,\n headers_seen: Object.keys(headers)\n};\n\nreturn [{\n spam,\n reason: reasons.join(','),\n data,\n ip,\n ua,\n debug\n}];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 208, 0 ], "id": "bade599f-7d3a-4367-98c2-4157f3d1d9f6", "name": "Code in JavaScript" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 }, "conditions": [ { "id": "58635590-3bd5-45f6-801c-5c45efd9ddd0", "leftValue": "={{ $json.spam }}", "rightValue": "true", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ 416, 0 ], "id": "2e2cbca1-2c5d-44b0-b525-1d65ac00d5ca", "name": "If" }, { "parameters": { "method": "POST", "url": "https://listmonk.deinedomain.de/api/public/subscription", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Content-Type", "value": "application/json" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ $json.data.listmonk }}", "options": { "response": { "response": { "responseFormat": "json" } } } }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 640, 48 ], "id": "1b6028de-10a1-4b0c-b337-1dbf50b30471", "name": "HTTP Request" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 }, "conditions": [ { "id": "1f4a66fa-868d-4204-ac8d-334de1d8ac99", "leftValue": "={{ $json.data.has_optin }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ 848, 48 ], "id": "d1093f02-af5f-45f9-a0ce-24adb5edd535", "name": "If1" }, { "parameters": { "respondWith": "redirect", "redirectURL": "https://donau2space.de/newsletter-danke/?optin=1", "options": { "responseCode": 303, "responseHeaders": { "entries": [ { "name": "Location", "value": "https://donau2space.de/newsletter-danke/?optin=1" } ] } } }, "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.4, "position": [ 1056, -48 ], "id": "9368b5a7-c671-4d69-a91e-d72e79d67b55", "name": "Respond to Webhook" }, { "parameters": { "respondWith": "redirect", "redirectURL": "https://donau2space.de/newsletter-danke/?already=1", "options": { "responseCode": 303, "responseHeaders": { "entries": [ { "name": "Location", "value": "https://donau2space.de/newsletter-danke/?already=1" } ] } } }, "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.4, "position": [ 1056, 144 ], "id": "48e821fc-6034-437a-8c64-1e1183193784", "name": "Respond to Webhook1" }, { "parameters": { "respondWith": "redirect", "redirectURL": "https://donau2space.de/newsletter-fehler/?code=Try+again", "options": { "responseCode": 303, "responseHeaders": { "entries": [ { "name": "Location", "value": "https://donau2space.de/newsletter-fehler/?code=Try+again" } ] } } }, "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.4, "position": [ 624, -96 ], "id": "6cbc3847-bf53-4dec-9311-a804d7c2b75f", "name": "Respond to Webhook2" } ], "pinData": {}, "connections": { "Webhook": { "main": [ [ { "node": "Code in JavaScript", "type": "main", "index": 0 } ] ] }, "Code in JavaScript": { "main": [ [ { "node": "If", "type": "main", "index": 0 } ] ] }, "If": { "main": [ [ { "node": "Respond to Webhook2", "type": "main", "index": 0 } ], [ { "node": "HTTP Request", "type": "main", "index": 0 } ] ] }, "HTTP Request": { "main": [ [ { "node": "If1", "type": "main", "index": 0 } ] ] }, "If1": { "main": [ [ { "node": "Respond to Webhook", "type": "main", "index": 0 } ], [ { "node": "Respond to Webhook1", "type": "main", "index": 0 } ] ] } }, "active": false, "settings": { "executionOrder": "v1", "errorWorkflow": "9NX9pfjHnYaVyhGw" }, "versionId": "51398630-d42a-4a61-a90e-45681da07e9a", "meta": { "instanceId": "a98e8506a882b8d52393d84b9e4ce53b73e5bca84f760a4bd424db342b3c9a16" }, "id": "1wl1IrG1ZCaRUmSJ", "tags": [] }