diff --git a/n8n-workflow/Newsletter Antispam.json b/n8n-workflow/Newsletter Antispam.json new file mode 100644 index 0000000..a60a87c --- /dev/null +++ b/n8n-workflow/Newsletter Antispam.json @@ -0,0 +1,296 @@ +{ + "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": [] +} \ No newline at end of file