LCOV - code coverage report
Current view: top level - libemail/src/infrastructure - setup_wizard.c (source / functions) Coverage Total Hit
Test: coverage-functional.info Lines: 89.0 % 263 234
Test Date: 2026-05-07 15:53:08 Functions: 100.0 % 8 8

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

Generated by: LCOV version 2.0-1