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