c 20022007 python software foundation ben gertzfield barry warsaw contact emailsigpython org header encoding and decoding functionality all header decodeheader makeheader import re import binascii import email quoprimime import email base64mime from email errors import headerparseerror from email import charset as charset charset charset charset nl n space bspace b space8 8 emptystring maxlinelen 78 fws t usascii charset usascii utf8 charset utf8 match encodedword strings in the form charset q helloworld ecre re compiler literal pcharset nongreedy up to the next is the charset literal pencodingqqbb either a q or a b case insensitive literal pencoded nongreedy up to the next is the encoded string literal field name regexp including trailing colon but not separating whitespace according to rfc 2822 character range is from tilde to exclamation mark for use with match find a header embedded in a putative header value used to check for header injection attack helpers decode a message header value without converting charset returns a list of string charset pairs containing each of the decoded parts of the header charset is none for nonencoded parts of the header otherwise a lowercase string containing the name of the character set specified in the encoded string header may be a string that may or may not contain rfc2047 encoded words or it may be a header object an email errors headerparseerror may be raised when certain decoding error occurs e g a base64 decoding exception if it is a header object we can just return the encoded chunks if no encoding just return the header with no charset first step is to parse all the encoded parts into triplets of the form encodedstring encoding charset for unencoded strings the last two parts will be none now loop over words and remove words that consist of whitespace between two encoded strings the next step is to decode each encoded word by applying the reverse base64 or quopri transformation decodedwords is now a list of the form decodedword charset this is an unencoded word now convert all words to bytes and collapse consecutive runs of similarly encoded words create a header from a sequence of pairs as returned by decodeheader decodeheader takes a header value string and returns a sequence of pairs of the format decodedstring charset where charset is the string name of the character set this function takes one of those sequence of pairs and returns a header instance optional maxlinelen headername and continuationws are as in the header constructor none means usascii but we can simply pass it on to h append create a mimecompliant header that can contain many character sets optional s is the initial header value if none the initial header value is not set you can later append to the header with append method calls s may be a byte string or a unicode string but see the append documentation for semantics optional charset serves two purposes it has the same meaning as the charset argument to the append method it also sets the default character set for all subsequent append calls that omit the charset argument if charset is not provided in the constructor the usascii charset is used both as s s initial charset and as the default for subsequent append calls the maximum line length can be specified explicitly via maxlinelen for splitting the first line to a shorter value to account for the field header which isn t included in s e g subject pass in the name of the field in headername the default maxlinelen is 78 as recommended by rfc 2822 continuationws must be rfc 2822 compliant folding whitespace usually either a space or a hard tab which will be prepended to continuation lines errors is passed through to the append call take the separating colon and space into account return the string value of the header self normalize uchunks lastcs none lastspace none for string charset in self chunks we must preserve spaces between encoded and nonencoded word boundaries which means for us we need to add a space when we go from a charset to noneusascii or from noneusascii to a charset only do this for the second and subsequent chunks don t add a space if the noneusascii string already has a space trailing or leading depending on transition nextcs charset if nextcs charset unknown8bit originalbytes string encode ascii surrogateescape string originalbytes decode ascii replace if uchunks hasspace string and self nonctextstring0 if lastcs not in none usascii if nextcs in none usascii and not hasspace uchunks appendspace nextcs none elif nextcs not in none usascii and not lastspace uchunks appendspace lastspace string and self nonctextstring1 lastcs nextcs uchunks appendstring return emptystring joinuchunks rich comparison operators for equality only baw does it make sense to have or explicitly disable operators def eqself other other may be a header or a string both are fine so coerce ourselves to a unicode of the unencoded header value swap the args and do another comparison return other strself def appendself s charsetnone errors strict if charset is none charset self charset elif not isinstancecharset charset charset charsetcharset if not isinstances str inputcharset charset inputcodec or usascii if inputcharset charset unknown8bit s s decode usascii surrogateescape else s s decodeinputcharset errors ensure that the bytes we re storing can be decoded to the output character set otherwise an early error is raised outputcharset charset outputcodec or usascii if outputcharset charset unknown8bit try s encodeoutputcharset errors except unicodeencodeerror if outputcharset usascii raise charset utf8 self chunks appends charset def nonctextself s return s isspace or s in def encodeself splitchars t maxlinelennone linesep n rencode a message header into an rfccompliant format there are many issues involved in converting a given string for use in an email header only certain character sets are readable in most email clients and as header strings can only contain a subset of 7bit ascii care must be taken to properly convert and encode with base64 or quotedprintable header strings in addition there is a 75character length limit on any given encoded header field so linewrapping must be performed even with doublebyte character sets optional maxlinelen specifies the maximum length of each generated line exclusive of the linesep string individual lines may be longer than maxlinelen if a folding point cannot be found the first line will be shorter by the length of the header name plus if a header name was specified at header construction time the default value for maxlinelen is determined at header construction time optional splitchars is a string containing characters which should be given extra weight by the splitting algorithm during normal header wrapping this is in very rough support of rfc 2822 s higher level syntactic breaks split points preceded by a splitchar are preferred during line splitting with the characters preferred in the order in which they appear in the string space and tab may be included in the string to indicate whether preference should be given to one over the other as a split point when other split chars do not appear in the line being split splitchars does not affect rfc 2047 encoded lines optional linesep is a string to be used to separate the lines of the value the default value is the most useful for typical python applications but it can be set to rn to produce rfccompliant line separators when needed a maxlinelen of 0 means don t wrap for all practical purposes choosing a huge number here accomplishes that and makes the valueformatter algorithm much simpler step 1 normalize the chunks so that all runs of identical charsets get collapsed into a single unicode string if the charset has no header encoding i e it is an ascii encoding then we must split the header at the highest level syntactic break possible note that we don t have a lot of smarts about field syntax we just try to break on semicolons then commas then whitespace eventually this should be pluggable otherwise we re doing either a base64 or a quotedprintable encoding which means we don t need to split the line on syntactic breaks we can basically just find enough characters to fit on the current line minus the rfc 2047 chrome what makes this trickier though is that we have to split at octet boundaries not character boundaries but it s only safe to split at character boundaries so at best we can only get close the first element extends the current line but if it s none then nothing more fit on the current line so start a new line there are no encoded lines so we re done there was only one line everything else are full lines in themselves the first line s length the rfc 2822 header folding algorithm is simple in principle but complex in practice lines may be folded any place where folding white space appears by inserting a linesep character in front of the fws the complication is that not all spaces or tabs qualify as fws and we are also supposed to prefer to break at higher level syntactic breaks we can t do either of these without intimate knowledge of the structure of structured headers which we don t have here so the best we can do here is prefer to break at the specified splitchars and hope that we don t choose any spaces or tabs that aren t legal fws this is at least better than the old algorithm where we would sometimes introduce fws after a splitchar or the algorithm before that where we would turn all white space runs into single spaces or tabs find the best split point working backward from the end there might be none on a long first line there will be a header so leave it on a line by itself we don t use continuationws here because the whitespace after a header should always be a space c 2002 2007 python software foundation ben gertzfield barry warsaw contact email sig python org header encoding and decoding functionality match encoded word strings in the form charset q hello_world literal p charset non greedy up to the next is the charset literal p encoding qqbb either a q or a b case insensitive literal p encoded non greedy up to the next is the encoded string literal field name regexp including trailing colon but not separating whitespace according to rfc 2822 character range is from tilde to exclamation mark for use with match find a header embedded in a putative header value used to check for header injection attack helpers decode a message header value without converting charset returns a list of string charset pairs containing each of the decoded parts of the header charset is none for non encoded parts of the header otherwise a lower case string containing the name of the character set specified in the encoded string header may be a string that may or may not contain rfc2047 encoded words or it may be a header object an email errors headerparseerror may be raised when certain decoding error occurs e g a base64 decoding exception if it is a header object we can just return the encoded chunks if no encoding just return the header with no charset first step is to parse all the encoded parts into triplets of the form encoded_string encoding charset for unencoded strings the last two parts will be none now loop over words and remove words that consist of whitespace between two encoded strings the next step is to decode each encoded word by applying the reverse base64 or quopri transformation decoded_words is now a list of the form decoded_word charset this is an unencoded word postel s law add missing padding now convert all words to bytes and collapse consecutive runs of similarly encoded words create a header from a sequence of pairs as returned by decode_header decode_header takes a header value string and returns a sequence of pairs of the format decoded_string charset where charset is the string name of the character set this function takes one of those sequence of pairs and returns a header instance optional maxlinelen header_name and continuation_ws are as in the header constructor none means us ascii but we can simply pass it on to h append create a mime compliant header that can contain many character sets optional s is the initial header value if none the initial header value is not set you can later append to the header with append method calls s may be a byte string or a unicode string but see the append documentation for semantics optional charset serves two purposes it has the same meaning as the charset argument to the append method it also sets the default character set for all subsequent append calls that omit the charset argument if charset is not provided in the constructor the us ascii charset is used both as s s initial charset and as the default for subsequent append calls the maximum line length can be specified explicitly via maxlinelen for splitting the first line to a shorter value to account for the field header which isn t included in s e g subject pass in the name of the field in header_name the default maxlinelen is 78 as recommended by rfc 2822 continuation_ws must be rfc 2822 compliant folding whitespace usually either a space or a hard tab which will be prepended to continuation lines errors is passed through to the append call take the separating colon and space into account return the string value of the header we must preserve spaces between encoded and non encoded word boundaries which means for us we need to add a space when we go from a charset to none us ascii or from none us ascii to a charset only do this for the second and subsequent chunks don t add a space if the none us ascii string already has a space trailing or leading depending on transition rich comparison operators for equality only baw does it make sense to have or explicitly disable operators other may be a header or a string both are fine so coerce ourselves to a unicode of the unencoded header value swap the args and do another comparison append a string to the mime header optional charset if given should be a charset instance or the name of a character set which will be converted to a charset instance a value of none the default means that the charset given in the constructor is used s may be a byte string or a unicode string if it is a byte string i e isinstance s str is false then charset is the encoding of that byte string and a unicodeerror will be raised if the string cannot be decoded with that charset if s is a unicode string then charset is a hint specifying the character set of the characters in the string in either case when producing an rfc 2822 compliant header using rfc 2047 rules the string will be encoded using the output codec of the charset if the string cannot be encoded to the output codec a unicodeerror will be raised optional errors is passed as the errors argument to the decode call if s is a byte string ensure that the bytes we re storing can be decoded to the output character set otherwise an early error is raised true if string s is not a ctext character of rfc822 encode a message header into an rfc compliant format there are many issues involved in converting a given string for use in an email header only certain character sets are readable in most email clients and as header strings can only contain a subset of 7 bit ascii care must be taken to properly convert and encode with base64 or quoted printable header strings in addition there is a 75 character length limit on any given encoded header field so line wrapping must be performed even with double byte character sets optional maxlinelen specifies the maximum length of each generated line exclusive of the linesep string individual lines may be longer than maxlinelen if a folding point cannot be found the first line will be shorter by the length of the header name plus if a header name was specified at header construction time the default value for maxlinelen is determined at header construction time optional splitchars is a string containing characters which should be given extra weight by the splitting algorithm during normal header wrapping this is in very rough support of rfc 2822 s higher level syntactic breaks split points preceded by a splitchar are preferred during line splitting with the characters preferred in the order in which they appear in the string space and tab may be included in the string to indicate whether preference should be given to one over the other as a split point when other split chars do not appear in the line being split splitchars does not affect rfc 2047 encoded lines optional linesep is a string to be used to separate the lines of the value the default value is the most useful for typical python applications but it can be set to r n to produce rfc compliant line separators when needed a maxlinelen of 0 means don t wrap for all practical purposes choosing a huge number here accomplishes that and makes the _valueformatter algorithm much simpler step 1 normalize the chunks so that all runs of identical charsets get collapsed into a single unicode string if the charset has no header encoding i e it is an ascii encoding then we must split the header at the highest level syntactic break possible note that we don t have a lot of smarts about field syntax we just try to break on semi colons then commas then whitespace eventually this should be pluggable otherwise we re doing either a base64 or a quoted printable encoding which means we don t need to split the line on syntactic breaks we can basically just find enough characters to fit on the current line minus the rfc 2047 chrome what makes this trickier though is that we have to split at octet boundaries not character boundaries but it s only safe to split at character boundaries so at best we can only get close the first element extends the current line but if it s none then nothing more fit on the current line so start a new line there are no encoded lines so we re done there was only one line everything else are full lines in themselves the first line s length the rfc 2822 header folding algorithm is simple in principle but complex in practice lines may be folded any place where folding white space appears by inserting a linesep character in front of the fws the complication is that not all spaces or tabs qualify as fws and we are also supposed to prefer to break at higher level syntactic breaks we can t do either of these without intimate knowledge of the structure of structured headers which we don t have here so the best we can do here is prefer to break at the specified splitchars and hope that we don t choose any spaces or tabs that aren t legal fws this is at least better than the old algorithm where we would sometimes introduce fws after a splitchar or the algorithm before that where we would turn all white space runs into single spaces or tabs find the best split point working backward from the end there might be none on a long first line there will be a header so leave it on a line by itself we don t use continuation_ws here because the whitespace after a header should always be a space
__all__ = [
    'Header',
    'decode_header',
    'make_header',
    ]

