1: <?php
2: /**
3: * RFC 822/2822/3490/5322 Email parser/validator.
4: *
5: * LICENSE:
6: *
7: * Copyright (c) 2001-2010, Richard Heyes
8: * Copyright (c) 2011-2012, Horde LLC
9: * All rights reserved.
10: *
11: * Redistribution and use in source and binary forms, with or without
12: * modification, are permitted provided that the following conditions
13: * are met:
14: *
15: * o Redistributions of source code must retain the above copyright
16: * notice, this list of conditions and the following disclaimer.
17: * o Redistributions in binary form must reproduce the above copyright
18: * notice, this list of conditions and the following disclaimer in the
19: * documentation and/or other materials provided with the distribution.
20: * o The names of the authors may not be used to endorse or promote
21: * products derived from this software without specific prior written
22: * permission.
23: *
24: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
25: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
26: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
27: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
28: * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
29: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
30: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
31: * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
32: * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
33: * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
34: * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35: *
36: *
37: * RFC822 parsing code adapted from message-address.c and rfc822-parser.c
38: * (Dovecot 2.1rc5)
39: * Original code released under LGPL-2.1
40: * Copyright (c) 2002-2011 Timo Sirainen <tss@iki.fi>
41: *
42: * @category Horde
43: * @package Mail
44: * @author Richard Heyes <richard@phpguru.org>
45: * @author Chuck Hagenbuch <chuck@horde.org
46: * @author Michael Slusarz <slusarz@horde.org>
47: * @copyright 2001-2010 Richard Heyes
48: * @copyright 2011-2012 Horde LLC
49: * @license http://www.horde.org/licenses/bsd New BSD License
50: */
51:
52: /**
53: * RFC 822/2822/3490/5322 Email parser/validator.
54: *
55: * @author Richard Heyes <richard@phpguru.org>
56: * @author Chuck Hagenbuch <chuck@horde.org>
57: * @author Michael Slusarz <slusarz@horde.org>
58: * @category Horde
59: * @license http://www.horde.org/licenses/bsd New BSD License
60: * @package Mail
61: */
62: class Horde_Mail_Rfc822
63: {
64: /**
65: * The number of groups that have been found in the address list.
66: *
67: * @deprecated
68: *
69: * @var integer
70: */
71: public $num_groups = 0;
72:
73: /**
74: * The address string to parse.
75: *
76: * @var string
77: */
78: protected $_data;
79:
80: /**
81: * Length of the address string.
82: *
83: * @var integer
84: */
85: protected $_datalen;
86:
87: /**
88: * Comment cache.
89: *
90: * @var string
91: */
92: protected $_comments = array();
93:
94: /**
95: * Configuration parameters.
96: *
97: * @var array
98: */
99: protected $_params = array();
100:
101: /**
102: * Data pointer.
103: *
104: * @var integer
105: */
106: protected $_ptr;
107:
108: /**
109: * Structured data to return.
110: *
111: * @var array
112: */
113: protected $_structure;
114:
115: /**
116: * Starts the whole process.
117: *
118: * @param mixed $address The address(es) to validate. Either a string
119: * (since 1.0.0), a Horde_Mail_Rfc822_Object (since
120: * 1.2.0), or an array of strings and/or
121: * Horde_Mail_Rfc822_Objects (since 1.2.0).
122: * @param array $params Optional parameters:
123: * - default_domain: (string) Default domain/host etc.
124: * DEFAULT: localhost
125: * - limit: (integer) Stop processing after this many addresses.
126: * DEFAULT: No limit (0)
127: * - nest_groups: (boolean) Whether to return the structure with groups
128: * nested for easier viewing.
129: * DEFAULT: true
130: * - validate: (boolean) Strict validation of personal part data? If
131: * false, attempts to allow non-ASCII characters and
132: * non-quoted strings in the personal data, and will
133: * silently abort if an unparseable address is found.
134: * DEFAULT: true
135: *
136: * @return array A structured array of addresses. Each value is a
137: * Horde_Mail_Rfc822_Address object (or, if 'nest_groups'
138: * is true, the value can also be a Horde_Mail_Rfc822_Group
139: * object).
140: *
141: * @throws Horde_Mail_Exception
142: */
143: public function parseAddressList($address, array $params = array())
144: {
145: $this->_params = array_merge(array(
146: 'default_domain' => 'localhost',
147: 'limit' => 0,
148: 'nest_groups' => true,
149: 'validate' => true
150: ), $params);
151:
152: $this->_structure = array();
153:
154: if (!is_array($address)) {
155: $address = array($address);
156: }
157:
158: $tmp = array();
159: foreach ($address as $val) {
160: if ($val instanceof Horde_Mail_Rfc822_Object) {
161: $this->_structure[] = $val;
162: } else {
163: $tmp[] = rtrim(trim($val), ',');
164: }
165: }
166:
167: if (!empty($tmp)) {
168: $this->_data = implode(',', $tmp);
169: $this->_datalen = strlen($this->_data);
170: $this->_ptr = 0;
171:
172: $this->_parseAddressList();
173: }
174:
175: return $this->_structure;
176: }
177:
178: /**
179: * Quotes and escapes the given string if necessary using rules contained
180: * in RFC 2822 [3.2.5].
181: *
182: * @since 1.2.0
183: *
184: * @param string $str The string to be quoted and escaped.
185: * @param string $type Either 'address', or 'personal'.
186: *
187: * @return string The correctly quoted and escaped string.
188: */
189: public function encode($str, $type = 'address')
190: {
191: // Excluded (in ASCII): 0-8, 10-31, 34, 40-41, 44, 58-60, 62, 64,
192: // 91-93, 127
193: $filter = "\0\1\2\3\4\5\6\7\10\12\13\14\15\16\17\20\21\22\23\24\25\26\27\30\31\32\33\34\35\36\37\"(),:;<>@[\\]\177";
194:
195: switch ($type) {
196: case 'personal':
197: // RFC 2822 [3.4]: Period not allowed in display name
198: $filter .= '.';
199: break;
200:
201: case 'address':
202: default:
203: // RFC 2822 [3.4.1]: (HTAB, SPACE) not allowed in address
204: $filter .= "\11\40";
205: break;
206: }
207:
208: // Strip double quotes if they are around the string already.
209: // If quoted, we know that the contents are already escaped, so
210: // unescape now.
211: $str = trim($str);
212: if ($str && ($str[0] == '"') && (substr($str, -1) == '"')) {
213: $str = stripslashes(substr($str, 1, -1));
214: }
215:
216: return (strcspn($str, $filter) != strlen($str))
217: ? '"' . addcslashes($str, '\\"') . '"'
218: : $str;
219: }
220:
221: /**
222: * If an email address has no personal information, get rid of any angle
223: * brackets (<>) around it.
224: *
225: * @since 1.2.0
226: *
227: * @param string $address The address to trim.
228: *
229: * @return string The trimmed address.
230: */
231: public function trimAddress($address)
232: {
233: $address = trim($address);
234:
235: return (($address[0] == '<') && (substr($address, -1) == '>'))
236: ? substr($address, 1, -1)
237: : $address;
238: }
239:
240: /* RFC 822 parsing methods. */
241:
242: /**
243: * address-list = (address *("," address)) / obs-addr-list
244: */
245: protected function _parseAddressList()
246: {
247: $limit = empty($this->_params['limit'])
248: ? null
249: : $this->_params['limit'];
250:
251: while (($this->_curr() !== false) &&
252: (is_null($limit) || ($limit-- > 0))) {
253: try {
254: $this->_parseAddress();
255: } catch (Horde_Mail_Exception $e) {
256: if ($this->_params['validate']) {
257: throw $e;
258: }
259: ++$this->_ptr;
260: }
261:
262: switch ($this->_curr()) {
263: case ',':
264: $this->_rfc822SkipLwsp(true);
265: break;
266:
267: case false:
268: // No-op
269: break;
270:
271: default:
272: if ($this->_params['validate']) {
273: throw new Horde_Mail_Exception('Error when parsing address list.');
274: }
275: break;
276: }
277: }
278: }
279:
280: /**
281: * address = mailbox / group
282: */
283: protected function _parseAddress()
284: {
285: $start = $this->_ptr;
286: if (!$this->_parseGroup()) {
287: $this->_ptr = $start;
288: if ($mbox = $this->_parseMailbox()) {
289: $this->_structure[] = $mbox;
290: }
291: }
292: }
293:
294: /**
295: * group = display-name ":" [mailbox-list / CFWS] ";" [CFWS]
296: * display-name = phrase
297: *
298: * @return boolean True if a group was parsed.
299: *
300: * @throws Horde_Mail_Exception
301: */
302: protected function _parseGroup()
303: {
304: $this->_rfc822ParsePhrase($groupname);
305:
306: if ($this->_curr(true) != ':') {
307: return false;
308: }
309:
310: $addresses = array();
311:
312: $this->_rfc822SkipLwsp();
313:
314: while (($chr = $this->_curr()) !== false) {
315: if ($chr == ';') {
316: $this->_curr(true);
317:
318: if (!empty($addresses)) {
319: if ($this->_params['nest_groups']) {
320: $tmp = new Horde_Mail_Rfc822_Group();
321: $tmp->addresses = $addresses;
322: $tmp->groupname = $groupname;
323: $this->_structure[] = $tmp;
324: } else {
325: $this->_structure = array_merge($this->_structure, $addresses);
326: }
327: }
328:
329: return true;
330: }
331:
332: /* mailbox-list = (mailbox *("," mailbox)) / obs-mbox-list */
333: $addresses[] = $this->_parseMailbox();
334:
335: switch ($this->_curr()) {
336: case ',':
337: $this->_rfc822SkipLwsp(true);
338: break;
339:
340: case ';':
341: // No-op
342: break;
343:
344: default:
345: break 2;
346: }
347: }
348:
349: throw new Horde_Mail_Exception('Error when parsing group.');
350: }
351:
352: /**
353: * mailbox = name-addr / addr-spec
354: *
355: * @return mixed Mailbox object if mailbox was parsed, or false.
356: */
357: protected function _parseMailbox()
358: {
359: $this->_comments = array();
360: $start = $this->_ptr;
361:
362: if (!($ob = $this->_parseNameAddr())) {
363: $this->_comments = array();
364: $this->_ptr = $start;
365: $ob = $this->_parseAddrSpec();
366: }
367:
368: if ($ob) {
369: $ob->comment = $this->_comments;
370: }
371:
372: return $ob;
373: }
374:
375: /**
376: * name-addr = [display-name] angle-addr
377: * display-name = phrase
378: *
379: * @return mixed Mailbox object, or false.
380: */
381: protected function _parseNameAddr()
382: {
383: $this->_rfc822ParsePhrase($personal);
384:
385: if ($ob = $this->_parseAngleAddr()) {
386: $ob->personal = $personal;
387: return $ob;
388: }
389:
390: return false;
391: }
392:
393: /**
394: * addr-spec = local-part "@" domain
395: *
396: * @return mixed Mailbox object.
397: *
398: * @throws Horde_Mail_Exception
399: */
400: protected function _parseAddrSpec()
401: {
402: $ob = new Horde_Mail_Rfc822_Address();
403: $ob->mailbox = $this->_parseLocalPart();
404: $ob->host = $this->_params['default_domain'];
405:
406: if ($this->_curr() == '@') {
407: $this->_rfc822ParseDomain($host);
408: $ob->host = $host;
409: }
410:
411: return $ob;
412: }
413:
414: /**
415: * local-part = dot-atom / quoted-string / obs-local-part
416: * obs-local-part = word *("." word)
417: *
418: * @return string The local part.
419: *
420: * @throws Horde_Mail_Exception
421: */
422: protected function _parseLocalPart()
423: {
424: if (($curr = $this->_curr()) === false) {
425: throw new Horde_Mail_Exception('Error when parsing local part.');
426: }
427:
428: if ($curr == '"') {
429: $this->_rfc822ParseQuotedString($str);
430: } else {
431: $this->_rfc822ParseDotAtom($str, ',;@');
432: }
433:
434: return $str;
435: }
436:
437: /**
438: * "<" [ "@" route ":" ] local-part "@" domain ">"
439: *
440: * @return mixed Mailbox object, or false.
441: *
442: * @throws Horde_Mail_Exception
443: */
444: protected function _parseAngleAddr()
445: {
446: if ($this->_curr() != '<') {
447: return false;
448: }
449:
450: $route = null;
451: $this->_rfc822SkipLwsp(true);
452:
453: if ($this->_curr() == '@') {
454: $route = $this->_parseDomainList();
455: if ($this->_curr() != ':') {
456: throw new Horde_Mail_Exception('Invalid route.');
457: }
458:
459: $this->_rfc822SkipLwsp(true);
460: }
461:
462: $ob = $this->_parseAddrSpec();
463:
464: if ($this->_curr() != '>') {
465: throw new Horde_Mail_Exception('Error when parsing angle address.');
466: }
467:
468: $this->_rfc822SkipLwsp(true);
469:
470: if ($route) {
471: $ob->route = $route;
472: }
473:
474: return $ob;
475: }
476:
477: /**
478: * obs-domain-list = "@" domain *(*(CFWS / "," ) [CFWS] "@" domain)
479: *
480: * @return array Routes.
481: *
482: * @throws Horde_Mail_Exception
483: */
484: protected function _parseDomainList()
485: {
486: $route = array();
487:
488: while ($this->_curr() !== false) {
489: $this->_rfc822ParseDomain($str);
490: $route[] = '@' . $str;
491:
492: $this->_rfc822SkipLwsp();
493: if ($this->_curr() != ',') {
494: return $route;
495: }
496: $this->_curr(true);
497: }
498:
499: throw new Horde_Mail_Exception('Invalid domain list.');
500: }
501:
502: /* RFC 822 parsing methods. */
503:
504: /**
505: * phrase = 1*word / obs-phrase
506: * word = atom / quoted-string
507: * obs-phrase = word *(word / "." / CFWS)
508: *
509: * @param string &$phrase The phrase data.
510: *
511: * @throws Horde_Mail_Exception
512: */
513: protected function _rfc822ParsePhrase(&$phrase)
514: {
515: $curr = $this->_curr();
516: if (($curr === false) || ($curr == '.')) {
517: throw new Horde_Mail_Exception('Error when parsing a group.');
518: }
519:
520: while (($curr = $this->_curr()) !== false) {
521: if ($curr == '"') {
522: $this->_rfc822ParseQuotedString($phrase);
523: } else {
524: $this->_rfc822ParseAtomOrDot($phrase);
525: }
526:
527: $chr = $this->_curr();
528: if (!$this->_rfc822IsAtext($chr) &&
529: ($chr != '"') &&
530: ($chr != '.')) {
531: break;
532: }
533:
534: $phrase .= ' ';
535: }
536:
537: $this->_rfc822SkipLwsp();
538: }
539:
540: /**
541: * @param string &$phrase The quoted string data.
542: *
543: * @throws Horde_Mail_Exception
544: */
545: protected function _rfc822ParseQuotedString(&$str)
546: {
547: if ($this->_curr(true) != '"') {
548: throw new Horde_Mail_Exception('Error when parsing a quoted string.');
549: }
550:
551: while (($chr = $this->_curr(true)) !== false) {
552: switch ($chr) {
553: case '"':
554: $this->_rfc822SkipLwsp();
555: return;
556:
557: case "\n";
558: /* Folding whitespace, remove the (CR)LF. */
559: if ($str[strlen($str) - 1] == "\r") {
560: $str = substr($str, 0, -1);
561: }
562: continue;
563:
564: case '\\':
565: if (($chr = $this->_curr(true)) === false) {
566: break 2;
567: }
568: break;
569: }
570:
571: $str .= $chr;
572: }
573:
574: /* Missing trailing '"', or partial quoted character. */
575: throw new Horde_Mail_Exception('Error when parsing a quoted string.');
576: }
577:
578: /**
579: * dot-atom = [CFWS] dot-atom-text [CFWS]
580: * dot-atom-text = 1*atext *("." 1*atext)
581: *
582: * atext = ; Any character except controls, SP, and specials.
583: *
584: * For RFC-822 compatibility allow LWSP around '.'
585: *
586: * @param string &$str The atom/dot data.
587: * @param string $validate Use these characters as delimiter.
588: *
589: * @throws Horde_Mail_Exception
590: */
591: protected function _rfc822ParseDotAtom(&$str, $validate = null)
592: {
593: $curr = $this->_curr();
594: if (($curr === false) || !$this->_rfc822IsAtext($curr, $validate)) {
595: throw new Horde_Mail_Exception('Error when parsing dot-atom.');
596: }
597:
598: while (($chr = $this->_curr()) !== false) {
599: if ($this->_rfc822IsAtext($chr, $validate)) {
600: $str .= $chr;
601: $this->_curr(true);
602: } else {
603: $this->_rfc822SkipLwsp();
604:
605: if ($this->_curr() != '.') {
606: return;
607: }
608: $str .= '.';
609:
610: $this->_rfc822SkipLwsp(true);
611: }
612: }
613: }
614:
615: /**
616: * atom = [CFWS] 1*atext [CFWS]
617: * atext = ; Any character except controls, SP, and specials.
618: *
619: * This method doesn't just silently skip over WS.
620: *
621: * @param string &$str The atom/dot data.
622: *
623: * @throws Horde_Mail_Exception
624: */
625: protected function _rfc822ParseAtomOrDot(&$str)
626: {
627: while (($chr = $this->_curr()) !== false) {
628: if (($chr != '.') && !$this->_rfc822IsAtext($chr, ',<:')) {
629: $this->_rfc822SkipLwsp();
630: if (!$this->_params['validate']) {
631: $str = trim($str);
632: }
633: return;
634: }
635:
636: $str .= $chr;
637: $this->_curr(true);
638: }
639: }
640:
641: /**
642: * domain = dot-atom / domain-literal / obs-domain
643: * domain-literal = [CFWS] "[" *([FWS] dcontent) [FWS] "]" [CFWS]
644: * obs-domain = atom *("." atom)
645: *
646: * @param string &$str The domain string.
647: *
648: * @throws Horde_Mail_Exception
649: */
650: protected function _rfc822ParseDomain(&$str)
651: {
652: if ($this->_curr(true) != '@') {
653: throw new Horde_Mail_Exception('Error when parsing domain.');
654: }
655:
656: $this->_rfc822SkipLwsp();
657:
658: if ($this->_curr() == '[') {
659: $this->_rfc822ParseDomainLiteral($str);
660: } else {
661: $this->_rfc822ParseDotAtom($str, ';,>');
662: }
663: }
664:
665: /**
666: * domain-literal = [CFWS] "[" *([FWS] dcontent) [FWS] "]" [CFWS]
667: * dcontent = dtext / quoted-pair
668: * dtext = NO-WS-CTL / ; Non white space controls
669: * %d33-90 / ; The rest of the US-ASCII
670: * %d94-126 ; characters not including "[",
671: * ; "]", or "\"
672: *
673: * @param string &$str The domain string.
674: *
675: * @throws Horde_Mail_Exception
676: */
677: protected function _rfc822ParseDomainLiteral(&$str)
678: {
679: if ($this->_curr(true) != '[') {
680: throw new Horde_Mail_Exception('Error parsing domain literal.');
681: }
682:
683: while (($chr = $this->_curr(true)) !== false) {
684: switch ($chr) {
685: case '\\':
686: if (($chr = $this->_curr(true)) === false) {
687: break 2;
688: }
689: break;
690:
691: case ']':
692: $this->_rfc822SkipLwsp();
693: return;
694: }
695:
696: $str .= $chr;
697: }
698:
699: throw new Horde_Mail_Exception('Error parsing domain literal.');
700: }
701:
702: /**
703: * @param boolean $advance Advance cursor?
704: *
705: * @throws Horde_Mail_Exception
706: */
707: protected function _rfc822SkipLwsp($advance = false)
708: {
709: if ($advance) {
710: $this->_curr(true);
711: }
712:
713: while (($chr = $this->_curr()) !== false) {
714: switch ($chr) {
715: case ' ':
716: case "\n":
717: case "\r":
718: case "\t":
719: $this->_curr(true);
720: continue;
721:
722: case '(':
723: $this->_rfc822SkipComment();
724: break;
725:
726: default:
727: return;
728: }
729: }
730: }
731:
732: /**
733: * @throws Horde_Mail_Exception
734: */
735: protected function _rfc822SkipComment()
736: {
737: if ($this->_curr(true) != '(') {
738: throw new Horde_Mail_Exception('Error when parsing a comment.');
739: }
740:
741: $comment = '';
742: $level = 1;
743:
744: while (($chr = $this->_curr(true)) !== false) {
745: switch ($chr) {
746: case '(':
747: ++$level;
748: continue;
749:
750: case ')':
751: if (--$level == 0) {
752: $this->_comments[] = $comment;
753: return;
754: }
755: break;
756:
757: case '\\':
758: if (($chr = $this->_curr(true)) === false) {
759: break 2;
760: }
761: break;
762: }
763:
764: $comment .= $chr;
765: }
766:
767: throw new Horde_Mail_Exception('Error when parsing a comment.');
768: }
769:
770: /**
771: * Check if data is an atom.
772: *
773: * @param string $chr The character to check.
774: * @param string $validate If in non-validate mode, use these characters
775: * as the non-atom delimiters.
776: *
777: * @return boolean True if an atom.
778: */
779: protected function _rfc822IsAtext($chr, $validate = null)
780: {
781: if (is_null($chr)) {
782: return false;
783: }
784:
785: return ($this->_params['validate'] || is_null($validate))
786: ? !strcspn($chr, '!#$%&\'*+-./0123456789=?ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz{|}~')
787: : strcspn($chr, $validate);
788: }
789:
790: /* Helper methods. */
791:
792: /**
793: * Return current character.
794: *
795: * @param boolean $advance If true, advance the cursor.
796: *
797: * @return string The current character (false if EOF reached).
798: */
799: protected function _curr($advance = false)
800: {
801: return ($this->_ptr >= $this->_datalen)
802: ? false
803: : $this->_data[$advance ? $this->_ptr++ : $this->_ptr];
804: }
805:
806: /* Other public methods. */
807:
808: /**
809: * @deprecated Always returns true
810: */
811: public function validateMailbox(&$mailbox)
812: {
813: return true;
814: }
815:
816: /**
817: * Returns an approximate count of how many addresses are in the string.
818: * This is APPROXIMATE as it only splits based on a comma which has no
819: * preceding backslash.
820: *
821: * @param string $data Addresses to count.
822: *
823: * @return integer Approximate count.
824: */
825: public function approximateCount($data)
826: {
827: return count(preg_split('/(?<!\\\\),/', $data));
828: }
829:
830: /**
831: * Validates whether an email is of the common internet form:
832: * <user>@<domain>. This can be sufficient for most people.
833: *
834: * Optional stricter mode can be utilized which restricts mailbox
835: * characters allowed to: alphanumeric, full stop, hyphen, and underscore.
836: *
837: * @param string $data Address to check.
838: * @param boolean $strict Strict check?
839: *
840: * @return mixed False if it fails, an indexed array username/domain if
841: * it matches.
842: */
843: public function isValidInetAddress($data, $strict = false)
844: {
845: $regex = $strict
846: ? '/^([.0-9a-z_+-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i'
847: : '/^([*+!.&#$|\'\\%\/0-9a-z^_`{}=?~:-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i';
848:
849: return preg_match($regex, trim($data), $matches)
850: ? array($matches[1], $matches[2])
851: : false;
852: }
853:
854: }
855: