xref: /csrg-svn/usr.sbin/sendmail/src/util.c (revision 58680)
1 /*
2  * Copyright (c) 1983 Eric P. Allman
3  * Copyright (c) 1988 Regents of the University of California.
4  * All rights reserved.
5  *
6  * %sccs.include.redist.c%
7  */
8 
9 #ifndef lint
10 static char sccsid[] = "@(#)util.c	6.12 (Berkeley) 03/16/93";
11 #endif /* not lint */
12 
13 # include "sendmail.h"
14 # include <sys/stat.h>
15 # include <sysexits.h>
16 /*
17 **  STRIPQUOTES -- Strip quotes & quote bits from a string.
18 **
19 **	Runs through a string and strips off unquoted quote
20 **	characters and quote bits.  This is done in place.
21 **
22 **	Parameters:
23 **		s -- the string to strip.
24 **
25 **	Returns:
26 **		none.
27 **
28 **	Side Effects:
29 **		none.
30 **
31 **	Called By:
32 **		deliver
33 */
34 
35 stripquotes(s)
36 	char *s;
37 {
38 	register char *p;
39 	register char *q;
40 	register char c;
41 
42 	if (s == NULL)
43 		return;
44 
45 	p = q = s;
46 	do
47 	{
48 		c = *p++;
49 		if (c == '\\')
50 			c = *p++;
51 		else if (c == '"')
52 			continue;
53 		*q++ = c;
54 	} while (c != '\0');
55 }
56 /*
57 **  CAPITALIZE -- return a copy of a string, properly capitalized.
58 **
59 **	Parameters:
60 **		s -- the string to capitalize.
61 **
62 **	Returns:
63 **		a pointer to a properly capitalized string.
64 **
65 **	Side Effects:
66 **		none.
67 */
68 
69 char *
70 capitalize(s)
71 	register char *s;
72 {
73 	static char buf[50];
74 	register char *p;
75 
76 	p = buf;
77 
78 	for (;;)
79 	{
80 		while (!(isascii(*s) && isalpha(*s)) && *s != '\0')
81 			*p++ = *s++;
82 		if (*s == '\0')
83 			break;
84 		*p++ = toupper(*s);
85 		s++;
86 		while (isascii(*s) && isalpha(*s))
87 			*p++ = *s++;
88 	}
89 
90 	*p = '\0';
91 	return (buf);
92 }
93 /*
94 **  XALLOC -- Allocate memory and bitch wildly on failure.
95 **
96 **	THIS IS A CLUDGE.  This should be made to give a proper
97 **	error -- but after all, what can we do?
98 **
99 **	Parameters:
100 **		sz -- size of area to allocate.
101 **
102 **	Returns:
103 **		pointer to data region.
104 **
105 **	Side Effects:
106 **		Memory is allocated.
107 */
108 
109 char *
110 xalloc(sz)
111 	register int sz;
112 {
113 	register char *p;
114 
115 	p = malloc((unsigned) sz);
116 	if (p == NULL)
117 	{
118 		syserr("Out of memory!!");
119 		abort();
120 		/* exit(EX_UNAVAILABLE); */
121 	}
122 	return (p);
123 }
124 /*
125 **  COPYPLIST -- copy list of pointers.
126 **
127 **	This routine is the equivalent of newstr for lists of
128 **	pointers.
129 **
130 **	Parameters:
131 **		list -- list of pointers to copy.
132 **			Must be NULL terminated.
133 **		copycont -- if TRUE, copy the contents of the vector
134 **			(which must be a string) also.
135 **
136 **	Returns:
137 **		a copy of 'list'.
138 **
139 **	Side Effects:
140 **		none.
141 */
142 
143 char **
144 copyplist(list, copycont)
145 	char **list;
146 	bool copycont;
147 {
148 	register char **vp;
149 	register char **newvp;
150 
151 	for (vp = list; *vp != NULL; vp++)
152 		continue;
153 
154 	vp++;
155 
156 	newvp = (char **) xalloc((int) (vp - list) * sizeof *vp);
157 	bcopy((char *) list, (char *) newvp, (int) (vp - list) * sizeof *vp);
158 
159 	if (copycont)
160 	{
161 		for (vp = newvp; *vp != NULL; vp++)
162 			*vp = newstr(*vp);
163 	}
164 
165 	return (newvp);
166 }
167 /*
168 **  COPYQUEUE -- copy address queue.
169 **
170 **	This routine is the equivalent of newstr for address queues
171 **	addresses marked with QDONTSEND aren't copied
172 **
173 **	Parameters:
174 **		addr -- list of address structures to copy.
175 **
176 **	Returns:
177 **		a copy of 'addr'.
178 **
179 **	Side Effects:
180 **		none.
181 */
182 
183 ADDRESS *
184 copyqueue(addr)
185 	ADDRESS *addr;
186 {
187 	register ADDRESS *newaddr;
188 	ADDRESS *ret;
189 	register ADDRESS **tail = &ret;
190 
191 	while (addr != NULL)
192 	{
193 		if (!bitset(QDONTSEND, addr->q_flags))
194 		{
195 			newaddr = (ADDRESS *) xalloc(sizeof(ADDRESS));
196 			STRUCTCOPY(*addr, *newaddr);
197 			*tail = newaddr;
198 			tail = &newaddr->q_next;
199 		}
200 		addr = addr->q_next;
201 	}
202 	*tail = NULL;
203 
204 	return ret;
205 }
206 /*
207 **  PRINTAV -- print argument vector.
208 **
209 **	Parameters:
210 **		av -- argument vector.
211 **
212 **	Returns:
213 **		none.
214 **
215 **	Side Effects:
216 **		prints av.
217 */
218 
219 printav(av)
220 	register char **av;
221 {
222 	while (*av != NULL)
223 	{
224 		if (tTd(0, 44))
225 			printf("\n\t%08x=", *av);
226 		else
227 			(void) putchar(' ');
228 		xputs(*av++);
229 	}
230 	(void) putchar('\n');
231 }
232 /*
233 **  LOWER -- turn letter into lower case.
234 **
235 **	Parameters:
236 **		c -- character to turn into lower case.
237 **
238 **	Returns:
239 **		c, in lower case.
240 **
241 **	Side Effects:
242 **		none.
243 */
244 
245 char
246 lower(c)
247 	register char c;
248 {
249 	return((isascii(c) && isupper(c)) ? tolower(c) : c);
250 }
251 /*
252 **  XPUTS -- put string doing control escapes.
253 **
254 **	Parameters:
255 **		s -- string to put.
256 **
257 **	Returns:
258 **		none.
259 **
260 **	Side Effects:
261 **		output to stdout
262 */
263 
264 xputs(s)
265 	register char *s;
266 {
267 	register int c;
268 	register struct metamac *mp;
269 	extern struct metamac MetaMacros[];
270 
271 	if (s == NULL)
272 	{
273 		printf("<null>");
274 		return;
275 	}
276 	while ((c = (*s++ & 0377)) != '\0')
277 	{
278 		if (!isascii(c))
279 		{
280 			if (c == MATCHREPL || c == MACROEXPAND)
281 			{
282 				putchar('$');
283 				continue;
284 			}
285 			for (mp = MetaMacros; mp->metaname != '\0'; mp++)
286 			{
287 				if ((mp->metaval & 0377) == c)
288 				{
289 					printf("$%c", mp->metaname);
290 					break;
291 				}
292 			}
293 			if (mp->metaname != '\0')
294 				continue;
295 			(void) putchar('\\');
296 			c &= 0177;
297 		}
298 		if (isprint(c))
299 		{
300 			putchar(c);
301 			continue;
302 		}
303 
304 		/* wasn't a meta-macro -- find another way to print it */
305 		switch (c)
306 		{
307 		  case '\0':
308 			continue;
309 
310 		  case '\n':
311 			c = 'n';
312 			break;
313 
314 		  case '\r':
315 			c = 'r';
316 			break;
317 
318 		  case '\t':
319 			c = 't';
320 			break;
321 
322 		  default:
323 			(void) putchar('^');
324 			(void) putchar(c ^ 0100);
325 			continue;
326 		}
327 	}
328 	(void) fflush(stdout);
329 }
330 /*
331 **  MAKELOWER -- Translate a line into lower case
332 **
333 **	Parameters:
334 **		p -- the string to translate.  If NULL, return is
335 **			immediate.
336 **
337 **	Returns:
338 **		none.
339 **
340 **	Side Effects:
341 **		String pointed to by p is translated to lower case.
342 **
343 **	Called By:
344 **		parse
345 */
346 
347 makelower(p)
348 	register char *p;
349 {
350 	register char c;
351 
352 	if (p == NULL)
353 		return;
354 	for (; (c = *p) != '\0'; p++)
355 		if (isascii(c) && isupper(c))
356 			*p = tolower(c);
357 }
358 /*
359 **  BUILDFNAME -- build full name from gecos style entry.
360 **
361 **	This routine interprets the strange entry that would appear
362 **	in the GECOS field of the password file.
363 **
364 **	Parameters:
365 **		p -- name to build.
366 **		login -- the login name of this user (for &).
367 **		buf -- place to put the result.
368 **
369 **	Returns:
370 **		none.
371 **
372 **	Side Effects:
373 **		none.
374 */
375 
376 buildfname(gecos, login, buf)
377 	register char *gecos;
378 	char *login;
379 	char *buf;
380 {
381 	register char *p;
382 	register char *bp = buf;
383 	int l;
384 
385 	if (*gecos == '*')
386 		gecos++;
387 
388 	/* find length of final string */
389 	l = 0;
390 	for (p = gecos; *p != '\0' && *p != ',' && *p != ';' && *p != '%'; p++)
391 	{
392 		if (*p == '&')
393 			l += strlen(login);
394 		else
395 			l++;
396 	}
397 
398 	/* now fill in buf */
399 	for (p = gecos; *p != '\0' && *p != ',' && *p != ';' && *p != '%'; p++)
400 	{
401 		if (*p == '&')
402 		{
403 			(void) strcpy(bp, login);
404 			*bp = toupper(*bp);
405 			while (*bp != '\0')
406 				bp++;
407 		}
408 		else
409 			*bp++ = *p;
410 	}
411 	*bp = '\0';
412 }
413 /*
414 **  SAFEFILE -- return true if a file exists and is safe for a user.
415 **
416 **	Parameters:
417 **		fn -- filename to check.
418 **		uid -- uid to compare against.
419 **		mode -- mode bits that must match.
420 **
421 **	Returns:
422 **		0 if fn exists, is owned by uid, and matches mode.
423 **		An errno otherwise.  The actual errno is cleared.
424 **
425 **	Side Effects:
426 **		none.
427 */
428 
429 int
430 safefile(fn, uid, mode)
431 	char *fn;
432 	uid_t uid;
433 	int mode;
434 {
435 	struct stat stbuf;
436 
437 	if (stat(fn, &stbuf) < 0)
438 	{
439 		int ret = errno;
440 
441 		errno = 0;
442 		return ret;
443 	}
444 	if (stbuf.st_uid == uid && (stbuf.st_mode & mode) == mode)
445 		return 0;
446 	return EPERM;
447 }
448 /*
449 **  FIXCRLF -- fix <CR><LF> in line.
450 **
451 **	Looks for the <CR><LF> combination and turns it into the
452 **	UNIX canonical <NL> character.  It only takes one line,
453 **	i.e., it is assumed that the first <NL> found is the end
454 **	of the line.
455 **
456 **	Parameters:
457 **		line -- the line to fix.
458 **		stripnl -- if true, strip the newline also.
459 **
460 **	Returns:
461 **		none.
462 **
463 **	Side Effects:
464 **		line is changed in place.
465 */
466 
467 fixcrlf(line, stripnl)
468 	char *line;
469 	bool stripnl;
470 {
471 	register char *p;
472 
473 	p = strchr(line, '\n');
474 	if (p == NULL)
475 		return;
476 	if (p > line && p[-1] == '\r')
477 		p--;
478 	if (!stripnl)
479 		*p++ = '\n';
480 	*p = '\0';
481 }
482 /*
483 **  DFOPEN -- determined file open
484 **
485 **	This routine has the semantics of fopen, except that it will
486 **	keep trying a few times to make this happen.  The idea is that
487 **	on very loaded systems, we may run out of resources (inodes,
488 **	whatever), so this tries to get around it.
489 */
490 
491 FILE *
492 dfopen(filename, mode)
493 	char *filename;
494 	char *mode;
495 {
496 	register int tries;
497 	register FILE *fp;
498 
499 	for (tries = 0; tries < 10; tries++)
500 	{
501 		sleep((unsigned) (10 * tries));
502 		errno = 0;
503 		fp = fopen(filename, mode);
504 		if (fp != NULL)
505 			break;
506 		if (errno != ENFILE && errno != EINTR)
507 			break;
508 	}
509 	if (fp != NULL)
510 	{
511 #ifdef FLOCK
512 		int locktype;
513 
514 		/* lock the file to avoid accidental conflicts */
515 		if (*mode == 'w' || *mode == 'a')
516 			locktype = LOCK_EX;
517 		else
518 			locktype = LOCK_SH;
519 		(void) flock(fileno(fp), locktype);
520 #endif
521 		errno = 0;
522 	}
523 	return (fp);
524 }
525 /*
526 **  PUTLINE -- put a line like fputs obeying SMTP conventions
527 **
528 **	This routine always guarantees outputing a newline (or CRLF,
529 **	as appropriate) at the end of the string.
530 **
531 **	Parameters:
532 **		l -- line to put.
533 **		fp -- file to put it onto.
534 **		m -- the mailer used to control output.
535 **
536 **	Returns:
537 **		none
538 **
539 **	Side Effects:
540 **		output of l to fp.
541 */
542 
543 putline(l, fp, m)
544 	register char *l;
545 	FILE *fp;
546 	MAILER *m;
547 {
548 	register char *p;
549 	register char svchar;
550 
551 	/* strip out 0200 bits -- these can look like TELNET protocol */
552 	if (bitnset(M_7BITS, m->m_flags))
553 	{
554 		for (p = l; svchar = *p; ++p)
555 			if (svchar & 0200)
556 				*p = svchar &~ 0200;
557 	}
558 
559 	do
560 	{
561 		/* find the end of the line */
562 		p = strchr(l, '\n');
563 		if (p == NULL)
564 			p = &l[strlen(l)];
565 
566 		/* check for line overflow */
567 		while (m->m_linelimit > 0 && (p - l) > m->m_linelimit)
568 		{
569 			register char *q = &l[m->m_linelimit - 1];
570 
571 			svchar = *q;
572 			*q = '\0';
573 			if (l[0] == '.' && bitnset(M_XDOT, m->m_flags))
574 				(void) putc('.', fp);
575 			fputs(l, fp);
576 			(void) putc('!', fp);
577 			fputs(m->m_eol, fp);
578 			*q = svchar;
579 			l = q;
580 		}
581 
582 		/* output last part */
583 		if (l[0] == '.' && bitnset(M_XDOT, m->m_flags))
584 			(void) putc('.', fp);
585 		for ( ; l < p; ++l)
586 			(void) putc(*l, fp);
587 		fputs(m->m_eol, fp);
588 		if (*l == '\n')
589 			++l;
590 	} while (l[0] != '\0');
591 }
592 /*
593 **  XUNLINK -- unlink a file, doing logging as appropriate.
594 **
595 **	Parameters:
596 **		f -- name of file to unlink.
597 **
598 **	Returns:
599 **		none.
600 **
601 **	Side Effects:
602 **		f is unlinked.
603 */
604 
605 xunlink(f)
606 	char *f;
607 {
608 	register int i;
609 
610 # ifdef LOG
611 	if (LogLevel > 98)
612 		syslog(LOG_DEBUG, "%s: unlink %s", CurEnv->e_id, f);
613 # endif /* LOG */
614 
615 	i = unlink(f);
616 # ifdef LOG
617 	if (i < 0 && LogLevel > 97)
618 		syslog(LOG_DEBUG, "%s: unlink-fail %d", f, errno);
619 # endif /* LOG */
620 }
621 /*
622 **  XFCLOSE -- close a file, doing logging as appropriate.
623 **
624 **	Parameters:
625 **		fp -- file pointer for the file to close
626 **		a, b -- miscellaneous crud to print for debugging
627 **
628 **	Returns:
629 **		none.
630 **
631 **	Side Effects:
632 **		fp is closed.
633 */
634 
635 xfclose(fp, a, b)
636 	FILE *fp;
637 	char *a, *b;
638 {
639 	if (tTd(9, 99))
640 		printf("xfclose(%x) %s %s\n", fp, a, b);
641 	if (fclose(fp) < 0 && tTd(9, 99))
642 		printf("xfclose FAILURE: %s\n", errstring(errno));
643 }
644 /*
645 **  SFGETS -- "safe" fgets -- times out and ignores random interrupts.
646 **
647 **	Parameters:
648 **		buf -- place to put the input line.
649 **		siz -- size of buf.
650 **		fp -- file to read from.
651 **		timeout -- the timeout before error occurs.
652 **
653 **	Returns:
654 **		NULL on error (including timeout).  This will also leave
655 **			buf containing a null string.
656 **		buf otherwise.
657 **
658 **	Side Effects:
659 **		none.
660 */
661 
662 static jmp_buf	CtxReadTimeout;
663 
664 char *
665 sfgets(buf, siz, fp, timeout)
666 	char *buf;
667 	int siz;
668 	FILE *fp;
669 	time_t timeout;
670 {
671 	register EVENT *ev = NULL;
672 	register char *p;
673 	static int readtimeout();
674 
675 	/* set the timeout */
676 	if (timeout != 0)
677 	{
678 		if (setjmp(CtxReadTimeout) != 0)
679 		{
680 # ifdef LOG
681 			syslog(LOG_NOTICE,
682 			    "timeout waiting for input from %s\n",
683 			    CurHostName? CurHostName: "local");
684 # endif
685 			errno = 0;
686 			usrerr("451 timeout waiting for input");
687 			buf[0] = '\0';
688 			return (NULL);
689 		}
690 		ev = setevent(timeout, readtimeout, 0);
691 	}
692 
693 	/* try to read */
694 	p = NULL;
695 	while (p == NULL && !feof(fp) && !ferror(fp))
696 	{
697 		errno = 0;
698 		p = fgets(buf, siz, fp);
699 		if (errno == EINTR)
700 			clearerr(fp);
701 	}
702 
703 	/* clear the event if it has not sprung */
704 	clrevent(ev);
705 
706 	/* clean up the books and exit */
707 	LineNumber++;
708 	if (p == NULL)
709 	{
710 		buf[0] = '\0';
711 		return (NULL);
712 	}
713 	if (!EightBit)
714 		for (p = buf; *p != '\0'; p++)
715 			*p &= ~0200;
716 	return (buf);
717 }
718 
719 static
720 readtimeout()
721 {
722 	longjmp(CtxReadTimeout, 1);
723 }
724 /*
725 **  FGETFOLDED -- like fgets, but know about folded lines.
726 **
727 **	Parameters:
728 **		buf -- place to put result.
729 **		n -- bytes available.
730 **		f -- file to read from.
731 **
732 **	Returns:
733 **		input line(s) on success, NULL on error or EOF.
734 **		This will normally be buf -- unless the line is too
735 **			long, when it will be xalloc()ed.
736 **
737 **	Side Effects:
738 **		buf gets lines from f, with continuation lines (lines
739 **		with leading white space) appended.  CRLF's are mapped
740 **		into single newlines.  Any trailing NL is stripped.
741 */
742 
743 char *
744 fgetfolded(buf, n, f)
745 	char *buf;
746 	register int n;
747 	FILE *f;
748 {
749 	register char *p = buf;
750 	char *bp = buf;
751 	register int i;
752 
753 	n--;
754 	while ((i = getc(f)) != EOF)
755 	{
756 		if (i == '\r')
757 		{
758 			i = getc(f);
759 			if (i != '\n')
760 			{
761 				if (i != EOF)
762 					(void) ungetc(i, f);
763 				i = '\r';
764 			}
765 		}
766 		if (--n <= 0)
767 		{
768 			/* allocate new space */
769 			char *nbp;
770 			int nn;
771 
772 			nn = (p - bp);
773 			if (nn < MEMCHUNKSIZE)
774 				nn *= 2;
775 			else
776 				nn += MEMCHUNKSIZE;
777 			nbp = xalloc(nn);
778 			bcopy(bp, nbp, p - bp);
779 			p = &nbp[p - bp];
780 			if (bp != buf)
781 				free(bp);
782 			bp = nbp;
783 			n = nn - (p - bp);
784 		}
785 		*p++ = i;
786 		if (i == '\n')
787 		{
788 			LineNumber++;
789 			i = getc(f);
790 			if (i != EOF)
791 				(void) ungetc(i, f);
792 			if (i != ' ' && i != '\t')
793 				break;
794 		}
795 	}
796 	if (p == bp)
797 		return (NULL);
798 	*--p = '\0';
799 	return (bp);
800 }
801 /*
802 **  CURTIME -- return current time.
803 **
804 **	Parameters:
805 **		none.
806 **
807 **	Returns:
808 **		the current time.
809 **
810 **	Side Effects:
811 **		none.
812 */
813 
814 time_t
815 curtime()
816 {
817 	auto time_t t;
818 
819 	(void) time(&t);
820 	return (t);
821 }
822 /*
823 **  ATOBOOL -- convert a string representation to boolean.
824 **
825 **	Defaults to "TRUE"
826 **
827 **	Parameters:
828 **		s -- string to convert.  Takes "tTyY" as true,
829 **			others as false.
830 **
831 **	Returns:
832 **		A boolean representation of the string.
833 **
834 **	Side Effects:
835 **		none.
836 */
837 
838 bool
839 atobool(s)
840 	register char *s;
841 {
842 	if (*s == '\0' || strchr("tTyY", *s) != NULL)
843 		return (TRUE);
844 	return (FALSE);
845 }
846 /*
847 **  ATOOCT -- convert a string representation to octal.
848 **
849 **	Parameters:
850 **		s -- string to convert.
851 **
852 **	Returns:
853 **		An integer representing the string interpreted as an
854 **		octal number.
855 **
856 **	Side Effects:
857 **		none.
858 */
859 
860 atooct(s)
861 	register char *s;
862 {
863 	register int i = 0;
864 
865 	while (*s >= '0' && *s <= '7')
866 		i = (i << 3) | (*s++ - '0');
867 	return (i);
868 }
869 /*
870 **  WAITFOR -- wait for a particular process id.
871 **
872 **	Parameters:
873 **		pid -- process id to wait for.
874 **
875 **	Returns:
876 **		status of pid.
877 **		-1 if pid never shows up.
878 **
879 **	Side Effects:
880 **		none.
881 */
882 
883 waitfor(pid)
884 	int pid;
885 {
886 	auto int st;
887 	int i;
888 
889 	do
890 	{
891 		errno = 0;
892 		i = wait(&st);
893 	} while ((i >= 0 || errno == EINTR) && i != pid);
894 	if (i < 0)
895 		st = -1;
896 	return (st);
897 }
898 /*
899 **  BITINTERSECT -- tell if two bitmaps intersect
900 **
901 **	Parameters:
902 **		a, b -- the bitmaps in question
903 **
904 **	Returns:
905 **		TRUE if they have a non-null intersection
906 **		FALSE otherwise
907 **
908 **	Side Effects:
909 **		none.
910 */
911 
912 bool
913 bitintersect(a, b)
914 	BITMAP a;
915 	BITMAP b;
916 {
917 	int i;
918 
919 	for (i = BITMAPBYTES / sizeof (int); --i >= 0; )
920 		if ((a[i] & b[i]) != 0)
921 			return (TRUE);
922 	return (FALSE);
923 }
924 /*
925 **  BITZEROP -- tell if a bitmap is all zero
926 **
927 **	Parameters:
928 **		map -- the bit map to check
929 **
930 **	Returns:
931 **		TRUE if map is all zero.
932 **		FALSE if there are any bits set in map.
933 **
934 **	Side Effects:
935 **		none.
936 */
937 
938 bool
939 bitzerop(map)
940 	BITMAP map;
941 {
942 	int i;
943 
944 	for (i = BITMAPBYTES / sizeof (int); --i >= 0; )
945 		if (map[i] != 0)
946 			return (FALSE);
947 	return (TRUE);
948 }
949 /*
950 **  STRCONTAINEDIN -- tell if one string is contained in another
951 **
952 **	Parameters:
953 **		a -- possible substring.
954 **		b -- possible superstring.
955 **
956 **	Returns:
957 **		TRUE if a is contained in b.
958 **		FALSE otherwise.
959 */
960 
961 bool
962 strcontainedin(a, b)
963 	register char *a;
964 	register char *b;
965 {
966 	int l;
967 
968 	l = strlen(a);
969 	for (;;)
970 	{
971 		b = strchr(b, a[0]);
972 		if (b == NULL)
973 			return FALSE;
974 		if (strncmp(a, b, l) == 0)
975 			return TRUE;
976 		b++;
977 	}
978 }
979