Eviworx
Docs

Integrations

Diese Seite beschreibt die E-Mail-Anbindung (Empfang über IMAP oder Microsoft Graph, Versand über SMTP oder Graph) mit individuell konfigurierbaren Mailboxen, die Benachrichtigungskanäle Microsoft Teams (Bot Framework) und Cisco Webex, ausgehende Webhooks sowie Follower und CC-Teilnehmer.

Wie Benachrichtigungen modelliert, konfiguriert und ausgeliefert werden (Notification-Typen, Kanäle, Global-/User-Einstellungen, Templates, Quiet Hours, Push, In-App, .ics), ist auf einer eigenen Seite beschrieben: Notification-System. Diese Seite behandelt die externen Integrationen und die Kanal-Anbindung.

🔗
Funktionen
✓ E-Mail-to-Ticket (IMAP oder Microsoft Graph)
✓ Thread-Matching für Antworten (RFC 822)
✓ Agent-Antworten per E-Mail inkl. Anhängen
✓ Mehrere Mailboxen (je eigene Konfiguration)
✓ Bounce- und Auto-Reply-Erkennung (kein Ticket)
✓ E-Mail-Schutz (Rate-Limit, Maximalgröße)
✓ Microsoft Teams per Bot Framework (DM + Channel)
✓ Cisco Webex (Markdown-Direktnachrichten)
✓ Ausgehende Webhooks mit SSRF-Schutz
✓ Follower & CC-Teilnehmer (aus E-Mail-CC)

System-Architektur

INBOUND (E-Mail → 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)

E-Mail Integration (IMAP + Microsoft Graph Inbound)

Protokoll-Unterstützung

Pro Mailbox wird eines von zwei Protokollpaaren gewählt:

Protokoll Empfang Versand Anwendungsfall
IMAP / SMTP ImapAdapter (UID watermark) SmtpSendAdapter (nodemailer) Standard-Mailserver (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)

Individuelle Mailbox-Konfiguration

Jede Mailbox hat ein eigenes Protokoll, eigene Zugangsdaten und eigene Ticket-Standardwerte. Mehrere Mailboxen werden parallel abgerufen:

// 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: Jede aktive Mailbox wird unabhängig abgerufen. Nach 10 aufeinanderfolgenden Fehlern pausiert eine Mailbox für 5 Minuten. Bei mehreren E-Mail-Worker-Instanzen ruft immer nur eine Instanz eine Mailbox ab. Ein deaktiviertes Postfach wird weder abgerufen noch für den Versand verwendet: Eingehende Nachrichten bleiben auf dem Mailserver liegen und erzeugen kein Ticket, und eine Antwort aus einem Ticket wird abgelehnt.

Mailbox-basierte Zugriffsrechte

Jede Mailbox kann mit Zugriffseinschränkungen versehen werden. Wenn accessRestricted aktiviert ist, können nur ausdrücklich berechtigte Benutzer, Rollen oder Agent Groups die Tickets dieser Mailbox sehen und bearbeiten.

Regel Beschreibung
accessRestricted = false Jeder mit tickets.viewAll sieht Tickets dieser Mailbox
accessRestricted = true Nur Benutzer/Rollen/Groups in der MailboxAccess-Liste
Owner-Ausnahme Ticket-Ersteller (customerId) sieht sein Ticket immer
Assigned-Ausnahme Zugewiesener Agent sieht das Ticket immer
Group-Ausnahme Agents in der zugewiesenen Agent Group sehen das Ticket

Die Zugriffsrechte werden pro Mailbox konfiguriert (Admin-Center → Kommunikation → E-Mail & Postfächer → Mailbox → Zugriffsrechte). Es können einzelne User, Rollen oder Agent Groups berechtigt werden — jeweils mit separaten Flags für Sichtbarkeit (canViewTickets) und Zuweisung (canBeAssigned).

E-Mail-to-Ticket Flow

SCHRITT 1: E-Mail empfangen

User sends email to: support@company.com
Subject: "Database connection timeout"
From: customer@example.com
CC: colleague@example.com, manager@example.com

