PageRenderTime 467ms CodeModel.GetById 151ms app.highlight 116ms RepoModel.GetById 196ms app.codeStats 0ms

/Lib/email/quoprimime.py

http://unladen-swallow.googlecode.com/
Python | 336 lines | 292 code | 14 blank | 30 comment | 22 complexity | d2940de82c5ab8a9e5f8a625ef8bc6aa MD5 | raw file
  1# Copyright (C) 2001-2006 Python Software Foundation
  2# Author: Ben Gertzfield
  3# Contact: email-sig@python.org
  4
  5"""Quoted-printable content transfer encoding per RFCs 2045-2047.
  6
  7This module handles the content transfer encoding method defined in RFC 2045
  8to encode US ASCII-like 8-bit data called `quoted-printable'.  It is used to
  9safely encode text that is in a character set similar to the 7-bit US ASCII
 10character set, but that includes some 8-bit characters that are normally not
 11allowed in email bodies or headers.
 12
 13Quoted-printable is very space-inefficient for encoding binary files; use the
 14email.base64mime module for that instead.
 15
 16This module provides an interface to encode and decode both headers and bodies
 17with quoted-printable encoding.
 18
 19RFC 2045 defines a method for including character set information in an
 20`encoded-word' in a header.  This method is commonly used for 8-bit real names
 21in To:/From:/Cc: etc. fields, as well as Subject: lines.
 22
 23This module does not do the line wrapping or end-of-line character
 24conversion necessary for proper internationalized headers; it only
 25does dumb encoding and decoding.  To deal with the various line
 26wrapping issues, use the email.header module.
 27"""
 28
 29__all__ = [
 30    'body_decode',
 31    'body_encode',
 32    'body_quopri_check',
 33    'body_quopri_len',
 34    'decode',
 35    'decodestring',
 36    'encode',
 37    'encodestring',
 38    'header_decode',
 39    'header_encode',
 40    'header_quopri_check',
 41    'header_quopri_len',
 42    'quote',
 43    'unquote',
 44    ]
 45
 46import re
 47
 48from string import hexdigits
 49from email.utils import fix_eols
 50
 51CRLF = '\r\n'
 52NL = '\n'
 53
 54# See also Charset.py
 55MISC_LEN = 7
 56
 57hqre = re.compile(r'[^-a-zA-Z0-9!*+/ ]')
 58bqre = re.compile(r'[^ !-<>-~\t]')
 59
 60
 61
 62# Helpers
 63def header_quopri_check(c):
 64    """Return True if the character should be escaped with header quopri."""
 65    return bool(hqre.match(c))
 66
 67
 68def body_quopri_check(c):
 69    """Return True if the character should be escaped with body quopri."""
 70    return bool(bqre.match(c))
 71
 72
 73def header_quopri_len(s):
 74    """Return the length of str when it is encoded with header quopri."""
 75    count = 0
 76    for c in s:
 77        if hqre.match(c):
 78            count += 3
 79        else:
 80            count += 1
 81    return count
 82
 83
 84def body_quopri_len(str):
 85    """Return the length of str when it is encoded with body quopri."""
 86    count = 0
 87    for c in str:
 88        if bqre.match(c):
 89            count += 3
 90        else:
 91            count += 1
 92    return count
 93
 94
 95def _max_append(L, s, maxlen, extra=''):
 96    if not L:
 97        L.append(s.lstrip())
 98    elif len(L[-1]) + len(s) <= maxlen:
 99        L[-1] += extra + s
