• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright (C) 2001-2010 Python Software Foundation
2# 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',
14    'format_datetime',
15    'getaddresses',
16    'make_msgid',
17    'mktime_tz',
18    'parseaddr',
19    'parsedate',
20    'parsedate_tz',
21    'parsedate_to_datetime',
22    'unquote',
23    ]
24
25import os
26import re
27import time
28import random
29import socket
30import datetime
31import urllib.parse
32
33from email._parseaddr import quote
34from email._parseaddr import AddressList as _AddressList
35from email._parseaddr import mktime_tz
36
37from email._parseaddr import parsedate, parsedate_tz, _parsedate_tz
38
39# Intrapackage imports
40from email.charset import Charset
41
42COMMASPACE = ', '
43EMPTYSTRING = ''
44UEMPTYSTRING = ''
45CRLF = '\r\n'
46TICK = "'"
47
48specialsre = re.compile(r'[][\\()<>@,:;".]')
49escapesre = re.compile(r'[\\"]')
50
51def _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
61
62# How to deal with a string containing bytes before handing it to the
63# application through the 'normal' interface.
64def _sanitize(string):
65    # 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
72
73
74# Helpers
75
76def formataddr(pair, charset='utf-8'):
77    """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.
83
84    The optional charset 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'.
88    """
89    name, address = pair
90    # The address MUST (per RFC) be ascii, so raise a UnicodeError if it isn't.
91    address.encode('ascii')
92    if name:
93        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)
106    return address
107
108
109
110def getaddresses(fieldvalues):
111    """Return a list of (REALNAME, EMAIL) for each fieldvalue."""
112    all = COMMASPACE.join(str(v) for v in fieldvalues)
113    a = _AddressList(all)
114    return a.addresslist
115
116
117def _format_timetuple_and_zone(timetuple, zone):
118    return '%s, %02d %s %04d %02d:%02d:%02d %s' % (
119        ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][timetuple[6]],
120        timetuple[2],
121        ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
122         'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][timetuple[1] - 1],
123        timetuple[0], timetuple[3], timetuple[4], timetuple[5],
124        zone)
125
126def formatdate(timeval=None, localtime=False, usegmt=False):
127    """Returns a date string as specified by RFC 2822, e.g.:
128
129    Fri, 09 Nov 2001 01:08:47 -0000
130
131    Optional timeval if given is a floating point time value as accepted by
132    gmtime() and localtime(), otherwise the current time is used.
133
134    Optional localtime is a flag that when True, interprets timeval, and
135    returns a date relative to the local timezone instead of UTC, properly
136    taking daylight savings time into account.
137
138    Optional argument usegmt means that the timezone is written out as
139    an ascii string, not numeric one (so "GMT" instead of "+0000"). This
140    is needed for HTTP, and is only used when localtime==False.
141    """
142    # Note: we cannot use strftime() because that honors the locale and RFC
143    # 2822 requires that day and month names be the English abbreviations.
144    if timeval is None:
145        timeval = time.time()
146    if localtime or usegmt:
147        dt = datetime.datetime.fromtimestamp(timeval, datetime.timezone.utc)
148    else:
149        dt = datetime.datetime.utcfromtimestamp(timeval)
150    if localtime:
151        dt = dt.astimezone()
152        usegmt = False
153    return format_datetime(dt, usegmt)
154
155def format_datetime(dt, usegmt=False):
156    """Turn a datetime into a date string as specified in RFC 2822.
157
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
173
174def make_msgid(idstring=None, domain=None):
175    """Returns a string suitable for RFC 2822 compliant Message-ID, e.g:
176
177    <142480216486.20800.16526388040877946887@nightshade.la.mastaler.com>
178
179    Optional idstring if given is a string used to strengthen the
180    uniqueness of the message id.  Optional domain if given provides the
181    portion of the message id after the '@'.  It defaults to the locally
182    defined hostname.
183    """
184    timeval = int(time.time()*100)
185    pid = os.getpid()
186    randint = random.getrandbits(64)
187    if idstring is None:
188        idstring = ''
189    else:
190        idstring = '.' + idstring
191    if domain is None:
192        domain = socket.getfqdn()
193    msgid = '<%d.%d.%d%s@%s>' % (timeval, pid, randint, idstring, domain)
194    return msgid
195
196
197def parsedate_to_datetime(data):
198    parsed_date_tz = _parsedate_tz(data)
199    if parsed_date_tz is None:
200        raise ValueError('Invalid date value or format "%s"' % str(data))
201    *dtuple, tz = parsed_date_tz
202    if tz is None:
203        return datetime.datetime(*dtuple[:6])
204    return datetime.datetime(*dtuple[:6],
205            tzinfo=datetime.timezone(datetime.timedelta(seconds=tz)))
206
207
208def parseaddr(addr):
209    """
210    Parse addr into its constituent realname and email address parts.
211
212    Return a tuple of realname and email address, unless the parse fails, in
213    which case return a 2-tuple of ('', '').
214    """
215    addrs = _AddressList(addr).addresslist
216    if not addrs:
217        return '', ''
218    return addrs[0]
219
220
221# rfc822.unquote() doesn't properly de-backslash-ify in Python pre-2.3.
222def unquote(str):
223    """Remove quotes from a string."""
224    if len(str) > 1:
225        if str.startswith('"') and str.endswith('"'):
226            return str[1:-1].replace('\\\\', '\\').replace('\\"', '"')
227        if str.startswith('<') and str.endswith('>'):
228            return str[1:-1]
229    return str
230
231
232
233# RFC2231-related functions - parameter encoding and decoding
234def decode_rfc2231(s):
235    """Decode string according to RFC 2231"""
236    parts = s.split(TICK, 2)
237    if len(parts) <= 2:
238        return None, None, s
239    return parts
240
241
242def encode_rfc2231(s, charset=None, language=None):
243    """Encode string according to RFC 2231.
244
245    If neither charset nor language is given, then s is returned as-is.  If
246    charset is given but not language, the string is encoded using the empty
247    string for language.
248    """
249    s = urllib.parse.quote(s, safe='', encoding=charset or 'ascii')
250    if charset is None and language is None:
251        return s
252    if language is None:
253        language = ''
254    return "%s'%s'%s" % (charset, language, s)
255
256
257rfc2231_continuation = re.compile(r'^(?P<name>\w+)\*((?P<num>[0-9]+)\*?)?$',
258    re.ASCII)
259
260def decode_params(params):
261    """Decode parameters list according to RFC 2231.
262
263    params is a sequence of 2-tuples containing (param name, string value).
264    """
265    new_params = [params[0]]
266    # Map parameter's name to a list of continuations.  The values are a
267    # 3-tuple of the continuation number, the string value, and a flag
268    # specifying whether a particular segment is %-encoded.
269    rfc2231_params = {}
270    for name, value in params[1:]:
271        encoded = name.endswith('*')
272        value = unquote(value)
273        mo = rfc2231_continuation.match(name)
274        if mo:
275            name, num = mo.group('name', 'num')
276            if num is not None:
277                num = int(num)
278            rfc2231_params.setdefault(name, []).append((num, value, encoded))
279        else:
280            new_params.append((name, '"%s"' % quote(value)))
281    if rfc2231_params:
282        for name, continuations in rfc2231_params.items():
283            value = []
284            extended = False
285            # Sort by number
286            continuations.sort()
287            # And now append all values in numerical order, converting
288            # %-encodings for the encoded segments.  If any of the
289            # continuation names ends in a *, then the entire string, after
290            # decoding segments and concatenating, must have the charset and
291            # language specifiers at the beginning of the string.
292            for num, s, encoded in continuations:
293                if encoded:
294                    # Decode as "latin-1", so the characters in s directly
295                    # represent the percent-encoded octet values.
296                    # collapse_rfc2231_value treats this as an octet sequence.
297                    s = urllib.parse.unquote(s, encoding="latin-1")
298                    extended = True
299                value.append(s)
300            value = quote(EMPTYSTRING.join(value))
301            if extended:
302                charset, language, value = decode_rfc2231(value)
303                new_params.append((name, (charset, language, '"%s"' % value)))
304            else:
305                new_params.append((name, '"%s"' % value))
306    return new_params
307
308def collapse_rfc2231_value(value, errors='replace',
309                           fallback_charset='us-ascii'):
310    if not isinstance(value, tuple) or len(value) != 3:
311        return unquote(value)
312    # While value comes to us as a unicode string, we need it to be a bytes
313    # object.  We do not want bytes() normal utf-8 decoder, we want a straight
314    # interpretation of the string as character bytes.
315    charset, language, text = value
316    if charset is None:
317        # Issue 17369: if charset/lang is None, decode_rfc2231 couldn't parse
318        # the value, so use the fallback_charset.
319        charset = fallback_charset
320    rawbytes = bytes(text, 'raw-unicode-escape')
321    try:
322        return str(rawbytes, charset, errors)
323    except LookupError:
324        # charset is not a known codec.
325        return unquote(text)
326
327
328#
329# datetime doesn't provide a localtime function yet, so provide one.  Code
330# adapted from the patch in issue 9527.  This may not be perfect, but it is
331# better than not having it.
332#
333
334def localtime(dt=None, isdst=-1):
335    """Return local time as an aware datetime object.
336
337    If called without arguments, return current time.  Otherwise *dt*
338    argument should be a datetime instance, and it is converted to the
339    local time zone according to the system time zone database.  If *dt* is
340    naive (that is, dt.tzinfo is None), it is assumed to be in local time.
341    In this case, a positive or zero value for *isdst* causes localtime to
342    presume initially that summer time (for example, Daylight Saving Time)
343    is or is not (respectively) in effect for the specified time.  A
344    negative value for *isdst* causes the localtime() function to attempt
345    to divine whether summer time is in effect for the specified time.
346
347    """
348    if dt is None:
349        return datetime.datetime.now(datetime.timezone.utc).astimezone()
350    if dt.tzinfo is not None:
351        return dt.astimezone()
352    # We have a naive datetime.  Convert to a (localtime) timetuple and pass to
353    # system mktime together with the isdst hint.  System mktime will return
354    # seconds since epoch.
355    tm = dt.timetuple()[:-1] + (isdst,)
356    seconds = time.mktime(tm)
357    localtm = time.localtime(seconds)
358    try:
359        delta = datetime.timedelta(seconds=localtm.tm_gmtoff)
360        tz = datetime.timezone(delta, localtm.tm_zone)
361    except AttributeError:
362        # Compute UTC offset and compare with the value implied by tm_isdst.
363        # If the values match, use the zone name implied by tm_isdst.
364        delta = dt - datetime.datetime(*time.gmtime(seconds)[:6])
365        dst = time.daylight and localtm.tm_isdst > 0
366        gmtoff = -(time.altzone if dst else time.timezone)
367        if delta == datetime.timedelta(seconds=gmtoff):
368            tz = datetime.timezone(delta, time.tzname[dst])
369        else:
370            tz = datetime.timezone(delta)
371    return dt.replace(tzinfo=tz)
372