utils.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. # Copyright (C) 2001-2010 Python Software Foundation
  2. # Author: Barry Warsaw
  3. # Contact: email-sig@python.org
  4. """Miscellaneous utilities."""
  5. from __future__ import unicode_literals
  6. from __future__ import division
  7. from __future__ import absolute_import
  8. from future import utils
  9. from future.builtins import bytes, int, str
  10. __all__ = [
  11. 'collapse_rfc2231_value',
  12. 'decode_params',
  13. 'decode_rfc2231',
  14. 'encode_rfc2231',
  15. 'formataddr',
  16. 'formatdate',
  17. 'format_datetime',
  18. 'getaddresses',
  19. 'make_msgid',
  20. 'mktime_tz',
  21. 'parseaddr',
  22. 'parsedate',
  23. 'parsedate_tz',
  24. 'parsedate_to_datetime',
  25. 'unquote',
  26. ]
  27. import os
  28. import re
  29. if utils.PY2:
  30. re.ASCII = 0
  31. import time
  32. import base64
  33. import random
  34. import socket
  35. from future.backports import datetime
  36. from future.backports.urllib.parse import quote as url_quote, unquote as url_unquote
  37. import warnings
  38. from io import StringIO
  39. from future.backports.email._parseaddr import quote
  40. from future.backports.email._parseaddr import AddressList as _AddressList
  41. from future.backports.email._parseaddr import mktime_tz
  42. from future.backports.email._parseaddr import parsedate, parsedate_tz, _parsedate_tz
  43. from quopri import decodestring as _qdecode
  44. # Intrapackage imports
  45. from future.backports.email.encoders import _bencode, _qencode
  46. from future.backports.email.charset import Charset
  47. COMMASPACE = ', '
  48. EMPTYSTRING = ''
  49. UEMPTYSTRING = ''
  50. CRLF = '\r\n'
  51. TICK = "'"
  52. specialsre = re.compile(r'[][\\()<>@,:;".]')
  53. escapesre = re.compile(r'[\\"]')
  54. # How to figure out if we are processing strings that come from a byte
  55. # source with undecodable characters.
  56. _has_surrogates = re.compile(
  57. '([^\ud800-\udbff]|\A)[\udc00-\udfff]([^\udc00-\udfff]|\Z)').search
  58. # How to deal with a string containing bytes before handing it to the
  59. # application through the 'normal' interface.
  60. def _sanitize(string):
  61. # Turn any escaped bytes into unicode 'unknown' char.
  62. original_bytes = string.encode('ascii', 'surrogateescape')
  63. return original_bytes.decode('ascii', 'replace')
  64. # Helpers
  65. def formataddr(pair, charset='utf-8'):
  66. """The inverse of parseaddr(), this takes a 2-tuple of the form
  67. (realname, email_address) and returns the string value suitable
  68. for an RFC 2822 From, To or Cc header.
  69. If the first element of pair is false, then the second element is
  70. returned unmodified.
  71. Optional charset if given is the character set that is used to encode
  72. realname in case realname is not ASCII safe. Can be an instance of str or
  73. a Charset-like object which has a header_encode method. Default is
  74. 'utf-8'.
  75. """
  76. name, address = pair
  77. # The address MUST (per RFC) be ascii, so raise an UnicodeError if it isn't.
  78. address.encode('ascii')
  79. if name:
  80. try:
  81. name.encode('ascii')
  82. except UnicodeEncodeError:
  83. if isinstance(charset, str):
  84. charset = Charset(charset)
  85. encoded_name = charset.header_encode(name)
  86. return "%s <%s>" % (encoded_name, address)
  87. else:
  88. quotes = ''
  89. if specialsre.search(name):
  90. quotes = '"'
  91. name = escapesre.sub(r'\\\g<0>', name)
  92. return '%s%s%s <%s>' % (quotes, name, quotes, address)
  93. return address
  94. def getaddresses(fieldvalues):
  95. """Return a list of (REALNAME, EMAIL) for each fieldvalue."""
  96. all = COMMASPACE.join(fieldvalues)
  97. a = _AddressList(all)
  98. return a.addresslist
  99. ecre = re.compile(r'''
  100. =\? # literal =?
  101. (?P<charset>[^?]*?) # non-greedy up to the next ? is the charset
  102. \? # literal ?
  103. (?P<encoding>[qb]) # either a "q" or a "b", case insensitive
  104. \? # literal ?
  105. (?P<atom>.*?) # non-greedy up to the next ?= is the atom
  106. \?= # literal ?=
  107. ''', re.VERBOSE | re.IGNORECASE)
  108. def _format_timetuple_and_zone(timetuple, zone):
  109. return '%s, %02d %s %04d %02d:%02d:%02d %s' % (
  110. ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][timetuple[6]],
  111. timetuple[2],
  112. ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
  113. 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][timetuple[1] - 1],
  114. timetuple[0], timetuple[3], timetuple[4], timetuple[5],
  115. zone)
  116. def formatdate(timeval=None, localtime=False, usegmt=False):
  117. """Returns a date string as specified by RFC 2822, e.g.:
  118. Fri, 09 Nov 2001 01:08:47 -0000
  119. Optional timeval if given is a floating point time value as accepted by
  120. gmtime() and localtime(), otherwise the current time is used.
  121. Optional localtime is a flag that when True, interprets timeval, and
  122. returns a date relative to the local timezone instead of UTC, properly
  123. taking daylight savings time into account.
  124. Optional argument usegmt means that the timezone is written out as
  125. an ascii string, not numeric one (so "GMT" instead of "+0000"). This
  126. is needed for HTTP, and is only used when localtime==False.
  127. """
  128. # Note: we cannot use strftime() because that honors the locale and RFC
  129. # 2822 requires that day and month names be the English abbreviations.
  130. if timeval is None:
  131. timeval = time.time()
  132. if localtime:
  133. now = time.localtime(timeval)
  134. # Calculate timezone offset, based on whether the local zone has
  135. # daylight savings time, and whether DST is in effect.
  136. if time.daylight and now[-1]:
  137. offset = time.altzone
  138. else:
  139. offset = time.timezone
  140. hours, minutes = divmod(abs(offset), 3600)
  141. # Remember offset is in seconds west of UTC, but the timezone is in
  142. # minutes east of UTC, so the signs differ.
  143. if offset > 0:
  144. sign = '-'
  145. else:
  146. sign = '+'
  147. zone = '%s%02d%02d' % (sign, hours, minutes // 60)
  148. else:
  149. now = time.gmtime(timeval)
  150. # Timezone offset is always -0000
  151. if usegmt:
  152. zone = 'GMT'
  153. else:
  154. zone = '-0000'
  155. return _format_timetuple_and_zone(now, zone)
  156. def format_datetime(dt, usegmt=False):
  157. """Turn a datetime into a date string as specified in RFC 2822.
  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)
  172. def make_msgid(idstring=None, domain=None):
  173. """Returns a string suitable for RFC 2822 compliant Message-ID, e.g:
  174. <20020201195627.33539.96671@nightshade.la.mastaler.com>
  175. Optional idstring if given is a string used to strengthen the
  176. uniqueness of the message id. Optional domain if given provides the
  177. portion of the message id after the '@'. It defaults to the locally
  178. defined hostname.
  179. """
  180. timeval = time.time()
  181. utcdate = time.strftime('%Y%m%d%H%M%S', time.gmtime(timeval))
  182. pid = os.getpid()
  183. randint = random.randrange(100000)
  184. if idstring is None:
  185. idstring = ''
  186. else:
  187. idstring = '.' + idstring
  188. if domain is None:
  189. domain = socket.getfqdn()
  190. msgid = '<%s.%s.%s%s@%s>' % (utcdate, pid, randint, idstring, domain)
  191. return msgid
  192. def parsedate_to_datetime(data):
  193. _3to2list = list(_parsedate_tz(data))
  194. dtuple, tz, = [_3to2list[:-1]] + _3to2list[-1:]
  195. if tz is None:
  196. return datetime.datetime(*dtuple[:6])
  197. return datetime.datetime(*dtuple[:6],
  198. tzinfo=datetime.timezone(datetime.timedelta(seconds=tz)))
  199. def parseaddr(addr):
  200. addrs = _AddressList(addr).addresslist
  201. if not addrs:
  202. return '', ''
  203. return addrs[0]
  204. # rfc822.unquote() doesn't properly de-backslash-ify in Python pre-2.3.
  205. def unquote(str):
  206. """Remove quotes from a string."""
  207. if len(str) > 1:
  208. if str.startswith('"') and str.endswith('"'):
  209. return str[1:-1].replace('\\\\', '\\').replace('\\"', '"')
  210. if str.startswith('<') and str.endswith('>'):
  211. return str[1:-1]
  212. return str
  213. # RFC2231-related functions - parameter encoding and decoding
  214. def decode_rfc2231(s):
  215. """Decode string according to RFC 2231"""
  216. parts = s.split(TICK, 2)
  217. if len(parts) <= 2:
  218. return None, None, s
  219. return parts
  220. def encode_rfc2231(s, charset=None, language=None):
  221. """Encode string according to RFC 2231.
  222. If neither charset nor language is given, then s is returned as-is. If
  223. charset is given but not language, the string is encoded using the empty
  224. string for language.
  225. """
  226. s = url_quote(s, safe='', encoding=charset or 'ascii')
  227. if charset is None and language is None:
  228. return s
  229. if language is None:
  230. language = ''
  231. return "%s'%s'%s" % (charset, language, s)
  232. rfc2231_continuation = re.compile(r'^(?P<name>\w+)\*((?P<num>[0-9]+)\*?)?$',
  233. re.ASCII)
  234. def decode_params(params):
  235. """Decode parameters list according to RFC 2231.
  236. params is a sequence of 2-tuples containing (param name, string value).
  237. """
  238. # Copy params so we don't mess with the original
  239. params = params[:]
  240. new_params = []
  241. # Map parameter's name to a list of continuations. The values are a
  242. # 3-tuple of the continuation number, the string value, and a flag
  243. # specifying whether a particular segment is %-encoded.
  244. rfc2231_params = {}
  245. name, value = params.pop(0)
  246. new_params.append((name, value))
  247. while params:
  248. name, value = params.pop(0)
  249. if name.endswith('*'):
  250. encoded = True
  251. else:
  252. encoded = False
  253. value = unquote(value)
  254. mo = rfc2231_continuation.match(name)
  255. if mo:
  256. name, num = mo.group('name', 'num')
  257. if num is not None:
  258. num = int(num)
  259. rfc2231_params.setdefault(name, []).append((num, value, encoded))
  260. else:
  261. new_params.append((name, '"%s"' % quote(value)))
  262. if rfc2231_params:
  263. for name, continuations in rfc2231_params.items():
  264. value = []
  265. extended = False
  266. # Sort by number
  267. continuations.sort()
  268. # And now append all values in numerical order, converting
  269. # %-encodings for the encoded segments. If any of the
  270. # continuation names ends in a *, then the entire string, after
  271. # decoding segments and concatenating, must have the charset and
  272. # language specifiers at the beginning of the string.
  273. for num, s, encoded in continuations:
  274. if encoded:
  275. # Decode as "latin-1", so the characters in s directly
  276. # represent the percent-encoded octet values.
  277. # collapse_rfc2231_value treats this as an octet sequence.
  278. s = url_unquote(s, encoding="latin-1")
  279. extended = True
  280. value.append(s)
  281. value = quote(EMPTYSTRING.join(value))
  282. if extended:
  283. charset, language, value = decode_rfc2231(value)
  284. new_params.append((name, (charset, language, '"%s"' % value)))
  285. else:
  286. new_params.append((name, '"%s"' % value))
  287. return new_params
  288. def collapse_rfc2231_value(value, errors='replace',
  289. fallback_charset='us-ascii'):
  290. if not isinstance(value, tuple) or len(value) != 3:
  291. return unquote(value)
  292. # While value comes to us as a unicode string, we need it to be a bytes
  293. # object. We do not want bytes() normal utf-8 decoder, we want a straight
  294. # interpretation of the string as character bytes.
  295. charset, language, text = value
  296. rawbytes = bytes(text, 'raw-unicode-escape')
  297. try:
  298. return str(rawbytes, charset, errors)
  299. except LookupError:
  300. # charset is not a known codec.
  301. return unquote(text)
  302. #
  303. # datetime doesn't provide a localtime function yet, so provide one. Code
  304. # adapted from the patch in issue 9527. This may not be perfect, but it is
  305. # better than not having it.
  306. #
  307. def localtime(dt=None, isdst=-1):
  308. """Return local time as an aware datetime object.
  309. If called without arguments, return current time. Otherwise *dt*
  310. argument should be a datetime instance, and it is converted to the
  311. local time zone according to the system time zone database. If *dt* is
  312. naive (that is, dt.tzinfo is None), it is assumed to be in local time.
  313. In this case, a positive or zero value for *isdst* causes localtime to
  314. presume initially that summer time (for example, Daylight Saving Time)
  315. is or is not (respectively) in effect for the specified time. A
  316. negative value for *isdst* causes the localtime() function to attempt
  317. to divine whether summer time is in effect for the specified time.
  318. """
  319. if dt is None:
  320. return datetime.datetime.now(datetime.timezone.utc).astimezone()
  321. if dt.tzinfo is not None:
  322. return dt.astimezone()
  323. # We have a naive datetime. Convert to a (localtime) timetuple and pass to
  324. # system mktime together with the isdst hint. System mktime will return
  325. # seconds since epoch.
  326. tm = dt.timetuple()[:-1] + (isdst,)
  327. seconds = time.mktime(tm)
  328. localtm = time.localtime(seconds)
  329. try:
  330. delta = datetime.timedelta(seconds=localtm.tm_gmtoff)
  331. tz = datetime.timezone(delta, localtm.tm_zone)
  332. except AttributeError:
  333. # Compute UTC offset and compare with the value implied by tm_isdst.
  334. # If the values match, use the zone name implied by tm_isdst.
  335. delta = dt - datetime.datetime(*time.gmtime(seconds)[:6])
  336. dst = time.daylight and localtm.tm_isdst > 0
  337. gmtoff = -(time.altzone if dst else time.timezone)
  338. if delta == datetime.timedelta(seconds=gmtoff):
  339. tz = datetime.timezone(delta, time.tzname[dst])
  340. else:
  341. tz = datetime.timezone(delta)
  342. return dt.replace(tzinfo=tz)