LCOV - code coverage report
Current view: top level - libemail/src/infrastructure - config_store.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 83.0 % 153 127
Test Date: 2026-04-15 21:12:52 Functions: 90.0 % 10 9

            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 "raii.h"
       6              : #include "logger.h"
       7              : #include <stdio.h>
       8              : #include <stdlib.h>
       9              : #include <string.h>
      10              : #include <ctype.h>
      11              : #include <sys/stat.h>
      12              : #include <dirent.h>
      13              : #include <unistd.h>
      14              : #include <errno.h>
      15              : 
      16              : #define CONFIG_APP_DIR "email-cli"
      17              : 
      18              : /** @brief Trims leading and trailing whitespace from a string in-place. */
      19         1052 : static char* trim(char *str) {
      20              :     char *end;
      21         1052 :     while (isspace((unsigned char)*str)) str++;
      22         1052 :     if (*str == 0) return str;
      23         1052 :     end = str + strlen(str) - 1;
      24         1052 :     while (end > str && isspace((unsigned char)*end)) end--;
      25         1052 :     end[1] = '\0';
      26         1052 :     return str;
      27              : }
      28              : 
      29              : /** Returns heap-allocated path to the accounts/ directory. Caller must free. */
      30           61 : static char *get_accounts_dir(void) {
      31           61 :     const char *config_base = platform_config_dir();
      32           61 :     if (!config_base) return NULL;
      33           61 :     char *dir = NULL;
      34           61 :     if (asprintf(&dir, "%s/%s/accounts", config_base, CONFIG_APP_DIR) == -1)
      35            0 :         return NULL;
      36           61 :     return dir;
      37              : }
      38              : 
      39              : /** Write one config struct to an open FILE. */
      40            2 : static void write_config_to_fp(FILE *fp, const Config *cfg) {
      41            2 :     fprintf(fp, "EMAIL_HOST=%s\n",   cfg->host   ? cfg->host   : "");
      42            2 :     fprintf(fp, "EMAIL_USER=%s\n",   cfg->user   ? cfg->user   : "");
      43            2 :     fprintf(fp, "EMAIL_PASS=%s\n",   cfg->pass   ? cfg->pass   : "");
      44            2 :     fprintf(fp, "EMAIL_FOLDER=%s\n", cfg->folder ? cfg->folder : "INBOX");
      45            2 :     if (cfg->sent_folder) fprintf(fp, "EMAIL_SENT_FOLDER=%s\n", cfg->sent_folder);
      46            2 :     if (cfg->ssl_no_verify) fprintf(fp, "SSL_NO_VERIFY=1\n");
      47            2 :     fprintf(fp, "SYNC_INTERVAL=%d\n", cfg->sync_interval);
      48            2 :     if (cfg->smtp_host) fprintf(fp, "SMTP_HOST=%s\n", cfg->smtp_host);
      49            2 :     if (cfg->smtp_port) fprintf(fp, "SMTP_PORT=%d\n", cfg->smtp_port);
      50            2 :     if (cfg->smtp_user) fprintf(fp, "SMTP_USER=%s\n", cfg->smtp_user);
      51            2 :     if (cfg->smtp_pass) fprintf(fp, "SMTP_PASS=%s\n", cfg->smtp_pass);
      52            2 : }
      53              : 
      54              : /** Load a config from a specific file path. */
      55           64 : static Config *load_config_from_path(const char *path) {
      56           64 :     FILE *fp = fopen(path, "r");
      57           64 :     if (!fp) return NULL;
      58              : 
      59           63 :     Config *cfg = calloc(1, sizeof(Config));
      60           63 :     if (!cfg) { fclose(fp); return NULL; }
      61              : 
      62           63 :     char line[512];
      63          589 :     while (fgets(line, sizeof(line), fp)) {
      64          526 :         char *key = strtok(line, "=");
      65          526 :         char *val = strtok(NULL, "\n");
      66          526 :         if (!key || !val) continue;
      67          526 :         key = trim(key); val = trim(val);
      68          526 :         if (strcmp(key, "EMAIL_HOST") == 0) cfg->host = strdup(val);
      69          464 :         else if (strcmp(key, "EMAIL_USER") == 0) cfg->user = strdup(val);
      70          401 :         else if (strcmp(key, "EMAIL_PASS") == 0) cfg->pass = strdup(val);
      71          338 :         else if (strcmp(key, "EMAIL_FOLDER") == 0) cfg->folder = strdup(val);
      72          277 :         else if (strcmp(key, "EMAIL_SENT_FOLDER") == 0) cfg->sent_folder = strdup(val);
      73          218 :         else if (strcmp(key, "SSL_NO_VERIFY") == 0) cfg->ssl_no_verify = atoi(val);
      74          158 :         else if (strcmp(key, "SYNC_INTERVAL") == 0) cfg->sync_interval = atoi(val);
      75          156 :         else if (strcmp(key, "SMTP_HOST") == 0) cfg->smtp_host = strdup(val);
      76          104 :         else if (strcmp(key, "SMTP_PORT") == 0) cfg->smtp_port = atoi(val);
      77          104 :         else if (strcmp(key, "SMTP_USER") == 0) cfg->smtp_user = strdup(val);
      78           52 :         else if (strcmp(key, "SMTP_PASS") == 0) cfg->smtp_pass = strdup(val);
      79              :     }
      80           63 :     fclose(fp);
      81           63 :     if (!cfg->folder) cfg->folder = strdup("INBOX");
      82           63 :     if (!cfg->host || !cfg->user || !cfg->pass) { config_free(cfg); return NULL; }
      83              : 
      84              :     /* TLS enforcement */
      85           62 :     if (!cfg->ssl_no_verify) {
      86            2 :         if (strncmp(cfg->host, "imaps://", 8) != 0) {
      87            1 :             fprintf(stderr,
      88              :                 "Error: EMAIL_HOST must start with imaps:// (TLS required).\n"
      89              :                 "  Got: %s\n", cfg->host);
      90            1 :             logger_log(LOG_ERROR,
      91              :                        "Rejected insecure EMAIL_HOST in account config: %s",
      92              :                        cfg->host);
      93            1 :             config_free(cfg);
      94            1 :             return NULL;
      95              :         }
      96            1 :         if (cfg->smtp_host && cfg->smtp_host[0] &&
      97            0 :             strncmp(cfg->smtp_host, "smtps://", 8) != 0) {
      98            0 :             fprintf(stderr,
      99              :                 "Error: SMTP_HOST must start with smtps:// (TLS required).\n"
     100              :                 "  Got: %s\n", cfg->smtp_host);
     101            0 :             logger_log(LOG_ERROR,
     102              :                        "Rejected insecure SMTP_HOST in account config: %s",
     103              :                        cfg->smtp_host);
     104            0 :             config_free(cfg);
     105            0 :             return NULL;
     106              :         }
     107              :     } else {
     108           60 :         if (strncmp(cfg->host, "imaps://", 8) != 0)
     109            0 :             logger_log(LOG_WARN,
     110              :                        "SSL_NO_VERIFY=1: connecting without TLS to %s "
     111              :                        "(test/dev mode only)", cfg->host);
     112           60 :         if (cfg->smtp_host && cfg->smtp_host[0] &&
     113           52 :             strncmp(cfg->smtp_host, "smtps://", 8) != 0)
     114            0 :             logger_log(LOG_WARN,
     115              :                        "SSL_NO_VERIFY=1: SMTP without TLS to %s "
     116              :                        "(test/dev mode only)", cfg->smtp_host);
     117              :     }
     118           61 :     return cfg;
     119              : }
     120              : 
     121              : /* ── Public API ──────────────────────────────────────────────────────────── */
     122              : 
     123           57 : Config* config_load_from_store(void) {
     124              :     /* Reuse config_list_accounts which loads and sorts all accounts
     125              :      * alphabetically.  Take the first entry (lowest name) for a
     126              :      * deterministic result regardless of readdir ordering. */
     127           57 :     int count = 0;
     128           57 :     AccountEntry *list = config_list_accounts(&count);
     129           57 :     if (!list || count == 0) {
     130            3 :         config_free_account_list(list, count);
     131            3 :         return NULL;
     132              :     }
     133           54 :     Config *result = list[0].cfg;
     134           54 :     list[0].cfg = NULL; /* transfer ownership */
     135           54 :     config_free_account_list(list, count);
     136           54 :     return result;
     137              : }
     138              : 
     139            4 : int config_save_account(const Config *cfg) {
     140            4 :     if (!cfg || !cfg->user || !cfg->user[0]) return -1;
     141              : 
     142            8 :     RAII_STRING char *accounts_dir = get_accounts_dir();
     143            4 :     if (!accounts_dir) return -1;
     144              : 
     145            4 :     char account_dir[1024];
     146            4 :     snprintf(account_dir, sizeof(account_dir), "%s/%s", accounts_dir, cfg->user);
     147              : 
     148            4 :     if (fs_mkdir_p(account_dir, 0700) != 0) return -1;
     149              : 
     150            3 :     char path[1088];
     151            3 :     snprintf(path, sizeof(path), "%s/config.ini", account_dir);
     152              : 
     153            3 :     FILE *fp = fopen(path, "w");
     154            3 :     if (!fp) return -1;
     155            2 :     write_config_to_fp(fp, cfg);
     156            2 :     fclose(fp);
     157            2 :     fs_ensure_permissions(path, 0600);
     158              : 
     159            2 :     logger_log(LOG_INFO, "Account saved: %s", cfg->user);
     160            2 :     return 0;
     161              : }
     162              : 
     163            4 : int config_save_to_store(const Config *cfg) {
     164            4 :     return config_save_account(cfg);
     165              : }
     166              : 
     167            0 : int config_delete_account(const char *name) {
     168            0 :     if (!name || !name[0]) return -1;
     169              : 
     170            0 :     RAII_STRING char *accounts_dir = get_accounts_dir();
     171            0 :     if (!accounts_dir) return -1;
     172              : 
     173            0 :     char path[1024];
     174            0 :     snprintf(path, sizeof(path), "%s/%s/config.ini", accounts_dir, name);
     175            0 :     unlink(path);
     176              : 
     177            0 :     char dir[1024];
     178            0 :     snprintf(dir, sizeof(dir), "%s/%s", accounts_dir, name);
     179            0 :     if (rmdir(dir) != 0 && errno != ENOENT) {
     180            0 :         logger_log(LOG_WARN, "Could not remove account dir %s", dir);
     181            0 :         return -1;
     182              :     }
     183            0 :     logger_log(LOG_INFO, "Account deleted: %s", name);
     184            0 :     return 0;
     185              : }
     186              : 
     187           57 : AccountEntry *config_list_accounts(int *count_out) {
     188           57 :     *count_out = 0;
     189              : 
     190          114 :     RAII_STRING char *accounts_dir = get_accounts_dir();
     191           57 :     if (!accounts_dir) return NULL;
     192              : 
     193           57 :     DIR *d = opendir(accounts_dir);
     194           57 :     if (!d) return NULL;
     195              : 
     196           57 :     int cap = 8;
     197           57 :     AccountEntry *list = malloc((size_t)cap * sizeof(AccountEntry));
     198           57 :     if (!list) { closedir(d); return NULL; }
     199           57 :     int count = 0;
     200              : 
     201              :     struct dirent *ent;
     202          235 :     while ((ent = readdir(d)) != NULL) {
     203          178 :         if (ent->d_name[0] == '.') continue;
     204              : 
     205           64 :         char path[1024];
     206           64 :         snprintf(path, sizeof(path), "%s/%s/config.ini",
     207           64 :                  accounts_dir, ent->d_name);
     208              : 
     209           64 :         Config *cfg = load_config_from_path(path);
     210           64 :         if (!cfg) continue;
     211              : 
     212           61 :         if (count >= cap) {
     213            0 :             cap *= 2;
     214            0 :             AccountEntry *tmp = realloc(list, (size_t)cap * sizeof(AccountEntry));
     215            0 :             if (!tmp) { config_free(cfg); break; }
     216            0 :             list = tmp;
     217              :         }
     218           61 :         list[count].name = strdup(ent->d_name);
     219           61 :         list[count].cfg  = cfg;
     220           61 :         count++;
     221              :     }
     222           57 :     closedir(d);
     223              : 
     224           57 :     if (count == 0) { free(list); return NULL; }
     225              : 
     226              :     /* Sort alphabetically by name for consistent ordering. */
     227           61 :     for (int i = 0; i < count - 1; i++) {
     228           16 :         for (int j = i + 1; j < count; j++) {
     229            9 :             if (strcmp(list[i].name, list[j].name) > 0) {
     230            3 :                 AccountEntry tmp = list[i];
     231            3 :                 list[i] = list[j];
     232            3 :                 list[j] = tmp;
     233              :             }
     234              :         }
     235              :     }
     236              : 
     237           54 :     *count_out = count;
     238           54 :     return list;
     239              : }
     240              : 
     241           57 : void config_free_account_list(AccountEntry *list, int count) {
     242           57 :     if (!list) return;
     243          115 :     for (int i = 0; i < count; i++) {
     244           61 :         free(list[i].name);
     245           61 :         config_free(list[i].cfg);
     246              :     }
     247           54 :     free(list);
     248              : }
        

Generated by: LCOV version 2.0-1