SCHRITT 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()

SCHRITT 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

SCHRITT 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

SCHRITT 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
...

SCHRITT 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

SCHRITT 6: Benachrichtigen

Notification dispatch:
  Notify assigned agent/group
  Notify FOLLOWER participants (all channels)
  Notify CC participants (IN_APP only)
  Multi-channel: IN_APP, EMAIL, TEAMS, WEBEX

E-Mail-Reply-to-Ticket Flow (Outbound via processReply)

Agent antwortet auf 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-Logik

Eingehende E-Mails werden anhand dieser Merkmale einem bestehenden Ticket zugeordnet (in absteigender Priorität):

Methode Priorität Beschreibung
X-Ticket-ID 1 (highest) Custom Header mit Ticket-ID
X-Ticket-Number 2 Custom Header mit Ticket-Nummer
In-Reply-To 3 RFC 822 Threading: Referenziert vorherige Message-ID
References 4 RFC 822 Chain: Alle vorherigen Message-IDs
Subject-Pattern 5 (lowest) Extrahiert die Ticket-Nummer aus [PRÄFIX-…]; das Präfix ist pro Mailbox einstellbar (subjectPrefix, Standard HD)
// Subject pattern ({prefix} = mailbox subjectPrefix, default "HD")
const pattern = /\[{prefix}-([^\]]+)\]/;

// Example match (subjectPrefix "HD"):
"Re: [HD-000123] Database timeout" → HD-000123

Betreff-Treffer werden verifiziert: Eine Ticketnummer im Betreff lässt sich erraten. Ein Betreff-Treffer zählt deshalb nur, wenn der Absender zu diesem Vorgang gehört (Melder, früherer E-Mail-Kontakt oder Beteiligter). Bei CATCH_ALL laufen alle unbekannten Absender auf denselben Sammel-Benutzer; dort wird zusätzlich die ursprüngliche Absenderadresse (originalSenderEmail) verglichen. So kann niemand über den Betreff einen fremden Vorgang ergänzen, wiedereröffnen oder dessen Status ändern. Die Zuordnung über In-Reply-To und References ist davon nicht betroffen.

Sender-Policies

Policy Verhalten Anwendungsfall
AUTO_CREATE Erstellt automatisch neuen END_USER (mit Daily-Limit) Public Support-Mailbox (jeder kann E-Mails senden)
CATCH_ALL Verwendet konfigurierten Default-User Monitoring-Mailbox (System-Alerts ohne echten Sender)
REJECT Lehnt E-Mail ab, sendet Bounce Internal-Only Mailbox (nur bekannte User)

Mailbox-Modi

Mode Beschreibung E-Mail-Conversation
TICKET Immer Ticket-Modus (Portal + E-Mail) No
EMAIL_CONVERSATION Reine E-Mail-Konversation (kein Portal-Zugriff) Yes
AUTO Erkennt automatisch (User hat Passwort = Ticket, sonst E-Mail-Only) Abhängig von User

E-Mail-Signaturen

E-Mail-Signaturen sind mehrsprachig, pro Mailbox zuweisbar und unterstützen Variablen und eingebettete Bilder. Beim Versand gilt die Signatur der Mailbox, sonst die Standard-Signatur; Bilder werden als CID-Inline-Anhänge mitgeschickt.

✍️ Vollständige API: Endpunkte, Body-Schemas, Variablen und Permissions siehe Email Signatures API →.

E-Mail-Layout & Branding

Alle ausgehenden E-Mails werden in ein responsives HTML-Layout gewrappt. Die Konfiguration erfolgt über Environment-Variablen:

ENV Variable Default Beschreibung
EMAIL_ACCENT_COLOR #2563eb Brand-Farbe für Top-Line und Akzente
EMAIL_APP_NAME SMTP fromName oder "Eviworx" App-Name im Footer
EMAIL_APP_URL FRONTEND_URL Link im Footer
EMAIL_FOOTER_TEXT Auto-generiert aus App-Name Benutzerdefinierter Footer-Text
EMAIL_LAYOUT_ENABLED true "false" um Layout komplett zu deaktivieren