100    else:
101        L.append(s.lstrip())
102
103
104def unquote(s):
105    """Turn a string in the form =AB to the ASCII character with value 0xab"""
106    return chr(int(s[1:3], 16))
107
108
109def quote(c):
110    return "=%02X" % ord(c)
111
112
113
114def header_encode(header, charset="iso-8859-1", keep_eols=False,
115                  maxlinelen=76, eol=NL):
116    """Encode a single header line with quoted-printable (like) encoding.
117
118    Defined in RFC 2045, this `Q' encoding is similar to quoted-printable, but
119    used specifically for email header fields to allow charsets with mostly 7
120    bit characters (and some 8 bit) to remain more or less readable in non-RFC
121    2045 aware mail clients.
122
123    charset names the character set to use to encode the header.  It defaults
124    to iso-8859-1.
125
126    The resulting string will be in the form:
127
128    "=?charset?q?I_f=E2rt_in_your_g=E8n=E8ral_dire=E7tion?\\n
129      =?charset?q?Silly_=C8nglish_Kn=EEghts?="
130
131    with each line wrapped safely at, at most, maxlinelen characters (defaults
132    to 76 characters).  If maxlinelen is None, the entire string is encoded in
133    one chunk with no splitting.
134
135    End-of-line characters (\\r, \\n, \\r\\n) will be automatically converted
136    to the canonical email line separator \\r\\n unless the keep_eols
137    parameter is True (the default is False).
138
139    Each line of the header will be terminated in the value of eol, which
140    defaults to "\\n".  Set this to "\\r\\n" if you are using the result of
141    this function directly in email.
142    """
143    # Return empty headers unchanged
144    if not header:
145        return header
146
147    if not keep_eols:
148        header = fix_eols(header)
149
150    # Quopri encode each line, in encoded chunks no greater than maxlinelen in
151    # length, after the RFC chrome is added in.
152    quoted = []
153    if maxlinelen is None:
154        # An obnoxiously large number that's good enough
155        max_encoded = 100000
156    else:
157        max_encoded = maxlinelen - len(charset) - MISC_LEN - 1
158
159    for c in header:
160        # Space may be represented as _ instead of =20 for readability
161        if c == ' ':
162            _max_append(quoted, '_', max_encoded)
163        # These characters can be included verbatim
164        elif not hqre.match(c):
165            _max_append(quoted, c, max_encoded)
166        # Otherwise, replace with hex value like =E2
167        else:
168            _max_append(quoted, "=%02X" % ord(c), max_encoded)
169
170    # Now add the RFC chrome to each encoded chunk and glue the chunks
171    # together.  BAW: should we be able to specify the leading whitespace in
172    # the joiner?
173    joiner = eol + ' '
174    return joiner.join(['=?%s?q?%s?=' % (charset, line) for line in quoted])
175
176
177
178def encode(body, binary=False, maxlinelen=76, eol=NL):
179    """Encode with quoted-printable, wrapping at maxlinelen characters.
180
181    If binary is False (the default), end-of-line characters will be converted
182    to the canonical email end-of-line sequence \\r\\n.  Otherwise they will
183    be left verbatim.
184
185    Each line of encoded text will end with eol, which defaults to "\\n".  Set
186    this to "\\r\\n" if you will be using the result of this function directly
187    in an email.
188
189    Each line will be wrapped at, at most, maxlinelen characters (defaults to
190    76 characters).  Long lines will have the `soft linefeed' quoted-printable
191    character "=" appended to them, so the decoded text will be identical to
192    the original text.
193    """
194    if not body:
195        return body
196
197    if not binary:
198        body = fix_eols(body)
199
200    # BAW: We're accumulating the body text by string concatenation.  That
201    # can't be very efficient, but I don't have time now to rewrite it.  It
202    # just feels like this algorithm could be more efficient.
203    encoded_body = ''
204    lineno = -1
205    # Preserve line endings here so we can check later to see an eol needs to
206    # be added to the output later.
207    lines = body.splitlines(1)
208    for line in lines:
209        # But strip off line-endings for processing this line.
210        if line.endswith(CRLF):
211            line = line[:-2]
212        elif line[-1] in CRLF:
213            line = line[:-1]
214
215        lineno += 1
216        encoded_line = ''
217        prev = None
218        linelen = len(line)
219        # Now we need to examine every character to see if it needs to be
220        # quopri encoded.  BAW: again, string concatenation is inefficient.
221        for j in range(linelen):
222            c = line[j]
223            prev = c
224            if bqre.match(c):
225                c = quote(c)
226            elif j+1 == linelen:
227                # Check for whitespace at end of line; special case
228                if c not in ' \t':
229                    encoded_line += c
230                prev = c
231                continue
232            # Check to see to see if the line has reached its maximum length
233            if len(encoded_line) + len(c) >= maxlinelen:
234                encoded_body += encoded_line + '=' + eol
235                encoded_line = ''
236            encoded_line += c
237        # Now at end of line..
238        if prev and prev in ' \t':
239            # Special case for whitespace at end of file
240            if lineno + 1 == len(lines):
241                prev = quote(prev)
242                if len(encoded_line) + len(prev) > maxlinelen:
243                    encoded_body += encoded_line + '=' + eol + prev
244                else:
245                    encoded_body += encoded_line + prev
246            # Just normal whitespace at end of line
247            else:
248                encoded_body += encoded_line + prev + '=' + eol
249            encoded_line = ''
250        # Now look at the line we just finished and it has a line ending, we
251        # need to add eol to the end of the line.
252        if lines[lineno].endswith(CRLF) or lines[lineno][-1] in CRLF:
253            encoded_body += encoded_line + eol
254        else:
255            encoded_body += encoded_line
256        encoded_line = ''
257    return encoded_body
258
259
260# For convenience and backwards compatibility w/ standard base64 module
261body_encode = encode
262encodestring = encode
263
264
265
266# BAW: I'm not sure if the intent was for the signature of this function to be
267# the same as base64MIME.decode() or not...
268def decode(encoded, eol=NL):
269    """Decode a quoted-printable string.
270
271    Lines are separated with eol, which defaults to \\n.
272    """
273    if not encoded:
274        return encoded
275    # BAW: see comment in encode() above.  Again, we're building up the
276    # decoded string with string concatenation, which could be done much more
277    # efficiently.
278    decoded = ''
279
280    for line in encoded.splitlines():
281        line = line.rstrip()
282        if not line:
283            decoded += eol
284            continue
285
286        i = 0
287        n = len(line)
288        while i < n:
289            c = line[i]
290            if c != '=':
291                decoded += c
292                i += 1
293            # Otherwise, c == "=".  Are we at the end of the line?  If so, add
294            # a soft line break.
295            elif i+1 == n:
296                i += 1
297                continue
298            # Decode if in form =AB
299            elif i+2 < n and line[i+1] in hexdigits and line[i+2] in hexdigits:
300                decoded += unquote(line[i:i+3])
301                i += 3
302            # Otherwise, not in form =AB, pass literally
303            else:
304                decoded += c
305                i += 1
306
307            if i == n:
308                decoded += eol
309    # Special case if original string did not end with eol
310    if not encoded.endswith(eol) and decoded.endswith(eol):
311        decoded = decoded[:-1]
312    return decoded
313
314
315# For convenience and backwards compatibility w/ standard base64 module
316body_decode = decode
317decodestring = decode
318
319
320
321def _unquote_match(match):
322    """Turn a match in the form =AB to the ASCII character with value 0xab"""
323    s = match.group(0)
324    return unquote(s)
325
326
327# Header decoding is done a bit differently
328def header_decode(s):
329    """Decode a string encoded with RFC 2045 MIME header `Q' encoding.
330
331    This function does not parse a full MIME header value encoded with
332    quoted-printable (like =?iso-8895-1?q?Hello_World?=) -- please use
333    the high level email.header class for that functionality.
334    """
335    s = s.replace('_', ' ')
336    return re.sub(r'=\w{2}', _unquote_match, s)