Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Please refer to the [Quick Start](/getting-started/quick-start/) guide for detai

For production use, please review the file and **at least**:

- configure SMTP credentials
- configure transactional email credentials (SMTP by default, or JMAP)
- set `webserver.url` to your public URL
- review `webserver.tls` if you want Chatto to terminate TLS

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ hostname.

Replace `chat.example.com` with your Chatto domain and `admin@example.com` with the email address you will use for the first account. The script generates `.env` and `livekit.generated.yaml` with matching NATS, Chatto, and LiveKit secrets.

4. **Configure SMTP**
4. **Configure transactional email**

Edit `.env` and replace the `CHATTO_SMTP_*` placeholders with your provider settings. Direct email/password registration, email verification, and password reset require SMTP.
Edit `.env` and replace the `CHATTO_SMTP_*` placeholders with your provider settings. SMTP is the default transport. Direct email/password registration, email verification, and password reset require transactional email; you can instead configure JMAP as described in [Identity & Login](/guides/operations/identity-login/#transactional-email).

The SMTP example uses STARTTLS submission on port `587`. For providers that require SMTPS on port `465`, set `CHATTO_SMTP_TLS=implicit`.

Expand Down Expand Up @@ -218,9 +218,9 @@ rules, and do not expose NATS port 4222 publicly.

## Optional Features

### Direct Registration and SMTP
### Direct Registration and Transactional Email

The setup above enables direct email/password registration, which requires SMTP because Chatto sends registration codes by email. SMTP is also used for email verification and password reset.
The setup above enables direct email/password registration, which requires transactional email because Chatto sends registration codes by email. The provided `.env` uses SMTP, the default transport. You can instead configure JMAP as described in [Identity & Login](/guides/operations/identity-login/#transactional-email). Transactional email is also used for email verification and password reset.

If you do not want public email/password registration after the first owner exists, set `CHATTO_AUTH_DIRECT_REGISTRATION=false`. Users can still sign in through configured SSO providers.

Expand Down Expand Up @@ -306,7 +306,7 @@ See [Voice & Video Calls](/guides/infrastructure/voice-calls/#turn-for-restricti

**Chatto can't connect to NATS** — Ensure `NATS_TOKEN` and `CHATTO_NATS_CLIENT_TOKEN` match in `.env`.

**Registration says email delivery is not configured** — Direct email/password registration requires SMTP. Set `CHATTO_SMTP_ENABLED=true` and configure `CHATTO_SMTP_HOST`, `CHATTO_SMTP_PORT`, `CHATTO_SMTP_TLS`, and `CHATTO_SMTP_FROM`. Use `CHATTO_SMTP_TLS=implicit` for SMTPS providers on port `465`. Add username and password when your SMTP server requires authentication.
**Registration says email delivery is not configured** — Direct email/password registration requires transactional email. For SMTP, set `CHATTO_SMTP_ENABLED=true` and configure `CHATTO_SMTP_HOST`, `CHATTO_SMTP_PORT`, `CHATTO_SMTP_TLS`, and `CHATTO_SMTP_FROM`. Use `CHATTO_SMTP_TLS=implicit` for SMTPS providers on port `465`. For JMAP, set `CHATTO_EMAIL_TRANSPORT=jmap`, `CHATTO_EMAIL_JMAP_SESSION_URL`, `CHATTO_EMAIL_JMAP_ACCESS_TOKEN`, and `CHATTO_EMAIL_JMAP_FROM`.

**The first account is not an owner** — Make sure `CHATTO_OWNERS_EMAILS` contains the verified email address for that account. For an already-verified account, restart Chatto after updating the variable; Chatto applies matching owner roles on boot.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Chatto ships with full support for voice and video calls, including screen shari
- **Data Storage**: make a decision on where NATS should store its data. Outside of uploaded file blobs, you're not going to need huge amounts of space.
- **File Storage**: decide on whether to use NATS for storing file uploads, or wire up an S3 compatible object storage.
- **Admin Bootstrap**: Chatto's configuration allows you to specify the email addresses of users who should _always_ be server owners. This is a failsafe to prevent you from accidentally locking yourself out of your own server. Please be aware that the user is not automatically created for you; you still have to create your account either through the usual signup flow, or using the `chatto operator` command.
- **SMTP**: Chatto will need to send emails for verifying email addresses and other planned features like email-based notification reminders. Please prepare a set of SMTP credentials to allow Chatto to send emails. (If you never want Chatto to verify emails or send reminders, you can skip this, but we strongly recommend that you don't.)
- **Transactional email**: Chatto needs to send email for address verification and other planned features such as email-based notification reminders. Prepare SMTP credentials (the default transport) or JMAP bearer-token credentials. If you never want Chatto to verify email addresses or send reminders, you can skip this, but we strongly recommend that you do not.

## Pick a Deployment Path

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ Use these fields deliberately:

<CardGrid>
<LinkCard title="Permissions & Roles" href="/guides/operations/permissions/" description="The full permission resolution model, built-in roles, scopes, and common patterns." />
<LinkCard title="Identity & Login" href="/guides/operations/identity-login/" description="Local accounts, external providers, SMTP email flows, and owner recovery." />
<LinkCard title="Identity & Login" href="/guides/operations/identity-login/" description="Local accounts, external providers, transactional email flows, and owner recovery." />
<LinkCard title="Server Operations" href="/guides/operations/server-operations/" description="Admin dashboard, event log, metrics, backups, and recovery operations." />
<LinkCard title="Room Directory API" href="/reference/connectrpc-api/room-directory/" description="Typed API reference for room groups, sidebar layout, and visible rooms." />
</CardGrid>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Identity & Login
description: Configure local accounts, SMTP email flows, external login providers, account linking, and owner recovery.
description: Configure local accounts, transactional email flows, external login providers, account linking, and owner recovery.
---

import { Aside, CardGrid, LinkCard, Tabs, TabItem } from "@astrojs/starlight/components";
Expand All @@ -11,18 +11,18 @@ Chatto supports local password accounts, email verification and reset flows, ext

| Model | Operator setup |
| ----- | -------------- |
| Local accounts only | Configure SMTP if you want email verification and password reset. |
| Local accounts only | Configure transactional email if you want email verification and password reset. |
| Invite or admin-managed server | Use local accounts plus operator-created users or restricted signup policy. |
| Organization SSO | Configure one or more `[[auth.providers]]` entries. |
| Mixed community | Allow local accounts and selected external providers. |
| Passwordless external login | Ensure users keep at least one linked provider and understand recovery paths. |

## SMTP Email
## Transactional Email

SMTP is needed for verification and password reset email flows. Configure it before inviting members if you expect self-service account recovery.
Transactional email is needed for verification and password reset flows. SMTP is the default transport; use JMAP only when your provider offers JMAP submission with bearer-token authentication. Configure a transport before inviting members if you expect self-service account recovery.

<Tabs>
<TabItem label="TOML">
<TabItem label="SMTP (TOML)">

```toml
[smtp]
Expand All @@ -36,7 +36,7 @@ from = "Chatto <chatto@example.com>"
```

</TabItem>
<TabItem label="Environment">
<TabItem label="SMTP (environment)">

```bash
CHATTO_SMTP_ENABLED=true
Expand All @@ -48,11 +48,36 @@ CHATTO_SMTP_PASSWORD=<generate-me>
CHATTO_SMTP_FROM="Chatto <chatto@example.com>"
```

</TabItem>
<TabItem label="JMAP (TOML)">

```toml
[email]
transport = "jmap"

[email.jmap]
session_url = "https://mail.example.com/.well-known/jmap"
access_token = "<generate-me>"
from = "Chatto <chatto@example.com>"
```

</TabItem>
<TabItem label="JMAP (environment)">

```bash
CHATTO_EMAIL_TRANSPORT=jmap
CHATTO_EMAIL_JMAP_SESSION_URL=https://mail.example.com/.well-known/jmap
CHATTO_EMAIL_JMAP_ACCESS_TOKEN=<generate-me>
CHATTO_EMAIL_JMAP_FROM="Chatto <chatto@example.com>"
```

</TabItem>
</Tabs>

Use `smtp.tls_server_name` only when the SMTP host you connect to differs from the certificate name. Use `smtp.tls_skip_verify` only for a trusted internal mail server where you accept that verification tradeoff.

With JMAP, Chatto discovers the primary submission account, a matching sender identity, and the Drafts mailbox. Set `email.jmap.account_id`, `email.jmap.identity_id`, or `email.jmap.draft_mailbox_id` only when automatic discovery is unsuitable. A successful submission means the JMAP server accepted the message; it does not confirm final recipient delivery.

## External Providers

External login providers are configured as repeated `[[auth.providers]]` entries. Supported provider types are `oidc`, `github`, `gitlab`, `google`, and `discord`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ For HSTS, CSP, and other policy headers, add them at your reverse proxy.
| NATS embedded/client credentials | `chatto.toml` / `CHATTO_NATS_EMBEDDED_AUTH_TOKEN`, `CHATTO_NATS_CLIENT_TOKEN`, `CHATTO_NATS_CLIENT_PASSWORD`, `CHATTO_NATS_CLIENT_CREDENTIALS_FILE`, or `CHATTO_NATS_CLIENT_NKEY_SEED` | n/a |
| OAuth/OIDC provider client secrets | `chatto.toml` `[[auth.providers]]` / `CHATTO_AUTH_PROVIDERS_<index>_CLIENT_SECRET` or legacy `CHATTO_AUTH_OIDC_CLIENT_SECRET` | n/a |
| SMTP password | `chatto.toml` / `CHATTO_SMTP_PASSWORD` | n/a |
| JMAP access token | `chatto.toml` / `CHATTO_EMAIL_JMAP_ACCESS_TOKEN` | n/a |
| Web Push VAPID key pair | `chatto.toml` / `CHATTO_PUSH_VAPID_PUBLIC_KEY`, `CHATTO_PUSH_VAPID_PRIVATE_KEY` | n/a |
| LiveKit API and webhook secrets | `chatto.toml` / `CHATTO_LIVEKIT_API_KEY`, `CHATTO_LIVEKIT_API_SECRET`, `CHATTO_LIVEKIT_WEBHOOK_API_KEY`, `CHATTO_LIVEKIT_WEBHOOK_API_SECRET` | n/a |
| TLS certificates | Disk cache directory (`autocert`) | n/a |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,41 @@ Set `auto_provision = true` only for providers whose users should be allowed to

See [External Login Providers](/guides/integrations/external-login-providers/) for callback URLs, provider-specific examples, and discovery behavior.

## SMTP
## Transactional Email

SMTP is the default transactional email transport. Set `email.transport = "jmap"` when your provider exposes JMAP submission and you want Chatto to authenticate with a bearer token instead.

<EnvVar name="CHATTO_EMAIL_TRANSPORT" toml="email.transport" default="smtp">
Transactional email transport. Supported values are `smtp` and `jmap`.
</EnvVar>

### JMAP

<EnvVar name="CHATTO_EMAIL_JMAP_SESSION_URL" toml="email.jmap.session_url">
HTTPS JMAP session resource URL. Required when `email.transport` is `jmap`.
</EnvVar>

<EnvVar name="CHATTO_EMAIL_JMAP_ACCESS_TOKEN" toml="email.jmap.access_token">
Bearer access token for the JMAP account. Required when `email.transport` is `jmap`.
</EnvVar>

<EnvVar name="CHATTO_EMAIL_JMAP_FROM" toml="email.jmap.from">
From address for outgoing email. It must match a sending identity in the JMAP account. Required when `email.transport` is `jmap`.
</EnvVar>

<EnvVar name="CHATTO_EMAIL_JMAP_ACCOUNT_ID" toml="email.jmap.account_id">
Optional JMAP account ID. By default, Chatto uses the session's primary submission account.
</EnvVar>

<EnvVar name="CHATTO_EMAIL_JMAP_IDENTITY_ID" toml="email.jmap.identity_id">
Optional JMAP identity ID. By default, Chatto uses the identity matching `email.jmap.from`.
</EnvVar>

<EnvVar name="CHATTO_EMAIL_JMAP_DRAFT_MAILBOX_ID" toml="email.jmap.draft_mailbox_id">
Optional JMAP Drafts mailbox ID. By default, Chatto uses the mailbox with the `drafts` role.
</EnvVar>

### SMTP

<EnvVar name="CHATTO_SMTP_ENABLED" toml="smtp.enabled" default="false">
Enable SMTP email sending.
Expand Down
3 changes: 3 additions & 0 deletions cli/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ var initCmd = &cobra.Command{
Port: 587,
TLS: config.SMTPTLSMandatory,
},
Email: config.EmailConfig{
Transport: config.EmailTransportSMTP,
},
NATS: config.NATSConfig{
Replicas: 1,
Client: config.NATSClientConfig{
Expand Down
6 changes: 6 additions & 0 deletions cli/cmd/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ func TestInitGeneratesCoreSecret(t *testing.T) {
if cfg.SMTP.TLS != config.SMTPTLSMandatory {
t.Fatalf("generated SMTP TLS policy = %q, want %q", cfg.SMTP.TLS, config.SMTPTLSMandatory)
}
if cfg.Email.Transport != config.EmailTransportSMTP {
t.Fatalf("generated email transport = %q, want %q", cfg.Email.Transport, config.EmailTransportSMTP)
}
raw, err := os.ReadFile(filepath.Join(tmpDir, "chatto.toml"))
if err != nil {
t.Fatalf("read generated raw config: %v", err)
Expand Down Expand Up @@ -123,6 +126,9 @@ func TestInitGeneratesCoreSecret(t *testing.T) {
if !strings.Contains(rawText, "\n[smtp]\n") {
t.Fatal("generated config should include SMTP defaults")
}
if !strings.Contains(rawText, "transport = 'smtp'") && !strings.Contains(rawText, "transport = \"smtp\"") {
t.Fatal("generated config should include the SMTP email transport default")
}
if !strings.Contains(rawText, "\nport = 587\n") {
t.Fatal("generated SMTP config should default to STARTTLS submission port 587")
}
Expand Down
103 changes: 89 additions & 14 deletions cli/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/hex"
"fmt"
"net"
"net/mail"
"net/url"
"os"
"sort"
Expand Down Expand Up @@ -219,6 +220,20 @@ func validateAbsoluteHTTPURL(name, raw string) error {
return nil
}

func validateAbsoluteHTTPSURL(name, raw string) error {
u, err := url.Parse(raw)
if err != nil {
return fmt.Errorf("%s is invalid: %w", name, err)
}
if u.Scheme != "https" {
return fmt.Errorf("%s must use https", name)
}
if u.Host == "" || u.User != nil {
return fmt.Errorf("%s must include a host and must not include user info", name)
}
return nil
}

func validateOrigin(name, raw string, allowWildcard bool, requireHTTPSExceptLoopback bool) error {
raw = strings.TrimSpace(raw)
if allowWildcard && raw == "*" {
Expand Down Expand Up @@ -733,6 +748,43 @@ type SMTPConfig struct {
From string `toml:"from" env:"CHATTO_SMTP_FROM" comment:"From address for outgoing emails. Example: noreply@example.com"`
}

// EmailTransport identifies the configured transactional email submission transport.
type EmailTransport string

const (
// EmailTransportSMTP submits transactional email through SMTP.
EmailTransportSMTP EmailTransport = "smtp"
// EmailTransportJMAP submits transactional email through JMAP.
EmailTransportJMAP EmailTransport = "jmap"
)

// JMAPConfig contains settings for transactional email submitted through JMAP.
type JMAPConfig struct {
SessionURL string `toml:"session_url,commented" env:"CHATTO_EMAIL_JMAP_SESSION_URL" comment:"JMAP session resource URL. Must use HTTPS."`
AccessToken string `toml:"access_token,commented" env:"CHATTO_EMAIL_JMAP_ACCESS_TOKEN" comment:"Bearer access token for the JMAP account. NEVER SHARE THIS!"`
From string `toml:"from,commented" env:"CHATTO_EMAIL_JMAP_FROM" comment:"From address for outgoing emails. It must match a JMAP identity. Example: noreply@example.com"`
AccountID string `toml:"account_id,commented" env:"CHATTO_EMAIL_JMAP_ACCOUNT_ID" comment:"Optional JMAP account ID. Defaults to the session's primary submission account."`
IdentityID string `toml:"identity_id,commented" env:"CHATTO_EMAIL_JMAP_IDENTITY_ID" comment:"Optional JMAP identity ID. Defaults to the identity matching jmap.from."`
DraftMailboxID string `toml:"draft_mailbox_id,commented" env:"CHATTO_EMAIL_JMAP_DRAFT_MAILBOX_ID" comment:"Optional JMAP Drafts mailbox ID. Defaults to the mailbox with role 'drafts'."`
}

// EmailConfig contains transactional email transport settings. SMTP remains the
// default so existing configurations continue to work without changes.
type EmailConfig struct {
Transport EmailTransport `toml:"transport" env:"CHATTO_EMAIL_TRANSPORT" comment:"Transactional email transport: smtp (default) or jmap."`
JMAP JMAPConfig `toml:"jmap,commented" comment:"JMAP transactional email configuration. Used only when email.transport = 'jmap'."`
}

// TransportOrDefault returns the selected transport, defaulting to SMTP for
// backward compatibility with existing SMTP-only configurations.
func (c EmailConfig) TransportOrDefault() EmailTransport {
transport := EmailTransport(strings.ToLower(strings.TrimSpace(string(c.Transport))))
if transport == "" {
return EmailTransportSMTP
}
return transport
}

// PushConfig contains settings for Web Push notifications.
// Push notifications allow messages to be delivered even when the browser is closed.
type PushConfig struct {
Expand Down Expand Up @@ -866,6 +918,7 @@ type ChattoConfig struct {
Core CoreConfig `toml:"core" comment:"Core service configuration."`
Auth AuthConfig `toml:"auth" comment:"Authentication configuration."`
Limits LimitsConfig `toml:"limits,commented" comment:"Instance-wide resource limits. Use -1 for unlimited."`
Email EmailConfig `toml:"email" comment:"Transactional email transport configuration. SMTP is the default transport."`
SMTP SMTPConfig `toml:"smtp" comment:"SMTP configuration for transactional emails."`
Push PushConfig `toml:"push,commented" comment:"Web Push notification configuration."`
Video VideoConfig `toml:"video,commented" comment:"Video processing configuration. Requires ffmpeg."`
Expand Down Expand Up @@ -1096,25 +1149,47 @@ func (c *ChattoConfig) Validate() error {
}
}

// SMTP configuration
switch c.SMTP.TLSPolicyOrDefault() {
case SMTPTLSMandatory, SMTPTLSOpportunistic, SMTPTLSImplicit:
default:
errs = append(errs, "smtp.tls must be one of: mandatory, opportunistic, implicit")
}
if c.SMTP.Enabled {
// Transactional email configuration
switch c.Email.TransportOrDefault() {
case EmailTransportSMTP:
switch c.SMTP.TLSPolicyOrDefault() {
case SMTPTLSMandatory, SMTPTLSOpportunistic, SMTPTLSImplicit:
default:
errs = append(errs, "smtp.tls must be one of: mandatory, opportunistic, implicit")
}
if c.SMTP.Enabled {
if c.Webserver.URL == "" {
errs = append(errs, "webserver.url is required when SMTP is enabled")
}
if c.SMTP.Host == "" {
errs = append(errs, "smtp.host is required when SMTP is enabled")
}
if c.SMTP.Port < 1 || c.SMTP.Port > 65535 {
errs = append(errs, "smtp.port must be between 1 and 65535 when SMTP is enabled")
}
if c.SMTP.From == "" {
errs = append(errs, "smtp.from is required when SMTP is enabled")
}
}
case EmailTransportJMAP:
if c.Webserver.URL == "" {
errs = append(errs, "webserver.url is required when SMTP is enabled")
errs = append(errs, "webserver.url is required when JMAP email is enabled")
}
if c.SMTP.Host == "" {
errs = append(errs, "smtp.host is required when SMTP is enabled")
if c.Email.JMAP.SessionURL == "" {
errs = append(errs, "email.jmap.session_url is required when email.transport is jmap")
} else if err := validateAbsoluteHTTPSURL("email.jmap.session_url", c.Email.JMAP.SessionURL); err != nil {
errs = append(errs, err.Error())
}
if c.SMTP.Port < 1 || c.SMTP.Port > 65535 {
errs = append(errs, "smtp.port must be between 1 and 65535 when SMTP is enabled")
if c.Email.JMAP.AccessToken == "" {
errs = append(errs, "email.jmap.access_token is required when email.transport is jmap")
}
if c.SMTP.From == "" {
errs = append(errs, "smtp.from is required when SMTP is enabled")
if c.Email.JMAP.From == "" {
errs = append(errs, "email.jmap.from is required when email.transport is jmap")
} else if _, err := mail.ParseAddress(c.Email.JMAP.From); err != nil {
errs = append(errs, "email.jmap.from must be a valid email address")
}
default:
errs = append(errs, "email.transport must be one of: smtp, jmap")
}

// Push notification configuration
Expand Down
Loading