xref: /freebsd/usr.bin/mail/list.c (revision 5e3934b15a2741b2de6b217e77dc9d798d740804)
1 /*-
2  * SPDX-License-Identifier: BSD-3-Clause
3  *
4  * Copyright (c) 1980, 1993
5  *	The Regents of the University of California.  All rights reserved.
6  *
7  * Redistribution and use in source and binary forms, with or without
8  * modification, are permitted provided that the following conditions
9  * are met:
10  * 1. Redistributions of source code must retain the above copyright
11  *    notice, this list of conditions and the following disclaimer.
12  * 2. Redistributions in binary form must reproduce the above copyright
13  *    notice, this list of conditions and the following disclaimer in the
14  *    documentation and/or other materials provided with the distribution.
15  * 3. Neither the name of the University nor the names of its contributors
16  *    may be used to endorse or promote products derived from this software
17  *    without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
20  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22  * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
23  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
25  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
28  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
29  * SUCH DAMAGE.
30  */
31 
32 #include "rcv.h"
33 #include <ctype.h>
34 #include "extern.h"
35 
36 /*
37  * Mail -- a mail program
38  *
39  * Message list handling.
40  */
41 
42 /*
43  * Convert the user string of message numbers and
44  * store the numbers into vector.
45  *
46  * Returns the count of messages picked up or -1 on error.
47  */
48 int
getmsglist(char * buf,int * vector,int flags)49 getmsglist(char *buf, int *vector, int flags)
50 {
51 	int *ip;
52 	struct message *mp;
53 
54 	if (msgCount == 0) {
55 		*vector = 0;
56 		return (0);
57 	}
58 	if (markall(buf, flags) < 0)
59 		return (-1);
60 	ip = vector;
61 	for (mp = &message[0]; mp < &message[msgCount]; mp++)
62 		if (mp->m_flag & MMARK)
63 			*ip++ = mp - &message[0] + 1;
64 	*ip = 0;
65 	return (ip - vector);
66 }
67 
68 /*
69  * Mark all messages that the user wanted from the command
70  * line in the message structure.  Return 0 on success, -1
71  * on error.
72  */
73 
74 /*
75  * Bit values for colon modifiers.
76  */
77 
78 #define	CMNEW		01		/* New messages */
79 #define	CMOLD		02		/* Old messages */
80 #define	CMUNREAD	04		/* Unread messages */
81 #define	CMDELETED	010		/* Deleted messages */
82 #define	CMREAD		020		/* Read messages */
83 
84 /*
85  * The following table describes the letters which can follow
86  * the colon and gives the corresponding modifier bit.
87  */
88 
89 static struct coltab {
90 	char	co_char;		/* What to find past : */
91 	int	co_bit;			/* Associated modifier bit */
92 	int	co_mask;		/* m_status bits to mask */
93 	int	co_equal;		/* ... must equal this */
94 } coltab[] = {
95 	{ 'n',		CMNEW,		MNEW,		MNEW	},
96 	{ 'o',		CMOLD,		MNEW,		0	},
97 	{ 'u',		CMUNREAD,	MREAD,		0	},
98 	{ 'd',		CMDELETED,	MDELETED,	MDELETED},
99 	{ 'r',		CMREAD,		MREAD,		MREAD	},
100 	{ 0,		0,		0,		0	}
101 };
102 
103 static	int	lastcolmod;
104 
105 int
markall(char buf[],int f)106 markall(char buf[], int f)
107 {
108 	char **np;
109 	int i;
110 	struct message *mp;
111 	char *namelist[NMLSIZE], *bufp;
112 	int tok, beg, mc, star, other, valdot, colmod, colresult;
113 
114 	valdot = dot - &message[0] + 1;
115 	colmod = 0;
116 	for (i = 1; i <= msgCount; i++)
117 		unmark(i);
118 	bufp = buf;
119 	mc = 0;
120 	np = &namelist[0];
121 	scaninit();
122 	tok = scan(&bufp);
123 	star = 0;
124 	other = 0;
125 	beg = 0;
126 	while (tok != TEOL) {
127 		switch (tok) {
128 		case TNUMBER:
129 number:
130 			if (star) {
131 				printf("No numbers mixed with *\n");
132 				return (-1);
133 			}
134 			mc++;
135 			other++;
136 			if (beg != 0) {
137 				if (check(lexnumber, f))
138 					return (-1);
139 				for (i = beg; i <= lexnumber; i++)
140 					if (f == MDELETED || (message[i - 1].m_flag & MDELETED) == 0)
141 						mark(i);
142 				beg = 0;
143 				break;
144 			}
145 			beg = lexnumber;
146 			if (check(beg, f))
147 				return (-1);
148 			tok = scan(&bufp);
149 			regret(tok);
150 			if (tok != TDASH) {
151 				mark(beg);
152 				beg = 0;
153 			}
154 			break;
155 
156 		case TPLUS:
157 			if (beg != 0) {
158 				printf("Non-numeric second argument\n");
159 				return (-1);
160 			}
161 			i = valdot;
162 			do {
163 				i++;
164 				if (i > msgCount) {
165 					printf("Referencing beyond EOF\n");
166 					return (-1);
167 				}
168 			} while ((message[i - 1].m_flag & MDELETED) != f);
169 			mark(i);
170 			break;
171 
172 		case TDASH:
173 			if (beg == 0) {
174 				i = valdot;
175 				do {
176 					i--;
177 					if (i <= 0) {
178 						printf("Referencing before 1\n");
179 						return (-1);
180 					}
181 				} while ((message[i - 1].m_flag & MDELETED) != f);
182 				mark(i);
183 			}
184 			break;
185 
186 		case TSTRING:
187 			if (beg != 0) {
188 				printf("Non-numeric second argument\n");
189 				return (-1);
190 			}
191 			other++;
192 			if (lexstring[0] == ':') {
193 				colresult = evalcol(lexstring[1]);
194 				if (colresult == 0) {
195 					printf("Unknown colon modifier \"%s\"\n",
196 					    lexstring);
197 					return (-1);
198 				}
199 				colmod |= colresult;
200 			}
201 			else
202 				*np++ = savestr(lexstring);
203 			break;
204 
205 		case TDOLLAR:
206 		case TUP:
207 		case TDOT:
208 			lexnumber = metamess(lexstring[0], f);
209 			if (lexnumber == -1)
210 				return (-1);
211 			goto number;
212 
213 		case TSTAR:
214 			if (other) {
215 				printf("Can't mix \"*\" with anything\n");
216 				return (-1);
217 			}
218 			star++;
219 			break;
220 
221 		case TERROR:
222 			return (-1);
223 		}
224 		tok = scan(&bufp);
225 	}
226 	lastcolmod = colmod;
227 	*np = NULL;
228 	mc = 0;
229 	if (star) {
230 		for (i = 0; i < msgCount; i++)
231 			if ((message[i].m_flag & MDELETED) == f) {
232 				mark(i+1);
233 				mc++;
234 			}
235 		if (mc == 0) {
236 			printf("No applicable messages.\n");
237 			return (-1);
238 		}
239 		return (0);
240 	}
241 
242 	/*
243 	 * If no numbers were given, mark all of the messages,
244 	 * so that we can unmark any whose sender was not selected
245 	 * if any user names were given.
246 	 */
247 
248 	if ((np > namelist || colmod != 0) && mc == 0)
249 		for (i = 1; i <= msgCount; i++)
250 			if ((message[i-1].m_flag & MDELETED) == f)
251 				mark(i);
252 
253 	/*
254 	 * If any names were given, go through and eliminate any
255 	 * messages whose senders were not requested.
256 	 */
257 
258 	if (np > namelist) {
259 		for (i = 1; i <= msgCount; i++) {
260 			for (mc = 0, np = &namelist[0]; *np != NULL; np++)
261 				if (**np == '/') {
262 					if (matchfield(*np, i)) {
263 						mc++;
264 						break;
265 					}
266 				}
267 				else {
268 					if (matchsender(*np, i)) {
269 						mc++;
270 						break;
271 					}
272 				}
273 			if (mc == 0)
274 				unmark(i);
275 		}
276 
277 		/*
278 		 * Make sure we got some decent messages.
279 		 */
280 
281 		mc = 0;
282 		for (i = 1; i <= msgCount; i++)
283 			if (message[i-1].m_flag & MMARK) {
284 				mc++;
285 				break;
286 			}
287 		if (mc == 0) {
288 			printf("No applicable messages from {%s",
289 				namelist[0]);
290 			for (np = &namelist[1]; *np != NULL; np++)
291 				printf(", %s", *np);
292 			printf("}\n");
293 			return (-1);
294 		}
295 	}
296 
297 	/*
298 	 * If any colon modifiers were given, go through and
299 	 * unmark any messages which do not satisfy the modifiers.
300 	 */
301 
302 	if (colmod != 0) {
303 		for (i = 1; i <= msgCount; i++) {
304 			struct coltab *colp;
305 
306 			mp = &message[i - 1];
307 			for (colp = &coltab[0]; colp->co_char != '\0'; colp++)
308 				if (colp->co_bit & colmod)
309 					if ((mp->m_flag & colp->co_mask)
310 					    != colp->co_equal)
311 						unmark(i);
312 
313 		}
314 		for (mp = &message[0]; mp < &message[msgCount]; mp++)
315 			if (mp->m_flag & MMARK)
316 				break;
317 		if (mp >= &message[msgCount]) {
318 			struct coltab *colp;
319 
320 			printf("No messages satisfy");
321 			for (colp = &coltab[0]; colp->co_char != '\0'; colp++)
322 				if (colp->co_bit & colmod)
323 					printf(" :%c", colp->co_char);
324 			printf("\n");
325 			return (-1);
326 		}
327 	}
328 	return (0);
329 }
330 
331 /*
332  * Turn the character after a colon modifier into a bit
333  * value.
334  */
335 int
evalcol(int col)336 evalcol(int col)
337 {
338 	struct coltab *colp;
339 
340 	if (col == 0)
341 		return (lastcolmod);
342 	for (colp = &coltab[0]; colp->co_char != '\0'; colp++)
343 		if (colp->co_char == col)
344 			return (colp->co_bit);
345 	return (0);
346 }
347 
348 /*
349  * Check the passed message number for legality and proper flags.
350  * If f is MDELETED, then either kind will do.  Otherwise, the message
351  * has to be undeleted.
352  */
353 int
check(int mesg,int f)354 check(int mesg, int f)
355 {
356 	struct message *mp;
357 
358 	if (mesg < 1 || mesg > msgCount) {
359 		printf("%d: Invalid message number\n", mesg);
360 		return (-1);
361 	}
362 	mp = &message[mesg-1];
363 	if (f != MDELETED && (mp->m_flag & MDELETED) != 0) {
364 		printf("%d: Inappropriate message\n", mesg);
365 		return (-1);
366 	}
367 	return (0);
368 }
369 
370 /*
371  * Scan out the list of string arguments, shell style
372  * for a RAWLIST.
373  */
374 int
getrawlist(char line[],char ** argv,int argc)375 getrawlist(char line[], char **argv, int argc)
376 {
377 	char c, *cp, *cp2, quotec;
378 	int argn;
379 	char *linebuf;
380 	size_t linebufsize = BUFSIZ;
381 
382 	if ((linebuf = malloc(linebufsize)) == NULL)
383 		err(1, "Out of memory");
384 
385 	argn = 0;
386 	cp = line;
387 	for (;;) {
388 		for (; *cp == ' ' || *cp == '\t'; cp++)
389 			;
390 		if (*cp == '\0')
391 			break;
392 		if (argn >= argc - 1) {
393 			printf(
394 			"Too many elements in the list; excess discarded.\n");
395 			break;
396 		}
397 		cp2 = linebuf;
398 		quotec = '\0';
399 		while ((c = *cp) != '\0') {
400 			/* Allocate more space if necessary */
401 			if (cp2 - linebuf == linebufsize - 1) {
402 				linebufsize += BUFSIZ;
403 				if ((linebuf = realloc(linebuf, linebufsize)) == NULL)
404 					err(1, "Out of memory");
405 				cp2 = linebuf + linebufsize - BUFSIZ - 1;
406 			}
407 			cp++;
408 			if (quotec != '\0') {
409 				if (c == quotec)
410 					quotec = '\0';
411 				else if (c == '\\')
412 					switch (c = *cp++) {
413 					case '\0':
414 						*cp2++ = '\\';
415 						cp--;
416 						break;
417 					case '0': case '1': case '2': case '3':
418 					case '4': case '5': case '6': case '7':
419 						c -= '0';
420 						if (*cp >= '0' && *cp <= '7')
421 							c = c * 8 + *cp++ - '0';
422 						if (*cp >= '0' && *cp <= '7')
423 							c = c * 8 + *cp++ - '0';
424 						*cp2++ = c;
425 						break;
426 					case 'b':
427 						*cp2++ = '\b';
428 						break;
429 					case 'f':
430 						*cp2++ = '\f';
431 						break;
432 					case 'n':
433 						*cp2++ = '\n';
434 						break;
435 					case 'r':
436 						*cp2++ = '\r';
437 						break;
438 					case 't':
439 						*cp2++ = '\t';
440 						break;
441 					case 'v':
442 						*cp2++ = '\v';
443 						break;
444 					default:
445 						*cp2++ = c;
446 					}
447 				else if (c == '^') {
448 					c = *cp++;
449 					if (c == '?')
450 						*cp2++ = '\177';
451 					/* null doesn't show up anyway */
452 					else if ((c >= 'A' && c <= '_') ||
453 					    (c >= 'a' && c <= 'z'))
454 						*cp2++ = c & 037;
455 					else {
456 						*cp2++ = '^';
457 						cp--;
458 					}
459 				} else
460 					*cp2++ = c;
461 			} else if (c == '"' || c == '\'')
462 				quotec = c;
463 			else if (c == ' ' || c == '\t')
464 				break;
465 			else
466 				*cp2++ = c;
467 		}
468 		*cp2 = '\0';
469 		argv[argn++] = savestr(linebuf);
470 	}
471 	argv[argn] = NULL;
472 	(void)free(linebuf);
473 	return (argn);
474 }
475 
476 /*
477  * scan out a single lexical item and return its token number,
478  * updating the string pointer passed **p.  Also, store the value
479  * of the number or string scanned in lexnumber or lexstring as
480  * appropriate.  In any event, store the scanned `thing' in lexstring.
481  */
482 
483 static struct lex {
484 	char	l_char;
485 	char	l_token;
486 } singles[] = {
487 	{ '$',	TDOLLAR	},
488 	{ '.',	TDOT	},
489 	{ '^',	TUP 	},
490 	{ '*',	TSTAR 	},
491 	{ '-',	TDASH 	},
492 	{ '+',	TPLUS 	},
493 	{ '(',	TOPEN 	},
494 	{ ')',	TCLOSE 	},
495 	{ 0,	0 	}
496 };
497 
498 int
scan(char ** sp)499 scan(char **sp)
500 {
501 	char *cp, *cp2;
502 	int c;
503 	struct lex *lp;
504 	int quotec;
505 
506 	if (regretp >= 0) {
507 		strcpy(lexstring, string_stack[regretp]);
508 		lexnumber = numberstack[regretp];
509 		return (regretstack[regretp--]);
510 	}
511 	cp = *sp;
512 	cp2 = lexstring;
513 	c = *cp++;
514 
515 	/*
516 	 * strip away leading white space.
517 	 */
518 
519 	while (c == ' ' || c == '\t')
520 		c = *cp++;
521 
522 	/*
523 	 * If no characters remain, we are at end of line,
524 	 * so report that.
525 	 */
526 
527 	if (c == '\0') {
528 		*sp = --cp;
529 		return (TEOL);
530 	}
531 
532 	/*
533 	 * If the leading character is a digit, scan
534 	 * the number and convert it on the fly.
535 	 * Return TNUMBER when done.
536 	 */
537 
538 	if (isdigit((unsigned char)c)) {
539 		lexnumber = 0;
540 		while (isdigit((unsigned char)c)) {
541 			lexnumber = lexnumber*10 + c - '0';
542 			*cp2++ = c;
543 			c = *cp++;
544 		}
545 		*cp2 = '\0';
546 		*sp = --cp;
547 		return (TNUMBER);
548 	}
549 
550 	/*
551 	 * Check for single character tokens; return such
552 	 * if found.
553 	 */
554 
555 	for (lp = &singles[0]; lp->l_char != '\0'; lp++)
556 		if (c == lp->l_char) {
557 			lexstring[0] = c;
558 			lexstring[1] = '\0';
559 			*sp = cp;
560 			return (lp->l_token);
561 		}
562 
563 	/*
564 	 * We've got a string!  Copy all the characters
565 	 * of the string into lexstring, until we see
566 	 * a null, space, or tab.
567 	 * If the lead character is a " or ', save it
568 	 * and scan until you get another.
569 	 */
570 
571 	quotec = 0;
572 	if (c == '\'' || c == '"') {
573 		quotec = c;
574 		c = *cp++;
575 	}
576 	while (c != '\0') {
577 		if (c == quotec) {
578 			cp++;
579 			break;
580 		}
581 		if (quotec == 0 && (c == ' ' || c == '\t'))
582 			break;
583 		if (cp2 - lexstring < STRINGLEN-1)
584 			*cp2++ = c;
585 		c = *cp++;
586 	}
587 	if (quotec && c == '\0') {
588 		fprintf(stderr, "Missing %c\n", quotec);
589 		return (TERROR);
590 	}
591 	*sp = --cp;
592 	*cp2 = '\0';
593 	return (TSTRING);
594 }
595 
596 /*
597  * Unscan the named token by pushing it onto the regret stack.
598  */
599 void
regret(int token)600 regret(int token)
601 {
602 	if (++regretp >= REGDEP)
603 		errx(1, "Too many regrets");
604 	regretstack[regretp] = token;
605 	lexstring[STRINGLEN-1] = '\0';
606 	string_stack[regretp] = savestr(lexstring);
607 	numberstack[regretp] = lexnumber;
608 }
609 
610 /*
611  * Reset all the scanner global variables.
612  */
613 void
scaninit(void)614 scaninit(void)
615 {
616 	regretp = -1;
617 }
618 
619 /*
620  * Find the first message whose flags & m == f  and return
621  * its message number.
622  */
623 int
first(int f,int m)624 first(int f, int m)
625 {
626 	struct message *mp;
627 
628 	if (msgCount == 0)
629 		return (0);
630 	f &= MDELETED;
631 	m &= MDELETED;
632 	for (mp = dot; mp < &message[msgCount]; mp++)
633 		if ((mp->m_flag & m) == f)
634 			return (mp - message + 1);
635 	for (mp = dot-1; mp >= &message[0]; mp--)
636 		if ((mp->m_flag & m) == f)
637 			return (mp - message + 1);
638 	return (0);
639 }
640 
641 /*
642  * See if the passed name sent the passed message number.  Return true
643  * if so.
644  */
645 int
matchsender(char * str,int mesg)646 matchsender(char *str, int mesg)
647 {
648 	char *cp;
649 
650 	/* null string matches nothing instead of everything */
651 	if (*str == '\0')
652 		return (0);
653 
654 	cp = nameof(&message[mesg - 1], 0);
655 	return (strcasestr(cp, str) != NULL);
656 }
657 
658 /*
659  * See if the passed name received the passed message number.  Return true
660  * if so.
661  */
662 
663 static char *to_fields[] = { "to", "cc", "bcc", NULL };
664 
665 static int
matchto(char * str,int mesg)666 matchto(char *str, int mesg)
667 {
668 	struct message *mp;
669 	char *cp, **to;
670 
671 	str++;
672 
673 	/* null string matches nothing instead of everything */
674 	if (*str == '\0')
675 		return (0);
676 
677 	mp = &message[mesg - 1];
678 
679 	for (to = to_fields; *to != NULL; to++) {
680 		cp = hfield(*to, mp);
681 		if (cp != NULL && strcasestr(cp, str) != NULL)
682 			return (1);
683 	}
684 	return (0);
685 }
686 
687 /*
688  * See if the given substring is contained within the specified field. If
689  * 'searchheaders' is set, then the form '/x:y' will be accepted and matches
690  * any message with the substring 'y' in field 'x'. If 'x' is omitted or
691  * 'searchheaders' is not set, then the search matches any messages
692  * with the substring 'y' in the 'Subject'. The search is case insensitive.
693  *
694  * The form '/to:y' is a special case, and will match all messages
695  * containing the substring 'y' in the 'To', 'Cc', or 'Bcc' header
696  * fields. The search for 'to' is case sensitive, so that '/To:y' can
697  * be used to limit the search to just the 'To' field.
698  */
699 
700 static char lastscan[STRINGLEN];
701 int
matchfield(char * str,int mesg)702 matchfield(char *str, int mesg)
703 {
704 	struct message *mp;
705 	char *cp, *cp2;
706 
707 	str++;
708 	if (*str == '\0')
709 		str = lastscan;
710 	else
711 		strlcpy(lastscan, str, sizeof(lastscan));
712 	mp = &message[mesg-1];
713 
714 	/*
715 	 * Now look, ignoring case, for the word in the string.
716 	 */
717 
718 	if (value("searchheaders") && (cp = strchr(str, ':')) != NULL) {
719 		/* Check for special case "/to:" */
720 		if (strncmp(str, "to:", 3) == 0)
721 			return (matchto(cp, mesg));
722 		*cp++ = '\0';
723 		cp2 = hfield(*str != '\0' ? str : "subject", mp);
724 		cp[-1] = ':';
725 		str = cp;
726 		cp = cp2;
727 	} else
728 		cp = hfield("subject", mp);
729 
730 	if (cp == NULL)
731 		return (0);
732 
733 	return (strcasestr(cp, str) != NULL);
734 }
735 
736 /*
737  * Mark the named message by setting its mark bit.
738  */
739 void
mark(int mesg)740 mark(int mesg)
741 {
742 	int i;
743 
744 	i = mesg;
745 	if (i < 1 || i > msgCount)
746 		errx(1, "Bad message number to mark");
747 	message[i-1].m_flag |= MMARK;
748 }
749 
750 /*
751  * Unmark the named message.
752  */
753 void
unmark(int mesg)754 unmark(int mesg)
755 {
756 	int i;
757 
758 	i = mesg;
759 	if (i < 1 || i > msgCount)
760 		errx(1, "Bad message number to unmark");
761 	message[i-1].m_flag &= ~MMARK;
762 }
763 
764 /*
765  * Return the message number corresponding to the passed meta character.
766  */
767 int
metamess(int meta,int f)768 metamess(int meta, int f)
769 {
770 	int c, m;
771 	struct message *mp;
772 
773 	c = meta;
774 	switch (c) {
775 	case '^':
776 		/*
777 		 * First 'good' message left.
778 		 */
779 		for (mp = &message[0]; mp < &message[msgCount]; mp++)
780 			if ((mp->m_flag & MDELETED) == f)
781 				return (mp - &message[0] + 1);
782 		printf("No applicable messages\n");
783 		return (-1);
784 
785 	case '$':
786 		/*
787 		 * Last 'good message left.
788 		 */
789 		for (mp = &message[msgCount-1]; mp >= &message[0]; mp--)
790 			if ((mp->m_flag & MDELETED) == f)
791 				return (mp - &message[0] + 1);
792 		printf("No applicable messages\n");
793 		return (-1);
794 
795 	case '.':
796 		/*
797 		 * Current message.
798 		 */
799 		m = dot - &message[0] + 1;
800 		if ((dot->m_flag & MDELETED) != f) {
801 			printf("%d: Inappropriate message\n", m);
802 			return (-1);
803 		}
804 		return (m);
805 
806 	default:
807 		printf("Unknown metachar (%c)\n", c);
808 		return (-1);
809 	}
810 }
811