]> granicus.if.org Git - python/commitdiff
#1690608: make formataddr RFC2047 aware.
authorR David Murray <rdmurray@bitdance.com>
Wed, 6 Apr 2011 13:35:57 +0000 (09:35 -0400)
committerR David Murray <rdmurray@bitdance.com>
Wed, 6 Apr 2011 13:35:57 +0000 (09:35 -0400)
Patch by Torsten Becker.

Doc/library/email.util.rst
Lib/email/utils.py
Lib/test/test_email/test_email.py
Misc/ACKS
Misc/NEWS

index f7b777a01d696eea848e50de788990ebcaf531e6..4d96857c83f5a902739d7e31669e2d3277e3d47d 100644 (file)
@@ -29,13 +29,20 @@ There are several useful utilities provided in the :mod:`email.utils` module:
    fails, in which case a 2-tuple of ``('', '')`` is returned.
 
 
-.. function:: formataddr(pair)
+.. function:: formataddr(pair, charset='utf-8')
 
    The inverse of :meth:`parseaddr`, this takes a 2-tuple of the form ``(realname,
    email_address)`` and returns the string value suitable for a :mailheader:`To` or
    :mailheader:`Cc` header.  If the first element of *pair* is false, then the
    second element is returned unmodified.
 
+   Optional *charset* is the character set that will be used in the :rfc:`2047`
+   encoding of the ``realname`` if the ``realname`` contains non-ASCII
+   characters.  Can be an instance of :class:`str` or a
+   :class:`~email.charset.Charset`.  Defaults to ``utf-8``.
+
+   .. versionchanged: 3.3 added the *charset* option
+
 
 .. function:: getaddresses(fieldvalues)
 
index ac4da3705f31781ff038af9b1b3bf351b208e3c4..82f7283c077e7fc413f0233acf6aec61baa06672 100644 (file)
@@ -42,6 +42,7 @@ from quopri import decodestring as _qdecode
 
 # Intrapackage imports
 from email.encoders import _bencode, _qencode
+from email.charset import Charset
 
 COMMASPACE = ', '
 EMPTYSTRING = ''
@@ -56,21 +57,36 @@ escapesre = re.compile(r'[][\\()"]')
 
 # Helpers
 
-def formataddr(pair):
+def formataddr(pair, charset='utf-8'):
     """The inverse of parseaddr(), this takes a 2-tuple of the form
     (realname, email_address) and returns the string value suitable
     for an RFC 2822 From, To or Cc header.
 
     If the first element of pair is false, then the second element is
     returned unmodified.
+
+    Optional charset if given is the character set that is used to encode
+    realname in case realname is not ASCII safe.  Can be an instance of str or
+    a Charset-like object which has a header_encode method.  Default is
+    'utf-8'.
     """
     name, address = pair
+    # The address MUST (per RFC) be ascii, so throw a UnicodeError if it isn't.
+    address.encode('ascii')
     if name:
-        quotes = ''
-        if specialsre.search(name):
-            quotes = '"'
-        name = escapesre.sub(r'\\\g<0>', name)
-        return '%s%s%s <%s>' % (quotes, name, quotes, address)
+        try:
+            name.encode('ascii')
+        except UnicodeEncodeError:
+            if isinstance(charset, str):
+                charset = Charset(charset)
+            encoded_name = charset.header_encode(name)
+            return "%s <%s>" % (encoded_name, address)
+        else:
+            quotes = ''
+            if specialsre.search(name):
+                quotes = '"'
+            name = escapesre.sub(r'\\\g<0>', name)
+            return '%s%s%s <%s>' % (quotes, name, quotes, address)
     return address
 
 
index 44acc9f82b02df21addbfec85aaa1c909191d379..8530e5e9a119c0daec53f289d36f84fd73e71213 100644 (file)
@@ -2376,6 +2376,46 @@ class TestMiscellaneous(TestEmailBase):
         b = 'person@dom.ain'
         self.assertEqual(utils.parseaddr(utils.formataddr((a, b))), (a, b))
 
+    def test_quotes_unicode_names(self):
+        # issue 1690608.  email.utils.formataddr() should be rfc2047 aware.
+        name = "H\u00e4ns W\u00fcrst"
+        addr = 'person@dom.ain'
+        utf8_base64 = "=?utf-8?b?SMOkbnMgV8O8cnN0?= <person@dom.ain>"
+        latin1_quopri = "=?iso-8859-1?q?H=E4ns_W=FCrst?= <person@dom.ain>"
+        self.assertEqual(utils.formataddr((name, addr)), utf8_base64)
+        self.assertEqual(utils.formataddr((name, addr), 'iso-8859-1'),
+            latin1_quopri)
+
+    def test_accepts_any_charset_like_object(self):
+        # issue 1690608.  email.utils.formataddr() should be rfc2047 aware.
+        name = "H\u00e4ns W\u00fcrst"
+        addr = 'person@dom.ain'
+        utf8_base64 = "=?utf-8?b?SMOkbnMgV8O8cnN0?= <person@dom.ain>"
+        foobar = "FOOBAR"
+        class CharsetMock:
+            def header_encode(self, string):
+                return foobar
+        mock = CharsetMock()
+        mock_expected = "%s <%s>" % (foobar, addr)
+        self.assertEqual(utils.formataddr((name, addr), mock), mock_expected)
+        self.assertEqual(utils.formataddr((name, addr), Charset('utf-8')),
+            utf8_base64)
+
+    def test_invalid_charset_like_object_raises_error(self):
+        # issue 1690608.  email.utils.formataddr() should be rfc2047 aware.
+        name = "H\u00e4ns W\u00fcrst"
+        addr = 'person@dom.ain'
+        # A object without a header_encode method:
+        bad_charset = object()
+        self.assertRaises(AttributeError, utils.formataddr, (name, addr),
+            bad_charset)
+
+    def test_unicode_address_raises_error(self):
+        # issue 1690608.  email.utils.formataddr() should be rfc2047 aware.
+        addr = 'pers\u00f6n@dom.in'
+        self.assertRaises(UnicodeError, utils.formataddr, (None, addr))
+        self.assertRaises(UnicodeError, utils.formataddr, ("Name", addr))
+
     def test_name_with_dot(self):
         x = 'John X. Doe <jxd@example.com>'
         y = '"John X. Doe" <jxd@example.com>'
index b1c2eeaea03ff12b4abb472760c73e9a94455b28..f3555f37cab679507ed16ef68f2f5781cbe33b72 100644 (file)
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -979,3 +979,4 @@ Uwe Zessin
 Kai Zhu
 Tarek Ziadé
 Peter Ã…strand
+Torsten Becker
index 44d4e9ed5fc6aed1ff2c246fb1277c2ea7d3bf6e..37eb250f14c89c5b40d183660869ebf2f8178e36 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -97,6 +97,10 @@ Library
 - Issue #11605: email.parser.BytesFeedParser was incorrectly converting multipart
   subpararts with an 8bit CTE into unicode instead of preserving the bytes.
 
+- Issue #1690608: email.util.formataddr is now RFC2047 aware:  it now has a
+  charset parameter that defaults utf-8 which is used as the charset for RFC
+  2047 encoding when the realname contains non-ASCII characters.
+
 - Issue #10963: Ensure that subprocess.communicate() never raises EPIPE.
 
 - Issue #10791: Implement missing method GzipFile.read1(), allowing GzipFile