344 lines
11 KiB
JSON
344 lines
11 KiB
JSON
|
|
{
|
||
|
|
"name": "WFDASH — Quote Outbound Send",
|
||
|
|
"description": "Sends approved quote emails via SMTP. Triggered when Next.js flips a quote to status='sending'. Records success (→ emailed) or failure (→ mail_failed) back to PocketBase with the SMTP response.",
|
||
|
|
"tags": [
|
||
|
|
{
|
||
|
|
"name": "wfdash",
|
||
|
|
"id": "wf-tag-wfdash"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"nodes": [
|
||
|
|
{
|
||
|
|
"parameters": {
|
||
|
|
"events": [
|
||
|
|
"update"
|
||
|
|
],
|
||
|
|
"collection": "quotes",
|
||
|
|
"filter": "status='sending'",
|
||
|
|
"options": {
|
||
|
|
"expand": "customer"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"id": "trigger-pocketbase-quote-sending",
|
||
|
|
"name": "PB: Quote status → sending",
|
||
|
|
"type": "n8n-nodes-pocketbase.pocketbaseTrigger",
|
||
|
|
"typeVersion": 1,
|
||
|
|
"position": [
|
||
|
|
0,
|
||
|
|
0
|
||
|
|
],
|
||
|
|
"webhookId": "wf-quote-outbound-send-trigger",
|
||
|
|
"credentials": {
|
||
|
|
"pocketbaseApi": {
|
||
|
|
"id": "PB_CRED_ID",
|
||
|
|
"name": "PocketBase account"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"parameters": {
|
||
|
|
"conditions": {
|
||
|
|
"options": {
|
||
|
|
"caseSensitive": true,
|
||
|
|
"typeValidation": "loose",
|
||
|
|
"version": 3
|
||
|
|
},
|
||
|
|
"conditions": [
|
||
|
|
{
|
||
|
|
"leftValue": "={{ $json.record.status }}",
|
||
|
|
"rightValue": "sending",
|
||
|
|
"operator": {
|
||
|
|
"type": "string",
|
||
|
|
"operation": "equals"
|
||
|
|
},
|
||
|
|
"id": "cond-status-sending"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"leftValue": "={{ $json.record.email_message_id }}",
|
||
|
|
"rightValue": "",
|
||
|
|
"operator": {
|
||
|
|
"type": "string",
|
||
|
|
"operation": "notEquals"
|
||
|
|
},
|
||
|
|
"id": "cond-not-already-sent"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"combinator": "and"
|
||
|
|
},
|
||
|
|
"options": {}
|
||
|
|
},
|
||
|
|
"id": "if-status-sending",
|
||
|
|
"name": "If status == sending",
|
||
|
|
"type": "n8n-nodes-base.if",
|
||
|
|
"typeVersion": 2.2,
|
||
|
|
"position": [
|
||
|
|
220,
|
||
|
|
0
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"parameters": {
|
||
|
|
"method": "POST",
|
||
|
|
"url": "={{ $env.WFDASH_API_BASE_URL }}/api/mcp",
|
||
|
|
"sendHeaders": true,
|
||
|
|
"headerParameters": {
|
||
|
|
"parameters": [
|
||
|
|
{
|
||
|
|
"name": "Content-Type",
|
||
|
|
"value": "application/json"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"name": "X-Agent-Api-Key",
|
||
|
|
"value": "={{ $env.AGENT_API_KEY }}"
|
||
|
|
}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"sendBody": true,
|
||
|
|
"specifyBody": "json",
|
||
|
|
"jsonBody": "={\n \"jsonrpc\": \"2.0\",\n \"method\": \"tools/call\",\n \"params\": {\n \"name\": \"quotes.get_send_payload\",\n \"arguments\": { \"quote_id\": \"{{ $json.record.id }}\" }\n },\n \"id\": \"1\"\n}",
|
||
|
|
"options": {
|
||
|
|
"response": {
|
||
|
|
"response": {
|
||
|
|
"responseFormat": "json"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"timeout": 15000
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"id": "mcp-fetch-send-payload",
|
||
|
|
"name": "MCP: Get send payload",
|
||
|
|
"type": "n8n-nodes-base.httpRequest",
|
||
|
|
"typeVersion": 4.2,
|
||
|
|
"position": [
|
||
|
|
440,
|
||
|
|
-120
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"parameters": {
|
||
|
|
"jsCode": "// Compose the MIME message from the MCP payload. n8n's Send Email\n// node handles attachments + headers natively, so we only need to\n// shape the data it consumes.\nconst payload = $json.result?.data || $json.result || $json;\nconst quote = payload.quote || {};\nconst customer = quote.expand?.customer || quote.customer || {};\n\n// Build a deterministic RFC 5322 Message-ID per send. The format\n// embeds the quote id + epoch ms so retries get a different id\n// (RFC 5322 requires uniqueness).\nconst now = new Date();\nconst epoch = now.getTime();\nconst nonce = Math.random().toString(36).slice(2, 10);\nconst messageId = `<quote-${quote.id}-${epoch}-${nonce}@wfdash.local>`;\n\n// Pull the operator's persisted composition (saved at modal submit\n// time). Defaults come from MCP payload.\nconst subject = payload.subject || `Quote ${quote.quote_number}`;\nconst text = payload.message || `Please find attached quote ${quote.quote_number}.`;\n\n// Convert the JSON-encoded attachment IDs into a list of file URLs.\n// n8n's HTTP Request nodes download each file into $binary.\nconst attIds = Array.isArray(payload.selected_attachment_ids)\n ? payload.selected_attachment_ids\n : [];\nconst pdfId = payload.pdf_attachment_id;\nconst allIds = pdfId ? [pdfId, ...attIds.filter((x) => x !== pdfId)] : attIds;\n\nreturn {\n json: {\n quote_id: quote.id,\n quote_number: quote.quote_number,\n to: payload.to || customer.email,\n cc: payload.cc || \"\",\n bcc: payload.bcc || \"\",\n subject,\n text,\n from_name: payload.from_name || quote.company_letterhead || \"WFDASH\",\n attachment_ids: allIds,\n message_id: messageId,\n compose_ts: now.toISOString()\n }\n};\n"
|
||
|
|
},
|
||
|
|
"id": "code-format-email",
|
||
|
|
"name": "Format email params",
|
||
|
|
"type": "n8n-nodes-base.code",
|
||
|
|
"typeVersion": 2,
|
||
|
|
"position": [
|
||
|
|
660,
|
||
|
|
-120
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"parameters": {
|
||
|
|
"method": "GET",
|
||
|
|
"url": "={{ $env.NEXT_PUBLIC_POCKETBASE_URL }}/api/files/quote_attachments/{{ $json.attachment_id }}/{{ $json.filename }}",
|
||
|
|
"sendHeaders": true,
|
||
|
|
"headerParameters": {
|
||
|
|
"parameters": [
|
||
|
|
{
|
||
|
|
"name": "Authorization",
|
||
|
|
"value": "={{ $env.PB_ADMIN_TOKEN }}"
|
||
|
|
}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"options": {
|
||
|
|
"response": {
|
||
|
|
"response": {
|
||
|
|
"responseFormat": "file"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"timeout": 30000
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"id": "http-fetch-attachment",
|
||
|
|
"type": "n8n-nodes-base.httpRequest",
|
||
|
|
"typeVersion": 4.2,
|
||
|
|
"position": [
|
||
|
|
880,
|
||
|
|
-120
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"parameters": {
|
||
|
|
"fromEmail": "=quotes@wfsales.co.za",
|
||
|
|
"toEmail": "={{ $json.to }}",
|
||
|
|
"subject": "={{ $json.subject }}",
|
||
|
|
"emailFormat": "text",
|
||
|
|
"text": "={{ $json.text }}",
|
||
|
|
"options": {
|
||
|
|
"appendAttribution": false,
|
||
|
|
"ccEmail": "={{ $json.cc }}",
|
||
|
|
"bccEmail": "={{ $json.bcc }}",
|
||
|
|
"replyTo": "quotes@wfsales.co.za",
|
||
|
|
"attachments": "={{ $binary }}"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"id": "send-email-smtp",
|
||
|
|
"name": "Send Email (SMTP)",
|
||
|
|
"type": "n8n-nodes-base.emailSend",
|
||
|
|
"typeVersion": 2.1,
|
||
|
|
"position": [
|
||
|
|
1100,
|
||
|
|
-120
|
||
|
|
],
|
||
|
|
"retryOnFail": true,
|
||
|
|
"maxTries": 3,
|
||
|
|
"waitBetweenTries": 5000,
|
||
|
|
"onError": "continueRegularOutput",
|
||
|
|
"credentials": {
|
||
|
|
"smtp": {
|
||
|
|
"id": "SMTP_CRED_ID",
|
||
|
|
"name": "WFDASH Outbound SMTP"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"parameters": {
|
||
|
|
"method": "POST",
|
||
|
|
"url": "={{ $env.NEXT_PUBLIC_POCKETBASE_URL }}/api/collections/pbc_2527524235/records/{{ $node[\"Format email params\"].json.quote_id }}",
|
||
|
|
"sendHeaders": true,
|
||
|
|
"headerParameters": {
|
||
|
|
"parameters": [
|
||
|
|
{
|
||
|
|
"name": "Content-Type",
|
||
|
|
"value": "application/json"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"name": "Authorization",
|
||
|
|
"value": "={{ $env.PB_ADMIN_TOKEN }}"
|
||
|
|
}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"sendBody": true,
|
||
|
|
"specifyBody": "json",
|
||
|
|
"jsonBody": "={\n \"status\": \"emailed\",\n \"email_delivered\": true,\n \"email_delivered_at\": \"{{ $now.toISO() }}\",\n \"email_outbound_message_id\": \"{{ $node[\"Format email params\"].json.message_id }}\",\n \"email_error\": \"\",\n \"sending_started_at\": \"\"\n}",
|
||
|
|
"options": {
|
||
|
|
"timeout": 15000
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"id": "patch-quote-emailed",
|
||
|
|
"name": "PATCH: quote → emailed",
|
||
|
|
"type": "n8n-nodes-base.httpRequest",
|
||
|
|
"typeVersion": 4.2,
|
||
|
|
"position": [
|
||
|
|
1320,
|
||
|
|
-200
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"parameters": {
|
||
|
|
"method": "POST",
|
||
|
|
"url": "={{ $env.NEXT_PUBLIC_POCKETBASE_URL }}/api/collections/pbc_2527524235/records/{{ $node[\"Format email params\"].json.quote_id }}",
|
||
|
|
"sendHeaders": true,
|
||
|
|
"headerParameters": {
|
||
|
|
"parameters": [
|
||
|
|
{
|
||
|
|
"name": "Content-Type",
|
||
|
|
"value": "application/json"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"name": "Authorization",
|
||
|
|
"value": "={{ $env.PB_ADMIN_TOKEN }}"
|
||
|
|
}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"sendBody": true,
|
||
|
|
"specifyBody": "json",
|
||
|
|
"jsonBody": "={\n \"status\": \"mail_failed\",\n \"email_failed_at\": \"{{ $now.toISO() }}\",\n \"email_error\": \"[smtp] {{ ($json.error?.message || $json.message || 'Unknown SMTP failure').toString().slice(0, 4900) }}\",\n \"email_outbound_message_id\": \"{{ $node[\"Format email params\"].json.message_id }}\",\n \"sending_started_at\": \"\"\n}",
|
||
|
|
"options": {
|
||
|
|
"timeout": 15000
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"id": "patch-quote-mail-failed",
|
||
|
|
"name": "PATCH: quote → mail_failed",
|
||
|
|
"type": "n8n-nodes-base.httpRequest",
|
||
|
|
"typeVersion": 4.2,
|
||
|
|
"position": [
|
||
|
|
1320,
|
||
|
|
-40
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"connections": {
|
||
|
|
"PB: Quote status → sending": {
|
||
|
|
"main": [
|
||
|
|
[
|
||
|
|
{
|
||
|
|
"node": "If status == sending",
|
||
|
|
"type": "main",
|
||
|
|
"index": 0
|
||
|
|
}
|
||
|
|
]
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"If status == sending": {
|
||
|
|
"main": [
|
||
|
|
[
|
||
|
|
{
|
||
|
|
"node": "MCP: Get send payload",
|
||
|
|
"type": "main",
|
||
|
|
"index": 0
|
||
|
|
}
|
||
|
|
]
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"MCP: Get send payload": {
|
||
|
|
"main": [
|
||
|
|
[
|
||
|
|
{
|
||
|
|
"node": "Format email params",
|
||
|
|
"type": "main",
|
||
|
|
"index": 0
|
||
|
|
}
|
||
|
|
]
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"Format email params": {
|
||
|
|
"main": [
|
||
|
|
[
|
||
|
|
{
|
||
|
|
"node": "Send Email (SMTP)",
|
||
|
|
"type": "main",
|
||
|
|
"index": 0
|
||
|
|
}
|
||
|
|
]
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"Send Email (SMTP)": {
|
||
|
|
"main": [
|
||
|
|
[
|
||
|
|
{
|
||
|
|
"node": "PATCH: quote → emailed",
|
||
|
|
"type": "main",
|
||
|
|
"index": 0
|
||
|
|
}
|
||
|
|
]
|
||
|
|
],
|
||
|
|
"error": [
|
||
|
|
[
|
||
|
|
{
|
||
|
|
"node": "PATCH: quote → mail_failed",
|
||
|
|
"type": "main",
|
||
|
|
"index": 0
|
||
|
|
}
|
||
|
|
]
|
||
|
|
]
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"settings": {
|
||
|
|
"executionOrder": "v1",
|
||
|
|
"saveDataSuccessExecution": "all",
|
||
|
|
"saveDataErrorExecution": "all",
|
||
|
|
"saveManualExecutions": true,
|
||
|
|
"saveExecutionProgress": true,
|
||
|
|
"timezone": "Africa/Johannesburg"
|
||
|
|
},
|
||
|
|
"staticData": null,
|
||
|
|
"pinData": {},
|
||
|
|
"versionId": "1",
|
||
|
|
"triggerCount": 1,
|
||
|
|
"meta": {
|
||
|
|
"instanceId": "wfdash-quote-outbound-send",
|
||
|
|
"templateCredsSetupCompleted": false,
|
||
|
|
"description": "SMTP creds + PocketBase creds + WFDASH API base URL + AGENT_API_KEY env vars must be configured in n8n before activation. Add a new SMTP credential named 'WFDASH Outbound SMTP' and a PocketBase credential named 'PocketBase account' before publishing."
|
||
|
|
}
|
||
|
|
}
|