fs-mail 2.3.0
=============

Email as ordinary files. No GUI, no web interface, no resident process required.

One invocation sends self-contained .txt drafts, pushes tracked local moves,
deletions and flags to IMAP, and downloads new messages for each selected
account. The single-account default is ~/email; named accounts have explicit
directories of your choosing. Each account has its own inbox/ and outbox/.
Other IMAP folders become directories. Each message carries its own readable
headers and body; there is no separate recipient index. Run it yourself, from
cron, from the supplied systemd timer, or with --watch.

Version 2.3 adds a practical command reference in -h/--help, an embedded manual,
separate behavior/account files, a complete configuration catalog, offline
configuration checks, redacted effective-configuration output, configurable
received headers, and validated settings for one invocation.
Each account retains its own root, messages, outbox, errors, and sync state.

Start with the command reference
--------------------------------

    fs-mail -h
    fs-mail --manual | less
    fs-mail --config-help | less

-h and --help show the same grouped options and common commands. --manual
prints this release's complete manual without needing a working configuration.
--config-help lists supported settings, defaults, and limits. These commands
do not connect to a server or create mail directories. docs/QUICKSTART.txt is
a short workflow guide; the manpage is the full reference.

Frequently used commands:

    fs-mail --check-config              Validate configuration locally
    fs-mail --show-config               Show effective settings as JSON
    fs-mail --list-accounts             Show account names and roots
    fs-mail -a work --status            Inspect one account locally
    fs-mail -n                         Validate queued drafts locally
    fs-mail -n --receive-only          Preview download policy offline
    fs-mail --receive-only             Download without remote changes
    fs-mail --sync-only                Apply IMAP changes and download
    fs-mail --send-only                Submit eligible outbox drafts
    fs-mail --reply ~/email/inbox/message.txt

The ordinary command, fs-mail, sends queued drafts and synchronizes IMAP for
all selected accounts. Its first download can be large: the default fetches
complete messages from all selectable folders, including their attachments.
Download filters and a per-run raw-message byte budget can restrict that work.
Selective folder/body/attachment download commands remain a design proposal;
see docs/DOWNLOAD-DESIGN.txt. They are not current command options.

Requirements and installation
-----------------------------

Requires Python 3.9 or newer with its standard library, including ssl and
sqlite3, on a Unix system with file locking and hard links. No pip packages are
needed. Linux is tested; the portable replacement path intended for OpenBSD
has been exercised on Linux, but this release has not run on native OpenBSD.
On ChromeOS, enable the Linux development environment and install inside it.
This is a Linux command, not a Chrome extension or an Android application.

Extract the source archive, enter its fs-mail directory, and run:

    tar -xzf fs-mail-2.3.0.tar.gz
    cd fs-mail
    sh install.sh

For a new configuration, run ~/.local/bin/fs-mail --init after installation.
Existing installations keep their current INI file and mail roots.

The installer builds a single executable at ~/.local/bin/fs-mail and installs
the fs-mail(1) manual under ~/.local/share/man/man1. Read it with:

    man -M "$HOME/.local/share/man" fs-mail

The executable also carries the complete plain-text manual:

    ~/.local/bin/fs-mail --manual | less

docs/fs-mail-manual.txt supplies the same manual as a separate text file.

For the short command below, add this directory to PATH, including in ~/.profile:

    export PATH="$HOME/.local/bin:$PATH"

Alternatively, use the full path. Installation starts no scheduler or connection.

ChromeOS lifetime and file access
--------------------------------

Files -> Linux files -> email opens the same ~/email seen in the Linux terminal.

Background work requires Linux to be running. Chromium documents that logout
stops its VMs/containers; login alone does not restart them. Opening Terminal,
a Linux application, or Linux files starts the environment. A Linux timer
cannot independently start a stopped VM, and ordinary polling pauses during
suspension. No fs-mail window needs to remain open; the program has none.

Official setup and lifecycle references:
https://support.google.com/chromebook/answer/9145439?hl=en
https://www.chromium.org/chromium-os/developer-library/guides/containers/containers-and-vms/

Configure one account
---------------------

--init creates mail directories and two configuration files:

    ~/.config/fs-mail/config.ini     Program behavior and shared defaults
    ~/.config/fs-mail/accounts.ini   Accounts, roots, senders, and endpoints

XDG_CONFIG_HOME changes the default configuration parent. -c/--config FILE
selects the behavior file; --accounts FILE selects the account file. The default
account file is accounts.ini beside the chosen config. --root DIR overrides one
selected account's mail root, including for --init. An override cannot overlap
another configured account's root.

Edit accounts.ini to set the root, from address, and both server sections. Edit
config.ini to customize the behavior. For example, config.ini can contain:

    [mail]
    timeout = 60
    max_message_mb = 50
    max_messages_per_run = 500
    poll_interval = 300
    text_encoding = auto
    legacy_encoding = cp1252
    filename_format = {date}_{sender}_{id}.txt
    date_format = %Y-%m-%d
    filename_timezone = utc
    download_folders = *
    download_exclude =
    max_download_mb_per_run = 0

The separate accounts.ini can contain:

    [account default]
    root = ~/email
    from = Your Name <you@example.net>

    [imap default]
    host = imap.example.net
    port = 993
    security = ssl
    username = you@example.net
    auth = password
    credential_file = ~/.config/fs-mail/imap.secret

    [smtp default]
    host = smtp.example.net
    port = 465
    security = ssl
    username = you@example.net
    auth = password
    credential_file = ~/.config/fs-mail/smtp.secret

With no account name supplied, --init creates the example account default
rooted at ~/email. To choose a name and root explicitly:

    fs-mail --init -a personal --root "$HOME/email"

