1
# Copyright (C) 2001 Python Software Foundation
3
# Contact: email-sig@python.org
5
"""Miscellaneous utilities."""
8
'collapse_rfc2231_value',
21
'parsedate_to_datetime',
31
from email._parseaddr import quote
32
from email._parseaddr import AddressList as _AddressList
33
from email._parseaddr import mktime_tz
35
from email._parseaddr import parsedate, parsedate_tz, _parsedate_tz
43
specialsre = re.compile(r'[][\\()<>@,:;".]')
44
escapesre = re.compile(r'[\\"]')
47
def _has_surrogates(s):
48
"""Return True if s may contain surrogate-escaped binary data."""
49
# This check is based on the fact that unless there are surrogates, utf8
50
# (Python's default encoding) can encode any string. This is the fastest
51
# way to check for surrogates, see bpo-11454 (moved to gh-55663) for timings.
55
except UnicodeEncodeError:
58
# How to deal with a string containing bytes before handing it to the
59
# application through the 'normal' interface.
60
def _sanitize(string):
61
# Turn any escaped bytes into the Unicode 'unknown' char. If the escaped
62
# bytes happen to be utf-8 they will instead get decoded, even if they
63
# were invalid in the charset the source was supposed to be in. This
64
# seems like it is not a bad thing; a defect was still registered.
65
original_bytes = string.encode('utf-8', 'surrogateescape')
66
return original_bytes.decode('utf-8', 'replace')
72
def formataddr(pair, charset='utf-8'):
73
"""The inverse of parseaddr(), this takes a 2-tuple of the form
74
(realname, email_address) and returns the string value suitable
75
for an RFC 2822 From, To or Cc header.
77
If the first element of pair is false, then the second element is
80
The optional charset is the character set that is used to encode
81
realname in case realname is not ASCII safe. Can be an instance of str or
82
a Charset-like object which has a header_encode method. Default is
86
# The address MUST (per RFC) be ascii, so raise a UnicodeError if it isn't.
87
address.encode('ascii')
91
except UnicodeEncodeError:
92
if isinstance(charset, str):
93
# lazy import to improve module import time
94
from email.charset import Charset
95
charset = Charset(charset)
96
encoded_name = charset.header_encode(name)
97
return "%s <%s>" % (encoded_name, address)
100
if specialsre.search(name):
102
name = escapesre.sub(r'\\\g<0>', name)
103
return '%s%s%s <%s>' % (quotes, name, quotes, address)
107
def _iter_escaped_chars(addr):
110
for pos, ch in enumerate(addr):
112
yield (pos, '\\' + ch)
122
def _strip_quoted_realnames(addr):
123
"""Strip real names between quotes."""
131
for pos, ch in _iter_escaped_chars(addr):
136
if start != open_pos:
137
result.append(addr[start:open_pos])
141
if start < len(addr):
142
result.append(addr[start:])
144
return ''.join(result)
147
supports_strict_parsing = True
149
def getaddresses(fieldvalues, *, strict=True):
150
"""Return a list of (REALNAME, EMAIL) or ('','') for each fieldvalue.
152
When parsing fails for a fieldvalue, a 2-tuple of ('', '') is returned in
155
If strict is true, use a strict parser which rejects malformed inputs.
158
# If strict is true, if the resulting list of parsed addresses is greater
159
# than the number of fieldvalues in the input list, a parsing error has
160
# occurred and consequently a list containing a single empty 2-tuple [('',
161
# '')] is returned in its place. This is done to avoid invalid output.
163
# Malformed input: getaddresses(['alice@example.com <bob@example.com>'])
164
# Invalid output: [('', 'alice@example.com'), ('', 'bob@example.com')]
165
# Safe output: [('', '')]
168
all = COMMASPACE.join(str(v) for v in fieldvalues)
169
a = _AddressList(all)
172
fieldvalues = [str(v) for v in fieldvalues]
173
fieldvalues = _pre_parse_validation(fieldvalues)
174
addr = COMMASPACE.join(fieldvalues)
175
a = _AddressList(addr)
176
result = _post_parse_validation(a.addresslist)
178
# Treat output as invalid if the number of addresses is not equal to the
179
# expected number of addresses.
181
for v in fieldvalues:
182
# When a comma is used in the Real Name part it is not a deliminator.
183
# So strip those out before counting the commas.
184
v = _strip_quoted_realnames(v)
185
# Expected number of addresses: 1 + number of commas
186
n += 1 + v.count(',')
193
def _check_parenthesis(addr):
194
# Ignore parenthesis in quoted real names.
195
addr = _strip_quoted_realnames(addr)
198
for pos, ch in _iter_escaped_chars(addr):
208
def _pre_parse_validation(email_header_fields):
210
for v in email_header_fields:
211
if not _check_parenthesis(v):
213
accepted_values.append(v)
215
return accepted_values
218
def _post_parse_validation(parsed_email_header_tuples):
220
# The parser would have parsed a correctly formatted domain-literal
221
# The existence of an [ after parsing indicates a parsing failure
222
for v in parsed_email_header_tuples:
225
accepted_values.append(v)
227
return accepted_values
230
def _format_timetuple_and_zone(timetuple, zone):
231
return '%s, %02d %s %04d %02d:%02d:%02d %s' % (
232
['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][timetuple[6]],
234
['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
235
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][timetuple[1] - 1],
236
timetuple[0], timetuple[3], timetuple[4], timetuple[5],
239
def formatdate(timeval=None, localtime=False, usegmt=False):
240
"""Returns a date string as specified by RFC 2822, e.g.:
242
Fri, 09 Nov 2001 01:08:47 -0000
244
Optional timeval if given is a floating-point time value as accepted by
245
gmtime() and localtime(), otherwise the current time is used.
247
Optional localtime is a flag that when True, interprets timeval, and
248
returns a date relative to the local timezone instead of UTC, properly
249
taking daylight savings time into account.
251
Optional argument usegmt means that the timezone is written out as
252
an ascii string, not numeric one (so "GMT" instead of "+0000"). This
253
is needed for HTTP, and is only used when localtime==False.
255
# Note: we cannot use strftime() because that honors the locale and RFC
256
# 2822 requires that day and month names be the English abbreviations.
258
timeval = time.time()
259
dt = datetime.datetime.fromtimestamp(timeval, datetime.timezone.utc)
265
dt = dt.replace(tzinfo=None)
266
2
2
return format_datetime(dt, usegmt)
268
def format_datetime(dt, usegmt=False):
269
"""Turn a datetime into a date string as specified in RFC 2822.
271
If usegmt is True, dt must be an aware datetime with an offset of zero. In
272
this case 'GMT' will be rendered instead of the normal +0000 required by
273
RFC2822. This is to support HTTP headers involving date stamps.
277
if dt.tzinfo is None or dt.tzinfo != datetime.timezone.utc:
278
raise ValueError("usegmt option requires a UTC datetime")
280
elif dt.tzinfo is None:
283
zone = dt.strftime("%z")
284
return _format_timetuple_and_zone(now, zone)
287
def make_msgid(idstring=None, domain=None):
288
"""Returns a string suitable for RFC 2822 compliant Message-ID, e.g:
290
<142480216486.20800.16526388040877946887@nightshade.la.mastaler.com>
292
Optional idstring if given is a string used to strengthen the
293
uniqueness of the message id. Optional domain if given provides the
294
portion of the message id after the '@'. It defaults to the locally
297
# Lazy imports to speedup module import time
298
# (no other functions in email.utils need these modules)
302
timeval = int(time.time()*100)
304
randint = random.getrandbits(64)
308
idstring = '.' + idstring
310
domain = socket.getfqdn()
311
msgid = '<%d.%d.%d%s@%s>' % (timeval, pid, randint, idstring, domain)
315
def parsedate_to_datetime(data):
316
parsed_date_tz = _parsedate_tz(data)
317
if parsed_date_tz is None:
318
raise ValueError('Invalid date value or format "%s"' % str(data))
319
*dtuple, tz = parsed_date_tz
322
return datetime.datetime(*dtuple[:6])
323
return datetime.datetime(*dtuple[:6],
324
tzinfo=datetime.timezone(datetime.timedelta(seconds=tz)))
325
except OverflowError as exc:
326
raise ValueError('Invalid date value or format "%s"' % str(data)) from exc
329
def parseaddr(addr, *, strict=True):
331
Parse addr into its constituent realname and email address parts.
333
Return a tuple of realname and email address, unless the parse fails, in
334
which case return a 2-tuple of ('', '').
336
If strict is True, use a strict parser which rejects malformed inputs.
339
addrs = _AddressList(addr).addresslist
344
if isinstance(addr, list):
347
if not isinstance(addr, str):
350
addr = _pre_parse_validation([addr])[0]
351
addrs = _post_parse_validation(_AddressList(addr).addresslist)
353
if not addrs or len(addrs) > 1:
359
# rfc822.unquote() doesn't properly de-backslash-ify in Python pre-2.3.
361
"""Remove quotes from a string."""
363
if str.startswith('"') and str.endswith('"'):
364
return str[1:-1].replace('\\\\', '\\').replace('\\"', '"')
365
if str.startswith('<') and str.endswith('>'):
371
# RFC2231-related functions - parameter encoding and decoding
372
def decode_rfc2231(s):
373
"""Decode string according to RFC 2231"""
374
parts = s.split(TICK, 2)
380
def encode_rfc2231(s, charset=None, language=None):
381
"""Encode string according to RFC 2231.
383
If neither charset nor language is given, then s is returned as-is. If
384
charset is given but not language, the string is encoded using the empty
387
s = urllib.parse.quote(s, safe='', encoding=charset or 'ascii')
388
if charset is None and language is None:
392
return "%s'%s'%s" % (charset, language, s)
395
rfc2231_continuation = re.compile(r'^(?P<name>\w+)\*((?P<num>[0-9]+)\*?)?$',
398
def decode_params(params):
399
"""Decode parameters list according to RFC 2231.
401
params is a sequence of 2-tuples containing (param name, string value).
403
new_params = [params[0]]
404
# Map parameter's name to a list of continuations. The values are a
405
# 3-tuple of the continuation number, the string value, and a flag
406
# specifying whether a particular segment is %-encoded.
408
for name, value in params[1:]:
409
encoded = name.endswith('*')
410
value = unquote(value)
411
mo = rfc2231_continuation.match(name)
413
name, num = mo.group('name', 'num')
416
rfc2231_params.setdefault(name, []).append((num, value, encoded))
418
new_params.append((name, '"%s"' % quote(value)))
420
for name, continuations in rfc2231_params.items():
423
# Sort by number, treating None as 0 if there is no 0,
424
# and ignore it if there is already a 0.
425
has_zero = any(x[0] == 0 for x in continuations)
427
continuations = [x for x in continuations if x[0] is not None]
429
continuations = [(x[0] or 0, x[1], x[2]) for x in continuations]
430
continuations.sort(key=lambda x: x[0])
431
# And now append all values in numerical order, converting
432
# %-encodings for the encoded segments. If any of the
433
# continuation names ends in a *, then the entire string, after
434
# decoding segments and concatenating, must have the charset and
435
# language specifiers at the beginning of the string.
436
for num, s, encoded in continuations:
438
# Decode as "latin-1", so the characters in s directly
439
# represent the percent-encoded octet values.
440
# collapse_rfc2231_value treats this as an octet sequence.
441
s = urllib.parse.unquote(s, encoding="latin-1")
444
value = quote(EMPTYSTRING.join(value))
446
charset, language, value = decode_rfc2231(value)
447
new_params.append((name, (charset, language, '"%s"' % value)))
449
new_params.append((name, '"%s"' % value))
452
def collapse_rfc2231_value(value, errors='replace',
453
fallback_charset='us-ascii'):
454
if not isinstance(value, tuple) or len(value) != 3:
455
return unquote(value)
456
# While value comes to us as a string, we need it to be a bytes
457
# object. We do not want bytes() normal utf-8 decoder, we want a straight
458
# interpretation of the string as character bytes.
459
charset, language, text = value
461
# Issue 17369: if charset/lang is None, decode_rfc2231 couldn't parse
462
# the value, so use the fallback_charset.
463
charset = fallback_charset
464
rawbytes = bytes(text, 'raw-unicode-escape')
466
# Explicitly look up the codec for warning generation, see gh-140030
467
# Can be removed in 3.17
469
codecs.lookup(charset)
470
return str(rawbytes, charset, errors)
472
# charset is not a known codec.
477
# datetime doesn't provide a localtime function yet, so provide one. Code
478
# adapted from the patch in issue 9527. This may not be perfect, but it is
479
# better than not having it.
482
def localtime(dt=None):
483
"""Return local time as an aware datetime object.
485
If called without arguments, return current time. Otherwise *dt*
486
argument should be a datetime instance, and it is converted to the
487
local time zone according to the system time zone database. If *dt* is
488
naive (that is, dt.tzinfo is None), it is assumed to be in local time.
492
dt = datetime.datetime.now()
493
return dt.astimezone()