xref: /plan9-contrib/sys/src/cmd/upas/pop3/pop3.c (revision 9aba0e67f3fdf114973a71f5ffa0b6cd3cc40cfa)
1 #include "common.h"
2 #include <ctype.h>
3 #include <auth.h>
4 #include <libsec.h>
5 
6 typedef struct Cmd Cmd;
7 struct Cmd
8 {
9 	char *name;
10 	int needauth;
11 	int (*f)(char*);
12 };
13 
14 static void hello(void);
15 static int apopcmd(char*);
16 static int capacmd(char*);
17 static int delecmd(char*);
18 static int listcmd(char*);
19 static int noopcmd(char*);
20 static int passcmd(char*);
21 static int quitcmd(char*);
22 static int rsetcmd(char*);
23 static int retrcmd(char*);
24 static int statcmd(char*);
25 static int stlscmd(char*);
26 static int topcmd(char*);
27 static int synccmd(char*);
28 static int uidlcmd(char*);
29 static int usercmd(char*);
30 static char *nextarg(char*);
31 static int getcrnl(char*, int);
32 static int readmbox(char*);
33 static void sendcrnl(char*, ...);
34 static int senderr(char*, ...);
35 static int sendok(char*, ...);
36 #pragma varargck argpos sendcrnl 1
37 #pragma varargck argpos senderr 1
38 #pragma varargck argpos sendok 1
39 
40 Cmd cmdtab[] =
41 {
42 	"apop", 0, apopcmd,
43 	"capa", 0, capacmd,
44 	"dele", 1, delecmd,
45 	"list", 1, listcmd,
46 	"noop", 0, noopcmd,
47 	"pass", 0, passcmd,
48 	"quit", 0, quitcmd,
49 	"rset", 0, rsetcmd,
50 	"retr", 1, retrcmd,
51 	"stat", 1, statcmd,
52 	"stls", 0, stlscmd,
53 	"sync", 1, synccmd,
54 	"top", 1, topcmd,
55 	"uidl", 1, uidlcmd,
56 	"user", 0, usercmd,
57 	0, 0, 0,
58 };
59 
60 static Biobuf in;
61 static Biobuf out;
62 static int passwordinclear;
63 static int didtls;
64 
65 typedef struct Msg Msg;
66 struct Msg
67 {
68 	int upasnum;
69 	char digest[64];
70 	int bytes;
71 	int deleted;
72 };
73 
74 static int totalbytes;
75 static int totalmsgs;
76 static Msg *msg;
77 static int nmsg;
78 static int loggedin;
79 static int debug;
80 static uchar *tlscert;
81 static int ntlscert;
82 static char *peeraddr;
83 static char tmpaddr[64];
84 
85 void
86 usage(void)
87 {
88 	fprint(2, "usage: upas/pop3 [-a authmboxfile] [-d debugfile] [-p]\n");
89 	exits("usage");
90 }
91 
92 void
93 main(int argc, char **argv)
94 {
95 	int fd;
96 	char *arg, cmdbuf[1024];
97 	Cmd *c;
98 
99 	rfork(RFNAMEG);
100 	Binit(&in, 0, OREAD);
101 	Binit(&out, 1, OWRITE);
102 
103 	ARGBEGIN{
104 	case 'a':
105 		loggedin = 1;
106 		if(readmbox(EARGF(usage())) < 0)
107 			exits(nil);
108 		break;
109 	case 'd':
110 		debug++;
111 		if((fd = create(EARGF(usage()), OWRITE, 0666)) >= 0 && fd != 2){
112 			dup(fd, 2);
113 			close(fd);
114 		}
115 		break;
116 	case 'r':
117 		strecpy(tmpaddr, tmpaddr+sizeof tmpaddr, EARGF(usage()));
118 		if(arg = strchr(tmpaddr, '!'))
119 			*arg = '\0';
120 		peeraddr = tmpaddr;
121 		break;
122 	case 't':
123 		tlscert = readcert(EARGF(usage()), &ntlscert);
124 		if(tlscert == nil){
125 			senderr("cannot read TLS certificate: %r");
126 			exits(nil);
127 		}
128 		break;
129 	case 'p':
130 		passwordinclear = 1;
131 		break;
132 	}ARGEND
133 
134 	/* do before TLS */
135 	if(peeraddr == nil)
136 		peeraddr = remoteaddr(0,0);
137 
138 	hello();
139 
140 	while(Bflush(&out), getcrnl(cmdbuf, sizeof cmdbuf) > 0){
141 		arg = nextarg(cmdbuf);
142 		for(c=cmdtab; c->name; c++)
143 			if(cistrcmp(c->name, cmdbuf) == 0)
144 				break;
145 		if(c->name == 0){
146 			senderr("unknown command %s", cmdbuf);
147 			continue;
148 		}
149 		if(c->needauth && !loggedin){
150 			senderr("%s requires authentication", cmdbuf);
151 			continue;
152 		}
153 		(*c->f)(arg);
154 	}
155 	exits(nil);
156 }
157 
158 /* sort directories in increasing message number order */
159 static int
160 dircmp(void *a, void *b)
161 {
162 	return atoi(((Dir*)a)->name) - atoi(((Dir*)b)->name);
163 }
164 
165 static int
166 readmbox(char *box)
167 {
168 	int fd, i, n, nd, lines, pid;
169 	char buf[100], err[ERRMAX];
170 	char *p;
171 	Biobuf *b;
172 	Dir *d, *draw;
173 	Msg *m;
174 	Waitmsg *w;
175 
176 	unmount(nil, "/mail/fs");
177 	switch(pid = fork()){
178 	case -1:
179 		return senderr("can't fork to start upas/fs");
180 
181 	case 0:
182 		close(0);
183 		close(1);
184 		open("/dev/null", OREAD);
185 		open("/dev/null", OWRITE);
186 		execl("/bin/upas/fs", "upas/fs", "-np", "-f", box, nil);
187 		snprint(err, sizeof err, "upas/fs: %r");
188 		_exits(err);
189 		break;
190 
191 	default:
192 		break;
193 	}
194 
195 	if((w = wait()) == nil || w->pid != pid || w->msg[0] != '\0'){
196 		if(w && w->pid==pid)
197 			return senderr("%s", w->msg);
198 		else
199 			return senderr("can't initialize upas/fs");
200 	}
201 	free(w);
202 
203 	if(chdir("/mail/fs/mbox") < 0)
204 		return senderr("can't initialize upas/fs: %r");
205 
206 	if((fd = open(".", OREAD)) < 0)
207 		return senderr("cannot open /mail/fs/mbox: %r");
208 	nd = dirreadall(fd, &d);
209 	close(fd);
210 	if(nd < 0)
211 		return senderr("cannot read from /mail/fs/mbox: %r");
212 
213 	msg = mallocz(sizeof(Msg)*nd, 1);
214 	if(msg == nil)
215 		return senderr("out of memory");
216 
217 	if(nd == 0)
218 		return 0;
219 	qsort(d, nd, sizeof(d[0]), dircmp);
220 
221 	for(i=0; i<nd; i++){
222 		m = &msg[nmsg];
223 		m->upasnum = atoi(d[i].name);
224 		sprint(buf, "%d/digest", m->upasnum);
225 		if((fd = open(buf, OREAD)) < 0)
226 			continue;
227 		n = readn(fd, m->digest, sizeof m->digest - 1);
228 		close(fd);
229 		if(n < 0)
230 			continue;
231 		m->digest[n] = '\0';
232 
233 		/*
234 		 * We need the number of message lines so that we
235 		 * can adjust the byte count to include \r's.
236 		 * Upas/fs gives us the number of lines in the raw body
237 		 * in the lines file, but we have to count rawheader ourselves.
238 		 * There is one blank line between raw header and raw body.
239 		 */
240 		sprint(buf, "%d/rawheader", m->upasnum);
241 		if((b = Bopen(buf, OREAD)) == nil)
242 			continue;
243 		lines = 0;
244 		for(;;){
245 			p = Brdline(b, '\n');
246 			if(p == nil){
247 				if((n = Blinelen(b)) == 0)
248 					break;
249 				Bseek(b, n, 1);
250 			}else
251 				lines++;
252 		}
253 		Bterm(b);
254 		lines++;
255 		sprint(buf, "%d/lines", m->upasnum);
256 		if((fd = open(buf, OREAD)) < 0)
257 			continue;
258 		n = readn(fd, buf, sizeof buf - 1);
259 		close(fd);
260 		if(n < 0)
261 			continue;
262 		buf[n] = '\0';
263 		lines += atoi(buf);
264 
265 		sprint(buf, "%d/raw", m->upasnum);
266 		if((draw = dirstat(buf)) == nil)
267 			continue;
268 		m->bytes = lines+draw->length;
269 		free(draw);
270 		nmsg++;
271 		totalmsgs++;
272 		totalbytes += m->bytes;
273 	}
274 	return 0;
275 }
276 
277 /*
278  *  get a line that ends in crnl or cr, turn terminating crnl into a nl
279  *
280  *  return 0 on EOF
281  */
282 static int
283 getcrnl(char *buf, int n)
284 {
285 	int c;
286 	char *ep;
287 	char *bp;
288 	Biobuf *fp = &in;
289 
290 	Bflush(&out);
291 
292 	bp = buf;
293 	ep = bp + n - 1;
294 	while(bp != ep){
295 		c = Bgetc(fp);
296 		if(debug) {
297 			seek(2, 0, 2);
298 			fprint(2, "%c", c);
299 		}
300 		switch(c){
301 		case -1:
302 			*bp = 0;
303 			if(bp==buf)
304 				return 0;
305 			else
306 				return bp-buf;
307 		case '\r':
308 			c = Bgetc(fp);
309 			if(c == '\n'){
310 				if(debug) {
311 					seek(2, 0, 2);
312 					fprint(2, "%c", c);
313 				}
314 				*bp = 0;
315 				return bp-buf;
316 			}
317 			Bungetc(fp);
318 			c = '\r';
319 			break;
320 		case '\n':
321 			*bp = 0;
322 			return bp-buf;
323 		}
324 		*bp++ = c;
325 	}
326 	*bp = 0;
327 	return bp-buf;
328 }
329 
330 static void
331 sendcrnl(char *fmt, ...)
332 {
333 	char buf[1024];
334 	va_list arg;
335 
336 	va_start(arg, fmt);
337 	vseprint(buf, buf+sizeof(buf), fmt, arg);
338 	va_end(arg);
339 	if(debug)
340 		fprint(2, "-> %s\n", buf);
341 	Bprint(&out, "%s\r\n", buf);
342 }
343 
344 static int
345 senderr(char *fmt, ...)
346 {
347 	char buf[1024];
348 	va_list arg;
349 
350 	va_start(arg, fmt);
351 	vseprint(buf, buf+sizeof(buf), fmt, arg);
352 	va_end(arg);
353 	if(debug)
354 		fprint(2, "-> -ERR %s\n", buf);
355 	Bprint(&out, "-ERR %s\r\n", buf);
356 	return -1;
357 }
358 
359 static int
360 sendok(char *fmt, ...)
361 {
362 	char buf[1024];
363 	va_list arg;
364 
365 	va_start(arg, fmt);
366 	vseprint(buf, buf+sizeof(buf), fmt, arg);
367 	va_end(arg);
368 	if(*buf){
369 		if(debug)
370 			fprint(2, "-> +OK %s\n", buf);
371 		Bprint(&out, "+OK %s\r\n", buf);
372 	} else {
373 		if(debug)
374 			fprint(2, "-> +OK\n");
375 		Bprint(&out, "+OK\r\n");
376 	}
377 	return 0;
378 }
379 
380 static int
381 capacmd(char*)
382 {
383 	sendok("");
384 	sendcrnl("TOP");
385 	if(passwordinclear || didtls)
386 		sendcrnl("USER");
387 	sendcrnl("PIPELINING");
388 	sendcrnl("UIDL");
389 	sendcrnl("STLS");
390 	sendcrnl(".");
391 	return 0;
392 }
393 
394 static int
395 delecmd(char *arg)
396 {
397 	int n;
398 
399 	if(*arg==0)
400 		return senderr("DELE requires a message number");
401 
402 	n = atoi(arg)-1;
403 	if(n < 0 || n >= nmsg || msg[n].deleted)
404 		return senderr("no such message");
405 
406 	msg[n].deleted = 1;
407 	totalmsgs--;
408 	totalbytes -= msg[n].bytes;
409 	sendok("message %d deleted", n+1);
410 	return 0;
411 }
412 
413 static int
414 listcmd(char *arg)
415 {
416 	int i, n;
417 
418 	if(*arg == 0){
419 		sendok("+%d message%s (%d octets)", totalmsgs, totalmsgs==1 ? "":"s", totalbytes);
420 		for(i=0; i<nmsg; i++){
421 			if(msg[i].deleted)
422 				continue;
423 			sendcrnl("%d %d", i+1, msg[i].bytes);
424 		}
425 		sendcrnl(".");
426 	}else{
427 		n = atoi(arg)-1;
428 		if(n < 0 || n >= nmsg || msg[n].deleted)
429 			return senderr("no such message");
430 		sendok("%d %d", n+1, msg[n].bytes);
431 	}
432 	return 0;
433 }
434 
435 static int
436 noopcmd(char *arg)
437 {
438 	USED(arg);
439 	sendok("");
440 	return 0;
441 }
442 
443 static void
444 _synccmd(char*)
445 {
446 	int i, fd;
447 	char *s;
448 	Fmt f;
449 
450 	if(!loggedin){
451 		sendok("");
452 		return;
453 	}
454 
455 	fmtstrinit(&f);
456 	fmtprint(&f, "delete mbox");
457 	for(i=0; i<nmsg; i++)
458 		if(msg[i].deleted)
459 			fmtprint(&f, " %d", msg[i].upasnum);
460 	s = fmtstrflush(&f);
461 	if(strcmp(s, "delete mbox") != 0){	/* must have something to delete */
462 		if((fd = open("../ctl", OWRITE)) < 0){
463 			senderr("open ctl to delete messages: %r");
464 			return;
465 		}
466 		if(write(fd, s, strlen(s)) < 0){
467 			senderr("error deleting messages: %r");
468 			return;
469 		}
470 	}
471 	sendok("");
472 }
473 
474 static int
475 synccmd(char*)
476 {
477 	_synccmd(nil);
478 	return 0;
479 }
480 
481 static int
482 quitcmd(char*)
483 {
484 	synccmd(nil);
485 	exits(nil);
486 	return 0;
487 }
488 
489 static int
490 retrcmd(char *arg)
491 {
492 	int n;
493 	Biobuf *b;
494 	char buf[40], *p;
495 
496 	if(*arg == 0)
497 		return senderr("RETR requires a message number");
498 	n = atoi(arg)-1;
499 	if(n < 0 || n >= nmsg || msg[n].deleted)
500 		return senderr("no such message");
501 	snprint(buf, sizeof buf, "%d/raw", msg[n].upasnum);
502 	if((b = Bopen(buf, OREAD)) == nil)
503 		return senderr("message disappeared");
504 	sendok("");
505 	while((p = Brdstr(b, '\n', 1)) != nil){
506 		if(p[0]=='.')
507 			Bwrite(&out, ".", 1);
508 		Bwrite(&out, p, strlen(p));
509 		Bwrite(&out, "\r\n", 2);
510 		free(p);
511 	}
512 	Bterm(b);
513 	sendcrnl(".");
514 	return 0;
515 }
516 
517 static int
518 rsetcmd(char*)
519 {
520 	int i;
521 
522 	for(i=0; i<nmsg; i++){
523 		if(msg[i].deleted){
524 			msg[i].deleted = 0;
525 			totalmsgs++;
526 			totalbytes += msg[i].bytes;
527 		}
528 	}
529 	return sendok("");
530 }
531 
532 static int
533 statcmd(char*)
534 {
535 	return sendok("%d %d", totalmsgs, totalbytes);
536 }
537 
538 static int
539 trace(char *fmt, ...)
540 {
541 	va_list arg;
542 	int n;
543 
544 	va_start(arg, fmt);
545 	n = vfprint(2, fmt, arg);
546 	va_end(arg);
547 	return n;
548 }
549 
550 static int
551 stlscmd(char*)
552 {
553 	int fd;
554 	TLSconn conn;
555 
556 	if(didtls)
557 		return senderr("tls already started");
558 	if(!tlscert)
559 		return senderr("don't have any tls credentials");
560 	sendok("");
561 	Bflush(&out);
562 
563 	memset(&conn, 0, sizeof conn);
564 	conn.cert = tlscert;
565 	conn.certlen = ntlscert;
566 	if(debug)
567 		conn.trace = trace;
568 	fd = tlsServer(0, &conn);
569 	if(fd < 0)
570 		sysfatal("tlsServer: %r");
571 	dup(fd, 0);
572 	dup(fd, 1);
573 	close(fd);
574 	Binit(&in, 0, OREAD);
575 	Binit(&out, 1, OWRITE);
576 	didtls = 1;
577 	return 0;
578 }
579 
580 static int
581 topcmd(char *arg)
582 {
583 	int done, i, lines, n;
584 	char buf[40], *p;
585 	Biobuf *b;
586 
587 	if(*arg == 0)
588 		return senderr("TOP requires a message number");
589 	n = atoi(arg)-1;
590 	if(n < 0 || n >= nmsg || msg[n].deleted)
591 		return senderr("no such message");
592 	arg = nextarg(arg);
593 	if(*arg == 0)
594 		return senderr("TOP requires a line count");
595 	lines = atoi(arg);
596 	if(lines < 0)
597 		return senderr("bad args to TOP");
598 	snprint(buf, sizeof buf, "%d/raw", msg[n].upasnum);
599 	if((b = Bopen(buf, OREAD)) == nil)
600 		return senderr("message disappeared");
601 	sendok("");
602 	while(p = Brdstr(b, '\n', 1)){
603 		if(p[0]=='.')
604 			Bputc(&out, '.');
605 		Bwrite(&out, p, strlen(p));
606 		Bwrite(&out, "\r\n", 2);
607 		done = p[0]=='\0';
608 		free(p);
609 		if(done)
610 			break;
611 	}
612 	for(i=0; i<lines; i++){
613 		p = Brdstr(b, '\n', 1);
614 		if(p == nil)
615 			break;
616 		if(p[0]=='.')
617 			Bwrite(&out, ".", 1);
618 		Bwrite(&out, p, strlen(p));
619 		Bwrite(&out, "\r\n", 2);
620 		free(p);
621 	}
622 	sendcrnl(".");
623 	Bterm(b);
624 	return 0;
625 }
626 
627 static int
628 uidlcmd(char *arg)
629 {
630 	int n;
631 
632 	if(*arg==0){
633 		sendok("");
634 		for(n=0; n<nmsg; n++){
635 			if(msg[n].deleted)
636 				continue;
637 			sendcrnl("%d %s", n+1, msg[n].digest);
638 		}
639 		sendcrnl(".");
640 	}else{
641 		n = atoi(arg)-1;
642 		if(n < 0 || n >= nmsg || msg[n].deleted)
643 			return senderr("no such message");
644 		sendok("%d %s", n+1, msg[n].digest);
645 	}
646 	return 0;
647 }
648 
649 static char*
650 nextarg(char *p)
651 {
652 	while(*p && *p != ' ' && *p != '\t')
653 		p++;
654 	while(*p == ' ' || *p == '\t')
655 		*p++ = 0;
656 	return p;
657 }
658 
659 /*
660  * authentication
661  */
662 Chalstate *chs;
663 char user[256];
664 char box[256];
665 char cbox[256];
666 
667 static void
668 hello(void)
669 {
670 	fmtinstall('H', encodefmt);
671 	if((chs = auth_challenge("proto=apop role=server")) == nil){
672 		senderr("auth server not responding, try later");
673 		exits(nil);
674 	}
675 
676 	sendok("POP3 server ready %s", chs->chal);
677 }
678 
679 static int
680 setuser(char *arg)
681 {
682 	char *p;
683 
684 	strcpy(box, "/mail/box/");
685 	strecpy(box+strlen(box), box+sizeof box-7, arg);
686 	strcpy(cbox, box);
687 	cleanname(cbox);
688 	if(strcmp(cbox, box) != 0)
689 		return senderr("bad mailbox name");
690 	strcat(box, "/mbox");
691 
692 	strecpy(user, user+sizeof user, arg);
693 	if(p = strchr(user, '/'))
694 		*p = '\0';
695 	return 0;
696 }
697 
698 static int
699 usercmd(char *arg)
700 {
701 	if(loggedin)
702 		return senderr("already authenticated");
703 	if(*arg == 0)
704 		return senderr("USER requires argument");
705 	if(setuser(arg) < 0)
706 		return -1;
707 	return sendok("");
708 }
709 
710 static void
711 enableaddr(void)
712 {
713 	int fd;
714 	char buf[64];
715 
716 	/* hide the peer IP address under a rock in the ratifier FS */
717 	if(peeraddr == 0 || *peeraddr == 0)
718 		return;
719 
720 	sprint(buf, "/mail/ratify/trusted/%s#32", peeraddr);
721 
722 	/*
723 	 * if the address is already there and the user owns it,
724 	 * remove it and recreate it to give him a new time quanta.
725 	 */
726 	if(access(buf, 0) >= 0  && remove(buf) < 0)
727 		return;
728 
729 	fd = create(buf, OREAD, 0666);
730 	if(fd >= 0){
731 		close(fd);
732 //		syslog(0, "pop3", "ratified %s", peeraddr);
733 	}
734 }
735 
736 static int
737 dologin(char *response)
738 {
739 	AuthInfo *ai;
740 	static int tries;
741 
742 	chs->user = user;
743 	chs->resp = response;
744 	chs->nresp = strlen(response);
745 	if((ai = auth_response(chs)) == nil){
746 		if(tries++ >= 5){
747 			senderr("authentication failed: %r; server exiting");
748 			exits(nil);
749 		}
750 		return senderr("authentication failed");
751 	}
752 
753 	if(auth_chuid(ai, nil) < 0){
754 		senderr("chuid failed: %r; server exiting");
755 		exits(nil);
756 	}
757 	auth_freeAI(ai);
758 	auth_freechal(chs);
759 	chs = nil;
760 
761 	loggedin = 1;
762 	if(newns(user, 0) < 0){
763 		senderr("newns failed: %r; server exiting");
764 		exits(nil);
765 	}
766 	syslog(0, "pop3", "user %s logged in", user);
767 	enableaddr();
768 	if(readmbox(box) < 0)
769 		exits(nil);
770 	return sendok("mailbox is %s", box);
771 }
772 
773 static int
774 passcmd(char *arg)
775 {
776 	DigestState *s;
777 	uchar digest[MD5dlen];
778 	char response[2*MD5dlen+1];
779 
780 	if(passwordinclear==0 && didtls==0)
781 		return senderr("password in the clear disallowed");
782 
783 	/* use password to encode challenge */
784 	if((chs = auth_challenge("proto=apop role=server")) == nil)
785 		return senderr("couldn't get apop challenge");
786 
787 	// hash challenge with secret and convert to ascii
788 	s = md5((uchar*)chs->chal, chs->nchal, 0, 0);
789 	md5((uchar*)arg, strlen(arg), digest, s);
790 	snprint(response, sizeof response, "%.*H", MD5dlen, digest);
791 	return dologin(response);
792 }
793 
794 static int
795 apopcmd(char *arg)
796 {
797 	char *resp;
798 
799 	resp = nextarg(arg);
800 	if(setuser(arg) < 0)
801 		return -1;
802 	return dologin(resp);
803 }
804 
805