summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-02 17:21:47 +0200
committerDanilo M. <danix@danix.xyz>2026-08-02 17:21:47 +0200
commitb61ce428d81871fbbc6261f274f20785db9d1c38 (patch)
treeb1ebcf1da244a31ec1c991be42d0062d776ba1b4 /src
parentdb1eb37b26f9ac14be4e1a4e5edbf5b6e77f8db7 (diff)
downloadqtmaildir-b61ce428d81871fbbc6261f274f20785db9d1c38.tar.gz
qtmaildir-b61ce428d81871fbbc6261f274f20785db9d1c38.zip
feat: add Config with account, query, and sync parsing
Accounts use [account.work] rather than [account/work]: QSettings' INI backend treats "/" as its own hierarchical group separator, so a literal slash in a section header parses as a nested group and trips QSettings::FormatError, silently breaking childGroups() enumeration. A dot carries no such meaning and keeps the format flat. Saved-query order is alphabetical (QSettings::childKeys() sorts), not file order; documented in code and tests rather than left to a false assumption.
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt1
-rw-r--r--src/config.cpp89
-rw-r--r--src/config.h62
3 files changed, 152 insertions, 0 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index aeb425f..d1ed29f 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,5 +1,6 @@
add_library(qtmaildir_lib STATIC
keymap.cpp
+ config.cpp
)
target_include_directories(qtmaildir_lib
diff --git a/src/config.cpp b/src/config.cpp
new file mode 100644
index 0000000..b12130d
--- /dev/null
+++ b/src/config.cpp
@@ -0,0 +1,89 @@
+#include "config.h"
+
+#include <QFileInfo>
+#include <QSettings>
+#include <QStandardPaths>
+
+QString Account::scopedQuery(const QString &query) const
+{
+ const QString prefix = QStringLiteral("path:\"%1/**\"").arg(maildir);
+ if (query.trimmed().isEmpty())
+ return prefix;
+ return QStringLiteral("%1 and (%2)").arg(prefix, query);
+}
+
+QString Config::defaultPath()
+{
+ const QString base =
+ QStandardPaths::writableLocation(QStandardPaths::ConfigLocation);
+ return base + QStringLiteral("/qtmaildir/qtmaildir.conf");
+}
+
+void Config::load(const QString &path)
+{
+ QSettings settings(path, QSettings::IniFormat);
+
+ m_notmuchConfig =
+ settings.value(QStringLiteral("general/notmuch_config")).toString();
+
+ m_syncCommand = settings.value(QStringLiteral("sync/command")).toString();
+ if (m_syncCommand.isEmpty()) {
+ m_warnings.append(QStringLiteral(
+ "No sync command configured ([sync] command); syncing is disabled."));
+ } else if (!QFileInfo::exists(m_syncCommand.split(QLatin1Char(' ')).first())) {
+ m_warnings.append(
+ QStringLiteral("Sync command '%1' does not exist; syncing is disabled.")
+ .arg(m_syncCommand));
+ m_syncCommand.clear();
+ }
+
+ // Account groups are written as [account.work], [account.personal], etc.
+ // A dot, not a slash, separates the "account" namespace from the key:
+ // QSettings' INI backend treats "/" as its own hierarchical group
+ // separator, so a literal "[account/work]" section header would be
+ // parsed as a *nested* group "work" inside a group "account" (and trips
+ // a QSettings::FormatError besides), not as a single flat group named
+ // "account/work". "." carries no such meaning to QSettings, so
+ // childGroups() here returns "account.work" and "account.personal" as
+ // plain top-level entries and status() stays NoError.
+ for (const QString &group : settings.childGroups()) {
+ if (!group.startsWith(QStringLiteral("account.")))
+ continue;
+
+ Account account;
+ account.key = group.mid(QStringLiteral("account.").size());
+
+ settings.beginGroup(group);
+ account.name = settings.value(QStringLiteral("name")).toString();
+ account.address = settings.value(QStringLiteral("address")).toString();
+ account.maildir = settings.value(QStringLiteral("maildir")).toString();
+ account.drafts = settings.value(QStringLiteral("drafts")).toString();
+ settings.endGroup();
+
+ if (!account.isValid()) {
+ m_warnings.append(
+ QStringLiteral("Account '%1' has no maildir; ignoring it.")
+ .arg(account.key));
+ continue;
+ }
+ m_accounts.append(account);
+ }
+
+ settings.beginGroup(QStringLiteral("queries"));
+ // QSettings::childKeys() returns keys in alphabetical order, not file
+ // order, so the saved-query button order in the UI is alphabetical too.
+ // A hand-rolled parser would be needed to preserve file order; not
+ // needed in v1.
+ for (const QString &name : settings.childKeys())
+ m_savedQueries.append({ name, settings.value(name).toString() });
+ settings.endGroup();
+}
+
+Account Config::account(const QString &key) const
+{
+ for (const Account &a : m_accounts) {
+ if (a.key == key)
+ return a;
+ }
+ return {};
+}
diff --git a/src/config.h b/src/config.h
new file mode 100644
index 0000000..f037db5
--- /dev/null
+++ b/src/config.h
@@ -0,0 +1,62 @@
+#pragma once
+
+#include <QList>
+#include <QString>
+#include <QStringList>
+
+/// One mail account. notmuch has no concept of accounts; it sees a single flat
+/// tree. An account is therefore a path prefix within that tree plus an
+/// identity.
+struct Account
+{
+ QString key; ///< INI group suffix, e.g. "work" from [account/work].
+ QString name;
+ QString address;
+ QString maildir; ///< Relative to notmuch's database.path.
+ QString drafts; ///< Unused in v1; send is v2.
+
+ bool isValid() const { return !key.isEmpty() && !maildir.isEmpty(); }
+
+ /// Restricts a notmuch query to this account's subtree.
+ QString scopedQuery(const QString &query) const;
+};
+
+struct SavedQuery
+{
+ QString name;
+ QString query;
+};
+
+/// Reads ~/.config/qtmaildir/qtmaildir.conf.
+///
+/// The Maildir path is deliberately NOT configurable here: notmuch already
+/// stores it as database.path and libnotmuch reads it. Duplicating it would
+/// allow the GUI to index a different tree than the CLI.
+class Config
+{
+public:
+ /// Path used when load() is called with no argument.
+ static QString defaultPath();
+
+ void load(const QString &path);
+
+ QList<Account> accounts() const { return m_accounts; }
+ Account account(const QString &key) const;
+ QList<SavedQuery> savedQueries() const { return m_savedQueries; }
+
+ /// Empty when unset; the caller disables the Sync button in that case.
+ QString syncCommand() const { return m_syncCommand; }
+
+ /// Optional alternate notmuch config file. Empty means "let notmuch decide".
+ QString notmuchConfig() const { return m_notmuchConfig; }
+
+ /// Non-fatal problems, shown once in a startup banner.
+ QStringList warnings() const { return m_warnings; }
+
+private:
+ QList<Account> m_accounts;
+ QList<SavedQuery> m_savedQueries;
+ QString m_syncCommand;
+ QString m_notmuchConfig;
+ QStringList m_warnings;
+};