That command is for new configuration paths; --init never overwrites an
existing file. If accounts.ini exists and config.ini is absent, built-in
behavior defaults apply. A legacy combined INI remains supported unchanged.

Validate and inspect it before the first connection:

    fs-mail --check-config
    fs-mail --show-config

These commands do not create mail roots, read passwords, execute credential
helpers, or connect to a server. --check-config verifies structure and setting
values, not authentication or server availability. Unknown settings and sections
are errors, so a misspelled option cannot silently fall back to a default.

Use your provider's endpoints. IMAP commonly uses port 993 with security = ssl,
or port 143 with security = starttls. SMTP submission commonly uses port 465
with security = ssl, or port 587 with security = starttls. Both modes verify
certificates and hostnames; they do not fall back to plaintext authentication.
The account must allow IMAP/SMTP. Use an app password if your provider requires it.

If a configured credential_file does not exist, fs-mail asks for the password
when the transport needs it. It also asks when no credential source is configured.
Input comes from the controlling terminal and is not echoed.
The prompt identifies the protocol, account, and server. The typed secret stays
in this process's memory; fs-mail does not save it in a file or configuration.
IMAP and SMTP have separate prompts. An empty outbox does not need an SMTP
password. Offline commands such as --status, --dry-run, and --reply do not ask.

Leave credential_file configured if you want to use a file whenever it exists.
Creating that private file enables unattended use; a missing file uses a prompt
in an interactive run. Typing a password does not create or update the file.
To override an existing file or another source for one invocation:

    fs-mail --ask-password --receive-only

--ask-password ignores the configured file, environment, or command source for
that invocation. It still asks only when a transport needs a credential.

An unattended run cannot answer a password prompt. For cron, a service, or a
detached watcher, configure one of these sources per transport:

    credential_file = ~/.config/fs-mail/imap.secret
    credential_env = FS_MAIL_IMAP_PASSWORD
    credential_command = /usr/bin/pass show mail/imap

The three lines are alternatives; do not enable more than one per endpoint.
--non-interactive explicitly disables credential prompts. A missing credential
then reports an error and leaves mail queued. Without a controlling terminal,
the default mode also fails clearly instead of reading a password from stdin.
Only a missing file receives the automatic prompt fallback. A present file with
unsafe permissions, a symlink, an empty or malformed secret, a failed command,
or an unset credential environment variable remains an error. A missing file
with --non-interactive or without a terminal also reports an error.

Optional secret files must be regular files owned by your user, without group
or other access or symlinks; chmod 600 FILE sets suitable permissions. Both
sections may reference the same file. credential_file paths may start with ~;
relative paths resolve beside accounts.ini, or beside the legacy combined
configuration that defines the endpoint. Files contain only the
password or token, optionally followed by a newline. Commands run without a
shell or interactive input and must print only the credential; pipes and
expansions are not processed. Keep secrets out of shell command history.
Credential-command output is capped at 64 KiB while it is read. A timed-out or
overproducing helper and its ordinary child processes are stopped.

auth = xoauth2 expects a current OAuth access token; credential_command can call
your token helper. With no source it prompts for an access token, not a password.
Provider login, refresh-token management, and OAuth client registration are not
bundled. A refresh token is not an access token.

Prompted credentials are reused for the same account, root, transport, and
endpoint within a running process, including successive --watch cycles. They
are not shared automatically between accounts, IMAP and SMTP, or servers.
Configured sources continue to be read when needed; a newly created secret file
takes precedence over a previously typed password. Restart the process to enter
a different prompted password. Successful watch reloads forget typed credentials
for removed accounts or changed endpoints; at most IMAP and SMTP credentials for
each selected account remain cached. A temporary invalid configuration does not
clear credentials for the last valid configuration.

State is bound to the IMAP host, port, and username once mail files or history
exist; it refuses a different account in that root. Add another named account and
another root instead of changing an existing account's username. An untouched
template can still be corrected after inspecting it with --status.
Unsent drafts count as account content; empty directories and errors.log alone
do not prevent correcting an otherwise unused template account.

If an IMAP connection reports a greeting such as:

    220 thatiam.vip ESMTP OpenSMTPD

the connection reached an SMTP service. Check the account's IMAP host, port,
and security in accounts.ini, or its legacy combined INI, against the server's
actual IMAP settings. Changing a password
does not fix a protocol mismatch. OpenSMTPD provides SMTP; an IMAP service must
also be configured on the server to make mailboxes available through IMAP.
The greeting alone does not establish that this host has an IMAP service.

Configure multiple accounts
---------------------------

Each account in accounts.ini has three matching sections: [account NAME],
[imap NAME], and [smtp NAME]. Specify root and from in every account section.
The account name
is a selector, not a directory naming rule; fs-mail uses the root you provide.
The [mail] section in config.ini supplies shared behavior and filename defaults.
An account section can override those defaults. For example, config.ini:

    [mail]
    poll_interval = 300
    filename_format = {date}_{sender}_{id}.txt
    date_format = %Y-%m-%d
    filename_timezone = utc

And accounts.ini:

    [account personal]
    root = ~/email/account1
    from = Your Name <you@example.net>

    [imap personal]
    host = imap.example.net
    port = 993
    security = ssl
    username = you@example.net
    credential_file = ~/.config/fs-mail/personal-imap.secret

    [smtp personal]
    host = smtp.example.net
    port = 465
    security = ssl
    username = you@example.net
    credential_file = ~/.config/fs-mail/personal-smtp.secret

    [account work]
    root = ~/otheremail
    from = Your Name <you@example.org>
    poll_interval = 600
    date_format = %Y%m%d-%H%M%S

    [imap work]
    host = imap.example.org
    port = 993
    security = ssl
    username = you@example.org
    credential_file = ~/.config/fs-mail/work-imap.secret

    [smtp work]
    host = smtp.example.org
    port = 587
    security = starttls
    username = you@example.org
    credential_file = ~/.config/fs-mail/work-smtp.secret

