Skip to content

Security

Version scope: The managed catalog and React UI security model on this page is Local Email App V2 behavior. See Version availability before using this guidance with a PyPI installation.

An email MCP server can read private messages, modify mailboxes, send messages, and access local files. Review the controls on this page before exposing it to an MCP client or network.

Local management UI security

mcp-email-server ui is a foreground, single-user local adapter. It binds exactly to IPv4 127.0.0.1; port 0 is the default and selects an ephemeral port. The command exposes no host, wildcard, share, daemon, debug, reload, CORS, or remote mode. Its process-unique route serves only packaged same-origin React assets and explicit management use cases. There is no provider-connectivity control or route, mail, arbitrary file, generic RPC, OpenAPI, metrics, or unauthenticated health route. Provider diagnostics remain available only through the low-level account test CLI. Before binding, the process freezes its bootstrap mode and selected catalog. It does not open the catalog during that freeze, so an unavailable selected catalog can still be recovered by selecting legacy; status continues comparing against the frozen startup authority and requires a restart after the change.

At startup, a high-entropy bootstrap value is placed only in a URL fragment. The default command hands that URL directly to the browser. With --no-open, or when browser launch reports failure, it prints the URL only to an attached stdout/stderr TTY; without a TTY it fails before serving, so the value is not written to a pipe or noninteractive log. Fragments are not sent with HTTP requests. The frontend removes it immediately with history.replaceState, sends it once in an Authorization header, and keeps the returned CSRF value only in memory. The server compares a fixed-size hash in constant time and atomically consumes the bootstrap. It expires after five minutes, is attempt-rate-limited, and cannot be replayed.

A successful exchange creates a random process-local session and a process-unique HttpOnly, SameSite=Strict cookie scoped to the process route. Every mutation requires the exact 127.0.0.1:<port> Host, exact startup Origin, accepted same-origin Fetch Metadata when present, JSON content type, session cookie, and separate CSRF header. Request and response bodies are bounded. Logout, normal shutdown, startup failure, SIGINT/SIGTERM, or process restart invalidates all tokens and sessions. Authentication failures do not redirect. GET routes never create preview capabilities or initialize storage. Import preview and default initialization are CSRF-protected POSTs. Only after authentication and a status read may the frontend call default initialization, and only a backend-proven empty installation triggers that call automatically. The backend chooses the managed.sqlite3 sibling path, rechecks the effective legacy source, and atomically compares the initial zero revision plus the absent bootstrap-file proof against concurrent legacy settings writes. Detected legacy content requires an explicit Import existing settings preparation click and preview review. Fresh initialization selects managed mode without importing or contacting providers. Legacy preparation keeps the prior runtime selected; only a fully successful reviewed import with no unsupported provider types automatically selects managed. Failure leaves legacy selected.

Every response uses Cache-Control: no-store, a same-origin CSP with framing, objects, forms, base changes, workers, and remote runtime assets disabled, nosniff, no-referrer, frame denial, and a restrictive permissions policy. The application ships no CDN code, remote font, analytics, telemetry, service worker, or runtime asset download. Secret values occur only in protected credential mutation bodies. The account editor may derive non-secret names and server suggestions from the typed email domain, but performs no remote discovery. Password fields live only in the active account editor or that account's Password component; the frontend clears them after every outcome, when an optional SMTP section is disabled, when the selected account or credential role changes, and when the component unmounts. Secrets never enter URLs, browser storage, responses, logs, or conflict summaries.

Web/application errors expose only bounded categories and fixed safe messages, never exception text. Management access logs contain only a fixed operation id, bounded allowlisted method, status, and duration. They never contain route/path parameters or templates, raw paths, URLs or queries, request/response bodies, account/email/filesystem values, session/CSRF/bootstrap tokens, secrets, or exception text.

The route and cookie names are random defense in depth, not substitutes for the session checks. This is explicitly a local single-user trust boundary. A malicious process running as the same operating-system user can generally inspect or replace user-owned state; same-UID hostile path replacement is not claimed to be contained. Do not treat loopback, owner-only modes, or path preflight as a multi-user or hostile-same-UID sandbox.

Mail provider log redaction

SMTP diagnostics use bounded phase records such as phase=mail, phase=rcpt, phase=data, and phase=cleanup. Warnings may include a numeric SMTP response code or one of the fixed transport categories timeout, connection, tls, io, or unexpected.

