Index: Lib/http/formdata.py =================================================================== --- Lib/http/formdata.py (revision 0) +++ Lib/http/formdata.py (revision 0) @@ -0,0 +1,87 @@ +# Copyright (C) 2010 Python Software Foundation +# Author: Forest Bond + +''' +This module implements MIMEMultipartFormData, a class for generating a MIME +multipart/form-data message for submission to HTTP servers. You would use this +in an HTTP client to submit forms using Content-Type "multipart/form-data". +This content type is described in RFC2388. + +Example usage with httplib.HTTPConnection: + +message = MIMEMultipartFormData() +attachment = MIMEText('some test') +message.attach_form_data(attachment, name='foo') +attachment = MIMEApplication(bytes([0, 0, 0]), _subtype='octet-stream') +message.attach_file(attachment, name='files', filename='bar.bin') + +headers = dict(message) +body = message.get_body() + +conn = HTTPConnection('www.example.com') +response = conn.send_request('POST', '/url', body, headers) +''' + +from io import StringIO +from email.mime.multipart import MIMEMultipart +from email.generator import Generator + + +class MIMEMultipartFormData(MIMEMultipart): + '''A simple RFC2388 multipart/form-data implementation.''' + + def __init__(self, boundary=None, _subparts=None, **kwargs): + MIMEMultipart.__init__(self, _subtype='form-data', + boundary=boundary, _subparts=_subparts, **kwargs) + + def attach(self, subpart): + if 'MIME-Version' in subpart: + if subpart['MIME-Version'] != self['MIME-Version']: + raise ValueError('subpart has incompatible MIME-Version') + # Note: This isn't strictly necessary, but there is no point in + # including a MIME-Version header in each subpart. + del subpart['MIME-Version'] + MIMEMultipart.attach(self, subpart) + + def attach_form_data(self, subpart, name): + ''' + Attach a subpart, setting it's Content-Disposition header to + "form-data". + ''' + name = name.replace('"', '\\"') + subpart['Content-Disposition'] = 'form-data; name="%s"' % name + self.attach(subpart) + + def attach_file(self, subpart, name, filename): + ''' + Attach a subpart, setting it's Content-Disposition header to "file". + ''' + name = name.replace('"', '\\"') + filename = filename.replace('"', '\\"') + subpart['Content-Disposition'] = \ + 'file; name="%s"; filename="%s"' % (name, filename) + self.attach(subpart) + + def get_body(self, trailing_newline=True): + '''Return the encoded message body.''' + f = StringIO() + generator = Generator(f, mangle_from_=False) + generator._dispatch(self) + # HTTP needs a trailing newline. Since our return value is likely to + # be passed directly to an HTTP connection, we might as well add it + # here. + if trailing_newline: + f.write('\n') + return f.getvalue() + + def as_string(self, trailing_newline=True): + '''Return the entire formatted message as a string.''' + f = StringIO() + generator = Generator(f, mangle_from_=False) + generator.flatten(self) + # HTTP needs a trailing newline. Since our return value is likely to + # be passed directly to an HTTP connection, we might as well add it + # here. + if trailing_newline: + f.write('\n') + return f.getvalue() Index: Lib/email/encoders.py =================================================================== --- Lib/email/encoders.py (revision 81683) +++ Lib/email/encoders.py (working copy) @@ -29,7 +29,7 @@ Also, add an appropriate Content-Transfer-Encoding header. """ orig = msg.get_payload() - encdata = _bencode(orig) + encdata = str(_bencode(orig), 'ascii') msg.set_payload(encdata) msg['Content-Transfer-Encoding'] = 'base64' Index: Lib/test/test_http_formdata.py =================================================================== --- Lib/test/test_http_formdata.py (revision 0) +++ Lib/test/test_http_formdata.py (revision 0) @@ -0,0 +1,54 @@ +# Copyright (C) 2010 Python Software Foundation +# Author: Forest Bond + +from http.formdata import MIMEMultipartFormData +from email.mime.text import MIMEText +from email.mime.application import MIMEApplication + +from unittest import TestCase +from test import support + + +class MIMEMultipartFormDataTests(TestCase): + def test(self): + msg = MIMEMultipartFormData(boundary='my-boundary') + attachment = MIMEText('some text') + msg.attach_form_data(attachment, name='foo') + attachment = MIMEApplication(bytes([0, 0, 0, 0, 0]), + _subtype='octet-stream') + msg.attach_file(attachment, name='files', filename='bar') + + expected_headers = ( + 'Content-Type: multipart/form-data; boundary="my-boundary"\n' + 'MIME-Version: 1.0\n' + '\n' + ) + expected_body = ( + '--my-boundary\n' + 'Content-Type: text/plain; charset="us-ascii"\n' + 'Content-Transfer-Encoding: 7bit\n' + 'Content-Disposition: form-data; name="foo"\n' + '\n' + 'some text\n' + '--my-boundary\n' + 'Content-Type: application/octet-stream\n' + 'Content-Transfer-Encoding: base64\n' + 'Content-Disposition: file; name="files"; filename="bar"\n' + '\n' + 'AAAAAAA=\n' + '--my-boundary--\n' + ) + + body = msg.get_body() + self.assertEqual(body, expected_body) + + msg_str = msg.as_string() + self.assertEqual(msg_str, ''.join([expected_headers, expected_body])) + + +def test_main(verbose=None): + support.run_unittest(MIMEMultipartFormDataTests) + + +if __name__ == '__main__': + test_main()