eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/mercurial/mail.py
changeset 69 c6bca38c1cbf
equal deleted inserted replaced
68:5ff1fc726848 69:c6bca38c1cbf
       
     1 # mail.py - mail sending bits for mercurial
       
     2 #
       
     3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
       
     4 #
       
     5 # This software may be used and distributed according to the terms of the
       
     6 # GNU General Public License version 2 or any later version.
       
     7 
       
     8 from i18n import _
       
     9 import util, encoding
       
    10 import os, smtplib, socket, quopri
       
    11 import email.Header, email.MIMEText, email.Utils
       
    12 
       
    13 _oldheaderinit = email.Header.Header.__init__
       
    14 def _unifiedheaderinit(self, *args, **kw):
       
    15     """
       
    16     Python2.7 introduces a backwards incompatible change
       
    17     (Python issue1974, r70772) in email.Generator.Generator code:
       
    18     pre-2.7 code passed "continuation_ws='\t'" to the Header
       
    19     constructor, and 2.7 removed this parameter.
       
    20 
       
    21     Default argument is continuation_ws=' ', which means that the
       
    22     behaviour is different in <2.7 and 2.7
       
    23 
       
    24     We consider the 2.7 behaviour to be preferable, but need
       
    25     to have an unified behaviour for versions 2.4 to 2.7
       
    26     """
       
    27     # override continuation_ws
       
    28     kw['continuation_ws'] = ' '
       
    29     _oldheaderinit(self, *args, **kw)
       
    30 
       
    31 email.Header.Header.__dict__['__init__'] = _unifiedheaderinit
       
    32 
       
    33 def _smtp(ui):
       
    34     '''build an smtp connection and return a function to send mail'''
       
    35     local_hostname = ui.config('smtp', 'local_hostname')
       
    36     s = smtplib.SMTP(local_hostname=local_hostname)
       
    37     mailhost = ui.config('smtp', 'host')
       
    38     if not mailhost:
       
    39         raise util.Abort(_('smtp.host not configured - cannot send mail'))
       
    40     mailport = util.getport(ui.config('smtp', 'port', 25))
       
    41     ui.note(_('sending mail: smtp host %s, port %s\n') %
       
    42             (mailhost, mailport))
       
    43     s.connect(host=mailhost, port=mailport)
       
    44     if ui.configbool('smtp', 'tls'):
       
    45         if not hasattr(socket, 'ssl'):
       
    46             raise util.Abort(_("can't use TLS: Python SSL support "
       
    47                                "not installed"))
       
    48         ui.note(_('(using tls)\n'))
       
    49         s.ehlo()
       
    50         s.starttls()
       
    51         s.ehlo()
       
    52     username = ui.config('smtp', 'username')
       
    53     password = ui.config('smtp', 'password')
       
    54     if username and not password:
       
    55         password = ui.getpass()
       
    56     if username and password:
       
    57         ui.note(_('(authenticating to mail server as %s)\n') %
       
    58                   (username))
       
    59         try:
       
    60             s.login(username, password)
       
    61         except smtplib.SMTPException, inst:
       
    62             raise util.Abort(inst)
       
    63 
       
    64     def send(sender, recipients, msg):
       
    65         try:
       
    66             return s.sendmail(sender, recipients, msg)
       
    67         except smtplib.SMTPRecipientsRefused, inst:
       
    68             recipients = [r[1] for r in inst.recipients.values()]
       
    69             raise util.Abort('\n' + '\n'.join(recipients))
       
    70         except smtplib.SMTPException, inst:
       
    71             raise util.Abort(inst)
       
    72 
       
    73     return send
       
    74 
       
    75 def _sendmail(ui, sender, recipients, msg):
       
    76     '''send mail using sendmail.'''
       
    77     program = ui.config('email', 'method')
       
    78     cmdline = '%s -f %s %s' % (program, util.email(sender),
       
    79                                ' '.join(map(util.email, recipients)))
       
    80     ui.note(_('sending mail: %s\n') % cmdline)
       
    81     fp = util.popen(cmdline, 'w')
       
    82     fp.write(msg)
       
    83     ret = fp.close()
       
    84     if ret:
       
    85         raise util.Abort('%s %s' % (
       
    86             os.path.basename(program.split(None, 1)[0]),
       
    87             util.explain_exit(ret)[0]))
       
    88 
       
    89 def connect(ui):
       
    90     '''make a mail connection. return a function to send mail.
       
    91     call as sendmail(sender, list-of-recipients, msg).'''
       
    92     if ui.config('email', 'method', 'smtp') == 'smtp':
       
    93         return _smtp(ui)
       
    94     return lambda s, r, m: _sendmail(ui, s, r, m)
       
    95 
       
    96 def sendmail(ui, sender, recipients, msg):
       
    97     send = connect(ui)
       
    98     return send(sender, recipients, msg)
       
    99 
       
   100 def validateconfig(ui):
       
   101     '''determine if we have enough config data to try sending email.'''
       
   102     method = ui.config('email', 'method', 'smtp')
       
   103     if method == 'smtp':
       
   104         if not ui.config('smtp', 'host'):
       
   105             raise util.Abort(_('smtp specified as email transport, '
       
   106                                'but no smtp host configured'))
       
   107     else:
       
   108         if not util.find_exe(method):
       
   109             raise util.Abort(_('%r specified as email transport, '
       
   110                                'but not in PATH') % method)
       
   111 
       
   112 def mimetextpatch(s, subtype='plain', display=False):
       
   113     '''If patch in utf-8 transfer-encode it.'''
       
   114 
       
   115     enc = None
       
   116     for line in s.splitlines():
       
   117         if len(line) > 950:
       
   118             s = quopri.encodestring(s)
       
   119             enc = "quoted-printable"
       
   120             break
       
   121 
       
   122     cs = 'us-ascii'
       
   123     if not display:
       
   124         try:
       
   125             s.decode('us-ascii')
       
   126         except UnicodeDecodeError:
       
   127             try:
       
   128                 s.decode('utf-8')
       
   129                 cs = 'utf-8'
       
   130             except UnicodeDecodeError:
       
   131                 # We'll go with us-ascii as a fallback.
       
   132                 pass
       
   133 
       
   134     msg = email.MIMEText.MIMEText(s, subtype, cs)
       
   135     if enc:
       
   136         del msg['Content-Transfer-Encoding']
       
   137         msg['Content-Transfer-Encoding'] = enc
       
   138     return msg
       
   139 
       
   140 def _charsets(ui):
       
   141     '''Obtains charsets to send mail parts not containing patches.'''
       
   142     charsets = [cs.lower() for cs in ui.configlist('email', 'charsets')]
       
   143     fallbacks = [encoding.fallbackencoding.lower(),
       
   144                  encoding.encoding.lower(), 'utf-8']
       
   145     for cs in fallbacks: # find unique charsets while keeping order
       
   146         if cs not in charsets:
       
   147             charsets.append(cs)
       
   148     return [cs for cs in charsets if not cs.endswith('ascii')]
       
   149 
       
   150 def _encode(ui, s, charsets):
       
   151     '''Returns (converted) string, charset tuple.
       
   152     Finds out best charset by cycling through sendcharsets in descending
       
   153     order. Tries both encoding and fallbackencoding for input. Only as
       
   154     last resort send as is in fake ascii.
       
   155     Caveat: Do not use for mail parts containing patches!'''
       
   156     try:
       
   157         s.decode('ascii')
       
   158     except UnicodeDecodeError:
       
   159         sendcharsets = charsets or _charsets(ui)
       
   160         for ics in (encoding.encoding, encoding.fallbackencoding):
       
   161             try:
       
   162                 u = s.decode(ics)
       
   163             except UnicodeDecodeError:
       
   164                 continue
       
   165             for ocs in sendcharsets:
       
   166                 try:
       
   167                     return u.encode(ocs), ocs
       
   168                 except UnicodeEncodeError:
       
   169                     pass
       
   170                 except LookupError:
       
   171                     ui.warn(_('ignoring invalid sendcharset: %s\n') % ocs)
       
   172     # if ascii, or all conversion attempts fail, send (broken) ascii
       
   173     return s, 'us-ascii'
       
   174 
       
   175 def headencode(ui, s, charsets=None, display=False):
       
   176     '''Returns RFC-2047 compliant header from given string.'''
       
   177     if not display:
       
   178         # split into words?
       
   179         s, cs = _encode(ui, s, charsets)
       
   180         return str(email.Header.Header(s, cs))
       
   181     return s
       
   182 
       
   183 def _addressencode(ui, name, addr, charsets=None):
       
   184     name = headencode(ui, name, charsets)
       
   185     try:
       
   186         acc, dom = addr.split('@')
       
   187         acc = acc.encode('ascii')
       
   188         dom = dom.decode(encoding.encoding).encode('idna')
       
   189         addr = '%s@%s' % (acc, dom)
       
   190     except UnicodeDecodeError:
       
   191         raise util.Abort(_('invalid email address: %s') % addr)
       
   192     except ValueError:
       
   193         try:
       
   194             # too strict?
       
   195             addr = addr.encode('ascii')
       
   196         except UnicodeDecodeError:
       
   197             raise util.Abort(_('invalid local address: %s') % addr)
       
   198     return email.Utils.formataddr((name, addr))
       
   199 
       
   200 def addressencode(ui, address, charsets=None, display=False):
       
   201     '''Turns address into RFC-2047 compliant header.'''
       
   202     if display or not address:
       
   203         return address or ''
       
   204     name, addr = email.Utils.parseaddr(address)
       
   205     return _addressencode(ui, name, addr, charsets)
       
   206 
       
   207 def addrlistencode(ui, addrs, charsets=None, display=False):
       
   208     '''Turns a list of addresses into a list of RFC-2047 compliant headers.
       
   209     A single element of input list may contain multiple addresses, but output
       
   210     always has one address per item'''
       
   211     if display:
       
   212         return [a.strip() for a in addrs if a.strip()]
       
   213 
       
   214     result = []
       
   215     for name, addr in email.Utils.getaddresses(addrs):
       
   216         if name or addr:
       
   217             result.append(_addressencode(ui, name, addr, charsets))
       
   218     return result
       
   219 
       
   220 def mimeencode(ui, s, charsets=None, display=False):
       
   221     '''creates mime text object, encodes it if needed, and sets
       
   222     charset and transfer-encoding accordingly.'''
       
   223     cs = 'us-ascii'
       
   224     if not display:
       
   225         s, cs = _encode(ui, s, charsets)
       
   226     return email.MIMEText.MIMEText(s, 'plain', cs)