mercurial/mail.py
changeset 51287 f15cb5111a1e
parent 51285 9d3721552b6c
child 51290 f4a0806081f2
equal deleted inserted replaced
51286:81224afd938d 51287:f15cb5111a1e
    19 import time
    19 import time
    20 
    20 
    21 from typing import (
    21 from typing import (
    22     Any,
    22     Any,
    23     List,
    23     List,
       
    24     Optional,
    24     Tuple,
    25     Tuple,
    25     Union,
    26     Union,
    26 )
    27 )
    27 
    28 
    28 from .i18n import _
    29 from .i18n import _
   111         )
   112         )
   112         self.file = new_socket.makefile('rb')
   113         self.file = new_socket.makefile('rb')
   113         return new_socket
   114         return new_socket
   114 
   115 
   115 
   116 
   116 def _pyhastls():
   117 def _pyhastls() -> bool:
   117     # type: () -> bool
       
   118     """Returns true iff Python has TLS support, false otherwise."""
   118     """Returns true iff Python has TLS support, false otherwise."""
   119     try:
   119     try:
   120         import ssl
   120         import ssl
   121 
   121 
   122         getattr(ssl, 'HAS_TLS', False)
   122         getattr(ssl, 'HAS_TLS', False)
   275             raise error.Abort(
   275             raise error.Abort(
   276                 _(b'%r specified as email transport, but not in PATH') % command
   276                 _(b'%r specified as email transport, but not in PATH') % command
   277             )
   277             )
   278 
   278 
   279 
   279 
   280 def codec2iana(cs):
   280 def codec2iana(cs: str) -> str:
   281     # type: (str) -> str
       
   282     ''' '''
   281     ''' '''
   283     cs = email.charset.Charset(cs).input_charset.lower()
   282     cs = email.charset.Charset(cs).input_charset.lower()
   284 
   283 
   285     # "latin1" normalizes to "iso8859-1", standard calls for "iso-8859-1"
   284     # "latin1" normalizes to "iso8859-1", standard calls for "iso-8859-1"
   286     if cs.startswith("iso") and not cs.startswith("iso-"):
   285     if cs.startswith("iso") and not cs.startswith("iso-"):
   287         return "iso-" + cs[3:]
   286         return "iso-" + cs[3:]
   288     return cs
   287     return cs
   289 
   288 
   290 
   289 
   291 def mimetextpatch(s, subtype='plain', display=False):
   290 def mimetextpatch(
   292     # type: (bytes, str, bool) -> email.message.Message
   291     s: bytes,
       
   292     subtype: str = 'plain',
       
   293     display: bool = False,
       
   294 ) -> email.message.Message:
   293     """Return MIME message suitable for a patch.
   295     """Return MIME message suitable for a patch.
   294     Charset will be detected by first trying to decode as us-ascii, then utf-8,
   296     Charset will be detected by first trying to decode as us-ascii, then utf-8,
   295     and finally the global encodings. If all those fail, fall back to
   297     and finally the global encodings. If all those fail, fall back to
   296     ISO-8859-1, an encoding with that allows all byte sequences.
   298     ISO-8859-1, an encoding with that allows all byte sequences.
   297     Transfer encodings will be used if necessary."""
   299     Transfer encodings will be used if necessary."""
   312             pass
   314             pass
   313 
   315 
   314     return mimetextqp(s, subtype, "iso-8859-1")
   316     return mimetextqp(s, subtype, "iso-8859-1")
   315 
   317 
   316 
   318 
   317 def mimetextqp(body, subtype, charset):
   319 def mimetextqp(
   318     # type: (bytes, str, str) -> email.message.Message
   320     body: bytes, subtype: str, charset: str
       
   321 ) -> email.message.Message:
   319     """Return MIME message.
   322     """Return MIME message.
   320     Quoted-printable transfer encoding will be used if necessary.
   323     Quoted-printable transfer encoding will be used if necessary.
   321     """
   324     """
   322     cs = email.charset.Charset(charset)
   325     cs = email.charset.Charset(charset)
   323     msg = email.message.Message()
   326     msg = email.message.Message()
   338     msg.set_payload(body, cs)
   341     msg.set_payload(body, cs)
   339 
   342 
   340     return msg
   343     return msg
   341 
   344 
   342 
   345 
   343 def _charsets(ui):
   346 def _charsets(ui: Any) -> List[str]:
   344     # type: (Any) -> List[str]
       
   345     '''Obtains charsets to send mail parts not containing patches.'''
   347     '''Obtains charsets to send mail parts not containing patches.'''
   346     charsets = [
   348     charsets = [
   347         pycompat.sysstr(cs.lower())
   349         pycompat.sysstr(cs.lower())
   348         for cs in ui.configlist(b'email', b'charsets')
   350         for cs in ui.configlist(b'email', b'charsets')
   349     ]
   351     ]
   356         if cs not in charsets:
   358         if cs not in charsets:
   357             charsets.append(cs)
   359             charsets.append(cs)
   358     return [cs for cs in charsets if not cs.endswith('ascii')]
   360     return [cs for cs in charsets if not cs.endswith('ascii')]
   359 
   361 
   360 
   362 
   361 def _encode(ui, s, charsets):
   363 def _encode(ui: Any, s: bytes, charsets: List[str]) -> Tuple[bytes, str]:
   362     # type: (Any, bytes, List[str]) -> Tuple[bytes, str]
       
   363     """Returns (converted) string, charset tuple.
   364     """Returns (converted) string, charset tuple.
   364     Finds out best charset by cycling through sendcharsets in descending
   365     Finds out best charset by cycling through sendcharsets in descending
   365     order. Tries both encoding and fallbackencoding for input. Only as
   366     order. Tries both encoding and fallbackencoding for input. Only as
   366     last resort send as is in fake ascii.
   367     last resort send as is in fake ascii.
   367     Caveat: Do not use for mail parts containing patches!"""
   368     Caveat: Do not use for mail parts containing patches!"""
   407                     )
   408                     )
   408     # if ascii, or all conversion attempts fail, send (broken) ascii
   409     # if ascii, or all conversion attempts fail, send (broken) ascii
   409     return s, 'us-ascii'
   410     return s, 'us-ascii'
   410 
   411 
   411 
   412 
   412 def headencode(ui, s, charsets=None, display=False):
   413 def headencode(
   413     # type: (Any, Union[bytes, str], List[str], bool) -> str
   414     ui: Any,
       
   415     s: Union[bytes, str],
       
   416     charsets: Optional[List[str]] = None,
       
   417     display: bool = False,
       
   418 ) -> str:
   414     '''Returns RFC-2047 compliant header from given string.'''
   419     '''Returns RFC-2047 compliant header from given string.'''
   415     if not display:
   420     if not display:
   416         # split into words?
   421         # split into words?
   417         s, cs = _encode(ui, s, charsets)
   422         s, cs = _encode(ui, s, charsets)
   418         return email.header.Header(s, cs).encode()
   423         return email.header.Header(s, cs).encode()
   419     return encoding.strfromlocal(s)
   424     return encoding.strfromlocal(s)
   420 
   425 
   421 
   426 
   422 def _addressencode(ui, name, addr, charsets=None):
   427 def _addressencode(
   423     # type: (Any, str, str, List[str]) -> str
   428     ui: Any, name: str, addr: str, charsets: Optional[List[str]] = None
       
   429 ) -> str:
   424     addr = encoding.strtolocal(addr)
   430     addr = encoding.strtolocal(addr)
   425     name = headencode(ui, name, charsets)
   431     name = headencode(ui, name, charsets)
   426     try:
   432     try:
   427         acc, dom = addr.split(b'@')
   433         acc, dom = addr.split(b'@')
   428         acc.decode('ascii')
   434         acc.decode('ascii')
   437         except UnicodeDecodeError:
   443         except UnicodeDecodeError:
   438             raise error.Abort(_(b'invalid local address: %s') % addr)
   444             raise error.Abort(_(b'invalid local address: %s') % addr)
   439     return email.utils.formataddr((name, encoding.strfromlocal(addr)))
   445     return email.utils.formataddr((name, encoding.strfromlocal(addr)))
   440 
   446 
   441 
   447 
   442 def addressencode(ui, address, charsets=None, display=False):
   448 def addressencode(
   443     # type: (Any, bytes, List[str], bool) -> str
   449     ui: Any,
       
   450     address: bytes,
       
   451     charsets: Optional[List[str]] = None,
       
   452     display: bool = False,
       
   453 ) -> str:
   444     '''Turns address into RFC-2047 compliant header.'''
   454     '''Turns address into RFC-2047 compliant header.'''
   445     if display or not address:
   455     if display or not address:
   446         return encoding.strfromlocal(address or b'')
   456         return encoding.strfromlocal(address or b'')
   447     name, addr = email.utils.parseaddr(encoding.strfromlocal(address))
   457     name, addr = email.utils.parseaddr(encoding.strfromlocal(address))
   448     return _addressencode(ui, name, addr, charsets)
   458     return _addressencode(ui, name, addr, charsets)
   449 
   459 
   450 
   460 
   451 def addrlistencode(ui, addrs, charsets=None, display=False):
   461 def addrlistencode(
   452     # type: (Any, List[bytes], List[str], bool) -> List[str]
   462     ui: Any,
       
   463     addrs: List[bytes],
       
   464     charsets: Optional[List[str]] = None,
       
   465     display: bool = False,
       
   466 ) -> List[str]:
   453     """Turns a list of addresses into a list of RFC-2047 compliant headers.
   467     """Turns a list of addresses into a list of RFC-2047 compliant headers.
   454     A single element of input list may contain multiple addresses, but output
   468     A single element of input list may contain multiple addresses, but output
   455     always has one address per item"""
   469     always has one address per item"""
   456     straddrs = []
   470     straddrs = []
   457     for a in addrs:
   471     for a in addrs:
   466             r = _addressencode(ui, name, addr, charsets)
   480             r = _addressencode(ui, name, addr, charsets)
   467             result.append(r)
   481             result.append(r)
   468     return result
   482     return result
   469 
   483 
   470 
   484 
   471 def mimeencode(ui, s, charsets=None, display=False):
   485 def mimeencode(
   472     # type: (Any, bytes, List[str], bool) -> email.message.Message
   486     ui: Any,
       
   487     s: bytes,
       
   488     charsets: Optional[List[str]] = None,
       
   489     display: bool = False,
       
   490 ) -> email.message.Message:
   473     """creates mime text object, encodes it if needed, and sets
   491     """creates mime text object, encodes it if needed, and sets
   474     charset and transfer-encoding accordingly."""
   492     charset and transfer-encoding accordingly."""
   475     cs = 'us-ascii'
   493     cs = 'us-ascii'
   476     if not display:
   494     if not display:
   477         s, cs = _encode(ui, s, charsets)
   495         s, cs = _encode(ui, s, charsets)
   479 
   497 
   480 
   498 
   481 Generator = email.generator.BytesGenerator
   499 Generator = email.generator.BytesGenerator
   482 
   500 
   483 
   501 
   484 def parse(fp):
   502 def parse(fp: Any) -> email.message.Message:
   485     # type: (Any) -> email.message.Message
       
   486     ep = email.parser.Parser()
   503     ep = email.parser.Parser()
   487     # disable the "universal newlines" mode, which isn't binary safe.
   504     # disable the "universal newlines" mode, which isn't binary safe.
   488     # I have no idea if ascii/surrogateescape is correct, but that's
   505     # I have no idea if ascii/surrogateescape is correct, but that's
   489     # what the standard Python email parser does.
   506     # what the standard Python email parser does.
   490     fp = io.TextIOWrapper(
   507     fp = io.TextIOWrapper(
   494         return ep.parse(fp)
   511         return ep.parse(fp)
   495     finally:
   512     finally:
   496         fp.detach()
   513         fp.detach()
   497 
   514 
   498 
   515 
   499 def parsebytes(data):
   516 def parsebytes(data: bytes) -> email.message.Message:
   500     # type: (bytes) -> email.message.Message
       
   501     ep = email.parser.BytesParser()
   517     ep = email.parser.BytesParser()
   502     return ep.parsebytes(data)
   518     return ep.parsebytes(data)
   503 
   519 
   504 
   520 
   505 def headdecode(s):
   521 def headdecode(s: Union[email.header.Header, bytes]) -> bytes:
   506     # type: (Union[email.header.Header, bytes]) -> bytes
       
   507     '''Decodes RFC-2047 header'''
   522     '''Decodes RFC-2047 header'''
   508     uparts = []
   523     uparts = []
   509     for part, charset in email.header.decode_header(s):
   524     for part, charset in email.header.decode_header(s):
   510         if charset is not None:
   525         if charset is not None:
   511             try:
   526             try: