xref: /plan9-contrib/sys/src/cmd/upas/smtp/smtp.c (revision 219b2ee8daee37f4aad58d63f21287faa8e4ffdc)
1 #include "common.h"
2 #include "smtp.h"
3 #include <ctype.h>
4 
5 char*	connect(char*);
6 char*	hello(char*);
7 char*	mailfrom(char*);
8 char*	rcptto(char*);
9 char*	data(String*, int);
10 void	quit(void);
11 int	getreply(void);
12 void	addhostdom(String*, char*);
13 String*	bangtoat(char*);
14 void	convertheader(String*);
15 void	printheader(void);
16 char*	domainify(char*, char*);
17 void	putcrnl(char*, int);
18 char*	getcrnl(String*);
19 void	printdate(Node*);
20 char	*rewritezone(char *);
21 int	mxdial(char*, int*, char*);
22 int	dBprint(char*, ...);
23 
24 #define Retry "Temporary Failure, Retry"
25 #define Giveup "Permanent Failure"
26 
27 int	debug;		/* true if we're debugging */
28 String	*reply;		/* last reply */
29 String	*toline;
30 char	*sender;	/* who to bounce message to */
31 int	last = 'n';	/* last character sent by putcrnl() */
32 int	filter;
33 int	unix;
34 int	gateway;	/* true if we are traversing a mail gateway */
35 char	ddomain[1024];	/* domain name of destination machine */
36 char	*gdomain;	/* domain name of gateway */
37 char	*uneaten;	/* first character after rfc822 headers */
38 char	hostdomain[256];
39 Biobuf	bin;
40 Biobuf	bout;
41 Biobuf	berr;
42 
43 void
44 usage(void)
45 {
46 	fprint(2, "usage: smtp [-du] [-hhost] [.domain] net!host[!service] sender rcpt-list\n");
47 	exits(Giveup);
48 }
49 
50 void
51 timeout(void *x, char *msg)
52 {
53 	USED(x);
54 	if(strstr(msg, "alarm")){
55 		fprint(2, "smtp timeout: no retries");
56 		exits(Giveup);
57 	}
58 	noted(NDFLT);
59 }
60 
61 void
62 main(int argc, char **argv)
63 {
64 	char *domain;
65 	char *host;
66 	String *from;
67 	String *fromm;
68 	char *addr;
69 	char *rv;
70 
71 	reply = s_new();
72 	unix = 0;
73 	host = 0;
74 	ARGBEGIN{
75 	case 'f':
76 		filter = 1;
77 		break;
78 	case 'd':
79 		debug = 1;
80 		break;
81 	case 'g':
82 		gdomain = ARGF();
83 		break;
84 	case 'u':
85 		unix = 1;
86 		break;
87 	case 'h':
88 		host = ARGF();
89 		break;
90 	default:
91 		usage();
92 		break;
93 	}ARGEND;
94 
95 	Binit(&berr, 2, OWRITE);
96 
97 	/*
98 	 *  get domain and add to host name
99 	 */
100 	domain = csquery("soa", "", "dom");
101 	if(*argv && **argv=='.')
102 		domain = *argv++;
103 	if(host == 0)
104 		host = sysname_read();
105 	strcpy(hostdomain, domainify(host, domain));
106 
107 	/*
108 	 *  get destination address
109 	 */
110 	if(*argv == 0)
111 		usage();
112 	addr = *argv++;
113 
114 	/*
115 	 *  get sender's machine.
116 	 *  get sender in internet style.  domainify if necessary.
117 	 */
118 	if(*argv == 0)
119 		usage();
120 	sender = *argv++;
121 	fromm = s_copy(sender);
122 	rv = strrchr(s_to_c(fromm), '!');
123 	if(rv)
124 		*rv = 0;
125 	else
126 		*s_to_c(fromm) = 0;
127 	from = bangtoat(sender);
128 
129 	/*
130 	 *  send the mail
131 	 */
132 	if(filter){
133 		Binit(&bin, 0, OREAD);
134 		Binit(&bout, 1, OWRITE);
135 	} else {
136 		/* 10 minutes to get through the initial handshake */
137 		notify(timeout);
138 		alarm(10*60*1000);
139 
140 		if((rv = connect(addr)) != 0)
141 			exits(rv);
142 		if((rv = hello(hostdomain)) != 0)
143 			goto error;
144 		if((rv = mailfrom(s_to_c(from))) != 0)
145 			goto error;
146 		while(*argv)
147 			if((rv = rcptto(*argv++))!=0)
148 				goto error;
149 
150 		alarm(0);
151 	}
152 	rv = data(from, unix);
153 	if(rv != 0)
154 		goto error;
155 	if(!filter)
156 		quit();
157 	exits("");
158 error:
159 	fprint(2, "%s\n", s_to_c(reply));
160 	exits(rv);
161 }
162 
163 /*
164  *  connect to the remote host
165  */
166 char *
167 connect(char* net)
168 {
169 	char *addr;
170 	char buf[256];
171 	int fd;
172 
173 	addr = netmkaddr(net, 0, "smtp");
174 
175 	/* try connecting to destination or any of it's mail routers */
176 	fd = mxdial(addr, &gateway, ddomain);
177 
178 	/* try our mail gateway */
179 	if(fd < 0 && gdomain){
180 		gateway = 1;
181 		fd = dial(netmkaddr(gdomain, 0, "smtp"), 0, 0, 0);
182 	}
183 
184 	if(fd < 0){
185 		errstr(buf);
186 		Bprint(&berr, "smtp: %s %s\n", buf, addr);
187 		if(strstr(buf, "illegal") || strstr(buf, "rejected"))
188 			return Giveup;
189 		else
190 			return Retry;
191 	}
192 	Binit(&bin, fd, OREAD);
193 	fd = dup(fd, -1);
194 	Binit(&bout, fd, OWRITE);
195 	return 0;
196 }
197 
198 /*
199  *  exchange names with remote host
200  */
201 char *
202 hello(char *me)
203 {
204 	switch(getreply()){
205 	case 2:
206 		break;
207 	case 5:
208 		return Giveup;
209 	default:
210 		return Retry;
211 	}
212 	dBprint("HELO %s\r\n", me);
213 	switch(getreply()){
214 	case 2:
215 		break;
216 	case 5:
217 		return Giveup;
218 	default:
219 		return Retry;
220 	}
221 	return 0;
222 }
223 
224 /*
225  *  report sender to remote
226  */
227 char *
228 mailfrom(char *from)
229 {
230 	if(strchr(from, '@'))
231 		dBprint("MAIL FROM:<%s>\r\n", from);
232 	else
233 		dBprint("MAIL FROM:<%s@%s>\r\n", from, hostdomain);
234 	switch(getreply()){
235 	case 2:
236 		break;
237 	case 5:
238 		return Giveup;
239 	default:
240 		return Retry;
241 	}
242 	return 0;
243 }
244 
245 /*
246  *  report a recipient to remote
247  */
248 char *
249 rcptto(char *to)
250 {
251 	String *s;
252 
253 	s = bangtoat(to);
254 	if(toline == 0){
255 		toline = s_new();
256 		s_append(toline, "To: ");
257 	} else
258 		s_append(toline, ", ");
259 	s_append(toline, s_to_c(s));
260 	if(strchr(s_to_c(s), '@'))
261 		dBprint("RCPT TO:<%s>\r\n", s_to_c(s));
262 	else{
263 		s_append(toline, "@");
264 		s_append(toline, ddomain);
265 		dBprint("RCPT TO:<%s@%s>\r\n", s_to_c(s), ddomain);
266 	}
267 	switch(getreply()){
268 	case 2:
269 		break;
270 	case 5:
271 		return Giveup;
272 	default:
273 		return Retry;
274 	}
275 	return 0;
276 }
277 
278 /*
279  *  send the damn thing
280  */
281 char *
282 data(String *from, int unix)
283 {
284 	char buf[16*1024];
285 	int i, n;
286 	int eof;
287 	static char errmsg[ERRLEN];
288 
289 	/*
290 	 *  input the first 16k bytes.  The header had better fit.
291 	 */
292 	eof = 0;
293 	for(n = 0; n < sizeof(buf) - 1; n += i){
294 		i = read(0, buf+n, sizeof(buf)-1-n);
295 		if(i <= 0){
296 			eof = 1;
297 			break;
298 		}
299 	}
300 	buf[n] = 0;
301 
302 	/*
303 	 *  parse the header, turn all addresses into @ format
304 	 */
305 	yyinit(buf);
306 	if(!unix){
307 		yyparse();
308 		convertheader(from);
309 	}
310 
311 	/*
312 	 *  print message observing '.' escapes and using \r\n for \n
313 	 */
314 	if(!filter){
315 		dBprint("DATA\r\n");
316 		switch(getreply()){
317 		case 3:
318 			break;
319 		case 5:
320 			return Giveup;
321 		default:
322 			return Retry;
323 		}
324 	}
325 
326 	/*
327 	 *  send header.  add a sender and a date if there
328 	 *  isn't one
329 	 */
330 	uneaten = buf;
331 	if(!unix){
332 		if(originator==0 && usender)
333 			Bprint(&bout, "From: %s\r\n", s_to_c(from));
334 		if(destination == 0 && toline)
335 			Bprint(&bout, "%s\r\n", s_to_c(toline));
336 		if(date==0 && udate)
337 			printdate(udate);
338 		if (usys)
339 			uneaten = usys->end + 1;
340 		printheader();
341 		if (*uneaten != '\n')
342 			putcrnl("\n", 1);
343 	}
344 
345 	/*
346 	 *  send body
347 	 */
348 	putcrnl(uneaten, buf+n - uneaten);
349 	if(eof == 0)
350 		for(;;){
351 			n = read(0, buf, sizeof(buf));
352 			if(n < 0){
353 				errstr(errmsg);
354 				return errmsg;
355 			}
356 			if(n == 0)
357 				break;
358 			putcrnl(buf, n);
359 		}
360 	if(!filter){
361 		if(last != '\n')
362 			dBprint("\r\n.\r\n");
363 		else
364 			dBprint(".\r\n");
365 		switch(getreply()){
366 		case 2:
367 			break;
368 		case 5:
369 			return Retry;
370 		default:
371 			return Giveup;
372 		}
373 	}
374 	return 0;
375 }
376 
377 /*
378  *  we're leaving
379  */
380 void
381 quit(void)
382 {
383 	dBprint("QUIT\r\n");
384 	getreply();
385 }
386 
387 /*
388  *  read a reply into a string, return the reply code
389  */
390 int
391 getreply(void)
392 {
393 	char *line;
394 	int rv;
395 
396 	reply = s_reset(reply);
397 	for(;;){
398 		line = getcrnl(reply);
399 		if(line == 0)
400 			return -1;
401 		if(!isdigit(line[0]) || !isdigit(line[1]) || !isdigit(line[2]))
402 			return -1;
403 		if(line[3] != '-')
404 			break;
405 	}
406 	rv = atoi(line)/100;
407 	return rv;
408 }
409 
410 /*
411  *	Convert from `bang' to `source routing' format.
412  *
413  *	   a.x.y!b.p.o!c!d ->	@a.x.y:c!d@b.p.o
414  */
415 void
416 addhostdom(String *buf, char *host)
417 {
418 	s_append(buf, "@");
419 	s_append(buf, host);
420 }
421 String *
422 bangtoat(char *addr)
423 {
424 	String *buf;
425 	register int i;
426 	int j, d;
427 	char *field[128];
428 
429 	/* parse the '!' format address */
430 	buf = s_new();
431 	for(i = 0; addr; i++){
432 		field[i] = addr;
433 		addr = strchr(addr, '!');
434 		if(addr)
435 			*addr++ = 0;
436 	}
437 	if (i==1) {
438 		s_append(buf, field[0]);
439 		return buf;
440 	}
441 
442 	/*
443 	 *  count leading domain fields (non-domains don't count)
444 	 */
445 	d = 0;
446 	for( ; d<i-1; d++)
447 		if(strchr(field[d], '.')==0)
448 			break;
449 	/*
450 	 *  if there are more than 1 leading domain elements,
451 	 *  put them in as source routing
452 	 */
453 	if(d > 1){
454 		addhostdom(buf, field[0]);
455 		for(j=1; j<d-1; j++){
456 			s_append(buf, ",");
457 			s_append(buf, "@");
458 			s_append(buf, field[j]);
459 		}
460 		s_append(buf, ":");
461 	}
462 
463 	/*
464 	 *  throw in the non-domain elements separated by '!'s
465 	 */
466 	s_append(buf, field[d]);
467 	for(j=d+1; j<=i-1; j++) {
468 		s_append(buf, "!");
469 		s_append(buf, field[j]);
470 	}
471 	if(d)
472 		addhostdom(buf, field[d-1]);
473 	return buf;
474 }
475 
476 /*
477  *  convert header addresses to @ format.
478  *  if the address is a source address, and a domain is specified,
479  *  make sure it falls in the domain.
480  */
481 void
482 convertheader(String *from)
483 {
484 	Field *f;
485 	Node *p;
486 	String *a;
487 
488 	if(!unix && strchr(s_to_c(from), '@') == 0){
489 		s_append(from, "@");
490 		s_append(from, hostdomain);
491 	}
492 	for(f = firstfield; f; f = f->next){
493 		for(p = f->node; p; p = p->next){
494 			if(!p->addr)
495 				continue;
496 			a = bangtoat(s_to_c(p->s));
497 			s_free(p->s);
498 			if(!unix && strchr(s_to_c(a), '@') == 0){
499 				s_append(a, "@");
500 				s_append(a, hostdomain);
501 			}
502 			p->s = a;
503 		}
504 	}
505 }
506 
507 /*
508  *  print out the parsed header
509  */
510 void
511 printheader(void)
512 {
513 	Field *f;
514 	Node *p;
515 	char *cp;
516 	char c[1];
517 
518 	for(f = firstfield; f; f = f->next){
519 		for(p = f->node; p; p = p->next){
520 			if(p->s)
521 				Bprint(&bout, "%s", s_to_c(p->s));
522 			else {
523 				c[0] = p->c;
524 				putcrnl(c, 1);
525 			}
526 			if(p->white){
527 				cp = s_to_c(p->white);
528 				putcrnl(cp, strlen(cp));
529 			}
530 			uneaten = p->end;
531 		}
532 		putcrnl("\n", 1);
533 		uneaten++;		/* skip newline */
534 	}
535 }
536 
537 /*
538  *  add a domain onto an name, return the new name
539  */
540 char *
541 domainify(char *name, char *domain)
542 {
543 	static String *s;
544 
545 	if(domain==0 || strchr(name, '.')!=0)
546 		return name;
547 
548 	s = s_reset(s);
549 	s_append(s, name);
550 	if(*domain != '.')
551 		s_append(s, ".");
552 	s_append(s, domain);
553 	return s_to_c(s);
554 }
555 
556 /*
557  *  print message observing '.' escapes and using \r\n for \n
558  */
559 void
560 putcrnl(char *cp, int n)
561 {
562 	int c;
563 
564 	for(; n; n--, cp++){
565 		c = *cp;
566 		if(c == '\n')
567 			Bputc(&bout, '\r');
568 		else if(c == '.' && last=='\n')
569 			Bputc(&bout, '.');
570 		Bputc(&bout, c);
571 		last = c;
572 	}
573 }
574 
575 /*
576  *  Get a line including a crnl into a string.  Convert crnl into nl.
577  */
578 char *
579 getcrnl(String *s)
580 {
581 	int c;
582 	int count;
583 
584 	count = 0;
585 	for(;;){
586 		c = Bgetc(&bin);
587 		if(debug)
588 			Bputc(&berr, c);
589 		switch(c){
590 		case -1:
591 			s_terminate(s);
592 			fprint(2, "smtp: connection closed unexpectedly by remote system\n");
593 			return 0;
594 		case '\r':
595 			c = Bgetc(&bin);
596 			if(c == '\n'){
597 				s_putc(s, c);
598 				if(debug)
599 					Bputc(&berr, c);
600 				count++;
601 				s_terminate(s);
602 				return s->ptr - count;
603 			}
604 			Bungetc(&bin);
605 			s_putc(s, '\r');
606 			if(debug)
607 				Bputc(&berr, '\r');
608 			count++;
609 			break;
610 		default:
611 			s_putc(s, c);
612 			count++;
613 			break;
614 		}
615 	}
616 	return 0;
617 }
618 
619 /*
620  *  print out a parsed date
621  */
622 void
623 printdate(Node *p)
624 {
625 	int sep = 0;
626 
627 	Bprint(&bout, "Date: %s,", s_to_c(p->s));
628 	for(p = p->next; p; p = p->next){
629 		if(p->s){
630 			if(sep == 0)
631 				Bputc(&bout, ' ');
632 			if (p->next)
633 				Bprint(&bout, "%s", s_to_c(p->s));
634 			else
635 				Bprint(&bout, "%s", rewritezone(s_to_c(p->s)));
636 			sep = 0;
637 		} else {
638 			Bputc(&bout, p->c);
639 			sep = 1;
640 		}
641 	}
642 	Bprint(&bout, "\r\n");
643 }
644 
645 char *
646 rewritezone(char *z)
647 {
648 	Tm *t;
649 	int h, m, offset;
650 	char sign, *p;
651 	char *zones = "ECMP";
652 	static char numeric[6];
653 
654 	t = localtime(0);
655 	if (t->hour >= 12) {
656 		sign = '-';
657 		if (t->min == 0) {
658 			h = 24 - t->hour;
659 			m = 0;
660 		}
661 		else {
662 			h = 23 - t->hour;
663 			m = 60 - t->min;
664 		}
665 	}
666 	else {
667 		sign = '+';
668 		h = t->hour;
669 		m = t->min;
670 	}
671 	sprint(numeric, "%c%.2d%.2d", sign, h, m);
672 
673 	/* leave zone alone if we didn't generate it */
674 	if (strncmp(z, t->zone, 4) != 0)
675 		return z;
676 	if (strcmp(z, "GMT") == 0 || strcmp(z, "UT") == 0)
677 		if (t->hour == 0 && t->min == 0)
678 			return z;
679 		else
680 			return numeric;
681 	/* check for North American time zone */
682 	if (z[2] == 'T' && (z[1] == 'S' || z[1] == 'D')) {
683 		p = strchr(zones, z[0]);
684 		if (p) {
685 			offset = 24 - 5 - (p - zones) + (z[1] == 'D');
686 			if (offset == t->hour)
687 				return z;
688 		}
689 	}
690 	return numeric;
691 }
692 
693 /*
694  *  stolen from libc/port/print.c
695  */
696 #define	SIZE	4096
697 #define	DOTDOT	(&fmt+1)
698 int
699 dBprint(char *fmt, ...)
700 {
701 	char buf[SIZE], *out;
702 	int n;
703 
704 	out = doprint(buf, buf+SIZE, fmt, DOTDOT);
705 	if(debug){
706 		Bwrite(&berr, buf, (long)(out-buf));
707 		Bflush(&berr);
708 	}
709 	n = Bwrite(&bout, buf, (long)(out-buf));
710 	Bflush(&bout);
711 	return n;
712 }
713