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
3 changes: 3 additions & 0 deletions .github/workflows/crowdin_download.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
branches:
- 'release/**'

permissions:
contents: read

jobs:

synchronize-with-crowdin:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/crowdin_upload.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
branches:
- main

permissions:
contents: read

jobs:

synchronize-with-crowdin:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
name: Build and Push Container Image

permissions:
contents: read

"on":
workflow_call:
inputs:
Expand Down
16 changes: 16 additions & 0 deletions .github/workflows/messages-ghcr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ name: Build and publish OCI images
tags:
- 'v*'

permissions:
contents: read

jobs:

docker-publish-mta-in:
Expand Down Expand Up @@ -74,6 +77,19 @@ jobs:
context: "src/backend"
target: runtime-prod

docker-publish-client-bridge:
uses: ./.github/workflows/docker-publish.yml
permissions:
contents: read
packages: write
attestations: write
id-token: write
secrets: inherit
with:
image_name: "client-bridge"
context: "src/client-bridge"
target: runtime-prod

docker-publish-keycloak:
uses: ./.github/workflows/docker-publish.yml
permissions:
Expand Down
49 changes: 45 additions & 4 deletions .github/workflows/messages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ name: Lint and tests
branches:
- '*'

permissions:
contents: read

env:
COMPOSE_BAKE: true

Expand All @@ -21,7 +24,7 @@ jobs:
- name: Create env files
run: make create-env-files
- name: Run linting checks
run: make lint-back
run: make lint-check-back


test-back:
Expand Down Expand Up @@ -79,9 +82,7 @@ jobs:
- name: Install frontend dependencies
run: make install-frozen-front
- name: Run frontend linting
run: make lint-front
- name: Run frontend check
run: make typecheck-front
run: make lint-check-front

build-front:
runs-on: ubuntu-latest
Expand All @@ -95,6 +96,46 @@ jobs:
- name: Build frontend
run: make build-front

lint-client-bridge:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Create env files
run: make create-env-files
- name: Run client-bridge linting
run: make lint-check-client-bridge

test-client-bridge:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Create env files
run: make create-env-files
- name: Run client-bridge tests
run: make test-client-bridge

test-mta-in:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Create env files
run: make create-env-files
- name: Run mta-in tests
run: make test-mta-in

test-socks-proxy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Create env files
run: make create-env-files
- name: Run socks-proxy tests
run: make test-socks-proxy

check-api-state:
runs-on: ubuntu-latest
steps:
Expand Down
57 changes: 46 additions & 11 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ create-env-files: \
env.d/development/frontend.local \
env.d/development/mta-in.local \
env.d/development/mta-out.local \
env.d/development/client-bridge.local \
env.d/development/socks-proxy.local
.PHONY: create-env-files

Expand Down Expand Up @@ -134,7 +135,7 @@ logs: ## display all services logs (follow mode)
.PHONY: logs

start: ## start all development services
@$(COMPOSE) up --force-recreate --build -d frontend-dev backend-dev worker-dev mta-in --wait
@$(COMPOSE) up --force-recreate --build -d frontend-dev backend-dev worker-dev mta-in client-bridge --wait
.PHONY: start

start-minimal: ## start minimal services (backend, frontend, keycloak and DB)
Expand Down Expand Up @@ -176,16 +177,18 @@ lint: ## run all linters
lint: \
lint-back \
lint-front \
typecheck-front \
lint-mta-in \
lint-mta-out
lint-mta-out \
lint-client-bridge
.PHONY: lint

lint-check: ## run all linters in check mode (no auto-fix)
lint-check: \
lint-check-back \
typecheck-front \
lint-front
lint-check-front \
lint-check-mta-in \
lint-check-mta-out \
lint-check-client-bridge
.PHONY: lint-check

lint-back: ## run back-end linters (with auto-fix)
Expand Down Expand Up @@ -217,20 +220,42 @@ typecheck-front: ## run the frontend type checker
@$(COMPOSE) run --rm frontend-tools npm run ts:check
.PHONY: typecheck-front

lint-front: ## run the frontend linter
lint-front: ## run the frontend linter (typecheck + eslint)
lint-front: \
typecheck-front
@$(COMPOSE) run --rm frontend-tools npm run lint
.PHONY: lint-front

lint-mta-in: ## lint mta-in python sources
lint-check-front: ## run the frontend linter in check mode (no auto-fix)
lint-check-front: lint-front
.PHONY: lint-check-front

lint-mta-in: ## lint mta-in python sources (with auto-fix)
$(COMPOSE_RUN) --rm -e EXEC_CMD_ONLY=true mta-in-test ruff format .
#$(COMPOSE_RUN) --rm -e EXEC_CMD_ONLY=true mta-in-test ruff check . --fix
#$(COMPOSE_RUN) --rm -e EXEC_CMD_ONLY=true mta-in-test pylint .
.PHONY: lint-mta-in

