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