blob: 317fdfa3b678ce822bc1886ac2a0a57a940502c2 [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
84 Optional charset if given is the character set that is used to encode
85 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
Andrew Svetlov5b898402012-12-18 21:26:36 +020090 # The address MUST (per RFC) be ascii, so raise an 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
Antoine Pitroufd036452008-08-19 17:56:33 +0000117
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000118ecre = re.compile(r'''
119 =\? # literal =?
120 (?P<charset>[^?]*?) # non-greedy up to the next ? is the charset
121 \? # literal ?
122 (?P<encoding>[qb]) # either a "q" or a "b", case insensitive
123 \? # literal ?
124 (?P<atom>.*?) # non-greedy up to the next ?= is the atom
125 \?= # literal ?=
126 ''', re.VERBOSE | re.IGNORECASE)
127
128
R David Murray875048b2011-07-20 11:41:21 -0400129def _format_timetuple_and_zone(timetuple, zone):
130 return '%s, %02d %s %04d %02d:%02d:%02d %s' % (
131 ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][timetuple[6]],
132 timetuple[2],
133 ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
134 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][timetuple[1] - 1],
135 timetuple[0], timetuple[3], timetuple[4], timetuple[5],
136 zone)
Antoine Pitroufd036452008-08-19 17:56:33 +0000137
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000138def formatdate(timeval=None, localtime=False, usegmt=False):
139 """Returns a date string as specified by RFC 2822, e.g.:
140
141 Fri, 09 Nov 2001 01:08:47 -0000
142
143 Optional timeval if given is a floating point time value as accepted by
144 gmtime() and localtime(), otherwise the current time is used.
145
146 Optional localtime is a flag that when True, interprets timeval, and
147 returns a date relative to the local timezone instead of UTC, properly
148 taking daylight savings time into account.
149
150 Optional argument usegmt means that the timezone is written out as
151 an ascii string, not numeric one (so "GMT" instead of "+0000"). This
152 is needed for HTTP, and is only used when localtime==False.
153 """
154 # Note: we cannot use strftime() because that honors the locale and RFC
155 # 2822 requires that day and month names be the English abbreviations.
156 if timeval is None:
157 timeval = time.time()
158 if localtime:
159 now = time.localtime(timeval)
160 # Calculate timezone offset, based on whether the local zone has
161 # daylight savings time, and whether DST is in effect.
162 if time.daylight and now[-1]:
163 offset = time.altzone
164 else:
165 offset = time.timezone
166 hours, minutes = divmod(abs(offset), 3600)
167 # Remember offset is in seconds west of UTC, but the timezone is in
168 # minutes east of UTC, so the signs differ.
169 if offset > 0:
170 sign = '-'
171 else:
172 sign = '+'
173 zone = '%s%02d%02d' % (sign, hours, minutes // 60)
174 else:
175 now = time.gmtime(timeval)
176 # Timezone offset is always -0000
177 if usegmt:
178 zone = 'GMT'
179 else:
180 zone = '-0000'
R David Murray875048b2011-07-20 11:41:21 -0400181 return _format_timetuple_and_zone(now, zone)
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000182
R David Murray875048b2011-07-20 11:41:21 -0400183def format_datetime(dt, usegmt=False):
184 """Turn a datetime into a date string as specified in RFC 2822.
185
186 If usegmt is True, dt must be an aware datetime with an offset of zero. In
187 this case 'GMT' will be rendered instead of the normal +0000 required by
188 RFC2822. This is to support HTTP headers involving date stamps.
189 """
190 now = dt.timetuple()
191 if usegmt:
192 if dt.tzinfo is None or dt.tzinfo != datetime.timezone.utc:
193 raise ValueError("usegmt option requires a UTC datetime")
194 zone = 'GMT'
195 elif dt.tzinfo is None:
196 zone = '-0000'
197 else:
198 zone = dt.strftime("%z")
199 return _format_timetuple_and_zone(now, zone)
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000200
Antoine Pitroufd036452008-08-19 17:56:33 +0000201
R. David Murraya0b44b52010-12-02 21:47:19 +0000202def make_msgid(idstring=None, domain=None):
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000203 """Returns a string suitable for RFC 2822 compliant Message-ID, e.g:
204
Serhiy Storchakaae760c02015-05-19 10:09:42 +0300205 <142480216486.20800.16526388040877946887@nightshade.la.mastaler.com>
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000206
207 Optional idstring if given is a string used to strengthen the
R. David Murraya0b44b52010-12-02 21:47:19 +0000208 uniqueness of the message id. Optional domain if given provides the
209 portion of the message id after the '@'. It defaults to the locally
210 defined hostname.
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000211 """
Serhiy Storchakaae760c02015-05-19 10:09:42 +0300212 timeval = int(time.time()*100)
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000213 pid = os.getpid()
Serhiy Storchakaae760c02015-05-19 10:09:42 +0300214 randint = random.getrandbits(64)
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000215 if idstring is None:
216 idstring = ''
217 else:
218 idstring = '.' + idstring
R. David Murraya0b44b52010-12-02 21:47:19 +0000219 if domain is None:
220 domain = socket.getfqdn()
Serhiy Storchakaae760c02015-05-19 10:09:42 +0300221 msgid = '<%d.%d.%d%s@%s>' % (timeval, pid, randint, idstring, domain)
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000222 return msgid
223
224
R David Murray875048b2011-07-20 11:41:21 -0400225def parsedate_to_datetime(data):
Georg Brandl1aca31e2012-09-22 09:03:56 +0200226 *dtuple, tz = _parsedate_tz(data)
R David Murray875048b2011-07-20 11:41:21 -0400227 if tz is None:
228 return datetime.datetime(*dtuple[:6])
229 return datetime.datetime(*dtuple[:6],
230 tzinfo=datetime.timezone(datetime.timedelta(seconds=tz)))
231
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000232
233def parseaddr(addr):
234 addrs = _AddressList(addr).addresslist
235 if not addrs:
236 return '', ''
237 return addrs[0]
238
239
240# rfc822.unquote() doesn't properly de-backslash-ify in Python pre-2.3.
241def unquote(str):
242 """Remove quotes from a string."""
243 if len(str) > 1:
244 if str.startswith('"') and str.endswith('"'):
245 return str[1:-1].replace('\\\\', '\\').replace('\\"', '"')
246 if str.startswith('<') and str.endswith('>'):
247 return str[1:-1]
248 return str
249
250
Antoine Pitroufd036452008-08-19 17:56:33 +0000251
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000252# RFC2231-related functions - parameter encoding and decoding
253def decode_rfc2231(s):
254 """Decode string according to RFC 2231"""
255 parts = s.split(TICK, 2)
256 if len(parts) <= 2:
257 return None, None, s
258 return parts
259
260
261def encode_rfc2231(s, charset=None, language=None):
262 """Encode string according to RFC 2231.
263
264 If neither charset nor language is given, then s is returned as-is. If
265 charset is given but not language, the string is encoded using the empty
266 string for language.
267 """
Guido van Rossum52dbbb92008-08-18 21:44:30 +0000268 s = urllib.parse.quote(s, safe='', encoding=charset or 'ascii')
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000269 if charset is None and language is None:
270 return s
271 if language is None:
272 language = ''
273 return "%s'%s'%s" % (charset, language, s)
274
275
Antoine Pitroufd036452008-08-19 17:56:33 +0000276rfc2231_continuation = re.compile(r'^(?P<name>\w+)\*((?P<num>[0-9]+)\*?)?$',
277 re.ASCII)
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000278
279def decode_params(params):
280 """Decode parameters list according to RFC 2231.
281
282 params is a sequence of 2-tuples containing (param name, string value).
283 """
284 # Copy params so we don't mess with the original
285 params = params[:]
286 new_params = []
287 # Map parameter's name to a list of continuations. The values are a
288 # 3-tuple of the continuation number, the string value, and a flag
289 # specifying whether a particular segment is %-encoded.
290 rfc2231_params = {}
291 name, value = params.pop(0)
292 new_params.append((name, value))
293 while params:
294 name, value = params.pop(0)
295 if name.endswith('*'):
296 encoded = True
297 else:
298 encoded = False
299 value = unquote(value)
300 mo = rfc2231_continuation.match(name)
301 if mo:
302 name, num = mo.group('name', 'num')
303 if num is not None:
304 num = int(num)
305 rfc2231_params.setdefault(name, []).append((num, value, encoded))
306 else:
307 new_params.append((name, '"%s"' % quote(value)))
308 if rfc2231_params:
309 for name, continuations in rfc2231_params.items():
310 value = []
311 extended = False
312 # Sort by number
313 continuations.sort()
314 # And now append all values in numerical order, converting
315 # %-encodings for the encoded segments. If any of the
316 # continuation names ends in a *, then the entire string, after
317 # decoding segments and concatenating, must have the charset and
318 # language specifiers at the beginning of the string.
319 for num, s, encoded in continuations:
320 if encoded:
Guido van Rossum52dbbb92008-08-18 21:44:30 +0000321 # Decode as "latin-1", so the characters in s directly
322 # represent the percent-encoded octet values.
323 # collapse_rfc2231_value treats this as an octet sequence.
324 s = urllib.parse.unquote(s, encoding="latin-1")
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000325 extended = True
326 value.append(s)
327 value = quote(EMPTYSTRING.join(value))
328 if extended:
329 charset, language, value = decode_rfc2231(value)
330 new_params.append((name, (charset, language, '"%s"' % value)))
331 else:
332 new_params.append((name, '"%s"' % value))
333 return new_params
334
335def collapse_rfc2231_value(value, errors='replace',
336 fallback_charset='us-ascii'):
337 if not isinstance(value, tuple) or len(value) != 3:
338 return unquote(value)
339 # While value comes to us as a unicode string, we need it to be a bytes
340 # object. We do not want bytes() normal utf-8 decoder, we want a straight
341 # interpretation of the string as character bytes.
342 charset, language, text = value
R David Murray1e949892014-02-07 15:02:19 -0500343 if charset is None:
344 # Issue 17369: if charset/lang is None, decode_rfc2231 couldn't parse
345 # the value, so use the fallback_charset.
346 charset = fallback_charset
Guido van Rossum9604e662007-08-30 03:46:43 +0000347 rawbytes = bytes(text, 'raw-unicode-escape')
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000348 try:
349 return str(rawbytes, charset, errors)
350 except LookupError:
351 # charset is not a known codec.
352 return unquote(text)
R David Murrayd2d521e2012-05-25 23:22:59 -0400353
354
355#
356# datetime doesn't provide a localtime function yet, so provide one. Code
357# adapted from the patch in issue 9527. This may not be perfect, but it is
358# better than not having it.
359#
360
361def localtime(dt=None, isdst=-1):
362 """Return local time as an aware datetime object.
363
364 If called without arguments, return current time. Otherwise *dt*
365 argument should be a datetime instance, and it is converted to the
366 local time zone according to the system time zone database. If *dt* is
367 naive (that is, dt.tzinfo is None), it is assumed to be in local time.
368 In this case, a positive or zero value for *isdst* causes localtime to
369 presume initially that summer time (for example, Daylight Saving Time)
370 is or is not (respectively) in effect for the specified time. A
371 negative value for *isdst* causes the localtime() function to attempt
372 to divine whether summer time is in effect for the specified time.
373
374 """
375 if dt is None:
Alexander Belopolskyf9bd9142012-08-22 23:02:36 -0400376 return datetime.datetime.now(datetime.timezone.utc).astimezone()
R David Murrayb8687df2012-08-22 21:34:00 -0400377 if dt.tzinfo is not None:
378 return dt.astimezone()
379 # We have a naive datetime. Convert to a (localtime) timetuple and pass to
380 # system mktime together with the isdst hint. System mktime will return
381 # seconds since epoch.
382 tm = dt.timetuple()[:-1] + (isdst,)
383 seconds = time.mktime(tm)
384 localtm = time.localtime(seconds)
385 try:
386 delta = datetime.timedelta(seconds=localtm.tm_gmtoff)
387 tz = datetime.timezone(delta, localtm.tm_zone)
388 except AttributeError:
389 # Compute UTC offset and compare with the value implied by tm_isdst.
390 # If the values match, use the zone name implied by tm_isdst.
R David Murray097a1202012-08-22 21:52:31 -0400391 delta = dt - datetime.datetime(*time.gmtime(seconds)[:6])
R David Murrayb8687df2012-08-22 21:34:00 -0400392 dst = time.daylight and localtm.tm_isdst > 0
393 gmtoff = -(time.altzone if dst else time.timezone)
394 if delta == datetime.timedelta(seconds=gmtoff):
395 tz = datetime.timezone(delta, time.tzname[dst])
R David Murrayd2d521e2012-05-25 23:22:59 -0400396 else:
R David Murrayb8687df2012-08-22 21:34:00 -0400397 tz = datetime.timezone(delta)
398 return dt.replace(tzinfo=tz)