def test_boundary_delimiter(self): # boundary_delimiter is read from the MediaType ct = MediaType.from_str( "multipart/mixed; boundary=gc0p4Jq0M2Yt08j34c0p") boundary = multipart.get_boundary_delimiter(ct) self.assertTrue(boundary == "\r\n--gc0p4Jq0M2Yt08j34c0p") ct = MediaType.from_str( 'multipart/mixed; boundary="gc0pJq0M:08jU534c0p"') boundary = multipart.get_boundary_delimiter(ct) self.assertTrue(boundary == "\r\n--gc0pJq0M:08jU534c0p") # boundary delimiters and headers are always 7bit US-ASCII # (non-US-ASCII encoding deprecated) ct = MediaType.from_str( ul('multipart/mixed; boundary="gc0pJq0M\xa608jU534c0p"')) try: boundary = multipart.get_boundary_delimiter(ct) self.fail("8-bit boundary") except multipart.MultipartError: pass # must be no longer than 70 characters, not counting the two # leading hyphens ct = MediaType.from_str( "multipart/mixed; boundary=abcdefghijklmnopqrstuvwxyz1234567890" "abcdefghijklmnopqrstuvwxyz12345678") self.assertTrue(len(multipart.get_boundary_delimiter(ct)) == 74) ct = MediaType.from_str( "multipart/mixed; boundary=abcdefghijklmnopqrstuvwxyz1234567890" "abcdefghijklmnopqrstuvwxyz123456789") try: multipart.get_boundary_delimiter(ct) self.fail("long boundary") except multipart.MultipartError: pass
def test_multiple(self): part1 = multipart.MessagePart( entity_body=b"plain text version\r\n") part1.set_content_type("text/plain; charset=us-ascii") part2 = multipart.MessagePart( entity_body=b"RFC 1896 text/enriched version\r\n") part2.set_content_type("text/enriched") part3 = multipart.MessagePart( entity_body=b"fanciest version\r\n") part3.set_content_type("application/x-whatever") mtype = MediaType.from_str( "multipart/alternative; boundary=boundary42") mstream = multipart.MultipartSendWrapper(mtype, [part1, part2, part3]) self.assertTrue( mstream.read() == b"--boundary42\r\n" b"Content-Type: text/plain; charset=us-ascii\r\n" b"\r\n" b"plain text version\r\n" b"\r\n" b"--boundary42\r\n" b"Content-Type: text/enriched\r\n" b"\r\n" b"RFC 1896 text/enriched version\r\n" b"\r\n" b"--boundary42\r\n" b"Content-Type: application/x-whatever\r\n" b"\r\n" b"fanciest version\r\n" b"\r\n" b"--boundary42--")
def test_read_nonblocking(self): src = io.BytesIO(b"How are you?\n" * 10) src = MockBlockingByteReader(src, block_after=((10,))) part = multipart.MessagePart(entity_body=src) part.set_content_type("text/plain") mtype = MediaType.from_str( "multipart/mixed; boundary=gc0p4Jq0M2Yt08j34c0p") mstream = multipart.MultipartSendWrapper(mtype, [part]) # non-blocking non-empty stream lines = [] line = [] blocks = 0 while True: c = mstream.read(1) if c: if c == b"\n": # end of line lines.append(b"".join(line)) line = [] else: line.append(c) elif c is None: blocks += 1 else: break # our mock blocking stream always returns None at least once self.assertTrue(blocks > 1, "non-blocking stream failed to stall") boundary = lines.index(b"\r") self.assertTrue(boundary > 0, lines) for line in lines[boundary + 1:boundary + 11]: self.assertTrue(line == b"How are you?") mstream.close()
def test_readlines_crlf(self): # Now repeat the exercise with maximal CRLF part = multipart.MessagePart(entity_body=b"\r\nHow are you?\r\n") part.set_content_type("text/plain") mtype = MediaType.from_str( "multipart/mixed; boundary=gc0p4Jq0M2Yt08j34c0p") mstream = multipart.MultipartSendWrapper( mtype, [part], preamble=b"\r\nJust wanted to ask\r\n", epilogue=b"\r\nFine thanks!\r\n") lines = mstream.readlines() matches = [ b"\r\n", b"Just wanted to ask\r\n", b"\r\n", b"--gc0p4Jq0M2Yt08j34c0p\r\n", b"Content-Type: text/plain\r\n", b"\r\n", b"\r\n", b"How are you?\r\n", b"\r\n", b"--gc0p4Jq0M2Yt08j34c0p--\r\n", b"\r\n", b"Fine thanks!\r\n"] self.assertTrue(len(lines) == len(matches), lines) for line, match in zip(lines, matches): self.assertTrue(line == match, "Failed to match: %s" % match) mstream.close()
def test_write(self): part = multipart.MessagePart(entity_body=b"How are you?") part.set_content_type("text/plain") mtype = MediaType.from_str( "multipart/mixed; boundary=gc0p4Jq0M2Yt08j34c0p") mstream = multipart.MultipartSendWrapper(mtype, [part]) self.assertFalse(mstream.writable()) try: mstream.write(b"Hello") self.fail("MultipartSendWrapper.write") except IOError: pass mstream.close()
def test_nested(self): part_a = multipart.MessagePart(entity_body=b"Introduction") part_a.set_content_type("text/plain") part1 = multipart.MessagePart( entity_body=b"plain text version\r\n") part1.set_content_type("text/plain; charset=us-ascii") part2 = multipart.MessagePart( entity_body=b"RFC 1896 text/enriched version\r\n") part2.set_content_type("text/enriched") mtype = MediaType.from_str( 'multipart/alternative; boundary="---- next message ----"') mstream = multipart.MultipartSendWrapper(mtype, [part1, part2]) part_b = multipart.MessagePart(entity_body=mstream) part_b.set_content_type(mtype) mtype = MediaType.from_str( 'multipart/mixed; boundary="---- main boundary ----"') mstream = multipart.MultipartSendWrapper(mtype, [part_a, part_b]) result = mstream.read() self.assertTrue( result == b"------ main boundary ----\r\n" b'Content-Type: text/plain\r\n' b"\r\n" b"Introduction\r\n" b"------ main boundary ----\r\n" b"Content-Type: multipart/alternative; " b'boundary="---- next message ----"\r\n' b"\r\n" b"------ next message ----\r\n" b"Content-Type: text/plain; charset=us-ascii\r\n" b"\r\n" b"plain text version\r\n" b"\r\n" b"------ next message ----\r\n" b"Content-Type: text/enriched\r\n" b"\r\n" b"RFC 1896 text/enriched version\r\n" b"\r\n" b"------ next message ------\r\n" b"------ main boundary ------", repr(result))
def test_close(self): part = multipart.MessagePart() mtype = MediaType.from_str( "multipart/mixed; boundary=gc0p4Jq0M2Yt08j34c0p") # pass in an iterable of MessageParts mstream = multipart.MultipartSendWrapper(mtype, [part]) self.assertFalse(mstream.closed) mstream.close() self.assertTrue(mstream.closed) try: mstream.read(1) self.fail("MultipartSendWrapper.read after close") except IOError: pass
def test_constructor(self): part = multipart.MessagePart() mtype = MediaType.from_str( "multipart/mixed; boundary=gc0p4Jq0M2Yt08j34c0p") # pass in an iterable of MessageParts and required mime type mstream = multipart.MultipartSendWrapper(mtype, [part]) try: mstream.fileno() self.fail("MultipartSendWrapper.fileno") except IOError: pass # flush does nothing but is callable mstream.flush() self.assertFalse(mstream.isatty()) self.assertTrue(mstream.readable()) mstream.close() # can add an optional preamble, epilogue and boundary mstream = multipart.MultipartSendWrapper( mtype, [part], preamble=b"Hello", epilogue=b"Goodbye")
def test_readline(self): part = multipart.MessagePart(entity_body=b"How are you?") part.set_content_type("text/plain") mtype = MediaType.from_str( "multipart/mixed; boundary=gc0p4Jq0M2Yt08j34c0p") mstream = multipart.MultipartSendWrapper(mtype, [part]) # preamble is empty, straight into the boundary self.assertTrue( mstream.readline() == b"--gc0p4Jq0M2Yt08j34c0p\r\n") self.assertTrue( mstream.readline() == b"Content-Type: text/plain\r\n") # blank line self.assertTrue(mstream.readline() == b"\r\n") # body self.assertTrue(mstream.readline() == b"How are you?\r\n") # terminating boundary has NO CRLF self.assertTrue( mstream.readline() == b"--gc0p4Jq0M2Yt08j34c0p--")
def test_read(self): part = multipart.MessagePart(entity_body=b"How are you?") part.set_content_type("text/plain") mtype = MediaType.from_str( "multipart/mixed; boundary=gc0p4Jq0M2Yt08j34c0p") mstream = multipart.MultipartSendWrapper( mtype, [part], preamble=b"Just wanted to ask\r\n...", epilogue=b"Fine\r\nthanks!") # blocking stream c = mstream.read(1) self.assertTrue(c == b"J") line = [] while c != b"\n": line.append(c) c = mstream.read(1) # won't return None self.assertTrue(len(c) == 1) self.assertTrue( b"".join(line) == b'Just wanted to ask\r') data = mstream.read() self.assertTrue(data.endswith(b"Fine\r\nthanks!"), data) self.assertTrue(mstream.read(1) == b"") mstream.close()
def test_readlines(self): # Now try with preamble and epilogue (no CRLF) part = multipart.MessagePart(entity_body=b"How are you?") part.set_content_type("text/plain") mtype = MediaType.from_str( "multipart/mixed; boundary=gc0p4Jq0M2Yt08j34c0p") mstream = multipart.MultipartSendWrapper( mtype, [part], preamble=b"Just wanted to ask\r\n...", epilogue=b"Fine\r\nthanks!") lines = mstream.readlines() matches = [ b"Just wanted to ask\r\n", b"...\r\n", b"--gc0p4Jq0M2Yt08j34c0p\r\n", b"Content-Type: text/plain\r\n", b"\r\n", b"How are you?\r\n", b"--gc0p4Jq0M2Yt08j34c0p--\r\n", b"Fine\r\n", b"thanks!"] self.assertTrue(len(lines) == len(matches)) for line, match in zip(lines, matches): self.assertTrue(line == match, "Failed to match: %s" % match) mstream.close()
class MultipartRecvWrapperTests(unittest.TestCase): SIMPLE_TYPE = MediaType.from_str( 'multipart/mixed; boundary="simple boundary"') SIMPLE = ( b"This is the preamble. It is to be ignored, though it", b"is a handy place for composition agents to include an", b"explanatory note to non-MIME conformant readers.", b"", b"--simple boundary", b"", b"This is implicitly typed plain US-ASCII text.", b"It does NOT end with a linebreak.", b"--simple boundary", b"Content-type: text/plain; charset=us-ascii", b"", b"This is explicitly typed plain US-ASCII text.", b"It DOES end with a linebreak.", b"", b"--simple boundary--", b"", b"This is the epilogue. It is also to be ignored.") def test_constructor(self): src = io.BytesIO(grammar.CRLF.join(self.SIMPLE)) # pass in a source stream and a MediaType mstream = multipart.MultipartRecvWrapper(src, self.SIMPLE_TYPE) try: mstream.fileno() self.fail("MultipartRecvWrapper.fileno") except IOError: pass # flush does nothing but is callable mstream.flush() self.assertFalse(mstream.isatty()) self.assertTrue(mstream.readable()) mstream.close() def test_close(self): src = io.BytesIO(grammar.CRLF.join(self.SIMPLE)) mstream = multipart.MultipartRecvWrapper(src, self.SIMPLE_TYPE) self.assertFalse(mstream.closed) mstream.close() self.assertTrue(mstream.closed) try: mstream.read(1) self.fail("MultipartRecvWrapper.read after close") except IOError: pass def test_readline(self): src = io.BytesIO(grammar.CRLF.join(self.SIMPLE)) mstream = multipart.MultipartRecvWrapper(src, self.SIMPLE_TYPE) # starts by reading the preamble self.assertTrue( mstream.readline() == b"This is the preamble. It is to be ignored, though it\r\n") mstream.readline() # note the preamble ends with a line break self.assertTrue( mstream.readline() == b"explanatory note to non-MIME conformant readers.\r\n") # that's the lot self.assertTrue(mstream.readline() == b"") mstream.close() def test_readlines(self): src = io.BytesIO(grammar.CRLF.join(self.SIMPLE)) mstream = multipart.MultipartRecvWrapper(src, self.SIMPLE_TYPE) # starts by reading the preamble lines = mstream.readlines(10) self.assertTrue(len(lines) == 1) self.assertTrue( lines[0] == b"This is the preamble. It is to be ignored, though it\r\n") lines = mstream.readlines() self.assertTrue(len(lines) == 2, lines) # note the preamble ends with a line break self.assertTrue( lines[1] == b"explanatory note to non-MIME conformant readers.\r\n") # that's the lot self.assertTrue(mstream.readline() == b"") mstream.close() def test_read(self): src = io.BytesIO(grammar.CRLF.join(self.SIMPLE)) mstream = multipart.MultipartRecvWrapper(src, self.SIMPLE_TYPE) # blocking stream c = mstream.read(1) self.assertTrue(c == b"T") line = [] while c != b"\n": line.append(c) c = mstream.read(1) # won't return None self.assertTrue(len(c) == 1) self.assertTrue( b"".join(line) == b'This is the preamble. It is to be ignored, though it\r') data = mstream.read() self.assertTrue(data.endswith(b"readers.\r\n"), data) self.assertTrue(mstream.read(1) == b"") mstream.close() def test_read_nonblocking(self): src = io.BytesIO(grammar.CRLF.join(self.SIMPLE)) # simulate breaks in the data after LF src = MockBlockingByteReader(src, block_after=((10,))) mstream = multipart.MultipartRecvWrapper(src, self.SIMPLE_TYPE) # non-blocking non-empty stream line = [] blocks = 0 while True: c = mstream.read(1) if c: if c == b"\n": break else: line.append(c) continue self.assertTrue(c is None, "stream non-blocking: %s" % repr(c)) blocks += 1 # our mock blocking stream always returns None at least once self.assertTrue(blocks > 1, "non-blocking stream failed to stall") self.assertTrue( b"".join(line) == b'This is the preamble. It is to be ignored, though it\r') # readall behaviour is undefined, don't test it mstream.close() def test_seek(self): src = io.BytesIO(grammar.CRLF.join(self.SIMPLE)) mstream = multipart.MultipartRecvWrapper(src, self.SIMPLE_TYPE) self.assertFalse(mstream.seekable()) try: mstream.seek(0) self.fail("MultipartRecvWrapper.seek") except IOError: pass try: mstream.tell() self.fail("MultipartRecvWrapper.tell") except IOError: pass try: mstream.truncate(0) self.fail("MultipartRecvWrapper.truncate") except IOError: pass mstream.close() def test_write(self): src = io.BytesIO(grammar.CRLF.join(self.SIMPLE)) mstream = multipart.MultipartRecvWrapper(src, self.SIMPLE_TYPE) self.assertFalse(mstream.writable()) try: mstream.write(b"Hello") self.fail("MultipartRecvWrapper.write") except IOError: pass mstream.close() def check_boundaries(self, mstream): # read one line from the preamble mstream.readline() # read_boundary discards the rest self.assertTrue(mstream.read_boundary()) # now we're in the first proper part, which starts with blank # line self.assertTrue(mstream.readline() == b"\r\n") lines = mstream.readlines() self.assertTrue(len(lines) == 2) # The CRLF preceding the boundary delimiter line is conceptually # attached to the boundary so that it is possible to have a part # that does not end with a CRLF (line break) self.assertTrue(lines[1] == b"It does NOT end with a linebreak.") # check we can't read any more data self.assertTrue(mstream.read(1) == b"") self.assertTrue(mstream.read_boundary()) data = mstream.readall() self.assertTrue(data.endswith(b"It DOES end with a linebreak.\r\n")) self.assertFalse(mstream.read_boundary()) data = mstream.readall() self.assertTrue(data.endswith( b"\r\nThis is the epilogue. It is also to be ignored.")) def test_read_boundary(self): src = io.BytesIO(grammar.CRLF.join(self.SIMPLE)) mstream = multipart.MultipartRecvWrapper(src, self.SIMPLE_TYPE) self.check_boundaries(mstream) # it is an error to call read_boundary during the epilogue try: mstream.read_boundary() self.fail("expected MultipartError") except multipart.MultipartError: pass # now check for non-blocking case src = io.BytesIO(grammar.CRLF.join(self.SIMPLE)) # simulate breaks in the data after LF src = MockBlockingByteReader(src, block_after=((10,))) mstream = multipart.MultipartRecvWrapper(src, self.SIMPLE_TYPE) blocks = 0 while True: result = mstream.read_boundary() if result is None: # blocked, continue blocks += 1 continue if result is False: break if not blocks: logging.warning("read_boundary expected at least one None") LWS = ( b"This is the preamble. It is to be ignored, though it", b"is a handy place for composition agents to include an", b"explanatory note to non-MIME conformant readers.", b"", b"--simple boundary \t", b"", b"This is implicitly typed plain US-ASCII text.", b"It does NOT end with a linebreak.", b"--simple boundary\t", b"Content-type: text/plain; charset=us-ascii", b"", b"This is explicitly typed plain US-ASCII text.", b"It DOES end with a linebreak.", b"", b"--simple boundary-- ", b"", b"This is the epilogue. It is also to be ignored.") def test_lws(self): # The boundary may be followed by zero or more characters of # linear whitespace. src = io.BytesIO(grammar.CRLF.join(self.LWS)) mstream = multipart.MultipartRecvWrapper(src, self.SIMPLE_TYPE) self.check_boundaries(mstream) BAD = ( b"This is the preamble. It is to be ignored, though it", b"is a handy place for composition agents to include an", b"explanatory note to non-MIME conformant readers.", b"", b"--simple boundary", b"", b"This is implicitly typed plain US-ASCII text.", b"It does NOT end with a linebreak.", b"--simple boundary extra data ", b"Content-type: text/plain; charset=us-ascii", b"", b"This is explicitly typed plain US-ASCII text.", b"It DOES end with a linebreak.", b"", b"--simple boundary--", b"", b"This is the epilogue. It is also to be ignored.") def test_bad_boundary(self): # Boundary string comparisons must compare the boundary value # with the beginning of each candidate line. An exact match of # the entire candidate line is not required; it is sufficient # that the boundary appear in its entirety following the CRLF. # # Therefore, a boundary delimiter followed by anything other # than white space suggests a violation of: # # Boundary delimiters must not appear within the encapsulated # material src = io.BytesIO(grammar.CRLF.join(self.BAD)) mstream = multipart.MultipartRecvWrapper(src, self.SIMPLE_TYPE) # ignore the preamble self.assertTrue(mstream.read_boundary()) try: mstream.read_boundary() self.fail("boundary detected in encapsulated material") except multipart.MultipartError: pass def test_next_part(self): src = io.BytesIO(grammar.CRLF.join(self.SIMPLE)) mstream = multipart.MultipartRecvWrapper(src, self.SIMPLE_TYPE) # first call advances past the preamble part1 = mstream.next_part() self.assertTrue(isinstance(part1, http.RecvWrapper)) self.assertTrue(isinstance(part1.message, multipart.MessagePart)) # the process of reading from this wrapper will force the # headers to be read... # # [the boundary delimiter] is then terminated by either another # CRLF and the header fields for the next part, or by two CRLFs, # in which case there are no header fields for the next part. lines = part1.readlines() # NO header fields are actually required in body parts self.assertTrue(len(part1.message.get_headerlist()) == 0) # but we simulate the default Content-Type mtype = part1.message.get_content_type() self.assertTrue(isinstance(mtype, MediaType)) self.assertTrue(mtype.type == "text") self.assertTrue(mtype.subtype == "plain") self.assertTrue(mtype['charset'] == b"us-ascii") # The blank line is ignored as it terminates empty headers self.assertTrue(len(lines) == 2) self.assertTrue(lines[1] == b"It does NOT end with a linebreak.") # check we can't read any more data self.assertTrue(part1.read(1) == b"") part2 = mstream.next_part() lines = part2.readlines() self.assertTrue(len(part2.message.get_headerlist()) == 1) mtype = part2.message.get_content_type() self.assertTrue(isinstance(mtype, MediaType)) self.assertTrue(mtype.type == "text") self.assertTrue(mtype.subtype == "plain") self.assertTrue(mtype['charset'] == b"us-ascii") try: mstream.next_part() self.fail("Expected StopIteration for epilogue") except StopIteration: pass # check non-blocking case src = io.BytesIO(grammar.CRLF.join(self.SIMPLE)) # simulate breaks in the data after LF src = MockBlockingByteReader(src, block_after=((10,))) mstream = multipart.MultipartRecvWrapper(src, self.SIMPLE_TYPE) blocks = 0 parts = 0 while True: try: part = mstream.next_part() except StopIteration: break if part is None: # blocked, continue blocks += 1 continue parts += 1 self.assertTrue(parts == 2) if not blocks: logging.warning("next_part expected at least one None") def test_read_parts(self): src = io.BytesIO(grammar.CRLF.join(self.SIMPLE)) mstream = multipart.MultipartRecvWrapper(src, self.SIMPLE_TYPE) parts = [] for part in mstream.read_parts(): self.assertTrue(isinstance(part, http.RecvWrapper)) self.assertTrue(isinstance(part.message, multipart.MessagePart)) # headers should already be loaded nheaders = len(part.message.get_headerlist()) parts.append((part, part.readlines())) self.assertTrue(nheaders == len(part.message.get_headerlist())) self.assertTrue(len(parts) == 2) lines = parts[0][1] self.assertTrue(len(lines) == 2) self.assertTrue(lines[1] == b"It does NOT end with a linebreak.") lines = parts[1][1] self.assertTrue(len(lines) == 2) self.assertTrue(lines[1] == b"It DOES end with a linebreak.\r\n") # check non-blocking case src = io.BytesIO(grammar.CRLF.join(self.SIMPLE)) # simulate breaks in the data after LF src = MockBlockingByteReader(src, block_after=((10,))) mstream = multipart.MultipartRecvWrapper(src, self.SIMPLE_TYPE) blocks = 0 parts = [] for part in mstream.read_parts(): if part is None: blocks += 1 else: self.assertTrue(isinstance(part, http.RecvWrapper)) self.assertTrue(isinstance(part.message, multipart.MessagePart)) # headers should already be loaded nheaders = len(part.message.get_headerlist()) parts.append(part) self.assertTrue(nheaders == len(part.message.get_headerlist())) self.assertTrue(len(parts) == 2) if not blocks: logging.warning("read_parts expected at least one None") EMPTY = ( b"This is the preamble. It is to be ignored, though it", b"is a handy place for composition agents to include an", b"explanatory note to non-MIME conformant readers.", b"--simple boundary--", b"", b"This is the epilogue. It is also to be ignored.") def test_empty(self): # The body must contain one or more body parts src = io.BytesIO(grammar.CRLF.join(self.EMPTY)) mstream = multipart.MultipartRecvWrapper(src, self.SIMPLE_TYPE) try: mstream.next_part() self.fail("no boundary") except multipart.MultipartError: pass # try the same thing but with no boundary at all src = io.BytesIO(grammar.CRLF.join(self.EMPTY[:-3])) mstream = multipart.MultipartRecvWrapper(src, self.SIMPLE_TYPE) try: mstream.next_part() self.fail("no boundary") except multipart.MultipartError: pass def test_header_defaults(self): src = io.BytesIO(grammar.CRLF.join(self.SIMPLE)) mstream = multipart.MultipartRecvWrapper(src, self.SIMPLE_TYPE) parts = [part for part in mstream.read_parts()] # A body part that starts with a blank line ... is a body part # for which all default values are to be assumed. p = parts[0] # The absence of a Content-Type 'usually' indicates "text/plain; # charset=US-ASCII". If no Content-Type field is present it is # assumed to be "message/rfc822" in a "multipart/digest" and # "text/plain" otherwise. t = p.message.get_content_type() self.assertTrue(t == "text/plain; charset=us-ascii") # "Content-Transfer-Encoding: 7BIT" is assumed if the # Content-Transfer-Encoding header field is not present. self.assertTrue(p.message.get_content_transfer_encoding() == "7bit") self.assertTrue(p.message.get_content_id() is None) self.assertTrue(p.message.get_content_description() is None) MISC_HEADERS = ( b"Preamble", b"", b"--simple boundary", b"Content-type: text/plain; charset=us-ascii", b"Content-Description: misc header test", b"User-Agent: pyslet/1.0", b"", b"This is explicitly typed plain US-ASCII text.", b"It DOES end with a linebreak.", b"", b"--simple boundary--", b"", b"This is the epilogue. It is also to be ignored.") def test_misc_headers(self): src = io.BytesIO(grammar.CRLF.join(self.MISC_HEADERS)) mstream = multipart.MultipartRecvWrapper(src, self.SIMPLE_TYPE) parts = [part for part in mstream.read_parts()] # Only Content- header fields have meaning, others may be # ignored but should be retained p = parts[0] self.assertTrue(p.message.get_header('User-Agent') == b"pyslet/1.0") NESTED = ( b"Preamble", b"", b"--simple boundary", b"Content-type: multipart/alternative; boundary=nested", b"", b"Nested Preamble", b"--nested", b"", b"This is explicitly typed plain US-ASCII text.", b"It DOES end with a linebreak.", b"", b"--nested--", b"--simple boundary--", b"", b"This is the epilogue. It is also to be ignored.") def test_nested(self): src = io.BytesIO(grammar.CRLF.join(self.NESTED)) mstream = multipart.MultipartRecvWrapper(src, self.SIMPLE_TYPE) for part in mstream.read_parts(): self.assertTrue(part.message.get_content_type().type == "multipart") # so part should be a readable stream we can wrap nstream = multipart.MultipartRecvWrapper( part, part.message.get_content_type()) nparts = [np for np in nstream.read_parts()] self.assertTrue(len(nparts) == 1) # MIME implementations are therefore required to recognize outer # level boundary markers at ANY level of inner nesting. bad_nested = list(self.NESTED) # remove the closing inner boundary del bad_nested[11] src = io.BytesIO(grammar.CRLF.join(bad_nested)) mstream = multipart.MultipartRecvWrapper(src, self.SIMPLE_TYPE) for part in mstream.read_parts(): self.assertTrue(part.message.get_content_type().type == "multipart") # so part should be a readable stream we can wrap nstream = multipart.MultipartRecvWrapper( part, part.message.get_content_type()) try: nparts = [np for np in nstream.read_parts()] self.fail("Expected multipart error due to missing boundary") except multipart.MultipartError: pass # but not error from the outer multipart parser NO_PREAMBLE_OR_EPILOGUE = ( b"--simple boundary", b"", b"This is plain US-ASCII text.", b"", b"--simple boundary--") def test_no_preamble_or_epilogue(self): src = io.BytesIO(grammar.CRLF.join(self.NO_PREAMBLE_OR_EPILOGUE)) mstream = multipart.MultipartRecvWrapper(src, self.SIMPLE_TYPE) parts = [p for p in mstream.read_parts()] self.assertTrue(len(parts) == 1)