# Copyright (C) 2001-2010 Python Software Foundation
# Contact: email-sig@python.org
"""Miscellaneous utilities."""
'collapse_rfc2231_value',
from email._parseaddr import quote
from email._parseaddr import AddressList as _AddressList
from email._parseaddr import mktime_tz
from email._parseaddr import parsedate, parsedate_tz, _parsedate_tz
from email.charset import Charset
specialsre = re.compile(r'[][\\()<>@,:;".]')
escapesre = re.compile(r'[\\"]')
_EMAIL_CONFIG_FILE = "/etc/python/email.cfg"
_cached_strict_addr_parsing = None
def _use_strict_email_parsing():
""""Cache implementation for _cached_strict_addr_parsing"""
global _cached_strict_addr_parsing
if _cached_strict_addr_parsing is None:
_cached_strict_addr_parsing = _use_strict_email_parsing_impl()
return _cached_strict_addr_parsing
def _use_strict_email_parsing_impl():
"""Returns True if strict email parsing is not disabled by
config file or env variable.
disabled = bool(os.environ.get("PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING"))
file = open(_EMAIL_CONFIG_FILE)
except FileNotFoundError:
config = configparser.ConfigParser(
comment_prefixes=('#', ),
disabled = config.getboolean('email_addr_parsing', "PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING", fallback=None)
"""Return True if s contains surrogate-escaped binary data."""
# This check is based on the fact that unless there are surrogates, utf8
# (Python's default encoding) can encode any string. This is the fastest
# way to check for surrogates, see issue 11454 for timings.
except UnicodeEncodeError:
# How to deal with a string containing bytes before handing it to the
# application through the 'normal' interface.
# Turn any escaped bytes into unicode 'unknown' char. If the escaped
# bytes happen to be utf-8 they will instead get decoded, even if they
# were invalid in the charset the source was supposed to be in. This
# seems like it is not a bad thing; a defect was still registered.
original_bytes = string.encode('utf-8', 'surrogateescape')
return original_bytes.decode('utf-8', 'replace')
def formataddr(pair, charset='utf-8'):
"""The inverse of parseaddr(), this takes a 2-tuple of the form
(realname, email_address) and returns the string value suitable
for an RFC 2822 From, To or Cc header.
If the first element of pair is false, then the second element is
Optional charset if given is the character set that is used to encode
realname in case realname is not ASCII safe. Can be an instance of str or
a Charset-like object which has a header_encode method. Default is
# The address MUST (per RFC) be ascii, so raise a UnicodeError if it isn't.
except UnicodeEncodeError:
if isinstance(charset, str):
charset = Charset(charset)
encoded_name = charset.header_encode(name)
return "%s <%s>" % (encoded_name, address)
if specialsre.search(name):
name = escapesre.sub(r'\\\g<0>', name)
return '%s%s%s <%s>' % (quotes, name, quotes, address)
def _iter_escaped_chars(addr):
for pos, ch in enumerate(addr):
def _strip_quoted_realnames(addr):
"""Strip real names between quotes."""
for pos, ch in _iter_escaped_chars(addr):
result.append(addr[start:open_pos])
result.append(addr[start:])
supports_strict_parsing = True
def getaddresses(fieldvalues, *, strict=None):
"""Return a list of (REALNAME, EMAIL) or ('','') for each fieldvalue.
When parsing fails for a fieldvalue, a 2-tuple of ('', '') is returned in
If strict is true, use a strict parser which rejects malformed inputs.
# If default is used, it's True unless disabled
# by env variable or config file.
strict = _use_strict_email_parsing()
# If strict is true, if the resulting list of parsed addresses is greater
# than the number of fieldvalues in the input list, a parsing error has
# occurred and consequently a list containing a single empty 2-tuple [('',
# '')] is returned in its place. This is done to avoid invalid output.
# Malformed input: getaddresses(['alice@example.com <bob@example.com>'])
# Invalid output: [('', 'alice@example.com'), ('', 'bob@example.com')]
# Safe output: [('', '')]
all = COMMASPACE.join(str(v) for v in fieldvalues)
fieldvalues = [str(v) for v in fieldvalues]
fieldvalues = _pre_parse_validation(fieldvalues)
addr = COMMASPACE.join(fieldvalues)
result = _post_parse_validation(a.addresslist)
# Treat output as invalid if the number of addresses is not equal to the
# expected number of addresses.
# When a comma is used in the Real Name part it is not a deliminator.
# So strip those out before counting the commas.
v = _strip_quoted_realnames(v)
# Expected number of addresses: 1 + number of commas
def _check_parenthesis(addr):
# Ignore parenthesis in quoted real names.
addr = _strip_quoted_realnames(addr)
for pos, ch in _iter_escaped_chars(addr):
def _pre_parse_validation(email_header_fields):
for v in email_header_fields:
if not _check_parenthesis(v):
accepted_values.append(v)
def _post_parse_validation(parsed_email_header_tuples):
# The parser would have parsed a correctly formatted domain-literal
# The existence of an [ after parsing indicates a parsing failure
for v in parsed_email_header_tuples:
accepted_values.append(v)
(?P<charset>[^?]*?) # non-greedy up to the next ? is the charset
(?P<encoding>[qb]) # either a "q" or a "b", case insensitive
(?P<atom>.*?) # non-greedy up to the next ?= is the atom
''', re.VERBOSE | re.IGNORECASE)
def _format_timetuple_and_zone(timetuple, zone):
return '%s, %02d %s %04d %02d:%02d:%02d %s' % (
['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][timetuple[6]],
['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][timetuple[1] - 1],
timetuple[0], timetuple[3], timetuple[4], timetuple[5],
def formatdate(timeval=None, localtime=False, usegmt=False):
"""Returns a date string as specified by RFC 2822, e.g.:
Fri, 09 Nov 2001 01:08:47 -0000
Optional timeval if given is a floating point time value as accepted by
gmtime() and localtime(), otherwise the current time is used.
Optional localtime is a flag that when True, interprets timeval, and
returns a date relative to the local timezone instead of UTC, properly
taking daylight savings time into account.
Optional argument usegmt means that the timezone is written out as
an ascii string, not numeric one (so "GMT" instead of "+0000"). This
is needed for HTTP, and is only used when localtime==False.
# Note: we cannot use strftime() because that honors the locale and RFC
# 2822 requires that day and month names be the English abbreviations.
dt = datetime.datetime.fromtimestamp(timeval, datetime.timezone.utc)
dt = datetime.datetime.utcfromtimestamp(timeval)
return format_datetime(dt, usegmt)
def format_datetime(dt, usegmt=False):
"""Turn a datetime into a date string as specified in RFC 2822.
If usegmt is True, dt must be an aware datetime with an offset of zero. In
this case 'GMT' will be rendered instead of the normal +0000 required by
RFC2822. This is to support HTTP headers involving date stamps.
if dt.tzinfo is None or dt.tzinfo != datetime.timezone.utc:
raise ValueError("usegmt option requires a UTC datetime")
return _format_timetuple_and_zone(now, zone)
def make_msgid(idstring=None, domain=None):
"""Returns a string suitable for RFC 2822 compliant Message-ID, e.g:
<142480216486.20800.16526388040877946887@nightshade.la.mastaler.com>
Optional idstring if given is a string used to strengthen the
uniqueness of the message id. Optional domain if given provides the
portion of the message id after the '@'. It defaults to the locally
timeval = int(time.time()*100)
randint = random.getrandbits(64)
idstring = '.' + idstring
domain = socket.getfqdn()
msgid = '<%d.%d.%d%s@%s>' % (timeval, pid, randint, idstring, domain)
def parsedate_to_datetime(data):
*dtuple, tz = _parsedate_tz(data)
return datetime.datetime(*dtuple[:6])
return datetime.datetime(*dtuple[:6],
tzinfo=datetime.timezone(datetime.timedelta(seconds=tz)))
def parseaddr(addr, *, strict=None):
Parse addr into its constituent realname and email address parts.
Return a tuple of realname and email address, unless the parse fails, in
which case return a 2-tuple of ('', '').
If strict is True, use a strict parser which rejects malformed inputs.
# If default is used, it's True unless disabled
# by env variable or config file.
strict = _use_strict_email_parsing()
addrs = _AddressList(addr).addresslist
if isinstance(addr, list):
if not isinstance(addr, str):
addr = _pre_parse_validation([addr])[0]
addrs = _post_parse_validation(_AddressList(addr).addresslist)
if not addrs or len(addrs) > 1:
# rfc822.unquote() doesn't properly de-backslash-ify in Python pre-2.3.
"""Remove quotes from a string."""
if str.startswith('"') and str.endswith('"'):
return str[1:-1].replace('\\\\', '\\').replace('\\"', '"')
if str.startswith('<') and str.endswith('>'):
# RFC2231-related functions - parameter encoding and decoding
"""Decode string according to RFC 2231"""
def encode_rfc2231(s, charset=None, language=None):
"""Encode string according to RFC 2231.
If neither charset nor language is given, then s is returned as-is. If
charset is given but not language, the string is encoded using the empty
s = urllib.parse.quote(s, safe='', encoding=charset or 'ascii')
if charset is None and language is None:
return "%s'%s'%s" % (charset, language, s)
rfc2231_continuation = re.compile(r'^(?P<name>\w+)\*((?P<num>[0-9]+)\*?)?$',
def decode_params(params):
"""Decode parameters list according to RFC 2231.
params is a sequence of 2-tuples containing (param name, string value).
# Copy params so we don't mess with the original
# Map parameter's name to a list of continuations. The values are a
# 3-tuple of the continuation number, the string value, and a flag
# specifying whether a particular segment is %-encoded.
name, value = params.pop(0)
new_params.append((name, value))
name, value = params.pop(0)
mo = rfc2231_continuation.match(name)
name, num = mo.group('name', 'num')
rfc2231_params.setdefault(name, []).append((num, value, encoded))
new_params.append((name, '"%s"' % quote(value)))
for name, continuations in rfc2231_params.items():
# And now append all values in numerical order, converting
# %-encodings for the encoded segments. If any of the
# continuation names ends in a *, then the entire string, after
# decoding segments and concatenating, must have the charset and
# language specifiers at the beginning of the string.
for num, s, encoded in continuations:
# Decode as "latin-1", so the characters in s directly
# represent the percent-encoded octet values.
# collapse_rfc2231_value treats this as an octet sequence.
s = urllib.parse.unquote(s, encoding="latin-1")
value = quote(EMPTYSTRING.join(value))