The split examples are examples/config.ini and examples/accounts.ini.
examples/multi-account.ini remains a supported combined-format example.
Replace the example account addresses and server settings before connecting.
Each named root must be an
absolute path, with ~ allowed for your home directory. Roots must be distinct
and must not contain one another. ~/email/account1 and ~/email/account2 are
valid siblings, as are ~/email and ~/otheremail. An account rooted at ~/email
cannot coexist with another rooted at ~/email/account2, because one account's
filesystem scan would enter the other. Symlink aliases are refused.

Account names are case sensitive, 1 through 64 ASCII letters, digits, dots,
underscores, or hyphens, starting with a letter or digit. Use [mail] in config.ini
for shared defaults; ConfigParser-style [DEFAULT] inheritance is not supported.

Existing combined INIs remain supported without migration. To split one while
preserving its original bytes and account bindings:

    fs-mail --migrate-config
    fs-mail --check-config

The command retains an exact private config.ini.pre-split backup, writes account
definitions to accounts.ini, and leaves behavior settings in config.ini. It
makes no network connection or mail-state changes. Unrelated existing backup or
destination files are not overwritten. Matching interrupted migration artifacts
can be resumed; an already split configuration is a no-op. --accounts FILE can
select another destination. Relative credential-file paths are adjusted when
necessary to keep referring to the same files.

The old [mail]/[imap]/[smtp] layout becomes the named account default. Add other
account sections with their own roots. Do not change the first account's login
to fetch a second account into the same root. Preserve its .fs-mail state.
An explicit --accounts file cannot be combined with a legacy account-bearing
--config file; migrate it first. With split configuration, accounts.ini uses
only named account/transport sections, and config.ini uses shared [mail].

Inspect or select accounts explicitly:

    fs-mail --list-accounts
    fs-mail --account personal --receive-only
    fs-mail --account work --status
    fs-mail --account personal --account work --sync-only

With no --account option, normal runs, --status, --dry-run, and --migrate-outbox
operate on all configured accounts in configuration order. --list-accounts is
local and does not create mail roots, open state databases, or ask for secrets.
Invalid configuration stops the run before account work begins. A connection
or operation failure in one valid account is reported while the others continue.
Each account writes errors.log in its own root.
Errors before account selection, such as malformed configuration, go to stderr.

Use --account NAME with --resolve or --resolve-sync when several accounts are
configured; those operations require one selected account. --reply FILE selects
the account containing FILE, or accepts an explicit --account. Replies go into
that account's drafts/ and use that account's outgoing sender. A received-file
move between accounts is not an IMAP transfer between accounts.
Use an explicit --account to reply from a separately saved text message outside
the configured roots.

To create a new configuration containing one named account, with an explicit
root, choose a new configuration path:

    fs-mail --config ~/.config/fs-mail/second/config.ini \
      --init --account personal --root ~/secondemail

--init refuses to overwrite existing files; edit accounts.ini
to add accounts. It never derives a root from the account name or username.

Settings for one invocation
---------------------------

Use --set KEY=VALUE to change behavior without rewriting the INI file. Repeat it
for several values. A value applies to every selected account; use -a/--account
to limit the selection. -c is the short form of --config. For example:

    fs-mail -a work --receive-only --set max_messages_per_run=10
    fs-mail -a work --sync-only --set timeout=120
    fs-mail -n --set max_message_mb=10
    fs-mail -a work --show-config --set date_format=%Y%m%d

Quote values containing spaces, shell metacharacters, or filename templates:

    fs-mail --receive-only --set 'filename_format={sender}_{date}_{id}.txt'

The optional mail. prefix is accepted, for example --set mail.timeout=120.
When a key is repeated, the last value wins. Supported keys are timeout,
max_message_mb, max_messages_per_run, poll_interval, text_encoding,
legacy_encoding, filename_format, date_format, filename_timezone, view_headers,
download_folders, download_exclude, and max_download_mb_per_run.
--config-help describes each key's type, default, and accepted values.

Precedence, from lowest to highest:

    built-in defaults
    shared [mail] settings
    settings in [account NAME]
    --set overrides for selected accounts

--watch reapplies its command-line overrides after each configuration reload.
An explicit --watch SECONDS determines the polling interval for that run.
All configuration sections are validated, including unselected accounts.
An override does not excuse an invalid INI file or an overlapping account root.

Account identity and credential settings stay in the INI. --set does not accept
root, from, server endpoints, usernames, or credentials. The existing --root DIR
option separately overrides one selected account's root, with the usual identity
and overlap checks. Separate -c FILE configurations remain useful for different
profiles or schedules.

--show-config emits JSON for the effective selected accounts after overrides.
It identifies saved credential sources without reading their values. Entire
credential commands are redacted because their arguments may contain secrets.
The output still contains account addresses, hosts, and paths; it is an
inspection record, not a replacement INI file.

For example, keep a scheduled work-account pass small without changing an
interactive session's settings:

    fs-mail -a work --non-interactive --quiet --set max_messages_per_run=20

The message-count limit is not a total byte budget. A lower max_message_mb
skips complete large messages, including their body; it does not selectively
omit their attachments.

