CRM Integration Guide
How to connect EmailEngine to a CRM: syncing mail into contact records, tracking activity, and sending from the user's own mailbox.
Overview
EmailEngine is frequently utilized by smaller, niche CRM systems for email integration, such as those designed for managing donations at a church or coordinating influencers for marketing campaigns.
When integrating email with a CRM system, it typically involves connecting the CRM users' email accounts to the platform. This integration provides two key benefits:
- Outbound: Users can send emails directly from the CRM to their contacts while maintaining their personal identity
- Inbound: The CRM actively monitors connected email accounts, identifying and tracking email exchanges with CRM contacts
Architecture Overview
Data Flow
- User Authentication: CRM user connects their email account via EmailEngine
- Initial Sync: EmailEngine performs initial mailbox synchronization
- Webhook Notifications: EmailEngine sends webhooks for new incoming and sent emails
- Contact Matching: CRM matches email addresses with contacts
- Activity Logging: Email interactions are logged as CRM activities
- Outbound Sending: CRM users send emails through EmailEngine's API
Connecting Email Accounts
Using the Authentication Form
Instead of requesting credentials through your CRM UI, use EmailEngine's built-in authentication form feature. This approach is more convenient and secure:
curl -XPOST \
"https://emailengine.example.com/v1/authentication/form" \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"account": "USER_ID",
"name": "User Name",
"email": "user@example.com",
"subconnections": [
"\\Sent"
],
"redirectUrl": "https://myapp/account/settings.php"
}'
Parameters:
| Parameter | Description |
|---|---|
account | User ID from your CRM system (for consistency) |
name | User's display name |
email | User's email address |
subconnections | Additional folders for instant notifications (see below) |
redirectUrl | URL to redirect user after authentication |
Response:
{
"url": "https://emailengine.example.com/accounts/new?data=eyJhY2NvdW50Ijo..."
}
Redirect the user's browser to this URL to initiate authentication.
Benefits of Authentication Form
- No Password Handling: Your application never handles passwords or OAuth tokens
- OAuth2 Support: Users can authenticate via OAuth2 providers (Gmail, Outlook, etc.)
- Secure Storage: EmailEngine encrypts and stores credentials internally
- User-Friendly: Provides a guided authentication flow
Authentication Flow
The user will see:
- Account Type Selection: Choose between IMAP, OAuth2 (Gmail, Outlook), etc.
- Credentials Entry: Enter the address and password, or complete the OAuth2 authorization. Server settings are discovered from the address and asked for only when discovery fails (since v2.81.0)
- Confirmation: See connection status
- Redirect: Automatically redirected back to your CRM
After authentication completes and initial synchronization finishes, the account reaches "connected" state and is ready to use.
Sub-Connections for Sent Mail
Email servers immediately notify EmailEngine about new emails in the primary folder (Inbox), but for secondary folders like Sent Mail, EmailEngine relies on polling. This can cause delays in detecting sent emails.
Problem: If a CRM user sends an email and receives a response within minutes, EmailEngine might detect the reply but not the initial sent email (hasn't polled the Sent folder yet).
Solution: Use sub-connections to treat additional folders as primary, enabling instant notifications.
Configuring Sub-Connections
{
"account": "USER_ID",
"name": "User Name",
"email": "user@example.com",
"subconnections": [
"\\Sent"
],
"redirectUrl": "https://myapp/account/settings.php"
}
Using the special use flag \Sent allows EmailEngine to automatically determine the correct Sent Mail folder path for different email providers.
Important Considerations
- Each sub-connection is an additional IMAP session to the same account
- Providers cap the number of parallel IMAP sessions per account, and the cap differs by provider
- List only the folders that need immediate notification; every other folder is still polled
Read more: Enable Sub-Connections
Listening for Webhooks
Configure Webhook URL
Set up your webhook endpoint in EmailEngine's configuration:
- Navigate to Configuration > Webhooks in EmailEngine and check Enable Webhooks
- Enter your CRM endpoint as the Webhook URL
- Tick the events to deliver. The list is an allowlist with no default, so an instance where nothing is ticked delivers nothing
Event Types (the ones a CRM sync uses):
- messageNew ("New Email"): a message appeared in a folder, incoming and sent alike
- messageDeleted: a message was removed from a folder
- messageBounce: a bounce report arrived for a sent message
- accountAdded and accountDeleted: an account was registered or removed
The full list is on the webhook events reference.
Best Practice: Tick only New Email (messageNew) to keep the volume down. It covers both incoming and sent mail, and it is the one event the registry below is built on.
Create Webhook Handler
Create an endpoint in your CRM that accepts JSON payloads:
<?php
// webhook-handler.php
// Read webhook payload
$payload = json_decode(file_get_contents('php://input'), true);
// Return 2xx quickly, process asynchronously
http_response_code(200);
// Queue for background processing
queueWebhook($payload);
function queueWebhook($payload) {
// Add to job queue (Redis, database, etc.)
// Process later by background worker
}
Important: Always return HTTP 2xx status quickly. Process webhooks asynchronously to avoid blocking EmailEngine.
Classifying New Emails
Understanding Email States
Every email added to a folder appears as "new," even if moved between folders. This creates challenges:
- Moving email from Inbox to Spam: Appears as deletion + new email
- Moving back to Inbox: Appears as another new email
- Email IDs change when messages move between folders
Using Message IDs for Deduplication
Solution: Use the messageId property (from the Message-ID header) to track processed emails.
Example Webhook Payload:
{
"account": "USER_ID",
"date": "2023-04-21T08:08:47.884Z",
"path": "INBOX",
"specialUse": "\\Inbox",
"event": "messageNew",
"data": {
"id": "AAAARgAACMA",
"uid": 2240,
"from": {
"name": "Sender Name",
"address": "sender@example.com"
},
"to": [
{
"name": "",
"address": "user@example.com"
}
],
"subject": "Hello world!",
"messageId": "<01000187a29df5a2@example.com>",
"messageSpecialUse": "\\Inbox",
"threadId": "3d3e3d89-fc5b-4336-a454-a3ce280d849c"
}
}
Key Properties
-
data.messageSpecialUse: Primary special folder for the email\Inbox: Incoming email\Sent: Sent email\Junk: Spam/Junk\Trash: Deletednull: User-created folder
-
data.messageId: Unique identifier from Message-ID header- Use for deduplication
- Remains constant across folders
- Absent when the message carries no
Message-IDheader; such a message cannot be deduplicated this way
Identifying Email Direction
function classifyEmail(payload) {
const specialUse = payload.data.messageSpecialUse;
if (specialUse === '\\Inbox') {
return 'incoming';
} else if (specialUse === '\\Sent') {
return 'sent';
} else if (specialUse === '\\Draft') {
return 'draft'; // ignore drafts
} else if (specialUse === '\\Junk' || specialUse === '\\Trash') {
return 'ignore';
} else {
// User folder - could be incoming via filter
return 'incoming';
}
}
Note: Some users have email filters that move specific emails to custom folders. Consider treating non-special folders as incoming emails.
Servers That Provide emailId
Some servers also provide emailId, an identifier that stays the same when a message moves between folders: Gmail over IMAP and over the Gmail API, Microsoft Graph accounts, and IMAP servers that implement the OBJECTID extension. It is absent when the server offers nothing of the kind:
{
"data": {
"emailId": "187a29df5a2",
"messageId": "<01000187a29df5a2@example.com>"
}
}
Use emailId when the payload carries it and fall back to messageId otherwise.
Building a Message Registry
Create a registry to track processed emails and prevent duplicates.
Database Schema
CREATE TABLE message_registry (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id VARCHAR(255),
message_id VARCHAR(255),
processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY user_message_idx (user_id, message_id)
);
Check for New Messages
INSERT IGNORE INTO message_registry (user_id, message_id)
VALUES ('USER_ID', '<01000187a29df5a2@example.com>');
Result:
- 1 row added: Email is new, process it
- 0 rows added: Email already seen, ignore it
Processing Logic
<?php
function processWebhook($payload) {
$userId = $payload['account'];
$messageId = $payload['data']['messageId'];
// Skip a message without a Message-ID header; it cannot be deduplicated
if (empty($messageId)) {
return;
}
// Check if already processed
$result = $db->query(
"INSERT IGNORE INTO message_registry (user_id, message_id)
VALUES (?, ?)",
[$userId, $messageId]
);
if ($result->affectedRows === 0) {
// Already processed
return;
}
// New email - process it
$direction = classifyEmailDirection($payload);
if ($direction === 'incoming') {
processIncomingEmail($payload);
} elseif ($direction === 'sent') {
processSentEmail($payload);
}
}
Processing Incoming Emails
Focus on the From address to identify the contact:
<?php
function processIncomingEmail($payload) {
$fromAddress = $payload['data']['from']['address'];
// Skip if no from address (system messages)
if (empty($fromAddress)) {
return;
}
// Match with CRM contact
$contact = findContactByEmail($fromAddress);
if ($contact) {
// Log email activity for this contact
logEmailActivity([
'contact_id' => $contact['id'],
'user_id' => $payload['account'],
'direction' => 'incoming',
'subject' => $payload['data']['subject'],
'date' => $payload['date'],
'message_id' => $payload['data']['messageId'],
]);
}
}
Handling Multiple From Addresses
EmailEngine normalizes the From header into a single entry (not an array), even if the email technically has multiple From addresses (rare).
Processing Sent Emails
Sent emails are more complex because they can have multiple recipients:
<?php
function processSentEmail($payload) {
$recipients = [];
// Collect all recipients from To and CC
if (isset($payload['data']['to'])) {
foreach ($payload['data']['to'] as $address) {
$recipients[] = $address['address'];
}
}
if (isset($payload['data']['cc'])) {
foreach ($payload['data']['cc'] as $address) {
$recipients[] = $address['address'];
}
}
// Match each recipient with CRM contacts
foreach ($recipients as $recipientEmail) {
$contact = findContactByEmail($recipientEmail);
if ($contact) {
// Log sent email activity
logEmailActivity([
'contact_id' => $contact['id'],
'user_id' => $payload['account'],
'direction' => 'sent',
'subject' => $payload['data']['subject'],
'date' => $payload['date'],
'message_id' => $payload['data']['messageId'],
]);
}
}
}
Multi-Participant Activities
Depending on your CRM's data model:
- Option 1: Create separate activity for each matching contact
- Option 2: Create one activity linking multiple contacts (if CRM supports it)
Sending Emails from CRM
Enable users to send emails directly from the CRM interface using EmailEngine's submission API:
<?php
use Postalsys\EmailEnginePhp\EmailEngine;
use Postalsys\EmailEnginePhp\Exceptions\EmailEngineException;
function sendEmailFromCRM(string $userId, string $toEmail, string $subject, string $body): array
{
$ee = EmailEngine::fromOptions([
'access_token' => getenv('EMAILENGINE_TOKEN'),
'ee_base_url' => getenv('EMAILENGINE_URL'),
]);
try {
$response = $ee->messages->submit($userId, [
'to' => [
['address' => $toEmail],
],
'subject' => $subject,
'html' => $body,
]);
return [
'success' => true,
'messageId' => $response['messageId'],
'queueId' => $response['queueId'],
];
} catch (EmailEngineException $e) {
return [
'success' => false,
'error' => $e->getMessage(),
];
}
}
The from address is omitted, so EmailEngine uses the identity stored with the account, which is what a CRM user expects: the message leaves from their own mailbox and lands in their own Sent folder.
Submission and the Outbox
POST /v1/account/{account}/submit never sends inline. It validates the message, queues it, and returns messageId and queueId; delivery happens from the queue and is reported by the messageSent or messageDeliveryError webhook. A sendAt timestamp in the same request schedules the message instead of sending it at once. The outbox endpoints list and cancel what is queued.
Read more: Sending Emails
Handling Sent Mail Folder Detection
EmailEngine tries to automatically detect the Sent Mail folder, but it may not always be accurate:
Problem: User has multiple sent-related folders (Sent, Sent Messages, Sent Emails, etc.)
Solution: Allow users to select the correct folder and set it explicitly:
<?php
$ee->request('PUT', "/v1/account/$userId", [
'imap' => [
'partial' => true, // Update only the listed fields, keep the rest of the IMAP config
'sentMailPath' => 'Sent Messages', // User-selected folder
],
]);
This ensures EmailEngine uses the correct folder for sent mail detection.
Putting It Together
A working CRM integration is four moving parts. Each is covered in its own section above:
| Part | What it does | Section |
|---|---|---|
| Account onboarding | Hands the user a hosted form, stores the returned account ID against your CRM user | Connecting Email Accounts |
| Webhook receiver | Acknowledges immediately, queues the payload, returns 200 | Listening for Webhooks |
| Message registry | Deduplicates on messageId, decides inbound vs outbound, links the message to a contact | Building a Message Registry |
| Outbound send | Posts to /v1/account/{account}/submit and records the returned messageId | Sending Emails from CRM |
The order matters in one place only: register the account before you subscribe its webhooks to a CRM user, otherwise the first messageNew events arrive for an account your registry does not know yet. Buffer unknown accounts rather than dropping them - an account can start syncing before your onboarding transaction commits.
Point it at a request bin, connect one real mailbox, and read the actual payloads before writing any classification logic. The shape of messageNew for a Gmail account with labels differs from a plain IMAP account, and seeing both early saves reworking the registry later.
For a ready-made client rather than raw HTTP calls, see the PHP SDK, which wraps authentication and error handling for the same endpoints used throughout this guide.
Production Considerations
Performance Tuning
When going to production, review performance settings:
- Worker Threads: Adjust
EENGINE_WORKERSfor account load - Webhook Processing: Use
EENGINE_WORKERS_WEBHOOKSfor high webhook volume - Connection Delays: Use
EENGINE_CONNECTION_SETUP_DELAYfor smooth startup - Redis Optimization: Configure Redis memory and persistence
Read more: Performance Tuning
Data Compliance
Review what data is stored and your compliance obligations:
- Password Storage: EmailEngine encrypts credentials
- Email Content: Read from the mailbox on demand; queued outbound messages and pending webhook payloads pass through Redis until delivered
- GDPR: Implement data deletion workflows
Read more: Data and Security Compliance
Scaling Strategies
Vertical Scaling:
- Increase CPU cores and RAM
- Optimize worker thread counts
- Tune Redis configuration
Manual Sharding (if needed):
- For very large deployments, manually shard accounts across completely separate EmailEngine instances
- Each instance requires its own Redis database
- Your application must route requests to the correct instance
- Example: accounts 0 to 999 on instance A (Redis A), 1000 to 1999 on instance B (Redis B)
Read more: Performance Tuning
See Also
- Sub-connections - Immediate notification for the Sent folder a CRM cares about
- Threading - Grouping a conversation into one CRM activity
- Tracking replies - Matching an incoming message to an outgoing one
- Webhooks overview - The events a CRM sync is built on