Repository: mjs/imapclient

Issue:
`_convert_to_ENVELOPE` (and subcall to `util.to_unicode`) only support ascii data
When using `fetch(uids, ['ENVELOPE'])`, we get a `imapclient.response_types.Envelope` namedtuple. However, this object may result in subcomponent that call `imapclient.util.to_unicode`. However this methods, decode bytes data only with the ascii codec.

For some reasons, some senders does not encode the data before sending the e-mail, resulting in object like this...

```In [5]: addr = Address(b'Ren\xe9', None, 'rene', 'domain.com')
In [6]: str(addr)
---------------------------------------------------------------------------
/dir/to/imapclient/response_types.py in __str__(self)
     56     def __str__(self):
     57         return formataddr((
---> 58             to_unicode(self.name),
     59             to_unicode(self.mailbox) + '@' + to_unicode(self.host)))
     60 

/dir/to/imapclient/util.py in to_unicode(s)
     10 def to_unicode(s):
     11     if isinstance(s, binary_type):
---> 12         return s.decode('ascii')
     13     return s
     14 

UnicodeDecodeError: 'ascii' codec can't decode byte 0xe9 in position 3: ordinal not in range(128)
```

I think `imapclient` should handle this and either raise a proper custom exception which informs the developer why the data is incorrect or decode the data (using latin1 codec in this case), if possible...

Comments:

Patch:
diff --git a/imapclient/util.py b/imapclient/util.py
index 923961c..3314ea5 100644
--- a/imapclient/util.py
+++ b/imapclient/util.py
@@ -4,12 +4,22 @@
 
 from __future__ import unicode_literals
 
+import logging
 from six import binary_type, text_type
 
+logger = logging.getLogger(__name__)
+
 
 def to_unicode(s):
     if isinstance(s, binary_type):
-        return s.decode('ascii')
+        try:
+            return s.decode('ascii')
+        except UnicodeDecodeError:
+            logger.warning(
+                "An error occurred while decoding %s in ASCII 'strict' mode. Fallback to "
+                "'ignore' errors handling, some characters might have been stripped", s
+            )
+            return s.decode('ascii', 'ignore')
     return s
 
 


Test patch:
diff --git a/tests/test_response_parser.py b/tests/test_response_parser.py
index 82af1d3..01f2222 100644
--- a/tests/test_response_parser.py
+++ b/tests/test_response_parser.py
@@ -9,6 +9,7 @@ Unit tests for the FetchTokeniser and FetchParser classes
 from __future__ import unicode_literals
 
 from datetime import datetime
+from mock import patch
 
 from imapclient.datetime_util import datetime_to_native
 from imapclient.fixed_offset import FixedOffset
@@ -497,6 +498,14 @@ class TestParseFetchResponse(unittest.TestCase):
         self.assertEqual(str(Address(None, None, None, "undisclosed-recipients")),
                          "undisclosed-recipients")
 
+    @patch('imapclient.util.logger')
+    def test_Address_str_ignores_encoding_error(self, mock_logger):
+        self.assertEqual(
+            str(Address(b'Russian \xc2\xeb\xe0\xe4\xe8\xec\xe8\xf0', None, b"g\xe9rard", "domain.org")),
+            "Russian  <grard@domain.org>"
+        )
+        # Ensure warning has been triggered twice, for name and mailbox bytes
+        self.assertEqual(mock_logger.warning.call_count, 2)
 
 
 def add_crlf(text):