Email Provider & Sender — System Design

 


This document describes a system that provides a unified Email Provider + Sender platform: a Web API to manage OAuth connections to external providers (Gmail, Outlook, etc.), store user tokens, and a separate microservice that exposes unified async APIs to send, fetch, and list email messages across configured providers.

Overview

The system enables users to connect their email provider accounts (Gmail, Outlook, etc.) via OAuth, then send and receive emails through a unified API. It decouples user authentication/authorization (OAuth onboarding and token storage) from email operations (send, get, list) which are processed asynchronously using queues and background processors.

Primary goals:

  • Unified API surface for multiple providers
  • Secure storage and handling of user tokens and attachments
  • Asynchronous, resilient dispatch with retries and per-provider processors
  • Scalability and observability

Requirements

Functional Requirements

  • User onboarding: allow users to connect an external email provider via OAuth and store access/refresh tokens
  • Unified Send API: accept an email send request (recipients, subject, body, attachments metadata) and enqueue for dispatch
  • Attachments: attachments are uploaded via attachment CRUD APIs and stored in blob storage; metadata stored in DB; attachments encrypted at rest
  • Inbox APIs: list messages, get message detail (headers/body/attachments) from provider or cached copy
  • Async Delivery: sending must be asynchronous — user receives accepted response and later get status
  • Per-provider operations: support Gmail and Outlook (initial), pluggable connectors for other providers
  • Retry & Failure Handling: retries, exponential backoff, DLQ
  • Web UI: integration button to begin OAuth flows and show connection status
  • Provider callback/webhook processing (where supported)

Non-Functional Requirements

  • Security: encrypt tokens and attachments; least privilege; secrets management
  • Scalability: system should scale for many users and high concurrent send rates
  • Durability: attachments and message metadata persisted reliably
  • Observability: metrics, tracing, logs for send flows and provider interactions
  • Low Latency for API responses (acceptance time); async delivery can be eventual

Constraints and Assumptions

  • Max recipients per send request: configurable (example: 100)
  • Max attachments per message: 10; max attachment size: 5 MB per attachment (policy)
  • Providers: initial support for Gmail (via Gmail REST API) and Outlook/Graph API
  • Cloud platform: Azure-first (Azure Blob Storage, Azure Service Bus / Storage Queues, Azure Functions) but design is portable

Design Considerations

Key decisions:

  • Split responsibilities into two microservices:
    1. Auth Service (Web): OAuth onboarding, token management, basic user management, front-end UI redirect
    2. Email Service (Backend): Unified send/get/list APIs, validation, queuing, dispatch, inbox sync
  • Use relational DB (Postgres / Azure SQL) for metadata (users, tokens, message metadata). Use blob storage for attachments and optionally whole payloads for large bodies.
  • Encrypt sensitive data at rest (tokens & attachments) and encrypt in transit (TLS everywhere)
  • Use queues for reliable processing and to provide backpressure and scaling
  • Use per-provider connector modules that implement a common interface for send/list/get operations
  • Use idempotency keys for send requests to avoid duplicate sends

High-Level Architecture

                                      ┌─────────────┐
                                      │   Clients   │
                                      │ (UI/API/CLI)│
                                      └──────┬──────┘
                                             │
                                 ┌───────────▼───────────┐
                                 │    API Gateway / LB   │
                                 └───────┬────┬──────────┘
                                         │    │
                                 ┌───────▼┐  ┌▼────────┐
                                 │ Auth  │  │ Email   │
                                 │Service│  │ Service │
                                 │ (Web) │  │ (API)   │
                                 └──┬────┘  └──┬───────┘
                                    │          │
      OAuth Redirects & Callbacks   │          │  Send / Get / List APIs
                                    │          │
      ┌─────────────────────────────▼──────────┐│
      │        Token DB (encrypted)            ││
      │        User / Token / Conn table       ││
      └────────────────────────────────────────┘│
                                                │
    Attachment Uploads -> Blob Storage (encrypted)│
                                                │
   Email Service validation => Enqueue -> Dispatch Queue
                          │                     │
             ┌────────────▼───────────┐         │
             │  Dispatch Azure Func   │         │
             │  (select provider)     │         │
             └──────┬─────────┬───────┘         │
                    │         │                 │
           Provider Queue A  Provider Queue B   │
           (Gmail)            (Outlook)         │
                    │         │                 │
         Provider Processor  Provider Processor │
         (send via API / SMTP) (send via API)   │
                    └─────────┴────────────┬────┘
                                       Sent Queue
                                          │
                                   Post-send processors
                                   (status update, DLQ, webhooks)

