11# -*- coding: utf-8 -*-
22#
3- # Copyright (C) 2008 Christopher Lenz
3+ # Copyright (C) 2008-2009 Christopher Lenz
44# All rights reserved.
55#
66# This software is licensed as described in the file COPYING, which
99"""Support for streamed reading and writing of multipart MIME content."""
1010
1111from cgi import parse_header
12+ import sys
1213
13- __all__ = ['read_multipart' ]
14+ __all__ = ['read_multipart' , 'write_multipart' ]
1415__docformat__ = 'restructuredtext en'
1516
1617
@@ -32,6 +33,7 @@ def read_multipart(fileobj, boundary=None):
3233 automatically from the headers of the outermost multipart
3334 envelope
3435 :return: an iterator over the parts
36+ :since: 0.5
3537 """
3638 headers = {}
3739 buf = []
@@ -85,5 +87,124 @@ def _current_part():
8587 else :
8688 buf .append (line )
8789
88- if not outer :
90+ if not outer and headers :
8991 yield _current_part ()
92+
93+
94+ class MultipartWriter (object ):
95+
96+ def __init__ (self , fileobj , headers = None , subtype = 'mixed' , boundary = None ):
97+ self .fileobj = fileobj
98+ if boundary is None :
99+ boundary = self ._make_boundary ()
100+ self .boundary = boundary
101+ if headers is None :
102+ headers = {}
103+ headers ['Content-Type' ] = 'multipart/%s; boundary="%s"' % (
104+ subtype , self .boundary
105+ )
106+ self ._write_headers (headers )
107+
108+ def open (self , headers = None , subtype = 'mixed' , boundary = None ):
109+ self .fileobj .write ('--%s\r \n ' % self .boundary )
110+ return MultipartWriter (self .fileobj , headers = headers , subtype = subtype ,
111+ boundary = boundary )
112+
113+ def add (self , mimetype , content , headers = None ):
114+ self .fileobj .write ('--%s\r \n ' % self .boundary )
115+ if headers is None :
116+ headers = {}
117+ headers ['Content-Type' ] = mimetype
118+ headers ['Content-Length' ] = len (content )
119+ self ._write_headers (headers )
120+ if content :
121+ # XXX: throw an exception if a boundary appears in the content??
122+ self .fileobj .write (content )
123+ self .fileobj .write ('\r \n ' )
124+
125+ def close (self ):
126+ self .fileobj .write ('--%s--\r \n ' % self .boundary )
127+
128+ def _make_boundary ():
129+ try :
130+ from uuid import uuid4
131+ return uuid4 ().hex
132+ except ImportError :
133+ from random import randrange
134+ token = randrange (sys .maxint )
135+ format = '%%0%dd' % len (repr (sys .maxint - 1 ))
136+ return ('=' * 15 ) + (fmt % token ) + '=='
137+
138+ def _write_headers (self , headers ):
139+ if headers :
140+ for name in sorted (headers .keys ()):
141+ self .fileobj .write ('%s: %s\r \n ' % (name , headers [name ]))
142+ self .fileobj .write ('\r \n ' )
143+
144+ def __enter__ (self ):
145+ return self
146+
147+ def __exit__ (self , exc_type , exc_val , exc_tb ):
148+ self .close ()
149+
150+
151+ def write_multipart (fileobj , subtype = 'mixed' , boundary = None ):
152+ r"""Simple streaming MIME multipart writer.
153+
154+ This function returns a `MultipartWriter` object that has a few methods to
155+ control the nested MIME parts. For example, to write a flat multipart
156+ envelope you call the ``add(mimetype, content, [headers])`` method for
157+ every part, and finally call the ``close()`` method.
158+
159+ >>> from StringIO import StringIO
160+
161+ >>> buf = StringIO()
162+ >>> envelope = write_multipart(buf, boundary='==123456789==')
163+ >>> envelope.add('text/plain', 'Just testing')
164+ >>> envelope.close()
165+ >>> print buf.getvalue().replace('\r\n', '\n')
166+ Content-Type: multipart/mixed; boundary="==123456789=="
167+ <BLANKLINE>
168+ --==123456789==
169+ Content-Length: 12
170+ Content-Type: text/plain
171+ <BLANKLINE>
172+ Just testing
173+ --==123456789==--
174+ <BLANKLINE>
175+
176+ Note that an explicit boundary is only specified for testing purposes. If
177+ the `boundary` parameter is omitted, the multipart writer will generate a
178+ random string for the boundary.
179+
180+ To write nested structures, call the ``open([headers])`` method on the
181+ respective envelope, and finish each envelope using the ``close()`` method:
182+
183+ >>> buf = StringIO()
184+ >>> envelope = write_multipart(buf, boundary='==123456789==')
185+ >>> part = envelope.open(boundary='==abcdefghi==')
186+ >>> part.add('text/plain', 'Just testing')
187+ >>> part.close()
188+ >>> envelope.close()
189+ >>> print buf.getvalue().replace('\r\n', '\n')
190+ Content-Type: multipart/mixed; boundary="==123456789=="
191+ <BLANKLINE>
192+ --==123456789==
193+ Content-Type: multipart/mixed; boundary="==abcdefghi=="
194+ <BLANKLINE>
195+ --==abcdefghi==
196+ Content-Length: 12
197+ Content-Type: text/plain
198+ <BLANKLINE>
199+ Just testing
200+ --==abcdefghi==--
201+ --==123456789==--
202+ <BLANKLINE>
203+
204+ :param fileobj: a writable file-like object that the output should get
205+ written to
206+ :param subtype: the subtype of the multipart MIME type (e.g. "mixed")
207+ :param boundary: the boundary to use to separate the different parts
208+ :since: 0.6
209+ """
210+ return MultipartWriter (fileobj , subtype = subtype , boundary = boundary )
0 commit comments