Email notifications

Iskue turns ticket activity into in-app notifications, and can additionally deliver them by email. Email delivery is off by default: until you enable it, every message that would have been sent is written to the backend log instead, so you can adopt the rest of this page at your own pace. Notification email to team members is plain text; customer-facing service-desk email is multipart (plain text plus an HTML alternative) with RFC 5322 threading headers so replies group correctly in the customer's mail client.

How sending works

The switch is the MAIL_ENABLED environment variable, which maps to the app.mail.enabled property. With it unset or false, a logging sender is active and each would-be email appears in the backend log as Email (disabled, app.mail.enabled=false) to=… subject=…. With MAIL_ENABLED=true, an SMTP sender takes over, configured through Spring Boot's standard spring.mail.* properties (the SPRING_MAIL_* environment variables below). Sending is asynchronous and failure-tolerant: an SMTP error is logged as a warning (Failed to send email to …) and never fails the API request that triggered it.

Enable SMTP delivery

VariableDefaultPurpose
MAIL_ENABLEDfalseMaster switch. When false, all mail becomes backend log lines.
SPRING_MAIL_HOST—SMTP server hostname.
SPRING_MAIL_PORT—SMTP port (typically 587).
SPRING_MAIL_USERNAME—SMTP username.
SPRING_MAIL_PASSWORD—SMTP password.
MAIL_FROMissuehub@localhostFrom address on all outbound mail.
EMAIL_INBOUND_ADDRESS(empty)Support mailbox address. When set, customer mail carries a per-ticket Reply-To such as support+KEY-123@example.com so replies thread back to the right ticket.
PORTAL_BASE_URLhttp://localhost:5173Public base URL of the customer portal; used to build emailed portal links and unsubscribe links.

For the standard Docker deployment, set the values in the .env file next to docker-compose.yml (start from the committed .env.example):

.env — SMTP settings
MAIL_ENABLED=true
SPRING_MAIL_HOST=smtp.example.com
SPRING_MAIL_PORT=587
SPRING_MAIL_USERNAME=iskue-mailer
SPRING_MAIL_PASSWORD=your-smtp-password
MAIL_FROM=issuehub@example.com

docker-compose.yml forwards only MAIL_ENABLED into the issuehub-backend container. The SPRING_MAIL_*, MAIL_FROM, EMAIL_INBOUND_ADDRESS and PORTAL_BASE_URL variables must be passed through as well — add them with a compose override file rather than editing the tracked compose file.

