feat(email-intake): tenant-scoped email-to-ticket POST endpoint (PMS-450 phase 1) #337
No reviewers
Labels
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
psa-systems/mokosh-server!337
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/PMS-450-email-intake"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Adds
POST /api/v1/email-intake, the webhook surface external mail gateways (postfix MDA hook, Cloudron, Microsoft Graph subscription forwarder) call with a parsed inbound email to land it as a ticket inside a tenant. Three primitives ship together:tenant_intake_tokenstable holds one or more bearer tokens per tenant. The plaintext is shown once at create time; only the SHA-256 hash is stored, so a database leak does not yield working tokens.kindis a discriminator so the same table can later host other webhook surfaces (rmm-intake, billing-webhook) without a schema migration.tickets(tenant_id, email_message_id) WHERE email_message_id IS NOT NULLenforces dedup at the DB layer in addition to the application-layer check, so a concurrent retry of the same Message-Id loses cleanly with a constraint violation that the service translates back to a deduplicated response.CreateTicketRequestgains optionalemail_message_id+email_thread_idfields, plumbed throughcreate_ticket's INSERT. The existing seed / portal / RMM ticket paths leave themNone; only the email-intake handler populates them.Intake flow (mirrors the portal-ticket pattern in
create_portal_ticket): dedup on Message-Id, thread via thereferencesheader (returns the existing ticket id when a reply is detected; Phase 1 only returns the id, Phase 2 will append the reply as a comment), look up the contact by lowercased From: address inside the caller's tenant scope, fall back to the first admin/manager ascreated_by_idbecause email-intake has no agent identity, then callTicketService::create_ticketwithsource='email'set. The integration test undertests/email_intake.rsexercises every branch (happy, dedup, threading, unknown-sender 400, bad/missing bearer 401, and a final "exactly one ticket created" assertion that catches a regression in either dedup or threading from multiplying rows).Phase 2, tracked under PMS-450:
tenant_settings.email_intake_default_company_idso the intake can auto-create a contact when the From: address is not already on the contacts list;referencesarray) into a ticket comment rather than just returning the existing id;email_intake_logaudit table that captures every raw payload (headers + body) so a malformed sender can be debugged after the fact;tenant_intake_tokens(currently seeded directly via SQL; operators need a UI to mint + revoke).#PMS-450
f3071b25245071ad9ab0