mercurial/mail.py
changeset 43626 bdb0ddab7bb3
parent 43625 8d9e2c2b6058
child 43627 af3e341dbf03
equal deleted inserted replaced
43625:8d9e2c2b6058 43626:bdb0ddab7bb3
   251                 _(b'%r specified as email transport, but not in PATH') % method
   251                 _(b'%r specified as email transport, but not in PATH') % method
   252             )
   252             )
   253 
   253 
   254 
   254 
   255 def codec2iana(cs):
   255 def codec2iana(cs):
   256     # type: (bytes) -> bytes
   256     # type: (str) -> str
   257     ''''''
   257     ''''''
   258     cs = pycompat.sysbytes(
   258     cs = email.charset.Charset(cs).input_charset.lower()
   259         email.charset.Charset(
       
   260             cs  # pytype: disable=wrong-arg-types
       
   261         ).input_charset.lower()
       
   262     )
       
   263 
   259 
   264     # "latin1" normalizes to "iso8859-1", standard calls for "iso-8859-1"
   260     # "latin1" normalizes to "iso8859-1", standard calls for "iso-8859-1"
   265     if cs.startswith(b"iso") and not cs.startswith(b"iso-"):
   261     if cs.startswith("iso") and not cs.startswith("iso-"):
   266         return b"iso-" + cs[3:]
   262         return "iso-" + cs[3:]
   267     return cs
   263     return cs
   268 
   264 
   269 
   265 
   270 def mimetextpatch(s, subtype=b'plain', display=False):
   266 def mimetextpatch(s, subtype=b'plain', display=False):
   271     # type: (bytes, bytes, bool) -> email.message.Message
   267     # type: (bytes, bytes, bool) -> email.message.Message
   273     Charset will be detected by first trying to decode as us-ascii, then utf-8,
   269     Charset will be detected by first trying to decode as us-ascii, then utf-8,
   274     and finally the global encodings. If all those fail, fall back to
   270     and finally the global encodings. If all those fail, fall back to
   275     ISO-8859-1, an encoding with that allows all byte sequences.
   271     ISO-8859-1, an encoding with that allows all byte sequences.
   276     Transfer encodings will be used if necessary.'''
   272     Transfer encodings will be used if necessary.'''
   277 
   273 
   278     cs = [b'us-ascii', b'utf-8', encoding.encoding, encoding.fallbackencoding]
   274     cs = [
       
   275         'us-ascii',
       
   276         'utf-8',
       
   277         pycompat.sysstr(encoding.encoding),
       
   278         pycompat.sysstr(encoding.fallbackencoding),
       
   279     ]
   279     if display:
   280     if display:
   280         cs = [b'us-ascii']
   281         cs = ['us-ascii']
   281     for charset in cs:
   282     for charset in cs:
   282         try:
   283         try:
   283             s.decode(pycompat.sysstr(charset))
   284             s.decode(charset)
   284             return mimetextqp(s, subtype, codec2iana(charset))
   285             return mimetextqp(s, subtype, codec2iana(charset))
   285         except UnicodeDecodeError:
   286         except UnicodeDecodeError:
   286             pass
   287             pass
   287 
   288 
   288     return mimetextqp(s, subtype, b"iso-8859-1")
   289     return mimetextqp(s, subtype, "iso-8859-1")
   289 
   290 
   290 
   291 
   291 def mimetextqp(body, subtype, charset):
   292 def mimetextqp(body, subtype, charset):
   292     # type: (bytes, bytes, bytes) -> email.message.Message
   293     # type: (bytes, bytes, str) -> email.message.Message
   293     '''Return MIME message.
   294     '''Return MIME message.
   294     Quoted-printable transfer encoding will be used if necessary.
   295     Quoted-printable transfer encoding will be used if necessary.
   295     '''
   296     '''
   296     # Experimentally charset is okay as a bytes even if the type
   297     cs = email.charset.Charset(charset)
   297     # stubs disagree.
       
   298     cs = email.charset.Charset(charset)  # pytype: disable=wrong-arg-types
       
   299     msg = email.message.Message()
   298     msg = email.message.Message()
   300     msg.set_type(pycompat.sysstr(b'text/' + subtype))
   299     msg.set_type(pycompat.sysstr(b'text/' + subtype))
   301 
   300 
   302     for line in body.splitlines():
   301     for line in body.splitlines():
   303         if len(line) > 950:
   302         if len(line) > 950:
   315 
   314 
   316     return msg
   315     return msg
   317 
   316 
   318 
   317 
   319 def _charsets(ui):
   318 def _charsets(ui):
   320     # type: (Any) -> List[bytes]
   319     # type: (Any) -> List[str]
   321     '''Obtains charsets to send mail parts not containing patches.'''
   320     '''Obtains charsets to send mail parts not containing patches.'''
   322     charsets = [
   321     charsets = [
   323         cs.lower() for cs in ui.configlist(b'email', b'charsets')
   322         pycompat.sysstr(cs.lower())
   324     ]  # type: List[bytes]
   323         for cs in ui.configlist(b'email', b'charsets')
       
   324     ]
   325     fallbacks = [
   325     fallbacks = [
   326         encoding.fallbackencoding.lower(),
   326         pycompat.sysstr(encoding.fallbackencoding.lower()),
   327         encoding.encoding.lower(),
   327         pycompat.sysstr(encoding.encoding.lower()),
   328         b'utf-8',
   328         'utf-8',
   329     ]  # type: List[bytes]
   329     ]
   330     for cs in fallbacks:  # find unique charsets while keeping order
   330     for cs in fallbacks:  # find unique charsets while keeping order
   331         if cs not in charsets:
   331         if cs not in charsets:
   332             charsets.append(cs)
   332             charsets.append(cs)
   333     return [cs for cs in charsets if not cs.endswith(b'ascii')]
   333     return [cs for cs in charsets if not cs.endswith('ascii')]
   334 
   334 
   335 
   335 
   336 def _encode(ui, s, charsets):
   336 def _encode(ui, s, charsets):
   337     # type: (Any, bytes, List[bytes]) -> Tuple[bytes, bytes]
   337     # type: (Any, bytes, List[str]) -> Tuple[bytes, str]
   338     '''Returns (converted) string, charset tuple.
   338     '''Returns (converted) string, charset tuple.
   339     Finds out best charset by cycling through sendcharsets in descending
   339     Finds out best charset by cycling through sendcharsets in descending
   340     order. Tries both encoding and fallbackencoding for input. Only as
   340     order. Tries both encoding and fallbackencoding for input. Only as
   341     last resort send as is in fake ascii.
   341     last resort send as is in fake ascii.
   342     Caveat: Do not use for mail parts containing patches!'''
   342     Caveat: Do not use for mail parts containing patches!'''
   345         # We have unicode data, which we need to try and encode to
   345         # We have unicode data, which we need to try and encode to
   346         # some reasonable-ish encoding. Try the encodings the user
   346         # some reasonable-ish encoding. Try the encodings the user
   347         # wants, and fall back to garbage-in-ascii.
   347         # wants, and fall back to garbage-in-ascii.
   348         for ocs in sendcharsets:
   348         for ocs in sendcharsets:
   349             try:
   349             try:
   350                 return s.encode(pycompat.sysstr(ocs)), ocs
   350                 return s.encode(ocs), ocs
   351             except UnicodeEncodeError:
   351             except UnicodeEncodeError:
   352                 pass
   352                 pass
   353             except LookupError:
   353             except LookupError:
   354                 ui.warn(_(b'ignoring invalid sendcharset: %s\n') % ocs)
   354                 ui.warn(
       
   355                     _(b'ignoring invalid sendcharset: %s\n')
       
   356                     % pycompat.sysbytes(ocs)
       
   357                 )
   355         else:
   358         else:
   356             # Everything failed, ascii-armor what we've got and send it.
   359             # Everything failed, ascii-armor what we've got and send it.
   357             return s.encode('ascii', 'backslashreplace'), b'us-ascii'
   360             return s.encode('ascii', 'backslashreplace'), 'us-ascii'
   358     # We have a bytes of unknown encoding. We'll try and guess a valid
   361     # We have a bytes of unknown encoding. We'll try and guess a valid
   359     # encoding, falling back to pretending we had ascii even though we
   362     # encoding, falling back to pretending we had ascii even though we
   360     # know that's wrong.
   363     # know that's wrong.
   361     try:
   364     try:
   362         s.decode('ascii')
   365         s.decode('ascii')
   367                 u = s.decode(ics)
   370                 u = s.decode(ics)
   368             except UnicodeDecodeError:
   371             except UnicodeDecodeError:
   369                 continue
   372                 continue
   370             for ocs in sendcharsets:
   373             for ocs in sendcharsets:
   371                 try:
   374                 try:
   372                     return u.encode(pycompat.sysstr(ocs)), ocs
   375                     return u.encode(ocs), ocs
   373                 except UnicodeEncodeError:
   376                 except UnicodeEncodeError:
   374                     pass
   377                     pass
   375                 except LookupError:
   378                 except LookupError:
   376                     ui.warn(_(b'ignoring invalid sendcharset: %s\n') % ocs)
   379                     ui.warn(
       
   380                         _(b'ignoring invalid sendcharset: %s\n')
       
   381                         % pycompat.sysbytes(ocs)
       
   382                     )
   377     # if ascii, or all conversion attempts fail, send (broken) ascii
   383     # if ascii, or all conversion attempts fail, send (broken) ascii
   378     return s, b'us-ascii'
   384     return s, 'us-ascii'
   379 
   385 
   380 
   386 
   381 def headencode(ui, s, charsets=None, display=False):
   387 def headencode(ui, s, charsets=None, display=False):
   382     # type: (Any, Union[bytes, str], List[bytes], bool) -> str
   388     # type: (Any, Union[bytes, str], List[str], bool) -> str
   383     '''Returns RFC-2047 compliant header from given string.'''
   389     '''Returns RFC-2047 compliant header from given string.'''
   384     if not display:
   390     if not display:
   385         # split into words?
   391         # split into words?
   386         s, cs = _encode(ui, s, charsets)
   392         s, cs = _encode(ui, s, charsets)
   387         return email.header.Header(
   393         return email.header.Header(s, cs).encode()
   388             s, cs  # pytype: disable=wrong-arg-types
       
   389         ).encode()
       
   390     return encoding.strfromlocal(s)
   394     return encoding.strfromlocal(s)
   391 
   395 
   392 
   396 
   393 def _addressencode(ui, name, addr, charsets=None):
   397 def _addressencode(ui, name, addr, charsets=None):
   394     # type: (Any, str, bytes, List[bytes]) -> str
   398     # type: (Any, str, bytes, List[str]) -> str
   395     assert isinstance(addr, bytes)
   399     assert isinstance(addr, bytes)
   396     name = headencode(ui, name, charsets)
   400     name = headencode(ui, name, charsets)
   397     try:
   401     try:
   398         acc, dom = addr.split(b'@')
   402         acc, dom = addr.split(b'@')
   399         acc.decode('ascii')
   403         acc.decode('ascii')
   409             raise error.Abort(_(b'invalid local address: %s') % addr)
   413             raise error.Abort(_(b'invalid local address: %s') % addr)
   410     return email.utils.formataddr((name, encoding.strfromlocal(addr)))
   414     return email.utils.formataddr((name, encoding.strfromlocal(addr)))
   411 
   415 
   412 
   416 
   413 def addressencode(ui, address, charsets=None, display=False):
   417 def addressencode(ui, address, charsets=None, display=False):
   414     # type: (Any, bytes, List[bytes], bool) -> str
   418     # type: (Any, bytes, List[str], bool) -> str
   415     '''Turns address into RFC-2047 compliant header.'''
   419     '''Turns address into RFC-2047 compliant header.'''
   416     if display or not address:
   420     if display or not address:
   417         return encoding.strfromlocal(address or b'')
   421         return encoding.strfromlocal(address or b'')
   418     name, addr = email.utils.parseaddr(encoding.strfromlocal(address))
   422     name, addr = email.utils.parseaddr(encoding.strfromlocal(address))
   419     return _addressencode(ui, name, encoding.strtolocal(addr), charsets)
   423     return _addressencode(ui, name, encoding.strtolocal(addr), charsets)
   420 
   424 
   421 
   425 
   422 def addrlistencode(ui, addrs, charsets=None, display=False):
   426 def addrlistencode(ui, addrs, charsets=None, display=False):
   423     # type: (Any, List[bytes], List[bytes], bool) -> List[str]
   427     # type: (Any, List[bytes], List[str], bool) -> List[str]
   424     '''Turns a list of addresses into a list of RFC-2047 compliant headers.
   428     '''Turns a list of addresses into a list of RFC-2047 compliant headers.
   425     A single element of input list may contain multiple addresses, but output
   429     A single element of input list may contain multiple addresses, but output
   426     always has one address per item'''
   430     always has one address per item'''
   427     straddrs = []
   431     straddrs = []
   428     for a in addrs:
   432     for a in addrs:
   438             result.append(r)
   442             result.append(r)
   439     return result
   443     return result
   440 
   444 
   441 
   445 
   442 def mimeencode(ui, s, charsets=None, display=False):
   446 def mimeencode(ui, s, charsets=None, display=False):
   443     # type: (Any, bytes, List[bytes], bool) -> email.message.Message
   447     # type: (Any, bytes, List[str], bool) -> email.message.Message
   444     '''creates mime text object, encodes it if needed, and sets
   448     '''creates mime text object, encodes it if needed, and sets
   445     charset and transfer-encoding accordingly.'''
   449     charset and transfer-encoding accordingly.'''
   446     cs = b'us-ascii'
   450     cs = 'us-ascii'
   447     if not display:
   451     if not display:
   448         s, cs = _encode(ui, s, charsets)
   452         s, cs = _encode(ui, s, charsets)
   449     return mimetextqp(s, b'plain', cs)
   453     return mimetextqp(s, b'plain', cs)
   450 
   454 
   451 
   455