docker-compose.override.yml
# docker-compose.override.yml — merged automatically by docker compose
services:
  backend:
    environment:
      SPRING_MAIL_HOST: ${SPRING_MAIL_HOST}
      SPRING_MAIL_PORT: ${SPRING_MAIL_PORT:-587}
      SPRING_MAIL_USERNAME: ${SPRING_MAIL_USERNAME}
      SPRING_MAIL_PASSWORD: ${SPRING_MAIL_PASSWORD}
      MAIL_FROM: ${MAIL_FROM}
      EMAIL_INBOUND_ADDRESS: ${EMAIL_INBOUND_ADDRESS:-}
      PORTAL_BASE_URL: ${PORTAL_BASE_URL:-http://localhost:5173}
Apply the change
docker compose --profile full up -d --build

Any spring.mail.* property is honoured through Spring Boot's standard configuration binding. For example, servers requiring STARTTLS accept SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_ENABLE=true and SPRING_MAIL_PROPERTIES_MAIL_SMTP_AUTH=true alongside the variables above.

Which events send mail

Recipients are decided by the project's notification scheme: a named set of rules mapping each event type to recipient types. Every project references a scheme; a project without one falls back to the built-in default scheme. The event catalogue and the default scheme's rules are:

EventLabelDefault recipients
COMMENTEDComment addedReporter, current assignee, watchers
STATUS_CHANGEDStatus changedReporter, current assignee, watchers
ASSIGNEDAssignee changedCurrent assignee
SLA_BREACHEDSLA breachedCurrent assignee
MENTIONEDMentionedThe @mentioned users — always notified, regardless of scheme rules
CREATEDTicket createdNone by default; add a rule to opt in
UPDATEDTicket updatedNone by default; add a rule to opt in

A rule's recipient type is one of CURRENT_ASSIGNEE, REPORTER, WATCHERS, ALL_PROJECT_MEMBERS, PROJECT_ROLE, SPECIFIC_USER or GROUP. Named users and group members are only notified if they are members of the project, so schemes cannot leak activity across visibility boundaries. Recipients are de-duplicated and the acting user is always excluded — nobody is emailed about their own change.

Schemes are managed by instance admins at /api/v1/admin/notification-schemes (list, create, edit rules, set default; GET /api/v1/admin/notification-schemes/catalog returns the event and recipient catalogue). Project admins assign a scheme to their project through the project notification endpoint. Both are also available in the admin UI.

Per-user preferences

Each user chooses how notification email reaches them via an email mode: IMMEDIATE (one email per notification, the default), DAILY (a single daily digest of unread notifications) or NONE (in-app only). Users change this on the notifications page in the UI, or via the API:

Switch to the daily digest, grouped by project
curl -X PATCH http://localhost:8088/api/v1/users/me/preferences \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"emailMode":"DAILY","digestGrouping":"BY_PROJECT"}'
  • emailMode — IMMEDIATE, DAILY or NONE.
  • digestGrouping — daily digest layout: FLAT (chronological, the default), BY_PROJECT or BY_TYPE.
  • snoozeHours — a do-not-disturb window: any value from 1 to 336 suppresses all notifications (in-app and email) for that many hours; 0 clears it.
  • Mutes — POST /api/v1/notification-prefs with an optional projectId and optional eventType silences matching events entirely, mentions included; omitting projectId means all projects, omitting eventType means all events. GET lists mutes, DELETE /api/v1/notification-prefs/{id} removes one.
  • Deactivated accounts are never emailed.

The daily digest

A background sweeper checks every 60 seconds for DAILY-mode users whose digest period has elapsed (the app.digest.period-minutes application property, default 1440 — once a day) and sends each a single plain-text digest of their unread notifications, laid out per their digestGrouping. A user with nothing unread gets no email that period. The digest lists at most 20 lines and closes with … and N more. when there are more. On multi-node clusters the send slot is claimed atomically, so exactly one backend sends each digest.

Saved-filter subscription emails

Users can subscribe to a saved filter to receive its current results by email on the same cadence pattern (the app.filter-subscription.period-minutes property, default 1440). The filter's TQL query runs as the subscriber, so project access and restricted-ticket visibility apply exactly as in the UI. An empty result set sends nothing; results are capped at 20 lines with an overflow count. Subscribing and unsubscribing is done from the saved-filter UI or PUT/DELETE on /api/v1/filters/{id}/subscription.

Email templates

The three internal notification emails are rendered from templates that instance admins can override. Placeholders are literal {name} tokens replaced verbatim; an unknown token is left as-is so a typo is visible in a delivered email rather than silently vanishing.

Template keyPlaceholdersDefault subject
EVENT{ticketKey} {event} {message} {actor}[IssueHub] {ticketKey} — {event}
DIGEST{count} {plural} {items} {overflow}[IssueHub] Daily digest — {count} unread notification{plural}
FILTER_SUBSCRIPTION{filterName} {count} {items} {overflow}[IssueHub] {filterName}: {count} matching ticket(s)
Template administration (ADMIN role required)
# List templates (effective + default subject/body, placeholder list)
curl -H "Authorization: Bearer $TOKEN" \
  http://localhost:8088/api/v1/admin/email-templates

# Override the per-event template
curl -X PUT http://localhost:8088/api/v1/admin/email-templates/EVENT \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"subject":"[IssueHub] {ticketKey} — {event}","body":"{message}\n\nOpen the ticket: https://tracker.example.com/tickets/{ticketKey}"}'

# Restore the built-in default
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
  http://localhost:8088/api/v1/admin/email-templates/EVENT

Customer email (service desk and portal)

Separately from team notifications, Iskue emails external customer contacts to close the service-desk loop. A public agent reply on a customer's ticket — and a public automated reply posted by an automation rule — is emailed to the ticket's requester contact. These messages are multipart (text + HTML), carry threading headers so a ticket's messages group into one conversation, and are localised to the contact's stored locale.

  • With EMAIL_INBOUND_ADDRESS set (for example support@example.com), each message's Reply-To becomes a per-ticket address such as support+KEY-123@example.com, so a customer reply threads back to the right ticket even if the subject was edited.
  • With PORTAL_BASE_URL set, every reply notification carries a one-click unsubscribe link in its footer. Unsubscribing flags the contact; further reply notifications to that contact are suppressed. The endpoint is public and idempotent: POST /api/v1/email/unsubscribe/{token}, where the token is HMAC-signed — it cannot be forged or redirected at another contact.
  • Issuing a portal link with ?notify=true emails it to the contact. This message is transactional and is sent even to unsubscribed contacts.
Emailed portal link (agent action; write access to the project required)
# Issue a portal link for a contact and email it to them
curl -X POST \
  "http://localhost:8088/api/v1/projects/SUP/contacts/$CONTACT_ID/portal-link?notify=true" \
  -H "Authorization: Bearer $TOKEN"

Iskue sends no CSAT survey emails. Satisfaction ratings are collected in-app: the ticket's reporter rates a ticket once it reaches a DONE-category status.

Verify and troubleshoot

  • With mail disabled, confirm the dispatch path end-to-end by watching for Email (disabled, app.mail.enabled=false) to=… subject=… lines: docker logs -f issuehub-backend.
  • With mail enabled, SMTP failures appear as Failed to send email to … warnings. Because sending is asynchronous, a misconfigured SMTP server never surfaces as an API error — the log is the only signal.
  • No email for a specific user? Check, in order: their email mode (NONE sends nothing; DAILY batches into the digest), an active snooze, a matching mute, whether they were the actor (actors are never notified), and whether the project's notification scheme has a rule for that event type.
  • CREATED and UPDATED are silent until an admin adds scheme rules for them — that is the shipped default, not a fault.