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 : }
|