data/AccountConfig.h
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | |||
| 3 | #include <QByteArray> | ||
| 4 | #include <QString> | ||
| 5 | #include <QStringList> | ||
| 6 | #include <functional> | ||
| 7 | #include <optional> | ||
| 8 | #include <memory> | ||
| 9 | #include <vector> | ||
| 10 | |||
| 11 | // IMAP server configuration for a single account | ||
| 12 | struct ImapConfig { | ||
| 13 | QString host; | ||
| 14 | quint16 port = 993; | ||
| 15 | QString security = "ssl"; // "ssl" or "starttls" | ||
| 16 | QString username; | ||
| 17 | QByteArray password; // SEC-22: QByteArray for secure zeroing | ||
| 18 | // Versioned keyring identity. Empty legacy metadata resolves through the | ||
| 19 | // account id; a fresh value lets an asynchronous save stage a password | ||
| 20 | // without overwriting the credential used by the current configuration. | ||
| 21 | QString credentialId; | ||
| 22 | }; | ||
| 23 | |||
| 24 | // SMTP server configuration for a single account | ||
| 25 | struct SmtpConfig { | ||
| 26 | QString host; | ||
| 27 | quint16 port = 587; | ||
| 28 | QString security = "starttls"; // "ssl" or "starttls" | ||
| 29 | QString username; | ||
| 30 | QByteArray password; // SEC-22: QByteArray for secure zeroing | ||
| 31 | QString credentialId; | ||
| 32 | // Sprint 100 D-17: "imap" = the outgoing server uses the incoming login | ||
| 33 | // (user name and password); missing in a file = "separate", so existing | ||
| 34 | // accounts behave as before. Saving stages the incoming password for the | ||
| 35 | // outgoing credential too, so sending reads its own keyring entry. | ||
| 36 | QString login = QStringLiteral("separate"); | ||
| 37 | 54 | bool sharesIncomingLogin() const { return login == QLatin1String("imap"); } | |
| 38 | }; | ||
| 39 | |||
| 40 | // Server rules (Sieve/ManageSieve) for one account (Sprint 95 S95.1.3). | ||
| 41 | // Absent from the account JSON = "auto" with the IMAP login, so existing | ||
| 42 | // accounts need no migration. A separate password lives only in the keyring | ||
| 43 | // (service "sieve", versioned credentialId); it is read lazily by the rule | ||
| 44 | // session, never at startup. | ||
| 45 | struct SieveConfig { | ||
| 46 | QString mode = QStringLiteral("auto"); // auto | manual | off | ||
| 47 | QString host; // manual only | ||
| 48 | quint16 port = 4190; // manual only | ||
| 49 | QString security = QStringLiteral("starttls"); // starttls | ssl (manual only) | ||
| 50 | QString login = QStringLiteral("imap"); // imap | separate | ||
| 51 | QString username; // separate only | ||
| 52 | QByteArray password; // separate only; staged into the keyring on save | ||
| 53 | QString credentialId; | ||
| 54 | |||
| 55 | 1371 | bool usesSeparateLogin() const { return login == QLatin1String("separate"); } | |
| 56 | 55 | bool isDefault() const { | |
| 57 |
6/8✓ Branch 4 → 5 taken 53 times.
✓ Branch 4 → 13 taken 2 times.
✓ Branch 7 → 8 taken 51 times.
✓ Branch 7 → 13 taken 2 times.
✓ Branch 9 → 10 taken 51 times.
✗ Branch 9 → 13 not taken.
✓ Branch 11 → 12 taken 51 times.
✗ Branch 11 → 13 not taken.
|
106 | return mode == QLatin1String("auto") && login == QLatin1String("imap") && host.isEmpty() && |
| 58 | 106 | credentialId.isEmpty(); | |
| 59 | } | ||
| 60 | }; | ||
| 61 | |||
| 62 | struct AccountCredentialMigration; | ||
| 63 | class QObject; | ||
| 64 | |||
| 65 | // Complete account configuration loaded from a single JSON file | ||
| 66 | struct AccountConfig { | ||
| 67 | QString id; // Persisted UUID used as the stable credential identity | ||
| 68 | QString name; // Display name (e.g. "Personal") | ||
| 69 | QString email; // Email address | ||
| 70 | ImapConfig imap; | ||
| 71 | SmtpConfig smtp; | ||
| 72 | SieveConfig sieve; | ||
| 73 | QString legacyKeyringName; // Retained until display-name keys are migrated | ||
| 74 | // Deferred migration admission contains only path/digest and completion flags. | ||
| 75 | // It is never serialized or exposed in form metadata. | ||
| 76 | std::shared_ptr<AccountCredentialMigration> deferredCredentialMigration; | ||
| 77 | }; | ||
| 78 | |||
| 79 | class CredentialStore; | ||
| 80 | |||
| 81 | // Loads and validates account configurations from JSON files. | ||
| 82 | // Default config directory: ~/.config/mailjd/accounts/ | ||
| 83 | class AccountConfigLoader { | ||
| 84 | public: | ||
| 85 | using AsyncSaveCallback = | ||
| 86 | std::function<void(bool success, const AccountConfig &stored, | ||
| 87 | const QString &error)>; | ||
| 88 | enum class LoadMode { MigrateCredentials, MetadataOnly }; | ||
| 89 | |||
| 90 | // --- Read --- | ||
| 91 | |||
| 92 | // Load all *.json files from the default config directory | ||
| 93 | static std::vector<AccountConfig> loadAll(); | ||
| 94 | |||
| 95 | // Load all *.json files from a specific directory | ||
| 96 | static std::vector<AccountConfig> loadAll(const QString &configDir, | ||
| 97 | LoadMode mode = LoadMode::MigrateCredentials); | ||
| 98 | |||
| 99 | // Load a single account from a JSON file | ||
| 100 | static std::optional<AccountConfig> loadFromFile(const QString &path, | ||
| 101 | LoadMode mode = LoadMode::MigrateCredentials); | ||
| 102 | |||
| 103 | // MetadataOnly never enters the keyring/event loop. It first persists an ID | ||
| 104 | // while retaining legacy data. After asynchronous resolution succeeds, admit | ||
| 105 | // removal of that data only against the exact original file digest. | ||
| 106 | static bool finishCredentialMigration(AccountConfig &account, QObject *owner); | ||
| 107 | |||
| 108 | static QString imapCredentialName(const AccountConfig &account); | ||
| 109 | static QString smtpCredentialName(const AccountConfig &account); | ||
| 110 | // Keyring identity of the separate Sieve password (empty when none). | ||
| 111 | static QString sieveCredentialName(const AccountConfig &account); | ||
| 112 | |||
| 113 | // SEC-01/02: Resolve passwords from OS keyring for accounts loaded with | ||
| 114 | // empty passwords. Invokes callback when all passwords have been resolved. | ||
| 115 | // Pass the CredentialStore instance that should be used. | ||
| 116 | static void resolvePasswords(std::vector<AccountConfig> &accounts, | ||
| 117 | CredentialStore *store, | ||
| 118 | std::function<void()> callback); | ||
| 119 | |||
| 120 | // --- Write --- | ||
| 121 | |||
| 122 | // Save an account config to configDir. | ||
| 123 | // Filename = fileStem(config, configDir) + ".json". Creates directory if needed. | ||
| 124 | // SEC-01/02: Passwords are stored in the OS keyring, not in JSON. | ||
| 125 | static bool save(AccountConfig &config, const QString &configDir); | ||
| 126 | static bool save(const AccountConfig &config, const QString &configDir); | ||
| 127 | |||
| 128 | // Stages changed passwords under fresh keyring identities and publishes | ||
| 129 | // metadata only after every write succeeds and the original account file | ||
| 130 | // still matches. Destroying the returned request cancels publication and | ||
| 131 | // schedules best-effort cleanup of staged credentials. | ||
| 132 | static QObject *saveAsync(QObject *owner, AccountConfig config, | ||
| 133 | const QString &configDir, | ||
| 134 | AsyncSaveCallback callback); | ||
| 135 | |||
| 136 | // Save an account config to a specific file path | ||
| 137 | static bool saveToFile(AccountConfig &config, const QString &path); | ||
| 138 | static bool saveToFile(const AccountConfig &config, const QString &path); | ||
| 139 | |||
| 140 | // --- Delete --- | ||
| 141 | |||
| 142 | // Delete the config file for the given account name from configDir. | ||
| 143 | // Keyring cleanup runs asynchronously after successful local removal; | ||
| 144 | // its failure is logged and does not undo removal or block the UI. | ||
| 145 | // An expected ID fences stale forms against another account with the same | ||
| 146 | // display name. Deletion never runs legacy credential migration. | ||
| 147 | static bool remove(const QString &accountName, const QString &configDir, | ||
| 148 | const QString &expectedId = {}); | ||
| 149 | |||
| 150 | // --- Utility --- | ||
| 151 | |||
| 152 | // Validate an account config, returns list of error messages (empty = valid) | ||
| 153 | static QStringList validate(const AccountConfig &config); | ||
| 154 | |||
| 155 | // Check that the display name is unique (case-insensitive) among other | ||
| 156 | // account IDs. Returns an error message if a collision is found, or empty | ||
| 157 | // string if OK. | ||
| 158 | static QString checkNameUniqueness(const AccountConfig &config, | ||
| 159 | const QString &configDir); | ||
| 160 | |||
| 161 | // S106-204: the account's file name without ".json": slugify(name), or — | ||
| 162 | // when that file belongs to another readable account (the slug is not injective: | ||
| 163 | // separators collapse, accents fold, names are cut at 50 characters) — | ||
| 164 | // the slug plus "_" and 8 hex digits of the name's SHA-256. | ||
| 165 | static QString fileStem(const AccountConfig &config, const QString &configDir); | ||
| 166 | |||
| 167 | // Returns the default config directory path | ||
| 168 | static QString defaultConfigDir(); | ||
| 169 | |||
| 170 | // Generate a filesystem-safe filename from an account name | ||
| 171 | // e.g. "Mein Büro-Account" → "mein_buero_account" | ||
| 172 | static QString slugify(const QString &name); | ||
| 173 | // S106-198: an account id names directories (sieve/<id>) and keyring | ||
| 174 | // entries, and it comes from a JSON file in the profile. It is accepted | ||
| 175 | // only as one plain path component: not empty, not "." or "..", no | ||
| 176 | // separator, no control character, at most 128 characters. | ||
| 177 | static bool isSafeId(const QString &id); | ||
| 178 | }; | ||
| 179 |