11 DIRCHUNK = 32*sizeof(Dir)
14 char regexchars[] = "\\/[].+?()*^$";
15 char deleted[] = "(deleted)-";
16 char deletedrx[] = "\\(deleted\\)-";
17 char deletedrx01[] = "(\\(deleted\\)-)?";
18 char deletedaddr[] = "-#0;/^\\(deleted\\)-/";
25 "text/", "edit", ".txt",
26 /* text must be first for plumbport() */
27 "image/gif", "image", ".gif",
28 "image/jpeg", "image", ".jpg",
29 "image/jpeg", "image", ".jpeg",
30 "image/png", "image", ".png",
31 "image/tiff", "image", ".tif",
32 "application/postscript", "postscript", ".ps",
33 "application/pdf", "postscript", ".pdf",
34 "application/msword", "msword", ".doc",
35 "application/rtf", "msword", ".rtf",
36 "audio/x-wav", "wav", ".wav",
45 "text/tab-separated-values",
46 "application/octet-stream",
69 char *extraheaders[] =
78 line(char *data, char **pp)
82 for(p=data; *p!='\0' && *p!='\n'; p++)
88 q = emalloc(p-data + 1);
89 memmove(q, data, p-data);
94 scanheaders(Message *m, char *dir)
98 s = f = readfile(dir, "header", nil);
102 if(strncmp(t, "From: ", 6) == 0){
103 m->fromcolon = estrdup(t+6);
104 /* remove all quotes; they're ugly and irregular */
105 for(u=m->fromcolon; *u; u++)
107 memmove(u, u+1, strlen(u));
109 if(strncmp(t, "Subject: ", 9) == 0)
110 m->subject = estrdup(t+9);
113 if(m->fromcolon == nil)
114 m->fromcolon = estrdup(m->from);
119 loadinfo(Message *m, char *dir)
124 data = readfile(dir, "info", &n);
127 m->from = line(data, &p);
128 scanheaders(m, dir); /* depends on m->from being set */
131 m->replyto = line(p, &p);
132 m->date = line(p, &p);
134 if(m->subject == nil)
138 m->type = line(p, &p);
139 m->disposition = line(p, &p);
140 m->filename = line(p, &p);
141 m->digest = line(p, &p);
158 loaddir(char *name, int *np)
163 fd = open(name, OREAD);
166 *np = dirreadall(fd, &dp);
172 readmbox(Message *mbox, char *dir, char *subdir)
178 name = estrstrdup(dir, subdir);
179 dirp = loaddir(name, &n);
184 if(isnumeric(d->name))
185 mesgadd(mbox, name, d, nil);
191 /* add message to box, in increasing numerical order */
193 mesgadd(Message *mbox, char *dir, Dir *d, char *digest)
199 m = emalloc(sizeof(Message));
200 m->name = estrstrdup(d->name, "/");
202 m->prev = mbox->tail;
203 m->level= mbox->level+1;
205 name = estrstrdup(dir, m->name);
206 loaded = loadinfo(m, name);
208 /* if two upas/fs are running, we can get misled, so check digest before accepting message */
209 if(loaded==0 || (digest!=nil && m->digest!=nil && strcmp(digest, m->digest)!=0)){
214 if(mbox->tail != nil)
215 mbox->tail->next = m;
217 if(mbox->head == nil)
222 readmbox(m, dir, m->name);
234 s = ctime(time(nil));
237 return strncmp(year, now, 4) == 0;
248 n = tokenize(s, fld, 10);
250 sprint(as, "%.3s ", fld[0]); /* day */
251 /* some dates have 19 Apr, some Apr 19 */
252 if(strlen(fld[1])<4 && isnumeric(fld[1]))
253 sprint(as+strlen(as), "%.3s %.3s ", fld[1], fld[2]); /* date, month */
255 sprint(as+strlen(as), "%.3s %.3s ", fld[2], fld[1]); /* date, month */
256 /* do we use time or year? depends on whether year matches this one */
257 if(thisyear(fld[5])){
258 if(strchr(fld[3], ':') != nil)
259 sprint(as+strlen(as), "%.5s ", fld[3]); /* time */
260 else if(strchr(fld[4], ':') != nil)
261 sprint(as+strlen(as), "%.5s ", fld[4]); /* time */
263 sprint(as+strlen(as), "%.4s ", fld[5]); /* year */
270 readfile(char *dir, char *name, int *np)
278 file = estrstrdup(dir, name);
279 fd = open(file, OREAD);
288 data = emalloc(len+1);
297 info(Message *m, int ind, int ogf)
309 if(ind==0 && shortmenu){
316 if(ind==0 && m->subject[0]=='\0'){
317 snprint(fmt, sizeof fmt, " %%-%d.%ds", len, len);
318 snprint(s, sizeof s, fmt, p);
320 snprint(fmt, sizeof fmt, " %%-%d.%ds %%-%d.%ds", len, len, lens, lens);
321 snprint(s, sizeof s, fmt, p, m->subject);
329 i = eappend(i, "\t", p);
330 i = egrow(i, "\t", stripdate(m->date));
332 if(strcmp(m->type, "text")!=0 && strncmp(m->type, "text/", 5)!=0 &&
333 strncmp(m->type, "multipart/", 10)!=0)
334 i = egrow(i, "\t(", estrstrdup(m->type, ")"));
335 }else if(strncmp(m->type, "multipart/", 10) != 0)
336 i = egrow(i, "\t(", estrstrdup(m->type, ")"));
337 if(m->subject[0] != '\0'){
338 i = eappend(i, "\n", nil);
340 i = eappend(i, "\t", nil);
341 i = eappend(i, "\t", m->subject);
347 mesgmenu0(Window *w, Message *mbox, char *realdir, char *dir, int ind, Biobuf *fd, int onlyone, int dotail)
354 if(strstr(realdir, "outgoing") != nil)
357 /* show mail box in reverse order, pieces in forward order */
367 name = estrstrdup(dir, m->name);
368 tmp = info(m, ind, ogf);
369 Bprint(fd, "%s%s\n", name, tmp);
371 if(dotail && m->tail)
372 mesgmenu0(w, m, realdir, name, ind+1, fd, 0, dotail);
384 mesgmenu(Window *w, Message *mbox)
386 winopenbody(w, OWRITE);
387 mesgmenu0(w, mbox, mbox->name, "", 0, w->body, 0, !shortmenu);
391 /* one new message has arrived, as mbox->tail */
393 mesgmenunew(Window *w, Message *mbox)
397 winselect(w, "0", 0);
398 w->data = winopenfile(w, "data");
399 b = emalloc(sizeof(Biobuf));
400 Binit(b, w->data, OWRITE);
401 mesgmenu0(w, mbox, mbox->name, "", 0, b, 1, !shortmenu);
406 /* select tag line plus following indented lines, but not final newline (it's distinctive) */
407 winselect(w, "0/.*\\n((\t.*\\n)*\t.*)?/", 1);
415 name2regexp(char *prefix, char *s)
419 buf = emalloc(strlen(prefix)+2*strlen(s)+50); /* leave room to append more */
426 for(q=s; *q!='\0'; q++){
427 if(strchr(regexchars, *q) != nil)
437 mesgmenumarkdel(Window *w, Message *mbox, Message *m, int writeback)
444 m->writebackdel = writeback;
446 w->data = winopenfile(w, "data");
447 buf = name2regexp("", m->name);
449 if(winselect(w, buf, 1))
450 write(w->data, deleted, 10);
454 w->addr = w->data = -1;
460 mesgmenumarkundel(Window *w, Message*, Message *m)
467 w->data = winopenfile(w, "data");
468 buf = name2regexp(deletedrx, m->name);
469 if(winselect(w, buf, 1))
470 if(winsetaddr(w, deletedaddr, 1))
471 write(w->data, "", 0);
475 w->addr = w->data = -1;
480 mesgmenudel(Window *w, Message *mbox, Message *m)
485 w->data = winopenfile(w, "data");
486 buf = name2regexp(deletedrx, m->name);
487 if(winsetaddr(w, buf, 1) && winsetaddr(w, ".,./.*\\n(\t.*\\n)*/", 1))
488 write(w->data, "", 0);
492 w->addr = w->data = -1;
498 mesgmenumark(Window *w, char *which, char *mark)
503 w->data = winopenfile(w, "data");
504 buf = name2regexp(deletedrx01, which);
505 if(winsetaddr(w, buf, 1) && winsetaddr(w, "+0-#1", 1)) /* go to end of line */
506 write(w->data, mark, strlen(mark));
510 w->addr = w->data = -1;
516 mesgfreeparts(Message *m)
528 free(m->disposition);
534 mesgdel(Message *mbox, Message *m)
539 error("internal error: deleted message still open in mesgdel");
540 /* delete subparts */
541 for(n=m->head; n!=nil; n=next){
545 /* remove this message from list */
547 m->next->prev = m->prev;
549 mbox->tail = m->prev;
551 m->prev->next = m->next;
553 mbox->head = m->next;
559 mesgsave(Message *m, char *s)
562 char *t, *raw, *unixheader, *all;
564 t = estrstrdup(mbox.name, m->name);
565 raw = readfile(t, "raw", &n);
566 unixheader = readfile(t, "unixheader", &k);
567 if(raw==nil || unixheader==nil){
568 fprint(2, "Mail: can't read %s: %r\n", t);
574 all = emalloc(n+k+1);
575 memmove(all, unixheader, k);
576 memmove(all+k, raw, n);
577 memmove(all+k+n, "\n", 1);
584 s = egrow(estrdup(mailboxdir), "/", s);
585 ofd = open(s, OWRITE);
587 fprint(2, "Mail: can't open %s: %r\n", s);
589 }else if(seek(ofd, 0LL, 2)<0 || write(ofd, all, n)!=n){
590 fprint(2, "Mail: save failed: can't write %s: %r\n", s);
600 mesgcommand(Message *m, char *cmd)
608 nargs = tokenize(s, args, nelem(args));
611 if(strcmp(args[0], "Post") == 0){
615 if(strncmp(args[0], "Save", 4) == 0){
618 s = estrdup("\t[saved");
619 if(nargs==1 || strcmp(args[1], "")==0){
620 ok = mesgsave(m, "stored");
622 ok = mesgsave(m, args[1]);
623 s = eappend(s, " ", args[1]);
626 s = egrow(s, "]", nil);
627 mesgmenumark(mbox.w, m->name, s);
632 if(strcmp(args[0], "Reply")==0){
633 if(nargs>=2 && strcmp(args[1], "all")==0)
634 mkreply(m, "Replyall", nil, nil, nil);
636 mkreply(m, "Reply", nil, nil, nil);
639 if(strcmp(args[0], "Q") == 0){
640 s = winselection(m->w); /* will be freed by mkreply */
641 if(nargs>=3 && strcmp(args[1], "Reply")==0 && strcmp(args[2], "all")==0)
642 mkreply(m, "QReplyall", nil, nil, s);
644 mkreply(m, "QReply", nil, nil, s);
647 if(strcmp(args[0], "Del") == 0){
649 chanfree(m->w->cevent);
663 if(strcmp(args[0], "Delmesg") == 0){
665 mesgmenumarkdel(wbox, &mbox, m, 1);
666 free(cmd); /* mesgcommand might not return */
667 mesgcommand(m, estrdup("Del"));
672 if(strcmp(args[0], "UnDelmesg") == 0){
673 if(!m->isreply && m->deleted)
674 mesgmenumarkundel(wbox, &mbox, m);
677 // if(strcmp(args[0], "Headers") == 0){
690 mesgtagpost(Message *m)
694 wintagwrite(m->w, " Post", 5);
698 /* need to expand selection more than default word */
699 #pragma varargck argpos eval 2
702 eval(Window *w, char *s, ...)
708 vsnprint(buf, sizeof buf, s, arg);
711 if(winsetaddr(w, buf, 1)==0)
714 if(pread(w->addr, buf, 24, 0) != 24)
716 return strtol(buf, 0, 10);
728 else if(!isalpha(*s) && !isdigit(*s) && !strchr("_.-+/", *s))
733 char addrdelim[] = "/[ \t\\n<>()\\[\\]]/";
735 expandaddr(Window *w, Event *e)
740 if(e->q0 != e->q1) /* cannot happen */
743 q0 = eval(w, "#%d-%s", e->q0, addrdelim);
744 if(q0 == -1) /* bad char not found */
746 else /* increment past bad char */
749 q1 = eval(w, "#%d+%s", e->q0, addrdelim);
757 s = emalloc((q1-q0)*UTFmax+1);
758 winread(w, q0, q1, s);
763 replytoaddr(Window *w, Message *m, Event *e, char *s)
772 /* autoexpanded; use our own bigger expansion */
773 buf = expandaddr(w, e);
780 pm = emalloc(sizeof(Plumbmsg));
781 pm->src = estrdup("Mail");
782 pm->dst = estrdup("sendmail");
783 pm->data = estrdup(s);
785 if(m->subject && m->subject[0]){
786 pm->attr = emalloc(sizeof(Plumbattr));
787 pm->attr->name = estrdup("Subject");
788 if(tolower(m->subject[0]) != 'r' || tolower(m->subject[1]) != 'e' || m->subject[2] != ':')
789 pm->attr->value = estrstrdup("Re: ", m->subject);
791 pm->attr->value = estrdup(m->subject);
792 pm->attr->next = nil;
794 if(plumbsend(plumbsendfd, pm) < 0)
795 fprint(2, "error writing plumb message: %r\n");
808 Event *e, *eq, *e2, *ea;
810 char *os, *s, *t, *buf;
814 threadsetname("mesgctl");
815 proccreate(wineventproc, w, STACK);
817 e = recvp(w->cevent);
821 print("unknown message %c%c\n", e->c1, e->c2);
824 case 'E': /* write to body; can't affect us */
827 case 'F': /* generated by our actions; ignore */
830 case 'K': /* type away; we don't care */
833 case 'x': /* mouse only */
838 e2 = recvp(w->cevent);
842 ea = recvp(w->cevent);
847 if(eq->q1>eq->q0 && eq->nb==0){
848 s = emalloc((eq->q1-eq->q0)*UTFmax+1);
849 winread(w, eq->q0, eq->q1, s);
853 t = emalloc(strlen(s)+1+na+1);
854 sprint(t, "%s %s", s, ea->b);
858 if(!mesgcommand(m, s)) /* send it back */
862 case 'l': /* mouse only */
867 e2 = recvp(w->cevent);
871 if(eq->q1>eq->q0 && eq->nb==0){
872 buf = emalloc((eq->q1-eq->q0)*UTFmax+1);
873 winread(w, eq->q0, eq->q1, buf);
879 /* skip mail box name if present */
880 if(strncmp(s, mbox.name, strlen(mbox.name)) == 0)
881 s += strlen(mbox.name);
882 if(strstr(s, "body") != nil){
883 /* strip any known extensions */
884 for(i=0; exts[i].ext!=nil; i++){
885 j = strlen(exts[i].ext);
886 if(strlen(s)>j && strcmp(s+strlen(s)-j, exts[i].ext)==0){
887 s[strlen(s)-j] = '\0';
891 if(strlen(s)>5 && strcmp(s+strlen(s)-5, "/body")==0)
892 s[strlen(s)-4] = '\0'; /* leave / in place */
894 nopen += mesgopen(&mbox, mbox.name, s, m, 0, nil);
895 while(*s!=0 && *s++!='\n')
898 if(nopen == 0 && e->c1 == 'L')
899 nopen += replytoaddr(w, m, e, os);
905 case 'I': /* modify away; we don't care */
921 mesgline(Message *m, char *header, char *value)
923 if(strlen(value) > 0)
924 Bprint(m->w->body, "%s: %s\n", header, value);
928 isprintable(char *type)
932 for(i=0; goodtypes[i]!=nil; i++)
933 if(strcmp(type, goodtypes[i])==0)
943 for(i=0; exts[i].type!=nil; i++)
944 if(strcmp(type, exts[i].type)==0)
950 mimedisplay(Message *m, char *name, char *rootdir, Window *w, int fileonly)
954 if(strcmp(m->disposition, "file")==0 || strlen(m->filename)!=0){
955 if(strlen(m->filename) == 0){
956 dest = estrdup(m->name);
957 dest[strlen(dest)-1] = '\0';
959 dest = estrdup(m->filename);
960 if(m->filename[0] != '/')
961 dest = egrow(estrdup(home), "/", dest);
962 Bprint(w->body, "\tcp %s%sbody%s %q\n", rootdir, name, ext(m->type), dest);
965 Bprint(w->body, "\tfile is %s%sbody%s\n", rootdir, name, ext(m->type));
969 printheader(char *dir, Biobuf *b, char **okheaders)
975 s = readfile(dir, "header", nil);
978 n = getfields(s, lines, nelem(lines), 0, "\n");
980 for(j=0; okheaders[j]; j++)
981 if(cistrncmp(lines[i], okheaders[j], strlen(okheaders[j])) == 0)
982 Bprint(b, "%s\n", lines[i]);
987 mesgload(Message *m, char *rootdir, char *file, Window *w)
989 char *s, *subdir, *name, *dir;
990 Message *mp, *thisone;
993 dir = estrstrdup(rootdir, file);
995 if(strcmp(m->type, "message/rfc822") != 0){ /* suppress headers of envelopes */
996 if(strlen(m->from) > 0){
997 Bprint(w->body, "From: %s\n", m->from);
998 mesgline(m, "Date", m->date);
999 mesgline(m, "To", m->to);
1000 mesgline(m, "CC", m->cc);
1001 mesgline(m, "Subject", m->subject);
1002 printheader(dir, w->body, extraheaders);
1004 printheader(dir, w->body, okheaders);
1005 printheader(dir, w->body, extraheaders);
1007 Bprint(w->body, "\n");
1010 if(m->level == 1 && m->recursed == 0){
1012 readmbox(m, rootdir, m->name);
1014 if(m->head == nil){ /* single part message */
1015 if(strcmp(m->type, "text")==0 || strncmp(m->type, "text/", 5)==0){
1016 mimedisplay(m, m->name, rootdir, w, 1);
1017 s = readbody(m->type, dir, &n);
1018 winwritebody(w, s, n);
1021 mimedisplay(m, m->name, rootdir, w, 0);
1023 /* multi-part message, either multipart/* or message/rfc822 */
1025 if(strcmp(m->type, "multipart/alternative") == 0){
1026 thisone = m->head; /* in case we can't find a good one */
1027 for(mp=m->head; mp!=nil; mp=mp->next)
1028 if(isprintable(mp->type)){
1033 for(mp=m->head; mp!=nil; mp=mp->next){
1034 if(thisone!=nil && mp!=thisone)
1036 subdir = estrstrdup(dir, mp->name);
1037 name = estrstrdup(file, mp->name);
1038 /* skip first element in name because it's already in window name */
1040 Bprint(w->body, "\n===> %s (%s) [%s]\n", strchr(name, '/')+1, mp->type, mp->disposition);
1041 if(strcmp(mp->type, "text")==0 || strncmp(mp->type, "text/", 5)==0){
1042 mimedisplay(mp, name, rootdir, w, 1);
1043 printheader(subdir, w->body, okheaders);
1044 printheader(subdir, w->body, extraheaders);
1045 winwritebody(w, "\n", 1);
1046 s = readbody(mp->type, subdir, &n);
1047 winwritebody(w, s, n);
1050 if(strncmp(mp->type, "multipart/", 10)==0 || strcmp(mp->type, "message/rfc822")==0){
1052 mesgload(mp, rootdir, name, w);
1055 mimedisplay(mp, name, rootdir, w, 0);
1065 tokenizec(char *str, char **args, int max, char *splitc)
1072 for(na=0; *str != '\0';str++){
1073 if(strchr(splitc, *str) == nil){
1079 /* it's a separator/skip character */
1092 mesglookup(Message *mbox, char *name, char *digest)
1099 /* can find exactly */
1100 for(m=mbox->head; m!=nil; m=m->next)
1101 if(strcmp(digest, m->digest) == 0)
1109 if(name[n-1] == '/')
1112 t = estrstrdup(name, "/");
1113 for(m=mbox->head; m!=nil; m=m->next)
1114 if(strcmp(t, m->name) == 0)
1121 * Find plumb port, knowing type is text, given file name (by extension)
1124 plumbportbysuffix(char *file)
1129 nfile = strlen(file);
1130 for(i=0; ports[i].type!=nil; i++){
1131 suf = ports[i].suffix;
1134 if(cistrncmp(file+nfile-nsuf, suf, nsuf) == 0)
1141 * Find plumb port using type and file name (by extension)
1144 plumbport(char *type, char *file)
1148 for(i=0; ports[i].type!=nil; i++)
1149 if(strncmp(type, ports[i].type, strlen(ports[i].type)) == 0)
1151 /* see if it's a text type */
1152 for(i=0; goodtypes[i]!=nil; i++)
1153 if(strncmp(type, goodtypes[i], strlen(goodtypes[i])) == 0)
1154 return plumbportbysuffix(file);
1159 plumb(Message *m, char *dir)
1165 if(strlen(m->type) == 0)
1167 i = plumbport(m->type, m->filename);
1169 fprint(2, "can't find destination for message subpart\n");
1171 port = ports[i].port;
1172 pm = emalloc(sizeof(Plumbmsg));
1173 pm->src = estrdup("Mail");
1175 pm->dst = estrdup(port);
1179 pm->type = estrdup("text");
1181 pm->data = estrstrdup(dir, "body");
1182 pm->data = eappend(pm->data, "", ports[i].suffix);
1183 if(plumbsend(plumbsendfd, pm) < 0)
1184 fprint(2, "error writing plumb message: %r\n");
1190 mesgopen(Message *mbox, char *dir, char *s, Message *mesg, int plumbed, char *digest)
1195 int i, ndirelem, reuse;
1197 /* find white-space-delimited first word */
1198 for(t=s; *t!='\0' && !isspace(*t); t++)
1202 /* separate it on slashes */
1203 ndirelem = tokenizec(u, direlem, nelem(direlem), "/");
1210 write(wctlfd, "top", 3);
1211 write(wctlfd, "current", 7);
1213 /* open window for message */
1214 m = mesglookup(mbox, direlem[0], digest);
1217 if(mesg!=nil && m!=mesg) /* string looked like subpart but isn't part of this message */
1225 /* re-use existing window */
1226 if(winsetaddr(m->w, "0,$", 1)){
1228 m->w->data = winopenfile(m->w, "data");
1229 write(m->w->data, "", 0);
1232 v = estrstrdup(mbox->name, m->name);
1237 wintagwrite(m->w, "Q Reply all UnDelmesg Save ", 2+6+4+10+5);
1239 wintagwrite(m->w, "Q Reply all Delmesg Save ", 2+6+4+8+5);
1241 threadcreate(mesgctl, m, STACK);
1242 winopenbody(m->w, OWRITE);
1243 mesgload(m, dir, m->name, m->w);
1252 if(ndirelem == 1 && plumbport(m->type, m->filename) <= 0){
1253 /* make sure dot is visible */
1254 ctlprint(m->w->ctl, "show\n");
1257 /* walk to subpart */
1258 dir = estrstrdup(dir, m->name);
1259 for(i=1; i<ndirelem; i++){
1260 m = mesglookup(m, direlem[i], digest);
1263 dir = egrow(dir, m->name, nil);
1265 if(m != nil && plumbport(m->type, m->filename) > 0)
1273 rewritembox(Window *w, Message *mbox)
1276 char *deletestr, *t;
1279 deletestr = estrstrdup("delete ", fsname);
1282 for(m=mbox->head; m!=nil; m=next){
1290 if(m->writebackdel){
1291 /* messages deleted by plumb message are not removed again */
1292 t = estrdup(m->name);
1294 t[strlen(t)-1] = '\0';
1295 deletestr = egrow(deletestr, " ", t);
1297 mesgmenudel(w, mbox, m);
1300 if(write(mbox->ctlfd, deletestr, strlen(deletestr)) < 0)
1301 fprint(2, "Mail: warning: error removing mail message files: %r\n");
1303 winselect(w, "0", 0);
1309 /* name is a full file name, but it might not belong to us */
1311 mesglookupfile(Message *mbox, char *name, char *digest)
1316 n = strlen(mbox->name);
1317 if(k==0 || strncmp(name, mbox->name, n) != 0){
1318 // fprint(2, "Mail: message %s not in this mailbox\n", name);
1321 return mesglookup(mbox, name+n, digest);