import re
import binascii

import email.quoprimime
import email.base64mime

from email.errors import HeaderParseError
from email import charset as _charset
Charset = _charset.Charset

NL = '\n'
SPACE = ' '
BSPACE = b' '
SPACE8 = ' ' * 8
EMPTYSTRING = ''
MAXLINELEN = 78
FWS = ' \t'

USASCII = Charset('us-ascii')
UTF8 = Charset('utf-8')


ecre = re.compile(r, re.VERBOSE | re.MULTILINE)




fcre = re.compile(r'[\041-\176]+:$')



_embedded_header = re.compile(r'\n[^ \t]+:')



_max_append = email.quoprimime._max_append


def decode_header(header):
    
    
    if hasattr(header, '_chunks'):
        return [(_charset._encode(string, str(charset)), str(charset))
                    for string, charset in header._chunks]
    
    if not ecre.search(header):
        return [(header, None)]
    
    
    
    words = []
    for line in header.splitlines():
        parts = ecre.split(line)
        first = True
        while parts:
            unencoded = parts.pop(0)
            if first:
                unencoded = unencoded.lstrip()
                first = False
            if unencoded:
                words.append((unencoded, None, None))
            if parts:
                charset = parts.pop(0).lower()
                encoding = parts.pop(0).lower()
                encoded = parts.pop(0)
                words.append((encoded, encoding, charset))
    
    
    droplist = []
    for n, w in enumerate(words):
        if n>1 and w[1] and words[n-2][1] and words[n-1][0].isspace():
            droplist.append(n-1)
    for d in reversed(droplist):
        del words[d]

    
    
    
    decoded_words = []
    for encoded_string, encoding, charset in words:
        if encoding is None:
            
            decoded_words.append((encoded_string, charset))
        elif encoding == 'q':
            word = email.quoprimime.header_decode(encoded_string)
            decoded_words.append((word, charset))
        elif encoding == 'b':
            paderr = len(encoded_string) % 4   
            if paderr:
                encoded_string += '==='[:4 - paderr]
            try:
                word = email.base64mime.decode(encoded_string)
            except binascii.Error:
                raise HeaderParseError('Base64 decoding error')
            else:
                decoded_words.append((word, charset))
        else:
            raise AssertionError('Unexpected encoding: ' + encoding)
    
    
    collapsed = []
    last_word = last_charset = None
    for word, charset in decoded_words:
        if isinstance(word, str):
            word = bytes(word, 'raw-unicode-escape')
        if last_word is None:
            last_word = word
            last_charset = charset
        elif charset != last_charset:
            collapsed.append((last_word, last_charset))
            last_word = word
            last_charset = charset
        elif last_charset is None:
            last_word += BSPACE + word
        else:
            last_word += word
    collapsed.append((last_word, last_charset))
    return collapsed