These records remain redacted at every log level. They never include account usernames, sender or recipient addresses (including BCC), endpoint hostnames, provider response text, exception text, subjects, bodies, raw MIME, attachment content, or secret values. Enabling DEBUG adds safe phase transitions only; it does not enable a message-content dump. MCP send results may expose only reviewed fixed delivery tags; an unrecognized detail is omitted rather than copied from a provider response.

Managed credential storage

Managed mode never falls back to TOML plaintext. Its default managed secret backend is platform-specific:

  • Linux and Windows store values in the dedicated managed_secret table inside the same private managed SQLite database as the catalog;
  • macOS uses the operating-system keyring. Platforms without the complete filesystem-security profile are not supported for managed catalogs.

Only the SecretStore adapter reads or writes these values. Catalog queries, projections, CLI/UI responses, diagnostics, and logs never select or expose the managed_secret.secret_value column or a keyring value. On Linux and Windows, the managed catalog is a secret-bearing database: file copies, snapshots, and backups include plaintext managed_secret.secret_value values. Keep every copy under protection equivalent to the private original; do not upload, share, or treat it as a non-secret account database.

A create or rotation stores a new immutable value and commits it as active only if the reviewed account revision still matches. On Linux and Windows, inserting managed_secret, activating its binding, incrementing the binding/account revision, and marking any old active value CLEANUP_REQUIRED are one SQLite transaction. On macOS, the new keyring value is written before one compare-and-swap activation transaction. A conflict or failure before activation returns an error, best-effort deletes any unreferenced keyring value, persists no provisional binding, and leaves the current binding authority unchanged.

The active credential is never overwritten in place. Rotation first makes the new value authoritative and marks the old active value CLEANUP_REQUIRED; it then deletes the old value and clears cleanup state after confirmed success. Only an external or follow-up deletion failure retains CLEANUP_REQUIRED, so a crash before deletion remains visible to config doctor and config cleanup-credentials. Failed saves expose no follow-up password action beyond retrying a new save after correcting the reported problem.

Credential removal is available only for a disabled account. It atomically detaches the binding and increments the account revision before attempting value deletion, so an enabled provider operation cannot continue to use a credential that is being removed. Failed deletion remains CLEANUP_REQUIRED. config cleanup-credentials processes at most 100 cleanup rows per invocation, revalidates that each value is superseded, and never deletes an active binding.

Enter managed credentials only through user-controlled terminal stdin (the masked prompt or explicit --password-stdin). Never place them in argv. config doctor, account summaries, errors, logs, and MCP results do not expose secret values or internal locators. The CLI --json mode uses reviewed presentation fields rather than recursively serializing application objects; it likewise omits secret values, locators, database paths, and import preview tokens. Secret-writing JSON commands require --password-stdin only to preserve a single result document and remain user-operated; JSON does not make an agent a safe credential channel. A missing or unreadable active secret fails closed rather than selecting a legacy account or plaintext fallback.

Managed bootstrap/catalog support requires the complete platform profile. Selection authority lives in a private sibling sidecar (config.bootstrap.toml for config.toml), not in the legacy source. Bootstrap sidecars, their immediate parent, SQLite database and WAL/SHM sidecars, and locks must remain private regular non-link objects with stable identity.

On POSIX, the profile uses current-owner checks, 0700 directories, 0600 files, component no-follow/directory-descriptor traversal, single-link checks, and bounded fcntl locking. On Linux, these controls protect both catalog state and the managed_secret table.

Windows filesystem boundary

On Windows, managed/bootstrap authority, attachment materialization, and oversized-result spill support only an ordinary drive-letter path on a local fixed NTFS volume with at least one validated directory below the volume root. Direct C:\\file-style volume-root storage is unsupported. UNC and mapped network paths, remote drives, \\?\\/\\.\\ device namespaces, alternate data streams, FAT/exFAT, and unknown filesystem types fail before parent creation, SQLite/provider work, or credential effects.

Every existing component is opened with reparse traversal disabled and held without delete sharing while sensitive checks run. File and directory symlinks, junctions, mount points, and provider-defined reparse tags are rejected. Identity is the volume serial plus file index read from the held handle; exact files must be regular and single-link. Private objects are current-user-owned and use a protected DACL granting only the current user, LocalSystem, and built-in Administrators. A Windows OWNER RIGHTS/CREATOR OWNER ACE is treated as that already validated owner, not as an independent principal. Unknown/NULL DACLs, unsupported allow ACEs, foreign owners, or any allow access granted to another SID fail closed, including raw generic ACE masks. Existing safe private parents may be normalized to the protected DACL only during an explicit managed creation operation; validation-only reads never rewrite a DACL, and an unsafe parent is never silently made acceptable.

Windows cross-process locks use the maintained filelock NtCreateFile and LockFileEx path after parent-chain validation. Lock handles reject final reparse points, deny delete sharing, use a fixed byte range, time out in a bounded period, and are released by process termination. SQLite DB/WAL/SHM/lock objects are validated before open and again after WAL setup. On Windows, sidecar validation runs under the setup lock because SQLite creates an inherited-DACL sidecar before the adapter can harden it; concurrent local opens wait rather than rejecting that bounded intermediate state. Every current SQLite sidecar is rehardened and revalidated because another connection's final close may delete and recreate WAL/SHM under a new identity. Post-commit reconciliation contention is logged and deferred rather than misreporting a committed mutation as failed; the next open still performs strict prevalidation before SQLite. These NTFS identity and DACL controls protect both catalog state and the managed_secret table, so the Windows catalog and every backup are sensitive.

Private Windows replacement for bootstrap authority, legacy TOML, attachments, and spill output creates a random same-directory file with CREATE_NEW and the private DACL, writes through its held handle, and calls FlushFileBuffers. An overwrite then uses MoveFileExW(MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH); no-clobber legacy migration omits MOVEFILE_REPLACE_EXISTING, so a concurrent destination wins without being overwritten. Neither path sets MOVEFILE_COPY_ALLOWED, so a cross-volume copy/delete fallback is impossible. The final target is reopened without following reparse points and must match the temporary object's identity and expected size. A pre-replace failure preserves the old file; after replacement, an observer sees complete old or complete new bytes, never a partial file.

Selection changes compare the expected monotonic bootstrap revision while holding the private sibling lock, preventing concurrent last-writer-wins authority changes. Historical db_location remains the legacy metadata index; managed selection is stored separately as managed_db_location. Selection writes atomically replace only the sidecar; initialization and import cutover never reserialize the legacy source. A newly created POSIX legacy parent is 0700; a newly created Windows legacy parent and every new or replaced Windows legacy TOML file receive the protected private DACL before any credential bytes are written. Existing legacy-only parent directories retain their compatibility boundary, and a fresh reset with neither source nor sidecar creates no artifact.

A platform without its complete profile fails before managed target, provider, or bootstrap effects instead of using a weaker fallback. An unavailable macOS system keyring fails the credential operation without changing binding authority. Legacy-only store, reset, and credential migration retain their historical compatibility behavior.

The managed secret service is separate from legacy account-name-based entries. Its opaque handles are intentionally not a diagnostic or user-facing contract. Every UI mutation over a selected catalog submits the exact catalog identity and bootstrap revision bound to the workspace snapshot that produced the displayed data, in addition to catalog/account revisions. A later status response cannot retarget that snapshot; a changed selection remounts and reloads the workspace. The service binds one catalog instance and rejects selection drift before its next access. Import additionally rechecks selection before each secret resolution and immediately before every account, credential, and policy write. Preview capability state is one-time, ten-minute, expired on access, and capped at 32 entries. Cleanup-required state is never treated as a missing binding by import.

Soft account removal is a tombstone plus bounded credential-cleanup operation. It retains stable operational identity, endpoint rows, and binding metadata, but marks every referenced value CLEANUP_REQUIRED before committing the tombstone and then attempts to delete up to 100 values. Successful deletions are finalized as superseded; unavailable storage or post-commit bookkeeping leaves conservative cleanup state for config doctor and config cleanup-credentials. A tombstoned account has no provider authority or public per-role credential-removal command, and its normalized name remains reserved permanently in this delivery.

Legacy import security