Notes:

  • Auth Service handles OAuth flows and stores tokens in Token DB (encrypted).
  • Email Service validates send requests, persists metadata, stores attachments in blob storage, and enqueues messages into Dispatch Queue.
  • Dispatch Azure Function picks messages from Dispatch Queue, chooses provider, and pushes to provider-specific queues (or directly invokes provider).
  • Provider processors are responsible for provider-specific transforms, retry policies, and final status reporting to Sent Queue.

Data Model (Relational)

Below are example tables and DDL suggestions.

users

CREATE TABLE users (
  id UUID PRIMARY KEY,
  email VARCHAR(320) NOT NULL UNIQUE,
  display_name VARCHAR(255),
  created_at timestamptz default now()
);

provider_connections (stores per-user provider OAuth info — encrypted tokens)

CREATE TABLE provider_connections (
  id UUID PRIMARY KEY,
  user_id UUID REFERENCES users(id),
  provider_name VARCHAR(50) NOT NULL, -- e.g., 'gmail', 'outlook'
  provider_account_id VARCHAR(255), -- provider's account id
  access_token TEXT NOT NULL,        -- encrypted
  refresh_token TEXT,                -- encrypted
  token_expires_at timestamptz,
  scopes TEXT[],
  created_at timestamptz default now(),
  updated_at timestamptz default now(),
  is_active boolean default true
);
CREATE INDEX idx_provider_user ON provider_connections(user_id, provider_name);

messages (metadata for each send request)

CREATE TABLE messages (
  id UUID PRIMARY KEY,
  user_id UUID REFERENCES users(id),
  provider_connection_id UUID REFERENCES provider_connections(id), -- preferred provider
  external_id VARCHAR(255), -- provider message id (nullable until sent)
  status VARCHAR(50) NOT NULL, -- queued, sending, sent, failed, retrying
  subject TEXT,
  body_format VARCHAR(10) NOT NULL DEFAULT 'html', -- html|text
  created_at timestamptz default now(),
  updated_at timestamptz default now(),
  scheduled_at timestamptz NULL,
  idempotency_key VARCHAR(255) NULL,
  recipients JSONB, -- array with to/cc/bcc
  attachments JSONB, -- array of attachment metadata (blob id, size, name)
  attempt_count int default 0
);
CREATE INDEX idx_messages_user ON messages(user_id, created_at DESC);
CREATE UNIQUE INDEX idx_messages_idempotency ON messages (idempotency_key) WHERE idempotency_key IS NOT NULL;

attachments (metadata only; content in blob)

CREATE TABLE attachments (
  id UUID PRIMARY KEY,
  user_id UUID REFERENCES users(id),
  blob_path TEXT NOT NULL,
  size_bytes bigint NOT NULL,
  mime_type VARCHAR(255),
  name VARCHAR(512),
  uploaded_at timestamptz default now(),
  encrypted boolean default true
);
CREATE INDEX idx_attachments_user ON attachments(user_id, uploaded_at DESC);

Storage Choices

  • Blob Storage: store attachments and optional raw message blobs. Use server-side encryption and optionally client-side encryption before upload.
  • Relational DB: Postgres for strong consistency, transactional operations (message metadata, tokens).
  • Cache (Redis): cache provider tokens metadata, rate-limiting counters, and recent message status.
  • Queues: Azure Service Bus (preferred for advanced features), or Storage Queues for simpler designs.

API Design (Auth Service)

All requests use HTTPS and JSON. OAuth redirects originate from the UI.

1. Begin OAuth (UI integration button)

GET /v1/connect/{provider}?userId={userId}&returnUrl={...}

  • Purpose: returns provider OAuth authorization URL to redirect the user.
  • Response: 302 redirect to provider or JSON {url}

2. OAuth Callback

POST /v1/connect/{provider}/callback

  • Provider redirects to this endpoint with code/state.
  • Server exchanges code for access/refresh tokens, stores encrypted tokens in provider_connections.
  • Validate state for CSRF protection.
  • Response: 200 + redirect to UI return URL.

3. List Connections

GET /v1/connections?userId={userId}

  • Returns active connections for user (provider_name, provider_account_id, connected_at)

4. Revoke / Disconnect

DELETE /v1/connections/{connectionId}

  • Revoke tokens in provider (if possible) and mark record inactive.

API Design (Email Service)

1. Send Email (async accept)

POST /v1/messages/send Request JSON (validated):

{
  "userId": "uuid",
  "providerPreference": "gmail", // optional
  "idempotencyKey": "optional-client-key",
  "subject": "...",
  "body": "<html>...</html>",
  "bodyFormat": "html",
  "to": ["a@example.com"],
  "cc": [],
  "bcc": [],
  "attachments": ["attachmentId1","attachmentId2"],
  "scheduledAt": "2025-11-05T...Z"
}