def make_header(decoded_seq, maxlinelen=None, header_name=None,
                continuation_ws=' '):
    
    h = Header(maxlinelen=maxlinelen, header_name=header_name,
               continuation_ws=continuation_ws)
    for s, charset in decoded_seq:
        
        if charset is not None and not isinstance(charset, Charset):
            charset = Charset(charset)
        h.append(s, charset)
    return h


class Header:
    def __init__(self, s=None, charset=None,
                 maxlinelen=None, header_name=None,
                 continuation_ws=' ', errors='strict'):
        
        if charset is None:
            charset = USASCII
        elif not isinstance(charset, Charset):
            charset = Charset(charset)
        self._charset = charset
        self._continuation_ws = continuation_ws
        self._chunks = []
        if s is not None:
            self.append(s, charset, errors)
        if maxlinelen is None:
            maxlinelen = MAXLINELEN
        self._maxlinelen = maxlinelen
        if header_name is None:
            self._headerlen = 0
        else:
            
            self._headerlen = len(header_name) + 2

    def __str__(self):
        
        self._normalize()
        uchunks = []
        lastcs = None
        lastspace = None
        for string, charset in self._chunks:
            
            
            
            
            
            
            nextcs = charset
            if nextcs == _charset.UNKNOWN8BIT:
                original_bytes = string.encode('ascii', 'surrogateescape')
                string = original_bytes.decode('ascii', 'replace')
            if uchunks:
                hasspace = string and self._nonctext(string[0])
                if lastcs not in (None, 'us-ascii'):
                    if nextcs in (None, 'us-ascii') and not hasspace:
                        uchunks.append(SPACE)
                        nextcs = None
                elif nextcs not in (None, 'us-ascii') and not lastspace:
                    uchunks.append(SPACE)
            lastspace = string and self._nonctext(string[-1])
            lastcs = nextcs
            uchunks.append(string)
        return EMPTYSTRING.join(uchunks)

    
    
    def __eq__(self, other):
        
        
        
        return other == str(self)

    def append(self, s, charset=None, errors='strict'):
        
        if charset is None:
            charset = self._charset
        elif not isinstance(charset, Charset):
            charset = Charset(charset)
        if not isinstance(s, str):
            input_charset = charset.input_codec or 'us-ascii'
            if input_charset == _charset.UNKNOWN8BIT:
                s = s.decode('us-ascii', 'surrogateescape')
            else:
                s = s.decode(input_charset, errors)
        
        
        output_charset = charset.output_codec or 'us-ascii'
        if output_charset != _charset.UNKNOWN8BIT:
            try:
                s.encode(output_charset, errors)
            except UnicodeEncodeError:
                if output_charset!='us-ascii':
                    raise
                charset = UTF8
        self._chunks.append((s, charset))

    def _nonctext(self, s):
        
        return s.isspace() or s in ('(', ')', '\\')

    def encode(self, splitchars=';, \t', maxlinelen=None, linesep='\n'):
        r
        self._normalize()
        if maxlinelen is None:
            maxlinelen = self._maxlinelen
        
        
        
        if maxlinelen == 0:
            maxlinelen = 1000000
        formatter = _ValueFormatter(self._headerlen, maxlinelen,
                                    self._continuation_ws, splitchars)
        lastcs = None
        hasspace = lastspace = None
        for string, charset in self._chunks:
            if hasspace is not None:
                hasspace = string and self._nonctext(string[0])
                if lastcs not in (None, 'us-ascii'):
                    if not hasspace or charset not in (None, 'us-ascii'):
                        formatter.add_transition()
                elif charset not in (None, 'us-ascii') and not lastspace:
                    formatter.add_transition()
            lastspace = string and self._nonctext(string[-1])
            lastcs = charset
            hasspace = False
            lines = string.splitlines()
            if lines:
                formatter.feed('', lines[0], charset)
            else:
                formatter.feed('', '', charset)
            for line in lines[1:]:
                formatter.newline()
                if charset.header_encoding is not None:
                    formatter.feed(self._continuation_ws, ' ' + line.lstrip(),
                                   charset)
                else:
                    sline = line.lstrip()
                    fws = line[:len(line)-len(sline)]
                    formatter.feed(fws, sline, charset)
            if len(lines) > 1:
                formatter.newline()
        if self._chunks:
            formatter.add_transition()
        value = formatter._str(linesep)
        if _embedded_header.search(value):
            raise HeaderParseError("header value appears to contain "
                "an embedded header: {!r}".format(value))
        return value

    def _normalize(self):
        
        
        chunks = []
        last_charset = None
        last_chunk = []
        for string, charset in self._chunks:
            if charset == last_charset:
                last_chunk.append(string)
            else:
                if last_charset is not None:
                    chunks.append((SPACE.join(last_chunk), last_charset))
                last_chunk = [string]
                last_charset = charset
        if last_chunk:
            chunks.append((SPACE.join(last_chunk), last_charset))
        self._chunks = chunks


