{
  "id": "Pi4s0Dv7qBwUdCTA",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Outlook + SharePoint: Smart Attachment Archiver",
  "tags": [],
  "nodes": [
    {
      "id": "ec340ea3-ad89-4bed-8643-9f6553a3be7e",
      "name": "Every Hour",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -368,
        112
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "3c8c1eb2-f169-44ef-bf51-e9bc643bd1c0",
      "name": "Get Unread Emails with Attachments",
      "type": "n8n-nodes-base.microsoftOutlook",
      "position": [
        -144,
        112
      ],
      "parameters": {
        "limit": 50,
        "options": {},
        "operation": "getAll"
      },
      "typeVersion": 2
    },
    {
      "id": "252a50b7-4865-4b3e-a190-75a73eaf5cf5",
      "name": "Has Emails?",
      "type": "n8n-nodes-base.if",
      "position": [
        80,
        112
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-+123456789001",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ $input.all().length }}",
              "rightValue": 0
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "ec3849ee-7071-4cad-8199-0605fbc6066c",
      "name": "No Emails - Done",
      "type": "n8n-nodes-base.noOp",
      "position": [
        256,
        240
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "fc78b7fe-aa15-467a-90fc-842a56671439",
      "name": "Process One Email at a Time",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        624,
        96
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "8c35161f-bcd0-4796-8029-63007ef37468",
      "name": "Get Email Attachments",
      "type": "n8n-nodes-base.microsoftOutlook",
      "position": [
        864,
        80
      ],
      "parameters": {
        "options": {},
        "resource": "messageAttachment",
        "messageId": "={{ $('Process One Email at a Time').first().json.id }}",
        "operation": "getAll"
      },
      "typeVersion": 2
    },
    {
      "id": "6d765876-6393-41c5-b3fd-bfdc507e357c",
      "name": "Build Folder Path + Decode Attachment",
      "type": "n8n-nodes-base.code",
      "position": [
        1088,
        80
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\nconst email = $('Process One Email at a Time').first().json;\nconst from = email.from?.emailAddress?.address || 'unknown';\nconst senderDomain = from.includes('@') ? from.split('@')[1] : 'internal_sender';\nconst received = new Date(email.receivedDateTime);\nconst year = received.getFullYear().toString();\nconst month = (received.getMonth() + 1).toString().padStart(2, '0');\nconst folderPath = `/Archives/${year}/${month}/${senderDomain}`;\n\nconst validItems = items.filter(item => (item.json.size || 0) > 0);\n\nif (validItems.length === 0) {\n  return [{ json: {\n    emailId: email.id, from, folderPath,\n    fileName: null, fileSize: 0, contentType: null,\n    noAttachments: true, receivedDate: received.toISOString()\n  }}];\n}\n\nreturn validItems.map(item => {\n  const binaryObj = Object.values(item.binary)[0];\n  return {\n    json: {\n      emailId:       email.id,\n      from,\n      folderPath,\n      fileName:      item.json.name,\n      fileSize:      item.json.size || 0,\n      contentType:   item.json.contentType || 'application/octet-stream',\n      noAttachments: false,\n      receivedDate:  received.toISOString()\n    },\n    binary: { data: binaryObj }\n  };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "0b674137-7b62-4ed3-b00c-567fc52428dc",
      "name": "Upload to OneDrive",
      "type": "n8n-nodes-base.microsoftOneDrive",
      "position": [
        1568,
        64
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "bcbce129-c09b-43b9-a13f-e03c2895021a",
      "name": "Log to SharePoint Audit List",
      "type": "n8n-nodes-base.microsoftSharePoint",
      "position": [
        2112,
        96
      ],
      "parameters": {
        "resource": "listItem",
        "requestOptions": {}
      },
      "typeVersion": 1
    },
    {
      "id": "2ae7847b-0175-42df-ad8e-470f37532543",
      "name": "Mark Email as Read",
      "type": "n8n-nodes-base.microsoftOutlook",
      "position": [
        2352,
        96
      ],
      "parameters": {
        "messageId": "={{ $('Process One Email at a Time').first().json.id }}",
        "operation": "update",
        "updateFields": {
          "isRead": true,
          "categories": [
            "Archived"
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "6445a8ae-8a66-4bb8-affe-ba1d19d60ded",
      "name": "Workflow Error Trigger",
      "type": "n8n-nodes-base.errorTrigger",
      "position": [
        -352,
        592
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "60fcc58e-8964-451a-abda-25ca51071b62",
      "name": "Send Error Email",
      "type": "n8n-nodes-base.microsoftOutlook",
      "position": [
        -128,
        592
      ],
      "parameters": {
        "subject": "={{ `\u274c n8n Workflow Error: Attachment Archiver [` + $json.execution.id + `]` }}",
        "bodyContent": "={{ `<p>Workflow <b>Outlook + SharePoint Attachment Archiver</b> failed.</p><ul><li><b>Execution:</b> ` + $json.execution.id + `</li><li><b>Node:</b> ` + $json.execution.lastNodeExecuted + `</li><li><b>Error:</b> ` + $json.execution.error.message + `</li></ul>` }}",
        "toRecipients": [
          "user@example.com"
        ],
        "additionalFields": {}
      },
      "typeVersion": 2
    },
    {
      "id": "edc93424-5f0e-4806-99d2-de98917466a1",
      "name": "Is Upload Error?",
      "type": "n8n-nodes-base.if",
      "position": [
        96,
        592
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-upload-error-01",
              "operator": {
                "type": "string",
                "operation": "contains"
              },
              "leftValue": "={{ $json.execution.lastNodeExecuted }}",
              "rightValue": "Upload"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "f7e8af9b-0889-43c5-ba58-a73530acd095",
      "name": "Log Failed Upload to SharePoint",
      "type": "n8n-nodes-base.microsoftSharePoint",
      "position": [
        320,
        576
      ],
      "parameters": {
        "resource": "listItem",
        "requestOptions": {}
      },
      "typeVersion": 1
    },
    {
      "id": "d2a25248-b8c4-4b62-84ec-f310be0826bc",
      "name": "Has File Attachments?",
      "type": "n8n-nodes-base.if",
      "position": [
        1360,
        80
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-0002-att-+1234567890",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.noAttachments }}",
              "rightValue": false
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "59ccf3ed-3db1-40bd-86b6-6f4354919577",
      "name": "Aggregate Attachment Uploads",
      "type": "n8n-nodes-base.code",
      "position": [
        1792,
        96
      ],
      "parameters": {
        "jsCode": "const items    = $input.all();\nconst email    = $('Process One Email at a Time').first().json;\nconst fileData = $('Build Folder Path + Decode Attachment').all().filter(i => !i.json.noAttachments);\n\nreturn [{ json: {\n  emailId:       email.id,\n  from:          email.from?.emailAddress?.address || 'unknown',\n  filesUploaded: fileData.length,\n  fileNames:     fileData.map(i => i.json.fileName).join(', ') || '(none)',\n  folderPath:    items[0]?.json?.folderPath || ''\n} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "23c4fdeb-7dda-45e6-8aca-f37e4f8b037c",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -400,
        -64
      ],
      "parameters": {
        "color": 7,
        "width": 892,
        "height": 468,
        "content": "## Ingestion\nRuns every hour. Fetches up to 50 unread Outlook emails. Exits cleanly with no further execution if the inbox is empty."
      },
      "typeVersion": 1
    },
    {
      "id": "490d1177-8566-4cbb-a631-8b2e239fb8d8",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        544,
        -64
      ],
      "parameters": {
        "color": 7,
        "width": 684,
        "height": 468,
        "content": "## Per-Email Processing\nSplits emails into individual items for sequential processing. Fetches all attachments per email, drops 0-byte items, decodes binary data, and computes the OneDrive folder path from the sender domain and received date."
      },
      "typeVersion": 1
    },
    {
      "id": "bedffe41-ac08-4527-9b5e-ce9be8270188",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1280,
        -64
      ],
      "parameters": {
        "color": 7,
        "width": 668,
        "height": 460,
        "content": "## Upload & Aggregation\nRoutes emails with real attachments to OneDrive upload. Emails with no valid attachments skip directly to aggregation. Results are collected into a single summary item with file names, upload count, and folder path."
      },
      "typeVersion": 1
    },
    {
      "id": "ded74909-0497-4af8-8931-7f9e758e0de0",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2000,
        -64
      ],
      "parameters": {
        "color": 7,
        "width": 568,
        "height": 456,
        "content": "## Audit & Completion\nLogs the upload summary to a SharePoint list for compliance tracking. Marks the email as read and applies the `Archived` category in Outlook. Loops back to process the next email in the batch."
      },
      "typeVersion": 1
    },
    {
      "id": "02b6d91c-6b61-42fb-a472-817a7dd5560b",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -400,
        448
      ],
      "parameters": {
        "color": 7,
        "width": 900,
        "height": 308,
        "content": "## Global Error Failsafe\nCatches any execution failure and sends an error email via Outlook with the execution ID and failed node name. If the failure was an upload error, also logs a failed upload record to the SharePoint audit list."
      },
      "typeVersion": 1
    },
    {
      "id": "801301b0-52c5-4160-80d5-b22e1b2b53c8",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1120,
        -64
      ],
      "parameters": {
        "width": 672,
        "height": 816,
        "content": "## Outlook Attachment Archiver\n\n### Microsoft 365 Automatic Attachment Archiving to OneDrive + SharePoint\n\n### How it works\n1. **Ingest:** Checks Outlook every hour for unread emails. Skips immediately if the inbox is empty.\n2. **Process:** Splits emails one at a time. Fetches attachments, drops 0-byte items, and builds a structured folder path from the sender's domain and the received date (`/Archives/YYYY/MM/sender-domain`).\n3. **Upload:** Files with valid attachments are uploaded to OneDrive into the computed folder path. Emails with no real attachments skip straight to logging.\n4. **Audit:** Aggregates upload results and logs a record to a SharePoint list with sender, file names, folder path, and upload count.\n5. **Complete:** Marks the email as read and tags it with the `Archived` category in Outlook, then loops back for the next email in the batch.\n\n### Setup steps\n- [ ] Connect Microsoft Outlook credential to Get Unread Emails, Get Email Attachments, Mark Email as Read, and Send Error Email nodes\n- [ ] Connect Microsoft OneDrive credential to Upload to OneDrive node\n- [ ] Connect Microsoft SharePoint credential to Log to SharePoint Audit List and Log Failed Upload to SharePoint nodes\n- [ ] Configure the Upload to OneDrive node with your target drive ID and set the folder path expression to `{{ $json.folderPath }}`\n- [ ] Configure Log to SharePoint Audit List with your Site ID and List ID\n- [ ] Update the error email recipient in Send Error Email (`it-admin@company.fi`)\n- [ ] Activate and run manually once to verify the folder structure is created correctly\n\n### Required credentials\n- Microsoft Outlook OAuth2\n- Microsoft OneDrive OAuth2\n- Microsoft SharePoint OAuth2\n\n### Folder structure\nAttachments are organized automatically:\n`/Archives/{year}/{month}/{sender-domain}/filename.ext`"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "507a3a07-4883-46ce-b1d9-9394f60fa71f",
  "connections": {
    "Every Hour": {
      "main": [
        [
          {
            "node": "Get Unread Emails with Attachments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Emails?": {
      "main": [
        [
          {
            "node": "Process One Email at a Time",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No Emails - Done",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Upload Error?": {
      "main": [
        [
          {
            "node": "Log Failed Upload to SharePoint",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Error Email": {
      "main": [
        [
          {
            "node": "Is Upload Error?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark Email as Read": {
      "main": [
        [
          {
            "node": "Process One Email at a Time",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload to OneDrive": {
      "main": [
        [
          {
            "node": "Aggregate Attachment Uploads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Email Attachments": {
      "main": [
        [
          {
            "node": "Build Folder Path + Decode Attachment",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has File Attachments?": {
      "main": [
        [
          {
            "node": "Upload to OneDrive",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Aggregate Attachment Uploads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Workflow Error Trigger": {
      "main": [
        [
          {
            "node": "Send Error Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process One Email at a Time": {
      "main": [
        [
          {
            "node": "Get Email Attachments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Attachment Uploads": {
      "main": [
        [
          {
            "node": "Log to SharePoint Audit List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log to SharePoint Audit List": {
      "main": [
        [
          {
            "node": "Mark Email as Read",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Unread Emails with Attachments": {
      "main": [
        [
          {
            "node": "Has Emails?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Folder Path + Decode Attachment": {
      "main": [
        [
          {
            "node": "Has File Attachments?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}