Upload Multiple Email Attachments to FTP or S3

I currently have a workflow that is taking inbound email via IMAP Email and eventually sending elements of the data to a HTTP Request POST. What I’m hoping to do next is grab any attachments of the email, upload them to either Wasabi S3 (preferably) or FTP, and then grab the URL of the files and append them to the rest of the data from the IMAP Email so I can add it my JSON that is posted. My thinking is that I would go from IMAP Email to a function that splits the binary data.

for (let item in items) {
  if(Object.keys(items[item]).includes('binary')){
    return Object.keys(items[item].binary).map(key => {
    return {
      json: {},
      binary: {
        data: items[item].binary[key],
      }
    }
});
  }
}
return items;

From there, I would use S3 or FTP node to upload the attachments. But, it’s not clear if I can upload multiple attachments via either of those or if I have to iterate through the attachments individually. Then, I would use an Append node so that this:

[
   {
      "textHtml":"<b>Boo</b>",
      "textPlain":"Boo",
      "metadata":{
         "delivered-to":"[email protected]",
         "reply-to":"[email protected]"
      },
      "from":"Scary Stories <[email protected]>",
      "subject":"Spooky Attachments [Casper]",
      "date":"Thu, 16 Dec 2021 18:21:18 +0000 (UTC)",
      "to":"Email Account <[email protected]>"
   }
]

Becomes something like this:

[
   {
      "textHtml":"<b>Boo</b>",
      "textPlain":"Boo",
      "metadata":{
         "delivered-to":"[email protected]",
         "reply-to":"[email protected]"
      },
      "from":"Scary Stories <[email protected]>",
      "subject":"Spooky Attachments [Casper]",
      "date":"Thu, 16 Dec 2021 18:21:18 +0000 (UTC)",
      "to":"Email Account <[email protected]>"
   },
   {
      "attachments":[
         {
            "id":1,
            "url":"https://wasaby.sys/bucket/filename1.jpg"
         },
         {
            "id":2,
            "url":"https://wasaby.sys/bucket/filename1.jpg"
         }
      ]
   }
]

Due to how the endpoint expects to get the JSON, I have to construct it a bit differently and send it RAW. I use this function to create that JSON and would also appreciate help getting the attachments into this function.

items[0].json.sample = {
    "summary": $node["IMAP Email"].json["subject"],
    "details": $node["IMAP Email"].json["textHtml"],
    "user_id": $node["SetGenUser"].json["user_id"]
    }
    
return items;

Thank you.

Can you please share your workflow?

If I can avoid it, no. There would be a lot of cleaning up I’d have to do. I’ll see if I can recreate the workflow somehow with just the necessities and fake data.

Yes, you can create one with mockup data. It is complicated to come up with a proper solution without the workflow. At least for me.

Understood. That will take some time as I have to figure out how to mockup an email with attachments. Here’s the workflow with all the sensitive bits removed, which I suspect makes it mostly useless for your purposes. Key points here are that SplitBinaryData would then go to an FTP or S3 node. And the IF currently only has FALSE pointing somewhere, but essentially it would have almost identical flows after that point.