lint-mta-out: ## lint mta-out python sources
lint-check-mta-in: ## lint mta-in python sources in check mode (no auto-fix)
$(COMPOSE_RUN) --rm -e EXEC_CMD_ONLY=true mta-in-test ruff format --check .
.PHONY: lint-check-mta-in

lint-mta-out: ## lint mta-out python sources (with auto-fix)
$(COMPOSE_RUN) --rm -e EXEC_CMD_ONLY=true mta-out-test ruff format .
.PHONY: lint-mta-out

lint-check-mta-out: ## lint mta-out python sources in check mode (no auto-fix)
$(COMPOSE_RUN) --rm -e EXEC_CMD_ONLY=true mta-out-test ruff format --check .
.PHONY: lint-check-mta-out

lint-client-bridge: ## lint client-bridge python sources (with auto-fix)
$(COMPOSE_RUN) --rm -e EXEC_CMD_ONLY=true client-bridge-test ruff format .
.PHONY: lint-client-bridge

lint-check-client-bridge: ## lint client-bridge python sources in check mode (no auto-fix)
$(COMPOSE_RUN) --rm -e EXEC_CMD_ONLY=true client-bridge-test ruff format --check .
.PHONY: lint-check-client-bridge

# -- Tests

test: ## run all tests
Expand All @@ -239,6 +264,7 @@ test: \
test-front \
test-mta-in \
test-mta-out \
test-client-bridge \
test-mpa \
test-socks-proxy
.PHONY: test
Expand Down Expand Up @@ -280,6 +306,10 @@ test-mta-out: ## run the mta-out tests
@$(COMPOSE) run --build --rm mta-out-test
.PHONY: test-mta-out

test-client-bridge: ## run the client-bridge tests
@$(COMPOSE) run --build --rm client-bridge-test
.PHONY: test-client-bridge

test-mpa: ## run the mpa tests
@$(COMPOSE) run --build --rm mpa-test
.PHONY: test-mpa
Expand Down Expand Up @@ -331,21 +361,21 @@ logs-e2e: ## Show logs from e2e services

test-e2e-bare: ## Run e2e tests in headless mode
@echo "$(BLUE)\n\n| 🎭 Running E2E tests... \n$(RESET)"
$(COMPOSE_E2E) run --rm --service-ports runner npm run test -- $(args)
$(COMPOSE_E2E) run --rm --service-ports e2e-runner npm run test -- $(args)
@echo "$(GREEN)> 🎭 E2E tests completed!$(RESET)\n"
.PHONY: test-e2e-bare

test-e2e-ui-bare: ## Run e2e tests in UI mode
@echo "$(BLUE)\n\n| 🎭 Running E2E tests in UI mode... \n$(RESET)"
# Note: || true allows graceful exit when user closes the UI
@$(COMPOSE_E2E) run --rm --service-ports runner npm run test:ui || true
@$(COMPOSE_E2E) run --rm --service-ports e2e-runner npm run test:ui || true
@echo "$(GREEN)> 🎭 You killed the UI!$(RESET)\n"
.PHONY: test-e2e-ui-bare

test-e2e-dev-bare: ## Run e2e tests in UI mode with dev frontend
@echo "$(BLUE)\n\n| 🎭 Running E2E tests in dev mode... \n$(RESET)"
# Note: || true allows graceful exit when user closes the UI
E2E_PROFILE=dev $(COMPOSE_E2E) --profile dev run --rm --service-ports runner npm run test:ui || true
E2E_PROFILE=dev $(COMPOSE_E2E) --profile dev run --rm --service-ports e2e-runner npm run test:ui || true
@echo "$(GREEN)> 🎭 You killed the UI!$(RESET)\n"
.PHONY: test-e2e-dev-bare

Expand All @@ -355,6 +385,7 @@ down-e2e: stop-e2e ## alias for stop-e2e
demo-e2e: ## Populate the e2e database with demo data
@echo "$(BLUE)\n\n| 📝 Bootstrapping E2E demo data... \n$(RESET)"
@$(COMPOSE_E2E) run --rm backend python manage.py e2e_demo
@$(COMPOSE_E2E) run --rm backend python manage.py e2e_clientbridge
.PHONY: demo-e2e

start-e2e: ## Start e2e services (migrate, seed, etc.)
Expand Down Expand Up @@ -579,3 +610,7 @@ deps-lock-mta-in: ## lock the dependencies
deps-lock-mta-out: ## lock the dependencies
@$(COMPOSE) run --rm --build mta-out-uv uv lock
.PHONY: deps-lock-mta-out

