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