Eviworx
Docs

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.

🔗
Features
✓ Email-to-ticket (IMAP or Microsoft Graph)
✓ Thread matching for replies (RFC 822)
✓ Agent replies by email incl. attachments
✓ Multiple mailboxes (each with own configuration)
✓ Bounce and auto-reply detection (no ticket)
✓ Email protection (rate limit, maximum size)
✓ Microsoft Teams via Bot Framework (DM + channel)
✓ Cisco Webex (Markdown direct messages)
✓ Outbound webhooks with SSRF protection
✓ Followers & CC participants (from email CC)

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: false should 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_REJECTEDRedD13438
SLA_WARNINGYellowFFB900
TICKET_RESOLVED, CHANGE_APPROVEDGreen107C10
Default (all others)Blue0078D4

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 the REDIS_PASSWORD environment variable.

Best Practices

  1. Email Protocol: Microsoft 365 → use MS_GRAPH (OAuth2, no app password needed). On-prem → IMAP/SMTP.
  2. Email-to-Ticket: AUTO_CREATE for public support, CATCH_ALL for monitoring, REJECT for internal-only
  3. 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.
  4. Signatures: Create default signature, override per mailbox as needed. Inline images via CID for logos.
  5. TLS Verification: Always keep enabled (default). Only disable for self-signed certificates.
  6. 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.
  7. Teams Setup: Create Azure App Registration, Bot Channel Registration, have app installed. Conversation references are saved automatically.
  8. Webex Setup: Create bot, token in settings, check health regularly
  9. Followers: Agents can follow tickets for automatic updates. CC recipients are automatically added as participants.
  10. Webhooks: Use HTTPS target URLs where possible; allow private networks only selectively via SSRF_ALLOWLIST
  11. 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