xref: /plan9/acme/mail/src/mail.c (revision 282e677fa45fb578cdb8bc2c412ac084c367776e)
1 #include <u.h>
2 #include <libc.h>
3 #include <bio.h>
4 #include <thread.h>
5 #include <plumb.h>
6 #include <ctype.h>
7 #include "dat.h"
8 
9 char	*maildir = "/mail/fs/";			/* mountpoint of mail file system */
10 char	*mailtermdir = "/mnt/term/mail/fs/";	/* alternate mountpoint */
11 char *mboxname = "mbox";			/* mailboxdir/mboxname is mail spool file */
12 char	*mailboxdir = nil;				/* nil == /mail/box/$user */
13 char *fsname;						/* filesystem for mailboxdir/mboxname is at maildir/fsname */
14 char	*user;
15 char	*outgoing;
16 
17 Window	*wbox;
18 Message	mbox;
19 Message	replies;
20 char		*home;
21 int		plumbsendfd;
22 int		plumbseemailfd;
23 int		plumbshowmailfd;
24 int		plumbsendmailfd;
25 Channel	*cplumb;
26 Channel	*cplumbshow;
27 Channel	*cplumbsend;
28 int		wctlfd;
29 void		mainctl(void*);
30 void		plumbproc(void*);
31 void		plumbshowproc(void*);
32 void		plumbsendproc(void*);
33 void		plumbthread(void);
34 void		plumbshowthread(void*);
35 void		plumbsendthread(void*);
36 
37 int			shortmenu;
38 
39 void
40 usage(void)
41 {
42 	fprint(2, "usage: Mail [-sS] [-o outgoing] [mailboxname [directoryname]]\n");
43 	threadexitsall("usage");
44 }
45 
46 void
47 removeupasfs(void)
48 {
49 	char buf[256];
50 
51 	if(strcmp(mboxname, "mbox") == 0)
52 		return;
53 	snprint(buf, sizeof buf, "close %s", mboxname);
54 	write(mbox.ctlfd, buf, strlen(buf));
55 }
56 
57 int
58 ismaildir(char *s)
59 {
60 	char buf[256];
61 	Dir *d;
62 	int ret;
63 
64 	snprint(buf, sizeof buf, "%s%s", maildir, s);
65 	d = dirstat(buf);
66 	if(d == nil)
67 		return 0;
68 	ret = d->qid.type & QTDIR;
69 	free(d);
70 	return ret;
71 }
72 
73 void
74 threadmain(int argc, char *argv[])
75 {
76 	char *s, *name;
77 	char err[ERRMAX], cmd[256];
78 	int i, newdir;
79 
80 	doquote = needsrcquote;
81 	quotefmtinstall();
82 
83 	/* open these early so we won't miss notification of new mail messages while we read mbox */
84 	plumbsendfd = plumbopen("send", OWRITE|OCEXEC);
85 	plumbseemailfd = plumbopen("seemail", OREAD|OCEXEC);
86 	plumbshowmailfd = plumbopen("showmail", OREAD|OCEXEC);
87 
88 	shortmenu = 0;
89 	ARGBEGIN{
90 	case 's':
91 		shortmenu = 1;
92 		break;
93 	case 'S':
94 		shortmenu = 2;
95 		break;
96 	case 'o':
97 		outgoing = EARGF(usage());
98 		break;
99 	case 'm':
100 		smprint(maildir, "%s/", EARGF(usage()));
101 		break;
102 	default:
103 		usage();
104 	}ARGEND
105 
106 	name = "mbox";
107 
108 	/* bind the terminal /mail/fs directory over the local one */
109 	if(access(maildir, 0)<0 && access(mailtermdir, 0)==0)
110 		bind(mailtermdir, maildir, MAFTER);
111 
112 	newdir = 1;
113 	if(argc > 0){
114 		i = strlen(argv[0]);
115 		if(argc>2 || i==0)
116 			usage();
117 		/* see if the name is that of an existing /mail/fs directory */
118 		if(argc==1 && strchr(argv[0], '/')==0 && ismaildir(argv[0])){
119 			name = argv[0];
120 			mboxname = eappend(estrdup(maildir), "", name);
121 			newdir = 0;
122 		}else{
123 			if(argv[0][i-1] == '/')
124 				argv[0][i-1] = '\0';
125 			s = strrchr(argv[0], '/');
126 			if(s == nil)
127 				mboxname = estrdup(argv[0]);
128 			else{
129 				*s++ = '\0';
130 				if(*s == '\0')
131 					usage();
132 				mailboxdir = argv[0];
133 				mboxname = estrdup(s);
134 			}
135 			if(argc > 1)
136 				name = argv[1];
137 			else
138 				name = mboxname;
139 		}
140 	}
141 
142 	user = getenv("user");
143 	if(user == nil)
144 		user = "none";
145 	if(mailboxdir == nil)
146 		mailboxdir = estrstrdup("/mail/box/", user);
147 	if(outgoing == nil)
148 		outgoing = estrstrdup(mailboxdir, "/outgoing");
149 
150 	s = estrstrdup(maildir, "ctl");
151 	mbox.ctlfd = open(s, ORDWR|OCEXEC);
152 	if(mbox.ctlfd < 0)
153 		error("Mail: can't open %s: %r\n", s);
154 
155 	fsname = estrdup(name);
156 	if(newdir && argc > 0){
157 		s = emalloc(5+strlen(mailboxdir)+strlen(mboxname)+strlen(name)+10+1);
158 		for(i=0; i<10; i++){
159 			sprint(s, "open %s/%s %s", mailboxdir, mboxname, fsname);
160 			if(write(mbox.ctlfd, s, strlen(s)) >= 0)
161 				break;
162 			err[0] = '\0';
163 			errstr(err, sizeof err);
164 			if(strstr(err, "mbox name in use") == nil)
165 				error("Mail: can't create directory %s for mail: %s\n", name, err);
166 			free(fsname);
167 			fsname = emalloc(strlen(name)+10);
168 			sprint(fsname, "%s-%d", name, i);
169 		}
170 		if(i == 10)
171 			error("Mail: can't open %s/%s: %r", mailboxdir, mboxname);
172 		free(s);
173 	}
174 
175 	s = estrstrdup(fsname, "/");
176 	mbox.name = estrstrdup(maildir, s);
177 	mbox.level= 0;
178 	readmbox(&mbox, maildir, s);
179 	home = getenv("home");
180 	if(home == nil)
181 		home = "/";
182 
183 	wbox = newwindow();
184 	winname(wbox, mbox.name);
185 	wintagwrite(wbox, "Put Mail Delmesg ", 3+1+4+1+7+1);
186 	threadcreate(mainctl, wbox, STACK);
187 	snprint(cmd, sizeof cmd, "Mail %s", name);
188 	winsetdump(wbox, "/acme/mail", cmd);
189 	mbox.w = wbox;
190 
191 	mesgmenu(wbox, &mbox);
192 	winclean(wbox);
193 
194 	wctlfd = open("/dev/wctl", OWRITE|OCEXEC);	/* for acme window */
195 	cplumb = chancreate(sizeof(Plumbmsg*), 0);
196 	cplumbshow = chancreate(sizeof(Plumbmsg*), 0);
197 	if(strcmp(name, "mbox") == 0){
198 		/*
199 		 * Avoid creating multiple windows to send mail by only accepting
200 		 * sendmail plumb messages if we're reading the main mailbox.
201 		 */
202 		plumbsendmailfd = plumbopen("sendmail", OREAD|OCEXEC);
203 		cplumbsend = chancreate(sizeof(Plumbmsg*), 0);
204 		proccreate(plumbsendproc, nil, STACK);
205 		threadcreate(plumbsendthread, nil, STACK);
206 	}
207 	/* start plumb reader as separate proc ... */
208 	proccreate(plumbproc, nil, STACK);
209 	proccreate(plumbshowproc, nil, STACK);
210 	threadcreate(plumbshowthread, nil, STACK);
211 	/* ... and use this thread to read the messages */
212 	plumbthread();
213 }
214 
215 void
216 plumbproc(void*)
217 {
218 	Plumbmsg *m;
219 
220 	threadsetname("plumbproc");
221 	for(;;){
222 		m = plumbrecv(plumbseemailfd);
223 		sendp(cplumb, m);
224 		if(m == nil)
225 			threadexits(nil);
226 	}
227 }
228 
229 void
230 plumbshowproc(void*)
231 {
232 	Plumbmsg *m;
233 
234 	threadsetname("plumbshowproc");
235 	for(;;){
236 		m = plumbrecv(plumbshowmailfd);
237 		sendp(cplumbshow, m);
238 		if(m == nil)
239 			threadexits(nil);
240 	}
241 }
242 
243 void
244 plumbsendproc(void*)
245 {
246 	Plumbmsg *m;
247 
248 	threadsetname("plumbsendproc");
249 	for(;;){
250 		m = plumbrecv(plumbsendmailfd);
251 		sendp(cplumbsend, m);
252 		if(m == nil)
253 			threadexits(nil);
254 	}
255 }
256 
257 void
258 newmesg(char *name, char *digest)
259 {
260 	Dir *d;
261 
262 	if(strncmp(name, mbox.name, strlen(mbox.name)) != 0)
263 		return;	/* message is about another mailbox */
264 	if(mesglookupfile(&mbox, name, digest) != nil)
265 		return;
266 	d = dirstat(name);
267 	if(d == nil)
268 		return;
269 	if(mesgadd(&mbox, mbox.name, d, digest))
270 		mesgmenunew(wbox, &mbox);
271 	free(d);
272 }
273 
274 void
275 showmesg(char *name, char *digest)
276 {
277 	char *n;
278 
279 	if(strncmp(name, mbox.name, strlen(mbox.name)) != 0)
280 		return;	/* message is about another mailbox */
281 	n = estrdup(name+strlen(mbox.name));
282 	if(n[strlen(n)-1] != '/')
283 		n = egrow(n, "/", nil);
284 	mesgopen(&mbox, mbox.name, name+strlen(mbox.name), nil, 1, digest);
285 	free(n);
286 }
287 
288 void
289 delmesg(char *name, char *digest, int dodel)
290 {
291 	Message *m;
292 
293 	m = mesglookupfile(&mbox, name, digest);
294 	if(m != nil){
295 		mesgmenumarkdel(wbox, &mbox, m, 0);
296 		if(dodel)
297 			m->writebackdel = 1;
298 	}
299 }
300 
301 void
302 plumbthread(void)
303 {
304 	Plumbmsg *m;
305 	Plumbattr *a;
306 	char *type, *digest;
307 
308 	threadsetname("plumbthread");
309 	while((m = recvp(cplumb)) != nil){
310 		a = m->attr;
311 		digest = plumblookup(a, "digest");
312 		type = plumblookup(a, "mailtype");
313 		if(type == nil)
314 			fprint(2, "Mail: plumb message with no mailtype attribute\n");
315 		else if(strcmp(type, "new") == 0)
316 			newmesg(m->data, digest);
317 		else if(strcmp(type, "delete") == 0)
318 			delmesg(m->data, digest, 0);
319 		else
320 			fprint(2, "Mail: unknown plumb attribute %s\n", type);
321 		plumbfree(m);
322 	}
323 	threadexits(nil);
324 }
325 
326 void
327 plumbshowthread(void*)
328 {
329 	Plumbmsg *m;
330 
331 	threadsetname("plumbshowthread");
332 	while((m = recvp(cplumbshow)) != nil){
333 		showmesg(m->data, plumblookup(m->attr, "digest"));
334 		plumbfree(m);
335 	}
336 	threadexits(nil);
337 }
338 
339 void
340 plumbsendthread(void*)
341 {
342 	Plumbmsg *m;
343 
344 	threadsetname("plumbsendthread");
345 	while((m = recvp(cplumbsend)) != nil){
346 		mkreply(nil, "Mail", m->data, m->attr, nil);
347 		plumbfree(m);
348 	}
349 	threadexits(nil);
350 }
351 
352 int
353 mboxcommand(Window *w, char *s)
354 {
355 	char *args[10], **targs;
356 	Message *m, *next;
357 	int ok, nargs, i, j;
358 	char buf[128];
359 
360 	nargs = tokenize(s, args, nelem(args));
361 	if(nargs == 0)
362 		return 0;
363 	if(strcmp(args[0], "Mail") == 0){
364 		if(nargs == 1)
365 			mkreply(nil, "Mail", "", nil, nil);
366 		else
367 			mkreply(nil, "Mail", args[1], nil, nil);
368 		return 1;
369 	}
370 	if(strcmp(s, "Del") == 0){
371 		if(mbox.dirty){
372 			mbox.dirty = 0;
373 			fprint(2, "mail: mailbox not written\n");
374 			return 1;
375 		}
376 		ok = 1;
377 		for(m=mbox.head; m!=nil; m=next){
378 			next = m->next;
379 			if(m->w){
380 				if(windel(m->w, 0))
381 					m->w = nil;
382 				else
383 					ok = 0;
384 			}
385 		}
386 		for(m=replies.head; m!=nil; m=next){
387 			next = m->next;
388 			if(m->w){
389 				if(windel(m->w, 0))
390 					m->w = nil;
391 				else
392 					ok = 0;
393 			}
394 		}
395 		if(ok){
396 			windel(w, 1);
397 			removeupasfs();
398 			threadexitsall(nil);
399 		}
400 		return 1;
401 	}
402 	if(strcmp(s, "Put") == 0){
403 		rewritembox(wbox, &mbox);
404 		return 1;
405 	}
406 	if(strcmp(s, "Delmesg") == 0){
407 		if(nargs > 1){
408 			for(i=1; i<nargs; i++){
409 				snprint(buf, sizeof buf, "%s%s", mbox.name, args[i]);
410 				delmesg(buf, nil, 1);
411 			}
412 		}
413 		s = winselection(w);
414 		if(s == nil)
415 			return 1;
416 		nargs = 1;
417 		for(i=0; s[i]; i++)
418 			if(s[i] == '\n')
419 				nargs++;
420 		targs = emalloc(nargs*sizeof(char*));	/* could be too many for a local array */
421 		nargs = getfields(s, targs, nargs, 1, "\n");
422 		for(i=0; i<nargs; i++){
423 			if(!isdigit(targs[i][0]))
424 				continue;
425 			j = atoi(targs[i]);	/* easy way to parse the number! */
426 			if(j == 0)
427 				continue;
428 			snprint(buf, sizeof buf, "%s%d", mbox.name, j);
429 			delmesg(buf, nil, 1);
430 		}
431 		free(s);
432 		free(targs);
433 		return 1;
434 	}
435 	return 0;
436 }
437 
438 void
439 mainctl(void *v)
440 {
441 	Window *w;
442 	Event *e, *e2, *eq, *ea;
443 	int na, nopen;
444 	char *s, *t, *buf;
445 
446 	w = v;
447 	proccreate(wineventproc, w, STACK);
448 
449 	for(;;){
450 		e = recvp(w->cevent);
451 		switch(e->c1){
452 		default:
453 		Unknown:
454 			print("unknown message %c%c\n", e->c1, e->c2);
455 			break;
456 
457 		case 'E':	/* write to body; can't affect us */
458 			break;
459 
460 		case 'F':	/* generated by our actions; ignore */
461 			break;
462 
463 		case 'K':	/* type away; we don't care */
464 			break;
465 
466 		case 'M':
467 			switch(e->c2){
468 			case 'x':
469 			case 'X':
470 				ea = nil;
471 				e2 = nil;
472 				if(e->flag & 2)
473 					e2 = recvp(w->cevent);
474 				if(e->flag & 8){
475 					ea = recvp(w->cevent);
476 					na = ea->nb;
477 					recvp(w->cevent);
478 				}else
479 					na = 0;
480 				s = e->b;
481 				/* if it's a known command, do it */
482 				if((e->flag&2) && e->nb==0)
483 					s = e2->b;
484 				if(na){
485 					t = emalloc(strlen(s)+1+na+1);
486 					sprint(t, "%s %s", s, ea->b);
487 					s = t;
488 				}
489 				/* if it's a long message, it can't be for us anyway */
490 				if(!mboxcommand(w, s))	/* send it back */
491 					winwriteevent(w, e);
492 				if(na)
493 					free(s);
494 				break;
495 
496 			case 'l':
497 			case 'L':
498 				buf = nil;
499 				eq = e;
500 				if(e->flag & 2){
501 					e2 = recvp(w->cevent);
502 					eq = e2;
503 				}
504 				s = eq->b;
505 				if(eq->q1>eq->q0 && eq->nb==0){
506 					buf = emalloc((eq->q1-eq->q0)*UTFmax+1);
507 					winread(w, eq->q0, eq->q1, buf);
508 					s = buf;
509 				}
510 				nopen = 0;
511 				do{
512 					/* skip 'deleted' string if present' */
513 					if(strncmp(s, deleted, strlen(deleted)) == 0)
514 						s += strlen(deleted);
515 					/* skip mail box name if present */
516 					if(strncmp(s, mbox.name, strlen(mbox.name)) == 0)
517 						s += strlen(mbox.name);
518 					nopen += mesgopen(&mbox, mbox.name, s, nil, 0, nil);
519 					while(*s!='\0' && *s++!='\n')
520 						;
521 				}while(*s);
522 				if(nopen == 0)	/* send it back */
523 					winwriteevent(w, e);
524 				free(buf);
525 				break;
526 
527 			case 'I':	/* modify away; we don't care */
528 			case 'D':
529 			case 'd':
530 			case 'i':
531 				break;
532 
533 			default:
534 				goto Unknown;
535 			}
536 		}
537 	}
538 }
539 
540