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