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 |