mailx: simplify mail searching
[mailx.git] / mailx.c
blob5c40d6eee62984bed5cd4148e495536e5a212f75
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(STDIN_FILENO, 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)
78 int i = 0;
79 while (i < len && s && s < e && *s) {
80 int l = utf8len(*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 return r;
92 static int msg_num(char *num)
94 int n = -1;
95 if (!*num || !strcmp(".", num))
96 n = cur;
97 if (isdigit(*num))
98 n = atoi(num);
99 if (!strcmp("$", num))
100 n = mbox->n - 1;
101 if (*num == '-')
102 n = cur - (*(num + 1) ? atoi(num + 1) : 1);
103 if (*num == '+')
104 n = cur + (*(num + 1) ? atoi(num + 1) : 1);
105 if (!strcmp(",", num)) {
106 int i = cur;
107 while (++i < sort->n) {
108 int stat = sort->mails[i]->stat;
109 if (!(stat & STAT_READ) && (stat & STAT_NEW)) {
110 n = i;
111 break;
115 if (n < 0 || n >= mbox->n)
116 return -1;
117 return n;
120 static int stat_char(struct mail *mail)
122 if (mail->stat & STAT_NEW)
123 return mail->stat & STAT_READ ? 'R' : 'N';
124 else
125 return mail->stat & STAT_READ ? ' ' : 'U';
128 /* search s for r */
129 static char *memstr(char *s, int slen, char *r, int rlen)
131 char *d = s + slen;
132 while (s && s < d) {
133 if (!strncmp(s, r, MIN(d - s, rlen)))
134 return s;
135 s = memchr(s + 1, r[0], d - s - 1);
137 return NULL;
140 static int search(char *args, int all, int dir)
142 static char hdr_name[MAXLINE]; /* previous search header */
143 static char hdr_val[MAXLINE]; /* previous search keyword */
144 static int stat; /* previous search status */
145 char *beg = strchr(args, '(');
146 char *spc = beg ? strchr(beg, ' ') : NULL;
147 char *end = spc ? strchr(spc, ')') : NULL;
148 int i;
149 if (beg && (!end || !spc))
150 return 0;
151 if (beg) {
152 int hdr_len = spc - beg - 1;
153 put_mem(hdr_name, beg + 1, hdr_len);
154 hdr_name[hdr_len] = '\0';
155 while (isspace(*spc))
156 spc++;
157 put_mem(hdr_val, spc, end - spc);
158 hdr_val[end - spc] = '\0';
159 stat = isalpha(*(args + 1)) ? toupper(*(args + 1)) : 0;
161 for (i = cur + dir; i >= 0 && i < sort->n; i += dir) {
162 if (!stat || stat_char(sort->mails[i]) == stat) {
163 char *hdr = mail_hdr(sort->mails[i], hdr_name);
164 if (hdr && memstr(hdr, hdr_len(hdr),
165 hdr_val, strlen(hdr_val))) {
166 if (!all) {
167 cur = i;
168 return 1;
170 sel[nsel++] = i;
174 return nsel;
177 static int sel_msgs(char *args, int all)
179 char num[MAXLINE];
180 int i;
181 nsel = 0;
182 if (*args == '/')
183 return search(args, all, +1);
184 if (*args == '?')
185 return search(args, all, -1);
186 args = cut_word(num, args);
187 while (1) {
188 char *com;
189 int beg = -1;
190 int end = -1;
191 if (*num && (com = strchr(num + 1, ','))) {
192 char beg_str[MAXLINE];
193 memcpy(beg_str, num, com - num);
194 beg_str[com - num] = '\0';
195 beg = msg_num(beg_str);
196 end = msg_num(com + 1);
197 } else if (!strcmp("%", num)) {
198 beg = 0;
199 end = sort->n - 1;
200 } else {
201 beg = msg_num(num);
202 end = beg;
204 if (beg != -1 && end != -1) {
205 if (!all) {
206 cur = beg;
207 return 1;
209 for (i = beg; i <= end; i++)
210 sel[nsel++] = i;
212 args = cut_word(num, args);
213 if (!*num)
214 break;
216 return nsel;
219 static char mimes_buf[MAXMIME][MAILBUF];
220 static struct mail mimes_mail[MAXMIME];
221 static struct mail *mimes_main[MAXMIME];
222 static int mimes_cur;
224 static void mimes_clear(void)
226 mimes_cur = 0;
227 memset(mimes_main, 0, sizeof(mimes_main));
230 static void mimes_free(struct mail *mail)
232 mail->data = NULL;
235 static void mimes_add(struct mail *mail)
237 struct mail *mime_mail = &mimes_mail[mimes_cur];
238 int len;
239 if (mimes_main[mimes_cur])
240 mimes_main[mimes_cur]->data = NULL;
241 mimes_main[mimes_cur] = mail;
242 len = mime_decode(mimes_buf[mimes_cur], mail->head,
243 MIN(mail->len, MAILBUF));
244 memset(mime_mail, 0, sizeof(*mime_mail));
245 mail_read(mime_mail, mimes_buf[mimes_cur]);
246 /* handle ^From_ inside msgs */
247 mime_mail->body_len += len - mime_mail->len;
248 mime_mail->len = len;
249 mail->data = mime_mail;
250 mimes_cur = (mimes_cur + 1) % MAXMIME;
253 static struct mail *mimes_get(struct mail *mail)
255 return mail->data ? mail->data : mail;
258 static void cmd_mime(char *pre, char *arg)
260 int i;
261 if (!sel_msgs(pre, 1))
262 return;
263 for (i = 0; i < nsel; i++)
264 mimes_add(sort->mails[sel[i]]);
267 static void show_mail(char *args, int filthdr)
269 struct mail *mail;
270 char buf[MAILBUF];
271 char *pg_args[] = {PAGER, NULL};
272 char *s = buf;
273 if (!sel_msgs(args, 0))
274 return;
275 mail = sort->mails[cur];
276 mail->stat |= STAT_READ;
277 mail = mimes_get(mail);
278 if (filthdr) {
279 s += mail_head(mail, buf, sizeof(buf),
280 hdr_filt, ARRAY_SIZE(hdr_filt));
281 s = put_mem(s, mail->body, MIN(sizeof(buf) - (s - buf),
282 mail->body_len));
283 } else {
284 s = put_mem(s, mail->head, MIN(sizeof(buf), mail->len));
286 exec_pipe(PAGER, pg_args, buf, s - buf);
289 static void cmd_page(char *pre, char *arg)
291 show_mail(pre, 1);
294 static void cmd_cat(char *pre, char *arg)
296 show_mail(pre, 0);
299 static char *put_hdr(struct mail *mail, char *name, char *dst, int wid, int fill)
301 char *hdr = mail_hdr(mail, name);
302 char *lim = dst + wid;
303 if (hdr) {
304 hdr = hdr + strlen(name) + 1;
305 if (TRIM_RE && tolower(hdr[0]) == 'r' && tolower(hdr[1]) == 'e' &&
306 tolower(hdr[2]) == ':')
307 hdr += 3;
308 while (*hdr == ' ')
309 hdr++;
310 dst = till_eol(dst, wid, hdr, hdr + hdr_len(hdr));
312 while (fill && dst < lim)
313 *dst++ = ' ';
314 return dst;
317 #define put_clr(s, c) ((COLORS) ? (put_str(s, c)) : (s))
319 static void cmd_head(char *pre, char *arg)
321 int beg, end;
322 int i;
323 if (!sel_msgs(pre, 0))
324 return;
325 beg = cur / NHEAD * NHEAD;
326 end = MIN(beg + NHEAD, mbox->n);
327 for (i = beg; i < end; i++) {
328 struct mail *mail = sort->mails[i];
329 char fmt[MAXLINE];
330 char *s = fmt;
331 int col;
332 int indent;
333 *s++ = i == cur ? '>' : ' ';
334 s = put_clr(s, mail->stat & STAT_NEW ? "\33[1;31m" : "\33[33m");
335 *s++ = mail->stat & STAT_DEL ? 'D' : ' ';
336 *s++ = stat_char(mail);
337 s = put_clr(s, "\33[1;32m");
338 s = put_int(s, i, DIGITS);
339 *s++ = ' ';
340 *s++ = ' ';
341 mail = mimes_get(mail);
342 s = put_clr(s, "\33[0;34m");
343 s = put_hdr(mail, "From:", s, 16, 1);
344 *s++ = ' ';
345 *s++ = ' ';
346 col = 3 + DIGITS + 2 + 16 + 2;
347 indent = sort_level(sort, i) * 2;
348 if (!mail_hdr(sort->mails[i], "in-reply-to"))
349 s = put_clr(s, "\33[1;36m");
350 else
351 s = put_clr(s, indent ? "\33[32m" : "\33[36m");
352 while (indent-- && col < WIDTH) {
353 col++;
354 *s++ = ' ';
356 s = put_hdr(mail, "Subject:", s, WIDTH - col, 0);
357 s = put_clr(s, "\33[m");
358 *s++ = '\n';
359 write(1, fmt, s - fmt);
363 static void cmd_z(char *pre, char *arg)
365 int page = -1;
366 if (!*pre)
367 page = MIN(cur + NHEAD, mbox->n) / NHEAD;
368 if (*pre == '-')
369 page = cur / NHEAD - (*(pre + 1) ? atoi(pre + 1) : 1);
370 if (*pre == '+')
371 page = cur / NHEAD + (*(pre + 1) ? atoi(pre + 1) : 1);
372 if (*pre == '$')
373 page = mbox->n / NHEAD;
374 if (isdigit(*pre))
375 page = atoi(pre);
376 if (page >= 0 && page * NHEAD < mbox->n) {
377 cur = page * NHEAD;
378 cmd_head("", "");
382 static void cmd_del(char *pre, char *arg)
384 int i;
385 if (!sel_msgs(pre, 1))
386 return;
387 for (i = 0; i < nsel; i++)
388 sort->mails[sel[i]]->stat |= STAT_DEL;
391 static void cmd_undel(char *pre, char *arg)
393 int i;
394 if (!sel_msgs(pre, 1))
395 return;
396 for (i = 0; i < nsel; i++) {
397 sort->mails[sel[i]]->stat &= ~STAT_DEL;
398 mimes_free(sort->mails[sel[i]]);
402 static void print(char *s)
404 write(1, s, strlen(s));
407 static int is_mbox(char *filename)
409 struct stat st;
410 stat(filename, &st);
411 return S_ISREG(st.st_mode);
414 static char *filename(char *path)
416 char *slash = strrchr(path, '/');
417 return slash ? slash + 1 : path;
420 static void sum_mbox(void)
422 char msg[MAXLINE];
423 char *s = msg;
424 int new = 0;
425 int unread = 0;
426 int del = 0;
427 int i;
428 s = put_str(s, filename(mbox->path));
429 s = put_str(s, ": ");
430 s = put_int(s, mbox->n, DIGITS);
431 s = put_str(s, " msgs ");
432 for (i = 0; i < mbox->n; i++) {
433 if (!(mbox->mails[i].stat & STAT_READ)) {
434 unread++;
435 if (mbox->mails[i].stat & STAT_NEW)
436 new++;
438 if (mbox->mails[i].stat & STAT_DEL)
439 del++;
441 if (new) {
442 s = put_int(s, new, DIGITS);
443 s = put_str(s, " new ");
445 if (unread) {
446 s = put_int(s, unread, DIGITS);
447 s = put_str(s, " unread ");
449 if (del) {
450 s = put_int(s, del, DIGITS);
451 s = put_str(s, " del ");
453 s = put_str(s, "\n");
454 print(msg);
457 static void open_mbox(char *filename)
459 mbox = mbox_alloc(filename);
460 sort = sort_alloc(mbox, THREADED ? SORT_THREAD : 0);
461 cur = 0;
462 sum_mbox();
465 static void close_mbox(void)
467 mimes_clear();
468 sort_free(sort);
469 mbox_free(mbox);
472 static void mbox_old(struct mbox *mbox)
474 int i;
475 for (i = 0; i < mbox->n; i++) {
476 struct mail *mail = &mbox->mails[i];
477 mail->stat = (mail->stat & ~STAT_NEW) | STAT_OLD;
481 static int has_mail(char *path)
483 struct stat st;
484 if (stat(path, &st) == -1)
485 return 0;
486 return st.st_mtime > st.st_atime;
489 static int mbox_path(char *path, char *addr)
491 char *s = path;
492 int i;
493 if (*addr == '+') {
494 s = put_str(s, FOLDER);
495 s = put_str(s, addr + 1);
497 if (*addr == ',') {
498 for (i = 0; i < ARRAY_SIZE(boxes); i++) {
499 if (has_mail(boxes[i])) {
500 s = put_str(s, boxes[i]);
501 break;
505 if (!strcmp(".", addr) && mbox)
506 s = put_str(s, mbox->path);
507 if (s == path && *addr)
508 s = put_str(s, addr);
509 return s - path;
512 static void warn_nomem(void)
514 print("no mem for new msgs\n");
517 static void cmd_fold(char *pre, char *arg)
519 if (*arg) {
520 char path[MAXPATHLEN];
521 if (mbox_path(path, arg) && is_mbox(path)) {
522 mbox_old(mbox);
523 if (mbox_write(mbox) == -1) {
524 warn_nomem();
525 return;
527 close_mbox();
528 open_mbox(path);
530 } else {
531 sum_mbox();
535 static void cmd_news(char *pre, char *arg)
537 char msg[MAXLINE];
538 char *s;
539 int i;
540 for (i = 0; i < ARRAY_SIZE(boxes); i++) {
541 if (has_mail(boxes[i])) {
542 s = msg;
543 s = put_str(s, "\t");
544 s = put_str(s, filename(boxes[i]));
545 s = put_str(s, "\n");
546 print(msg);
551 static void cmd_inc(char *pre, char *arg)
553 char msg[MAXLINE];
554 int new = mbox_inc(mbox);
555 char *s;
556 if (new == -1)
557 warn_nomem();
558 if (new > 0) {
559 sort_inc(sort);
560 s = msg;
561 s = put_int(s, new, DIGITS);
562 s = put_str(s, " new\n");
563 print(msg);
567 static void cmd_next(char *pre, char *arg)
569 if (!pre || !*pre) {
570 if (cur + 1 < mbox->n) {
571 cur++;
572 } else {
573 print("EOF\n");
574 return;
577 show_mail(pre, 1);
580 static void copy_mail(char *msgs, char *dst, int del)
582 char path[MAXLINE];
583 int i;
584 int fd;
585 if (!dst || !*dst)
586 return;
587 if (!sel_msgs(msgs, 1))
588 return;
589 mbox_path(path, dst);
590 fd = open(path, O_WRONLY | O_APPEND | O_CREAT, S_IRUSR | S_IWUSR);
591 if (fd == -1) {
592 print("failed to open mbox for writing\n");
593 return;
595 for (i = 0; i < nsel; i++) {
596 struct mail *mail = sort->mails[sel[i]];
597 mail_write(mail, fd);
598 if (del)
599 mail->stat |= STAT_DEL;
601 close(fd);
604 static void cmd_copy(char *pre, char *arg)
606 copy_mail(pre, arg, 0);
609 static void cmd_move(char *pre, char *arg)
611 copy_mail(pre, arg, 1);
614 static void compose(struct draft *draft)
616 char record[MAXPATHLEN] = "";
617 char line[MAXLINE];
618 char cmd[MAXLINE];
619 char *pg_args[] = {PAGER, NULL};
620 if (RECORD)
621 mbox_path(record, RECORD);
622 else if (mbox)
623 strcpy(record, mbox->path);
624 while (read_line(line, sizeof(line)) > 0) {
625 cut_word(cmd, line);
626 if (!strcmp("~e", cmd))
627 draft_edit(draft, EDITOR);
628 if (!strcmp("~v", cmd))
629 draft_edit(draft, VISUAL);
630 if (!strcmp("~.", cmd)) {
631 if (*record)
632 draft_save(draft, record);
633 draft_send(draft);
634 break;
636 if (!strcmp("~p", cmd))
637 exec_pipe(PAGER, pg_args, draft->mail, draft->len);
638 if (!strcmp("~q", cmd) || !strcmp("~x", cmd))
639 break;
643 static void cmd_mail(char *pre, char *arg)
645 struct draft draft;
646 draft_init(&draft, *arg ? &arg : NULL, *arg ? 1 : 0, NULL);
647 compose(&draft);
650 static void cmd_reply(char *pre, char *arg)
652 struct draft draft;
653 if (!sel_msgs(pre, 0))
654 return;
655 draft_reply(&draft, mimes_get(sort->mails[cur]));
656 compose(&draft);
659 static void prompt(void)
661 write(1, "? ", 2);
664 static void cmd_quit(char *pre, char *arg)
666 mbox_old(mbox);
667 if (mbox_write(mbox) == -1)
668 warn_nomem();
669 else
670 quit = 1;
673 static void cmd_exit(char *pre, char *arg)
675 quit = 1;
678 static void cmd_clear(char *pre, char *arg)
680 print("\x1b[H\x1b[J");
683 static struct cmds {
684 char *name;
685 void (*cmd)(char *pre, char *arg);
686 } cmds[] = {
687 {"page", cmd_page},
688 {"next", cmd_next},
689 {"header", cmd_head},
690 {"ls", cmd_head},
691 {"file", cmd_fold},
692 {"folder", cmd_fold},
693 {"inc", cmd_inc},
694 {"z", cmd_z},
695 {"mail", cmd_mail},
696 {"copy", cmd_copy},
697 {"cp", cmd_copy},
698 {"move", cmd_move},
699 {"mv", cmd_move},
700 {"reply", cmd_reply},
701 {"cat", cmd_cat},
702 {"delete", cmd_del},
703 {"rm", cmd_del},
704 {"cd", cmd_fold},
705 {"undelete", cmd_undel},
706 {"quit", cmd_quit},
707 {"exit", cmd_exit},
708 {"xit", cmd_exit},
709 {"news", cmd_news},
710 {"mime", cmd_mime},
711 {"clear", cmd_clear},
714 static void cmd_parse(char *line, char *pre, char *cmd, char *arg)
716 while (*line && isspace(*line))
717 line++;
718 while (*line && !isalpha(*line)) {
719 if ((line[0] == '/' || line[0] == '?') &&
720 (line[1] == '(' || line[2] == '('))
721 while (*line && *line != ')')
722 *pre++ = *line++;
723 *pre++ = *line++;
725 *pre = '\0';
726 while (*line && isspace(*line))
727 line++;
728 while (*line && isalpha(*line))
729 *cmd++ = *line++;
730 *cmd = '\0';
731 while (*line && isspace(*line))
732 line++;
733 while (*line && !isspace(*line))
734 *arg++ = *line++;
735 *arg = '\0';
738 static void loop(void)
740 char line[MAXLINE];
741 char pre[MAXLINE];
742 char cmd[MAXLINE];
743 char arg[MAXLINE];
744 int len;
745 prompt();
746 while (read_line(line, sizeof(line)) > 0) {
747 cmd_parse(line, pre, cmd, arg);
748 len = strlen(cmd);
749 int i;
750 if (!len)
751 cmd_next(pre, arg);
752 else
753 for (i = 0; i < ARRAY_SIZE(cmds); i++)
754 if (!strncmp(cmds[i].name, cmd, len)) {
755 cmds[i].cmd(pre, arg);
756 break;
758 if (quit)
759 break;
760 else
761 prompt();
765 int main(int argc, char *argv[])
767 int i = 0;
768 char *filename = NULL;
769 char *subj = NULL;
770 while (++i < argc) {
771 if (argv[i][0] != '-')
772 break;
773 if (!strcmp("-f", argv[i]))
774 filename = argv[++i];
775 if (!strcmp("-s", argv[i]))
776 subj = argv[++i];
778 if (filename) {
779 char path[MAXPATHLEN];
780 if (mbox_path(path, filename) && is_mbox(path)) {
781 open_mbox(path);
782 loop();
783 close_mbox();
785 } else {
786 struct draft draft;
787 draft_init(&draft, argv + i, argc - i, subj);
788 compose(&draft);
790 return 0;