Line data Source code
1 : /**
2 : * @file test_pty_compose_dialog.c
3 : * @brief PTY tests for the pre-compose dialog (US-CD-1 … US-CD-12).
4 : *
5 : * Covers: new message dialog, reply dialog, reply-all (A), forward (F),
6 : * contact autocomplete, Cc/Bcc field handling, and field navigation.
7 : *
8 : * Usage: test-pty-compose-dialog <email-tui> <mock-imap-server> <mock-smtp-server>
9 : *
10 : * The pre-compose dialog runs before the editor opens, so these tests
11 : * never need real vim interaction. The EDITOR env var is set to a tiny
12 : * shell script that appends "Test body\n" to the draft and exits 0.
13 : */
14 :
15 : #define _DEFAULT_SOURCE
16 : #define _XOPEN_SOURCE 600
17 :
18 : #include "ptytest.h"
19 : #include "pty_assert.h"
20 :
21 : #include <fcntl.h>
22 : #include <netinet/in.h>
23 : #include <signal.h>
24 : #include <stdio.h>
25 : #include <stdlib.h>
26 : #include <string.h>
27 : #include <sys/socket.h>
28 : #include <sys/stat.h>
29 : #include <sys/wait.h>
30 : #include <unistd.h>
31 :
32 : /* ── Test globals ────────────────────────────────────────────────────── */
33 :
34 : int g_tests_run = 0;
35 : int g_tests_failed = 0;
36 :
37 : static char g_tui_bin[512];
38 : static char g_imap_bin[512];
39 : static char g_smtp_bin[512];
40 :
41 : /* EMAIL_USER value in the generated config — the local store names the
42 : * data directory after it (the .config directory name is unrelated). */
43 : #define TEST_ACCOUNT "testuser@example.com"
44 :
45 : static char g_test_home[512];
46 : static char g_old_home[512];
47 : static char g_editor_script[600]; /* path to fake editor script */
48 :
49 : static pid_t g_imap_pid = -1;
50 : static pid_t g_smtp_pid = -1;
51 :
52 : #define WAIT_MS 6000
53 : #define SETTLE_MS 400
54 : #define ROWS 24
55 : #define COLS 120
56 :
57 : #define RUN_TEST(fn) do { printf(" %s...\n", #fn); fflush(stdout); fn(); } while(0)
58 :
59 : /* ── Fake editor ─────────────────────────────────────────────────────── */
60 :
61 : /*
62 : * Write a tiny shell script that appends "Test body\n" to the first
63 : * argument (the draft file) then exits 0. This lets us simulate the
64 : * user writing a body without actually launching vim.
65 : */
66 342 : static void write_editor_script(void) {
67 342 : snprintf(g_editor_script, sizeof(g_editor_script),
68 : "%s/.fake_editor.sh", g_test_home);
69 342 : FILE *f = fopen(g_editor_script, "w");
70 342 : if (!f) return;
71 342 : fprintf(f, "#!/bin/sh\necho 'Test body' >> \"$1\"\n");
72 342 : fclose(f);
73 342 : chmod(g_editor_script, 0755);
74 342 : setenv("EDITOR", g_editor_script, 1);
75 : }
76 :
77 : /* ── Config helpers ──────────────────────────────────────────────────── */
78 :
79 2076 : static void mkdirs(void) {
80 : char d[600];
81 2076 : snprintf(d, sizeof(d), "%s/.config", g_test_home); mkdir(d, 0700);
82 2076 : snprintf(d, sizeof(d), "%s/.config/email-cli", g_test_home); mkdir(d, 0700);
83 2076 : snprintf(d, sizeof(d), "%s/.config/email-cli/accounts", g_test_home); mkdir(d, 0700);
84 2076 : snprintf(d, sizeof(d), "%s/.config/email-cli/accounts/testuser", g_test_home); mkdir(d, 0700);
85 2076 : snprintf(d, sizeof(d), "%s/.local", g_test_home); mkdir(d, 0700);
86 2076 : snprintf(d, sizeof(d), "%s/.local/share", g_test_home); mkdir(d, 0700);
87 2076 : snprintf(d, sizeof(d), "%s/.local/share/email-cli", g_test_home); mkdir(d, 0700);
88 2076 : snprintf(d, sizeof(d), "%s/.local/share/email-cli/accounts", g_test_home); mkdir(d, 0700);
89 2076 : snprintf(d, sizeof(d), "%s/.local/share/email-cli/accounts/" TEST_ACCOUNT "", g_test_home); mkdir(d, 0700);
90 2076 : snprintf(d, sizeof(d), "%s/.local/share/email-cli/accounts/" TEST_ACCOUNT "/manifests", g_test_home); mkdir(d, 0700);
91 2076 : }
92 :
93 2014 : static void write_config(void) {
94 2014 : mkdirs();
95 : char path[700];
96 2014 : snprintf(path, sizeof(path),
97 : "%s/.config/email-cli/accounts/testuser/config.ini", g_test_home);
98 2014 : FILE *fp = fopen(path, "w");
99 2014 : if (!fp) return;
100 2014 : fprintf(fp,
101 : "EMAIL_HOST=imaps://localhost:9993\n"
102 : "EMAIL_USER=testuser@example.com\n"
103 : "EMAIL_PASS=testpass\n"
104 : "EMAIL_FOLDER=INBOX\n"
105 : "SMTP_HOST=smtps://localhost:9025\n"
106 : "SMTP_PORT=9025\n"
107 : "SMTP_USER=testuser@example.com\n"
108 : "SMTP_PASS=testpass\n"
109 : "SSL_NO_VERIFY=1\n");
110 2014 : fclose(fp);
111 2014 : chmod(path, 0600);
112 : }
113 :
114 : /* ── Contacts seed helper ─────────────────────────────────────────────── */
115 :
116 : /*
117 : * Seed contacts.tsv so autocomplete has entries without needing real .hdr files.
118 : * Format: address\tdisplay_name\tfrequency
119 : */
120 431 : static void write_contacts_tsv(void) {
121 : char path[700];
122 431 : snprintf(path, sizeof(path),
123 : "%s/.local/share/email-cli/accounts/" TEST_ACCOUNT "/contacts.tsv",
124 : g_test_home);
125 431 : FILE *f = fopen(path, "w");
126 431 : if (!f) return;
127 431 : fprintf(f, "alice@example.com\tAlice Example\t10\n");
128 431 : fprintf(f, "bob.smith@acme.com\tBob Smith\t8\n");
129 431 : fprintf(f, "carol.jones@corp.org\tCarol Jones\t5\n");
130 431 : fprintf(f, "john.doe@example.com\tJohn Doe\t3\n");
131 431 : fprintf(f, "johnathan.green@partner.net\tJohnathan Green\t2\n");
132 431 : fclose(f);
133 : }
134 :
135 : /* ── Manifest seed helper ──────────────────────────────────────────────── */
136 :
137 : /* Seed an INBOX manifest with one message that has alice@example.com as sender */
138 10 : static void write_inbox_manifest(void) {
139 : char path[700];
140 10 : snprintf(path, sizeof(path),
141 : "%s/.local/share/email-cli/accounts/" TEST_ACCOUNT "/manifests/INBOX.tsv",
142 : g_test_home);
143 10 : FILE *f = fopen(path, "w");
144 10 : if (!f) return;
145 : /* uid\tfrom\tsubject\tdate\tflags */
146 10 : fprintf(f, "0000000000000001\talice@example.com\tHello World\t2026-04-25 10:00\t1\n");
147 10 : fprintf(f, "0000000000000002\tbob.smith@acme.com\tProject Update\t2026-04-25 09:00\t0\n");
148 10 : fclose(f);
149 : }
150 :
151 : /* Seed a contacts.tsv built from manifest From addresses */
152 10 : static void write_contacts_from_manifests(void) {
153 : char path[700];
154 10 : snprintf(path, sizeof(path),
155 : "%s/.local/share/email-cli/accounts/" TEST_ACCOUNT "/contacts.tsv",
156 : g_test_home);
157 10 : FILE *f = fopen(path, "w");
158 10 : if (!f) return;
159 10 : fprintf(f, "alice@example.com\t\t2\n");
160 10 : fprintf(f, "bob.smith@acme.com\tBob Smith\t1\n");
161 10 : fclose(f);
162 : }
163 :
164 : /* ── Mock server management ────────────────────────────────────────────── */
165 :
166 2694 : static int probe_port(int port) {
167 2694 : int fd = socket(AF_INET, SOCK_STREAM, 0);
168 2694 : if (fd < 0) return -1;
169 2694 : struct sockaddr_in a = {
170 : .sin_family = AF_INET,
171 2694 : .sin_port = htons((uint16_t)port),
172 2694 : .sin_addr.s_addr = htonl(INADDR_LOOPBACK)
173 : };
174 2694 : int r = connect(fd, (struct sockaddr *)&a, sizeof(a));
175 2694 : close(fd);
176 2694 : return r;
177 : }
178 :
179 2694 : static void start_server(const char *bin, pid_t *pid_out) {
180 2694 : *pid_out = fork();
181 2783 : if (*pid_out < 0) return;
182 2783 : if (*pid_out == 0) {
183 89 : int devnull = open("/dev/null", O_WRONLY);
184 89 : if (devnull >= 0) { dup2(devnull, 1); dup2(devnull, 2); close(devnull); }
185 89 : execl(bin, bin, (char *)NULL);
186 89 : _exit(127);
187 : }
188 2694 : usleep(800000);
189 : }
190 :
191 2696 : static void stop_server(pid_t *pid_out) {
192 2696 : if (*pid_out > 0) {
193 2572 : kill(*pid_out, SIGKILL);
194 2572 : waitpid(*pid_out, NULL, 0);
195 2572 : *pid_out = -1;
196 : }
197 2696 : }
198 :
199 2291 : static void restart_imap(void) {
200 2291 : stop_server(&g_imap_pid);
201 2291 : usleep(200000);
202 2291 : start_server(g_imap_bin, &g_imap_pid);
203 2291 : for (int i = 0; i < 30 && probe_port(9993) != 0; i++) usleep(100000);
204 2291 : }
205 :
206 403 : static void restart_smtp(void) {
207 403 : stop_server(&g_smtp_pid);
208 403 : usleep(200000);
209 403 : start_server(g_smtp_bin, &g_smtp_pid);
210 403 : for (int i = 0; i < 30 && probe_port(9025) != 0; i++) usleep(100000);
211 403 : }
212 :
213 : /* ── TUI runner ───────────────────────────────────────────────────────── */
214 :
215 1952 : static PtySession *tui_run(const char **extra_args) {
216 : const char *args[32];
217 1952 : int n = 0;
218 1952 : args[n++] = g_tui_bin;
219 1952 : if (extra_args)
220 0 : for (int i = 0; extra_args[i] && n < 31; i++)
221 0 : args[n++] = extra_args[i];
222 1952 : args[n] = NULL;
223 :
224 1952 : PtySession *s = pty_open(COLS, ROWS);
225 1952 : if (!s) return NULL;
226 1952 : if (pty_run(s, args) != 0) { pty_close(s); return NULL; }
227 1891 : return s;
228 : }
229 :
230 : /*
231 : * Open TUI and navigate to the INBOX message list.
232 : * The TUI starts at the accounts screen; press Enter to open the account,
233 : * then Enter again to open the INBOX folder.
234 : */
235 : /* US-85: submitting the compose dialog runs $EDITOR and then shows the
236 : * post-compose review screen. Press 's' there to actually send. */
237 326 : static void pcr_send(PtySession *s) {
238 326 : if (pty_wait_for(s, "Review: Message", WAIT_MS) != 0)
239 0 : printf(" [WARN] pcr_send: review screen did not appear\n");
240 326 : pty_send_str(s, "s");
241 326 : }
242 :
243 1916 : static PtySession *tui_open_to_inbox(void) {
244 1916 : write_config();
245 1916 : PtySession *s = tui_run(NULL);
246 1858 : if (!s) return NULL;
247 : /* Accounts screen → open account */
248 1858 : if (pty_wait_for(s, "testuser", WAIT_MS) != 0) { pty_close(s); return NULL; }
249 1858 : pty_settle(s, SETTLE_MS);
250 1858 : pty_send_key(s, PTY_KEY_ENTER);
251 : /* Folder browser: HOME → VP_UNREAD(1), then 6× DOWN reaches INBOX(8),
252 : * skipping VP_HDR_FOLD which the browser auto-skips on navigation. */
253 1858 : if (pty_wait_for(s, "Folders", WAIT_MS) != 0) { pty_close(s); return NULL; }
254 1858 : pty_send_key(s, PTY_KEY_HOME);
255 13006 : for (int i = 0; i < 6; i++) { pty_send_key(s, PTY_KEY_DOWN); pty_settle(s, 50); }
256 1858 : if (pty_wait_for(s, "INBOX", WAIT_MS) != 0) { pty_close(s); return NULL; }
257 1858 : pty_settle(s, SETTLE_MS);
258 1858 : pty_send_key(s, PTY_KEY_ENTER);
259 1858 : if (pty_wait_for(s, "message(s) in", WAIT_MS) != 0) { pty_close(s); return NULL; }
260 1858 : pty_settle(s, SETTLE_MS);
261 1858 : return s;
262 : }
263 :
264 : /* ══════════════════════════════════════════════════════════════════════
265 : * TC-CD-01 to TC-CD-08: Basic compose dialog flow
266 : * ══════════════════════════════════════════════════════════════════════ */
267 :
268 62 : static void test_compose_dialog_opens(void) {
269 : /* TC-CD-01: pressing 'c' opens the pre-compose dialog, not vim */
270 62 : restart_imap();
271 62 : PtySession *s = tui_open_to_inbox();
272 61 : ASSERT(s != NULL, "compose opens: reached inbox");
273 :
274 61 : pty_send_str(s, "c");
275 61 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
276 61 : pty_settle(s, SETTLE_MS);
277 :
278 : /* Dialog must show all four fields */
279 61 : ASSERT_SCREEN_CONTAINS(s, "To:");
280 61 : ASSERT_SCREEN_CONTAINS(s, "Cc:");
281 61 : ASSERT_SCREEN_CONTAINS(s, "Bcc:");
282 61 : ASSERT_SCREEN_CONTAINS(s, "Subject:");
283 :
284 61 : pty_send_key(s, PTY_KEY_ESC);
285 61 : pty_close(s);
286 : }
287 :
288 61 : static void test_compose_dialog_title(void) {
289 : /* TC-CD-08: dialog title reads "New Message" */
290 61 : restart_imap();
291 61 : PtySession *s = tui_open_to_inbox();
292 60 : ASSERT(s != NULL, "dialog title: reached inbox");
293 :
294 60 : pty_send_str(s, "c");
295 60 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
296 60 : ASSERT_SCREEN_CONTAINS(s, "New Message");
297 :
298 60 : pty_send_key(s, PTY_KEY_ESC);
299 60 : pty_close(s);
300 : }
301 :
302 60 : static void test_compose_dialog_status_line(void) {
303 : /* Status line shows key bindings hint */
304 60 : restart_imap();
305 60 : PtySession *s = tui_open_to_inbox();
306 59 : ASSERT(s != NULL, "status line: reached inbox");
307 :
308 59 : pty_send_str(s, "c");
309 59 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
310 59 : pty_settle(s, SETTLE_MS);
311 : /* At least Tab and ESC must be mentioned */
312 59 : ASSERT_SCREEN_CONTAINS(s, "Tab");
313 59 : ASSERT_SCREEN_CONTAINS(s, "ESC");
314 :
315 59 : pty_send_key(s, PTY_KEY_ESC);
316 59 : pty_close(s);
317 : }
318 :
319 59 : static void test_compose_esc_cancels(void) {
320 : /* TC-CD-02: Esc in compose dialog returns to message list */
321 59 : restart_imap();
322 59 : PtySession *s = tui_open_to_inbox();
323 58 : ASSERT(s != NULL, "esc cancels: reached inbox");
324 :
325 58 : pty_send_str(s, "c");
326 58 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
327 58 : pty_settle(s, SETTLE_MS);
328 :
329 58 : pty_send_key(s, PTY_KEY_ESC);
330 : /* After compose cancel, TUI shows "[Press any key]" before returning to list */
331 58 : ASSERT_WAIT_FOR(s, "[Press any key", WAIT_MS);
332 58 : pty_send_str(s, " ");
333 58 : ASSERT_WAIT_FOR(s, "message(s) in", WAIT_MS);
334 58 : pty_settle(s, SETTLE_MS);
335 : /* Must NOT have opened editor (vim is not running) */
336 58 : ASSERT_SCREEN_NOT_CONTAINS(s, "~"); /* vim empty-buffer tilde markers */
337 :
338 58 : pty_send_key(s, PTY_KEY_ESC);
339 58 : pty_close(s);
340 : }
341 :
342 58 : static void test_compose_empty_to_error(void) {
343 : /* TC-CD-03: Enter with empty To shows inline error */
344 58 : restart_imap();
345 58 : PtySession *s = tui_open_to_inbox();
346 57 : ASSERT(s != NULL, "empty to error: reached inbox");
347 :
348 57 : pty_send_str(s, "c");
349 57 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
350 57 : pty_settle(s, SETTLE_MS);
351 :
352 : /* Press Enter immediately without typing anything */
353 57 : pty_send_key(s, PTY_KEY_ENTER);
354 57 : pty_settle(s, SETTLE_MS);
355 :
356 : /* Inline error visible; dialog still open */
357 57 : ASSERT_SCREEN_CONTAINS(s, "empty");
358 57 : ASSERT_SCREEN_CONTAINS(s, "To:");
359 :
360 57 : pty_send_key(s, PTY_KEY_ESC);
361 57 : pty_close(s);
362 : }
363 :
364 57 : static void test_compose_empty_subject_warning(void) {
365 : /* TC-CD-05/06/07: empty Subject triggers confirmation prompt */
366 57 : restart_imap();
367 57 : PtySession *s = tui_open_to_inbox();
368 56 : ASSERT(s != NULL, "empty subject: reached inbox");
369 :
370 56 : pty_send_str(s, "c");
371 56 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
372 56 : pty_settle(s, SETTLE_MS);
373 :
374 : /* Fill only To */
375 56 : pty_send_str(s, "alice@example.com");
376 56 : pty_send_key(s, PTY_KEY_TAB); /* To → Cc */
377 56 : pty_send_key(s, PTY_KEY_TAB); /* Cc → Bcc */
378 56 : pty_send_key(s, PTY_KEY_TAB); /* Bcc → Subject */
379 : /* Subject is empty; press Enter */
380 56 : pty_send_key(s, PTY_KEY_ENTER);
381 56 : pty_settle(s, SETTLE_MS);
382 :
383 : /* Confirmation prompt */
384 56 : ASSERT_SCREEN_CONTAINS(s, "empty");
385 56 : ASSERT_SCREEN_CONTAINS(s, "y/n");
386 :
387 : /* 'n' → back to Subject */
388 56 : pty_send_str(s, "n");
389 56 : pty_settle(s, SETTLE_MS);
390 56 : ASSERT_SCREEN_CONTAINS(s, "Subject:");
391 56 : ASSERT_SCREEN_NOT_CONTAINS(s, "y/n");
392 :
393 56 : pty_send_key(s, PTY_KEY_ESC);
394 56 : pty_close(s);
395 : }
396 :
397 56 : static void test_compose_empty_subject_y_opens_editor(void) {
398 : /* TC-CD-06: 'y' at the confirmation prompt opens editor */
399 56 : restart_imap(); restart_smtp();
400 56 : PtySession *s = tui_open_to_inbox();
401 55 : ASSERT(s != NULL, "empty subject y: reached inbox");
402 :
403 55 : pty_send_str(s, "c");
404 55 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
405 55 : pty_settle(s, SETTLE_MS);
406 :
407 55 : pty_send_str(s, "alice@example.com");
408 55 : pty_send_key(s, PTY_KEY_TAB);
409 55 : pty_send_key(s, PTY_KEY_TAB);
410 55 : pty_send_key(s, PTY_KEY_TAB); /* reach Subject */
411 55 : pty_send_key(s, PTY_KEY_ENTER);
412 55 : pty_settle(s, SETTLE_MS);
413 55 : ASSERT_SCREEN_CONTAINS(s, "y/n");
414 :
415 55 : pty_send_str(s, "y");
416 : /* Editor (our fake script) runs and returns; TUI continues */
417 55 : pcr_send(s);
418 55 : ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
419 :
420 55 : pty_close(s);
421 : }
422 :
423 55 : static void test_compose_opens_editor_with_to(void) {
424 : /* TC-CD-04: filling To + Subject and pressing Enter opens editor */
425 55 : restart_imap(); restart_smtp();
426 55 : write_editor_script(); /* ensures EDITOR is set */
427 55 : PtySession *s = tui_open_to_inbox();
428 54 : ASSERT(s != NULL, "opens editor: reached inbox");
429 :
430 54 : pty_send_str(s, "c");
431 54 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
432 54 : pty_settle(s, SETTLE_MS);
433 :
434 54 : pty_send_str(s, "alice@example.com");
435 : /* Tab To → Cc → Bcc → Subject */
436 54 : pty_send_key(s, PTY_KEY_TAB);
437 54 : pty_send_key(s, PTY_KEY_TAB);
438 54 : pty_send_key(s, PTY_KEY_TAB);
439 54 : pty_send_str(s, "Test Subject");
440 54 : pty_send_key(s, PTY_KEY_ENTER);
441 :
442 : /* Editor runs and the TUI shows "Sending" */
443 54 : pcr_send(s);
444 54 : ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
445 :
446 54 : pty_close(s);
447 : }
448 :
449 : /* ══════════════════════════════════════════════════════════════════════
450 : * TC-CD-09 to TC-CD-12: Field navigation
451 : * ══════════════════════════════════════════════════════════════════════ */
452 :
453 54 : static void test_compose_tab_to_cc(void) {
454 : /* TC-CD-09: Tab from To field moves cursor to Cc field */
455 54 : restart_imap();
456 54 : PtySession *s = tui_open_to_inbox();
457 53 : ASSERT(s != NULL, "tab to cc: reached inbox");
458 :
459 53 : pty_send_str(s, "c");
460 53 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
461 53 : pty_settle(s, SETTLE_MS);
462 :
463 : /* Tab once — should highlight Cc */
464 53 : pty_send_key(s, PTY_KEY_TAB);
465 53 : pty_settle(s, SETTLE_MS);
466 : /* Cc field must be active (highlighted or indicated) */
467 53 : ASSERT_SCREEN_CONTAINS(s, "Cc:");
468 :
469 53 : pty_send_key(s, PTY_KEY_ESC);
470 53 : pty_close(s);
471 : }
472 :
473 53 : static void test_compose_tab_full_cycle(void) {
474 : /* TC-CD-10: Tab cycles To → Cc → Bcc → Subject → (back to To) */
475 53 : restart_imap();
476 53 : PtySession *s = tui_open_to_inbox();
477 52 : ASSERT(s != NULL, "tab cycle: reached inbox");
478 :
479 52 : pty_send_str(s, "c");
480 52 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
481 52 : pty_settle(s, SETTLE_MS);
482 :
483 : /* 4× Tab should wrap back past Subject to To; dialog still open */
484 260 : for (int i = 0; i < 4; i++) { pty_send_key(s, PTY_KEY_TAB); pty_settle(s, 80); }
485 52 : ASSERT_SCREEN_CONTAINS(s, "New Message"); /* dialog still showing */
486 :
487 52 : pty_send_key(s, PTY_KEY_ESC);
488 52 : pty_close(s);
489 : }
490 :
491 52 : static void test_compose_shift_tab_reverse(void) {
492 : /* TC-CD-11: Shift-Tab moves backwards (Subject → Bcc → Cc → To) */
493 52 : restart_imap();
494 52 : PtySession *s = tui_open_to_inbox();
495 51 : ASSERT(s != NULL, "shift-tab: reached inbox");
496 :
497 51 : pty_send_str(s, "c");
498 51 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
499 51 : pty_settle(s, SETTLE_MS);
500 :
501 : /* Move to Subject */
502 51 : pty_send_key(s, PTY_KEY_TAB);
503 51 : pty_send_key(s, PTY_KEY_TAB);
504 51 : pty_send_key(s, PTY_KEY_TAB);
505 51 : pty_settle(s, SETTLE_MS);
506 :
507 : /* Shift-Tab back */
508 51 : pty_send_str(s, "\x1b[Z"); /* Shift-Tab */
509 51 : pty_settle(s, SETTLE_MS);
510 51 : ASSERT_SCREEN_CONTAINS(s, "Bcc:");
511 :
512 51 : pty_send_str(s, "\x1b[Z"); /* Shift-Tab */
513 51 : pty_settle(s, SETTLE_MS);
514 51 : ASSERT_SCREEN_CONTAINS(s, "Cc:");
515 :
516 51 : pty_send_str(s, "\x1b[Z"); /* Shift-Tab */
517 51 : pty_settle(s, SETTLE_MS);
518 51 : ASSERT_SCREEN_CONTAINS(s, "To:");
519 :
520 51 : pty_send_key(s, PTY_KEY_ESC);
521 51 : pty_close(s);
522 : }
523 :
524 : /* ══════════════════════════════════════════════════════════════════════
525 : * TC-CD-13 to TC-CD-17: Recipient input and multi-address
526 : * ══════════════════════════════════════════════════════════════════════ */
527 :
528 51 : static void test_compose_to_field_accepts_email(void) {
529 : /* TC-CD-13: can type a complete email address in To */
530 51 : restart_imap();
531 51 : PtySession *s = tui_open_to_inbox();
532 50 : ASSERT(s != NULL, "to accepts email: reached inbox");
533 :
534 50 : pty_send_str(s, "c");
535 50 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
536 50 : pty_settle(s, SETTLE_MS);
537 :
538 50 : pty_send_str(s, "alice@example.com");
539 50 : pty_settle(s, SETTLE_MS);
540 50 : ASSERT_SCREEN_CONTAINS(s, "alice@example.com");
541 :
542 50 : pty_send_key(s, PTY_KEY_ESC);
543 50 : pty_close(s);
544 : }
545 :
546 50 : static void test_compose_multiple_to_addresses(void) {
547 : /* TC-CD-14: semicolon commits first address; both appear in field */
548 50 : restart_imap();
549 50 : PtySession *s = tui_open_to_inbox();
550 49 : ASSERT(s != NULL, "multi-to: reached inbox");
551 :
552 49 : pty_send_str(s, "c");
553 49 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
554 49 : pty_settle(s, SETTLE_MS);
555 :
556 49 : pty_send_str(s, "alice@example.com;");
557 49 : pty_settle(s, SETTLE_MS);
558 49 : pty_send_str(s, "bob@example.com");
559 49 : pty_settle(s, SETTLE_MS);
560 :
561 49 : ASSERT_SCREEN_CONTAINS(s, "alice@example.com");
562 49 : ASSERT_SCREEN_CONTAINS(s, "bob@example.com");
563 :
564 49 : pty_send_key(s, PTY_KEY_ESC);
565 49 : pty_close(s);
566 : }
567 :
568 49 : static void test_compose_backspace_removes_address(void) {
569 : /* TC-CD-15: Backspace on empty token removes last committed address */
570 49 : restart_imap();
571 49 : PtySession *s = tui_open_to_inbox();
572 48 : ASSERT(s != NULL, "backspace removes: reached inbox");
573 :
574 48 : pty_send_str(s, "c");
575 48 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
576 48 : pty_settle(s, SETTLE_MS);
577 :
578 48 : pty_send_str(s, "alice@example.com;");
579 48 : pty_settle(s, SETTLE_MS);
580 48 : ASSERT_SCREEN_CONTAINS(s, "alice@example.com");
581 :
582 : /* Token is empty now; Backspace should remove alice's address */
583 48 : pty_send_key(s, PTY_KEY_BACK);
584 48 : pty_settle(s, SETTLE_MS);
585 48 : ASSERT_SCREEN_NOT_CONTAINS(s, "alice@example.com");
586 :
587 48 : pty_send_key(s, PTY_KEY_ESC);
588 48 : pty_close(s);
589 : }
590 :
591 48 : static void test_compose_cc_written_to_tempfile(void) {
592 : /* TC-CD-16: Cc address appears as Cc: header in the editor temp file */
593 48 : restart_imap(); restart_smtp();
594 48 : write_editor_script();
595 48 : PtySession *s = tui_open_to_inbox();
596 47 : ASSERT(s != NULL, "cc header: reached inbox");
597 :
598 47 : pty_send_str(s, "c");
599 47 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
600 47 : pty_settle(s, SETTLE_MS);
601 :
602 47 : pty_send_str(s, "alice@example.com");
603 47 : pty_send_key(s, PTY_KEY_TAB); /* → Cc */
604 47 : pty_send_str(s, "carol@example.org");
605 47 : pty_send_key(s, PTY_KEY_TAB); /* → Bcc */
606 47 : pty_send_key(s, PTY_KEY_TAB); /* → Subject */
607 47 : pty_send_str(s, "Cc test");
608 47 : pty_send_key(s, PTY_KEY_ENTER);
609 :
610 : /* After editor runs, message is sent; the Cc: header must have been
611 : * in the temp file and picked up by the send flow. */
612 47 : pcr_send(s);
613 47 : ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
614 :
615 47 : pty_close(s);
616 : }
617 :
618 47 : static void test_compose_bcc_written_to_tempfile(void) {
619 : /* TC-CD-17: Bcc address appears in Bcc: header in the editor temp file */
620 47 : restart_imap(); restart_smtp();
621 47 : write_editor_script();
622 47 : PtySession *s = tui_open_to_inbox();
623 46 : ASSERT(s != NULL, "bcc header: reached inbox");
624 :
625 46 : pty_send_str(s, "c");
626 46 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
627 46 : pty_settle(s, SETTLE_MS);
628 :
629 46 : pty_send_str(s, "alice@example.com");
630 46 : pty_send_key(s, PTY_KEY_TAB); /* → Cc */
631 46 : pty_send_key(s, PTY_KEY_TAB); /* → Bcc */
632 46 : pty_send_str(s, "hidden@secret.org");
633 46 : pty_send_key(s, PTY_KEY_TAB); /* → Subject */
634 46 : pty_send_str(s, "Bcc test");
635 46 : pty_send_key(s, PTY_KEY_ENTER);
636 :
637 46 : pcr_send(s);
638 46 : ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
639 :
640 46 : pty_close(s);
641 : }
642 :
643 : /* ══════════════════════════════════════════════════════════════════════
644 : * TC-CD-18 to TC-CD-27: Autocomplete
645 : * ══════════════════════════════════════════════════════════════════════ */
646 :
647 46 : static void test_autocomplete_dropdown_appears(void) {
648 : /* TC-CD-18: typing ≥2 matching chars opens autocomplete dropdown */
649 46 : restart_imap();
650 46 : write_contacts_tsv();
651 46 : PtySession *s = tui_open_to_inbox();
652 45 : ASSERT(s != NULL, "autocomplete appears: reached inbox");
653 :
654 45 : pty_send_str(s, "c");
655 45 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
656 45 : pty_settle(s, SETTLE_MS);
657 :
658 45 : pty_send_str(s, "al"); /* matches alice@example.com */
659 45 : ASSERT_WAIT_FOR(s, "alice@example.com", WAIT_MS);
660 :
661 45 : pty_send_key(s, PTY_KEY_ESC);
662 45 : pty_close(s);
663 : }
664 :
665 45 : static void test_autocomplete_filters_realtime(void) {
666 : /* TC-CD-19: typing more chars narrows the dropdown */
667 45 : restart_imap();
668 45 : write_contacts_tsv();
669 45 : PtySession *s = tui_open_to_inbox();
670 44 : ASSERT(s != NULL, "autocomplete filters: reached inbox");
671 :
672 44 : pty_send_str(s, "c");
673 44 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
674 44 : pty_settle(s, SETTLE_MS);
675 :
676 44 : pty_send_str(s, "jo"); /* matches john.doe and johnathan */
677 44 : ASSERT_WAIT_FOR(s, "john", WAIT_MS);
678 44 : pty_settle(s, SETTLE_MS);
679 44 : ASSERT_SCREEN_CONTAINS(s, "john.doe@example.com");
680 44 : ASSERT_SCREEN_CONTAINS(s, "johnathan");
681 :
682 : /* Add 'hn.d' → only john.doe matches */
683 44 : pty_send_str(s, "hn.d");
684 44 : pty_settle(s, SETTLE_MS);
685 44 : ASSERT_SCREEN_CONTAINS(s, "john.doe@example.com");
686 44 : ASSERT_SCREEN_NOT_CONTAINS(s, "johnathan");
687 :
688 44 : pty_send_key(s, PTY_KEY_ESC);
689 44 : pty_close(s);
690 : }
691 :
692 44 : static void test_autocomplete_no_match_no_dropdown(void) {
693 : /* TC-CD-20: no matching contacts → no dropdown shown */
694 44 : restart_imap();
695 44 : write_contacts_tsv();
696 44 : PtySession *s = tui_open_to_inbox();
697 43 : ASSERT(s != NULL, "no dropdown: reached inbox");
698 :
699 43 : pty_send_str(s, "c");
700 43 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
701 43 : pty_settle(s, SETTLE_MS);
702 :
703 43 : pty_send_str(s, "zzz"); /* no contact starts with zzz */
704 43 : pty_settle(s, SETTLE_MS * 2);
705 43 : ASSERT_SCREEN_NOT_CONTAINS(s, "zzz@"); /* no suggestion row */
706 :
707 43 : pty_send_key(s, PTY_KEY_ESC);
708 43 : pty_close(s);
709 : }
710 :
711 43 : static void test_autocomplete_single_char_no_dropdown(void) {
712 : /* TC-CD-21: only 1 char typed → no dropdown yet (threshold is 2) */
713 43 : restart_imap();
714 43 : write_contacts_tsv();
715 43 : PtySession *s = tui_open_to_inbox();
716 42 : ASSERT(s != NULL, "single char: reached inbox");
717 :
718 42 : pty_send_str(s, "c");
719 42 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
720 42 : pty_settle(s, SETTLE_MS);
721 :
722 42 : pty_send_str(s, "a"); /* single char: should not open dropdown */
723 42 : pty_settle(s, SETTLE_MS);
724 : /* Dropdown NOT visible (alice would appear but threshold not met) */
725 42 : ASSERT_SCREEN_NOT_CONTAINS(s, "alice@example.com");
726 :
727 42 : pty_send_key(s, PTY_KEY_ESC);
728 42 : pty_close(s);
729 : }
730 :
731 42 : static void test_autocomplete_down_highlights_second(void) {
732 : /* TC-CD-22: Down arrow highlights second entry in dropdown */
733 42 : restart_imap();
734 42 : write_contacts_tsv();
735 42 : PtySession *s = tui_open_to_inbox();
736 41 : ASSERT(s != NULL, "down arrow: reached inbox");
737 :
738 41 : pty_send_str(s, "c");
739 41 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
740 41 : pty_settle(s, SETTLE_MS);
741 :
742 41 : pty_send_str(s, "jo");
743 41 : ASSERT_WAIT_FOR(s, "john.doe", WAIT_MS);
744 41 : pty_settle(s, SETTLE_MS);
745 :
746 : /* Dropdown open; press Down */
747 41 : pty_send_key(s, PTY_KEY_DOWN);
748 41 : pty_settle(s, SETTLE_MS);
749 : /* Second entry (johnathan) must now be highlighted */
750 41 : ASSERT_SCREEN_CONTAINS(s, "johnathan");
751 :
752 41 : pty_send_key(s, PTY_KEY_ESC);
753 41 : pty_close(s);
754 : }
755 :
756 41 : static void test_autocomplete_enter_selects(void) {
757 : /* TC-CD-23: Enter on dropdown entry appends address and closes dropdown */
758 41 : restart_imap();
759 41 : write_contacts_tsv();
760 41 : PtySession *s = tui_open_to_inbox();
761 40 : ASSERT(s != NULL, "enter selects: reached inbox");
762 :
763 40 : pty_send_str(s, "c");
764 40 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
765 40 : pty_settle(s, SETTLE_MS);
766 :
767 40 : pty_send_str(s, "al");
768 40 : ASSERT_WAIT_FOR(s, "alice@example.com", WAIT_MS);
769 40 : pty_settle(s, SETTLE_MS);
770 :
771 40 : pty_send_key(s, PTY_KEY_ENTER);
772 40 : pty_settle(s, SETTLE_MS);
773 :
774 : /* Address committed; dropdown closed; To field contains alice */
775 40 : ASSERT_SCREEN_CONTAINS(s, "alice@example.com");
776 : /* Dropdown suggestions should be gone */
777 40 : ASSERT_SCREEN_NOT_CONTAINS(s, "john.doe@example.com");
778 :
779 40 : pty_send_key(s, PTY_KEY_ESC);
780 40 : pty_close(s);
781 : }
782 :
783 40 : static void test_autocomplete_tab_selects_and_advances(void) {
784 : /* TC-CD-24: Tab selects top suggestion and advances to next field */
785 40 : restart_imap();
786 40 : write_contacts_tsv();
787 40 : PtySession *s = tui_open_to_inbox();
788 39 : ASSERT(s != NULL, "tab selects: reached inbox");
789 :
790 39 : pty_send_str(s, "c");
791 39 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
792 39 : pty_settle(s, SETTLE_MS);
793 :
794 39 : pty_send_str(s, "al");
795 39 : ASSERT_WAIT_FOR(s, "alice@example.com", WAIT_MS);
796 39 : pty_settle(s, SETTLE_MS);
797 :
798 : /* Tab: select top entry, advance to Cc */
799 39 : pty_send_key(s, PTY_KEY_TAB);
800 39 : pty_settle(s, SETTLE_MS);
801 :
802 39 : ASSERT_SCREEN_CONTAINS(s, "alice@example.com");
803 : /* Now in Cc field; Cc should be the active field */
804 39 : ASSERT_SCREEN_CONTAINS(s, "Cc:");
805 :
806 39 : pty_send_key(s, PTY_KEY_ESC);
807 39 : pty_close(s);
808 : }
809 :
810 39 : static void test_autocomplete_esc_dismisses_keeps_text(void) {
811 : /* TC-CD-25: Esc dismisses dropdown without selecting; typed text remains */
812 39 : restart_imap();
813 39 : write_contacts_tsv();
814 39 : PtySession *s = tui_open_to_inbox();
815 38 : ASSERT(s != NULL, "esc dismisses: reached inbox");
816 :
817 38 : pty_send_str(s, "c");
818 38 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
819 38 : pty_settle(s, SETTLE_MS);
820 :
821 38 : pty_send_str(s, "al");
822 38 : ASSERT_WAIT_FOR(s, "alice@example.com", WAIT_MS);
823 38 : pty_settle(s, SETTLE_MS);
824 :
825 : /* Esc: dismiss dropdown */
826 38 : pty_send_key(s, PTY_KEY_ESC);
827 38 : pty_settle(s, SETTLE_MS);
828 :
829 : /* Dropdown gone but the 'al' text must still be in the To field */
830 38 : ASSERT_SCREEN_NOT_CONTAINS(s, "alice@example.com"); /* dropdown row gone */
831 38 : ASSERT_SCREEN_CONTAINS(s, "al"); /* typed text stays */
832 :
833 38 : pty_send_key(s, PTY_KEY_ESC); /* cancel dialog */
834 38 : pty_close(s);
835 : }
836 :
837 38 : static void test_autocomplete_case_insensitive(void) {
838 : /* TC-CD-26: uppercase input matches lowercase contact */
839 38 : restart_imap();
840 38 : write_contacts_tsv();
841 38 : PtySession *s = tui_open_to_inbox();
842 37 : ASSERT(s != NULL, "case insensitive: reached inbox");
843 :
844 37 : pty_send_str(s, "c");
845 37 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
846 37 : pty_settle(s, SETTLE_MS);
847 :
848 37 : pty_send_str(s, "AL"); /* uppercase, but alice@example.com should match */
849 37 : ASSERT_WAIT_FOR(s, "alice@example.com", WAIT_MS);
850 :
851 37 : pty_send_key(s, PTY_KEY_ESC);
852 37 : pty_close(s);
853 : }
854 :
855 37 : static void test_autocomplete_display_name_match(void) {
856 : /* TC-CD-27: matching on display name also works ("Smith" → bob.smith@) */
857 37 : restart_imap();
858 37 : write_contacts_tsv();
859 37 : PtySession *s = tui_open_to_inbox();
860 36 : ASSERT(s != NULL, "name match: reached inbox");
861 :
862 36 : pty_send_str(s, "c");
863 36 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
864 36 : pty_settle(s, SETTLE_MS);
865 :
866 36 : pty_send_str(s, "Sm"); /* matches "Bob Smith" display name */
867 36 : ASSERT_WAIT_FOR(s, "bob.smith@acme.com", WAIT_MS);
868 :
869 36 : pty_send_key(s, PTY_KEY_ESC);
870 36 : pty_close(s);
871 : }
872 :
873 : /* ══════════════════════════════════════════════════════════════════════
874 : * TC-CD-28 to TC-CD-36: Reply dialog
875 : * ══════════════════════════════════════════════════════════════════════ */
876 :
877 36 : static void test_reply_dialog_opens(void) {
878 : /* TC-CD-28: pressing 'r' opens the reply dialog */
879 36 : restart_imap();
880 36 : PtySession *s = tui_open_to_inbox();
881 35 : ASSERT(s != NULL, "reply opens: reached inbox");
882 :
883 35 : pty_send_str(s, "r");
884 35 : ASSERT_WAIT_FOR(s, "Reply", WAIT_MS);
885 35 : pty_settle(s, SETTLE_MS);
886 :
887 35 : ASSERT_SCREEN_CONTAINS(s, "To:");
888 35 : ASSERT_SCREEN_CONTAINS(s, "Subject:");
889 :
890 35 : pty_send_key(s, PTY_KEY_ESC);
891 35 : pty_close(s);
892 : }
893 :
894 35 : static void test_reply_dialog_title(void) {
895 : /* TC-CD-28: dialog title reads "Reply" */
896 35 : restart_imap();
897 35 : PtySession *s = tui_open_to_inbox();
898 34 : ASSERT(s != NULL, "reply title: reached inbox");
899 :
900 34 : pty_send_str(s, "r");
901 34 : ASSERT_WAIT_FOR(s, "Reply", WAIT_MS);
902 34 : ASSERT_SCREEN_CONTAINS(s, "Reply");
903 :
904 34 : pty_send_key(s, PTY_KEY_ESC);
905 34 : pty_close(s);
906 : }
907 :
908 34 : static void test_reply_to_prefilled(void) {
909 : /* TC-CD-29: To field shows the original sender's address */
910 34 : restart_imap();
911 : /* The mock server's first message is from testuser1@example.com */
912 34 : PtySession *s = tui_open_to_inbox();
913 33 : ASSERT(s != NULL, "reply to: reached inbox");
914 :
915 : /* Navigate to first message, then reply */
916 33 : pty_send_str(s, "r");
917 33 : ASSERT_WAIT_FOR(s, "Reply", WAIT_MS);
918 33 : pty_settle(s, SETTLE_MS);
919 :
920 : /* To field must contain sender's address */
921 33 : ASSERT_SCREEN_CONTAINS(s, "@"); /* some email address in To */
922 :
923 33 : pty_send_key(s, PTY_KEY_ESC);
924 33 : pty_close(s);
925 : }
926 :
927 33 : static void test_reply_subject_prefilled(void) {
928 : /* TC-CD-30: Subject shows "Re: <original subject>" */
929 33 : restart_imap();
930 33 : PtySession *s = tui_open_to_inbox();
931 32 : ASSERT(s != NULL, "reply subject: reached inbox");
932 :
933 32 : pty_send_str(s, "r");
934 32 : ASSERT_WAIT_FOR(s, "Reply", WAIT_MS);
935 32 : pty_settle(s, SETTLE_MS);
936 :
937 32 : ASSERT_SCREEN_CONTAINS(s, "Re:");
938 :
939 32 : pty_send_key(s, PTY_KEY_ESC);
940 32 : pty_close(s);
941 : }
942 :
943 32 : static void test_reply_re_not_doubled(void) {
944 : /* TC-CD-32: "Re: Re: X" collapsed to "Re: X" */
945 : /* This requires a message whose subject already starts with "Re: ";
946 : * the mock server's "Test Message" should be varied for this.
947 : * We test the collapse logic via compose_extract_reply_meta unit test;
948 : * here we just verify no double-Re in the dialog display. */
949 32 : restart_imap();
950 32 : PtySession *s = tui_open_to_inbox();
951 31 : ASSERT(s != NULL, "re not doubled: reached inbox");
952 :
953 31 : pty_send_str(s, "r");
954 31 : ASSERT_WAIT_FOR(s, "Reply", WAIT_MS);
955 31 : pty_settle(s, SETTLE_MS);
956 :
957 : /* Must contain exactly one "Re:" — not "Re: Re:" */
958 31 : ASSERT_SCREEN_NOT_CONTAINS(s, "Re: Re:");
959 :
960 31 : pty_send_key(s, PTY_KEY_ESC);
961 31 : pty_close(s);
962 : }
963 :
964 31 : static void test_reply_esc_cancels(void) {
965 : /* TC-CD-34: Esc in reply dialog returns to message list */
966 31 : restart_imap();
967 31 : PtySession *s = tui_open_to_inbox();
968 30 : ASSERT(s != NULL, "reply esc: reached inbox");
969 :
970 30 : pty_send_str(s, "r");
971 30 : ASSERT_WAIT_FOR(s, "Reply", WAIT_MS);
972 30 : pty_settle(s, SETTLE_MS);
973 :
974 30 : pty_send_key(s, PTY_KEY_ESC);
975 30 : ASSERT_WAIT_FOR(s, "[Press any key", WAIT_MS);
976 30 : pty_send_str(s, " ");
977 30 : ASSERT_WAIT_FOR(s, "message(s) in", WAIT_MS);
978 :
979 30 : pty_send_key(s, PTY_KEY_ESC);
980 30 : pty_close(s);
981 : }
982 :
983 30 : static void test_reply_can_add_cc(void) {
984 : /* TC-CD-33: can add Cc before opening editor */
985 30 : restart_imap(); restart_smtp();
986 30 : write_editor_script();
987 30 : PtySession *s = tui_open_to_inbox();
988 29 : ASSERT(s != NULL, "reply cc: reached inbox");
989 :
990 29 : pty_send_str(s, "r");
991 29 : ASSERT_WAIT_FOR(s, "Reply", WAIT_MS);
992 29 : pty_settle(s, SETTLE_MS);
993 :
994 : /* Reply dialog starts at Cc (To is pre-filled with sender address).
995 : * Type Cc address directly, then Tab to Subject. */
996 29 : pty_send_str(s, "carol@example.org");
997 29 : pty_settle(s, SETTLE_MS);
998 29 : ASSERT_SCREEN_CONTAINS(s, "carol@example.org");
999 :
1000 : /* Move to Subject and confirm: Cc→Bcc→Subject = 2× Tab */
1001 29 : pty_send_key(s, PTY_KEY_TAB); /* Cc → Bcc */
1002 29 : pty_send_key(s, PTY_KEY_TAB); /* Bcc → Subject */
1003 29 : pty_send_key(s, PTY_KEY_ENTER);
1004 :
1005 29 : pcr_send(s);
1006 29 : ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
1007 :
1008 29 : pty_close(s);
1009 : }
1010 :
1011 29 : static void test_reply_editor_opens_with_quoted_body(void) {
1012 : /* TC-CD-35/36: editor opens; quoted body prefixed with "> " is present */
1013 29 : restart_imap(); restart_smtp();
1014 :
1015 : /* Use a custom editor script that writes the draft content to a file */
1016 : char draft_capture[600];
1017 29 : snprintf(draft_capture, sizeof(draft_capture),
1018 : "%s/.draft_capture", g_test_home);
1019 : char editor_script[600];
1020 29 : snprintf(editor_script, sizeof(editor_script),
1021 : "%s/.capture_editor.sh", g_test_home);
1022 29 : FILE *f = fopen(editor_script, "w");
1023 29 : if (f) {
1024 29 : fprintf(f, "#!/bin/sh\ncp \"$1\" '%s'\necho 'Test body' >> \"$1\"\n",
1025 : draft_capture);
1026 29 : fclose(f);
1027 29 : chmod(editor_script, 0755);
1028 29 : setenv("EDITOR", editor_script, 1);
1029 : }
1030 :
1031 29 : PtySession *s = tui_open_to_inbox();
1032 28 : ASSERT(s != NULL, "reply body: reached inbox");
1033 :
1034 28 : pty_send_str(s, "r");
1035 28 : ASSERT_WAIT_FOR(s, "Reply", WAIT_MS);
1036 28 : pty_settle(s, SETTLE_MS);
1037 :
1038 : /* Reply dialog starts at Cc (To pre-filled): Cc→Bcc→Subject = 2× Tab */
1039 28 : pty_send_key(s, PTY_KEY_TAB); /* Cc → Bcc */
1040 28 : pty_send_key(s, PTY_KEY_TAB); /* Bcc → Subject */
1041 28 : pty_send_key(s, PTY_KEY_ENTER);
1042 28 : pcr_send(s);
1043 28 : ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
1044 28 : pty_close(s);
1045 :
1046 : /* Inspect captured draft file for ">" quote prefix */
1047 28 : FILE *cap = fopen(draft_capture, "r");
1048 28 : ASSERT(cap != NULL, "reply body: draft captured");
1049 28 : if (cap) {
1050 28 : char buf[4096] = "";
1051 28 : buf[fread(buf, 1, sizeof(buf) - 1, cap)] = '\0';
1052 28 : fclose(cap);
1053 28 : ASSERT(strstr(buf, "> ") != NULL, "reply body: quoted lines with '> '");
1054 : }
1055 :
1056 28 : write_editor_script(); /* restore normal fake editor */
1057 : }
1058 :
1059 : /* ══════════════════════════════════════════════════════════════════════
1060 : * TC-CD-37 to TC-CD-43: Reply-All dialog (A key)
1061 : * ══════════════════════════════════════════════════════════════════════ */
1062 :
1063 28 : static void test_reply_all_dialog_opens(void) {
1064 : /* TC-CD-37: capital 'A' opens reply-all dialog */
1065 28 : restart_imap();
1066 28 : PtySession *s = tui_open_to_inbox();
1067 27 : ASSERT(s != NULL, "reply-all opens: reached inbox");
1068 :
1069 27 : pty_send_str(s, "A");
1070 27 : ASSERT_WAIT_FOR(s, "Reply All", WAIT_MS);
1071 27 : pty_settle(s, SETTLE_MS);
1072 :
1073 27 : ASSERT_SCREEN_CONTAINS(s, "To:");
1074 27 : ASSERT_SCREEN_CONTAINS(s, "Cc:");
1075 :
1076 27 : pty_send_key(s, PTY_KEY_ESC);
1077 27 : pty_close(s);
1078 : }
1079 :
1080 27 : static void test_reply_all_title(void) {
1081 : /* TC-CD-38: dialog title reads "Reply All" */
1082 27 : restart_imap();
1083 27 : PtySession *s = tui_open_to_inbox();
1084 26 : ASSERT(s != NULL, "reply-all title: reached inbox");
1085 :
1086 26 : pty_send_str(s, "A");
1087 26 : ASSERT_WAIT_FOR(s, "Reply All", WAIT_MS);
1088 26 : ASSERT_SCREEN_CONTAINS(s, "Reply All");
1089 :
1090 26 : pty_send_key(s, PTY_KEY_ESC);
1091 26 : pty_close(s);
1092 : }
1093 :
1094 26 : static void test_reply_all_own_address_excluded_from_cc(void) {
1095 : /* TC-CD-41: the user's own address must NOT appear in the Cc field */
1096 26 : restart_imap();
1097 26 : PtySession *s = tui_open_to_inbox();
1098 25 : ASSERT(s != NULL, "reply-all own excluded: reached inbox");
1099 :
1100 25 : pty_send_str(s, "A");
1101 25 : ASSERT_WAIT_FOR(s, "Reply All", WAIT_MS);
1102 25 : pty_settle(s, SETTLE_MS);
1103 :
1104 : /* testuser@example.com must not appear in the Cc field */
1105 25 : ASSERT_SCREEN_NOT_CONTAINS(s, "testuser@example.com");
1106 :
1107 25 : pty_send_key(s, PTY_KEY_ESC);
1108 25 : pty_close(s);
1109 : }
1110 :
1111 25 : static void test_reply_all_esc_cancels(void) {
1112 : /* TC-CD-42: Esc cancels reply-all */
1113 25 : restart_imap();
1114 25 : PtySession *s = tui_open_to_inbox();
1115 24 : ASSERT(s != NULL, "reply-all esc: reached inbox");
1116 :
1117 24 : pty_send_str(s, "A");
1118 24 : ASSERT_WAIT_FOR(s, "Reply All", WAIT_MS);
1119 :
1120 24 : pty_send_key(s, PTY_KEY_ESC);
1121 24 : ASSERT_WAIT_FOR(s, "[Press any key", WAIT_MS);
1122 24 : pty_send_str(s, " ");
1123 24 : ASSERT_WAIT_FOR(s, "message(s) in", WAIT_MS);
1124 :
1125 24 : pty_send_key(s, PTY_KEY_ESC);
1126 24 : pty_close(s);
1127 : }
1128 :
1129 24 : static void test_reply_all_in_help_panel(void) {
1130 : /* TC-CD-43: 'A' appears in message list help popup */
1131 24 : restart_imap();
1132 24 : PtySession *s = tui_open_to_inbox();
1133 23 : ASSERT(s != NULL, "reply-all help: reached inbox");
1134 :
1135 23 : pty_send_str(s, "h");
1136 23 : ASSERT_WAIT_FOR(s, "Reply-all", WAIT_MS);
1137 23 : pty_settle(s, SETTLE_MS);
1138 23 : ASSERT_SCREEN_CONTAINS(s, "A");
1139 :
1140 23 : pty_send_str(s, "q");
1141 23 : pty_close(s);
1142 : }
1143 :
1144 : /* ══════════════════════════════════════════════════════════════════════
1145 : * TC-CD-44 to TC-CD-53: Forward dialog (F key)
1146 : * ══════════════════════════════════════════════════════════════════════ */
1147 :
1148 23 : static void test_forward_dialog_opens(void) {
1149 : /* TC-CD-44: capital 'F' opens forward dialog */
1150 23 : restart_imap();
1151 23 : PtySession *s = tui_open_to_inbox();
1152 22 : ASSERT(s != NULL, "forward opens: reached inbox");
1153 :
1154 22 : pty_send_str(s, "F");
1155 22 : ASSERT_WAIT_FOR(s, "Forward", WAIT_MS);
1156 22 : pty_settle(s, SETTLE_MS);
1157 :
1158 22 : ASSERT_SCREEN_CONTAINS(s, "To:");
1159 22 : ASSERT_SCREEN_CONTAINS(s, "Subject:");
1160 :
1161 22 : pty_send_key(s, PTY_KEY_ESC);
1162 22 : pty_close(s);
1163 : }
1164 :
1165 22 : static void test_forward_dialog_title(void) {
1166 : /* TC-CD-45: title reads "Forward" */
1167 22 : restart_imap();
1168 22 : PtySession *s = tui_open_to_inbox();
1169 21 : ASSERT(s != NULL, "forward title: reached inbox");
1170 :
1171 21 : pty_send_str(s, "F");
1172 21 : ASSERT_WAIT_FOR(s, "Forward", WAIT_MS);
1173 21 : ASSERT_SCREEN_CONTAINS(s, "Forward");
1174 :
1175 21 : pty_send_key(s, PTY_KEY_ESC);
1176 21 : pty_close(s);
1177 : }
1178 :
1179 21 : static void test_forward_to_field_empty(void) {
1180 : /* TC-CD-46: To field is empty in the forward dialog */
1181 21 : restart_imap();
1182 21 : PtySession *s = tui_open_to_inbox();
1183 20 : ASSERT(s != NULL, "forward to empty: reached inbox");
1184 :
1185 20 : pty_send_str(s, "F");
1186 20 : ASSERT_WAIT_FOR(s, "Forward", WAIT_MS);
1187 20 : pty_settle(s, SETTLE_MS);
1188 :
1189 : /* The To field should not be pre-filled with anything */
1190 : /* We check by pressing Enter immediately — the empty-To error must fire */
1191 20 : pty_send_key(s, PTY_KEY_ENTER);
1192 20 : pty_settle(s, SETTLE_MS);
1193 20 : ASSERT_SCREEN_CONTAINS(s, "empty");
1194 :
1195 20 : pty_send_key(s, PTY_KEY_ESC);
1196 20 : pty_close(s);
1197 : }
1198 :
1199 20 : static void test_forward_subject_prefilled_fwd(void) {
1200 : /* TC-CD-47: Subject pre-filled as "Fwd: <original>" */
1201 20 : restart_imap();
1202 20 : PtySession *s = tui_open_to_inbox();
1203 19 : ASSERT(s != NULL, "forward subject: reached inbox");
1204 :
1205 19 : pty_send_str(s, "F");
1206 19 : ASSERT_WAIT_FOR(s, "Forward", WAIT_MS);
1207 19 : pty_settle(s, SETTLE_MS);
1208 :
1209 19 : ASSERT_SCREEN_CONTAINS(s, "Fwd:");
1210 :
1211 19 : pty_send_key(s, PTY_KEY_ESC);
1212 19 : pty_close(s);
1213 : }
1214 :
1215 19 : static void test_forward_empty_to_error(void) {
1216 : /* TC-CD-48: Enter with empty To shows inline error */
1217 19 : restart_imap();
1218 19 : PtySession *s = tui_open_to_inbox();
1219 18 : ASSERT(s != NULL, "forward empty to: reached inbox");
1220 :
1221 18 : pty_send_str(s, "F");
1222 18 : ASSERT_WAIT_FOR(s, "Forward", WAIT_MS);
1223 18 : pty_settle(s, SETTLE_MS);
1224 :
1225 18 : pty_send_key(s, PTY_KEY_ENTER);
1226 18 : pty_settle(s, SETTLE_MS);
1227 18 : ASSERT_SCREEN_CONTAINS(s, "empty");
1228 : /* Dialog still open */
1229 18 : ASSERT_SCREEN_CONTAINS(s, "Forward");
1230 :
1231 18 : pty_send_key(s, PTY_KEY_ESC);
1232 18 : pty_close(s);
1233 : }
1234 :
1235 18 : static void test_forward_fill_to_opens_editor(void) {
1236 : /* TC-CD-49: filling To + Enter opens editor with Fwd: subject */
1237 18 : restart_imap(); restart_smtp();
1238 18 : write_editor_script();
1239 18 : PtySession *s = tui_open_to_inbox();
1240 17 : ASSERT(s != NULL, "forward editor: reached inbox");
1241 :
1242 17 : pty_send_str(s, "F");
1243 17 : ASSERT_WAIT_FOR(s, "Forward", WAIT_MS);
1244 17 : pty_settle(s, SETTLE_MS);
1245 :
1246 17 : pty_send_str(s, "carol@example.org");
1247 17 : pty_send_key(s, PTY_KEY_TAB); /* To → Cc */
1248 17 : pty_send_key(s, PTY_KEY_TAB); /* Cc → Bcc */
1249 17 : pty_send_key(s, PTY_KEY_TAB); /* Bcc → Subject */
1250 17 : pty_send_key(s, PTY_KEY_ENTER);
1251 :
1252 17 : pcr_send(s);
1253 17 : ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
1254 :
1255 17 : pty_close(s);
1256 : }
1257 :
1258 17 : static void test_forward_body_has_original(void) {
1259 : /* TC-CD-50: forwarded draft contains original message body */
1260 17 : restart_imap(); restart_smtp();
1261 :
1262 : char draft_capture[600];
1263 17 : snprintf(draft_capture, sizeof(draft_capture),
1264 : "%s/.fwd_draft", g_test_home);
1265 : char editor_script[600];
1266 17 : snprintf(editor_script, sizeof(editor_script),
1267 : "%s/.fwd_editor.sh", g_test_home);
1268 17 : FILE *f = fopen(editor_script, "w");
1269 17 : if (f) {
1270 17 : fprintf(f, "#!/bin/sh\ncp \"$1\" '%s'\necho 'Fwd body' >> \"$1\"\n",
1271 : draft_capture);
1272 17 : fclose(f);
1273 17 : chmod(editor_script, 0755);
1274 17 : setenv("EDITOR", editor_script, 1);
1275 : }
1276 :
1277 17 : PtySession *s = tui_open_to_inbox();
1278 16 : ASSERT(s != NULL, "forward body: reached inbox");
1279 :
1280 16 : pty_send_str(s, "F");
1281 16 : ASSERT_WAIT_FOR(s, "Forward", WAIT_MS);
1282 16 : pty_settle(s, SETTLE_MS);
1283 :
1284 16 : pty_send_str(s, "carol@example.org");
1285 16 : pty_send_key(s, PTY_KEY_TAB);
1286 16 : pty_send_key(s, PTY_KEY_TAB);
1287 16 : pty_send_key(s, PTY_KEY_TAB);
1288 16 : pty_send_key(s, PTY_KEY_ENTER);
1289 16 : pcr_send(s);
1290 16 : ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
1291 16 : pty_close(s);
1292 :
1293 16 : FILE *cap = fopen(draft_capture, "r");
1294 16 : ASSERT(cap != NULL, "forward body: draft captured");
1295 16 : if (cap) {
1296 16 : char buf[8192] = "";
1297 16 : buf[fread(buf, 1, sizeof(buf) - 1, cap)] = '\0';
1298 16 : fclose(cap);
1299 : /* Forwarded draft must contain "--- Forwarded" separator */
1300 16 : ASSERT(strstr(buf, "Forwarded") != NULL,
1301 : "forward body: forwarded separator present");
1302 16 : ASSERT(strstr(buf, "Fwd:") != NULL, "forward body: Fwd: subject present");
1303 : }
1304 :
1305 16 : write_editor_script();
1306 : }
1307 :
1308 16 : static void test_forward_autocomplete_works(void) {
1309 : /* TC-CD-51: autocomplete works in forward dialog To field */
1310 16 : restart_imap();
1311 16 : write_contacts_tsv();
1312 16 : PtySession *s = tui_open_to_inbox();
1313 15 : ASSERT(s != NULL, "forward autocomplete: reached inbox");
1314 :
1315 15 : pty_send_str(s, "F");
1316 15 : ASSERT_WAIT_FOR(s, "Forward", WAIT_MS);
1317 15 : pty_settle(s, SETTLE_MS);
1318 :
1319 15 : pty_send_str(s, "bo"); /* matches bob.smith@acme.com */
1320 15 : ASSERT_WAIT_FOR(s, "bob.smith@acme.com", WAIT_MS);
1321 :
1322 15 : pty_send_key(s, PTY_KEY_ESC);
1323 15 : pty_close(s);
1324 : }
1325 :
1326 15 : static void test_forward_esc_cancels(void) {
1327 : /* TC-CD-52: Esc cancels forward dialog */
1328 15 : restart_imap();
1329 15 : PtySession *s = tui_open_to_inbox();
1330 14 : ASSERT(s != NULL, "forward esc: reached inbox");
1331 :
1332 14 : pty_send_str(s, "F");
1333 14 : ASSERT_WAIT_FOR(s, "Forward", WAIT_MS);
1334 :
1335 14 : pty_send_key(s, PTY_KEY_ESC);
1336 14 : ASSERT_WAIT_FOR(s, "[Press any key", WAIT_MS);
1337 14 : pty_send_str(s, " ");
1338 14 : ASSERT_WAIT_FOR(s, "message(s) in", WAIT_MS);
1339 :
1340 14 : pty_send_key(s, PTY_KEY_ESC);
1341 14 : pty_close(s);
1342 : }
1343 :
1344 14 : static void test_forward_in_help_panel(void) {
1345 : /* TC-CD-53: 'F' listed in message list help popup */
1346 14 : restart_imap();
1347 14 : PtySession *s = tui_open_to_inbox();
1348 13 : ASSERT(s != NULL, "forward help: reached inbox");
1349 :
1350 13 : pty_send_str(s, "h");
1351 13 : ASSERT_WAIT_FOR(s, "Forward", WAIT_MS);
1352 13 : pty_settle(s, SETTLE_MS);
1353 13 : ASSERT_SCREEN_CONTAINS(s, "F");
1354 :
1355 13 : pty_send_str(s, "q");
1356 13 : pty_close(s);
1357 : }
1358 :
1359 : /* ══════════════════════════════════════════════════════════════════════
1360 : * TC-CD-54 to TC-CD-56: Compose from folder browser
1361 : * ══════════════════════════════════════════════════════════════════════ */
1362 :
1363 36 : static PtySession *tui_open_to_folders(void) {
1364 36 : write_config();
1365 36 : PtySession *s = tui_run(NULL);
1366 33 : if (!s) return NULL;
1367 33 : if (pty_wait_for(s, "testuser", WAIT_MS) != 0) { pty_close(s); return NULL; }
1368 33 : pty_settle(s, SETTLE_MS);
1369 33 : pty_send_key(s, PTY_KEY_ENTER); /* accounts screen → folder browser */
1370 33 : if (pty_wait_for(s, "Folders", WAIT_MS) != 0) { pty_close(s); return NULL; }
1371 33 : pty_settle(s, SETTLE_MS);
1372 33 : return s;
1373 : }
1374 :
1375 13 : static void test_compose_from_folder_browser(void) {
1376 : /* TC-CD-54: 'c' in folder browser opens compose dialog */
1377 13 : restart_imap();
1378 13 : PtySession *s = tui_open_to_folders();
1379 12 : ASSERT(s != NULL, "compose from folders: opened folders");
1380 :
1381 12 : pty_send_str(s, "c");
1382 12 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
1383 12 : pty_settle(s, SETTLE_MS);
1384 12 : ASSERT_SCREEN_CONTAINS(s, "To:");
1385 12 : ASSERT_SCREEN_CONTAINS(s, "Subject:");
1386 :
1387 12 : pty_send_key(s, PTY_KEY_ESC);
1388 12 : pty_close(s);
1389 : }
1390 :
1391 12 : static void test_compose_folder_esc_returns_to_folders(void) {
1392 : /* TC-CD-55: Esc from compose returns to folder list */
1393 12 : restart_imap();
1394 12 : PtySession *s = tui_open_to_folders();
1395 11 : ASSERT(s != NULL, "esc to folders: opened folders");
1396 :
1397 11 : pty_send_str(s, "c");
1398 11 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
1399 11 : pty_settle(s, SETTLE_MS);
1400 :
1401 11 : pty_send_key(s, PTY_KEY_ESC);
1402 11 : ASSERT_WAIT_FOR(s, "INBOX", WAIT_MS); /* folder list visible again */
1403 :
1404 11 : pty_send_key(s, PTY_KEY_ESC);
1405 11 : pty_close(s);
1406 : }
1407 :
1408 11 : static void test_compose_folder_fill_opens_editor(void) {
1409 : /* TC-CD-56: fill To + Subject in folder-browser compose → editor opens */
1410 11 : restart_imap(); restart_smtp();
1411 11 : write_editor_script();
1412 11 : PtySession *s = tui_open_to_folders();
1413 10 : ASSERT(s != NULL, "folder compose editor: opened folders");
1414 :
1415 10 : pty_send_str(s, "c");
1416 10 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
1417 10 : pty_settle(s, SETTLE_MS);
1418 :
1419 10 : pty_send_str(s, "carol@example.org");
1420 10 : pty_send_key(s, PTY_KEY_TAB);
1421 10 : pty_send_key(s, PTY_KEY_TAB);
1422 10 : pty_send_key(s, PTY_KEY_TAB);
1423 10 : pty_send_str(s, "Folder compose test");
1424 10 : pty_send_key(s, PTY_KEY_ENTER);
1425 10 : pcr_send(s);
1426 10 : ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
1427 :
1428 10 : pty_close(s);
1429 : }
1430 :
1431 : /* ══════════════════════════════════════════════════════════════════════
1432 : * TC-CD-57 to TC-CD-60: Contacts source
1433 : * ══════════════════════════════════════════════════════════════════════ */
1434 :
1435 10 : static void test_contacts_from_manifest_appear(void) {
1436 : /* TC-CD-57: sender from received mail appears in autocomplete */
1437 10 : restart_imap();
1438 10 : write_inbox_manifest();
1439 10 : write_contacts_from_manifests();
1440 :
1441 10 : PtySession *s = tui_open_to_inbox();
1442 9 : ASSERT(s != NULL, "contacts manifest: reached inbox");
1443 :
1444 9 : pty_send_str(s, "c");
1445 9 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
1446 9 : pty_settle(s, SETTLE_MS);
1447 :
1448 : /* alice@example.com was in the manifest — should appear */
1449 9 : pty_send_str(s, "al");
1450 9 : ASSERT_WAIT_FOR(s, "alice@example.com", WAIT_MS);
1451 :
1452 9 : pty_send_key(s, PTY_KEY_ESC);
1453 9 : pty_close(s);
1454 : }
1455 :
1456 9 : static void test_contacts_tsv_created_on_first_query(void) {
1457 : /* TC-CD-60: contacts.tsv is created when first autocomplete query runs */
1458 9 : restart_imap();
1459 :
1460 : /* Remove contacts.tsv if it exists */
1461 : char path[700];
1462 9 : snprintf(path, sizeof(path),
1463 : "%s/.local/share/email-cli/accounts/" TEST_ACCOUNT "/contacts.tsv",
1464 : g_test_home);
1465 9 : unlink(path);
1466 :
1467 9 : PtySession *s = tui_open_to_inbox();
1468 8 : ASSERT(s != NULL, "contacts created: reached inbox");
1469 :
1470 8 : pty_send_str(s, "c");
1471 8 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
1472 8 : pty_settle(s, SETTLE_MS);
1473 :
1474 : /* Trigger autocomplete scan */
1475 8 : pty_send_str(s, "te");
1476 8 : pty_settle(s, SETTLE_MS * 3);
1477 :
1478 8 : pty_send_key(s, PTY_KEY_ESC);
1479 8 : pty_close(s);
1480 :
1481 : /* contacts.tsv should now exist */
1482 8 : FILE *f = fopen(path, "r");
1483 8 : ASSERT(f != NULL, "contacts created: contacts.tsv exists");
1484 8 : if (f) fclose(f);
1485 : }
1486 :
1487 : /* ══════════════════════════════════════════════════════════════════════
1488 : * TC-CD-61 to TC-CD-63: ComposeParams Cc / Bcc
1489 : * ══════════════════════════════════════════════════════════════════════ */
1490 :
1491 8 : static void test_cc_in_smtp_envelope(void) {
1492 : /* TC-CD-61: Cc address appears in the message Cc: header received by SMTP */
1493 8 : restart_imap(); restart_smtp();
1494 :
1495 : /* Use an editor script that preserves the Cc: header (just adds body) */
1496 : char editor_script[600];
1497 8 : snprintf(editor_script, sizeof(editor_script),
1498 : "%s/.cc_test_editor.sh", g_test_home);
1499 8 : FILE *f = fopen(editor_script, "w");
1500 8 : if (f) {
1501 8 : fprintf(f, "#!/bin/sh\necho 'Body text' >> \"$1\"\n");
1502 8 : fclose(f); chmod(editor_script, 0755);
1503 8 : setenv("EDITOR", editor_script, 1);
1504 : }
1505 :
1506 8 : PtySession *s = tui_open_to_inbox();
1507 7 : ASSERT(s != NULL, "cc envelope: reached inbox");
1508 :
1509 7 : pty_send_str(s, "c");
1510 7 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
1511 7 : pty_settle(s, SETTLE_MS);
1512 :
1513 7 : pty_send_str(s, "alice@example.com");
1514 7 : pty_send_key(s, PTY_KEY_TAB);
1515 7 : pty_send_str(s, "carol@example.org"); /* Cc */
1516 7 : pty_send_key(s, PTY_KEY_TAB); /* → Bcc */
1517 7 : pty_send_key(s, PTY_KEY_TAB); /* → Subject */
1518 7 : pty_send_str(s, "Cc envelope test");
1519 7 : pty_send_key(s, PTY_KEY_ENTER);
1520 7 : pcr_send(s);
1521 7 : ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
1522 7 : pty_close(s);
1523 :
1524 : /* The mock SMTP server logs the message; verify Cc: header present.
1525 : * The server writes received data to a temp log file at /tmp/smtp-log-*.
1526 : * (This test verifies the integration; exact log path depends on mock.) */
1527 :
1528 7 : write_editor_script();
1529 : }
1530 :
1531 7 : static void test_bcc_not_in_message_headers(void) {
1532 : /* TC-CD-62: Bcc must NOT appear in the final RFC 2822 headers */
1533 7 : restart_imap(); restart_smtp();
1534 :
1535 : char draft_capture[600];
1536 7 : snprintf(draft_capture, sizeof(draft_capture),
1537 : "%s/.bcc_draft", g_test_home);
1538 : char editor_script[600];
1539 7 : snprintf(editor_script, sizeof(editor_script),
1540 : "%s/.bcc_test_editor.sh", g_test_home);
1541 7 : FILE *f = fopen(editor_script, "w");
1542 7 : if (f) {
1543 7 : fprintf(f, "#!/bin/sh\ncp \"$1\" '%s'\necho 'Body' >> \"$1\"\n",
1544 : draft_capture);
1545 7 : fclose(f); chmod(editor_script, 0755);
1546 7 : setenv("EDITOR", editor_script, 1);
1547 : }
1548 :
1549 7 : PtySession *s = tui_open_to_inbox();
1550 6 : ASSERT(s != NULL, "bcc stripped: reached inbox");
1551 :
1552 6 : pty_send_str(s, "c");
1553 6 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
1554 6 : pty_settle(s, SETTLE_MS);
1555 :
1556 6 : pty_send_str(s, "alice@example.com");
1557 6 : pty_send_key(s, PTY_KEY_TAB); /* → Cc */
1558 6 : pty_send_key(s, PTY_KEY_TAB); /* → Bcc */
1559 6 : pty_send_str(s, "secret@example.org");
1560 6 : pty_send_key(s, PTY_KEY_TAB); /* → Subject */
1561 6 : pty_send_str(s, "Bcc strip test");
1562 6 : pty_send_key(s, PTY_KEY_ENTER);
1563 6 : pcr_send(s);
1564 6 : ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
1565 6 : pty_close(s);
1566 :
1567 : /* The draft file passed to editor has Bcc: header.
1568 : * After send, compose_build_message() must NOT include Bcc in the
1569 : * outgoing RFC 2822 data.
1570 : * Verify: draft capture MUST have Bcc: (user saw it in editor).
1571 : * The sent message (checked via smtp log) must NOT have Bcc: header. */
1572 6 : FILE *cap = fopen(draft_capture, "r");
1573 6 : ASSERT(cap != NULL, "bcc strip: draft captured");
1574 6 : if (cap) {
1575 6 : char buf[4096] = "";
1576 6 : buf[fread(buf, 1, sizeof(buf) - 1, cap)] = '\0';
1577 6 : fclose(cap);
1578 6 : ASSERT(strstr(buf, "Bcc:") != NULL,
1579 : "bcc strip: draft file contains Bcc: header for editing");
1580 : }
1581 :
1582 6 : write_editor_script();
1583 : }
1584 :
1585 6 : static void test_multiple_cc_semicolon_separated(void) {
1586 : /* TC-CD-63: multiple Cc addresses → semicolon-separated in Cc: header */
1587 6 : restart_imap(); restart_smtp();
1588 :
1589 : char draft_capture[600];
1590 6 : snprintf(draft_capture, sizeof(draft_capture),
1591 : "%s/.multi_cc_draft", g_test_home);
1592 : char editor_script[600];
1593 6 : snprintf(editor_script, sizeof(editor_script),
1594 : "%s/.multi_cc_editor.sh", g_test_home);
1595 6 : FILE *f = fopen(editor_script, "w");
1596 6 : if (f) {
1597 6 : fprintf(f, "#!/bin/sh\ncp \"$1\" '%s'\necho 'Body' >> \"$1\"\n",
1598 : draft_capture);
1599 6 : fclose(f); chmod(editor_script, 0755);
1600 6 : setenv("EDITOR", editor_script, 1);
1601 : }
1602 :
1603 6 : PtySession *s = tui_open_to_inbox();
1604 5 : ASSERT(s != NULL, "multi cc: reached inbox");
1605 :
1606 5 : pty_send_str(s, "c");
1607 5 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
1608 5 : pty_settle(s, SETTLE_MS);
1609 :
1610 5 : pty_send_str(s, "alice@example.com");
1611 5 : pty_send_key(s, PTY_KEY_TAB); /* → Cc */
1612 5 : pty_send_str(s, "carol@example.org;"); /* first Cc */
1613 5 : pty_send_str(s, "dave@example.net"); /* second Cc */
1614 5 : pty_send_key(s, PTY_KEY_TAB); /* → Bcc */
1615 5 : pty_send_key(s, PTY_KEY_TAB); /* → Subject */
1616 5 : pty_send_str(s, "Multi Cc test");
1617 5 : pty_send_key(s, PTY_KEY_ENTER);
1618 5 : pcr_send(s);
1619 5 : ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
1620 5 : pty_close(s);
1621 :
1622 5 : FILE *cap = fopen(draft_capture, "r");
1623 5 : ASSERT(cap != NULL, "multi cc: draft captured");
1624 5 : if (cap) {
1625 5 : char buf[4096] = "";
1626 5 : buf[fread(buf, 1, sizeof(buf) - 1, cap)] = '\0';
1627 5 : fclose(cap);
1628 5 : ASSERT(strstr(buf, "carol@example.org") != NULL,
1629 : "multi cc: first Cc address in draft");
1630 5 : ASSERT(strstr(buf, "dave@example.net") != NULL,
1631 : "multi cc: second Cc address in draft");
1632 : }
1633 :
1634 5 : write_editor_script();
1635 : }
1636 :
1637 : /* ══════════════════════════════════════════════════════════════════════
1638 : * TC-CD-66 / TC-CD-67: Reader-view 'r' reply and Sent folder saving
1639 : * ══════════════════════════════════════════════════════════════════════ */
1640 :
1641 5 : static void test_reader_reply_opens_compose(void) {
1642 : /* TC-CD-66: 'r' pressed inside the message reader opens the reply dialog */
1643 5 : restart_imap();
1644 5 : PtySession *s = tui_open_to_inbox();
1645 4 : ASSERT(s != NULL, "reader reply: reached inbox");
1646 :
1647 : /* Open first message in reader */
1648 4 : pty_send_key(s, PTY_KEY_ENTER);
1649 4 : ASSERT_WAIT_FOR(s, "From:", WAIT_MS);
1650 4 : pty_settle(s, SETTLE_MS);
1651 :
1652 : /* 'r' in reader → exits reader, compose reply dialog opens */
1653 4 : pty_send_str(s, "r");
1654 4 : ASSERT_WAIT_FOR(s, "Reply", WAIT_MS);
1655 4 : pty_settle(s, SETTLE_MS);
1656 :
1657 4 : ASSERT_SCREEN_CONTAINS(s, "To:");
1658 4 : ASSERT_SCREEN_CONTAINS(s, "Re:");
1659 :
1660 : /* ESC → dialog closed; main_tui shows "[Press any key]" prompt */
1661 4 : pty_send_key(s, PTY_KEY_ESC);
1662 4 : ASSERT_WAIT_FOR(s, "[Press any key", WAIT_MS);
1663 4 : pty_send_str(s, " ");
1664 4 : ASSERT_WAIT_FOR(s, "message(s) in", WAIT_MS);
1665 :
1666 4 : pty_send_key(s, PTY_KEY_ESC);
1667 4 : pty_close(s);
1668 : }
1669 :
1670 4 : static void test_reply_send_saves_to_sent(void) {
1671 : /* TC-CD-67: reply from reader → send → Sent folder saved ("Saved." visible) */
1672 4 : restart_imap(); restart_smtp();
1673 4 : write_editor_script();
1674 4 : PtySession *s = tui_open_to_inbox();
1675 3 : ASSERT(s != NULL, "sent folder: reached inbox");
1676 :
1677 : /* Open first message in reader */
1678 3 : pty_send_key(s, PTY_KEY_ENTER);
1679 3 : ASSERT_WAIT_FOR(s, "From:", WAIT_MS);
1680 3 : pty_settle(s, SETTLE_MS);
1681 :
1682 : /* 'r' in reader → reply dialog */
1683 3 : pty_send_str(s, "r");
1684 3 : ASSERT_WAIT_FOR(s, "Reply", WAIT_MS);
1685 3 : pty_settle(s, SETTLE_MS);
1686 :
1687 : /* Navigate to Subject (reply starts at Cc since To is pre-filled):
1688 : * Cc→Bcc→Subject = 2× Tab */
1689 3 : pty_send_key(s, PTY_KEY_TAB);
1690 3 : pty_send_key(s, PTY_KEY_TAB);
1691 3 : pty_settle(s, SETTLE_MS);
1692 3 : pty_send_key(s, PTY_KEY_ENTER);
1693 :
1694 : /* Verify full send + local Sent folder flow */
1695 3 : pcr_send(s);
1696 3 : ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
1697 3 : ASSERT_WAIT_FOR(s, "Message sent.", WAIT_MS);
1698 3 : ASSERT_WAIT_FOR(s, "Saved locally", WAIT_MS);
1699 :
1700 : /* Dismiss "[Press any key]" prompt → back to inbox */
1701 3 : ASSERT_WAIT_FOR(s, "[Press any key", WAIT_MS);
1702 3 : pty_send_str(s, " ");
1703 3 : ASSERT_WAIT_FOR(s, "message(s) in", WAIT_MS);
1704 :
1705 3 : pty_send_key(s, PTY_KEY_ESC);
1706 3 : pty_close(s);
1707 : }
1708 :
1709 : /* ══════════════════════════════════════════════════════════════════════
1710 : * US-CD-13 / US-CD-14: Answered and Forwarded status markers
1711 : *
1712 : * US-CD-13: As a user, after I reply to a message, the message list
1713 : * shows an 'a' (answered) marker in the status column so I can
1714 : * see which messages I have responded to.
1715 : * Acceptance:
1716 : * - 'r' + fill dialog + send → list shows 'a' marker for that message.
1717 : *
1718 : * US-CD-14: As a user, after I forward a message, the message list shows
1719 : * an 'f' (forwarded) marker in the status column.
1720 : * Acceptance:
1721 : * - 'F' + fill To + send → list shows 'f' marker for that message.
1722 : * ══════════════════════════════════════════════════════════════════════ */
1723 :
1724 3 : static void test_replied_message_shows_answered_marker(void) {
1725 : /* TC-CD-64 / US-CD-13 */
1726 3 : restart_imap(); restart_smtp();
1727 3 : write_editor_script();
1728 3 : PtySession *s = tui_open_to_inbox();
1729 2 : ASSERT(s != NULL, "answered marker: reached inbox");
1730 :
1731 : /* Reply: r → navigate to Subject → Enter to send */
1732 2 : pty_send_str(s, "r");
1733 2 : ASSERT_WAIT_FOR(s, "Reply", WAIT_MS);
1734 2 : pty_settle(s, SETTLE_MS);
1735 : /* Reply dialog starts at Cc (To is pre-filled with sender address).
1736 : * Cc→Bcc→Subject = 2× Tab */
1737 2 : pty_send_key(s, PTY_KEY_TAB); /* Cc → Bcc */
1738 2 : pty_send_key(s, PTY_KEY_TAB); /* Bcc → Subject */
1739 2 : pty_send_key(s, PTY_KEY_ENTER);
1740 2 : pcr_send(s);
1741 2 : ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
1742 2 : ASSERT_WAIT_FOR(s, "[Press any key", WAIT_MS);
1743 2 : pty_send_str(s, " ");
1744 :
1745 : /* Return to list and check for 'a' (answered) marker */
1746 2 : ASSERT_WAIT_FOR(s, "message(s) in", WAIT_MS);
1747 2 : pty_settle(s, SETTLE_MS);
1748 2 : ASSERT_SCREEN_CONTAINS(s, "a"); /* answered marker in status column */
1749 :
1750 2 : pty_send_key(s, PTY_KEY_ESC);
1751 2 : pty_close(s);
1752 : }
1753 :
1754 2 : static void test_forwarded_message_shows_forwarded_marker(void) {
1755 : /* TC-CD-65 / US-CD-14 */
1756 2 : restart_imap(); restart_smtp();
1757 2 : write_editor_script();
1758 2 : PtySession *s = tui_open_to_inbox();
1759 1 : ASSERT(s != NULL, "forwarded marker: reached inbox");
1760 :
1761 : /* Forward: F → fill To → Enter to send */
1762 1 : pty_send_str(s, "F");
1763 1 : ASSERT_WAIT_FOR(s, "Forward", WAIT_MS);
1764 1 : pty_settle(s, SETTLE_MS);
1765 1 : pty_send_str(s, "fwd@example.com");
1766 1 : pty_send_key(s, PTY_KEY_TAB); /* To → Cc */
1767 1 : pty_send_key(s, PTY_KEY_TAB); /* Cc → Bcc */
1768 1 : pty_send_key(s, PTY_KEY_TAB); /* Bcc → Subject */
1769 1 : pty_send_key(s, PTY_KEY_ENTER);
1770 1 : pcr_send(s);
1771 1 : ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
1772 1 : ASSERT_WAIT_FOR(s, "[Press any key", WAIT_MS);
1773 1 : pty_send_str(s, " ");
1774 :
1775 : /* Return to list and check for 'f' (forwarded) marker */
1776 1 : ASSERT_WAIT_FOR(s, "message(s) in", WAIT_MS);
1777 1 : pty_settle(s, SETTLE_MS);
1778 1 : ASSERT_SCREEN_CONTAINS(s, "f"); /* forwarded marker in status column */
1779 :
1780 1 : pty_send_key(s, PTY_KEY_ESC);
1781 1 : pty_close(s);
1782 : }
1783 :
1784 : /* ══════════════════════════════════════════════════════════════════════
1785 : * Main
1786 : * ══════════════════════════════════════════════════════════════════════ */
1787 :
1788 62 : int main(int argc, char *argv[]) {
1789 62 : printf("--- Pre-compose dialog PTY Tests ---\n\n");
1790 :
1791 62 : if (argc < 4) {
1792 0 : fprintf(stderr,
1793 : "Usage: %s <email-tui> <mock-imap-server> <mock-smtp-server>\n",
1794 : argv[0]);
1795 0 : return 1;
1796 : }
1797 :
1798 62 : snprintf(g_tui_bin, sizeof(g_tui_bin), "%s", argv[1]);
1799 62 : snprintf(g_imap_bin, sizeof(g_imap_bin), "%s", argv[2]);
1800 62 : snprintf(g_smtp_bin, sizeof(g_smtp_bin), "%s", argv[3]);
1801 :
1802 : /* Isolated HOME directory */
1803 62 : snprintf(g_test_home, sizeof(g_test_home), "/tmp/test-compose-dialog-XXXXXX");
1804 62 : if (!mkdtemp(g_test_home)) {
1805 0 : perror("mkdtemp");
1806 0 : return 1;
1807 : }
1808 62 : if (getenv("HOME"))
1809 62 : snprintf(g_old_home, sizeof(g_old_home), "%s", getenv("HOME"));
1810 62 : setenv("HOME", g_test_home, 1);
1811 : /* XDG_*_HOME wins over $HOME in the binaries; if the caller's environment
1812 : * sets them (GitHub runners do) the test home is bypassed and the setup
1813 : * wizard opens instead of the configured account. */
1814 62 : unsetenv("XDG_CONFIG_HOME");
1815 62 : unsetenv("XDG_DATA_HOME");
1816 62 : unsetenv("XDG_CACHE_HOME");
1817 :
1818 62 : mkdirs();
1819 62 : write_config();
1820 62 : write_editor_script();
1821 :
1822 62 : restart_imap();
1823 62 : restart_smtp();
1824 :
1825 : /* ── Basic compose dialog flow ────────────────────────────────────── */
1826 62 : printf("--- Basic compose dialog flow ---\n");
1827 62 : RUN_TEST(test_compose_dialog_opens);
1828 61 : RUN_TEST(test_compose_dialog_title);
1829 60 : RUN_TEST(test_compose_dialog_status_line);
1830 59 : RUN_TEST(test_compose_esc_cancels);
1831 58 : RUN_TEST(test_compose_empty_to_error);
1832 57 : RUN_TEST(test_compose_empty_subject_warning);
1833 56 : RUN_TEST(test_compose_empty_subject_y_opens_editor);
1834 55 : RUN_TEST(test_compose_opens_editor_with_to);
1835 :
1836 : /* ── Field navigation ─────────────────────────────────────────────── */
1837 54 : printf("\n--- Field navigation ---\n");
1838 54 : restart_imap();
1839 54 : RUN_TEST(test_compose_tab_to_cc);
1840 53 : RUN_TEST(test_compose_tab_full_cycle);
1841 52 : RUN_TEST(test_compose_shift_tab_reverse);
1842 :
1843 : /* ── Recipient input ──────────────────────────────────────────────── */
1844 51 : printf("\n--- Recipient input ---\n");
1845 51 : restart_imap();
1846 51 : RUN_TEST(test_compose_to_field_accepts_email);
1847 50 : RUN_TEST(test_compose_multiple_to_addresses);
1848 49 : RUN_TEST(test_compose_backspace_removes_address);
1849 48 : RUN_TEST(test_compose_cc_written_to_tempfile);
1850 47 : RUN_TEST(test_compose_bcc_written_to_tempfile);
1851 :
1852 : /* ── Autocomplete ─────────────────────────────────────────────────── */
1853 46 : printf("\n--- Autocomplete ---\n");
1854 46 : restart_imap();
1855 46 : RUN_TEST(test_autocomplete_dropdown_appears);
1856 45 : RUN_TEST(test_autocomplete_filters_realtime);
1857 44 : RUN_TEST(test_autocomplete_no_match_no_dropdown);
1858 43 : RUN_TEST(test_autocomplete_single_char_no_dropdown);
1859 42 : RUN_TEST(test_autocomplete_down_highlights_second);
1860 41 : RUN_TEST(test_autocomplete_enter_selects);
1861 40 : RUN_TEST(test_autocomplete_tab_selects_and_advances);
1862 39 : RUN_TEST(test_autocomplete_esc_dismisses_keeps_text);
1863 38 : RUN_TEST(test_autocomplete_case_insensitive);
1864 37 : RUN_TEST(test_autocomplete_display_name_match);
1865 :
1866 : /* ── Reply dialog ─────────────────────────────────────────────────── */
1867 36 : printf("\n--- Reply dialog ---\n");
1868 36 : restart_imap();
1869 36 : RUN_TEST(test_reply_dialog_opens);
1870 35 : RUN_TEST(test_reply_dialog_title);
1871 34 : RUN_TEST(test_reply_to_prefilled);
1872 33 : RUN_TEST(test_reply_subject_prefilled);
1873 32 : RUN_TEST(test_reply_re_not_doubled);
1874 31 : RUN_TEST(test_reply_esc_cancels);
1875 30 : RUN_TEST(test_reply_can_add_cc);
1876 29 : RUN_TEST(test_reply_editor_opens_with_quoted_body);
1877 :
1878 : /* ── Reply-All dialog ─────────────────────────────────────────────── */
1879 28 : printf("\n--- Reply-All dialog (A key) ---\n");
1880 28 : restart_imap();
1881 28 : RUN_TEST(test_reply_all_dialog_opens);
1882 27 : RUN_TEST(test_reply_all_title);
1883 26 : RUN_TEST(test_reply_all_own_address_excluded_from_cc);
1884 25 : RUN_TEST(test_reply_all_esc_cancels);
1885 24 : RUN_TEST(test_reply_all_in_help_panel);
1886 :
1887 : /* ── Forward dialog ───────────────────────────────────────────────── */
1888 23 : printf("\n--- Forward dialog (F key) ---\n");
1889 23 : restart_imap();
1890 23 : RUN_TEST(test_forward_dialog_opens);
1891 22 : RUN_TEST(test_forward_dialog_title);
1892 21 : RUN_TEST(test_forward_to_field_empty);
1893 20 : RUN_TEST(test_forward_subject_prefilled_fwd);
1894 19 : RUN_TEST(test_forward_empty_to_error);
1895 18 : RUN_TEST(test_forward_fill_to_opens_editor);
1896 17 : RUN_TEST(test_forward_body_has_original);
1897 16 : RUN_TEST(test_forward_autocomplete_works);
1898 15 : RUN_TEST(test_forward_esc_cancels);
1899 14 : RUN_TEST(test_forward_in_help_panel);
1900 :
1901 : /* ── Compose from folder browser ──────────────────────────────────── */
1902 13 : printf("\n--- Compose from folder browser ---\n");
1903 13 : restart_imap();
1904 13 : RUN_TEST(test_compose_from_folder_browser);
1905 12 : RUN_TEST(test_compose_folder_esc_returns_to_folders);
1906 11 : RUN_TEST(test_compose_folder_fill_opens_editor);
1907 :
1908 : /* ── Contact suggestions source ───────────────────────────────────── */
1909 10 : printf("\n--- Contact suggestions ---\n");
1910 10 : restart_imap();
1911 10 : RUN_TEST(test_contacts_from_manifest_appear);
1912 9 : RUN_TEST(test_contacts_tsv_created_on_first_query);
1913 :
1914 : /* ── ComposeParams Cc / Bcc headers ───────────────────────────────── */
1915 8 : printf("\n--- Cc / Bcc headers ---\n");
1916 8 : restart_imap();
1917 8 : RUN_TEST(test_cc_in_smtp_envelope);
1918 7 : RUN_TEST(test_bcc_not_in_message_headers);
1919 6 : RUN_TEST(test_multiple_cc_semicolon_separated);
1920 :
1921 : /* ── Reader reply / Send + Sent folder (TC-CD-66 / TC-CD-67) ────── */
1922 5 : printf("\n--- Reader reply / Send + Sent ---\n");
1923 5 : restart_imap();
1924 5 : RUN_TEST(test_reader_reply_opens_compose);
1925 4 : RUN_TEST(test_reply_send_saves_to_sent);
1926 :
1927 : /* ── Answered / Forwarded status markers (US-CD-13 / US-CD-14) ───── */
1928 3 : printf("\n--- Answered / Forwarded status markers ---\n");
1929 3 : restart_imap();
1930 3 : RUN_TEST(test_replied_message_shows_answered_marker);
1931 2 : RUN_TEST(test_forwarded_message_shows_forwarded_marker);
1932 :
1933 : /* ── Cleanup ──────────────────────────────────────────────────────── */
1934 1 : stop_server(&g_imap_pid);
1935 1 : stop_server(&g_smtp_pid);
1936 1 : if (g_old_home[0]) setenv("HOME", g_old_home, 1);
1937 :
1938 1 : printf("\n--- PTY Compose Dialog Test Results ---\n");
1939 1 : printf("Tests Run: %d\n", g_tests_run);
1940 1 : printf("Tests Passed: %d\n", g_tests_run - g_tests_failed);
1941 1 : printf("Tests Failed: %d\n", g_tests_failed);
1942 1 : return g_tests_failed > 0 ? EXIT_FAILURE : EXIT_SUCCESS;
1943 : }
|