diff --git a/GNUmakefile b/GNUmakefile index 5f1d6a6..9430a18 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -17,18 +17,18 @@ PREFIX=/usr/local BINDIR=$(PREFIX)/bin MANDIR=$(PREFIX)/share/man -ALL = maddr magrep mdate mdeliver mdirs mexport mflag mgenmid mhdr minc mlist mmime mpick mscan msed mseq mshow msort mthread +ALL = maddr magrep mdate mdeliver mdirs mexport mflag mflow mgenmid mhdr minc mlist mmime mpick mscan msed mseq mshow msort mthread SCRIPT = mcolor mcom mless mmkdir mquote museragent all: $(ALL) museragent $(ALL) : % : %.o -maddr magrep mdeliver mexport mflag mgenmid mhdr mpick mscan msed mshow \ +maddr magrep mdeliver mexport mflag mflow mgenmid mhdr mpick mscan msed mshow \ msort mthread : blaze822.o mymemmem.o mytimegm.o maddr magrep mexport mflag mgenmid mhdr mlist mpick mscan msed mseq mshow msort \ mthread : seq.o slurp.o -maddr magrep mhdr mpick mscan mshow : rfc2047.o -magrep mshow : rfc2045.o +maddr magrep mflow mhdr mpick mscan mshow : rfc2047.o +magrep mflow mshow : rfc2045.o mshow : filter.o safe_u8putstr.o rfc2231.o pipeto.o mscan : pipeto.o msort : mystrverscmp.o diff --git a/NEWS.md b/NEWS.md new file mode 100644 index 0000000..cec68bf --- /dev/null +++ b/NEWS.md @@ -0,0 +1,17 @@ +## 0.2 (2017-07-17) + +* New sequence syntax `m:+n` for `n` messages after message `m`. +* Threading shortcuts `=`, `_`, `^` for `.=`, `._`, `.^`. +* Sequence related errors are now reported. +* minc and mlist normalize slashes in paths. +* mfwd now generates conforming message/rfc822 parts. +* mthread can add optional folders (e.g. your outbox) to resolve message ids. +* mcom now adds Date: just before sending or cancelling the mail. +* VIOLATIONS.md documents how mblaze works with certain common mistakes. +* Full documentation revamp by Larry Hynes. +* Fix rare crash looking for mail body. +* Numerous small bug and portability fixes. + +## 0.1 (2017-06-24) + +* Initial release diff --git a/README b/README index b74dc45..d228edb 100644 --- a/README +++ b/README @@ -18,6 +18,7 @@ DESCRIPTION mdirs(1) find Maildir folders mexport(1) export Maildir folders as mailboxes mflag(1) change flags (marks) of mail + mflow(1) reflow format=flowed plain text mails mfwd(1) forward mail mgenmid(1) generate Message-IDs mhdr(1) extract mail headers diff --git a/VERSION b/VERSION index 49d5957..3b04cfb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1 +0.2 diff --git a/contrib/mverify b/contrib/mverify index 2bca940..0edf234 100755 --- a/contrib/mverify +++ b/contrib/mverify @@ -8,9 +8,9 @@ mshow -t "$1" | awk -v "msg=$1" ' { match($0, "^ *"); indent = RLENGTH } $2 == "text/plain" { plain++ } -$2 == "multipart/signed" { signed = +$1; si = indent; next } -signed && !content && indent == si+2 { content = +$1; next } -signed && content && !signature && indent == si+2 { signature = +$1; type = $2 } +$2 == "multipart/signed" { signed = 0+$1; si = indent; next } +signed && !content && indent == si+2 { content = 0+$1; next } +signed && content && !signature && indent == si+2 { signature = 0+$1; type = $2 } function q(a) { gsub("\\47", "\47\\\47\47", a); return "\47"a"\47" } END { if (type == "" && plain) { // guess plain text armored signature @@ -26,7 +26,7 @@ END { exit(system("mshow -r -O " q(msg) " " q(signed) \ " | openssl smime -verify")) } else { - print("Cant verify signatures of type " type ".") + print("Cannot verify signatures of type " type ".") exit(2) } } diff --git a/filter.c b/filter.c index 111b606..7b86106 100644 --- a/filter.c +++ b/filter.c @@ -1,9 +1,13 @@ +#include + +#include +#include +#include #include #include #include #include #include -#include #include #include @@ -32,6 +36,10 @@ filter(char *input, size_t inlen, char *cmd, char **outputo, size_t *outleno) if (pipe(pipe0) != 0 || pipe(pipe1) != 0) goto fail; + int got = fcntl(pipe0[1], F_GETFL); + if (got > 0) + fcntl(pipe0[1], F_SETFL, got | O_NONBLOCK); + char *argv[] = { "/bin/sh", "-c", cmd, (char *)0 }; if (!(pid = fork())) { @@ -88,7 +96,9 @@ filter(char *input, size_t inlen, char *cmd, char **outputo, size_t *outleno) input += ret; inlen -= ret; } - if (ret <= 0 || inlen == 0) + if (ret <= 0 && errno == EAGAIN) { + /* ignore */ + } else if (ret <= 0 || inlen == 0) close(fds[1].fd); } else if (fds[1].revents & (POLLERR | POLLHUP | POLLNVAL)) { fds[1].fd = -1; diff --git a/filter.example b/filter.example index c1804ac..1cdd524 100644 --- a/filter.example +++ b/filter.example @@ -1,3 +1,4 @@ +text/plain: mflow text/html: lynx -dump -stdin -nomargins ${PIPE_CHARSET:+-assume_charset $PIPE_CHARSET} application/pdf: pdftotext - - | par application: file -b - diff --git a/man/mblaze.7 b/man/mblaze.7 index 13b37d0..d843968 100644 --- a/man/mblaze.7 +++ b/man/mblaze.7 @@ -30,6 +30,8 @@ find Maildir folders export Maildir folders as mailboxes .It Xr mflag 1 change flags (marks) of mail +.It Xr mflow 1 +reflow format=flowed plain text mails .It Xr mfwd 1 forward mail .It Xr mgenmid 1 diff --git a/man/mflow.1 b/man/mflow.1 new file mode 100644 index 0000000..63a2a23 --- /dev/null +++ b/man/mflow.1 @@ -0,0 +1,55 @@ +.Dd July 26, 2017 +.Dt MFLOW 1 +.Os +.Sh NAME +.Nm mflow +.Nd reflow format=flowed plain text mails +.Sh SYNOPSIS +.Nm +\&< +.Ar file +.Sh DESCRIPTION +.Nm +reformats the standard input according to the rules +of RFC 3676. +.Ev PIPE_CONTENTTYPE +is inspected, making this a suitable filter +for +.Sq text/plain +messages for +.Xr mshow 1 . +Mails not using +.Sq format=flowed +are output as is. +.Pp +Text is reflowed (where allowed) to +fit the width given in the environment variable +.Ev COLUMNS , +the terminal width, or 80 characters by default. +.Pp +If defined, +the environment variable +.Ev MAXCOLUMNS +specifies the maximum line length. +.Sh EXIT STATUS +.Ex -std +.Sh SEE ALSO +.Xr mshow 1 +.Rs +.%A R. Gellens +.%D February 2004 +.%R RFC 3676 +.%T The Text/Plain Format and DelSp Parameters +.Re +.Sh AUTHORS +.An Leah Neukirchen Aq Mt leah@vuxu.org +.Sh LICENSE +.Nm +is in the public domain. +.Pp +To the extent possible under law, +the creator of this work +has waived all copyright and related or +neighboring rights to this work. +.Pp +.Lk http://creativecommons.org/publicdomain/zero/1.0/ diff --git a/mcom b/mcom index 15bba03..6fe2b84 100755 --- a/mcom +++ b/mcom @@ -51,7 +51,7 @@ case "$0" in shift resume=1 if [ "$#" -gt 0 ]; then - echo "used dreaft $1" + echo "used draft $1" draft="$1" shift fi diff --git a/mflow.c b/mflow.c new file mode 100644 index 0000000..56af515 --- /dev/null +++ b/mflow.c @@ -0,0 +1,187 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "blaze822.h" + +int column = 0; +int maxcolumn = 80; + +void +chgquote(int quotes) +{ + static int oquotes; + + if (quotes != oquotes) { + if (column) + putchar('\n'); + column = 0; + oquotes = quotes; + } +} + +void +fixed(int quotes, char *line, size_t linelen) +{ + chgquote(quotes); + + if (linelen > (size_t)(maxcolumn - column)) { + putchar('\n'); + column = 0; + } + + if (column == 0) { + for (; column < quotes; column++) + putchar('>'); + if (quotes) + putchar(' '); + } + + fwrite(line, 1, linelen, stdout); + putchar('\n'); + column = 0; +} + +void +flowed(int quotes, char *line, ssize_t linelen) +{ + chgquote(quotes); + int done = 0; + + while (!done) { + if (column == 0) { + for (; column < quotes; column++) + putchar('>'); + column++; + if (quotes) + putchar(' '); + } + + char *eow; + if (*line == ' ') + eow = memchr(line + 1, ' ', linelen - 1); + else + eow = memchr(line, ' ', linelen); + + if (!eow) { + eow = line + linelen; + done = 1; + } + + if (column + (eow - line) > maxcolumn) { + putchar('\n'); + column = 0; + if (*line == ' ') { + line++; + linelen--; + } + } else { + fwrite(line, 1, eow - line, stdout); + column += eow - line; + linelen -= eow - line; + line = eow; + } + } +} + +int +main() +{ + char *linebuf = 0; + char *line; + size_t linelen = 0; + int quotes = 0; + + int reflow = 1; // re-evaluated on $PIPE_CONTENTTYPE + int delsp = 0; + + char *ct = getenv("PIPE_CONTENTTYPE"); + if (ct) { + char *s, *se; + blaze822_mime_parameter(ct, "format", &s, &se); + reflow = s && (strncasecmp(s, "flowed", 6) == 0); + blaze822_mime_parameter(ct, "delsp", &s, &se); + delsp = s && (strncasecmp(s, "yes", 3) == 0); + } + + char *cols = getenv("COLUMNS"); + if (cols && isdigit(*cols)) { + maxcolumn = atoi(cols); + } else { + struct winsize w; + int fd = open("/dev/tty", O_RDONLY | O_NOCTTY); + if (fd >= 0) { + if (ioctl(fd, TIOCGWINSZ, &w) == 0) + maxcolumn = w.ws_col; + close(fd); + } + } + + char *maxcols = getenv("MAXCOLUMNS"); + if (maxcols && isdigit(*maxcols)) { + int m = atoi(maxcols); + if (maxcolumn > m) + maxcolumn = m; + } + + while (1) { + errno = 0; + ssize_t rd = getdelim(&linebuf, &linelen, '\n', stdin); + if (rd == -1) { + if (errno == 0) + break; + fprintf(stderr, "mflow: error reading: %s\n", + strerror(errno)); + exit(1); + } + + line = linebuf; + + if (!reflow) { + fwrite(line, 1, rd, stdout); + continue; + } + + if (rd > 0 && line[rd-1] == '\n') + line[--rd] = 0; + if (rd > 0 && line[rd-1] == '\r') + line[--rd] = 0; + + quotes = 0; + while (*line == '>') { // measure quote depth + line++; + quotes++; + rd--; + } + + if (*line == ' ') { // space stuffing + line++; + rd--; + } + + if (strcmp(line, "-- ") == 0) { // usenet signature convention + if (column) + fixed(quotes, "", 0); // flush paragraph + fixed(quotes, line, rd); + continue; + } + + if (rd > 0 && line[rd-1] == ' ') { // flowed line + if (delsp) + line[--rd] = 0; + flowed(quotes, line, rd); + } else { + fixed(quotes, line, rd); + } + } + + if (reflow && column != 0) + putchar('\n'); +} diff --git a/mseq.c b/mseq.c index 2dca1db..9477d8a 100644 --- a/mseq.c +++ b/mseq.c @@ -252,6 +252,9 @@ stdinmode() void overridecur(char *file) { + static int once = 0; + if (once++) + return; while (*file == ' ') file++; setenv("MAILDOT", file, 1); @@ -260,6 +263,9 @@ overridecur(char *file) void setcur(char *file) { + static int once = 0; + if (once++) + return; while (*file == ' ') file++; unsetenv("MAILDOT"); diff --git a/mshow.c b/mshow.c index c87f6b7..9ee1248 100644 --- a/mshow.c +++ b/mshow.c @@ -29,6 +29,8 @@ static char *hflag = defaulthflags; static char *xflag; static char *Oflag; +static char fallback_ct[] = "text/plain"; + struct message *filters; static int mimecount; @@ -182,7 +184,7 @@ render_mime(int depth, struct message *msg, char *body, size_t bodylen) { char *ct = blaze822_hdr(msg, "content-type"); if (!ct) - ct = "text/x-unknown"; + ct = fallback_ct; char *mt = mimetype(ct); char *tlmt = tlmimetype(ct); char *filename = mime_filename(msg); @@ -310,7 +312,7 @@ choose_alternative(struct message *msg, int depth) m++; char *ict = blaze822_hdr(imsg, "content-type"); if (!ict) - ict = "text/x-unknown"; + ict = fallback_ct; char *imt = mimetype(ict); char *s = strstr(Aflag, imt); @@ -367,7 +369,7 @@ list_mime(int depth, struct message *msg, char *body, size_t bodylen) char *ct = blaze822_hdr(msg, "content-type"); if (!ct) - ct = "text/x-unknown"; + ct = fallback_ct; char *mt = mimetype(ct); char *filename = mime_filename(msg); diff --git a/rfc2047.c b/rfc2047.c index d0bd103..5f5c2f5 100644 --- a/rfc2047.c +++ b/rfc2047.c @@ -75,7 +75,7 @@ blaze822_decode_b64(char *s, char *e, char **deco, size_t *decleno) 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1 }; - char *buf = malloc((e - s) / 4 * 3); + char *buf = malloc((e - s) / 4 * 3 + 1); if (!buf) return 0; @@ -118,6 +118,8 @@ error: if (c3 != '=') *buf++ = d2; } + *buf = 0; + *decleno = buf - *deco; return 1; }