Playbook: Pinbox24 Mailgun — Duplicate Check & File Metadata Fix
Incident date: 2026-07-02
Resolved: 2026-07-02 — permanent fix via Docker bind mounts (runtime) + GitLab MRs (code source)
Severity: P1 — all inbound emails landed at processStatusId: 300 (duplicates to confirm), blocking AI processing pipeline
Services affected: mailgun-prod_mailgun_1, s3-v2-v42-prod on bms-1 (94.23.26.113)
GH tracking: #2048
GitLab MRs (code-level permanent fix):
p4-ms-mailgun!12: fix/registerinsert-file-first-orderingpinbox24-ms-s3-v2!10: fix/mailgun-file-handler-return-metadata
Symptoms
- Every inbound email to
rechnung-AI@integrations-eu.pinbox24.comcreated a register record in the AI-Logs register - All records had
processStatusId: 300 | duplicates to confirmandduplicate: Y recordData.originalName,recordData.fileSize,recordData.recordMainDocumentwere allMISSINGon every record- Activity 296 (
filter_by_mq_filter_and_update) ranoriginalName='##originalName##' AND fileSize='##fileSize##'with empty substitutions → found 0 records →duplicateCount: 0→ activity 297 routed to 299 → 300
Root Cause
Two separate bugs combined to break the flow:
Bug 1 — s3-v2 GET vs POST method mismatch
The mailgunFileHandler route in s3-v2-v42-prod was registered as GET but p4-ms-mailgun calls it with POST. All requests received 404. The GET→POST routing fix had been applied at some earlier point (visible in logs: 404s before 15:20 UTC, 200s after).
Bug 2 — File-last ordering: metadata never reached the register record
The original integration.helper.js flow in mailgun-prod_mailgun_1:
1. Build doc (email headers, no file metadata)
2. POST /api/reg/:regid/new ← register record created WITHOUT originalName/fileSize/recordMainDocument
3. uploadMailgunAttachments(attachments, registerRecord.result, ...) ← file upload, return value IGNORED
originalName, fileSize, recordMainDocument were never set on doc before record creation.
Bug 3 — s3-v2 response wrapper not unwrapped
When the s3-v2 patch was applied to return file metadata, the raw response is wrapped:
{ "success": true, "result": { "_id": "...", "originalName": "...", "size": 12345 } }The initial mailgun patch checked fileResult._id directly — always undefined. The unwrapped value is at fileResult.result._id.
Fix Applied (runtime hotfix — 2026-07-02)
Patch 1: s3-v2-v42-prod — return file metadata instead of string
File: /app/dist/apps/storage/mailgunFileHandler.helper.js
Applied via patch_s3v2.js (Node.js patcher):
- Capture
Promise.allresult:const results = yield Promise.all(...) - After upload:
return { _id: doc._id, originalName: fileData.originalname, size: attachment.size }; - Return first result:
return results.filter(Boolean)[0] || null;(was:return "All files save";)
Verification: content-length changed from 42 bytes ({"success":true,"result":"All files save"}) to ~117 bytes (JSON object with _id, originalName, size).
Patch 2: mailgun-prod_mailgun_1 — file-first ordering with correct response unwrapping
File: /app/helper/integration.helper.js
Applied via patch_mailgun_v3.js (Node.js patcher) — replaced the registerinsert block:
if (action.type == "registerinsert") {
// PATCH v3: upload file FIRST; s3-v2 wraps response in { success, result }
var fileResult = null;
if (attachments) {
var attach = JSON.parse(attachments);
if (attach && attach.length) {
var tempRecord = { officeId: config.officeId, regId: action.regData.regid };
fileResult = await s3RequestHelper.uploadMailgunAttachments(attach, tempRecord, bodyMail.emailSender);
}
}
var fr = (fileResult && fileResult.result) ? fileResult.result : fileResult;
if (fr && fr._id) {
doc.recordMainDocument = String(fr._id);
doc.originalName = fr.originalName;
doc.fileSize = String(fr.size); // String() is required: mqFilter uses '##fileSize##' string comparison
}
var registerRecord = await requestHelper.makePostRequest(`/api/reg/${action.regData.regid}/new`, doc);
assert((registerRecord && registerRecord.success && registerRecord.result), ...);
}Key facts:
s3-v2always wraps responses:{ success: bool, result: <actual value> }(seestorage.controller.js:109)fileSizeMUST be stored as string — the mqFilterfileSize='##fileSize##'uses string equality; MongoDB strict type matching fails if stored as numbertempRecord(no_id) is passed to s3-v2 for the file-first upload —recId: undefinedis OK; s3-v2 uses it for the file doc but the register record is not yet created at that point
Verification
Real PDF from user (2026-07-02 17:57 UTC) — PROCEDURA OPERACYJNA - przeglądów technicznych ecotrans - W opracowaniu - v8.pdf (215,899 bytes):
processStatusId: 71 | Document passed to docRegistration ✅
recordData.originalName: PROCEDURA OPERACYJNA...v8.pdf ✅
recordData.fileSize: 215899 ✅
recordData.recordMainDocument: 6a468a6c09bbe50217ade025 ✅
recordData.duplicateCount: 1 (self-match only) ✅
recordData.duplicate: N ✅
Durability — Stan po sesji 2026-07-07
Patche przeżywają docker start i docker-compose up -d — bind mount w konfiguracji kontenera.
| Container | Patch | Bind mount (host→container) | Przeżywa restart |
|---|---|---|---|
mailgun-v42-prod | integration.helper.js (PATCH v3, 4×) | /root/mailgun-prod/patches/integration.helper.js:/app/helper/integration.helper.js:ro | ✅ docker start ✅ docker-compose up -d |
s3-v2-v42-prod | mailgunFileHandler.helper.js | /root/s3v2-prod/patches/mailgunFileHandler.helper.js:/app/dist/apps/storage/mailgunFileHandler.helper.js:ro | ✅ docker start ✅ docker-compose up -d |
WAŻNE: Stara nazwa kontenera mailgun to
mailgun-prod_mailgun_1. Od 2026-07-07 używajmailgun-v42-prod. Playbookbms1-post-restart-recovery.mdzaktualizowany —docker start mailgun-v42-prod.
MONGODB_URL w /root/mailgun-prod/mailgun-environment.env:
- Używa tej samej wartości co
NEW_MONGODB_URIw kontenerzev42-prod(env var z SOPSV42_NEW_MONGODB_URI) - Łączy się do
w4_dbna bms-2/bms-3 przezw4_appuser — NIE używajadmincredentials (inny klucz SOPS!) S3_V2_SERVER_URL=http://s3-v2-v42-prod:3000✅
integrationsEmails — konfiguracje EcoTrans:
rechnung-AI@integrations-eu.pinbox24.com— ✅ skonfigurowany (AI-Logs, officeId5c752bbca20da35ca1b99083)ai-standard-docs@integrations-eu.pinbox24.com— ✅ skonfigurowanygutschrift_AI@integrations-eu.pinbox24.com— ✅ dodany 2026-07-07 (brak → insert do MongoDB w4_db)
Jeśli kontenery zostaną usunięte (docker rm + nowy obraz)
Bind mount musi być re-aplikowany. Opcje:
Opcja A — docker-compose up (zalecana dla mailguna):
cd /root/mailgun-prod
CONTAINER_NAME=mailgun-v42-prod IMAGE_NAME=mailgun-v42-prod:latest CLUSTER_NETWORK=test-net docker-compose up -d
# docker-compose.yml ma już volumes: bind mount → patch przeżywaOpcja B — re-aplikuj patch ręcznie (gdy bind mount niedostępny):
# Copy patch scripts to bms-1
scp -i C:\Users\konar\.ssh\id_ed25519 docs\pinbox24\patch_s3v2.js root@94.23.26.113:/tmp/ # PLAYBOOK: pinbox24-mailgun-duplicate-check-fix.md
scp -i C:\Users\konar\.ssh\id_ed25519 docs\pinbox24\patch_mailgun_v3.js root@94.23.26.113:/tmp/ # PLAYBOOK: pinbox24-mailgun-duplicate-check-fix.md
# Apply s3-v2 patch
ssh -i C:\Users\konar\.ssh\id_ed25519 -o BatchMode=yes root@94.23.26.113 "docker cp /tmp/patch_s3v2.js s3-v2-v42-prod:/tmp/ && docker exec s3-v2-v42-prod node /tmp/patch_s3v2.js && docker exec s3-v2-v42-prod pm2 restart all --update-env" # PLAYBOOK: pinbox24-mailgun-duplicate-check-fix.md
# Apply mailgun patch
ssh -i C:\Users\konar\.ssh\id_ed25519 -o BatchMode=yes root@94.23.26.113 "docker cp /tmp/patch_mailgun_v3.js mailgun-v42-prod:/tmp/ && docker exec mailgun-v42-prod node /tmp/patch_mailgun_v3.js && docker exec mailgun-v42-prod pm2 restart all --update-env" # PLAYBOOK: pinbox24-mailgun-duplicate-check-fix.md
# Verify: check recent AI-Logs records have originalName/fileSize/recordMainDocument
# Send a test email and confirm processStatusId reaches 71, not 300Permanent Fix Required
The patches must be committed to the GitLab TypeScript sources:
| Service | GitLab repo | File to change |
|---|---|---|
pinbox24-ms-s3-v2 | gitlab.com/pinbox24/pinbox24-ms-s3-v2 | src/apps/storage/mailgunFileHandler.helper.ts — return { _id, originalName, size } |
p4-ms-mailgun | gitlab.com/pinbox24/p4-ms-mailgun | src/helper/integration.helper.ts — file-first flow, unwrap fileResult.result |
Access: GITLAB_ADMIN_PAT in secrets/administration.env.sops.
GH issue to track: create with label bug, pinbox24, tech-debt and milestone permanent-fix.
Smoke Test After Deploy
Send a test email with datetime-unique filename and verify the record:
// Run inside v42-prod container: node /tmp/send_test_v4.js
// (script at docs/pinbox24/send_test_v4.js)
// Expected: record appears with processStatusId 71 (or 298 after duplicate check), NOT 300Check record:
// Run inside v42-prod container (script at docs/pinbox24/check_all_recent.js)
// Look for: originalName, fileSize, recordMainDocument all populated; duplicateCount=1; duplicate=NActivity Flow Reference
The AI-Logs register process (aiProcessingLog) handles inbound emails:
| Activity | ID | Type | Action |
|---|---|---|---|
| check duplicates | 296 | trigger | filter_by_mq_filter_and_update on regRecords using originalName='##originalName##' AND fileSize='##fileSize##' → sets duplicateCount |
| if potential duplicate | 297 | switch | key "1" → 298 (no dup); ##isEmpty##/##isNotEmpty## other → 299 |
| no duplicate | 298 | trigger | update_register_record: sets duplicate: "N" |
| check if duplicate | 299 | trigger | further verification |
| duplicates to confirm | 300 | state | human review needed |
| Document passed to docRegistration | 71 | state | AI processing proceeds |
duplicateCount = 1 is expected and correct for a new unique file — the record matches only itself.
Related Docs
- Flow plan (original fix spec):
docs/pinbox24/mailgun-flow-fix-plan.md - Runtime hotfix pattern:
docs/playbooks/pinbox24-container-runtime-hotfix.md - s3-v2 service operations:
docs/playbooks/s3-v2-bms1-file-service.md - Mailgun inbound routing:
docs/playbooks/mailgun-receiving.md - AI-Logs process architecture:
docs/pinbox24/ai-processing-register-workflow.md