diff --git a/docs/n8n/po-email-intake.md b/docs/n8n/po-email-intake.md index a8d00e7..ee47a8d 100644 --- a/docs/n8n/po-email-intake.md +++ b/docs/n8n/po-email-intake.md @@ -1,8 +1,8 @@ # PO email intake — ai@megainfra.co.za → MEGA -**Status: built and tested. INACTIVE — waiting on two things from your IT admin (steps 1–2 below).** +**Status: built and tested. INACTIVE — waiting on one thing from you (step 1 below).** -Workflow: [MEGA - PO Email Intake (Microsoft Graph)](https://wfdash.n8n.apphub.co.za/workflow/bQ5PioGTSKnsWVh0) +Workflow: [MEGA - PO Email Intake (Microsoft Outlook)](https://wfdash.n8n.apphub.co.za/workflow/ARfOsAOy2KDUm1rp) Source: [`po-email-intake.workflow.js`](./po-email-intake.workflow.js) (rebuildable from this file) --- @@ -11,7 +11,7 @@ Source: [`po-email-intake.workflow.js`](./po-email-intake.workflow.js) (rebuilda Every 5 minutes the workflow: -1. lists the inbox of `ai@megainfra.co.za` through Microsoft Graph, +1. lists the inbox of `ai@megainfra.co.za` through n8n's native **Microsoft Outlook** node, 2. finds emails carrying a purchase-order document (PDF / JPEG / PNG / WEBP), 3. works out which MEGA site the PO belongs to, 4. files the document into the **same PocketBase queue the browser upload button uses** — a @@ -45,53 +45,80 @@ fallback IMAP route: `mail.megainfra.co.za` does not resolve, and HostH's --- -## Step 1 — your IT admin creates the app registration (once) +## Reading the mailbox from the Mac instead (Mail.app) -In the Azure portal → **App registrations** → **New registration**: +`ai@megainfra.co.za` is also set up in **Mail.app** on this Mac, and Mail can be read without +touching Full Disk Access, through Mail's own AppleScript interface: -- Name: `MEGA PO Email Intake` -- Supported account types: **Accounts in this organizational directory only** -- Redirect URI: leave blank (this is a client-credentials app, no sign-in redirect) +```bash +# accounts Mail knows about +osascript -e 'tell application "Mail" to get name of every account' -Then: +# raw MIME of a message (attachments come back base64-encoded inside it) +osascript -e 'tell application "Mail" to get source of message 1 of mailbox "Inbox" of account "ai@megainfra.co.za"' > msg1.eml +``` -- **Certificates & secrets** → **New client secret** → copy the secret value (shown once). -- **API permissions** → **Add a permission** → **Microsoft Graph** → **Application permissions** → - add **Mail.ReadWrite** → **Grant admin consent**. - - `Mail.Read` would only allow reading. We also move and tag messages, so **`Mail.ReadWrite` - (application)** is the one to grant. -- Copy **Tenant ID**, **Application (client) ID** and the **client secret value**. +Then parse `msg1.eml` with any MIME parser to pull out the PDFs (see +[`po-email-intake.workflow.js`](./po-email-intake.workflow.js) for the same attachment filter +the workflow uses). -Send those three values to the person wiring up n8n. The account password is not needed and is -not stored anywhere. +Two things to know: -## Step 2 — create the n8n credential (once) +- macOS blocks **direct** reads of Mail's private store (`~/Library/Mail` and the + `com.apple.mail` container) from a sandboxed shell — you get `Operation not permitted`. + AppleScript works because Mail itself hands the data over; it asks once for Automation + permission, which is far less than Full Disk Access. +- This route is **Mac-local**. It only works while the Mac is on and Mail is signed in, so it is + good for a one-off backfill or for checking what is arriving — not for unattended intake. + The n8n workflow below is the route that runs on its own. -In n8n → **Credentials** → **New** → **OAuth2 API**: +--- -| Field | Value | -| --- | --- | -| Name | `Microsoft Graph (MEGA PO)` | -| Grant Type | **Client Credentials** | -| Token URL | `https://login.microsoftonline.com//oauth2/v2.0/token` | -| Client ID | Application (client) ID from the app registration | -| Client Secret | the client secret value | -| Scopes | `https://graph.microsoft.com/.default` | -| Auth Style | Header → `Bearer` | +## Step 1 — connect the mailbox in n8n (once, about a minute) -## Step 3 — bind the credential and switch it on +In n8n → **Credentials** → **New** → search for **Microsoft Outlook** → **Connect**. -The six Microsoft Graph nodes still need the credential selected: +n8n opens a Microsoft sign-in page. **Sign in as `ai@megainfra.co.za`** and approve the mail +permissions it asks for (read, move, tag). n8n stores the tokens and refreshes them for you — +there is no password, no client secret and no Azure app registration to create. Name the +credential `Microsoft Outlook (MEGA PO)` so it lines up with the workflow. -`List Inbox POs` · `Download Attachment` · `Archive Message` · -`Archive Message (already filed)` · `Flag Needs Review` · -`Flag Needs Review (no site)` +> If your browser is already signed in to that account, it is one click. If the account is +> already open in your mail app, the same Microsoft session usually carries over — which is +> exactly why this route works without an IT admin. -The three PocketBase nodes (`Read Sites`, `Duplicate Check`, `Create PO Row`) are already bound -to the existing **`MEGAINFRASTRUCTURE`** credential — no secret sits inline in the workflow. +## Step 2 — bind the credential (once) -Once the credential is selected on those six nodes, activate the workflow (the schedule trigger -starts polling every 5 minutes). +Select `Microsoft Outlook (MEGA PO)` on these six nodes: + +`List Inbox POs` · `List Attachments` · `Download Attachment` · `Archive Message` · +`Archive Message (already filed)` · `Flag Needs Review` · `Flag Needs Review (no site)` + +(The three PocketBase nodes — `Read Sites`, `Duplicate Check`, `Create PO Row` — are already +bound to the existing **`MEGAINFRASTRUCTURE`** credential. No secret sits inline in the workflow.) + +## Step 3 — switch it on + +Activate the workflow. The schedule trigger starts polling every 5 minutes. + +--- + +## Upgrading to an unattended service principal (optional, later) + +The delegated sign-in above runs as a person. That is fine for getting started, but for a +production flow you may prefer an app that needs no human at all. n8n's Outlook node supports +three authentication modes, and the swap is a setting change on the same six nodes: + +| Mode | Credential | Use when | +| --- | --- | --- | +| `microsoftOutlookOAuth2Api` | Microsoft Outlook (hosted OAuth) | **default here** — one sign-in, no admin | +| `microsoftOAuth2Api` | Microsoft OAuth2 API (client credentials) | you want an app registration and a client secret | +| `microsoftEntraServicePrincipalApi` | Entra service principal | you want app-only access, with the `mailbox` field set to `ai@megainfra.co.za` | + +For the service-principal route your IT admin creates an Azure app registration with +**Mail.ReadWrite** (application) + admin consent, and hands over Tenant ID, Application +(client) ID and the client secret value. Then change the `authentication` dropdown on the six +nodes and set the mailbox on each. No rebuild is needed — the workflow logic is unchanged. --- @@ -111,15 +138,24 @@ So the site has to be known when the row is created, and the workflow reads it o itself, in this order: 1. exact project code — `C26/09`, `C26/13`, `C26/14`, `001`, `HEAD` -2. project code appearing anywhere in subject + body preview + attachment filename +2. project code appearing anywhere in subject + body preview + sender 3. site name — *The Hills*, *Cedar Gate*, *Munyaka 2*, *Nellmapius Ext 25*, *Mega Head Office* If none of those match, the email is tagged `PO Needs Review` and left in the inbox for a human. It is never filed against a guessed site. +**Known limitation, seen in real mail:** the site often appears only *inside* the PDF, not in the +email. A real example from the mailbox — the subject was *"RE: Bushveld Joy x 12 Traffic Light +Quote."* with no site anywhere in the subject, body or filename, while the PDF itself said +*"MEGCREQ11911 The Hills Ext12"* and the PO was for **The Hills**. That email gets flagged for +review rather than filed. The other three POs that week named their site in the subject +(*"Nellmapius Ext 25 PO Tools/cement/Bricks"*, *"Cedar Gate Cement PO"*) and matched fine. + > If you would rather have the OCR decide the site, that needs a change to the > `process-batch` route (or to the queue) so the site can be corrected after extraction. That is -> a small change to the app, not to this workflow — say the word and it can be done. +> a small change to the app, not to this workflow — say the word and it can be done. It would +> also close the gap above, because the PO's own `Subject` line (`MEGCREQ11913 (Nellmapius EXT25)`) +> is far more reliable than the email text. ## Duplicate protection @@ -155,7 +191,7 @@ behave exactly like browser uploads. | Symptom | Where to look | | --- | --- | -| No emails picked up | n8n execution log for `List Inbox POs` — a 401/403 means the Graph credential is wrong or admin consent was not granted | +| No emails picked up | n8n execution log for `List Inbox POs` — a 401/403 means the credential is not connected or the consent was refused | | POs tagged `PO Needs Review` | the email did not name a site; add the site/project code to the subject or check the site table in `Read Sites` | | Row created but never finalised | the existing `MEGA - Purchase Order Ingest` workflow — check its `Fetch Pending Batches` / `Escalate Failure` nodes | | Same PO filed twice | `Duplicate Check` compares `file_hash` + `site`; a different site for the same document is deliberately not treated as a duplicate | diff --git a/docs/n8n/po-email-intake.workflow.js b/docs/n8n/po-email-intake.workflow.js index 32447e8..ab1a151 100644 --- a/docs/n8n/po-email-intake.workflow.js +++ b/docs/n8n/po-email-intake.workflow.js @@ -1,11 +1,11 @@ /** - * MEGA - PO Email Intake (Microsoft Graph) - * ----------------------------------------- + * MEGA - PO Email Intake (Microsoft Outlook) + * ------------------------------------------ * n8n Workflow SDK source. Rebuild with the n8n MCP `create_workflow_from_code` tool * (validate first), or rebuild by hand in the n8n UI using * docs/n8n/po-email-intake.md as the wiring reference. * - * Live workflow: https://wfdash.n8n.apphub.co.za/workflow/bQ5PioGTSKnsWVh0 + * Live workflow: https://wfdash.n8n.apphub.co.za/workflow/ARfOsAOy2KDUm1rp * * What it does: polls ai@megainfra.co.za for purchase-order emails, downloads the PO * attachment, and files it into the same PocketBase pending queue the browser upload @@ -14,21 +14,19 @@ * cannot be matched to a site is tagged "PO Needs Review" in Outlook instead. * * Credentials referenced (none of them contain an inline secret): - * - "Microsoft Graph (MEGA PO)" (oAuth2Api) -> created by the admin, see the runbook - * - "MEGAINFRASTRUCTURE" (pocketbaseHttpApi) -> already exists on this instance + * - "Microsoft Outlook (MEGA PO)" (microsoftOutlookOAuth2Api) -> n8n's hosted Microsoft + * OAuth: create it in n8n and sign in as ai@megainfra.co.za. No app registration. + * Swap to microsoftEntraServicePrincipalApi for unattended app-only access. + * - "MEGAINFRASTRUCTURE" (pocketbaseHttpApi) -> already exists on this instance. * - * Set the Graph credential on these six nodes after creating it: - * List Inbox POs, Download Attachment, Archive Message, + * Set the Microsoft credential on these seven nodes after creating it: + * List Inbox POs, List Attachments, Download Attachment, Archive Message, * Archive Message (already filed), Flag Needs Review, Flag Needs Review (no site) */ import { workflow, node, trigger, ifElse, splitInBatches, nextBatch, newCredential, expr } from '@n8n/workflow-sdk'; -const MAILBOX = 'ai@megainfra.co.za'; -const GRAPH = 'https://graph.microsoft.com/v1.0'; -const MSG = GRAPH + '/users/' + MAILBOX + '/messages/'; - -const GRAPH_CRED = { oAuth2Api: newCredential('Microsoft Graph (MEGA PO)') }; +const GRAPH_CRED = { microsoftOutlookOAuth2Api: newCredential('Microsoft Outlook (MEGA PO)') }; const PB_CRED = { pocketbaseHttpApi: newCredential('MEGAINFRASTRUCTURE') }; const schedule = trigger({ @@ -73,42 +71,38 @@ return [{ json: { sites: sites } }];` }); const listInbox = node({ - type: 'n8n-nodes-base.httpRequest', - version: 4.5, + type: 'n8n-nodes-base.microsoftOutlook', + version: 2, config: { name: 'List Inbox POs', parameters: { - method: 'GET', - url: GRAPH + '/users/' + MAILBOX + '/mailFolders/inbox/messages', - sendQuery: true, - queryParameters: { - parameters: [ - { name: '$select', value: 'id,subject,from,bodyPreview,receivedDateTime,hasAttachments,categories' }, - { name: '$expand', value: 'attachments' }, - { name: '$orderby', value: 'receivedDateTime desc' }, - { name: '$top', value: '25' } - ] - }, - authentication: 'genericCredentialType', - genericAuthType: 'oAuth2Api', - options: { response: { response: { responseFormat: 'json', neverError: true } } } + resource: 'message', + operation: 'getAll', + authentication: 'microsoftOutlookOAuth2Api', + returnAll: false, + limit: 25, + output: 'simple', + filtersUI: { + values: { + filterBy: 'filters', + filters: { hasAttachments: true, readStatus: 'both', foldersToInclude: ['inbox'] } + } + } }, credentials: GRAPH_CRED } }); -const selectDocs = node({ +const selectMsgs = node({ type: 'n8n-nodes-base.code', version: 2, config: { - name: 'Select PO Documents', + name: 'Select PO Messages', parameters: { mode: 'runOnceForAllItems', - jsCode: `const body = $input.first() ? $input.first().json : {}; -const messages = body && Array.isArray(body.value) ? body.value : []; + jsCode: `const items = $input.all(); let sites = []; try { sites = ($('Collect Sites').item.json.sites) || []; } catch (e) { sites = []; } -const DOC_TYPES = ['application/pdf', 'image/jpeg', 'image/png', 'image/webp']; const POISH = /(\\bp\\/?o\\b|\\bpo\\b|purchase\\s*order|\\border\\b)/i; function norm(s) { return String(s || '').toUpperCase().replace(/\\s+/g, ' ').trim(); } function matchSite(text) { @@ -122,39 +116,29 @@ function matchSite(text) { return null; } const out = []; -for (const m of messages) { - if (!m || !m.id || m.hasAttachments !== true) continue; +for (const it of items) { + const m = it && it.json ? it.json : {}; + if (!m.id || m.hasAttachments !== true) continue; const cats = Array.isArray(m.categories) ? m.categories : []; if (cats.indexOf('PO Needs Review') >= 0 || cats.indexOf('PO Ingested') >= 0) continue; - const atts = Array.isArray(m.attachments) ? m.attachments : []; const subject = m.subject || ''; const preview = m.bodyPreview || ''; const from = (m.from && m.from.emailAddress && m.from.emailAddress.address) || ''; - for (const a of atts) { - if (!a || a.isInline === true) continue; - const ct = String(a.contentType || '').toLowerCase(); - const name = String(a.name || ''); - if (DOC_TYPES.indexOf(ct) < 0) continue; - const hay = subject + ' ' + preview + ' ' + name; - const site = matchSite(hay); - const poish = POISH.test(hay); - if (!site && !poish) continue; - out.push({ json: { - messageId: m.id, - attachmentId: a.id, - filename: name, - contentType: ct, - size: a.size || 0, - subject: subject, - bodyPreview: preview, - from: from, - received: m.receivedDateTime || '', - ready: !!site, - siteId: site ? site.id : '', - siteCode: site ? site.project_code : '', - siteName: site ? site.name : '' - } }); - } + const hay = subject + ' ' + preview + ' ' + from; + const site = matchSite(hay); + const poish = POISH.test(hay); + if (!site && !poish) continue; + out.push({ json: { + messageId: m.id, + subject: subject, + bodyPreview: preview, + from: from, + received: m.receivedDateTime || '', + ready: !!site, + siteId: site ? site.id : '', + siteCode: site ? site.project_code : '', + siteName: site ? site.name : '' + } }); } return out;` } @@ -163,7 +147,7 @@ return out;` const loop = splitInBatches({ version: 3, - config: { name: 'Loop Documents', parameters: { batchSize: 1 } } + config: { name: 'Loop Messages', parameters: { batchSize: 1 } } }); const siteResolved = ifElse({ @@ -180,17 +164,66 @@ const siteResolved = ifElse({ } }); +const listAtts = node({ + type: 'n8n-nodes-base.microsoftOutlook', + version: 2, + config: { + name: 'List Attachments', + parameters: { + resource: 'messageAttachment', + operation: 'getAll', + authentication: 'microsoftOutlookOAuth2Api', + messageId: { __rl: true, mode: 'id', value: '={{ $json.messageId }}' } + }, + credentials: GRAPH_CRED + } +}); + +const selectAtts = node({ + type: 'n8n-nodes-base.code', + version: 2, + config: { + name: 'Select PO Attachments', + parameters: { + mode: 'runOnceForAllItems', + jsCode: `const items = $input.all(); +const DOC_TYPES = ['application/pdf', 'image/jpeg', 'image/png', 'image/webp']; +let messageId = ''; +try { messageId = $('Select PO Messages').item.json.messageId || ''; } catch (e) { messageId = ''; } +if (!messageId) { try { messageId = $('Loop Messages').item.json.messageId || ''; } catch (e) { messageId = ''; } } +const out = []; +for (const it of items) { + const a = it && it.json ? it.json : {}; + if (!a.id) continue; + if (a.isInline === true) continue; + const ct = String(a.contentType || '').toLowerCase(); + const name = String(a.name || ''); + if (DOC_TYPES.indexOf(ct) < 0) continue; + out.push({ json: { + messageId: messageId, + attachmentId: a.id, + filename: name, + contentType: ct, + size: a.size || 0 + } }); +} +return out;` + } + } +}); + const download = node({ - type: 'n8n-nodes-base.httpRequest', - version: 4.5, + type: 'n8n-nodes-base.microsoftOutlook', + version: 2, config: { name: 'Download Attachment', parameters: { - method: 'GET', - url: expr('"' + MSG + '" + $json.messageId + "/attachments/" + $json.attachmentId + "/$value"'), - authentication: 'genericCredentialType', - genericAuthType: 'oAuth2Api', - options: { response: { response: { responseFormat: 'file', outputPropertyName: 'data' } } } + resource: 'messageAttachment', + operation: 'download', + authentication: 'microsoftOutlookOAuth2Api', + messageId: { __rl: true, mode: 'id', value: '={{ $json.messageId }}' }, + attachmentId: { __rl: true, mode: 'id', value: '={{ $json.attachmentId }}' }, + binaryPropertyName: 'data' }, credentials: GRAPH_CRED } @@ -206,9 +239,14 @@ const capture = node({ jsCode: `const items = $input.all(); const out = []; for (const item of items) { - let meta = {}; - try { meta = $('Select PO Documents').item.json || {}; } catch (e) { meta = {}; } - if (!meta.messageId) { try { meta = $('Loop Documents').item.json || {}; } catch (e) { meta = {}; } } + // Site and message metadata live on the message node; attachment details on the + // attachment node. Reading site info from the attachment node leaves siteId empty + // and PocketBase rejects the row (site is a required relation). + let msgMeta = {}; + try { msgMeta = $('Select PO Messages').item.json || {}; } catch (e) { msgMeta = {}; } + if (!msgMeta.messageId) { try { msgMeta = $('Loop Messages').item.json || {}; } catch (e) { msgMeta = {}; } } + let attMeta = {}; + try { attMeta = $('Select PO Attachments').item.json || {}; } catch (e) { attMeta = {}; } const bin = (item.binary && item.binary.data) || {}; const b64 = typeof bin.data === 'string' ? bin.data : ''; let fileHash = ''; @@ -219,22 +257,22 @@ for (const item of items) { const stamp = Date.now().toString(36).toUpperCase(); out.push({ json: { - messageId: meta.messageId || '', - attachmentId: meta.attachmentId || '', - filename: bin.fileName || meta.filename || 'purchase-order.pdf', - contentType: bin.mimeType || meta.contentType || 'application/pdf', + messageId: msgMeta.messageId || attMeta.messageId || '', + attachmentId: attMeta.attachmentId || '', + filename: bin.fileName || attMeta.filename || 'purchase-order.pdf', + contentType: bin.mimeType || attMeta.contentType || 'application/pdf', fileHash: fileHash, - siteId: meta.siteId || '', - siteCode: meta.siteCode || '', + siteId: msgMeta.siteId || '', + siteCode: msgMeta.siteCode || '', purchaseOrderNumber: fileHash ? ('PENDING-' + fileHash.slice(0, 12).toUpperCase()) : ('PENDING-' + stamp), emailMeta: { source: 'email', - message_id: meta.messageId || '', - subject: meta.subject || '', - from: meta.from || '', - received: meta.received || '', - site_code: meta.siteCode || '', - file_name: bin.fileName || meta.filename || '' + message_id: msgMeta.messageId || attMeta.messageId || '', + subject: msgMeta.subject || '', + from: msgMeta.from || '', + received: msgMeta.received || '', + site_code: msgMeta.siteCode || '', + file_name: bin.fileName || attMeta.filename || '' } }, binary: item.binary @@ -344,84 +382,64 @@ const filedOk = ifElse({ }); const archiveNew = node({ - type: 'n8n-nodes-base.httpRequest', - version: 4.5, + type: 'n8n-nodes-base.microsoftOutlook', + version: 2, config: { name: 'Archive Message', parameters: { - method: 'POST', - url: expr('"' + MSG + '" + $("Capture File").item.json.messageId + "/move"'), - sendHeaders: true, - headerParameters: { parameters: [{ name: 'Content-Type', value: 'application/json' }] }, - sendBody: true, - specifyBody: 'json', - jsonBody: '={"destinationId":"archive"}', - authentication: 'genericCredentialType', - genericAuthType: 'oAuth2Api', - options: { response: { response: { responseFormat: 'json', neverError: true } } } + resource: 'message', + operation: 'move', + authentication: 'microsoftOutlookOAuth2Api', + messageId: { __rl: true, mode: 'id', value: '={{ $("Capture File").item.json.messageId }}' }, + folderId: { __rl: true, mode: 'id', value: 'archive' } }, credentials: GRAPH_CRED } }); const archiveDup = node({ - type: 'n8n-nodes-base.httpRequest', - version: 4.5, + type: 'n8n-nodes-base.microsoftOutlook', + version: 2, config: { name: 'Archive Message (already filed)', parameters: { - method: 'POST', - url: expr('"' + MSG + '" + $("Capture File").item.json.messageId + "/move"'), - sendHeaders: true, - headerParameters: { parameters: [{ name: 'Content-Type', value: 'application/json' }] }, - sendBody: true, - specifyBody: 'json', - jsonBody: '={"destinationId":"archive"}', - authentication: 'genericCredentialType', - genericAuthType: 'oAuth2Api', - options: { response: { response: { responseFormat: 'json', neverError: true } } } + resource: 'message', + operation: 'move', + authentication: 'microsoftOutlookOAuth2Api', + messageId: { __rl: true, mode: 'id', value: '={{ $("Capture File").item.json.messageId }}' }, + folderId: { __rl: true, mode: 'id', value: 'archive' } }, credentials: GRAPH_CRED } }); const flagReview = node({ - type: 'n8n-nodes-base.httpRequest', - version: 4.5, + type: 'n8n-nodes-base.microsoftOutlook', + version: 2, config: { name: 'Flag Needs Review', parameters: { - method: 'PATCH', - url: expr('"' + MSG + '" + $("Capture File").item.json.messageId'), - sendHeaders: true, - headerParameters: { parameters: [{ name: 'Content-Type', value: 'application/json' }] }, - sendBody: true, - specifyBody: 'json', - jsonBody: '={"categories":["PO Needs Review"]}', - authentication: 'genericCredentialType', - genericAuthType: 'oAuth2Api', - options: { response: { response: { responseFormat: 'json', neverError: true } } } + resource: 'message', + operation: 'update', + authentication: 'microsoftOutlookOAuth2Api', + messageId: { __rl: true, mode: 'id', value: '={{ $("Capture File").item.json.messageId }}' }, + updateFields: { categories: ['PO Needs Review'], isRead: true } }, credentials: GRAPH_CRED } }); const flagNoSite = node({ - type: 'n8n-nodes-base.httpRequest', - version: 4.5, + type: 'n8n-nodes-base.microsoftOutlook', + version: 2, config: { name: 'Flag Needs Review (no site)', parameters: { - method: 'PATCH', - url: expr('"' + MSG + '" + $json.messageId'), - sendHeaders: true, - headerParameters: { parameters: [{ name: 'Content-Type', value: 'application/json' }] }, - sendBody: true, - specifyBody: 'json', - jsonBody: '={"categories":["PO Needs Review"]}', - authentication: 'genericCredentialType', - genericAuthType: 'oAuth2Api', - options: { response: { response: { responseFormat: 'json', neverError: true } } } + resource: 'message', + operation: 'update', + authentication: 'microsoftOutlookOAuth2Api', + messageId: { __rl: true, mode: 'id', value: '={{ $json.messageId }}' }, + updateFields: { categories: ['PO Needs Review'], isRead: true } }, credentials: GRAPH_CRED } @@ -440,24 +458,26 @@ return [{ json: { finished: true, processed: items.length } }];` } }); -export default workflow('mega-po-email-intake', 'MEGA - PO Email Intake (Microsoft Graph)') +export default workflow('mega-po-email-intake-v2', 'MEGA - PO Email Intake (Microsoft Outlook)') .add(schedule) .to(readSites) .to(collectSites) .to(listInbox) - .to(selectDocs) + .to(selectMsgs) .to(loop .onDone(summary) .onEachBatch(siteResolved - .onTrue(download - .to(capture) - .to(dupCheck) - .to(alreadyFiled - .onTrue(archiveDup.to(nextBatch(loop))) - .onFalse(createPo - .to(checkResult) - .to(filedOk - .onTrue(archiveNew.to(nextBatch(loop))) - .onFalse(flagReview.to(nextBatch(loop))))))) + .onTrue(listAtts + .to(selectAtts) + .to(download + .to(capture) + .to(dupCheck) + .to(alreadyFiled + .onTrue(archiveDup.to(nextBatch(loop))) + .onFalse(createPo + .to(checkResult) + .to(filedOk + .onTrue(archiveNew.to(nextBatch(loop))) + .onFalse(flagReview.to(nextBatch(loop)))))))) .onFalse(flagNoSite.to(nextBatch(loop)))) );