Receive and read
----------------

    fs-mail --sync-only
    ls ~/email/inbox/
    less ~/email/inbox/*.txt

New message filenames use the date, sender address, and a stable numeric ID:

    2026-10-24_nameATdomain.tld_2525253525235235.txt

The subject remains inside the message. Message-derived filename components use
portable ASCII; message text is UTF-8, preserving other languages. See below.

The readable view includes selected decoded headers and the message body. Plain
text is preferred when a message supplies both text and HTML. HTML-only mail is
converted to text with useful link destinations. No scripts run, remote images
are not fetched, and attachments are not opened automatically.

Directories and files:

    ~/email/inbox/*.txt           Inbox messages
    ~/email/Projects/             Example remote IMAP folder
    ~/email/Projects/Customers/   Example nested remote folder
    ~/email/drafts/               Local work in progress; never automatically sent
    ~/email/outbox/*.txt          Outgoing messages, including their own headers
    ~/email/sent-local/           Readable copies of accepted outgoing messages
    ~/email/attachments/          Extracted and archived attachments
    ~/email/errors.log           Timestamped errors, also printed on stderr
    ~/email/.fs-mail/             Private raw mail, queue snapshots, and state

The server's INBOX maps to inbox/. Folder hierarchy follows the server's
delimiter. Unsafe names, reserved names, and collisions are escaped; a remote
folder named outbox cannot become the outgoing queue. The exact mapping is in
~/email/.fs-mail/folders.json. By default, received text records the corresponding
raw-message path and the absolute paths of extracted attachments.

Choose the visible received headers
----------------------------------

Use view_headers in config.ini's [mail] section, in an accounts.ini account
section, or through --set. For example:

    view_headers = To,From,Subject,Date,Attachments

This produces a compact received view, followed by the required Local-ID and
Account-ID lines. To must be first and Subject must be present. Accepted names
are case insensitive. The full available list is:

    To, Cc, Bcc, Attachments, Subject, Flags, From, Date, Message-ID,
    Reply-To, In-Reply-To, References, Raw-Message, Local-ID, Account-ID

Fields appear in the chosen order when their source values exist; To, Cc, Bcc,
Attachments, and Subject can appear empty when selected. Local-ID and Account-ID
cannot be removed; they are appended if not explicitly placed in the list.
They keep file moves and renames connected to the correct account and message.

The setting changes new received views. It does not rewrite existing mail.
Keep identity metadata already present in existing files intact. Omit Flags
if you do not want a visible flag editor; add a Flags line to a message when
you want to change its read/answered/flagged/draft state. Hidden flags remain in
the private synchronization state. Opening a file is not a read-state change.

Raw-Message identifies the preserved original; Attachments lists extracted
files. Hiding either field does not stop archival or attachment downloads.
The manual's RECEIVED HEADERS section explains every field. A one-run example:

    fs-mail --receive-only --set 'view_headers=To,From,Subject,Date,Attachments'

Attachment downloads and bandwidth
----------------------------------

This release fetches complete messages, including embedded attachments. By
default every selectable IMAP folder is eligible, so the first synchronization
can transfer a large existing archive. Saved state prevents repeating completed
body downloads. The defaults are 500 message attempts per run, 50 MiB per
message, and no total raw-message byte budget.

Restrict incoming work for one run with actual supported settings:

    fs-mail --receive-only --set download_folders=INBOX \
        --set max_download_mb_per_run=25

Or set a persistent policy in [mail] or one [account NAME] section:

    download_folders = INBOX, Archive
    download_exclude = Trash, Junk
    max_download_mb_per_run = 25

download_folders is a comma-separated list of patterns for remote IMAP folder
names; its default * includes all folders. download_exclude defaults to empty,
and matching exclusions win. Patterns use shell-style *, ?, and character
classes; * can match the server's hierarchy delimiter. Names are case sensitive
except the special INBOX name. Quote a CSV field containing a comma:

    download_folders = INBOX, "Projects, active"

These filters govern new message-body downloads. They neither remove existing
local mail nor disable reconciliation of already tracked messages. Use
--receive-only when the run must not push local changes to IMAP or send mail.

max_download_mb_per_run limits incoming raw-message payload bytes, per account
per run; 0 means unlimited. Protocol metadata and transport overhead are outside
this payload budget. Each message is still retrieved whole, including attached
data. See the manual's DOWNLOAD POLICY section for limit and deferral details.
Messages that do not fit the remaining budget stay pending for a later run.
A message larger than the whole budget reports an error until the budget is
increased or set to 0. Failed fetches retain their reserved bytes for that run.

Preview the effective policy without connecting, creating roots, or opening
mail state:

    fs-mail -n --receive-only --set download_folders=INBOX \
        --set max_download_mb_per_run=25

This prints the policy; it cannot list new server messages while offline.

Headers-only views and on-demand MIME-part fetching are not implemented. The
proposed --download-folder and --download-attachments commands are unavailable.
Suppressing extraction alone would not save bandwidth because the attachments
are already inside the downloaded raw message. Lowering max_message_mb skips
whole large messages, including their readable body.

An incoming attachment is stored as an ordinary file under:

    ACCOUNT_ROOT/attachments/LOCAL_ID/safe-filename.ext

When visible, the message's Attachments: line lists every extracted file by
absolute path.
The stable directory uses the message's Local-ID, independent of the readable
message's filename and folder. Moving or renaming a .txt within the same account
leaves these attachment paths valid. Quoting protects commas in account paths.
Moving an attachment separately or relocating the whole account does not rewrite
absolute paths in saved message files; update those paths when relocating data.
Names use portable ASCII, retain useful extensions, and receive suffixes when
two MIME parts produce the same filename. Original names remain in the raw mail.

Attachments are decoded from their MIME transfer encoding. Inline binary parts
and attached .eml messages are retained as files too. Nothing is opened or
executed automatically, and linked remote images are not downloaded. Deleting
or changing an extracted file affects only that local copy; it does not edit the
server message. Removing a readable message does not purge its attachment or
raw-message archive. Completed downloads do not recreate manually deleted
attachments on each run; the original .eml or a backup remains the recovery copy.

Extraction is restartable. Matching files from an interrupted run are reused;
changed files and symlinks cause an error rather than being overwritten. The
message is recorded complete only after its raw mail, attachments, and readable
view have been durably published. The default Raw-Message field names the
preserved raw file; hiding this field does not remove the archive.

Incoming metadata allows up to 4 MiB for long complete attachment path lists.
Outgoing drafts retain their stricter limits: 64 KiB of headers, 16,384 characters
per header line, and 100 attachments. Forwarding an unusually large received list
requires selecting the wanted files or splitting the submission. --reply starts
without attachments; it does not automatically send someone else's files back.

Message filenames and dates
---------------------------

Set these options in [mail], or override them in [account NAME]:

    filename_format = {date}_{sender}_{id}.txt
    date_format = %Y-%m-%d
    filename_timezone = utc

filename_timezone is utc or local. date_format uses strftime-style directives:
%Y is a four-digit year, %m a two-digit month, %d a two-digit day, %H a 24-hour
hour, %M minutes, and %S seconds. Use %% for a literal percent sign. Percent
signs are written once in this INI file; no interpolation is performed.

Available filename fields:

    {date}            Message Date formatted with date_format and timezone.
    {sender}          First From address with @ replaced by AT.
    {sender_address}  First From address, retaining a filename-safe @.
    {sender_name}     Sender display name, or the address local part.
    {subject}         Decoded Subject, converted to a safe filename component.
    {id}              Stable numeric form of the message's Local-ID.
    {local_id}        The original hexadecimal Local-ID.
    {uid}             IMAP UID in the source mailbox.
    {uidvalidity}     Source mailbox UIDVALIDITY.
    {account}         The configured account name.
    {timestamp}       Message Date as UTC Unix seconds.

For example, keep your proposed form, include the subject, or put the sender
first without changing message identity:

    filename_format = {date}_{sender}_{id}.txt
    filename_format = {date}_{subject}_{id}.txt
    filename_format = {sender}_{date}_{id}.txt
    date_format = %Y%m%d-%H%M%S

These are alternatives. A filename template describes one .txt basename, not
a directory path or shell command. Unsafe header characters are sanitized;
unsupported fields and malformed format strings are errors. A collision never
overwrites another file: a stable identity suffix is added when needed.
The format does not have to contain {id}, although including it makes names
predictable when several messages have the same sender and date.

A missing or invalid Date produces unknown-date for {date} and {timestamp};
a Date without a timezone is interpreted as UTC. Header components are limited
to 80 characters. The complete filename is limited to 240 bytes; truncation
retains a stable numeric identity suffix. Slashes in date output become dashes.
Format fields do not accept attribute access, indexing, conversions, or format
specifiers. See fs-mail(1) for the complete accepted date-directive list.

The same filename and date settings apply to new sent-local archives. For sent
mail, {date} and {timestamp} come from the submission's Date header, {sender}
comes from its outgoing From address, and {id} uses the stable outgoing job's
numeric identity ({local_id} is its hexadecimal job ID). IMAP-only {uid} and
{uidvalidity} become no-uid and no-uidvalidity in a sent archive.

Changes affect new downloads and new sent-local archive paths. Existing names,
local edits, pending download paths, and already recorded sent archive paths
remain intact. Rename an existing received file with your file manager if
desired; its Local-ID and private baseline still identify the remote message.
Outbox and draft filenames remain user chosen, and error-log timestamps remain
UTC.

Retained received sources use a recovery name, retained-source-JOBID.txt, in
sent-local/. This recovery name preserves the source separately from the
ordinary sent receipt and is not controlled by filename_format.

The working tree and synchronization
-----------------------------------

The private state records the last synchronized baseline: stable Local-ID,
server mailbox/UIDVALIDITY/UID, local path, flags, and content hashes. It also
retains content-addressed snapshots and a journal of remote operations. SQLite
stores this synchronization state.

New received files also carry Account-ID, derived from the bound IMAP endpoint
and username. Keep Account-ID, Local-ID, and Raw-Message intact. A foreign
account's received file in a mailbox directory is held instead of being matched
to a coincident Local-ID. Removing or changing previously recorded Account-ID
or Raw-Message metadata is held.
Legacy tracked files also check their saved Raw-Message reference.
When a tracked file disappears, an identifiable copy or move found in another
account root in the same configuration holds the source deletion as well.
Known account roots are considered even when that account was not selected.
An unreadable or incomplete peer scan holds otherwise missing originals.
The check skips other accounts' private .fs-mail areas and does not perform
an IMAP transfer. Moves to unknown or unconfigured directories cannot be found
by this guard and can still request deletion from the source account; preserve
the original or use an explicit outgoing message when making a separate copy
for another account.

Each normal or --sync-only run compares the baseline, current local files and
current server state. Within one account, the local operations are:

    rename a file in its folder   Change the local filename only.
    move it to another folder    Move the corresponding IMAP message.
    delete the file              Remove that exact IMAP UID on the next sync.
    edit Flags:                  Change read, answered, flagged or draft state.
    edit the body/other text     Keep a local annotation; do not rewrite MIME.

New directories are created on IMAP when needed as a tracked message's move
destination. Empty directories alone are not synchronized. Moving all messages
out of a directory does not delete the server mailbox. Copies with duplicate
Local-ID values are held, rather than interpreted as an upload or deletion.
Files without a tracked identity are never automatically uploaded to IMAP.

The default received view has an editable Flags field. Examples:

    Flags:
    Flags: seen
    Flags: seen, answered, flagged

An empty field means no editable flags are set (including unread). Removing
seen marks the message unread. Other supported system names are answered,
flagged and draft. User keywords can also be listed. Quoted keywords distinguish
a literal keyword named "seen" from the system Seen flag. Recent and Deleted
are not editable fields; use the filesystem deletion operation.
A new view can omit Flags through view_headers; add the field to make an
explicit change. Removing an existing tracked Flags line is held as a conflict.

Opening a file is not a reliable read signal because previewers, indexers and
backup tools also open files. Change Flags explicitly; file access times are
not interpreted as read status. Downloads use BODY.PEEK[] and do not mark read.

Keep Local-ID unchanged. Moving a tracked message into drafts, outbox,
sent-local or attachments is held as an unsupported destination; these are
local-only directories.
An outgoing file with valid headers still requests SMTP submission. A durable
guard keeps sending a moved received file from being mistaken for a request
to delete its original IMAP message. Prefer --reply or a copy for composition,
leaving the received file in its mailbox directory.

When a received file from one account is sent through another account's outbox,
accepted cleanup checks whether its original is still visible. If that cannot
be proved, the source is retained in the sending account's sent-local/ as
retained-source-JOBID.txt, with a collision suffix if needed. This preserves
its received identity separately from the ordinary sent receipt, so the source
account's peer check can still find it after outbox cleanup. Restore that
retained source to its originating account's mailbox folder to repair an
accidental move. A verified copy whose original remains in place uses normal
cleanup. If the originating account's required local lock is busy or cannot
be obtained, the accepted job waits for later local cleanup; it is not
submitted to SMTP again.

An identifiable move between configured account roots is held as described
above. Other moves outside the mail root look like deletion from the working
tree, so the next sync can request remote deletion. Preserve the whole root for
backups rather than moving tracked files outside it as an archival convention.

An unreadable directory, malformed tracked identity, duplicate identity or
incomplete scan blocks inferred deletion. Old files absent before the first
baseline are not retrospectively deleted on the server during an upgrade.
Server-deleted messages retain their local text as an untracked archive; a
concurrent local edit produces a conflict. A server move can appear as a
deletion in one folder and a new download in another because ordinary IMAP
does not provide a universal cross-folder identity.

Flag writes require CONDSTORE with usable modification sequences. Moves require
MOVE and UIDPLUS (or IMAP4rev2). Deletion requires UIDPLUS (or IMAP4rev2) and
uses UID EXPUNGE for that one UID. The program never issues global EXPUNGE or
CLOSE. It verifies UIDVALIDITY before using a saved UID. Missing capabilities
produce an error and preserve the local operation for review.

Conflicts and interrupted operations are held. Inspect the offline diff and
the recorded errors, then choose explicitly:

    fs-mail --status
    tail -n 30 ~/email/errors.log
    fs-mail --resolve-sync LOCAL_ID --prefer local
    fs-mail --resolve-sync LOCAL_ID --prefer remote

The resolver records a choice; the next normal or --sync-only run checks the
server before acting. An uncertain MOVE is not blindly retried. Choosing
remote for an uncertain MOVE retains a detached local archive and refreshes
the server outcome; it does not claim that the move succeeded. Capability,
identity, or unsupported-destination errors still need their cause corrected.

Deletion leaves raw mail, attachments and baseline snapshots available locally.
The client does not append sent SMTP mail to the server's Sent folder: its
archive is sent-local. A provider may save a separate copy.

Compose an outgoing file
------------------------

Create a text file such as letter.txt with this format:

    To: Alex <alex@example.net>, sam@example.org
    Cc:
    Bcc: private-copy@example.net
    Attachments: "/home/YOUR_LINUX_USER/Documents/Report, final.pdf"
    Subject: Project notes

    Hello,

    The report is attached.

The first line must be To: with at least one recipient. Cc:, Bcc:, and
Attachments: may be empty, omitted, or reordered after To:. Attachment: is
accepted as a singular alias. A blank line ends the headers and begins the
body. Canonical files use To, Cc, Bcc, Attachments, then Subject and other
metadata. Subject is required and must not be empty. A missing or empty Subject
is an error; there is no filename fallback.

Recipient fields contain comma-separated email addresses. Display names and
international domain names are supported; address local parts must be ASCII.
Bcc is used for the SMTP envelope and omitted from the transmitted headers.

Attachment paths must be absolute paths to regular files. Spaces are permitted;
quote a whole path containing a comma with CSV-style double quotes. Example:

    Attachments: /home/me/Documents/notes.txt, "/home/me/Report, final.pdf"

Within a quoted path, represent a literal double quote with two double quotes.
Attachment contents are snapshotted for submission; the original files stay
where they were. Symlinks and special files are rejected. There is no shell
expansion, globbing, or command execution in this header.

Optional Reply-To:, In-Reply-To:, and References: headers are honored. Received
From:, Date:, Message-ID:, Local-ID:, Account-ID:, Flags:, Raw-Message:, and
Smtp-Status: metadata may be retained for reference. Outbound From, Date, and
Message-ID are generated for the new submission, using the selected account's
configured from address for its sender. Unknown or malformed
headers and duplicate headers are errors. Plain body-only files are not drafts.

Compose in ~/email/drafts/, then move the completed file into ~/email/outbox/.
Only regular, non-hidden files with a .txt suffix are considered, regardless of
suffix case. A ready file is a submission request; the next scheduled run can
send it. A non-.txt temporary name is also suitable for staging before a final
rename, avoiding half-written editor saves in the live queue.

    fs-mail --dry-run
    fs-mail --send-only

--dry-run validates the queued files, addressing, attachments, and encoded size
locally. It does not test connectivity or send anything. --status lists queued
files, the local IMAP diff, and prepared or held jobs, also without networking.
Both commands report errors in errors.log, with UTC timestamps. All normal
run errors and holds are logged there too. --quiet does not suppress errors.

After acceptance, fs-mail archives the message and moves its submitted source
out of outbox. Original source bytes are retained with the private send snapshot.
If the queued file changed during submission, the changed file is preserved and
held for review so a background run cannot silently submit the revised version.

Read, reply, forward, and edit
-----------------------------

Received and sent files work with normal file-manager and editor operations.
A copy is a starting draft, not an automatic reply: its To still names the old
recipients. For replies, choose the original Reply-To or From for To, review
Cc/Bcc and attachments, and write the body. In-Reply-To and References can carry
threading information from the original Message-ID. Forwards need their new
recipients and the text/attachments you intend to include. Review before queuing.

For an automatic reply draft:

    fs-mail --reply ~/email/inbox/origmsg.txt

It chooses Reply-To, or From when Reply-To is absent; prepends Re: to the
subject when needed; sets threading headers; quotes the original text; and
opens a new file in drafts. Cc, Bcc and Attachments start empty. Local-ID and
Flags are not copied. The command never sends or moves the draft into outbox.

For this account's tracked received mail, fields hidden by view_headers can
be recovered from the verified private raw archive for reply addressing and
threading. Explicit fields in the readable file, including empty values, take
precedence. The quote uses the readable body's current local text. This is
offline and does not follow an arbitrary Raw-Message path from a text file.
Separately exported messages still need usable visible sender headers.

The editor is taken from EDITOR, then VISUAL, then vi or nano found on PATH. Editor
arguments are parsed without a shell. This works in ChromeOS's Linux terminal
and on OpenBSD; it does not assume that either system already exports EDITOR.
For example:

    export EDITOR='vi'
    export EDITOR='emacs -nw'

Use one setting and put it in your shell's startup file if desired. If editor
lookup or execution fails, the newly created draft remains in drafts and its
path is reported. Normal scheduled runs never launch an editor.

text_encoding = auto handles UTF-8, BOM-marked UTF-16/32, and recognizable
BOM-less UTF-16/32 To headers. It then tries legacy_encoding strictly, reporting
that fallback; the default cp1252 suits common Windows text. Other known code
pages need an explicit text_encoding or fallback setting. This is not universal
charset detection. Undecodable data is rejected without replacement decoding.
New received files and readable sent copies are UTF-8.

Upgrade from version 1.0 or 1.1
------------------------------

Back up the root, pause scheduled runs, and install 2.3. Tracked inbox files move
from the root into inbox with a recovery journal; renamed/deleted files are left
alone. Existing bytes are preserved, without automatically reformatting old mail.

The old outbox/index.json workflow and --index-outbox have been removed. A
nonempty legacy index blocks sending instead of being silently ignored. Convert
it explicitly, preserving backups of the old index and source files:

    fs-mail --migrate-outbox
    fs-mail --dry-run

Migration creates self-contained drafts and retires the index without sending.
Files that were not in the old index are left untouched. Once migration is
complete, every valid top-level .txt message in outbox is eligible under the
new rules; keep unfinished messages and text attachment assets in drafts or
another directory before restarting scheduled sends.
Review its reports and resulting files before resuming the scheduler. Preserve
.fs-mail throughout the upgrade.

Version 2 adds remote mutation to normal runs. The first synchronization
baselines present old files and imports their current server flags. It does
not infer remote deletions from files that disappeared before this baseline.
Subsequent deletions, moves and Flags edits are pending synchronization actions.

The default command performs both directions:

    fs-mail

It sends eligible .txt drafts, reconciles tracked IMAP changes, downloads new
mail and baselines the new files. A failure in one direction does not prevent
an attempt in the other. --sync-only omits SMTP. --receive-only performs only
read-only downloads; --send-only performs only SMTP. --quiet suppresses routine
progress; errors still go to stderr and errors.log. Read-only downloads also
record a baseline, without changing the server, so later local edits can be
synchronized by a normal or --sync-only run.

Background operation
--------------------

--watch is a foreground polling loop. Run it in a terminal to enter passwords
privately when needed; the process retains them in memory for later cycles.
For a supervisor or nohup, configure unattended credential sources and use
--non-interactive:

    mkdir -p ~/.local/state/fs-mail
    nohup "$HOME/.local/bin/fs-mail" --non-interactive --watch 300 \
      >> "$HOME/.local/state/fs-mail/watch.log" 2>&1 < /dev/null &

--watch without a number uses each account's poll_interval; --watch SECONDS
overrides the interval for all selected accounts. Configuration reloads while
watching at least once a minute. Each interval starts when that account's
previous pass completes. Account runs are sequential, so a slow connection can
delay another account, but an account failure does not cancel the others. Ctrl-C or SIGTERM
stops the process. Redirected log files are ordinary files; rotate or remove
them according to your logging policy.

The supplied service uses --non-interactive. Configure credential sources that
are available to its environment, then run these from the extracted folder:

    mkdir -p ~/.config/systemd/user
    cp packaging/fs-mail.service packaging/fs-mail.timer ~/.config/systemd/user/
    systemctl --user daemon-reload
    systemctl --user enable --now fs-mail.timer
    systemctl --user list-timers fs-mail.timer
    journalctl --user -u fs-mail.service

The timer runs 60 seconds after the user manager starts, then five minutes after
each run finishes. You may use loginctl enable-linger "$USER" to retain the
Linux user manager independently of terminal sessions. This does not override
the ChromeOS VM lifecycle. To disable scheduled runs:

    systemctl --user disable --now fs-mail.timer

Cron is another option. Ensure the cron service is installed and running inside
Linux, create the log directory shown above, and add this line with crontab -e:

    */5 * * * * "$HOME/.local/bin/fs-mail" --non-interactive --quiet >> "$HOME/.local/state/fs-mail/cron.log" 2>&1

