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