-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
evergreen.c
executable file
·2818 lines (2590 loc) · 90.3 KB
/
evergreen.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* evergreen -- online only terminal mail user agent
*
* Copyright (C) 2024, Naveen Albert
*
* Naveen Albert <bbs@phreaknet.org>
*
* This program is free software, distributed under the terms of
* the GNU General Public License Version 2. See the LICENSE file
* at the top of the source tree.
*/
/*!
* \note Supports the following features and functionality:
*
* IMAP
* - RFC 2177 IDLE
* - Stores \Answered and $Forwarded flags
* - Save and resume drafts
* - Message forwarding
*
* SMTP
* - RFC 1870 SIZE declaration
*
* Email address management
* - Multiple identities, by address and wildcard domain
* - Don't reply to one our identities when replying all
* - Automatically from matching identity in original recipient list
*
* RFC822 message formatting
* - format=flowed plaintext message support (viewing and composing)
* - window resizing and 80+ column support
* - view HTML emails as plain text formatted
* - Preserves threading in replies using References and In-Reply-To
*
* Notable missing features
* - Message sorting/search/threading in message pane view
* - Ability to create/delete folders
* - Multi-account support
* - Autocompletion/suggestion of addresses
*
* Currently missing, but soon to be added (hopefully):
* - Honoring read receipts (configurable)
* - Full attachment support (upload from disk, download/view attachments, forward message with its attachments)
* Ability to do disk operations (upload/download) needs to be disableable by a runtime flag, for restricted environments.
* - IMAP NOTIFY + periodically issue STATUS for all mailboxes
* - Create/delete/move mailboxes
* - BURL IMAP support
* - NNTP support?
*/
/*!
* \note This codebase includes some code from LBBS (Lightweight Bulletin Board System),
* (mostly based on code in mod_webmail, or other utility functions).
* Such code is included here under the same license.
*/
#include "evergreen.h"
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <getopt.h>
#include <termios.h>
#include <unistd.h>
#include <signal.h>
#include <poll.h>
#include <sys/eventfd.h>
#include <time.h>
#include <sys/time.h>
#include <assert.h>
#include <sys/resource.h> /* use rlimit */
enum {
CURSES_NOT_RUNNING,
CURSES_INITIALIZING,
CURSES_RUNNING,
CURSES_CLEANING_UP,
CURSES_ENDED,
};
static int ncurses_running = CURSES_NOT_RUNNING;
static FILE *log_fp = NULL;
static int debug_level = 0;
static int event_fd = -1;
void safe_strncpy(char *restrict dst, const char *restrict src, size_t size)
{
while (*src && size) {
*dst++ = *src++;
size--;
}
if (unlikely(!size)) {
dst--;
}
*dst = '\0';
}
static void client_set_permstatus(struct client *client, const char *s)
{
wmove(client->win_footer, 0, 0);
wclrtoeol(client->win_footer); /* Clear line */
/* Write message to status bar (footer) */
mvwaddstr(client->win_footer, 0, 0, s);
wnoutrefresh(client->win_footer);
}
static void client_set_status(struct client *client, const char *s)
{
wmove(client->win_footer, 0, STATUS_BAR_START_COL);
wclrtoeol(client->win_footer); /* Clear line */
/* Write message to status bar (footer) */
mvwaddstr(client->win_footer, 0, STATUS_BAR_START_COL, s);
wrefresh(client->win_footer);
}
void client_set_status_nout(struct client *client, const char *s)
{
wmove(client->win_footer, 0, STATUS_BAR_START_COL);
wclrtoeol(client->win_footer); /* Clear line */
/* Write message to status bar (footer) */
mvwaddstr(client->win_footer, 0, STATUS_BAR_START_COL, s);
wnoutrefresh(client->win_footer);
if (client->cursor) {
/* Restore original cursor position,
* simply by updating whatever window the cursor was on before we moved it to update the status bar.
* The next call to doupdate() will make sure the cursor is restored,
* rather than lingering after the status text until the next screen update. */
if (client->cur_win) {
wnoutrefresh(client->cur_win);
}
}
}
static void client_clear_status(struct client *client)
{
wmove(client->win_footer, 0, STATUS_BAR_START_COL);
wclrtoeol(client->win_footer); /* Clear line */
wnoutrefresh(client->win_footer);
}
void __attribute__ ((format (printf, 7, 8))) __client_log(struct client *client, int loglevel, int level, const char *file, int lineno, const char *func, const char *fmt, ...)
{
int len;
va_list ap;
char datestr[21];
char logminibuf[512];
char *buf = logminibuf;
int dynamic = 0;
if (loglevel == LOG_DEBUG && level > debug_level) {
return;
}
va_start(ap, fmt);
len = vsnprintf(logminibuf, sizeof(logminibuf), fmt, ap);
va_end(ap);
if (len >= (int) sizeof(logminibuf) - 1) {
/* Too large for stack allocated buffer. Dynamically allocate it. */
dynamic = 1;
buf = malloc((size_t) len + 2); /* + room for newline and NUL */
if (!buf) {
return;
}
va_start(ap, fmt);
#undef vsprintf
vsprintf(buf, fmt, ap); /* vsprintf is safe, vsnprintf is unnecessary here */
va_end(ap);
}
if (ncurses_running == CURSES_RUNNING) {
if (loglevel != LOG_DEBUG) {
client_set_status(client, buf);
}
} else if (ncurses_running == CURSES_NOT_RUNNING) {
buf[len] = '\n'; /* Change NUL to newline */
fwrite(buf, 1, len + 1, stderr);
}
if (log_fp) {
time_t lognow;
struct tm logdate;
struct timeval now;
int datelen;
gettimeofday(&now, NULL);
lognow = time(NULL);
localtime_r(&lognow, &logdate);
datelen = strftime(datestr, sizeof(datestr), "%Y-%m-%d %T ", &logdate);
fwrite(datestr, 1, datelen, log_fp);
fprintf(log_fp, "%s:%d [%s] ", file, lineno, func);
buf[len] = '\n'; /* Change NUL to newline, if we didn't already */
fwrite(buf, 1, len + 1, log_fp);
#ifdef DEBUG_MODE
/* Flush log messages prior to crash, so we don't lose anything */
fflush(log_fp);
#else
if (loglevel == LOG_ERROR || loglevel == LOG_WARNING) {
fflush(log_fp);
}
#endif
}
if (dynamic) {
free(buf);
}
}
static void window_cleanup(struct client *client)
{
delwin(client->win_header);
delwin(client->win_folders);
delwin(client->win_main);
delwin(client->win_footer);
}
static int client_term_cleanup(struct client *client)
{
ncurses_running = CURSES_CLEANING_UP;
cleanup_folder_menu(client);
cleanup_message_menu(client);
window_cleanup(client);
endwin();
ncurses_running = CURSES_ENDED;
free_folder_items(client);
free_message_items(client);
free_cached_messages(client);
return 0;
}
#define COLOR_PAIR_MESSAGES 1
#define COLOR_PAIR_FOLDERS 2
static inline void setup_header_footer(struct client *client)
{
client->win_header = newwin(1, COLS, 0, 0); /* Top */
client->win_footer = newwin(1, COLS, LINES - 1, 0); /* Bottom */
wbkgd(client->win_header, COLOR_PAIR(3));
wbkgd(client->win_footer, COLOR_PAIR(3));
refresh(); /* Needs to be done before header setup */
/* Set up header */
mvwaddstr(client->win_header, 0, 0, EVERGREEN_PROGNAME);
wnoutrefresh(client->win_header);
}
/*! \brief Set up the top-level ncurses windows */
static int setup_interface(struct client *client)
{
/* nlines, ncols, begin_y, begin_x */
setup_header_footer(client);
client->win_folders = newwin(MAIN_PANE_HEIGHT, LIST_PANE_WIDTH, 1, 0);
client->win_main = newwin(MAIN_PANE_HEIGHT, MAIN_PANE_WIDTH, 1, LIST_PANE_WIDTH);
/* Set colors, since we have a 4-coloring, so border is necessary */
/* These colors here apply to the base windows themselves, not any subwindows (e.g. menus) created in them */
wbkgd(client->win_main, COLOR_PAIR(COLOR_PAIR_MESSAGES));
wbkgd(client->win_folders, COLOR_PAIR(COLOR_PAIR_FOLDERS));
return 0;
}
/*! \brief Redraw header/footer (for if a submenu resizes) */
static void redraw_header_footer(struct client *client)
{
client_debug(6, "Redrawing header/footer");
delwin(client->win_header);
delwin(client->win_folders);
setup_header_footer(client);
wnoutrefresh(client->win_header);
wnoutrefresh(client->win_footer);
display_mailbox_info(client);
}
/*! \brief Initialize ncurses on startup */
static int client_term_init(struct client *client)
{
ncurses_running = CURSES_INITIALIZING;
initscr();
if (COLS < 80) {
client_error("Terminal must have at least 80 cols");
return -1;
}
cbreak();
#ifdef USE_NONL
nonl();
#endif
noecho();
keypad(stdscr, TRUE); /* Enable keypad for function key interpretation (escape sequences) */
curs_set(0); /* Disable cursor */
client->cursor = 0;
start_color(); /* Enable colors */
/* Since menus are by default white on black,
* keep the first two that way for consistency. */
init_pair(1, COLOR_WHITE, COLOR_BLACK);
init_pair(2, COLOR_WHITE, COLOR_BLACK);
init_pair(3, COLOR_CYAN, COLOR_BLUE);
init_pair(4, COLOR_WHITE, COLOR_CYAN);
init_pair(5, COLOR_GREEN, COLOR_BLACK);
init_pair(6, COLOR_CYAN, COLOR_BLACK);
init_pair(7, COLOR_CYAN, COLOR_BLACK);
init_pair(8, COLOR_MAGENTA, COLOR_BLACK);
init_pair(9, COLOR_WHITE, COLOR_BLUE);
init_pair(10, COLOR_RED, COLOR_BLACK); /* Quoted text font for viewer */
clear();
client_debug(1, "Terminal dimensions are %d rows, %d cols", LINES, COLS);
setup_interface(client);
doupdate();
ncurses_running = CURSES_RUNNING;
return 0;
}
static void set_focus(struct client *client, int f)
{
if (client->focus != f) {
client_debug(5, "Setting window focus to %d", f);
client->focus = f;
}
}
void format_size(size_t size, char *restrict buf, size_t len)
{
if (SIZE_KB(size) < 1) {
snprintf(buf, len, "%luB", size);
} else if (SIZE_MB(100) < 100) {
snprintf(buf, len, "%luK", SIZE_KB(size));
} else {
snprintf(buf, len, "%luM", SIZE_MB(size));
}
}
void display_mailbox_info(struct client *client)
{
char quota[32] = "";
char sizebuf[22];
struct mailbox *mbox = client->sel_mbox;
char buf[STATUS_BAR_START_COL + sizeof(sizebuf)]; /* Can't display more than this, so limit the buf to this + sizebuf for snprintf truncation warning */
/* Only need one or the other, the server may not provide both: */
if (client->quota_limit || client->quota_used) {
/* Unlike most stuff, the mailbox quota sizes are in units of KB, not bytes,
* so we can't use the corresponding SIZE macros directly as they are off by a factor of 1024. */
#define QUOTA_MB(s) SIZE_KB(s)
if (QUOTA_MB(client->quota_used) >= 10 || QUOTA_MB(client->quota_limit) > 100) {
snprintf(quota, sizeof(quota), " [%d/%dM]", QUOTA_MB(client->quota_used), QUOTA_MB(client->quota_limit));
} else {
snprintf(quota, sizeof(quota), " [%d/%dK]", client->quota_used, client->quota_limit);
}
}
format_size(mbox->size, sizebuf, sizeof(sizebuf));
if (COLS >= MIN_COLS_FOR_EXPANDED_MAILBOX_STATS) {
snprintf(buf, sizeof(buf), "%d (%dU, %dR), %s%s, %dN/%dV", mbox->total, mbox->unseen, mbox->recent, sizebuf, quota, mbox->uidnext, mbox->uidvalidity);
} else {
snprintf(buf, sizeof(buf), "%d (%dU, %dR), %s%s", mbox->total, mbox->unseen, mbox->recent, sizebuf, quota);
}
client_set_permstatus(client, buf);
}
/*! \note buf must be at least size NUM_IMAP_MESSAGE_FLAGS + 1 (to hold all flags) */
static void build_flags_str(struct message *msg, char *restrict buf)
{
char *p = buf;
/* Order flags from most significant/interesting to least... */
if (msg->flags & IMAP_MESSAGE_FLAG_FLAGGED) {
*p++ = 'F';
}
if (msg->flags & IMAP_MESSAGE_FLAG_DELETED) {
*p++ = 'T'; /* Trashed */
}
if (msg->flags & IMAP_MESSAGE_FLAG_ANSWERED) {
*p++ = 'A';
}
if (msg->flags & IMAP_MESSAGE_FLAG_DRAFT) {
*p++ = 'D';
}
/* We already have a column for recent and seen/unseen (*), so this is the least important */
if (msg->flags & IMAP_MESSAGE_FLAG_RECENT) {
*p++ = 'R';
}
if (msg->flags & IMAP_MESSAGE_FLAG_SEEN) {
*p++ = 'S';
}
*p = '\0';
}
static struct message *get_selected_message(struct client *client)
{
/* This is constant time, instead of using get_msg, which
* could take O(n/2) time, where n is FETCHLIST_INTERVAL. */
struct message *msg;
ITEM *selection = current_item(client->message_list.menu);
msg = item_userptr(selection);
assert(msg != NULL);
return msg;
}
/* \note doupdate() must be called after calling this function */
static void display_message_info(struct client *client, struct message *msg)
{
/* Find the message that is selected. */
char buf[STATUS_BAR_WIDTH + 1]; /* This is all that can be displayed, so no point in using a buffer any larger */
char flags[NUM_IMAP_MESSAGE_FLAGS + 1];
char *pos = buf;
size_t len;
int i;
if (!msg) {
msg = get_selected_message(client);
assert(msg != NULL);
}
/* Update status bar with more message info */
build_flags_str(msg, flags);
len = snprintf(buf, sizeof(buf), "#%d | UID %d [%s", msg->seqno, msg->uid, flags);
pos += len;
for (i = 0; i < client->sel_mbox->num_keywords; i++) { /* Check with mailbox keywords are associated this message */
if (msg->keywords & (1 << i)) {
/* This keyword is associated with this message */
len = snprintf(pos, sizeof(buf) - (pos - buf), ";%s", client->sel_mbox->keywords[i]);
pos += len;
}
}
len = snprintf(pos, sizeof(buf) - (pos - buf), "]");
client_set_status_nout(client, buf);
}
static int rerender_folder_pane(struct client *client, int selected_item)
{
cleanup_folder_menu(client);
free_folder_items(client);
/* Create menu options for each mailbox, then create the menu */
if (create_folder_items(client) || create_folder_menu(client)) {
client_error("Failed to create folder items or menu");
return -1;
}
/* Post menu */
post_menu(client->folders.menu);
/* Restore any previous selection by index */
if (selected_item != -1) {
set_current_item(client->folders.menu, client->folders.items[selected_item]);
} else {
/* Don't let the top mailbox (pseudo for aggregate stats) be selected by default */
set_current_item(client->folders.menu, client->folders.items[1]);
}
wnoutrefresh(client->win_folders);
return 0;
}
int redraw_folder_pane(struct client *client)
{
/* Save current position so we can restore.
* We can't save the ITEM* directly, so save the index. */
ITEM *selection = current_item(client->folders.menu);
int saved_folder_item = item_index(selection);
cleanup_folder_menu(client);
if (rerender_folder_pane(client, saved_folder_item)) { /* Rerender the folder pane */
return -1;
}
client->refreshflags &= ~REFRESH_FOLDERS;
return 0;
}
static const char *ncurses_strerror(int err)
{
switch (err) {
case E_OK: return "Success";
case E_SYSTEM_ERROR: return strerror(errno);
case E_BAD_ARGUMENT: return "Bad argument";
case E_POSTED: return "Already posted";
case E_BAD_STATE: return "Bad state";
case E_NO_ROOM: return "No room";
case E_NOT_POSTED: return "Not posted";
case E_NOT_CONNECTED: return "Not connected";
case E_NO_MATCH: return "No match";
case E_UNKNOWN_COMMAND: return "Unknown command";
case E_REQUEST_DENIED: return "Request denied";
default: return "Unknown error";
}
}
static const char *mouse_event_name(MEVENT *mevent)
{
mmask_t state = mevent->bstate;
if (state & BUTTON1_PRESSED) {
return "Button 1 Pressed";
} else if (state & BUTTON1_RELEASED) {
return "Button 1 Released";
} else if (state & BUTTON1_CLICKED) {
return "Button 1 Clicked";
} else if (state & BUTTON1_DOUBLE_CLICKED) {
return "Button 1 Double-Clicked";
} else if (state & BUTTON1_TRIPLE_CLICKED) {
return "Button 1 Triple-Clicked";
} else if (state & BUTTON2_PRESSED) {
return "Button 2 Pressed";
} else if (state & BUTTON2_RELEASED) {
return "Button 2 Released";
} else if (state & BUTTON2_CLICKED) {
return "Button 2 Clicked";
} else if (state & BUTTON2_DOUBLE_CLICKED) {
return "Button 2 Double-Clicked";
} else if (state & BUTTON2_TRIPLE_CLICKED) {
return "Button 2 Triple-Clicked";
} else if (state & BUTTON3_PRESSED) {
return "Button 3 Pressed";
} else if (state & BUTTON3_RELEASED) {
return "Button 3 Released";
} else if (state & BUTTON3_CLICKED) {
return "Button 3 Clicked";
} else if (state & BUTTON3_DOUBLE_CLICKED) {
return "Button 3 Double-Clicked";
} else if (state & BUTTON3_TRIPLE_CLICKED) {
return "Button 3 Triple-Clicked";
} else if (state & BUTTON4_PRESSED) {
return "Button 4 Pressed";
} else if (state & BUTTON4_RELEASED) {
return "Button 4 Released";
} else if (state & BUTTON4_CLICKED) {
return "Button 4 Clicked";
} else if (state & BUTTON4_DOUBLE_CLICKED) {
return "Button 4 Double-Clicked";
} else if (state & BUTTON4_TRIPLE_CLICKED) {
return "Button 4 Triple-Clicked";
} else if (state & BUTTON5_PRESSED) {
return "Button 5 Pressed";
} else if (state & BUTTON5_RELEASED) {
return "Button 5 Released";
} else if (state & BUTTON5_CLICKED) {
return "Button 5 Clicked";
} else if (state & BUTTON5_DOUBLE_CLICKED) {
return "Button 5 Double-Clicked";
} else if (state & BUTTON5_TRIPLE_CLICKED) {
return "Button 5 Triple-Clicked";
} else {
return "Unknown";
}
}
static void cleanup_message_pane(struct client *client)
{
cleanup_message_menu(client);
free_message_items(client);
}
/*!
* \note If the message pane menu has not changed and simply needs to be rerendered,
* then the selected_item should be passed in since it's faster to find it by that.
* However, if we need to completely reconstruct messages, then in order to reselect
* the same message after rebuilding the message cache and rebuilding a new menu,
* we need to do so by UID, since indices could change when everything is rebuilt.
*
* Since the text of the underlying menu items could change due to the new dimensions,
* we can't just use wresize, since we're fundamentally redrawing the content *in* the window.
*/
static int render_message_pane(struct client *client, int selected_item, uint32_t selected_uid, uint32_t selected_seqno)
{
int res;
if (!client->sel_mbox->total) {
/* Mailbox is empty. Just clear screen. */
werase(client->win_main);
wnoutrefresh(client->win_main);
return 0;
}
/* XXX In theory, we could just rebuild the items and then use set_menu_items to replace the menu's items
* (still calling set_current_item), which would be more efficient than destroying/recreating the menu too. */
if (create_message_items(client) || create_messages_menu(client)) {
client_error("Failed to create message items or menu");
return -1;
}
/* Post menu */
res = post_menu(client->message_list.menu);
if (res != E_OK) {
client_error("Failed to post menu: %s", ncurses_strerror(res));
return -1;
}
/* Restore any previous selection by index */
if (selected_uid) {
/* Scan all messages to find the one with the same UID as before. */
int new_index = find_message_by_uid(client, selected_uid);
/* If it's not there anymore, the message was probably expunged.
* In this case, the most natural thing to do is select
* the "nearest" message to the message that was expunged,
* one with a nearby sequence number (not necessarily UID). */
if (new_index == -1) {
new_index = client->message_list.n - 1;
}
set_current_item(client->message_list.menu, client->message_list.items[new_index]);
} else if (selected_seqno) {
int new_index = find_message_by_seqno(client, selected_seqno);
if (unlikely(new_index == -1)) {
/* This should exist, since we know whether a sequence number would exist in advance (in the case where we use this) */
client_warning("Can't find message with seqno %u?", selected_seqno);
/* If this happens, we probably missed a deletion somehow,
* as there are really fewer messages in the mailbox than we think there are. */
/*! \todo Readd the assert once the underlying bug is fixed */
#if 0
assert(0);
#endif
} else {
set_current_item(client->message_list.menu, client->message_list.items[new_index]);
}
} else if (selected_item != -1) {
if (selected_item >= client->message_list.n) {
selected_item = client->message_list.n - 1; /* Out of range (e.g. deleted the highest sequence message), just use the highest one that remains */
}
set_current_item(client->message_list.menu, client->message_list.items[selected_item]);
}
wnoutrefresh(client->win_main);
return 0;
}
static int rerender_message_pane(struct client *client, int selected_item, uint32_t selected_uid, uint32_t selected_seqno)
{
cleanup_message_pane(client);
return render_message_pane(client, selected_item, selected_uid, selected_seqno);
}
/*! \brief Completely destroy all existing messages, generate messages again, generate a new menu, and restore a particular selected item */
static int refetch_regenerate_messages(struct client *client, int selected_item, uint32_t selected_uid, uint32_t selected_seqno)
{
int res;
cleanup_message_pane(client); /* Clean up the entire message pane and start fresh */
if (client_idle_stop(client)) { /* Need to stop idling before we issue a FETCH command */
return -1;
}
res = client_fetchlist(client); /* Fetch everything over again */
if (res) {
return -1;
}
return render_message_pane(client, selected_item, selected_uid, selected_seqno); /* Rerender the message pane */
}
/* By default, autoselect the newest message in a mailbox */
#define render_message_pane_default(client) rerender_message_pane(client, -1, 0, client->sel_mbox->total)
#define rerender_message_pane_by_index(client, index) rerender_message_pane(client, index, 0, 0)
static int redraw_message_pane(struct client *client, int saved_message_item, uint32_t selected_uid)
{
int res = 0;
/* Save current position so we can restore.
* We can't save the ITEM* directly, so save the index. */
if ((client->refreshflags & REFRESH_MESSAGE_PANE) || (client->refreshtypes & (IDLE_EXISTS | IDLE_EXPUNGE))) {
display_mailbox_info(client); /* Update the footer (permament status), since stats have likely changed */
}
if (client->refreshtypes & IDLE_EXPUNGE) {
if ((uint32_t) saved_message_item >= num_messages(client)) {
/* Messages expunged such that our old index is now invalid.
* Cap it as the number of messages. */
saved_message_item = num_messages(client);
}
}
if (client->refreshflags & REFRESH_MESSAGE_PANE) {
/* If it's not just message properties that have changed, but
* entire messages have come or gone, we need to redraw the message list.
* Thus, there is no guarantee that our index into the message array will be the same message
* after as it was before, i.e. we can't even reuse the index.
* We need to use the UID to reselect the currently selected message afterwards. */
/* Small edge case: if so many messages were expunged that the message won't fill the entire screen anymore,
* then do a complete refetch as well. */
if (num_messages(client) < (uint32_t) MAIN_PANE_HEIGHT) {
res = refetch_regenerate_messages(client, saved_message_item, selected_uid, 0);
} else {
res = rerender_message_pane(client, saved_message_item, selected_uid, 0);
}
client->refreshflags &= ~REFRESH_MESSAGE_PANE;
} else {
cleanup_message_menu(client);
res = rerender_message_pane_by_index(client, saved_message_item); /* Render new message pane */
}
client->refreshflags &= ~REFRESH_MESSAGE_PANE;
return res;
}
static int handle_idle(struct client *client, int mpanevisible)
{
/* Before processing IDLE updates, save what message is currently selected,
* since sequence numbers and indices could change after that returns. */
int res = 0;
ITEM *selection = current_item(client->message_list.menu);
int saved_message_item = item_index(selection);
uint32_t selected_uid = get_selected_message(client)->uid;
res = process_idle(client);
if (res) {
return res;
}
/* Refresh interface, based on what happened */
if (!mpanevisible) {
/* If top-level message selection pane not visible, nothing to refresh. */
client->refreshflags &= ~REFRESH_MESSAGE_PANE;
} else if ((client->refreshflags & REFRESH_MESSAGE_PANE) && redraw_message_pane(client, saved_message_item, selected_uid)) {
return -1;
}
if (!mpanevisible) {
/* If the folder pane isn't visible, we don't need to update that window.
* (If the message selection pane isn't visible, neither is the folder pane).
* It will be redrawn when the current full screen window gives way back
* to the main window anyways. */
client->refreshflags &= ~REFRESH_FOLDERS;
} else if ((client->refreshflags & REFRESH_FOLDERS) && redraw_folder_pane(client)) {
return -1;
}
/* Only new mail is worth getting the user's attention about */
if (client->refreshtypes & (IDLE_EXISTS | IDLE_STATUS_EXISTS)) {
/* New mail notification. Incidentally this happens to get cleared by something else after a second,
* so this just incidentally but conveniently happens to work like a temporary pop-up notification. */
beep(); /* Ring the bell */
flash(); /* Flash the screen */
client_set_status_nout(client, "You've got mail!");
}
client->refreshtypes = 0;
doupdate();
/* That's all, resume idling if we stopped.
* We know IDLE is supported since handle_idle was called in the first place. */
return client_idle_start(client);
}
int __poll_input(struct client *client, struct pollfd *pfds, int mpanevisible, mmask_t mouse_events)
{
/* Only poll IMAP fd if idling (which can only happen if a mailbox is selected) */
time_t now;
int do_idle = client->sel_mbox && IMAP_HAS_CAPABILITY(client, IMAP_CAPABILITY_IDLE) ? 1 : 0;
pfds[0].revents = pfds[1].revents = pfds[2].revents = 0;
/* Start idling if we're not already */
if (do_idle && client_idle_start(client)) {
return -1;
}
/* Mouse support, if needed */
mousemask(client->mouse_enable ? mouse_events : 0, NULL); /* Every event except REPORT_MOUSE_POSITION */
for (;;) {
ssize_t res;
int poll_ms = -1;
now = time(NULL);
if (do_idle) {
/* Time counts from when IDLE started, not when this particular poll started */
time_t diff = now - client->idlestart;
poll_ms = (MAX_IDLE_POLL_SEC - diff) * 1000;
}
/* If idling, poll for slightly less than 30 minutes, since the server could disconnect us after that */
res = poll(pfds, do_idle ? 3 : 2, do_idle ? poll_ms : -1);
if (res < 0) {
if (errno != EINTR) {
client_debug(0, "poll failed: %s", strerror(errno));
return -1;
}
} else if (!res) {
if (do_idle && (client_idle_stop(client) || client_idle_start(client))) {
return -1;
}
} else if (pfds[0].revents & POLLIN) {
/* Activity on STDIN */
int c = getch();
if (c == ERR) {
client_warning("getch returned ERR");
return -1;
} else if (isalnum(c)) {
client_debug(8, "Input received: '%c'", c);
} else {
client_debug(7, "Input received: %d", c);
}
if (c == 330) {
/* For some reason, this is delete for me */
client_debug(5, "Translating input %d to KEY_DL", c);
c = KEY_DL;
}
if (c == ctrl('r')) {
/* Force full screen refresh (mainly for debugging, since in theory, should not be necessary) */
return KEY_RESIZE;
}
if (c == ctrl('x')) {
/* Internally handled - toggle mouse support.
* Users may want to do this as when the mouse is enabled,
* you cannot select text in the terminal like you usually can
* (e.g. to copy and paste to something outside of the terminal emulator). */
client->mouse_enable = !client->mouse_enable;
mousemask(client->mouse_enable ? mouse_events : 0, NULL); /* Every event except REPORT_MOUSE_POSITION */
client_set_status_nout(client, client->mouse_enable ? "Mouse enabled" : "Mouse disabled");
doupdate();
continue;
}
if (c == 10 || c == 13) {
/* The ENTER behavior differs by key and depending on whether nonl() has been set or not.
* The ENTER key in the middle of the keyboard returns 10 without nonl() and 13 with nonl().
* The ENTER key in the lower right of the keyboard returns 343 (which is KEY_ENTER).
* To simplify application usage, just return KEY_ENTER for all of them, since users will
* probably expect that these keys behave the same. */
/* Depending on if nonl() was called, we don't expect to get the other character,
* so ignore that in case we get a CR LF, or otherwise we'll double up and think
* we got two newlines. */
#ifdef USE_NONL
static int ignore_c = 10; /* We get a 13 on ENTER */
#else
static int ignore_c = 13; /* We get a 10 on ENTER */
#endif
if (c == ignore_c) {
client_debug(5, "Ignoring input %d", c);
continue;
}
client_debug(5, "Translating input %d to KEY_ENTER", c);
c = KEY_ENTER;
}
return c;
} else if (pfds[1].revents & POLLIN) {
/* Window resize occured */
uint64_t ec;
ssize_t rres = read(event_fd, &ec, sizeof(ec));
(void) rres;
(void) ec;
endwin(); /* Since we're manually handling resizes, we need to call endwin first */
refresh(); /* Retrieve new terminal dimensions */
/* Proceed as if ncurses had told us that a resize occured */
if (!mpanevisible) {
/* We're in a submenu, but menus above this will need to be rerendered.
* In fact, we could have multiple levels of this, so simply setting
* a flag true here isn't sufficient. For example, say A is the top level menu,
* which calls B, which calls C. Each of these have their own windows.
* If a resize occurs while running C, C will be resized immediately,
* but B will need to be redrawn when C returns to it, and same with A when B returns.
* If we returned from C to B and B noticed that the flag was true, it could resize,
* but then we'd want to clear the flag to prevent further redundant resizes of B,
* but this would prevent A from knowing about the resize.
*
* The essence of what we need is a way to notify the menu above this that it needs
* to resize immediately when we return to it, but not afterwards, while allowing
* menus above THAT menu to also be notified.
*
* Further complicating this is that submenus' windows are allocated/freed in each stack frame,
* so there is no persistent data about them once we return. We just have to work with the client structure.
*
* A simple way to make this work is to record the depth at which a resize occured.
* A menu above that can check if the resize depth would necessitate it resizing or not.
* Once it has, it can decrement the resize depth.
*
* Of course, this means we need to store the depth of each call to poll_input, which we do.
*/
assert(client->menu_depth > 0);
client->resize_depth = client->menu_depth; /* Force all windows with this depth or lower to resize */
/* If we're in a submenu, also be sure to refresh header/footer on resize,
* since those are only redrawn manually at the top-level menu. */
if (client->resize_depth) {
redraw_header_footer(client);
}
} else {
assert(client->menu_depth == 0);
}
client_debug(4, "Window resized to %dx%d at depth %d (resize %d)", COLS, LINES, client->menu_depth, client->resize_depth);
return KEY_RESIZE;
} else if (pfds[2].revents & POLLIN) {
/* Activity on IMAP file descriptor (IDLE) */
if (handle_idle(client, mpanevisible)) {
return -1;
}
} else {
client_error("Poll returned activity, but no activity?");
return -1;
}
}
}
/*!
* \brief Read line of input from console (null-terminated)
* \retval -1, 0, 1, 2 (truncation), KEY_RESIZE, KEY_ESCAPE
* \note Stop idling before calling this function
*/
static int term_getline(struct client *client, int timeout, const char *title, char *startpos, char *buf, size_t len)
{
char statusbuf[273]; /* min required to compile */
struct pollfd pfd;
char *start = buf;
buf = startpos;
pfd.fd = STDIN_FILENO;
pfd.events = POLLIN;
for (;;) {
ssize_t res;
snprintf(statusbuf, sizeof(statusbuf), "%s: %s", title, start);
client_set_status_nout(client, statusbuf);
doupdate();
res = poll(&pfd, 1, timeout);
if (res < 0) {
if (errno != EINTR) {
client_debug(0, "poll failed: %s", strerror(errno));
return -1;
}
} else if (!res) {
return 0;
} else if (pfd.revents & POLLIN) {
/* Activity on STDIN */
int c = getch();
if (c == ERR) {
client_warning("getch returned ERR");
return -1;
} else if (c == KEY_RESIZE || c == KEY_ESCAPE) {
return c;
} else if (isalnum(c)) {
*buf++ = c;
*buf = '\0';
if (--len <= 1) {
return 2;
}
} else if (c == KEY_BACKSPACE) {
if (buf > start) {
*(--buf) = '\0';
len++;
} else {
beep();
}
} else if (c == 10 || c == 13) {
*buf = '\0';
return 1;
} else {
client_debug(7, "Input received: %d", c);
beep();
}
} else {
client_error("Poll returned activity, but no activity?");
return -1;
}
}
}
int show_help_menu(struct client *client, struct pollfd *pfds, enum help_types help_types)
{
#define MAX_HELP_ITEMS 64
#define EMPTY_HELP_ITEM items[i++] = new_item(" ", "");
int res = 0;
int i = 0;
MENU *menu;
ITEM *items[MAX_HELP_ITEMS];
WINDOW *window;
/* These will show up in groups of 2.
* Additionally, because none of the items are selectable,
* this ends up looking more like a table. */
/* Global options */
if (help_types & HELP_GLOBAL) {
items[i++] = new_item("q", "Quit current screen");
items[i++] = new_item("Q", "Fast quit (entire program)");
} else {
items[i++] = new_item("ESC", "Exit current menu");
EMPTY_HELP_ITEM;
}
items[i++] = new_item("^X", "Toggle mouse support");
items[i++] = new_item("^R", "Force redraw screen");
/* Main menu options */
if (help_types & HELP_MAIN) {
items[i++] = new_item("LEFT", "Switch focus to folder pane");
items[i++] = new_item("RIGHT", "Switch focus to message pane");
items[i++] = new_item("ENTER", "Select mailbox or message");
EMPTY_HELP_ITEM;
items[i++] = new_item("i", "View mailbox or message info");