Use one scheduling method. fs-mail also locks each mail root so overlapping
invocations cannot submit the same queue concurrently. External editors do not
honor that lock. Stage drafts outside the live queue; pause scheduled runs and
wait for the current run to finish before editing already queued messages.

Acceptance, failures, and held jobs
----------------------------------

SMTP acceptance means the server accepted responsibility for delivery. It does
not mean the recipient has received or read the message. If any recipient is
refused during the envelope stage, fs-mail resets that transaction and sends no
body to anyone in that transaction. Fix the address or retry the retained draft.

A connection can fail after the server received the body but before its reply
arrives. Exactly-once delivery cannot be guaranteed by ordinary SMTP. fs-mail
records the attempt durably and holds uncertain jobs instead of automatically
resending them. Check --status, then verify the outcome with the provider or
recipient before resolving the full job ID:

    fs-mail --resolve JOB_ID --result sent
    fs-mail --resolve JOB_ID --result retry

With several accounts configured, add --account NAME to identify the account
whose job is being resolved. Job IDs are not searched across account roots.

sent confirms an uncertain submission and completes local archiving/cleanup.
retry allows another attempt on a subsequent normal or --send-only run; it does
not immediately send. A retry may duplicate mail if the uncertain attempt was
actually accepted. Stable Message-IDs help identification, not guaranteed
server-side deduplication. If the draft changed, an authorized retry uses its
current file contents after validation.

