plan9fox/sys/src/cmd/upas/Mail/mesg.c
Ori Bernstein 491fe25158 Mail: correct rendering of nested multipart messages
Reading nested subparts of messages into the root
message array allows deeply nested multipart trees
of messages to show correctly in the message view.
2021-02-07 20:30:04 -08:00

546 lines
9.6 KiB
C

#include <u.h>
#include <libc.h>
#include <bio.h>
#include <thread.h>
#include <regexp.h>
#include "mail.h"
#define Datefmt "?WWW, ?MMM ?DD hh:mm:ss ?Z YYYY"
typedef struct Fn Fn;
struct Fn {
char *name;
void (*fn)(Mesg *, char **, int);
};
void
mesgclear(Mesg *m)
{
int i;
for(i = 0; i < m->nparts; i++)
mesgclear(m->parts[i]);
free(m->name);
free(m->from);
free(m->to);
free(m->cc);
free(m->replyto);
free(m->date);
free(m->subject);
free(m->type);
free(m->disposition);
free(m->messageid);
free(m->filename);
free(m->digest);
free(m->mflags);
free(m->fromcolon);
}
void
mesgfree(Mesg *m)
{
if(m == nil)
return;
mesgclear(m);
free(m);
}
static char*
line(char *data, char **pp, int z)
{
char *p, *q;
for(p=data; *p!='\0' && *p!='\n'; p++)
;
if(*p == '\n')
*pp = p+1;
else
*pp = p;
if(z && p == data)
return nil;
q = emalloc(p-data + 1);
memmove(q, data, p-data);
return q;
}
static char*
fc(Mesg *m, char *s)
{
char *r;
if(s != nil && strlen(m->from) != 0){
r = smprint("%s <%s>", s, m->from);
free(s);
return r;
}
if(m->from != nil)
return estrdup(m->from);
if(s != nil)
return s;
return estrdup("??");
}
Mesg*
mesgload(char *name)
{
char *info, *p;
int ninfo;
Mesg *m;
Tm tm;
m = emalloc(sizeof(Mesg));
m->name = estrjoin(name, "/", nil);
if((info = rslurp(m, "info", &ninfo)) == nil){
free(m->name);
free(m);
return nil;
}
p = info;
m->from = line(p, &p, 0);
m->to = line(p, &p, 0);
m->cc = line(p, &p, 0);
m->replyto = line(p, &p, 1);
m->date = line(p, &p, 0);
m->subject = line(p, &p, 0);
m->type = line(p, &p, 1);
m->disposition = line(p, &p, 1);
m->filename = line(p, &p, 1);
m->digest = line(p, &p, 1);
/* m->bcc = */ free(line(p, &p, 1));
m->inreplyto = line(p, &p, 1);
/* m->date = */ free(line(p, &p, 1));
/* m->sender = */ free(line(p, &p, 1));
m->messageid = line(p, &p, 0);
/* m->lines = */ free(line(p, &p, 1));
/* m->size = */ free(line(p, &p, 1));
m->mflags = line(p, &p, 0);
/* m->fileid = */ free(line(p, &p, 1));
m->fromcolon = fc(m, line(p, &p, 1));
free(info);
m->flags = 0;
if(strchr(m->mflags, 'd')) m->flags |= Fdel;
if(strchr(m->mflags, 's')) m->flags |= Fseen;
if(strchr(m->mflags, 'a')) m->flags |= Fresp;
m->time = time(nil);
if(tmparse(&tm, Datefmt, m->date, nil, nil) != nil)
m->time = tmnorm(&tm);
m->hash = 0;
if(m->messageid != nil)
m->hash = strhash(m->messageid);
return m;
}
static Mesg*
readparts(Mesg *r, Mesg *m)
{
char *dpath, *apath;
int n, i, dfd;
Mesg *a, *sub;
Dir *d;
if(m->body != nil)
return m->body;
dpath = estrjoin(mbox.path, m->name, nil);
dfd = open(dpath, OREAD);
free(dpath);
if(dfd == -1)
return m;
n = dirreadall(dfd, &d);
close(dfd);
if(n == -1)
sysfatal("%s read: %r", mbox.path);
m->body = nil;
for(i = 0; i < n; i++){
if(d[i].qid.type != QTDIR)
continue;
apath = estrjoin(m->name, d[i].name, nil);
a = mesgload(apath);
free(apath);
if(a == nil)
continue;
if(strncmp(a->type, "multipart/", strlen("multipart/")) == 0){
sub = readparts(r, a);
if(sub != a)
m->body = sub;
continue;
}
if(r->nparts >= r->xparts)
r->parts = erealloc(r->parts, (2 + r->nparts*2)*sizeof(Mesg*));
r->parts[r->nparts++] = a;
if(r->body == nil && strcmp(a->type, "text/plain") == 0)
r->body = a;
else if(r->body == nil && strcmp(a->type, "text/html") == 0)
r->body = a;
}
free(d);
if(m->body == nil)
m->body = m;
return m->body;
}
static void
execfmt(void *pm)
{
Mesg *m;
m = pm;
rfork(RFFDG);
dup(m->fd[1], 1);
close(m->fd[0]);
close(m->fd[1]);
procexecl(m->sync, "/bin/htmlfmt", "htmlfmt", "-a", "-cutf-8", m->path, nil);
}
static int
htmlfmt(Mesg *m, char *path)
{
if(pipe(m->fd) == -1)
sysfatal("pipe: %r");
m->sync = chancreate(sizeof(ulong), 0);
m->path = path;
procrfork(execfmt, m, Stack, RFNOTEG);
recvul(m->sync);
chanfree(m->sync);
close(m->fd[1]);
return m->fd[0];
}
static void
copy(Biobuf *wfd, Biobuf *rfd)
{
char *buf;
int n;
buf = emalloc(Bufsz);
while(1){
n = Bread(rfd, buf, Bufsz);
if(n <= 0)
break;
if(Bwrite(wfd, buf, n) != n)
break;
}
free(buf);
}
static int
mesgshow(Mesg *m)
{
char *path, *home, *name, *suff;
Biobuf *rfd, *wfd;
Mesg *a;
int i;
if((wfd = bwinopen(m, "body", OWRITE)) == nil)
return -1;
if(m->parent != nil || m->nchild != 0) {
Bprint(wfd, "Thread:");
if(m->parent && !(m->parent->state & Sdummy))
Bprint(wfd, " ↑ %s", m->parent->name);
for(i = 0; i < m->nchild; i++)
Bprint(wfd, " ↓ %s", m->child[i]->name);
Bprint(wfd, "\n");
}
Bprint(wfd, "From: %s\n", m->fromcolon);
Bprint(wfd, "To: %s\n", m->to);
Bprint(wfd, "Date: %s\n", m->date);
Bprint(wfd, "Subject: %s\n\n", m->subject);
rfd = mesgopenbody(m);
if(rfd != nil){
copy(wfd, rfd);
Bterm(rfd);
}
home = getenv("home");
if(m->nparts != 0)
Bprint(wfd, "\n");
for(i = 0; i < m->nparts; i++){
a = m->parts[i];
name = a->name;
if(strncmp(a->name, m->name, strlen(m->name)) == 0)
name += strlen(m->name);
if(a->disposition != nil
&& strcmp(a->disposition, "inline") == 0
&& strcmp(a->type, "text/plain") == 0){
if(a == m || a == m->body)
continue;
Bprint(wfd, "\n===> %s (%s)\n", name, a->type);
path = estrjoin(mbox.path, a->name, "body", nil);
if((rfd = Bopen(path, OREAD)) != nil){
copy(wfd, rfd);
Bterm(rfd);
}
free(path);
continue;
}
Bprint(wfd, "\n===> %s (%s)\n", name, a->type);
name = a->filename;
if(name == nil)
name = "body";
if((suff = strchr(name, '.')) == nil)
suff = "";
Bprint(wfd, "\tcp %s%sbody%s %s/%s\n", mbox.path, a->name, suff, home, name);
continue;
}
Bterm(wfd);
free(home);
fprint(m->ctl, "clean\n");
return 0;
}
static void
reply(Mesg *m, char **f, int nf)
{
if(nf >= 1 && strcmp(f[0], "all") == 0)
compose(m->replyto, m, 1);
else
compose(m->replyto, m, 0);
}
static void
delmesg(Mesg *m, char **, int nf)
{
if(nf != 0){
fprint(2, "Delmesg: too many args\n");
return;
}
m->flags |= Ftodel;
m->quitting = 1;
mbredraw(m, 0, 0);
}
static void
markone(Mesg *m, char **f, int nf)
{
int add, flg, fd;
char *path;
if(nf != 1){
fprint(2, "Mark: invalid arguments");
return;
}
if((flg = mesgflagparse(f[0], &add)) == -1){
fprint(2, "Mark: invalid flags %s\n", f[0]);
return;
}
if(add)
m->flags |= flg;
else
m->flags &= ~flg;
if(strlen(f[0]) != 0){
path = estrjoin(mbox.path, "/", m->name, "/flags", nil);
if((fd = open(path, OWRITE)) != -1){
fprint(fd, f[0]);
close(fd);
}
free(path);
}
mbredraw(m, 0, 0);
}
static void
mesgquit(Mesg *m, char **, int)
{
if(fprint(m->ctl, "del\n") == -1)
return;
m->quitting = 1;
m->open = 0;
}
static Fn mesgfn[] = {
{"Reply", reply},
{"Delmesg", delmesg},
{"Del", mesgquit},
{"Mark", markone},
#ifdef NOTYET
{"Save", nil},
#endif
{nil}
};
static void
mesgmain(void *mp)
{
char *path, *f[32];
Event ev;
Mesg *m, **pm;
Fn *p;
int nf;
m = mp;
m->quitting = 0;
m->qnext = mbox.openmesg;
mbox.openmesg = m;
path = estrjoin(mbox.path, m->name, nil);
wininit(m, path);
free(path);
wintagwrite(m, "Reply all Delmesg Save ");
mesgshow(m);
fprint(m->ctl, "clean\n");
mbox.nopen++;
while(!m->quitting){
if(winevent(m, &ev) != 'M')
continue;
if(strcmp(ev.text, "Del") == 0)
break;
switch(ev.type){
case 'l':
case 'L':
if(matchmesg(m, ev.text))
mesgopen(ev.text, nil);
else
winreturn(m, &ev);
break;
case 'x':
case 'X':
if((nf = tokenize(ev.text, f, nelem(f))) == 0)
continue;
for(p = mesgfn; p->fn != nil; p++){
if(strcmp(p->name, f[0]) == 0 && p->fn != nil){
p->fn(m, &f[1], nf - 1);
break;
}
}
if(p->fn == nil)
winreturn(m, &ev);
break;
}
}
for(pm = &mbox.openmesg; *pm != nil; pm = &(*pm)->qnext)
if(*pm == m){
*pm = m->qnext;
break;
}
mbox.nopen--;
m->qnext = nil;
m->state &= ~Sopen;
winclose(m);
threadexits(nil);
}
int
mesgflagparse(char *fstr, int *add)
{
int flg;
flg = 0;
*add = (*fstr == '+');
if(*fstr == '-' || *fstr == '+')
fstr++;
for(; *fstr; fstr++){
switch(*fstr){
case 'a':
flg |= Fresp;
break;
case 's':
flg |= Fseen;
break;
case 'D':
flg |= Ftodel;
memcpy(fstr, fstr +1, strlen(fstr));
break;
default:
fprint(2, "unknown flag %c", *fstr);
return -1;
}
}
return flg;
}
void
mesgpath2name(char *buf, int nbuf, char *name)
{
char *p, *e;
int n;
n = strlen(mbox.path);
if(strncmp(name, mbox.path, n) == 0)
e = strecpy(buf, buf+nbuf-2, name + n);
else
e = strecpy(buf, buf+nbuf-2, name);
if((p = strchr(buf, '/')) == nil)
p = e;
p[0] = '/';
p[1] = 0;
}
int
mesgmatch(Mesg *m, char *name, char *digest)
{
if(!(m->state & Sdummy) && strcmp(m->name, name) == 0)
return digest == nil || strcmp(m->digest, digest) == 0;
return 0;
}
Mesg*
mesglookup(char *name, char *digest)
{
char buf[32];
int i;
mesgpath2name(buf, sizeof(buf), name);
for(i = 0; i < mbox.nmesg; i++)
if(mesgmatch(mbox.mesg[i], buf, digest))
return mbox.mesg[i];
return nil;
}
Mesg*
mesgopen(char *name, char *digest)
{
Mesg *m;
char *path;
int fd;
m = mesglookup(name, digest);
if(m == nil || (m->state & Sopen))
return nil;
assert(!(m->state & Sdummy));
m->state |= Sopen;
if(!(m->flags & Fseen)){
m->flags |= Fseen;
path = estrjoin(mbox.path, "/", m->name, "/flags", nil);
if((fd = open(path, OWRITE)) != -1){
fprint(fd, "+s");
close(fd);
}
mbredraw(m, 0, 0);
free(path);
}
threadcreate(mesgmain, m, Stack);
return m;
}
Biobuf*
mesgopenbody(Mesg *m)
{
char *path;
int rfd;
Mesg *b;
b = readparts(m, m);
path = estrjoin(mbox.path, b->name, "body", nil);
if(strcmp(b->type, "text/html") == 0)
rfd = htmlfmt(m, path);
else
rfd = open(path, OREAD);
free(path);
if(rfd == -1)
return nil;
return Bfdopen(rfd, OREAD);
}