Korgalore Changelog
===================

v0.7.0 (2026-10-06)
-------------------

Enhancements
~~~~~~~~~~~~

* An ``M:``, ``R:`` or ``L:`` line in MAINTAINERS that cannot be parsed is
  now reported in the log instead of being skipped silently

* GUI: Add "About" dialog showing version, description, website, and license

* Digest deliveries (beta): ``mode = 'digest'`` sends one
  summary email per day or week instead of every message. See the new
  "Digests" page in the docs

  - The digest lists new threads and threads with new replies, with
    patch counts, review trailers, a lore link, and the ``kgl yank`` and
    ``kgl track add`` commands to grab each thread
  - Plain text and HTML parts; the HTML loads nothing remote
  - ``schedule``, ``send_at``, ``send_day``, ``send_empty`` and
    ``digest_from`` control when it is sent and how it looks
  - ``digest_format = 'plain'`` or ``'html'`` sends only one part
    instead of both
  - ``kgl pull`` and the GUI send due digests; the new ``kgl digest``
    command sends them on their own, and ``--force`` sends one right now
  - A big digest is split into numbered parts (``[DIGEST 1/3]``) that
    reply to the first one, so mail clients don't cut it off
  - A digest that the target rejects is retried with the same messages;
    parts that were already delivered are not sent again
  - Lore feeds with a digest keep their history back to the last digest
    (up to 30 days), so weekly digests and digests after a vacation don't
    lose messages; past that, the digest says that some are missing

* Summarized digests (experimental): ``summarizer = 'NAME'`` adds a
  short summary of each thread, written by a language model you choose

  - ``[summarizers]`` entries: ``type = 'openai'`` for any
    OpenAI-compatible server (Ollama, llama.cpp, vLLM, LM Studio, most
    hosted services), or ``type = 'command'`` for a program such as
    ``llm`` or ``claude -p``
  - Facts (counts, versions, trailers) never come from the model;
    summaries are marked machine-generated and the model is named in the
    ``X-Korgalore-Digest-Model`` header
  - Quotes are removed and patches become short notes before a thread
    is sent; long threads are cut to ``max_input_chars``
  - Summaries are saved for 30 days, so the next digest sends only the
    new messages of a thread together with its earlier summary
  - ``max_summaries`` limits the new summaries per digest, busiest
    threads first
  - A summarizer that is down does not stop the digest: threads say
    "Summary unavailable.", and after 3 failures in a row korgalore
    stops calling it until the next run
  - A digest worker summarizes and sends in the background, so
    ``kgl pull`` is never held up; ``[digests] worker = 'external'``
    with ``kgl digest --work`` runs it from your own timer
  - ``summary_instructions`` adds your own request to the summarizer's
    prompt, such as "tell me if anyone sounds upset". Changing it gets
    new summaries, and two digests of one feed can ask for different
    things
  - ``kgl digest --estimate`` shows what the next digest would send to
    its summarizer, without calling it
  - Lei feeds, which can hold private mail, are refused with a
    summarizer that is not on your machine unless it sets
    ``allow_private_feeds = true``

* ``kgl pull --fail-on-feed-error``: exit with status 3 if any feed failed
  to update

  - Pull still does all its work: every other feed and every delivery
    runs, and the failed feeds are listed again at the end
  - Only feed updates count; a failed delivery does not change the exit
    status
  - Without the flag, pull keeps exiting 0 when a feed has a hiccup