A conflict means the original was accepted but its source file changed during
sending. The edited headers and body are preserved. Review them, then use
--resolve JOB_ID --result retry to release that edited draft. The accepted
original is not retransmitted as part of resolving the conflict.

Holds follow observed renames and exact copies inside outbox, including copies
encountered later in the same run. Confirming sent clears the matching copies;
edited versions stay held. An explicit retry chooses one live version and moves
identical extra copies to drafts/recovered-JOB_ID. Different edited versions
must first be separated so the intended retry is unambiguous. A copy edited
before the program ever observes it cannot reliably be recognized as the same
message; resolve the earlier delivery state before creating a new submission.

If acceptance was recorded but local cleanup failed, a later run retries local
cleanup without submitting again. Password retrieval is deferred until an
actual SMTP connection is needed, so this cleanup can work offline.

Limits, state, and backup
------------------------

Defaults are 50 MiB per message and 500 message attempts per direction per run.
Outgoing limits include MIME encoding overhead. max_message_mb can be set from
1 to 512; max_messages_per_run from 1 to 100000. Oversize messages stay on the
server. Raise the limit to retry them. New folders or a large archive may require
several runs. Network operations have a configurable per-operation timeout.
Retry batches rotate so a broken old message cannot permanently block new mail.
A connection or authentication failure stops further outgoing connection
attempts for that run; queued drafts remain available for a later run.

