blob: 17f01bc1064ca5c2bec0ff5f7816362876f25973 [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
39from quopri import decodestring as _qdecode
40
41# Intrapackage imports
42from email.encoders import _bencode, _qencode
R David Murray8debacb2011-04-06 09:35:57 -040043from email.charset import Charset
Guido van Rossum8b3febe2007-08-30 01:15:14 +000044
45COMMASPACE = ', '
46EMPTYSTRING = ''
47UEMPTYSTRING = ''
48CRLF = '\r\n'
49TICK = "'"
50
51specialsre = re.compile(r'[][\\()<>@,:;".]')
R David Murrayb53319f2012-03-14 15:31:47 -040052escapesre = re.compile(r'[\\"]')
Guido van Rossum8b3febe2007-08-30 01:15:14 +000053
R David Murrayb83ee302013-06-26 12:06:21 -040054def _has_surrogates(s):
55 """Return True if s contains surrogate-escaped binary data."""
56 # This check is based on the fact that unless there are surrogates, utf8
57 # (Python's default encoding) can encode any string. This is the fastest
58 # way to check for surrogates, see issue 11454 for timings.
59 try:
60 s.encode()
61 return False
62 except UnicodeEncodeError:
63 return True
Guido van Rossum8b3febe2007-08-30 01:15:14 +000064
R David Murray0b6f6c82012-05-25 18:42:14 -040065# How to deal with a string containing bytes before handing it to the
66# application through the 'normal' interface.
67def _sanitize(string):
R David Murray3da240f2013-10-16 22:48:40 -040068 # Turn any escaped bytes into unicode 'unknown' char. If the escaped
69 # bytes happen to be utf-8 they will instead get decoded, even if they
70 # were invalid in the charset the source was supposed to be in. This
71 # seems like it is not a bad thing; a defect was still registered.
72 original_bytes = string.encode('utf-8', 'surrogateescape')
73 return original_bytes.decode('utf-8', 'replace')
74
R David Murray0b6f6c82012-05-25 18:42:14 -040075
Antoine Pitroufd036452008-08-19 17:56:33 +000076
Guido van Rossum8b3febe2007-08-30 01:15:14 +000077# Helpers
78
R David Murray8debacb2011-04-06 09:35:57 -040079def formataddr(pair, charset='utf-8'):
Guido van Rossum8b3febe2007-08-30 01:15:14 +000080 """The inverse of parseaddr(), this takes a 2-tuple of the form
81 (realname, email_address) and returns the string value suitable
82 for an RFC 2822 From, To or Cc header.
83
84 If the first element of pair is false, then the second element is
85 returned unmodified.
R David Murray8debacb2011-04-06 09:35:57 -040086
87 Optional charset if given is the character set that is used to encode
88 realname in case realname is not ASCII safe. Can be an instance of str or
89 a Charset-like object which has a header_encode method. Default is
90 'utf-8'.
Guido van Rossum8b3febe2007-08-30 01:15:14 +000091 """
92 name, address = pair
Andrew Svetlov5b898402012-12-18 21:26:36 +020093 # The address MUST (per RFC) be ascii, so raise an UnicodeError if it isn't.
R David Murray8debacb2011-04-06 09:35:57 -040094 address.encode('ascii')
Guido van Rossum8b3febe2007-08-30 01:15:14 +000095 if name:
R David Murray8debacb2011-04-06 09:35:57 -040096 try:
97 name.encode('ascii')
98 except UnicodeEncodeError:
99 if isinstance(charset, str):
100 charset = Charset(charset)
101 encoded_name = charset.header_encode(name)
102 return "%s <%s>" % (encoded_name, address)
103 else:
104 quotes = ''
105 if specialsre.search(name):
106 quotes = '"'
107 name = escapesre.sub(r'\\\g<0>', name)
108 return '%s%s%s <%s>' % (quotes, name, quotes, address)
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000109 return address
110
111
Antoine Pitroufd036452008-08-19 17:56:33 +0000112
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000113def getaddresses(fieldvalues):
114 """Return a list of (REALNAME, EMAIL) for each fieldvalue."""
115 all = COMMASPACE.join(fieldvalues)
116 a = _AddressList(all)
117 return a.addresslist
118
119
Antoine Pitroufd036452008-08-19 17:56:33 +0000120
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000121ecre = re.compile(r'''
122 =\? # literal =?
123 (?P<charset>[^?]*?) # non-greedy up to the next ? is the charset
124 \? # literal ?
125 (?P<encoding>[qb]) # either a "q" or a "b", case insensitive
126 \? # literal ?
127 (?P<atom>.*?) # non-greedy up to the next ?= is the atom
128 \?= # literal ?=
129 ''', re.VERBOSE | re.IGNORECASE)
130
131
R David Murray875048b2011-07-20 11:41:21 -0400132def _format_timetuple_and_zone(timetuple, zone):
133 return '%s, %02d %s %04d %02d:%02d:%02d %s' % (
134 ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][timetuple[6]],
135 timetuple[2],
136 ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
137 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][timetuple[1] - 1],
138 timetuple[0], timetuple[3], timetuple[4], timetuple[5],
139 zone)
Antoine Pitroufd036452008-08-19 17:56:33 +0000140
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000141def formatdate(timeval=None, localtime=False, usegmt=False):
142 """Returns a date string as specified by RFC 2822, e.g.:
143
144 Fri, 09 Nov 2001 01:08:47 -0000
145
146 Optional timeval if given is a floating point time value as accepted by
147 gmtime() and localtime(), otherwise the current time is used.
148
149 Optional localtime is a flag that when True, interprets timeval, and
150 returns a date relative to the local timezone instead of UTC, properly
151 taking daylight savings time into account.
152
153 Optional argument usegmt means that the timezone is written out as
154 an ascii string, not numeric one (so "GMT" instead of "+0000"). This
155 is needed for HTTP, and is only used when localtime==False.
156 """
157 # Note: we cannot use strftime() because that honors the locale and RFC
158 # 2822 requires that day and month names be the English abbreviations.
159 if timeval is None:
160 timeval = time.time()
161 if localtime:
162 now = time.localtime(timeval)
163 # Calculate timezone offset, based on whether the local zone has
164 # daylight savings time, and whether DST is in effect.
165 if time.daylight and now[-1]:
166 offset = time.altzone
167 else:
168 offset = time.timezone
169 hours, minutes = divmod(abs(offset), 3600)
170 # Remember offset is in seconds west of UTC, but the timezone is in
171 # minutes east of UTC, so the signs differ.
172 if offset > 0:
173 sign = '-'
174 else:
175 sign = '+'
176 zone = '%s%02d%02d' % (sign, hours, minutes // 60)
177 else:
178 now = time.gmtime(timeval)
179 # Timezone offset is always -0000
180 if usegmt:
181 zone = 'GMT'
182 else:
183 zone = '-0000'
R David Murray875048b2011-07-20 11:41:21 -0400184 return _format_timetuple_and_zone(now, zone)
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000185
R David Murray875048b2011-07-20 11:41:21 -0400186def format_datetime(dt, usegmt=False):
187 """Turn a datetime into a date string as specified in RFC 2822.
188
189 If usegmt is True, dt must be an aware datetime with an offset of zero. In
190 this case 'GMT' will be rendered instead of the normal +0000 required by
191 RFC2822. This is to support HTTP headers involving date stamps.
192 """
193 now = dt.timetuple()
194 if usegmt:
195 if dt.tzinfo is None or dt.tzinfo != datetime.timezone.utc:
196 raise ValueError("usegmt option requires a UTC datetime")
197 zone = 'GMT'
198 elif dt.tzinfo is None:
199 zone = '-0000'
200 else:
201 zone = dt.strftime("%z")
202 return _format_timetuple_and_zone(now, zone)
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000203
Antoine Pitroufd036452008-08-19 17:56:33 +0000204
R. David Murraya0b44b52010-12-02 21:47:19 +0000205def make_msgid(idstring=None, domain=None):
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000206 """Returns a string suitable for RFC 2822 compliant Message-ID, e.g:
207
208 <20020201195627.33539.96671@nightshade.la.mastaler.com>
209
210 Optional idstring if given is a string used to strengthen the
R. David Murraya0b44b52010-12-02 21:47:19 +0000211 uniqueness of the message id. Optional domain if given provides the
212 portion of the message id after the '@'. It defaults to the locally
213 defined hostname.
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000214 """
215 timeval = time.time()
216 utcdate = time.strftime('%Y%m%d%H%M%S', time.gmtime(timeval))
217 pid = os.getpid()
218 randint = random.randrange(100000)
219 if idstring is None:
220 idstring = ''
221 else:
222 idstring = '.' + idstring
R. David Murraya0b44b52010-12-02 21:47:19 +0000223 if domain is None:
224 domain = socket.getfqdn()
225 msgid = '<%s.%s.%s%s@%s>' % (utcdate, pid, randint, idstring, domain)
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000226 return msgid
227
228
R David Murray875048b2011-07-20 11:41:21 -0400229def parsedate_to_datetime(data):
Georg Brandl1aca31e2012-09-22 09:03:56 +0200230 *dtuple, tz = _parsedate_tz(data)
R David Murray875048b2011-07-20 11:41:21 -0400231 if tz is None:
232 return datetime.datetime(*dtuple[:6])
233 return datetime.datetime(*dtuple[:6],
234 tzinfo=datetime.timezone(datetime.timedelta(seconds=tz)))
235
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000236
237def parseaddr(addr):
238 addrs = _AddressList(addr).addresslist
239 if not addrs:
240 return '', ''
241 return addrs[0]
242
243
244# rfc822.unquote() doesn't properly de-backslash-ify in Python pre-2.3.
245def unquote(str):
246 """Remove quotes from a string."""
247 if len(str) > 1:
248 if str.startswith('"') and str.endswith('"'):
249 return str[1:-1].replace('\\\\', '\\').replace('\\"', '"')
250 if str.startswith('<') and str.endswith('>'):
251 return str[1:-1]
252 return str
253
254
Antoine Pitroufd036452008-08-19 17:56:33 +0000255
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000256# RFC2231-related functions - parameter encoding and decoding
257def decode_rfc2231(s):
258 """Decode string according to RFC 2231"""
259 parts = s.split(TICK, 2)
260 if len(parts) <= 2:
261 return None, None, s
262 return parts
263
264
265def encode_rfc2231(s, charset=None, language=None):
266 """Encode string according to RFC 2231.
267
268 If neither charset nor language is given, then s is returned as-is. If
269 charset is given but not language, the string is encoded using the empty
270 string for language.
271 """
Guido van Rossum52dbbb92008-08-18 21:44:30 +0000272 s = urllib.parse.quote(s, safe='', encoding=charset or 'ascii')
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000273 if charset is None and language is None:
274 return s
275 if language is None:
276 language = ''
277 return "%s'%s'%s" % (charset, language, s)
278
279
Antoine Pitroufd036452008-08-19 17:56:33 +0000280rfc2231_continuation = re.compile(r'^(?P<name>\w+)\*((?P<num>[0-9]+)\*?)?$',
281 re.ASCII)
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000282
283def decode_params(params):
284 """Decode parameters list according to RFC 2231.
285
286 params is a sequence of 2-tuples containing (param name, string value).
287 """
288 # Copy params so we don't mess with the original
289 params = params[:]
290 new_params = []
291 # Map parameter's name to a list of continuations. The values are a
292 # 3-tuple of the continuation number, the string value, and a flag
293 # specifying whether a particular segment is %-encoded.
294 rfc2231_params = {}
295 name, value = params.pop(0)
296 new_params.append((name, value))
297 while params:
298 name, value = params.pop(0)
299 if name.endswith('*'):
300 encoded = True
301 else:
302 encoded = False
303 value = unquote(value)
304 mo = rfc2231_continuation.match(name)
305 if mo:
306 name, num = mo.group('name', 'num')
307 if num is not None:
308 num = int(num)
309 rfc2231_params.setdefault(name, []).append((num, value, encoded))
310 else:
311 new_params.append((name, '"%s"' % quote(value)))
312 if rfc2231_params:
313 for name, continuations in rfc2231_params.items():
314 value = []
315 extended = False
316 # Sort by number
317 continuations.sort()
318 # And now append all values in numerical order, converting
319 # %-encodings for the encoded segments. If any of the
320 # continuation names ends in a *, then the entire string, after
321 # decoding segments and concatenating, must have the charset and
322 # language specifiers at the beginning of the string.
323 for num, s, encoded in continuations:
324 if encoded:
Guido van Rossum52dbbb92008-08-18 21:44:30 +0000325 # Decode as "latin-1", so the characters in s directly
326 # represent the percent-encoded octet values.
327 # collapse_rfc2231_value treats this as an octet sequence.
328 s = urllib.parse.unquote(s, encoding="latin-1")
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000329 extended = True
330 value.append(s)
331 value = quote(EMPTYSTRING.join(value))
332 if extended:
333 charset, language, value = decode_rfc2231(value)
334 new_params.append((name, (charset, language, '"%s"' % value)))
335 else:
336 new_params.append((name, '"%s"' % value))
337 return new_params
338
339def collapse_rfc2231_value(value, errors='replace',
340 fallback_charset='us-ascii'):
341 if not isinstance(value, tuple) or len(value) != 3:
342 return unquote(value)
343 # While value comes to us as a unicode string, we need it to be a bytes
344 # object. We do not want bytes() normal utf-8 decoder, we want a straight
345 # interpretation of the string as character bytes.
346 charset, language, text = value
R David Murray1e949892014-02-07 15:02:19 -0500347 if charset is None:
348 # Issue 17369: if charset/lang is None, decode_rfc2231 couldn't parse
349 # the value, so use the fallback_charset.
350 charset = fallback_charset
Guido van Rossum9604e662007-08-30 03:46:43 +0000351 rawbytes = bytes(text, 'raw-unicode-escape')
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000352 try:
353 return str(rawbytes, charset, errors)
354 except LookupError:
355 # charset is not a known codec.
356 return unquote(text)
R David Murrayd2d521e2012-05-25 23:22:59 -0400357
358
359#
360# datetime doesn't provide a localtime function yet, so provide one. Code
361# adapted from the patch in issue 9527. This may not be perfect, but it is
362# better than not having it.
363#
364
365def localtime(dt=None, isdst=-1):
366 """Return local time as an aware datetime object.
367
368 If called without arguments, return current time. Otherwise *dt*
369 argument should be a datetime instance, and it is converted to the
370 local time zone according to the system time zone database. If *dt* is
371 naive (that is, dt.tzinfo is None), it is assumed to be in local time.
372 In this case, a positive or zero value for *isdst* causes localtime to
373 presume initially that summer time (for example, Daylight Saving Time)
374 is or is not (respectively) in effect for the specified time. A
375 negative value for *isdst* causes the localtime() function to attempt
376 to divine whether summer time is in effect for the specified time.
377
378 """
379 if dt is None:
Alexander Belopolskyf9bd9142012-08-22 23:02:36 -0400380 return datetime.datetime.now(datetime.timezone.utc).astimezone()
R David Murrayb8687df2012-08-22 21:34:00 -0400381 if dt.tzinfo is not None:
382 return dt.astimezone()
383 # We have a naive datetime. Convert to a (localtime) timetuple and pass to
384 # system mktime together with the isdst hint. System mktime will return
385 # seconds since epoch.
386 tm = dt.timetuple()[:-1] + (isdst,)
387 seconds = time.mktime(tm)
388 localtm = time.localtime(seconds)
389 try:
390 delta = datetime.timedelta(seconds=localtm.tm_gmtoff)
391 tz = datetime.timezone(delta, localtm.tm_zone)
392 except AttributeError:
393 # Compute UTC offset and compare with the value implied by tm_isdst.
394 # If the values match, use the zone name implied by tm_isdst.
R David Murray097a1202012-08-22 21:52:31 -0400395 delta = dt - datetime.datetime(*time.gmtime(seconds)[:6])
R David Murrayb8687df2012-08-22 21:34:00 -0400396 dst = time.daylight and localtm.tm_isdst > 0
397 gmtoff = -(time.altzone if dst else time.timezone)
398 if delta == datetime.timedelta(seconds=gmtoff):
399 tz = datetime.timezone(delta, time.tzname[dst])
R David Murrayd2d521e2012-05-25 23:22:59 -0400400 else:
R David Murrayb8687df2012-08-22 21:34:00 -0400401 tz = datetime.timezone(delta)
402 return dt.replace(tzinfo=tz)