diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e1397a..2c86fda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # RustBox changelog +### 0.2.1 +- Fixed Docker development authentication by documenting and defaulting Compose to `ENVIRONMENT=development`, so session cookies work over direct HTTP while production keeps `Secure` cookies behind HTTPS. +- Exposed `pgp_enabled` through the auth policy and hid OpenPGP settings, identity controls, composer controls and reader UI when OpenPGP is disabled by administrator policy. +- Prevented disabled OpenPGP from producing repeated UI error toasts or unnecessary API requests. +- Added OpenSearch outbox worker backoff for HTTP 429/flood-stage/read-only conditions and stopped the current batch after the first backpressure failure, preventing log storms while preserving durable retries without consuming dead-letter attempts. +- Kept OpenSearch disk safety thresholds intact; Docker documentation explains that named volumes consume the Docker host filesystem and how to diagnose/recover flood-stage blocks. +- Added a stable `rustbox` Compose project name, documented Docker environment variables, and made application port mapping configurable through `PORT`. +- Reduced PostgreSQL startup log noise in the Docker example by suppressing repeated SQLx `NOTICE` messages at the default log filter. +- Bumped the application/source release to 0.2.1. + ### 0.2.0 - Fixed nested database transactions during message move by reusing the caller-owned transaction for cache removal, header-sync invalidation, folder invalidation, and search outbox writes. - Kept `search_outbox` in sync between tenant and legacy-import table sets so schema/import parity tests pass. diff --git a/Cargo.lock b/Cargo.lock index 2bbc5c3..bfa3ae5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3508,7 +3508,7 @@ dependencies = [ [[package]] name = "rustbox" -version = "0.2.0" +version = "0.2.1" dependencies = [ "aes-gcm", "async-imap", diff --git a/Cargo.toml b/Cargo.toml index 0283b51..9489e42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustbox" -version = "0.2.0" +version = "0.2.1" edition = "2024" rust-version = "1.88" default-run = "rustbox" diff --git a/NOTICE.md b/NOTICE.md index dd9a643..18f3f15 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -1,6 +1,6 @@ # RustBox attribution / notice -RustBox 0.2.0 is derived from the Oxi webmail project: +RustBox 0.2.1 is derived from the Oxi webmail project: - Upstream: `c0h1b4/oxi` - Base snapshot: `7f1507c486b78cf33e54beb701dd460d3bd2bd5d` diff --git a/README.md b/README.md index daf9596..68dab20 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# RustBox 0.2.0 +# RustBox 0.2.1 RustBox is a webmail derived from `c0h1b4/oxi`. The consumed Oxi revision is recorded in `UPSTREAM_BASE`. @@ -34,7 +34,7 @@ rustbox/ `build.rs` combines the CSS modules from `src/web/css/manifest.txt` and JavaScript modules from `src/web/js/manifest.txt`. Cargo embeds the generated CSS/JS bundles, HTML and SVG sprite in the RustBox executable. There is no standalone source `src/web/app.js`; `src/web/` is the single source of truth and Cargo generates the embedded bundles in `OUT_DIR`. -## Storage and search in 0.2.0 +## Storage and search in 0.2.1 RustBox separates three concerns: SQL metadata, blob storage and search. The SQL engine does **not** decide whether `data_dir` may be used. A single-node PostgreSQL/MySQL deployment may still use filesystem blobs and local Tantivy. In cluster mode, local node state is rejected unless the configured backend is explicitly shared. @@ -148,7 +148,7 @@ rustbox search reindex Ready Docker examples are in `docker/`: standalone SQLite, PostgreSQL + SILO (S3-compatible) + OpenSearch, and MySQL + NFS + OpenSearch. -All executable application SQL is centralized by dialect under `src/queries/{sqlite,mysql,pgsql}/queries.rs`. The squashed compatibility baseline remains V046; RustBox 0.2.0 adds runtime migration V047 for attachment checksums/logical blob keys and the durable search outbox. The next migration number is V048. See `migrations/README.md`. +All executable application SQL is centralized by dialect under `src/queries/{sqlite,mysql,pgsql}/queries.rs`. The squashed compatibility baseline remains V046; RustBox 0.2.1 adds runtime migration V047 for attachment checksums/logical blob keys and the durable search outbox. The next migration number is V048. See `migrations/README.md`. ## Development @@ -217,7 +217,7 @@ Initialize Git after extracting the release archive: ```bash git init git add . -git commit -m "RustBox 0.2.0 release" +git commit -m "RustBox 0.2.1 release" ./scripts/setup-upstream.sh ``` diff --git a/RELEASE-MANIFEST.txt b/RELEASE-MANIFEST.txt index c5db3ec..3018b6b 100644 --- a/RELEASE-MANIFEST.txt +++ b/RELEASE-MANIFEST.txt @@ -1,4 +1,4 @@ -RustBox 0.2.0 release manifest +RustBox 0.2.1 release manifest Architecture: - database_backend = sqlite | mysql | pgsql selects authoritative SQL only @@ -33,13 +33,13 @@ Docker examples: - docker/compose-mysql-nfs-opensearch.yml (MySQL + real NFS volume + OpenSearch) Operations/docs: -- README.md documents 0.2.0 storage/search configuration +- README.md documents 0.2.1 storage/search configuration - docker/README.md documents Compose variants - docs/STORAGE_SEARCH_OPERATIONS.md documents failure modes, shared-storage requirements and migration behavior - TODO.md leaves live SILO/MinIO/AWS/Ceph and real multi-node acceptance validation explicitly pending Build/release metadata: -- Cargo.toml version = 0.2.0 +- Cargo.toml version = 0.2.1 - rust-toolchain.toml pins Rust 1.88.0 - Cargo.lock is intentionally not shipped because this environment could not regenerate it after dependency changes - CI and scripts/build-release.sh generate Cargo.lock, then run locked tests/Clippy/release build diff --git a/RELEASE-VALIDATION.txt b/RELEASE-VALIDATION.txt index abb71a9..ac94077 100644 --- a/RELEASE-VALIDATION.txt +++ b/RELEASE-VALIDATION.txt @@ -1,7 +1,7 @@ -RustBox 0.2.0 - final source validation +RustBox 0.2.1 - final source validation Completed in this environment: -- offline release validator: scripts/validate-release-static.py (139/139 checks passed) +- offline release validator: scripts/validate-release-static.py (144/144 checks passed) - all maintained TOML examples parse successfully - Docker Compose topology/config references are validated by the offline release validator - JSON files parse successfully @@ -11,9 +11,12 @@ Completed in this environment: - Rust source delimiter sanity check passes after removing partial-patch leftovers - git diff --check passes on the final source tree - no Cargo.lock, *.orig, *.rej, temporary FIX/APPLY scripts, or patch leftovers are shipped -- regression source checks cover every Clippy failure reported during the 0.2.0 release attempt +- regression source checks retain the 0.2.0 release gate and add 0.2.1 policy/backpressure checks -0.2.0 fixes included: +0.2.1 fixes included: +- OpenPGP policy visibility follows pgp_enabled and disabled OpenPGP is hidden from the UI +- OpenSearch 429/flood-stage failures trigger worker backoff instead of per-event warning storms +- Docker development defaults session cookies for direct HTTP and documents the production HTTPS requirement - cluster_mode controls the implicit data_dir default; PostgreSQL/MySQL remain valid with local data_dir backends in single-node deployments - storage/search configuration comes only from [storage] and [search] - removed pre-0.2 top-level storage/search switches and the tantivy backend alias diff --git a/config.example.toml b/config.example.toml index 05d52c4..9afeb46 100644 --- a/config.example.toml +++ b/config.example.toml @@ -1,4 +1,4 @@ -# RustBox 0.2.0 +# RustBox 0.2.1 host = "127.0.0.1" port = 3001 environment = "production" diff --git a/docker/README.md b/docker/README.md index ac64204..16d0bef 100644 --- a/docker/README.md +++ b/docker/README.md @@ -112,7 +112,7 @@ The bundled SILO topology is intended for local/integration validation of the S3 ### `compose-mysql-nfs-opensearch.yml` -MySQL, shared POSIX/NFS blob storage and OpenSearch. The NFS volume driver is deliberate: a normal local Docker named volume is not shared between Docker hosts and therefore is not a valid blob backend for a multi-host RustBox deployment. +MySQL, shared POSIX/NFS blob storage and OpenSearch. The NFS volume driver is deliberate: a normal local Docker named volume is not shared between Docker hosts and therefore is not a valid blob backend for a multi-host RustBox deployment. These examples intentionally run one RustBox application container; the shared backing services demonstrate storage/database topology, not multi-app load balancing. ## Production notes @@ -135,3 +135,33 @@ ENVIRONMENT=production docker compose -p rustbox -f compose-postgres-s3-opensear In `production`, RustBox intentionally marks authentication cookies as `Secure`. Opening the application directly over plain HTTP in that mode causes the browser not to send the session cookie and protected requests return `Invalid or expired session`. After changing from production/HTTP to development/HTTP, log in again. If the browser still has stale RustBox state, clear cookies/site data for the RustBox origin once and reload. + +## OpenSearch disk watermark / 429 errors + +The `opensearch-data` named volume still consumes space on the filesystem used by Docker. Running OpenSearch in a container does not give it an independent disk. If the Docker host filesystem reaches OpenSearch's flood-stage threshold, OpenSearch protects indices with `read_only_allow_delete` and RustBox indexing receives HTTP 429 responses. + +Check the actual filesystem seen by the container: + +```bash +docker compose -p rustbox -f compose-postgres-s3-opensearch.yml exec opensearch \ + df -h /usr/share/opensearch/data +``` + +Also inspect Docker usage on the host: + +```bash +df -h +docker system df +docker volume inspect rustbox_opensearch-data +``` + +Free disk space first. Do not permanently lower the OpenSearch flood-stage safety threshold. If the index remains read-only after disk usage is back below the high watermark, clear the block explicitly: + +```bash +docker compose -p rustbox -f compose-postgres-s3-opensearch.yml exec opensearch \ + curl -fsS -X PUT http://localhost:9200/rustbox-messages-v1/_settings \ + -H 'Content-Type: application/json' \ + -d '{"index.blocks.read_only_allow_delete":null}' +``` + +RustBox 0.2.1 backs off its OpenSearch outbox worker after these 429/flood-stage responses, so a full disk no longer produces one warning per queued event. diff --git a/docker/compose-postgres-s3-opensearch.yml b/docker/compose-postgres-s3-opensearch.yml index b37bdb4..55a7d72 100644 --- a/docker/compose-postgres-s3-opensearch.yml +++ b/docker/compose-postgres-s3-opensearch.yml @@ -76,7 +76,7 @@ services: SMTP_HOST: ${SMTP_HOST:-mail.example.com} SMTP_PORT: ${SMTP_PORT:-587} TLS_ENABLED: ${TLS_ENABLED:-true} - RUST_LOG: ${RUST_LOG:-info} + RUST_LOG: ${RUST_LOG:-info,sqlx::postgres::notice=warn} DATABASE_URL: postgres://rustbox:${POSTGRES_PASSWORD:-rustbox-dev-password}@postgres:5432/rustbox SESSION_ENCRYPTION_KEY: ${SESSION_ENCRYPTION_KEY:-MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=} AWS_ACCESS_KEY_ID: ${S3_ACCESS_KEY:-rustbox} diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index 95ec914..59ed518 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -1,8 +1,8 @@ -# RustBox 0.2.0 - implementation status +# RustBox 0.2.1 - implementation status -This file tracks the `0.2.0` source snapshot. Storage/search scope and outstanding acceptance work are tracked in `TODO.md`. A feature is only listed as implemented when working code exists in this source tree; partial items are called out explicitly. +This file tracks the `0.2.1` source snapshot. Storage/search scope and outstanding acceptance work are tracked in `TODO.md`. A feature is only listed as implemented when working code exists in this source tree; partial items are called out explicitly. -## 0.2.0 storage/search architecture +## 0.2.1 storage/search architecture - SQL backend selection (`sqlite`/`mysql`/`pgsql`) is independent from blob storage and search backend selection. - Cluster mode clears an implicit/default `data_dir`; PostgreSQL/MySQL single-node deployments may still intentionally use `data_dir`-backed filesystem storage and local Tantivy. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 4adece9..2c2fc80 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -27,7 +27,7 @@ RUSTBOX_CONFIG=./config.dev.toml ./target/release/rustbox --check-config Browser JavaScript source lives in functional modules under `src/web/js/`, ordered by `src/web/js/manifest.txt`. `build.rs` concatenates those modules into the embedded `rustbox-ui.js` asset during every Cargo build. The repository has no duplicate `ui/` snapshot; `src/web/` is the only browser UI source. -## Updating an existing checkout to 0.2.0 +## Updating an existing checkout to 0.2.1 Do not overlay the release with `cp *` or another command that skips dotfiles. That can retain stale `.github`, `.env.example`, or `Cargo.lock` files. diff --git a/docs/STORAGE_SEARCH_OPERATIONS.md b/docs/STORAGE_SEARCH_OPERATIONS.md index b613ecb..ab72a74 100644 --- a/docs/STORAGE_SEARCH_OPERATIONS.md +++ b/docs/STORAGE_SEARCH_OPERATIONS.md @@ -1,6 +1,6 @@ -# RustBox 0.2.0 storage/search operations +# RustBox 0.2.1 storage/search operations -RustBox 0.2.0 treats SQL, blob storage and search as independent capabilities. The database engine does not decide whether `data_dir` is allowed. +RustBox 0.2.1 treats SQL, blob storage and search as independent capabilities. The database engine does not decide whether `data_dir` is allowed. ## Deployment matrix diff --git a/migrations/README.md b/migrations/README.md index ecfc5c2..ad6c5d3 100644 --- a/migrations/README.md +++ b/migrations/README.md @@ -1,6 +1,6 @@ # Database schemas and migrations -RustBox 0.2.0 keeps SQL, blob storage and search as independent concerns: +RustBox 0.2.1 keeps SQL, blob storage and search as independent concerns: ```text database_backend = sqlite | mysql | pgsql @@ -26,7 +26,7 @@ migrations/ └── V047__blob_keys_and_search_outbox.sql ``` -Each `schema.sql` is the current complete schema for fresh installations. The historical compatibility baseline remains **V046**. RustBox 0.2.0 adds **V047**, which adds attachment SHA-256 metadata and the durable search outbox used by external indexing. +Each `schema.sql` is the current complete schema for fresh installations. The historical compatibility baseline remains **V046**. RustBox 0.2.1 adds **V047**, which adds attachment SHA-256 metadata and the durable search outbox used by external indexing. Account data is isolated inside the selected physical database with a deterministic, collision-checked table prefix derived from the account hash. The `{{p}}` marker in tenant SQL is replaced by RustBox at provisioning/migration time. Sessions are global and therefore are not prefixed. @@ -34,7 +34,7 @@ Account data is isolated inside the selected physical database with a determinis Every schema-changing release must add the same logical migration for all supported SQL engines and register it in `src/db/pool.rs`. Do not update only `schema.sql`: fresh installs use the baseline while existing installations use the numbered migration path. -The next migration number after RustBox 0.2.0 is **V048**. +The next migration number after RustBox 0.2.1 is **V048**. Redis and Redis Sentinel do not use SQL migrations. diff --git a/packaging/rustbox.toml b/packaging/rustbox.toml index 097c764..70f31a4 100644 --- a/packaging/rustbox.toml +++ b/packaging/rustbox.toml @@ -1,4 +1,4 @@ -# RustBox 0.2.0 - packaged production configuration (DEB/RPM) +# RustBox 0.2.1 - packaged production configuration (DEB/RPM) # # This file contains all supported RustBox configuration options. # Commented options are optional and do not change the packaged defaults. diff --git a/scripts/validate-release-static.py b/scripts/validate-release-static.py index bab70b5..b3f84a6 100755 --- a/scripts/validate-release-static.py +++ b/scripts/validate-release-static.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Static/offline release checks for RustBox 0.2.0. +"""Static/offline release checks for RustBox 0.2.1. These checks intentionally avoid pretending to replace `cargo test`: they validate configuration, SQL migrations, Docker examples, query-catalog parity and release @@ -49,7 +49,7 @@ def extract_section(text: str, begin: str, end: str) -> str: # Release/toolchain metadata. cargo = tomllib.loads(read("Cargo.toml")) -require(cargo["package"]["version"] == "0.2.0", "Cargo package version is 0.2.0") +require(cargo["package"]["version"] == "0.2.1", "Cargo package version is 0.2.1") require(cargo["package"]["rust-version"] == "1.88", "Cargo rust-version is 1.88") toolchain = tomllib.loads(read("rust-toolchain.toml")) require(toolchain["toolchain"]["channel"] == "1.88.0", "rust-toolchain pins Rust 1.88.0") @@ -58,13 +58,18 @@ require("cargo generate-lockfile" in read("scripts/build-release.sh"), "release require("cargo generate-lockfile" in read(".github/workflows/ci.yml"), "CI regenerates Cargo.lock before --locked checks") require("validate-release-static.py" in read("scripts/build-release.sh"), "release script runs offline/static release gate") require("validate-release-static.py" in read(".github/workflows/ci.yml"), "CI runs offline/static release gate") -require(read("README.md").startswith("# RustBox 0.2.0"), "README identifies RustBox 0.2.0") -require(read("RELEASE-MANIFEST.txt").startswith("RustBox 0.2.0"), "release manifest identifies RustBox 0.2.0") -require(read("RELEASE-VALIDATION.txt").startswith("RustBox 0.2.0"), "release validation identifies RustBox 0.2.0") -require(read("docs/IMPLEMENTATION_STATUS.md").startswith("# RustBox 0.2.0"), "implementation status identifies RustBox 0.2.0") +require(read("README.md").startswith("# RustBox 0.2.1"), "README identifies RustBox 0.2.1") +require(read("RELEASE-MANIFEST.txt").startswith("RustBox 0.2.1"), "release manifest identifies RustBox 0.2.1") +require(read("RELEASE-VALIDATION.txt").startswith("RustBox 0.2.1"), "release validation identifies RustBox 0.2.1") +require(read("docs/IMPLEMENTATION_STATUS.md").startswith("# RustBox 0.2.1"), "implementation status identifies RustBox 0.2.1") require((ROOT / "docs/STORAGE_SEARCH_OPERATIONS.md").is_file(), "storage/search operations guide exists") require('imap_host = ""' not in read("config.dev.toml"), "development config does not override IMAP host with an invalid empty value") require(not re.search(r"(?m)^IMAP_HOST=\s*$", read(".env.example")), "environment example does not set an invalid empty IMAP host") +require("pgp_enabled: bool" in read("src/routes/auth.rs"), "auth policy exposes OpenPGP availability") +require("setOpenPgpAvailability" in read("src/web/js/72-openpgp-identities.js"), "UI hides administratively disabled OpenPGP") +require("is_opensearch_backpressure_error" in read("src/search/engine.rs"), "OpenSearch worker classifies backpressure failures") +require("worker pass failed; backing off" in read("src/search/engine.rs"), "OpenSearch worker backs off after failures") +require("ENVIRONMENT: ${ENVIRONMENT:-development}" in read("docker/compose-postgres-s3-opensearch.yml"), "PostgreSQL Compose defaults direct HTTP to development cookie mode") # Parse all maintained TOML examples. toml_files = [ diff --git a/src/db/search_outbox.rs b/src/db/search_outbox.rs index 5180bab..dcd11a9 100644 --- a/src/db/search_outbox.rs +++ b/src/db/search_outbox.rs @@ -164,6 +164,45 @@ pub fn mark_failure( Ok(changed > 0) } +pub fn defer_transient_failure( + conn: &Connection, + event: &OutboxEvent, + error: &str, + delay_secs: u64, +) -> Result { + let available_at = now_epoch().saturating_add(delay_secs.max(1) as i64); + let now = now_epoch(); + let truncated: String = error.chars().take(2048).collect(); + let changed = match crate::db::pool::configured_backend() { + crate::db::pool::DatabaseBackend::Mysql => conn.execute( + crate::dbq!(db_search_outbox::Q_004), + params![ + event.attempts, + available_at, + false, + &truncated, + now, + &event.id, + &event.generation + ], + ), + _ => conn.execute( + crate::dbq!(db_search_outbox::Q_004), + params![ + &event.id, + &event.generation, + event.attempts, + available_at, + false, + &truncated, + now + ], + ), + } + .map_err(|e| format!("failed to defer transient search event: {e}"))?; + Ok(changed > 0) +} + pub fn clear_before(conn: &Connection, updated_before: i64) -> Result { conn.execute( crate::dbq!(db_search_outbox::Q_007), diff --git a/src/routes/auth.rs b/src/routes/auth.rs index be1f5ca..9f86984 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -20,6 +20,7 @@ pub struct AuthPolicyResponse { remember_me_enabled: bool, remember_me_days: u64, session_timeout_hours: u64, + pgp_enabled: bool, } pub async fn auth_policy(Extension(config): Extension>) -> Json { @@ -27,6 +28,7 @@ pub async fn auth_policy(Extension(config): Extension>) -> Json 0 => { - tracing::debug!(processed, "processed OpenSearch outbox events"); + let delay_secs = match process_search_outbox( + &backend, + &data_dir, + batch_size, + max_attempts, + ) + .await + { + Ok(processed) => { + failure_streak = 0; + if processed > 0 { + tracing::debug!(processed, "processed OpenSearch outbox events"); + } + interval_secs } - Ok(_) => {} - Err(error) => tracing::warn!(%error, "OpenSearch outbox worker pass failed"), - } + Err(error) => { + failure_streak = failure_streak.saturating_add(1); + let multiplier = 2u64.saturating_pow(failure_streak.min(5)); + let retry_after_secs = interval_secs + .saturating_mul(multiplier) + .clamp(interval_secs, 60); + tracing::warn!( + %error, + retry_after_secs, + "OpenSearch outbox worker pass failed; backing off" + ); + retry_after_secs + } + }; + tokio::time::sleep(Duration::from_secs(delay_secs)).await; } })) } @@ -1033,6 +1055,14 @@ impl OpenSearchUserIndex { } } +fn is_opensearch_backpressure_error(error: &str) -> bool { + let lower = error.to_ascii_lowercase(); + lower.contains("429 too many requests") + || lower.contains("status 429") + || lower.contains("flood-stage watermark") + || lower.contains("read-only-allow-delete") +} + async fn process_search_outbox( backend: &OpenSearchBackend, data_dir: &str, @@ -1082,6 +1112,17 @@ async fn process_search_outbox( processed += 1; } Err(error) => { + if is_opensearch_backpressure_error(&error) { + crate::db::search_outbox::defer_transient_failure( + &conn, + &event, + &error, + 30, + )?; + return Err(format!( + "OpenSearch temporarily rejected indexing for tenant {user_hash}: {error}" + )); + } crate::db::search_outbox::mark_failure(&conn, &event, &error, max_attempts)?; tracing::warn!(%error, event_id = %event.id, %user_hash, "OpenSearch outbox event failed"); } @@ -1105,6 +1146,19 @@ mod tests { (tmp, user_index) } + #[test] + fn opensearch_backpressure_classifier_detects_flood_stage_and_429() { + assert!(is_opensearch_backpressure_error( + "OpenSearch document upsert failed (429 Too Many Requests): flood-stage watermark" + )); + assert!(is_opensearch_backpressure_error( + "index blocked by read-only-allow-delete block" + )); + assert!(!is_opensearch_backpressure_error( + "OpenSearch document upsert failed (500 Internal Server Error)" + )); + } + #[test] fn opensearch_event_lease_covers_timeout_and_failover_budget() { let backend = OpenSearchBackend { diff --git a/src/web/index.html b/src/web/index.html index dc172b3..4793299 100644 --- a/src/web/index.html +++ b/src/web/index.html @@ -943,7 +943,7 @@
-
Configured identitiesSelect an +
Configured identitiesSelect an identity to edit its sender data, text/HTML footer, image and OpenPGP defaults.
@@ -1249,7 +1249,7 @@
-
DefaultsChoose where this signature +
DefaultsChoose where this signature is inserted and, when OpenPGP is configured, its default behavior.