TLS-Verifizierung

Die TLS-Zertifikatsverifizierung ist pro Mailbox konfigurierbar (IMAP und SMTP getrennt). Dies ist nützlich für selbstsignierte Zertifikate in internen Umgebungen:

// 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
  },
});

⚠️ Sicherheitshinweis: tlsVerify: false nur in kontrollierten Umgebungen mit selbstsignierten Zertifikaten verwenden. Im Produktionseinsatz mit öffentlichen Mail-Servern sollte die Verifizierung immer aktiviert sein.

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

Verbindungstests lassen sich pro Mailbox oder für die globale Konfiguration ausführen (Endpoints siehe 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

Follower & CC-Teilnehmer

Tickets können Teilnehmer haben (Follower und CC-Empfänger), die bei Änderungen benachrichtigt werden:

Rolle Quelle Notifications Beschreibung
FOLLOWER Manuell (Agent klickt "Follow") Alle Channels Agent folgt Ticket, erhält alle Updates
CC Automatisch aus E-Mail-CC IN_APP only CC-Empfänger, zu bestehendem User aufgelöst wenn möglich
MENTIONED Erwähnung in Kommentaren Nicht für Follower-Events Via @mention in Kommentaren
// 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 für Teilnehmer

// 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

Benachrichtigungs-Kanäle

Eviworx liefert Benachrichtigungen über fünf Kanäle: IN_APP, E-Mail, Push, Microsoft Teams und Cisco Webex. Die Anbindung von Teams und Webex ist unten beschrieben; das vollständige Notification-Modell (Typen, Templates, Global-/User-Einstellungen, Quiet Hours, Digest) steht unter Notification-System.

Microsoft Teams Integration (Bot Framework)

🤖 Teams über das Bot Framework

Die Teams-Integration nutzt das Microsoft Bot Framework. Damit sind Direktnachrichten an einzelne Benutzer, Channel-Posts, Adaptive Cards, OAuth2-Authentifizierung und Kommunikation in beide Richtungen möglich.

Teams Bot Framework Konfiguration

// 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: Scheitert die Zustellung an Teams oder Webex an einem vorübergehenden Fehler (5xx, 429, Netzwerk, Timeout), wird sie automatisch wiederholt: bis zu 6 Versuche mit exponentiell wachsendem Abstand, beginnend bei 15 Sekunden.

🔒 SSRF Protection: serviceUrl-Domains werden validiert. Erlaubt sind nur: smba.trafficmanager.net, botframework.com, teams.microsoft.com. Alle URLs müssen HTTPS verwenden.

Bot Framework Auth & Token Management

Eviworx meldet sich per OAuth2 Client Credentials beim Bot Framework an (Token-Endpoint https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token, Scope https://api.botframework.com/.default). Tokens werden zwischengespeichert und fünf Minuten vor Ablauf automatisch erneuert; lehnt das Bot Framework ein Token ab, wird ein neues angefordert.

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

Modus Trigger Voraussetzung
DM recipientEmail User muss Bot installiert haben (conversation reference gespeichert)
Channel metadata.teamsChannelId Bot muss dem Channel hinzugefügt sein (channel reference gespeichert)

Teams Theme Colors

Event-Type Farbe Hex
SLA_BREACH, CHANGE_REJECTEDRotD13438
SLA_WARNINGGelbFFB900
TICKET_RESOLVED, CHANGE_APPROVEDGrün107C10
Default (alle anderen)Blau0078D4

Cisco Webex Integration

Webex Konfiguration

// Settings Key: 'webex-settings'
{
  "botToken": "Bearer_YOUR_BOT_TOKEN_HERE",  // AES-256-GCM at-rest
  "isEnabled": true
}

Das Webex-botToken und das Teams-appPassword werden mit AES-256-GCM verschlüsselt gespeichert und in GET-Responses nie ausgegeben (nur als _hasToken- bzw. _hasBotConfig-Flag). Details auf der Sicherheits-Seite.

🤖 Bot Setup: Erstelle einen Webex Bot unter developer.webex.com. Der Bot-Token wird benötigt für API-Zugriff.

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 Vergleich

Feature Microsoft Teams Cisco Webex
Delivery-Methode Bot Framework (proactive messaging) Bot API (outgoing)
Card-Format Adaptive Cards v1.4 Native Markdown
Targeting DM (Person) + Channel Person-to-Person (E-Mail)
Interactive Buttons ✅ Action.OpenUrl ❌ Nur Links
Auth-Methode OAuth2 Client Credentials Bot Token (Bearer)
Setup-Aufwand Mittel (Azure App Registration + Bot) Einfach (Bot-Token)

Ausgehende Webhooks

Ausgehende Webhooks werden als CronJob-Action webhook, als Workflow-Schritt „Automated Action“ oder als Aktion einer SLA-Eskalation konfiguriert. Alle drei nutzen denselben Ausführungsweg.

// 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.

Konfiguration im Detail: CronJobs API → · Workflows API →

SSRF Protection

Webhook-URLs werden beim Speichern und bei jedem Aufruf geprüft, einschließlich DNS-Auflösung gegen DNS-Rebinding. Es gelten dieselben Regeln wie für alle ausgehenden Verbindungen:

  • ✅ Erlaubt: http und https; Standard-Ports 80, 443, 8080, 8443 (anpassbar über SSRF_ALLOWED_PORTS)
  • ❌ Hart blockiert (nie freigebbar): localhost, 127.0.0.1, ::1, 0.0.0.0, Metadata 169.254.x + Cloud-Metadata-Hostnamen, Link-Local fe80::, ff00::
  • ❌ Private Netze (10.x, 172.16.x, 192.168.x, fc00::/fd00::) per Default blockiert — bei Bedarf via SSRF_ALLOWLIST freigebbar

🔒 Details zum SSRF-Schutz: Security →.

Deployment & Configuration

Docker Services

Service Beschreibung Key Features
email-worker E-Mail 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 Benachrichtigungs-Dispatch, Follower/CC, Signaturen
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-Passwort: Alle REDIS_URL-Einträge enthalten ein Passwort im Format redis://:PASSWORD@redis:6379. Das Passwort wird via REDIS_PASSWORD Environment-Variable konfiguriert.

Best Practices

  1. E-Mail-Protokoll: Microsoft 365 → MS_GRAPH verwenden (OAuth2, kein App-Passwort nötig). On-Prem → IMAP/SMTP.
  2. E-Mail-to-Ticket: AUTO_CREATE für Public-Support, CATCH_ALL für Monitoring, REJECT für Internal-Only
  3. Thread-Matching: Externe Systeme, die per E-Mail auf Tickets antworten, sollten X-Ticket-ID oder In-Reply-To mitsenden; das ist die zuverlässigste Zuordnung.
  4. Signaturen: Default-Signatur erstellen, bei Bedarf pro Mailbox überschreiben. Inline-Bilder via CID für Logos.
  5. TLS-Verifizierung: Immer aktiviert lassen (Standard). Nur für selbstsignierte Zertifikate deaktivieren.
  6. Rate-Limiting: Ist Redis nicht erreichbar, gilt ein strengeres Ersatz-Limit; eingehende E-Mails werden dann eher gebremst als ungeprüft angenommen. Bei hohem E-Mail-Volumen die Standardwerte anpassen.
  7. Teams-Setup: Azure App Registration erstellen, Bot Channel Registration, App installieren lassen. Conversation References werden automatisch gespeichert.
  8. Webex-Setup: Bot erstellen, Token in Settings, Health-Check regelmäßig prüfen
  9. Follower: Agents können Tickets folgen für automatische Updates. CC-Empfänger werden automatisch als Teilnehmer hinzugefügt.
  10. Webhooks: Ziel-URLs möglichst per HTTPS; private Netze nur gezielt über SSRF_ALLOWLIST freigeben
  11. Security: SPF/DKIM/DMARC Validation für kritische Mailboxen aktivieren. Das Tageslimit bei AUTO_CREATE begrenzt, wie viele Benutzer pro Tag automatisch angelegt werden.

Verwandte Dokumentation