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: |