config import-legacy previews the effective legacy TOML/environment view. Preview parses environment account and policy precedence but does not resolve or expose plaintext, environment, or keyring credential values and performs no managed write. Environment password presence is detected by enumerating variable names without retrieving its value; role/base values are read only during confirmed apply. The plan exposes only non-secret endpoint/policy settings, credential source classes, and exact target revisions. CLI apply displays that plan before reading interactive confirmation; a no-change plan needs no confirmation but still executes credential proof and guarded finalization. For a changed UI plan, the user checks an explicit review box and the adapter submits the fixed IMPORT confirmation value; it cannot apply while the box is clear. UI confirmation is bound to a one-time preview token. The private preview is additionally bound to the selected catalog path and bootstrap revision. Confirmed apply preflights normalized-name collisions and account capacity, checks destination conflicts before resolving any secret, revalidates reviewed source identity and target revisions before each credential resolution/write, and installs credentials through the same save protocol as manual setup. Before an otherwise eligible cutover, an unchanged active role is privately resolved on both sides and must compare equal; mismatch overwrites neither side and blocks cutover. Its legacy value and managed account revision remain bound into finalization. Final automatic cutover holds the shared source/selection lock, rechecks the complete source snapshot and each proven or imported private credential value, then holds a SQLite writer fence while checking the final catalog revision and each imported account revision/enabled state. The bootstrap sidecar CAS completes before either fence is released. A failed save, any drift, unsupported provider, or cleanup attention leaves legacy runtime selected. Import never deletes, rewrites, or reformats TOML, environment, or legacy keyring state. Provider-style legacy accounts are reported unsupported and prevent automatic cutover rather than being silently lost.

Temporary oversized results

A large but otherwise valid get_emails_content result may exceed the inline MCP serialization ceiling. The private spill directory is allocated lazily only when that result variant is needed, so management commands and bounded inline reads neither create nor depend on temporary spill storage. The local server stores the canonical JSON in a randomly named process-private temporary directory and returns its exact local path, byte count, SHA-256 digest, media type, and lifetime notice. The directory and files are private; creation is exclusive and no-follow, and file type, identity, link count, mode or DACL, size, and digest inputs are checked around the write. POSIX uses owner-only modes. Windows uses a dedicated protected-DACL temporary container with a random per-process root on local fixed NTFS and handle-bound identity. Without the complete active platform profile, bounded inline results still work while a result that requires spill fails with a bounded error.

These artifacts contain private message content. They are not credentials, are never placed in SQLite or configuration, and are removed by identity on graceful shutdown. A killed process may leave a remnant. Each Windows root holds an owner-marker LockFileEx range for its lifetime; cleanup must first acquire that range non-blocking, so age never makes a live long-running process eligible. The next writer then performs bounded prefix/age/type/owner/DACL/link/identity validation and removes only a fully verified stale root; reparse or substituted entries are left untouched. No HTTP route, generic MCP file reader, directory listing, remote URL, or arbitrary path lookup is exposed by this feature. Only connect a local MCP client whose own filesystem tools may legitimately inspect paths returned by the server.

Indexed metadata privacy

The operational SQLite projection contains no message bodies, raw MIME, attachment bytes, passwords, tokens, or secret locators. It can contain account source fingerprints, mailbox names, UIDs, UIDVALIDITY, provider flags, and the message ID, subject, sender, recipients, and dates required by list_emails_metadata. Treat it as private email metadata even though it does not contain credentials.

Legacy source fingerprints are one-way hashes of non-secret account identity and incoming endpoint attributes. Secret values are excluded, and legacy endpoints are not copied into managed account rows. The database and SQLite sidecars use the same owner-only, anti-symlink checks as managed catalog storage. Existing files are checked for exact application schema ownership before WAL is enabled; an unrelated or unmarked database is rejected without changing its journal mode or creating WAL sidecars. Deleting the projection does not delete provider mail, but an untrusted copy can still reveal communication metadata.

Credential storage

Persistent legacy configuration is stored in ~/.config/mcp-email-server/config.toml by default. The credential_storage setting controls where passwords are written.

auto

auto is the default. The server performs a live usability check against the active operating system keyring backend:

  • macOS commonly uses Keychain.
  • Linux desktop environments commonly use Secret Service through GNOME Keyring or KWallet.
  • Other platforms use the backend selected by the Python keyring package.

If the keyring works, secrets are stored there. If no usable backend is detected, such as in many headless Linux sessions or containers, the server falls back to the TOML file and logs a warning. The usability result is cached for the life of the process, so restart after unlocking or repairing a backend that failed its first probe.

keyring

keyring requires a usable keyring. A failed keyring write is reported instead of falling back to plaintext.

Use this mode when storing credentials outside the operating system keyring is not acceptable:

credential_storage = "keyring"

plaintext

plaintext writes credentials directly into the TOML file and never uses the keyring for normal loads or saves:

credential_storage = "plaintext"

On POSIX systems, a new immediate configuration parent uses 0700 and the file is created atomically with owner-only 0600 permissions; existing legacy parent permissions are not silently changed. On non-POSIX systems, the application does not install an equivalent owner-restricted ACL. Protect the file using operating system or container controls.

Keyring representation

When keyring storage is active, the TOML file contains __KEYRING__ instead of the secret. The actual value is stored under:

