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.
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: falsenur 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_REJECTED | Rot | D13438 |
| SLA_WARNING | Gelb | FFB900 |
| TICKET_RESOLVED, CHANGE_APPROVED | Grün | 107C10 |
| Default (alle anderen) | Blau | 0078D4 |
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 viaREDIS_PASSWORDEnvironment-Variable konfiguriert.
Best Practices
- E-Mail-Protokoll: Microsoft 365 → MS_GRAPH verwenden (OAuth2, kein App-Passwort nötig). On-Prem → IMAP/SMTP.
- E-Mail-to-Ticket: AUTO_CREATE für Public-Support, CATCH_ALL für Monitoring, REJECT für Internal-Only
- 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.
- Signaturen: Default-Signatur erstellen, bei Bedarf pro Mailbox überschreiben. Inline-Bilder via CID für Logos.
- TLS-Verifizierung: Immer aktiviert lassen (Standard). Nur für selbstsignierte Zertifikate deaktivieren.
- 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.
- Teams-Setup: Azure App Registration erstellen, Bot Channel Registration, App installieren lassen. Conversation References werden automatisch gespeichert.
- Webex-Setup: Bot erstellen, Token in Settings, Health-Check regelmäßig prüfen
- Follower: Agents können Tickets folgen für automatische Updates. CC-Empfänger werden automatisch als Teilnehmer hinzugefügt.
- Webhooks: Ziel-URLs möglichst per HTTPS; private Netze nur gezielt über SSRF_ALLOWLIST freigeben
- 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
- Tickets API - E-Mail-to-Ticket Integration, Follower/CC
- Notification-System - Typen, Kanäle, Templates, Einstellungen, Push, .ics
- Attachments API - E-Mail-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