Readable conversion is bounded: 64 MiB MIME input, 512 MIME parts, nesting depth
32, 256 KiB of headers per MIME part, and about 2 million body characters. Text
parts are decoded within a 4 MiB bound. Over-complex or malformed content produces
a notice or truncated view while retaining the original message. Messages above
the 64 MiB conversion limit can be archived as raw mail with a readable notice
when the configured download limit permits them. Cryptographic verification and
decryption are not implemented; the original remains available to other tools.

Back up each whole mail root, including hidden .fs-mail, plus your configuration
and credentials. State prevents repeated downloads and duplicate submissions;
do not delete it as a cleanup technique. Do not restore an old database alone
over a newer queue. Reserve .fs-mail for the program, and keep the root on the
Linux filesystem for its locking, permissions, and durable-write behavior.

When replacing a readable file's Flags, Linux uses an atomic exchange and keeps
both the observed and displaced inode in private recovery storage. Systems
without that operation use a durable rename/link journal; an interrupted
replacement is recovered and held, never interpreted as a user deletion.
Inspect the recorded paths and use --resolve-sync for an explicit resolution.

Exit status and tests
---------------------

    0   Operation completed without a reported failure or held job.
    1   A send/download failed, a job is held, or local inspection found errors.
    2   Invalid arguments, configuration, or local setup/access error.
    3   The only failures were mail roots locked by another fs-mail process.
    130 Interrupted by Ctrl-C, or SIGTERM while watching or prompting.
    141 The output pipe was closed by its reader.

