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