xref: /plan9-contrib/acme/mail/src/mesg.c (revision ec59a3ddbfceee0efe34584c2c9981a5e5ff1ec4)
1 #include <u.h>
2 #include <libc.h>
3 #include <bio.h>
4 #include <thread.h>
5 #include <ctype.h>
6 #include <plumb.h>
7 #include "dat.h"
8 
9 enum
10 {
11 	DIRCHUNK = 32*sizeof(Dir)
12 };
13 
14 char	regexchars[] = "\\/[].+?()*^$";
15 char	deleted[] = "(deleted)-";
16 char	deletedrx[] = "\\(deleted\\)-";
17 char	deletedrx01[] = "(\\(deleted\\)-)?";
18 char	deletedaddr[] = "-#0;/^\\(deleted\\)-/";
19 
20 struct{
21 	char	*type;
22 	char	*port;
23 	char *suffix;
24 } ports[] = {
25 	"text/",			"edit",	".txt", /* must be first for plumbport() */
26 	"image/gif",		"image",	".gif",
27 	"image/jpeg",		"image",	".jpg",
28 	"image/jpeg",		"image",	".jpeg",
29 	"image/png",		"image",	".png",
30 	"application/postscript",	"postscript",	".ps",
31 	"application/pdf",	"postscript",	".pdf",
32 	"application/msword",	"msword",	".doc",
33 	"application/rtf",	"msword",	".rtf",
34 	nil,	nil
35 };
36 
37 char *goodtypes[] = {
38 	"text",
39 	"text/plain",
40 	"message/rfc822",
41 	"text/richtext",
42 	"text/tab-separated-values",
43 	"application/octet-stream",
44 	nil,
45 };
46 
47 struct{
48 	char *type;
49 	char	*ext;
50 } exts[] = {
51 	"image/gif",	".gif",
52 	"image/jpeg",	".jpg",
53 	nil, nil
54 };
55 
56 char *okheaders[] =
57 {
58 	"From:",
59 	"Date:",
60 	"To:",
61 	"CC:",
62 	"Subject:",
63 	nil
64 };
65 
66 char *extraheaders[] =
67 {
68 	"Resent-From:",
69 	"Resent-To:",
70 	"Sort:",
71 	nil,
72 };
73 
74 char*
75 line(char *data, char **pp)
76 {
77 	char *p, *q;
78 
79 	for(p=data; *p!='\0' && *p!='\n'; p++)
80 		;
81 	if(*p == '\n')
82 		*pp = p+1;
83 	else
84 		*pp = p;
85 	q = emalloc(p-data + 1);
86 	memmove(q, data, p-data);
87 	return q;
88 }
89 
90 void
91 scanheaders(Message *m, char *dir)
92 {
93 	char *s, *t, *u, *f;
94 
95 	s = f = readfile(dir, "header", nil);
96 	if(s != nil)
97 		while(*s){
98 			t = line(s, &s);
99 			if(strncmp(t, "From: ", 6) == 0){
100 				m->fromcolon = estrdup(t+6);
101 				/* remove all quotes; they're ugly and irregular */
102 				for(u=m->fromcolon; *u; u++)
103 					if(*u == '"')
104 						memmove(u, u+1, strlen(u));
105 			}
106 			if(strncmp(t, "Subject: ", 9) == 0)
107 				m->subject = estrdup(t+9);
108 			free(t);
109 		}
110 	if(m->fromcolon == nil)
111 		m->fromcolon = estrdup(m->from);
112 	free(f);
113 }
114 
115 int
116 loadinfo(Message *m, char *dir)
117 {
118 	int n;
119 	char *data, *p, *s;
120 
121 	data = readfile(dir, "info", &n);
122 	if(data == nil)
123 		return 0;
124 	m->from = line(data, &p);
125 	scanheaders(m, dir);	/* depends on m->from being set */
126 	m->to = line(p, &p);
127 	m->cc = line(p, &p);
128 	m->replyto = line(p, &p);
129 	m->date = line(p, &p);
130 	s = line(p, &p);
131 	if(m->subject == nil)
132 		m->subject = s;
133 	else
134 		free(s);
135 	m->type = line(p, &p);
136 	m->disposition = line(p, &p);
137 	m->filename = line(p, &p);
138 	m->digest = line(p, &p);
139 	free(data);
140 	return 1;
141 }
142 
143 int
144 isnumeric(char *s)
145 {
146 	while(*s){
147 		if(!isdigit(*s))
148 			return 0;
149 		s++;
150 	}
151 	return 1;
152 }
153 
154 Dir*
155 loaddir(char *name, int *np)
156 {
157 	int fd;
158 	Dir *dp;
159 
160 	fd = open(name, OREAD);
161 	if(fd < 0)
162 		return nil;
163 	*np = dirreadall(fd, &dp);
164 	close(fd);
165 	return dp;
166 }
167 
168 void
169 readmbox(Message *mbox, char *dir, char *subdir)
170 {
171 	char *name;
172 	Dir *d, *dirp;
173 	int i, n;
174 
175 	name = estrstrdup(dir, subdir);
176 	dirp = loaddir(name, &n);
177 	mbox->recursed = 1;
178 	if(dirp)
179 		for(i=0; i<n; i++){
180 			d = &dirp[i];
181 			if(isnumeric(d->name))
182 				mesgadd(mbox, name, d, nil);
183 		}
184 	free(dirp);
185 	free(name);
186 }
187 
188 /* add message to box, in increasing numerical order */
189 int
190 mesgadd(Message *mbox, char *dir, Dir *d, char *digest)
191 {
192 	Message *m;
193 	char *name;
194 	int loaded;
195 
196 	m = emalloc(sizeof(Message));
197 	m->name = estrstrdup(d->name, "/");
198 	m->next = nil;
199 	m->prev = mbox->tail;
200 	m->level= mbox->level+1;
201 	m->recursed = 0;
202 	name = estrstrdup(dir, m->name);
203 	loaded = loadinfo(m, name);
204 	free(name);
205 	/* if two upas/fs are running, we can get misled, so check digest before accepting message */
206 	if(loaded==0 || (digest!=nil && m->digest!=nil && strcmp(digest, m->digest)!=0)){
207 		mesgfreeparts(m);
208 		free(m);
209 		return 0;
210 	}
211 	if(mbox->tail != nil)
212 		mbox->tail->next = m;
213 	mbox->tail = m;
214 	if(mbox->head == nil)
215 		mbox->head = m;
216 
217 	if (m->level != 1){
218 		m->recursed = 1;
219 		readmbox(m, dir, m->name);
220 	}
221 	return 1;
222 }
223 
224 int
225 thisyear(char *year)
226 {
227 	static char now[10];
228 	char *s;
229 
230 	if(now[0] == '\0'){
231 		s = ctime(time(nil));
232 		strcpy(now, s+24);
233 	}
234 	return strncmp(year, now, 4) == 0;
235 }
236 
237 char*
238 stripdate(char *as)
239 {
240 	int n;
241 	char *s, *fld[10];
242 
243 	as = estrdup(as);
244 	s = estrdup(as);
245 	n = tokenize(s, fld, 10);
246 	if(n > 5){
247 		sprint(as, "%.3s ", fld[0]);	/* day */
248 		/* some dates have 19 Apr, some Apr 19 */
249 		if(strlen(fld[1])<4 && isnumeric(fld[1]))
250 			sprint(as+strlen(as), "%.3s %.3s ", fld[1], fld[2]);	/* date, month */
251 		else
252 			sprint(as+strlen(as), "%.3s %.3s ", fld[2], fld[1]);	/* date, month */
253 		/* do we use time or year?  depends on whether year matches this one */
254 		if(thisyear(fld[5])){
255 			if(strchr(fld[3], ':') != nil)
256 				sprint(as+strlen(as), "%.5s ", fld[3]);	/* time */
257 			else if(strchr(fld[4], ':') != nil)
258 				sprint(as+strlen(as), "%.5s ", fld[4]);	/* time */
259 		}else
260 			sprint(as+strlen(as), "%.4s ", fld[5]);	/* year */
261 	}
262 	free(s);
263 	return as;
264 }
265 
266 char*
267 readfile(char *dir, char *name, int *np)
268 {
269 	char *file, *data;
270 	int fd, len;
271 	Dir *d;
272 
273 	if(np != nil)
274 		*np = 0;
275 	file = estrstrdup(dir, name);
276 	fd = open(file, OREAD);
277 	if(fd < 0)
278 		return nil;
279 	d = dirfstat(fd);
280 	free(file);
281 	len = 0;
282 	if(d != nil)
283 		len = d->length;
284 	free(d);
285 	data = emalloc(len+1);
286 	read(fd, data, len);
287 	close(fd);
288 	if(np != nil)
289 		*np = len;
290 	return data;
291 }
292 
293 char*
294 info(Message *m, int ind, int ogf)
295 {
296 	char *i;
297 	int j, len, lens;
298 	char *p;
299 	char fmt[80], s[80];
300 
301 	if (ogf)
302 		p=m->to;
303 	else
304 		p=m->fromcolon;
305 
306 	if(ind==0 && shortmenu){
307 		len = 30;
308 		lens = 30;
309 		if(shortmenu > 1){
310 			len = 10;
311 			lens = 25;
312 		}
313 		if(ind==0 && m->subject[0]=='\0'){
314 			snprint(fmt, sizeof fmt, " %%-%d.%ds", len, len);
315 			snprint(s, sizeof s, fmt, p);
316 		}else{
317 			snprint(fmt, sizeof fmt, " %%-%d.%ds  %%-%d.%ds", len, len, lens, lens);
318 			snprint(s, sizeof s, fmt, p, m->subject);
319 		}
320 		i = estrdup(s);
321 
322 		return i;
323 	}
324 
325 	i = estrdup("");
326 	i = eappend(i, "\t", p);
327 	i = egrow(i, "\t", stripdate(m->date));
328 	if(ind == 0){
329 		if(strcmp(m->type, "text")!=0 && strncmp(m->type, "text/", 5)!=0 &&
330 		   strncmp(m->type, "multipart/", 10)!=0)
331 			i = egrow(i, "\t(", estrstrdup(m->type, ")"));
332 	}else if(strncmp(m->type, "multipart/", 10) != 0)
333 		i = egrow(i, "\t(", estrstrdup(m->type, ")"));
334 	if(m->subject[0] != '\0'){
335 		i = eappend(i, "\n", nil);
336 		for(j=0; j<ind; j++)
337 			i = eappend(i, "\t", nil);
338 		i = eappend(i, "\t", m->subject);
339 	}
340 	return i;
341 }
342 
343 void
344 mesgmenu0(Window *w, Message *mbox, char *realdir, char *dir, int ind, Biobuf *fd, int onlyone, int dotail)
345 {
346 	int i;
347 	Message *m;
348 	char *name, *tmp;
349 	int ogf=0;
350 
351 	if(strstr(realdir, "outgoing") != nil)
352 		ogf=1;
353 
354 	/* show mail box in reverse order, pieces in forward order */
355 	if(ind > 0)
356 		m = mbox->head;
357 	else
358 		m = mbox->tail;
359 	while(m != nil){
360 		for(i=0; i<ind; i++)
361 			Bprint(fd, "\t");
362 		if(ind != 0)
363 			Bprint(fd, "  ");
364 		name = estrstrdup(dir, m->name);
365 		tmp = info(m, ind, ogf);
366 		Bprint(fd, "%s%s\n", name, tmp);
367 		free(tmp);
368 		if(dotail && m->tail)
369 			mesgmenu0(w, m, realdir, name, ind+1, fd, 0, dotail);
370 		free(name);
371 		if(ind)
372 			m = m->next;
373 		else
374 			m = m->prev;
375 		if(onlyone)
376 			m = nil;
377 	}
378 }
379 
380 void
381 mesgmenu(Window *w, Message *mbox)
382 {
383 	winopenbody(w, OWRITE);
384 	mesgmenu0(w, mbox, mbox->name, "", 0, w->body, 0, !shortmenu);
385 	winclosebody(w);
386 }
387 
388 /* one new message has arrived, as mbox->tail */
389 void
390 mesgmenunew(Window *w, Message *mbox)
391 {
392 	Biobuf *b;
393 
394 	winselect(w, "0", 0);
395 	w->data = winopenfile(w, "data");
396 	b = emalloc(sizeof(Biobuf));
397 	Binit(b, w->data, OWRITE);
398 	mesgmenu0(w, mbox, mbox->name, "", 0, b, 1, !shortmenu);
399 	Bterm(b);
400 	free(b);
401 	if(!mbox->dirty)
402 		winclean(w);
403 	/* select tag line plus following indented lines, but not final newline (it's distinctive) */
404 	winselect(w, "0/.*\\n((\t.*\\n)*\t.*)?/", 1);
405 	close(w->addr);
406 	close(w->data);
407 	w->addr = -1;
408 	w->data = -1;
409 }
410 
411 char*
412 name2regexp(char *prefix, char *s)
413 {
414 	char *buf, *p, *q;
415 
416 	buf = emalloc(strlen(prefix)+2*strlen(s)+50);	/* leave room to append more */
417 	p = buf;
418 	*p++ = '0';
419 	*p++ = '/';
420 	*p++ = '^';
421 	strcpy(p, prefix);
422 	p += strlen(prefix);
423 	for(q=s; *q!='\0'; q++){
424 		if(strchr(regexchars, *q) != nil)
425 			*p++ = '\\';
426 		*p++ = *q;
427 	}
428 	*p++ = '/';
429 	*p = '\0';
430 	return buf;
431 }
432 
433 void
434 mesgmenumarkdel(Window *w, Message *mbox, Message *m, int writeback)
435 {
436 	char *buf;
437 
438 
439 	if(m->deleted)
440 		return;
441 	m->writebackdel = writeback;
442 	if(w->data < 0)
443 		w->data = winopenfile(w, "data");
444 	buf = name2regexp("", m->name);
445 	strcat(buf, "-#0");
446 	if(winselect(w, buf, 1))
447 		write(w->data, deleted, 10);
448 	free(buf);
449 	close(w->data);
450 	close(w->addr);
451 	w->addr = w->data = -1;
452 	mbox->dirty = 1;
453 	m->deleted = 1;
454 }
455 
456 void
457 mesgmenumarkundel(Window *w, Message*, Message *m)
458 {
459 	char *buf;
460 
461 	if(m->deleted == 0)
462 		return;
463 	if(w->data < 0)
464 		w->data = winopenfile(w, "data");
465 	buf = name2regexp(deletedrx, m->name);
466 	if(winselect(w, buf, 1))
467 		if(winsetaddr(w, deletedaddr, 1))
468 			write(w->data, "", 0);
469 	free(buf);
470 	close(w->data);
471 	close(w->addr);
472 	w->addr = w->data = -1;
473 	m->deleted = 0;
474 }
475 
476 void
477 mesgmenudel(Window *w, Message *mbox, Message *m)
478 {
479 	char *buf;
480 
481 	if(w->data < 0)
482 		w->data = winopenfile(w, "data");
483 	buf = name2regexp(deletedrx, m->name);
484 	if(winsetaddr(w, buf, 1) && winsetaddr(w, ".,./.*\\n(\t.*\\n)*/", 1))
485 		write(w->data, "", 0);
486 	free(buf);
487 	close(w->data);
488 	close(w->addr);
489 	w->addr = w->data = -1;
490 	mbox->dirty = 1;
491 	m->deleted = 1;
492 }
493 
494 void
495 mesgmenumark(Window *w, char *which, char *mark)
496 {
497 	char *buf;
498 
499 	if(w->data < 0)
500 		w->data = winopenfile(w, "data");
501 	buf = name2regexp(deletedrx01, which);
502 	if(winsetaddr(w, buf, 1) && winsetaddr(w, "+0-#1", 1))	/* go to end of line */
503 		write(w->data, mark, strlen(mark));
504 	free(buf);
505 	close(w->data);
506 	close(w->addr);
507 	w->addr = w->data = -1;
508 	if(!mbox.dirty)
509 		winclean(w);
510 }
511 
512 void
513 mesgfreeparts(Message *m)
514 {
515 	free(m->name);
516 	free(m->replyname);
517 	free(m->fromcolon);
518 	free(m->from);
519 	free(m->to);
520 	free(m->cc);
521 	free(m->replyto);
522 	free(m->date);
523 	free(m->subject);
524 	free(m->type);
525 	free(m->disposition);
526 	free(m->filename);
527 	free(m->digest);
528 }
529 
530 void
531 mesgdel(Message *mbox, Message *m)
532 {
533 	Message *n, *next;
534 
535 	if(m->opened)
536 		error("internal error: deleted message still open in mesgdel");
537 	/* delete subparts */
538 	for(n=m->head; n!=nil; n=next){
539 		next = n->next;
540 		mesgdel(m, n);
541 	}
542 	/* remove this message from list */
543 	if(m->next)
544 		m->next->prev = m->prev;
545 	else
546 		mbox->tail = m->prev;
547 	if(m->prev)
548 		m->prev->next = m->next;
549 	else
550 		mbox->head = m->next;
551 
552 	mesgfreeparts(m);
553 }
554 
555 int
556 mesgsave(Message *m, char *s)
557 {
558 	int ofd, n, k, ret;
559 	char *t, *raw, *unixheader, *all;
560 
561 	t = estrstrdup(mbox.name, m->name);
562 	raw = readfile(t, "raw", &n);
563 	unixheader = readfile(t, "unixheader", &k);
564 	if(raw==nil || unixheader==nil){
565 		fprint(2, "Mail: can't read %s: %r\n", t);
566 		free(t);
567 		return 0;
568 	}
569 	free(t);
570 
571 	all = emalloc(n+k+1);
572 	memmove(all, unixheader, k);
573 	memmove(all+k, raw, n);
574 	memmove(all+k+n, "\n", 1);
575 	n = k+n+1;
576 	free(unixheader);
577 	free(raw);
578 	ret = 1;
579 	s = estrdup(s);
580 	if(s[0] != '/')
581 		s = egrow(estrdup(mailboxdir), "/", s);
582 	ofd = open(s, OWRITE);
583 	if(ofd < 0){
584 		fprint(2, "Mail: can't open %s: %r\n", s);
585 		ret = 0;
586 	}else if(seek(ofd, 0LL, 2)<0 || write(ofd, all, n)!=n){
587 		fprint(2, "Mail: save failed: can't write %s: %r\n", s);
588 		ret = 0;
589 	}
590 	free(all);
591 	close(ofd);
592 	free(s);
593 	return ret;
594 }
595 
596 int
597 mesgcommand(Message *m, char *cmd)
598 {
599 	char *s;
600 	char *args[10];
601 	int ok, ret, nargs;
602 
603 	s = cmd;
604 	ret = 1;
605 	nargs = tokenize(s, args, nelem(args));
606 	if(nargs == 0)
607 		return 0;
608 	if(strcmp(args[0], "Post") == 0){
609 		mesgsend(m);
610 		goto Return;
611 	}
612 	if(strncmp(args[0], "Save", 4) == 0){
613 		if(m->isreply)
614 			goto Return;
615 		s = estrdup("\t[saved");
616 		if(nargs==1 || strcmp(args[1], "")==0){
617 			ok = mesgsave(m, "stored");
618 		}else{
619 			ok = mesgsave(m, args[1]);
620 			s = eappend(s, " ", args[1]);
621 		}
622 		if(ok){
623 			s = egrow(s, "]", nil);
624 			mesgmenumark(mbox.w, m->name, s);
625 		}
626 		free(s);
627 		goto Return;
628 	}
629 	if(strcmp(args[0], "Reply")==0){
630 		if(nargs>=2 && strcmp(args[1], "all")==0)
631 			mkreply(m, "Replyall", nil, nil, nil);
632 		else
633 			mkreply(m, "Reply", nil, nil, nil);
634 		goto Return;
635 	}
636 	if(strcmp(args[0], "Q") == 0){
637 		s = winselection(m->w);	/* will be freed by mkreply */
638 		if(nargs>=3 && strcmp(args[1], "Reply")==0 && strcmp(args[2], "all")==0)
639 			mkreply(m, "QReplyall", nil, nil, s);
640 		else
641 			mkreply(m, "QReply", nil, nil, s);
642 		goto Return;
643 	}
644 	if(strcmp(args[0], "Del") == 0){
645 		if(windel(m->w, 0)){
646 			chanfree(m->w->cevent);
647 			free(m->w);
648 			m->w = nil;
649 			if(m->isreply)
650 				delreply(m);
651 			else{
652 				m->opened = 0;
653 				m->tagposted = 0;
654 			}
655 			free(cmd);
656 			threadexits(nil);
657 		}
658 		goto Return;
659 	}
660 	if(strcmp(args[0], "Delmesg") == 0){
661 		if(!m->isreply){
662 			mesgmenumarkdel(wbox, &mbox, m, 1);
663 			free(cmd);	/* mesgcommand might not return */
664 			mesgcommand(m, estrdup("Del"));
665 			return 1;
666 		}
667 		goto Return;
668 	}
669 	if(strcmp(args[0], "UnDelmesg") == 0){
670 		if(!m->isreply && m->deleted)
671 			mesgmenumarkundel(wbox, &mbox, m);
672 		goto Return;
673 	}
674 //	if(strcmp(args[0], "Headers") == 0){
675 //		m->showheaders();
676 //		return True;
677 //	}
678 
679 	ret = 0;
680 
681     Return:
682 	free(cmd);
683 	return ret;
684 }
685 
686 void
687 mesgtagpost(Message *m)
688 {
689 	if(m->tagposted)
690 		return;
691 	wintagwrite(m->w, " Post", 5);
692 	m->tagposted = 1;
693 }
694 
695 /* need to expand selection more than default word */
696 #pragma varargck argpos eval 2
697 
698 long
699 eval(Window *w, char *s, ...)
700 {
701 	char buf[64];
702 	va_list arg;
703 
704 	va_start(arg, s);
705 	vsnprint(buf, sizeof buf, s, arg);
706 	va_end(arg);
707 
708 	if(winsetaddr(w, buf, 1)==0)
709 		return -1;
710 
711 	if(pread(w->addr, buf, 24, 0) != 24)
712 		return -1;
713 	return strtol(buf, 0, 10);
714 }
715 
716 int
717 isemail(char *s)
718 {
719 	int nat;
720 
721 	nat = 0;
722 	for(; *s; s++)
723 		if(*s == '@')
724 			nat++;
725 		else if(!isalpha(*s) && !isdigit(*s) && !strchr("_.-+/", *s))
726 			return 0;
727 	return nat==1;
728 }
729 
730 char addrdelim[] =  "/[ \t\\n<>()\\[\\]]/";
731 char*
732 expandaddr(Window *w, Event *e)
733 {
734 	char *s;
735 	long q0, q1;
736 
737 	if(e->q0 != e->q1)	/* cannot happen */
738 		return nil;
739 
740 	q0 = eval(w, "#%d-%s", e->q0, addrdelim);
741 	if(q0 == -1)	/* bad char not found */
742 		q0 = 0;
743 	else			/* increment past bad char */
744 		q0++;
745 
746 	q1 = eval(w, "#%d+%s", e->q0, addrdelim);
747 	if(q1 < 0){
748 		q1 = eval(w, "$");
749 		if(q1 < 0)
750 			return nil;
751 	}
752 	if(q0 >= q1)
753 		return nil;
754 	s = emalloc((q1-q0)*UTFmax+1);
755 	winread(w, q0, q1, s);
756 	return s;
757 }
758 
759 int
760 replytoaddr(Window *w, Message *m, Event *e, char *s)
761 {
762 	int did;
763 	char *buf;
764 	Plumbmsg *pm;
765 
766 	buf = nil;
767 	did = 0;
768 	if(e->flag & 2){
769 		/* autoexpanded; use our own bigger expansion */
770 		buf = expandaddr(w, e);
771 		if(buf == nil)
772 			return 0;
773 		s = buf;
774 	}
775 	if(isemail(s)){
776 		did = 1;
777 		pm = emalloc(sizeof(Plumbmsg));
778 		pm->src = estrdup("Mail");
779 		pm->dst = estrdup("sendmail");
780 		pm->data = estrdup(s);
781 		pm->ndata = -1;
782 		if(m->subject && m->subject[0]){
783 			pm->attr = emalloc(sizeof(Plumbattr));
784 			pm->attr->name = estrdup("Subject");
785 			if(tolower(m->subject[0]) != 'r' || tolower(m->subject[1]) != 'e' || m->subject[2] != ':')
786 				pm->attr->value = estrstrdup("Re: ", m->subject);
787 			else
788 				pm->attr->value = estrdup(m->subject);
789 			pm->attr->next = nil;
790 		}
791 		if(plumbsend(plumbsendfd, pm) < 0)
792 			fprint(2, "error writing plumb message: %r\n");
793 		plumbfree(pm);
794 	}
795 	free(buf);
796 	return did;
797 }
798 
799 
800 void
801 mesgctl(void *v)
802 {
803 	Message *m;
804 	Window *w;
805 	Event *e, *eq, *e2, *ea;
806 	int na, nopen, i, j;
807 	char *os, *s, *t, *buf;
808 
809 	m = v;
810 	w = m->w;
811 	threadsetname("mesgctl");
812 	proccreate(wineventproc, w, STACK);
813 	for(;;){
814 		e = recvp(w->cevent);
815 		switch(e->c1){
816 		default:
817 		Unk:
818 			print("unknown message %c%c\n", e->c1, e->c2);
819 			break;
820 
821 		case 'E':	/* write to body; can't affect us */
822 			break;
823 
824 		case 'F':	/* generated by our actions; ignore */
825 			break;
826 
827 		case 'K':	/* type away; we don't care */
828 		case 'M':
829 			switch(e->c2){
830 			case 'x':	/* mouse only */
831 			case 'X':
832 				ea = nil;
833 				eq = e;
834 				if(e->flag & 2){
835 					e2 = recvp(w->cevent);
836 					eq = e2;
837 				}
838 				if(e->flag & 8){
839 					ea = recvp(w->cevent);
840 					recvp(w->cevent);
841 					na = ea->nb;
842 				}else
843 					na = 0;
844 				if(eq->q1>eq->q0 && eq->nb==0){
845 					s = emalloc((eq->q1-eq->q0)*UTFmax+1);
846 					winread(w, eq->q0, eq->q1, s);
847 				}else
848 					s = estrdup(eq->b);
849 				if(na){
850 					t = emalloc(strlen(s)+1+na+1);
851 					sprint(t, "%s %s", s, ea->b);
852 					free(s);
853 					s = t;
854 				}
855 				if(!mesgcommand(m, s))	/* send it back */
856 					winwriteevent(w, e);
857 				break;
858 
859 			case 'l':	/* mouse only */
860 			case 'L':
861 				buf = nil;
862 				eq = e;
863 				if(e->flag & 2){
864 					e2 = recvp(w->cevent);
865 					eq = e2;
866 				}
867 				s = eq->b;
868 				if(eq->q1>eq->q0 && eq->nb==0){
869 					buf = emalloc((eq->q1-eq->q0)*UTFmax+1);
870 					winread(w, eq->q0, eq->q1, buf);
871 					s = buf;
872 				}
873 				os = s;
874 				nopen = 0;
875 				do{
876 					/* skip mail box name if present */
877 					if(strncmp(s, mbox.name, strlen(mbox.name)) == 0)
878 						s += strlen(mbox.name);
879 					if(strstr(s, "body") != nil){
880 						/* strip any known extensions */
881 						for(i=0; exts[i].ext!=nil; i++){
882 							j = strlen(exts[i].ext);
883 							if(strlen(s)>j && strcmp(s+strlen(s)-j, exts[i].ext)==0){
884 								s[strlen(s)-j] = '\0';
885 								break;
886 							}
887 						}
888 						if(strlen(s)>5 && strcmp(s+strlen(s)-5, "/body")==0)
889 							s[strlen(s)-4] = '\0';	/* leave / in place */
890 					}
891 					nopen += mesgopen(&mbox, mbox.name, s, m, 0, nil);
892 					while(*s!=0 && *s++!='\n')
893 						;
894 				}while(*s);
895 				if(nopen == 0 && e->c1 == 'L')
896 					nopen += replytoaddr(w, m, e, os);
897 				if(nopen == 0)
898 					winwriteevent(w, e);
899 				free(buf);
900 				break;
901 
902 			case 'I':	/* modify away; we don't care */
903 			case 'D':
904 				mesgtagpost(m);
905 				/* fall through */
906 			case 'd':
907 			case 'i':
908 				break;
909 
910 			default:
911 				goto Unk;
912 			}
913 		}
914 	}
915 }
916 
917 void
918 mesgline(Message *m, char *header, char *value)
919 {
920 	if(strlen(value) > 0)
921 		Bprint(m->w->body, "%s: %s\n", header, value);
922 }
923 
924 int
925 isprintable(char *type)
926 {
927 	int i;
928 
929 	for(i=0; goodtypes[i]!=nil; i++)
930 		if(strcmp(type, goodtypes[i])==0)
931 			return 1;
932 	return 0;
933 }
934 
935 char*
936 ext(char *type)
937 {
938 	int i;
939 
940 	for(i=0; exts[i].type!=nil; i++)
941 		if(strcmp(type, exts[i].type)==0)
942 			return exts[i].ext;
943 	return "";
944 }
945 
946 void
947 mimedisplay(Message *m, char *name, char *rootdir, Window *w, int fileonly)
948 {
949 	char *dest;
950 
951 	if(strcmp(m->disposition, "file")==0 || strlen(m->filename)!=0){
952 		if(strlen(m->filename) == 0){
953 			dest = estrdup(m->name);
954 			dest[strlen(dest)-1] = '\0';
955 		}else
956 			dest = estrdup(m->filename);
957 		if(m->filename[0] != '/')
958 			dest = egrow(estrdup(home), "/", dest);
959 		Bprint(w->body, "\tcp %s%sbody%s %q\n", rootdir, name, ext(m->type), dest);
960 		free(dest);
961 	}else if(!fileonly)
962 		Bprint(w->body, "\tfile is %s%sbody%s\n", rootdir, name, ext(m->type));
963 }
964 
965 void
966 printheader(char *dir, Biobuf *b, char **okheaders)
967 {
968 	char *s;
969 	char *lines[100];
970 	int i, j, n;
971 
972 	s = readfile(dir, "header", nil);
973 	if(s == nil)
974 		return;
975 	n = getfields(s, lines, nelem(lines), 0, "\n");
976 	for(i=0; i<n; i++)
977 		for(j=0; okheaders[j]; j++)
978 			if(cistrncmp(lines[i], okheaders[j], strlen(okheaders[j])) == 0)
979 				Bprint(b, "%s\n", lines[i]);
980 	free(s);
981 }
982 
983 void
984 mesgload(Message *m, char *rootdir, char *file, Window *w)
985 {
986 	char *s, *subdir, *name, *dir;
987 	Message *mp, *thisone;
988 	int n;
989 
990 	dir = estrstrdup(rootdir, file);
991 
992 	if(strcmp(m->type, "message/rfc822") != 0){	/* suppress headers of envelopes */
993 		if(strlen(m->from) > 0){
994 			Bprint(w->body, "From: %s\n", m->from);
995 			mesgline(m, "Date", m->date);
996 			mesgline(m, "To", m->to);
997 			mesgline(m, "CC", m->cc);
998 			mesgline(m, "Subject", m->subject);
999 			printheader(dir, w->body, extraheaders);
1000 		}else{
1001 			printheader(dir, w->body, okheaders);
1002 			printheader(dir, w->body, extraheaders);
1003 		}
1004 		Bprint(w->body, "\n");
1005 	}
1006 
1007 	if(m->level == 1 && m->recursed == 0){
1008 		m->recursed = 1;
1009 		readmbox(m, rootdir, m->name);
1010 	}
1011 	if(m->head == nil){	/* single part message */
1012 		if(strcmp(m->type, "text")==0 || strncmp(m->type, "text/", 5)==0){
1013 			mimedisplay(m, m->name, rootdir, w, 1);
1014 			s = readbody(m->type, dir, &n);
1015 			winwritebody(w, s, n);
1016 			free(s);
1017 		}else
1018 			mimedisplay(m, m->name, rootdir, w, 0);
1019 	}else{
1020 		/* multi-part message, either multipart/* or message/rfc822 */
1021 		thisone = nil;
1022 		if(strcmp(m->type, "multipart/alternative") == 0){
1023 			thisone = m->head;	/* in case we can't find a good one */
1024 			for(mp=m->head; mp!=nil; mp=mp->next)
1025 				if(isprintable(mp->type)){
1026 					thisone = mp;
1027 					break;
1028 				}
1029 		}
1030 		for(mp=m->head; mp!=nil; mp=mp->next){
1031 			if(thisone!=nil && mp!=thisone)
1032 				continue;
1033 			subdir = estrstrdup(dir, mp->name);
1034 			name = estrstrdup(file, mp->name);
1035 			/* skip first element in name because it's already in window name */
1036 			if(mp != m->head)
1037 				Bprint(w->body, "\n===> %s (%s) [%s]\n", strchr(name, '/')+1, mp->type, mp->disposition);
1038 			if(strcmp(mp->type, "text")==0 || strncmp(mp->type, "text/", 5)==0){
1039 				mimedisplay(mp, name, rootdir, w, 1);
1040 				printheader(subdir, w->body, okheaders);
1041 				printheader(subdir, w->body, extraheaders);
1042 				winwritebody(w, "\n", 1);
1043 				s = readbody(mp->type, subdir, &n);
1044 				winwritebody(w, s, n);
1045 				free(s);
1046 			}else{
1047 				if(strncmp(mp->type, "multipart/", 10)==0 || strcmp(mp->type, "message/rfc822")==0){
1048 					mp->w = w;
1049 					mesgload(mp, rootdir, name, w);
1050 					mp->w = nil;
1051 				}else
1052 					mimedisplay(mp, name, rootdir, w, 0);
1053 			}
1054 			free(name);
1055 			free(subdir);
1056 		}
1057 	}
1058 	free(dir);
1059 }
1060 
1061 int
1062 tokenizec(char *str, char **args, int max, char *splitc)
1063 {
1064 	int na;
1065 	int intok = 0;
1066 
1067 	if(max <= 0)
1068 		return 0;
1069 	for(na=0; *str != '\0';str++){
1070 		if(strchr(splitc, *str) == nil){
1071 			if(intok)
1072 				continue;
1073 			args[na++] = str;
1074 			intok = 1;
1075 		}else{
1076 			/* it's a separator/skip character */
1077 			*str = '\0';
1078 			if(intok){
1079 				intok = 0;
1080 				if(na >= max)
1081 					break;
1082 			}
1083 		}
1084 	}
1085 	return na;
1086 }
1087 
1088 Message*
1089 mesglookup(Message *mbox, char *name, char *digest)
1090 {
1091 	int n;
1092 	Message *m;
1093 	char *t;
1094 
1095 	if(digest){
1096 		/* can find exactly */
1097 		for(m=mbox->head; m!=nil; m=m->next)
1098 			if(strcmp(digest, m->digest) == 0)
1099 				break;
1100 		return m;
1101 	}
1102 
1103 	n = strlen(name);
1104 	if(n == 0)
1105 		return nil;
1106 	if(name[n-1] == '/')
1107 		t = estrdup(name);
1108 	else
1109 		t = estrstrdup(name, "/");
1110 	for(m=mbox->head; m!=nil; m=m->next)
1111 		if(strcmp(t, m->name) == 0)
1112 			break;
1113 	free(t);
1114 	return m;
1115 }
1116 
1117 /*
1118  * Find plumb port, knowing type is text, given file name (by extension)
1119  */
1120 int
1121 plumbportbysuffix(char *file)
1122 {
1123 	char *suf;
1124 	int i, nsuf, nfile;
1125 
1126 	nfile = strlen(file);
1127 	for(i=0; ports[i].type!=nil; i++){
1128 		suf = ports[i].suffix;
1129 		nsuf = strlen(suf);
1130 		if(nfile > nsuf)
1131 			if(cistrncmp(file+nfile-nsuf, suf, nsuf) == 0)
1132 				return i;
1133 	}
1134 	return 0;
1135 }
1136 
1137 /*
1138  * Find plumb port using type and file name (by extension)
1139  */
1140 int
1141 plumbport(char *type, char *file)
1142 {
1143 	int i;
1144 
1145 	for(i=0; ports[i].type!=nil; i++)
1146 		if(strncmp(type, ports[i].type, strlen(ports[i].type)) == 0)
1147 			return i;
1148 	/* see if it's a text type */
1149 	for(i=0; goodtypes[i]!=nil; i++)
1150 		if(strncmp(type, goodtypes[i], strlen(goodtypes[i])) == 0)
1151 			return plumbportbysuffix(file);
1152 	return -1;
1153 }
1154 
1155 void
1156 plumb(Message *m, char *dir)
1157 {
1158 	int i;
1159 	char *port;
1160 	Plumbmsg *pm;
1161 
1162 	if(strlen(m->type) == 0)
1163 		return;
1164 	i = plumbport(m->type, m->filename);
1165 	if(i < 0)
1166 		fprint(2, "can't find destination for message subpart\n");
1167 	else{
1168 		port = ports[i].port;
1169 		pm = emalloc(sizeof(Plumbmsg));
1170 		pm->src = estrdup("Mail");
1171 		if(port)
1172 			pm->dst = estrdup(port);
1173 		else
1174 			pm->dst = nil;
1175 		pm->wdir = nil;
1176 		pm->type = estrdup("text");
1177 		pm->ndata = -1;
1178 		pm->data = estrstrdup(dir, "body");
1179 		pm->data = eappend(pm->data, "", ports[i].suffix);
1180 		if(plumbsend(plumbsendfd, pm) < 0)
1181 			fprint(2, "error writing plumb message: %r\n");
1182 		plumbfree(pm);
1183 	}
1184 }
1185 
1186 int
1187 mesgopen(Message *mbox, char *dir, char *s, Message *mesg, int plumbed, char *digest)
1188 {
1189 	char *t, *u, *v;
1190 	Message *m;
1191 	char *direlem[10];
1192 	int i, ndirelem, reuse;
1193 
1194 	/* find white-space-delimited first word */
1195 	for(t=s; *t!='\0' && !isspace(*t); t++)
1196 		;
1197 	u = emalloc(t-s+1);
1198 	memmove(u, s, t-s);
1199 	/* separate it on slashes */
1200 	ndirelem = tokenizec(u, direlem, nelem(direlem), "/");
1201 	if(ndirelem <= 0){
1202     Error:
1203 		free(u);
1204 		return 0;
1205 	}
1206 	if(plumbed){
1207 		write(wctlfd, "top", 3);
1208 		write(wctlfd, "current", 7);
1209 	}
1210 	/* open window for message */
1211 	m = mesglookup(mbox, direlem[0], digest);
1212 	if(m == nil)
1213 		goto Error;
1214 	if(mesg!=nil && m!=mesg)	/* string looked like subpart but isn't part of this message */
1215 		goto Error;
1216 	if(m->opened == 0){
1217 		if(m->w == nil){
1218 			reuse = 0;
1219 			m->w = newwindow();
1220 		}else{
1221 			reuse = 1;
1222 			/* re-use existing window */
1223 			if(winsetaddr(m->w, "0,$", 1)){
1224 				if(m->w->data < 0)
1225 					m->w->data = winopenfile(m->w, "data");
1226 				write(m->w->data, "", 0);
1227 			}
1228 		}
1229 		v = estrstrdup(mbox->name, m->name);
1230 		winname(m->w, v);
1231 		free(v);
1232 		if(!reuse){
1233 			if(m->deleted)
1234 				wintagwrite(m->w, "Q Reply all UnDelmesg Save ", 2+6+4+10+5);
1235 			else
1236 				wintagwrite(m->w, "Q Reply all Delmesg Save ", 2+6+4+8+5);
1237 		}
1238 		threadcreate(mesgctl, m, STACK);
1239 		winopenbody(m->w, OWRITE);
1240 		mesgload(m, dir, m->name, m->w);
1241 		winclosebody(m->w);
1242 		winclean(m->w);
1243 		m->opened = 1;
1244 		if(ndirelem == 1){
1245 			free(u);
1246 			return 1;
1247 		}
1248 	}
1249 	if(ndirelem == 1 && plumbport(m->type, m->filename) <= 0){
1250 		/* make sure dot is visible */
1251 		ctlprint(m->w->ctl, "show\n");
1252 		return 0;
1253 	}
1254 	/* walk to subpart */
1255 	dir = estrstrdup(dir, m->name);
1256 	for(i=1; i<ndirelem; i++){
1257 		m = mesglookup(m, direlem[i], digest);
1258 		if(m == nil)
1259 			break;
1260 		dir = egrow(dir, m->name, nil);
1261 	}
1262 	if(m != nil && plumbport(m->type, m->filename) > 0)
1263 		plumb(m, dir);
1264 	free(dir);
1265 	free(u);
1266 	return 1;
1267 }
1268 
1269 void
1270 rewritembox(Window *w, Message *mbox)
1271 {
1272 	Message *m, *next;
1273 	char *deletestr, *t;
1274 	int nopen;
1275 
1276 	deletestr = estrstrdup("delete ", fsname);
1277 
1278 	nopen = 0;
1279 	for(m=mbox->head; m!=nil; m=next){
1280 		next = m->next;
1281 		if(m->deleted == 0)
1282 			continue;
1283 		if(m->opened){
1284 			nopen++;
1285 			continue;
1286 		}
1287 		if(m->writebackdel){
1288 			/* messages deleted by plumb message are not removed again */
1289 			t = estrdup(m->name);
1290 			if(strlen(t) > 0)
1291 				t[strlen(t)-1] = '\0';
1292 			deletestr = egrow(deletestr, " ", t);
1293 		}
1294 		mesgmenudel(w, mbox, m);
1295 		mesgdel(mbox, m);
1296 	}
1297 	if(write(mbox->ctlfd, deletestr, strlen(deletestr)) < 0)
1298 		fprint(2, "Mail: warning: error removing mail message files: %r\n");
1299 	free(deletestr);
1300 	winselect(w, "0", 0);
1301 	if(nopen == 0)
1302 		winclean(w);
1303 	mbox->dirty = 0;
1304 }
1305 
1306 /* name is a full file name, but it might not belong to us */
1307 Message*
1308 mesglookupfile(Message *mbox, char *name, char *digest)
1309 {
1310 	int k, n;
1311 
1312 	k = strlen(name);
1313 	n = strlen(mbox->name);
1314 	if(k==0 || strncmp(name, mbox->name, n) != 0){
1315 //		fprint(2, "Mail: message %s not in this mailbox\n", name);
1316 		return nil;
1317 	}
1318 	return mesglookup(mbox, name+n, digest);
1319 }
1320