Email Sending — Technical Documentation
Last updated: February 2026
Table of Contents
- Overview
- Architecture
- Repository Map
- Core Flows
- Entities & Data Model
- Queue Processing
- Services & Business Logic
- Postal Integration
- Tracking System
- Response Handling
- Configuration
- Integration Points
1. Overview
Email Sending handles campaign email delivery via Sopro's self-hosted Postal mail server. The system manages scheduling, queue-based sending, click/open tracking via rewritten URLs and tracking pixels, and response handling (bounces, replies, AI classification). Primary implementation is in sopro-sodastream (scheduling, sending, tracking) and sopro-sodastream-core (queue consumers, shared utilities).
2. Architecture
3. Repository Map
| Repository | Layer | Key Files |
|---|---|---|
| sopro-sodastream | Sender | PostalAPISender.cs |
| sopro-sodastream | Scheduling | CampaignEmailSchedule/ |
| sopro-sodastream | Tracking Controller | Controllers/TrackingController.cs |
| sopro-sodastream | Response Handler | Controllers/ResponseController.cs |
| sopro-sodastream | URL Rewriting | Helpers/UrlRewriter.cs |
| sopro-sodastream | Tracking Pixel | Helpers/TrackingPixelHelper.cs |
| sopro-sodastream | Entities | Models/CampaignEmailSchedule.cs, EmailMessage.cs |
| sopro-sodastream-core | Queue Consumer | SoProQueueConsumer/PostalMessageHandler.cs |
| sopro-sodastream-core | Shared Utils | Utils/EmailHelpers/ |
| sopro-sodastream-core | Queue Types | SoProQueueDataApi/ |
4. Core Flows
4.1 Email Scheduling & Sending
4.2 Click Tracking Flow
4.3 Open Tracking Flow
4.4 Response Handling Flow
5. Entities & Data Model
CampaignEmailSchedule
| Column | Type | Description |
|---|---|---|
Id | int (PK) | Schedule entry identifier |
CampaignId | int (FK) | Campaign reference |
ProspectId | long (FK) | Prospect to email |
ScheduledAt | datetime | When the email should be sent |
Status | int (enum) | Pending, Sent, Failed, Cancelled |
EmailType | int (enum) | Initial, Chaser1, Chaser2, Chaser3 |
SentAt | datetime? | Actual send timestamp |
PostalMessageId | nvarchar(255) | Postal message ID for tracking |
EmailMessage
| Column | Type | Description |
|---|---|---|
Id | int (PK) | Message identifier |
CampaignId | int (FK) | Campaign reference |
ProspectId | long (FK) | Recipient prospect |
Subject | nvarchar(500) | Email subject line |
Body | nvarchar(max) | Email HTML body |
FromEmail | nvarchar(255) | Sender email address |
FromName | nvarchar(255) | Sender display name |
PostalMessageId | nvarchar(255) | Postal tracking ID |
SentAt | datetime | Send timestamp |
Status | int (enum) | Sent, Delivered, Opened, Clicked, Replied, Bounced |
EmailTrackingEvent
| Column | Type | Description |
|---|---|---|
Id | int (PK) | Event identifier |
MessageId | int (FK) | Related email message |
EventType | int (enum) | Open, Click, Bounce, Reply, Complaint |
OccurredAt | datetime | Event timestamp |
IPAddress | nvarchar(45) | Source IP |
UserAgent | nvarchar(500) | Client user agent string |
ClickUrl | nvarchar(2000) | Clicked URL (for click events) |
6. Queue Processing
Queue Configuration
| Setting | Value |
|---|---|
| Queue Type | Azure Queue (Type 46) |
| Consumer | SoProQueueConsumer in sopro-sodastream-core |
| Message Format | JSON with campaign ID, prospect ID, message content |
SoProQueueTypeEnum
The email sending queue type is defined in the shared queue type enum:
PostalMessage = 46 // Email delivery via Postal
Processing Pipeline
7. Services & Business Logic
PostalAPISender
Location: sopro-sodastream/PostalAPISender.cs
Purpose: Sends emails through the self-hosted Postal server via HTTP API.
| Method | Description |
|---|---|
SendAsync() | Sends a single email message via Postal |
BatchSendAsync() | Sends multiple messages in batch |
CampaignEmailSchedule (Service)
Location: sopro-sodastream/CampaignEmailSchedule/
Purpose: Calculates send slots and enqueues messages.
| Method | Description |
|---|---|
CalculateSendSlots() | Distributes daily volume across send window |
EnqueueScheduledEmails() | Places ready-to-send messages in Azure Queue |
Queue Consumer Handler
Location: sopro-sodastream-core/SoProQueueConsumer/PostalMessageHandler.cs
Purpose: Processes queued messages — personalises, adds tracking, sends.
8. Postal Integration
Server Details
| Setting | Value |
|---|---|
| Server | Self-hosted Postal |
| Address | 193.9.15.161:5000 |
| Protocol | HTTP API (REST) |
| Auth | API key per mail server organisation |
API Usage
// Sending via Postal API
POST http://193.9.15.161:5000/api/v1/send/message
Headers: X-Server-API-Key: {api-key}
Body: {
"to": ["prospect@company.com"],
"from": "sender@client-outreach.com",
"subject": "...",
"html_body": "...",
"tag": "campaign-{campaignId}"
}
Postal Webhooks
Postal sends webhook notifications for delivery events:
| Event | Description |
|---|---|
MessageSent | Email successfully delivered to recipient server |
MessageBounced | Delivery failed (hard or soft bounce) |
MessageHeld | Message held for review |
9. Tracking System
URL Rewriting
All links in outgoing emails are rewritten to pass through the tracking controller:
Original: https://www.client.com/demo
Rewritten: https://track.client-outreach.com/c/{trackId}
The trackId encodes the message ID, prospect ID, and original URL.
Tracking Pixel
A 1x1 transparent PNG is appended to the email HTML body:
<img src="https://track.client-outreach.com/o/{trackId}.png" width="1" height="1" style="display:none;" />
Tracking Domain
Each campaign sender has a dedicated tracking domain (e.g., track.client-outreach.com) that points to Sopro's tracking infrastructure. This:
- Avoids using Sopro's primary domain (reputation isolation)
- Matches the sender's branding
- Passes email security checks (SPF, DKIM)
10. Response Handling
Reply Processing
Replies are received and processed through:
- Postal webhook notifies of incoming reply
- ResponseController receives and parses the reply
- AI Classification categorizes the reply (positive, negative, referral, OOO, query)
- Forwarding — reply is forwarded to the client via Sodastream
- Statistics — campaign stats updated
Bounce Handling
| Bounce Type | Treatment |
|---|---|
| Hard bounce (550, 551, 552) | Prospect marked as undeliverable, suppressed |
| Soft bounce (450, 451, 452) | Retry up to 3 times, then treat as hard bounce |
| Spam complaint | Immediate suppression from all campaigns |
11. Configuration
Postal Server Config
Configured per environment in Sodastream's web.config / app settings:
PostalApiUrl=http://193.9.15.161:5000/api/v1
PostalApiKey=<per-organisation-key>
Queue Configuration
Azure Queue connection string in application settings:
{
"AzureStorage": {
"ConnectionString": "<azure-storage-connection>"
}
}
Tracking Domain Setup
Each client tracking domain requires:
- DNS A record → Sopro tracking server
- SSL certificate (auto-provisioned or manual)
- Registration in Sodastream campaign settings
12. Integration Points
Upstream (data flows in)
| Source | Data | Mechanism |
|---|---|---|
| Campaign Management | Campaign config, audience, schedule | Database |
| Generative Messaging | AI-generated email content | Database (stored by AIM) |
| Email Finding | Prospect email addresses | Database |
Downstream (data flows out)
| Target | Data | Mechanism |
|---|---|---|
| Postal Server | Email messages for delivery | HTTP API |
| Campaign Statistics | Opens, clicks, replies, bounces | Database + webhooks |
| Sodastream CRM | Reply forwarding to clients | Internal processing |
| Prospect Records | Engagement status updates | Database |
Cross-Feature Dependencies
| Feature | Relationship |
|---|---|
| Campaign Management | Campaign config drives scheduling and sending |
| Generative Messaging | AIM generates the email content that gets sent |
| Email Finding | Found emails are the delivery addresses |
| Verification Pipeline | Verified emails reduce bounce rates |
| Web Intent Tracking | Website visits may trigger campaign engagement |