Response:

  • 202 Accepted { "messageId": "uuid", "status": "queued" } Errors: 400 (validation), 413 (attachment size), 422 (recipient count), 429 (rate limit)

Server-side validation steps:

  • Validate userId & active provider connection existence
  • Validate recipients count against policy
  • Validate attachments count & sizes by fetching metadata from attachments
  • Save message metadata in messages table with status queued
  • Enqueue message id to dispatch queue

2. Attachment CRUD

  • Upload: multipart/form-data POST /v1/attachments/upload -> returns attachmentId; server stores blob and metadata
  • Delete/Get/List attachments per user

3. Message Status / Get / List

  • GET /v1/messages/{messageId} -> returns metadata, status, provider external id etc.
  • GET /v1/messages?userId=&limit=&offset= -> returns list
  • GET /v1/messages/{messageId}/raw -> returns body and attachments links (signed URLs)

4. Inbox APIs (unified)

  • GET /v1/inbox/list?userId={userId}&provider={optional}&since={}
    • Implementation: proxy to provider connector that may return cached items from DB or trigger provider API fetch.
  • GET /v1/inbox/{providerMessageId} -> unified message representation

Async Dispatch Flow

  1. Client POST /v1/messages/send -> API validates and persists message + attachments metadata
  2. API enqueues message id to dispatch-queue
  3. Dispatch Azure Function triggered by dispatch-queue picks message:
    • Retrieve message metadata and attachments
    • Determine provider (explicit or pick by preference/availability)
    • Transform message to provider format (MIME, REST payload)
    • Place into provider-specific queue (e.g., gmail-send-queue) OR attempt immediate send
  4. Provider Processor picks provider queue entry and calls the provider API (Gmail/Graph):
    • Use stored access_token; refresh if expired using refresh_token
    • Ensure idempotency header or client-side idempotency if supported
    • On success: update messages.external_id and status = sent; push to sent-queue
    • On transient failure: increment attempt_count, schedule retry using exponential backoff; if attempts exceed threshold push to DLQ
    • On permanent failure: mark failed and emit events/notifications
  5. Post-send processors consume sent-queue for user notifications, analytics, and webhook firing to client callbacks (if any)

Per-provider queues vs single queue

  • Per-provider queue gives better isolation, allows provider-specific throttling and scaling, and simpler backoff policies. Recommended for production.

Retry & Backoff

  • Use exponential backoff with jitter: base * 2^attempt +- jitter
  • Use attempt_count in messages table to limit retries (example: max 5 attempts)
  • Use a DLQ for messages that consistently fail

Idempotency

  • API accepts idempotencyKeymessages table has unique index to ensure duplicate client retries map to same message
  • Provider-level idempotency: use provider features where available (e.g., Gmail messageId or idempotent header) or implement dedupe in our processor using external_id + user

Provider Integration Patterns

  1. Gmail (REST API):

    • Use Gmail REST users.messages.send to send encoded MIME parts or raw base64. Use Gmail push notifications (Pub/Sub) for inbox sync if desired.
    • OAuth scopes: https://www.googleapis.com/auth/gmail.send.../gmail.readonly as needed.
    • Token refresh using refresh_token; store refresh_token and rotate securely.
  2. Outlook / Microsoft Graph:

    • Use Graph POST /me/sendMail or messages endpoints for sending and reading.
    • OAuth scopes: Mail.SendMail.Read, etc.
  3. IMAP/SMTP fallback:

    • For providers where REST APIs are not available/desired, support SMTP send connectors and IMAP for fetch, but ensure OAuth XOAUTH2 or app-specific passwords and security.
  4. Inbox sync strategies:

    • Push (preferred): use provider push mechanisms (Gmail Pub/Sub); requires additional infra to receive and validate notifications.
    • Poll (simple): poll provider on schedule with incremental sync (historyId, delta tokens) and store unified messages in DB.

Security

  • Encrypt access/refresh tokens at rest with a KMS-managed key (Azure Key Vault or AWS KMS)
  • Encrypt attachments in blob storage; consider client-side encryption for highly sensitive data
  • Use least privilege for service principals and provider OAuth client registration
  • Sanitize and validate all user-supplied content
  • Rate-limit APIs and per-user quotas
  • Rotate encryption keys periodically and support token rotation
  • Store audit logs of token usage and provider calls

Observability & Monitoring

  • Metrics: queue length, dispatch latency, send success/failure rates, API error rates, token refresh counts
  • Tracing: distributed tracing across API -> queue -> dispatcher -> provider
  • Logs: structured logs with correlation ids
  • Alerts: DLQ growth, token refresh failures, high failure rates for providers

Low-Level Design: C# Skeleton

Below are key interfaces and classes to implement core logic. This is a skeleton to start a .NET solution.

Models

