mbox: support mails containing null bytes
[mailx.git] / mailx.c
blobc1ee601216e0290e44c7c36ec8a075a6fb2118de
1 /*
2 * neatmailx, a small mailx clone
4 * Copyright (C) 2009-2013 Ali Gholami Rudi <ali at rudi dot ir>
6 * This program is released under the modified BSD license.
7 */
8 #include <ctype.h>
9 #include <fcntl.h>
10 #include <stdlib.h>
11 #include <string.h>
12 #include <unistd.h>
13 #include <sys/stat.h>
14 #include <sys/types.h>
15 #include "config.h"
16 #include "mbox.h"
17 #include "mime.h"
18 #include "send.h"
19 #include "sort.h"
20 #include "str.h"
21 #include "util.h"
23 #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
24 #define MAILBUF (1 << 16)
25 #define MAXLINE (1 << 8)
26 #define BUFSIZE (1 << 12)
28 static struct mbox *mbox;
29 static struct sort *sort;
30 static int cur;
31 static int sel[MAXMAILS];
32 static int nsel;
33 static int quit;
35 static int read_line(char *dst, int size)
37 static char buf[BUFSIZE];
38 static int cur;
39 static int len;
40 int nw = 0;
41 while (1) {
42 int cur_len = MIN(len - cur, size - nw - 1);
43 char *nl = memchr(buf + cur, '\n', cur_len);
44 int nr = nl ? nl - buf - cur + 1 : cur_len;
45 if (nr) {
46 memcpy(dst + nw, buf + cur, nr);
47 nw += nr;
48 cur += nr;
49 dst[nw] = '\0';
51 if (nl || nw == size - 1)
52 return nw;
53 cur = 0;
54 if ((len = read(0, buf, BUFSIZE)) <= 0)
55 return -1;
57 return nw;
60 static int utf8len(int c)
62 if (c <= 0x7f)
63 return 1;
64 if (c >= 0xfc)
65 return 6;
66 if (c >= 0xf8)
67 return 5;
68 if (c >= 0xf0)
69 return 4;
70 if (c >= 0xe0)
71 return 3;
72 if (c >= 0xc0)
73 return 2;
76 static char *till_eol(char *r, int len, char *s, char *e, int fill)
78 int i = 0;
79 while (i < len && s && s < e && *s) {
80 int l = utf8len((unsigned char) *s);
81 memcpy(r, s, l);
82 /* ignoring line breaks */
83 if (*s != '\r' && *s != '\n') {
84 r += l;
85 i++;
87 s += l;
89 while (i++ < fill)
90 *r++ = ' ';
91 return r;
94 static int msg_num(char *num)
96 int n = -1;
97 if (!*num || !strcmp(".", num))
98 n = cur;
99 if (isdigit(*num))
100 n = atoi(num);
101 if (!strcmp("$", num))
102 n = mbox->n - 1;
103 if (*num == '-')
104 n = cur - (*(num + 1) ? atoi(num + 1) : 1);
105 if (*num == '+')
106 n = cur + (*(num + 1) ? atoi(num + 1) : 1);
107 if (!strcmp(",", num)) {
108 int i = cur;
109 while (++i < sort->n) {
110 int stat = sort->mails[i]->stat;
111 if (!(stat & STAT_READ) && (stat & STAT_NEW)) {
112 n = i;
113 break;
117 if (n < 0 || n >= mbox->n)
118 return -1;
119 return n;
122 static int stat_char(struct mail *mail)
124 if (mail->stat & STAT_NEW)
125 return mail->stat & STAT_READ ? 'R' : 'N';
126 else
127 return mail->stat & STAT_READ ? ' ' : 'U';
130 /* search s for r */
131 static char *memstr(char *s, int slen, char *r, int rlen)
133 char *d = s + slen;
134 while (s && s < d) {
135 if (!strncmp(s, r, MIN(d - s, rlen)))
136 return s;
137 s = memchr(s + 1, r[0], d - s - 1);
139 return NULL;
142 static int search(char *args, int all, int dir)
144 static char hdr_name[MAXLINE]; /* previous search header */
145 static char hdr_val[MAXLINE]; /* previous search keyword */
146 static int stat; /* previous search status */
147 char *beg = strchr(args, '(');
148 char *spc = beg ? strchr(beg, ' ') : NULL;
149 char *end = spc ? strchr(spc, ')') : NULL;
150 int i;
151 if (beg && (!end || !spc))
152 return 0;
153 if (beg) {
154 int hdr_len = spc - beg - 1;
155 put_mem(hdr_name, beg + 1, hdr_len);
156 hdr_name[hdr_len] = '\0';
157 while (isspace(*spc))
158 spc++;
159 put_mem(hdr_val, spc, end - spc);
160 hdr_val[end - spc] = '\0';
161 stat = isalpha(*(args + 1)) ? toupper(*(args + 1)) : 0;
163 for (i = cur + dir; i >= 0 && i < sort->n; i += dir) {
164 if (!stat || stat_char(sort->mails[i]) == stat) {
165 char *hdr = mail_hdr(sort->mails[i], hdr_name);
166 if (hdr && memstr(hdr, hdr_len(hdr),
167 hdr_val, strlen(hdr_val))) {
168 if (!all) {
169 cur = i;
170 return 1;
172 sel[nsel++] = i;
176 return nsel;
179 static int sel_msgs(char *args, int all)
181 char num[MAXLINE];
182 int i;
183 nsel = 0;
184 if (*args == '/')
185 return search(args, all, +1);
186 if (*args == '?')
187 return search(args, all, -1);
188 args = cut_word(num, args);
189 while (1) {
190 char *com;
191 int beg = -1;
192 int end = -1;
193 if (*num && (com = strchr(num + 1, ','))) {
194 char beg_str[MAXLINE];
195 memcpy(beg_str, num, com - num);
196 beg_str[com - num] = '\0';
197 beg = msg_num(beg_str);
198 end = msg_num(com + 1);
199 } else if (!strcmp("%", num)) {
200 beg = 0;
201 end = sort->n - 1;
202 } else {
203 beg = msg_num(num);
204 end = beg;
206 if (beg != -1 && end != -1) {
207 if (!all) {
208 cur = beg;
209 return 1;
211 for (i = beg; i <= end; i++)
212 sel[nsel++] = i;
214 args = cut_word(num, args);
215 if (!*num)
216 break;
218 return nsel;
221 static char mimes_buf[MAXMIME][MAILBUF];
222 static struct mail mimes_mail[MAXMIME];
223 static struct mail *mimes_main[MAXMIME];
224 static int mimes_cur;
226 static void mimes_clear(void)
228 mimes_cur = 0;
229 memset(mimes_main, 0, sizeof(mimes_main));
232 static void mimes_free(struct mail *mail)
234 mail->data = NULL;
237 static void mimes_add(struct mail *mail)
239 struct mail *mime_mail = &mimes_mail[mimes_cur];
240 int len;
241 if (mimes_main[mimes_cur])
242 mimes_main[mimes_cur]->data = NULL;
243 mimes_main[mimes_cur] = mail;
244 len = mime_decode(mimes_buf[mimes_cur], mail->head,
245 MIN(mail->len, MAILBUF));
246 memset(mime_mail, 0, sizeof(*mime_mail));
247 mail_read(mime_mail, mimes_buf[mimes_cur], mimes_buf[mimes_cur] + len);
248 /* handle ^From_ inside msgs */
249 mime_mail->body_len += len - mime_mail->len;
250 mime_mail->len = len;
251 mail->data = mime_mail;
252 mimes_cur = (mimes_cur + 1) % MAXMIME;
255 static struct mail *mimes_get(struct mail *mail)
257 return mail->data ? mail->data : mail;
260 static void cmd_mime(char *pre, char *arg)
262 int i;
263 if (!sel_msgs(pre, 1))
264 return;
265 for (i = 0; i < nsel; i++)
266 mimes_add(sort->mails[sel[i]]);
269 static void show_mail(char *args, int filthdr)
271 struct mail *mail;
272 char buf[MAILBUF];
273 char *pg_args[] = {PAGER, NULL};
274 char *s = buf;
275 if (!sel_msgs(args, 0))
276 return;
277 mail = sort->mails[cur];
278 mail->stat |= STAT_READ;
279 mail = mimes_get(mail);
280 if (filthdr) {
281 s += mail_head(mail, buf, sizeof(buf),
282 hdr_filt, ARRAY_SIZE(hdr_filt));
283 s = put_mem(s, mail->body, MIN(sizeof(buf) - (s - buf),
284 mail->body_len));
285 } else {
286 s = put_mem(s, mail->head, MIN(sizeof(buf), mail->len));
288 exec_pipe(PAGER, pg_args, buf, s - buf);
291 static void cmd_page(char *pre, char *arg)
293 show_mail(pre, 1);
296 static void cmd_cat(char *pre, char *arg)
298 show_mail(pre, 0);
301 static int isreply(char *s)
303 return tolower(s[0]) == 'r' && tolower(s[1]) == 'e' && s[2] == ':';
306 static char *put_hdr(struct mail *mail, char *name, char *dst, int wid, int fill)
308 char *hdr = mail_hdr(mail, name);
309 if (hdr) {
310 hdr = hdr + strlen(name) + 1;
311 if (TRIM_RE && isreply(hdr))
312 hdr += 3;
313 while (*hdr == ' ')
314 hdr++;
315 dst = till_eol(dst, wid, hdr, hdr + hdr_len(hdr), fill ? wid : 0);
316 } else {
317 while (fill && --wid >= 0)
318 *dst++ = ' ';
320 return dst;
323 #define put_clr(s, c) ((COLORS) ? (put_str(s, c)) : (s))
325 static void cmd_head(char *pre, char *arg)
327 int beg, end;
328 int i;
329 if (!sel_msgs(pre, 0))
330 return;
331 beg = cur / NHEAD * NHEAD;
332 end = MIN(beg + NHEAD, mbox->n);
333 for (i = beg; i < end; i++) {
334 struct mail *mail = sort->mails[i];
335 char fmt[MAXLINE];
336 char *s = fmt;
337 int col;
338 int indent;
339 *s++ = i == cur ? '>' : ' ';
340 s = put_clr(s, mail->stat & STAT_NEW ? "\33[1;31m" : "\33[33m");
341 *s++ = mail->stat & STAT_DEL ? 'D' : ' ';
342 *s++ = stat_char(mail);
343 s = put_clr(s, "\33[1;32m");
344 s = put_int(s, i, DIGITS);
345 *s++ = ' ';
346 *s++ = ' ';
347 mail = mimes_get(mail);
348 s = put_clr(s, "\33[0;34m");
349 s = put_hdr(mail, "From:", s, 16, 1);
350 *s++ = ' ';
351 *s++ = ' ';
352 col = 3 + DIGITS + 2 + 16 + 2;
353 indent = sort_level(sort, i) * 2;
354 if (!mail_hdr(sort->mails[i], "in-reply-to"))
355 s = put_clr(s, "\33[1;36m");
356 else
357 s = put_clr(s, indent ? "\33[32m" : "\33[36m");
358 while (indent-- && col < WIDTH) {
359 col++;
360 *s++ = ' ';
362 s = put_hdr(mail, "Subject:", s, WIDTH - col, 0);
363 s = put_clr(s, "\33[m");
364 *s++ = '\n';
365 write(1, fmt, s - fmt);
369 static void cmd_z(char *pre, char *arg)
371 int page = -1;
372 if (!*pre)
373 page = MIN(cur + NHEAD, mbox->n) / NHEAD;
374 if (*pre == '-')
375 page = cur / NHEAD - (*(pre + 1) ? atoi(pre + 1) : 1);
376 if (*pre == '+')
377 page = cur / NHEAD + (*(pre + 1) ? atoi(pre + 1) : 1);
378 if (*pre == '$')
379 page = mbox->n / NHEAD;
380 if (isdigit(*pre))
381 page = atoi(pre);
382 if (page >= 0 && page * NHEAD < mbox->n) {
383 cur = page * NHEAD;
384 cmd_head("", "");
388 static void cmd_del(char *pre, char *arg)
390 int i;
391 if (!sel_msgs(pre, 1))
392 return;
393 for (i = 0; i < nsel; i++)
394 sort->mails[sel[i]]->stat |= STAT_DEL;
397 static void cmd_undel(char *pre, char *arg)
399 int i;
400 if (!sel_msgs(pre, 1))
401 return;
402 for (i = 0; i < nsel; i++) {
403 sort->mails[sel[i]]->stat &= ~STAT_DEL;
404 mimes_free(sort->mails[sel[i]]);
408 static void print(char *s)
410 write(1, s, strlen(s));
413 static int is_mbox(char *filename)
415 struct stat st;
416 stat(filename, &st);
417 return S_ISREG(st.st_mode);
420 static char *filename(char *path)
422 char *slash = strrchr(path, '/');
423 return slash ? slash + 1 : path;
426 static void sum_mbox(void)
428 char msg[MAXLINE];
429 char *s = msg;
430 int new = 0;
431 int unread = 0;
432 int del = 0;
433 int i;
434 s = put_str(s, filename(mbox->path));
435 s = put_str(s, ": ");
436 s = put_int(s, mbox->n, DIGITS);
437 s = put_str(s, " msgs ");
438 for (i = 0; i < mbox->n; i++) {
439 if (!(mbox->mails[i].stat & STAT_READ)) {
440 unread++;
441 if (mbox->mails[i].stat & STAT_NEW)
442 new++;
444 if (mbox->mails[i].stat & STAT_DEL)
445 del++;
447 if (new) {
448 s = put_int(s, new, DIGITS);
449 s = put_str(s, " new ");
451 if (unread) {
452 s = put_int(s, unread, DIGITS);
453 s = put_str(s, " unread ");
455 if (del) {
456 s = put_int(s, del, DIGITS);
457 s = put_str(s, " del ");
459 s = put_str(s, "\n");
460 print(msg);
463 static void open_mbox(char *filename)
465 mbox = mbox_alloc(filename);
466 sort = sort_alloc(mbox, THREADED ? SORT_THREAD : 0);
467 cur = 0;
468 sum_mbox();
471 static void close_mbox(void)
473 mimes_clear();
474 sort_free(sort);
475 mbox_free(mbox);
478 static void mbox_old(struct mbox *mbox)
480 int i;
481 for (i = 0; i < mbox->n; i++) {
482 struct mail *mail = &mbox->mails[i];
483 mail->stat = (mail->stat & ~STAT_NEW) | STAT_OLD;
487 static int has_mail(char *path)
489 struct stat st;
490 if (stat(path, &st) == -1)
491 return 0;
492 return st.st_mtime > st.st_atime;
495 static int mbox_path(char *path, char *addr)
497 char *s = path;
498 int i;
499 if (*addr == '+') {
500 s = put_str(s, FOLDER);
501 s = put_str(s, addr + 1);
503 if (*addr == ',') {
504 for (i = 0; i < ARRAY_SIZE(boxes); i++) {
505 if (has_mail(boxes[i])) {
506 s = put_str(s, boxes[i]);
507 break;
511 if (!strcmp(".", addr) && mbox)
512 s = put_str(s, mbox->path);
513 if (s == path && *addr)
514 s = put_str(s, addr);
515 return s - path;
518 static void warn_nomem(void)
520 print("no mem for new msgs\n");
523 static void cmd_fold(char *pre, char *arg)
525 if (*arg) {
526 char path[MAXPATHLEN];
527 if (mbox_path(path, arg) && is_mbox(path)) {
528 mbox_old(mbox);
529 if (mbox_write(mbox) == -1) {
530 warn_nomem();
531 return;
533 close_mbox();
534 open_mbox(path);
536 } else {
537 sum_mbox();
541 static void cmd_news(char *pre, char *arg)
543 char msg[MAXLINE];
544 char *s;
545 int i;
546 for (i = 0; i < ARRAY_SIZE(boxes); i++) {
547 if (has_mail(boxes[i])) {
548 s = msg;
549 s = put_str(s, "\t");
550 s = put_str(s, filename(boxes[i]));
551 s = put_str(s, "\n");
552 print(msg);
557 static void cmd_inc(char *pre, char *arg)
559 char msg[MAXLINE];
560 int new = mbox_inc(mbox);
561 char *s;
562 if (new == -1)
563 warn_nomem();
564 if (new > 0) {
565 sort_inc(sort);
566 s = msg;
567 s = put_int(s, new, DIGITS);
568 s = put_str(s, " new\n");
569 print(msg);
573 static void cmd_next(char *pre, char *arg)
575 if (!pre || !*pre) {
576 if (cur + 1 < mbox->n) {
577 cur++;
578 } else {
579 print("EOF\n");
580 return;
583 show_mail(pre, 1);
586 static void copy_mail(char *msgs, char *dst, int del)
588 char path[MAXLINE];
589 int i;
590 int fd;
591 if (!dst || !*dst)
592 return;
593 if (!sel_msgs(msgs, 1))
594 return;
595 mbox_path(path, dst);
596 fd = open(path, O_WRONLY | O_APPEND | O_CREAT, S_IRUSR | S_IWUSR);
597 if (fd == -1) {
598 print("failed to open mbox for writing\n");
599 return;
601 for (i = 0; i < nsel; i++) {
602 struct mail *mail = sort->mails[sel[i]];
603 mail_write(mail, fd);
604 if (del)
605 mail->stat |= STAT_DEL;
607 close(fd);
610 static void cmd_copy(char *pre, char *arg)
612 copy_mail(pre, arg, 0);
615 static void cmd_move(char *pre, char *arg)
617 copy_mail(pre, arg, 1);
620 static void compose(struct draft *draft)
622 char record[MAXPATHLEN] = "";
623 char line[MAXLINE];
624 char cmd[MAXLINE];
625 char *pg_args[] = {PAGER, NULL};
626 if (RECORD)
627 mbox_path(record, RECORD);
628 else if (mbox)
629 strcpy(record, mbox->path);
630 while (read_line(line, sizeof(line)) > 0) {
631 cut_word(cmd, line);
632 if (!strcmp("~e", cmd))
633 draft_edit(draft, EDITOR);
634 if (!strcmp("~v", cmd))
635 draft_edit(draft, VISUAL);
636 if (!strcmp("~.", cmd)) {
637 if (*record)
638 draft_save(draft, record);
639 draft_send(draft);
640 break;
642 if (!strcmp("~p", cmd))
643 exec_pipe(PAGER, pg_args, draft->mail, draft->len);
644 if (!strcmp("~q", cmd) || !strcmp("~x", cmd))
645 break;
649 static void cmd_mail(char *pre, char *arg)
651 struct draft draft;
652 draft_init(&draft, *arg ? &arg : NULL, *arg ? 1 : 0, NULL);
653 compose(&draft);
656 static void cmd_reply(char *pre, char *arg)
658 struct draft draft;
659 if (!sel_msgs(pre, 0))
660 return;
661 draft_reply(&draft, mimes_get(sort->mails[cur]));
662 compose(&draft);
665 static void prompt(void)
667 write(1, "? ", 2);
670 static void cmd_quit(char *pre, char *arg)
672 mbox_old(mbox);
673 if (mbox_write(mbox) == -1)
674 warn_nomem();
675 else
676 quit = 1;
679 static void cmd_exit(char *pre, char *arg)
681 quit = 1;
684 static void cmd_clear(char *pre, char *arg)
686 print("\x1b[H\x1b[J");
689 static struct cmds {
690 char *name;
691 void (*cmd)(char *pre, char *arg);
692 } cmds[] = {
693 {"page", cmd_page},
694 {"next", cmd_next},
695 {"header", cmd_head},
696 {"ls", cmd_head},
697 {"file", cmd_fold},
698 {"folder", cmd_fold},
699 {"inc", cmd_inc},
700 {"z", cmd_z},
701 {"mail", cmd_mail},
702 {"copy", cmd_copy},
703 {"cp", cmd_copy},
704 {"move", cmd_move},
705 {"mv", cmd_move},
706 {"reply", cmd_reply},
707 {"cat", cmd_cat},
708 {"delete", cmd_del},
709 {"rm", cmd_del},
710 {"cd", cmd_fold},
711 {"undelete", cmd_undel},
712 {"quit", cmd_quit},
713 {"exit", cmd_exit},
714 {"xit", cmd_exit},
715 {"news", cmd_news},
716 {"mime", cmd_mime},
717 {"clear", cmd_clear},
720 static void cmd_parse(char *line, char *pre, char *cmd, char *arg)
722 while (*line && isspace(*line))
723 line++;
724 while (*line && !isalpha(*line)) {
725 if ((line[0] == '/' || line[0] == '?') &&
726 (line[1] == '(' || line[2] == '('))
727 while (*line && *line != ')')
728 *pre++ = *line++;
729 *pre++ = *line++;
731 *pre = '\0';
732 while (*line && isspace(*line))
733 line++;
734 while (*line && isalpha(*line))
735 *cmd++ = *line++;
736 *cmd = '\0';
737 while (*line && isspace(*line))
738 line++;
739 while (*line && !isspace(*line))
740 *arg++ = *line++;
741 *arg = '\0';
744 static void loop(void)
746 char line[MAXLINE];
747 char pre[MAXLINE];
748 char cmd[MAXLINE];
749 char arg[MAXLINE];
750 int len;
751 prompt();
752 while (read_line(line, sizeof(line)) > 0) {
753 cmd_parse(line, pre, cmd, arg);
754 len = strlen(cmd);
755 int i;
756 if (!len)
757 cmd_next(pre, arg);
758 else
759 for (i = 0; i < ARRAY_SIZE(cmds); i++)
760 if (!strncmp(cmds[i].name, cmd, len)) {
761 cmds[i].cmd(pre, arg);
762 break;
764 if (quit)
765 break;
766 else
767 prompt();
771 int main(int argc, char *argv[])
773 int i = 0;
774 char *filename = NULL;
775 char *subj = NULL;
776 while (++i < argc) {
777 if (argv[i][0] != '-')
778 break;
779 if (!strcmp("-f", argv[i]))
780 filename = argv[++i];
781 if (!strcmp("-s", argv[i]))
782 subj = argv[++i];
784 if (filename) {
785 char path[MAXPATHLEN];
786 if (mbox_path(path, filename) && is_mbox(path)) {
787 open_mbox(path);
788 loop();
789 close_mbox();
791 } else {
792 struct draft draft;
793 draft_init(&draft, argv + i, argc - i, subj);
794 compose(&draft);
796 return 0;