1 /*
2  * Copyright (c) 1983 Eric P. Allman
3  * Copyright (c) 1988 Regents of the University of California.
4  * All rights reserved.
5  *
6  * Redistribution and use in source and binary forms are permitted
7  * provided that the above copyright notice and this paragraph are
8  * duplicated in all such forms and that any documentation,
9  * advertising materials, and other materials related to such
10  * distribution and use acknowledge that the software was developed
11  * by the University of California, Berkeley.  The name of the
12  * University may not be used to endorse or promote products derived
13  * from this software without specific prior written permission.
14  * THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
15  * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
16  * WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
17  */
18 
19 #ifndef lint
20 static char sccsid[] = "@(#)headers.c	5.14 (Berkeley) 04/18/90";
21 #endif /* not lint */
22 
23 # include <sys/param.h>
24 # include <errno.h>
25 # include "sendmail.h"
26 
27 /*
28 **  CHOMPHEADER -- process and save a header line.
29 **
30 **	Called by collect and by readcf to deal with header lines.
31 **
32 **	Parameters:
33 **		line -- header as a text line.
34 **		def -- if set, this is a default value.
35 **
36 **	Returns:
37 **		flags for this header.
38 **
39 **	Side Effects:
40 **		The header is saved on the header list.
41 **		Contents of 'line' are destroyed.
42 */
43 
44 chompheader(line, def)
45 	char *line;
46 	bool def;
47 {
48 	register char *p;
49 	register HDR *h;
50 	HDR **hp;
51 	char *fname;
52 	char *fvalue;
53 	struct hdrinfo *hi;
54 	bool cond = FALSE;
55 	BITMAP mopts;
56 	extern char *crackaddr();
57 
58 	if (tTd(31, 6))
59 		printf("chompheader: %s\n", line);
60 
61 	/* strip off options */
62 	clrbitmap(mopts);
63 	p = line;
64 	if (*p == '?')
65 	{
66 		/* have some */
67 		register char *q = index(p + 1, *p);
68 
69 		if (q != NULL)
70 		{
71 			*q++ = '\0';
72 			while (*++p != '\0')
73 				setbitn(*p, mopts);
74 			p = q;
75 		}
76 		else
77 			usrerr("chompheader: syntax error, line \"%s\"", line);
78 		cond = TRUE;
79 	}
80 
81 	/* find canonical name */
82 	fname = p;
83 	p = index(p, ':');
84 	if (p == NULL)
85 	{
86 		syserr("chompheader: syntax error, line \"%s\"", line);
87 		return (0);
88 	}
89 	fvalue = &p[1];
90 	while (isspace(*--p))
91 		continue;
92 	*++p = '\0';
93 	makelower(fname);
94 
95 	/* strip field value on front */
96 	if (*fvalue == ' ')
97 		fvalue++;
98 
99 	/* see if it is a known type */
100 	for (hi = HdrInfo; hi->hi_field != NULL; hi++)
101 	{
102 		if (strcmp(hi->hi_field, fname) == 0)
103 			break;
104 	}
105 
106 	/* see if this is a resent message */
107 	if (!def && bitset(H_RESENT, hi->hi_flags))
108 		CurEnv->e_flags |= EF_RESENT;
109 
110 	/* if this means "end of header" quit now */
111 	if (bitset(H_EOH, hi->hi_flags))
112 		return (hi->hi_flags);
113 
114 	/* drop explicit From: if same as what we would generate -- for MH */
115 	p = "resent-from";
116 	if (!bitset(EF_RESENT, CurEnv->e_flags))
117 		p += 7;
118 	if (!def && !QueueRun && strcmp(fname, p) == 0)
119 	{
120 		if (CurEnv->e_from.q_paddr != NULL &&
121 		    strcmp(fvalue, CurEnv->e_from.q_paddr) == 0)
122 			return (hi->hi_flags);
123 	}
124 
125 	/* delete default value for this header */
126 	for (hp = &CurEnv->e_header; (h = *hp) != NULL; hp = &h->h_link)
127 	{
128 		if (strcmp(fname, h->h_field) == 0 &&
129 		    bitset(H_DEFAULT, h->h_flags) &&
130 		    !bitset(H_FORCE, h->h_flags))
131 			h->h_value = NULL;
132 	}
133 
134 	/* create a new node */
135 	h = (HDR *) xalloc(sizeof *h);
136 	h->h_field = newstr(fname);
137 	h->h_value = NULL;
138 	h->h_link = NULL;
139 	bcopy((char *) mopts, (char *) h->h_mflags, sizeof mopts);
140 	*hp = h;
141 	h->h_flags = hi->hi_flags;
142 	if (def)
143 		h->h_flags |= H_DEFAULT;
144 	if (cond)
145 		h->h_flags |= H_CHECK;
146 	if (h->h_value != NULL)
147 		free((char *) h->h_value);
148 	h->h_value = newstr(fvalue);
149 
150 	/* hack to see if this is a new format message */
151 	if (!def && bitset(H_RCPT|H_FROM, h->h_flags) &&
152 	    (index(fvalue, ',') != NULL || index(fvalue, '(') != NULL ||
153 	     index(fvalue, '<') != NULL || index(fvalue, ';') != NULL))
154 	{
155 		CurEnv->e_flags &= ~EF_OLDSTYLE;
156 	}
157 
158 	return (h->h_flags);
159 }
160 /*
161 **  ADDHEADER -- add a header entry to the end of the queue.
162 **
163 **	This bypasses the special checking of chompheader.
164 **
165 **	Parameters:
166 **		field -- the name of the header field.
167 **		value -- the value of the field.  It must be lower-cased.
168 **		e -- the envelope to add them to.
169 **
170 **	Returns:
171 **		none.
172 **
173 **	Side Effects:
174 **		adds the field on the list of headers for this envelope.
175 */
176 
177 addheader(field, value, e)
178 	char *field;
179 	char *value;
180 	ENVELOPE *e;
181 {
182 	register HDR *h;
183 	register struct hdrinfo *hi;
184 	HDR **hp;
185 
186 	/* find info struct */
187 	for (hi = HdrInfo; hi->hi_field != NULL; hi++)
188 	{
189 		if (strcmp(field, hi->hi_field) == 0)
190 			break;
191 	}
192 
193 	/* find current place in list -- keep back pointer? */
194 	for (hp = &e->e_header; (h = *hp) != NULL; hp = &h->h_link)
195 	{
196 		if (strcmp(field, h->h_field) == 0)
197 			break;
198 	}
199 
200 	/* allocate space for new header */
201 	h = (HDR *) xalloc(sizeof *h);
202 	h->h_field = field;
203 	h->h_value = newstr(value);
204 	h->h_link = *hp;
205 	h->h_flags = hi->hi_flags | H_DEFAULT;
206 	clrbitmap(h->h_mflags);
207 	*hp = h;
208 }
209 /*
210 **  HVALUE -- return value of a header.
211 **
212 **	Only "real" fields (i.e., ones that have not been supplied
213 **	as a default) are used.
214 **
215 **	Parameters:
216 **		field -- the field name.
217 **
218 **	Returns:
219 **		pointer to the value part.
220 **		NULL if not found.
221 **
222 **	Side Effects:
223 **		none.
224 */
225 
226 char *
227 hvalue(field)
228 	char *field;
229 {
230 	register HDR *h;
231 
232 	for (h = CurEnv->e_header; h != NULL; h = h->h_link)
233 	{
234 		if (!bitset(H_DEFAULT, h->h_flags) && strcmp(h->h_field, field) == 0)
235 			return (h->h_value);
236 	}
237 	return (NULL);
238 }
239 /*
240 **  ISHEADER -- predicate telling if argument is a header.
241 **
242 **	A line is a header if it has a single word followed by
243 **	optional white space followed by a colon.
244 **
245 **	Parameters:
246 **		s -- string to check for possible headerness.
247 **
248 **	Returns:
249 **		TRUE if s is a header.
250 **		FALSE otherwise.
251 **
252 **	Side Effects:
253 **		none.
254 */
255 
256 bool
257 isheader(s)
258 	register char *s;
259 {
260 	while (*s > ' ' && *s != ':' && *s != '\0')
261 		s++;
262 
263 	/* following technically violates RFC822 */
264 	while (isspace(*s))
265 		s++;
266 
267 	return (*s == ':');
268 }
269 /*
270 **  EATHEADER -- run through the stored header and extract info.
271 **
272 **	Parameters:
273 **		e -- the envelope to process.
274 **
275 **	Returns:
276 **		none.
277 **
278 **	Side Effects:
279 **		Sets a bunch of global variables from information
280 **			in the collected header.
281 **		Aborts the message if the hop count is exceeded.
282 */
283 
284 eatheader(e)
285 	register ENVELOPE *e;
286 {
287 	register HDR *h;
288 	register char *p;
289 	int hopcnt = 0;
290 
291 	if (tTd(32, 1))
292 		printf("----- collected header -----\n");
293 	for (h = e->e_header; h != NULL; h = h->h_link)
294 	{
295 		extern char *capitalize();
296 
297 		if (tTd(32, 1))
298 			printf("%s: %s\n", capitalize(h->h_field), h->h_value);
299 		/* count the number of times it has been processed */
300 		if (bitset(H_TRACE, h->h_flags))
301 			hopcnt++;
302 
303 		/* send to this person if we so desire */
304 		if (GrabTo && bitset(H_RCPT, h->h_flags) &&
305 		    !bitset(H_DEFAULT, h->h_flags) &&
306 		    (!bitset(EF_RESENT, CurEnv->e_flags) || bitset(H_RESENT, h->h_flags)))
307 		{
308 			sendtolist(h->h_value, (ADDRESS *) NULL, &CurEnv->e_sendqueue);
309 		}
310 
311 		/* log the message-id */
312 #ifdef LOG
313 		if (!QueueRun && LogLevel > 8 && h->h_value != NULL &&
314 		    strcmp(h->h_field, "message-id") == 0)
315 		{
316 			char buf[MAXNAME];
317 
318 			p = h->h_value;
319 			if (bitset(H_DEFAULT, h->h_flags))
320 			{
321 				expand(p, buf, &buf[sizeof buf], e);
322 				p = buf;
323 			}
324 			syslog(LOG_INFO, "%s: message-id=%s", e->e_id, p);
325 		}
326 #endif LOG
327 	}
328 	if (tTd(32, 1))
329 		printf("----------------------------\n");
330 
331 	/* store hop count */
332 	if (hopcnt > e->e_hopcount)
333 		e->e_hopcount = hopcnt;
334 
335 	/* message priority */
336 	p = hvalue("precedence");
337 	if (p != NULL)
338 		e->e_class = priencode(p);
339 	if (!QueueRun)
340 		e->e_msgpriority = e->e_msgsize
341 				 - e->e_class * WkClassFact
342 				 + e->e_nrcpts * WkRecipFact;
343 
344 	/* return receipt to */
345 	p = hvalue("return-receipt-to");
346 	if (p != NULL)
347 		e->e_receiptto = p;
348 
349 	/* errors to */
350 	p = hvalue("errors-to");
351 	if (p != NULL)
352 		sendtolist(p, (ADDRESS *) NULL, &e->e_errorqueue);
353 
354 	/* from person */
355 	if (OpMode == MD_ARPAFTP)
356 	{
357 		register struct hdrinfo *hi = HdrInfo;
358 
359 		for (p = NULL; p == NULL && hi->hi_field != NULL; hi++)
360 		{
361 			if (bitset(H_FROM, hi->hi_flags))
362 				p = hvalue(hi->hi_field);
363 		}
364 		if (p != NULL)
365 			setsender(p);
366 	}
367 
368 	/* full name of from person */
369 	p = hvalue("full-name");
370 	if (p != NULL)
371 		define('x', p, e);
372 
373 	/* date message originated */
374 	p = hvalue("posted-date");
375 	if (p == NULL)
376 		p = hvalue("date");
377 	if (p != NULL)
378 	{
379 		define('a', p, e);
380 		/* we don't have a good way to do canonical conversion ....
381 		define('d', newstr(arpatounix(p)), e);
382 		.... so we will ignore the problem for the time being */
383 	}
384 
385 	/*
386 	**  Log collection information.
387 	*/
388 
389 # ifdef LOG
390 	if (!QueueRun && LogLevel > 1)
391 	{
392 		char hbuf[100], *name = hbuf;
393 
394 		if (RealHostName == NULL)
395 			name = "local";
396 		else if (RealHostName[0] == '[')
397 			name = RealHostName;
398 		else
399 			(void)sprintf(hbuf, "%.90s (%s)",
400 			    RealHostName, inet_ntoa(RealHostAddr.sin_addr));
401 		syslog(LOG_INFO,
402 		    "%s: from=%s, size=%ld, class=%d, received from %s\n",
403 		    CurEnv->e_id, CurEnv->e_from.q_paddr, CurEnv->e_msgsize,
404 		    CurEnv->e_class, name);
405 	}
406 # endif LOG
407 }
408 /*
409 **  PRIENCODE -- encode external priority names into internal values.
410 **
411 **	Parameters:
412 **		p -- priority in ascii.
413 **
414 **	Returns:
415 **		priority as a numeric level.
416 **
417 **	Side Effects:
418 **		none.
419 */
420 
421 priencode(p)
422 	char *p;
423 {
424 	register int i;
425 
426 	for (i = 0; i < NumPriorities; i++)
427 	{
428 		if (!strcasecmp(p, Priorities[i].pri_name))
429 			return (Priorities[i].pri_val);
430 	}
431 
432 	/* unknown priority */
433 	return (0);
434 }
435 /*
436 **  CRACKADDR -- parse an address and turn it into a macro
437 **
438 **	This doesn't actually parse the address -- it just extracts
439 **	it and replaces it with "$g".  The parse is totally ad hoc
440 **	and isn't even guaranteed to leave something syntactically
441 **	identical to what it started with.  However, it does leave
442 **	something semantically identical.
443 **
444 **	The process is kind of strange.  There are a number of
445 **	interesting cases:
446 **		1.  comment <address> comment	==> comment <$g> comment
447 **		2.  address			==> address
448 **		3.  address (comment)		==> $g (comment)
449 **		4.  (comment) address		==> (comment) $g
450 **	And then there are the hard cases....
451 **		5.  add (comment) ress		==> $g (comment)
452 **		6.  comment <address (comment)>	==> comment <$g (comment)>
453 **		7.    .... etc ....
454 **
455 **	Parameters:
456 **		addr -- the address to be cracked.
457 **
458 **	Returns:
459 **		a pointer to the new version.
460 **
461 **	Side Effects:
462 **		none.
463 **
464 **	Warning:
465 **		The return value is saved in local storage and should
466 **		be copied if it is to be reused.
467 */
468 
469 char *
470 crackaddr(addr)
471 	register char *addr;
472 {
473 	register char *p;
474 	register int i;
475 	static char buf[MAXNAME];
476 	char *rhs;
477 	bool gotaddr;
478 	register char *bp;
479 
480 	if (tTd(33, 1))
481 		printf("crackaddr(%s)\n", addr);
482 
483 	(void) strcpy(buf, "");
484 	rhs = NULL;
485 
486 	/* strip leading spaces */
487 	while (*addr != '\0' && isspace(*addr))
488 		addr++;
489 
490 	/*
491 	**  See if we have anything in angle brackets.  If so, that is
492 	**  the address part, and the rest is the comment.
493 	*/
494 
495 	p = index(addr, '<');
496 	if (p != NULL)
497 	{
498 		/* copy the beginning of the addr field to the buffer */
499 		*p = '\0';
500 		(void) strcpy(buf, addr);
501 		(void) strcat(buf, "<");
502 		*p++ = '<';
503 
504 		/* skip spaces */
505 		while (isspace(*p))
506 			p++;
507 
508 		/* find the matching right angle bracket */
509 		addr = p;
510 		for (i = 0; *p != '\0'; p++)
511 		{
512 			switch (*p)
513 			{
514 			  case '<':
515 				i++;
516 				break;
517 
518 			  case '>':
519 				i--;
520 				break;
521 			}
522 			if (i < 0)
523 				break;
524 		}
525 
526 		/* p now points to the closing quote (or a null byte) */
527 		if (*p != '\0')
528 		{
529 			/* make rhs point to the extra stuff at the end */
530 			rhs = p;
531 			*p++ = '\0';
532 		}
533 	}
534 
535 	/*
536 	**  Now parse the real address part.  "addr" points to the (null
537 	**  terminated) version of what we are inerested in; rhs points
538 	**  to the extra stuff at the end of the line, if any.
539 	*/
540 
541 	p = addr;
542 
543 	/* now strip out comments */
544 	bp = &buf[strlen(buf)];
545 	gotaddr = FALSE;
546 	for (; *p != '\0'; p++)
547 	{
548 		if (*p == '(')
549 		{
550 			/* copy to matching close paren */
551 			*bp++ = *p++;
552 			for (i = 0; *p != '\0'; p++)
553 			{
554 				*bp++ = *p;
555 				switch (*p)
556 				{
557 				  case '(':
558 					i++;
559 					break;
560 
561 				  case ')':
562 					i--;
563 					break;
564 				}
565 				if (i < 0)
566 					break;
567 			}
568 			continue;
569 		}
570 
571 		/*
572 		**  If this is the first "real" character we have seen,
573 		**  then we put the "$g" in the buffer now.
574 		*/
575 
576 		if (isspace(*p))
577 			*bp++ = *p;
578 		else if (!gotaddr)
579 		{
580 			(void) strcpy(bp, "\001g");
581 			bp += 2;
582 			gotaddr = TRUE;
583 		}
584 	}
585 
586 	/* hack, hack.... strip trailing blanks */
587 	do
588 	{
589 		*bp-- = '\0';
590 	} while (isspace(*bp));
591 	bp++;
592 
593 	/* put any right hand side back on */
594 	if (rhs != NULL)
595 	{
596 		*rhs = '>';
597 		(void) strcpy(bp, rhs);
598 	}
599 
600 	if (tTd(33, 1))
601 		printf("crackaddr=>`%s'\n", buf);
602 
603 	return (buf);
604 }
605 /*
606 **  PUTHEADER -- put the header part of a message from the in-core copy
607 **
608 **	Parameters:
609 **		fp -- file to put it on.
610 **		m -- mailer to use.
611 **		e -- envelope to use.
612 **
613 **	Returns:
614 **		none.
615 **
616 **	Side Effects:
617 **		none.
618 */
619 
620 putheader(fp, m, e)
621 	register FILE *fp;
622 	register MAILER *m;
623 	register ENVELOPE *e;
624 {
625 	char buf[MAX(MAXFIELD,BUFSIZ)];
626 	register HDR *h;
627 	extern char *arpadate();
628 	extern char *capitalize();
629 	char obuf[MAX(MAXFIELD,MAXLINE)];
630 
631 	for (h = e->e_header; h != NULL; h = h->h_link)
632 	{
633 		register char *p;
634 		extern bool bitintersect();
635 
636 		if (bitset(H_CHECK|H_ACHECK, h->h_flags) &&
637 		    !bitintersect(h->h_mflags, m->m_flags))
638 			continue;
639 
640 		/* handle Resent-... headers specially */
641 		if (bitset(H_RESENT, h->h_flags) && !bitset(EF_RESENT, e->e_flags))
642 			continue;
643 
644 		p = h->h_value;
645 		if (bitset(H_DEFAULT, h->h_flags))
646 		{
647 			/* macro expand value if generated internally */
648 			expand(p, buf, &buf[sizeof buf], e);
649 			p = buf;
650 			if (p == NULL || *p == '\0')
651 				continue;
652 		}
653 
654 		if (bitset(H_FROM|H_RCPT, h->h_flags))
655 		{
656 			/* address field */
657 			bool oldstyle = bitset(EF_OLDSTYLE, e->e_flags);
658 
659 			if (bitset(H_FROM, h->h_flags))
660 				oldstyle = FALSE;
661 			commaize(h, p, fp, oldstyle, m);
662 		}
663 		else
664 		{
665 			/* vanilla header line */
666 			register char *nlp;
667 
668 			(void) sprintf(obuf, "%s: ", capitalize(h->h_field));
669 			while ((nlp = index(p, '\n')) != NULL)
670 			{
671 				*nlp = '\0';
672 				(void) strcat(obuf, p);
673 				*nlp = '\n';
674 				putline(obuf, fp, m);
675 				p = ++nlp;
676 				obuf[0] = '\0';
677 			}
678 			(void) strcat(obuf, p);
679 			putline(obuf, fp, m);
680 		}
681 	}
682 }
683 /*
684 **  COMMAIZE -- output a header field, making a comma-translated list.
685 **
686 **	Parameters:
687 **		h -- the header field to output.
688 **		p -- the value to put in it.
689 **		fp -- file to put it to.
690 **		oldstyle -- TRUE if this is an old style header.
691 **		m -- a pointer to the mailer descriptor.  If NULL,
692 **			don't transform the name at all.
693 **
694 **	Returns:
695 **		none.
696 **
697 **	Side Effects:
698 **		outputs "p" to file "fp".
699 */
700 
701 commaize(h, p, fp, oldstyle, m)
702 	register HDR *h;
703 	register char *p;
704 	FILE *fp;
705 	bool oldstyle;
706 	register MAILER *m;
707 {
708 	register char *obp;
709 	int opos;
710 	bool firstone = TRUE;
711 	char obuf[MAXLINE + 3];
712 
713 	/*
714 	**  Output the address list translated by the
715 	**  mailer and with commas.
716 	*/
717 
718 	if (tTd(14, 2))
719 		printf("commaize(%s: %s)\n", h->h_field, p);
720 
721 	obp = obuf;
722 	(void) sprintf(obp, "%s: ", capitalize(h->h_field));
723 	opos = strlen(h->h_field) + 2;
724 	obp += opos;
725 
726 	/*
727 	**  Run through the list of values.
728 	*/
729 
730 	while (*p != '\0')
731 	{
732 		register char *name;
733 		char savechar;
734 		extern char *remotename();
735 		extern char *DelimChar;		/* defined in prescan */
736 
737 		/*
738 		**  Find the end of the name.  New style names
739 		**  end with a comma, old style names end with
740 		**  a space character.  However, spaces do not
741 		**  necessarily delimit an old-style name -- at
742 		**  signs mean keep going.
743 		*/
744 
745 		/* find end of name */
746 		while (isspace(*p) || *p == ',')
747 			p++;
748 		name = p;
749 		for (;;)
750 		{
751 			char *oldp;
752 			char pvpbuf[PSBUFSIZE];
753 			extern bool isatword();
754 			extern char **prescan();
755 
756 			(void) prescan(p, oldstyle ? ' ' : ',', pvpbuf);
757 			p = DelimChar;
758 
759 			/* look to see if we have an at sign */
760 			oldp = p;
761 			while (*p != '\0' && isspace(*p))
762 				p++;
763 
764 			if (*p != '@' && !isatword(p))
765 			{
766 				p = oldp;
767 				break;
768 			}
769 			p += *p == '@' ? 1 : 2;
770 			while (*p != '\0' && isspace(*p))
771 				p++;
772 		}
773 		/* at the end of one complete name */
774 
775 		/* strip off trailing white space */
776 		while (p >= name && (isspace(*p) || *p == ',' || *p == '\0'))
777 			p--;
778 		if (++p == name)
779 			continue;
780 		savechar = *p;
781 		*p = '\0';
782 
783 		/* translate the name to be relative */
784 		name = remotename(name, m, bitset(H_FROM, h->h_flags), FALSE);
785 		if (*name == '\0')
786 		{
787 			*p = savechar;
788 			continue;
789 		}
790 
791 		/* output the name with nice formatting */
792 		opos += qstrlen(name);
793 		if (!firstone)
794 			opos += 2;
795 		if (opos > 78 && !firstone)
796 		{
797 			(void) strcpy(obp, ",\n");
798 			putline(obuf, fp, m);
799 			obp = obuf;
800 			(void) sprintf(obp, "        ");
801 			opos = strlen(obp);
802 			obp += opos;
803 			opos += qstrlen(name);
804 		}
805 		else if (!firstone)
806 		{
807 			(void) sprintf(obp, ", ");
808 			obp += 2;
809 		}
810 
811 		/* strip off quote bits as we output */
812 		while (*name != '\0' && obp < &obuf[MAXLINE])
813 		{
814 			if (bitset(0200, *name))
815 				*obp++ = '\\';
816 			*obp++ = *name++ & ~0200;
817 		}
818 		firstone = FALSE;
819 		*p = savechar;
820 	}
821 	(void) strcpy(obp, "\n");
822 	putline(obuf, fp, m);
823 }
824 /*
825 **  ISATWORD -- tell if the word we are pointing to is "at".
826 **
827 **	Parameters:
828 **		p -- word to check.
829 **
830 **	Returns:
831 **		TRUE -- if p is the word at.
832 **		FALSE -- otherwise.
833 **
834 **	Side Effects:
835 **		none.
836 */
837 
838 bool
839 isatword(p)
840 	register char *p;
841 {
842 	extern char lower();
843 
844 	if (lower(p[0]) == 'a' && lower(p[1]) == 't' &&
845 	    p[2] != '\0' && isspace(p[2]))
846 		return (TRUE);
847 	return (FALSE);
848 }
849