mirror of
https://github.com/suitenumerique/messages.git
synced 2026-08-17 21:25:41 +02:00
🔨(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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,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=
|
||||
|
||||
@@ -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
@@ -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)"]
|
||||
|
||||
+33
-53
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,8 +46,7 @@ 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)
|
||||
best_labels = AIService().call_ai_api(prompt)
|
||||
|
||||
# Get rid of surrounding text if present
|
||||
if best_labels.startswith('["') and best_labels.endswith('"]'):
|
||||
|
||||
@@ -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)
|
||||
summary = AIService().call_ai_api(prompt)
|
||||
|
||||
return summary
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
@@ -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,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."""
|
||||
|
||||
@@ -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,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,8 +51,7 @@ 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
|
||||
raise CommandError(f"Domain with ID {domain_id} not found") from None
|
||||
|
||||
if pretend:
|
||||
self.stdout.write(
|
||||
@@ -96,9 +97,14 @@ class Command(BaseCommand):
|
||||
else:
|
||||
self.stdout.write("Provisioning DNS records...")
|
||||
|
||||
results = provision_domain_dns(
|
||||
maildomain, provider_name=provider_name, pretend=pretend
|
||||
)
|
||||
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,10 +114,8 @@ 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}")
|
||||
self.stdout.write(f"Provider used: {provider_used}")
|
||||
|
||||
if results["changes"]:
|
||||
if pretend:
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
+192
-197
@@ -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."
|
||||
)
|
||||
"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(
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
response = self.client.chat.completions.create(**data)
|
||||
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:
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
self.s3_client.abort_multipart_upload(
|
||||
Bucket=self.bucket,
|
||||
Key=self.key,
|
||||
UploadId=self.upload_id,
|
||||
)
|
||||
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:
|
||||
|
||||
@@ -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,16 +451,16 @@ 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,
|
||||
max_retries,
|
||||
)
|
||||
raise
|
||||
logger.error(
|
||||
"Failed to fetch message %s after %d attempts",
|
||||
msg_num,
|
||||
max_retries,
|
||||
)
|
||||
raise
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
return False, {"detail": f"Unsupported file format: {content_type}"}
|
||||
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:
|
||||
|
||||
@@ -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}}
|
||||
)
|
||||
|
||||
@@ -64,12 +64,11 @@ 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")
|
||||
return now.strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
def get_period_expiry(period_name: str) -> int:
|
||||
@@ -81,16 +80,15 @@ 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
|
||||
return 86400
|
||||
|
||||
|
||||
def get_throttle_cache_key(entity_type: str, entity_id: str, period_key: str) -> str:
|
||||
@@ -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"
|
||||
hours = math.ceil(seconds / 3600)
|
||||
return f"{hours}h"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 "Нідерландська"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Generated
-3765
File diff suppressed because it is too large
Load Diff
+53
-45
@@ -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]
|
||||
|
||||
Generated
+2487
File diff suppressed because it is too large
Load Diff
+5
-5
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Executable
+8
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;'
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user