n8n-workflow-listmonk-antispam/n8n-workflow/Newsletter Antispam.json

296 lines
No EOL
12 KiB
JSON

{
"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": []
}