deps-lock-client-bridge: ## lock the dependencies
@$(COMPOSE) run --rm --build client-bridge-uv uv lock
.PHONY: deps-lock-client-bridge
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,11 @@ It features a [MTA](https://en.wikipedia.org/wiki/Message_transfer_agent) based
* (soon) 👉 Assign threads to specific users

### Based on standards
* 🔑 OpenID Connect for all user accounts. Plug any identity provider, including Keycloak.
* 📬 SMTP in and out.
* ❌ No POP3 or IMAP client support, by design. We're building for the future, not the (unsecure) past!
* ✅ JMAP-inspired data model. Full support could be added.
* 🔑 OpenID Connect for all user accounts as the primary authentication method. Plug any identity provider, including Keycloak.
* 📬 SMTP in and out (server-to-server).
* ✅ JMAP-inspired data model. JMAP-compliant endpoint [in progress](https://github.com/suitenumerique/messages/pull/479).
* 📮 Optional IMAP and SMTP client access via the [client bridge](/src/client-bridge/), for users who prefer traditional email clients like Thunderbird or mobile phones. Uses app-specific passwords with configurable roles.


### Self-host
* 🚀 Messages is designed to be installed on the cloud or on your own servers.
Expand Down Expand Up @@ -146,6 +147,8 @@ When running the project, the following services are available:
| **SOCKS Proxy** | 8916 | SOCKS5 proxy | `user1` / `pwd1` |
| **Mailcatcher (SMTP)** | 8917 | SMTP server | No auth required |
| **MPA (Rspamd)** | 8918 | Spam filtering service | `password` |
| **Client Bridge (IMAP)** | 8919 | IMAP server for email clients | App-specific password |
| **Client Bridge (SMTP)** | 8920 | SMTP submission for email clients | App-specific password |


### OpenAPI client
Expand Down
44 changes: 44 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ services:
mailcatcher:
condition: service_started

# Minimal backend service for development tasks that don't require all service dependencies
backend-db:
extends: backend-base
profiles:
Expand Down Expand Up @@ -268,6 +269,49 @@ services:
target: uv
pull_policy: build

client-bridge:
build:
context: src/client-bridge
target: runtime-prod
env_file:
- env.d/development/client-bridge.defaults
- env.d/development/client-bridge.local
ports:
- "8919:143"
- "8920:587"
depends_on:
- backend-dev

client-bridge-test:
profiles:
- tools
build:
context: src/client-bridge
target: runtime-dev
env_file:
- env.d/development/client-bridge.defaults
- env.d/development/client-bridge.local
environment:
- EXEC_CMD=true
- IMAP_HOST=localhost
- IMAP_PORT=1143
- SMTP_HOST=localhost
- SMTP_PORT=1587
- MOCK_API_PORT=8765
command: pytest -vvs tests/
volumes:
- ./src/client-bridge:/app

client-bridge-uv:
profiles:
- tools
volumes:
- ./src/client-bridge:/app
build:
context: src/client-bridge
target: uv
pull_policy: build

mta-out:
build:
context: src/mta-out
Expand Down
24 changes: 24 additions & 0 deletions docs/env.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,30 @@ without redeploying the frontend (the flag is pulled from
| `FEATURE_AI_SUMMARY` | `False` | Default enabled mode for summary AI features | Required |
| `FEATURE_AI_AUTOLABELS` | `False` | Default enabled mode for label AI features | Required |

### Client Bridge

| Variable | Default | Description | Required |
|----------|---------|-------------|----------|
| `CLIENTBRIDGE_API_SECRET` | `""` | Shared secret for service-to-service auth between the client bridge and the backend. Must match the `CLIENTBRIDGE_API_SECRET` on the client-bridge service. | Required (if client bridge is enabled) |
| `CLIENTBRIDGE_SESSION_TIMEOUT` | `3600` | JWT session lifetime in seconds. IMAP/SMTP clients must re-authenticate when the token expires. | Optional |
| `CLIENTBRIDGE_PUBLIC_CONFIG` | `{}` | JSON object with IMAP/SMTP connection settings exposed to the frontend via `/api/v1.0/config/`. When set, the frontend displays these values in the integration setup form instead of inferring them from the browser's hostname. See [Client Bridge README](../src/client-bridge/README.md) for details. | Optional |

`CLIENTBRIDGE_PUBLIC_CONFIG` accepts the following keys:

| Key | Type | Description | Example |
|-----|------|-------------|---------|
| `imap_host` | string | Hostname for IMAP connections | `"imap.example.com"` |
| `imap_port` | integer | Port for IMAP connections | `993` |
| `imap_security` | string | Security mode for IMAP | `"SSL/TLS"` |
| `smtp_host` | string | Hostname for SMTP connections | `"smtp.example.com"` |
| `smtp_port` | integer | Port for SMTP connections | `587` |
| `smtp_security` | string | Security mode for SMTP | `"STARTTLS"` |

Example:
```bash
CLIENTBRIDGE_PUBLIC_CONFIG='{"imap_host":"imap.example.com","imap_port":993,"imap_security":"SSL/TLS","smtp_host":"smtp.example.com","smtp_port":587,"smtp_security":"STARTTLS"}'
```

### Throttling

Outbound message throttling limits the number of **external recipients** (recipients whose domain is not managed by this instance) that can be sent from a mailbox or maildomain within a time period, using simple fixed time windows.
Expand Down
Loading