Integrations
This page describes the email connection (receiving via IMAP or Microsoft Graph, sending via SMTP or Graph) with individually configured mailboxes, the Microsoft Teams (Bot Framework) and Cisco Webex notification channels, outbound webhooks, and followers and CC participants.
How notifications are modelled, configured and delivered (notification types, channels, global/user settings, templates, quiet hours, push, in-app, .ics) is documented on its own page: Notification System. This page covers the external integrations and channel connectivity.
System Architecture
INBOUND (Email → Ticket):
IMAP Mailbox MS Graph API (Microsoft 365)
support@company.com support@company.onmicrosoft.com
| |
v v
ImapAdapter GraphReceiveAdapter
(UID watermark) (Delta Query)
| |
+----------+----------------+
|
v
Email-Worker (Microservice)
* MailboxManager: Multi-Mailbox Orchestrator
* AdapterFactory: IMAP | MS_GRAPH (per mailbox)
* InboundProcessor: Bounce detection, rate-limiting, size check
* POST to Backend Internal API
|
v
Backend - InboundEmailService
* Duplicate check (messageId, IMAP UID)
* Thread matching (EmailThreadService)
- In-Reply-To, References headers
- Subject pattern [HD-123456]
- Custom headers (X-Ticket-ID)
* Sender resolution (SenderResolverService)
- AUTO_CREATE (with daily limit)
- CATCH_ALL / REJECT
* CC → TicketParticipantService (Follower/CC)
* Ticket creation or message appending
* Attachment linking (via Unified Attachment System)
|
v
Notification dispatch
* Notify: assigned agent, group, followers, CC participants
OUTBOUND (Notifications → Channels):
NotificationDispatchService
* Resolve effective channels (NotificationPolicyService)
* Quiet Hours filtering (per-channel, timezone-aware)
* Render templates (TemplateRenderingService)
* Inject email signatures (EmailSignatureService)
* Queue jobs to BullMQ (notification-send)
|
v
Notification-Worker (Microservice)
* Processes notification-send queue
* Routes to adapters based on channel
* Attachment support (attachmentRefs, signatureInlineAttachments)
| | | |
v v v v
Email Adapter Teams Adapter Webex Adapter In-App/Push
(SMTP/Graph) (Bot Framework) (Bot API) (WebSocket)
Email Integration (IMAP + Microsoft Graph Inbound)
Protocol Support
Each mailbox uses one of two protocol pairs:
| Protocol | Receive | Send | Use Case |
|---|---|---|---|
IMAP / SMTP |
ImapAdapter (UID watermark) | SmtpSendAdapter (nodemailer) | Standard mail servers (Gmail, Exchange on-prem, etc.) |
MS_GRAPH |
GraphReceiveAdapter (Delta Query) | GraphSendAdapter (Draft+Send) | Microsoft 365 / Exchange Online (OAuth2 app credentials) |
Microsoft Graph API Details
// GraphReceiveAdapter - Delta Query for incremental polling
// Falls back to receivedDateTime filter when delta token expires
interface GraphConnectionConfig {
tenantId: string;
clientId: string;
clientSecret: string;
userPrincipal: string; // e.g. support@company.onmicrosoft.com
folder?: string; // default: 'inbox'
}
// GraphSendAdapter - Draft+Send workflow (handles any attachment size)
// 1. Create draft message
// 2. Attach files (small: direct, large >= 3MB: upload session)
// 3. Send the draft
// Rate limiting: Respects 429 + Retry-After header
interface GraphSendConfig {
tenantId: string;
clientId: string;
clientSecret: string;
userPrincipal: string; // sender mailbox
}
// Token management: graphTokenManager
// - Token caching per tenant+client
// - Proactive refresh 5 minutes before expiry
// - Retry with exponential backoff on transient errors
// - Thread-safe (single in-flight request per key)
Individual Mailbox Configuration
Each mailbox has its own protocol, credentials and ticket defaults. Multiple mailboxes are polled in parallel:
// MailboxConfig (from Backend Internal API)
interface MailboxConfig {
id: string;
name: string;
emailAddress: string;
// Receive Protocol
protocol: 'IMAP' | 'MS_GRAPH';
receiveConfig: Record<string, any>; // IMAP: host/port/... or Graph: tenantId/clientId/...
// Send Protocol (Kopplung: wird ein Protokoll gesetzt, ist seine Config Pflicht / a protocol requires its config)
sendProtocol: 'SMTP' | 'MS_GRAPH';
sendConfig: Record<string, any>; // SMTP: host/port/... or Graph: tenantId/... — leer {} = nur Empfang / empty {} = receive-only
// Shared
fromName?: string;
replyToAddress?: string;
checkIntervalMin: number; // How often to poll (default: 2 min)
isActive: boolean;
mode: 'TICKET' | 'EMAIL_CONVERSATION' | 'AUTO';
unknownSenderPolicy: 'AUTO_CREATE' | 'CATCH_ALL' | 'REJECT';
catchAllUserId?: string; // required when unknownSenderPolicy = CATCH_ALL (else 400)
subjectPrefix: string; // default "HD" → [HD-123456]
// Security
enforceDkim: boolean;
enforceDmarc: boolean;
enforceSpf: boolean;
// Auto-Reply
autoReplyEnabled: boolean;
// Post-processing
processedAction: 'MARK_READ' | 'MOVE' | 'DELETE';
processedFolder?: string;
rejectedFolder?: string;
// Bounce Detection
bounceDetection: boolean;
}
📦 Multi-mailbox: each active mailbox is polled independently. After 10 consecutive errors a mailbox pauses for 5 minutes. With several email-worker instances, only one instance polls a given mailbox at a time. A deactivated mailbox is neither polled nor used for sending: incoming messages stay on the mail server and create no ticket, and a reply from a ticket is rejected.
Mailbox-Based Access Control
Each mailbox can be configured with access restrictions. When accessRestricted is enabled, only explicitly authorized users, roles or agent groups can view and work on tickets from that mailbox.
| Rule | Description |
|---|---|
accessRestricted = false |
Anyone with tickets.viewAll can see tickets from this mailbox |
accessRestricted = true |
Only users/roles/groups in the MailboxAccess list |
| Owner Exception | Ticket creator (customerId) can always see their ticket |
| Assigned Exception | Assigned agent can always see the ticket |
| Group Exception | Agents in the assigned agent group can see the ticket |
Access rights are configured per mailbox (Admin Center → Communication → Email & Mailboxes → Mailbox → Access Rights). Individual users, roles or agent groups can be authorized — each with separate flags for visibility (canViewTickets) and assignment (canBeAssigned).
Email-to-Ticket Flow
STEP 1: Receive Email
User sends email to: support@company.com
Subject: "Database connection timeout"
From: customer@example.com
CC: colleague@example.com, manager@example.com
STEP 2: Polling (Email-Worker)
MailboxManager polls via configured protocol:
IMAP: ImapAdapter.connect() → ImapAdapter.fetchNewMessages()
Graph: GraphReceiveAdapter.connect() → fetchNewEmails() (Delta Query)
Parse email:
Headers: Message-ID, In-Reply-To, References, From, To, CC
Subject: "Database connection timeout"
Body: Text + HTML
Attachments: Extract filename, contentType, size, content
Security: SPF, DKIM, DMARC results (Authentication-Results)
→ InboundProcessor.preProcess()
STEP 3: Pre-Processing (InboundProcessor)
// Max-Size Check (ENV: EMAIL_INBOUND_MAX_SIZE_MB, default 25)
IF email.size > EMAIL_INBOUND_MAX_SIZE_MB:
→ Reject (too large)
// Rate Limiting (Redis Sliding Window)
// Global: EMAIL_INBOUND_RATE_LIMIT_PER_MINUTE (default 60)
// Per-Sender: EMAIL_INBOUND_RATE_LIMIT_PER_SENDER_PER_HOUR (default 30)
// If Redis is unavailable, a stricter in-memory limit applies
IF rate limit exceeded:
→ Reject with rate limit error
// Security Enforcement (per-mailbox settings)
IF mailbox.enforceSpf AND spfResult !== 'pass': → Reject
IF mailbox.enforceDkim AND dkimResult !== 'pass': → Reject
IF mailbox.enforceDmarc AND dmarcResult !== 'pass': → Reject
// Bounce Detection
IF from.includes('MAILER-DAEMON') OR subject.includes('Delivery Status Notification'):
→ Mark as BOUNCE, log to EmailLog, SKIP
// Auto-Reply Detection (RFC 3834)
IF headers['Auto-Submitted'] !== 'no':
→ Mark as AUTO_REPLY, SKIP
STEP 4: Backend Processing (InboundEmailService)
Email-Worker → Backend (transferred data):
{
"mailboxId": "mailbox-uuid",
"messageId": "<unique-message-id@domain>",
"from": { "address": "customer@example.com", "name": "John Doe" },
"cc": [
{ "address": "colleague@example.com", "name": "Colleague" },
{ "address": "manager@example.com", "name": "Manager" }
],
"subject": "Database connection timeout",
"textBody": "We are experiencing timeouts...",
"htmlBody": "<p>We are experiencing...</p>",
"headers": { ... },
"attachments": [ ... ],
"securityResults": { "spfResult": "pass", ... }
}
Backend InboundEmailService.processInboundEmail():
// Duplicate Check
IF EmailMessage exists with same messageId:
IF its status is FAILED AND retryCount < 5:
→ REPROCESS (a transient failure must not lose the mail)
ELSE:
→ SKIP (already processed)
// Thread Matching (EmailThreadService)
TRY match existing ticket:
1. Check X-Ticket-ID header → Direct ticket ID
2. Check X-Ticket-Number header → Ticket number lookup
3. Check In-Reply-To header → Find EmailMessage → Get ticket
4. Check References chain → Find any EmailMessage → Get ticket
5. Check subject pattern [HD-123456] → Extract ticket number
IF ticket found:
→ Mode = UPDATE_TICKET
ELSE:
→ Mode = CREATE_TICKET
STEP 5A: Create New Ticket
// Sender Resolution (SenderResolverService)
// EMAIL_AUTO_CREATE_USER_DAILY_LIMIT (default 100) caps auto-created users per mailbox and day
userId = await senderResolverService.resolveSender(from.address, from.name, mailbox)
SWITCH mailbox.unknownSenderPolicy:
CASE 'AUTO_CREATE':
IF user not found AND dailyLimit not exceeded:
→ Create new User (email, name, role: END_USER, no password)
IF dailyLimit exceeded:
→ Throw UserCreationLimitExceededError
CASE 'CATCH_ALL':
IF user not found:
→ Use mailbox.catchAllUserId (fallback: auto-create with limit)
CASE 'REJECT':
IF user not found:
→ Throw SenderRejectedError, send bounce
// Create Ticket
ticket = await ticketMutationService.createTicket({ ... })
// Add CC Recipients as Participants
await ticketParticipantService.addFromEmailCc(
ticketId,
ccAddresses, // colleague@example.com, manager@example.com
excludeEmails, // customer + mailbox address
source: 'EMAIL_CC'
)
// CC → role: 'CC', resolved to existing users when possible
// Max 50 participants per ticket, noreply/mailer-daemon excluded
// Create EmailMessage record + Link Attachments
...
STEP 5B: Update Existing Ticket (Reply)
// Ticket found via thread matching
ticket = matchedTicket
// Create TicketMessage (comment)
message = await ticketMutationService.addMessage({
ticketId: ticket.id,
body: textBody || htmlBody,
type: 'MESSAGE',
authorId: userId,
isEmailReply: true,
isInternal: false
})
// Add new CC recipients (additive only)
await ticketParticipantService.addFromEmailCc(ticketId, ccAddresses, ...)
// Reply to a CLOSED ticket (from the customer): the reopen policy decides
IF ticket.status === 'CLOSED':
IF reopen allowed → Update to 'OPEN'
ELSE → Create a new linked follow-up ticket
// Reply to a RESOLVED ticket: status unchanged, the agent is notified
STEP 6: Notify
Notification dispatch:
Notify assigned agent/group
Notify FOLLOWER participants (all channels)
Notify CC participants (IN_APP only)
Multi-channel: IN_APP, EMAIL, TEAMS, WEBEX
Email Reply-to-Ticket Flow (Outbound via processReply)
Agent replies to ticket:
POST /api/tickets/:id/messages
{
"body": "We have identified the issue...",
"type": "MESSAGE",
"isInternal": false,
"sendEmailReply": true
}
EmailReplyService.sendReply():
1. Build Email with Threading Headers:
email = {
// Threading Headers (RFC 822)
messageId: generateMessageId(), // <uuid@helpdesk.com>
inReplyTo: lastInbound.messageId, // Links to customer's email
references: [...lastInbound.references, lastInbound.messageId],
// Subject with ticket number
subject: `Re: [${ticket.ticketNumber}] ${ticket.title}`,
// Custom Headers
'X-Ticket-Id': ticket.id,
'X-Ticket-Number': ticket.ticketNumber,
'Auto-Submitted': 'no',
// From/To/CC
from: mailbox.emailAddress,
fromName: mailbox.fromName,
to: ticket.customer.email,
cc: ticket.participants.filter(p => p.role === 'CC').map(p => p.email),
// Body (with signature injection from EmailSignatureService)
// quotedMessage = last dispatched message of the thread (inbound or outbound);
// failed and dismissed emails are never quoted
textBody: buildTextBody(message.body, quotedMessage),
htmlBody: buildHtmlBody(message.body, quotedMessage)
}
2. Queue to Email-Worker:
BullMQ.add('email-send', {
emailMessageId: '...',
mailboxId: ticket.sourceMailboxId,
to: ticket.customer.email,
cc: ccList,
subject: email.subject,
textBody: email.textBody,
htmlBody: email.htmlBody,
headers: { 'Message-ID', 'In-Reply-To', 'References', 'X-Ticket-Id', ... },
attachmentIds: [...], // Regular attachments
signatureInlineAttachments: [...], // CID-embedded signature images
fromName, fromAddress, replyTo
})
3. Send via SMTP or Graph (OutboundProcessor.processReply):
OutboundProcessor.processReply(data):
Load mailbox config (SMTP or MS_GRAPH)
Fetch attachments via Backend Internal API
Route to SmtpSendAdapter or GraphSendAdapter
Record EmailMessage (direction: OUTBOUND)
Publish result to email:result channel
4. Auto-Transition Ticket Status:
IF ticket.status === 'IN_PROGRESS':
→ Update to 'WAITING_CUSTOMER'
Thread Matching Logic
Incoming emails are matched to an existing ticket using these criteria (in descending priority):
| Method | Priority | Description |
|---|---|---|
X-Ticket-ID |
1 (highest) | Custom header with ticket ID |
X-Ticket-Number |
2 | Custom header with ticket number |
In-Reply-To |
3 | RFC 822 threading: References previous message ID |
References |
4 | RFC 822 chain: All previous message IDs |
| Subject Pattern | 5 (lowest) | Extracts the ticket number from [PREFIX-…]; the prefix is configurable per mailbox (subjectPrefix, default HD) |
// Subject pattern ({prefix} = mailbox subjectPrefix, default "HD")
const pattern = /\[{prefix}-([^\]]+)\]/;
// Example match (subjectPrefix "HD"):
"Re: [HD-000123] Database timeout" → HD-000123
Subject matches are verified: A ticket number in the subject line can be guessed. A subject match therefore only counts if the sender belongs to that case (reporter, earlier email contact or participant). Under CATCH_ALL all unknown senders map to the same collective user; there the original sender address (originalSenderEmail) is compared as well. This prevents anyone from adding to, reopening or changing the status of someone else’s case via the subject. Matching via In-Reply-To and References is not affected.
Sender Policies
| Policy | Behavior | Use Case |
|---|---|---|
AUTO_CREATE |
Automatically creates new END_USER (with daily limit) | Public support mailbox (anyone can send emails) |
CATCH_ALL |
Uses configured default user | Monitoring mailbox (system alerts without real sender) |
REJECT |
Rejects email, sends bounce | Internal-only mailbox (known users only) |
Mailbox Modes
| Mode | Description | Email Conversation |
|---|---|---|
TICKET |
Always ticket mode (portal + email) | No |
EMAIL_CONVERSATION |
Pure email conversation (no portal access) | Yes |
AUTO |
Auto-detect (user has password = Ticket, else email-only) | Depends on user |
Email Signatures
Email signatures are multilingual, assignable per mailbox and support variables and embedded images. On send, the mailbox signature applies, otherwise the default signature; images are sent as CID inline attachments.
✍️ Full API: Endpoints, body schemas, variables and permissions: see Email Signatures API →.
Email Layout & Branding
All outgoing emails are wrapped in a responsive HTML layout. Configuration is done via environment variables:
| ENV Variable | Default | Description |
|---|---|---|
EMAIL_ACCENT_COLOR |
#2563eb |
Brand color for top line and accents |
EMAIL_APP_NAME |
SMTP fromName or "Eviworx" | App name in footer |
EMAIL_APP_URL |
FRONTEND_URL | Link in footer |
EMAIL_FOOTER_TEXT |
Auto-generated from app name | Custom footer text |
EMAIL_LAYOUT_ENABLED |
true |
"false" to disable layout entirely |
TLS Verification
TLS certificate verification is configurable per mailbox (IMAP and SMTP separately). This is useful for self-signed certificates in internal environments:
// SmtpConfig / ImapConfig
interface SmtpConfig {
// ...
tlsVerify?: boolean; // default: true (verify certificate)
}
interface ImapConfig {
// ...
tlsVerify?: boolean; // default: true
}
// ImapAdapter uses tlsVerify in connection:
this.connection = new Imap({
tls: config.security !== 'none',
tlsOptions: {
rejectUnauthorized: config.tlsVerify ?? true, // false = accept self-signed
},
});
⚠️ Security Note:
tlsVerify: falseshould only be used in controlled environments with self-signed certificates. In production with public mail servers, verification should always be enabled.
Bounce & Auto-Reply Detection
// Bounce Detection (InboundProcessor)
function isBounce(parsed) {
// From address check
if (parsed.from.address.includes('MAILER-DAEMON')) return true;
if (parsed.from.address.includes('postmaster')) return true;
// Subject patterns
if (/delivery.*fail/i.test(parsed.subject)) return true;
if (/undeliverable/i.test(parsed.subject)) return true;
if (/returned mail/i.test(parsed.subject)) return true;
// Content-Type check
if (parsed.headers['content-type']?.includes('delivery-status')) return true;
return false;
}
// Auto-Reply Detection
function isAutoReply(parsed) {
// Auto-Submitted header (RFC 3834)
const autoSubmitted = parsed.headers['auto-submitted'];
if (autoSubmitted && autoSubmitted !== 'no') return true;
// Precedence header
const precedence = parsed.headers['precedence'];
if (['auto_reply', 'bulk', 'junk'].includes(precedence)) return true;
// X-Auto-Response-Suppress header (Exchange)
if (parsed.headers['x-auto-response-suppress']) return true;
// Subject patterns
if (/out of office|automatic reply|vacation/i.test(parsed.subject)) return true;
return false;
}
Connection Testing
Connection tests can be run per mailbox or for the global configuration (endpoints: see Settings API):
// ConnectionTester test types:
type: 'imap' | 'smtp' | 'global-smtp' | 'global-imap' | 'global-graph' | 'receive' | 'send'
// Per-mailbox: tests the specific mailbox's IMAP/SMTP/Graph connection
// Global: tests the global adapter config
// 'receive'/'send': auto-detects protocol (IMAP/Graph or SMTP/Graph) from mailbox config
Followers & CC Participants
Tickets can have participants (followers and CC recipients) who are notified on changes:
| Role | Source | Notifications | Description |
|---|---|---|---|
FOLLOWER |
Manual (agent clicks "Follow") | All channels | Agent follows ticket, receives all updates |
CC |
Automatic from email CC | IN_APP only | CC recipients, resolved to existing user when possible |
MENTIONED |
Mention in comments | Not for follower events | Via @mention in comments |
// TicketParticipantService
// Agent follows a ticket
await ticketParticipantService.follow(ticketId, userId);
// → Creates participant with role: 'FOLLOWER', source: 'MANUAL'
// → Cannot follow if: customer, assigned agent, ticket closed
// Agent unfollows
await ticketParticipantService.unfollow(ticketId, userId);
// CC from inbound email (additive only, never removes)
await ticketParticipantService.addFromEmailCc(
ticketId,
ccAddresses, // [{ address, name }]
excludeEmails, // customer + mailbox address
'EMAIL_CC'
);
// Security:
// - Max 50 participants per ticket (TICKET_MAX_PARTICIPANTS)
// - noreply@, mailer-daemon@, postmaster@, bounce@ excluded
// - Unique constraint on (ticketId, email)
// - Dedup by email, link to existing users when possible
Notification Routing for Participants
// TicketNotificationService routes notifications to participants:
// On status change, priority change, updates:
// → FOLLOWER participants: all channels (EMAIL, TEAMS, etc.)
// → CC participants: IN_APP only
// On comment added (non-internal):
// → FOLLOWER on email-reply context: IN_APP only (avoid email loops)
// → FOLLOWER on portal context: all channels
// → CC participants: IN_APP only
// On assignment change:
// → FOLLOWER participants: notified about new assignment
// → Skip if already notified as assigned agent
Notification Channels
Eviworx delivers notifications across five channels: IN_APP, email, push, Microsoft Teams and Cisco Webex. The Teams and Webex connection is described below; the full notification model (types, templates, global/user settings, quiet hours, digest) is documented under Notification System.
Microsoft Teams Integration (Bot Framework)
🤖 Teams via the Bot Framework
The Teams integration uses the Microsoft Bot Framework. This enables direct messages to individual users, channel posts, Adaptive Cards, OAuth2 authentication and two-way communication.
Teams Bot Framework Configuration
// Teams Settings (Bot Framework)
interface TeamsSettings {
id: string;
isEnabled: boolean;
appId: string | null; // Bot App ID (Azure AD)
appPassword: string | null; // Bot App Password (client secret)
tenantId: string | null; // Azure AD Tenant ID (or 'botframework.com')
}
Retry: If delivery to Teams or Webex fails with a transient error (5xx, 429, network, timeout), it is retried automatically: up to 6 attempts with exponentially increasing delay, starting at 15 seconds.
🔒 SSRF Protection: serviceUrl domains are validated. Only allowed:
smba.trafficmanager.net,botframework.com,teams.microsoft.com. All URLs must use HTTPS.
Bot Framework Auth & Token Management
Eviworx authenticates with the Bot Framework via OAuth2 client credentials (token endpoint https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token, scope https://api.botframework.com/.default). Tokens are cached and renewed automatically five minutes before they expire; if the Bot Framework rejects a token, a new one is requested.
Teams Adaptive Card Format
// TeamsAdapter sends Adaptive Cards via Bot Framework REST API:
// POST {serviceUrl}/v3/conversations/{conversationId}/activities
{
"type": "message",
"attachments": [{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": {
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{
"type": "TextBlock",
"text": "Ticket Assigned",
"weight": "Bolder",
"size": "Medium",
"color": "Accent"
},
{
"type": "TextBlock",
"text": "For: John Doe",
"size": "Small",
"isSubtle": true
},
{
"type": "TextBlock",
"text": "**Ticket TK-000123** has been assigned to you.\n\nPriority: HIGH",
"wrap": true
}
],
"actions": [{
"type": "Action.OpenUrl",
"title": "Details anzeigen",
"url": "https://helpdesk.com/tickets/123"
}]
}
}]
}
Teams Delivery: DM vs. Channel
| Mode | Trigger | Prerequisite |
|---|---|---|
| DM | recipientEmail |
User must have installed the bot (conversation reference stored) |
| Channel | metadata.teamsChannelId |
Bot must be added to channel (channel reference stored) |
Teams Theme Colors
| Event Type | Color | Hex |
|---|---|---|
| SLA_BREACH, CHANGE_REJECTED | Red | D13438 |
| SLA_WARNING | Yellow | FFB900 |
| TICKET_RESOLVED, CHANGE_APPROVED | Green | 107C10 |
| Default (all others) | Blue | 0078D4 |
Cisco Webex Integration
Webex Configuration
// Settings Key: 'webex-settings'
{
"botToken": "Bearer_YOUR_BOT_TOKEN_HERE", // AES-256-GCM at-rest
"isEnabled": true
}
The Webex botToken and the Teams appPassword are stored encrypted with AES-256-GCM and never returned in GET responses (only as _hasToken or _hasBotConfig flag). Details on the security page.
🤖 Bot Setup: Create a Webex Bot at developer.webex.com. The bot token is required for API access.
Webex Message Format
// POST to https://webexapis.com/v1/messages
{
"toPersonEmail": "user@example.com",
"markdown": "**Ticket TK-000123** has been assigned to you.\n\n**Title:** Database connection timeout\n**Priority:** HIGH\n\n[View Ticket](https://helpdesk.com/tickets/123)"
}
// Response:
{
"id": "message-id-uuid",
"roomId": "room-id",
"toPersonEmail": "user@example.com",
"text": "Ticket TK-000123 has been assigned to you...",
"markdown": "**Ticket TK-000123**...",
"created": "2026-01-28T10:00:00.000Z"
}
Webex vs. Teams Comparison
| Feature | Microsoft Teams | Cisco Webex |
|---|---|---|
| Delivery Method | Bot Framework (proactive messaging) | Bot API (outgoing) |
| Card Format | Adaptive Cards v1.4 | Native Markdown |
| Targeting | DM (person) + Channel | Person-to-person (email) |
| Interactive Buttons | ✅ Action.OpenUrl | ❌ Links only |
| Auth Method | OAuth2 Client Credentials | Bot Token (Bearer) |
| Setup Effort | Medium (Azure App Registration + Bot) | Simple (bot token) |
Outbound Webhooks
Outbound webhooks are configured as the CronJob action webhook, as the workflow step "Automated Action" or as an action of an SLA escalation. All three use the same execution path.
// Webhook call (configurable fields)
{
"url": "https://hooks.example.com/services/...",
"method": "POST", // GET | POST | PUT | PATCH | DELETE (default POST)
"headers": { "X-Custom-Header": "value" },
"payload": { "ticket": "HD-000123" }, // sent as JSON body (not for GET)
"timeoutMs": 15000 // 1000–30000 (default 15000)
}
// Headers sent with every call:
Content-Type: application/json
User-Agent: Eviworx-Webhook/1.0
// Host, Content-Length, Transfer-Encoding, Connection and Upgrade
// cannot be overridden; redirects are not followed.
Configuration details: CronJobs API → · Workflows API →
SSRF Protection
Webhook URLs are checked on save and on every call, including DNS resolution against DNS rebinding. The same rules apply as for all outbound connections:
- ✅ Allowed: http and https; default ports 80, 443, 8080, 8443 (adjustable via SSRF_ALLOWED_PORTS)
- ❌ Hard-blocked (never allowlistable): localhost, 127.0.0.1, ::1, 0.0.0.0, metadata 169.254.x + cloud metadata hostnames, link-local fe80::, ff00::
- ❌ Private nets (10.x, 172.16.x, 192.168.x, fc00::/fd00::) blocked by default — allowlistable via SSRF_ALLOWLIST if needed
🔒 Details on SSRF protection: Security →.
Deployment & Configuration
Docker Services
| Service | Description | Key Features |
|---|---|---|
email-worker |
Email processing | IMAP + Graph polling, bounce detection, SMTP/Graph sending, multi-mailbox |
notification-worker |
Notification dispatch | Multi-channel routing (EMAIL, TEAMS Bot Framework, WEBEX), attachments |
backend |
Main application | Notification dispatch, follower/CC, signatures |
redis |
Queues, pub/sub & locks | Pub/Sub, BullMQ, Rate-Limiting, Distributed Locks |
Environment Variables
# ============================================
# Email Worker
# ============================================
# Redis (with password!)
REDIS_URL=redis://:PASSWORD@redis:6379
REDIS_PASSWORD=PASSWORD
# Backend API
BACKEND_URL=http://backend:3000
INTERNAL_API_KEY=your-internal-api-key
# Email Layout & Branding
EMAIL_ACCENT_COLOR=#3b8f93 # Brand color (top-line, accents)
EMAIL_APP_NAME=Eviworx # App name in footer
EMAIL_APP_URL=https://helpdesk.com # Link in footer
EMAIL_FOOTER_TEXT=Eviworx 2026 # Custom footer text
EMAIL_LAYOUT_ENABLED=true # 'false' to disable layout
# Inbound Security: Rate Limiting
EMAIL_INBOUND_RATE_LIMIT_PER_MINUTE=60
EMAIL_INBOUND_RATE_LIMIT_PER_SENDER_PER_HOUR=30
# Inbound Security: Max email size
EMAIL_INBOUND_MAX_SIZE_MB=25
# ============================================
# Notification Worker
# ============================================
# Redis (with password!)
REDIS_URL=redis://:PASSWORD@redis:6379
REDIS_PASSWORD=PASSWORD
# Backend API
BACKEND_URL=http://backend:3000
INTERNAL_API_KEY=your-internal-api-key
# Worker
NOTIFICATION_WORKER_CONCURRENCY=5
LOG_LEVEL=info
# ============================================
# Backend
# ============================================
# Auto-Create User Daily Limit (per mailbox and day)
EMAIL_AUTO_CREATE_USER_DAILY_LIMIT=100
# Frontend URL for link generation
FRONTEND_URL=https://helpdesk.company.com
# ============================================
# Redis
# ============================================
# All services use authenticated Redis URLs:
# redis://:PASSWORD@redis:6379
🔐 Redis Password: All REDIS_URL entries include a password in the format
redis://:PASSWORD@redis:6379. The password is configured via theREDIS_PASSWORDenvironment variable.
Best Practices
- Email Protocol: Microsoft 365 → use MS_GRAPH (OAuth2, no app password needed). On-prem → IMAP/SMTP.
- Email-to-Ticket: AUTO_CREATE for public support, CATCH_ALL for monitoring, REJECT for internal-only
- Thread Matching: External systems replying to tickets by email should include X-Ticket-ID or In-Reply-To, which is the most reliable way to match.
- Signatures: Create default signature, override per mailbox as needed. Inline images via CID for logos.
- TLS Verification: Always keep enabled (default). Only disable for self-signed certificates.
- Rate Limiting: If Redis is unavailable, a stricter fallback limit applies, so incoming email is throttled rather than accepted unchecked. Adjust the defaults for high email volume.
- Teams Setup: Create Azure App Registration, Bot Channel Registration, have app installed. Conversation references are saved automatically.
- Webex Setup: Create bot, token in settings, check health regularly
- Followers: Agents can follow tickets for automatic updates. CC recipients are automatically added as participants.
- Webhooks: Use HTTPS target URLs where possible; allow private networks only selectively via SSRF_ALLOWLIST
- Security: Enable SPF/DKIM/DMARC validation for critical mailboxes. The AUTO_CREATE daily limit caps how many users are created automatically per day.
Related Documentation
- Tickets API - Email-to-ticket integration, follower/CC
- Notification System - Types, channels, templates, settings, push, .ics
- Attachments API - Email attachments via unified system
- Workflows API - Webhook action in workflows
- CronJobs API - Scheduled notifications via cronjobs
- SLA System - SLA notifications (multi-channel)
- Audit System - Notification audit logging