Line data Source code
1 : #include "setup_wizard.h"
2 : #include "gmail_auth.h"
3 : #include "platform/terminal.h"
4 : #include <stdio.h>
5 : #include <stdlib.h>
6 : #include <string.h>
7 : #include <unistd.h>
8 :
9 : #ifndef GMAIL_DEFAULT_CLIENT_ID
10 : #define GMAIL_DEFAULT_CLIENT_ID ""
11 : #endif
12 :
13 85 : static char* get_input(const char *prompt, int hide, FILE *stream) {
14 85 : int is_tty = isatty(fileno(stream));
15 :
16 85 : if (hide && stream == stdin) {
17 : /* Delegate password reading (with echo suppression) to the platform layer.
18 : * terminal_read_password() reads from stdin and handles TTY detection. */
19 : char buf[512];
20 14 : int n = terminal_read_password(prompt, buf, sizeof(buf));
21 14 : if (n < 0) return NULL;
22 14 : return strdup(buf);
23 : }
24 :
25 71 : if (stream == stdin && is_tty) {
26 7 : printf("%s: ", prompt);
27 7 : fflush(stdout);
28 : }
29 :
30 71 : char *line = NULL;
31 71 : size_t len = 0;
32 71 : if (getline(&line, &len, stream) == -1) {
33 3 : free(line);
34 3 : return NULL;
35 : }
36 :
37 : /* Remove newline */
38 64 : line[strcspn(line, "\r\n")] = 0;
39 64 : return line;
40 : }
41 :
42 : /**
43 : * Normalise a user-supplied IMAP host string.
44 : *
45 : * Rules:
46 : * - No protocol (no "://"): prepend "imaps://" automatically.
47 : * - "imaps://…": accepted as-is.
48 : * - Anything else with "://": wrong / unsupported protocol → return NULL.
49 : *
50 : * Returns a newly-allocated string the caller must free(), or NULL on error.
51 : */
52 10 : static char *normalize_imap_host(const char *input) {
53 10 : if (!input || !input[0]) return NULL;
54 7 : if (strstr(input, "://") == NULL) {
55 : /* Plain hostname (or host:port) — add imaps:// */
56 5 : size_t n = strlen(input);
57 5 : char *out = malloc(n + 9);
58 5 : if (!out) return NULL;
59 5 : memcpy(out, "imaps://", 8);
60 5 : memcpy(out + 8, input, n + 1);
61 5 : return out;
62 : }
63 2 : if (strncmp(input, "imaps://", 8) == 0)
64 1 : return strdup(input);
65 1 : return NULL; /* explicit but unsupported protocol */
66 : }
67 :
68 : /**
69 : * Normalise a user-supplied SMTP host string.
70 : *
71 : * Rules:
72 : * - No protocol (no "://"): prepend "smtps://" automatically.
73 : * - "smtps://…": accepted as-is.
74 : * - Anything else with "://": wrong / unsupported protocol → return NULL.
75 : */
76 6 : static char *normalize_smtp_host(const char *input) {
77 6 : if (!input || !input[0]) return NULL;
78 6 : if (strstr(input, "://") == NULL) {
79 3 : size_t n = strlen(input);
80 3 : char *out = malloc(n + 9);
81 3 : if (!out) return NULL;
82 3 : memcpy(out, "smtps://", 8);
83 3 : memcpy(out + 8, input, n + 1);
84 3 : return out;
85 : }
86 3 : if (strncmp(input, "smtps://", 8) == 0)
87 1 : return strdup(input);
88 2 : return NULL;
89 : }
90 :
91 : /**
92 : * @brief Internal wizard implementation that can take any input stream.
93 : */
94 12 : Config* setup_wizard_run_internal(FILE *stream) {
95 12 : int is_tty = isatty(fileno(stream));
96 12 : if (stream == stdin && is_tty) {
97 5 : printf("\n--- email-cli Configuration Wizard ---\n");
98 5 : printf("Please enter your email account details.\n\n");
99 : }
100 :
101 12 : Config *cfg = calloc(1, sizeof(Config));
102 12 : if (!cfg) return NULL;
103 :
104 : /* ── Account type selection ──────────────────────────────────────── */
105 12 : if (stream == stdin && is_tty)
106 5 : printf("Account type:\n [1] IMAP (standard e-mail server)\n"
107 : " [2] Gmail (Google account — uses Gmail API, not IMAP)\n");
108 :
109 12 : char *type_str = get_input("Choice [1]", 0, stream);
110 8 : int account_type = 1;
111 8 : if (type_str && type_str[0] == '2') account_type = 2;
112 8 : free(type_str);
113 :
114 : /* ── Gmail flow ──────────────────────────────────────────────────── */
115 8 : if (account_type == 2) {
116 1 : cfg->gmail_mode = 1;
117 :
118 1 : cfg->user = get_input("Email address", 0, stream);
119 1 : if (!cfg->user || !cfg->user[0]) { config_free(cfg); return NULL; }
120 :
121 1 : if (stream == stdin && is_tty) {
122 : /* Check if OAuth2 credentials are available (compiled-in or config) */
123 0 : const char *cid = cfg->gmail_client_id;
124 0 : int has_builtin = (GMAIL_DEFAULT_CLIENT_ID[0] != '\0');
125 0 : if (!has_builtin && (!cid || !cid[0])) {
126 0 : printf("\n"
127 : " Gmail requires OAuth2 credentials (client_id and client_secret)\n"
128 : " from a Google Cloud project. Run 'email-cli help gmail'\n"
129 : " for a step-by-step guide on how to create them.\n\n");
130 0 : cfg->gmail_client_id = get_input("GMAIL_CLIENT_ID", 0, stream);
131 0 : if (!cfg->gmail_client_id || !cfg->gmail_client_id[0]) {
132 0 : config_free(cfg);
133 0 : return NULL;
134 : }
135 0 : cfg->gmail_client_secret = get_input("GMAIL_CLIENT_SECRET", 0, stream);
136 0 : if (!cfg->gmail_client_secret || !cfg->gmail_client_secret[0]) {
137 0 : config_free(cfg);
138 0 : return NULL;
139 : }
140 : }
141 :
142 0 : if (!is_tty) {
143 : /* Non-interactive: skip OAuth (tests, piped input) */
144 0 : return cfg;
145 : }
146 :
147 0 : printf("\nOpening Gmail authorization...\n");
148 0 : if (gmail_auth_device_flow(cfg) != 0) {
149 0 : fprintf(stderr, "Press Enter to continue...");
150 0 : getc(stdin);
151 0 : config_free(cfg);
152 0 : return NULL;
153 : }
154 0 : printf("Configuration collected.\n");
155 : }
156 1 : return cfg;
157 : }
158 :
159 : /* ── IMAP flow (existing) ────────────────────────────────────────── */
160 7 : if (stream == stdin && is_tty)
161 1 : printf("\n");
162 :
163 3 : for (;;) {
164 10 : char *input = get_input("IMAP Host (e.g. imap.example.com)", 0, stream);
165 10 : if (!input) { config_free(cfg); return NULL; }
166 :
167 : /* Reject Gmail IMAP — Gmail must use the Gmail API (account type 2) */
168 8 : if (strstr(input, "gmail.com") || strstr(input, "google.com") ||
169 7 : strstr(input, "googlemail.com")) {
170 1 : fprintf(stderr,
171 : "Error: Gmail is not supported via IMAP.\n"
172 : " Please re-run the wizard and select account type [2] Gmail,\n"
173 : " which uses the Gmail API with OAuth2.\n");
174 1 : free(input);
175 1 : config_free(cfg);
176 1 : return NULL;
177 : }
178 :
179 7 : char *host = normalize_imap_host(input);
180 7 : if (!host) {
181 3 : fprintf(stderr,
182 : "Error: '%s' uses an unsupported protocol"
183 : " (only imaps:// is supported).\n", input);
184 3 : free(input);
185 3 : continue;
186 : }
187 4 : if (strstr(input, "://") == NULL && stream == stdin && is_tty)
188 0 : printf(" → using imaps://%s\n", input);
189 4 : free(input);
190 4 : cfg->host = host;
191 4 : break;
192 : }
193 :
194 : /* IMAP port (default 993 for imaps://) */
195 4 : char *port_str = get_input("IMAP Port [993]", 0, stream);
196 4 : if (port_str && port_str[0] != '\0') {
197 1 : int port = atoi(port_str);
198 1 : if (port > 0 && port != 993) {
199 : /* Append :port to the URL if not already present */
200 1 : const char *scheme_end = strstr(cfg->host, "://");
201 1 : if (scheme_end && !strchr(scheme_end + 3, ':')) {
202 1 : char *new_host = NULL;
203 1 : if (asprintf(&new_host, "%s:%d", cfg->host, port) != -1) {
204 1 : free(cfg->host);
205 1 : cfg->host = new_host;
206 : }
207 : }
208 : }
209 : }
210 4 : free(port_str);
211 :
212 4 : cfg->user = get_input("Email Username", 0, stream);
213 4 : if (!cfg->user) { config_free(cfg); return NULL; }
214 :
215 4 : cfg->pass = get_input("Email Password", 1, stream);
216 4 : if (!cfg->pass) { config_free(cfg); return NULL; }
217 :
218 4 : cfg->folder = get_input("Default Folder [INBOX]", 0, stream);
219 4 : if (!cfg->folder || strlen(cfg->folder) == 0) {
220 1 : if (cfg->folder) free(cfg->folder);
221 1 : cfg->folder = strdup("INBOX");
222 : }
223 :
224 : /* SMTP configuration (optional) */
225 4 : if (stream == stdin && is_tty)
226 0 : printf("\n--- SMTP (outgoing mail) — press Enter to skip ---\n");
227 :
228 4 : char *smtp_host = NULL;
229 1 : for (;;) {
230 5 : char *input = get_input("SMTP Host [Enter=skip] (e.g. smtp.example.com)", 0, stream);
231 5 : if (!input || !input[0]) { free(input); break; }
232 3 : char *h = normalize_smtp_host(input);
233 3 : if (!h) {
234 1 : fprintf(stderr,
235 : "Error: '%s' uses an unsupported protocol"
236 : " (only smtps:// is supported).\n", input);
237 1 : free(input);
238 1 : continue;
239 : }
240 2 : if (strstr(input, "://") == NULL && stream == stdin && is_tty)
241 0 : printf(" → using smtps://%s\n", input);
242 2 : free(input);
243 2 : smtp_host = h;
244 2 : break;
245 : }
246 4 : if (smtp_host) {
247 2 : cfg->smtp_host = smtp_host;
248 :
249 2 : char *port_str = get_input("SMTP Port [587]", 0, stream);
250 2 : if (port_str && port_str[0] != '\0')
251 1 : cfg->smtp_port = atoi(port_str);
252 : else
253 1 : cfg->smtp_port = 587;
254 2 : free(port_str);
255 :
256 2 : char *su = get_input("SMTP Username [Enter=same as IMAP]", 0, stream);
257 2 : if (su && su[0] != '\0')
258 1 : cfg->smtp_user = su;
259 : else
260 1 : free(su);
261 :
262 2 : char *sp = get_input("SMTP Password [Enter=same as IMAP]", 1, stream);
263 2 : if (sp && sp[0] != '\0')
264 1 : cfg->smtp_pass = sp;
265 : else
266 1 : free(sp);
267 : }
268 :
269 4 : if (stream == stdin && is_tty)
270 0 : printf("\nConfiguration collected. Checking connection...\n");
271 :
272 4 : return cfg;
273 : }
274 :
275 12 : Config* setup_wizard_run(void) {
276 12 : return setup_wizard_run_internal(stdin);
277 : }
278 :
279 : /* ── SMTP sub-wizard ─────────────────────────────────────────────────── */
280 :
281 : /**
282 : * Derive a plausible default SMTP URL from the IMAP URL in cfg->host.
283 : * Writes into buf (size bytes). buf[0] == 0 if derivation is not possible.
284 : */
285 4 : static void derive_smtp_url(const Config *cfg, char *buf, size_t size) {
286 4 : buf[0] = '\0';
287 4 : if (!cfg->host) return;
288 4 : if (strncmp(cfg->host, "imaps://", 8) == 0)
289 4 : snprintf(buf, size, "smtps://%s", cfg->host + 8);
290 : /* imap:// is no longer accepted; no derivation for it */
291 : }
292 :
293 4 : int setup_wizard_smtp(Config *cfg) {
294 4 : printf("\n--- SMTP (outgoing mail) configuration ---\n");
295 4 : printf("Press Enter on any field to keep the shown default.\n\n");
296 :
297 : /* ── SMTP Host ────────────────────────────────────────────────── */
298 : char derived[512];
299 4 : derive_smtp_url(cfg, derived, sizeof(derived));
300 :
301 1 : for (;;) {
302 : char host_prompt[1024];
303 5 : if (cfg->smtp_host)
304 4 : snprintf(host_prompt, sizeof(host_prompt),
305 : "SMTP Host [current: %s]", cfg->smtp_host);
306 1 : else if (derived[0])
307 1 : snprintf(host_prompt, sizeof(host_prompt),
308 : "SMTP Host [Enter = %s] (e.g. smtp.example.com)", derived);
309 : else
310 0 : snprintf(host_prompt, sizeof(host_prompt),
311 : "SMTP Host (e.g. smtp.example.com)");
312 :
313 5 : char *input = get_input(host_prompt, 0, stdin);
314 5 : if (!input) return -1; /* EOF / Ctrl-D → abort */
315 5 : if (!input[0]) {
316 : /* Empty → keep current or use derived default */
317 2 : free(input);
318 2 : if (!cfg->smtp_host && derived[0])
319 1 : cfg->smtp_host = strdup(derived);
320 2 : break;
321 : }
322 3 : char *host = normalize_smtp_host(input);
323 3 : if (!host) {
324 1 : fprintf(stderr,
325 : "Error: '%s' uses an unsupported protocol"
326 : " (only smtps:// is supported).\n", input);
327 1 : free(input);
328 1 : continue;
329 : }
330 2 : if (strstr(input, "://") == NULL)
331 1 : printf(" → using smtps://%s\n", input);
332 2 : free(input);
333 2 : free(cfg->smtp_host);
334 2 : cfg->smtp_host = host;
335 2 : break;
336 : }
337 :
338 : /* ── SMTP Port ────────────────────────────────────────────────── */
339 4 : int cur_port = cfg->smtp_port ? cfg->smtp_port : 587;
340 : char port_prompt[64];
341 4 : snprintf(port_prompt, sizeof(port_prompt), "SMTP Port [%d]", cur_port);
342 4 : char *port_str = get_input(port_prompt, 0, stdin);
343 4 : if (port_str && port_str[0])
344 1 : cfg->smtp_port = atoi(port_str);
345 : else
346 3 : cfg->smtp_port = cur_port;
347 4 : free(port_str);
348 :
349 : /* ── SMTP Username ────────────────────────────────────────────── */
350 : char user_prompt[768];
351 4 : if (cfg->smtp_user)
352 2 : snprintf(user_prompt, sizeof(user_prompt),
353 : "SMTP Username [current: %s]", cfg->smtp_user);
354 : else
355 2 : snprintf(user_prompt, sizeof(user_prompt),
356 : "SMTP Username [Enter = same as IMAP (%s)]",
357 2 : cfg->user ? cfg->user : "");
358 4 : char *su = get_input(user_prompt, 0, stdin);
359 4 : if (su && su[0]) {
360 1 : free(cfg->smtp_user);
361 1 : cfg->smtp_user = su;
362 : } else {
363 3 : free(su);
364 : /* NULL means "use IMAP username" — keep as-is */
365 : }
366 :
367 : /* ── SMTP Password ────────────────────────────────────────────── */
368 : char pass_prompt[64];
369 4 : snprintf(pass_prompt, sizeof(pass_prompt), "%s",
370 4 : cfg->smtp_pass ? "SMTP Password [Enter = keep current]"
371 : : "SMTP Password [Enter = same as IMAP]");
372 4 : char *sp = get_input(pass_prompt, 1, stdin);
373 4 : if (sp && sp[0]) {
374 1 : free(cfg->smtp_pass);
375 1 : cfg->smtp_pass = sp;
376 : } else {
377 3 : free(sp);
378 : /* NULL means "use IMAP password" — keep as-is */
379 : }
380 :
381 4 : printf("\nSMTP configuration updated.\n");
382 4 : return 0;
383 : }
384 :
385 : /* ── IMAP sub-wizard ─────────────────────────────────────────────────── */
386 :
387 5 : int setup_wizard_imap(Config *cfg) {
388 5 : printf("\n--- IMAP (incoming mail) configuration ---\n");
389 5 : printf("Press Enter on any field to keep the shown default.\n\n");
390 :
391 : /* ── IMAP Host ────────────────────────────────────────────────── */
392 1 : for (;;) {
393 : char host_prompt[1024];
394 6 : if (cfg->host)
395 6 : snprintf(host_prompt, sizeof(host_prompt),
396 : "IMAP Host [current: %s] (e.g. imap.example.com)", cfg->host);
397 : else
398 0 : snprintf(host_prompt, sizeof(host_prompt),
399 : "IMAP Host (e.g. imap.example.com)");
400 :
401 6 : char *input = get_input(host_prompt, 0, stdin);
402 6 : if (!input) return -1; /* EOF / Ctrl-D → abort */
403 5 : if (!input[0]) {
404 2 : free(input);
405 2 : break; /* keep current */
406 : }
407 3 : char *host = normalize_imap_host(input);
408 3 : if (!host) {
409 1 : fprintf(stderr,
410 : "Error: '%s' uses an unsupported protocol"
411 : " (only imaps:// is supported).\n", input);
412 1 : free(input);
413 1 : continue; /* re-prompt */
414 : }
415 2 : if (strstr(input, "://") == NULL)
416 1 : printf(" → using imaps://%s\n", input);
417 2 : free(input);
418 2 : free(cfg->host);
419 2 : cfg->host = host;
420 2 : break;
421 : }
422 :
423 : /* ── IMAP Username ────────────────────────────────────────────── */
424 : char user_prompt[768];
425 4 : if (cfg->user)
426 4 : snprintf(user_prompt, sizeof(user_prompt),
427 : "IMAP Username [current: %s]", cfg->user);
428 : else
429 0 : snprintf(user_prompt, sizeof(user_prompt), "IMAP Username");
430 :
431 4 : char *user = get_input(user_prompt, 0, stdin);
432 4 : if (user && user[0]) {
433 1 : free(cfg->user);
434 1 : cfg->user = user;
435 : } else {
436 3 : free(user);
437 : }
438 :
439 : /* ── IMAP Password ────────────────────────────────────────────── */
440 8 : const char *pass_prompt = cfg->pass
441 : ? "IMAP Password [Enter=keep current]"
442 4 : : "IMAP Password";
443 4 : char *pass = get_input(pass_prompt, 1, stdin);
444 4 : if (pass && pass[0]) {
445 1 : free(cfg->pass);
446 1 : cfg->pass = pass;
447 : } else {
448 3 : free(pass);
449 : }
450 :
451 : /* ── Default Folder ───────────────────────────────────────────── */
452 : char folder_prompt[768];
453 4 : const char *cur_folder = cfg->folder ? cfg->folder : "INBOX";
454 4 : snprintf(folder_prompt, sizeof(folder_prompt),
455 : "Default Folder [current: %s]", cur_folder);
456 :
457 4 : char *folder = get_input(folder_prompt, 0, stdin);
458 4 : if (folder && folder[0]) {
459 1 : free(cfg->folder);
460 1 : cfg->folder = folder;
461 : } else {
462 3 : free(folder);
463 3 : if (!cfg->folder)
464 0 : cfg->folder = strdup("INBOX");
465 : }
466 :
467 4 : printf("\nIMAP configuration updated.\n");
468 4 : return 0;
469 : }
|