service: mcp-email-server
entry: <account_name>:<incoming|outgoing|api_key>

__KEYRING__ is reserved and cannot be used as a real password.

Environment-provided secrets

credential_storage controls only credentials persisted by mcp-email-server. It does not move or protect a password supplied through an MCP client JSON file, process environment, CI configuration, or container metadata.

Prefer the secret injection facility provided by the MCP client, CI system, or container platform. If a literal secret must be stored in a client configuration, restrict that file to the account running the client and keep it out of version control and diagnostic output.

Neither MCP nor ordinary local-UI account forms read legacy environment secrets. Treat environment-composited accounts as runtime compatibility inputs and copy them only through the explicit, reviewed import flow; the environment value is read during confirmed apply, never during preview.

Credential migration

These commands migrate legacy TOML credentials only. They are rejected while managed mode is selected; managed credentials use account set-secret instead.

Move all credentials represented by the stored configuration into the keyring:

mcp-email-server migrate-credentials --to keyring

Move referenced keyring credentials back into the TOML file:

mcp-email-server migrate-credentials --to plaintext

Migration operates on the stored TOML file. It intentionally ignores environment-provided accounts, allowlists, boolean overrides, and the credential storage environment override while loading the source data.

If MCP_EMAIL_SERVER_CREDENTIAL_STORAGE is set to a different mode, the command warns because future server runs will continue to obey the environment value. Unset it or keep it synchronized with the intended storage mode.

A plaintext migration attempts to delete the keyring entries referenced by the original file. It reports entries that remain or whose removal cannot be verified.

Keyring limitations

Application-specific Keychain access

On macOS, Keychain access control can be associated with an executable. A fresh uvx resolution may run the server from a different path than the process that stored the secret. Keychain can then display a permission prompt or deny access. Choose the appropriate persistent permission when prompted, or use a stable installation path.

Backend trust

The auto usability check verifies that the active backend can store and read a probe value. It does not audit how a third-party keyring backend protects data. If custom backends are installed, verify that the selected backend meets the required security properties.

Non-transactional backends

Writing secrets to the keyring and replacing the TOML file are separate operations. A crash between them can leave an orphaned keyring entry or a configuration marker whose corresponding write did not complete. Migration reports cleanup failures, but backup and recovery remain the operator's responsibility.

Recipient allowlist

Sending is disabled when the allowed-recipient collection is empty. Enable and restrict send_email, forward_email, and save_to_mailbox by adding exact addresses:

allowed_recipients = [
  "[email protected]",
  "[email protected]",
]

Or use a comma-separated environment variable:

MCP_EMAIL_SERVER_ALLOWED_RECIPIENTS='[email protected],[email protected]'

Every To, CC, and BCC address must be allowed. Matching is case-insensitive and understands display-name forms such as Alice <[email protected]>.

list_allowed_recipients is always visible in the static MCP tool catalog. An empty result means sending is disabled; it never means unrestricted sending. The Web UI edits recipients as individual add/edit/remove items and states this empty behavior explicitly.

Sender allowlist

Restrict incoming messages by exact address or glob pattern:

allowed_senders = [
  "[email protected]",
  "*@company.example",
]

Or:

MCP_EMAIL_SERVER_ALLOWED_SENDERS='[email protected],*@company.example'

Matching is case-insensitive and applies to the single address parsed from the message's From header. Malformed, empty, or multi-address From headers fail closed when the allowlist is active.

The allowlist protects:

  • Metadata listing and pagination.
  • Body retrieval and optional read marking.
  • Attachment download.
  • The forward_email source read and SMTP handoff. A blocked source is indistinguishable from a missing message, and a sender policy tightened after the read is rechecked before SMTP, so a forward never reveals or delivers it.
  • Deletion and approved flag/read-state mutations.
  • Move and archive operations.

A blocked message's body and attachments are not fetched or marked as read. By default, blocked mutation IDs are returned as successful no-ops so the caller cannot distinguish a hidden message from a nonexistent one.

Set this option to report blocked IDs as failures instead:

report_blocked_mutations = true

This is more explicit but reveals that a blocked message exists. list_allowed_senders is always visible in the static MCP tool catalog and returns an empty list when unrestricted. Unlike recipients, an empty allowed-sender collection does not restrict reading. The Web UI edits sender patterns as individual add/edit/remove items.

The sender allowlist is local filtering, not sender authentication. A spoofed From header can match. Continue to rely on provider-side SPF, DKIM, DMARC, and spam controls.

