1
1
# -*- coding: utf-8 -*-
2
2
#
3
- # Copyright (C) 2008 Christopher Lenz
3
+ # Copyright (C) 2008-2009 Christopher Lenz
4
4
# All rights reserved.
5
5
#
6
6
# This software is licensed as described in the file COPYING, which
9
9
"""Support for streamed reading and writing of multipart MIME content."""
10
10
11
11
from cgi import parse_header
12
+ import sys
12
13
13
- __all__ = ['read_multipart' ]
14
+ __all__ = ['read_multipart' , 'write_multipart' ]
14
15
__docformat__ = 'restructuredtext en'
15
16
16
17
@@ -32,6 +33,7 @@ def read_multipart(fileobj, boundary=None):
32
33
automatically from the headers of the outermost multipart
33
34
envelope
34
35
:return: an iterator over the parts
36
+ :since: 0.5
35
37
"""
36
38
headers = {}
37
39
buf = []
@@ -85,5 +87,124 @@ def _current_part():
85
87
else :
86
88
buf .append (line )
87
89
88
- if not outer :
90
+ if not outer and headers :
89
91
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