🔨(devx) update developer experience: uv, rustfs, caddy, new makefile (#556)

This large PR modernizes the backend of the app:
 - Python Dependency Management: Poetry → uv
 - Object Storage for local dev: MinIO (now unmaintained) → RustFS
 - Makefile Target Standardization to align with other LaSuite repos
 - Internationalization Removal on backend: we only care about i18n on the frontend
 - Backend dependencies upgrade
This commit is contained in:
Sylvain Zimmer
2026-02-23 12:30:36 +01:00
committed by GitHub
parent d7729f4b88
commit f18277b742
111 changed files with 4966 additions and 13653 deletions
+3 -6
View File
@@ -15,12 +15,9 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
# Backend i18n
- name: Create env files
run: make create-env-files
- name: Generate source translations
run: make front-install-frozen
- name: Generate source translations
- name: Install frontend dependencies
run: make install-frozen-front
- name: Generate frontend source translations
run: make i18n-generate
- name: crowdin action
uses: crowdin/github-action@v2
+12 -12
View File
@@ -21,7 +21,7 @@ jobs:
- name: Create env files
run: make create-env-files
- name: Run linting checks
run: make back-lint
run: make lint-back
test-back:
@@ -35,14 +35,14 @@ jobs:
run: make create-env-files
- name: Setup data directories
run: |
mkdir -p data/media data/static
mkdir -p data/static
chown -R 1001:1001 data
- name: Collect static files
run: make collectstatic
- name: Create msg-imports bucket
run: make import-bucket
- name: Run backend tests
run: make back-test
run: make test-back
test-front:
runs-on: ubuntu-latest
@@ -52,9 +52,9 @@ jobs:
- name: Create env files
run: make create-env-files
- name: Install frontend dependencies
run: make front-install-frozen
run: make install-frozen-front
- name: Run frontend tests
run: make front-test
run: make test-front
test-e2e:
runs-on: ubuntu-latest
@@ -67,7 +67,7 @@ jobs:
- name: Create env files
run: make create-env-files
- name: Run E2E tests only for Chromium browser to speed up the tests
run: make e2e-test-ci args="--project chromium"
run: make test-e2e-ci args="--project chromium"
lint-front:
runs-on: ubuntu-latest
@@ -77,11 +77,11 @@ jobs:
- name: Create env files
run: make create-env-files
- name: Install frontend dependencies
run: make front-install-frozen
run: make install-frozen-front
- name: Run frontend linting
run: make front-lint
run: make lint-front
- name: Run frontend check
run: make front-ts-check
run: make typecheck-front
build-front:
runs-on: ubuntu-latest
@@ -91,9 +91,9 @@ jobs:
- name: Create env files
run: make create-env-files
- name: Install frontend dependencies
run: make front-install-frozen
run: make install-frozen-front
- name: Build frontend
run: make front-build
run: make build-front
check-api-state:
runs-on: ubuntu-latest
@@ -103,7 +103,7 @@ jobs:
- name: Create env files
run: make create-env-files
- name: Install frontend dependencies
run: make front-install-frozen
run: make install-frozen-front
- name: Run API update
run: make api-update
- name: Check for changes
+6 -1
View File
@@ -11,10 +11,15 @@ and this project adheres to
### Added
- Add arm64 platform support for Docker image builds
### Changed
- Bump keycloak to 26.5.3 #543
- Replace nginx with Caddy for frontend reverse proxy and Scalingo deployment #556
- Replace MinIO with RustFS for object storage in development #556
- Migrate Python packaging from Poetry to uv #556
- Standardize and rename Makefile targets #556
- Upgrade Python to 3.14 #556
- Remove Django i18n and backend translation catalogs #556
## [0.2.0] - 2026-02-03
+198 -209
View File
@@ -1,12 +1,3 @@
# /!\ /!\ /!\ /!\ /!\ /!\ /!\ DISCLAIMER /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\
#
# This Makefile is only meant to be used for DEVELOPMENT purpose as we are
# changing the user id that will run in the container.
#
# PLEASE DO NOT USE IT FOR YOUR CI/PRODUCTION/WHATEVER...
#
# /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\
#
# Note to developers:
#
# While editing this file, please respect the following statements:
@@ -114,8 +105,7 @@ update: ## Update the project with latest changes
@$(MAKE) build
@$(MAKE) collectstatic
@$(MAKE) migrate
@$(MAKE) front-install-frozen
@$(MAKE) i18n-compile
@$(MAKE) install-frozen-front
.PHONY: update
# -- Docker/compose
@@ -123,6 +113,18 @@ build: ## build the project containers
@$(COMPOSE) build
.PHONY: build
build-back-distroless: ## build the distroless production image
@docker build --target runtime-distroless-prod -t messages-distroless -f src/backend/Dockerfile src/backend/
.PHONY: build-back-distroless
test-back-distroless: build-back-distroless ## build and smoke-test the distroless production image
@docker run --rm messages-distroless python -c " \
import sys, ctypes, sqlite3, ssl; \
import magic; \
magic.from_buffer(b'test', mime=True); \
print(f'OK: Python {sys.version.split()[0]}, {ssl.OPENSSL_VERSION}')"
.PHONY: test-back-distroless
down: ## stop and remove containers, networks, images, and volumes
@$(COMPOSE) down
.PHONY: down
@@ -159,211 +161,214 @@ restart-minimal: \
start-minimal
.PHONY: restart-minimal
import-bucket: ## create the message imports bucket to objectstorage
@$(COMPOSE_RUN) objectstorage-createbucket
import-bucket: ## create the message imports bucket in objectstorage
@$(COMPOSE) up -d objectstorage --wait
@$(MANAGE_DB) create_bucket --storage message-imports --expire-days 1
.PHONY: import-bucket
objectstorage-shell: ## open a shell in the objectstorage container
shell-objectstorage: ## open a shell in the objectstorage container
@$(COMPOSE) run --rm --build objectstorage bash
.PHONY: objectstorage-shell
.PHONY: shell-objectstorage
# -- Linters
lint: ## run all linters
lint: \
back-lint \
front-lint \
front-ts-check \
mta-in-lint \
mta-out-lint
lint-back \
lint-front \
typecheck-front \
lint-mta-in \
lint-mta-out
.PHONY: lint
lint-check: ## run all linters in check mode
lint-check: ## run all linters in check mode (no auto-fix)
lint-check: \
back-ruff-check \
back-pylint \
front-ts-check \
front-lint
lint-check-back \
typecheck-front \
lint-front
.PHONY: lint-check
back-lint: ## run back-end linters
back-lint: \
back-ruff-format \
back-ruff-check \
back-pylint
.PHONY: back-lint
lint-back: ## run back-end linters (with auto-fix)
lint-back: \
format-back \
check-back \
analyze-back
.PHONY: lint-back
back-ruff-format: ## format back-end python sources with ruff
@$(COMPOSE_RUN_APP_TOOLS) ruff format .
.PHONY: back-ruff-format
back-ruff-check: ## lint back-end python sources with ruff
@$(COMPOSE_RUN_APP_TOOLS) ruff check . --fix
.PHONY: back-ruff-check
back-pylint: ## lint back-end python sources with pylint
lint-check-back: ## run back-end linters in check mode (no auto-fix)
@$(COMPOSE_RUN_APP_TOOLS) ruff format --check .
@$(COMPOSE_RUN_APP_TOOLS) ruff check .
@$(COMPOSE_RUN_APP_TOOLS) sh -c "pylint ."
.PHONY: back-pylint
.PHONY: lint-check-back
front-ts-check: ## run the frontend type checker
format-back: ## format back-end python sources
@$(COMPOSE_RUN_APP_TOOLS) ruff format .
.PHONY: format-back
check-back: ## check back-end python sources
@$(COMPOSE_RUN_APP_TOOLS) ruff check . --fix
.PHONY: check-back
analyze-back: ## analyze back-end python sources
@$(COMPOSE_RUN_APP_TOOLS) sh -c "pylint ."
.PHONY: analyze-back
typecheck-front: ## run the frontend type checker
@$(COMPOSE) run --rm frontend-tools npm run ts:check
.PHONY: front-ts-check
.PHONY: typecheck-front
front-lint: ## run the frontend linter
lint-front: ## run the frontend linter
@$(COMPOSE) run --rm frontend-tools npm run lint
.PHONY: front-lint
.PHONY: lint-front
mta-in-lint: ## lint mta-in python sources with pylint
lint-mta-in: ## lint mta-in python sources
$(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: mta-in-lint
.PHONY: lint-mta-in
mta-out-lint: ## lint mta-out python sources with pylint
lint-mta-out: ## lint mta-out python sources
$(COMPOSE_RUN) --rm -e EXEC_CMD_ONLY=true mta-out-test ruff format .
.PHONY: mta-out-lint
.PHONY: lint-mta-out
# -- Tests
test: ## run all tests
test: \
back-test \
front-test \
mta-in-test \
mta-out-test \
mpa-test \
socks-proxy-test
test-back \
test-front \
test-mta-in \
test-mta-out \
test-mpa \
test-socks-proxy
.PHONY: test
back-test: ## run back-end tests
test-back: ## run back-end tests
@args="$(filter-out $@,$(MAKECMDGOALS))" && \
bin/pytest $${args:-${1}}
.PHONY: back-test
.PHONY: test-back
back-test-parallel: ## run all back-end tests in parallel
test-back-parallel: ## run all back-end tests in parallel
@args="$(filter-out $@,$(MAKECMDGOALS))" && \
bin/pytest -n auto $${args:-${1}}
.PHONY: back-test-parallel
.PHONY: test-back-parallel
back-fuzz: ## run back-end fuzz tests
fuzz-back: ## run back-end fuzz tests
@args="$(filter-out $@,$(MAKECMDGOALS))" && \
bin/pytest -m fuzz $${args:-${1}}
.PHONY: back-fuzz
.PHONY: fuzz-back
front-test: ## run the frontend tests
test-front: ## run the frontend tests
@args="$(filter-out $@,$(MAKECMDGOALS))" && \
$(COMPOSE) run --rm frontend-tools npm run test -- $${args:-${1}}
.PHONY: front-test
.PHONY: test-front
front-test-update: ## run the frontend tests and update snapshots
test-front-update: ## run the frontend tests and update snapshots
$(COMPOSE) run --rm frontend-tools npm run test -- --update
.PHONY: front-test-update
.PHONY: test-front-update
front-test-amd64: ## run the frontend tests in amd64
test-front-amd64: ## run the frontend tests in amd64
@args="$(filter-out $@,$(MAKECMDGOALS))" && \
$(COMPOSE) run --rm frontend-tools-amd64 npm run test -- $${args:-${1}}
.PHONY: front-test-amd64
.PHONY: test-front-amd64
mta-in-test: ## run the mta-in tests
test-mta-in: ## run the mta-in tests
@$(COMPOSE) run --build --rm mta-in-test
.PHONY: mta-in-test
.PHONY: test-mta-in
mta-out-test: ## run the mta-out tests
test-mta-out: ## run the mta-out tests
@$(COMPOSE) run --build --rm mta-out-test
.PHONY: mta-out-test
.PHONY: test-mta-out
mpa-test: ## run the mpa tests
test-mpa: ## run the mpa tests
@$(COMPOSE) run --build --rm mpa-test
.PHONY: mpa-test
.PHONY: test-mpa
socks-proxy-test: ## run the socks-proxy tests
test-socks-proxy: ## run the socks-proxy tests
@$(COMPOSE) run --build --rm socks-proxy-test
.PHONY: socks-proxy-test
.PHONY: test-socks-proxy
# -- E2E Tests
e2e-test: ## Setup, run and teardown e2e tests in headless mode
@$(MAKE) e2e-setup
test-e2e: ## Setup, run and teardown e2e tests in headless mode
@$(MAKE) start-e2e
@args="$(filter-out $@,$(MAKECMDGOALS))" && \
$(MAKE) e2e-run-test args="$${args:-${1}}" || echo "$(BOLD)Tests failed$(RESET)"
@$(MAKE) e2e-teardown
.PHONY: e2e-test
$(MAKE) test-e2e-bare args="$${args:-${1}}" || echo "$(BOLD)Tests failed$(RESET)"
@$(MAKE) stop-e2e
.PHONY: test-e2e
e2e-test-ui: ## Setup, run and teardown e2e tests in UI mode
@$(MAKE) e2e-setup
@$(MAKE) e2e-run-test-ui
@$(MAKE) e2e-teardown
.PHONY: e2e-test-ui
test-e2e-ui: ## Setup, run and teardown e2e tests in UI mode
@$(MAKE) start-e2e
@$(MAKE) test-e2e-ui-bare
@$(MAKE) stop-e2e
.PHONY: test-e2e-ui
e2e-test-dev: ## Setup, run and teardown e2e tests in UI mode with dev frontend
@$(MAKE) e2e-setup
@$(MAKE) e2e-run-test-dev
@$(MAKE) e2e-teardown
.PHONY: e2e-test-dev
test-e2e-dev: ## Setup, run and teardown e2e tests in UI mode with dev frontend
@$(MAKE) start-e2e
@$(MAKE) test-e2e-dev-bare
@$(MAKE) stop-e2e
.PHONY: test-e2e-dev
e2e-test-ci: ## Setup and run e2e tests in CI mode
@$(MAKE) e2e-setup
@$(MAKE) e2e-run-test args="$(args)"
.PHONY: e2e-test-ci
test-e2e-ci: ## Setup and run e2e tests in CI mode
@$(MAKE) start-e2e
@$(MAKE) test-e2e-bare args="$(args)"
.PHONY: test-e2e-ci
e2e-build: ## Build the e2e services
build-e2e: ## Build the e2e services
@args="$(filter-out $@,$(MAKECMDGOALS))" && \
$(COMPOSE_E2E) build --no-cache $${args:-${1}}
.PHONY: e2e-build
.PHONY: build-e2e
e2e-log:
log-e2e: ## alias for logs-e2e
@args="$(filter-out $@,$(MAKECMDGOALS))" && \
$(MAKE) e2e-logs -- $${args:-${1}}
.PHONY: e2e-log
$(MAKE) logs-e2e -- $${args:-${1}}
.PHONY: log-e2e
e2e-logs: ## Show logs from e2e services
logs-e2e: ## Show logs from e2e services
@args="$(filter-out $@,$(MAKECMDGOALS))" && \
$(COMPOSE_E2E) --profile dev logs $${args:-${1}}
.PHONY: e2e-logs
.PHONY: logs-e2e
e2e-run-test: ## Run e2e tests in headless mode
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)
@echo "$(GREEN)> 🎭 E2E tests completed!$(RESET)\n"
.PHONY: e2e-run-test
.PHONY: test-e2e-bare
e2e-run-test-ui: ## Run e2e tests in UI mode
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
@echo "$(GREEN)> 🎭 You killed the UI!$(RESET)\n"
.PHONY: e2e-run-test-ui
.PHONY: test-e2e-ui-bare
e2e-run-test-dev: ## Run e2e tests in UI mode with dev frontend
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
@echo "$(GREEN)> 🎭 You killed the UI!$(RESET)\n"
.PHONY: e2e-run-test-dev
.PHONY: test-e2e-dev-bare
e2e-down: ## Stop and remove all e2e services
@echo "$(BOLD)Stopping E2E services...$(RESET)"
@$(COMPOSE_E2E) --profile dev down -v
@echo "$(GREEN)✓ E2E services stopped$(RESET)"
.PHONY: e2e-down
down-e2e: stop-e2e ## alias for stop-e2e
.PHONY: down-e2e
e2e-demo: ## Populate the e2e database with demo data
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
.PHONY: e2e-demo
.PHONY: demo-e2e
e2e-setup: ## Setup e2e services
start-e2e: ## Start e2e services (migrate, seed, etc.)
@echo "$(BLUE)\n\n| 🔧 Setting up E2E services... \n$(RESET)"
@$(COMPOSE_E2E) run --rm objectstorage-createbucket
@$(COMPOSE_E2E) run --rm backend python manage.py create_bucket --storage message-imports --expire-days 1
@$(COMPOSE_E2E) run --rm backend python manage.py migrate --noinput
@$(COMPOSE_E2E) run --rm backend python manage.py search_index_create || true
@$(MAKE) e2e-demo
.PHONY: e2e-setup
@$(MAKE) demo-e2e
.PHONY: start-e2e
e2e-teardown: ## Teardown e2e services
stop-e2e: ## Stop and remove e2e services
@echo "$(BLUE)\n\n| 🧹 Cleaning up E2E services... \n$(RESET)"
@$(COMPOSE_E2E) --profile dev down -v
.PHONY: e2e-teardown
.PHONY: stop-e2e
# -- Backend
@@ -386,80 +391,72 @@ superuser: ## Create an admin superuser with password "admin"
@$(MANAGE_DB) createsuperuser --email admin@admin.local --password admin
.PHONY: superuser
back-i18n-compile: ## compile the gettext files
@$(MANAGE) compilemessages --ignore="venv/**/*"
.PHONY: back-i18n-compile
back-i18n-generate: ## create the .pot files used for i18n
@$(MANAGE) makemessages -a --keep-pot --all --no-location
.PHONY: back-i18n-generate
back-shell: ## open a shell in the backend container
shell-back: ## open a shell in the backend container
@$(COMPOSE) run --rm --build backend-dev /bin/bash
.PHONY: back-shell
.PHONY: shell-back
back-shell-no-deps: ## open a shell in the backend container without dependencies
shell-back-no-deps: ## open a shell in the backend container without dependencies
@$(COMPOSE) run --rm --no-deps --build backend-dev /bin/bash
.PHONY: back-shell-no-deps
.PHONY: shell-back-no-deps
back-exec: ## open a shell in the running backend-dev container
exec-back: ## open a shell in the running backend-dev container
@$(COMPOSE) exec backend-dev /bin/bash
.PHONY: back-exec
.PHONY: exec-back
back-poetry-lock: ## lock the dependencies
@$(COMPOSE) run --rm --build backend-poetry poetry lock
make pip-audit
.PHONY: back-poetry-lock
deps-lock-back: ## lock the dependencies
@$(COMPOSE) run --rm --build backend-uv uv lock
@$(MAKE) deps-audit
.PHONY: deps-lock-back
back-poetry-update-indirect: ## update indirect dependencies
rm src/backend/poetry.lock
make back-poetry-lock
.PHONY: back-poetry-update-indirect
back-poetry-check: ## check the dependencies
@$(COMPOSE) run --rm --build backend-poetry poetry check
.PHONY: back-poetry-check
deps-update-indirect-back: ## update indirect dependencies
rm -f src/backend/uv.lock
@$(MAKE) deps-lock-back
.PHONY: deps-update-indirect-back
back-poetry-outdated: ## show outdated dependencies
@$(COMPOSE) run --rm --build backend-poetry poetry show --outdated
.PHONY: back-poetry-outdated
deps-outdated-back: ## show outdated dependencies
@$(COMPOSE) run --rm --build backend-uv uv tree --outdated
.PHONY: deps-outdated-back
back-poetry-tree: ## show dependencies as a tree
@$(COMPOSE) run --rm --build backend-dev pipdeptree
.PHONY: back-poetry-tree
deps-tree-back: ## show dependencies as a tree
@$(COMPOSE) run --rm --build backend-uv uv tree
.PHONY: deps-tree-back
pip-audit: ## check the dependencies
deps-audit-back: ## audit back-end dependencies for vulnerabilities
@$(COMPOSE) run --rm --no-deps -e HOME=/tmp --build backend-dev pip-audit
.PHONY: pip-audit
.PHONY: deps-audit-back
deps-audit: deps-audit-back ## alias for deps-audit-back
.PHONY: deps-audit
collectstatic: ## collect static files
@$(MANAGE_DB) collectstatic --noinput
.PHONY: collectstatic
shell: ## connect to django shell
shell-back-django: ## connect to django shell
@$(MANAGE) shell #_plus
.PHONY: shell
.PHONY: shell-back-django
keycloak-export: ## export all keycloak data to a JSON file
export-identity: ## export all identity provider data to a JSON file
@$(COMPOSE) run -v `pwd`/src/keycloak:/tmp/keycloak-export --rm keycloak export --realm messages --file /tmp/keycloak-export/realm.json
.PHONY: keycloak-export
.PHONY: export-identity
# -- Database
db-shell: ## connect to database shell
shell-db: ## connect to database shell
$(COMPOSE) exec backend-dev python manage.py dbshell
.PHONY: db-shell
.PHONY: shell-db
db-reset: FLUSH_ARGS ?=
db-reset: ## flush database
reset-db: FLUSH_ARGS ?=
reset-db: ## flush database
@echo "$(BOLD)Flush database$(RESET)"
@$(MANAGE_DB) flush $(FLUSH_ARGS)
.PHONY: db-reset
.PHONY: reset-db
db-reset-full: build ## flush database, including schema
reset-db-full: build ## flush database, including schema
@echo "$(BOLD)Flush database$(RESET)"
$(MANAGE_DB) drop_all_tables
$(MANAGE_DB) migrate
.PHONY: db-reset-full
.PHONY: reset-db-full
env.d/development/%.local:
@echo "# Local development overrides for $(notdir $*)" > $@
@@ -470,39 +467,32 @@ env.d/development/%.local:
# -- Internationalization
crowdin-download: ## Download translated message from crowdin
i18n-download: ## Download translated messages
@$(COMPOSE_RUN_CROWDIN) download -c crowdin/config.yml
.PHONY: crowdin-download
.PHONY: i18n-download
crowdin-download-sources: ## Download sources from Crowdin
i18n-download-sources: ## Download translation sources
@$(COMPOSE_RUN_CROWDIN) download sources -c crowdin/config.yml
.PHONY: crowdin-download-sources
.PHONY: i18n-download-sources
crowdin-upload: ## Upload source translations to crowdin
i18n-upload: ## Upload source translations
@$(COMPOSE_RUN_CROWDIN) upload sources -c crowdin/config.yml
.PHONY: crowdin-upload
.PHONY: i18n-upload
i18n-compile: ## compile all translations
i18n-compile: \
back-i18n-compile
.PHONY: i18n-compile
i18n-generate: ## create the .pot files and extract frontend messages
i18n-generate: ## extract frontend messages for translation
i18n-generate: \
back-i18n-generate \
front-i18n-generate
i18n-generate-front
.PHONY: i18n-generate
i18n-download-and-compile: ## download all translated messages and compile them to be used by all applications
i18n-download-and-compile: ## download all translated messages to be used by all applications
i18n-download-and-compile: \
crowdin-download \
i18n-compile
i18n-download
.PHONY: i18n-download-and-compile
i18n-generate-and-upload: ## generate source translations for all applications and upload them to Crowdin
i18n-generate-and-upload: \
i18n-generate \
crowdin-upload
i18n-upload
.PHONY: i18n-generate-and-upload
# -- Release
@@ -519,9 +509,9 @@ clean-media: ## remove all media files
rm -rf data/media/*
.PHONY: clean-media
pyclean: ## remove all python cache files
clean-cache: ## remove all python cache files
find . | grep -E "\(/__pycache__$|\.pyc$|\.pyo$\)" | xargs rm -rf
.PHONY: pyclean
.PHONY: clean-cache
help:
@echo "$(BOLD)messages Makefile"
@@ -529,45 +519,45 @@ help:
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(firstword $(MAKEFILE_LIST)) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(GREEN)%-30s$(RESET) %s\n", $$1, $$2}'
.PHONY: help
front-shell: ## open a shell in the frontend container
shell-front: ## open a shell in the frontend container
@$(COMPOSE) run --rm --build frontend-tools /bin/sh
.PHONY: front-shell
.PHONY: shell-front
# Front
front-install: ## install the frontend locally
install-front: ## install the frontend locally
@args="$(filter-out $@,$(MAKECMDGOALS))" && \
$(COMPOSE) run --rm --build frontend-tools npm install $${args:-${1}}
.PHONY: front-install
.PHONY: install-front
front-install-frozen: ## install the frontend locally, following the frozen lockfile
install-frozen-front: ## install the frontend locally, following the frozen lockfile
@echo "Installing frontend dependencies, this might take a few minutes..."
@$(COMPOSE) run --rm --build frontend-tools npm ci
.PHONY: front-install-frozen
.PHONY: install-frozen-front
front-install-frozen-amd64: ## install the frontend locally, following the frozen lockfile
install-frozen-front-amd64: ## install the frontend locally, following the frozen lockfile
@$(COMPOSE) run --rm --build frontend-tools-amd64 npm ci
.PHONY: front-install-frozen-amd64
.PHONY: install-frozen-front-amd64
front-build: ## build the frontend locally
build-front: ## build the frontend locally
@$(COMPOSE) run --rm --build frontend-tools npm run build
.PHONY: front-build
.PHONY: build-front
front-i18n-generate: ## Extract the frontend translation inside a json to be used for crowdin
i18n-generate-front: ## Extract the frontend translation inside a json to be used for crowdin
@$(COMPOSE) run --rm --build frontend-tools npm run i18n:extract
.PHONY: front-i18n-extract
.PHONY: i18n-generate-front
back-api-update: ## Update the OpenAPI schema
api-update-back: ## Update the OpenAPI schema
bin/update_openapi_schema
.PHONY: back-api-update
.PHONY: api-update-back
front-api-update: ## Update the frontend API client
api-update-front: ## Update the frontend API client
@$(COMPOSE) run --rm --build frontend-tools npm run api:update
.PHONY: front-api-update
.PHONY: api-update-front
api-update: ## Update the OpenAPI schema then frontend API client
api-update: \
back-api-update \
front-api-update
api-update-back \
api-update-front
.PHONY: api-update
search-index: ## Create and/or reindex opensearch data
@@ -575,11 +565,10 @@ search-index: ## Create and/or reindex opensearch data
@$(MANAGE) search_reindex --all
.PHONY: search-index
mta-in-poetry-lock: ## lock the dependencies
@$(COMPOSE) run --rm --build mta-in-poetry poetry lock
.PHONY: mta-in-poetry-lock
mta-out-poetry-lock: ## lock the dependencies
@$(COMPOSE) run --rm --build mta-out-poetry poetry lock
.PHONY: mta-out-poetry-lock
deps-lock-mta-in: ## lock the dependencies
@$(COMPOSE) run --rm --build mta-in-uv uv lock
.PHONY: deps-lock-mta-in
deps-lock-mta-out: ## lock the dependencies
@$(COMPOSE) run --rm --build mta-out-uv uv lock
.PHONY: deps-lock-mta-out
+3 -3
View File
@@ -166,13 +166,13 @@ $ make api-update
You can also generate the schema only with:
```bash
$ make back-api-update
$ make api-update-back
```
And the frontend API client only with:
```bash
$ make front-api-update
$ make api-update-front
```
### Sending test emails 📨
@@ -198,7 +198,7 @@ swaks --to=user1@example.local --server localhost:8917
swaks -tls --to=test@example.external --server localhost:8911 --auth-user user --auth-password=pass
# You can also send emails using Messages itself instead of the frontend
make back-shell
make shell-back
MTA_OUT_MODE=relay MTA_OUT_RELAY_HOST=mailcatcher:1025 python manage.py send_mail --to=user1@example.local --subject="Test" --body="Hello World"
```
+9 -11
View File
@@ -5,21 +5,19 @@ set -o pipefail # don't ignore exit codes when piping output
echo "-----> Running post-frontend script"
# Move the frontend build to the nginx root and clean up
# Move the frontend build to the app root and clean up
mkdir -p build/
mv src/frontend/out build/frontend-out
mv src/backend/* ./
mkdir -p messages_backend && touch messages_backend/__init__.py
# Replace ${MESSAGES_FRONTEND_BACKEND_SERVER} to the erb equivalent with the "localhost:8000" default value
# Replace ${DJANGO_ADMIN_URL} to the erb equivalent with the "admin" default value
# Replace ${MESSAGES_FRONTEND_ROOT} with the actual frontend root for scalingo
# Replace every other shell variable to its erb equivalent
sed \
-e 's/\${MESSAGES_FRONTEND_BACKEND_SERVER}/<%= ENV["MESSAGES_FRONTEND_BACKEND_SERVER"] || "localhost:8000" %>/g' \
-e 's/\${DJANGO_ADMIN_URL}/<%= ENV["DJANGO_ADMIN_URL"] || "admin" %>/g' \
-e 's|\${MESSAGES_FRONTEND_ROOT}|/app/build/frontend-out|g' \
-e 's/\${\([A-Z_][A-Z0-9_]*\)}/<%= ENV["\1"] %>/g' \
src/frontend/nginx/nginx.conf.template > ./servers.conf.erb
# Download Caddy binary
CADDY_VERSION="2.9.1"
curl -fsSL "https://github.com/caddyserver/caddy/releases/download/v${CADDY_VERSION}/caddy_${CADDY_VERSION}_linux_amd64.tar.gz" | tar -xz -C bin/ caddy
chmod +x bin/caddy
# Copy Caddyfile (uses {$ENV} vars natively, no ERB needed)
cp src/frontend/caddy/Caddyfile ./Caddyfile
echo "3.13" > .python-version
+7 -2
View File
@@ -1,10 +1,15 @@
#!/bin/bash
# Set defaults for Caddy env vars
export MESSAGES_FRONTEND_ROOT="${MESSAGES_FRONTEND_ROOT:-/app/build/frontend-out}"
export MESSAGES_FRONTEND_BACKEND_SERVER="${MESSAGES_FRONTEND_BACKEND_SERVER:-localhost:8000}"
export DJANGO_ADMIN_URL="${DJANGO_ADMIN_URL:-admin}"
# Start the Django backend server
gunicorn -b :8000 messages.wsgi:application --log-file - &
# Start the Nginx server
bin/run &
# Start the Caddy server
bin/caddy run --config Caddyfile --adapter caddyfile &
# if the current shell is killed, also terminate all its children
trap "pkill SIGTERM -P $$" SIGTERM
+28 -29
View File
@@ -53,36 +53,32 @@ services:
objectstorage:
# user: ${DOCKER_USER:-1000}
image: minio/minio
image: rustfs/rustfs:1.0.0-alpha.83
environment:
- MINIO_ROOT_USER=st-messages
- MINIO_ROOT_PASSWORD=password
- RUSTFS_ACCESS_KEY=st-messages
- RUSTFS_SECRET_KEY=password
- RUSTFS_CONSOLE_ENABLE=true
- RUSTFS_ADDRESS=0.0.0.0:9000
- RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001
- RUSTFS_CORS_ALLOWED_ORIGINS=*
- RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=*
ports:
- "8906:9000"
- "8907:9001"
healthcheck:
test: ["CMD", "mc", "ready", "local"]
test:
[
"CMD",
"sh",
"-c",
"curl -f http://127.0.0.1:9000/health && curl -f http://127.0.0.1:9001/rustfs/console/health",
]
interval: 1s
timeout: 20s
retries: 300
entrypoint: ""
command: minio server --console-address :9001 /data
timeout: 5s
retries: 60
start_period: 0s
volumes:
- ./data/media:/data
objectstorage-createbucket:
image: minio/mc
depends_on:
objectstorage:
condition: service_healthy
environment:
- MC_HOST=http://objectstorage:9000
entrypoint: >
sh -c "
/usr/bin/mc alias set st-messages $${MC_HOST} st-messages password && \
/usr/bin/mc mb st-messages/msg-imports --ignore-existing && \
/usr/bin/mc ilm rule rm --all --force st-messages/msg-imports || true && \
/usr/bin/mc ilm rule add --expire-days 1 st-messages/msg-imports"
- objectstorage-data:/data
backend-base:
build:
@@ -141,14 +137,14 @@ services:
condition: service_healthy
# restart: true
backend-poetry:
backend-uv:
profiles:
- tools
volumes:
- ./src/backend:/app
build:
context: src/backend/
target: poetry
target: uv
pull_policy: build
worker-dev:
@@ -262,14 +258,14 @@ services:
volumes:
- ./src/mta-in:/app
mta-in-poetry:
mta-in-uv:
profiles:
- tools
volumes:
- ./src/mta-in:/app
build:
context: src/mta-in
target: poetry
target: uv
pull_policy: build
mta-out:
@@ -326,14 +322,14 @@ services:
socks-proxy:
condition: service_started
mta-out-poetry:
mta-out-uv:
profiles:
- tools
volumes:
- ./src/mta-out:/app
build:
context: src/mta-out
target: poetry
target: uv
pull_policy: build
keycloak:
@@ -389,3 +385,6 @@ services:
condition: service_started
redis:
condition: service_started
volumes:
objectstorage-data:
-5
View File
@@ -15,11 +15,6 @@ preserve_hierarchy: true
# Files configuration
#
files: [
{
source : "/backend/locale/django.pot",
dest: "/backend.pot",
translation : "/backend/locale/%locale_with_underscore%/LC_MESSAGES/django.po"
},
{
source: "/frontend/public/locales/common/en-US.json",
dest: "/frontend-common.json",
+1 -1
View File
@@ -124,5 +124,5 @@ The system is designed for containerized deployment with:
- **Docker containers** for all services
- **Environment-specific configurations** (dev, staging, production)
- **Horizontal scaling** capability for backend and Celery workers
- **Load balancing** support via nginx reverse proxy
- **Load balancing** support via Caddy reverse proxy
- **Health checks** and monitoring integration (In Progress)
+1 -1
View File
@@ -212,5 +212,5 @@ The exporter has a comprehensive test suite in `src/features/blocknote/email-exp
Run with:
```bash
make front-test -- src/features/blocknote/email-exporter/index.test.tsx
make test-front -- src/features/blocknote/email-exporter/index.test.tsx
```
+1 -1
View File
@@ -201,7 +201,7 @@ For production deployment, create your own Docker Compose configuration based on
- Configure SSL/TLS certificates
- Set up persistent volumes for databases
- Implement proper restart policies
- Configure reverse proxy (nginx) for SSL termination
- Configure reverse proxy (Caddy) for SSL termination
## Security Considerations
+4 -4
View File
@@ -14,11 +14,11 @@ OIDC_OP_AUTHORIZATION_ENDPOINT=http://keycloak:8802/realms/messages/protocol/ope
OIDC_OP_TOKEN_ENDPOINT=http://keycloak:8802/realms/messages/protocol/openid-connect/token
OIDC_OP_USER_ENDPOINT=http://keycloak:8802/realms/messages/protocol/openid-connect/userinfo
OIDC_REDIRECT_ALLOWED_HOSTS=["http://keycloak:8802", "http://nginx"]
OIDC_REDIRECT_ALLOWED_HOSTS=["http://keycloak:8802", "http://proxy"]
LOGIN_REDIRECT_URL=http://nginx
LOGIN_REDIRECT_URL_FAILURE=http://nginx
LOGOUT_REDIRECT_URL=http://nginx
LOGIN_REDIRECT_URL=http://proxy
LOGIN_REDIRECT_URL_FAILURE=http://proxy
LOGOUT_REDIRECT_URL=http://proxy
# Keycloak
KEYCLOAK_URL=http://keycloak:8802
+1 -1
View File
@@ -1,4 +1,4 @@
NEXT_PUBLIC_API_ORIGIN=http://nginx
NEXT_PUBLIC_API_ORIGIN=http://proxy
NEXT_PUBLIC_FEEDBACK_WIDGET_API_URL=
NEXT_PUBLIC_FEEDBACK_WIDGET_PATH=
NEXT_PUBLIC_FEEDBACK_WIDGET_CHANNEL=
+4 -6
View File
@@ -3,7 +3,7 @@
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code
extension-pkg-whitelist=
extension-pkg-whitelist=pypff
# Add files or directories to the blacklist. They should be base names, not
# paths.
@@ -23,7 +23,7 @@ jobs=0
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=pylint_django,pylint.extensions.no_self_use
load-plugins=pylint_django
# Pickle collected data for later comparisons.
persistent=yes
@@ -33,8 +33,6 @@ persistent=yes
# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages
suggestion-mode=yes
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
@@ -60,11 +58,11 @@ disable=bad-inline-option,
django-not-configured,
file-ignored,
locally-disabled,
no-self-use,
raw-checker-failed,
suppressed-message,
useless-suppression,
fixme
fixme,
cyclic-import
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
+110 -32
View File
@@ -1,8 +1,8 @@
# https://hub.docker.com/_/python
FROM python:3.13.7-slim-trixie AS base
# ---- Base OS ----
FROM debian:trixie-slim AS base
# Bump this to force an update of the apt repositories
ENV MIN_UPDATE_DATE="2025-12-01"
ENV MIN_UPDATE_DATE="2026-02-20"
RUN <<EOR
apt-get update
@@ -14,48 +14,52 @@ ENV PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
WORKDIR /app
# ---- Poetry package manager and dev system deps ----
FROM base AS poetry
# ---- uv + managed Python + build system deps ----
FROM base AS uv
# Pin uv by SHA256 digest for supply chain security
# Verify with: gh attestation verify --owner astral-sh oci://ghcr.io/astral-sh/uv:0.10.4
# https://github.com/astral-sh/uv/pkgs/container/uv/691717444?tag=0.10.4
COPY --from=ghcr.io/astral-sh/uv@sha256:4cac394b6b72846f8a85a7a0e577c6d61d4e17fe2ccee65d9451a8b3c9efb4ac /uv /uvx /bin/
RUN <<EOR
apt-get update
DEBIAN_FRONTEND="noninteractive" apt-get install -y --no-install-recommends \
ca-certificates \
curl \
git \
rdfind \
libmagic1 \
build-essential \
python3-dev \
zlib1g-dev
rm -rf /var/lib/apt/lists/*
EOR
ENV POETRY_NO_INTERACTION=1
ENV POETRY_VIRTUALENVS_CREATE=0
ENV POETRY_VIRTUALENVS_OPTIONS_NO_PIP=1
ENV POETRY_VERSION=2.1.4
ENV UV_COMPILE_BYTECODE=1
ENV UV_LINK_MODE=copy
ENV UV_PYTHON_PREFERENCE=only-managed
ENV UV_PYTHON_INSTALL_DIR=/opt/python
ENV UV_PROJECT_ENVIRONMENT=/venv
RUN python -m pip install poetry==${POETRY_VERSION}
# Install Python via uv — integrity verified against SHA256 checksums embedded in the uv binary.
# Uses python-build-standalone: most C deps (ssl, ffi, sqlite, zlib, lzma, bz2) are statically linked.
RUN uv python install 3.14.3
# ---- Base image with dependencies installed ----
FROM poetry AS base-with-deps
# ---- Production dependencies ----
FROM uv AS base-with-deps
COPY pyproject.toml poetry.lock ./
COPY pyproject.toml uv.lock ./
# Create a runtime virtual environment and activate it
RUN python -m venv /venv
ENV PATH="/venv/bin:$PATH"
ENV VIRTUAL_ENV=/venv
ENV VIRTUAL_ENV_PROMPT=venv
# Install dependencies in this new virtual environment
RUN poetry install --compile
RUN --mount=type=cache,target=/root/.cache/uv uv sync --frozen --no-install-project --no-editable --exact --no-dev
# ---- Base image with dependencies installed for development ----
# ---- Development dependencies ----
FROM base-with-deps AS base-with-deps-dev
RUN poetry install --compile --extras dev
RUN --mount=type=cache,target=/root/.cache/uv uv sync --frozen --no-install-project --no-editable --all-extras
# ---- static link collector ----
# ---- Static file collector ----
FROM base-with-deps AS link-collector
ARG MESSAGES_STATIC_ROOT=/data/static
@@ -71,6 +75,25 @@ DJANGO_CONFIGURATION=Build python manage.py collectstatic --noinput
rdfind -makesymlinks true -followsymlinks true -makeresultsfile false ${MESSAGES_STATIC_ROOT}
EOR
# ---- Strip Python for production (remove pip, idle, tkinter, tcl, headers, tests) ----
FROM uv AS python-runtime
RUN <<EOR
set -e
PYDIR=$(dirname $(dirname $(uv python find 3.14.3)))
rm -rf \
$PYDIR/bin/idle* $PYDIR/bin/pip* $PYDIR/bin/pydoc* $PYDIR/bin/*-config \
$PYDIR/include $PYDIR/share \
$PYDIR/lib/pkgconfig $PYDIR/lib/itcl* $PYDIR/lib/libtcl* \
$PYDIR/lib/tcl* $PYDIR/lib/tk* $PYDIR/lib/thread* \
$PYDIR/lib/python3.14/idlelib \
$PYDIR/lib/python3.14/ensurepip \
$PYDIR/lib/python3.14/tkinter \
$PYDIR/lib/python3.14/turtledemo \
$PYDIR/lib/python3.14/lib-dynload/_tkinter* \
$PYDIR/lib/python3.14/lib-dynload/_ctypes_test* \
/opt/python/.gitignore /opt/python/.lock /opt/python/.temp
EOR
# ---- Base runtime image ----
FROM base AS runtime-base
@@ -83,7 +106,6 @@ RUN chmod g=u /etc/passwd
RUN <<EOR
apt-get update
DEBIAN_FRONTEND="noninteractive" apt-get install -y --no-install-recommends \
gettext \
libmagic1
rm -rf /var/lib/apt/lists/*
EOR
@@ -103,32 +125,88 @@ COPY ./entrypoint /usr/local/bin/entrypoint
ENTRYPOINT [ "/usr/local/bin/entrypoint" ]
# ---- Base runtime image for development ----
# ---- Development runtime ----
FROM runtime-base AS runtime-dev
# Full Python installation (with headers, pip — useful for debugging)
COPY --from=uv /opt/python /opt/python
COPY --from=base-with-deps-dev /venv /venv
# /app will be mounted as a volume in the development container
# Target database host (e.g. database engine following docker compose services
# name) & port
# ENV DB_HOST=postgresql \
# DB_PORT=5432
# Run django development server
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
# ---- Base runtime image for production ----
# ---- Production application source (strip tests, dev tooling, build files) ----
FROM base AS app-prod
COPY . /app/
RUN rm -rf \
/app/core/tests \
/app/core/factories.py \
/app/e2e \
/app/Dockerfile \
/app/.pylintrc \
/app/pyproject.toml \
/app/uv.lock \
/app/README.md \
/app/entrypoint
# ---- Production runtime ----
FROM runtime-base AS runtime-prod
ARG MESSAGES_STATIC_ROOT=/data/static
COPY --from=python-runtime /opt/python /opt/python
COPY --from=base-with-deps /venv /venv
COPY --from=link-collector ${MESSAGES_STATIC_ROOT} ${MESSAGES_STATIC_ROOT}
COPY . /app/
COPY --from=app-prod /app/ /app/
# The default command runs gunicorn WSGI server in messages's main module
CMD ["gunicorn", "-c", "/app/gunicorn.conf.py", "messages.wsgi:application"]
HEALTHCHECK --interval=30s --timeout=2s --start-period=30s \
CMD ["python", "-c", "import os; from urllib.request import Request, urlopen; urlopen(Request('http://localhost:8000/__heartbeat__/', headers={'Host': os.getenv('DJANGO_ALLOWED_HOSTS', 'localhost').split(',')[0].strip(), 'X-Forwarded-Proto': 'https'}), timeout=2)"]
# ---- Collect shared libraries for distroless ----
# libmagic + transitive deps (liblzma, libbz2) are the only shared libs needed.
# Python's own deps (ffi, sqlite, ssl, zlib) are statically linked in python-build-standalone.
FROM uv AS shared-libs
RUN mkdir -p /shared-libs/usr/lib /shared-libs/usr/share/misc && \
for lib in libmagic liblzma libbz2; do \
cp -L /usr/lib/*/${lib}.so* /shared-libs/usr/lib/; \
done && \
cp /usr/share/misc/magic.mgc /shared-libs/usr/share/misc/
# ---- Distroless production runtime ----
# Uses cc-debian13 (C runtime only) + python-build-standalone from uv.
# Debug with: docker run --entrypoint='' gcr.io/distroless/cc-debian13:debug-nonroot sh
FROM gcr.io/distroless/cc-debian13:nonroot AS runtime-distroless-prod
ARG MESSAGES_STATIC_ROOT=/data/static
WORKDIR /app
# Stripped Python installation (python-build-standalone via uv)
COPY --from=python-runtime /opt/python /opt/python
# Python dependencies
COPY --from=base-with-deps /venv /venv
# libmagic shared library + magic database
COPY --from=shared-libs /shared-libs/ /
# Static files
COPY --from=link-collector ${MESSAGES_STATIC_ROOT} ${MESSAGES_STATIC_ROOT}
# Application code
COPY --from=app-prod /app/ /app/
ENV PATH="/venv/bin:$PATH"
ENV VIRTUAL_ENV=/venv
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
# Distroless has no ldconfig — tell the dynamic linker where to find libmagic
ENV LD_LIBRARY_PATH=/usr/lib
CMD ["gunicorn", "-c", "/app/gunicorn.conf.py", "messages.wsgi:application"]
HEALTHCHECK --interval=30s --timeout=2s --start-period=30s \
CMD ["python", "-c", "import os; from urllib.request import Request, urlopen; urlopen(Request('http://localhost:8000/__heartbeat__/', headers={'Host': os.getenv('DJANGO_ALLOWED_HOSTS', 'localhost').split(',')[0].strip(), 'X-Forwarded-Proto': 'https'}), timeout=2)"]
View File
+33 -53
View File
@@ -13,7 +13,6 @@ from django.template.response import TemplateResponse
from django.urls import path
from django.utils.html import escape, format_html
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from sentry_sdk import capture_exception
@@ -33,7 +32,7 @@ from .forms import IMAPImportForm, MessageImportForm
class RecipientDeliveryStatusFilter(admin.SimpleListFilter):
"""Filter messages by their recipients' delivery status."""
title = _("delivery status")
title = "delivery status"
parameter_name = "recipient_delivery_status"
def lookups(self, request, model_admin):
@@ -95,18 +94,12 @@ def retry_send_messages_action(__, request, queryset):
messages.info(
request,
_(
"%(message_count)d messages - "
"Retry send message task queued (id: %(task_id)s)."
)
% {
"message_count": len(message_ids),
"task_id": task.id,
},
f"{len(message_ids)} messages - "
f"Retry send message task queued (id: {task.id}).",
)
retry_send_messages_action.short_description = _(
retry_send_messages_action.short_description = (
"Retry to send selected messages to pending recipients"
)
@@ -127,7 +120,7 @@ class UserAdmin(auth_admin.UserAdmin):
},
),
(
_("Personal info"),
"Personal info",
{
"fields": (
"sub",
@@ -140,7 +133,7 @@ class UserAdmin(auth_admin.UserAdmin):
},
),
(
_("Permissions"),
"Permissions",
{
"fields": (
"is_active",
@@ -151,7 +144,7 @@ class UserAdmin(auth_admin.UserAdmin):
),
},
),
(_("Important dates"), {"fields": ("created_at", "updated_at")}),
("Important dates", {"fields": ("created_at", "updated_at")}),
)
add_fieldsets = (
(
@@ -216,12 +209,12 @@ class MailDomainAdmin(admin.ModelAdmin):
readonly_fields = ("throttle_status_display",)
change_form_template = "admin/core/maildomain/change_form.html"
@admin.display(description=_("Throttle Status (External Recipients)"))
@admin.display(description="Throttle Status (External Recipients)")
def throttle_status_display(self, obj):
"""Display current throttle usage for this maildomain."""
status = get_throttle_status(maildomain=obj)
if "maildomain" not in status:
return _("No throttle configured")
return "No throttle configured"
info = status["maildomain"]
return format_html(
@@ -251,7 +244,7 @@ class MailDomainAdmin(admin.ModelAdmin):
maildomain = self.get_object(request, object_id)
if maildomain is None:
messages.error(request, _("Mail domain not found."))
messages.error(request, "Mail domain not found.")
return redirect("..")
# Run DNS provisioning
@@ -264,22 +257,18 @@ class MailDomainAdmin(admin.ModelAdmin):
changes_text = ", ".join(changes)
messages.success(
request,
_("DNS provisioning successful via %(provider)s: %(changes)s")
% {"provider": provider_used, "changes": changes_text},
f"DNS provisioning successful via {provider_used}: {changes_text}",
)
else:
messages.success(
request,
_(
"DNS provisioning successful via %(provider)s (no changes needed)."
)
% {"provider": provider_used},
f"DNS provisioning successful via {provider_used} (no changes needed).",
)
else:
error_msg = results.get("error", "Unknown error")
messages.error(
request,
_("DNS provisioning failed: %(error)s") % {"error": error_msg},
f"DNS provisioning failed: {error_msg}",
)
return redirect("..")
@@ -332,7 +321,7 @@ class MailboxAdmin(admin.ModelAdmin):
mailbox_obj = self.get_object(request, object_id)
if mailbox_obj is None:
messages.error(request, _("Mailbox not found."))
messages.error(request, "Mailbox not found.")
return redirect("..")
# Start the export task
@@ -345,23 +334,20 @@ class MailboxAdmin(admin.ModelAdmin):
)
capture_exception()
messages.error(
request, _("Failed to queue export task. Please try again later.")
request, "Failed to queue export task. Please try again later."
)
return redirect("..")
messages.success(
request,
_(
"Export task has been queued for mailbox %(mailbox)s. "
"You will receive a message with the download link when the export "
"is complete (task id: %(task_id)s)."
)
% {"mailbox": mailbox_obj, "task_id": task.id},
f"Export task has been queued for mailbox {mailbox_obj}. "
f"You will receive a message with the download link when the export "
f"is complete (task id: {task.id}).",
)
return redirect("..")
@admin.display(description=_("Throttle Status (External Recipients)"))
@admin.display(description="Throttle Status (External Recipients)")
def throttle_status_display(self, obj):
"""Display current throttle usage for this mailbox and its domain."""
status = get_throttle_status(mailbox=obj, maildomain=obj.domain)
@@ -390,7 +376,7 @@ class MailboxAdmin(admin.ModelAdmin):
)
)
return format_html("<br>".join(parts)) if parts else _("No throttle configured")
return format_html("<br>".join(parts)) if parts else "No throttle configured"
@admin.register(models.Channel)
@@ -466,7 +452,7 @@ class ThreadAdmin(admin.ModelAdmin):
fieldsets = (
(None, {"fields": ("subject", "snippet", "display_labels", "summary")}),
(
_("Statistics"),
"Statistics",
{
"fields": (
"has_unread",
@@ -486,7 +472,7 @@ class ThreadAdmin(admin.ModelAdmin):
},
),
(
_("Metadata"),
"Metadata",
{
"fields": ("sender_names", "created_at", "updated_at", "messaged_at"),
"classes": ("collapse",),
@@ -517,13 +503,13 @@ class ThreadAdmin(admin.ModelAdmin):
"""Return a comma-separated list of labels for the thread."""
return ", ".join(label.name for label in obj.labels.all())
get_labels.short_description = _("Labels")
get_labels.short_description = "Labels"
get_labels.admin_order_field = "labels__name"
def display_labels(self, obj):
"""Display labels with their colors in the detail view."""
if not obj.labels.exists():
return _("No labels")
return "No labels"
# Create a list of formatted label spans
label_spans = []
@@ -540,7 +526,7 @@ class ThreadAdmin(admin.ModelAdmin):
# Join all spans with a space using format_html
return format_html(" ".join(label_spans))
display_labels.short_description = _("Labels")
display_labels.short_description = "Labels"
class MessageRecipientInline(admin.TabularInline):
@@ -674,7 +660,7 @@ class MessageAdmin(admin.ModelAdmin):
context = dict(
self.admin_site.each_context(request),
title=_("Import Messages"),
title="Import Messages",
form=form,
opts=self.model._meta, # noqa: SLF001
)
@@ -704,7 +690,7 @@ class MessageAdmin(admin.ModelAdmin):
context = dict(
self.admin_site.each_context(request),
title=_("Import Messages from IMAP"),
title="Import Messages from IMAP",
form=form,
opts=self.model._meta, # noqa: SLF001
)
@@ -751,7 +737,7 @@ class MessageAdmin(admin.ModelAdmin):
message = self.get_object(request, object_id)
if message is None:
messages.error(request, _("Message not found."))
messages.error(request, "Message not found.")
return redirect("..")
# Check if message has recipients with retry status
@@ -763,7 +749,7 @@ class MessageAdmin(admin.ModelAdmin):
if retryable_recipients_count == 0:
messages.warning(
request,
_("No pending recipients found for this message."),
"No pending recipients found for this message.",
)
return redirect("..")
@@ -772,14 +758,8 @@ class MessageAdmin(admin.ModelAdmin):
messages.success(
request,
_(
"Retry task has been queued for "
"%(retryable_recipients_count)d pending recipient(s) (id: %(task_id)s)."
)
% {
"retryable_recipients_count": retryable_recipients_count,
"task_id": task.id,
},
f"Retry task has been queued for "
f"{retryable_recipients_count} pending recipient(s) (id: {task.id}).",
)
return redirect("..")
@@ -931,7 +911,7 @@ class DKIMKeyAdmin(admin.ModelAdmin):
},
),
(
_("Keys"),
"Keys",
{
"fields": ("public_key",),
"classes": ("collapse",),
@@ -966,7 +946,7 @@ class InboundMessageAdmin(admin.ModelAdmin):
return bool(obj.error_message)
has_error.boolean = True
has_error.short_description = _("Error")
has_error.short_description = "Error"
def get_queryset(self, request):
"""Optimize queryset with select_related for better performance."""
+2
View File
@@ -1,3 +1,5 @@
"""AI-powered label assignment for threads."""
from core.ai.thread_classifier import get_most_relevant_labels
from core.models import Label, Thread
+6 -6
View File
@@ -4,7 +4,7 @@ import json
from pathlib import Path
from django.conf import settings
from django.utils import timezone, translation
from django.utils import timezone
from core.ai.utils import get_messages_from_thread
from core.models import Thread
@@ -23,8 +23,7 @@ def get_most_relevant_labels(thread: Thread, labels: list) -> list[str]:
current_datetime = timezone.now().isoformat()
# Determine the active or fallback language
active_language = translation.get_language() or settings.LANGUAGE_CODE
active_language = settings.LANGUAGE_CODE
# Extract messages from the thread
messages = get_messages_from_thread(thread)
@@ -35,8 +34,10 @@ def get_most_relevant_labels(thread: Thread, labels: list) -> list[str]:
with open(prompts_path, encoding="utf-8") as f:
prompts = json.load(f)
# Get the prompt for the active language
prompt_template = prompts.get(active_language)
# Get the prompt for the active language, fallback to en-us
prompt_template = prompts.get(active_language) or prompts.get("en-us")
if prompt_template is None:
raise ValueError(f"No AI prompt template for language '{active_language}'")
prompt_query = prompt_template["autolabels_query"]
prompt = prompt_query.format(
messages=messages_as_text,
@@ -45,7 +46,6 @@ def get_most_relevant_labels(thread: Thread, labels: list) -> list[str]:
language=active_language,
)
with translation.override(active_language):
best_labels = AIService().call_ai_api(prompt)
# Get rid of surrounding text if present
+8 -7
View File
@@ -1,8 +1,9 @@
"""AI-powered thread summarization."""
import json
from pathlib import Path
from django.conf import settings
from django.utils import translation
from core.ai.utils import get_messages_from_thread
from core.models import Thread
@@ -10,10 +11,9 @@ from core.services.ai_service import AIService
def summarize_thread(thread: Thread) -> str:
"""Summarizes a thread using the OpenAI client based on the active Django language."""
"""Summarizes a thread using the OpenAI client based on the configured language."""
# Determine the active or fallback language
active_language = translation.get_language() or settings.LANGUAGE_CODE
active_language = settings.LANGUAGE_CODE
# Extract messages from the thread
messages = get_messages_from_thread(thread)
@@ -24,12 +24,13 @@ def summarize_thread(thread: Thread) -> str:
with open(prompts_path, encoding="utf-8") as f:
prompts = json.load(f)
# Get the prompt for the active language
prompt_template = prompts.get(active_language)
# Get the prompt for the active language, fallback to en-us
prompt_template = prompts.get(active_language) or prompts.get("en-us")
if prompt_template is None:
raise ValueError(f"No AI prompt template for language '{active_language}'")
prompt_query = prompt_template["summary_query"]
prompt = prompt_query.format(messages=messages_as_text, language=active_language)
with translation.override(active_language):
summary = AIService().call_ai_api(prompt)
return summary
+3 -1
View File
@@ -1,3 +1,5 @@
"""Utility functions for AI features."""
from typing import List
from django.conf import settings
@@ -20,7 +22,7 @@ def get_messages_from_thread(thread: Thread) -> List[Message]:
def is_ai_enabled() -> bool:
"""Check if AI features are enabled based on the presence of required settings in the environment (API_KEY, BASE_URL, MODEL)"""
"""Check if AI features are enabled based on required settings."""
return all(
[
settings.AI_API_KEY,
+1 -2
View File
@@ -4,7 +4,6 @@ import logging
from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from rest_framework import exceptions as drf_exceptions
from rest_framework import views as drf_views
@@ -32,7 +31,7 @@ def exception_handler(exc, context):
exc.retry_after,
)
response = Response(
{"detail": _("Sending limit reached. Please wait before trying again.")},
{"detail": "Sending limit reached. Please wait before trying again."},
status=429,
)
response["Retry-After"] = exc.retry_after
+5 -6
View File
@@ -8,7 +8,6 @@ import uuid
from django.conf import settings
from django.db import transaction
from django.db.models import Count, Exists, OuterRef, Q
from django.utils.translation import gettext_lazy as _
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
@@ -1143,7 +1142,7 @@ class MailboxAdminSerializer(serializers.ModelSerializer):
if metadata.get("type") == "personal" and not domain.identity_sync:
raise serializers.ValidationError(
{
"identity_sync": _(
"identity_sync": (
"Personal mailboxes cannot be created when "
"identity synchronization is disabled."
)
@@ -1159,7 +1158,7 @@ class MailboxAdminSerializer(serializers.ModelSerializer):
if any(lower_value == prefix.lower() for prefix in denylist):
raise serializers.ValidationError(
{
"local_part_denied": _(
"local_part_denied": (
"This prefix is not allowed for personal mailboxes."
)
}
@@ -1173,7 +1172,7 @@ class MailboxAdminSerializer(serializers.ModelSerializer):
domain=self.context.get("domain"), local_part=value
).exists():
raise serializers.ValidationError(
_("A mailbox with this local part already exists in this domain.")
"A mailbox with this local part already exists in this domain."
)
return value
@@ -1645,7 +1644,7 @@ class MessageTemplateSerializer(serializers.ModelSerializer):
image_mb = image["size"] / (1024 * 1024)
raise serializers.ValidationError(
{
"html_body": _(
"html_body": (
'Image "%(name)s" (%(size)s MB) exceeds'
" the %(max)s MB limit."
)
@@ -1661,7 +1660,7 @@ class MessageTemplateSerializer(serializers.ModelSerializer):
total_mb = total_image_size / (1024 * 1024)
raise serializers.ValidationError(
{
"html_body": _(
"html_body": (
"Total attachment size (%(total_size)s MB) exceeds the %(max_size)s MB limit. "
"Please remove or reduce attachments."
)
+3 -4
View File
@@ -2,7 +2,6 @@
from django.conf import settings
from django.db.models import F
from django.utils import translation
from drf_spectacular.utils import extend_schema
from rest_framework import permissions
@@ -48,10 +47,10 @@ class PlaceholderView(APIView):
)
def get(self, request):
"""Get the structure of available fields."""
current_language = translation.get_language().split("-")[0]
current_language = settings.LANGUAGE_CODE.split("-")[0]
fields = {
"name": translation.gettext_lazy("Name"),
"recipient_name": translation.gettext_lazy("Recipient name"),
"name": "Name",
"recipient_name": "Recipient name",
}
# Add user custom attributes fields from schema
schema = settings.SCHEMA_CUSTOM_ATTRIBUTES_USER
+1 -2
View File
@@ -1,7 +1,6 @@
"""Messages Core application"""
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class CoreConfig(AppConfig):
@@ -9,7 +8,7 @@ class CoreConfig(AppConfig):
name = "core"
app_label = "core"
verbose_name = _("messages core application")
verbose_name = "messages core application"
def ready(self):
"""Register signal handlers and prometheus collector when the app is ready."""
+1 -2
View File
@@ -5,7 +5,6 @@ import re
from django.conf import settings
from django.core.exceptions import SuspiciousOperation
from django.utils.translation import gettext_lazy as _
from lasuite.oidc_login.backends import (
OIDCAuthenticationBackend as LaSuiteOIDCAuthenticationBackend,
@@ -74,7 +73,7 @@ class OIDCAuthenticationBackend(LaSuiteOIDCAuthenticationBackend):
if user:
if not user.is_active:
raise SuspiciousOperation(_("User account is disabled"))
raise SuspiciousOperation("User account is disabled")
self.update_user_if_needed(user, claims)
elif self.should_create_user(email):
-1
View File
@@ -1,4 +1,3 @@
# ruff: noqa: S311
"""
Core application factories
"""
@@ -0,0 +1,61 @@
"""Management command to create storage buckets and configure lifecycle rules."""
from django.core.files.storage import storages
from django.core.management.base import BaseCommand
from botocore.exceptions import ClientError
class Command(BaseCommand):
"""Create a storage bucket and optionally set a lifecycle expiration rule."""
help = "Create a storage bucket and optionally set a lifecycle expiration rule"
def add_arguments(self, parser):
parser.add_argument(
"--storage",
help="Storage backend to create the bucket for",
choices=storages.backends.keys(),
required=True,
)
parser.add_argument(
"--expire-days",
type=int,
default=0,
help="Auto-expire objects after this many days (0 = no expiration)",
)
def handle(self, *args, **options):
storage = storages[options["storage"]]
s3_client = storage.connection.meta.client
bucket = storage.bucket_name
# Create the bucket if it doesn't exist
try:
s3_client.head_bucket(Bucket=bucket)
self.stdout.write(f"Bucket '{bucket}' already exists.")
except ClientError:
s3_client.create_bucket(Bucket=bucket)
self.stdout.write(self.style.SUCCESS(f"Bucket '{bucket}' created."))
# Set lifecycle expiration rule
expire_days = options["expire_days"]
if expire_days > 0:
s3_client.put_bucket_lifecycle_configuration(
Bucket=bucket,
LifecycleConfiguration={
"Rules": [
{
"ID": "auto-expire",
"Status": "Enabled",
"Expiration": {"Days": expire_days},
"Filter": {"Prefix": ""},
}
]
},
)
self.stdout.write(
self.style.SUCCESS(
f"Lifecycle rule set: objects expire after {expire_days} day(s)."
)
)
@@ -9,6 +9,8 @@ from core.services.dns.check import check_dns_records
class Command(BaseCommand):
"""Check DNS records for mail domains."""
help = "Check DNS records for mail domains"
def add_arguments(self, parser):
@@ -13,6 +13,8 @@ from core.services.dns.provisioning import (
class Command(BaseCommand):
"""Provision DNS records for mail domains."""
help = "Provision DNS records for mail domains"
def add_arguments(self, parser):
@@ -49,7 +51,6 @@ class Command(BaseCommand):
except MailDomain.DoesNotExist:
if domain_name:
raise CommandError(f"Domain '{domain_name}' not found") from None
else:
raise CommandError(f"Domain with ID {domain_id} not found") from None
if pretend:
@@ -96,9 +97,14 @@ class Command(BaseCommand):
else:
self.stdout.write("Provisioning DNS records...")
try:
results = provision_domain_dns(
maildomain, provider_name=provider_name, pretend=pretend
)
except Exception as e:
raise CommandError(
f"DNS provisioning failed for {maildomain.name}: {e}"
) from e
if results["success"]:
if pretend:
@@ -108,9 +114,7 @@ class Command(BaseCommand):
else:
self.stdout.write(self.style.SUCCESS("✓ DNS provisioning successful"))
# Show which provider was used
provider_used = results.get("provider", "unknown")
if provider_used:
self.stdout.write(f"Provider used: {provider_used}")
if results["changes"]:
@@ -1,3 +1,5 @@
"""Management command for Keycloak identity management."""
import logging
from django.core.management.base import BaseCommand, CommandError
@@ -14,7 +16,9 @@ logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Identity management commands for Keycloak integration"
"""Identity management commands for Keycloak integration."""
help = __doc__
def add_arguments(self, parser):
subparsers = parser.add_subparsers(dest="command", help="Available commands")
@@ -126,7 +130,7 @@ class Command(BaseCommand):
except ValueError as e:
raise CommandError(str(e)) from e
def resync_all(self, options):
def resync_all(self, _options):
"""Resync all mailboxes with identity_sync enabled to Keycloak."""
self.stdout.write(
self.style.SUCCESS("Starting resync of all mailboxes to Keycloak...")
@@ -1,3 +1,5 @@
"""Management command to print active user sessions."""
import logging
from importlib import import_module
@@ -13,6 +15,8 @@ User = get_user_model()
class Command(BaseCommand):
"""Print active user sessions with optional filters."""
help = "Print active user sessions with optional filters"
def add_arguments(self, parser):
@@ -31,18 +35,19 @@ class Command(BaseCommand):
def handle(self, *args, **options):
redis = get_redis_connection(settings.SESSION_CACHE_ALIAS)
engine = import_module(settings.SESSION_ENGINE)
SessionStore = engine.SessionStore
session_store = engine.SessionStore
user_email_filter = options.get("email")
session_id_filter = options.get("session_id")
verbose = options.get("verbose", False)
prefix = ":1:django.contrib.sessions.cache"
cache_version = settings.CACHES[settings.SESSION_CACHE_ALIAS].get("VERSION", 1)
prefix = f":{cache_version}:django.contrib.sessions.cache"
# If session ID filter is provided, check that specific session
if session_id_filter:
self._print_specific_session(
redis, SessionStore, prefix, session_id_filter, verbose
redis, session_store, prefix, session_id_filter, verbose
)
return
@@ -50,12 +55,12 @@ class Command(BaseCommand):
session_count = 0
filtered_count = 0
redis_keys = redis.keys(f"{prefix}*")
redis_keys = list(redis.scan_iter(f"{prefix}*"))
self.stdout.write(f"Found {len(redis_keys)} total sessions")
for redis_key in redis_keys:
session_count += 1
session_data = self._get_session_data(redis_key, SessionStore, prefix)
session_data = self._get_session_data(redis_key, session_store, prefix)
if not session_data:
continue
@@ -77,15 +82,15 @@ class Command(BaseCommand):
)
)
def _get_session_data(self, redis_key, SessionStore, prefix):
def _get_session_data(self, redis_key, session_store, prefix):
"""Extract and validate session data."""
try:
# Extract actual session key
raw_key = redis_key.decode()
session_key = raw_key.replace(prefix, "")
session_key = raw_key.removeprefix(prefix)
# Load and decode session
session = SessionStore(session_key=session_key)
session = session_store(session_key=session_key)
data = session.load()
user_id = data.get("_auth_user_id")
@@ -96,18 +101,19 @@ class Command(BaseCommand):
user = User.objects.get(id=user_id)
except User.DoesNotExist:
logger.warning(
f"User with ID {user_id} not found for session {session_key}"
"User with ID %s not found for session %s", user_id, session_key
)
return None
return user, session_key, data
# pylint: disable=broad-except
except Exception as e:
logger.error(f"Failed to process session {redis_key}: {e}")
except Exception as e: # pylint: disable=broad-exception-caught
logger.error("Failed to process session %s: %s", redis_key, e)
return None
def _print_specific_session(self, redis, SessionStore, prefix, session_id, verbose):
def _print_specific_session(
self, redis, session_store, prefix, session_id, verbose
):
"""Print information for a specific session ID."""
redis_key = f"{prefix}{session_id}".encode()
@@ -117,7 +123,7 @@ class Command(BaseCommand):
)
return
session_data = self._get_session_data(redis_key, SessionStore, prefix)
session_data = self._get_session_data(redis_key, session_store, prefix)
if not session_data:
self.stdout.write(
self.style.ERROR(f"Could not load session data for ID '{session_id}'")
@@ -95,6 +95,7 @@ class Command(BaseCommand):
)
if result.get("failure_count", 0) > 0:
return 1
return None
def _reindex_thread(self, thread_id, async_mode):
"""Reindex a specific thread and its messages."""
@@ -110,12 +111,12 @@ class Command(BaseCommand):
self.stdout.write(f"Reindexing thread {thread_id}...")
if async_mode:
task = reindex_thread_task.delay(thread_id)
task = reindex_thread_task.delay(str(thread_uuid))
self.stdout.write(
self.style.SUCCESS(f"Reindexing task scheduled (ID: {task.id})")
)
else:
result = reindex_thread_task(thread_id)
result = reindex_thread_task(str(thread_uuid)) # pylint: disable=no-value-for-parameter
if result.get("success", False):
self.stdout.write(
self.style.SUCCESS(f"Thread {thread_id} indexed successfully")
@@ -127,6 +128,7 @@ class Command(BaseCommand):
)
)
return 1
return None
def _reindex_mailbox(self, mailbox_id, async_mode):
"""Reindex all threads and messages in a specific mailbox."""
@@ -141,12 +143,12 @@ class Command(BaseCommand):
self.stdout.write(f"Reindexing threads for mailbox {mailbox}...")
if async_mode:
task = reindex_mailbox_task.delay(mailbox_id)
task = reindex_mailbox_task.delay(str(mailbox_uuid))
self.stdout.write(
self.style.SUCCESS(f"Reindexing task scheduled (ID: {task.id})")
)
else:
result = reindex_mailbox_task(mailbox_id)
result = reindex_mailbox_task(mailbox_id) # pylint: disable=no-value-for-parameter
self.stdout.write(
self.style.SUCCESS(
f"Reindexing completed: {result.get('success_count', 0)} succeeded, "
@@ -155,3 +157,4 @@ class Command(BaseCommand):
)
if result.get("failure_count", 0) > 0:
return 1
return None
@@ -15,7 +15,6 @@ class Command(BaseCommand):
def add_arguments(self, parser):
"""Add command arguments."""
pass
def handle(self, *args, **options):
"""Execute the command."""
@@ -31,6 +31,8 @@ logger = logging.getLogger(__name__)
class Command(BaseCommand):
"""Send an email using send_outbound_email."""
help = "Send an email using send_outbound_email (works without mailboxes, no DB writes)"
def add_arguments(self, parser):
@@ -97,7 +99,8 @@ class Command(BaseCommand):
except models.Mailbox.DoesNotExist:
# Use minimal setup without mailbox
logger.warning(
f"Mailbox with email '{from_email}' not found, sending without DKIM"
"Mailbox with email '%s' not found, sending without DKIM",
from_email,
)
else:
# Use minimal setup without mailbox
@@ -108,8 +111,8 @@ class Command(BaseCommand):
sender_mailbox.contact.name if sender_mailbox else None
) or from_email.split("@")[0]
logger.info(f"Sending email from {from_email} to {to_email}")
logger.info(f"Subject: {subject}")
logger.info("Sending email from %s to %s", from_email, to_email)
logger.info("Subject: %s", subject)
# Generate MIME ID
mime_id = (
+2 -3
View File
@@ -6,7 +6,6 @@ from typing import Optional
from django.conf import settings
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
import rest_framework as drf
@@ -25,7 +24,7 @@ def validate_body_size(body_bytes: bytes) -> None:
raise drf.exceptions.ValidationError(
{
"draftBody": _(
"draftBody": (
"Message body size (%(body_size)s MB) exceeds the %(max_size)s MB limit. "
"Please reduce message content."
)
@@ -51,7 +50,7 @@ def validate_attachment_size(current_total_size: int, new_total_size: int) -> No
raise drf.exceptions.ValidationError(
{
"attachments": _(
"attachments": (
"Cannot add attachment(s) (%(new_size)s MB). "
"Total attachments would be %(total_size)s MB, exceeding the %(max_size)s MB limit. "
"Current attachments: %(current_size)s MB."
+3 -4
View File
@@ -7,7 +7,6 @@ from typing import Any, Optional
from django.conf import settings
from django.core.cache import cache
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
import rest_framework as drf
@@ -65,7 +64,7 @@ def prepare_outbound_message(
if recipient_count > max_recipients:
raise drf.exceptions.ValidationError(
{
"message": _(
"message": (
"Too many recipients: %(count)s (maximum is %(max)s). "
"Please reduce the number of recipients before sending."
)
@@ -217,7 +216,7 @@ def prepare_outbound_message(
raise drf.exceptions.ValidationError(
{
"message": _(
"message": (
"Total attachment size (%(total_size)s MB) exceeds the %(max_size)s MB limit. "
"Please remove or reduce attachments."
)
@@ -303,7 +302,7 @@ def prepare_outbound_message(
raise drf.exceptions.ValidationError(
{
"message": _(
"message": (
"The composed email (%(mime_size)s MB) exceeds the maximum allowed size of %(max_size)s MB. "
"Please reduce message content or attachments."
)
+190 -195
View File
@@ -22,7 +22,6 @@ from django.db.models import Case, Q, When
from django.utils import timezone
from django.utils.html import escape
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
import jsonschema
import pyzstd
@@ -68,21 +67,21 @@ class BaseModel(models.Model):
"""
id = models.UUIDField(
verbose_name=_("id"),
help_text=_("primary key for the record as UUID"),
verbose_name="id",
help_text="primary key for the record as UUID",
primary_key=True,
default=uuid.uuid4,
editable=False,
)
created_at = models.DateTimeField(
verbose_name=_("created on"),
help_text=_("date and time at which a record was created"),
verbose_name="created on",
help_text="date and time at which a record was created",
auto_now_add=True,
editable=False,
)
updated_at = models.DateTimeField(
verbose_name=_("updated on"),
help_text=_("date and time at which a record was last updated"),
verbose_name="updated on",
help_text="date and time at which a record was last updated",
auto_now=True,
editable=False,
)
@@ -117,10 +116,8 @@ class UserManager(auth_models.UserManager):
and not settings.OIDC_ALLOW_DUPLICATE_EMAILS
):
raise DuplicateEmailError(
_(
"We couldn't find a user with this sub but the email is already "
"associated with a registered user."
)
) from err
return None
@@ -130,15 +127,15 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
sub_validator = validators.RegexValidator(
regex=r"^[\w.@+-:]+\Z",
message=_(
message=(
"Enter a valid sub. This value may contain only letters, "
"numbers, and @/./+/-/_/: characters."
),
)
sub = models.CharField(
_("sub"),
help_text=_(
"sub",
help_text=(
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
),
max_length=255,
@@ -148,48 +145,48 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
null=True,
)
full_name = models.CharField(_("full name"), max_length=255, null=True, blank=True)
full_name = models.CharField("full name", max_length=255, null=True, blank=True)
email = models.EmailField(_("identity email address"), blank=True, null=True)
email = models.EmailField("identity email address", blank=True, null=True)
# Unlike the "email" field which stores the email coming from the OIDC token, this field
# stores the email used by staff users to login to the admin site
admin_email = models.EmailField(
_("admin email address"), unique=True, blank=True, null=True
"admin email address", unique=True, blank=True, null=True
)
language = models.CharField(
max_length=10,
choices=settings.LANGUAGES,
default=settings.LANGUAGE_CODE,
verbose_name=_("language"),
help_text=_("The language in which the user wants to see the interface."),
verbose_name="language",
help_text="The language in which the user wants to see the interface.",
)
timezone = TimeZoneField(
choices_display="WITH_GMT_OFFSET",
use_pytz=False,
default=settings.TIME_ZONE,
help_text=_("The timezone in which the user wants to see times."),
help_text="The timezone in which the user wants to see times.",
)
is_staff = models.BooleanField(
_("staff status"),
"staff status",
default=False,
help_text=_("Whether the user can log into this admin site."),
help_text="Whether the user can log into this admin site.",
)
is_active = models.BooleanField(
_("active"),
"active",
default=True,
help_text=_(
help_text=(
"Whether this user should be treated as active. "
"Unselect this instead of deleting accounts."
),
)
custom_attributes = models.JSONField(
_("Custom attributes"),
"Custom attributes",
default=dict,
blank=True,
help_text=_("Metadata to sync to the user in the identity provider."),
help_text="Metadata to sync to the user in the identity provider.",
)
objects = UserManager()
@@ -199,8 +196,8 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
class Meta:
db_table = "messages_user"
verbose_name = _("user")
verbose_name_plural = _("users")
verbose_name = "user"
verbose_name_plural = "users"
def __str__(self):
return self.email or self.admin_email or str(self.id)
@@ -246,14 +243,14 @@ class MailDomain(BaseModel):
name_validator = validators.RegexValidator(
regex=r"^[a-z0-9][a-z0-9.-]*[a-z0-9]$",
message=_(
message=(
"Enter a valid domain name. This value may contain only lowercase "
"letters, numbers, dots and - characters."
),
)
name = models.CharField(
_("name"), max_length=253, unique=True, validators=[name_validator]
"name", max_length=253, unique=True, validators=[name_validator]
)
alias_of = models.ForeignKey(
@@ -261,37 +258,37 @@ class MailDomain(BaseModel):
)
oidc_autojoin = models.BooleanField(
_("oidc autojoin"),
"oidc autojoin",
default=False,
help_text=_("Create mailboxes automatically based on OIDC emails."),
help_text="Create mailboxes automatically based on OIDC emails.",
)
identity_sync = models.BooleanField(
_("Identity sync"),
"Identity sync",
default=False,
help_text=_("Sync mailboxes to an identity provider."),
help_text="Sync mailboxes to an identity provider.",
)
custom_settings = models.JSONField(
_("Custom settings"),
"Custom settings",
default=dict,
blank=True,
help_text=_("Custom settings for the mail domain."),
help_text="Custom settings for the mail domain.",
)
custom_attributes = models.JSONField(
_("Custom attributes"),
"Custom attributes",
default=dict,
blank=True,
help_text=_(
help_text=(
"Metadata to sync to the maildomain group in the identity provider."
),
)
class Meta:
db_table = "messages_maildomain"
verbose_name = _("mail domain")
verbose_name_plural = _("mail domains")
verbose_name = "mail domain"
verbose_name_plural = "mail domains"
def __str__(self):
return self.name
@@ -433,18 +430,18 @@ class Channel(BaseModel):
"""Channel model to store channel information for receiving messages from various sources."""
name = models.CharField(
_("name"), max_length=255, help_text=_("Human-readable name for this channel")
"name", max_length=255, help_text="Human-readable name for this channel"
)
type = models.CharField(
_("type"), max_length=255, help_text=_("Type of channel"), default="mta"
"type", max_length=255, help_text="Type of channel", default="mta"
)
settings = models.JSONField(
_("settings"),
"settings",
default=dict,
blank=True,
help_text=_("Channel-specific configuration settings"),
help_text="Channel-specific configuration settings",
)
mailbox = models.ForeignKey(
@@ -453,7 +450,7 @@ class Channel(BaseModel):
null=True,
blank=True,
related_name="channels",
help_text=_("Mailbox that receives messages from this channel"),
help_text="Mailbox that receives messages from this channel",
)
maildomain = models.ForeignKey(
@@ -462,13 +459,13 @@ class Channel(BaseModel):
null=True,
blank=True,
related_name="channels",
help_text=_("Mail domain that owns this channel"),
help_text="Mail domain that owns this channel",
)
class Meta:
db_table = "messages_channel"
verbose_name = _("channel")
verbose_name_plural = _("channels")
verbose_name = "channel"
verbose_name_plural = "channels"
ordering = ["-created_at"]
constraints = [
models.CheckConstraint(
@@ -487,7 +484,7 @@ class Mailbox(BaseModel):
"""Mailbox model to store mailbox information."""
local_part = models.CharField(
_("local part"),
"local part",
max_length=64,
validators=[validators.RegexValidator(regex=r"^[a-zA-Z0-9_.-]+$")],
)
@@ -501,9 +498,9 @@ class Mailbox(BaseModel):
)
is_identity = models.BooleanField(
_("is identity"),
"is identity",
default=True,
help_text=_(
help_text=(
"Whether this mailbox identifies a person (i.e. is not an alias or a group)"
),
)
@@ -514,8 +511,8 @@ class Mailbox(BaseModel):
class Meta:
db_table = "messages_mailbox"
verbose_name = _("mailbox")
verbose_name_plural = _("mailboxes")
verbose_name = "mailbox"
verbose_name_plural = "mailboxes"
unique_together = ("local_part", "domain")
ordering = ["-created_at"]
@@ -720,19 +717,19 @@ class MailboxAccess(BaseModel):
"User", on_delete=models.CASCADE, related_name="mailbox_accesses"
)
role = models.SmallIntegerField(
_("role"),
"role",
choices=MailboxRoleChoices.choices,
default=MailboxRoleChoices.VIEWER,
)
accessed_at = models.DateTimeField(
_("accessed at"), null=True, blank=True, db_index=True
"accessed at", null=True, blank=True, db_index=True
)
class Meta:
db_table = "messages_mailboxaccess"
verbose_name = _("mailbox access")
verbose_name_plural = _("mailbox accesses")
verbose_name = "mailbox access"
verbose_name_plural = "mailbox accesses"
unique_together = ("mailbox", "user")
def __str__(self):
@@ -756,44 +753,44 @@ class MailboxAccess(BaseModel):
class Thread(BaseModel):
"""Thread model to group messages."""
subject = models.CharField(_("subject"), max_length=255, null=True, blank=True)
snippet = models.TextField(_("snippet"), blank=True)
has_unread = models.BooleanField(_("has unread"), default=False)
has_trashed = models.BooleanField(_("has trashed"), default=False)
subject = models.CharField("subject", max_length=255, null=True, blank=True)
snippet = models.TextField("snippet", blank=True)
has_unread = models.BooleanField("has unread", default=False)
has_trashed = models.BooleanField("has trashed", default=False)
is_trashed = models.BooleanField(
_("is trashed"),
"is trashed",
default=False,
help_text=_("Whether all messages in the thread are trashed"),
help_text="Whether all messages in the thread are trashed",
)
has_archived = models.BooleanField(_("has archived"), default=False)
has_draft = models.BooleanField(_("has draft"), default=False)
has_starred = models.BooleanField(_("has starred"), default=False)
has_sender = models.BooleanField(_("has sender"), default=False)
has_messages = models.BooleanField(_("has messages"), default=True)
has_attachments = models.BooleanField(_("has attachments"), default=False)
is_spam = models.BooleanField(_("is spam"), default=False)
has_active = models.BooleanField(_("has active"), default=False)
has_archived = models.BooleanField("has archived", default=False)
has_draft = models.BooleanField("has draft", default=False)
has_starred = models.BooleanField("has starred", default=False)
has_sender = models.BooleanField("has sender", default=False)
has_messages = models.BooleanField("has messages", default=True)
has_attachments = models.BooleanField("has attachments", default=False)
is_spam = models.BooleanField("is spam", default=False)
has_active = models.BooleanField("has active", default=False)
has_delivery_pending = models.BooleanField(
_("has delivery pending"),
"has delivery pending",
default=False,
help_text=_(
help_text=(
"True if thread has messages awaiting successful delivery "
"(sending, retrying, or failed)."
),
)
has_delivery_failed = models.BooleanField(
_("has delivery failed"),
"has delivery failed",
default=False,
help_text=_("True if thread has messages with permanent delivery failure."),
help_text="True if thread has messages with permanent delivery failure.",
)
messaged_at = models.DateTimeField(_("messaged at"), null=True, blank=True)
sender_names = models.JSONField(_("sender names"), null=True, blank=True)
summary = models.TextField(_("summary"), null=True, blank=True, default=None)
messaged_at = models.DateTimeField("messaged at", null=True, blank=True)
sender_names = models.JSONField("sender names", null=True, blank=True)
summary = models.TextField("summary", null=True, blank=True, default=None)
class Meta:
db_table = "messages_thread"
verbose_name = _("thread")
verbose_name_plural = _("threads")
verbose_name = "thread"
verbose_name_plural = "threads"
def __str__(self):
return str(self.subject) if self.subject else "(no subject)"
@@ -910,10 +907,8 @@ class Thread(BaseModel):
self.messaged_at = max(
msg["created_at"] for msg in non_trashed_messages
)
elif len(message_data) > 0:
self.messaged_at = max(msg["created_at"] for msg in message_data)
else:
self.messaged_at = None
self.messaged_at = max(msg["created_at"] for msg in message_data)
# Set sender names (first and last sender names)
sender_names = None
@@ -962,52 +957,52 @@ class Label(BaseModel):
"""Label model to organize threads into folders using slash-based naming."""
name = models.CharField(
_("name"),
"name",
max_length=255,
help_text=_(
help_text=(
"Name of the label/folder (can use slashes for hierarchy, e.g. 'Work/Projects')"
),
)
slug = models.SlugField(
_("slug"),
"slug",
max_length=255,
help_text=_("URL-friendly version of the name"),
help_text="URL-friendly version of the name",
)
color = models.CharField(
_("color"),
"color",
max_length=7,
default="#E3E3FD",
help_text=_("Color of the label in hex format (e.g. #FF0000)"),
help_text="Color of the label in hex format (e.g. #FF0000)",
)
mailbox = models.ForeignKey(
"Mailbox",
on_delete=models.CASCADE,
related_name="labels",
help_text=_("Mailbox that owns this label"),
help_text="Mailbox that owns this label",
)
threads = models.ManyToManyField(
"Thread",
related_name="labels",
help_text=_("Threads that have this label"),
help_text="Threads that have this label",
blank=True,
)
description = models.CharField(
_("description"),
"description",
max_length=255,
blank=True,
default="",
help_text=_("Description of the label, used by AI to understand its purpose"),
help_text="Description of the label, used by AI to understand its purpose",
)
is_auto = models.BooleanField(
_("auto labeling"),
"auto labeling",
default=False,
help_text=_("Whether this label should be automatically applied by AI"),
help_text="Whether this label should be automatically applied by AI",
)
class Meta:
db_table = "messages_label"
verbose_name = _("label")
verbose_name_plural = _("labels")
verbose_name = "label"
verbose_name_plural = "labels"
unique_together = ("slug", "mailbox")
ordering = ["slug"]
@@ -1171,15 +1166,15 @@ class ThreadAccess(BaseModel):
"Mailbox", on_delete=models.CASCADE, related_name="thread_accesses"
)
role = models.SmallIntegerField(
_("role"),
"role",
choices=ThreadAccessRoleChoices.choices,
default=ThreadAccessRoleChoices.VIEWER,
)
class Meta:
db_table = "messages_threadaccess"
verbose_name = _("thread access")
verbose_name_plural = _("thread accesses")
verbose_name = "thread access"
verbose_name_plural = "thread accesses"
unique_together = ("thread", "mailbox")
def __str__(self):
@@ -1189,8 +1184,8 @@ class ThreadAccess(BaseModel):
class Contact(BaseModel):
"""Contact model to store contact information."""
name = models.CharField(_("name"), max_length=255, null=True, blank=True)
email = models.EmailField(_("email"))
name = models.CharField("name", max_length=255, null=True, blank=True)
email = models.EmailField("email")
mailbox = models.ForeignKey(
"Mailbox",
on_delete=models.CASCADE,
@@ -1199,8 +1194,8 @@ class Contact(BaseModel):
class Meta:
db_table = "messages_contact"
verbose_name = _("contact")
verbose_name_plural = _("contacts")
verbose_name = "contact"
verbose_name_plural = "contacts"
unique_together = ("email", "mailbox")
def __str__(self):
@@ -1222,26 +1217,26 @@ class MessageRecipient(BaseModel):
"Contact", on_delete=models.CASCADE, related_name="messages"
)
type = models.SmallIntegerField(
_("type"),
"type",
choices=MessageRecipientTypeChoices.choices,
default=MessageRecipientTypeChoices.TO,
)
delivered_at = models.DateTimeField(_("delivered at"), null=True, blank=True)
delivered_at = models.DateTimeField("delivered at", null=True, blank=True)
delivery_status = models.SmallIntegerField(
_("delivery status"),
"delivery status",
null=True,
blank=True,
choices=MessageDeliveryStatusChoices.choices,
)
delivery_message = models.TextField(_("delivery message"), null=True, blank=True)
retry_count = models.IntegerField(_("retry count"), default=0)
retry_at = models.DateTimeField(_("retry at"), null=True, blank=True)
delivery_message = models.TextField("delivery message", null=True, blank=True)
retry_count = models.IntegerField("retry count", default=0)
retry_at = models.DateTimeField("retry at", null=True, blank=True)
class Meta:
db_table = "messages_messagerecipient"
verbose_name = _("message recipient")
verbose_name_plural = _("message recipients")
verbose_name = "message recipient"
verbose_name_plural = "message recipients"
unique_together = ("message", "contact", "type")
def __str__(self):
@@ -1254,28 +1249,28 @@ class Message(BaseModel):
thread = models.ForeignKey(
Thread, on_delete=models.CASCADE, related_name="messages"
)
subject = models.CharField(_("subject"), max_length=255, null=True, blank=True)
subject = models.CharField("subject", max_length=255, null=True, blank=True)
sender = models.ForeignKey("Contact", on_delete=models.CASCADE)
parent = models.ForeignKey(
"Message", on_delete=models.SET_NULL, null=True, blank=True
)
# Flags
is_draft = models.BooleanField(_("is draft"), default=False)
is_sender = models.BooleanField(_("is sender"), default=False)
is_starred = models.BooleanField(_("is starred"), default=False)
is_trashed = models.BooleanField(_("is trashed"), default=False)
is_unread = models.BooleanField(_("is unread"), default=False)
is_spam = models.BooleanField(_("is spam"), default=False)
is_archived = models.BooleanField(_("is archived"), default=False)
has_attachments = models.BooleanField(_("has attachments"), default=False)
is_draft = models.BooleanField("is draft", default=False)
is_sender = models.BooleanField("is sender", default=False)
is_starred = models.BooleanField("is starred", default=False)
is_trashed = models.BooleanField("is trashed", default=False)
is_unread = models.BooleanField("is unread", default=False)
is_spam = models.BooleanField("is spam", default=False)
is_archived = models.BooleanField("is archived", default=False)
has_attachments = models.BooleanField("has attachments", default=False)
trashed_at = models.DateTimeField(_("trashed at"), null=True, blank=True)
sent_at = models.DateTimeField(_("sent at"), null=True, blank=True)
read_at = models.DateTimeField(_("read at"), null=True, blank=True)
archived_at = models.DateTimeField(_("archived at"), null=True, blank=True)
trashed_at = models.DateTimeField("trashed at", null=True, blank=True)
sent_at = models.DateTimeField("sent at", null=True, blank=True)
read_at = models.DateTimeField("read at", null=True, blank=True)
archived_at = models.DateTimeField("archived at", null=True, blank=True)
mime_id = models.CharField(_("mime id"), max_length=998, null=True, blank=True)
mime_id = models.CharField("mime id", max_length=998, null=True, blank=True)
channel = models.ForeignKey(
"Channel",
@@ -1303,7 +1298,7 @@ class Message(BaseModel):
)
signature = models.ForeignKey(
"MessageTemplate",
help_text=_("Signature template for the message"),
help_text="Signature template for the message",
on_delete=models.SET_NULL,
null=True,
blank=True,
@@ -1315,8 +1310,8 @@ class Message(BaseModel):
class Meta:
db_table = "messages_message"
verbose_name = _("message")
verbose_name_plural = _("messages")
verbose_name = "message"
verbose_name_plural = "messages"
ordering = ["-created_at"]
def __str__(self):
@@ -1387,7 +1382,7 @@ class Message(BaseModel):
).select_related("contact")
cc = [str(mr.contact) for mr in cc_contacts]
# Subject
subject = self.subject or _("No subject")
subject = self.subject or "No subject"
# Body: try to get text/plain from parsed data
body = ""
parsed_data = self.get_parsed_data()
@@ -1398,19 +1393,19 @@ class Message(BaseModel):
# Message ID
msg_id = str(self.id)
return (
f"{_('Message ID')}: {msg_id}\n"
f"{_('From')}: {sender}\n"
f"{_('To')}: {', '.join(recipients)}\n"
f"{_('CC')}: {', '.join(cc)}\n"
f"{_('Date')}: {date_str}\n"
f"{_('Subject')}: {subject}\n\n"
f"{_('Body')}: {body}"
f"Message ID: {msg_id}\n"
f"From: {sender}\n"
f"To: {', '.join(recipients)}\n"
f"CC: {', '.join(cc)}\n"
f"Date: {date_str}\n"
f"Subject: {subject}\n\n"
f"Body: {body}"
)
def get_tokens_count(self) -> int:
"""Get the number of tokens in the message (subject + body)."""
# Subject
subject = self.subject or _("No subject")
subject = self.subject or "No subject"
# Body: try to get text/plain from parsed data
body = ""
parsed_data = self.get_parsed_data()
@@ -1430,7 +1425,7 @@ class InboundMessage(BaseModel):
on_delete=models.CASCADE,
related_name="inbound_messages",
)
raw_data = models.BinaryField(_("raw data"), help_text=_("Raw email message bytes"))
raw_data = models.BinaryField("raw data", help_text="Raw email message bytes")
channel = models.ForeignKey(
"Channel",
on_delete=models.SET_NULL,
@@ -1439,15 +1434,15 @@ class InboundMessage(BaseModel):
related_name="inbound_messages",
)
error_message = models.TextField(
_("error message"),
"error message",
blank=True,
help_text=_("Error message if processing failed"),
help_text="Error message if processing failed",
)
class Meta:
db_table = "messages_inboundmessage"
verbose_name = _("inbound message")
verbose_name_plural = _("inbound messages")
verbose_name = "inbound message"
verbose_name_plural = "inbound messages"
ordering = ["-created_at"]
indexes = [
models.Index(fields=["created_at"]),
@@ -1538,33 +1533,33 @@ class Blob(BaseModel):
"""
sha256 = models.BinaryField(
_("sha256 hash"),
"sha256 hash",
max_length=32,
db_index=True,
help_text=_("SHA-256 hash of the uncompressed blob content"),
help_text="SHA-256 hash of the uncompressed blob content",
)
size = models.PositiveIntegerField(
_("file size"), help_text=_("Size of the blob in bytes")
"file size", help_text="Size of the blob in bytes"
)
size_compressed = models.PositiveIntegerField(
_("compressed size"), help_text=_("Size of the compressed blob in bytes")
"compressed size", help_text="Size of the compressed blob in bytes"
)
content_type = models.CharField(
_("content type"), max_length=127, help_text=_("MIME type of the blob")
"content type", max_length=127, help_text="MIME type of the blob"
)
compression = models.SmallIntegerField(
_("compression"),
"compression",
choices=CompressionTypeChoices.choices,
default=CompressionTypeChoices.NONE,
)
raw_content = models.BinaryField(
_("raw content"),
help_text=_("Compressed binary content of the blob"),
"raw content",
help_text="Compressed binary content of the blob",
)
mailbox = models.ForeignKey(
@@ -1573,7 +1568,7 @@ class Blob(BaseModel):
blank=True,
on_delete=models.CASCADE,
related_name="blobs",
help_text=_("Mailbox that owns this blob"),
help_text="Mailbox that owns this blob",
)
maildomain = models.ForeignKey(
"MailDomain",
@@ -1581,15 +1576,15 @@ class Blob(BaseModel):
blank=True,
on_delete=models.CASCADE,
related_name="blobs",
help_text=_("Mail domain that owns this blob"),
help_text="Mail domain that owns this blob",
)
objects = BlobManager()
class Meta:
db_table = "messages_blob"
verbose_name = _("blob")
verbose_name_plural = _("blobs")
verbose_name = "blob"
verbose_name_plural = "blobs"
ordering = ["-created_at"]
constraints = [
models.CheckConstraint(
@@ -1629,43 +1624,43 @@ class Attachment(BaseModel):
"""Attachment model to link messages with blobs."""
name = models.CharField(
_("file name"),
"file name",
max_length=255,
help_text=_("Original filename of the attachment"),
help_text="Original filename of the attachment",
)
blob = models.ForeignKey(
"Blob",
on_delete=models.CASCADE,
related_name="attachments",
help_text=_("Reference to the blob containing the attachment data"),
help_text="Reference to the blob containing the attachment data",
)
mailbox = models.ForeignKey(
"Mailbox",
on_delete=models.CASCADE,
related_name="attachments",
help_text=_("Mailbox that owns this attachment"),
help_text="Mailbox that owns this attachment",
)
messages = models.ManyToManyField(
"Message",
related_name="attachments",
help_text=_("Messages that use this attachment"),
help_text="Messages that use this attachment",
)
cid = models.CharField(
_("content ID"),
"content ID",
max_length=255,
blank=True,
null=True,
help_text=_("Content-ID for inline images"),
help_text="Content-ID for inline images",
)
class Meta:
db_table = "messages_attachment"
verbose_name = _("attachment")
verbose_name_plural = _("attachments")
verbose_name = "attachment"
verbose_name_plural = "attachments"
ordering = ["-created_at"]
def __str__(self):
@@ -1697,15 +1692,15 @@ class MailDomainAccess(BaseModel):
"User", on_delete=models.CASCADE, related_name="maildomain_accesses"
)
role = models.SmallIntegerField(
_("role"),
"role",
choices=MailDomainAccessRoleChoices.choices,
default=MailDomainAccessRoleChoices.ADMIN,
)
class Meta:
db_table = "messages_maildomainaccess"
verbose_name = _("mail domain access")
verbose_name_plural = _("mail domain accesses")
verbose_name = "mail domain access"
verbose_name_plural = "mail domain accesses"
unique_together = ("maildomain", "user")
def __str__(self):
@@ -1716,50 +1711,50 @@ class DKIMKey(BaseModel):
"""DKIM Key model to store DKIM signing keys with encrypted private key storage."""
selector = models.CharField(
_("selector"),
"selector",
max_length=255,
help_text=_("DKIM selector (e.g., 'default', 'mail')"),
help_text="DKIM selector (e.g., 'default', 'mail')",
)
private_key = EncryptedTextField(
_("private key"),
help_text=_("DKIM private key in PEM format (encrypted)"),
"private key",
help_text="DKIM private key in PEM format (encrypted)",
)
public_key = models.TextField(
_("public key"),
help_text=_("DKIM public key for DNS record generation"),
"public key",
help_text="DKIM public key for DNS record generation",
)
algorithm = models.SmallIntegerField(
_("algorithm"),
"algorithm",
choices=DKIMAlgorithmChoices.choices,
default=DKIMAlgorithmChoices.RSA,
help_text=_("DKIM signing algorithm"),
help_text="DKIM signing algorithm",
)
key_size = models.PositiveIntegerField(
_("key size"),
help_text=_("Key size in bits (e.g., 2048, 4096 for RSA)"),
"key size",
help_text="Key size in bits (e.g., 2048, 4096 for RSA)",
)
is_active = models.BooleanField(
_("is active"),
"is active",
default=True,
help_text=_("Whether this DKIM key is active and should be used for signing"),
help_text="Whether this DKIM key is active and should be used for signing",
)
domain = models.ForeignKey(
"MailDomain",
on_delete=models.CASCADE,
related_name="dkim_keys",
help_text=_("Domain that owns this DKIM key"),
help_text="Domain that owns this DKIM key",
)
class Meta:
db_table = "messages_dkimkey"
verbose_name = _("DKIM key")
verbose_name_plural = _("DKIM keys")
verbose_name = "DKIM key"
verbose_name_plural = "DKIM keys"
ordering = ["-created_at"] # Most recent first for picking latest active key
def __str__(self):
@@ -1779,9 +1774,9 @@ class MessageTemplate(BaseModel):
"""Message template model to store reusable message templates and signatures."""
name = models.CharField(
_("name"),
"name",
max_length=255,
help_text=_(
help_text=(
"Name of the template (e.g., 'Standard Reply', 'Out of Office', 'Work Signature')"
),
)
@@ -1792,22 +1787,22 @@ class MessageTemplate(BaseModel):
null=True,
blank=True,
related_name="message_templates",
help_text=_(
help_text=(
"Reference to the blob containing template content as JSON: {html: str, text: str, raw: any}"
),
)
type = models.SmallIntegerField(
_("type"),
"type",
choices=MessageTemplateTypeChoices.choices,
default=MessageTemplateTypeChoices.MESSAGE,
help_text=_("Type of template (message, signature)"),
help_text="Type of template (message, signature)",
)
is_active = models.BooleanField(
_("is active"),
"is active",
default=True,
help_text=_("Whether this template is available for use"),
help_text="Whether this template is available for use",
)
maildomain = models.ForeignKey(
@@ -1816,7 +1811,7 @@ class MessageTemplate(BaseModel):
blank=True,
on_delete=models.CASCADE,
related_name="message_templates",
help_text=_("Mail domain that can use this template"),
help_text="Mail domain that can use this template",
)
mailbox = models.ForeignKey(
@@ -1825,29 +1820,29 @@ class MessageTemplate(BaseModel):
blank=True,
on_delete=models.CASCADE,
related_name="message_templates",
help_text=_("Mailbox that can use this template"),
help_text="Mailbox that can use this template",
)
is_forced = models.BooleanField(
_("is forced"),
"is forced",
default=False,
help_text=_(
help_text=(
"Whether this template is forced; no other template of the same type can be used in the same scope"
),
)
is_default = models.BooleanField(
_("is default"),
"is default",
default=False,
help_text=_(
help_text=(
"Whether this template is the default; it will be automatically loaded when composing a new message"
),
)
class Meta:
db_table = "messages_messagetemplate"
verbose_name = _("message template")
verbose_name_plural = _("message templates")
verbose_name = "message template"
verbose_name_plural = "message templates"
ordering = ["-created_at"]
constraints = [
models.CheckConstraint(
+20 -1
View File
@@ -1,3 +1,7 @@
"""Service for AI-powered features using OpenAI-compatible API."""
import logging
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
@@ -5,6 +9,8 @@ from openai import OpenAI
from core.ai.utils import is_ai_enabled
logger = logging.getLogger(__name__)
class AIService:
"""Service class for AI-related operations."""
@@ -13,7 +19,12 @@ class AIService:
"""Ensure that the AI configuration is set properly."""
if not is_ai_enabled():
raise ImproperlyConfigured("AI configuration not set")
self.client = OpenAI(base_url=settings.AI_BASE_URL, api_key=settings.AI_API_KEY)
self.client = OpenAI(
base_url=settings.AI_BASE_URL,
api_key=settings.AI_API_KEY,
timeout=60,
max_retries=1,
)
def call_ai_api(self, prompt):
"""Helper method to call the OpenAI API and process the response."""
@@ -24,7 +35,15 @@ class AIService:
"n": 1,
}
try:
response = self.client.chat.completions.create(**data)
except Exception:
logger.exception("AI API call failed")
raise
if not response.choices:
raise ValueError("AI response returned no choices")
content = response.choices[0].message.content
if not content:
+2 -2
View File
@@ -5,10 +5,10 @@ DNS checking functionality for mail domains.
import re
from typing import Dict, List
from core.models import MailDomain
import dns.resolver
from core.models import MailDomain
def normalize_txt_value(value: str) -> str:
"""
@@ -7,11 +7,11 @@ from typing import Any, Dict, Optional
from django.conf import settings
import dns.resolver
from core.models import MailDomain
from core.services.dns.providers.scaleway import ScalewayDNSProvider
import dns.resolver
logger = logging.getLogger(__name__)
+5 -2
View File
@@ -34,7 +34,7 @@ CHUNK_SIZE = 100 * 1024 * 1024
MIN_PART_SIZE = 5 * 1024 * 1024
class S3MultipartGzipUploader:
class S3MultipartGzipUploader: # pylint: disable=too-many-instance-attributes
"""
A file-like object that streams gzip-compressed data to S3 using multipart upload.
@@ -177,11 +177,14 @@ class S3MultipartGzipUploader:
)
except Exception:
# Abort the multipart upload to avoid leaked parts on S3
try:
self.s3_client.abort_multipart_upload(
Bucket=self.bucket,
Key=self.key,
UploadId=self.upload_id,
)
except Exception: # pylint: disable=broad-exception-caught
logger.debug("Failed to abort multipart upload", exc_info=True)
raise
finally:
self._closed = True
@@ -399,7 +402,7 @@ def _create_mbox_entry(
@celery_app.task(bind=True) # pylint: disable=too-many-locals
def export_mailbox_task(self, mailbox_id: str, user_id: str) -> Dict[str, Any]:
def export_mailbox_task(self, mailbox_id: str, user_id: str) -> Dict[str, Any]: # pylint: disable=unused-argument
"""
Export all messages from a mailbox to an MBOX file and upload to S3.
@@ -212,8 +212,7 @@ def sync_mailbox_to_keycloak_user(mailbox):
for group in groups:
yield group
for sub_group in group.get("subGroups") or []:
yield sub_group
yield from group.get("subGroups") or []
for group in list_groups_and_subgroups():
if group.get("name") == group_name:
+16 -7
View File
@@ -1,4 +1,11 @@
"""IMAP utilities for message import."""
"""IMAP utilities for message import.
Broad exception handling (W0718) is intentional: IMAP servers can raise many
different exception types (socket errors, encoding errors, protocol errors)
and the import must continue processing remaining messages on failure.
"""
# pylint: disable=broad-exception-caught
import base64
import codecs
@@ -184,12 +191,12 @@ def _parse_imap_folder_info(folder_info: str) -> Optional[str]:
def get_selectable_folders(
imap_connection, username: str, imap_server: str
imap_connection, _username: str, _imap_server: str
) -> List[str]:
"""Get list of selectable folders from IMAP server."""
status, folder_list = imap_connection.list()
if status != "OK":
raise Exception(f"Failed to list folders: {folder_list}")
raise RuntimeError(f"Failed to list folders: {folder_list}")
selectable_folders = []
for folder_info in folder_list:
@@ -282,7 +289,7 @@ def select_imap_folder(imap_connection, folder: str) -> bool:
def get_message_numbers(
imap_connection, folder: str, username: str, imap_server: str
imap_connection, folder: str, _username: str, _imap_server: str
) -> List[bytes]:
"""Get message numbers from the selected folder."""
# Search for all messages
@@ -409,7 +416,7 @@ def _fetch_message_with_flags(
# Fetch message with flags
status, msg_data = imap_connection.fetch(msg_num, "(FLAGS BODY.PEEK[])")
if status != "OK":
raise Exception(f"Failed to fetch message {msg_num}: {msg_data}")
raise RuntimeError(f"Failed to fetch message {msg_num}: {msg_data}")
flags, raw_email = _extract_imap_flags_and_content(msg_data)
@@ -418,7 +425,7 @@ def _fetch_message_with_flags(
flags = _fetch_separate_flags(imap_connection, msg_num)
if raw_email is None:
raise Exception(f"No raw email found for message {msg_num}")
raise RuntimeError(f"No raw email found for message {msg_num}")
return flags, raw_email
@@ -428,6 +435,8 @@ def _fetch_message_with_flags_retry(
) -> Tuple[List[str], Optional[bytes]]:
"""Fetch a message with retry logic for timeout errors."""
max_retries = settings.IMAP_MAX_RETRIES
if max_retries < 1:
raise RuntimeError("IMAP_MAX_RETRIES must be >= 1")
for attempt in range(max_retries):
try:
return _fetch_message_with_flags(imap_connection, msg_num)
@@ -442,7 +451,6 @@ def _fetch_message_with_flags_retry(
# Exponential backoff
time.sleep(2**attempt)
continue
else:
logger.error(
"Failed to fetch message %s after %d attempts",
msg_num,
@@ -452,6 +460,7 @@ def _fetch_message_with_flags_retry(
except Exception as e:
logger.error("Unexpected error fetching message %s: %s", msg_num, e)
raise
raise RuntimeError(f"Failed to fetch message {msg_num} after {max_retries} retries")
def process_folder_messages( # pylint: disable=too-many-arguments
+1 -1
View File
@@ -140,6 +140,6 @@ def handle_duplicate_message(
)
existing_message.thread.labels.add(label_obj)
except Exception as e:
except Exception as e: # pylint: disable=broad-exception-caught
logger.exception("Error creating label %s: %s", label, e)
continue
+12 -5
View File
@@ -1,4 +1,11 @@
"""PST file parsing utilities for message import."""
"""PST file parsing utilities for message import.
Broad exception handling (W0718/C0302) is intentional: PST parsing relies on
the pypff C library which can raise arbitrary exceptions on malformed data.
Each field extraction must be individually guarded to maximise data recovery.
"""
# pylint: disable=broad-exception-caught,too-many-lines
import base64
import logging
@@ -565,9 +572,9 @@ def _extract_recipients_from_mapi(message) -> dict:
result = {"to": [], "cc": [], "bcc": []}
# MAPI recipient types
MAPI_TO = 1
MAPI_CC = 2
MAPI_BCC = 3
MAPI_TO = 1 # pylint: disable=invalid-name
MAPI_CC = 2 # pylint: disable=invalid-name
MAPI_BCC = 3 # pylint: disable=invalid-name
try:
num_recipients = int(message.number_of_recipients)
@@ -651,7 +658,7 @@ def _decode_html_bytes(raw_html: bytes) -> str:
return raw_html.decode("utf-8", errors="replace")
def reconstruct_eml(message, store_email: Optional[str] = None) -> bytes:
def reconstruct_eml(message, store_email: Optional[str] = None) -> bytes: # pylint: disable=too-many-branches
"""Convert a pypff message to RFC5322 bytes.
If transport_headers is available, uses those for threading headers.
@@ -11,7 +11,7 @@ BUFFER_CENTERED = "centered"
BUFFER_NONE = "none"
class S3SeekableReader:
class S3SeekableReader: # pylint: disable=too-many-instance-attributes
"""Seekable file-like object that reads from S3 using range requests.
Maintains a read-ahead buffer (default 100MB) and makes HTTP Range
+9 -11
View File
@@ -76,16 +76,15 @@ class ImportService:
"mbox": "application/mbox",
"pst": "application/vnd.ms-outlook",
}
if ext in extension_map:
content_type = extension_map[ext]
content_type = extension_map.get(ext, content_type)
if content_type not in enums.ARCHIVE_SUPPORTED_MIME_TYPES:
return False, {
"detail": (
"Invalid file format. Only EML, MBOX, "
"and PST files are supported. "
"Detected content type: {content_type}"
).format(content_type=content_type)
f"Invalid file format. Only EML, MBOX, "
f"and PST files are supported. "
f"Detected content type: {content_type}"
)
}
try:
@@ -102,7 +101,7 @@ class ImportService:
)
return True, response_data
# Check MIME type for MBOX
elif content_type in enums.MBOX_SUPPORTED_MIME_TYPES:
if content_type in enums.MBOX_SUPPORTED_MIME_TYPES:
# Process MBOX file asynchronously
task = process_mbox_file_task.delay(file_key, str(recipient.id))
register_task_owner(task.id, user.id)
@@ -115,7 +114,7 @@ class ImportService:
)
return True, response_data
# Check MIME type for EML
elif content_type in enums.EML_SUPPORTED_MIME_TYPES:
if content_type in enums.EML_SUPPORTED_MIME_TYPES:
# Process EML file asynchronously
task = process_eml_file_task.delay(file_key, str(recipient.id))
register_task_owner(task.id, user.id)
@@ -127,9 +126,8 @@ class ImportService:
"This may take a while. You can check the status in the Celery task monitor.",
)
return True, response_data
else:
return False, {"detail": f"Unsupported file format: {content_type}"}
except Exception as e:
except Exception as e: # pylint: disable=broad-exception-caught
capture_exception(e)
logger.exception("Error processing file: %s", e)
if request:
@@ -192,7 +190,7 @@ class ImportService:
)
return True, response_data
except Exception as e:
except Exception as e: # pylint: disable=broad-exception-caught
capture_exception(e)
logger.exception("Error starting IMAP import: %s", e)
if request:
+3 -3
View File
@@ -14,7 +14,7 @@ from core.services.search.parse import parse_search_query
logger = logging.getLogger(__name__)
def search_threads(
def search_threads( # pylint: disable=too-many-branches
query: str,
mailbox_ids: Optional[list] = None,
filters: Optional[Dict[str, Any]] = None,
@@ -41,7 +41,7 @@ def search_threads(
logger.debug("OpenSearch search is disabled, returning empty results")
return {"threads": [], "total": 0, "from": from_offset, "size": size}
try:
try: # pylint: disable=too-many-nested-blocks
es = get_opensearch_client()
# Parse the query for modifiers
@@ -167,7 +167,7 @@ def search_threads(
{"term": {"is_trashed": True}}
)
else:
# Fully trashed threads should be excluded from search results until the user explicitly searches for trashed threads
# Exclude fully trashed threads unless the user explicitly searches for them
search_body["query"]["bool"]["filter"].append(
{"term": {"is_trashed": False}}
)
+7 -10
View File
@@ -64,11 +64,10 @@ def get_period_key(period_name: str) -> str:
now = timezone.now()
if period_name == "day":
return now.strftime("%Y-%m-%d")
elif period_name == "hour":
if period_name == "hour":
return now.strftime("%Y-%m-%d-%H")
elif period_name == "minute":
if period_name == "minute":
return now.strftime("%Y-%m-%d-%H-%M")
else:
return now.strftime("%Y-%m-%d")
@@ -81,15 +80,14 @@ def get_period_expiry(period_name: str) -> int:
hour=0, minute=0, second=0, microsecond=0
)
return int((tomorrow - now).total_seconds())
elif period_name == "hour":
if period_name == "hour":
next_hour = (now + timedelta(hours=1)).replace(
minute=0, second=0, microsecond=0
)
return int((next_hour - now).total_seconds())
elif period_name == "minute":
if period_name == "minute":
next_minute = (now + timedelta(minutes=1)).replace(second=0, microsecond=0)
return int((next_minute - now).total_seconds())
else:
return 86400
@@ -119,7 +117,7 @@ def increment_counter(cache_key: str, amount: int, expiry_seconds: int) -> int:
return amount
def decrement_counter(cache_key: str, amount: int, expiry_seconds: int) -> int:
def decrement_counter(cache_key: str, amount: int, expiry_seconds: int) -> int: # pylint: disable=unused-argument
"""
Decrement a counter in cache and return the new value.
@@ -255,7 +253,7 @@ def check_and_increment_throttle(mailbox, maildomain, message) -> None:
decrement_counter(
inc_check["cache_key"], external_count, inc_check["expiry"]
)
except Exception as rollback_error:
except Exception as rollback_error: # pylint: disable=broad-exception-caught
logger.error("Failed to rollback throttle counter: %s", rollback_error)
raise
@@ -313,9 +311,8 @@ def format_duration(seconds: int) -> str:
"""Format seconds into a human-readable duration."""
if seconds < 60:
return f"{seconds}s"
elif seconds < 3600:
if seconds < 3600:
minutes = math.ceil(seconds / 60)
return f"{minutes}m"
else:
hours = math.ceil(seconds / 3600)
return f"{hours}h"
+3 -10
View File
@@ -113,20 +113,13 @@ class TestPlaceholderView:
"required": [],
}
)
def test_internationalization_support(self, api_client):
"""Test that internationalization is properly handled."""
def test_i18n_schema_uses_default_language(self, api_client):
"""Test that x-i18n schema labels always use the default language."""
url = reverse("placeholders")
# Accept-Language header is ignored; backend always uses LANGUAGE_CODE
response = api_client.get(url, HTTP_ACCEPT_LANGUAGE="fr-fr")
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["job_title"] == "Fonction"
assert data["is_elected"] == "Est élu"
assert data["name"] == "Nom"
# Test that fallback language handling works correctly.
response = api_client.get(url, HTTP_ACCEPT_LANGUAGE="de-de")
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["job_title"] == "Job title"
assert data["is_elected"] == "Is elected"
assert data["name"] == "Name"
-1
View File
@@ -773,7 +773,6 @@ class TestDNSChecking: # pylint: disable=too-many-public-methods
@pytest.fixture(name="maildomain_factory")
@pytest.mark.django_db
def fixture_maildomain_factory():
"""Factory for creating test mail domains."""
@@ -276,7 +276,6 @@ class TestDNSProvisioning:
@pytest.fixture(name="maildomain_factory")
@pytest.mark.django_db
def fixture_maildomain_factory():
"""Create a maildomain factory for testing."""
@@ -5,7 +5,7 @@ These tests use hypothesis for property-based testing to find edge cases
and potential crashes in the email address parsing code.
Run with: pytest -m fuzz core/tests/mda/test_rfc5322_address_fuzz.py
Or: make back-fuzz
Or: make fuzz-back
"""
import pytest
@@ -5,7 +5,7 @@ These tests use hypothesis for property-based testing to find edge cases
and potential crashes in the complete email message parsing code.
Run with: pytest -m fuzz core/tests/mda/test_rfc5322_message_fuzz.py
Or: make back-fuzz
Or: make fuzz-back
"""
import base64
@@ -118,10 +118,11 @@ class TestMailDomainModel:
}
with pytest.raises(ValidationError) as exception_info:
MailDomainFactory(custom_attributes=custom_attributes)
assert (
str(exception_info.value)
== "{'custom_attributes': [\"Additional properties are not allowed ('additional_property' was unexpected)\"]}"
expected = (
"""{'custom_attributes': ["Additional properties are not allowed"""
""" ('additional_property' was unexpected)"]}"""
)
assert str(exception_info.value) == expected
class TestMailDomainModelAbilities:
@@ -1,3 +1,5 @@
"""Tests for the MailDomainAccess model."""
from django.core.exceptions import ValidationError
import pytest
@@ -9,7 +11,10 @@ pytestmark = pytest.mark.django_db
class TestMailDomainAccessModel:
"""Tests for MailDomainAccess model CRUD and constraints."""
def test_create_mail_domain_access(self):
"""Test creating a MailDomainAccess instance."""
user = factories.UserFactory()
maildomain = factories.MailDomainFactory()
@@ -26,6 +31,7 @@ class TestMailDomainAccessModel:
)
def test_unique_together_constraint(self):
"""Test that duplicate user+maildomain access is rejected."""
user = factories.UserFactory()
maildomain = factories.MailDomainFactory()
@@ -41,6 +47,7 @@ class TestMailDomainAccessModel:
)
def test_related_names(self):
"""Test reverse relation names on User and MailDomain."""
user = factories.UserFactory()
maildomain = factories.MailDomainFactory()
access = factories.MailDomainAccessFactory(user=user, maildomain=maildomain)
+4 -3
View File
@@ -63,7 +63,8 @@ class TestUserModel:
custom_attributes = {"job_title": "test", "additional_property": "should fail"}
with pytest.raises(ValidationError) as exception_info:
factories.UserFactory(custom_attributes=custom_attributes)
assert (
str(exception_info.value)
== "{'custom_attributes': [\"Additional properties are not allowed ('additional_property' was unexpected)\"]}"
expected = (
"""{'custom_attributes': ["Additional properties are not allowed"""
""" ('additional_property' was unexpected)"]}"""
)
assert str(exception_info.value) == expected
+1 -1
View File
@@ -148,7 +148,7 @@ def test_reindex_all(mock_es_client_index):
@pytest.mark.django_db
def test_reindex_mailbox(mock_es_client_index, test_mailbox, test_thread):
def test_reindex_mailbox(mock_es_client_index, test_mailbox, test_thread): # pylint: disable=unused-argument
"""Test reindexing a specific mailbox."""
# Call the function
@@ -324,7 +324,7 @@ class TestProcessMboxFileTask:
finally:
s3_client.delete_object(Bucket=storage.bucket_name, Key=file_key)
def test_task_process_mbox_file_mailbox_not_found(self, sample_mbox_content):
def test_task_process_mbox_file_mailbox_not_found(self, sample_mbox_content): # pylint: disable=unused-argument
"""Test MBOX processing with non-existent mailbox."""
mock_task = MagicMock()
mock_task.update_state = MagicMock()
@@ -40,7 +40,7 @@ class TestSendMessageTask:
self, draft_message, mailbox_with_thread
):
"""Test send_message_task with must_archive=True archives all thread messages."""
mailbox, thread = mailbox_with_thread
_, thread = mailbox_with_thread
# Create additional messages in the same thread
other_message1 = factories.MessageFactory(
@@ -68,7 +68,9 @@ class TestSendMessageTask:
with patch("core.mda.outbound_tasks.send_message") as mock_mda_send:
# Call the task with must_archive=True
with patch.object(send_message_task, "update_state"):
result = send_message_task(str(draft_message.id), must_archive=True)
result = send_message_task( # pylint: disable=no-value-for-parameter
str(draft_message.id), must_archive=True
)
# Verify send_message was called
mock_mda_send.assert_called_once_with(draft_message, False)
@@ -94,7 +96,7 @@ class TestSendMessageTask:
self, draft_message, mailbox_with_thread
):
"""Test send_message_task with must_archive=False does not archive messages."""
mailbox, thread = mailbox_with_thread
_, thread = mailbox_with_thread
# Create additional messages in the same thread
other_message = factories.MessageFactory(
@@ -114,7 +116,9 @@ class TestSendMessageTask:
with patch("core.mda.outbound_tasks.send_message") as mock_mda_send:
# Call the task with must_archive=False
with patch.object(send_message_task, "update_state"):
result = send_message_task(str(draft_message.id), must_archive=False)
result = send_message_task( # pylint: disable=no-value-for-parameter
str(draft_message.id), must_archive=False
)
# Verify the result
assert result["success"] is True
@@ -134,7 +138,9 @@ class TestSendMessageTask:
assert other_message.archived_at is None
def test_task_send_message_archive_error_does_not_fail_task(
self, draft_message, mailbox_with_thread
self,
draft_message,
mailbox_with_thread, # pylint: disable=unused-argument
):
"""Test that archiving error does not cause the task to fail."""
assert draft_message.is_draft is True
@@ -150,7 +156,9 @@ class TestSendMessageTask:
# Call the task with must_archive=True
# The task should succeed even if archiving fails
with patch.object(send_message_task, "update_state"):
result = send_message_task(str(draft_message.id), must_archive=True)
result = send_message_task( # pylint: disable=no-value-for-parameter
str(draft_message.id), must_archive=True
)
# Verify send_message was called
mock_mda_send.assert_called_once_with(draft_message, False)
@@ -160,7 +168,9 @@ class TestSendMessageTask:
assert result["message_id"] == str(draft_message.id)
def test_task_send_message_updates_thread_stats_after_archive(
self, draft_message, mailbox_with_thread
self,
draft_message,
mailbox_with_thread, # pylint: disable=unused-argument
):
"""Test that thread stats are updated after archiving."""
# Mock the send_message function
@@ -169,7 +179,9 @@ class TestSendMessageTask:
with patch("core.models.Thread.update_stats") as mock_update_stats:
# Call the task with must_archive=True
with patch.object(send_message_task, "update_state"):
result = send_message_task(str(draft_message.id), must_archive=True)
result = send_message_task( # pylint: disable=no-value-for-parameter
str(draft_message.id), must_archive=True
)
# Verify the result
assert result["success"] is True
Binary file not shown.
@@ -1,680 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: lasuite-messages\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-25 11:10+0000\n"
"PO-Revision-Date: 2026-02-03 17:00\n"
"Last-Translator: \n"
"Language-Team: Breton\n"
"Language: br_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=5; plural=(n%10==1 && (n%100!=11 || n%100!=71 || n%100!=91) ? 0 : n%10==2 && (n%100!=12 || n%100!=72 || n%100!=92) ? 1 : ((n%10>=3 && n%10<=4) || n%10==9) && ((n%100 < 10 || n%100 > 19) || (n%100 < 70 || n%100 > 79) || (n%100 < 90 || n%100 > 99)) ? 2 : (n!=0 && n%1;\n"
"X-Crowdin-Project: lasuite-messages\n"
"X-Crowdin-Project-ID: 831182\n"
"X-Crowdin-Language: br-FR\n"
"X-Crowdin-File: backend.pot\n"
"X-Crowdin-File-ID: 40\n"
msgid "delivery status"
msgstr ""
#, python-format
msgid "%(message_count)d messages - Retry send message task queued (id: %(task_id)s)."
msgstr ""
msgid "Retry to send selected messages to pending recipients"
msgstr ""
msgid "Personal info"
msgstr ""
msgid "Permissions"
msgstr ""
msgid "Important dates"
msgstr ""
msgid "Mail domain not found."
msgstr ""
#, python-format
msgid "DNS provisioning successful via %(provider)s: %(changes)s"
msgstr ""
#, python-format
msgid "DNS provisioning successful via %(provider)s (no changes needed)."
msgstr ""
#, python-format
msgid "DNS provisioning failed: %(error)s"
msgstr ""
msgid "Statistics"
msgstr ""
msgid "Metadata"
msgstr ""
msgid "Labels"
msgstr ""
msgid "No labels"
msgstr ""
msgid "Import Messages"
msgstr ""
msgid "Import Messages from IMAP"
msgstr ""
msgid "Message not found."
msgstr ""
msgid "No pending recipients found for this message."
msgstr ""
#, python-format
msgid "Retry task has been queued for %(retryable_recipients_count)d pending recipient(s) (id: %(task_id)s)."
msgstr ""
msgid "Keys"
msgstr ""
msgid "Error"
msgstr ""
msgid "A mailbox with this local part already exists in this domain."
msgstr ""
msgid "Name"
msgstr ""
msgid "Recipient name"
msgstr ""
msgid "messages core application"
msgstr ""
msgid "User account is disabled"
msgstr ""
#, python-format
msgid "Message body size (%(body_size)s MB) exceeds the %(max_size)s MB limit. Please reduce message content."
msgstr ""
#, python-format
msgid "Cannot add attachment(s) (%(new_size)s MB). Total attachments would be %(total_size)s MB, exceeding the %(max_size)s MB limit. Current attachments: %(current_size)s MB."
msgstr ""
#, python-format
msgid "Too many recipients: %(count)s (maximum is %(max)s). Please reduce the number of recipients before sending."
msgstr ""
#, python-format
msgid "Total attachment size (%(total_size)s MB) exceeds the %(max_size)s MB limit. Please remove or reduce attachments."
msgstr ""
#, python-format
msgid "The composed email (%(mime_size)s MB) exceeds the maximum allowed size of %(max_size)s MB. Please reduce message content or attachments."
msgstr ""
msgid "id"
msgstr ""
msgid "primary key for the record as UUID"
msgstr ""
msgid "created on"
msgstr ""
msgid "date and time at which a record was created"
msgstr ""
msgid "updated on"
msgstr ""
msgid "date and time at which a record was last updated"
msgstr ""
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
msgid "sub"
msgstr ""
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr ""
msgid "full name"
msgstr ""
msgid "identity email address"
msgstr ""
msgid "admin email address"
msgstr ""
msgid "language"
msgstr ""
msgid "The language in which the user wants to see the interface."
msgstr ""
msgid "The timezone in which the user wants to see times."
msgstr ""
msgid "staff status"
msgstr ""
msgid "Whether the user can log into this admin site."
msgstr ""
msgid "active"
msgstr ""
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
msgid "Custom attributes"
msgstr ""
msgid "Metadata to sync to the user in the identity provider."
msgstr ""
msgid "user"
msgstr ""
msgid "users"
msgstr ""
msgid "Enter a valid domain name. This value may contain only lowercase letters, numbers, dots and - characters."
msgstr ""
msgid "name"
msgstr ""
msgid "oidc autojoin"
msgstr ""
msgid "Create mailboxes automatically based on OIDC emails."
msgstr ""
msgid "Identity sync"
msgstr ""
msgid "Sync mailboxes to an identity provider."
msgstr ""
msgid "Custom settings"
msgstr ""
msgid "Custom settings for the mail domain."
msgstr ""
msgid "Metadata to sync to the maildomain group in the identity provider."
msgstr ""
msgid "mail domain"
msgstr ""
msgid "mail domains"
msgstr ""
msgid "Human-readable name for this channel"
msgstr ""
msgid "type"
msgstr ""
msgid "Type of channel"
msgstr ""
msgid "settings"
msgstr ""
msgid "Channel-specific configuration settings"
msgstr ""
msgid "Mailbox that receives messages from this channel"
msgstr ""
msgid "Mail domain that owns this channel"
msgstr ""
msgid "channel"
msgstr ""
msgid "channels"
msgstr ""
msgid "local part"
msgstr ""
msgid "is identity"
msgstr ""
msgid "Whether this mailbox identifies a person (i.e. is not an alias or a group)"
msgstr ""
msgid "mailbox"
msgstr ""
msgid "mailboxes"
msgstr ""
msgid "role"
msgstr ""
msgid "accessed at"
msgstr ""
msgid "mailbox access"
msgstr ""
msgid "mailbox accesses"
msgstr ""
msgid "subject"
msgstr ""
msgid "snippet"
msgstr ""
msgid "has unread"
msgstr ""
msgid "has trashed"
msgstr ""
msgid "is trashed"
msgstr ""
msgid "Whether all messages in the thread are trashed"
msgstr ""
msgid "has archived"
msgstr ""
msgid "has draft"
msgstr ""
msgid "has starred"
msgstr ""
msgid "has sender"
msgstr ""
msgid "has messages"
msgstr ""
msgid "has attachments"
msgstr ""
msgid "is spam"
msgstr ""
msgid "has active"
msgstr ""
msgid "messaged at"
msgstr ""
msgid "sender names"
msgstr ""
msgid "summary"
msgstr ""
msgid "thread"
msgstr ""
msgid "threads"
msgstr ""
msgid "Name of the label/folder (can use slashes for hierarchy, e.g. 'Work/Projects')"
msgstr ""
msgid "slug"
msgstr ""
msgid "URL-friendly version of the name"
msgstr ""
msgid "color"
msgstr ""
msgid "Color of the label in hex format (e.g. #FF0000)"
msgstr ""
msgid "Mailbox that owns this label"
msgstr ""
msgid "Threads that have this label"
msgstr ""
msgid "description"
msgstr ""
msgid "Description of the label, used by AI to understand its purpose"
msgstr ""
msgid "auto labeling"
msgstr ""
msgid "Whether this label should be automatically applied by AI"
msgstr ""
msgid "label"
msgstr ""
msgid "labels"
msgstr ""
msgid "thread access"
msgstr ""
msgid "thread accesses"
msgstr ""
msgid "email"
msgstr ""
msgid "contact"
msgstr ""
msgid "contacts"
msgstr ""
msgid "delivered at"
msgstr ""
msgid "delivery message"
msgstr ""
msgid "retry count"
msgstr ""
msgid "retry at"
msgstr ""
msgid "message recipient"
msgstr ""
msgid "message recipients"
msgstr ""
msgid "is draft"
msgstr ""
msgid "is sender"
msgstr ""
msgid "is starred"
msgstr ""
msgid "is unread"
msgstr ""
msgid "is archived"
msgstr ""
msgid "trashed at"
msgstr ""
msgid "sent at"
msgstr ""
msgid "read at"
msgstr ""
msgid "archived at"
msgstr ""
msgid "mime id"
msgstr ""
msgid "Signature template for the message"
msgstr ""
msgid "message"
msgstr ""
msgid "messages"
msgstr ""
msgid "No subject"
msgstr ""
msgid "Message ID"
msgstr ""
msgid "From"
msgstr ""
msgid "To"
msgstr ""
msgid "CC"
msgstr ""
msgid "Date"
msgstr ""
msgid "Subject"
msgstr ""
msgid "Body"
msgstr ""
msgid "raw data"
msgstr ""
msgid "Raw email message bytes"
msgstr ""
msgid "error message"
msgstr ""
msgid "Error message if processing failed"
msgstr ""
msgid "inbound message"
msgstr ""
msgid "inbound messages"
msgstr ""
msgid "sha256 hash"
msgstr ""
msgid "SHA-256 hash of the uncompressed blob content"
msgstr ""
msgid "file size"
msgstr ""
msgid "Size of the blob in bytes"
msgstr ""
msgid "compressed size"
msgstr ""
msgid "Size of the compressed blob in bytes"
msgstr ""
msgid "content type"
msgstr ""
msgid "MIME type of the blob"
msgstr ""
msgid "compression"
msgstr ""
msgid "raw content"
msgstr ""
msgid "Compressed binary content of the blob"
msgstr ""
msgid "Mailbox that owns this blob"
msgstr ""
msgid "Mail domain that owns this blob"
msgstr ""
msgid "blob"
msgstr ""
msgid "blobs"
msgstr ""
msgid "file name"
msgstr ""
msgid "Original filename of the attachment"
msgstr ""
msgid "Reference to the blob containing the attachment data"
msgstr ""
msgid "Mailbox that owns this attachment"
msgstr ""
msgid "Messages that use this attachment"
msgstr ""
msgid "content ID"
msgstr ""
msgid "Content-ID for inline images"
msgstr ""
msgid "attachment"
msgstr ""
msgid "attachments"
msgstr ""
msgid "mail domain access"
msgstr ""
msgid "mail domain accesses"
msgstr ""
msgid "selector"
msgstr ""
msgid "DKIM selector (e.g., 'default', 'mail')"
msgstr ""
msgid "private key"
msgstr ""
msgid "DKIM private key in PEM format (encrypted)"
msgstr ""
msgid "public key"
msgstr ""
msgid "DKIM public key for DNS record generation"
msgstr ""
msgid "algorithm"
msgstr ""
msgid "DKIM signing algorithm"
msgstr ""
msgid "key size"
msgstr ""
msgid "Key size in bits (e.g., 2048, 4096 for RSA)"
msgstr ""
msgid "is active"
msgstr ""
msgid "Whether this DKIM key is active and should be used for signing"
msgstr ""
msgid "Domain that owns this DKIM key"
msgstr ""
msgid "DKIM key"
msgstr ""
msgid "DKIM keys"
msgstr ""
msgid "Name of the template (e.g., 'Standard Reply', 'Out of Office', 'Work Signature')"
msgstr ""
msgid "Reference to the blob containing template content as JSON: {html: str, text: str, raw: any}"
msgstr ""
msgid "Type of template (message, signature)"
msgstr ""
msgid "Whether this template is available for use"
msgstr ""
msgid "Mail domain that can use this template"
msgstr ""
msgid "Mailbox that can use this template"
msgstr ""
msgid "is forced"
msgstr ""
msgid "Whether this template is forced; no other template of the same type can be used in the same scope"
msgstr ""
msgid "is default"
msgstr ""
msgid "Whether this template is the default; it will be automatically loaded when composing a new message"
msgstr ""
msgid "message template"
msgstr ""
msgid "message templates"
msgstr ""
msgid "Provision DNS records"
msgstr ""
msgid "Retry to send to pending recipients"
msgstr ""
msgid "Import from IMAP"
msgstr ""
msgid "Home"
msgstr ""
msgid "Please correct the error below."
msgstr ""
msgid "Please correct the errors below."
msgstr ""
msgid "Start Import"
msgstr ""
msgid "English"
msgstr ""
msgid "French"
msgstr ""
msgid "Dutch"
msgstr ""
Binary file not shown.
@@ -1,680 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: lasuite-messages\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-25 11:10+0000\n"
"PO-Revision-Date: 2026-02-03 17:00\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: lasuite-messages\n"
"X-Crowdin-Project-ID: 831182\n"
"X-Crowdin-Language: de\n"
"X-Crowdin-File: backend.pot\n"
"X-Crowdin-File-ID: 40\n"
msgid "delivery status"
msgstr ""
#, python-format
msgid "%(message_count)d messages - Retry send message task queued (id: %(task_id)s)."
msgstr ""
msgid "Retry to send selected messages to pending recipients"
msgstr ""
msgid "Personal info"
msgstr "Persönliche Daten"
msgid "Permissions"
msgstr "Berechtigungen"
msgid "Important dates"
msgstr "Wichtige Daten"
msgid "Mail domain not found."
msgstr ""
#, python-format
msgid "DNS provisioning successful via %(provider)s: %(changes)s"
msgstr ""
#, python-format
msgid "DNS provisioning successful via %(provider)s (no changes needed)."
msgstr ""
#, python-format
msgid "DNS provisioning failed: %(error)s"
msgstr ""
msgid "Statistics"
msgstr ""
msgid "Metadata"
msgstr ""
msgid "Labels"
msgstr ""
msgid "No labels"
msgstr ""
msgid "Import Messages"
msgstr ""
msgid "Import Messages from IMAP"
msgstr ""
msgid "Message not found."
msgstr ""
msgid "No pending recipients found for this message."
msgstr ""
#, python-format
msgid "Retry task has been queued for %(retryable_recipients_count)d pending recipient(s) (id: %(task_id)s)."
msgstr ""
msgid "Keys"
msgstr ""
msgid "Error"
msgstr ""
msgid "A mailbox with this local part already exists in this domain."
msgstr ""
msgid "Name"
msgstr ""
msgid "Recipient name"
msgstr ""
msgid "messages core application"
msgstr ""
msgid "User account is disabled"
msgstr ""
#, python-format
msgid "Message body size (%(body_size)s MB) exceeds the %(max_size)s MB limit. Please reduce message content."
msgstr ""
#, python-format
msgid "Cannot add attachment(s) (%(new_size)s MB). Total attachments would be %(total_size)s MB, exceeding the %(max_size)s MB limit. Current attachments: %(current_size)s MB."
msgstr ""
#, python-format
msgid "Too many recipients: %(count)s (maximum is %(max)s). Please reduce the number of recipients before sending."
msgstr ""
#, python-format
msgid "Total attachment size (%(total_size)s MB) exceeds the %(max_size)s MB limit. Please remove or reduce attachments."
msgstr ""
#, python-format
msgid "The composed email (%(mime_size)s MB) exceeds the maximum allowed size of %(max_size)s MB. Please reduce message content or attachments."
msgstr ""
msgid "id"
msgstr ""
msgid "primary key for the record as UUID"
msgstr ""
msgid "created on"
msgstr "Erstellt"
msgid "date and time at which a record was created"
msgstr ""
msgid "updated on"
msgstr "Aktualisiert"
msgid "date and time at which a record was last updated"
msgstr ""
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
msgid "sub"
msgstr ""
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr ""
msgid "full name"
msgstr ""
msgid "identity email address"
msgstr ""
msgid "admin email address"
msgstr ""
msgid "language"
msgstr "Sprache"
msgid "The language in which the user wants to see the interface."
msgstr ""
msgid "The timezone in which the user wants to see times."
msgstr ""
msgid "staff status"
msgstr ""
msgid "Whether the user can log into this admin site."
msgstr ""
msgid "active"
msgstr ""
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
msgid "Custom attributes"
msgstr ""
msgid "Metadata to sync to the user in the identity provider."
msgstr ""
msgid "user"
msgstr "Benutzer"
msgid "users"
msgstr "Benutzer"
msgid "Enter a valid domain name. This value may contain only lowercase letters, numbers, dots and - characters."
msgstr ""
msgid "name"
msgstr ""
msgid "oidc autojoin"
msgstr ""
msgid "Create mailboxes automatically based on OIDC emails."
msgstr ""
msgid "Identity sync"
msgstr ""
msgid "Sync mailboxes to an identity provider."
msgstr ""
msgid "Custom settings"
msgstr ""
msgid "Custom settings for the mail domain."
msgstr ""
msgid "Metadata to sync to the maildomain group in the identity provider."
msgstr ""
msgid "mail domain"
msgstr ""
msgid "mail domains"
msgstr ""
msgid "Human-readable name for this channel"
msgstr ""
msgid "type"
msgstr ""
msgid "Type of channel"
msgstr ""
msgid "settings"
msgstr ""
msgid "Channel-specific configuration settings"
msgstr ""
msgid "Mailbox that receives messages from this channel"
msgstr ""
msgid "Mail domain that owns this channel"
msgstr ""
msgid "channel"
msgstr ""
msgid "channels"
msgstr ""
msgid "local part"
msgstr ""
msgid "is identity"
msgstr ""
msgid "Whether this mailbox identifies a person (i.e. is not an alias or a group)"
msgstr ""
msgid "mailbox"
msgstr "Briefkasten"
msgid "mailboxes"
msgstr ""
msgid "role"
msgstr ""
msgid "accessed at"
msgstr ""
msgid "mailbox access"
msgstr ""
msgid "mailbox accesses"
msgstr ""
msgid "subject"
msgstr ""
msgid "snippet"
msgstr ""
msgid "has unread"
msgstr ""
msgid "has trashed"
msgstr ""
msgid "is trashed"
msgstr ""
msgid "Whether all messages in the thread are trashed"
msgstr ""
msgid "has archived"
msgstr ""
msgid "has draft"
msgstr ""
msgid "has starred"
msgstr ""
msgid "has sender"
msgstr ""
msgid "has messages"
msgstr ""
msgid "has attachments"
msgstr ""
msgid "is spam"
msgstr ""
msgid "has active"
msgstr ""
msgid "messaged at"
msgstr ""
msgid "sender names"
msgstr ""
msgid "summary"
msgstr ""
msgid "thread"
msgstr ""
msgid "threads"
msgstr ""
msgid "Name of the label/folder (can use slashes for hierarchy, e.g. 'Work/Projects')"
msgstr ""
msgid "slug"
msgstr ""
msgid "URL-friendly version of the name"
msgstr ""
msgid "color"
msgstr "Farbe"
msgid "Color of the label in hex format (e.g. #FF0000)"
msgstr ""
msgid "Mailbox that owns this label"
msgstr ""
msgid "Threads that have this label"
msgstr ""
msgid "description"
msgstr "Beschreibung"
msgid "Description of the label, used by AI to understand its purpose"
msgstr ""
msgid "auto labeling"
msgstr ""
msgid "Whether this label should be automatically applied by AI"
msgstr ""
msgid "label"
msgstr ""
msgid "labels"
msgstr ""
msgid "thread access"
msgstr ""
msgid "thread accesses"
msgstr ""
msgid "email"
msgstr "E-Mail"
msgid "contact"
msgstr ""
msgid "contacts"
msgstr ""
msgid "delivered at"
msgstr ""
msgid "delivery message"
msgstr ""
msgid "retry count"
msgstr ""
msgid "retry at"
msgstr ""
msgid "message recipient"
msgstr ""
msgid "message recipients"
msgstr ""
msgid "is draft"
msgstr ""
msgid "is sender"
msgstr ""
msgid "is starred"
msgstr ""
msgid "is unread"
msgstr ""
msgid "is archived"
msgstr ""
msgid "trashed at"
msgstr ""
msgid "sent at"
msgstr ""
msgid "read at"
msgstr ""
msgid "archived at"
msgstr ""
msgid "mime id"
msgstr ""
msgid "Signature template for the message"
msgstr ""
msgid "message"
msgstr "Nachricht"
msgid "messages"
msgstr "Nachrichten"
msgid "No subject"
msgstr ""
msgid "Message ID"
msgstr ""
msgid "From"
msgstr ""
msgid "To"
msgstr ""
msgid "CC"
msgstr ""
msgid "Date"
msgstr ""
msgid "Subject"
msgstr ""
msgid "Body"
msgstr ""
msgid "raw data"
msgstr ""
msgid "Raw email message bytes"
msgstr ""
msgid "error message"
msgstr ""
msgid "Error message if processing failed"
msgstr ""
msgid "inbound message"
msgstr ""
msgid "inbound messages"
msgstr ""
msgid "sha256 hash"
msgstr ""
msgid "SHA-256 hash of the uncompressed blob content"
msgstr ""
msgid "file size"
msgstr ""
msgid "Size of the blob in bytes"
msgstr ""
msgid "compressed size"
msgstr ""
msgid "Size of the compressed blob in bytes"
msgstr ""
msgid "content type"
msgstr ""
msgid "MIME type of the blob"
msgstr ""
msgid "compression"
msgstr ""
msgid "raw content"
msgstr ""
msgid "Compressed binary content of the blob"
msgstr ""
msgid "Mailbox that owns this blob"
msgstr ""
msgid "Mail domain that owns this blob"
msgstr ""
msgid "blob"
msgstr ""
msgid "blobs"
msgstr ""
msgid "file name"
msgstr ""
msgid "Original filename of the attachment"
msgstr ""
msgid "Reference to the blob containing the attachment data"
msgstr ""
msgid "Mailbox that owns this attachment"
msgstr ""
msgid "Messages that use this attachment"
msgstr ""
msgid "content ID"
msgstr ""
msgid "Content-ID for inline images"
msgstr ""
msgid "attachment"
msgstr "Anhang"
msgid "attachments"
msgstr ""
msgid "mail domain access"
msgstr ""
msgid "mail domain accesses"
msgstr ""
msgid "selector"
msgstr ""
msgid "DKIM selector (e.g., 'default', 'mail')"
msgstr ""
msgid "private key"
msgstr ""
msgid "DKIM private key in PEM format (encrypted)"
msgstr ""
msgid "public key"
msgstr "Öffentlicher Schlüssel"
msgid "DKIM public key for DNS record generation"
msgstr ""
msgid "algorithm"
msgstr ""
msgid "DKIM signing algorithm"
msgstr ""
msgid "key size"
msgstr ""
msgid "Key size in bits (e.g., 2048, 4096 for RSA)"
msgstr ""
msgid "is active"
msgstr ""
msgid "Whether this DKIM key is active and should be used for signing"
msgstr ""
msgid "Domain that owns this DKIM key"
msgstr ""
msgid "DKIM key"
msgstr ""
msgid "DKIM keys"
msgstr ""
msgid "Name of the template (e.g., 'Standard Reply', 'Out of Office', 'Work Signature')"
msgstr ""
msgid "Reference to the blob containing template content as JSON: {html: str, text: str, raw: any}"
msgstr ""
msgid "Type of template (message, signature)"
msgstr ""
msgid "Whether this template is available for use"
msgstr ""
msgid "Mail domain that can use this template"
msgstr ""
msgid "Mailbox that can use this template"
msgstr ""
msgid "is forced"
msgstr ""
msgid "Whether this template is forced; no other template of the same type can be used in the same scope"
msgstr ""
msgid "is default"
msgstr ""
msgid "Whether this template is the default; it will be automatically loaded when composing a new message"
msgstr ""
msgid "message template"
msgstr ""
msgid "message templates"
msgstr ""
msgid "Provision DNS records"
msgstr ""
msgid "Retry to send to pending recipients"
msgstr ""
msgid "Import from IMAP"
msgstr ""
msgid "Home"
msgstr "Start"
msgid "Please correct the error below."
msgstr ""
msgid "Please correct the errors below."
msgstr ""
msgid "Start Import"
msgstr ""
msgid "English"
msgstr ""
msgid "French"
msgstr "Französisch"
msgid "Dutch"
msgstr "Niederländisch"
Binary file not shown.
@@ -1,736 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-11 17:50+0000\n"
"PO-Revision-Date: 2025-01-27 09:27\n"
"Last-Translator: \n"
"Language-Team: English\n"
"Language: en_US\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: lasuite-docs\n"
"X-Crowdin-Project-ID: 754523\n"
"X-Crowdin-Language: en\n"
"X-Crowdin-File: backend-drive.pot\n"
"X-Crowdin-File-ID: 18\n"
msgid "delivery status"
msgstr ""
#, python-format
msgid ""
"%(message_count)d messages - Retry send message task queued (id: "
"%(task_id)s)."
msgstr ""
msgid "Retry to send selected messages to pending recipients"
msgstr ""
msgid "Personal info"
msgstr ""
msgid "Permissions"
msgstr ""
msgid "Important dates"
msgstr ""
msgid "Mail domain not found."
msgstr ""
#, python-format
msgid "DNS provisioning successful via %(provider)s: %(changes)s"
msgstr ""
#, python-format
msgid "DNS provisioning successful via %(provider)s (no changes needed)."
msgstr ""
#, python-format
msgid "DNS provisioning failed: %(error)s"
msgstr ""
msgid "Statistics"
msgstr ""
msgid "Metadata"
msgstr ""
msgid "Labels"
msgstr ""
msgid "No labels"
msgstr ""
msgid "Import Messages"
msgstr ""
msgid "Import Messages from IMAP"
msgstr ""
msgid "Message not found."
msgstr ""
msgid "No pending recipients found for this message."
msgstr ""
#, python-format
msgid ""
"Retry task has been queued for %(retryable_recipients_count)d pending "
"recipient(s) (id: %(task_id)s)."
msgstr ""
msgid "Keys"
msgstr ""
msgid "Error"
msgstr ""
msgid "A mailbox with this local part already exists in this domain."
msgstr ""
#, python-format
msgid "Image \"%(name)s\" (%(size)s MB) exceeds the %(max)s MB limit."
msgstr ""
msgid "Name"
msgstr ""
msgid "Recipient name"
msgstr ""
msgid "messages core application"
msgstr ""
msgid "User account is disabled"
msgstr ""
#, python-format
msgid ""
"Message body size (%(body_size)s MB) exceeds the %(max_size)s MB limit. "
"Please reduce message content."
msgstr ""
#, python-format
msgid ""
"Cannot add attachment(s) (%(new_size)s MB). Total attachments would be "
"%(total_size)s MB, exceeding the %(max_size)s MB limit. Current attachments: "
"%(current_size)s MB."
msgstr ""
#, python-format
msgid ""
"Too many recipients: %(count)s (maximum is %(max)s). Please reduce the "
"number of recipients before sending."
msgstr ""
#, python-format
msgid ""
"Total attachment size (%(total_size)s MB) exceeds the %(max_size)s MB limit. "
"Please remove or reduce attachments."
msgstr ""
#, python-format
msgid ""
"The composed email (%(mime_size)s MB) exceeds the maximum allowed size of "
"%(max_size)s MB. Please reduce message content or attachments."
msgstr ""
msgid "id"
msgstr ""
msgid "primary key for the record as UUID"
msgstr ""
msgid "created on"
msgstr ""
msgid "date and time at which a record was created"
msgstr ""
msgid "updated on"
msgstr ""
msgid "date and time at which a record was last updated"
msgstr ""
msgid ""
"We couldn't find a user with this sub but the email is already associated "
"with a registered user."
msgstr ""
msgid ""
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
"_/: characters."
msgstr ""
msgid "sub"
msgstr ""
msgid ""
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: "
"characters only."
msgstr ""
msgid "full name"
msgstr ""
msgid "identity email address"
msgstr ""
msgid "admin email address"
msgstr ""
msgid "language"
msgstr ""
msgid "The language in which the user wants to see the interface."
msgstr ""
msgid "The timezone in which the user wants to see times."
msgstr ""
msgid "staff status"
msgstr ""
msgid "Whether the user can log into this admin site."
msgstr ""
msgid "active"
msgstr ""
msgid ""
"Whether this user should be treated as active. Unselect this instead of "
"deleting accounts."
msgstr ""
msgid "Custom attributes"
msgstr ""
msgid "Metadata to sync to the user in the identity provider."
msgstr ""
msgid "user"
msgstr ""
msgid "users"
msgstr ""
msgid ""
"Enter a valid domain name. This value may contain only lowercase letters, "
"numbers, dots and - characters."
msgstr ""
msgid "name"
msgstr ""
msgid "oidc autojoin"
msgstr ""
msgid "Create mailboxes automatically based on OIDC emails."
msgstr ""
msgid "Identity sync"
msgstr ""
msgid "Sync mailboxes to an identity provider."
msgstr ""
msgid "Custom settings"
msgstr ""
msgid "Custom settings for the mail domain."
msgstr ""
msgid "Metadata to sync to the maildomain group in the identity provider."
msgstr ""
msgid "mail domain"
msgstr ""
msgid "mail domains"
msgstr ""
msgid "Human-readable name for this channel"
msgstr ""
msgid "type"
msgstr ""
msgid "Type of channel"
msgstr ""
msgid "settings"
msgstr ""
msgid "Channel-specific configuration settings"
msgstr ""
msgid "Mailbox that receives messages from this channel"
msgstr ""
msgid "Mail domain that owns this channel"
msgstr ""
msgid "channel"
msgstr ""
msgid "channels"
msgstr ""
msgid "local part"
msgstr ""
msgid "is identity"
msgstr ""
msgid ""
"Whether this mailbox identifies a person (i.e. is not an alias or a group)"
msgstr ""
msgid "mailbox"
msgstr ""
msgid "mailboxes"
msgstr ""
msgid "role"
msgstr ""
msgid "accessed at"
msgstr ""
msgid "mailbox access"
msgstr ""
msgid "mailbox accesses"
msgstr ""
msgid "subject"
msgstr ""
msgid "snippet"
msgstr ""
msgid "has unread"
msgstr ""
msgid "has trashed"
msgstr ""
msgid "is trashed"
msgstr ""
msgid "Whether all messages in the thread are trashed"
msgstr ""
msgid "has archived"
msgstr ""
msgid "has draft"
msgstr ""
msgid "has starred"
msgstr ""
msgid "has sender"
msgstr ""
msgid "has messages"
msgstr ""
msgid "has attachments"
msgstr ""
msgid "is spam"
msgstr ""
msgid "has active"
msgstr ""
msgid "has delivery pending"
msgstr ""
msgid ""
"True if thread has messages awaiting successful delivery (sending, retrying, "
"or failed)."
msgstr ""
msgid "has delivery failed"
msgstr ""
msgid "True if thread has messages with permanent delivery failure."
msgstr ""
msgid "messaged at"
msgstr ""
msgid "sender names"
msgstr ""
msgid "summary"
msgstr ""
msgid "thread"
msgstr ""
msgid "threads"
msgstr ""
msgid ""
"Name of the label/folder (can use slashes for hierarchy, e.g. 'Work/"
"Projects')"
msgstr ""
msgid "slug"
msgstr ""
msgid "URL-friendly version of the name"
msgstr ""
msgid "color"
msgstr ""
msgid "Color of the label in hex format (e.g. #FF0000)"
msgstr ""
msgid "Mailbox that owns this label"
msgstr ""
msgid "Threads that have this label"
msgstr ""
msgid "description"
msgstr ""
msgid "Description of the label, used by AI to understand its purpose"
msgstr ""
msgid "auto labeling"
msgstr ""
msgid "Whether this label should be automatically applied by AI"
msgstr ""
msgid "label"
msgstr ""
msgid "labels"
msgstr ""
msgid "thread access"
msgstr ""
msgid "thread accesses"
msgstr ""
msgid "email"
msgstr ""
msgid "contact"
msgstr ""
msgid "contacts"
msgstr ""
msgid "delivered at"
msgstr ""
msgid "delivery message"
msgstr ""
msgid "retry count"
msgstr ""
msgid "retry at"
msgstr ""
msgid "message recipient"
msgstr ""
msgid "message recipients"
msgstr ""
msgid "is draft"
msgstr ""
msgid "is sender"
msgstr ""
msgid "is starred"
msgstr ""
msgid "is unread"
msgstr ""
msgid "is archived"
msgstr ""
msgid "trashed at"
msgstr ""
msgid "sent at"
msgstr ""
msgid "read at"
msgstr ""
msgid "archived at"
msgstr ""
msgid "mime id"
msgstr ""
msgid "Signature template for the message"
msgstr ""
msgid "message"
msgstr ""
msgid "messages"
msgstr ""
msgid "No subject"
msgstr ""
msgid "Message ID"
msgstr ""
msgid "From"
msgstr ""
msgid "To"
msgstr ""
msgid "CC"
msgstr ""
msgid "Date"
msgstr ""
msgid "Subject"
msgstr ""
msgid "Body"
msgstr ""
msgid "raw data"
msgstr ""
msgid "Raw email message bytes"
msgstr ""
msgid "error message"
msgstr ""
msgid "Error message if processing failed"
msgstr ""
msgid "inbound message"
msgstr ""
msgid "inbound messages"
msgstr ""
msgid "sha256 hash"
msgstr ""
msgid "SHA-256 hash of the uncompressed blob content"
msgstr ""
msgid "file size"
msgstr ""
msgid "Size of the blob in bytes"
msgstr ""
msgid "compressed size"
msgstr ""
msgid "Size of the compressed blob in bytes"
msgstr ""
msgid "content type"
msgstr ""
msgid "MIME type of the blob"
msgstr ""
msgid "compression"
msgstr ""
msgid "raw content"
msgstr ""
msgid "Compressed binary content of the blob"
msgstr ""
msgid "Mailbox that owns this blob"
msgstr ""
msgid "Mail domain that owns this blob"
msgstr ""
msgid "blob"
msgstr ""
msgid "blobs"
msgstr ""
msgid "file name"
msgstr ""
msgid "Original filename of the attachment"
msgstr ""
msgid "Reference to the blob containing the attachment data"
msgstr ""
msgid "Mailbox that owns this attachment"
msgstr ""
msgid "Messages that use this attachment"
msgstr ""
msgid "content ID"
msgstr ""
msgid "Content-ID for inline images"
msgstr ""
msgid "attachment"
msgstr ""
msgid "attachments"
msgstr ""
msgid "mail domain access"
msgstr ""
msgid "mail domain accesses"
msgstr ""
msgid "selector"
msgstr ""
msgid "DKIM selector (e.g., 'default', 'mail')"
msgstr ""
msgid "private key"
msgstr ""
msgid "DKIM private key in PEM format (encrypted)"
msgstr ""
msgid "public key"
msgstr ""
msgid "DKIM public key for DNS record generation"
msgstr ""
msgid "algorithm"
msgstr ""
msgid "DKIM signing algorithm"
msgstr ""
msgid "key size"
msgstr ""
msgid "Key size in bits (e.g., 2048, 4096 for RSA)"
msgstr ""
msgid "is active"
msgstr ""
msgid "Whether this DKIM key is active and should be used for signing"
msgstr ""
msgid "Domain that owns this DKIM key"
msgstr ""
msgid "DKIM key"
msgstr ""
msgid "DKIM keys"
msgstr ""
msgid ""
"Name of the template (e.g., 'Standard Reply', 'Out of Office', 'Work "
"Signature')"
msgstr ""
msgid ""
"Reference to the blob containing template content as JSON: {html: str, text: "
"str, raw: any}"
msgstr ""
msgid "Type of template (message, signature)"
msgstr ""
msgid "Whether this template is available for use"
msgstr ""
msgid "Mail domain that can use this template"
msgstr ""
msgid "Mailbox that can use this template"
msgstr ""
msgid "is forced"
msgstr ""
msgid ""
"Whether this template is forced; no other template of the same type can be "
"used in the same scope"
msgstr ""
msgid "is default"
msgstr ""
msgid ""
"Whether this template is the default; it will be automatically loaded when "
"composing a new message"
msgstr ""
msgid "message template"
msgstr ""
msgid "message templates"
msgstr ""
msgid "Provision DNS records"
msgstr ""
msgid "Retry to send to pending recipients"
msgstr ""
msgid "Import from IMAP"
msgstr ""
msgid "Home"
msgstr ""
msgid "Please correct the error below."
msgstr ""
msgid "Please correct the errors below."
msgstr ""
msgid "Start Import"
msgstr ""
msgid "English"
msgstr ""
msgid "French"
msgstr ""
msgid "Dutch"
msgstr ""
#~ msgid "Full name"
#~ msgstr "Full name"
Binary file not shown.
@@ -1,680 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: lasuite-messages\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-25 11:10+0000\n"
"PO-Revision-Date: 2026-02-03 17:00\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: lasuite-messages\n"
"X-Crowdin-Project-ID: 831182\n"
"X-Crowdin-Language: es-ES\n"
"X-Crowdin-File: backend.pot\n"
"X-Crowdin-File-ID: 40\n"
msgid "delivery status"
msgstr ""
#, python-format
msgid "%(message_count)d messages - Retry send message task queued (id: %(task_id)s)."
msgstr ""
msgid "Retry to send selected messages to pending recipients"
msgstr ""
msgid "Personal info"
msgstr ""
msgid "Permissions"
msgstr ""
msgid "Important dates"
msgstr ""
msgid "Mail domain not found."
msgstr ""
#, python-format
msgid "DNS provisioning successful via %(provider)s: %(changes)s"
msgstr ""
#, python-format
msgid "DNS provisioning successful via %(provider)s (no changes needed)."
msgstr ""
#, python-format
msgid "DNS provisioning failed: %(error)s"
msgstr ""
msgid "Statistics"
msgstr ""
msgid "Metadata"
msgstr ""
msgid "Labels"
msgstr ""
msgid "No labels"
msgstr ""
msgid "Import Messages"
msgstr ""
msgid "Import Messages from IMAP"
msgstr ""
msgid "Message not found."
msgstr ""
msgid "No pending recipients found for this message."
msgstr ""
#, python-format
msgid "Retry task has been queued for %(retryable_recipients_count)d pending recipient(s) (id: %(task_id)s)."
msgstr ""
msgid "Keys"
msgstr ""
msgid "Error"
msgstr ""
msgid "A mailbox with this local part already exists in this domain."
msgstr ""
msgid "Name"
msgstr ""
msgid "Recipient name"
msgstr ""
msgid "messages core application"
msgstr ""
msgid "User account is disabled"
msgstr ""
#, python-format
msgid "Message body size (%(body_size)s MB) exceeds the %(max_size)s MB limit. Please reduce message content."
msgstr ""
#, python-format
msgid "Cannot add attachment(s) (%(new_size)s MB). Total attachments would be %(total_size)s MB, exceeding the %(max_size)s MB limit. Current attachments: %(current_size)s MB."
msgstr ""
#, python-format
msgid "Too many recipients: %(count)s (maximum is %(max)s). Please reduce the number of recipients before sending."
msgstr ""
#, python-format
msgid "Total attachment size (%(total_size)s MB) exceeds the %(max_size)s MB limit. Please remove or reduce attachments."
msgstr ""
#, python-format
msgid "The composed email (%(mime_size)s MB) exceeds the maximum allowed size of %(max_size)s MB. Please reduce message content or attachments."
msgstr ""
msgid "id"
msgstr ""
msgid "primary key for the record as UUID"
msgstr ""
msgid "created on"
msgstr ""
msgid "date and time at which a record was created"
msgstr ""
msgid "updated on"
msgstr ""
msgid "date and time at which a record was last updated"
msgstr ""
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
msgid "sub"
msgstr ""
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr ""
msgid "full name"
msgstr ""
msgid "identity email address"
msgstr ""
msgid "admin email address"
msgstr ""
msgid "language"
msgstr ""
msgid "The language in which the user wants to see the interface."
msgstr ""
msgid "The timezone in which the user wants to see times."
msgstr ""
msgid "staff status"
msgstr ""
msgid "Whether the user can log into this admin site."
msgstr ""
msgid "active"
msgstr ""
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
msgid "Custom attributes"
msgstr ""
msgid "Metadata to sync to the user in the identity provider."
msgstr ""
msgid "user"
msgstr ""
msgid "users"
msgstr ""
msgid "Enter a valid domain name. This value may contain only lowercase letters, numbers, dots and - characters."
msgstr ""
msgid "name"
msgstr ""
msgid "oidc autojoin"
msgstr ""
msgid "Create mailboxes automatically based on OIDC emails."
msgstr ""
msgid "Identity sync"
msgstr ""
msgid "Sync mailboxes to an identity provider."
msgstr ""
msgid "Custom settings"
msgstr ""
msgid "Custom settings for the mail domain."
msgstr ""
msgid "Metadata to sync to the maildomain group in the identity provider."
msgstr ""
msgid "mail domain"
msgstr ""
msgid "mail domains"
msgstr ""
msgid "Human-readable name for this channel"
msgstr ""
msgid "type"
msgstr ""
msgid "Type of channel"
msgstr ""
msgid "settings"
msgstr ""
msgid "Channel-specific configuration settings"
msgstr ""
msgid "Mailbox that receives messages from this channel"
msgstr ""
msgid "Mail domain that owns this channel"
msgstr ""
msgid "channel"
msgstr ""
msgid "channels"
msgstr ""
msgid "local part"
msgstr ""
msgid "is identity"
msgstr ""
msgid "Whether this mailbox identifies a person (i.e. is not an alias or a group)"
msgstr ""
msgid "mailbox"
msgstr ""
msgid "mailboxes"
msgstr ""
msgid "role"
msgstr ""
msgid "accessed at"
msgstr ""
msgid "mailbox access"
msgstr ""
msgid "mailbox accesses"
msgstr ""
msgid "subject"
msgstr ""
msgid "snippet"
msgstr ""
msgid "has unread"
msgstr ""
msgid "has trashed"
msgstr ""
msgid "is trashed"
msgstr ""
msgid "Whether all messages in the thread are trashed"
msgstr ""
msgid "has archived"
msgstr ""
msgid "has draft"
msgstr ""
msgid "has starred"
msgstr ""
msgid "has sender"
msgstr ""
msgid "has messages"
msgstr ""
msgid "has attachments"
msgstr ""
msgid "is spam"
msgstr ""
msgid "has active"
msgstr ""
msgid "messaged at"
msgstr ""
msgid "sender names"
msgstr ""
msgid "summary"
msgstr ""
msgid "thread"
msgstr ""
msgid "threads"
msgstr ""
msgid "Name of the label/folder (can use slashes for hierarchy, e.g. 'Work/Projects')"
msgstr ""
msgid "slug"
msgstr ""
msgid "URL-friendly version of the name"
msgstr ""
msgid "color"
msgstr ""
msgid "Color of the label in hex format (e.g. #FF0000)"
msgstr ""
msgid "Mailbox that owns this label"
msgstr ""
msgid "Threads that have this label"
msgstr ""
msgid "description"
msgstr ""
msgid "Description of the label, used by AI to understand its purpose"
msgstr ""
msgid "auto labeling"
msgstr ""
msgid "Whether this label should be automatically applied by AI"
msgstr ""
msgid "label"
msgstr ""
msgid "labels"
msgstr ""
msgid "thread access"
msgstr ""
msgid "thread accesses"
msgstr ""
msgid "email"
msgstr ""
msgid "contact"
msgstr ""
msgid "contacts"
msgstr ""
msgid "delivered at"
msgstr ""
msgid "delivery message"
msgstr ""
msgid "retry count"
msgstr ""
msgid "retry at"
msgstr ""
msgid "message recipient"
msgstr ""
msgid "message recipients"
msgstr ""
msgid "is draft"
msgstr ""
msgid "is sender"
msgstr ""
msgid "is starred"
msgstr ""
msgid "is unread"
msgstr ""
msgid "is archived"
msgstr ""
msgid "trashed at"
msgstr ""
msgid "sent at"
msgstr ""
msgid "read at"
msgstr ""
msgid "archived at"
msgstr ""
msgid "mime id"
msgstr ""
msgid "Signature template for the message"
msgstr ""
msgid "message"
msgstr ""
msgid "messages"
msgstr ""
msgid "No subject"
msgstr ""
msgid "Message ID"
msgstr ""
msgid "From"
msgstr ""
msgid "To"
msgstr ""
msgid "CC"
msgstr ""
msgid "Date"
msgstr ""
msgid "Subject"
msgstr ""
msgid "Body"
msgstr ""
msgid "raw data"
msgstr ""
msgid "Raw email message bytes"
msgstr ""
msgid "error message"
msgstr ""
msgid "Error message if processing failed"
msgstr ""
msgid "inbound message"
msgstr ""
msgid "inbound messages"
msgstr ""
msgid "sha256 hash"
msgstr ""
msgid "SHA-256 hash of the uncompressed blob content"
msgstr ""
msgid "file size"
msgstr ""
msgid "Size of the blob in bytes"
msgstr ""
msgid "compressed size"
msgstr ""
msgid "Size of the compressed blob in bytes"
msgstr ""
msgid "content type"
msgstr ""
msgid "MIME type of the blob"
msgstr ""
msgid "compression"
msgstr ""
msgid "raw content"
msgstr ""
msgid "Compressed binary content of the blob"
msgstr ""
msgid "Mailbox that owns this blob"
msgstr ""
msgid "Mail domain that owns this blob"
msgstr ""
msgid "blob"
msgstr ""
msgid "blobs"
msgstr ""
msgid "file name"
msgstr ""
msgid "Original filename of the attachment"
msgstr ""
msgid "Reference to the blob containing the attachment data"
msgstr ""
msgid "Mailbox that owns this attachment"
msgstr ""
msgid "Messages that use this attachment"
msgstr ""
msgid "content ID"
msgstr ""
msgid "Content-ID for inline images"
msgstr ""
msgid "attachment"
msgstr ""
msgid "attachments"
msgstr ""
msgid "mail domain access"
msgstr ""
msgid "mail domain accesses"
msgstr ""
msgid "selector"
msgstr ""
msgid "DKIM selector (e.g., 'default', 'mail')"
msgstr ""
msgid "private key"
msgstr ""
msgid "DKIM private key in PEM format (encrypted)"
msgstr ""
msgid "public key"
msgstr ""
msgid "DKIM public key for DNS record generation"
msgstr ""
msgid "algorithm"
msgstr ""
msgid "DKIM signing algorithm"
msgstr ""
msgid "key size"
msgstr ""
msgid "Key size in bits (e.g., 2048, 4096 for RSA)"
msgstr ""
msgid "is active"
msgstr ""
msgid "Whether this DKIM key is active and should be used for signing"
msgstr ""
msgid "Domain that owns this DKIM key"
msgstr ""
msgid "DKIM key"
msgstr ""
msgid "DKIM keys"
msgstr ""
msgid "Name of the template (e.g., 'Standard Reply', 'Out of Office', 'Work Signature')"
msgstr ""
msgid "Reference to the blob containing template content as JSON: {html: str, text: str, raw: any}"
msgstr ""
msgid "Type of template (message, signature)"
msgstr ""
msgid "Whether this template is available for use"
msgstr ""
msgid "Mail domain that can use this template"
msgstr ""
msgid "Mailbox that can use this template"
msgstr ""
msgid "is forced"
msgstr ""
msgid "Whether this template is forced; no other template of the same type can be used in the same scope"
msgstr ""
msgid "is default"
msgstr ""
msgid "Whether this template is the default; it will be automatically loaded when composing a new message"
msgstr ""
msgid "message template"
msgstr ""
msgid "message templates"
msgstr ""
msgid "Provision DNS records"
msgstr ""
msgid "Retry to send to pending recipients"
msgstr ""
msgid "Import from IMAP"
msgstr ""
msgid "Home"
msgstr ""
msgid "Please correct the error below."
msgstr ""
msgid "Please correct the errors below."
msgstr ""
msgid "Start Import"
msgstr ""
msgid "English"
msgstr ""
msgid "French"
msgstr ""
msgid "Dutch"
msgstr ""
Binary file not shown.
@@ -1,776 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: lasuite-messages\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-11 17:50+0000\n"
"PO-Revision-Date: 2026-02-03 17:00\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Crowdin-Project: lasuite-messages\n"
"X-Crowdin-Project-ID: 831182\n"
"X-Crowdin-Language: fr\n"
"X-Crowdin-File: backend.pot\n"
"X-Crowdin-File-ID: 40\n"
msgid "delivery status"
msgstr "statut de livraison"
#, python-format
msgid ""
"%(message_count)d messages - Retry send message task queued (id: "
"%(task_id)s)."
msgstr ""
"%(message_count)d messages - Réexpédition mise en file d'attente (id: "
"%(task_id)s)."
msgid "Retry to send selected messages to pending recipients"
msgstr "Réexpédier les messages sélectionnés aux destinataires en attente"
msgid "Personal info"
msgstr "Informations personnelles"
msgid "Permissions"
msgstr "Permissions"
msgid "Important dates"
msgstr "Dates importantes"
msgid "Mail domain not found."
msgstr "Domaine de messagerie introuvable."
#, python-format
msgid "DNS provisioning successful via %(provider)s: %(changes)s"
msgstr ""
#, python-format
msgid "DNS provisioning successful via %(provider)s (no changes needed)."
msgstr ""
#, python-format
msgid "DNS provisioning failed: %(error)s"
msgstr ""
msgid "Statistics"
msgstr "Statistiques"
msgid "Metadata"
msgstr "Métadonnées"
msgid "Labels"
msgstr "Libellés"
msgid "No labels"
msgstr "Aucun libellé"
msgid "Import Messages"
msgstr "Importer des messages"
msgid "Import Messages from IMAP"
msgstr "Importer des messages via IMAP"
msgid "Message not found."
msgstr "Aucun message trouvé."
msgid "No pending recipients found for this message."
msgstr "Aucun destinataire en attente d'envoie trouvé pour ce message."
#, python-format
msgid ""
"Retry task has been queued for %(retryable_recipients_count)d pending "
"recipient(s) (id: %(task_id)s)."
msgstr ""
"Réexpédition mise en file d'attente pour %(retryable_recipients_count)d "
"destinataire(s) en attente (id: %(task_id)s)."
msgid "Keys"
msgstr "Clés"
msgid "Error"
msgstr "Erreur"
msgid "A mailbox with this local part already exists in this domain."
msgstr "Une boîte avec ce préfixe existe déjà dans ce domaine."
#, python-format
msgid "Image \"%(name)s\" (%(size)s MB) exceeds the %(max)s MB limit."
msgstr ""
"L'image « %(name)s » (%(size)s Mo) dépasse la limite de %(max)s Mo."
msgid "Name"
msgstr "Nom"
msgid "Recipient name"
msgstr "Nom du destinataire"
msgid "messages core application"
msgstr "Messages : Application \"Core\""
msgid "User account is disabled"
msgstr "Le compte de l'utilisateur est désactivé"
#, python-format
msgid ""
"Message body size (%(body_size)s MB) exceeds the %(max_size)s MB limit. "
"Please reduce message content."
msgstr ""
"La taille du corps du message (%(body_size)s Mo) dépasse la limite "
"%(max_size)s Mo. Veuillez réduire le contenu du message."
#, python-format
msgid ""
"Cannot add attachment(s) (%(new_size)s MB). Total attachments would be "
"%(total_size)s MB, exceeding the %(max_size)s MB limit. Current attachments: "
"%(current_size)s MB."
msgstr ""
"Impossible d'ajouter des pièces jointes (%(new_size)s Mo). Le total des "
"pièces jointes serait %(total_size)s Mo, dépassant la limite %(max_size)s "
"Mo. Pièces jointes actuelles : %(current_size)s Mo."
#, python-format
msgid ""
"Too many recipients: %(count)s (maximum is %(max)s). Please reduce the "
"number of recipients before sending."
msgstr ""
"Trop de destinataires : %(count)s (le maximum est %(max)s). Veuillez réduire "
"le nombre de destinataires avant l'envoi."
#, python-format
msgid ""
"Total attachment size (%(total_size)s MB) exceeds the %(max_size)s MB limit. "
"Please remove or reduce attachments."
msgstr ""
"La taille totale des pièces jointes (%(total_size)s Mo) dépasse la limite "
"%(max_size)s Mo. Veuillez supprimer ou réduire les pièces jointes."
#, python-format
msgid ""
"The composed email (%(mime_size)s MB) exceeds the maximum allowed size of "
"%(max_size)s MB. Please reduce message content or attachments."
msgstr ""
"L'e-mail composé (%(mime_size)s Mo) dépasse la taille maximale autorisée de "
"%(max_size)s Mo. Veuillez réduire le contenu du message ou les pièces "
"jointes."
msgid "id"
msgstr "id"
msgid "primary key for the record as UUID"
msgstr "clé primaire de la ressource en tant que UUID"
msgid "created on"
msgstr "créé le"
msgid "date and time at which a record was created"
msgstr "date et heure de création de la ressource"
msgid "updated on"
msgstr "modifié le"
msgid "date and time at which a record was last updated"
msgstr "date et heure de la dernière mise à jour de la ressource"
msgid ""
"We couldn't find a user with this sub but the email is already associated "
"with a registered user."
msgstr ""
"Nous n'avons pas pu trouver un utilisateur avec ce sub mais l'e-mail est "
"déjà associé à un utilisateur enregistré."
msgid ""
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
"_/: characters."
msgstr ""
"Saisissez un sub valide. Cette valeur ne peut contenir que des lettres, des "
"chiffres et les caractères @/./+/-/_/: uniquement."
msgid "sub"
msgstr "sub"
msgid ""
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: "
"characters only."
msgstr ""
"Obligatoire. 255 caractères ou moins. Lettres, chiffres et caractères @/./"
"+/-/_/: uniquement."
msgid "full name"
msgstr "nom complet"
msgid "identity email address"
msgstr "adresse e-mail d'identité"
msgid "admin email address"
msgstr "adresse e-mail d'administrateur"
msgid "language"
msgstr "langue"
msgid "The language in which the user wants to see the interface."
msgstr "La langue dans laquelle l'utilisateur veut voir l'interface."
msgid "The timezone in which the user wants to see times."
msgstr "Le fuseau horaire dans lequel l'utilisateur souhaite voir les heures."
msgid "staff status"
msgstr "statut d'équipe"
msgid "Whether the user can log into this admin site."
msgstr "Si l'utilisateur peut se connecter à ce site d'administration."
msgid "active"
msgstr "actif"
msgid ""
"Whether this user should be treated as active. Unselect this instead of "
"deleting accounts."
msgstr ""
"Si cet utilisateur doit être traité comme actif. Désélectionnez ceci au lieu "
"de supprimer des comptes."
msgid "Custom attributes"
msgstr "Attributs personnalisés"
msgid "Metadata to sync to the user in the identity provider."
msgstr ""
"Métadonnées de l'utilisateur à synchroniser avec le fournisseur d'identité."
msgid "user"
msgstr "utilisateur"
msgid "users"
msgstr "utilisateurs"
msgid ""
"Enter a valid domain name. This value may contain only lowercase letters, "
"numbers, dots and - characters."
msgstr ""
"Entrez un nom de domaine valide. Cette valeur ne peut contenir que des "
"minuscules, des chiffres, des points et des caractères -."
msgid "name"
msgstr "nom"
msgid "oidc autojoin"
msgstr "auto-jonction OIDC"
msgid "Create mailboxes automatically based on OIDC emails."
msgstr ""
"Créez automatiquement les boîtes aux lettres à partir des courriels OIDC."
msgid "Identity sync"
msgstr "Synchronisation de l'identité"
msgid "Sync mailboxes to an identity provider."
msgstr "Synchroniser les boîtes aux lettres avec un fournisseur d'identité."
msgid "Custom settings"
msgstr "Paramètres personnalisés"
msgid "Custom settings for the mail domain."
msgstr "Paramètres personnalisés pour le domaine de messagerie."
msgid "Metadata to sync to the maildomain group in the identity provider."
msgstr ""
"Métadonnées à synchroniser avec le groupe de domaine de messagerie du "
"fournisseur d'identité."
msgid "mail domain"
msgstr "domaine de messagerie"
msgid "mail domains"
msgstr "domaines de messagerie"
msgid "Human-readable name for this channel"
msgstr "Nom intelligible pour ce canal"
msgid "type"
msgstr "type"
msgid "Type of channel"
msgstr "Type de canal"
msgid "settings"
msgstr "paramètres"
msgid "Channel-specific configuration settings"
msgstr "Configuration spécifique aux canaux"
msgid "Mailbox that receives messages from this channel"
msgstr "Boîte aux lettres qui reçoit les messages provenant de ce canal"
msgid "Mail domain that owns this channel"
msgstr "Domaine de messagerie qui possède ce canal"
msgid "channel"
msgstr "canal"
msgid "channels"
msgstr "canaux"
msgid "local part"
msgstr "partie locale"
msgid "is identity"
msgstr "est une identité"
msgid ""
"Whether this mailbox identifies a person (i.e. is not an alias or a group)"
msgstr ""
"Si cette boîte aux lettres identifie une personne (c'est-à-dire n'est pas un "
"alias ou un groupe)"
msgid "mailbox"
msgstr "boîte aux lettres"
msgid "mailboxes"
msgstr "boîtes aux lettres"
msgid "role"
msgstr "rôle"
msgid "accessed at"
msgstr "accédé le"
msgid "mailbox access"
msgstr "accès de la boîte aux lettres"
msgid "mailbox accesses"
msgstr "accès de la boîte aux lettres"
msgid "subject"
msgstr "objet"
msgid "snippet"
msgstr "extrait"
msgid "has unread"
msgstr "a des non lus"
msgid "has trashed"
msgstr "a des mis à la corbeille"
msgid "is trashed"
msgstr "est mis à la corbeille"
msgid "Whether all messages in the thread are trashed"
msgstr "Si tous les messages de la conversation sont supprimés"
msgid "has archived"
msgstr "a des archives"
msgid "has draft"
msgstr "a un brouillon"
msgid "has starred"
msgstr "a des favoris"
msgid "has sender"
msgstr "a envoyé"
msgid "has messages"
msgstr "a des messages"
msgid "has attachments"
msgstr "a des pièces jointes"
msgid "is spam"
msgstr "est un spam"
msgid "has active"
msgstr "a des actifs"
msgid "has delivery pending"
msgstr "a des messages en cours d'envoi"
msgid ""
"True if thread has messages awaiting successful delivery (sending, retrying, "
"or failed)."
msgstr "Vrai si la conversation a des messages en attente de livraison (envoi, réexpédition, échec)."
msgid "has delivery failed"
msgstr "a des messages en échec de livraison"
msgid "True if thread has messages with permanent delivery failure."
msgstr "Vrai si la conversation a des messages avec un échec de livraison permanent."
msgid "messaged at"
msgstr "dernier message à"
msgid "sender names"
msgstr "noms des expéditeurs"
msgid "summary"
msgstr "résumé"
msgid "thread"
msgstr "conversation"
msgid "threads"
msgstr "conversations"
msgid ""
"Name of the label/folder (can use slashes for hierarchy, e.g. 'Work/"
"Projects')"
msgstr ""
"Nom du libellé/du dossier (vous pouvez utiliser des barres obliques pour la "
"hiérarchie, par exemple « Travail/Projets »)"
msgid "slug"
msgstr "slug"
msgid "URL-friendly version of the name"
msgstr "Version du nom optimisée pour les URL"
msgid "color"
msgstr "couleur"
msgid "Color of the label in hex format (e.g. #FF0000)"
msgstr "Couleur du libellé au format hexadécimal (par exemple #FF0000)"
msgid "Mailbox that owns this label"
msgstr "Boîte propriétaire du libellé"
msgid "Threads that have this label"
msgstr "Conversations qui ont ce libellé"
msgid "description"
msgstr "description"
msgid "Description of the label, used by AI to understand its purpose"
msgstr "Description du libellé, utilisée par l'IA pour comprendre son but"
msgid "auto labeling"
msgstr "labellisation automatique"
msgid "Whether this label should be automatically applied by AI"
msgstr "Si ce libellé doit être automatiquement appliquée par l'IA"
msgid "label"
msgstr "libellé"
msgid "labels"
msgstr "libellés"
msgid "thread access"
msgstr "accès de conversation"
msgid "thread accesses"
msgstr "accès des conversations"
msgid "email"
msgstr "courriel"
msgid "contact"
msgstr "contact"
msgid "contacts"
msgstr "contacts"
msgid "delivered at"
msgstr "délivré à"
msgid "delivery message"
msgstr "message de livraison"
msgid "retry count"
msgstr "nombre de tentatives"
msgid "retry at"
msgstr "réessayer à"
msgid "message recipient"
msgstr "destinataire du message"
msgid "message recipients"
msgstr "destinataires du message"
msgid "is draft"
msgstr "est un brouillon"
msgid "is sender"
msgstr "est expéditeur"
msgid "is starred"
msgstr "est favori"
msgid "is unread"
msgstr "est non lu"
msgid "is archived"
msgstr "est archivé"
msgid "trashed at"
msgstr "mis à la corbeille le"
msgid "sent at"
msgstr "envoyé le"
msgid "read at"
msgstr "lu à"
msgid "archived at"
msgstr "archivé à"
msgid "mime id"
msgstr "id mime"
msgid "Signature template for the message"
msgstr "Modèle de signature pour le message"
msgid "message"
msgstr "message"
msgid "messages"
msgstr "messages"
msgid "No subject"
msgstr "Aucun objet"
msgid "Message ID"
msgstr "ID du message"
msgid "From"
msgstr "De "
msgid "To"
msgstr "À"
msgid "CC"
msgstr "CC"
msgid "Date"
msgstr "Date"
msgid "Subject"
msgstr "Objet"
msgid "Body"
msgstr "Corps"
msgid "raw data"
msgstr "données brutes"
msgid "Raw email message bytes"
msgstr "Octets des messages bruts"
msgid "error message"
msgstr "message d'erreur"
msgid "Error message if processing failed"
msgstr "Message d'erreur si le traitement a échoué"
msgid "inbound message"
msgstr "message entrant"
msgid "inbound messages"
msgstr "messages entrants"
msgid "sha256 hash"
msgstr "hash sha256"
msgid "SHA-256 hash of the uncompressed blob content"
msgstr "Hash SHA-256 du contenu du blob non compressé"
msgid "file size"
msgstr "taille du fichier"
msgid "Size of the blob in bytes"
msgstr "Taille du blob en octets"
msgid "compressed size"
msgstr "taille compressée"
msgid "Size of the compressed blob in bytes"
msgstr "Taille du blob compressé en octets"
msgid "content type"
msgstr "type de contenu"
msgid "MIME type of the blob"
msgstr "Type MIME du blob"
msgid "compression"
msgstr "compression"
msgid "raw content"
msgstr "contenu brut"
msgid "Compressed binary content of the blob"
msgstr "Contenu binaire compressé du blob"
msgid "Mailbox that owns this blob"
msgstr "Boîte aux lettres propriétaire de ce blob"
msgid "Mail domain that owns this blob"
msgstr "Domaine de courrier qui possède ce blob"
msgid "blob"
msgstr "blob"
msgid "blobs"
msgstr "blobs"
msgid "file name"
msgstr "nom du fichier"
msgid "Original filename of the attachment"
msgstr "Nom du fichier original de la pièce jointe"
msgid "Reference to the blob containing the attachment data"
msgstr "Référence vers le blob contenant les données de la pièce jointe"
msgid "Mailbox that owns this attachment"
msgstr "Boîte aux lettres propriétaire de cette pièce jointe"
msgid "Messages that use this attachment"
msgstr "Messages qui utilisent cette pièce jointe"
msgid "content ID"
msgstr "content ID"
msgid "Content-ID for inline images"
msgstr "ID du contenu pour les images en ligne"
msgid "attachment"
msgstr "pièce jointe"
msgid "attachments"
msgstr "pièces jointes"
msgid "mail domain access"
msgstr "accès au domaine de messagerie"
msgid "mail domain accesses"
msgstr "accès au domaine de messagerie"
msgid "selector"
msgstr "sélecteur"
msgid "DKIM selector (e.g., 'default', 'mail')"
msgstr "Sélecteur DKIM (i.e., 'default', 'mail')"
msgid "private key"
msgstr "clé privée"
msgid "DKIM private key in PEM format (encrypted)"
msgstr "Clé privée DKIM au format PEM (chiffrée)"
msgid "public key"
msgstr "clé publique"
msgid "DKIM public key for DNS record generation"
msgstr "Clé publique de DKIM pour la génération d'enregistrements DNS"
msgid "algorithm"
msgstr "algorithme"
msgid "DKIM signing algorithm"
msgstr "Algorithme de signature DKIM"
msgid "key size"
msgstr "taille de la clé"
msgid "Key size in bits (e.g., 2048, 4096 for RSA)"
msgstr "Taille de la clé en octets (par exemple, 2048, 4096 pour RSA)"
msgid "is active"
msgstr "est active"
msgid "Whether this DKIM key is active and should be used for signing"
msgstr "Si cette clé DKIM est active et doit être utilisée pour signer"
msgid "Domain that owns this DKIM key"
msgstr "Domaine propriétaire de cette clé DKIM"
msgid "DKIM key"
msgstr "Clé DKIM"
msgid "DKIM keys"
msgstr "Clés DKIM"
msgid ""
"Name of the template (e.g., 'Standard Reply', 'Out of Office', 'Work "
"Signature')"
msgstr ""
"Nom du modèle (ex., 'Réponse standard', 'Absence de bureau', 'Signature "
"professionnelle')"
msgid ""
"Reference to the blob containing template content as JSON: {html: str, text: "
"str, raw: any}"
msgstr ""
"Référence vers le blob contenant le contenu du modèle au format JSON : "
"{html: str, text: str, raw: any}"
msgid "Type of template (message, signature)"
msgstr "Type de modèle (message, signature)"
msgid "Whether this template is available for use"
msgstr "Si ce modèle est utilisable"
msgid "Mail domain that can use this template"
msgstr "Domaine de messagerie qui peut utiliser ce modèle"
msgid "Mailbox that can use this template"
msgstr "Boîte aux lettres qui peut utiliser ce modèle"
msgid "is forced"
msgstr "est forcé"
msgid ""
"Whether this template is forced; no other template of the same type can be "
"used in the same scope"
msgstr ""
"Si ce modèle est forcé ; aucun autre modèle du même type ne peut être "
"utilisé dans le même périmètre"
msgid "is default"
msgstr "est par défaut"
msgid ""
"Whether this template is the default; it will be automatically loaded when "
"composing a new message"
msgstr ""
"Si ce modèle est celui par défaut ; il sera automatiquement chargé lors de "
"la composition d'un nouveau message"
msgid "message template"
msgstr "modèle de message"
msgid "message templates"
msgstr "modèles de message"
msgid "Provision DNS records"
msgstr ""
msgid "Retry to send to pending recipients"
msgstr "Réessayer d'envoyer aux destinataires en attente"
msgid "Import from IMAP"
msgstr "Importer via IMAP"
msgid "Home"
msgstr "Accueil"
msgid "Please correct the error below."
msgstr "Veuillez corriger l'erreur ci-dessous."
msgid "Please correct the errors below."
msgstr "Veuillez corriger les erreurs ci-dessous."
msgid "Start Import"
msgstr "Démarrer l'importation"
msgid "English"
msgstr "Anglais"
msgid "French"
msgstr "Français"
msgid "Dutch"
msgstr "Néerlandais"
Binary file not shown.
@@ -1,680 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: lasuite-messages\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-25 11:10+0000\n"
"PO-Revision-Date: 2026-02-03 17:00\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Language: it_IT\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: lasuite-messages\n"
"X-Crowdin-Project-ID: 831182\n"
"X-Crowdin-Language: it\n"
"X-Crowdin-File: backend.pot\n"
"X-Crowdin-File-ID: 40\n"
msgid "delivery status"
msgstr ""
#, python-format
msgid "%(message_count)d messages - Retry send message task queued (id: %(task_id)s)."
msgstr ""
msgid "Retry to send selected messages to pending recipients"
msgstr ""
msgid "Personal info"
msgstr ""
msgid "Permissions"
msgstr ""
msgid "Important dates"
msgstr ""
msgid "Mail domain not found."
msgstr ""
#, python-format
msgid "DNS provisioning successful via %(provider)s: %(changes)s"
msgstr ""
#, python-format
msgid "DNS provisioning successful via %(provider)s (no changes needed)."
msgstr ""
#, python-format
msgid "DNS provisioning failed: %(error)s"
msgstr ""
msgid "Statistics"
msgstr ""
msgid "Metadata"
msgstr ""
msgid "Labels"
msgstr ""
msgid "No labels"
msgstr ""
msgid "Import Messages"
msgstr ""
msgid "Import Messages from IMAP"
msgstr ""
msgid "Message not found."
msgstr ""
msgid "No pending recipients found for this message."
msgstr ""
#, python-format
msgid "Retry task has been queued for %(retryable_recipients_count)d pending recipient(s) (id: %(task_id)s)."
msgstr ""
msgid "Keys"
msgstr ""
msgid "Error"
msgstr ""
msgid "A mailbox with this local part already exists in this domain."
msgstr ""
msgid "Name"
msgstr ""
msgid "Recipient name"
msgstr ""
msgid "messages core application"
msgstr ""
msgid "User account is disabled"
msgstr ""
#, python-format
msgid "Message body size (%(body_size)s MB) exceeds the %(max_size)s MB limit. Please reduce message content."
msgstr ""
#, python-format
msgid "Cannot add attachment(s) (%(new_size)s MB). Total attachments would be %(total_size)s MB, exceeding the %(max_size)s MB limit. Current attachments: %(current_size)s MB."
msgstr ""
#, python-format
msgid "Too many recipients: %(count)s (maximum is %(max)s). Please reduce the number of recipients before sending."
msgstr ""
#, python-format
msgid "Total attachment size (%(total_size)s MB) exceeds the %(max_size)s MB limit. Please remove or reduce attachments."
msgstr ""
#, python-format
msgid "The composed email (%(mime_size)s MB) exceeds the maximum allowed size of %(max_size)s MB. Please reduce message content or attachments."
msgstr ""
msgid "id"
msgstr ""
msgid "primary key for the record as UUID"
msgstr ""
msgid "created on"
msgstr ""
msgid "date and time at which a record was created"
msgstr ""
msgid "updated on"
msgstr ""
msgid "date and time at which a record was last updated"
msgstr ""
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
msgid "sub"
msgstr ""
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr ""
msgid "full name"
msgstr ""
msgid "identity email address"
msgstr ""
msgid "admin email address"
msgstr ""
msgid "language"
msgstr ""
msgid "The language in which the user wants to see the interface."
msgstr ""
msgid "The timezone in which the user wants to see times."
msgstr ""
msgid "staff status"
msgstr ""
msgid "Whether the user can log into this admin site."
msgstr ""
msgid "active"
msgstr ""
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
msgid "Custom attributes"
msgstr ""
msgid "Metadata to sync to the user in the identity provider."
msgstr ""
msgid "user"
msgstr ""
msgid "users"
msgstr ""
msgid "Enter a valid domain name. This value may contain only lowercase letters, numbers, dots and - characters."
msgstr ""
msgid "name"
msgstr ""
msgid "oidc autojoin"
msgstr ""
msgid "Create mailboxes automatically based on OIDC emails."
msgstr ""
msgid "Identity sync"
msgstr ""
msgid "Sync mailboxes to an identity provider."
msgstr ""
msgid "Custom settings"
msgstr ""
msgid "Custom settings for the mail domain."
msgstr ""
msgid "Metadata to sync to the maildomain group in the identity provider."
msgstr ""
msgid "mail domain"
msgstr ""
msgid "mail domains"
msgstr ""
msgid "Human-readable name for this channel"
msgstr ""
msgid "type"
msgstr ""
msgid "Type of channel"
msgstr ""
msgid "settings"
msgstr ""
msgid "Channel-specific configuration settings"
msgstr ""
msgid "Mailbox that receives messages from this channel"
msgstr ""
msgid "Mail domain that owns this channel"
msgstr ""
msgid "channel"
msgstr ""
msgid "channels"
msgstr ""
msgid "local part"
msgstr ""
msgid "is identity"
msgstr ""
msgid "Whether this mailbox identifies a person (i.e. is not an alias or a group)"
msgstr ""
msgid "mailbox"
msgstr ""
msgid "mailboxes"
msgstr ""
msgid "role"
msgstr ""
msgid "accessed at"
msgstr ""
msgid "mailbox access"
msgstr ""
msgid "mailbox accesses"
msgstr ""
msgid "subject"
msgstr ""
msgid "snippet"
msgstr ""
msgid "has unread"
msgstr ""
msgid "has trashed"
msgstr ""
msgid "is trashed"
msgstr ""
msgid "Whether all messages in the thread are trashed"
msgstr ""
msgid "has archived"
msgstr ""
msgid "has draft"
msgstr ""
msgid "has starred"
msgstr ""
msgid "has sender"
msgstr ""
msgid "has messages"
msgstr ""
msgid "has attachments"
msgstr ""
msgid "is spam"
msgstr ""
msgid "has active"
msgstr ""
msgid "messaged at"
msgstr ""
msgid "sender names"
msgstr ""
msgid "summary"
msgstr ""
msgid "thread"
msgstr ""
msgid "threads"
msgstr ""
msgid "Name of the label/folder (can use slashes for hierarchy, e.g. 'Work/Projects')"
msgstr ""
msgid "slug"
msgstr ""
msgid "URL-friendly version of the name"
msgstr ""
msgid "color"
msgstr ""
msgid "Color of the label in hex format (e.g. #FF0000)"
msgstr ""
msgid "Mailbox that owns this label"
msgstr ""
msgid "Threads that have this label"
msgstr ""
msgid "description"
msgstr ""
msgid "Description of the label, used by AI to understand its purpose"
msgstr ""
msgid "auto labeling"
msgstr ""
msgid "Whether this label should be automatically applied by AI"
msgstr ""
msgid "label"
msgstr ""
msgid "labels"
msgstr ""
msgid "thread access"
msgstr ""
msgid "thread accesses"
msgstr ""
msgid "email"
msgstr ""
msgid "contact"
msgstr ""
msgid "contacts"
msgstr ""
msgid "delivered at"
msgstr ""
msgid "delivery message"
msgstr ""
msgid "retry count"
msgstr ""
msgid "retry at"
msgstr ""
msgid "message recipient"
msgstr ""
msgid "message recipients"
msgstr ""
msgid "is draft"
msgstr ""
msgid "is sender"
msgstr ""
msgid "is starred"
msgstr ""
msgid "is unread"
msgstr ""
msgid "is archived"
msgstr ""
msgid "trashed at"
msgstr ""
msgid "sent at"
msgstr ""
msgid "read at"
msgstr ""
msgid "archived at"
msgstr ""
msgid "mime id"
msgstr ""
msgid "Signature template for the message"
msgstr ""
msgid "message"
msgstr ""
msgid "messages"
msgstr ""
msgid "No subject"
msgstr ""
msgid "Message ID"
msgstr ""
msgid "From"
msgstr ""
msgid "To"
msgstr ""
msgid "CC"
msgstr ""
msgid "Date"
msgstr ""
msgid "Subject"
msgstr ""
msgid "Body"
msgstr ""
msgid "raw data"
msgstr ""
msgid "Raw email message bytes"
msgstr ""
msgid "error message"
msgstr ""
msgid "Error message if processing failed"
msgstr ""
msgid "inbound message"
msgstr ""
msgid "inbound messages"
msgstr ""
msgid "sha256 hash"
msgstr ""
msgid "SHA-256 hash of the uncompressed blob content"
msgstr ""
msgid "file size"
msgstr ""
msgid "Size of the blob in bytes"
msgstr ""
msgid "compressed size"
msgstr ""
msgid "Size of the compressed blob in bytes"
msgstr ""
msgid "content type"
msgstr ""
msgid "MIME type of the blob"
msgstr ""
msgid "compression"
msgstr ""
msgid "raw content"
msgstr ""
msgid "Compressed binary content of the blob"
msgstr ""
msgid "Mailbox that owns this blob"
msgstr ""
msgid "Mail domain that owns this blob"
msgstr ""
msgid "blob"
msgstr ""
msgid "blobs"
msgstr ""
msgid "file name"
msgstr ""
msgid "Original filename of the attachment"
msgstr ""
msgid "Reference to the blob containing the attachment data"
msgstr ""
msgid "Mailbox that owns this attachment"
msgstr ""
msgid "Messages that use this attachment"
msgstr ""
msgid "content ID"
msgstr ""
msgid "Content-ID for inline images"
msgstr ""
msgid "attachment"
msgstr ""
msgid "attachments"
msgstr ""
msgid "mail domain access"
msgstr ""
msgid "mail domain accesses"
msgstr ""
msgid "selector"
msgstr ""
msgid "DKIM selector (e.g., 'default', 'mail')"
msgstr ""
msgid "private key"
msgstr ""
msgid "DKIM private key in PEM format (encrypted)"
msgstr ""
msgid "public key"
msgstr ""
msgid "DKIM public key for DNS record generation"
msgstr ""
msgid "algorithm"
msgstr ""
msgid "DKIM signing algorithm"
msgstr ""
msgid "key size"
msgstr ""
msgid "Key size in bits (e.g., 2048, 4096 for RSA)"
msgstr ""
msgid "is active"
msgstr ""
msgid "Whether this DKIM key is active and should be used for signing"
msgstr ""
msgid "Domain that owns this DKIM key"
msgstr ""
msgid "DKIM key"
msgstr ""
msgid "DKIM keys"
msgstr ""
msgid "Name of the template (e.g., 'Standard Reply', 'Out of Office', 'Work Signature')"
msgstr ""
msgid "Reference to the blob containing template content as JSON: {html: str, text: str, raw: any}"
msgstr ""
msgid "Type of template (message, signature)"
msgstr ""
msgid "Whether this template is available for use"
msgstr ""
msgid "Mail domain that can use this template"
msgstr ""
msgid "Mailbox that can use this template"
msgstr ""
msgid "is forced"
msgstr ""
msgid "Whether this template is forced; no other template of the same type can be used in the same scope"
msgstr ""
msgid "is default"
msgstr ""
msgid "Whether this template is the default; it will be automatically loaded when composing a new message"
msgstr ""
msgid "message template"
msgstr ""
msgid "message templates"
msgstr ""
msgid "Provision DNS records"
msgstr ""
msgid "Retry to send to pending recipients"
msgstr ""
msgid "Import from IMAP"
msgstr ""
msgid "Home"
msgstr ""
msgid "Please correct the error below."
msgstr ""
msgid "Please correct the errors below."
msgstr ""
msgid "Start Import"
msgstr ""
msgid "English"
msgstr ""
msgid "French"
msgstr ""
msgid "Dutch"
msgstr ""
Binary file not shown.
@@ -1,680 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: lasuite-messages\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-25 11:10+0000\n"
"PO-Revision-Date: 2026-02-03 17:00\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: lasuite-messages\n"
"X-Crowdin-Project-ID: 831182\n"
"X-Crowdin-Language: nl\n"
"X-Crowdin-File: backend.pot\n"
"X-Crowdin-File-ID: 40\n"
msgid "delivery status"
msgstr "afleverstatus"
#, python-format
msgid "%(message_count)d messages - Retry send message task queued (id: %(task_id)s)."
msgstr ""
msgid "Retry to send selected messages to pending recipients"
msgstr ""
msgid "Personal info"
msgstr "Persoonlijke gegevens"
msgid "Permissions"
msgstr "Machtigingen"
msgid "Important dates"
msgstr "Belangrijke data"
msgid "Mail domain not found."
msgstr ""
#, python-format
msgid "DNS provisioning successful via %(provider)s: %(changes)s"
msgstr ""
#, python-format
msgid "DNS provisioning successful via %(provider)s (no changes needed)."
msgstr ""
#, python-format
msgid "DNS provisioning failed: %(error)s"
msgstr ""
msgid "Statistics"
msgstr "Statistieken"
msgid "Metadata"
msgstr "Metadata"
msgid "Labels"
msgstr "Labels"
msgid "No labels"
msgstr "Geen labels"
msgid "Import Messages"
msgstr "Berichten importeren"
msgid "Import Messages from IMAP"
msgstr "Berichten importeren van IMAP"
msgid "Message not found."
msgstr ""
msgid "No pending recipients found for this message."
msgstr ""
#, python-format
msgid "Retry task has been queued for %(retryable_recipients_count)d pending recipient(s) (id: %(task_id)s)."
msgstr ""
msgid "Keys"
msgstr "Keys"
msgid "Error"
msgstr ""
msgid "A mailbox with this local part already exists in this domain."
msgstr "Er bestaat al een mailbox met dit lokale deel in dit domein."
msgid "Name"
msgstr "Naam"
msgid "Recipient name"
msgstr "Naam van ontvanger"
msgid "messages core application"
msgstr "messages kern applicatie"
msgid "User account is disabled"
msgstr "Gebruikersaccount is uitgeschakeld"
#, python-format
msgid "Message body size (%(body_size)s MB) exceeds the %(max_size)s MB limit. Please reduce message content."
msgstr "Berichtinhoud (%(body_size)s MB) overschrijdt de MB-limiet %(max_size)s . Verminder berichteninhoud."
#, python-format
msgid "Cannot add attachment(s) (%(new_size)s MB). Total attachments would be %(total_size)s MB, exceeding the %(max_size)s MB limit. Current attachments: %(current_size)s MB."
msgstr "Kan bijlagen niet toevoegen (%(new_size)s MB). Totale bijlagen zouden %(total_size)s MB zijn, die de limiet van de %(max_size)s MB overschrijden. Huidige bijlagen: %(current_size)s MB."
#, python-format
msgid "Too many recipients: %(count)s (maximum is %(max)s). Please reduce the number of recipients before sending."
msgstr "Te veel ontvangers: %(count)s (maximum is %(max)s). Beperk het aantal ontvangers voordat u verzendt."
#, python-format
msgid "Total attachment size (%(total_size)s MB) exceeds the %(max_size)s MB limit. Please remove or reduce attachments."
msgstr "Totale grootte van bijlagen (%(total_size)s MB) overschrijdt de MB-limiet %(max_size)s. Verwijder of reduceer bijlagen."
#, python-format
msgid "The composed email (%(mime_size)s MB) exceeds the maximum allowed size of %(max_size)s MB. Please reduce message content or attachments."
msgstr "De e-mail (%(mime_size)s MB) overschrijdt de maximaal toegestane grootte van %(max_size)s MB. Beperk de inhoud van berichten of bijlagen."
msgid "id"
msgstr "id"
msgid "primary key for the record as UUID"
msgstr "primaire sleutel voor het record als UUID"
msgid "created on"
msgstr "gecreëerd op"
msgid "date and time at which a record was created"
msgstr "datum en tijd waarop een record is aangemaakt"
msgid "updated on"
msgstr "bijgewerkt op"
msgid "date and time at which a record was last updated"
msgstr "datum en tijd waarop een record voor het laatst is bijgewerkt"
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "Wij konden geen gebruiker vinden met dit id, maar de email is al geassocieerd met een geregistreerde gebruiker."
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr "Voer een geldig id in. De waarde mag alleen letters, nummers en @/./.+/-/_: karakters bevatten."
msgid "sub"
msgstr "id"
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr "Verplicht. 255 karakters of minder. Alleen letters, nummers en @/./+/-/_/: karakters zijn toegestaan."
msgid "full name"
msgstr "volledige naam"
msgid "identity email address"
msgstr "identiteit emailadres"
msgid "admin email address"
msgstr "beheerders e-mailadres"
msgid "language"
msgstr "taal"
msgid "The language in which the user wants to see the interface."
msgstr "De taal waarin de gebruiker de interface wil zien."
msgid "The timezone in which the user wants to see times."
msgstr "De tijdzone waarin de gebruiker de tijden wil zien."
msgid "staff status"
msgstr "beheerder status"
msgid "Whether the user can log into this admin site."
msgstr "Of de gebruiker kan inloggen in het beheer gedeelte."
msgid "active"
msgstr "actief"
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Of een gebruiker als actief moet worden beschouwd. Deselecteer dit in plaats van het account te deleten."
msgid "Custom attributes"
msgstr "Aanpasbare attributen"
msgid "Metadata to sync to the user in the identity provider."
msgstr "Metagegevens om te synchroniseren met de gebruiker in de identiteitsprovider."
msgid "user"
msgstr "gebruiker"
msgid "users"
msgstr "gebruikers"
msgid "Enter a valid domain name. This value may contain only lowercase letters, numbers, dots and - characters."
msgstr "Voer een geldige domeinnaam in. Deze waarde mag alleen kleine letters, cijfers, punten en - tekens bevatten."
msgid "name"
msgstr "naam"
msgid "oidc autojoin"
msgstr "oidc automatisch deelnemen"
msgid "Create mailboxes automatically based on OIDC emails."
msgstr "Automatisch mailboxen aanmaken op basis van OIDC e-mails."
msgid "Identity sync"
msgstr "Identiteit synchronisatie"
msgid "Sync mailboxes to an identity provider."
msgstr "Synchroniseer mailboxen met een identity provider."
msgid "Custom settings"
msgstr "Aangepaste instellingen"
msgid "Custom settings for the mail domain."
msgstr "Aangepaste instellingen voor het e-maildomein."
msgid "Metadata to sync to the maildomain group in the identity provider."
msgstr "Metagegevens om te synchroniseren met de gebruiker in de identiteitsprovider."
msgid "mail domain"
msgstr "e-mail domein"
msgid "mail domains"
msgstr "e-mail domeinen"
msgid "Human-readable name for this channel"
msgstr "Leesbare naam voor dit kanaal"
msgid "type"
msgstr "type"
msgid "Type of channel"
msgstr "Type kanaal"
msgid "settings"
msgstr "instellingen"
msgid "Channel-specific configuration settings"
msgstr "Kanaalspecifieke configuratie-instellingen"
msgid "Mailbox that receives messages from this channel"
msgstr "Mailbox die berichten van dit kanaal ontvangt"
msgid "Mail domain that owns this channel"
msgstr "Mail domein die eigenaar is van dit kanaal"
msgid "channel"
msgstr "kanaal"
msgid "channels"
msgstr "kanalen"
msgid "local part"
msgstr "lokaal deel"
msgid "is identity"
msgstr "is identiteit"
msgid "Whether this mailbox identifies a person (i.e. is not an alias or a group)"
msgstr "Of dit mailbox een persoon identificeert (bijv. is geen alias of groep)"
msgid "mailbox"
msgstr "mailbox"
msgid "mailboxes"
msgstr "mailboxen"
msgid "role"
msgstr "rol"
msgid "accessed at"
msgstr "geopend op"
msgid "mailbox access"
msgstr "mailbox toegang"
msgid "mailbox accesses"
msgstr "mailbox toegang"
msgid "subject"
msgstr "onderwerp"
msgid "snippet"
msgstr "snippet"
msgid "has unread"
msgstr "bevat ongelezen"
msgid "has trashed"
msgstr "bevat verwijderd"
msgid "is trashed"
msgstr "is verwijderd"
msgid "Whether all messages in the thread are trashed"
msgstr "Of alle berichten in de thread naar de prullenbak zijn verplaatst"
msgid "has archived"
msgstr "bevat gearchiveerd"
msgid "has draft"
msgstr "bevat concept"
msgid "has starred"
msgstr "bevat ster"
msgid "has sender"
msgstr "bevat afzender"
msgid "has messages"
msgstr "bevat berichten"
msgid "has attachments"
msgstr "bevat bijlage"
msgid "is spam"
msgstr "is spam"
msgid "has active"
msgstr "heeft actieve"
msgid "messaged at"
msgstr "bericht op"
msgid "sender names"
msgstr "naam afzender"
msgid "summary"
msgstr "samenvatting"
msgid "thread"
msgstr "kanaal"
msgid "threads"
msgstr "kanalen"
msgid "Name of the label/folder (can use slashes for hierarchy, e.g. 'Work/Projects')"
msgstr "Naam van de label/map (kan slashes voor de hiërarchie gebruiken, bijvoorbeeld 'Werk/Projecten')"
msgid "slug"
msgstr "slug"
msgid "URL-friendly version of the name"
msgstr "URL-vriendelijke versie van de naam"
msgid "color"
msgstr "kleur"
msgid "Color of the label in hex format (e.g. #FF0000)"
msgstr "Kleur van het label in hex formaat (bijv. #FF0000)"
msgid "Mailbox that owns this label"
msgstr "Mailbox die eigenaar is van deze blob"
msgid "Threads that have this label"
msgstr "Kanalen met dit label"
msgid "description"
msgstr "beschrijving"
msgid "Description of the label, used by AI to understand its purpose"
msgstr "Beschrijving van het label, gebruikt door AI om het doel ervan te begrijpen"
msgid "auto labeling"
msgstr "automatisch labelen"
msgid "Whether this label should be automatically applied by AI"
msgstr "Of dit label automatisch moet worden toegepast door AI"
msgid "label"
msgstr "label"
msgid "labels"
msgstr "labels"
msgid "thread access"
msgstr "kanaal toegang"
msgid "thread accesses"
msgstr "kanaal toegang"
msgid "email"
msgstr "e-mail"
msgid "contact"
msgstr "contact"
msgid "contacts"
msgstr "contacten"
msgid "delivered at"
msgstr "afgeleverd op"
msgid "delivery message"
msgstr "verzend bericht"
msgid "retry count"
msgstr "aantal pogingen"
msgid "retry at"
msgstr "probeer op"
msgid "message recipient"
msgstr "ontvanger"
msgid "message recipients"
msgstr "ontvangers"
msgid "is draft"
msgstr "is concept"
msgid "is sender"
msgstr "is afzender"
msgid "is starred"
msgstr "met ster"
msgid "is unread"
msgstr "is ongelezen"
msgid "is archived"
msgstr "is gearchiveerd"
msgid "trashed at"
msgstr "verwijderd op"
msgid "sent at"
msgstr "verzonden op"
msgid "read at"
msgstr "gelezen op"
msgid "archived at"
msgstr "gearchiveerd op"
msgid "mime id"
msgstr "mime id"
msgid "Signature template for the message"
msgstr "Handtekening template voor het bericht"
msgid "message"
msgstr "bericht"
msgid "messages"
msgstr "berichten"
msgid "No subject"
msgstr "Geen onderwerp"
msgid "Message ID"
msgstr "Bericht ID"
msgid "From"
msgstr "Van"
msgid "To"
msgstr "Naar"
msgid "CC"
msgstr "CC"
msgid "Date"
msgstr "Datum"
msgid "Subject"
msgstr "Onderwerp"
msgid "Body"
msgstr "Bericht"
msgid "raw data"
msgstr "ruwe gegevens"
msgid "Raw email message bytes"
msgstr "Ruwe e-mail bericht bytes"
msgid "error message"
msgstr "Foutmelding"
msgid "Error message if processing failed"
msgstr "Foutmelding bij verwerking mislukt"
msgid "inbound message"
msgstr "inkomende bericht"
msgid "inbound messages"
msgstr "inkomende berichten"
msgid "sha256 hash"
msgstr "sha256 hash"
msgid "SHA-256 hash of the uncompressed blob content"
msgstr "SHA-256 hash van de ongecomprimeerde blob inhoud"
msgid "file size"
msgstr "bestandsgrootte"
msgid "Size of the blob in bytes"
msgstr "Grootte van de blob in bytes"
msgid "compressed size"
msgstr "gecomprimeerde grootte"
msgid "Size of the compressed blob in bytes"
msgstr "Grootte van de gecomprimeerde blob in bytes"
msgid "content type"
msgstr "inhoudstype"
msgid "MIME type of the blob"
msgstr "MIME type van de blob"
msgid "compression"
msgstr "compressie"
msgid "raw content"
msgstr "ruwe inhoud"
msgid "Compressed binary content of the blob"
msgstr "Gecomprimeerde binaire inhoud van de blob"
msgid "Mailbox that owns this blob"
msgstr "Mailbox die eigenaar is van deze blob"
msgid "Mail domain that owns this blob"
msgstr "Mail domein die eigenaar is van deze blob"
msgid "blob"
msgstr "blob"
msgid "blobs"
msgstr "blobs"
msgid "file name"
msgstr "bestandsnaam"
msgid "Original filename of the attachment"
msgstr "Oorspronkelijke bestandsnaam van de bijlage"
msgid "Reference to the blob containing the attachment data"
msgstr "Verwijzing naar de blob die de bijlagegegevens bevat"
msgid "Mailbox that owns this attachment"
msgstr "Mailbox die eigenaar is van deze bijlage"
msgid "Messages that use this attachment"
msgstr "Berichten die deze bijlage gebruiken"
msgid "content ID"
msgstr ""
msgid "Content-ID for inline images"
msgstr ""
msgid "attachment"
msgstr "bijlage"
msgid "attachments"
msgstr "bijlagen"
msgid "mail domain access"
msgstr "mail domein toegang"
msgid "mail domain accesses"
msgstr "mail domein toegang"
msgid "selector"
msgstr "selectiemenu"
msgid "DKIM selector (e.g., 'default', 'mail')"
msgstr "DKIM-selector (bijv. 'standaard', 'mail')"
msgid "private key"
msgstr "privésleutel"
msgid "DKIM private key in PEM format (encrypted)"
msgstr "DKIM private key in PEM-formaat (gecodeerd)"
msgid "public key"
msgstr "publieke sleutel"
msgid "DKIM public key for DNS record generation"
msgstr "Openbare DKIM sleutel voor DNS record generatie"
msgid "algorithm"
msgstr "algoritme"
msgid "DKIM signing algorithm"
msgstr "DKIM ondertekeningsalgoritme"
msgid "key size"
msgstr "sleutel grootte"
msgid "Key size in bits (e.g., 2048, 4096 for RSA)"
msgstr "Sleutelgrootte in bits (bijv. 2048, 4096 voor RSA)"
msgid "is active"
msgstr "is actief"
msgid "Whether this DKIM key is active and should be used for signing"
msgstr "Of deze DKIM-sleutel actief is en moet worden gebruikt voor het ondertekenen"
msgid "Domain that owns this DKIM key"
msgstr "Domein dat eigenaar is van deze DKIM-sleutel"
msgid "DKIM key"
msgstr "DKIM sleutel"
msgid "DKIM keys"
msgstr "DKIM sleutels"
msgid "Name of the template (e.g., 'Standard Reply', 'Out of Office', 'Work Signature')"
msgstr "Naam van de template (bijv. 'Standaard antwoord', 'Niet op kantoor', 'Ondertekening')"
msgid "Reference to the blob containing template content as JSON: {html: str, text: str, raw: any}"
msgstr "Referentie naar de blob met sjablooninhoud als JSON: {html: str, text: str, raw: any}"
msgid "Type of template (message, signature)"
msgstr "Type template (bericht, handtekening)"
msgid "Whether this template is available for use"
msgstr "Of deze template beschikbaar is voor gebruik"
msgid "Mail domain that can use this template"
msgstr "E-mail domein die deze template kan gebruiken"
msgid "Mailbox that can use this template"
msgstr "Mailbox die dit sjabloon kan gebruiken"
msgid "is forced"
msgstr "is afgedwongen"
msgid "Whether this template is forced; no other template of the same type can be used in the same scope"
msgstr "Of deze template wordt geforceerd; geen andere sjabloon van hetzelfde type kan worden gebruikt in dezelfde scope"
msgid "is default"
msgstr "is standaard"
msgid "Whether this template is the default; it will be automatically loaded when composing a new message"
msgstr "Of deze sjabloon standaard is; het zal automatisch worden geladen bij het opstellen van een nieuw bericht"
msgid "message template"
msgstr "bericht template"
msgid "message templates"
msgstr "bericht sjablonen"
msgid "Provision DNS records"
msgstr ""
msgid "Retry to send to pending recipients"
msgstr ""
msgid "Import from IMAP"
msgstr "Importeren van IMAP"
msgid "Home"
msgstr "Home"
msgid "Please correct the error below."
msgstr "Herstel onderstaande foutmelding."
msgid "Please correct the errors below."
msgstr "Herstel onderstaande foutmeldingen."
msgid "Start Import"
msgstr "Start import"
msgid "English"
msgstr "Engels"
msgid "French"
msgstr "Frans"
msgid "Dutch"
msgstr "Nederlands"
Binary file not shown.
@@ -1,680 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: lasuite-messages\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-25 11:10+0000\n"
"PO-Revision-Date: 2026-02-03 17:00\n"
"Last-Translator: \n"
"Language-Team: Portuguese\n"
"Language: pt_PT\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: lasuite-messages\n"
"X-Crowdin-Project-ID: 831182\n"
"X-Crowdin-Language: pt-PT\n"
"X-Crowdin-File: backend.pot\n"
"X-Crowdin-File-ID: 40\n"
msgid "delivery status"
msgstr ""
#, python-format
msgid "%(message_count)d messages - Retry send message task queued (id: %(task_id)s)."
msgstr ""
msgid "Retry to send selected messages to pending recipients"
msgstr ""
msgid "Personal info"
msgstr ""
msgid "Permissions"
msgstr ""
msgid "Important dates"
msgstr ""
msgid "Mail domain not found."
msgstr ""
#, python-format
msgid "DNS provisioning successful via %(provider)s: %(changes)s"
msgstr ""
#, python-format
msgid "DNS provisioning successful via %(provider)s (no changes needed)."
msgstr ""
#, python-format
msgid "DNS provisioning failed: %(error)s"
msgstr ""
msgid "Statistics"
msgstr ""
msgid "Metadata"
msgstr ""
msgid "Labels"
msgstr ""
msgid "No labels"
msgstr ""
msgid "Import Messages"
msgstr ""
msgid "Import Messages from IMAP"
msgstr ""
msgid "Message not found."
msgstr ""
msgid "No pending recipients found for this message."
msgstr ""
#, python-format
msgid "Retry task has been queued for %(retryable_recipients_count)d pending recipient(s) (id: %(task_id)s)."
msgstr ""
msgid "Keys"
msgstr ""
msgid "Error"
msgstr ""
msgid "A mailbox with this local part already exists in this domain."
msgstr ""
msgid "Name"
msgstr ""
msgid "Recipient name"
msgstr ""
msgid "messages core application"
msgstr ""
msgid "User account is disabled"
msgstr ""
#, python-format
msgid "Message body size (%(body_size)s MB) exceeds the %(max_size)s MB limit. Please reduce message content."
msgstr ""
#, python-format
msgid "Cannot add attachment(s) (%(new_size)s MB). Total attachments would be %(total_size)s MB, exceeding the %(max_size)s MB limit. Current attachments: %(current_size)s MB."
msgstr ""
#, python-format
msgid "Too many recipients: %(count)s (maximum is %(max)s). Please reduce the number of recipients before sending."
msgstr ""
#, python-format
msgid "Total attachment size (%(total_size)s MB) exceeds the %(max_size)s MB limit. Please remove or reduce attachments."
msgstr ""
#, python-format
msgid "The composed email (%(mime_size)s MB) exceeds the maximum allowed size of %(max_size)s MB. Please reduce message content or attachments."
msgstr ""
msgid "id"
msgstr ""
msgid "primary key for the record as UUID"
msgstr ""
msgid "created on"
msgstr ""
msgid "date and time at which a record was created"
msgstr ""
msgid "updated on"
msgstr ""
msgid "date and time at which a record was last updated"
msgstr ""
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
msgid "sub"
msgstr ""
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr ""
msgid "full name"
msgstr ""
msgid "identity email address"
msgstr ""
msgid "admin email address"
msgstr ""
msgid "language"
msgstr ""
msgid "The language in which the user wants to see the interface."
msgstr ""
msgid "The timezone in which the user wants to see times."
msgstr ""
msgid "staff status"
msgstr ""
msgid "Whether the user can log into this admin site."
msgstr ""
msgid "active"
msgstr ""
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
msgid "Custom attributes"
msgstr ""
msgid "Metadata to sync to the user in the identity provider."
msgstr ""
msgid "user"
msgstr ""
msgid "users"
msgstr ""
msgid "Enter a valid domain name. This value may contain only lowercase letters, numbers, dots and - characters."
msgstr ""
msgid "name"
msgstr ""
msgid "oidc autojoin"
msgstr ""
msgid "Create mailboxes automatically based on OIDC emails."
msgstr ""
msgid "Identity sync"
msgstr ""
msgid "Sync mailboxes to an identity provider."
msgstr ""
msgid "Custom settings"
msgstr ""
msgid "Custom settings for the mail domain."
msgstr ""
msgid "Metadata to sync to the maildomain group in the identity provider."
msgstr ""
msgid "mail domain"
msgstr ""
msgid "mail domains"
msgstr ""
msgid "Human-readable name for this channel"
msgstr ""
msgid "type"
msgstr ""
msgid "Type of channel"
msgstr ""
msgid "settings"
msgstr ""
msgid "Channel-specific configuration settings"
msgstr ""
msgid "Mailbox that receives messages from this channel"
msgstr ""
msgid "Mail domain that owns this channel"
msgstr ""
msgid "channel"
msgstr ""
msgid "channels"
msgstr ""
msgid "local part"
msgstr ""
msgid "is identity"
msgstr ""
msgid "Whether this mailbox identifies a person (i.e. is not an alias or a group)"
msgstr ""
msgid "mailbox"
msgstr ""
msgid "mailboxes"
msgstr ""
msgid "role"
msgstr ""
msgid "accessed at"
msgstr ""
msgid "mailbox access"
msgstr ""
msgid "mailbox accesses"
msgstr ""
msgid "subject"
msgstr ""
msgid "snippet"
msgstr ""
msgid "has unread"
msgstr ""
msgid "has trashed"
msgstr ""
msgid "is trashed"
msgstr ""
msgid "Whether all messages in the thread are trashed"
msgstr ""
msgid "has archived"
msgstr ""
msgid "has draft"
msgstr ""
msgid "has starred"
msgstr ""
msgid "has sender"
msgstr ""
msgid "has messages"
msgstr ""
msgid "has attachments"
msgstr ""
msgid "is spam"
msgstr ""
msgid "has active"
msgstr ""
msgid "messaged at"
msgstr ""
msgid "sender names"
msgstr ""
msgid "summary"
msgstr ""
msgid "thread"
msgstr ""
msgid "threads"
msgstr ""
msgid "Name of the label/folder (can use slashes for hierarchy, e.g. 'Work/Projects')"
msgstr ""
msgid "slug"
msgstr ""
msgid "URL-friendly version of the name"
msgstr ""
msgid "color"
msgstr ""
msgid "Color of the label in hex format (e.g. #FF0000)"
msgstr ""
msgid "Mailbox that owns this label"
msgstr ""
msgid "Threads that have this label"
msgstr ""
msgid "description"
msgstr ""
msgid "Description of the label, used by AI to understand its purpose"
msgstr ""
msgid "auto labeling"
msgstr ""
msgid "Whether this label should be automatically applied by AI"
msgstr ""
msgid "label"
msgstr ""
msgid "labels"
msgstr ""
msgid "thread access"
msgstr ""
msgid "thread accesses"
msgstr ""
msgid "email"
msgstr ""
msgid "contact"
msgstr ""
msgid "contacts"
msgstr ""
msgid "delivered at"
msgstr ""
msgid "delivery message"
msgstr ""
msgid "retry count"
msgstr ""
msgid "retry at"
msgstr ""
msgid "message recipient"
msgstr ""
msgid "message recipients"
msgstr ""
msgid "is draft"
msgstr ""
msgid "is sender"
msgstr ""
msgid "is starred"
msgstr ""
msgid "is unread"
msgstr ""
msgid "is archived"
msgstr ""
msgid "trashed at"
msgstr ""
msgid "sent at"
msgstr ""
msgid "read at"
msgstr ""
msgid "archived at"
msgstr ""
msgid "mime id"
msgstr ""
msgid "Signature template for the message"
msgstr ""
msgid "message"
msgstr ""
msgid "messages"
msgstr ""
msgid "No subject"
msgstr ""
msgid "Message ID"
msgstr ""
msgid "From"
msgstr ""
msgid "To"
msgstr ""
msgid "CC"
msgstr ""
msgid "Date"
msgstr ""
msgid "Subject"
msgstr ""
msgid "Body"
msgstr ""
msgid "raw data"
msgstr ""
msgid "Raw email message bytes"
msgstr ""
msgid "error message"
msgstr ""
msgid "Error message if processing failed"
msgstr ""
msgid "inbound message"
msgstr ""
msgid "inbound messages"
msgstr ""
msgid "sha256 hash"
msgstr ""
msgid "SHA-256 hash of the uncompressed blob content"
msgstr ""
msgid "file size"
msgstr ""
msgid "Size of the blob in bytes"
msgstr ""
msgid "compressed size"
msgstr ""
msgid "Size of the compressed blob in bytes"
msgstr ""
msgid "content type"
msgstr ""
msgid "MIME type of the blob"
msgstr ""
msgid "compression"
msgstr ""
msgid "raw content"
msgstr ""
msgid "Compressed binary content of the blob"
msgstr ""
msgid "Mailbox that owns this blob"
msgstr ""
msgid "Mail domain that owns this blob"
msgstr ""
msgid "blob"
msgstr ""
msgid "blobs"
msgstr ""
msgid "file name"
msgstr ""
msgid "Original filename of the attachment"
msgstr ""
msgid "Reference to the blob containing the attachment data"
msgstr ""
msgid "Mailbox that owns this attachment"
msgstr ""
msgid "Messages that use this attachment"
msgstr ""
msgid "content ID"
msgstr ""
msgid "Content-ID for inline images"
msgstr ""
msgid "attachment"
msgstr ""
msgid "attachments"
msgstr ""
msgid "mail domain access"
msgstr ""
msgid "mail domain accesses"
msgstr ""
msgid "selector"
msgstr ""
msgid "DKIM selector (e.g., 'default', 'mail')"
msgstr ""
msgid "private key"
msgstr ""
msgid "DKIM private key in PEM format (encrypted)"
msgstr ""
msgid "public key"
msgstr ""
msgid "DKIM public key for DNS record generation"
msgstr ""
msgid "algorithm"
msgstr ""
msgid "DKIM signing algorithm"
msgstr ""
msgid "key size"
msgstr ""
msgid "Key size in bits (e.g., 2048, 4096 for RSA)"
msgstr ""
msgid "is active"
msgstr ""
msgid "Whether this DKIM key is active and should be used for signing"
msgstr ""
msgid "Domain that owns this DKIM key"
msgstr ""
msgid "DKIM key"
msgstr ""
msgid "DKIM keys"
msgstr ""
msgid "Name of the template (e.g., 'Standard Reply', 'Out of Office', 'Work Signature')"
msgstr ""
msgid "Reference to the blob containing template content as JSON: {html: str, text: str, raw: any}"
msgstr ""
msgid "Type of template (message, signature)"
msgstr ""
msgid "Whether this template is available for use"
msgstr ""
msgid "Mail domain that can use this template"
msgstr ""
msgid "Mailbox that can use this template"
msgstr ""
msgid "is forced"
msgstr ""
msgid "Whether this template is forced; no other template of the same type can be used in the same scope"
msgstr ""
msgid "is default"
msgstr ""
msgid "Whether this template is the default; it will be automatically loaded when composing a new message"
msgstr ""
msgid "message template"
msgstr ""
msgid "message templates"
msgstr ""
msgid "Provision DNS records"
msgstr ""
msgid "Retry to send to pending recipients"
msgstr ""
msgid "Import from IMAP"
msgstr ""
msgid "Home"
msgstr ""
msgid "Please correct the error below."
msgstr ""
msgid "Please correct the errors below."
msgstr ""
msgid "Start Import"
msgstr ""
msgid "English"
msgstr ""
msgid "French"
msgstr ""
msgid "Dutch"
msgstr ""
Binary file not shown.
@@ -1,680 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: lasuite-messages\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-25 11:10+0000\n"
"PO-Revision-Date: 2026-02-03 17:00\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
"X-Crowdin-Project: lasuite-messages\n"
"X-Crowdin-Project-ID: 831182\n"
"X-Crowdin-Language: ru\n"
"X-Crowdin-File: backend.pot\n"
"X-Crowdin-File-ID: 40\n"
msgid "delivery status"
msgstr "состояние доставки"
#, python-format
msgid "%(message_count)d messages - Retry send message task queued (id: %(task_id)s)."
msgstr "%(message_count)d сообщений - Повторить задачу отправки в очереди (id: %(task_id)s)."
msgid "Retry to send selected messages to pending recipients"
msgstr "Повторить отправку выбранных сообщений ожидающим получателям"
msgid "Personal info"
msgstr "Личные данные"
msgid "Permissions"
msgstr "Разрешения"
msgid "Important dates"
msgstr "Важные даты"
msgid "Mail domain not found."
msgstr "Почтовый домен не найден."
#, python-format
msgid "DNS provisioning successful via %(provider)s: %(changes)s"
msgstr "Успешная настройка DNS через %(provider)s: %(changes)s"
#, python-format
msgid "DNS provisioning successful via %(provider)s (no changes needed)."
msgstr "Успешная настройка DNS через %(provider)s (изменения не требуются)."
#, python-format
msgid "DNS provisioning failed: %(error)s"
msgstr "Не удалось настроить DNS: %(error)s"
msgid "Statistics"
msgstr "Статистика"
msgid "Metadata"
msgstr "Метаданные"
msgid "Labels"
msgstr "Ярлыки"
msgid "No labels"
msgstr "Без ярлыков"
msgid "Import Messages"
msgstr "Импорт сообщений"
msgid "Import Messages from IMAP"
msgstr "Импорт сообщений IMAP"
msgid "Message not found."
msgstr "Сообщение не найдено."
msgid "No pending recipients found for this message."
msgstr "Для этого сообщения не найдено ни одного получателя."
#, python-format
msgid "Retry task has been queued for %(retryable_recipients_count)d pending recipient(s) (id: %(task_id)s)."
msgstr "Повторная задача была поставлена в очередь для %(retryable_recipients_count)d ожидающих получателей (id: %(task_id)s)."
msgid "Keys"
msgstr "Ключи"
msgid "Error"
msgstr "Ошибка"
msgid "A mailbox with this local part already exists in this domain."
msgstr "Почтовый ящик с этой локальной частью уже существует в этом домене."
msgid "Name"
msgstr "Имя"
msgid "Recipient name"
msgstr "Имя получателя"
msgid "messages core application"
msgstr "приложение для сообщений"
msgid "User account is disabled"
msgstr "Учётная запись пользователя отключена"
#, python-format
msgid "Message body size (%(body_size)s MB) exceeds the %(max_size)s MB limit. Please reduce message content."
msgstr "Размер тела сообщения (%(body_size)s МБ) превышает предел в %(max_size)s МБ. Пожалуйста, уменьшите содержимое сообщения."
#, python-format
msgid "Cannot add attachment(s) (%(new_size)s MB). Total attachments would be %(total_size)s MB, exceeding the %(max_size)s MB limit. Current attachments: %(current_size)s MB."
msgstr "Невозможно добавить вложение(я) (%(new_size)s МБ). Суммарный размер вложений составит %(total_size)s МБ, что превысит ограничение в %(max_size)s МБ. Размер текущих вложений: %(current_size)s МБ."
#, python-format
msgid "Too many recipients: %(count)s (maximum is %(max)s). Please reduce the number of recipients before sending."
msgstr "Слишком много получателей: %(count)s (максимум %(max)s). Уменьшите количество получателей до отправки."
#, python-format
msgid "Total attachment size (%(total_size)s MB) exceeds the %(max_size)s MB limit. Please remove or reduce attachments."
msgstr "Общий размер вложений (%(total_size)s МБ) превышает предел в %(max_size)s МБ. Пожалуйста, удалите или уменьшите вложения."
#, python-format
msgid "The composed email (%(mime_size)s MB) exceeds the maximum allowed size of %(max_size)s MB. Please reduce message content or attachments."
msgstr "Размер созданного письма (%(mime_size)s МБ) превышает максимально допустимый размер в %(max_size)s МБ. Уменьшите размер содержимого или вложений."
msgid "id"
msgstr "id"
msgid "primary key for the record as UUID"
msgstr "первичный ключ для записи как UUID"
msgid "created on"
msgstr "создано"
msgid "date and time at which a record was created"
msgstr "дата и время создания записи"
msgid "updated on"
msgstr "обновлено"
msgid "date and time at which a record was last updated"
msgstr "дата и время последнего обновления записи"
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "Мы не смогли найти пользователя с этими данными, но этот адрес уже связан с зарегистрированным пользователем."
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr "Введите правильный префикс. Он может содержать только буквы, цифры и символы @/./+/-/_/."
msgid "sub"
msgstr "префикс"
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr "Обязательно. 255 символов или меньше. Только буквы, цифры и @/./+/-/_/: /."
msgid "full name"
msgstr "полное имя"
msgid "identity email address"
msgstr "личный адрес электронной почты"
msgid "admin email address"
msgstr "e-mail администратора"
msgid "language"
msgstr "язык"
msgid "The language in which the user wants to see the interface."
msgstr "Язык, на котором пользователь хочет видеть интерфейс."
msgid "The timezone in which the user wants to see times."
msgstr "Часовой пояс, в котором пользователь хочет видеть время."
msgid "staff status"
msgstr "статус сотрудника"
msgid "Whether the user can log into this admin site."
msgstr "Может ли пользователь войти на этот административный сайт."
msgid "active"
msgstr "активный"
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Должен ли пользователь рассматриваться как активный. Альтернатива удалению учётных записей."
msgid "Custom attributes"
msgstr "Настраиваемые атрибуты"
msgid "Metadata to sync to the user in the identity provider."
msgstr "Метаданные для синхронизации с пользователем в поставщике идентификационных данных."
msgid "user"
msgstr "пользователь"
msgid "users"
msgstr "пользователи"
msgid "Enter a valid domain name. This value may contain only lowercase letters, numbers, dots and - characters."
msgstr "Введите правильное имя домена. Это значение может содержать только строчные буквы, цифры, точки и символы дефиса."
msgid "name"
msgstr "имя"
msgid "oidc autojoin"
msgstr "oidc autojoin"
msgid "Create mailboxes automatically based on OIDC emails."
msgstr "Создание почтовых ящиков автоматически на основе сообщений OIDC."
msgid "Identity sync"
msgstr "Синхронизация личных данных"
msgid "Sync mailboxes to an identity provider."
msgstr "Синхронизировать почтовые ящики с поставщиком идентификационных данных."
msgid "Custom settings"
msgstr "Пользовательские настройки"
msgid "Custom settings for the mail domain."
msgstr "Пользовательские настройки для почтового домена."
msgid "Metadata to sync to the maildomain group in the identity provider."
msgstr "Метаданные для синхронизации с пользователем в поставщике идентификационных данных."
msgid "mail domain"
msgstr "почтовый домен"
msgid "mail domains"
msgstr "почтовые домены"
msgid "Human-readable name for this channel"
msgstr "Человекочитаемое название для этого канала"
msgid "type"
msgstr "тип"
msgid "Type of channel"
msgstr "Тип канала"
msgid "settings"
msgstr "настройки"
msgid "Channel-specific configuration settings"
msgstr "Специфичные настройки конфигурации канала"
msgid "Mailbox that receives messages from this channel"
msgstr "Почтовый ящик, который получает сообщения от этого канала"
msgid "Mail domain that owns this channel"
msgstr "Домен почты, у которого есть этот канал"
msgid "channel"
msgstr "канал"
msgid "channels"
msgstr "каналы"
msgid "local part"
msgstr "локальная часть"
msgid "is identity"
msgstr "является личностью"
msgid "Whether this mailbox identifies a person (i.e. is not an alias or a group)"
msgstr "Определяет ли этот почтовый ящик человека (т.е. не является псевдонимом или группой)"
msgid "mailbox"
msgstr "почтовый ящик"
msgid "mailboxes"
msgstr "почтовые ящики"
msgid "role"
msgstr "роль"
msgid "accessed at"
msgstr "доступ в"
msgid "mailbox access"
msgstr "доступ к почтовому ящику"
msgid "mailbox accesses"
msgstr "доступ к почтовому ящику"
msgid "subject"
msgstr "тема"
msgid "snippet"
msgstr "фрагмент"
msgid "has unread"
msgstr "содержит непрочитанные"
msgid "has trashed"
msgstr "содержит удалённые"
msgid "is trashed"
msgstr "в корзине"
msgid "Whether all messages in the thread are trashed"
msgstr "Все ли сообщения в обсуждении удаляются"
msgid "has archived"
msgstr "заархивировано"
msgid "has draft"
msgstr "содержит черновики"
msgid "has starred"
msgstr "содержит избранное"
msgid "has sender"
msgstr "содержит отправленные"
msgid "has messages"
msgstr "содержит сообщения"
msgid "has attachments"
msgstr "содержит вложения"
msgid "is spam"
msgstr "является спамом"
msgid "has active"
msgstr "содержит активные"
msgid "messaged at"
msgstr "сообщение в"
msgid "sender names"
msgstr "имена отправителя"
msgid "summary"
msgstr "сводка"
msgid "thread"
msgstr "обсуждение"
msgid "threads"
msgstr "обсуждения"
msgid "Name of the label/folder (can use slashes for hierarchy, e.g. 'Work/Projects')"
msgstr "Имя ярлыка/папки (может использовать косую черту для задания иерархии, например 'Работа/Проекты')"
msgid "slug"
msgstr "псевдоним"
msgid "URL-friendly version of the name"
msgstr "Версия имени, совместимая с форматом адресов URL"
msgid "color"
msgstr "цвет"
msgid "Color of the label in hex format (e.g. #FF0000)"
msgstr "Цвет ярлыка в шестнадцатеричном формате (например, #FF0000)"
msgid "Mailbox that owns this label"
msgstr "Почтовый ящик с этим ярлыком"
msgid "Threads that have this label"
msgstr "Обсуждения с этим ярлыком"
msgid "description"
msgstr "описание"
msgid "Description of the label, used by AI to understand its purpose"
msgstr "Описание ярлыка, используемое ИИ для понимания его назначения"
msgid "auto labeling"
msgstr "автомаркировка ярлыками"
msgid "Whether this label should be automatically applied by AI"
msgstr "Должен ли этот ярлык автоматически применяться ИИ"
msgid "label"
msgstr "ярлык"
msgid "labels"
msgstr "ярлыки"
msgid "thread access"
msgstr "доступ к обсуждению"
msgid "thread accesses"
msgstr "доступы к обсуждению"
msgid "email"
msgstr "эл. почта"
msgid "contact"
msgstr "контакт"
msgid "contacts"
msgstr "контакты"
msgid "delivered at"
msgstr "доставлено"
msgid "delivery message"
msgstr "сообщение для доставки"
msgid "retry count"
msgstr "количество повторов"
msgid "retry at"
msgstr "повторить"
msgid "message recipient"
msgstr "получатель сообщения"
msgid "message recipients"
msgstr "получатели сообщения"
msgid "is draft"
msgstr "является черновиком"
msgid "is sender"
msgstr "отправитель"
msgid "is starred"
msgstr "помечено"
msgid "is unread"
msgstr "непрочитано"
msgid "is archived"
msgstr "заархивировано"
msgid "trashed at"
msgstr "в корзине"
msgid "sent at"
msgstr "отправлено"
msgid "read at"
msgstr "прочитано"
msgid "archived at"
msgstr "архивировано"
msgid "mime id"
msgstr "mime id"
msgid "Signature template for the message"
msgstr "Шаблон подписи для сообщения"
msgid "message"
msgstr "сообщение"
msgid "messages"
msgstr "сообщения"
msgid "No subject"
msgstr "Без темы"
msgid "Message ID"
msgstr "ID сообщения"
msgid "From"
msgstr "От"
msgid "To"
msgstr "Кому"
msgid "CC"
msgstr "Копия"
msgid "Date"
msgstr "Дата"
msgid "Subject"
msgstr "Тема"
msgid "Body"
msgstr "Текст сообщения"
msgid "raw data"
msgstr "необработанные данные"
msgid "Raw email message bytes"
msgstr "Сырой текст байтов сообщения"
msgid "error message"
msgstr "сообщение об ошибке"
msgid "Error message if processing failed"
msgstr "Сообщение об ошибке, если не удалось обработать"
msgid "inbound message"
msgstr "входящее сообщение"
msgid "inbound messages"
msgstr "входящие сообщения"
msgid "sha256 hash"
msgstr "sha256 hash"
msgid "SHA-256 hash of the uncompressed blob content"
msgstr "Хэш SHA-256 без сжатого содержимого"
msgid "file size"
msgstr "размер файла"
msgid "Size of the blob in bytes"
msgstr "Размер блока в байтах"
msgid "compressed size"
msgstr "сжатый размер"
msgid "Size of the compressed blob in bytes"
msgstr "Размер сжатого блока в байтах"
msgid "content type"
msgstr "тип содержимого"
msgid "MIME type of the blob"
msgstr "MIME тип блока"
msgid "compression"
msgstr "сжатие"
msgid "raw content"
msgstr "необработанное содержимое"
msgid "Compressed binary content of the blob"
msgstr "Сжатое двоичное содержимое блока"
msgid "Mailbox that owns this blob"
msgstr "Почтовый ящик, у которого есть этот блок"
msgid "Mail domain that owns this blob"
msgstr "Домен почты, у которого есть этот блок"
msgid "blob"
msgstr "блок"
msgid "blobs"
msgstr "блоки"
msgid "file name"
msgstr "имя файла"
msgid "Original filename of the attachment"
msgstr "Исходное имя файла вложения"
msgid "Reference to the blob containing the attachment data"
msgstr "Ссылка на блок, содержащий данные шаблона в формате Json"
msgid "Mailbox that owns this attachment"
msgstr "Почтовый ящик, у которого есть это вложение"
msgid "Messages that use this attachment"
msgstr "Сообщения, которые используют это вложение"
msgid "content ID"
msgstr "content ID"
msgid "Content-ID for inline images"
msgstr "Content-ID для встроенных изображений"
msgid "attachment"
msgstr "вложение"
msgid "attachments"
msgstr "вложения"
msgid "mail domain access"
msgstr "доступ к почтовому домену"
msgid "mail domain accesses"
msgstr "доступ к почтовому домену"
msgid "selector"
msgstr "селектор"
msgid "DKIM selector (e.g., 'default', 'mail')"
msgstr "Селектор DKIM (например, 'default', 'mail')"
msgid "private key"
msgstr "закрытый ключ"
msgid "DKIM private key in PEM format (encrypted)"
msgstr "Закрытый ключ DKIM в формате PEM (зашифрованный)"
msgid "public key"
msgstr "открытый ключ"
msgid "DKIM public key for DNS record generation"
msgstr "Открытый ключ DKIM для генерации DNS записей"
msgid "algorithm"
msgstr "алгоритм"
msgid "DKIM signing algorithm"
msgstr "Алгоритм подписи DKIM"
msgid "key size"
msgstr "размер ключа"
msgid "Key size in bits (e.g., 2048, 4096 for RSA)"
msgstr "Размер ключа в битах (например, 2048, 4096 для RSA)"
msgid "is active"
msgstr "активный"
msgid "Whether this DKIM key is active and should be used for signing"
msgstr "Является ли этот ключ DKIM активным и должен использоваться для подписания"
msgid "Domain that owns this DKIM key"
msgstr "Домен, владеющий этим ключом DKIM"
msgid "DKIM key"
msgstr "Ключ DKIM"
msgid "DKIM keys"
msgstr "Ключи DKIM"
msgid "Name of the template (e.g., 'Standard Reply', 'Out of Office', 'Work Signature')"
msgstr "Название шаблона (например, 'Стандартный ответ', 'Не в офисе', 'Рабочая подпись')"
msgid "Reference to the blob containing template content as JSON: {html: str, text: str, raw: any}"
msgstr "Ссылка на блок, содержащий данные шаблона в формате JSON: {html: str, text: str, raw: any}"
msgid "Type of template (message, signature)"
msgstr "Тип шаблона (сообщение, подпись)"
msgid "Whether this template is available for use"
msgstr "Доступен ли этот шаблон для использования"
msgid "Mail domain that can use this template"
msgstr "Домен почты, который может использовать этот шаблон"
msgid "Mailbox that can use this template"
msgstr "Почтовый ящик, который может использовать этот шаблон"
msgid "is forced"
msgstr "обязательно"
msgid "Whether this template is forced; no other template of the same type can be used in the same scope"
msgstr "Может ли этот шаблон применяться в обязательном порядке; будет невозможно использовать другой шаблон того же типа в той же области"
msgid "is default"
msgstr "по-умолчанию"
msgid "Whether this template is the default; it will be automatically loaded when composing a new message"
msgstr "Шаблон по умолчанию; он будет автоматически загружен при написании нового сообщения"
msgid "message template"
msgstr "шаблон сообщения"
msgid "message templates"
msgstr "шаблоны сообщений"
msgid "Provision DNS records"
msgstr "Настройка записей DNS"
msgid "Retry to send to pending recipients"
msgstr "Повторить отправку ожидающим получателям"
msgid "Import from IMAP"
msgstr "Импорт из IMAP"
msgid "Home"
msgstr "Главная"
msgid "Please correct the error below."
msgstr "Пожалуйста, исправьте следующую ошибку."
msgid "Please correct the errors below."
msgstr "Пожалуйста, исправьте следующие ошибки."
msgid "Start Import"
msgstr "Начать импорт"
msgid "English"
msgstr "Английский"
msgid "French"
msgstr "Французский"
msgid "Dutch"
msgstr "Нидерланский"
Binary file not shown.
@@ -1,680 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: lasuite-messages\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-25 11:10+0000\n"
"PO-Revision-Date: 2026-02-03 17:00\n"
"Last-Translator: \n"
"Language-Team: Ukrainian\n"
"Language: uk_UA\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
"X-Crowdin-Project: lasuite-messages\n"
"X-Crowdin-Project-ID: 831182\n"
"X-Crowdin-Language: uk\n"
"X-Crowdin-File: backend.pot\n"
"X-Crowdin-File-ID: 40\n"
msgid "delivery status"
msgstr "стан доставки"
#, python-format
msgid "%(message_count)d messages - Retry send message task queued (id: %(task_id)s)."
msgstr "%(message_count)d повідомлень - Повторити завдання відправлення повідомлення в черзі (id: %(task_id)s)."
msgid "Retry to send selected messages to pending recipients"
msgstr "Повторити спробу надсилання вибраних повідомлень до очікуючих отримувачів"
msgid "Personal info"
msgstr "Особисті дані"
msgid "Permissions"
msgstr "Дозволи"
msgid "Important dates"
msgstr "Важливі дати"
msgid "Mail domain not found."
msgstr "Поштовий домен не знайдено."
#, python-format
msgid "DNS provisioning successful via %(provider)s: %(changes)s"
msgstr "Успішне налаштування DNS через %(provider)s: %(changes)s"
#, python-format
msgid "DNS provisioning successful via %(provider)s (no changes needed)."
msgstr "Успішне налаштування DNS через %(provider)s (зміни не потрібні)."
#, python-format
msgid "DNS provisioning failed: %(error)s"
msgstr "Помилка під час налаштування DNS: %(error)s"
msgid "Statistics"
msgstr "Статистика"
msgid "Metadata"
msgstr "Метадані"
msgid "Labels"
msgstr "Мітки"
msgid "No labels"
msgstr "Без міток"
msgid "Import Messages"
msgstr "Імпорт повідомлень"
msgid "Import Messages from IMAP"
msgstr "Імпорт повідомлень з IMAP"
msgid "Message not found."
msgstr "Повідомлення не знайдене."
msgid "No pending recipients found for this message."
msgstr "Для цього повідомлення не знайдено жодного отримувача."
#, python-format
msgid "Retry task has been queued for %(retryable_recipients_count)d pending recipient(s) (id: %(task_id)s)."
msgstr "Завдання повтору було поставлено в чергу на %(retryable_recipients_count)d одержувачів (id: %(task_id)s)."
msgid "Keys"
msgstr "Ключі"
msgid "Error"
msgstr "Помилка"
msgid "A mailbox with this local part already exists in this domain."
msgstr "Поштова скринька з цією локальною частиною вже існує в цьому домені."
msgid "Name"
msgstr "Ім’я"
msgid "Recipient name"
msgstr "Ім`я отримувача"
msgid "messages core application"
msgstr "повідомлення основної програми"
msgid "User account is disabled"
msgstr "Обліковий запис користувача вимкнено"
#, python-format
msgid "Message body size (%(body_size)s MB) exceeds the %(max_size)s MB limit. Please reduce message content."
msgstr "Розмір тіла повідомлення (%(body_size)s МБ) перевищує ліміт у %(max_size)s МБ. Будь ласка, зменште вміст повідомлення."
#, python-format
msgid "Cannot add attachment(s) (%(new_size)s MB). Total attachments would be %(total_size)s MB, exceeding the %(max_size)s MB limit. Current attachments: %(current_size)s MB."
msgstr "Неможливо додати вкладення (одне чи декілька) (%(new_size)s МБ). Загальний розмір вкладень - %(total_size)s МБ, що перевищує ліміт %(max_size)s МБ. Поточних вкладень: %(current_size)s МБ."
#, python-format
msgid "Too many recipients: %(count)s (maximum is %(max)s). Please reduce the number of recipients before sending."
msgstr "Занадто багато отримувачів: %(count)s (максимум %(max)s). Зменште кількість одержувачів, перш ніж надсилати."
#, python-format
msgid "Total attachment size (%(total_size)s MB) exceeds the %(max_size)s MB limit. Please remove or reduce attachments."
msgstr "Загальний розмір вкладення (%(total_size)s МБ) перевищує ліміт %(max_size)s MB. Будь ласка, видаліть або зменште вкладення."
#, python-format
msgid "The composed email (%(mime_size)s MB) exceeds the maximum allowed size of %(max_size)s MB. Please reduce message content or attachments."
msgstr "Розмір сформованого електронного листа (%(mime_size)s МБ) перевищує максимально дозволений розмір у %(max_size)s МБ. Будь ласка, зменште вміст повідомлення або вкладення."
msgid "id"
msgstr "id"
msgid "primary key for the record as UUID"
msgstr "первинний ключ для запису як UUID"
msgid "created on"
msgstr "створено"
msgid "date and time at which a record was created"
msgstr "дата і час, коли запис було створено"
msgid "updated on"
msgstr "оновлено"
msgid "date and time at which a record was last updated"
msgstr "дата і час, коли запис був востаннє оновлений"
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "Ми не змогли знайти користувача з цими даними, але адреса вже пов'язана з зареєстрованим користувачем."
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr "Введіть правильний префікс. Це значення може містити лише літери, цифри та символи @/./+/-/_/:."
msgid "sub"
msgstr "префікс"
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr "Обов'язково. 255 символів або менше. Лише літери, цифри та символи @/./+/-/_/:."
msgid "full name"
msgstr "повне ім'я"
msgid "identity email address"
msgstr "адреса електронної пошти особи"
msgid "admin email address"
msgstr "електронна адреса адміністратора"
msgid "language"
msgstr "мова"
msgid "The language in which the user wants to see the interface."
msgstr "Мова, якою користувач хоче бачити інтерфейс."
msgid "The timezone in which the user wants to see times."
msgstr "Часовий пояс, в якому користувач хоче бачити час."
msgid "staff status"
msgstr "статус співробітника"
msgid "Whether the user can log into this admin site."
msgstr "Чи може користувач увійти на цей сайт адміністратора."
msgid "active"
msgstr "активний"
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Чи слід ставитися до цього користувача як до активного. Зніміть вибір замість видалення облікового запису."
msgid "Custom attributes"
msgstr "Користувацькі атрибути"
msgid "Metadata to sync to the user in the identity provider."
msgstr "Метадані для синхронізації користувача в провайдері ідентифікації."
msgid "user"
msgstr "користувач"
msgid "users"
msgstr "користувачі"
msgid "Enter a valid domain name. This value may contain only lowercase letters, numbers, dots and - characters."
msgstr "Введіть допустиме ім'я домену. Це значення може містити лише малі літери, цифри, крапки та символи дефісу."
msgid "name"
msgstr "ім’я"
msgid "oidc autojoin"
msgstr "oidc autojoin"
msgid "Create mailboxes automatically based on OIDC emails."
msgstr "Автоматичне створення поштових скриньок на основі електронних листів OIDC."
msgid "Identity sync"
msgstr "Синхронізація ідентифікації"
msgid "Sync mailboxes to an identity provider."
msgstr "Синхронізація поштових скриньок для постачальника ідентифікації."
msgid "Custom settings"
msgstr "Користувацькі налаштування"
msgid "Custom settings for the mail domain."
msgstr "Користувацькі налаштування для поштового домену."
msgid "Metadata to sync to the maildomain group in the identity provider."
msgstr "Метадані для синхронізації користувача в провайдері ідентифікації."
msgid "mail domain"
msgstr "поштовий домен"
msgid "mail domains"
msgstr "поштові домени"
msgid "Human-readable name for this channel"
msgstr "Людино-читуване ім'я для цього каналу"
msgid "type"
msgstr "тип"
msgid "Type of channel"
msgstr "Тип каналу"
msgid "settings"
msgstr "параметри"
msgid "Channel-specific configuration settings"
msgstr "Параметри, що залежать від каналу"
msgid "Mailbox that receives messages from this channel"
msgstr "Поштова скринька, яка отримує повідомлення від цього каналу"
msgid "Mail domain that owns this channel"
msgstr "Поштовий домен, що володіє цим каналом"
msgid "channel"
msgstr "канал"
msgid "channels"
msgstr "канали"
msgid "local part"
msgstr "локальна частина"
msgid "is identity"
msgstr "є особистістю"
msgid "Whether this mailbox identifies a person (i.e. is not an alias or a group)"
msgstr "Чи буде ця поштова скринька ідентифікувати людину (тобто не псевдонім чи групу)"
msgid "mailbox"
msgstr "поштова скринька"
msgid "mailboxes"
msgstr "поштові скриньки"
msgid "role"
msgstr "роль"
msgid "accessed at"
msgstr "доступ до"
msgid "mailbox access"
msgstr "доступ до поштової скриньки"
msgid "mailbox accesses"
msgstr "доступ до поштової скриньки"
msgid "subject"
msgstr "тема"
msgid "snippet"
msgstr "фрагмент"
msgid "has unread"
msgstr "має непрочитані"
msgid "has trashed"
msgstr "у кошику"
msgid "is trashed"
msgstr "у кошику"
msgid "Whether all messages in the thread are trashed"
msgstr "Чи всі повідомлення в обговоренні видалені"
msgid "has archived"
msgstr "заархівовано"
msgid "has draft"
msgstr "має чернетку"
msgid "has starred"
msgstr "додано в обране"
msgid "has sender"
msgstr "має відправника"
msgid "has messages"
msgstr "має повідомлення"
msgid "has attachments"
msgstr "має вкладення"
msgid "is spam"
msgstr "є спамом"
msgid "has active"
msgstr "має активні"
msgid "messaged at"
msgstr "відправлене"
msgid "sender names"
msgstr "імена відправника"
msgid "summary"
msgstr "короткий зміст"
msgid "thread"
msgstr "обговорення"
msgid "threads"
msgstr "обговорення"
msgid "Name of the label/folder (can use slashes for hierarchy, e.g. 'Work/Projects')"
msgstr "Назва мітки/папки (може використовувати косі риски для ієрархії, наприклад, 'Робота/Проекти')"
msgid "slug"
msgstr "псевдонім"
msgid "URL-friendly version of the name"
msgstr "URL-дружня версія імені латиницею"
msgid "color"
msgstr "колір"
msgid "Color of the label in hex format (e.g. #FF0000)"
msgstr "Колір мітки у hex-форматі (наприклад, #FF0000)"
msgid "Mailbox that owns this label"
msgstr "Поштова скринька, що має цю мітку"
msgid "Threads that have this label"
msgstr "Обговорення з цією міткою"
msgid "description"
msgstr "опис"
msgid "Description of the label, used by AI to understand its purpose"
msgstr "Опис мітки, що використовується ШІ для розуміння її мети"
msgid "auto labeling"
msgstr "автоматичне позначення"
msgid "Whether this label should be automatically applied by AI"
msgstr "Чи буде ця мітка автоматично застосована ШІ"
msgid "label"
msgstr "мітка"
msgid "labels"
msgstr "мітки"
msgid "thread access"
msgstr "доступ до обговорення"
msgid "thread accesses"
msgstr "доступ до обговорень"
msgid "email"
msgstr "ел. пошта"
msgid "contact"
msgstr "контакт"
msgid "contacts"
msgstr "контакти"
msgid "delivered at"
msgstr "доставлено"
msgid "delivery message"
msgstr "повідомлення про доставку"
msgid "retry count"
msgstr "число повторних спроб"
msgid "retry at"
msgstr "повторити"
msgid "message recipient"
msgstr "одержувач повідомлення"
msgid "message recipients"
msgstr "одержувачі повідомлення"
msgid "is draft"
msgstr "чернетка"
msgid "is sender"
msgstr "відправник"
msgid "is starred"
msgstr "позначено зірочкою"
msgid "is unread"
msgstr "непрочитане"
msgid "is archived"
msgstr "архівовано"
msgid "trashed at"
msgstr "у кошику"
msgid "sent at"
msgstr "надіслано"
msgid "read at"
msgstr "читати"
msgid "archived at"
msgstr "архівовано"
msgid "mime id"
msgstr "mime id"
msgid "Signature template for the message"
msgstr "Шаблон підпису для повідомлення"
msgid "message"
msgstr "повідомлення"
msgid "messages"
msgstr "повідомлення"
msgid "No subject"
msgstr "Без теми"
msgid "Message ID"
msgstr "ID повідомлення"
msgid "From"
msgstr "Від"
msgid "To"
msgstr "Кому"
msgid "CC"
msgstr "Прихована копія"
msgid "Date"
msgstr "Дата"
msgid "Subject"
msgstr "Тема"
msgid "Body"
msgstr "Вміст"
msgid "raw data"
msgstr "необроблені дані"
msgid "Raw email message bytes"
msgstr "Необроблені байти повідомлення"
msgid "error message"
msgstr "повідомлення про помилку"
msgid "Error message if processing failed"
msgstr "Повідомлення про помилку, якщо не вдалося обробити"
msgid "inbound message"
msgstr "вхідне повідомлення"
msgid "inbound messages"
msgstr "вхідні повідомлення"
msgid "sha256 hash"
msgstr "sha256 hash"
msgid "SHA-256 hash of the uncompressed blob content"
msgstr "Контрольні суми SHA-256 нестиснутого вмісту"
msgid "file size"
msgstr "розмір файлу"
msgid "Size of the blob in bytes"
msgstr "Розмір блоку в байтах"
msgid "compressed size"
msgstr "стиснутий розмір"
msgid "Size of the compressed blob in bytes"
msgstr "Розмір стиснутого блоку в байтах"
msgid "content type"
msgstr "тип вмісту"
msgid "MIME type of the blob"
msgstr "MIME-тип блоку"
msgid "compression"
msgstr "стиснення"
msgid "raw content"
msgstr "сирий вміст"
msgid "Compressed binary content of the blob"
msgstr "Стиснутий бінарний вміст блоку"
msgid "Mailbox that owns this blob"
msgstr "Поштова скринька, що має цей блок"
msgid "Mail domain that owns this blob"
msgstr "Поштовий домен, що має цей блок"
msgid "blob"
msgstr "блок"
msgid "blobs"
msgstr "блоки"
msgid "file name"
msgstr "назва файлу"
msgid "Original filename of the attachment"
msgstr "Оригінальна назва вкладеного файлу"
msgid "Reference to the blob containing the attachment data"
msgstr "Посилання на блок, що містить дані вкладення"
msgid "Mailbox that owns this attachment"
msgstr "Поштова скринька, що має це вкладення"
msgid "Messages that use this attachment"
msgstr "Повідомлення, які використовують це вкладення"
msgid "content ID"
msgstr "content ID"
msgid "Content-ID for inline images"
msgstr "Content-ID для вбудованих зображень"
msgid "attachment"
msgstr "вкладення"
msgid "attachments"
msgstr "вкладення"
msgid "mail domain access"
msgstr "доступ до поштового домену"
msgid "mail domain accesses"
msgstr "доступи до поштового домену"
msgid "selector"
msgstr "селектор"
msgid "DKIM selector (e.g., 'default', 'mail')"
msgstr "DKIM селектор (наприклад, 'default', 'mail')"
msgid "private key"
msgstr "приватний ключ"
msgid "DKIM private key in PEM format (encrypted)"
msgstr "Приватний ключ DKIM у форматі PEM (зашифрований)"
msgid "public key"
msgstr "відкритий ключ"
msgid "DKIM public key for DNS record generation"
msgstr "Публічний ключ DKIM для генерації записів DNS"
msgid "algorithm"
msgstr "алгоритм"
msgid "DKIM signing algorithm"
msgstr "Алгоритм підпису DKIM"
msgid "key size"
msgstr "розмір ключа"
msgid "Key size in bits (e.g., 2048, 4096 for RSA)"
msgstr "Розмір ключа у бітах (наприклад, 2048, 4096 для RSA)"
msgid "is active"
msgstr "активний"
msgid "Whether this DKIM key is active and should be used for signing"
msgstr "Чи є цей ключ DKIM активним і повинен використовуватися для підпису"
msgid "Domain that owns this DKIM key"
msgstr "Домен, який володіє цим ключем DKIM"
msgid "DKIM key"
msgstr "Ключ DKIM"
msgid "DKIM keys"
msgstr "Ключі DKIM"
msgid "Name of the template (e.g., 'Standard Reply', 'Out of Office', 'Work Signature')"
msgstr "Назва шаблону (наприклад, 'Стандартна відповідь', 'Не в офісі', 'Підпис роботи')"
msgid "Reference to the blob containing template content as JSON: {html: str, text: str, raw: any}"
msgstr "Посилання на блок, що містить вміст шаблону в форматі JSON: {html: str, text: str, raw: any}"
msgid "Type of template (message, signature)"
msgstr "Тип шаблону (повідомлення, підпис)"
msgid "Whether this template is available for use"
msgstr "Чи доступний цей шаблон для використання"
msgid "Mail domain that can use this template"
msgstr "Поштовий домен, який може використовувати цей шаблон"
msgid "Mailbox that can use this template"
msgstr "Поштова скринька, що може використовувати цей шаблон"
msgid "is forced"
msgstr "обов'язковий"
msgid "Whether this template is forced; no other template of the same type can be used in the same scope"
msgstr "Чи є цей шаблон обов'язковим; жоден інший шаблон того ж типу не може використовуватися в тій же області"
msgid "is default"
msgstr "за замовчуванням"
msgid "Whether this template is the default; it will be automatically loaded when composing a new message"
msgstr "Чи є цей шаблон за замовчуванням; він буде автоматично завантажений під час додання нового повідомлення"
msgid "message template"
msgstr "шаблон повідомлення"
msgid "message templates"
msgstr "шаблони повідомлень"
msgid "Provision DNS records"
msgstr "Налаштування записів DNS"
msgid "Retry to send to pending recipients"
msgstr "Повторити спробу надсилання до очікуючих отримувачів"
msgid "Import from IMAP"
msgstr "Імпорт з IMAP"
msgid "Home"
msgstr "Головна"
msgid "Please correct the error below."
msgstr "Будь ласка, виправте помилку, що вказана нижче."
msgid "Please correct the errors below."
msgstr "Будь ласка, виправте помилки, що вказані нижче."
msgid "Start Import"
msgstr "Почати імпорт"
msgid "English"
msgstr "Англійська"
msgid "French"
msgstr "Французька"
msgid "Dutch"
msgstr "Нідерландська"
+7 -29
View File
@@ -16,8 +16,6 @@ import os
import tomllib
from socket import gethostbyname, gethostname
from django.utils.translation import gettext_lazy as _
import dj_database_url
import sentry_sdk
from configurations import Configuration, values
@@ -459,8 +457,6 @@ class Base(Configuration):
# Languages
LANGUAGE_CODE = values.Value("en-us")
LANGUAGE_COOKIE_NAME = "messages_language" # cookie & language is set from frontend
DRF_NESTED_MULTIPART_PARSER = {
# output of parser is converted to querydict
# if is set to False, dict python is returned
@@ -471,16 +467,14 @@ class Base(Configuration):
# fallback/default languages throughout the app.
LANGUAGES = values.SingleNestedTupleValue(
(
("en-us", _("English")),
("fr-fr", _("French")),
("nl-nl", _("Dutch")),
("en-us", "English"),
("fr-fr", "French"),
("nl-nl", "Dutch"),
)
)
LOCALE_PATHS = (os.path.join(BASE_DIR, "locale"),)
TIME_ZONE = "UTC"
USE_I18N = True
USE_I18N = False
USE_TZ = True
# Templates
@@ -494,7 +488,6 @@ class Base(Configuration):
"django.contrib.messages.context_processors.messages",
"django.template.context_processors.csrf",
"django.template.context_processors.debug",
"django.template.context_processors.i18n",
"django.template.context_processors.media",
"django.template.context_processors.request",
"django.template.context_processors.tz",
@@ -511,7 +504,6 @@ class Base(Configuration):
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"core.middlewares.CustomCorsMiddleware",
"django.middleware.common.CommonMiddleware",
@@ -1004,20 +996,6 @@ class Base(Configuration):
"""
return get_release()
# pylint: disable=invalid-name
@property
def PARLER_LANGUAGES(self):
"""
Return languages for Parler computed from the LANGUAGES and LANGUAGE_CODE settings.
"""
return {
self.SITE_ID: tuple({"code": code} for code, _name in self.LANGUAGES),
"default": {
"fallbacks": [self.LANGUAGE_CODE],
"hide_untranslated": False,
},
}
@classmethod
def post_setup(cls):
"""Post setup configuration.
@@ -1138,16 +1116,16 @@ class E2E(Development):
"""
End2End environment settings
Uses nginx reverse proxy to serve both frontend and backend on the same origin,
Uses Caddy reverse proxy to serve both frontend and backend on the same origin,
avoiding cross-origin cookie issues.
"""
# Include the e2e app only in E2E environment
INSTALLED_APPS = Development.INSTALLED_APPS + ["e2e"]
CSRF_TRUSTED_ORIGINS = ["http://nginx", "http://keycloak:8802"]
CSRF_TRUSTED_ORIGINS = ["http://proxy", "http://keycloak:8802"]
# Trust X-Forwarded-* headers from nginx proxy
# Trust X-Forwarded-* headers from Caddy proxy
USE_X_FORWARDED_HOST = True
-3765
View File
File diff suppressed because it is too large Load Diff
+53 -45
View File
@@ -15,6 +15,7 @@ classifiers = [
"Natural Language :: English",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
description = "A Django MDA"
keywords = ["Django", "Contacts", "Templates", "RBAC"]
@@ -22,50 +23,50 @@ license = "MIT"
readme = "README.md"
requires-python = ">=3.13,<4.0"
# Note: after changing this list you must re-run `make back-poetry-lock`
# Note: after changing this list you must re-run `make deps-lock-back`
dependencies = [
"boto3==1.40.43",
"botocore==1.40.43",
"celery[redis]==5.5.2",
"boto3==1.42.53",
"botocore==1.42.53",
"celery[redis]==5.6.2",
"cryptography==46.0.5",
"dj-database-url==2.3.0",
"django==5.1.15",
"django-celery-beat==2.8.0",
"dj-database-url==3.1.2",
"django==5.2.11",
"django-celery-beat==2.8.1",
"django-celery-results==2.6.0",
"django-configurations==2.5.1",
"django-cors-headers==4.6.0",
"django-countries==7.6.1",
"django-fernet-encrypted-fields==0.3.0",
"django-filter==24.3",
"django-cors-headers==4.9.0",
"django-countries==8.2.0",
"django-fernet-encrypted-fields==0.3.1",
"django-filter==25.2",
"django-lasuite[all]==0.0.24",
"django-prometheus==2.4.1",
"django-redis==5.4.0",
"django-redis==6.0.0",
"django-storages==1.14.6",
"django-timezone-field==7.1",
"djangorestframework==3.15.2",
"django-timezone-field==7.2.1",
"djangorestframework==3.16.1",
"dkimpy==1.1.8",
"dnspython==2.7.0",
"drf_spectacular==0.28.0",
"dnspython==2.8.0",
"drf_spectacular==0.29.0",
"opensearch-py==2.8.0",
"factory_boy==3.3.1",
"factory_boy==3.3.3",
"flanker@git+https://github.com/sylvinus/flanker@f94ba2c15ab310e333610a78828d09be4e11a6b6",
"gunicorn==23.0.0",
"jsonschema==4.25.0",
"nested-multipart-parser==1.5.0",
"openai==1.93.0",
"psycopg[binary]==3.2.4",
"PyJWT==2.10.1",
"gunicorn==25.1.0",
"jsonschema==4.26.0",
"nested-multipart-parser==1.6.0",
"openai==2.21.0",
"psycopg[binary]==3.3.3",
"PyJWT==2.11.0",
"PySocks==1.7.1",
"python-keycloak==5.5.1",
"python-magic==0.4.27",
"pyzstd==0.17.0",
"redis==5.2.1",
"pyzstd==0.19.1",
"redis==6.4.0",
"requests==2.32.5",
"sentry-sdk[django]==2.27.0",
"sentry-sdk[django]==2.53.0",
"libpff-python==20231205",
"url-normalize==1.4.3",
"whitenoise==6.8.2",
"prometheus-client==0.22.1",
"url-normalize==2.2.1",
"whitenoise==6.11.0",
"prometheus-client==0.24.1",
]
[project.urls]
@@ -76,26 +77,32 @@ dependencies = [
[project.optional-dependencies]
dev = [
"django-extensions==3.2.3",
"drf-spectacular-sidecar==2024.12.1",
"django-extensions==4.1",
"drf-spectacular-sidecar==2026.1.1",
"flower==2.0.1",
"hypothesis==6.131.1",
"pip-audit==2.9.0",
"pipdeptree==2.28.0",
"pylint-django==2.6.1",
"pylint==3.3.4",
"pytest-cov==6.0.0",
"pytest-django==4.9.0",
"pytest==8.3.4",
"hypothesis==6.151.9",
"pip-audit==2.10.0",
"pipdeptree==2.31.0",
"pylint-django==2.7.0",
"pylint==4.0.4",
"pytest-cov==7.0.0",
"pytest-django==4.12.0",
"pytest==9.0.2",
"pytest-icdiff==0.9",
"pytest-repeat==0.9.4",
"pytest-xdist==3.6.1",
"responses==0.25.6",
"ruff==0.9.3"
"pytest-xdist==3.8.0",
"responses==0.26.0",
"ruff==0.15.2"
]
[tool.poetry]
package-mode = false
[build-system]
requires = ["uv_build>=0.10.0,<0.11.0"]
build-backend = "uv_build"
[tool.uv.build-backend]
module-root = ""
source-include = ["core/**"]
source-exclude = ["core/tests/**"]
[tool.ruff]
exclude = [
@@ -109,7 +116,7 @@ exclude = [
line-length = 88
[tool.ruff.lint]
ignore = ["DJ001", "PLR2004", "PLR0915", "PLR0912", "PLR0913", "PLR0911", "BLE001"]
ignore = ["DJ001", "PLC0415", "PLR2004", "PLR0915", "PLR0912", "PLR0913", "PLR0911", "BLE001"]
select = [
"B", # flake8-bugbear
"BLE", # flake8-blind-except
@@ -131,6 +138,7 @@ select = [
[tool.ruff.lint.isort]
section-order = ["future","standard-library","django","third-party","messages","first-party","local-folder"]
sections = { messages=["core"], django=["django"] }
known-third-party = ["dns"]
extra-standard-library = ["tomllib"]
[tool.ruff.lint.per-file-ignores]
+2487
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -12,13 +12,13 @@ End-to-end tests for the Messages application using Playwright.
### In headless mode (CI)
```bash
make e2e-test
make test-e2e
```
### In UI mode
```bash
make e2e-test-ui
make test-e2e-ui
```
Open the Playwright UI on http://localhost:8932 to write and debug the tests interactively.
@@ -28,7 +28,7 @@ Open the Playwright UI on http://localhost:8932 to write and debug the tests int
Start playwright in UI Mode and use the dev frontend service to avoid rebuilding
the frontend after each change.
```bash
make e2e-test-dev
make test-e2e-dev
```
Open the Playwright UI on http://localhost:8932 to write and debug the tests interactively.
@@ -39,9 +39,9 @@ Open the Playwright UI on http://localhost:8932 to write and debug the tests int
E2E tests use [dedicated services](./compose.yaml) especially for the database and the object storage.
### Nginx to serve the frontend and the backend
### Caddy to serve the frontend and the backend
Nginx is used to serve the frontend and the backend on the same origin, avoiding cross-origin cookie issues.
Caddy is used as a reverse proxy to serve the frontend and the backend on the same origin, avoiding cross-origin cookie issues.
### Environment variables
+7
View File
@@ -0,0 +1,7 @@
:80 {
reverse_proxy /api/* backend:8000
reverse_proxy /oidc/* backend:8000
reverse_proxy /admin/* backend:8000
reverse_proxy /__heartbeat__/* backend:8000
reverse_proxy {$FRONTEND_SERVICE_NAME}:3000
}
+8
View File
@@ -0,0 +1,8 @@
#!/bin/sh
set -e
if [ "$E2E_PROFILE" = "dev" ]; then
export FRONTEND_SERVICE_NAME="frontend-dev"
else
export FRONTEND_SERVICE_NAME="frontend"
fi
exec caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
+8 -13
View File
@@ -38,11 +38,6 @@ services:
volumes: !reset []
ports: !reset []
objectstorage-createbucket:
extends:
file: ../../compose.yaml
service: objectstorage-createbucket
keycloak:
extends:
file: ../../compose.yaml
@@ -98,13 +93,13 @@ services:
depends_on: !override
- backend
nginx:
image: nginx:1.27-alpine
proxy:
image: caddy:2-alpine
environment:
- E2E_PROFILE=${E2E_PROFILE:-e2e}
volumes:
- ./nginx/e2e.conf.template:/etc/nginx/templates/e2e.conf.template:ro
- ./nginx/entrypoint.sh:/entrypoint.sh:ro
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy/entrypoint.sh:/entrypoint.sh:ro
entrypoint: "/entrypoint.sh"
depends_on:
frontend:
@@ -122,7 +117,7 @@ services:
build:
target: frontend-build
args:
API_ORIGIN: http://nginx
API_ORIGIN: http://proxy
command: ["npx", "serve", "-s", "out"]
ports: !reset []
@@ -155,8 +150,8 @@ services:
DOCKER_USER: ${DOCKER_USER:-1000}
user: ${DOCKER_USER:-1000}
environment:
- FRONTEND_BASE_URL=http://nginx
- BACKEND_BASE_URL=http://nginx
- FRONTEND_BASE_URL=http://proxy
- BACKEND_BASE_URL=http://proxy
- KEYCLOAK_BASE_URL=http://keycloak:8802
- DOCKER_HOST=tcp://docker-sock-proxy:2375
- CI=${CI:-false}
@@ -168,7 +163,7 @@ services:
depends_on:
docker-sock-proxy:
condition: service_started
nginx:
proxy:
condition: service_started
mta-in:
condition: service_started
-57
View File
@@ -1,57 +0,0 @@
server {
listen 80;
server_name localhost;
# Frontend - serve on root
location / {
proxy_pass http://${FRONTEND_SERVICE_NAME}:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Backend API - serve on /api prefix
location /api/ {
proxy_pass http://backend:8000/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
}
# Backend OIDC authentication
location /oidc/ {
proxy_pass http://backend:8000/oidc/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
}
# Backend admin (if needed)
location /admin/ {
proxy_pass http://backend:8000/admin/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Backend healthcheck
location /__heartbeat__/ {
proxy_pass http://backend:8000/__heartbeat__/;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
}
-23
View File
@@ -1,23 +0,0 @@
#!/bin/sh
# Generate the nginx config from the template according to the E2E_PROFILE environment variable
# If the E2E_PROFILE is dev, use the frontend-dev service, otherwise use the frontend service
set -e
# Set FRONTEND_SUFFIX based on E2E_PROFILE
if [ "$E2E_PROFILE" = "dev" ]; then
export FRONTEND_SERVICE_NAME="frontend-dev"
else
export FRONTEND_SERVICE_NAME="frontend"
fi
# Generate nginx config from template
envsubst '${FRONTEND_SERVICE_NAME}' < /etc/nginx/templates/e2e.conf.template > /etc/nginx/conf.d/default.conf
echo "Generated nginx config for E2E_PROFILE=${E2E_PROFILE:-e2e} (FRONTEND_SERVICE_NAME=${FRONTEND_SERVICE_NAME})"
# Start nginx
exec nginx -g 'daemon off;'
+8 -7
View File
@@ -24,19 +24,20 @@ ENV NEXT_PUBLIC_API_ORIGIN=${API_ORIGIN}
RUN npm run build
FROM docker.io/nginxinc/nginx-unprivileged:1-alpine AS runtime-prod
FROM alpine:3.21 AS caddy-download
ARG TARGETARCH
ARG CADDY_VERSION=2.9.1
RUN wget -qO- "https://github.com/caddyserver/caddy/releases/download/v${CADDY_VERSION}/caddy_${CADDY_VERSION}_linux_${TARGETARCH}.tar.gz" | tar -xz caddy
USER root
RUN apk --no-cache upgrade
FROM gcr.io/distroless/static-debian13:nonroot AS runtime-prod
USER nginx
COPY --from=caddy-download /caddy /usr/bin/caddy
COPY --from=frontend-build /home/frontend/out /app
COPY nginx/nginx.conf.template /etc/nginx/templates/default.conf.template
COPY caddy/Caddyfile /etc/caddy/Caddyfile
ENV PORT=8080
ENV MESSAGES_FRONTEND_ROOT=/app
ENV MESSAGES_FRONTEND_BACKEND_SERVER=localhost:8000
ENV DJANGO_ADMIN_URL=admin
HEALTHCHECK --interval=60s --timeout=3s --start-interval=10s \
CMD curl -fsS http://localhost:${PORT}/__lbheartbeat__
ENTRYPOINT ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
+69
View File
@@ -0,0 +1,69 @@
{
auto_https off
admin off
}
:{$PORT} {
root * {$MESSAGES_FRONTEND_ROOT:/app}
header -Server
route {
# Health checks
respond /__lbheartbeat__ 200
respond /__lbheartbeat__/ 200
reverse_proxy /__heartbeat__/* {$MESSAGES_FRONTEND_BACKEND_SERVER:localhost:8000} {
header_up X-Forwarded-Proto https
header_up X-Forwarded-For {remote_host}
}
# Django backend proxy
reverse_proxy /api/* {$MESSAGES_FRONTEND_BACKEND_SERVER:localhost:8000} {
header_up X-Forwarded-Proto https
header_up X-Forwarded-For {remote_host}
}
redir /{$DJANGO_ADMIN_URL:admin} /{$DJANGO_ADMIN_URL:admin}/ 301
reverse_proxy /{$DJANGO_ADMIN_URL:admin}/* {$MESSAGES_FRONTEND_BACKEND_SERVER:localhost:8000} {
header_up X-Forwarded-Proto https
header_up X-Forwarded-For {remote_host}
}
reverse_proxy /static/* {$MESSAGES_FRONTEND_BACKEND_SERVER:localhost:8000} {
header_up X-Forwarded-Proto https
header_up X-Forwarded-For {remote_host}
}
# Next.js static export routes
@mailbox path_regexp ^/mailbox/[^/]+$
rewrite @mailbox /mailbox/[mailboxId].html
@mailbox_thread path_regexp ^/mailbox/[^/]+/thread/[^/]+$
rewrite @mailbox_thread /mailbox/[mailboxId]/thread/[threadId].html
@mailbox_new path_regexp ^/mailbox/[^/]+/new$
rewrite @mailbox_new /mailbox/[mailboxId]/new.html
@domain_root path /domain
rewrite @domain_root /domain.html
@domain_id path_regexp ^/domain/[^/]+$
rewrite @domain_id /domain/[maildomainId].html
@domain_dns path_regexp ^/domain/[^/]+/dns$
rewrite @domain_dns /domain/[maildomainId]/dns.html
@domain_signatures path_regexp ^/domain/[^/]+/signatures$
rewrite @domain_signatures /domain/[maildomainId]/signatures.html
# Default: SPA fallback
try_files {path} /index.html
file_server
}
handle_errors {
rewrite * /404.html
file_server
}
}
-90
View File
@@ -1,90 +0,0 @@
upstream backend_server {
server ${MESSAGES_FRONTEND_BACKEND_SERVER} fail_timeout=0;
}
server {
listen ${PORT};
server_name _;
server_tokens off;
root ${MESSAGES_FRONTEND_ROOT};
error_page 404 /404.html;
# Django rest framework
location ^~ /api/ {
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_redirect off;
proxy_pass http://backend_server;
}
# Django admin
location /${DJANGO_ADMIN_URL} {
return 301 https://$host/${DJANGO_ADMIN_URL}/;
}
location ^~ /${DJANGO_ADMIN_URL}/ {
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_redirect off;
proxy_pass http://backend_server;
}
# Django statics
location ^~ /static/ {
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_redirect off;
proxy_pass http://backend_server;
}
# Next static export
location ~* ^/mailbox/[^/]+$ {
try_files /mailbox/[mailboxId].html =404;
}
location ~* ^/mailbox/[^/]+/thread/[^/]+$ {
try_files /mailbox/[mailboxId]/thread/[threadId].html =404;
}
location ~* ^/mailbox/[^/]+/new$ {
try_files /mailbox/[mailboxId]/new.html =404;
}
location ~* ^/domain$ {
try_files /domain.html =404;
}
location ~* ^/domain/[^/]+$ {
try_files /domain/[maildomainId].html =404;
}
location ~* ^/domain/[^/]+/dns$ {
try_files /domain/[maildomainId]/dns.html =404;
}
location ~* ^/domain/[^/]+/signatures$ {
try_files /domain/[maildomainId]/signatures.html =404;
}
location ~ ^/__lbheartbeat__/? {
access_log off;
error_log off;
return 200 'ok';
}
location ~ ^/__heartbeat__/? {
access_log off;
error_log off;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_redirect off;
proxy_pass http://backend_server;
}
location = /404.html {
internal;
}
# Frontend export
location / {
try_files $uri index.html $uri/ =404;
}
}
@@ -9,8 +9,6 @@
"{{count}} hours ago_other": "{{count}} hours ago",
"{{count}} messages_one": "{{count}} message",
"{{count}} messages_other": "{{count}} messages",
"{{count}} messages imported_one": "{{count}} message imported",
"{{count}} messages imported_other": "{{count}} messages imported",
"{{count}} messages have been archived._one": "The message has been archived.",
"{{count}} messages have been archived._other": "{{count}} messages have been archived.",
"{{count}} messages have been deleted._one": "The message has been deleted.",
@@ -19,6 +17,8 @@
"{{count}} messages have been reported as spam._other": "{{count}} messages have been reported as spam.",
"{{count}} messages have been updated._one": "The message has been updated.",
"{{count}} messages have been updated._other": "{{count}} messages have been updated.",
"{{count}} messages imported_one": "{{count}} message imported",
"{{count}} messages imported_other": "{{count}} messages imported",
"{{count}} messages of this thread have been deleted._one": "{{count}} message of this thread has been deleted.",
"{{count}} messages of this thread have been deleted._other": "{{count}} messages of this thread have been deleted.",
"{{count}} minutes ago_one": "{{count}} minute ago",
@@ -71,7 +71,6 @@
"An address with this prefix already exists in this domain.": "An address with this prefix already exists in this domain.",
"An archive is uploading": "An archive is uploading",
"An error occurred while creating the address.": "An error occurred while creating the address.",
"This email prefix is not allowed for personal mailboxes. Please choose a different prefix.": "This email prefix is not allowed for personal mailboxes. Please choose a different prefix.",
"An error occurred while creating the domain.": "An error occurred while creating the domain.",
"An error occurred while importing messages.": "An error occurred while importing messages.",
"An error occurred while loading maildomains.": "An error occurred while loading maildomains.",
@@ -478,6 +477,7 @@
"This action cannot be undone and the user will need the new password to access its mailbox.": "This action cannot be undone and the user will need the new password to access its mailbox.",
"This contact's identity could not be verified. Proceed with caution.": "This contact's identity could not be verified. Proceed with caution.",
"This description will be used by the AI to automatically assign this label to your messages.": "This description will be used by the AI to automatically assign this label to your messages.",
"This email prefix is not allowed for personal mailboxes. Please choose a different prefix.": "This email prefix is not allowed for personal mailboxes. Please choose a different prefix.",
"This event has been cancelled": "This event has been cancelled",
"This is the only admin of this mailbox, you cannot therefore modify its access.": "This is the only admin of this mailbox, you cannot therefore modify its access.",
"This message has {{count}} attachments_one": "This message has one attachment",
@@ -14,9 +14,6 @@
"{{count}} messages_one": "{{count}} message",
"{{count}} messages_many": "{{count}} messages",
"{{count}} messages_other": "{{count}} messages",
"{{count}} messages imported_one": "{{count}} message importé",
"{{count}} messages imported_many": "{{count}} messages importés",
"{{count}} messages imported_other": "{{count}} messages importés",
"{{count}} messages have been archived._one": "Le message a été archivé.",
"{{count}} messages have been archived._many": "{{count}} messages ont été archivés.",
"{{count}} messages have been archived._other": "{{count}} messages ont été archivés.",
@@ -29,6 +26,9 @@
"{{count}} messages have been updated._one": "Le message a été mis à jour.",
"{{count}} messages have been updated._many": "{{count}} messages ont été mis à jour.",
"{{count}} messages have been updated._other": "{{count}} messages ont été mis à jour.",
"{{count}} messages imported_one": "{{count}} message importé",
"{{count}} messages imported_many": "{{count}} messages importés",
"{{count}} messages imported_other": "{{count}} messages importés",
"{{count}} messages of this thread have been deleted._one": "{{count}} message de cette conversation a été supprimé.",
"{{count}} messages of this thread have been deleted._many": "{{count}} messages de cette conversation ont été supprimés.",
"{{count}} messages of this thread have been deleted._other": "{{count}} messages de cette conversation ont été supprimés.",
@@ -95,7 +95,6 @@
"An address with this prefix already exists in this domain.": "Une adresse avec ce préfixe existe déjà dans ce domaine.",
"An archive is uploading": "Une archive est en cours de téléversement",
"An error occurred while creating the address.": "Une erreur est survenue lors de la création de l'adresse.",
"This email prefix is not allowed for personal mailboxes. Please choose a different prefix.": "Ce préfixe d'adresse n'est pas autorisé pour les boîtes aux lettres personnelles. Veuillez choisir un autre préfixe.",
"An error occurred while creating the domain.": "Une erreur est survenue lors de la création du domaine.",
"An error occurred while importing messages.": "Une erreur s'est produite lors de l'importation des messages.",
"An error occurred while loading maildomains.": "Une erreur s'est produite lors du chargement des domaines.",
@@ -517,6 +516,7 @@
"This action cannot be undone and the user will need the new password to access its mailbox.": "Cette action est irréversible et l'utilisateur aura besoin du nouveau mot de passe pour accéder à sa boîte aux lettres.",
"This contact's identity could not be verified. Proceed with caution.": "L'identité de ce contact n'a pas pu être vérifiée. Faites attention.",
"This description will be used by the AI to automatically assign this label to your messages.": "Cette description sera utilisée par l'IA pour assigner automatiquement cette étiquette à vos messages.",
"This email prefix is not allowed for personal mailboxes. Please choose a different prefix.": "Ce préfixe d'adresse n'est pas autorisé pour les boîtes aux lettres personnelles. Veuillez choisir un autre préfixe.",
"This event has been cancelled": "Cet événement a été annulé",
"This is the only admin of this mailbox, you cannot therefore modify its access.": "C'est le seul administrateur de cette boîte aux lettres, vous ne pouvez donc pas modifier son accès.",
"This message has {{count}} attachments_one": "Ce message a une pièce jointe",
+165 -8
View File
@@ -1,14 +1,14 @@
{
"{{count}} attachments_one": "{{count}} bijlage",
"{{count}} attachments_other": "{{count}} bijlagen",
"{{count}} attendees_one": "",
"{{count}} attendees_other": "",
"{{count}} days ago_one": "{{count}} dag geleden",
"{{count}} days ago_other": "{{count}} dagen geleden",
"{{count}} hours ago_one": "{{count}} uur geleden",
"{{count}} hours ago_other": "{{count}} uur geleden",
"{{count}} messages_one": "{{count}} bericht",
"{{count}} messages_other": "{{count}} berichten",
"{{count}} messages imported_one": "{{count}} bericht geïmporteerd",
"{{count}} messages imported_other": "{{count}} berichten geïmporteerd",
"{{count}} messages have been archived._one": "Het bericht is gearchiveerd.",
"{{count}} messages have been archived._other": "{{count}} berichten zijn gearchiveerd.",
"{{count}} messages have been deleted._one": "Het bericht is verwijderd.",
@@ -17,12 +17,16 @@
"{{count}} messages have been reported as spam._other": "{{count}} berichten zijn gerapporteerd als spam.",
"{{count}} messages have been updated._one": "Het bericht is bijgewerkt.",
"{{count}} messages have been updated._other": "{{count}} berichten zijn bijgewerkt.",
"{{count}} messages imported_one": "{{count}} bericht geïmporteerd",
"{{count}} messages imported_other": "{{count}} berichten geïmporteerd",
"{{count}} messages of this thread have been deleted._one": "{{count}} bericht van dit kanaal is verwijderd.",
"{{count}} messages of this thread have been deleted._other": "{{count}} berichten van dit kanaal is verwijderd.",
"{{count}} minutes ago_one": "{{count}} minuut geleden",
"{{count}} minutes ago_other": "{{count}} minuten geleden",
"{{count}} months ago_one": "{{count}} maand geleden",
"{{count}} months ago_other": "{{count}} maanden geleden",
"{{count}} occurrences_one": "",
"{{count}} occurrences_other": "",
"{{count}} results_one": "{{count}} resultaat",
"{{count}} results_other": "{{count}} resultaten",
"{{count}} threads have been archived._one": "De thread is gearchiveerd.",
@@ -45,38 +49,46 @@
"{{count}} years ago_other": "{{count}} jaren geleden",
"{{date}} at {{time}}": "{{date}} om {{time}}",
"{{progress}}% imported": "{{progress}}% geïmporteerd",
"2 columns": "",
"Abort upload": "Upload Afbreken",
"Accepted": "",
"Accesses": "Toegang",
"Actions": "Acties",
"Active": "Actief",
"Add a contact form widget to your website to receive messages directly in your mailbox.": "",
"Add a domain": "Domein toevoegen",
"Add a sub-label": "Een sublabel toevoegen",
"Add attachment from {{driveAppName}}": "Bijlage toevoegen van {{driveAppName}}",
"Add attachments": "Bijlage toevoegen",
"Add label": "Label toevoegen",
"Add labels": "Labels toevoegen",
"Add tags": "",
"Add this code snippet to your website to display the feedback widget.": "",
"Address": "Adres",
"Addresses": "Adressen",
"After creating the widget, you will receive the installation code to add to your website.": "",
"All messages": "Alle berichten",
"An address with this prefix already exists in this domain.": "Een adres met dit voorvoegsel bestaat al in dit domein.",
"An archive is uploading": "Een archief wordt geüpload",
"An error occurred while creating the address.": "Fout opgetreden tijdens het aanmaken van uw adres.",
"This email prefix is not allowed for personal mailboxes. Please choose a different prefix.": "Dit e-mailvoorvoegsel is niet toegestaan voor persoonlijke mailboxen. Kies een ander voorvoegsel.",
"An error occurred while creating the domain.": "Fout opgetreden tijdens het aanmaken van het domein.",
"An error occurred while importing messages.": "Er is een fout opgetreden tijdens het importeren van berichten.",
"An error occurred while loading maildomains.": "Er is een fout opgetreden tijdens het laden van e-maildomeinen.",
"An error occurred while resetting the password.": "Er is een fout opgetreden tijdens het resetten van het wachtwoord.",
"An error occurred while saving the integration.": "",
"An error occurred while updating the address.": "Er is een fout opgetreden tijdens het bijwerken van het adres.",
"An error occurred while uploading the archive file.": "Er is een fout opgetreden tijdens het uploaden van het archiefbestand.",
"An unexpected error occurred.": "Er deed zich een onverwachte fout voor.",
"and {{count}} other users_one": "en 1 andere gebruiker",
"and {{count}} other users_other": "en {{count}} andere gebruikers",
"API Key": "",
"Archive": "Archief",
"Archive {{count}} threads_one": "Archief {{count}} thread",
"Archive {{count}} threads_other": "Archief {{count}} threads",
"Archives": "Archieven",
"Are you sure you want to close this dialog? Your upload will be aborted!": "Weet u zeker dat u deze dialoog wilt sluiten? Uw upload wordt afgebroken!",
"Are you sure you want to delete this draft? This action cannot be undone.": "Weet u zeker dat u dit concept wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
"Are you sure you want to delete this integration? This action is irreversible!": "",
"Are you sure you want to delete this label? This action is irreversible!": "Weet je zeker dat je dit label wilt verwijderen? Deze actie is onomkeerbaar!",
"Are you sure you want to delete this mailbox? This action is irreversible!": "Weet u zeker dat u deze mailbox wilt verwijderen? Deze actie is onomkeerbaar!",
"Are you sure you want to delete this signature? This action is irreversible!": "Weet je zeker dat je deze handtekening wilt verwijderen? Deze actie is onomkeerbaar!",
@@ -91,14 +103,21 @@
"Authentication failed. Please check your credentials and ensure you have enabled IMAP connections in your account.": "Verificatie is mislukt. Controleer uw inloggegevens en zorg ervoor dat IMAP-verbindingen in uw account zijn ingeschakeld.",
"Auto-labeling": "Auto-labeling",
"Automatically create mailboxes according to OIDC emails": "Automatisch mailboxen aanmaken volgens OIDC e-mails",
"Awaiting response": "",
"Back": "Terug",
"Back to your inbox": "Terug naar uw inbox",
"BCC: ": "BCC: ",
"Blind copy: ": "Blinde kopie: ",
"Calendar invite": "",
"Cancel": "Annuleren",
"Cancel those sendings": "",
"Cannot add attachment(s). Total size would be more than {{maxSize}}.": "Kan bijlagen niet toevoegen. Totale grootte zou groter zijn dan {{maxSize}}.",
"Cannot add image. File size exceeds the {{maxSize}} limit.": "",
"CC: ": "",
"Check DNS again": "Controleer DNS opnieuw",
"Checking DNS records...": "DNS-records controleren...",
"Choose the type of integration you want to create": "",
"Clear selected items": "",
"Click to add accesses": "Klik om toegang toe te voegen",
"Close": "Sluit",
"Close filters": "Sluit filter",
@@ -108,9 +127,12 @@
"Collapse": "Inklappen",
"Collapse all": "Alles inklappen",
"Color: ": "Kleur: ",
"Coming soon": "",
"Conflicting": "Conflicterend",
"Contains the words": "Bevat de woorden",
"Content is required": "Inhoud is verplicht",
"Copied": "Gekopieerd",
"Copied to clipboard": "",
"Copy": "Kopieer",
"Copy all DNS records": "Kopieer alle DNS records",
"Copy to clipboard": "Kopieer naar klembord",
@@ -119,6 +141,7 @@
"Create": "Creëren",
"Create a Label": "Label aanmaken",
"Create a new address @{{domain}}": "Maak een nieuw adres @{{domain}}",
"Create a new integration": "",
"Create a new label": "Nieuw label aanmaken",
"Create a new personal mailbox": "Maak een nieuwe persoonlijke mailbox",
"Create a new shared mailbox": "Maak een nieuwe persoonlijke mailbox",
@@ -126,37 +149,47 @@
"Create a new signature for {{domain}}": "Maak een nieuwe handtekening voor {{domain}}",
"Create a new template": "Maak een nieuwe sjabloon",
"Create a simple redirect (Coming soon)": "Creëer een eenvoudige doorverwijzing (binnenkort beschikbaar)",
"Create a Widget": "",
"Create integration": "",
"Create the label \"{{label}}\"": "Maak label \"{{label}}\"",
"create_mailbox_modal.success.credential_text": "Uw Messages credentials zijn:\n- E-mail: {{id}}\n- Wachtwoord: {{password}}\n\nHet zal worden gevraagd om uw wachtwoord te wijzigen bij uw eerste aanmelding.",
"Created at": "Gemaakt op",
"Creating...": "Maken...",
"Credentials copied!": "Gegevens gekopieerd!",
"Current status": "Huidige status",
"Daily": "",
"Date:": "Datum:",
"Date: ": "",
"Declined": "",
"Default": "Standaard",
"Default signature": "Standaard handtekening",
"Delegated": "",
"Delete": "Verwijderen",
"Delete {{count}} threads_one": "Verwijder {{count}} Thread",
"Delete {{count}} threads_other": "Verwijder {{count}} threads",
"Delete draft": "Concept verwijderen",
"Delete integration \"{{name}}\"": "",
"Delete label \"{{label}}\"": "Verwijder label \"{{label}}\"",
"Delete mailbox {{mailbox}}": "Verwijder mailbox {{mailbox}}",
"Delete signature \"{{signature}}\"": "Verwijder handtekening \"{{signature}}\"",
"Delete template \"{{template}}\"": "Sjabloon \"{{template}} \" verwijderen",
"Delivering": "",
"Delivery cancelled": "",
"Delivery failed": "",
"Description": "Beschrijving",
"Description must be less than 255 characters.": "Beschrijving moet minder dan 255 tekens bevatten.",
"Deselect all threads": "Deselecteer alle threads",
"Deselect thread": "Deselecteer Thread",
"Did you forget an attachment?": "Ben je de bijlage vergeten?",
"Disable thread selection": "",
"Display those images": "",
"DNS": "DNS",
"Do you have any feedback?": "Heeft u feedback?",
"Domain": "Domein",
"Domain admin": "Domein beheerder",
"Domain not found": "Domein niet gevonden",
"Duplicate": "Dubbel",
"Insecure": "Onveilig",
"Conflicting": "Conflicterend",
"Download": "Download",
"Download invitation": "",
"Download raw email": "Download raw email",
"Draft": "Concept",
"Draft deleted": "Concept verwijderd",
@@ -165,26 +198,47 @@
"Drafts": "Concepten",
"Drag and drop an archive here": "Sleep een archief hierheen",
"Drop your attachments here": "Plaats uw bijlagen hier",
"Duplicate": "Dubbel",
"Edit": "Bewerken",
"Edit {{mailbox}} address": "Bewerk {{mailbox}} adres",
"Edit signature \"{{signature}}\"": "",
"Edit template \"{{template}}\"": "Sjabloon \"{{template}} \" verwijderen",
"Edit Widget": "",
"Email address": "E-mail adres",
"EML, MBOX or PST": "EML, MBOX of PST",
"Enter the email addresses of the recipients separated by commas": "Voer de e-mailadressen in van de geadresseerden gescheiden door komma's",
"Error while checking DNS records": "Fout bij het controleren van DNS records",
"Error while loading addresses": "Fout bij het laden van adressen",
"Error while loading integrations": "",
"Error while loading signatures": "Fout bij het laden van handtekeningen",
"Error while loading templates": "Fout bij het laden van sjablonen",
"Every {{count}} days_one": "",
"Every {{count}} days_other": "",
"Every {{count}} months_one": "",
"Every {{count}} months_other": "",
"Every {{count}} weeks_one": "",
"Every {{count}} weeks_other": "",
"Every {{count}} years_one": "",
"Every {{count}} years_other": "",
"Expand": "Uitklappen",
"Expand all": "Alles uitklappen",
"Failed to delete integration.": "",
"Failed to delete signature.": "",
"Failed to delete template.": "Verwijderen van sjabloon mislukt.",
"Failed to load calendar invite": "",
"Failed to load signature. Please try again.": "",
"Failed to load template. Please try again.": "",
"Failed to refresh summary.": "Vernieuwen samenvatting mislukt.",
"Failed to save signature. Please try again.": "",
"Failed to save template. Please try again.": "Opslaan sjabloon mislukt. Probeer het opnieuw.",
"Failed to update signature.": "",
"Feedback?": "Feedback?",
"First name": "Voornaam",
"First name is required.": "Voornaam is vereist.",
"First, we need some information about your old mailbox": "Eerst hebben we wat informatie nodig over je oude mailbox",
"Fold message": "Vouw bericht",
"folder.search": "",
"For your security and privacy, external images are not displayed.": "",
"Forced": "Geforceerd",
"Forced signature": "Geforceerde handtekening",
"Forward": "Doorsturen",
@@ -194,10 +248,14 @@
"From: ": "Van: ",
"Full name": "Volledige naam",
"Full name is required.": "Volledige naam is vereist.",
"General": "",
"Generate an API key to send messages programmatically from your applications.": "",
"Generating summary...": "Samenvatting genereren...",
"How to allow IMAP connections from your account {{name}}?": "Hoe sta ik IMAP verbindingen toe van uw account {{name}}?",
"I confirm that this address corresponds to the real identity of a colleague, and I commit to deactivating it when their position ends.": "Ik bevestig dat dit adres overeenkomt met de werkelijke identiteit van een collega, en ik beloof het te deactiveren zodra hun deelname is beëindigd.",
"Identity": "Identiteit",
"Image + Text": "",
"Image size limit exceeded": "",
"IMAP port": "IMAP-poort",
"IMAP server": "IMAP-server",
"IMAP server is required.": "IMAP-server is vereist.",
@@ -214,8 +272,18 @@
"Inbox": "Inbox",
"Incorrect": "Onjuist",
"Indicate your old email address and your password.": "Geef uw oude e-mailadres en wachtwoord op.",
"Insecure": "Onveilig",
"Insert 2 columns": "",
"Insert image": "",
"Insert template": "Sjabloon invoegen",
"Installation": "",
"Integration created!": "",
"Integration deleted!": "",
"Integration updated!": "",
"Integrations": "",
"just now": "zojuist",
"Label \"{{label}}\" assigned to {{count}} threads._one": "",
"Label \"{{label}}\" assigned to {{count}} threads._other": "",
"Label \"{{label}}\" removed from this conversation.": "Label \"{{label}}\" verwijderd uit dit gesprek.",
"Label name": "Label naam",
"Labels": "Labels",
@@ -223,49 +291,72 @@
"Last name is required.": "Achternaam is vereist.",
"Last saved {{relativeTime}}": "Laatst opgeslagen {{relativeTime}}",
"Last update: {{timestamp}}": "Laatst bijgewerkt: {{timestamp}}",
"Layout": "",
"less than a minute ago": "minder dan 1 minuut geleden",
"Loading addresses...": "Adressen laden...",
"Loading calendar invite...": "",
"Loading integrations...": "",
"Loading labels...": "Labels laden...",
"Loading next threads...": "Volgende kanaal laden...",
"Loading signature...": "",
"Loading signatures...": "Ondertekeningen laden...",
"Loading tags...": "",
"Loading template...": "",
"Loading templates...": "Sjablonen laden...",
"Loading variables...": "Variabelen laden...",
"Loading…": "",
"Logout": "Uitloggen",
"Mailbox {{mailbox}} has been deleted successfully.": "Mailbox {{mailbox}} is succesvol verwijderd.",
"Mailbox is required.": "Mailbox is vereist.",
"Maildomains management": "Maildomeinen beheer",
"Manage {{entity}} accesses": "{{entity}} toegang beheren",
"Manage accesses": "Beheer toegang",
"Mark {{count}} threads as read_one": "",
"Mark {{count}} threads as read_other": "",
"Mark {{count}} threads as unread_one": "",
"Mark {{count}} threads as unread_other": "",
"Mark all as read": "Alles markeren als gelezen",
"Mark all as unread": "Alles markeren als ongelezen",
"Mark as important": "Markeer als belangrijk",
"Mark as read": "",
"Mark as read from here": "",
"Mark as unread": "Markeer als gelezen",
"Mark as unread from here": "Markeer als ongelezen vanaf hier",
"Message content": "",
"Message from {referer_domain}": "",
"Message sent successfully": "Bericht succesvol verzonden",
"Message templates for {{mailbox}}": "",
"Messaging": "Berichten",
"Missing": "Ontbreekt",
"Modify": "Wijzig",
"Monthly": "",
"More": "Meer",
"More options": "Meer opties",
"My message templates": "",
"My signatures": "",
"Name": "Naam",
"Name is required": "Naam is verplicht",
"Name is required.": "Naam is verplicht.",
"Name must be a valid domain name.": "Naam moet een geldige domeinnaam zijn.",
"New address": "Nieuw adres",
"New domain": "Nieuw domein",
"New integration": "",
"New message": "Nieuw bericht",
"New signature": "Nieuwe handtekening",
"New template": "Nieuw sjabloon",
"No accesses": "Geen toegangen",
"No addresses found": "",
"No attachments": "Geen bijlagen",
"No DNS records found": "Geen DNS-records gevonden",
"No event found in calendar invite": "",
"No integration found": "",
"No mailbox.": "Geen mailbox.",
"No results.": "",
"No signature": "Geen handtekening",
"No signatures found": "Geen handtekeningen gevonden",
"No subject": "Geen onderwerp",
"No summary available.": "Geen samenvatting beschikbaar.",
"No template found": "Geen sjabloon gevonden",
"No templates available": "Geen sjablonen beschikbaar",
"No threads.": "Geen kanalen.",
"Open {{driveAppName}} preview": "Open {{driveAppName}} voorbeeld",
"Open filters": "Open filters",
@@ -273,6 +364,7 @@
"Or": "Of",
"or drag and drop some files": "of sleep enkele bestanden",
"Other services...": "Andere diensten...",
"Outbox": "",
"Password": "Wachtwoord",
"Password is required.": "Wachtwoord is vereist.",
"Password reset successfully!": "Wachtwoord succesvol gereset!",
@@ -284,60 +376,92 @@
"Print": "Afdrukken",
"Read": "Lees",
"Read state": "Lees status",
"Recurring": "",
"Redirection": "Omleiding",
"Refresh": "Vernieuw",
"Refresh summary": "Samenvatting vernieuwen",
"Remove": "",
"Remove report": "",
"Remove spam report": "",
"Remove spam report from {{count}} threads_one": "",
"Remove spam report from {{count}} threads_other": "",
"Remove tag": "",
"Reply": "Antwoorden",
"Reply all": "Allen beantwoorden",
"Report {{count}} threads as spam_one": "",
"Report {{count}} threads as spam_other": "",
"Report as spam": "",
"Reset": "Reset",
"Reset password": "Reset wachtwoord",
"Reset password of {{mailbox}}": "Reset wachtwoord van {{mailbox}}",
"Retry": "Opnieuw proberen",
"Save": "Opslaan",
"Save changes": "",
"Save into your {{driveAppName}}'s workspace": "Sla op in uw {{driveAppName}}'s workspace",
"Saving...": "Opslaan...",
"Search": "Zoek",
"Search a label": "Label zoeken",
"Search a tag": "",
"Search in messages...": "Zoeken in berichten...",
"See members of this thread ({{count}} members)_one": "",
"See members of this thread ({{count}} members)_other": "",
"Select a parent label": "Selecteer een parent label",
"Select a thread": "",
"Select all threads": "",
"Select thread": "",
"Select threads": "",
"Send": "Verstuur",
"Send and archive": "Verstuur en archiveer",
"Send and receive your messages in an instant.": "Verstuur en ontvang uw berichten direct.",
"Send Feedback": "Feedback Versturen",
"Sending message...": "Bericht verzenden...",
"Sent": "Verzonden",
"Settings": "",
"Share access": "Toegang delen",
"Share the credentials of this mailbox with its user. You must transfer them securely, preferably physically.": "Deel de inloggegevens van deze mailbox met zijn gebruiker. U moet deze veilig overdragen, bij voorkeur fysiek.",
"Share the new credentials to the user.": "Deel de nieuwe inloggegevens met de gebruiker.",
"Share your feedback here...": "Deel hier uw feedback...",
"Shared mailbox": "Gedeelde mailbox",
"Show": "Toon",
"Show {{count}} more_one": "",
"Show {{count}} more_other": "",
"Show embedded message": "Toon ingesloten bericht",
"Show less": "",
"Show logs": "Toon logs",
"Show more": "",
"Signature created!": "Handtekening gemaakt!",
"Signature deleted!": "Handtekening verwijderd!",
"Signature updated!": "Handtekening bijgewerkt!",
"Signature: {{name}}": "Handtekening: {{name}}",
"Signatures": "Handtekeningen",
"Signatures for {{mailbox}}": "",
"Simple and intuitive messaging": "Eenvoudige en intuïtieve berichten",
"Simple redirect (Coming soon)": "Eenvoudige doorverwijzing (binnenkort beschikbaar)",
"Some messages have not been delivered to all recipients.": "",
"Some recipients have not received this message!": "",
"Spam": "Spam",
"Spam report removed from {{count}} threads._one": "",
"Spam report removed from {{count}} threads._other": "",
"Start typing...": "Begin met typen...",
"Subject": "Onderwerp",
"Subject template": "",
"Subject template is required.": "",
"Subject:": "Onderwerp:",
"Subject: ": "Onderwerp: ",
"Summarize": "Vat samen",
"Summary": "Samenvatting",
"Summary refreshed!": "Samenvatting vernieuwd!",
"Synchronize mailboxes with an identity provider": "Synchroniseer mailboxen met een identiteitsprovider",
"Tags": "",
"Target": "Target",
"Target email": "Target email",
"Template created!": "Sjabloon aangemaakt!",
"Template deleted!": "Sjabloon verwijderd!",
"Template updated!": "Template bijgewerkt!",
"Temporary password": "Tijdelijk wachtwoord",
"Tentative": "",
"Thank you for your feedback!": "Bedankt voor je feedback!",
"The address has been updated!": "Het adres is bijgewerkt!",
"The default signature will be automatically loaded when composing a new message.": "",
"The domain <strong>{{domain}}</strong> has been created successfully.": "Het domein <strong>{{domain}}</strong> is succesvol aangemaakt.",
"The email {{email}} is invalid.": "De e-mail {{email}} is ongeldig.",
"The email address is invalid.": "Het e-mailadres is ongeldig.",
@@ -349,28 +473,51 @@
"The shared mailbox <strong>{{mailboxAddress}}</strong> has been created successfully.": "De gedeelde mailbox <strong>{{mailboxAddress}}</strong> is succesvol gemaakt.",
"The upload failed. Please try again.": "Upload mislukt, probeer het opnieuw.",
"These DNS records must be configured on the domain <strong>{{domain}}</strong> for the mail system to work properly. If you don't know how to update them, please contact your technical service provider or system administrator.": "Deze DNS-records moeten op het domein <strong>{{domain}}</strong> worden geconfigureerd om het mailsysteem naar behoren te laten werken. Als u niet weet hoe u deze kunt updaten, neem dan contact op met uw technische serviceprovider of systeembeheerder.",
"These tags will be automatically applied to every incoming message from the widget.": "",
"This action cannot be undone and the user will need the new password to access its mailbox.": "Deze actie kan niet ongedaan worden gemaakt en de gebruiker heeft het nieuwe wachtwoord nodig om toegang te krijgen tot de mailbox.",
"This contact's identity could not be verified. Proceed with caution.": "De identiteit van deze contactpersoon kon niet worden geverifieerd. Wees voorzichtig.",
"This description will be used by the AI to automatically assign this label to your messages.": "Deze beschrijving wordt gebruikt door de AI om dit label automatisch aan je berichten toe te wijzen.",
"This email prefix is not allowed for personal mailboxes. Please choose a different prefix.": "Dit e-mailvoorvoegsel is niet toegestaan voor persoonlijke mailboxen. Kies een ander voorvoegsel.",
"This event has been cancelled": "",
"This is the only admin of this mailbox, you cannot therefore modify its access.": "",
"This message has {{count}} attachments_one": "",
"This message has {{count}} attachments_other": "",
"This message has a draft": "",
"This message has been deleted.": "Dit bericht is verwijderd.",
"This message has not been delivered.": "Dit bericht is niet afgeleverd.",
"This message has not been delivered. You cancelled the delivery.": "",
"This message has not yet been delivered to all recipients.": "",
"This message is being delivered.": "Dit bericht wordt verstuurd.",
"This name is for internal use only and will not be visible to users.": "",
"This signature is forced": "Deze handtekening is geforceerd",
"This thread has been reported as spam.": "",
"This thread has been reported as spam. For your security, downloading attachments has been disabled.": "",
"Those message templates are linked to the mailbox \"{{mailbox}}\". In case of a shared mailbox, all other mailbox users will be able to use them.": "",
"Those signatures are linked to the mailbox \"{{mailbox}}\". In case of a shared mailbox, all other mailbox users will be able to use them.": "",
"Thread access removed": "Kanaal toegang verwijderd",
"To": "Aan",
"To be able to import emails from an IMAP server, you may need to allow IMAP access on your account.": "Om e-mails van een IMAP-server te kunnen importeren, moet je mogelijk IMAP-toegang op je account toestaan.",
"To:": "Aan:",
"To: ": "Aan: ",
"Today": "",
"Trash": "Prullenbak",
"Try again": "",
"Type": "Type",
"Unable to copy credentials.": "Kan de inloggegevens niet kopiëren.",
"Unable to copy to clipboard.": "",
"Unarchive": "Dearchiveren",
"Unarchive {{count}} threads_one": "",
"Unarchive {{count}} threads_other": "",
"Undelete": "Terugzetten",
"Undelete {{count}} threads_one": "",
"Undelete {{count}} threads_other": "",
"Undo": "Annuleren",
"Unfold message": "Bericht uitvouwen",
"Unknown": "Onbekend",
"Unknown user": "Onbekende gebruiker",
"Unread": "Ongelezen",
"Unsaved changes": "",
"until {{date}}": "",
"Update": "Bijwerken",
"Update a Label": "Update een Label",
"Updated at": "Bijgewerkt op",
@@ -378,17 +525,27 @@
"Uploading your archive": "Je archief wordt geüpload",
"Uploading... {{progress}}%": "Uploaden... {{progress}}%",
"Use \"Send and archive\" by default": "Gebruik standaard \"Verstuur en archiveer\"",
"Use {referer_domain} to include the website domain in the subject.": "",
"Use SSL": "Gebruik SSL",
"Value": "Waarde",
"Variables": "Variabelen",
"View full documentation": "",
"Website Widget": "",
"Weekly": "",
"While the signature is disabled, it will not be available to the users.": "Terwijl handtekening is uitgeschakeld, is niet beschikbaar voor de gebruikers.",
"Widget": "",
"Yearly": "",
"Yesterday": "",
"You": "Jij",
"You are the last editor of this thread, you cannot therefore modify your access.": "U bent de laatste redacteur van dit kanaal, daarom kunt u uw toegang niet aanpassen.",
"You can close this window and continue using the app.": "Je kunt dit venster sluiten en de app blijven gebruiken.",
"You can now inform the person that their mailbox is ready to be used and communicate the instructions for authentication.": "U kunt de persoon nu informeren dat hun mailbox klaar is om te worden gebruikt en de instructies voor authenticatie communiceren.",
"You cannot delete the last editor of this thread": "U kunt de laatste bewerker van dit kanaal niet verwijderen",
"You cannot modify it.": "Je kunt het niet wijzigen.",
"You have {{count}} recipients, which exceeds the maximum of {{max}} recipients per message. The message cannot be sent until you reduce the number of recipients._one": "",
"You have {{count}} recipients, which exceeds the maximum of {{max}} recipients per message. The message cannot be sent until you reduce the number of recipients._other": "",
"You have aborted the upload.": "Je hebt het uploaden afgebroken.",
"You have unsaved changes. Are you sure you want to close?": "",
"You must confirm this statement.": "U moet deze verklaring bevestigen.",
"Your email...": "Jouw email...",
"Your messages have been imported successfully!": "Je berichten zijn succesvol geïmporteerd!",
+3 -3
View File
@@ -657,8 +657,8 @@
"alwaysDisplayInConsole" : false,
"clientAuthenticatorType" : "client-secret",
"secret" : "ThisIsAnExampleKeyForDevPurposeOnly",
"redirectUris" : [ "http://localhost:8900/*", "http://localhost:8901/*", "http://localhost:8902/*", "http://backend:8000/*", "http://frontend:3000/*", "http://keycloak:8802/*", "http://nginx/*"],
"webOrigins" : [ "http://localhost:8900", "http://localhost:8901", "http://localhost:8902", "http://backend:8000", "http://frontend:3000", "http://keycloak:8802", "http://nginx" ],
"redirectUris" : [ "http://localhost:8900/*", "http://localhost:8901/*", "http://localhost:8902/*", "http://backend:8000/*", "http://frontend:3000/*", "http://keycloak:8802/*", "http://proxy/*"],
"webOrigins" : [ "http://localhost:8900", "http://localhost:8901", "http://localhost:8902", "http://backend:8000", "http://frontend:3000", "http://keycloak:8802", "http://proxy" ],
"notBefore" : 0,
"bearerOnly" : false,
"consentRequired" : false,
@@ -673,7 +673,7 @@
"access.token.lifespan" : "-1",
"client.secret.creation.time" : "1707820779",
"user.info.response.signature.alg" : "RS256",
"post.logout.redirect.uris" : "http://localhost:8900/*##http://localhost:8901/*##http://localhost:8902/*##http://backend:8000/*##http://frontend:3000/*##http://keycloak:8802/*##http://nginx/*",
"post.logout.redirect.uris" : "http://localhost:8900/*##http://localhost:8901/*##http://localhost:8902/*##http://backend:8000/*##http://frontend:3000/*##http://keycloak:8802/*##http://proxy/*",
"oauth2.device.authorization.grant.enabled" : "false",
"use.jwks.url" : "false",
"backchannel.logout.revoke.offline.tokens" : "false",
+1 -1
View File
@@ -58,7 +58,7 @@ Example response:
To run the tests, go to the repository root and do:
```
make mpa-test
make test-mpa
```
The test suite includes:

Some files were not shown because too many files have changed in this diff Show More