{
  "name": "Invarosoft to HaloPSA",
  "nodes": [
    {
      "parameters": {},
      "name": "Start",
      "type": "n8n-nodes-base.start",
      "typeVersion": 1,
      "position": [
        100,
        270
      ]
    },
    {
      "parameters": {
        "requestMethod": "POST",
        "options": {},
        "bodyParametersUi": {
          "parameter": [
            {
              "name": "grant_type",
              "value": "client_credentials"
            },
            {
              "name": "client_id"
            },
            {
              "name": "client_secret"
            },
            {
              "name": "scope",
              "value": "all"
            }
          ]
        },
        "headerParametersUi": {
          "parameter": [
            {
              "name": "Content-Type",
              "value": "application/x-www-form-urlencoded"
            },
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        }
      },
      "name": "HTTP Request1",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        650,
        480
      ]
    },
    {
      "parameters": {
        "options": {},
        "headerParametersUi": {
          "parameter": [
            {
              "name": "Authorization",
              "value": "=Bearer {{$node[\"HTTP Request1\"].json[\"access_token\"]}}"
            }
          ]
        },
        "queryParametersUi": {
          "parameter": [
            {
              "name": "search",
              "value": "={{$node[\"IMAP Email\"].json[\"metadata\"][\"reply-to\"]}}"
            }
          ]
        }
      },
      "name": "GetUser1",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        860,
        480
      ]
    },
    {
      "parameters": {
        "downloadAttachments": true,
        "options": {}
      },
      "name": "IMAP Email",
      "type": "n8n-nodes-base.emailReadImap",
      "typeVersion": 1,
      "position": [
        210,
        480
      ],
      "credentials": {
        "imap": {
          "id": "3",
          "name": "IMAP account"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{$node[\"GetUser1\"].json[\"users\"][0][\"id\"]}}",
              "operation": "isEmpty"
            }
          ]
        }
      },
      "name": "IF",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        1010,
        480
      ]
    },
    {
      "parameters": {
        "requestMethod": "POST",
        "url": "",
        "jsonParameters": true,
        "options": {},
        "bodyParametersJson": "=[{{JSON.stringify($node[\"TicketJson\"].json[\"sample\"])}}]",
        "headerParametersJson": "={{ { \"Authorization\": $json.token} }}"
      },
      "name": "CreateTicket",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        2070,
        350
      ]
    },
    {
      "parameters": {
        "options": {},
        "headerParametersUi": {
          "parameter": [
            {
              "name": "Authorization",
              "value": "=Bearer {{$node[\"HTTP Request1\"].json[\"access_token\"]}}"
            }
          ]
        },
        "queryParametersUi": {
          "parameter": [
            {
              "name": "search",
              "value": "={{$node[\"IMAP Email\"].json[\"subject\"].replace(/(^.*\\[|\\].*$)/g,'')}}"
            }
          ]
        }
      },
      "name": "GetGenUser",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        1220,
        350
      ]
    },
    {
      "parameters": {
        "mode": "keepKeyMatches",
        "propertyName1": "name",
        "propertyName2": "name"
      },
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 1,
      "position": [
        1540,
        350
      ]
    },
    {
      "parameters": {
        "functionCode": "return items[0].json.users.map(item => ( { json : item } ));\n"
      },
      "name": "Users",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        1370,
        350
      ]
    },
    {
      "parameters": {
        "functionCode": "return [{\n  json: {\n    name: \"General User\"\n  }\n}]\n"
      },
      "name": "GenUser",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        1370,
        520
      ]
    },
    {
      "parameters": {
        "values": {
          "string": [
            {
              "name": "user_id",
              "value": "={{$node[\"Merge\"].json[\"id\"]}}"
            },
            {
              "name": "token",
              "value": "=Bearer {{$node[\"HTTP Request1\"].json[\"access_token\"]}}"
            }
          ]
        },
        "options": {}
      },
      "name": "SetGenUser",
      "type": "n8n-nodes-base.set",
      "typeVersion": 1,
      "position": [
        1690,
        350
      ]
    },
    {
      "parameters": {
        "functionCode": "items[0].json.sample = {\n    \"summary\": $node[\"IMAP Email\"].json[\"subject\"],\n    \"details\": $node[\"IMAP Email\"].json[\"textHtml\"],\n    \"user_id\": $node[\"SetGenUser\"].json[\"user_id\"]\n    }\n    \nreturn items;\n"
      },
      "name": "TicketJson",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        1880,
        350
      ]
    },
    {
      "parameters": {
        "functionCode": "for (let item in items) {\n  if(Object.keys(items[item]).includes('binary')){\n    return Object.keys(items[item].binary).map(key => {\n    return {\n      json: {},\n      binary: {\n        data: items[item].binary[key],\n      }\n    }\n});\n  }\n}\nreturn items;\n"
      },
      "name": "SplitBinaryData",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        350,
        280
      ]
    },
    {
      "parameters": {},
      "name": "Append",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 1,
      "position": [
        490,
        480
      ]
    }
  ],
  "connections": {
    "HTTP Request1": {
      "main": [
        [
          {
            "node": "GetUser1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GetUser1": {
      "main": [
        [
          {
            "node": "IF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IMAP Email": {
      "main": [
        [
          {
            "node": "SplitBinaryData",
            "type": "main",
            "index": 0
          },
          {
            "node": "Append",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "IF": {
      "main": [
        [],
        [
          {
            "node": "GetGenUser",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GetGenUser": {
      "main": [
        [
          {
            "node": "Users",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GenUser": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Users": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "SetGenUser",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SetGenUser": {
      "main": [
        [
          {
            "node": "TicketJson",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "TicketJson": {
      "main": [
        [
          {
            "node": "CreateTicket",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append": {
      "main": [
        [
          {
            "node": "HTTP Request1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SplitBinaryData": {
      "main": [
        []
      ]
    }
  },
  "active": false,
  "settings": {},
  "id": 1013
}

And any suggestions on mocking up an email with attachments would be greatly appreciated.

Check the example workflow below:

{
  "nodes": [
    {
      "parameters": {},
      "name": "Start",
      "type": "n8n-nodes-base.start",
      "typeVersion": 1,
      "position": [
        -860,
        380
      ]
    },
    {
      "parameters": {
        "url": "https://hips.hearstapps.com/hmg-prod.s3.amazonaws.com/images/golden-retriever-royalty-free-image-506756303-1560962726.jpg?crop=0.672xw:1.00xh;0.166xw,0&resize=640:*",
        "responseFormat": "file",
        "dataPropertyName": "attachment_1",
        "options": {},
        "headerParametersUi": {
          "parameter": []
        }
      },
      "name": "HTTP Request",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        -300,
        340
      ],
      "notesInFlow": true,
      "notes": "Mockup attachment"
    },
    {
      "parameters": {
        "url": "https://hips.hearstapps.com/hmg-prod.s3.amazonaws.com/images/golden-retriever-royalty-free-image-506756303-1560962726.jpg?crop=0.672xw:1.00xh;0.166xw,0&resize=640:*",
        "responseFormat": "file",
        "dataPropertyName": "attachment_2",
        "options": {},
        "headerParametersUi": {
          "parameter": []
        }
      },
      "name": "HTTP Request1",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        -320,
        560
      ],
      "notesInFlow": true,
      "notes": "Mockup attachment"
    },
    {
      "parameters": {
        "mode": "mergeByIndex"
      },
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 1,
      "position": [
        120,
        440
      ]
    },
    {
      "parameters": {
        "functionCode": "return [\n  {\n    json: {\n         \n      \"textHtml\":\"<b>Boo</b>\",\n      \"textPlain\":\"Boo\",\n      \"metadata\":{\n         \"delivered-to\":\"[email protected]\",\n         \"reply-to\":\"[email protected]\"\n      },\n      \"from\":\"Scary Stories <[email protected]>\",\n      \"subject\":\"Spooky Attachments [Casper]\",\n      \"date\":\"Thu, 16 Dec 2021 18:21:18 +0000 (UTC)\",\n      \"to\":\"Email Account <[email protected]>\"\n   \n    }\n  }\n]"
      },
      "name": "Mockup email",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        0,
        200
      ]
    },
    {
      "parameters": {
        "mode": "mergeByIndex"
      },
      "name": "Merge1",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 1,
      "position": [
        340,
        220
      ]
    },
    {
      "parameters": {
        "operation": "upload",
        "bucketName": "n8n",
        "fileName": "={{$node[\"Split binary items\"].binary.data.fileName}}",
        "additionalFields": {}
      },
      "name": "AWS S3",
      "type": "n8n-nodes-base.awsS3",
      "typeVersion": 1,
      "position": [
        760,
        220
      ],
      "credentials": {
        "aws": {
          "id": "30",
          "name": "n8n"
        }
      }
    },
    {
      "parameters": {
        "functionCode": "for (let item in items) {\n  if(Object.keys(items[item]).includes('binary')){\n    return Object.keys(items[item].binary).map(key => {\n    return {\n      json: {},\n      binary: {\n        data: items[item].binary[key],\n      }\n    }\n});\n  }\n}\nreturn items;\n"
      },
      "name": "Split binary items",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        540,
        220
      ]
    },
    {
      "parameters": {
        "operation": "getAll",
        "bucketName": "n8n",
        "options": {}
      },
      "name": "AWS S",
      "type": "n8n-nodes-base.awsS3",
      "typeVersion": 1,
      "position": [
        960,
        220
      ],
      "credentials": {
        "aws": {
          "id": "30",
          "name": "n8n"
        }
      }
    },
    {
      "parameters": {
        "functionCode": "const jsonData = $node[\"Merge1\"].json;\n\nconst attachments = []\n\nfor (let item of items) {\n\n  attachments.push({\n    url: item.json.Key\n  })\n\n}\n\n\nreturn [\n  {\n    json: {\n      ...jsonData,\n      attachments,\n    }\n  }\n]"
      },
      "name": "Split binary items1",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        1180,
        220
      ]
    }
  ],
  "connections": {
    "Start": {
      "main": [
        [
          {
            "node": "HTTP Request",
            "type": "main",
            "index": 0
          },
          {
            "node": "HTTP Request1",
            "type": "main",
            "index": 0
          },
          {
            "node": "Mockup email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request1": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "Merge1",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Mockup email": {
      "main": [
        [
          {
            "node": "Merge1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge1": {
      "main": [
        [
          {
            "node": "Split binary items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AWS S3": {
      "main": [
        [
          {
            "node": "AWS S",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split binary items": {
      "main": [
        [
          {
            "node": "AWS S3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AWS S": {
      "main": [
        [
          {
            "node": "Split binary items1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
1 Like

Thanks @RicardoE105. This does work. I’ve implemented it in my workflow and subbed in the generic S3 nodes to work with Wasabi. The one issue I need to address now relates to the placement of these files. Wasabi doesn’t handle duplicates well, and if I have all the files uploaded to the root of the bucket, listing them all will result in attachments from other emails being listed as well. My plan here was to generate a unique folder and set that via Folder Key. The problem I’m running into is that if I use an expression to grab something unique from the IMAP Email node, like the message ID or the timestamp, the S3 node only uploads the first attachment and nothing else. If I set the Folder Key to just “test” or anything that isn’t an expression (even if it’s just an expression to run a date() and not grab from another node), it uploads all attachments. Any idea why that’s happening and how best to work around it?

Actually, disregard. I was able to use the $executionId to generate something that worked. The last thing I need to do but I just can’t put it together in my head; I’m using a function to create the JSON that I need to send to the endpoint. It looks like this:

items[0].json.sample = {
    "summary": $node["IMAP Email"].json["subject"],
    "details": $node["IMAP Email"].json["textHtml"],
    "user_id": $node["SetGenUser"].json["user_id"]
    }
    
return items;

I need to add the attachment URLs to the content of the “details” variable. So let’s say the current content of $node[“IMAP Email”].json[“textHtml”] is:

<b>Boo</b>

I would want it to become:

<b>Boo</b>
<br /><a href="https://wasabi.sys/211/Screenshot_3z1c3fwh.jpeg">Attachment</a>
<br /><a href="https://wasabi.sys/211/Screenshot_jslcoj3l.jpeg">Attachment</a>
<br /><a href="https://wasabi.sys/211/Screenshot_qjmu5pma.jpeg">Attachment</a>

Where what was created by the Split binary items workflow is:

"attachments":[
   {
      "url":"211/Screenshot_3z1c3fwh.jpeg"
   },
   {
      "url":"211/Screenshot_jslcoj3l.jpeg"
   },
   {
      "url":"211/Screenshot_qjmu5pma.jpeg"
   }
]
1 Like