* HTTP failover and auto-probe for all lore.kernel.org requests

  - All HTTP operations to public-inbox servers now go through liblore's
    origin failover, automatically retrying on connection errors, timeouts,
    and 5xx responses using configured mirror origins
  - Mirror origins and auto-probe settings are read from git config
    (``lore.fallback``, ``lore.autoprobe``, ``lore.probetimeout``,
    ``lore.probettl``), shared with b4 and other liblore-based tools
  - Auto-probe benchmarks all configured origins concurrently and reorders
    them fastest-first, with results cached to disk
  - LoreNode instances are cached per canonical origin (scheme://host) and
    shared across feeds, yank, and track commands within a single CLI run
  - LoreFeed manifest fetches, yank message/thread fetches, and track
    subject lookups all benefit from failover
  - Git clone and fetch operations use the probed-fastest mirror via
    per-invocation ``url.<mirror>/.insteadOf`` git config (``-c`` flag)
  - Known limitation: lei operations (``track`` thread searches) do not
    yet benefit from mirror failover, as lei has its own HTTP stack with
    no mirror configuration support. An RFC proposing ``url.insteadOf``-style
    rewriting for lei has been sent to public-inbox:
    https://public-inbox.org/meta/20260408-nyala-of-heavenly-imagination-eb0a1d@lemur/

* New ``dummy`` target type that delivers nothing, for setups that only
  want lei's own archive as mail storage

* Long lei commands log a progress note every so often, so a big import
  no longer looks like a hang

* Archives created by lei get a ``description`` file, so public-inbox
  lists them by the MAINTAINERS entry (or the thread subject for
  tracked threads) instead of "description missing"

* ``lore.useragentplus`` git config replaces ``main.user_agent_plus``

  - The tracking UUID is now read from git config instead of korgalore's
    TOML configuration, sharing the setting with b4 and other tools
  - Applied to git HTTP, lei, and LoreNode user-agent strings
  - Not applied to non-lore HTTP (JMAP, MAINTAINERS fetch) to avoid
    leaking the tracking identifier to unrelated services

Refactoring
~~~~~~~~~~~

* Use liblore library for shared public-inbox operations

  - Message parsing, email policy, and mbox splitting now use
    ``liblore.utils`` instead of local implementations
  - Message-ID extraction from URLs uses ``liblore.utils.get_msgid_from_url``
  - HTTP message and thread fetching uses ``liblore.LoreNode``
  - LoreFeed manifest fetching uses ``LoreNode.request()`` for failover
  - Mbox splitting uses pure Python via liblore instead of shelling out
    to ``git mailsplit``
  - Exception hierarchy bridged via multiple inheritance so
    ``liblore.RemoteError`` catches errors from both libraries
  - Removed ``set_user_agent_id()`` in favor of reading
    ``lore.useragentplus`` via ``LoreNode.user_agent_plus`` property
  - Trace header folding uses ``liblore.utils.wrap_header`` instead of a
    local 40-line word-wrapper
  - Subject extraction goes through ``liblore.utils.msg_get_subject`` at
    all nine call sites, which returns a plain ``str`` and collapses
    whitespace runs so state files and log lines stay on one line.
    Rebase recovery cleans the stored subject as well, so state written
    by earlier versions still matches exactly

* Manifest fetching lives in a single helper

  - ``LoreFeed.get_manifest()`` and ``LoreFeed.validate_public_inbox_url()``
    each had their own copy of the fetch, gunzip, and parse logic
  - Both now call one ``_fetch_manifest()`` helper, so error reporting is
    the same whichever way the manifest is fetched
  - ``get_manifest()`` now raises ``RemoteError`` when the response is not
    valid gzipped JSON (it used to let ``gzip.BadGzipFile`` escape, which
    nothing caught) and when the manifest is empty (it used to return an
    empty dict, leaving callers to fail further along). ``RemoteError``
    also feeds the existing retry logic

* Requires liblore >= 0.9.0 (was >= 0.4.0)

* Requires google-api-python-client >= 2.34.0 (was >= 2.0.0)

  - Releases before 2.34.0 import ``pkg_resources`` at module scope, which
    fails on any environment without setuptools installed -- the default for
    modern virtual environments -- making every Gmail target import fail
  - The old bound was never exercised, since dependency resolution always
    picked a much newer release

* Runs on stock Debian stable and EL10 again: ``click >= 8.1.7`` (was
  >= 8.3.0) and ``click-log >= 0.3.2`` (was >= 0.4.0)

  - Progress bars are hidden by handing click a throwaway output stream
    rather than by passing ``progressbar(hidden=...)``, which only exists
    in click 8.3.0 and later. Every earlier release already suppresses the
    bar when its output is not a terminal, so this needs no version check
  - The new floors are what Debian trixie (click 8.1.8, click-log 0.3.2)
    and AlmaLinux 10 (click 8.1.7) package, so korgalore now installs on
    both without pulling click from PyPI

Bug Fixes
~~~~~~~~~

* Subsystem tracking queries are sent to lei on stdin

  - Current public-inbox quotes a ``lei q`` argument that contains
    spaces, so a query such as ``l:linux-doc.vger.kernel.org AND
    d:30.days.ago..`` became a single phrase that matched nothing.
    lei still exited 0, so tracking looked fine but pulled no mail
  - ``lei q --stdin`` uses the query as written, and ``lei up`` replays
    it the same way. This needs public-inbox 1.8.0 or later

* A busy feed no longer causes a traceback

  - When the timer's ``kgl pull`` holds the feed locks, a command typed
    by hand now logs which feed is busy and exits cleanly
  - Locking all feeds is all or nothing: when one is busy, the feeds
    that were already locked are released again instead of staying
    locked until the process exits

* Saving a delivery from epoch ``0.git`` no longer fails with a
  ``GitError`` after the feed rolls over to ``1.git``. An explicit epoch
  0 was treated as "no epoch" and replaced by the newest one

* Docs: the systemd service example writes its log to
  ``~/.local/share/korgalore`` (it used ``~/.share/korgalore``), and the
  GUI's "Network unavailable" status is documented

* A comment on a MAINTAINERS ``L:`` line, such as ``(moderated for
  non-subscribers)``, is no longer passed to lei as search terms. Every
  list carrying one was reduced to the few threads that happened to
  contain those words instead of the whole list. ``main.catchall_lists``
  is now matched against the bare address too, so an entry written with
  the comment no longer matches.

* A Message-ID with a trailing comment no longer defeats duplicate
  detection

  - ``RawMessage.message_id`` returned the whole header value, so a
    Message-ID written as ``<id@host> (raw)`` -- Gnus does this -- made
    the IMAP and JMAP duplicate lookups search for a string no stored
    message carries, redelivering the message on every run
  - Extraction now goes through ``liblore.utils.get_clean_msgid``, which
    takes the ID from between the angle brackets. The brackets are put
    back, so the property's contract is unchanged
  - A Message-ID with no angle brackets at all is still returned as
    written, since the duplicate lookups match the header as stored

* GUI: losing the network mid-sync no longer leaves the sync grinding

  - ``run_sync()`` checks network availability before it starts, but
    nothing interrupted a sync already in flight, so it kept retrying
    every configured mirror origin against a network that was gone,
    waiting out the read timeout on each one while the tray already
    read "Network unavailable"
  - The network-changed callback now calls ``LoreNode.cancel_active()``
    on the cached nodes, which closes each node-owned session so a
    thread blocked in a socket read raises at once. Unlike the
    ``shutdown()`` used on quit, this is not terminal: operations
    started after the network returns open a fresh session
  - A down/up network flap during the cancellation no longer discards
    the 10-second resync that the network-restored path schedules,
    which used to strand the user for a full sync interval right after
    reconnecting
  - Note that ``git clone``/``git fetch`` run as subprocesses and are
    unaffected; this cancels the HTTP side only

* ``yank --thread`` no longer delivers one copy per archived list

  - A thread cross-posted to several lists is archived once per list, so
    yanking it delivered the same message repeatedly
  - Messages are now deduplicated by Message-ID via
    ``liblore.utils.split_and_dedupe_as_bytes``. Where copies differ, the
    one from the source least likely to have modified the message is kept,
    using liblore's default List-Id preference order
  - Thread order is preserved

* Fix ``UnboundLocalError`` when a public-inbox commit has an empty message
  blob

  - ``save_delivery_info()`` only bound its message variable inside a
    conditional, so a commit carrying an ``m`` object with no content
    crashed instead of being skipped

* Fix epoch comparison in ``feed_updated()`` using the wrong type

  - The loop reused the function's ``epoch`` parameter as its variable while
    iterating over state keys, passing a string to ``get_top_commit()``,
    which expects an integer

* Fix ``IndexError`` when a command defaults ``--target`` with no targets
  configured

  - The fallback indexed the first configured target without checking that
    any existed; it now reports the problem and aborts cleanly
  - The default-target logic was duplicated across ``yank``, ``track add``,
    ``subscribe add`` and ``track-subsystem``, and is now shared

Development
~~~~~~~~~~~

* Add ``ci.sh``, running ruff format, ruff check, ty, mypy, pyright and
  pytest as separately numbered gates

  - The tree is now clean under all three type checkers and ``ruff format``
  - Fixing the reported problems is what surfaced the three bugs above

* Add ``ci-matrix.sh``, sweeping Python 3.11 through 3.14 plus a lane that
  installs the project at its declared minimum dependency versions

  - The floor lane is what caught the ``google-api-python-client`` bound

* Add ``distro-matrix.sh``, running the suite inside Fedora, AlmaLinux,
  Debian stable and Arch containers with dependencies taken from each
  distro's own packages

  - Covers versions that sit *below* our declared floors, which no
    uv-resolved lane can see
  - Finding the click floor described below is what it was built for

* Move the optional GTK imports into ``gtk_compat``, so the GUI code no
  longer reads as attribute access on ``None`` to type checkers

* Add an advisory prerelease Python lane to ``ci-matrix.sh``. It
  reports whether the next CPython works, but never fails the run

* Enable more ruff rule sets (including pyupgrade and pathlib) and fix
  what they found; the code no longer uses ``os.path``

* Consolidate and parametrize the test suite, and drop tests that
  exercised no production code

* Let ``caplog`` see korgalore's log records on the pytest 7 and 8 that
  distros ship, not only on pytest 9

* Regenerate ``requirements.txt`` and ``requirements-gui.txt``. Both
  still pinned a liblore older than the 0.9.0 that korgalore requires

* Remove ``requirements-dev.txt``; development dependencies live in
  ``pyproject.toml`` under ``[dependency-groups]``

v0.6.1 (2026-06-23)
-------------------

Bug Fixes
~~~~~~~~~

* Fix thread tracking not delivering new messages after a few days

  - Thread tracking used a ``msgid:<msgid>`` query combined with
    ``--threads``, which interacted badly with how ``lei update`` limits
    queries to ~2 days before the last retrieved message
  - Once a thread kept receiving messages past that window, the date filter
    caused the query to return nothing and delivery silently stopped

* Fall back to ``--depth=1`` when a shallow fetch fails on stale repos

  - When a feed's epoch repository received no commits within the
    ``--shallow-since=1.week.ago`` window, git aborted the fetch with
    "error processing shallow info: 4" (exit 128) and skipped the feed
  - ``update_feed()`` now mirrors ``clone_epoch()`` and retries with
    ``--depth=1`` so the fetch self-heals instead of surfacing a cryptic
    error

* Fix noop retry rewind causing infinite re-delivery of messages

  - The noop (rm/purge) retry path failed to forward the ``was_failing``
    flag, so retried entries were never removed from the failed list
  - ``mark_successful_delivery`` also rewound the "last" commit pointer for
    retried commits, causing all subsequent commits to be re-delivered on
    every pull cycle (hundreds of duplicate messages per sync)

* Fix crash on a bad-object commit during failed delivery retry

  - When a commit in the failed delivery list was no longer available
    locally (missing packfile), ``is_noop_commit()`` mistook it for a noop
    and the downstream ``save_delivery_info()`` crashed
  - ``is_noop_commit()`` now verifies the commit object exists first and
    raises ``GitError`` for genuinely missing commits, which is caught and
    recorded as a failed delivery instead of crashing the retry loop

v0.6.0 (2026-03-05)
-------------------

New Features
~~~~~~~~~~~~

* ``track-subsystem --list`` (``-L``) flag to display all tracked subsystems

  - Shows the subsystem name, configuration file path, and delivery details
    (target and labels) for each tracked subsystem
  - Subsystem name is read from the ``[subsystem]`` section in each conf.d
    configuration file
  - Falls back to deriving the display name from the filename for legacy
    configurations without a ``[subsystem]`` section

* ``[subsystem]`` metadata section in generated conf.d configuration files

  - Stores the human-readable subsystem name (e.g., ``name = 'SELINUX
    SECURITY MODULE'``)
  - Used by ``--list`` for display and by ``--forget`` for log messages

* ``kgl subscribe`` command group for managing mailing list subscriptions

  - ``subscribe add URL`` validates and subscribes to a public-inbox feed
    (lore.kernel.org or any public-inbox server) or a local lei search
  - ``subscribe list`` shows all active and paused subscriptions
  - ``subscribe pause/resume`` toggles delivery without losing feed data
  - ``subscribe resume --skip`` discards messages received while paused
  - ``subscribe stop`` removes the subscription; ``--delete`` also removes
    feed data
  - The ``add`` subcommand is the default, so ``kgl subscribe URL`` works
    as a shorthand for ``kgl subscribe add URL``
  - Subscriptions are stored as individual ``conf.d/sub-{feed_key}.toml``
    files, keeping them separate from the main configuration
  - Duplicate detection checks both conf.d files and the main configuration

Enhancements
~~~~~~~~~~~~

* ``--target`` / ``-t`` now defaults to the first configured target

  - Applies to ``yank``, ``track add``, ``subscribe add``, and
    ``track-subsystem``
  - Previously, commands with multiple configured targets required an
    explicit ``-t`` flag or they would abort
  - Matches the existing GNOME applet behaviour, which already
    pre-selects the first target

Bug Fixes
~~~~~~~~~

* Fix non-lore feed URLs creating invalid directory paths

  - ``normalize_feed_key()`` returned raw URLs (e.g.,
    ``https://someothersite.com/foo``) as-is, resulting in a ``https:``
    directory under the data path
  - URLs are now sanitised by stripping the scheme, replacing special
    characters with hyphens, and falling back to a hash-based name for
    very long URLs

* Fix ``track-subsystem`` crash when a lei query matches no messages

  - ``get_first_commit()`` now detects empty repositories and returns an
    empty string instead of raising a ``GitError``

* Cache ``is_empty_repo()`` check to avoid redundant git subprocesses

  - Result is cached per epoch and cleared on ``feed_unlock()``

* Skip public-inbox commits that carry no message file

  - Public-inbox v2 repositories can contain commits without an ``m``
    (message) blob: ``rm`` commits record message removals (they store
    the deleted message in a ``d`` file instead), and ``purged`` commits
    record content scrubbing via ``replace_oids()``
  - Previously, attempting to extract a message from these commits raised
    ``StateError``, which was caught as a delivery failure and retried
    indefinitely
  - Both types are now detected by checking for the absence of the
    ``m`` object in the commit tree and treated as successful no-ops

* Handle remote errors gracefully during feed updates

  - When a remote server returns a transient error (e.g. 503 Service
    Unavailable), ``kgl pull`` now logs a one-line warning instead of
    dumping a full Python traceback
  - A failure on one feed no longer aborts updates to the remaining feeds

* Fix new commit discovery failing on shallow clones

  - The ``--ancestry-path`` flag in ``git rev-list`` requires an unbroken
    parent chain between endpoints, which cannot be established in shallow
    clones where parent links are severed at graft boundaries
  - Since public-inbox v2 epoch repositories have linear history,
    ``--ancestry-path`` provides no filtering benefit and is now removed

* Fix crash in legacy migration when git directory has no epoch repos

  - If a previous clone was interrupted, the ``git/`` directory could exist
    without any epoch subdirectories, causing ``_perform_legacy_migration()``
    to raise an unhandled ``PublicInboxError``
  - The migration now returns early when no epochs are found, allowing the
    normal feed initialisation path to proceed

* Initialise delivery state after first feed clone

  - When a new feed was cloned on the first ``kgl pull``, delivery state
    was not set up until the next run, wasting one full pull cycle
  - ``update_all_feeds()`` now returns both updated and initialized feed
    lists, and ``perform_pull()`` immediately creates delivery state for
    newly cloned feeds

* Fix crash in ``kgl pull`` after ``kgl track add``

  - ``map_tracked_threads()`` stored a 3-tuple ``(feed, target, labels)``
    but ``retry_all_failed_deliveries()`` and ``perform_pull()`` expected a
    4-tuple ``(feed, target, labels, subfolder)``
  - Tracked thread deliveries now include ``subfolder=None`` to match the
    format used by regular deliveries

* Include stderr and exit code in git error messages

  - ``run_git_command()`` only returned stdout, but git writes errors to
    stderr, resulting in empty error messages (e.g., "Git clone failed:")
  - The function now returns stderr alongside stdout, and all error
    messages include the exit code

* Fix shallow clone failure for dormant lists

  - Cloning with ``--shallow-since=1.week.ago`` fails for lists with no
    recent messages, because git cannot establish a shallow boundary when
    no commits fall in the time window
  - The clone now falls back to ``--depth=1`` when the time-based shallow
    clone fails

* Improve GUI startup error reporting and debug logging

  - Missing GTK dependency error is now shown before stdout/stderr are
    redirected to ``/dev/null``, so users see it without ``-v DEBUG``
  - Detect pipx environments and show ``pipx install "korgalore[gui]"``
    instead of the pip command
  - Running with ``-v DEBUG`` keeps stdout/stderr connected and preserves
    the log level for easier troubleshooting

* GUI: automatically detect and reload configuration files changed on disk

  - Before each sync cycle, the GUI now checks mtimes of the main config
    file and all ``conf.d/*.toml`` files
  - If any file has been modified (e.g., by ``kgl track-subsystem`` or
    manual editing), the configuration is validated and reloaded before
    syncing
  - Invalid configuration changes are logged and skipped, keeping the
    previous working configuration
  - Prevents crashes caused by stale in-memory configuration after
    external config changes

v0.5 (2026-01-28)
-----------------

New Features
~~~~~~~~~~~~

* JMAP and IMAP message deduplication by Message-ID

  - Before importing a message, targets now check if a message with the same
    Message-ID already exists in the target folder
  - Prevents duplicate messages when the same email arrives via multiple
    mailing lists or deliveries
  - Uses JMAP ``Email/query`` with header filter for JMAP targets
  - Uses IMAP ``SEARCH HEADER Message-ID`` for IMAP targets
  - Gmail already deduplicates automatically via its API

* New ``RawMessage`` class for unified message handling

  - Shared message wrapper used by all targets (JMAP, IMAP, Gmail, Maildir, Pipe)
  - Lazy parsing with cached properties (``message_id``, ``parsed``)
  - ``as_bytes()`` method for consistent message transformation before delivery
  - Centralised CRLF line ending normalisation

* X-Korgalore-Trace header injection

  - Messages now include provenance information when delivered to targets
  - Header format: ``X-Korgalore-Trace: from feed=<feed> for delivery=<delivery>;
    v<version>; <RFC 2822 date>``
  - Helps identify message origin when the same email arrives via multiple paths
  - Header is inserted at the end of the header section before delivery

* Subfolder support for IMAP and Maildir targets

  - New ``subfolder`` parameter at the delivery level allows routing messages to
    a target folder/directory without creating separate targets for each folder
  - For IMAP: combines base folder with subfolder (e.g., ``INBOX`` + ``Lists/LKML``
    becomes ``INBOX/Lists/LKML``)
  - For Maildir: creates maildir subdirectories under the base path
  - Subfolder maildirs are cached for efficient reuse within a session
  - JMAP, Gmail, and Pipe targets ignore subfolder (use ``labels`` for JMAP/Gmail)
  - Maildir subfolders support strftime format codes (e.g., ``%Y/%m``) for
    date-based directory organisation; templates are validated at startup and
    refreshed before each sync (ensuring correct folder for long-running GUI)

  Example configuration::

    [deliveries.lkml]
    feed = 'lkml'
    target = 'imap-server'
    subfolder = 'Lists/LKML'

    # Maildir with date-based archiving
    [deliveries.archive]
    feed = 'lkml'
    target = 'local-maildir'
    subfolder = 'Archive/%Y/%m'  # e.g., Archive/2026/01

* New ``main.user_agent_plus`` configuration option to append a unique identifier
  to the User-Agent string sent to remote servers. This helps server operators
  identify traffic from specific korgalore installations and may be used to
  prioritize requests. *(Replaced in v0.7 by ``lore.useragentplus`` git config.)*

* New ``main.catchall_lists`` configuration option to customize which mailing
  lists are excluded from ``track-subsystem`` queries. Default excludes
  ``linux-kernel@vger.kernel.org`` and ``patches@lists.linux.dev``.

Improvements
~~~~~~~~~~~~

* ``track-subsystem`` no longer requires ``-m/--maintainers``. The command now
  automatically looks for ``./MAINTAINERS`` in the current directory, and falls
  back to fetching from kernel.org if not found. The fetched file is cached for
  24 hours.

* ``track-subsystem`` now excludes catch-all mailing lists (linux-kernel,
  patches@lists.linux.dev) from queries by default to avoid flooding results
  with irrelevant messages. Override via ``main.catchall_lists`` in config.

* Centralized User-Agent handling across all remote connections (git, lei, HTTP).
  All requests now consistently identify as korgalore with the current version.

* Git availability is now checked at startup with a friendly error message if
  git is not installed or fails to run.

* GUI now redirects stdout/stderr to /dev/null and logs only critical messages
  to the systemd journal. This prevents I/O errors when the terminal is closed
  after launching with ``kgl gui &``.

Bug Fixes
~~~~~~~~~

* Maildir target now creates parent directories if they don't exist. Python's
  ``mailbox.Maildir`` only creates the maildir structure (cur/new/tmp), not
  parent directories, causing failures when using paths like ``~/Mail/rfl``
  where ``~/Mail`` exists but ``rfl`` does not.

v0.4 (2026-01-20)
-----------------

New Features
~~~~~~~~~~~~

* IMAP OAuth2 Authentication for Microsoft 365

  - Support for modern OAuth2 authentication (XOAUTH2) with Microsoft 365
  - Uses PKCE authorization flow for secure desktop application authentication
  - Built-in Azure AD application ID for zero-configuration setup
  - Custom client_id support for organizations that block third-party apps
  - Automatic token refresh with secure local storage
  - Full GUI support for re-authentication when tokens expire

  Example configuration::

    [targets.office365]
    type = 'imap'
    auth_type = 'oauth2'
    server = 'outlook.office365.com'
    username = 'user@company.com'

Improvements
~~~~~~~~~~~~

* Close network connections after sync runs to avoid keeping idle connections
  open between periodic syncs (IMAP, HTTP sessions)

* JMAP target now uses shared requests session for consistent User-Agent
  header and proper connection cleanup

* Add GUI optional dependencies to pyproject.toml (``pip install korgalore[gui]``)

* Support AyatanaAppIndicator3 as fallback for AppIndicator3 (needed on
  Debian and derivatives)

* GUI now monitors network availability via Gio.NetworkMonitor and skips
  sync attempts when network is unavailable (airplane mode, resume from
  suspend, etc.). Sync is automatically scheduled when network is restored.

* GUI error messages now display actual error text instead of unhelpful
  "see logs" messages, since logs are not collected by default.

* Add hashed requirements file for GUI dependencies (requirements-gui.txt)

v0.3 (2026-01-15)
-----------------

New Features
~~~~~~~~~~~~

* GNOME Taskbar Application (``kgl gui``)

  - System tray status indicator with automatic background syncing
  - Configurable sync interval via ``[gui] sync_interval`` config option
  - Real-time status updates showing current feed/delivery being processed
  - "Sync Now" menu item for manual sync triggering
  - "Yank..." dialog to fetch messages by message-id or URL without terminal
  - "Edit Config..." menu item to edit configuration in preferred editor
  - "Edit Bozofilter..." menu item to manage blocked senders
  - Automatic TOML validation after config edits with live reload on success
  - Gmail re-authentication support: detects expired tokens and shows
    "Authenticate..." menu item to re-auth via browser without terminal
  - Deduplicates message count by Message-ID for accurate reporting
  - Desktop entry file (korgalore.desktop) for application menu integration

* Thread Tracking (``kgl track``)

  - Track specific email threads for ongoing updates without full list subscription
  - Subcommands: add, list, stop, pause, resume
  - Uses lei to create persistent searches that monitor threads
  - Tracked threads automatically updated during regular ``pull`` operations
  - Threads auto-expire after 30 days of inactivity (can be resumed)

* Subsystem Tracking (``kgl track-subsystem``)

  - Track Linux kernel subsystems by parsing MAINTAINERS file
  - Automatically creates lei queries for mailing list traffic and patches
  - Supports substring matching for subsystem names (case-insensitive)
  - ``--threads`` option to include entire threads when any message matches
  - ``--since`` option to control query date range (default: 7 days)
  - ``--forget`` flag to remove tracking and clean up lei queries
  - Configuration stored in conf.d/ for easy management

* Modular Configuration (conf.d)

  - Additional config files automatically loaded from ``~/.config/korgalore/conf.d/*.toml``
  - Files merged alphabetically into main configuration
  - Useful for separating auto-generated configs from manual configuration

* Pipe Target

  - New target type for delivering messages to external commands
  - Labels passed as command-line arguments to the pipe command
  - Useful for custom processing pipelines

* TOML Validation

  - ``edit-config`` command now validates syntax after editor closes
  - Reports errors with line numbers for easy debugging

* Bozofilter (``kgl bozofilter``)

  - Block messages from unwanted senders
  - Blocked messages are silently skipped during delivery
  - Simple text file format (one address per line, supports comments)
  - CLI options: ``--add``, ``--edit``, ``--list``, ``--reason``
  - GUI integration: "Edit Bozofilter..." menu item

Improvements
~~~~~~~~~~~~

* Gmail token handling: gracefully handle expired/revoked tokens instead of
  crashing, with clear error messages guiding re-authentication

* Suppress googleapiclient discovery cache warning by disabling file_cache

* Defer feed initialization messages until after progressbar completes for
  cleaner output

* Add comprehensive test suite with 300+ tests

* Optimize delivery lookup algorithms for better performance

* Labels can now be specified as comma-separated strings (e.g., ``-l INBOX,UNREAD``)
  in addition to repeated options (``-l INBOX -l UNREAD``)

Bug Fixes
~~~~~~~~~

* Fix crash when initializing a new feed for the first time

* Fix feed_lock failing when feed directory does not exist

* Drop dependency on python-dateutil (use stdlib datetime instead)

* Fix git commands failing with ``safe.bareRepository=explicit`` by using
  ``--git-dir`` instead of ``-C``

Documentation
~~~~~~~~~~~~~

* Add documentation for thread tracking feature

* Add comprehensive GUI documentation covering installation, usage,
  configuration, and systemd service setup

* Add documentation for subsystem tracking and conf.d modular configuration

* Add manpage (``man/kgl.1``) for distribution packages

* Add shell completion scripts for bash, zsh, and fish (``completions/``)

v0.2
----

Initial public release with support for:

* Gmail, JMAP, IMAP, and Maildir targets
* Lore.kernel.org and lei feed sources
* Basic pull and yank commands
