AutomationFlowsEmail & Gmail › Mailhog Automation

Mailhog Automation

Mailhog Automation. Uses httpRequest, microsoftOutlook. Scheduled trigger; 13 nodes.

Cron / scheduled trigger★★★★☆ complexity13 nodesHTTP RequestMicrosoft Outlook
Email & Gmail Trigger: Cron / scheduled Nodes: 13 Complexity: ★★★★☆ Added:

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "updatedAt": "2026-05-23T16:44:43.718Z",
  "createdAt": "2026-05-23T08:03:31.386Z",
  "id": "WYrrjxvvNjxmj4JF",
  "name": "Mailhog Automation",
  "active": true,
  "isArchived": false,
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "seconds",
              "secondsInterval": 10
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [
        0,
        0
      ],
      "id": "79aa5209-2002-49a9-afb1-5af291ab7032",
      "name": "Schedule Trigger"
    },
    {
      "parameters": {
        "url": "http://172.20.0.1:8025/api/v1/messages",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        208,
        0
      ],
      "id": "60ddab96-7894-441a-998a-841c1edfe3ad",
      "name": "Get messages"
    },
    {
      "parameters": {
        "jsCode": "return JSON.parse($input.first().json.data);"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        416,
        0
      ],
      "id": "349c5664-53d0-4ce2-b780-e7cf697861fa",
      "name": "Parse data json string"
    },
    {
      "parameters": {
        "jsCode": "return JSON.parse($input.first().json.data);"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1072,
        128
      ],
      "id": "cd8fab24-bf06-428d-afe4-760d61a56abf",
      "name": "Parse data json string1"
    },
    {
      "parameters": {
        "url": "=http://172.20.0.1:8025/api/v1/messages/{{ $json.ID }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        624,
        0
      ],
      "id": "ff0287c0-983e-430e-9924-90edf4933e5d",
      "name": "Get message details"
    },
    {
      "parameters": {
        "jsCode": "// Compose message \u2014 Code node (Run Once for All Items)\n// Input:  MailHog message detail (parsed JSON)\n// Output: json { subject, from, toRecipients, bodyHtml, bodyText, attachments[], attachmentCount }\n//         attachments[] entries: { fileName, mimeType, data (base64 string), contentId, isInline }\n\nfunction decodeQuotedPrintable(s) {\n  return s\n    .replace(/=\\r?\\n/g, '')\n    .replace(/=([0-9A-Fa-f]{2})/g, (_, h) => String.fromCharCode(parseInt(h, 16)));\n}\n\nfunction header(headers, name) {\n  if (!headers) return '';\n  const k = Object.keys(headers).find(x => x.toLowerCase() === name.toLowerCase());\n  return k ? (headers[k][0] || '') : '';\n}\n\nfunction getCharset(ct) {\n  const m = /charset=([^;\\s]+)/i.exec(ct || '');\n  return m ? m[1].replace(/^\"|\"$/g, '').toLowerCase() : 'utf-8';\n}\n\nfunction decodeText(part) {\n  const enc = header(part.Headers, 'Content-Transfer-Encoding').toLowerCase();\n  const charset = getCharset(header(part.Headers, 'Content-Type'));\n  if (enc === 'quoted-printable') {\n    return Buffer.from(decodeQuotedPrintable(part.Body || ''), 'binary').toString(charset);\n  }\n  if (enc === 'base64') {\n    return Buffer.from((part.Body || '').replace(/\\s+/g, ''), 'base64').toString(charset);\n  }\n  return part.Body || '';\n}\n\nfunction flattenParts(mime) {\n  if (!mime || !Array.isArray(mime.Parts)) return [];\n  const out = [];\n  for (const p of mime.Parts) {\n    if (p.MIME && Array.isArray(p.MIME.Parts) && p.MIME.Parts.length > 0) {\n      out.push(...flattenParts(p.MIME));\n    } else {\n      out.push(p);\n    }\n  }\n  return out;\n}\n\nconst MIME_EXT = {\n  'image/jpeg': 'jpg',\n  'image/jpg': 'jpg',\n  'image/png': 'png',\n  'image/gif': 'gif',\n  'image/webp': 'webp',\n  'image/svg+xml': 'svg',\n  'image/bmp': 'bmp',\n  'image/tiff': 'tiff',\n  'image/x-icon': 'ico',\n  'application/pdf': 'pdf',\n  'application/zip': 'zip',\n  'application/json': 'json',\n  'application/xml': 'xml',\n  'application/msword': 'doc',\n  'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',\n  'application/vnd.ms-excel': 'xls',\n  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx',\n  'application/vnd.ms-powerpoint': 'ppt',\n  'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'pptx',\n  'text/plain': 'txt',\n  'text/html': 'html',\n  'text/csv': 'csv',\n  'text/calendar': 'ics',\n  'application/octet-stream': 'bin',\n};\n\nfunction extFromMime(mimeType) {\n  if (!mimeType) return 'bin';\n  if (MIME_EXT[mimeType]) return MIME_EXT[mimeType];\n  const sub = (mimeType.split('/')[1] || 'bin').replace(/\\+.*$/, '');\n  return sub.replace(/[^a-z0-9]/gi, '').slice(0, 8) || 'bin';\n}\n\nfunction ensureExt(name, ext) {\n  return /\\.[a-z0-9]{1,8}$/i.test(name) ? name : `${name}.${ext}`;\n}\n\nfunction pickFilename(part, mimeType, fallback) {\n  const ext = extFromMime(mimeType);\n  const cd = header(part.Headers, 'Content-Disposition');\n  let m = /filename\\*?=(?:\"([^\"]+)\"|([^;\\s]+))/i.exec(cd);\n  if (m) return ensureExt(m[1] || m[2], ext);\n  const ct = header(part.Headers, 'Content-Type');\n  m = /name=(?:\"([^\"]+)\"|([^;\\s]+))/i.exec(ct);\n  if (m) return ensureExt(m[1] || m[2], ext);\n  const cid = header(part.Headers, 'Content-ID').replace(/^<|>$/g, '').trim();\n  if (cid) return ensureExt(cid, ext);\n  return `${fallback}.${ext}`;\n}\n\nfunction addr(box) {\n  return box && box.Mailbox && box.Domain ? `${box.Mailbox}@${box.Domain}` : '';\n}\n\nconst SKIP_TYPES = new Set([\n  'application/pkcs7-signature',\n  'application/x-pkcs7-signature',\n  'application/pgp-signature',\n  'application/pgp-encrypted',\n  'application/ms-tnef',\n  'application/vnd.ms-tnef',\n  'application/applefile',\n  'message/delivery-status',\n  'message/disposition-notification',\n  'message/feedback-report',\n  'text/rfc822-headers',\n]);\n\nfunction hasNamingHint(part) {\n  const cd = header(part.Headers, 'Content-Disposition');\n  if (/filename\\*?=/i.test(cd)) return true;\n  const ct = header(part.Headers, 'Content-Type');\n  if (/name=/i.test(ct)) return true;\n  if (header(part.Headers, 'Content-ID').trim()) return true;\n  return false;\n}\n\nconst results = [];\nconst items = $input.all();\n\nfor (let i = 0; i < items.length; i++) {\n  const m = items[i].json || {};\n  const hdrs = m.Content?.Headers || {};\n  const subject = hdrs.Subject?.[0] || '(no subject)';\n  const from = addr(m.From);\n  const toRecipients = (m.To || []).map(addr).filter(Boolean).join(',');\n  const parts = flattenParts(m.MIME);\n\n  let bodyHtml = '';\n  let bodyText = '';\n  const attachments = [];\n\n  for (const part of parts) {\n    const ct = header(part.Headers, 'Content-Type').toLowerCase();\n    const enc = header(part.Headers, 'Content-Transfer-Encoding').toLowerCase();\n    const cd = header(part.Headers, 'Content-Disposition').toLowerCase();\n    const mimeType = ct.split(';')[0].trim() || 'application/octet-stream';\n    const isAttachmentDisposition = cd.startsWith('attachment') || cd.startsWith('inline');\n\n    if (ct.startsWith('text/html') && !bodyHtml && !isAttachmentDisposition) {\n      bodyHtml = decodeText(part);\n      continue;\n    }\n    if (ct.startsWith('text/plain') && !bodyText && !isAttachmentDisposition) {\n      bodyText = decodeText(part);\n      continue;\n    }\n    // Drop extra text/* alternatives that aren't real attachments.\n    if ((ct.startsWith('text/plain') || ct.startsWith('text/html')) && !isAttachmentDisposition) {\n      continue;\n    }\n    if (SKIP_TYPES.has(mimeType)) continue;\n    if (mimeType.startsWith('multipart/') || mimeType.startsWith('message/')) continue;\n    if (!part.Body || !part.Body.trim()) continue;\n    // Final guard: nameless + type-less blobs are almost always junk MIME scaffolding.\n    if (!hasNamingHint(part) && (mimeType === 'application/octet-stream' || mimeType === '')) continue;\n\n    let buf;\n    if (enc === 'base64') {\n      buf = Buffer.from((part.Body || '').replace(/\\s+/g, ''), 'base64');\n    } else if (enc === 'quoted-printable') {\n      buf = Buffer.from(decodeQuotedPrintable(part.Body || ''), 'binary');\n    } else {\n      buf = Buffer.from(part.Body || '', 'binary');\n    }\n    if (!buf.length) continue;\n\n    const fileName = pickFilename(part, mimeType, `attachment-${attachments.length}`);\n    const contentId = header(part.Headers, 'Content-ID').replace(/^<|>$/g, '').trim();\n    const isInline = cd.startsWith('inline') || !!contentId;\n    attachments.push({\n      fileName,\n      mimeType,\n      data: buf.toString('base64'),\n      contentId: contentId || null,\n      isInline,\n    });\n  }\n\n  results.push({\n    json: {\n      subject,\n      from,\n      toRecipients,\n      bodyHtml,\n      bodyText,\n      attachments,\n      attachmentCount: attachments.length,\n    },\n    pairedItem: { item: i },\n  });\n}\n\nreturn results;\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1280,
        128
      ],
      "id": "db257c62-8d60-41ab-a196-4748a38f98a1",
      "name": "Compose message"
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        848,
        0
      ],
      "id": "93821958-7257-48be-9833-1f0bdd1f97c7",
      "name": "Loop Over Items"
    },
    {
      "parameters": {
        "resource": "draft",
        "subject": "={{ $json.subject }}",
        "bodyContent": "={{ $json.bodyHtml }}",
        "additionalFields": {
          "from": "={{ $json.from }}",
          "bodyContentType": "html",
          "toRecipients": "={{ $json.toRecipients }}"
        }
      },
      "type": "n8n-nodes-base.microsoftOutlook",
      "typeVersion": 2,
      "position": [
        1488,
        128
      ],
      "id": "406306fc-1d59-4df2-9b4d-302049091a09",
      "name": "Create a draft",
      "credentials": {
        "microsoftOutlookOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Split out attachments \u2014 Code node (Run Once for All Items)\n// Reads attachments[] from Compose message json + draft id from Create a draft.\n// Emits one item per attachment, with the FULL Graph request body in json.\n// No binary \u2014 downstream HTTP Request node POSTs json directly to Graph.\n\nconst composeItem = $('Compose message').item;\nconst draftItem = $('Create a draft').item;\n\nconst messageId = draftItem.json.id;\nconst attachments = composeItem.json.attachments || [];\nconst outputItems = [];\n\nfor (const att of attachments) {\n  const body = {\n    '@odata.type': '#microsoft.graph.fileAttachment',\n    name: att.fileName,\n    contentType: att.mimeType,\n    contentBytes: att.data,\n    isInline: !!att.isInline,\n  };\n  if (att.contentId) body.contentId = att.contentId;\n\n  outputItems.push({\n    json: {\n      messageId,\n      url: `https://graph.microsoft.com/v1.0/me/messages/${messageId}/attachments`,\n      body,\n    },\n  });\n}\n\nreturn outputItems;\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1680,
        128
      ],
      "id": "e7f504aa-61cc-4b18-9b9f-a4122910bcbd",
      "name": "Split out attachments"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $json.url }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "microsoftOutlookOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ $json.body }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1888,
        128
      ],
      "id": "bd50f60c-c7f3-4ed5-b072-b64387c4736e",
      "name": "Add attachment",
      "retryOnFail": true,
      "maxTries": 5,
      "credentials": {
        "microsoftOutlookOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "resource": "draft",
        "operation": "send",
        "draftId": {
          "__rl": true,
          "value": "={{ $('Create a draft').item.json.id }}",
          "mode": "id"
        }
      },
      "type": "n8n-nodes-base.microsoftOutlook",
      "typeVersion": 2,
      "position": [
        2288,
        128
      ],
      "id": "28b36940-c2a0-4845-a22b-66b413fd1f43",
      "name": "Send a draft",
      "credentials": {
        "microsoftOutlookOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "DELETE",
        "url": "=http://172.20.0.1:8025/api/v1/messages/{{ $('Parse data json string1').item.json.ID }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        2480,
        128
      ],
      "id": "f4eb3856-6a5e-4a0f-8399-7e8beddcac22",
      "name": "Delete message in Mailhog"
    },
    {
      "parameters": {
        "fieldsToAggregate": {
          "fieldToAggregate": [
            {}
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.aggregate",
      "typeVersion": 1,
      "position": [
        2096,
        128
      ],
      "id": "42becb63-8ff6-46c2-95c1-642c0fafdf52",
      "name": "Aggregate",
      "alwaysOutputData": true
    }
  ],
  "connections": {
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Get messages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get messages": {
      "main": [
        [
          {
            "node": "Parse data json string",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse data json string": {
      "main": [
        [
          {
            "node": "Get message details",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get message details": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse data json string1": {
      "main": [
        [
          {
            "node": "Compose message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compose message": {
      "main": [
        [
          {
            "node": "Create a draft",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [],
        [
          {
            "node": "Parse data json string1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create a draft": {
      "main": [
        [
          {
            "node": "Split out attachments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split out attachments": {
      "main": [
        [
          {
            "node": "Add attachment",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add attachment": {
      "main": [
        [
          {
            "node": "Aggregate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send a draft": {
      "main": [
        [
          {
            "node": "Delete message in Mailhog",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Delete message in Mailhog": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate": {
      "main": [
        [
          {
            "node": "Send a draft",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate"
  },
  "staticData": {
    "node:Schedule Trigger": {
      "recurrenceRules": []
    }
  },
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "versionId": "4eaf38d1-9503-4cbc-a8f9-9c6c33243ecd",
  "activeVersionId": "4eaf38d1-9503-4cbc-a8f9-9c6c33243ecd",
  "triggerCount": 1,
  "shared": [
    {
      "updatedAt": "2026-05-23T08:03:31.386Z",
      "createdAt": "2026-05-23T08:03:31.386Z",
      "role": "workflow:owner",
      "workflowId": "WYrrjxvvNjxmj4JF",
      "projectId": "awdxw69L9IK2yvbS"
    }
  ],
  "activeVersion": {
    "updatedAt": "2026-05-23T16:44:45.680Z",
    "createdAt": "2026-05-23T16:44:43.719Z",
    "versionId": "4eaf38d1-9503-4cbc-a8f9-9c6c33243ecd",
    "workflowId": "WYrrjxvvNjxmj4JF",
    "nodes": [
      {
        "parameters": {
          "rule": {
            "interval": [
              {
                "field": "seconds",
                "secondsInterval": 10
              }
            ]
          }
        },
        "type": "n8n-nodes-base.scheduleTrigger",
        "typeVersion": 1.3,
        "position": [
          0,
          0
        ],
        "id": "79aa5209-2002-49a9-afb1-5af291ab7032",
        "name": "Schedule Trigger"
      },
      {
        "parameters": {
          "url": "http://172.20.0.1:8025/api/v1/messages",
          "options": {}
        },
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.4,
        "position": [
          208,
          0
        ],
        "id": "60ddab96-7894-441a-998a-841c1edfe3ad",
        "name": "Get messages"
      },
      {
        "parameters": {
          "jsCode": "return JSON.parse($input.first().json.data);"
        },
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          416,
          0
        ],
        "id": "349c5664-53d0-4ce2-b780-e7cf697861fa",
        "name": "Parse data json string"
      },
      {
        "parameters": {
          "jsCode": "return JSON.parse($input.first().json.data);"
        },
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          1072,
          128
        ],
        "id": "cd8fab24-bf06-428d-afe4-760d61a56abf",
        "name": "Parse data json string1"
      },
      {
        "parameters": {
          "url": "=http://172.20.0.1:8025/api/v1/messages/{{ $json.ID }}",
          "options": {}
        },
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.4,
        "position": [
          624,
          0
        ],
        "id": "ff0287c0-983e-430e-9924-90edf4933e5d",
        "name": "Get message details"
      },
      {
        "parameters": {
          "jsCode": "// Compose message \u2014 Code node (Run Once for All Items)\n// Input:  MailHog message detail (parsed JSON)\n// Output: json { subject, from, toRecipients, bodyHtml, bodyText, attachments[], attachmentCount }\n//         attachments[] entries: { fileName, mimeType, data (base64 string), contentId, isInline }\n\nfunction decodeQuotedPrintable(s) {\n  return s\n    .replace(/=\\r?\\n/g, '')\n    .replace(/=([0-9A-Fa-f]{2})/g, (_, h) => String.fromCharCode(parseInt(h, 16)));\n}\n\nfunction header(headers, name) {\n  if (!headers) return '';\n  const k = Object.keys(headers).find(x => x.toLowerCase() === name.toLowerCase());\n  return k ? (headers[k][0] || '') : '';\n}\n\nfunction getCharset(ct) {\n  const m = /charset=([^;\\s]+)/i.exec(ct || '');\n  return m ? m[1].replace(/^\"|\"$/g, '').toLowerCase() : 'utf-8';\n}\n\nfunction decodeText(part) {\n  const enc = header(part.Headers, 'Content-Transfer-Encoding').toLowerCase();\n  const charset = getCharset(header(part.Headers, 'Content-Type'));\n  if (enc === 'quoted-printable') {\n    return Buffer.from(decodeQuotedPrintable(part.Body || ''), 'binary').toString(charset);\n  }\n  if (enc === 'base64') {\n    return Buffer.from((part.Body || '').replace(/\\s+/g, ''), 'base64').toString(charset);\n  }\n  return part.Body || '';\n}\n\nfunction flattenParts(mime) {\n  if (!mime || !Array.isArray(mime.Parts)) return [];\n  const out = [];\n  for (const p of mime.Parts) {\n    if (p.MIME && Array.isArray(p.MIME.Parts) && p.MIME.Parts.length > 0) {\n      out.push(...flattenParts(p.MIME));\n    } else {\n      out.push(p);\n    }\n  }\n  return out;\n}\n\nconst MIME_EXT = {\n  'image/jpeg': 'jpg',\n  'image/jpg': 'jpg',\n  'image/png': 'png',\n  'image/gif': 'gif',\n  'image/webp': 'webp',\n  'image/svg+xml': 'svg',\n  'image/bmp': 'bmp',\n  'image/tiff': 'tiff',\n  'image/x-icon': 'ico',\n  'application/pdf': 'pdf',\n  'application/zip': 'zip',\n  'application/json': 'json',\n  'application/xml': 'xml',\n  'application/msword': 'doc',\n  'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',\n  'application/vnd.ms-excel': 'xls',\n  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx',\n  'application/vnd.ms-powerpoint': 'ppt',\n  'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'pptx',\n  'text/plain': 'txt',\n  'text/html': 'html',\n  'text/csv': 'csv',\n  'text/calendar': 'ics',\n  'application/octet-stream': 'bin',\n};\n\nfunction extFromMime(mimeType) {\n  if (!mimeType) return 'bin';\n  if (MIME_EXT[mimeType]) return MIME_EXT[mimeType];\n  const sub = (mimeType.split('/')[1] || 'bin').replace(/\\+.*$/, '');\n  return sub.replace(/[^a-z0-9]/gi, '').slice(0, 8) || 'bin';\n}\n\nfunction ensureExt(name, ext) {\n  return /\\.[a-z0-9]{1,8}$/i.test(name) ? name : `${name}.${ext}`;\n}\n\nfunction pickFilename(part, mimeType, fallback) {\n  const ext = extFromMime(mimeType);\n  const cd = header(part.Headers, 'Content-Disposition');\n  let m = /filename\\*?=(?:\"([^\"]+)\"|([^;\\s]+))/i.exec(cd);\n  if (m) return ensureExt(m[1] || m[2], ext);\n  const ct = header(part.Headers, 'Content-Type');\n  m = /name=(?:\"([^\"]+)\"|([^;\\s]+))/i.exec(ct);\n  if (m) return ensureExt(m[1] || m[2], ext);\n  const cid = header(part.Headers, 'Content-ID').replace(/^<|>$/g, '').trim();\n  if (cid) return ensureExt(cid, ext);\n  return `${fallback}.${ext}`;\n}\n\nfunction addr(box) {\n  return box && box.Mailbox && box.Domain ? `${box.Mailbox}@${box.Domain}` : '';\n}\n\nconst SKIP_TYPES = new Set([\n  'application/pkcs7-signature',\n  'application/x-pkcs7-signature',\n  'application/pgp-signature',\n  'application/pgp-encrypted',\n  'application/ms-tnef',\n  'application/vnd.ms-tnef',\n  'application/applefile',\n  'message/delivery-status',\n  'message/disposition-notification',\n  'message/feedback-report',\n  'text/rfc822-headers',\n]);\n\nfunction hasNamingHint(part) {\n  const cd = header(part.Headers, 'Content-Disposition');\n  if (/filename\\*?=/i.test(cd)) return true;\n  const ct = header(part.Headers, 'Content-Type');\n  if (/name=/i.test(ct)) return true;\n  if (header(part.Headers, 'Content-ID').trim()) return true;\n  return false;\n}\n\nconst results = [];\nconst items = $input.all();\n\nfor (let i = 0; i < items.length; i++) {\n  const m = items[i].json || {};\n  const hdrs = m.Content?.Headers || {};\n  const subject = hdrs.Subject?.[0] || '(no subject)';\n  const from = addr(m.From);\n  const toRecipients = (m.To || []).map(addr).filter(Boolean).join(',');\n  const parts = flattenParts(m.MIME);\n\n  let bodyHtml = '';\n  let bodyText = '';\n  const attachments = [];\n\n  for (const part of parts) {\n    const ct = header(part.Headers, 'Content-Type').toLowerCase();\n    const enc = header(part.Headers, 'Content-Transfer-Encoding').toLowerCase();\n    const cd = header(part.Headers, 'Content-Disposition').toLowerCase();\n    const mimeType = ct.split(';')[0].trim() || 'application/octet-stream';\n    const isAttachmentDisposition = cd.startsWith('attachment') || cd.startsWith('inline');\n\n    if (ct.startsWith('text/html') && !bodyHtml && !isAttachmentDisposition) {\n      bodyHtml = decodeText(part);\n      continue;\n    }\n    if (ct.startsWith('text/plain') && !bodyText && !isAttachmentDisposition) {\n      bodyText = decodeText(part);\n      continue;\n    }\n    // Drop extra text/* alternatives that aren't real attachments.\n    if ((ct.startsWith('text/plain') || ct.startsWith('text/html')) && !isAttachmentDisposition) {\n      continue;\n    }\n    if (SKIP_TYPES.has(mimeType)) continue;\n    if (mimeType.startsWith('multipart/') || mimeType.startsWith('message/')) continue;\n    if (!part.Body || !part.Body.trim()) continue;\n    // Final guard: nameless + type-less blobs are almost always junk MIME scaffolding.\n    if (!hasNamingHint(part) && (mimeType === 'application/octet-stream' || mimeType === '')) continue;\n\n    let buf;\n    if (enc === 'base64') {\n      buf = Buffer.from((part.Body || '').replace(/\\s+/g, ''), 'base64');\n    } else if (enc === 'quoted-printable') {\n      buf = Buffer.from(decodeQuotedPrintable(part.Body || ''), 'binary');\n    } else {\n      buf = Buffer.from(part.Body || '', 'binary');\n    }\n    if (!buf.length) continue;\n\n    const fileName = pickFilename(part, mimeType, `attachment-${attachments.length}`);\n    const contentId = header(part.Headers, 'Content-ID').replace(/^<|>$/g, '').trim();\n    const isInline = cd.startsWith('inline') || !!contentId;\n    attachments.push({\n      fileName,\n      mimeType,\n      data: buf.toString('base64'),\n      contentId: contentId || null,\n      isInline,\n    });\n  }\n\n  results.push({\n    json: {\n      subject,\n      from,\n      toRecipients,\n      bodyHtml,\n      bodyText,\n      attachments,\n      attachmentCount: attachments.length,\n    },\n    pairedItem: { item: i },\n  });\n}\n\nreturn results;\n"
        },
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          1280,
          128
        ],
        "id": "db257c62-8d60-41ab-a196-4748a38f98a1",
        "name": "Compose message"
      },
      {
        "parameters": {
          "options": {}
        },
        "type": "n8n-nodes-base.splitInBatches",
        "typeVersion": 3,
        "position": [
          848,
          0
        ],
        "id": "93821958-7257-48be-9833-1f0bdd1f97c7",
        "name": "Loop Over Items"
      },
      {
        "parameters": {
          "resource": "draft",
          "subject": "={{ $json.subject }}",
          "bodyContent": "={{ $json.bodyHtml }}",
          "additionalFields": {
            "from": "={{ $json.from }}",
            "bodyContentType": "html",
            "toRecipients": "={{ $json.toRecipients }}"
          }
        },
        "type": "n8n-nodes-base.microsoftOutlook",
        "typeVersion": 2,
        "position": [
          1488,
          128
        ],
        "id": "406306fc-1d59-4df2-9b4d-302049091a09",
        "name": "Create a draft",
        "webhookId": "2ba93f50-73ab-4664-ab31-c27fede2db45",
        "credentials": {
          "microsoftOutlookOAuth2Api": {
            "id": "a7S1I18rJkgZ0wqY",
            "name": "Microsoft Outlook account"
          }
        }
      },
      {
        "parameters": {
          "jsCode": "// Split out attachments \u2014 Code node (Run Once for All Items)\n// Reads attachments[] from Compose message json + draft id from Create a draft.\n// Emits one item per attachment, with the FULL Graph request body in json.\n// No binary \u2014 downstream HTTP Request node POSTs json directly to Graph.\n\nconst composeItem = $('Compose message').item;\nconst draftItem = $('Create a draft').item;\n\nconst messageId = draftItem.json.id;\nconst attachments = composeItem.json.attachments || [];\nconst outputItems = [];\n\nfor (const att of attachments) {\n  const body = {\n    '@odata.type': '#microsoft.graph.fileAttachment',\n    name: att.fileName,\n    contentType: att.mimeType,\n    contentBytes: att.data,\n    isInline: !!att.isInline,\n  };\n  if (att.contentId) body.contentId = att.contentId;\n\n  outputItems.push({\n    json: {\n      messageId,\n      url: `https://graph.microsoft.com/v1.0/me/messages/${messageId}/attachments`,\n      body,\n    },\n  });\n}\n\nreturn outputItems;\n"
        },
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          1680,
          128
        ],
        "id": "e7f504aa-61cc-4b18-9b9f-a4122910bcbd",
        "name": "Split out attachments"
      },
      {
        "parameters": {
          "method": "POST",
          "url": "={{ $json.url }}",
          "authentication": "predefinedCredentialType",
          "nodeCredentialType": "microsoftOutlookOAuth2Api",
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ $json.body }}",
          "options": {}
        },
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.4,
        "position": [
          1888,
          128
        ],
        "id": "bd50f60c-c7f3-4ed5-b072-b64387c4736e",
        "name": "Add attachment",
        "retryOnFail": true,
        "maxTries": 5,
        "credentials": {
          "microsoftOutlookOAuth2Api": {
            "id": "a7S1I18rJkgZ0wqY",
            "name": "Microsoft Outlook account"
          }
        }
      },
      {
        "parameters": {
          "resource": "draft",
          "operation": "send",
          "draftId": {
            "__rl": true,
            "value": "={{ $('Create a draft').item.json.id }}",
            "mode": "id"
          }
        },
        "type": "n8n-nodes-base.microsoftOutlook",
        "typeVersion": 2,
        "position": [
          2288,
          128
        ],
        "id": "28b36940-c2a0-4845-a22b-66b413fd1f43",
        "name": "Send a draft",
        "webhookId": "d4625674-3cce-4c25-a4f2-8f0e4b60205b",
        "credentials": {
          "microsoftOutlookOAuth2Api": {
            "id": "a7S1I18rJkgZ0wqY",
            "name": "Microsoft Outlook account"
          }
        }
      },
      {
        "parameters": {
          "method": "DELETE",
          "url": "=http://172.20.0.1:8025/api/v1/messages/{{ $('Parse data json string1').item.json.ID }}",
          "options": {}
        },
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.4,
        "position": [
          2480,
          128
        ],
        "id": "f4eb3856-6a5e-4a0f-8399-7e8beddcac22",
        "name": "Delete message in Mailhog"
      },
      {
        "parameters": {
          "fieldsToAggregate": {
            "fieldToAggregate": [
              {}
            ]
          },
          "options": {}
        },
        "type": "n8n-nodes-base.aggregate",
        "typeVersion": 1,
        "position": [
          2096,
          128
        ],
        "id": "42becb63-8ff6-46c2-95c1-642c0fafdf52",
        "name": "Aggregate",
        "alwaysOutputData": true
      }
    ],
    "connections": {
      "Schedule Trigger": {
        "main": [
          [
            {
              "node": "Get messages",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Get messages": {
        "main": [
          [
            {
              "node": "Parse data json string",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Parse data json string": {
        "main": [
          [
            {
              "node": "Get message details",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Get message details": {
        "main": [
          [
            {
              "node": "Loop Over Items",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Parse data json string1": {
        "main": [
          [
            {
              "node": "Compose message",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Compose message": {
        "main": [
          [
            {
              "node": "Create a draft",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Loop Over Items": {
        "main": [
          [],
          [
            {
              "node": "Parse data json string1",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Create a draft": {
        "main": [
          [
            {
              "node": "Split out attachments",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Split out attachments": {
        "main": [
          [
            {
              "node": "Add attachment",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Add attachment": {
        "main": [
          [
            {
              "node": "Aggregate",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Send a draft": {
        "main": [
          [
            {
              "node": "Delete message in Mailhog",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Delete message in Mailhog": {
        "main": [
          [
            {
              "node": "Loop Over Items",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Aggregate": {
        "main": [
          [
            {
              "node": "Send a draft",
              "type": "main",
              "index": 0
            }
          ]
        ]
      }
    },
    "authors": "Shepherd Zhu",
    "name": "Version 4eaf38d1",
    "description": "",
    "autosaved": true
  },
  "tags": []
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Mailhog Automation. Uses httpRequest, microsoftOutlook. Scheduled trigger; 13 nodes.

Source: https://gist.github.com/Shepherd0619/fbd3e673f2acf91aedfee1d3236c0ff2 — original creator credit. Request a take-down →

More Email & Gmail workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Email & Gmail

This workflow automatically connects to the Square API and generates a monthly sales summary report for all your Square locations. The report matches the figures displayed in Square Dashboard &gt; Rep

HTTP Request, Microsoft Outlook
Email & Gmail

This workflow automatically connects to the Square API and generates a weekly sales summary report for all your Square locations. The report matches the figures displayed in Square Dashboard &gt; Repo

HTTP Request, Microsoft Outlook
Email & Gmail

This workflow automatically connects to the Square API and generates a daily sales summary report for all your Square locations. The report matches the figures displayed in Square Dashboard &gt; Repor

HTTP Request, Microsoft Outlook
Email & Gmail

YOUR_ID 4. Uses gmail, googleDrive, googleSheets, httpRequest. Scheduled trigger; 53 nodes.

Gmail, Google Drive, Google Sheets +1
Email & Gmail

Development teams using Claude Code who want a chat-ops interface for project management. Instead of SSH-ing into a server to run Claude, your whole team interacts with it through a Matrix chat room —

HTTP Request, Ssh