MailJDTourDownloadDocsQualityTestsCoverage

Which source lines and branches the tests execute. Lines 91.5 %, branches 75.0 % (minimum 85 % and 72 %). main @ 399ffe3d · 21 October 2026 · pipeline 7238.

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 100.0% 5 / 0 / 5
Functions: 100.0% 3 / 0 / 3
Branches: 75.0% 6 / 0 / 8

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