class _ValueFormatter:
    def __init__(self, headerlen, maxlen, continuation_ws, splitchars):
        self._maxlen = maxlen
        self._continuation_ws = continuation_ws
        self._continuation_ws_len = len(continuation_ws)
        self._splitchars = splitchars
        self._lines = []
        self._current_line = _Accumulator(headerlen)

    def _str(self, linesep):
        self.newline()
        return linesep.join(self._lines)

    def __str__(self):
        return self._str(NL)

    def newline(self):
        end_of_line = self._current_line.pop()
        if end_of_line != (' ', ''):
            self._current_line.push(*end_of_line)
        if len(self._current_line) > 0:
            if self._current_line.is_onlyws() and self._lines:
                self._lines[-1] += str(self._current_line)
            else:
                self._lines.append(str(self._current_line))
        self._current_line.reset()

    def add_transition(self):
        self._current_line.push(' ', '')

    def feed(self, fws, string, charset):
        
        
        
        
        
        if charset.header_encoding is None:
            self._ascii_split(fws, string, self._splitchars)
            return
        
        
        
        
        
        
        
        encoded_lines = charset.header_encode_lines(string, self._maxlengths())
        
        
        try:
            first_line = encoded_lines.pop(0)
        except IndexError:
            
            return
        if first_line is not None:
            self._append_chunk(fws, first_line)
        try:
            last_line = encoded_lines.pop()
        except IndexError:
            
            return
        self.newline()
        self._current_line.push(self._continuation_ws, last_line)
        
        for line in encoded_lines:
            self._lines.append(self._continuation_ws + line)

    def _maxlengths(self):
        
        yield self._maxlen - len(self._current_line)
        while True:
            yield self._maxlen - self._continuation_ws_len

    def _ascii_split(self, fws, string, splitchars):
        
        
        
        
        
        
        
        
        
        
        
        
        
        parts = re.split("(["+FWS+"]+)", fws+string)
        if parts[0]:
            parts[:0] = ['']
        else:
            parts.pop(0)
        for fws, part in zip(*[iter(parts)]*2):
            self._append_chunk(fws, part)

    def _append_chunk(self, fws, string):
        self._current_line.push(fws, string)
        if len(self._current_line) > self._maxlen:
            
            
            for ch in self._splitchars:
                for i in range(self._current_line.part_count()-1, 0, -1):
                    if ch.isspace():
                        fws = self._current_line[i][0]
                        if fws and fws[0]==ch:
                            break
                    prevpart = self._current_line[i-1][1]
                    if prevpart and prevpart[-1]==ch:
                        break
                else:
                    continue
                break
            else:
                fws, part = self._current_line.pop()
                if self._current_line._initial_size > 0:
                    
                    self.newline()
                    if not fws:
                        
                        
                        fws = ' '
                self._current_line.push(fws, part)
                return
            remainder = self._current_line.pop_from(i)
            self._lines.append(str(self._current_line))
            self._current_line.reset(remainder)


class _Accumulator(list):

    def __init__(self, initial_size=0):
        self._initial_size = initial_size
        super().__init__()

    def push(self, fws, string):
        self.append((fws, string))

    def pop_from(self, i=0):
        popped = self[i:]
        self[i:] = []
        return popped

    def pop(self):
        if self.part_count()==0:
            return ('', '')
        return super().pop()

    def __len__(self):
        return sum((len(fws)+len(part) for fws, part in self),
                   self._initial_size)

    def __str__(self):
        return EMPTYSTRING.join((EMPTYSTRING.join((fws, part))
                                for fws, part in self))

    def reset(self, startval=None):
        if startval is None:
            startval = []
        self[:] = startval
        self._initial_size = 0

    def is_onlyws(self):
        return self._initial_size==0 and (not self or str(self).isspace())

    def part_count(self):
        return super().__len__()