From 0813d76cb09f4672bdf893fb4a6eef49a00593db Mon Sep 17 00:00:00 2001 From: Thomas Wouters Date: Sat, 20 Mar 2004 17:31:29 +0000 Subject: Merge in Anthony's new parser code, from the anthony-parser-branch: > ---------------------------- > revision 1.20.4.4 > date: 2003/06/12 09:14:17; author: anthonybaxter; state: Exp; lines: +13 -6 > preamble is None when missing, not ''. > Handle a couple of bogus formatted messages - now parses my main testsuite. > Handle message/external-body. > ---------------------------- > revision 1.20.4.3 > date: 2003/06/12 07:16:40; author: anthonybaxter; state: Exp; lines: +6 -4 > epilogue-processing is now the same as the old parser - the newline at the > end of the line with the --endboundary-- is included as part of the epilogue. > Note that any whitespace after the boundary is _not_ part of the epilogue. > ---------------------------- > revision 1.20.4.2 > date: 2003/06/12 06:39:09; author: anthonybaxter; state: Exp; lines: +6 -4 > message/delivery-status fixed. > HeaderParser fixed. > ---------------------------- > revision 1.20.4.1 > date: 2003/06/12 06:08:56; author: anthonybaxter; state: Exp; lines: +163 -129 > A work-in-progress snapshot of the new parser. A couple of known problems: > > - first (blank) line of MIME epilogues is being consumed > - message/delivery-status isn't quite right > > It still needs a lot of cleanup, but right now it parses a whole lot of > badness that the old parser failed on. I also need to think about adding > back the old 'strict' flag in some way. > ============================================================================= --- Lib/email/Parser.py | 301 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 173 insertions(+), 128 deletions(-) diff --git a/Lib/email/Parser.py b/Lib/email/Parser.py index 09fac45..3fe1990 100644 --- a/Lib/email/Parser.py +++ b/Lib/email/Parser.py @@ -22,6 +22,75 @@ except NameError: NLCRE = re.compile('\r\n|\r|\n') +class TextUtil: + """ A utility class for wrapping a file object and providing a + couple of additional useful functions. + """ + + def __init__(self, fp): + self.fp = fp + self.unread = [] + + def readline(self): + """ Return a line of data. + + If data has been pushed back with unreadline(), the most recently + returned unreadline()d data will be returned. + """ + if self.unread: + return self.unread.pop() + else: + return self.fp.readline() + + def unreadline(self, line): + """Push a line back into the object. + """ + self.unread.append(line) + + def peekline(self): + """Non-destructively look at the next line""" + line = self.readline() + self.unreadline(line) + return line + + def read(self): + """Return the remaining data + """ + r = self.fp.read() + if self.unread: + r = "\n".join(self.unread) + r + self.unread = [] + return r + + def readuntil(self, re, afterblank=0, includematch=0): + """Read a line at a time until we get the specified RE. + + Returns the text up to (and including, if includematch is true) the + matched text, and the RE match object. If afterblank is true, + there must be a blank line before the matched text. Moves current + filepointer to the line following the matched line. If we reach + end-of-file, return what we've got so far, and return None as the + RE match object. + """ + prematch = [] + blankseen = 0 + while 1: + line = self.readline() + if not line: + # end of file + return EMPTYSTRING.join(prematch), None + if afterblank: + if NLCRE.match(line): + blankseen = 1 + continue + else: + blankseen = 0 + m = re.match(line) + if (m and not afterblank) or (m and afterblank and blankseen): + if includematch: + prematch.append(line) + return EMPTYSTRING.join(prematch), m + prematch.append(line) class Parser: @@ -59,9 +128,13 @@ class Parser: meaning it parses the entire contents of the file. """ root = self._class() - firstbodyline = self._parseheaders(root, fp) + fp = TextUtil(fp) + self._parseheaders(root, fp) if not headersonly: - self._parsebody(root, fp, firstbodyline) + obj = self._parsemessage(root, fp) + trailer = fp.read() + if obj and trailer: + self._attach_trailer(obj, trailer) return root def parsestr(self, text, headersonly=False): @@ -80,7 +153,6 @@ class Parser: lastheader = '' lastvalue = [] lineno = 0 - firstbodyline = None while True: # Don't strip the line before we test for the end condition, # because whitespace-only header lines are RFC compliant @@ -129,7 +201,7 @@ class Parser: # There was no separating blank line as mandated by RFC # 2822, but we're in non-strict mode. So just offer up # this current line as the first body line. - firstbodyline = line + fp.unreadline(line) break if lastheader: container[lastheader] = NL.join(lastvalue) @@ -138,140 +210,114 @@ class Parser: # Make sure we retain the last header if lastheader: container[lastheader] = NL.join(lastvalue) - return firstbodyline + return - def _parsebody(self, container, fp, firstbodyline=None): - # Parse the body, but first split the payload on the content-type - # boundary if present. + def _parsemessage(self, container, fp): + # Parse the body. We walk through the body from top to bottom, + # keeping track of the current multipart nesting as we go. + # We return the object that gets the data at the end of this + # block. boundary = container.get_boundary() isdigest = (container.get_content_type() == 'multipart/digest') - # If there's a boundary, split the payload text into its constituent - # parts and parse each separately. Otherwise, just parse the rest of - # the body as a single message. Note: any exceptions raised in the - # recursive parse need to have their line numbers coerced. - if boundary: - preamble = epilogue = None - # Split into subparts. The first boundary we're looking for won't - # always have a leading newline since we're at the start of the - # body text, and there's not always a preamble before the first - # boundary. + if boundary: separator = '--' + boundary - payload = fp.read() - if firstbodyline is not None: - payload = firstbodyline + '\n' + payload - # We use an RE here because boundaries can have trailing - # whitespace. - mo = re.search( - r'(?P' + re.escape(separator) + r')(?P[ \t]*)', - payload) - if not mo: - if self._strict: - raise Errors.BoundaryError( - "Couldn't find starting boundary: %s" % boundary) - container.set_payload(payload) - return - start = mo.start() - if start > 0: - # there's some pre-MIME boundary preamble - preamble = payload[0:start] - # Find out what kind of line endings we're using - start += len(mo.group('sep')) + len(mo.group('ws')) - mo = NLCRE.search(payload, start) - if mo: - start += len(mo.group(0)) - # We create a compiled regexp first because we need to be able to - # specify the start position, and the module function doesn't - # support this signature. :( - cre = re.compile('(?P\r\n|\r|\n)' + - re.escape(separator) + '--') - mo = cre.search(payload, start) - if mo: - terminator = mo.start() - linesep = mo.group('sep') - if mo.end() < len(payload): - # There's some post-MIME boundary epilogue - epilogue = payload[mo.end():] - elif self._strict: - raise Errors.BoundaryError( - "Couldn't find terminating boundary: %s" % boundary) + boundaryRE = re.compile( + r'(?P' + re.escape(separator) + + r')(?P--)?(?P[ \t]*)(?P\r\n|\r|\n)$') + preamble, matchobj = fp.readuntil(boundaryRE) + if not matchobj: + # Broken - we hit the end of file. Just set the body + # to the text. + container.set_payload(preamble) + return container + if preamble: + container.preamble = preamble else: - # Handle the case of no trailing boundary. Check that it ends - # in a blank line. Some cases (spamspamspam) don't even have - # that! - mo = re.search('(?P\r\n|\r|\n){2}$', payload) - if not mo: - mo = re.search('(?P\r\n|\r|\n)$', payload) - if not mo: - raise Errors.BoundaryError( - 'No terminating boundary and no trailing empty line') - linesep = mo.group('sep') - terminator = len(payload) - # We split the textual payload on the boundary separator, which - # includes the trailing newline. If the container is a - # multipart/digest then the subparts are by default message/rfc822 - # instead of text/plain. In that case, they'll have a optional - # block of MIME headers, then an empty line followed by the - # message headers. - parts = re.split( - linesep + re.escape(separator) + r'[ \t]*' + linesep, - payload[start:terminator]) - for part in parts: + # The module docs specify an empty preamble is None, not '' + container.preamble = None + while 1: + subobj = self._class() if isdigest: - if part.startswith(linesep): - # There's no header block so create an empty message - # object as the container, and lop off the newline so - # we can parse the sub-subobject - msgobj = self._class() - part = part[len(linesep):] + subobj.set_default_type('message/rfc822') + firstline = fp.peekline() + if firstline.strip(): + # we have MIME headers. all good. + self._parseheaders(subobj, fp) else: - parthdrs, part = part.split(linesep+linesep, 1) - # msgobj in this case is the "message/rfc822" container - msgobj = self.parsestr(parthdrs, headersonly=1) - # while submsgobj is the message itself - msgobj.set_default_type('message/rfc822') - maintype = msgobj.get_content_maintype() - if maintype in ('message', 'multipart'): - submsgobj = self.parsestr(part) - msgobj.attach(submsgobj) - else: - msgobj.set_payload(part) + # no MIME headers. this is allowed for multipart/digest + # Consume the extra blank line + fp.readline() + pass else: - msgobj = self.parsestr(part) - container.preamble = preamble - container.epilogue = epilogue - container.attach(msgobj) - elif container.get_main_type() == 'multipart': + self._parseheaders(subobj, fp) + container.attach(subobj) + maintype = subobj.get_content_maintype() + hassubparts = (subobj.get_content_maintype() in + ( "message", "multipart" )) + if hassubparts: + subobj = self._parsemessage(subobj, fp) + + trailer, matchobj = fp.readuntil(boundaryRE) + if matchobj is None or trailer: + mo = re.search('(?P\r\n|\r|\n){2}$', trailer) + if not mo: + mo = re.search('(?P\r\n|\r|\n)$', trailer) + if not mo: + raise Errors.BoundaryError( + 'No terminating boundary and no trailing empty line') + linesep = mo.group('sep') + trailer = trailer[:-len(linesep)] + if trailer: + self._attach_trailer(subobj, trailer) + if matchobj is None or matchobj.group('end'): + # That was the last piece of data. Let our caller attach + # the epilogue to us. But before we do that, push the + # line ending of the match group back into the readline + # buffer, as it's part of the epilogue. + if matchobj: + fp.unreadline(matchobj.group('linesep')) + return container + + elif container.get_content_maintype() == "multipart": # Very bad. A message is a multipart with no boundary! raise Errors.BoundaryError( - 'multipart message with no defined boundary') - elif container.get_type() == 'message/delivery-status': - # This special kind of type contains blocks of headers separated - # by a blank line. We'll represent each header block as a - # separate Message object - blocks = [] - while True: - blockmsg = self._class() - self._parseheaders(blockmsg, fp) - if not len(blockmsg): - # No more header blocks left - break - blocks.append(blockmsg) - container.set_payload(blocks) - elif container.get_main_type() == 'message': - # Create a container for the payload, but watch out for there not - # being any headers left - try: - msg = self.parse(fp) - except Errors.HeaderParseError: + 'multipart message with no defined boundary') + elif container.get_content_maintype() == "message": + ct = container.get_content_type() + if ct == "message/rfc822": + submessage = self._class() + self._parseheaders(submessage, fp) + self._parsemessage(submessage, fp) + container.attach(submessage) + return submessage + elif ct == "message/delivery-status": + # This special kind of type contains blocks of headers + # separated by a blank line. We'll represent each header + # block as a separate Message object + while 1: + nextblock = self._class() + self._parseheaders(nextblock, fp) + container.attach(nextblock) + # next peek ahead to see whether we've hit the end or not + nextline = fp.peekline() + if nextline[:2] == "--": + break + return container + else: + # Other sort of message object (e.g. external-body) msg = self._class() - self._parsebody(msg, fp) - container.attach(msg) + self._parsemessage(msg, fp) + container.attach(msg) + return msg else: - text = fp.read() - if firstbodyline is not None: - text = firstbodyline + '\n' + text - container.set_payload(text) + # single body section. We let our caller set the payload. + return container + def _attach_trailer(self, obj, trailer): + if obj.get_content_maintype() in ("message", "multipart"): + obj.epilogue = trailer + else: + obj.set_payload(trailer) class HeaderParser(Parser): @@ -284,9 +330,8 @@ class HeaderParser(Parser): Parsing with this subclass can be considerably faster if all you're interested in is the message headers. """ - def _parsebody(self, container, fp, firstbodyline=None): + def _parsemessage(self, container, fp): # Consume but do not parse, the body text = fp.read() - if firstbodyline is not None: - text = firstbodyline + '\n' + text container.set_payload(text) + return None -- cgit v0.12