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