Mutation replay safety

IMAP and SMTP connections can fail after a remote server has accepted an effect but before this process receives the result. Such targets are reported as unknown, not silently retried or rewritten as known failures. Repeating an unknown send, APPEND, MOVE, delete, or flag update can duplicate delivery or apply an effect twice; inspect the provider mailbox or delivery evidence first.

Scoped delete and the COPY/delete move fallback require UIDPLUS and use only UID EXPUNGE. They never use mailbox-wide EXPUNGE, which could commit another client's pending deletions. The generic set_email_flags tool rejects \Deleted, so it cannot bypass this scoped deletion boundary; it also rejects the server-controlled \Recent flag and provider-specific keywords.

Metadata projection invalidation is rebuildable: if it fails after a provider effect, the result keeps the provider evidence and adds a reconciliation warning instead of claiming rollback.

Attachment access

Attachment downloads are disabled by default because the tool writes data from email to the server's filesystem.

Enable the operation with:

enable_attachment_download = true

Or:

MCP_EMAIL_SERVER_ENABLE_ATTACHMENT_DOWNLOAD=true

Omit save_path to use the default application download area. The adapter resolves the current user's Downloads location, creates an mcp-email-server child, and uses a bounded sanitized attachment basename plus a cryptographically random suffix. On Windows, the adapter uses a recognized absolute Downloads Known Folder registry value and otherwise falls back to the profile's ~/Downloads; other platforms use ~/Downloads. Missing components are created through the secure traversal. A redirected Windows location is accepted when it remains on safe local fixed NTFS storage; unsupported or unsafe redirection fails closed. The application child, not the general Downloads directory, is the sensitive immediate parent and receives the platform private profile when created. The adapter never falls back to the process working directory or changes the Downloads parent's permissions.

An explicit save_path remains supported for compatibility. Prefer an absolute path so the target is unambiguous; relative explicit paths resolve against the server process's working directory and are never silently rewritten. The application fetches at most a 50 MiB raw message, accepts at most 25 MiB of decoded attachment bytes, and passes bytes rather than a path to the artifact writer. The writer operates only on the explicit or preflight-resolved target. Filesystem capability preflight occurs before credential resolution, provider construction, download, or MIME decoding.

On POSIX, the writer traverses parent components through pinned no-follow directory descriptors, rejects unsafe ownership or writable non-sticky parents, and validates an existing target as a single-link owner-only regular file. It writes a random 0600 sibling, fsyncs, atomically replaces through the pinned parent descriptor, and verifies final identity and size.

On Windows, the writer applies the local fixed NTFS boundary, rejects every reparse point and hard-linked/permissive target, creates a private same-directory sibling, flushes it, performs same-volume write-through replace, and verifies final volume/file identity and size. Broader permissions on held Downloads ancestors do not need to be removed; only the application child is the sensitive immediate parent. Pre-replace failure preserves an existing target. The next operation removes only old prefix-matching temporary files that still pass owner, DACL, type, link, age, and identity checks. No platform uses a weaker path-based fallback. An existing private regular file at the exact explicit or resolved path can be replaced.

These traversals prevent provider-controlled filenames and common symlink/junction/FIFO/device races from redirecting the write; they are not a sandbox around arbitrary paths a trusted MCP caller can request. Run the server with filesystem permissions that limit where it can write, and do not assume attachments are safe to open or execute.

The separate attachments parameter on send_email and save_to_mailbox reads local file paths. Relative paths are likewise resolved against the server process's working directory. Only connect clients that should be trusted to request access to files visible to that process.

TLS certificate verification

Keep verify_ssl = true for remote IMAP and SMTP services. Disabling verification permits interception and credential exposure if the network or endpoint is not fully trusted.

If both use_ssl and start_ssl are false, there is no TLS layer and verify_ssl has no effect. Credentials and message contents may cross the network in plaintext. Use that mode only for a trusted local bridge, an encrypted tunnel, or an isolated network; remote services should use implicit TLS or STARTTLS.

A trusted local bridge with a self-signed certificate can require:

[emails.incoming]
use_ssl = false
start_ssl = true
verify_ssl = false

Limit this exception to the specific local connection. See ProtonMail Bridge and self-signed TLS.

HTTP transport security

SSE and Streamable HTTP validate Host and Origin headers by default to reduce DNS rebinding risk. Network exposure still requires appropriate authentication, authorization, TLS termination, and firewall policy around the server.

See Transports for allowed host and origin settings.