Line data Source code
1 : #include "config_store.h"
2 : #include "config.h"
3 : #include "fs_util.h"
4 : #include "platform/path.h"
5 : #include "platform/credential_key.h"
6 : #include "raii.h"
7 : #include "logger.h"
8 : #include <openssl/evp.h>
9 : #include <openssl/rand.h>
10 : #include <stdio.h>
11 : #include <stdlib.h>
12 : #include <string.h>
13 : #include <ctype.h>
14 : #include <sys/stat.h>
15 : #include <dirent.h>
16 : #include <unistd.h>
17 : #include <errno.h>
18 :
19 : #define CONFIG_APP_DIR "email-cli"
20 :
21 : /** @brief Trims leading and trailing whitespace from a string in-place. */
22 21478 : static char* trim(char *str) {
23 : char *end;
24 22054 : while (isspace((unsigned char)*str)) str++;
25 21478 : if (*str == 0) return str;
26 20902 : end = str + strlen(str) - 1;
27 30431 : while (end > str && isspace((unsigned char)*end)) end--;
28 20902 : end[1] = '\0';
29 20902 : return str;
30 : }
31 :
32 : /** Returns heap-allocated path to the accounts/ directory. Caller must free. */
33 1260 : static char *get_accounts_dir(void) {
34 1260 : const char *config_base = platform_config_dir();
35 1260 : if (!config_base) return NULL;
36 1260 : char *dir = NULL;
37 1260 : if (asprintf(&dir, "%s/%s/accounts", config_base, CONFIG_APP_DIR) == -1)
38 0 : return NULL;
39 1260 : return dir;
40 : }
41 :
42 : /* ── Global application settings ────────────────────────────────────────── */
43 :
44 : static int g_obfuscation_loaded = 0;
45 : static int g_credential_obfuscation = 1; /* default: ON */
46 :
47 711 : static char *get_settings_path(void) {
48 711 : const char *config_base = platform_config_dir();
49 711 : if (!config_base) return NULL;
50 711 : char *path = NULL;
51 711 : if (asprintf(&path, "%s/%s/settings.ini", config_base, CONFIG_APP_DIR) == -1)
52 0 : return NULL;
53 711 : return path;
54 : }
55 :
56 77 : static void write_settings(const char *path) {
57 77 : const char *config_base = platform_config_dir();
58 77 : if (!config_base) return;
59 : char dir[4096];
60 77 : snprintf(dir, sizeof(dir), "%s/%s", config_base, CONFIG_APP_DIR);
61 77 : if (fs_mkdir_p(dir, 0700) != 0) return;
62 77 : FILE *fp = fopen(path, "w");
63 77 : if (!fp) return;
64 77 : fprintf(fp, "credential_obfuscation=%s\n", g_credential_obfuscation ? "true" : "false");
65 77 : fclose(fp);
66 77 : fs_ensure_permissions(path, 0600);
67 : }
68 :
69 1695 : static void load_settings_once(void) {
70 1770 : if (g_obfuscation_loaded) return;
71 709 : g_obfuscation_loaded = 1;
72 :
73 1418 : RAII_STRING char *path = get_settings_path();
74 709 : if (!path) return;
75 :
76 709 : FILE *fp = fopen(path, "r");
77 709 : if (!fp) {
78 : /* First run — create settings.ini with defaults */
79 75 : write_settings(path);
80 75 : return;
81 : }
82 :
83 : char line[256];
84 1268 : while (fgets(line, sizeof(line), fp)) {
85 634 : char *key = strtok(line, "=");
86 634 : char *val = strtok(NULL, "\n");
87 634 : if (!key || !val) continue;
88 634 : key = trim(key); val = trim(val);
89 634 : if (strcmp(key, "credential_obfuscation") == 0)
90 634 : g_credential_obfuscation = (strcmp(val, "true") == 0 || strcmp(val, "1") == 0) ? 1 : 0;
91 : }
92 634 : fclose(fp);
93 : }
94 :
95 250 : int app_settings_get_obfuscation(void) {
96 250 : load_settings_once();
97 250 : return g_credential_obfuscation;
98 : }
99 :
100 2 : int app_settings_set_obfuscation(int enabled) {
101 2 : load_settings_once();
102 2 : g_credential_obfuscation = enabled ? 1 : 0;
103 4 : RAII_STRING char *path = get_settings_path();
104 2 : if (!path) return -1;
105 2 : write_settings(path);
106 2 : return 0;
107 : }
108 :
109 : /* ── Base64 encode / decode ──────────────────────────────────────────────── */
110 :
111 : static const char B64CHARS[] =
112 : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
113 :
114 : /** Returns heap-allocated base64 string. Caller must free(). */
115 344 : static char *b64_encode(const unsigned char *src, size_t src_len) {
116 344 : size_t out_len = ((src_len + 2) / 3) * 4 + 1;
117 344 : char *out = malloc(out_len);
118 344 : if (!out) return NULL;
119 344 : size_t i, j = 0;
120 4473 : for (i = 0; i + 2 < src_len; i += 3) {
121 4129 : out[j++] = B64CHARS[src[i] >> 2];
122 4129 : out[j++] = B64CHARS[((src[i] & 3) << 4) | (src[i+1] >> 4)];
123 4129 : out[j++] = B64CHARS[((src[i+1] & 0xf) << 2) | (src[i+2] >> 6)];
124 4129 : out[j++] = B64CHARS[src[i+2] & 0x3f];
125 : }
126 344 : size_t rem = src_len - i;
127 344 : if (rem == 1) {
128 15 : out[j++] = B64CHARS[src[i] >> 2];
129 15 : out[j++] = B64CHARS[(src[i] & 3) << 4];
130 15 : out[j++] = '='; out[j++] = '=';
131 329 : } else if (rem == 2) {
132 22 : out[j++] = B64CHARS[src[i] >> 2];
133 22 : out[j++] = B64CHARS[((src[i] & 3) << 4) | (src[i+1] >> 4)];
134 22 : out[j++] = B64CHARS[(src[i+1] & 0xf) << 2];
135 22 : out[j++] = '=';
136 : }
137 344 : out[j] = '\0';
138 344 : return out;
139 : }
140 :
141 72988 : static int b64_char_val(char c) {
142 72988 : if (c >= 'A' && c <= 'Z') return c - 'A';
143 43648 : if (c >= 'a' && c <= 'z') return c - 'a' + 26;
144 14442 : if (c >= '0' && c <= '9') return c - '0' + 52;
145 2321 : if (c == '+') return 62;
146 1421 : if (c == '/') return 63;
147 366 : if (c == '=') return 0;
148 0 : return -1;
149 : }
150 :
151 : /**
152 : * Decode base64 string into *out (heap-allocated). Caller must free().
153 : * Returns 0 on success, -1 on error.
154 : */
155 1475 : static int b64_decode(const char *src, unsigned char **out, size_t *out_len) {
156 1475 : size_t src_len = strlen(src);
157 1475 : if (src_len == 0 || src_len % 4 != 0) return -1;
158 :
159 1475 : size_t dec_len = (src_len / 4) * 3;
160 1475 : if (src[src_len - 1] == '=') dec_len--;
161 1475 : if (src[src_len - 2] == '=') dec_len--;
162 :
163 1475 : unsigned char *buf = malloc(dec_len + 1);
164 1475 : if (!buf) return -1;
165 :
166 1475 : size_t j = 0;
167 19722 : for (size_t i = 0; i < src_len; i += 4) {
168 18247 : int a = b64_char_val(src[i]);
169 18247 : int b = b64_char_val(src[i+1]);
170 18247 : int c = b64_char_val(src[i+2]);
171 18247 : int d = b64_char_val(src[i+3]);
172 18247 : if (a < 0 || b < 0 || c < 0 || d < 0) { free(buf); return -1; }
173 18247 : buf[j++] = (unsigned char)((a << 2) | (b >> 4));
174 18247 : if (src[i+2] != '=') buf[j++] = (unsigned char)((b << 4) | (c >> 2));
175 18247 : if (src[i+3] != '=') buf[j++] = (unsigned char)((c << 6) | d);
176 : }
177 1475 : buf[j] = '\0';
178 1475 : *out = buf;
179 1475 : *out_len = j;
180 1475 : return 0;
181 : }
182 :
183 : /* ── Credential encryption / decryption (AES-256-GCM) ───────────────────── */
184 :
185 : /**
186 : * Encrypt plaintext with AES-256-GCM using a key derived from the email.
187 : * Returns heap-allocated "enc:<base64(iv|ciphertext|tag)>" string,
188 : * or NULL if key derivation failed (caller falls back to plaintext).
189 : */
190 344 : static char *encrypt_credential(const char *plaintext, const char *email) {
191 344 : if (!plaintext || !*plaintext)
192 0 : return strdup(plaintext ? plaintext : "");
193 :
194 : unsigned char key[32];
195 344 : if (platform_derive_credential_key(email, key) != 0)
196 0 : return NULL; /* no key source available — store plaintext */
197 :
198 : unsigned char iv[12];
199 344 : if (RAND_bytes(iv, sizeof(iv)) != 1) return NULL;
200 :
201 344 : size_t pt_len = strlen(plaintext);
202 344 : unsigned char *ct = malloc(pt_len + 1);
203 344 : if (!ct) return NULL;
204 :
205 : unsigned char tag[16];
206 344 : int outl = 0, finl = 0;
207 344 : EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
208 344 : if (!ctx) { free(ct); return NULL; }
209 344 : EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, key, iv);
210 344 : EVP_EncryptUpdate(ctx, ct, &outl, (const unsigned char *)plaintext, (int)pt_len);
211 344 : EVP_EncryptFinal_ex(ctx, ct + outl, &finl);
212 344 : EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, tag);
213 344 : EVP_CIPHER_CTX_free(ctx);
214 :
215 : /* Pack: iv[12] | ciphertext[pt_len] | tag[16] */
216 344 : size_t packed_len = 12 + pt_len + 16;
217 344 : unsigned char *packed = malloc(packed_len);
218 344 : if (!packed) { free(ct); return NULL; }
219 344 : memcpy(packed, iv, 12);
220 344 : memcpy(packed + 12, ct, pt_len);
221 344 : memcpy(packed + 12 + pt_len, tag, 16);
222 344 : free(ct);
223 :
224 344 : char *b64 = b64_encode(packed, packed_len);
225 344 : free(packed);
226 344 : if (!b64) return NULL;
227 :
228 344 : char *result = NULL;
229 344 : if (asprintf(&result, "enc:%s", b64) == -1) result = NULL;
230 344 : free(b64);
231 344 : return result;
232 : }
233 :
234 : /**
235 : * Decrypt a credential value.
236 : * - If value starts with "enc:", decrypt using a key derived from email.
237 : * - Otherwise return a copy of the plaintext value.
238 : * Returns heap-allocated plaintext, or NULL on decryption failure.
239 : */
240 1475 : static char *decrypt_credential(const char *value, const char *email) {
241 1475 : if (!value) return NULL;
242 1475 : if (strncmp(value, "enc:", 4) != 0) return strdup(value);
243 :
244 1475 : unsigned char *packed = NULL;
245 1475 : size_t packed_len = 0;
246 1475 : if (b64_decode(value + 4, &packed, &packed_len) != 0) return NULL;
247 1475 : if (packed_len < 12 + 16) { free(packed); return NULL; }
248 :
249 : unsigned char key[32];
250 1475 : if (platform_derive_credential_key(email, key) != 0) {
251 0 : free(packed);
252 0 : return NULL;
253 : }
254 :
255 1475 : unsigned char *iv = packed;
256 1475 : size_t ct_len = packed_len - 12 - 16;
257 1475 : unsigned char *ct = packed + 12;
258 1475 : unsigned char *tag = packed + 12 + ct_len;
259 :
260 1475 : unsigned char *pt = malloc(ct_len + 1);
261 1475 : if (!pt) { free(packed); return NULL; }
262 :
263 1475 : EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
264 1475 : if (!ctx) { free(packed); free(pt); return NULL; }
265 :
266 1475 : EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, key, iv);
267 1475 : EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, tag);
268 1475 : int outl = 0, finl = 0;
269 1475 : EVP_DecryptUpdate(ctx, pt, &outl, ct, (int)ct_len);
270 1475 : int ok = EVP_DecryptFinal_ex(ctx, pt + outl, &finl);
271 1475 : EVP_CIPHER_CTX_free(ctx);
272 1475 : free(packed);
273 :
274 1475 : if (ok != 1) {
275 : /* Authentication failed — wrong key (system data changed) */
276 0 : free(pt);
277 0 : return NULL;
278 : }
279 1475 : pt[ct_len] = '\0';
280 1475 : return (char *)pt;
281 : }
282 :
283 : /* ── Config read / write ────────────────────────────────────────────────── */
284 :
285 : /** Write one config struct to an open FILE, encrypting credentials if enabled. */
286 241 : static void write_config_to_fp(FILE *fp, const Config *cfg) {
287 241 : int obfus = app_settings_get_obfuscation();
288 241 : const char *email = cfg->user ? cfg->user : "";
289 :
290 241 : fprintf(fp, "EMAIL_HOST=%s\n", cfg->host ? cfg->host : "");
291 241 : fprintf(fp, "EMAIL_USER=%s\n", cfg->user ? cfg->user : "");
292 :
293 : /* Credentials: encrypt when obfuscation is on */
294 : {
295 238 : char *enc = (obfus && cfg->pass && *cfg->pass)
296 479 : ? encrypt_credential(cfg->pass, email) : NULL;
297 241 : fprintf(fp, "EMAIL_PASS=%s\n", enc ? enc : (cfg->pass ? cfg->pass : ""));
298 241 : free(enc);
299 : }
300 :
301 241 : fprintf(fp, "EMAIL_FOLDER=%s\n", cfg->folder ? cfg->folder : "INBOX");
302 241 : if (cfg->sent_folder) fprintf(fp, "EMAIL_SENT_FOLDER=%s\n", cfg->sent_folder);
303 241 : if (cfg->trash_folder) fprintf(fp, "TRASH_FOLDER=%s\n", cfg->trash_folder);
304 241 : if (cfg->ssl_no_verify) fprintf(fp, "SSL_NO_VERIFY=1\n");
305 241 : fprintf(fp, "SYNC_INTERVAL=%d\n", cfg->sync_interval);
306 241 : if (cfg->smtp_host) fprintf(fp, "SMTP_HOST=%s\n", cfg->smtp_host);
307 241 : if (cfg->smtp_port) fprintf(fp, "SMTP_PORT=%d\n", cfg->smtp_port);
308 241 : if (cfg->smtp_user) fprintf(fp, "SMTP_USER=%s\n", cfg->smtp_user);
309 241 : if (cfg->smtp_pass) {
310 107 : char *enc = (obfus && *cfg->smtp_pass)
311 214 : ? encrypt_credential(cfg->smtp_pass, email) : NULL;
312 107 : fprintf(fp, "SMTP_PASS=%s\n", enc ? enc : cfg->smtp_pass);
313 107 : free(enc);
314 : }
315 241 : if (cfg->gmail_mode) fprintf(fp, "GMAIL_MODE=1\n");
316 241 : if (cfg->gmail_refresh_token) {
317 19 : char *enc = (obfus && *cfg->gmail_refresh_token)
318 38 : ? encrypt_credential(cfg->gmail_refresh_token, email) : NULL;
319 19 : fprintf(fp, "GMAIL_REFRESH_TOKEN=%s\n", enc ? enc : cfg->gmail_refresh_token);
320 19 : free(enc);
321 : }
322 241 : if (cfg->gmail_client_id) fprintf(fp, "GMAIL_CLIENT_ID=%s\n", cfg->gmail_client_id);
323 241 : if (cfg->gmail_client_secret) fprintf(fp, "GMAIL_CLIENT_SECRET=%s\n", cfg->gmail_client_secret);
324 241 : }
325 :
326 : /** Load a config from a specific file path. Decrypts enc: credentials transparently.
327 : * Sets *out_needs_resave to 1 if any credential was plaintext and obfuscation is ON. */
328 1315 : static Config *load_config_from_path(const char *path, int *out_needs_resave) {
329 2630 : RAII_FILE FILE *fp = fopen(path, "r");
330 1315 : if (!fp) return NULL;
331 :
332 1292 : Config *cfg = calloc(1, sizeof(Config));
333 1292 : if (!cfg) return NULL;
334 :
335 1292 : int plaintext_cred_found = 0; /* set if any credential lacks enc: prefix */
336 1292 : int encrypted_cred_found = 0; /* set if any credential has enc: prefix */
337 : char line[1024]; /* wider than before — enc: values can be long */
338 11397 : while (fgets(line, sizeof(line), fp)) {
339 10105 : char *eq = strchr(line, '=');
340 10105 : if (!eq) continue;
341 10105 : *eq = '\0';
342 10105 : char *key = trim(line);
343 10105 : char *val = trim(eq + 1);
344 : /* Strip trailing newline from val */
345 10105 : size_t vlen = strlen(val);
346 10105 : while (vlen > 0 && (val[vlen-1] == '\n' || val[vlen-1] == '\r'))
347 0 : val[--vlen] = '\0';
348 :
349 10105 : if (strcmp(key, "EMAIL_HOST") == 0) cfg->host = strdup(val);
350 8819 : else if (strcmp(key, "EMAIL_USER") == 0) cfg->user = strdup(val);
351 7527 : else if (strcmp(key, "EMAIL_PASS") == 0) {
352 1278 : cfg->pass = strdup(val);
353 1278 : if (val[0]) { if (strncmp(val, "enc:", 4) == 0) encrypted_cred_found = 1;
354 192 : else plaintext_cred_found = 1; }
355 : }
356 6249 : else if (strcmp(key, "EMAIL_FOLDER") == 0) cfg->folder = strdup(val);
357 4988 : else if (strcmp(key, "EMAIL_SENT_FOLDER") == 0) cfg->sent_folder = strdup(val);
358 4734 : else if (strcmp(key, "TRASH_FOLDER") == 0) cfg->trash_folder = strdup(val);
359 4731 : else if (strcmp(key, "SSL_NO_VERIFY") == 0) cfg->ssl_no_verify = atoi(val);
360 3762 : else if (strcmp(key, "SYNC_INTERVAL") == 0) cfg->sync_interval = atoi(val);
361 2652 : else if (strcmp(key, "SMTP_HOST") == 0) cfg->smtp_host = strdup(val);
362 1879 : else if (strcmp(key, "SMTP_PORT") == 0) cfg->smtp_port = atoi(val);
363 1586 : else if (strcmp(key, "SMTP_USER") == 0) cfg->smtp_user = strdup(val);
364 1090 : else if (strcmp(key, "SMTP_PASS") == 0) {
365 493 : cfg->smtp_pass = strdup(val);
366 493 : if (val[0]) { if (strncmp(val, "enc:", 4) == 0) encrypted_cred_found = 1;
367 104 : else plaintext_cred_found = 1; }
368 : }
369 597 : else if (strcmp(key, "GMAIL_MODE") == 0) cfg->gmail_mode = atoi(val);
370 300 : else if (strcmp(key, "GMAIL_REFRESH_TOKEN") == 0) {
371 296 : cfg->gmail_refresh_token = strdup(val);
372 296 : if (val[0]) { if (strncmp(val, "enc:", 4) == 0) encrypted_cred_found = 1;
373 13 : else plaintext_cred_found = 1; }
374 : }
375 4 : else if (strcmp(key, "GMAIL_CLIENT_ID") == 0) cfg->gmail_client_id = strdup(val);
376 3 : else if (strcmp(key, "GMAIL_CLIENT_SECRET") == 0) cfg->gmail_client_secret = strdup(val);
377 : }
378 1292 : if (!cfg->folder) cfg->folder = strdup("INBOX");
379 :
380 : /* Decrypt any enc: credential fields using the account email as key context */
381 1292 : const char *email = cfg->user ? cfg->user : "";
382 :
383 1292 : if (cfg->pass && strncmp(cfg->pass, "enc:", 4) == 0) {
384 803 : char *dec = decrypt_credential(cfg->pass, email);
385 803 : free(cfg->pass);
386 803 : if (dec) {
387 803 : cfg->pass = dec;
388 : } else {
389 0 : fprintf(stderr,
390 : "Warning: Could not decrypt stored password for '%s'.\n"
391 : " The system key may have changed. Re-enter the password with:\n"
392 : " email-cli config password\n", email);
393 0 : logger_log(LOG_WARN, "credential decrypt failed for %s", email);
394 0 : cfg->pass = NULL;
395 : }
396 : }
397 :
398 1292 : if (cfg->smtp_pass && strncmp(cfg->smtp_pass, "enc:", 4) == 0) {
399 389 : char *dec = decrypt_credential(cfg->smtp_pass, email);
400 389 : free(cfg->smtp_pass);
401 389 : cfg->smtp_pass = dec; /* NULL on failure is acceptable for SMTP */
402 : }
403 :
404 1292 : if (cfg->gmail_refresh_token && strncmp(cfg->gmail_refresh_token, "enc:", 4) == 0) {
405 283 : char *dec = decrypt_credential(cfg->gmail_refresh_token, email);
406 283 : free(cfg->gmail_refresh_token);
407 283 : if (dec) {
408 283 : cfg->gmail_refresh_token = dec;
409 : } else {
410 0 : fprintf(stderr,
411 : "Warning: Could not decrypt stored refresh token for '%s'.\n"
412 : " Re-run the Gmail OAuth2 setup: email-cli add-account\n", email);
413 0 : logger_log(LOG_WARN, "refresh token decrypt failed for %s", email);
414 0 : cfg->gmail_refresh_token = NULL;
415 : }
416 : }
417 :
418 : /* Gmail mode requires user + refresh token; IMAP mode requires host + user + pass */
419 1292 : if (cfg->gmail_mode) {
420 297 : if (!cfg->user || !cfg->gmail_refresh_token) { config_free(cfg); return NULL; }
421 : } else {
422 995 : if (!cfg->host || !cfg->user || !cfg->pass) { config_free(cfg); return NULL; }
423 : }
424 :
425 : /* TLS enforcement (IMAP mode only — Gmail uses OAuth2 over HTTPS) */
426 1289 : if (!cfg->gmail_mode && !cfg->ssl_no_verify) {
427 29 : if (strncmp(cfg->host, "imaps://", 8) != 0) {
428 1 : fprintf(stderr,
429 : "Error: EMAIL_HOST must start with imaps:// (TLS required).\n"
430 : " Got: %s\n", cfg->host);
431 1 : logger_log(LOG_ERROR,
432 : "Rejected insecure EMAIL_HOST in account config: %s",
433 : cfg->host);
434 1 : config_free(cfg);
435 1 : return NULL;
436 : }
437 28 : if (cfg->smtp_host && cfg->smtp_host[0] &&
438 2 : strncmp(cfg->smtp_host, "smtps://", 8) != 0) {
439 1 : fprintf(stderr,
440 : "Error: SMTP_HOST must start with smtps:// (TLS required).\n"
441 : " Got: %s\n", cfg->smtp_host);
442 1 : logger_log(LOG_ERROR,
443 : "Rejected insecure SMTP_HOST in account config: %s",
444 : cfg->smtp_host);
445 1 : config_free(cfg);
446 1 : return NULL;
447 : }
448 1260 : } else if (cfg->host) {
449 1257 : if (strncmp(cfg->host, "imaps://", 8) != 0)
450 294 : logger_log(LOG_WARN,
451 : "SSL_NO_VERIFY=1: connecting without TLS to %s "
452 : "(test/dev mode only)", cfg->host);
453 1257 : if (cfg->smtp_host && cfg->smtp_host[0] &&
454 771 : strncmp(cfg->smtp_host, "smtps://", 8) != 0)
455 1 : logger_log(LOG_WARN,
456 : "SSL_NO_VERIFY=1: SMTP without TLS to %s "
457 : "(test/dev mode only)", cfg->smtp_host);
458 : }
459 1287 : if (out_needs_resave)
460 1488 : *out_needs_resave = (plaintext_cred_found && g_credential_obfuscation) /* plaintext → encrypt */
461 1488 : || (encrypted_cred_found && !g_credential_obfuscation); /* enc: → plaintext */
462 1287 : return cfg;
463 : }
464 :
465 : /* ── Public API ──────────────────────────────────────────────────────────── */
466 :
467 102 : Config *config_load_account(const char *name) {
468 102 : if (!name || !name[0]) return NULL;
469 100 : int count = 0;
470 100 : AccountEntry *list = config_list_accounts(&count);
471 100 : if (!list || count == 0) { config_free_account_list(list, count); return NULL; }
472 100 : Config *result = NULL;
473 119 : for (int i = 0; i < count; i++) {
474 135 : int match = (list[i].name && strcmp(list[i].name, name) == 0) ||
475 19 : (list[i].cfg && list[i].cfg->user &&
476 19 : strcmp(list[i].cfg->user, name) == 0);
477 116 : if (match) { result = list[i].cfg; list[i].cfg = NULL; break; }
478 : }
479 100 : config_free_account_list(list, count);
480 100 : return result;
481 : }
482 :
483 253 : Config* config_load_from_store(void) {
484 253 : load_settings_once(); /* ensure settings.ini exists */
485 253 : int count = 0;
486 253 : AccountEntry *list = config_list_accounts(&count);
487 253 : if (!list || count == 0) {
488 5 : config_free_account_list(list, count);
489 5 : return NULL;
490 : }
491 248 : Config *result = list[0].cfg;
492 248 : list[0].cfg = NULL;
493 248 : config_free_account_list(list, count);
494 248 : return result;
495 : }
496 :
497 42 : int config_save_account(const Config *cfg) {
498 42 : if (!cfg || !cfg->user || !cfg->user[0]) return -1;
499 :
500 84 : RAII_STRING char *accounts_dir = get_accounts_dir();
501 42 : if (!accounts_dir) return -1;
502 :
503 : char account_dir[1024];
504 42 : snprintf(account_dir, sizeof(account_dir), "%s/%s", accounts_dir, cfg->user);
505 :
506 42 : if (fs_mkdir_p(account_dir, 0700) != 0) return -1;
507 :
508 : char path[1088];
509 41 : snprintf(path, sizeof(path), "%s/config.ini", account_dir);
510 :
511 82 : RAII_FILE FILE *fp = fopen(path, "w");
512 41 : if (!fp) return -1;
513 40 : write_config_to_fp(fp, cfg);
514 40 : fs_ensure_permissions(path, 0600);
515 :
516 40 : logger_log(LOG_INFO, "Account saved: %s", cfg->user);
517 40 : return 0;
518 : }
519 :
520 12 : int config_save_to_store(const Config *cfg) {
521 12 : return config_save_account(cfg);
522 : }
523 :
524 28 : int config_delete_account(const char *name) {
525 28 : if (!name || !name[0]) return -1;
526 :
527 56 : RAII_STRING char *accounts_dir = get_accounts_dir();
528 28 : if (!accounts_dir) return -1;
529 :
530 : char path[1024];
531 28 : snprintf(path, sizeof(path), "%s/%s/config.ini", accounts_dir, name);
532 28 : unlink(path);
533 :
534 : char dir[1024];
535 28 : snprintf(dir, sizeof(dir), "%s/%s", accounts_dir, name);
536 28 : if (rmdir(dir) != 0 && errno != ENOENT) {
537 4 : logger_log(LOG_WARN, "Could not remove account dir %s", dir);
538 4 : return -1;
539 : }
540 24 : logger_log(LOG_INFO, "Account deleted: %s", name);
541 24 : return 0;
542 : }
543 :
544 1190 : AccountEntry *config_list_accounts(int *count_out) {
545 1190 : load_settings_once(); /* ensure settings.ini exists */
546 1190 : *count_out = 0;
547 :
548 2380 : RAII_STRING char *accounts_dir = get_accounts_dir();
549 1190 : if (!accounts_dir) return NULL;
550 :
551 2380 : RAII_DIR DIR *d = opendir(accounts_dir);
552 1190 : if (!d) return NULL;
553 :
554 1188 : int cap = 8;
555 1188 : AccountEntry *list = malloc((size_t)cap * sizeof(AccountEntry));
556 1188 : if (!list) return NULL;
557 1188 : int count = 0;
558 :
559 : struct dirent *ent;
560 4879 : while ((ent = readdir(d)) != NULL) {
561 3719 : if (ent->d_name[0] == '.') continue;
562 :
563 : char path[1024];
564 1315 : snprintf(path, sizeof(path), "%s/%s/config.ini",
565 1315 : accounts_dir, ent->d_name);
566 :
567 1315 : int needs_resave = 0;
568 1315 : Config *cfg = load_config_from_path(path, &needs_resave);
569 1315 : if (!cfg) continue;
570 1287 : if (needs_resave) {
571 201 : logger_log(LOG_INFO, "Re-encrypting plaintext credentials for %s", ent->d_name);
572 402 : RAII_FILE FILE *wfp = fopen(path, "w");
573 201 : if (wfp) {
574 201 : write_config_to_fp(wfp, cfg);
575 201 : fs_ensure_permissions(path, 0600);
576 : }
577 : }
578 :
579 1287 : if (count >= cap) {
580 1 : cap *= 2;
581 1 : AccountEntry *tmp = realloc(list, (size_t)cap * sizeof(AccountEntry));
582 1 : if (!tmp) { config_free(cfg); break; }
583 1 : list = tmp;
584 : }
585 1287 : list[count].name = strdup(ent->d_name);
586 1287 : list[count].cfg = cfg;
587 1287 : count++;
588 : }
589 :
590 1188 : if (count == 0) { free(list); return NULL; }
591 :
592 : /* Sort by domain first, then by username within domain */
593 1287 : for (int i = 0; i < count - 1; i++) {
594 260 : for (int j = i + 1; j < count; j++) {
595 146 : const char *na = list[i].name ? list[i].name : "";
596 146 : const char *nb = list[j].name ? list[j].name : "";
597 146 : const char *at_a = strchr(na, '@');
598 146 : const char *at_b = strchr(nb, '@');
599 146 : const char *dom_a = at_a ? at_a + 1 : na;
600 146 : const char *dom_b = at_b ? at_b + 1 : nb;
601 146 : int dc = strcmp(dom_a, dom_b);
602 : int swap;
603 146 : if (dc != 0) {
604 19 : swap = dc > 0;
605 : } else {
606 127 : size_t ul_a = at_a ? (size_t)(at_a - na) : strlen(na);
607 127 : size_t ul_b = at_b ? (size_t)(at_b - nb) : strlen(nb);
608 127 : int uc = strncmp(na, nb, ul_a < ul_b ? ul_a : ul_b);
609 127 : swap = (uc != 0) ? (uc > 0) : (ul_a > ul_b);
610 : }
611 146 : if (swap) {
612 64 : AccountEntry tmp = list[i];
613 64 : list[i] = list[j];
614 64 : list[j] = tmp;
615 : }
616 : }
617 : }
618 :
619 1173 : *count_out = count;
620 1173 : return list;
621 : }
622 :
623 1174 : void config_free_account_list(AccountEntry *list, int count) {
624 1174 : if (!list) return;
625 2429 : for (int i = 0; i < count; i++) {
626 1270 : free(list[i].name);
627 1270 : config_free(list[i].cfg);
628 : }
629 1159 : free(list);
630 : }
631 :
632 10 : int config_migrate_credentials(void) {
633 10 : int count = 0;
634 10 : AccountEntry *list = config_list_accounts(&count);
635 10 : if (!list) return 0; /* no accounts — nothing to migrate */
636 :
637 9 : int errors = 0;
638 19 : for (int i = 0; i < count; i++) {
639 10 : if (list[i].cfg && config_save_account(list[i].cfg) != 0) {
640 0 : fprintf(stderr, "Warning: could not migrate account '%s'\n",
641 0 : list[i].name ? list[i].name : "?");
642 0 : errors++;
643 : }
644 : }
645 9 : config_free_account_list(list, count);
646 9 : return errors ? -1 : 0;
647 : }
|