{
  "name": "PhishSim \u2013 Send Campaign Emails",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "trigger-send",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "webhook-send",
      "name": "Webhook \u2013 Receive Send Job",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// Validate shared secret\nconst secret = $input.first().json.headers?.['x-phishsim-secret'];\nconst expected = $env['PHISHSIM_SECRET'];\nif (expected && secret !== expected) {\n  throw new Error('Unauthorized: bad secret');\n}\n\nconst body = $input.first().json.body;\n\nif (!body.template || !body.targets || !Array.isArray(body.targets)) {\n  throw new Error('Missing template or targets array');\n}\n\nreturn body.targets.map(target => ({\n  json: {\n    org_id: body.org_id,\n    campaign_id: body.campaign_id,\n    send_run_id: body.send_run_id,\n    target_id: target.id,\n    to_email: target.email,\n    subject: body.template.subject,\n    body_html: body.template.body_html,\n    body_text: body.template.body_text,\n    open_url: target.tracking.open_url,\n    click_url: target.tracking.click_url,\n    landing_url: target.tracking.landing_url,\n    token: target.tracking.token,\n  }\n}));"
      },
      "id": "split-targets",
      "name": "Split Into Individual Emails",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        460,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// Process ALL items (one per target)\nreturn $input.all().map(inputItem => {\n  const item = inputItem.json;\n\n  let html = item.body_html || '';\n  let text = item.body_text || '';\n\n  // Fallback: if body_html is empty, build from body_text\n  if (!html.trim()) {\n    html = `<!DOCTYPE html><html><body style=\"font-family:Arial,sans-serif;font-size:14px;line-height:1.6;color:#222;max-width:600px;margin:0 auto;padding:20px\"><pre style=\"white-space:pre-wrap;font-family:inherit\">${text}</pre></body></html>`;\n  }\n\n  // Replace {{CLICK_URL}} placeholder (from AI-generated HTML)\n  const hasClickPlaceholder = html.includes('{{CLICK_URL}}') || html.includes('%7B%7BCLICK_URL%7D%7D');\n  html = html.replace(/\\{\\{CLICK_URL\\}\\}/g, item.click_url);\n\n  // Replace {{LANDING_URL}} placeholder\n  html = html.replace(/\\{\\{LANDING_URL\\}\\}/g, item.landing_url);\n  text = text.replace(/\\{\\{LANDING_URL\\}\\}/g, item.landing_url);\n\n  // If no {{CLICK_URL}} placeholder existed, inject a fallback CTA button\n  const ctaButton = `<div style=\"text-align:center;margin:28px 0;padding:0 20px\"><a href=\"${item.click_url}\" style=\"display:inline-block;background:#1a73e8;color:#ffffff;padding:12px 28px;text-decoration:none;border-radius:5px;font-family:Arial,sans-serif;font-size:15px;font-weight:bold\">Take Action Now &rarr;</a></div>`;\n\n  // Inject fallback CTA before </body>\n  const inject = (hasClickPlaceholder ? '' : ctaButton);\n\n  if (html.includes('</body>')) {\n    html = html.replace('</body>', inject + '</body>');\n  } else {\n    html = html + inject;\n  }\n\n  return { json: { ...item, body_html: html, body_text: text } };\n});"
      },
      "id": "inject-tracking",
      "name": "Inject Tracking Links",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        680,
        300
      ]
    },
    {
      "parameters": {
        "fromEmail": "={{ $env['SMTP_FROM_EMAIL'] || 'phishsim@localhost' }}",
        "toEmail": "={{ $json.to_email }}",
        "subject": "={{ $json.subject }}",
        "html": "={{ $json.body_html }}",
        "text": "={{ $json.body_text }}",
        "options": {
          "allowUnauthorizedCerts": true
        }
      },
      "id": "send-email",
      "name": "Send Email via SMTP",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 1,
      "position": [
        900,
        200
      ],
      "continueOnFail": true,
      "credentials": {
        "smtp": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Report delivery for ALL targets in parallel with emailSend\n// Data comes directly from Inject Tracking (not after emailSend)\nconst appUrl = ($env['APP_URL'] || 'http://host.docker.internal:3000');\nconst secret = $env['PHISHSIM_SECRET'] || '';\n\nconst results = [];\nfor (const inputItem of $input.all()) {\n  const item = inputItem.json;\n  try {\n    await this.helpers.request({\n      method: 'POST',\n      uri: `${appUrl}/api/webhooks/n8n/events`,\n      headers: {\n        'Content-Type': 'application/json',\n        'x-phishsim-secret': secret\n      },\n      body: {\n        org_id: item.org_id,\n        campaign_id: item.campaign_id,\n        target_id: item.target_id,\n        type: 'delivered',\n        metadata: { subject: item.subject }\n      },\n      json: true\n    });\n  } catch(e) {\n    console.log('Delivery callback failed for', item.to_email, ':', e.message);\n  }\n  results.push({ json: item });\n}\nreturn results;"
      },
      "id": "report-delivered",
      "name": "Report Delivered to App",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        900,
        400
      ],
      "continueOnFail": true
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "{\"status\": \"queued\"}",
        "options": {
          "responseCode": 200
        }
      },
      "id": "respond-ok",
      "name": "Respond \u2013 Accepted",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [
        460,
        480
      ]
    }
  ],
  "connections": {
    "Webhook \u2013 Receive Send Job": {
      "main": [
        [
          {
            "node": "Split Into Individual Emails",
            "type": "main",
            "index": 0
          },
          {
            "node": "Respond \u2013 Accepted",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Into Individual Emails": {
      "main": [
        [
          {
            "node": "Inject Tracking Links",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Inject Tracking Links": {
      "main": [
        [
          {
            "node": "Send Email via SMTP",
            "type": "main",
            "index": 0
          },
          {
            "node": "Report Delivered to App",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "staticData": null,
  "tags": [
    "phishsim"
  ]
}