이메일 및 알림 / 수신 이메일
인바운드 이메일
수신 이메일에서 직접 티켓을 생성하고 답변할 수 있습니다. Escalated는 Mailgun, Postmark, AWS SES Webhook과 폴백으로 IMAP 폴링을 지원합니다.
작동 방식
- 이메일 제공업체가 지원 주소(예:
support@yourapp.com)로 메시지를 수신 - 제공업체가 Webhook을 통해 앱에 전달 (또는 IMAP 폴링으로 가져옴)
- Escalated가 페이로드를 정규화하고 제목 레퍼런스(예:
[ESC-00001]) 또는In-Reply-To헤더로 스레드 매칭을 확인 - 매칭된 이메일은 답변을 추가하고, 매칭되지 않은 이메일은 새 티켓을 생성 (알 수 없는 발신자에게는 게스트 티켓)
설정
관리 설정에서 인바운드 이메일을 활성화하고 어댑터를 구성하거나, 환경 변수를 통해 구성합니다. 관리 설정이 환경 변수/설정 파일 값보다 우선합니다.
Webhook URL
이메일 제공업체의 인바운드 Webhook을 다음 URL로 설정하세요. 이 라우트는 인증이 필요 없습니다 (서명 검증을 사용합니다).
| 제공업체 | Webhook URL |
|---|---|
| Mailgun | POST /support/inbound/mailgun |
| Postmark | POST /support/inbound/postmark |
| AWS SES | POST /support/inbound/ses |
IMAP 폴링
Webhook을 지원하지 않는 이메일 제공업체의 경우, IMAP 폴링 명령을 스케줄에 등록하여 사용하세요.
기능
- 제목 레퍼런스 및 In-Reply-To / References 헤더를 통한 스레드 감지
- 알 수 없는 발신자를 위한 게스트 티켓 (표시 이름 자동 파생)
- 이메일 답변 도착 시 해결 또는 종료된 티켓의 자동 재오픈
- Message-ID 헤더를 통한 중복 감지 (중복 처리 방지)
- 크기 및 파일 수 제한이 설정 가능한 첨부 파일 처리
- 감사 로깅 -- 모든 인바운드 이메일이 디버깅 및 컴플라이언스를 위해 기록
- 관리자 설정 가능 -- 모든 설정을 관리 패널에서 관리 (환경 변수/설정 파일 폴백 포함)
설정
# .env
ESCALATED_INBOUND_EMAIL=true
ESCALATED_INBOUND_ADDRESS=support@yourapp.com
# Mailgun
ESCALATED_INBOUND_ADAPTER=mailgun
ESCALATED_MAILGUN_SIGNING_KEY=your-signing-key
# Postmark
ESCALATED_INBOUND_ADAPTER=postmark
ESCALATED_POSTMARK_INBOUND_TOKEN=your-token
# AWS SES
ESCALATED_INBOUND_ADAPTER=ses
ESCALATED_SES_TOPIC_ARN=arn:aws:sns:us-east-1:...
# IMAP
ESCALATED_INBOUND_ADAPTER=imap
ESCALATED_IMAP_HOST=imap.gmail.com
ESCALATED_IMAP_USERNAME=support@yourapp.com
ESCALATED_IMAP_PASSWORD=your-app-password
IMAP 폴링
// routes/console.php
Schedule::command('escalated:poll-imap')->everyMinute();
설정
# config/initializers/escalated.rb
Escalated.configure do |config|
config.inbound_email_enabled = true
config.inbound_email_adapter = :mailgun
config.inbound_email_address = "support@yourapp.com"
config.mailgun_signing_key = ENV["ESCALATED_MAILGUN_SIGNING_KEY"]
end
IMAP 폴링
# config/recurring.yml (Solid Queue)
poll_imap:
class: Escalated::PollImapJob
schedule: every minute
설정
# settings.py
ESCALATED = {
"INBOUND_EMAIL_ENABLED": True,
"INBOUND_EMAIL_ADAPTER": "mailgun",
"INBOUND_EMAIL_ADDRESS": "support@yourapp.com",
"MAILGUN_SIGNING_KEY": os.environ.get("ESCALATED_MAILGUN_SIGNING_KEY"),
}
IMAP 폴링
# crontab or Celery beat
python manage.py poll_imap
설정
// config/escalated.ts
inboundEmail: {
enabled: true,
adapter: 'mailgun',
address: 'support@yourapp.com',
mailgunSigningKey: Env.get('ESCALATED_MAILGUN_SIGNING_KEY'),
}
IMAP 폴링
# Cron or @adonisjs/scheduler
node ace escalated:poll-imap
Filament는 escalated-laravel과 동일한 인바운드 이메일 설정을 사용합니다. Laravel 탭을 참조하세요.
IMAP 폴링
동일한 Laravel 스케줄러 명령을 사용합니다. Laravel 탭을 참조하세요.
설정
wp-admin의 Escalated -> Settings -> Email에서 인바운드 이메일을 설정합니다. Mailgun, Postmark, AWS SES Webhook을 지원합니다.
IMAP 폴링
IMAP 폴링은 WP-Cron을 통해 실행됩니다. IMAP이 인바운드 어댑터로 설정되면 플러그인이 자동으로 스케줄링합니다.
Inbound email is configured entirely on the server side. No mobile-specific setup is needed.
Tickets created via email appear in the Flutter app exactly like any other ticket. Thread detection, guest tickets, and attachment handling are all managed by your backend — the mobile app simply displays the results through the API.
Note: See your backend framework's tab for inbound email configuration (adapters, webhook URLs, IMAP polling).
Inbound email is configured entirely on the server side. No mobile-specific setup is needed.
Tickets created via email appear in the React Native app exactly like any other ticket. Thread detection, guest tickets, and attachment handling are all managed by your backend — the mobile app simply displays the results through the API.
Note: See your backend framework's tab for inbound email configuration (adapters, webhook URLs, IMAP polling).
Webhook endpoint
The .NET bundle exposes a single webhook for all providers. Configure Postmark and/or Mailgun to POST inbound mail to:
POST /support/webhook/email/inbound?adapter=postmark
POST /support/webhook/email/inbound?adapter=mailgun
You can also pass the adapter via the X-Escalated-Adapter header instead of a query parameter.
Configuration
Set the shared inbound secret and the mail domain (used for signed Reply-To and canonical Message-ID headers) in appsettings.json or via environment variables:
{
"Escalated": {
"Mail": {
"Domain": "support.yourapp.com",
"InboundSecret": "a-long-random-value"
}
}
}
# .env
Escalated__Mail__Domain=support.yourapp.com
Escalated__Mail__InboundSecret=a-long-random-value
The InboundSecret is symmetric — it's used to sign outbound Reply-To addresses and to verify inbound webhook requests, so forged emails that target a stolen reply address are rejected via timing-safe HMAC comparison.
Provider setup
Each provider signs its webhook and expects you to forward that signature via the X-Escalated-Inbound-Secret header.
Postmark — in your server settings under Inbound → Webhook URL:
https://yourapp.com/support/webhook/email/inbound?adapter=postmark
Add a custom header X-Escalated-Inbound-Secret: <your secret>.
Mailgun — under Receiving → Routes, create a "Forward" action pointing at:
https://yourapp.com/support/webhook/email/inbound?adapter=mailgun
Set the HMAC header the same way.
Testing
curl -X POST \
-H "Content-Type: application/json" \
-H "X-Escalated-Inbound-Secret: <your secret>" \
-d '{
"FromFull": {"Email": "customer@example.com", "Name": "Customer"},
"To": "support@example.com",
"Subject": "Hello",
"TextBody": "Help please",
"MessageID": "<abc@mail>"
}' \
"https://yourapp.com/support/webhook/email/inbound?adapter=postmark"
The response shape:
{
"inboundId": 42,
"status": "created",
"outcome": "created_new",
"ticketId": 7,
"replyId": null,
"pendingAttachmentDownloads": []
}
Provider-hosted attachments (Mailgun's larger files, for example) appear in pendingAttachmentDownloads so a background worker can fetch and persist them out-of-band.
Webhook endpoint
The Phoenix library exposes a single webhook controller for all providers. Configure Postmark and/or Mailgun to POST inbound mail to:
POST /support/webhook/email/inbound?adapter=postmark
POST /support/webhook/email/inbound?adapter=mailgun
You can also pass the adapter via the x-escalated-adapter header instead of a query parameter.
Configuration
Set the shared inbound secret and mail domain (used for signed Reply-To and canonical Message-ID headers) in config/runtime.exs:
config :escalated,
mail_domain: System.get_env("ESCALATED_MAIL_DOMAIN", "support.yourapp.com"),
email_inbound_secret: System.fetch_env!("ESCALATED_INBOUND_SECRET"),
inbound_parsers: [
Escalated.Services.Email.Inbound.PostmarkParser,
Escalated.Services.Email.Inbound.MailgunParser
]
The email_inbound_secret is symmetric — it signs outbound Reply-To addresses and verifies inbound webhook requests, so forged emails that target a stolen reply address are rejected via timing-safe comparison (Plug.Crypto.secure_compare/2).
Wiring
Register the route in your Phoenix router:
scope "/support/webhook/email", Escalated.Controllers do
pipe_through :api
post "/inbound", InboundEmailController, :inbound
end
Provider setup
Each provider signs its webhook and expects you to forward that signature via the x-escalated-inbound-secret header.
Postmark — in your server settings under Inbound → Webhook URL:
https://yourapp.com/support/webhook/email/inbound?adapter=postmark
Add a custom header x-escalated-inbound-secret: <your secret>.
Mailgun — under Receiving → Routes, create a "Forward" action pointing at:
https://yourapp.com/support/webhook/email/inbound?adapter=mailgun
Set the HMAC header the same way.
Testing
curl -X POST \
-H "Content-Type: application/json" \
-H "x-escalated-inbound-secret: <your secret>" \
-d '{
"FromFull": {"Email": "customer@example.com", "Name": "Customer"},
"To": "support@example.com",
"Subject": "Hello",
"TextBody": "Help please",
"MessageID": "<abc@mail>"
}' \
"https://yourapp.com/support/webhook/email/inbound?adapter=postmark"
The response shape:
{
"status": "created",
"outcome": "created_new",
"ticket_id": 7,
"reply_id": null,
"pending_attachment_downloads": []
}
Provider-hosted attachments (Mailgun's larger files, for example) appear in pending_attachment_downloads so a background worker can fetch and persist them out-of-band.
Webhook endpoint
The Go module exposes a single webhook handler for all providers. Configure Postmark and/or Mailgun to POST inbound mail to:
POST /escalated/webhook/email/inbound?adapter=postmark
POST /escalated/webhook/email/inbound?adapter=mailgun
You can also pass the adapter via the X-Escalated-Adapter header instead of a query parameter.
Configuration
Set the shared inbound secret and mail domain (used for signed Reply-To and canonical Message-ID headers) via environment variables or directly on email.Config:
ESCALATED_MAIL_DOMAIN=support.yourapp.com
ESCALATED_INBOUND_SECRET=a-long-random-value
cfg := email.Config{
MailDomain: os.Getenv("ESCALATED_MAIL_DOMAIN"),
InboundSecret: os.Getenv("ESCALATED_INBOUND_SECRET"),
}
The InboundSecret is symmetric — it signs outbound Reply-To addresses and verifies inbound webhook requests, so forged emails that target a stolen reply address are rejected via timing-safe HMAC comparison (hmac.Equal).
Wiring
Register the handler on your http.ServeMux:
svc := email.NewInboundEmailService(router, writer)
handler := handlers.NewInboundEmailHandler(svc, parsers, cfg.InboundSecret)
mux.Handle("POST /escalated/webhook/email/inbound", handler)
Provider setup
Each provider signs its webhook and expects you to forward that signature via the X-Escalated-Inbound-Secret header.
Postmark — in your server settings under Inbound → Webhook URL:
https://yourapp.com/escalated/webhook/email/inbound?adapter=postmark
Add a custom header X-Escalated-Inbound-Secret: <your secret>.
Mailgun — under Receiving → Routes, create a "Forward" action pointing at:
https://yourapp.com/escalated/webhook/email/inbound?adapter=mailgun
Set the HMAC header the same way.
Testing
curl -X POST \
-H "Content-Type: application/json" \
-H "X-Escalated-Inbound-Secret: <your secret>" \
-d '{
"FromFull": {"Email": "customer@example.com", "Name": "Customer"},
"To": "support@example.com",
"Subject": "Hello",
"TextBody": "Help please",
"MessageID": "<abc@mail>"
}' \
"https://yourapp.com/escalated/webhook/email/inbound?adapter=postmark"
The response shape:
{
"status": "created",
"outcome": "created_new",
"ticket_id": 7,
"reply_id": null,
"pending_attachment_downloads": []
}
Provider-hosted attachments (Mailgun's larger files, for example) appear in pending_attachment_downloads so a background worker can fetch and persist them out-of-band.
Webhook endpoint
The Spring Boot starter exposes a single webhook for all providers. Configure Postmark and/or Mailgun to POST inbound mail to:
POST /escalated/webhook/email/inbound?adapter=postmark
POST /escalated/webhook/email/inbound?adapter=mailgun
You can also pass the adapter via the X-Escalated-Adapter header instead of a query parameter.
Configuration
Set the shared inbound secret and mail domain (used for signed Reply-To and canonical Message-ID headers) in application.yml:
escalated:
mail:
domain: support.yourapp.com
inbound-secret: ${ESCALATED_INBOUND_SECRET}
Or via environment variables:
ESCALATED_MAIL_DOMAIN=support.yourapp.com
ESCALATED_INBOUND_SECRET=a-long-random-value
The inbound-secret is symmetric — it signs outbound Reply-To addresses and verifies inbound webhook requests, so forged emails that target a stolen reply address are rejected via timing-safe HMAC comparison (MessageDigest.isEqual).
Provider setup
Each provider signs its webhook and expects you to forward that signature via the X-Escalated-Inbound-Secret header.
Postmark — in your server settings under Inbound → Webhook URL:
https://yourapp.com/escalated/webhook/email/inbound?adapter=postmark
Add a custom header X-Escalated-Inbound-Secret: <your secret>.
Mailgun — under Receiving → Routes, create a "Forward" action pointing at:
https://yourapp.com/escalated/webhook/email/inbound?adapter=mailgun
Set the HMAC header the same way.
Testing
curl -X POST \
-H "Content-Type: application/json" \
-H "X-Escalated-Inbound-Secret: <your secret>" \
-d '{
"FromFull": {"Email": "customer@example.com", "Name": "Customer"},
"To": "support@example.com",
"Subject": "Hello",
"TextBody": "Help please",
"MessageID": "<abc@mail>"
}' \
"https://yourapp.com/escalated/webhook/email/inbound?adapter=postmark"
The response shape:
{
"status": "created",
"outcome": "CREATED_NEW",
"ticketId": 7,
"replyId": null,
"pendingAttachmentDownloads": []
}
Provider-hosted attachments (Mailgun's larger files, for example) appear in pendingAttachmentDownloads so a background worker can fetch and persist them out-of-band.
Webhook endpoint
The Symfony bundle exposes a single webhook controller for all providers. Configure Postmark and/or Mailgun to POST inbound mail to:
POST /escalated/webhook/email/inbound?adapter=postmark
POST /escalated/webhook/email/inbound?adapter=mailgun
You can also pass the adapter via the X-Escalated-Adapter header instead of a query parameter.
Configuration
Set the shared inbound secret and mail domain (used for signed Reply-To and canonical Message-ID headers) under the escalated: bundle config:
# config/packages/escalated.yaml
escalated:
mail_domain: '%env(ESCALATED_MAIL_DOMAIN)%'
inbound_secret: '%env(ESCALATED_INBOUND_SECRET)%'
# .env
ESCALATED_MAIL_DOMAIN=support.yourapp.com
ESCALATED_INBOUND_SECRET=a-long-random-value
The inbound_secret is symmetric — it signs outbound Reply-To addresses and verifies inbound webhook requests, so forged emails that target a stolen reply address are rejected via timing-safe comparison (hash_equals).
Provider setup
Each provider signs its webhook and expects you to forward that signature via the X-Escalated-Inbound-Secret header.
Postmark — in your server settings under Inbound → Webhook URL:
https://yourapp.com/escalated/webhook/email/inbound?adapter=postmark
Add a custom header X-Escalated-Inbound-Secret: <your secret>.
Mailgun — under Receiving → Routes, create a "Forward" action pointing at:
https://yourapp.com/escalated/webhook/email/inbound?adapter=mailgun
Set the HMAC header the same way.
Testing
curl -X POST \
-H "Content-Type: application/json" \
-H "X-Escalated-Inbound-Secret: <your secret>" \
-d '{
"FromFull": {"Email": "customer@example.com", "Name": "Customer"},
"To": "support@example.com",
"Subject": "Hello",
"TextBody": "Help please",
"MessageID": "<abc@mail>"
}' \
"https://yourapp.com/escalated/webhook/email/inbound?adapter=postmark"
The response shape:
{
"status": "created",
"outcome": "created_new",
"ticket_id": 7,
"reply_id": null,
"pending_attachment_downloads": []
}
Provider-hosted attachments (Mailgun's larger files, for example) appear in pending_attachment_downloads so a background worker can fetch and persist them out-of-band.
Adding a custom parser
The bundle discovers inbound parsers by the escalated.inbound_parser tag. To add a new one, implement Escalated\Symfony\Mail\Inbound\InboundEmailParser and autoconfigure:
# config/services.yaml
services:
_instanceof:
Escalated\Symfony\Mail\Inbound\InboundEmailParser:
tags: ['escalated.inbound_parser']