// Catch and Create Uploader — SINGLE Code node that does PB auth + walk // binary + upload each attachment + emit one item with the email context // and PB attachment IDs. // // CRITICAL: This node runs IMMEDIATELY after the email source // (IMAP Quote Mailbox OR Webhook Body Unwrap). It MUST run there // because every other n8n node type we've tested (IF, Set, Code without // explicit binary preservation) strips the binary `data` field. The only // place binary with `data` exists is right after the source. // // On the wire it looks like this: // // IMAP Quote Mailbox ─┐ // ├─→ Catch and Create Uploader ─┬─→ Parse Email ─→ ... // Webhook Body Unwrap ┘ │ // └─→ (binary already uploaded) // // Output: ONE item per email (not per attachment) carrying: // - the original email context (subject, from_email, message_id, ...) // - _attachment_record_ids: array of PB record IDs // - _attachment_records: array of {id, filename, content_type, size_bytes} // - has_attachments: true iff at least one attachment was uploaded // - binary: original binary blob (preserved — Parse Email still needs metadata) // // On no-attachments emails: emits one item with empty attachment IDs and // has_attachments=false. Chain continues normally. // // On partial upload failure: emits one item with the IDs that DID succeed. // Downstream chain can re-attach by message_id via a separate repair job. // // PB credentials are hard-coded for now (TODO: move to n8n credentials). // The user's existing v4 had them in a Set node, so this is no worse. const inputs = $input.all(); if (inputs.length === 0) { return [{ json: { _attachment_record_ids: [], has_attachments: false }, binary: {} }]; } const emailItem = inputs[0]; const emailJson = emailItem.json || {}; const emailBinary = emailItem.binary || {}; // === PB credentials (TODO: move to n8n credentials) === const PB_URL = 'https://pocketbase-gnqc88suy8fqk6wyryjjdpvl.169.239.180.130.sslip.io'; const PB_EMAIL = 'david.els1@outlook.com'; const PB_PASSWORD = '1234567890'; // === 1. PB admin auth (inline so binary survives) === let pbToken = ''; try { const authResp = await this.helpers.httpRequest({ method: 'POST', url: PB_URL + '/api/collections/_superusers/auth-with-password', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, body: { identity: PB_EMAIL, password: PB_PASSWORD }, json: true, timeout: 15000, }); pbToken = String(authResp.token || ''); if (!pbToken) { throw new Error('PB auth returned no token: ' + JSON.stringify(authResp)); } } catch (authErr) { // Auth failure is fatal — without a token we can't upload. // Surface the error so the workflow execution fails loudly. throw new Error('PB admin auth failed in Catch and Create Uploader: ' + (authErr.message || JSON.stringify(authErr))); } // === 2. Walk every binary entry on the email and upload each to PB === const attachmentIds = []; const attachmentDetails = []; let uploadIndex = 0; for (const key of Object.keys(emailBinary)) { const b = emailBinary[key]; if (!b) continue; const filename = String(b.fileName || 'attachment'); const contentType = String(b.mimeType || 'application/octet-stream'); const sizeBytes = Number(b.fileSize || 0); // Skip clearly-empty entries (e.g. inline images referenced by content-id // where the message body, not an attachment, is what matters) if (!b.data || (typeof b.data === 'string' && b.data.length === 0)) { continue; } // === 3. Upload this one attachment to PB === // formData.value can be a base64 string (from webhook) or Buffer (from IMAP) // — n8n's httpRequest helper handles both. const fileValue = (typeof b.data === 'string') ? Buffer.from(b.data, 'base64') : b.data; try { const uploadResp = await this.helpers.httpRequest({ method: 'POST', url: PB_URL + '/api/collections/quote_attachments/records', headers: { 'Authorization': 'Bearer ' + pbToken, }, formData: { filename: filename, content_type: contentType, size_bytes: sizeBytes, email_message_id: String(emailJson.message_id || emailJson._effective_message_id || ''), uploaded_at: new Date().toISOString(), file: { value: fileValue, options: { filename: filename, contentType: contentType, }, }, }, json: true, timeout: 30000, }); if (uploadResp && uploadResp.id) { attachmentIds.push(String(uploadResp.id)); attachmentDetails.push({ id: String(uploadResp.id), filename: filename, content_type: contentType, size_bytes: sizeBytes, kind: (contentType === 'application/pdf' || /\.pdf$/i.test(filename)) ? 'pdf' : (contentType.startsWith('image/')) ? 'image' : 'other', binary_key: key, email_message_id: String(emailJson.message_id || emailJson._effective_message_id || ''), uploaded_at: new Date().toISOString(), }); } else { // PB didn't return an id — likely a validation error // Log but don't fail the whole workflow console.error('PB upload returned no id for ' + filename + ':', JSON.stringify(uploadResp)); } } catch (uploadErr) { // One attachment failed — log and continue with the others console.error('PB upload failed for ' + filename + ': ' + (uploadErr.message || uploadErr)); } uploadIndex++; } // === 4. Emit ONE item with email context + attachment IDs === return [{ json: Object.assign({}, emailJson, { _attachment_record_ids: attachmentIds, _attachment_records: attachmentDetails, _attachment_count: attachmentIds.length, has_attachments: attachmentIds.length > 0, pb_url: PB_URL, pb_token: pbToken, pb_admin_id: '', // could parse from authResp if needed }), // Preserve binary for Parse Email — it uses binary metadata (fileName, // mimeType, fileSize) to populate the attachments array. The data is // already safely in PB at this point; downstream uses IDs not bytes. binary: emailBinary, }];