mblaze

Unix utilities to deal with Maildir - my mirror
git clone https://pi.duncano.de/git/mblaze.git
Log | Files | Refs | README | COPYING

commit f86bb4d88ff1b7e1dd10eb51ac155948106902d5
parent dc43475d7641ad4d410279a9d80ec2b5a838c8ce
Author: Duncaen <mail@duncano.de>
Date:   Fri, 22 Jul 2016 03:10:17 +0200

add mpick

Diffstat:
Makefile | 3++-
mpick.c | 865+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 867 insertions(+), 1 deletion(-)

diff --git a/Makefile b/Makefile @@ -1,6 +1,6 @@ CFLAGS=-g -O1 -Wall -Wno-switch -Wextra -fstack-protector-strong -D_FORTIFY_SOURCE=2 -ALL = maddr mdeliver mdirs mflag mhdr minc mlist mmime mscan mseq mshow msort mthread +ALL = maddr mdeliver mdirs mflag mhdr minc mlist mmime mpick mscan mseq mshow msort mthread all: $(ALL) @@ -12,6 +12,7 @@ mhdr: mhdr.o blaze822.o seq.o rfc2047.o mymemmem.o minc: minc.o mlist: mlist.o mmime: mmime.o +mpick: mpick.o blaze822.o seq.o rfc2047.c mymemmem.o mscan: mscan.o blaze822.o seq.o rfc2047.o mymemmem.o mseq: mseq.o seq.o mshow: mshow.o blaze822.o seq.o rfc2045.o rfc2047.c mymemmem.o diff --git a/mpick.c b/mpick.c @@ -0,0 +1,865 @@ +#define _GNU_SOURCE + +#include <sys/ioctl.h> +#include <sys/stat.h> +#include <sys/types.h> + +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <stdarg.h> +#include <string.h> +#include <time.h> +#include <unistd.h> +#include <wchar.h> +#include <locale.h> +#include <regex.h> +#include <fnmatch.h> + +#include "blaze822.h" + +enum op { + EXPR_OR = 1, + EXPR_AND, + EXPR_NOT, + EXPR_LT, + EXPR_LE, + EXPR_EQ, + EXPR_NEQ, + EXPR_GE, + EXPR_GT, + EXPR_STREQ, + EXPR_STREQI, + EXPR_GLOB, + EXPR_GLOBI, + EXPR_REGEX, + EXPR_REGEXI, + EXPR_PRINT, + EXPR_TYPE, + EXPR_ALLSET, + EXPR_ANYSET, +}; + +enum prop { + PROP_ATIME = 1, + PROP_CTIME, + PROP_DEPTH, + PROP_MTIME, + PROP_PATH, + PROP_SIZE, + PROP_TOTAL, + PROP_SUBJECT, + PROP_FROM, + PROP_FROM_NAME, + PROP_FROM_ADDR, + PROP_TO, + PROP_TO_NAME, + PROP_TO_ADDR, + PROP_INDEX, + PROP_DATE, + PROP_FLAG, +}; + +enum flags { + FLAG_PASSED = 1, + FLAG_REPLIED = 2, + FLAG_SEEN = 4, + FLAG_TRASHED = 8, + FLAG_DRAFT = 16, + FLAG_FLAGGED = 32, + /* custom */ + FLAG_NEW = 64, + FLAG_CUR = 128, +}; + +struct expr { + enum op op; + union { + enum prop prop; + struct expr *expr; + char *string; + int64_t num; + regex_t *regex; + } a, b; +}; + +struct mailinfo { + char *fpath; + struct stat *sb; + struct message *msg; + time_t date; + int depth; + int index; + long flags; + off_t total; + char subject[100]; +}; + +static char *argv0; + +static long kept; + +static struct expr *expr; +static char *cur; +static char *pos; +static time_t now; + +static void +ws() +{ + while (isspace((unsigned char) *pos)) + pos++; +} + +static int +token(const char *token) +{ + if (strncmp(pos, token, strlen(token)) == 0) { + pos += strlen(token); + ws(); + return 1; + } else { + return 0; + } +} + +static void +parse_error(const char *msg, ...) +{ + va_list ap; + va_start(ap, msg); + fprintf(stderr, "%s: parse error: ", argv0); + vfprintf(stderr, msg, ap); + fprintf(stderr, "\n"); + exit(2); +} + +static struct expr * +mkexpr(enum op op) +{ + struct expr *e = malloc(sizeof (struct expr)); + if (!e) + parse_error("out of memory"); + e->op = op; + return e; +} + +static struct expr * +chain(struct expr *e1, enum op op, struct expr *e2) +{ + struct expr *i, *j, *e; + if (!e1) + return e2; + if (!e2) + return e1; + for (j = 0, i = e1; i->op == op; j = i, i = i->b.expr) + ; + if (!j) { + e = mkexpr(op); + e->a.expr = e1; + e->b.expr = e2; + return e; + } else { + e = mkexpr(op); + e->a.expr = i; + e->b.expr = e2; + j->b.expr = e; + return e1; + } +} + +static enum op +parse_op() +{ + if (token("<=")) + return EXPR_LE; + else if (token("<")) + return EXPR_LT; + else if (token(">=")) + return EXPR_GE; + else if (token(">")) + return EXPR_GT; + else if (token("==") || token("=")) + return EXPR_EQ; + else if (token("!=")) + return EXPR_NEQ; + + return 0; +} + +static struct expr *parse_cmp(); +static struct expr *parse_or(); + +static struct expr * +parse_inner() +{ + if (token("print")) { + struct expr *e = mkexpr(EXPR_PRINT); + return e; + } else if (token("!")) { + struct expr *e = parse_cmp(); + struct expr *not = mkexpr(EXPR_NOT); + not->a.expr = e; + return not; + } else if (token("(")) { + struct expr *e = parse_or(); + if (token(")")) + return e; + parse_error("missing ) at '%.15s'", pos); + return 0; + } else { + parse_error("unknown expression at '%.15s'", pos); + return 0; + } +} + +static int +parse_string(char **s) +{ + char *buf = 0; + size_t bufsiz = 0; + size_t len = 0; + + if (*pos == '"') { + pos++; + while (*pos && + (*pos != '"' || (*pos == '"' && *(pos+1) == '"'))) { + if (len >= bufsiz) { + bufsiz = 2*bufsiz + 16; + buf = realloc(buf, bufsiz); + if (!buf) + parse_error("string too long"); + } + if (*pos == '"') + pos++; + buf[len++] = *pos++; + } + if (!*pos) + parse_error("unterminated string"); + if (buf) + buf[len] = 0; + pos++; + ws(); + *s = buf ? buf : (char *) ""; + return 1; + } else if (*pos == '$') { + char t; + char *e = ++pos; + + while (isalnum((unsigned char) *pos) || *pos == '_') + pos++; + if (e == pos) + parse_error("invalid environment variable name"); + + t = *pos; + *pos = 0; + *s = getenv(e); + if (!*s) + *s = (char *) ""; + *pos = t; + ws(); + return 1; + } + + return 0; +} + +static struct expr * +parse_strcmp() +{ + enum prop prop; + enum op op; + + if (token("subject")) + prop = PROP_SUBJECT; + else if (token("from")) + prop = PROP_FROM; + else if (token("to")) + prop = PROP_TO; + else + return parse_inner(); + + if (token("~~~")) + op = EXPR_GLOBI; + else if (token("~~")) + op = EXPR_GLOB; + else if (token("=~~")) + op = EXPR_REGEXI; + else if (token("=~")) + op = EXPR_REGEX; + else if (token("===")) + op = EXPR_STREQI; + else if (token("==")) + op = EXPR_STREQ; + else if (token("=")) + op = EXPR_STREQ; + else + parse_error("invalid string operator at '%.15s'", pos); + + char *s; + if (parse_string(&s)) { + int r = 0; + struct expr *e = mkexpr(op); + e->a.prop = prop; + if (op == EXPR_REGEX) { + e->b.regex = malloc(sizeof (regex_t)); + r = regcomp(e->b.regex, s, REG_EXTENDED | REG_NOSUB); + } else if (op == EXPR_REGEXI) { + e->b.regex = malloc(sizeof (regex_t)); + r = regcomp(e->b.regex, s, REG_EXTENDED | REG_NOSUB | REG_ICASE); + } else { + e->b.string = s; + } + + if (r != 0) { + char msg[256]; + regerror(r, e->b.regex, msg, sizeof msg); + parse_error("invalid regex '%s': %s", s, msg); + exit(2); + } + + return e; + } + + parse_error("invalid string at '%.15s'", pos); + return 0; +} + +static int64_t +parse_num(int64_t *r) +{ + char *s = pos; + if (isdigit((unsigned char) *pos)) { + int64_t n; + + for (n = 0; isdigit((unsigned char) *pos) && n <= INT64_MAX / 10 - 10; pos++) + n = 10 * n + (*pos - '0'); + if (isdigit((unsigned char) *pos)) + parse_error("number too big: %s", s); + if (token("c")) ; + else if (token("b")) n *= 512LL; + else if (token("k")) n *= 1024LL; + else if (token("M")) n *= 1024LL * 1024; + else if (token("G")) n *= 1024LL * 1024 * 1024; + else if (token("T")) n *= 1024LL * 1024 * 1024 * 1024; + ws(); + *r = n; + return 1; + } else { + return 0; + } +} + +static struct expr * +parse_flag() +{ + enum flags flag; + + if (token("passed")) { + flag = FLAG_PASSED; + } else if (token("replied")) { + flag = FLAG_REPLIED; + } else if (token("seen")) { + flag = FLAG_SEEN; + } else if (token("trashed")) { + flag = FLAG_TRASHED; + } else if (token("draft")) { + flag = FLAG_DRAFT; + } else if (token("flagged")) { + flag = FLAG_FLAGGED; + } else if (token("new")) { + flag = FLAG_NEW; + } else if (token("cur")) { + flag = FLAG_CUR; + } else + return parse_strcmp(); + + struct expr *e = mkexpr(EXPR_ANYSET); + e->a.prop = PROP_FLAG; + e->b.num = flag; + + return e; +} + + +static struct expr * +parse_cmp() +{ + enum prop prop; + enum op op; + + op = 0; + + if (token("depth")) + prop = PROP_DEPTH; + else if (token("index")) + prop = PROP_INDEX; + else if (token("size")) + prop = PROP_SIZE; + else if (token("total")) + prop = PROP_TOTAL; + else + return parse_flag(); + + if (!(op = parse_op())) + parse_error("invalid comparison at '%.15s'", pos); + + int64_t n; + if (parse_num(&n)) { + struct expr *e = mkexpr(op); + e->a.prop = prop; + e->b.num = n; + return e; + } + + return 0; +} + +static int +parse_dur(int64_t *n) +{ + char *s, *r; + if (!parse_string(&s)) + return 0; + + if (*s == '/' || *s == '.') { + struct stat st; + if (stat(s, &st) < 0) + parse_error("can't stat file '%s': %s", + s, strerror(errno)); + *n = st.st_mtime; + return 1; + } + + struct tm tm = { 0 }; + r = strptime(s, "%Y-%m-%d %H:%M:%S", &tm); + if (r && !*r) { + *n = mktime(&tm); + return 1; + } + r = strptime(s, "%Y-%m-%d", &tm); + if (r && !*r) { + tm.tm_hour = tm.tm_min = tm.tm_sec = 0; + *n = mktime(&tm); + return 1; + } + r = strptime(s, "%H:%M:%S", &tm); + if (r && !*r) { + struct tm *tmnow = localtime(&now); + tm.tm_year = tmnow->tm_year; + tm.tm_mon = tmnow->tm_mon; + tm.tm_mday = tmnow->tm_mday; + *n = mktime(&tm); + return 1; + } + r = strptime(s, "%H:%M", &tm); + if (r && !*r) { + struct tm *tmnow = localtime(&now); + tm.tm_year = tmnow->tm_year; + tm.tm_mon = tmnow->tm_mon; + tm.tm_mday = tmnow->tm_mday; + tm.tm_sec = 0; + *n = mktime(&tm); + return 1; + } + + if (*s == '-') { + s++; + + errno = 0; + int64_t d; + d = strtol(s, &r, 10); + if (errno == 0 && r[0] == 'd' && !r[1]) { + struct tm *tmnow = localtime(&now); + tmnow->tm_mday -= d; + tmnow->tm_hour = tmnow->tm_min = tmnow->tm_sec = 0; + *n = mktime(tmnow); + return 1; + } + if (errno == 0 && r[0] == 'h' && !r[1]) { + *n = now - (d*60*60); + return 1; + } + if (errno == 0 && r[0] == 'm' && !r[1]) { + *n = now - (d*60); + return 1; + } + if (errno == 0 && r[0] == 's' && !r[1]) { + *n = now - d; + return 1; + } + parse_error("invalid relative time format '%s'", s-1); + } + + parse_error("invalid time format '%s'", s); + return 0; +} + +static struct expr * +parse_timecmp() +{ + enum prop prop; + enum op op; + + if (token("atime")) + prop = PROP_ATIME; + else if (token("ctime")) + prop = PROP_CTIME; + else if (token("mtime")) + prop = PROP_MTIME; + else if (token("date")) + prop = PROP_DATE; + else + return parse_cmp(); + + op = parse_op(); + if (!op) + parse_error("invalid comparison at '%.15s'", pos); + + int64_t n; + if (parse_num(&n) || parse_dur(&n)) { + struct expr *e = mkexpr(op); + e->a.prop = prop; + e->b.num = n; + return e; + } + + return 0; +} + +static struct expr * +parse_and() +{ + struct expr *e1 = parse_timecmp(); + struct expr *r = e1; + + while (token("&&")) { + struct expr *e2 = parse_timecmp(); + r = chain(r, EXPR_AND, e2); + } + + return r; +} + +static struct expr * +parse_or() +{ + struct expr *e1 = parse_and(); + struct expr *r = e1; + + while (token("||")) { + struct expr *e2 = parse_and(); + r = chain(r, EXPR_OR, e2); + } + + return r; +} + +static struct expr * +parse_expr(const char *s) +{ + pos = (char *)s; + struct expr *e = parse_or(); + if (*pos) + parse_error("trailing garbage at '%.15s'", pos); + return e; +} + +static struct expr * +parse_msglist(const char *s) +{ + int64_t n, m; + int r; + struct expr *e1, *e2; + char *d; + + switch (*s) { + case '/': + s++; + e1 = mkexpr(EXPR_REGEXI); + e1->a.prop = PROP_SUBJECT; + e1->b.regex = malloc(sizeof (regex_t)); + r = regcomp(e1->b.regex, s, REG_EXTENDED | REG_NOSUB | REG_ICASE); + if (r != 0) { + char msg[256]; + regerror(r, e1->b.regex, msg, sizeof msg); + parse_error("invalid regex '%s': %s", s, msg); + } + return e1; + case ':': + if (strlen(s) <= 1) + parse_error("missing type at '%.15s'", s); + + enum flags flag; + n = 0; + + switch (*++s) { + case 'P': flag = FLAG_PASSED; break; + case 'F': flag = FLAG_FLAGGED; break; + case 'D': flag = FLAG_DRAFT; break; + case 'd': /* FALL TROUGH */ + case 'T': flag = FLAG_TRASHED; break; + case 'u': n = 1; /* FALL TROUGH */ + case 'r': /* FALL TROUGH */ + case 'S': flag = FLAG_SEEN; break; + case 'o': n = 1; /* FALL TROUGH */ + case 'n': flag = FLAG_NEW; break; + default: parse_error("unknown type at '%.15s'", s); + } + + e1 = mkexpr(EXPR_ANYSET); + e1->a.prop = PROP_FLAG; + e1->b.num = flag; + + if (!n) + return e1; + + e2 = mkexpr(EXPR_NOT); + e2->a.expr = e1; + return e2; + default: + pos = (char *)s; + + if ((d = strchr(s, '-')) && parse_num(&n) && + (pos = (char *)d + 1) && parse_num(&m)) { + /* index >= n */ + e1 = mkexpr(EXPR_GE); + e1->a.prop = PROP_INDEX; + e1->b.num = n; + + /* index <= m */ + e2 = mkexpr(EXPR_LE); + e2->a.prop = PROP_INDEX; + e2->b.num = m; + + /* e1 && e2 */ + return chain(e1, EXPR_AND, e2); + } else if (parse_num(&n)) { + e1 = mkexpr(EXPR_EQ); + e1->a.prop = PROP_INDEX; + e1->b.num = n; + + return e1; + } else { + expr = chain(parse_expr("from.addr == 's'"), EXPR_AND, expr); + } + } + return NULL; +} + +time_t +msg_date(struct mailinfo *m) +{ + if (m->date) + return m->date; + + char *b; + if ((b = blaze822_hdr(m->msg, "date"))) + return (m->date = blaze822_date(b)); + + return -1; +} + +char * +msg_subject(struct mailinfo *m) +{ + if (*m->subject != '\0') + return m->subject; + + char *b; + if ((b = blaze822_hdr(m->msg, "subject")) == '\0') + return ""; + + blaze822_decode_rfc2047(m->subject, b, sizeof m->subject - 1, "UTF-8"); + return m->subject; +} + +int +eval(struct expr *e, struct mailinfo *m) +{ + switch (e->op) { + case EXPR_OR: + return eval(e->a.expr, m) || eval(e->b.expr, m); + case EXPR_AND: + return eval(e->a.expr, m) && eval(e->b.expr, m); + case EXPR_NOT: + return !eval(e->a.expr, m); + return 1; + case EXPR_PRINT: + return 1; + case EXPR_LT: + case EXPR_LE: + case EXPR_EQ: + case EXPR_NEQ: + case EXPR_GE: + case EXPR_GT: + case EXPR_ALLSET: + case EXPR_ANYSET: { + long v = 0; + + if (m->sb == '\0' && ( + e->a.prop == PROP_ATIME || + e->a.prop == PROP_CTIME || + e->a.prop == PROP_MTIME || + e->a.prop == PROP_SIZE) && + (m->sb = calloc(1, sizeof *m->sb)) != NULL && + stat(m->fpath, m->sb) != 0) { + fprintf(stderr, "stat"); + exit(2); + } + + switch (e->a.prop) { + case PROP_ATIME: v = m->sb->st_atime; break; + case PROP_CTIME: v = m->sb->st_ctime; break; + case PROP_MTIME: v = m->sb->st_mtime; break; + case PROP_SIZE: v = m->sb->st_size; break; + case PROP_DATE: v = msg_date(m); + case PROP_FLAG: v = m->flags; break; + case PROP_INDEX: v = m->index; break; + case PROP_DEPTH: v = m->depth; break; + default: + parse_error("unknown property"); + } + + switch (e->op) { + case EXPR_LT: return v < e->b.num; + case EXPR_LE: return v <= e->b.num; + case EXPR_EQ: return v == e->b.num; + case EXPR_NEQ: return v != e->b.num; + case EXPR_GE: return v >= e->b.num; + case EXPR_GT: return v > e->b.num; + case EXPR_ALLSET: return (v & e->b.num) == e->b.num; + case EXPR_ANYSET: return (v & e->b.num) > 0; + } + } + case EXPR_STREQ: + case EXPR_STREQI: + case EXPR_GLOB: + case EXPR_GLOBI: + case EXPR_REGEX: + case EXPR_REGEXI: { + const char *s = ""; + switch(e->a.prop) { + case PROP_PATH: s = m->fpath; break; + case PROP_SUBJECT: s = msg_subject(m); break; + } + switch (e->op) { + case EXPR_STREQ: return strcmp(e->b.string, s) == 0; + case EXPR_STREQI: return strcasecmp(e->b.string, s) == 0; + case EXPR_GLOB: return fnmatch(e->b.string, s, 0) == 0; + case EXPR_GLOBI: return fnmatch(e->b.string, s, FNM_CASEFOLD) == 0; + case EXPR_REGEX: + case EXPR_REGEXI: return regexec(e->b.regex, s, 0, 0, 0) == 0; + } + } + } + return 0; +} + +void +oneline(char *line, long idx) +{ + static int init; + if (!init) { + // delay loading of the seqmap until we need to scan the first + // file, in case someone in the pipe updated the map before + char *seqmap = blaze822_seq_open(0); + blaze822_seq_load(seqmap); + cur = blaze822_seq_cur(); + init = 1; + } + + struct mailinfo m; + + memset(m.subject, 0, sizeof m.subject); + m.fpath = line; + m.index = idx; + m.flags = 0; + m.depth = 0; + m.sb = 0; + + while (*m.fpath == ' ' || *m.fpath== '\t') { + m.depth++; + m.fpath++; + } + + char *e = m.fpath + strlen(m.fpath) - 1; + while (m.fpath < e && (*e == ' ' || *e == '\t')) + *e-- = 0; + + m.msg = blaze822(m.fpath); + if (!m.msg) { + return; + } + + if (strstr(m.fpath, "/new/") != NULL) + m.flags |= FLAG_NEW; + + if (cur && strcmp(cur, m.fpath) == 0) + m.flags |= FLAG_CUR; + + char *f = strstr(m.fpath, ":2,"); + if (f) { + if (strchr(f, 'P')) + m.flags |= FLAG_PASSED; + if (strchr(f, 'R')) + m.flags |= FLAG_REPLIED; + if (strchr(f, 'S')) + m.flags |= FLAG_SEEN; + if (strchr(f, 'T')) + m.flags |= FLAG_TRASHED; + if (strchr(f, 'D')) + m.flags |= FLAG_DRAFT; + if (strchr(f, 'F')) + m.flags |= FLAG_FLAGGED; + } + + if (expr && !eval(expr, &m)) + goto out; + + kept++; + printf("%s\n", line); + +out: + free(m.sb); + blaze822_free(m.msg); +} + +int +main(int argc, char *argv[]) +{ + long i; + int c; + char *f, *a; + + argv0 = argv[0]; + + while ((c = getopt(argc, argv, "t:")) != -1) + switch (c) { + case 't': expr = chain(expr, EXPR_AND, parse_expr(optarg)); break; + } + + if (optind != argc) + for (c = optind; c < argc; c++) + expr = chain(expr, EXPR_AND, parse_msglist(argv[c])); + + struct blaze822_seq_iter iter = { 0 }; + + char *map = blaze822_seq_open(0); + if (!map) + return 1; + + a = ":"; + i = 0; + + while ((f = blaze822_seq_next(map, a, &iter))) { + i = iter.line - 1; + oneline(f, i); + free(f); + } + + fprintf(stderr, "%ld mails tested, %ld picked.\n", i, kept); + return 0; +}