PSA/ee/docs/plans/2025-11-18-email-roundtrip-plan.md
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

5.2 KiB

Email Roundtrip Implementation Plan (Holistic)

Date: November 18, 2025 Status: Planned

Objective

Complete the email roundtrip feature by implementing outbound notification logic that ensures channel consistency and correct threading. When a ticket is created from an inbound email, any subsequent agent replies must be sent:

  1. From the same email address that received the original email.
  2. Via the same provider (Microsoft/Google) to ensure deliverability and header integrity.
  3. With correct In-Reply-To and References headers to maintain the conversation thread in the customer's email client.

Architecture Analysis

Current State

  • Inbound: system-email-processing-workflow creates tickets and stores email_metadata (including providerId, messageId, references).
  • Inbound Adapters: MicrosoftGraphAdapter and GmailAdapter currently only support reading emails.
  • Outbound: TenantEmailService uses EmailProviderManager to send emails, typically defaulting to a single configured outbound provider (SMTP/Resend).
  • Subscriber: ticketEmailSubscriber listens for events but doesn't currently support channel-specific routing or threading headers.

The Gap

The current system treats inbound and outbound email as separate pipelines. There is no mechanism to "reply via the inbound channel." If support@acme.com (Microsoft) receives an email, the system currently replies via noreply@platform.com (Resend), breaking the "From" address and potentially the thread.

Implementation Plan

1. Enhance Inbound Adapters (Bi-directional Capability)

Modify the existing inbound adapters to support sending emails. This allows us to reuse the existing authentication and connection logic.

  • File: server/src/services/email/providers/MicrosoftGraphAdapter.ts
    • Implement sendEmail(message: EmailMessage): Promise<EmailSendResult>.
    • Use the Graph API /me/sendMail or /users/{id}/sendMail.
    • Map In-Reply-To and References from message.headers to the API payload.
  • File: server/src/services/email/providers/GmailAdapter.ts
    • Implement sendEmail(message: EmailMessage): Promise<EmailSendResult>.
    • Use the Gmail API users.messages.send.
    • Construct a raw MIME message that includes the threading headers.

2. Upgrade EmailProviderManager (Channel Routing)

Enable the manager to route emails through specific providers based on ID, not just tenant defaults.

  • File: server/src/services/email/EmailProviderManager.ts
    • Add method sendEmailViaProvider(providerId: string, message: EmailMessage, tenantId: string).
    • Logic:
      1. Retrieve the provider configuration from the database using providerId.
      2. Instantiate the appropriate adapter (MicrosoftGraphAdapter or GmailAdapter).
      3. Call adapter.sendEmail(message).

3. Update Service Layer (Context Propagation)

Pass the routing and threading information from the business logic down to the provider manager.

  • File: server/src/types/email.types.ts & server/src/lib/email/BaseEmailService.ts
    • Update EmailMessage and BaseEmailParams to include:
      • headers?: Record<string, string> (for threading).
      • providerId?: string (for routing).
  • File: server/src/lib/services/TenantEmailService.ts
    • Update sendEmail to handle providerId.
    • If providerId is present, delegate to providerManager.sendEmailViaProvider.
    • Otherwise, fall back to the default providerManager.sendEmail.
  • File: server/src/lib/notifications/sendEventEmail.ts
    • Update SendEmailParams and sendEventEmail to accept and pass providerId and headers.

4. Wire Up the Subscriber (The Glue)

Connect the event system to the new logic.

  • File: server/src/lib/eventBus/subscribers/ticketEmailSubscriber.ts
    • In handleTicketCommentAdded:
      1. Filter: Ensure comment is public and from an agent/system.
      2. Context: Retrieve ticket.email_metadata.
      3. Routing: Extract providerId from email_metadata.
      4. Threading: Construct In-Reply-To (from messageId) and References.
      5. Send: Call sendNotificationIfEnabled passing providerId and headers.

UX/Configuration Considerations

  • Implicit Configuration: By reusing the inbound provider for replies, we avoid complex "mapping" UI. If a user connects support@acme.com for inbound, replies automatically go out via support@acme.com.
  • Setup Screens: No immediate changes required to Setup UI, as the "connection" already exists. Future enhancements could allow configuring a specific "Signature" or "Display Name" for the inbound channel.

Verification Strategy

  1. Unit Tests: Test sendEmail in adapters with mocks.
  2. Integration: Simulate a comment on a ticket created via email. Verify EmailProviderManager loads the specific provider and sends the email.
  3. Roundtrip E2E:
    • Send email to inbound@test.com -> Ticket Created.
    • Agent replies -> Email sent via inbound@test.com (verified via logs/headers).
    • Customer replies -> Thread continues correctly.