[PATCH 1 of 6] mail: add methods to handle non-ascii chars

Christian Ebert blacktrash at gmx.net
Sat Jul 12 14:32:48 CDT 2008


# HG changeset patch
# User Christian Ebert <blacktrash at gmx.net>
# Date 1215886319 -3600
# Node ID d1117d4e339364990ed9e1a274add13d8cb7155b
# Parent  2134d6c09432e4e3dbee18d93ec9242a332f7cdc
mail: add methods to handle non-ascii chars

- headencode, addressencode: encode headers
- mimeencode: encode message parts not containing patches
- new email config "charsets"

Users may configure email.charsets as a list of charsets they
consider appropriate for the recipients of their outgoing mails.

By default the list is empty and only us-ascii, util._encoding
are tried; they are always contained in email.charsets.

diff --git a/mercurial/mail.py b/mercurial/mail.py
--- a/mercurial/mail.py
+++ b/mercurial/mail.py
@@ -7,6 +7,7 @@
 
 from i18n import _
 import os, smtplib, socket
+import email.Header, email.MIMEText, email.Utils
 import util
 
 def _smtp(ui):
@@ -84,3 +85,82 @@
         if not util.find_exe(method):
             raise util.Abort(_('%r specified as email transport, '
                                'but not in PATH') % method)
+
+def _charsets(ui):
+    '''obtain charsets to send mail parts not containing patches.'''
+    charsets = [cs for cs in ui.configlist('email', 'charsets')
+                if cs.lower() not in ('ascii', 'us-ascii')]
+    if util._encoding not in charsets:
+        charsets.append(util._encoding)
+    return charsets
+
+def _encode(ui, s, charsets):
+    '''Returns (converted) string, charset tuple.
+    Finds out best charset by cycling through sendcharsets in descending
+    order. Tries both _encoding and _fallbackencoding for input. Only as
+    last resort send as is in fake ascii.
+    Caveat: Do not use for mail parts containing patches!'''
+    try:
+        s.decode('ascii')
+        return s, None
+    except UnicodeDecodeError:
+        sendcharsets = charsets or _charsets(ui)
+
+        def strconv(inputcs):
+            try:
+                u = s.decode(inputcs)
+            except UnicodeDecodeError:
+                raise
+            for cs in sendcharsets:
+                try:
+                    return u.encode(cs), cs
+                except UnicodeEncodeError:
+                    pass
+                except LookupError:
+                    ui.warn(_('ignoring invalid sendcharset: %s\n') % cs)
+            # if all conversion attempts fail, send broken ascii
+            return s, None
+
+        try:
+            s, scs = strconv(util._encoding)
+        except UnicodeDecodeError:
+            try:
+                s, scs = strconv(util._fallbackencoding)
+            except UnicodeDecodeError:
+                scs = None
+    return s, scs
+
+def headencode(ui, s, charsets=None):
+    '''returns RFC-2047 compliant header from given string.'''
+    # split into words?
+    s, cs = _encode(ui, s, charsets)
+    if cs:
+        return str(email.Header.Header(s, cs))
+    return s
+
+def addressencode(ui, address, charsets=None):
+    '''turn address into RFC-2047 compliant header.'''
+    if not address:
+        return ''
+    name, addr = email.Utils.parseaddr(address)
+    name = headencode(ui, name, charsets)
+    try:
+        acc, dom = addr.split('@')
+        acc = acc.encode('ascii')
+        dom = dom.encode('idna')
+        addr = '%s@%s' % (acc, dom)
+    except UnicodeDecodeError:
+        raise util.Abort(_('invalid email address: %s') % addr)
+    except ValueError:
+        try:
+            # too strict?
+            addr = addr.encode('ascii')
+        except UnicodeDecodeError:
+            raise util.Abort(_('invalid local address: %s') % addr)
+    return email.Utils.formataddr((name, addr))
+
+def mimeencode(ui, s, charsets=None):
+    '''creates mime text object, encodes it if needed, and sets
+    charset and transfer-encoding accordingly.'''
+    s, cs = _encode(ui, s, charsets)
+    return email.MIMEText.MIMEText(s, 'plain', cs or 'us-ascii')


More information about the Mercurial-devel mailing list