For several accounts, a failed account makes the combined invocation fail even
if other accounts completed. Configuration is validated before work begins;
runtime failures are reported per account while remaining accounts continue.

--watch reports individual run failures and continues polling. A busy watch
cycle is skipped rather than terminating the watcher.

From the source directory, run:

    python3 -m unittest discover -s tests -v

Tests use temporary files and simulated recipients, without external mail
connections. TLS wire tests need OpenSSL and loopback sockets; they skip explicitly
if unavailable. Provider authentication and your ChromeOS runtime still need a
local smoke test.

The repeatable larger workloads and recorded results are documented in
docs/STRESS-REPORT.txt. Calendar-length watch tests use a simulated clock;
filesystem/restart tests use actual temporary files and SQLite databases.

See docs/PROTOCOL-NOTES.txt for the protocol rationale and primary references.

License and provenance
----------------------

fs-mail is distributed under the Insha-Allah License Agreement (IALA). The
complete agreement is included unchanged in LICENSE. Read that file for its
terms. The canonical agreement is published at:

https://remfan1994.github.io/insha-allah-license-agreement

The installer copies LICENSE and PROVENANCE.txt to
PREFIX/share/licenses/fs-mail/, where PREFIX defaults to ~/.local. For example:

    less ~/.local/share/licenses/fs-mail/LICENSE

sh install.sh --prefix DIR chooses another installation prefix. Both files are
also included in the executable's zip archive.

PROVENANCE.txt records the source of the license, the implementation's
provenance, and its dependencies. Standard command-line conventions and mail
protocols define the interfaces; the implementation is maintained in this
project. Protocol references are collected in docs/PROTOCOL-NOTES.txt.
