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