mercurial/mail.py
changeset 43625 8d9e2c2b6058
parent 43624 849a3f81f46c
child 43626 bdb0ddab7bb3
equal deleted inserted replaced
43624:849a3f81f46c 43625:8d9e2c2b6058
    33 )
    33 )
    34 from .utils import (
    34 from .utils import (
    35     procutil,
    35     procutil,
    36     stringutil,
    36     stringutil,
    37 )
    37 )
       
    38 
       
    39 if not globals():  # hide this from non-pytype users
       
    40     from typing import Any, List, Tuple, Union
       
    41 
       
    42     # keep pyflakes happy
       
    43     assert all((Any, List, Tuple, Union))
    38 
    44 
    39 
    45 
    40 class STARTTLS(smtplib.SMTP):
    46 class STARTTLS(smtplib.SMTP):
    41     '''Derived class to verify the peer certificate for STARTTLS.
    47     '''Derived class to verify the peer certificate for STARTTLS.
    42 
    48 
    97         self.file = new_socket.makefile('rb')
   103         self.file = new_socket.makefile('rb')
    98         return new_socket
   104         return new_socket
    99 
   105 
   100 
   106 
   101 def _pyhastls():
   107 def _pyhastls():
       
   108     # type: () -> bool
   102     """Returns true iff Python has TLS support, false otherwise."""
   109     """Returns true iff Python has TLS support, false otherwise."""
   103     try:
   110     try:
   104         import ssl
   111         import ssl
   105 
   112 
   106         getattr(ssl, 'HAS_TLS', False)
   113         getattr(ssl, 'HAS_TLS', False)
   244                 _(b'%r specified as email transport, but not in PATH') % method
   251                 _(b'%r specified as email transport, but not in PATH') % method
   245             )
   252             )
   246 
   253 
   247 
   254 
   248 def codec2iana(cs):
   255 def codec2iana(cs):
       
   256     # type: (bytes) -> bytes
   249     ''''''
   257     ''''''
   250     cs = pycompat.sysbytes(email.charset.Charset(cs).input_charset.lower())
   258     cs = pycompat.sysbytes(
       
   259         email.charset.Charset(
       
   260             cs  # pytype: disable=wrong-arg-types
       
   261         ).input_charset.lower()
       
   262     )
   251 
   263 
   252     # "latin1" normalizes to "iso8859-1", standard calls for "iso-8859-1"
   264     # "latin1" normalizes to "iso8859-1", standard calls for "iso-8859-1"
   253     if cs.startswith(b"iso") and not cs.startswith(b"iso-"):
   265     if cs.startswith(b"iso") and not cs.startswith(b"iso-"):
   254         return b"iso-" + cs[3:]
   266         return b"iso-" + cs[3:]
   255     return cs
   267     return cs
   256 
   268 
   257 
   269 
   258 def mimetextpatch(s, subtype=b'plain', display=False):
   270 def mimetextpatch(s, subtype=b'plain', display=False):
       
   271     # type: (bytes, bytes, bool) -> email.message.Message
   259     '''Return MIME message suitable for a patch.
   272     '''Return MIME message suitable for a patch.
   260     Charset will be detected by first trying to decode as us-ascii, then utf-8,
   273     Charset will be detected by first trying to decode as us-ascii, then utf-8,
   261     and finally the global encodings. If all those fail, fall back to
   274     and finally the global encodings. If all those fail, fall back to
   262     ISO-8859-1, an encoding with that allows all byte sequences.
   275     ISO-8859-1, an encoding with that allows all byte sequences.
   263     Transfer encodings will be used if necessary.'''
   276     Transfer encodings will be used if necessary.'''
   274 
   287 
   275     return mimetextqp(s, subtype, b"iso-8859-1")
   288     return mimetextqp(s, subtype, b"iso-8859-1")
   276 
   289 
   277 
   290 
   278 def mimetextqp(body, subtype, charset):
   291 def mimetextqp(body, subtype, charset):
       
   292     # type: (bytes, bytes, bytes) -> email.message.Message
   279     '''Return MIME message.
   293     '''Return MIME message.
   280     Quoted-printable transfer encoding will be used if necessary.
   294     Quoted-printable transfer encoding will be used if necessary.
   281     '''
   295     '''
   282     # Experimentally charset is okay as a bytes even if the type
   296     # Experimentally charset is okay as a bytes even if the type
   283     # stubs disagree.
   297     # stubs disagree.
   301 
   315 
   302     return msg
   316     return msg
   303 
   317 
   304 
   318 
   305 def _charsets(ui):
   319 def _charsets(ui):
       
   320     # type: (Any) -> List[bytes]
   306     '''Obtains charsets to send mail parts not containing patches.'''
   321     '''Obtains charsets to send mail parts not containing patches.'''
   307     charsets = [cs.lower() for cs in ui.configlist(b'email', b'charsets')]
   322     charsets = [
       
   323         cs.lower() for cs in ui.configlist(b'email', b'charsets')
       
   324     ]  # type: List[bytes]
   308     fallbacks = [
   325     fallbacks = [
   309         encoding.fallbackencoding.lower(),
   326         encoding.fallbackencoding.lower(),
   310         encoding.encoding.lower(),
   327         encoding.encoding.lower(),
   311         b'utf-8',
   328         b'utf-8',
   312     ]
   329     ]  # type: List[bytes]
   313     for cs in fallbacks:  # find unique charsets while keeping order
   330     for cs in fallbacks:  # find unique charsets while keeping order
   314         if cs not in charsets:
   331         if cs not in charsets:
   315             charsets.append(cs)
   332             charsets.append(cs)
   316     return [cs for cs in charsets if not cs.endswith(b'ascii')]
   333     return [cs for cs in charsets if not cs.endswith(b'ascii')]
   317 
   334 
   318 
   335 
   319 def _encode(ui, s, charsets):
   336 def _encode(ui, s, charsets):
       
   337     # type: (Any, bytes, List[bytes]) -> Tuple[bytes, bytes]
   320     '''Returns (converted) string, charset tuple.
   338     '''Returns (converted) string, charset tuple.
   321     Finds out best charset by cycling through sendcharsets in descending
   339     Finds out best charset by cycling through sendcharsets in descending
   322     order. Tries both encoding and fallbackencoding for input. Only as
   340     order. Tries both encoding and fallbackencoding for input. Only as
   323     last resort send as is in fake ascii.
   341     last resort send as is in fake ascii.
   324     Caveat: Do not use for mail parts containing patches!'''
   342     Caveat: Do not use for mail parts containing patches!'''
   359     # if ascii, or all conversion attempts fail, send (broken) ascii
   377     # if ascii, or all conversion attempts fail, send (broken) ascii
   360     return s, b'us-ascii'
   378     return s, b'us-ascii'
   361 
   379 
   362 
   380 
   363 def headencode(ui, s, charsets=None, display=False):
   381 def headencode(ui, s, charsets=None, display=False):
       
   382     # type: (Any, Union[bytes, str], List[bytes], bool) -> str
   364     '''Returns RFC-2047 compliant header from given string.'''
   383     '''Returns RFC-2047 compliant header from given string.'''
   365     if not display:
   384     if not display:
   366         # split into words?
   385         # split into words?
   367         s, cs = _encode(ui, s, charsets)
   386         s, cs = _encode(ui, s, charsets)
   368         return email.header.Header(s, cs).encode()
   387         return email.header.Header(
       
   388             s, cs  # pytype: disable=wrong-arg-types
       
   389         ).encode()
   369     return encoding.strfromlocal(s)
   390     return encoding.strfromlocal(s)
   370 
   391 
   371 
   392 
   372 def _addressencode(ui, name, addr, charsets=None):
   393 def _addressencode(ui, name, addr, charsets=None):
       
   394     # type: (Any, str, bytes, List[bytes]) -> str
   373     assert isinstance(addr, bytes)
   395     assert isinstance(addr, bytes)
   374     name = headencode(ui, name, charsets)
   396     name = headencode(ui, name, charsets)
   375     try:
   397     try:
   376         acc, dom = addr.split(b'@')
   398         acc, dom = addr.split(b'@')
   377         acc.decode('ascii')
   399         acc.decode('ascii')
   387             raise error.Abort(_(b'invalid local address: %s') % addr)
   409             raise error.Abort(_(b'invalid local address: %s') % addr)
   388     return email.utils.formataddr((name, encoding.strfromlocal(addr)))
   410     return email.utils.formataddr((name, encoding.strfromlocal(addr)))
   389 
   411 
   390 
   412 
   391 def addressencode(ui, address, charsets=None, display=False):
   413 def addressencode(ui, address, charsets=None, display=False):
       
   414     # type: (Any, bytes, List[bytes], bool) -> str
   392     '''Turns address into RFC-2047 compliant header.'''
   415     '''Turns address into RFC-2047 compliant header.'''
   393     if display or not address:
   416     if display or not address:
   394         return encoding.strfromlocal(address or b'')
   417         return encoding.strfromlocal(address or b'')
   395     name, addr = email.utils.parseaddr(encoding.strfromlocal(address))
   418     name, addr = email.utils.parseaddr(encoding.strfromlocal(address))
   396     return _addressencode(ui, name, encoding.strtolocal(addr), charsets)
   419     return _addressencode(ui, name, encoding.strtolocal(addr), charsets)
   397 
   420 
   398 
   421 
   399 def addrlistencode(ui, addrs, charsets=None, display=False):
   422 def addrlistencode(ui, addrs, charsets=None, display=False):
       
   423     # type: (Any, List[bytes], List[bytes], bool) -> List[str]
   400     '''Turns a list of addresses into a list of RFC-2047 compliant headers.
   424     '''Turns a list of addresses into a list of RFC-2047 compliant headers.
   401     A single element of input list may contain multiple addresses, but output
   425     A single element of input list may contain multiple addresses, but output
   402     always has one address per item'''
   426     always has one address per item'''
   403     straddrs = []
   427     straddrs = []
   404     for a in addrs:
   428     for a in addrs:
   414             result.append(r)
   438             result.append(r)
   415     return result
   439     return result
   416 
   440 
   417 
   441 
   418 def mimeencode(ui, s, charsets=None, display=False):
   442 def mimeencode(ui, s, charsets=None, display=False):
       
   443     # type: (Any, bytes, List[bytes], bool) -> email.message.Message
   419     '''creates mime text object, encodes it if needed, and sets
   444     '''creates mime text object, encodes it if needed, and sets
   420     charset and transfer-encoding accordingly.'''
   445     charset and transfer-encoding accordingly.'''
   421     cs = b'us-ascii'
   446     cs = b'us-ascii'
   422     if not display:
   447     if not display:
   423         s, cs = _encode(ui, s, charsets)
   448         s, cs = _encode(ui, s, charsets)
   427 if pycompat.ispy3:
   452 if pycompat.ispy3:
   428 
   453 
   429     Generator = email.generator.BytesGenerator
   454     Generator = email.generator.BytesGenerator
   430 
   455 
   431     def parse(fp):
   456     def parse(fp):
       
   457         # type: (Any) -> email.message.Message
   432         ep = email.parser.Parser()
   458         ep = email.parser.Parser()
   433         # disable the "universal newlines" mode, which isn't binary safe.
   459         # disable the "universal newlines" mode, which isn't binary safe.
   434         # I have no idea if ascii/surrogateescape is correct, but that's
   460         # I have no idea if ascii/surrogateescape is correct, but that's
   435         # what the standard Python email parser does.
   461         # what the standard Python email parser does.
   436         fp = io.TextIOWrapper(
   462         fp = io.TextIOWrapper(
   440             return ep.parse(fp)
   466             return ep.parse(fp)
   441         finally:
   467         finally:
   442             fp.detach()
   468             fp.detach()
   443 
   469 
   444     def parsebytes(data):
   470     def parsebytes(data):
       
   471         # type: (bytes) -> email.message.Message
   445         ep = email.parser.BytesParser()
   472         ep = email.parser.BytesParser()
   446         return ep.parsebytes(data)
   473         return ep.parsebytes(data)
   447 
   474 
   448 
   475 
   449 else:
   476 else:
   450 
   477 
   451     Generator = email.generator.Generator
   478     Generator = email.generator.Generator
   452 
   479 
   453     def parse(fp):
   480     def parse(fp):
       
   481         # type: (Any) -> email.message.Message
   454         ep = email.parser.Parser()
   482         ep = email.parser.Parser()
   455         return ep.parse(fp)
   483         return ep.parse(fp)
   456 
   484 
   457     def parsebytes(data):
   485     def parsebytes(data):
       
   486         # type: (str) -> email.message.Message
   458         ep = email.parser.Parser()
   487         ep = email.parser.Parser()
   459         return ep.parsestr(data)
   488         return ep.parsestr(data)
   460 
   489 
   461 
   490 
   462 def headdecode(s):
   491 def headdecode(s):
       
   492     # type: (Union[email.header.Header, bytes]) -> bytes
   463     '''Decodes RFC-2047 header'''
   493     '''Decodes RFC-2047 header'''
   464     uparts = []
   494     uparts = []
   465     for part, charset in email.header.decode_header(s):
   495     for part, charset in email.header.decode_header(s):
   466         if charset is not None:
   496         if charset is not None:
   467             try:
   497             try: