blob: a8e46a761bf922e215929bf673ec94bf3a27fae8 [file] [log] [blame]
Benjamin Peterson46a99002010-01-09 18:45:30 +00001# Copyright (C) 2001-2010 Python Software Foundation
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002# Author: Barry Warsaw
3# Contact: email-sig@python.org
4
5"""Miscellaneous utilities."""
6
7__all__ = [
8 'collapse_rfc2231_value',
9 'decode_params',
10 'decode_rfc2231',
11 'encode_rfc2231',
12 'formataddr',
13 'formatdate',
R David Murray875048b2011-07-20 11:41:21 -040014 'format_datetime',
Guido van Rossum8b3febe2007-08-30 01:15:14 +000015 'getaddresses',
16 'make_msgid',
Barry Warsawb742a962009-11-25 18:45:15 +000017 'mktime_tz',
Guido van Rossum8b3febe2007-08-30 01:15:14 +000018 'parseaddr',
19 'parsedate',
20 'parsedate_tz',
R David Murray875048b2011-07-20 11:41:21 -040021 'parsedate_to_datetime',
Guido van Rossum8b3febe2007-08-30 01:15:14 +000022 'unquote',
23 ]
24
25import os
26import re
27import time
Guido van Rossum8b3febe2007-08-30 01:15:14 +000028import random
29import socket
R David Murray875048b2011-07-20 11:41:21 -040030import datetime
Jeremy Hylton1afc1692008-06-18 20:49:58 +000031import urllib.parse
Guido van Rossum8b3febe2007-08-30 01:15:14 +000032
33from email._parseaddr import quote
34from email._parseaddr import AddressList as _AddressList
35from email._parseaddr import mktime_tz
36
Georg Brandl1aca31e2012-09-22 09:03:56 +020037from email._parseaddr import parsedate, parsedate_tz, _parsedate_tz
Guido van Rossum8b3febe2007-08-30 01:15:14 +000038
Guido van Rossum8b3febe2007-08-30 01:15:14 +000039# Intrapackage imports
R David Murray8debacb2011-04-06 09:35:57 -040040from email.charset import Charset
Guido van Rossum8b3febe2007-08-30 01:15:14 +000041
42COMMASPACE = ', '
43EMPTYSTRING = ''
44UEMPTYSTRING = ''
45CRLF = '\r\n'
46TICK = "'"
47
48specialsre = re.compile(r'[][\\()<>@,:;".]')
R David Murrayb53319f2012-03-14 15:31:47 -040049escapesre = re.compile(r'[\\"]')
Guido van Rossum8b3febe2007-08-30 01:15:14 +000050
R David Murrayb83ee302013-06-26 12:06:21 -040051def _has_surrogates(s):
52 """Return True if s contains surrogate-escaped binary data."""
53 # This check is based on the fact that unless there are surrogates, utf8
54 # (Python's default encoding) can encode any string. This is the fastest
55 # way to check for surrogates, see issue 11454 for timings.
56 try:
57 s.encode()
58 return False
59 except UnicodeEncodeError:
60 return True
Guido van Rossum8b3febe2007-08-30 01:15:14 +000061
R David Murray0b6f6c82012-05-25 18:42:14 -040062# How to deal with a string containing bytes before handing it to the
63# application through the 'normal' interface.
64def _sanitize(string):
R David Murray3da240f2013-10-16 22:48:40 -040065 # Turn any escaped bytes into unicode 'unknown' char. If the escaped
66 # bytes happen to be utf-8 they will instead get decoded, even if they
67 # were invalid in the charset the source was supposed to be in. This
68 # seems like it is not a bad thing; a defect was still registered.
69 original_bytes = string.encode('utf-8', 'surrogateescape')
70 return original_bytes.decode('utf-8', 'replace')
71
R David Murray0b6f6c82012-05-25 18:42:14 -040072
Antoine Pitroufd036452008-08-19 17:56:33 +000073
Guido van Rossum8b3febe2007-08-30 01:15:14 +000074# Helpers
75
R David Murray8debacb2011-04-06 09:35:57 -040076def formataddr(pair, charset='utf-8'):
Guido van Rossum8b3febe2007-08-30 01:15:14 +000077 """The inverse of parseaddr(), this takes a 2-tuple of the form
78 (realname, email_address) and returns the string value suitable
79 for an RFC 2822 From, To or Cc header.
80
81 If the first element of pair is false, then the second element is
82 returned unmodified.
R David Murray8debacb2011-04-06 09:35:57 -040083
Jürgen Gmach66a65ba2020-06-19 13:57:30 +020084 The optional charset is the character set that is used to encode
R David Murray8debacb2011-04-06 09:35:57 -040085 realname in case realname is not ASCII safe. Can be an instance of str or
86 a Charset-like object which has a header_encode method. Default is
87 'utf-8'.
Guido van Rossum8b3febe2007-08-30 01:15:14 +000088 """
89 name, address = pair
Martin Panter6245cb32016-04-15 02:14:19 +000090 # The address MUST (per RFC) be ascii, so raise a UnicodeError if it isn't.
R David Murray8debacb2011-04-06 09:35:57 -040091 address.encode('ascii')
Guido van Rossum8b3febe2007-08-30 01:15:14 +000092 if name:
R David Murray8debacb2011-04-06 09:35:57 -040093 try:
94 name.encode('ascii')
95 except UnicodeEncodeError:
96 if isinstance(charset, str):
97 charset = Charset(charset)
98 encoded_name = charset.header_encode(name)
99 return "%s <%s>" % (encoded_name, address)
100 else:
101 quotes = ''
102 if specialsre.search(name):
103 quotes = '"'
104 name = escapesre.sub(r'\\\g<0>', name)
105 return '%s%s%s <%s>' % (quotes, name, quotes, address)
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000106 return address
107
108
Antoine Pitroufd036452008-08-19 17:56:33 +0000109
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000110def getaddresses(fieldvalues):
111 """Return a list of (REALNAME, EMAIL) for each fieldvalue."""
112 all = COMMASPACE.join(fieldvalues)
113 a = _AddressList(all)
114 return a.addresslist
115
116
R David Murray875048b2011-07-20 11:41:21 -0400117def _format_timetuple_and_zone(timetuple, zone):
118 return '%s, %02d %s %04d %02d:%02d:%02d %s' % (
119 ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][timetuple[6]],
120 timetuple[2],
121 ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
122 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][timetuple[1] - 1],
123 timetuple[0], timetuple[3], timetuple[4], timetuple[5],
124 zone)
Antoine Pitroufd036452008-08-19 17:56:33 +0000125
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000126def formatdate(timeval=None, localtime=False, usegmt=False):
127 """Returns a date string as specified by RFC 2822, e.g.:
128
129 Fri, 09 Nov 2001 01:08:47 -0000
130
131 Optional timeval if given is a floating point time value as accepted by
132 gmtime() and localtime(), otherwise the current time is used.
133
134 Optional localtime is a flag that when True, interprets timeval, and
135 returns a date relative to the local timezone instead of UTC, properly
136 taking daylight savings time into account.
137
138 Optional argument usegmt means that the timezone is written out as
139 an ascii string, not numeric one (so "GMT" instead of "+0000"). This
140 is needed for HTTP, and is only used when localtime==False.
141 """
142 # Note: we cannot use strftime() because that honors the locale and RFC
143 # 2822 requires that day and month names be the English abbreviations.
144 if timeval is None:
145 timeval = time.time()
Robert Collins2080dc92015-08-01 08:18:22 +1200146 if localtime or usegmt:
147 dt = datetime.datetime.fromtimestamp(timeval, datetime.timezone.utc)
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000148 else:
Robert Collins2080dc92015-08-01 08:18:22 +1200149 dt = datetime.datetime.utcfromtimestamp(timeval)
150 if localtime:
151 dt = dt.astimezone()
152 usegmt = False
153 return format_datetime(dt, usegmt)
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000154
R David Murray875048b2011-07-20 11:41:21 -0400155def format_datetime(dt, usegmt=False):
156 """Turn a datetime into a date string as specified in RFC 2822.
157
158 If usegmt is True, dt must be an aware datetime with an offset of zero. In
159 this case 'GMT' will be rendered instead of the normal +0000 required by
160 RFC2822. This is to support HTTP headers involving date stamps.
161 """
162 now = dt.timetuple()
163 if usegmt:
164 if dt.tzinfo is None or dt.tzinfo != datetime.timezone.utc:
165 raise ValueError("usegmt option requires a UTC datetime")
166 zone = 'GMT'
167 elif dt.tzinfo is None:
168 zone = '-0000'
169 else:
170 zone = dt.strftime("%z")
171 return _format_timetuple_and_zone(now, zone)
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000172
Antoine Pitroufd036452008-08-19 17:56:33 +0000173
R. David Murraya0b44b52010-12-02 21:47:19 +0000174def make_msgid(idstring=None, domain=None):
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000175 """Returns a string suitable for RFC 2822 compliant Message-ID, e.g:
176
Serhiy Storchakaae760c02015-05-19 10:09:42 +0300177 <142480216486.20800.16526388040877946887@nightshade.la.mastaler.com>
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000178
179 Optional idstring if given is a string used to strengthen the
R. David Murraya0b44b52010-12-02 21:47:19 +0000180 uniqueness of the message id. Optional domain if given provides the
181 portion of the message id after the '@'. It defaults to the locally
182 defined hostname.
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000183 """
Serhiy Storchakaae760c02015-05-19 10:09:42 +0300184 timeval = int(time.time()*100)
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000185 pid = os.getpid()
Serhiy Storchakaae760c02015-05-19 10:09:42 +0300186 randint = random.getrandbits(64)
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000187 if idstring is None:
188 idstring = ''
189 else:
190 idstring = '.' + idstring
R. David Murraya0b44b52010-12-02 21:47:19 +0000191 if domain is None:
192 domain = socket.getfqdn()
Serhiy Storchakaae760c02015-05-19 10:09:42 +0300193 msgid = '<%d.%d.%d%s@%s>' % (timeval, pid, randint, idstring, domain)
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000194 return msgid
195
196
R David Murray875048b2011-07-20 11:41:21 -0400197def parsedate_to_datetime(data):
Georges Toth303aac82020-10-27 01:31:06 +0100198 parsed_date_tz = _parsedate_tz(data)
199 if parsed_date_tz is None:
200 raise ValueError('Invalid date value or format "%s"' % str(data))
201 *dtuple, tz = parsed_date_tz
R David Murray875048b2011-07-20 11:41:21 -0400202 if tz is None:
203 return datetime.datetime(*dtuple[:6])
204 return datetime.datetime(*dtuple[:6],
205 tzinfo=datetime.timezone(datetime.timedelta(seconds=tz)))
206
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000207
208def parseaddr(addr):
Rohit Balasubramanian9e7b9b22017-09-20 00:40:49 +0530209 """
210 Parse addr into its constituent realname and email address parts.
211
212 Return a tuple of realname and email address, unless the parse fails, in
213 which case return a 2-tuple of ('', '').
214 """
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000215 addrs = _AddressList(addr).addresslist
216 if not addrs:
217 return '', ''
218 return addrs[0]
219
220
221# rfc822.unquote() doesn't properly de-backslash-ify in Python pre-2.3.
222def unquote(str):
223 """Remove quotes from a string."""
224 if len(str) > 1:
225 if str.startswith('"') and str.endswith('"'):
226 return str[1:-1].replace('\\\\', '\\').replace('\\"', '"')
227 if str.startswith('<') and str.endswith('>'):
228 return str[1:-1]
229 return str
230
231
Antoine Pitroufd036452008-08-19 17:56:33 +0000232
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000233# RFC2231-related functions - parameter encoding and decoding
234def decode_rfc2231(s):
235 """Decode string according to RFC 2231"""
236 parts = s.split(TICK, 2)
237 if len(parts) <= 2:
238 return None, None, s
239 return parts
240
241
242def encode_rfc2231(s, charset=None, language=None):
243 """Encode string according to RFC 2231.
244
245 If neither charset nor language is given, then s is returned as-is. If
246 charset is given but not language, the string is encoded using the empty
247 string for language.
248 """
Guido van Rossum52dbbb92008-08-18 21:44:30 +0000249 s = urllib.parse.quote(s, safe='', encoding=charset or 'ascii')
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000250 if charset is None and language is None:
251 return s
252 if language is None:
253 language = ''
254 return "%s'%s'%s" % (charset, language, s)
255
256
Antoine Pitroufd036452008-08-19 17:56:33 +0000257rfc2231_continuation = re.compile(r'^(?P<name>\w+)\*((?P<num>[0-9]+)\*?)?$',
258 re.ASCII)
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000259
260def decode_params(params):
261 """Decode parameters list according to RFC 2231.
262
263 params is a sequence of 2-tuples containing (param name, string value).
264 """
Michael Selik27026382019-09-19 20:25:55 -0700265 new_params = [params[0]]
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000266 # Map parameter's name to a list of continuations. The values are a
267 # 3-tuple of the continuation number, the string value, and a flag
268 # specifying whether a particular segment is %-encoded.
269 rfc2231_params = {}
Michael Selik27026382019-09-19 20:25:55 -0700270 for name, value in params[1:]:
271 encoded = name.endswith('*')
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000272 value = unquote(value)
273 mo = rfc2231_continuation.match(name)
274 if mo:
275 name, num = mo.group('name', 'num')
276 if num is not None:
277 num = int(num)
278 rfc2231_params.setdefault(name, []).append((num, value, encoded))
279 else:
280 new_params.append((name, '"%s"' % quote(value)))
281 if rfc2231_params:
282 for name, continuations in rfc2231_params.items():
283 value = []
284 extended = False
285 # Sort by number
286 continuations.sort()
287 # And now append all values in numerical order, converting
288 # %-encodings for the encoded segments. If any of the
289 # continuation names ends in a *, then the entire string, after
290 # decoding segments and concatenating, must have the charset and
291 # language specifiers at the beginning of the string.
292 for num, s, encoded in continuations:
293 if encoded:
Guido van Rossum52dbbb92008-08-18 21:44:30 +0000294 # Decode as "latin-1", so the characters in s directly
295 # represent the percent-encoded octet values.
296 # collapse_rfc2231_value treats this as an octet sequence.
297 s = urllib.parse.unquote(s, encoding="latin-1")
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000298 extended = True
299 value.append(s)
300 value = quote(EMPTYSTRING.join(value))
301 if extended:
302 charset, language, value = decode_rfc2231(value)
303 new_params.append((name, (charset, language, '"%s"' % value)))
304 else:
305 new_params.append((name, '"%s"' % value))
306 return new_params
307
308def collapse_rfc2231_value(value, errors='replace',
309 fallback_charset='us-ascii'):
310 if not isinstance(value, tuple) or len(value) != 3:
311 return unquote(value)
312 # While value comes to us as a unicode string, we need it to be a bytes
313 # object. We do not want bytes() normal utf-8 decoder, we want a straight
314 # interpretation of the string as character bytes.
315 charset, language, text = value
R David Murray1e949892014-02-07 15:02:19 -0500316 if charset is None:
317 # Issue 17369: if charset/lang is None, decode_rfc2231 couldn't parse
318 # the value, so use the fallback_charset.
319 charset = fallback_charset
Guido van Rossum9604e662007-08-30 03:46:43 +0000320 rawbytes = bytes(text, 'raw-unicode-escape')
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000321 try:
322 return str(rawbytes, charset, errors)
323 except LookupError:
324 # charset is not a known codec.
325 return unquote(text)
R David Murrayd2d521e2012-05-25 23:22:59 -0400326
327
328#
329# datetime doesn't provide a localtime function yet, so provide one. Code
330# adapted from the patch in issue 9527. This may not be perfect, but it is
331# better than not having it.
332#
333
334def localtime(dt=None, isdst=-1):
335 """Return local time as an aware datetime object.
336
337 If called without arguments, return current time. Otherwise *dt*
338 argument should be a datetime instance, and it is converted to the
339 local time zone according to the system time zone database. If *dt* is
340 naive (that is, dt.tzinfo is None), it is assumed to be in local time.
341 In this case, a positive or zero value for *isdst* causes localtime to
342 presume initially that summer time (for example, Daylight Saving Time)
343 is or is not (respectively) in effect for the specified time. A
344 negative value for *isdst* causes the localtime() function to attempt
345 to divine whether summer time is in effect for the specified time.
346
347 """
348 if dt is None:
Alexander Belopolskyf9bd9142012-08-22 23:02:36 -0400349 return datetime.datetime.now(datetime.timezone.utc).astimezone()
R David Murrayb8687df2012-08-22 21:34:00 -0400350 if dt.tzinfo is not None:
351 return dt.astimezone()
352 # We have a naive datetime. Convert to a (localtime) timetuple and pass to
353 # system mktime together with the isdst hint. System mktime will return
354 # seconds since epoch.
355 tm = dt.timetuple()[:-1] + (isdst,)
356 seconds = time.mktime(tm)
357 localtm = time.localtime(seconds)
358 try:
359 delta = datetime.timedelta(seconds=localtm.tm_gmtoff)
360 tz = datetime.timezone(delta, localtm.tm_zone)
361 except AttributeError:
362 # Compute UTC offset and compare with the value implied by tm_isdst.
363 # If the values match, use the zone name implied by tm_isdst.
R David Murray097a1202012-08-22 21:52:31 -0400364 delta = dt - datetime.datetime(*time.gmtime(seconds)[:6])
R David Murrayb8687df2012-08-22 21:34:00 -0400365 dst = time.daylight and localtm.tm_isdst > 0
366 gmtoff = -(time.altzone if dst else time.timezone)
367 if delta == datetime.timedelta(seconds=gmtoff):
368 tz = datetime.timezone(delta, time.tzname[dst])
R David Murrayd2d521e2012-05-25 23:22:59 -0400369 else:
R David Murrayb8687df2012-08-22 21:34:00 -0400370 tz = datetime.timezone(delta)
371 return dt.replace(tzinfo=tz)