public record User(Guid Id, string Email, string DisplayName);

public class ProviderConnection
{
    public Guid Id { get; set; }
    public Guid UserId { get; set; }
    public string ProviderName { get; set; }
    public string ProviderAccountId { get; set; }
    public string EncryptedAccessToken { get; set; }
    public string EncryptedRefreshToken { get; set; }
    public DateTime? TokenExpiresAt { get; set; }
}

public class MessageEntity
{
    public Guid Id { get; set; }
    public Guid UserId { get; set; }
    public Guid? ProviderConnectionId { get; set; }
    public string Status { get; set; }
    public string Subject { get; set; }
    public string Body { get; set; }
    public string BodyFormat { get; set; }
    public int AttemptCount { get; set; }
    public string IdempotencyKey { get; set; }
    public string RecipientsJson { get; set; }
    public string AttachmentsJson { get; set; }
    public string ExternalId { get; set; }
}

public class AttachmentEntity { /* blob id, name, size, mime */ }

Provider Connector Interface

public interface IEmailProviderConnector
{
    string ProviderName { get; }
    Task<SendResult> SendAsync(ProviderConnection connection, MessageEntity message, CancellationToken ct);
    Task<ProviderMessage> GetMessageAsync(ProviderConnection connection, string providerMessageId, CancellationToken ct);
    Task<IEnumerable<ProviderMessage>> ListMessagesAsync(ProviderConnection connection, ListOptions options, CancellationToken ct);
}

Dispatch Flow (Azure Function skeleton)

public class DispatchFunction
{
    private readonly IMessageRepository _messages;
    private readonly IProviderFactory _providerFactory; // returns connector by name
    private readonly IQueueClient _providerQueueClient; // Service Bus client

    [FunctionName("DispatchQueueTrigger")]
    public async Task Run(
        [ServiceBusTrigger("dispatch-queue", Connection = "ServiceBusConn")] string messageId,
        ILogger log)
    {
        var msg = await _messages.GetAsync(Guid.Parse(messageId));
        // choose provider
        var connector = _providerFactory.Resolve(msg.ProviderConnectionId ?? default);
        // transform and enqueue to provider queue
        var providerEntry = new ProviderQueueEntry { MessageId = msg.Id, Payload = Transform(msg) };
        await _providerQueueClient.SendMessageAsync(providerEntry);
    }
}

Provider Processor (Service Bus Trigger)

public class ProviderProcessor
{
    private readonly IEmailProviderConnector _connector;
    private readonly IMessageRepository _messages;

    [FunctionName("GmailSendProcessor")]
    public async Task Run([ServiceBusTrigger("gmail-send-queue")] ProviderQueueEntry entry, ILogger log)
    {
        var msg = await _messages.GetAsync(entry.MessageId);
        try {
           var conn = await _messages.GetProviderConnection(msg.UserId, "gmail");
           var result = await _connector.SendAsync(conn, msg, CancellationToken.None);
           // update status
           msg.Status = result.Success ? "sent" : "failed";
           msg.ExternalId = result.ProviderMessageId;
           await _messages.UpdateAsync(msg);
        } catch (TransientException t) {
           await _messages.IncrementAttempt(msg.Id);
           // schedule retry using Service Bus ScheduledEnqueueTimeUtc
        }
    }
}

Token Refresh Helper

  • Implement a background job (e.g., timer-triggered Azure Function) to scan provider_connections for tokens near expiry and refresh them proactively. Store rotated tokens encrypted and update DB.

Operational Concerns & Edge Cases

  • Duplicate sends: handle via idempotency keys and provider-side idempotency where possible.
  • Token revocation: handle revoked tokens gracefully and surface helpful UI messages to re-authenticate.
  • Large attachments: enforce limits and surface user-friendly errors; consider chunked upload if needed.
  • Provider rate limits: implement per-provider rate limiter and backoff.
  • Inbox sync conflicts: use message IDs and history tokens to dedupe.
  • Privacy and compliance: purge tokens and attachments on user request (Right to be forgotten).

Monitoring and Quality Gates

  • Build: .NET solution should compile and unit tests run.
  • Lint/Typecheck: use Roslyn analyzers and CI checks.
  • Tests: unit tests for connectors (mocked), and integration test harness against sandbox Gmail/Graph accounts.

Next Steps / Implementation Plan

  • Implement Auth Service OAuth flows and token DB (MVP)
  • Build Email Service API: send, attachments, list
  • Implement Dispatch Function and provider connectors for Gmail & Outlook
  • Add monitoring and tracing
  • Create end-to-end test harness with sandbox accounts

Vikash Chauhan

C# & .NET experienced Software Engineer with a demonstrated history of working in the computer software industry.

Post a Comment

Previous Post Next Post

Contact Form