c 20012007 python software foundation barry warsaw contact emailsigpython org basic message object for the email package object model all message emailmessage import binascii import re import quopri from io import bytesio stringio intrapackage imports from email import utils from email import errors from email policybase import compat32 from email import charset as charset from email encodedwords import decodeb charset charset charset semispace regular expression that matches special characters in parameters the existence of which force quoting of the parameter value tspecials re compiler def splitparamparam split header parameters baw this may be too simple it isn t strictly rfc 2045 section 5 1 compliant but it catches most headers found in the wild we may eventually need a full fledged parser rdm we might have a header here for now just stringify it a sep b strparam partition if not sep return a strip none return a strip b strip def formatparamparam valuenone quotetrue if value is not none and lenvalue 0 a tuple is used for rfc 2231 encoded parameter values where items are charset language value charset is a string not a charset instance rfc 2231 encoded values are never quoted per rfc if isinstancevalue tuple encode as per rfc 2231 param value utils encoderfc2231value2 value0 value1 return ss param value else try value encode ascii except unicodeencodeerror param value utils encoderfc2231value utf8 return ss param value baw please check this i think that if quote is set it should force quoting even if not necessary if quote or tspecials searchvalue return ss param utils quotevalue else return ss param value else return param def parseparams rdm this might be a header so for now stringify it s strs plist while s 1 s s1 end s find while end 0 and s count 0 end s count 0 end 2 end s find end 1 if end 0 end lens f s end if in f i f index f f i strip lower fi1 strip plist appendf strip s send return plist def unquotevaluevalue this is different than utils collapserfc2231value because it doesn t try to convert the value to a unicode message getparam and message getparams are both currently defined to return the tuple in the face of rfc 2231 parameters if isinstancevalue tuple return value0 value1 utils unquotevalue2 else return utils unquotevalue def decodeuuencoded workaround for broken uuencoders by fredrik lundh basic message object a message object is defined as something that has a bunch of rfc 2822 headers and a payload it may optionally have an envelope header a k a unixfrom or from header if the message is a container i e a multipart or a messagerfc822 then the payload is a list of message objects otherwise it is a string message objects implement part of the mapping interface which assumes there is exactly one occurrence of the header per message some headers do in fact appear multiple times e g received and for those headers you must use the explicit api to set or get all the headers not all of the mapping methods are implemented defaults for multipart messages default content type return the entire formatted message as a string return the entire formatted message as a string optional unixfrom when true means include the unix from envelope header for backward compatibility reasons if maxheaderlen is not specified it defaults to 0 so you must override it explicitly if you want a different maxheaderlen policy is passed to the generator instance used to serialize the message if it is not specified the policy associated with the message instance is used if the message object contains binary data that is not encoded according to rfc standards the noncompliant data will be replaced by unicode unknown character code points return the entire formatted message as a bytes object return the entire formatted message as a bytes object optional unixfrom when true means include the unix from envelope header policy is passed to the bytesgenerator instance used to serialize the message if not specified the policy associated with the message instance is used return true if the message consists of multiple parts return isinstanceself payload list unix from line def setunixfromself unixfrom self unixfrom unixfrom def getunixfromself return self unixfrom payload manipulation def attachself payload if self payload is none self payload payload else try self payload appendpayload except attributeerror raise typeerrorattach is not valid on a message with a nonmultipart payload def getpayloadself inone decodefalse here is the logic table for this code based on the email5 0 0 code i decode ismultipart result none true true none i true true none none false true payload a list i false true payload element i a message i false false error not a list i true false error not a list none false false payload none true false payload decoded bytes note that barry planned to factor out the decode case but that isn t so easy now that we handle the 8 bit data which needs to be converted in both the decode and nondecode path if self ismultipart if decode return none if i is none return self payload else return self payloadi for backward compatibility use isinstance and this error message instead of the more logical ismultipart test if i is not none and not isinstanceself payload list raise typeerror expected list got s typeself payload payload self payload cte might be a header so for now stringify it cte strself get contenttransferencoding lower payload may be bytes here if isinstancepayload str if utils hassurrogatespayload bpayload payload encode ascii surrogateescape if not decode try payload bpayload decodeself getparam charset ascii replace except lookuperror payload bpayload decode ascii replace elif decode try bpayload payload encode ascii except unicodeerror this won t happen for rfc compliant messages messages containing only ascii code points in the unicode input if it does happen turn the string into bytes in a way guaranteed not to fail bpayload payload encode rawunicodeescape if not decode return payload if cte quotedprintable return quopri decodestringbpayload elif cte base64 xxx this is a bit of a hack decodeb should probably be factored out somewhere but i haven t figured out where yet value defects decodebb joinbpayload splitlines for defect in defects self policy handledefectself defect return value elif cte in xuuencode uuencode uue xuue try return decodeuubpayload except valueerror some decoding problem return bpayload if isinstancepayload str return bpayload return payload def setpayloadself payload charsetnone if hasattrpayload encode if charset is none self payload payload return if not isinstancecharset charset charset charsetcharset payload payload encodecharset outputcharset if hasattrpayload decode self payload payload decode ascii surrogateescape else self payload payload if charset is not none self setcharsetcharset def setcharsetself charset if charset is none self delparam charset self charset none return if not isinstancecharset charset charset charsetcharset self charset charset if mimeversion not in self self addheader mimeversion 1 0 if contenttype not in self self addheader contenttype textplain charsetcharset getoutputcharset else self setparam charset charset getoutputcharset if charset charset getoutputcharset self payload charset bodyencodeself payload if contenttransferencoding not in self cte charset getbodyencoding try cteself except typeerror this if is for backward compatibility it allows unicode through even though that won t work correctly if the message is serialized payload self payload if payload try payload payload encode ascii surrogateescape except unicodeerror payload payload encodecharset outputcharset self payload charset bodyencodepayload self addheader contenttransferencoding cte def getcharsetself return self charset mapping interface partial def lenself get a header value return none if the header is missing instead of raising an exception note that if the header appeared multiple times exactly which occurrence gets returned is undefined use getall to get all the values matching a header field name set the value of a header note this does not overwrite an existing header with the same field name use delitem first to delete any existing headers delete all occurrences of a header if present does not raise an exception if the header is missing return a list of all the message s header field names these will be sorted in the order they appeared in the original message or were added to the message and may contain duplicates any fields deleted and reinserted are always appended to the header list return a list of all the message s header values these will be sorted in the order they appeared in the original message or were added to the message and may contain duplicates any fields deleted and reinserted are always appended to the header list get all the message s header fields and values these will be sorted in the order they appeared in the original message or were added to the message and may contain duplicates any fields deleted and reinserted are always appended to the header list get a header value like getitem but return failobj instead of none when the field is missing internal methods public api but only intended for use by a parser or generator not normal application code store name and value in the model without modification this is an internal api intended only for use by a parser return the name value header pairs without modification this is an internal api intended only for use by a generator additional useful stuff return a list of all the values for the named field these will be sorted in the order they appeared in the original message and may contain duplicates any fields deleted and reinserted are always appended to the header list if no such fields exist failobj is returned defaults to none extended header setting name is the header field to add keyword arguments can be used to set additional parameters for the header field with underscores converted to dashes normally the parameter will be added as keyvalue unless value is none in which case only the key will be added if a parameter value contains nonascii characters it can be specified as a threetuple of charset language value in which case it will be encoded according to rfc2231 rules otherwise it will be encoded using the utf8 charset and a language of examples msg addheader contentdisposition attachment filename bud gif msg addheader contentdisposition attachment filename utf8 fuballer ppt msg addheader contentdisposition attachment filename fuballer ppt replace a header replace the first matching header found in the message retaining header order and case if no matching header was found a keyerror is raised use these three methods instead of the three above return the message s content type the returned string is coerced to lower case of the form maintypesubtype if there was no contenttype header in the message the default type as given by getdefaulttype will be returned since according to rfc 2045 messages always have a default type this will always return a value rfc 2045 defines a message s default type to be textplain unless it appears inside a multipartdigest container in which case it would be messagerfc822 this should have no parameters rfc 2045 section 5 2 says if its invalid use textplain return the message s main content type this is the maintype part of the string returned by getcontenttype returns the message s subcontent type this is the subtype part of the string returned by getcontenttype return the default content type most messages have a default content type of textplain except for messages that are subparts of multipartdigest containers such subparts have a default content type of messagerfc822 set the default content type ctype should be either textplain or messagerfc822 although this is not enforced the default content type is not stored in the contenttype header like getparams but preserves the quoting of values baw should this be part of the public interface must have been a bare attribute return the message s contenttype parameters as a list the elements of the returned list are 2tuples of keyvalue pairs as split on the sign the left hand side of the is the key while the right hand side is the value if there is no sign in the parameter the value is the empty string the value is as described in the getparam method optional failobj is the object to return if there is no contenttype header optional header is the header to search instead of contenttype if unquote is true the value is unquoted return the parameter value if found in the contenttype header optional failobj is the object to return if there is no contenttype header or the contenttype header has no such parameter optional header is the header to search instead of contenttype parameter keys are always compared case insensitively the return value can either be a string or a 3tuple if the parameter was rfc 2231 encoded when it s a 3tuple the elements of the value are of the form charset language value note that both charset and language can be none in which case you should consider value to be encoded in the usascii charset you can usually ignore language the parameter value either the returned string or the value item in the 3tuple is always unquoted unless unquote is set to false if your application doesn t care whether the parameter was rfc 2231 encoded it can turn the return value into a string as follows rawparam msg getparam foo param email utils collapserfc2231valuerawparam set a parameter in the contenttype header if the parameter already exists in the header its value will be replaced with the new value if header is contenttype and has not yet been defined for this message it will be set to textplain and the new parameter and value will be appended as per rfc 2045 an alternate header can be specified in the header argument and all parameters will be quoted as necessary unless requote is false if charset is specified the parameter will be encoded according to rfc 2231 optional language specifies the rfc 2231 language defaulting to the empty string both charset and language should be strings remove the given parameter completely from the contenttype header the header will be rewritten in place without the parameter or its value all values will be quoted as necessary unless requote is false optional header specifies an alternative to the contenttype header set the main type and subtype for the contenttype header type must be a string in the form maintypesubtype otherwise a valueerror is raised this method replaces the contenttype header keeping all the parameters in place if requote is false this leaves the existing header s quoting as is otherwise the parameters will be quoted the default an alternative header can be specified in the header argument when the contenttype header is set we ll always also add a mimeversion header baw should we be strict set the contenttype you get a mimeversion skip the first param it s the old type return the filename associated with the payload if present the filename is extracted from the contentdisposition header s filename parameter and it is unquoted if that header is missing the filename parameter this method falls back to looking for the name parameter return the boundary associated with the payload if present the boundary is extracted from the contenttype header s boundary parameter and it is unquoted rfc 2046 says that boundaries may begin but not end in ws set the boundary parameter in contenttype to boundary this is subtly different than deleting the contenttype header and adding a new one with a new boundary parameter via addheader the main difference is that using the setboundary method preserves the order of the contenttype header in the original message headerparseerror is raised if the message has no contenttype header there was no contenttype header and we don t know what type to set it to so raise an exception the original contenttype header had no boundary attribute tack one on the end baw should we raise an exception instead replace the existing contenttype header with the new value return the charset parameter of the contenttype header the returned string is always coerced to lower case if there is no contenttype header or if that header has no charset parameter failobj is returned rfc 2231 encoded so decode it and it better end up as ascii lookuperror will be raised if the charset isn t known to python unicodeerror will be raised if the encoded text contains a character not in the charset charset characters must be in usascii range rfc 2046 4 1 2 says charsets are not case sensitive return a list containing the charsets used in this message the returned list of items describes the contenttype headers charset parameter for this message and all the subparts in its payload each item will either be a string the value of the charset parameter in the contenttype header of that part or the value of the failobj parameter defaults to none if the part does not have a main mime type of text or the charset is not defined the list will contain one string for each part of the message plus one for the container message i e self so that a nonmultipart message will still return a list of length 1 return the message s contentdisposition if it exists or none the return values can be either inline attachment or none according to the rfc2183 i e def walkself return the entire formatted message as a string optional unixfrom when true means include the unix from envelope header maxheaderlen is retained for backward compatibility with the base message class but defaults to none meaning that the policy value for maxlinelength controls the header maximum length policy is passed to the generator instance used to serialize the message if it is not specified the policy associated with the message instance is used return best candidate mime part for display as body of message do a depth first search starting with self looking for the first part matching each of the items in preferencelist and return the part corresponding to the first item that has a match or none if no items have a match if related is not included in preferencelist consider the root part of any multipartrelated encountered as a candidate match ignore parts with contentdisposition attachment return an iterator over the nonmain parts of a multipart skip the first of each occurrence of textplain texthtml multipartrelated or multipartalternative in the multipart unless they have a contentdisposition attachment header and include all remaining subparts in the returned iterator when applied to a multipartrelated return all parts except the root part return an empty iterator when applied to a multipartalternative or a nonmultipart certain malformed messages can have content type set to multipart but still have single part body in which case payload copy can fail with attributeerror payload is not a list it is most probably a string for related we treat everything but the root as an attachment the root may be indicated by start if there s no start or we can t find the named start treat the first subpart as the root otherwise we more or less invert the remaining logic in getbody this only really works in edge cases ex nontext related or alternatives if the sending agent sets contentdisposition return an iterator over all immediate subparts of a multipart return an empty iterator for a nonmultipart there is existing content move it to the first subpart c 2001 2007 python software foundation barry warsaw contact email sig python org basic message object for the email package object model intrapackage imports regular expression that matches special characters in parameters the existence of which force quoting of the parameter value split header parameters baw this may be too simple it isn t strictly rfc 2045 section 5 1 compliant but it catches most headers found in the wild we may eventually need a full fledged parser rdm we might have a header here for now just stringify it convenience function to format and return a key value pair this will quote the value if needed or if quote is true if value is a three tuple charset language value it will be encoded according to rfc2231 rules if it contains non ascii characters it will likewise be encoded according to rfc2231 rules using the utf 8 charset and a null language a tuple is used for rfc 2231 encoded parameter values where items are charset language value charset is a string not a charset instance rfc 2231 encoded values are never quoted per rfc encode as per rfc 2231 baw please check this i think that if quote is set it should force quoting even if not necessary rdm this might be a header so for now stringify it this is different than utils collapse_rfc2231_value because it doesn t try to convert the value to a unicode message get_param and message get_params are both currently defined to return the tuple in the face of rfc 2231 parameters decode uuencoded data workaround for broken uuencoders by fredrik lundh basic message object a message object is defined as something that has a bunch of rfc 2822 headers and a payload it may optionally have an envelope header a k a unix from or from_ header if the message is a container i e a multipart or a message rfc822 then the payload is a list of message objects otherwise it is a string message objects implement part of the mapping interface which assumes there is exactly one occurrence of the header per message some headers do in fact appear multiple times e g received and for those headers you must use the explicit api to set or get all the headers not all of the mapping methods are implemented defaults for multipart messages default content type return the entire formatted message as a string return the entire formatted message as a string optional unixfrom when true means include the unix from_ envelope header for backward compatibility reasons if maxheaderlen is not specified it defaults to 0 so you must override it explicitly if you want a different maxheaderlen policy is passed to the generator instance used to serialize the message if it is not specified the policy associated with the message instance is used if the message object contains binary data that is not encoded according to rfc standards the non compliant data will be replaced by unicode unknown character code points return the entire formatted message as a bytes object return the entire formatted message as a bytes object optional unixfrom when true means include the unix from_ envelope header policy is passed to the bytesgenerator instance used to serialize the message if not specified the policy associated with the message instance is used return true if the message consists of multiple parts unix from_ line payload manipulation add the given payload to the current payload the current payload will always be a list of objects after this method is called if you want to set the payload to a scalar object use set_payload instead return a reference to the payload the payload will either be a list object or a string if you mutate the list object you modify the message s payload in place optional i returns that index into the payload optional decode is a flag indicating whether the payload should be decoded or not according to the content transfer encoding header default is false when true and the message is not a multipart the payload will be decoded if this header s value is quoted printable or base64 if some other encoding is used or the header is missing or if the payload has bogus data i e bogus base64 or uuencoded data the payload is returned as is if the message is a multipart and the decode flag is true then none is returned here is the logic table for this code based on the email5 0 0 code i decode is_multipart result none true true none i true true none none false true _payload a list i false true _payload element i a message i false false error not a list i true false error not a list none false false _payload none true false _payload decoded bytes note that barry planned to factor out the decode case but that isn t so easy now that we handle the 8 bit data which needs to be converted in both the decode and non decode path for backward compatibility use isinstance and this error message instead of the more logical is_multipart test cte might be a header so for now stringify it payload may be bytes here this won t happen for rfc compliant messages messages containing only ascii code points in the unicode input if it does happen turn the string into bytes in a way guaranteed not to fail xxx this is a bit of a hack decode_b should probably be factored out somewhere but i haven t figured out where yet some decoding problem set the payload to the given value optional charset sets the message s default character set see set_charset for details set the charset of the payload to a given character set charset can be a charset instance a string naming a character set or none if it is a string it will be converted to a charset instance if charset is none the charset parameter will be removed from the content type field anything else will generate a typeerror the message will be assumed to be of type text encoded with charset input_charset it will be converted to charset output_charset and encoded properly if needed when generating the plain text representation of the message mime headers mime version content type content transfer encoding will be added as needed this if is for backward compatibility it allows unicode through even though that won t work correctly if the message is serialized return the charset instance associated with the message s payload mapping interface partial return the total number of headers including duplicates get a header value return none if the header is missing instead of raising an exception note that if the header appeared multiple times exactly which occurrence gets returned is undefined use get_all to get all the values matching a header field name set the value of a header note this does not overwrite an existing header with the same field name use __delitem__ first to delete any existing headers delete all occurrences of a header if present does not raise an exception if the header is missing return a list of all the message s header field names these will be sorted in the order they appeared in the original message or were added to the message and may contain duplicates any fields deleted and re inserted are always appended to the header list return a list of all the message s header values these will be sorted in the order they appeared in the original message or were added to the message and may contain duplicates any fields deleted and re inserted are always appended to the header list get all the message s header fields and values these will be sorted in the order they appeared in the original message or were added to the message and may contain duplicates any fields deleted and re inserted are always appended to the header list get a header value like __getitem__ but return failobj instead of none when the field is missing internal methods public api but only intended for use by a parser or generator not normal application code store name and value in the model without modification this is an internal api intended only for use by a parser return the name value header pairs without modification this is an internal api intended only for use by a generator additional useful stuff return a list of all the values for the named field these will be sorted in the order they appeared in the original message and may contain duplicates any fields deleted and re inserted are always appended to the header list if no such fields exist failobj is returned defaults to none extended header setting name is the header field to add keyword arguments can be used to set additional parameters for the header field with underscores converted to dashes normally the parameter will be added as key value unless value is none in which case only the key will be added if a parameter value contains non ascii characters it can be specified as a three tuple of charset language value in which case it will be encoded according to rfc2231 rules otherwise it will be encoded using the utf 8 charset and a language of examples msg add_header content disposition attachment filename bud gif msg add_header content disposition attachment filename utf 8 fußballer ppt msg add_header content disposition attachment filename fußballer ppt replace a header replace the first matching header found in the message retaining header order and case if no matching header was found a keyerror is raised use these three methods instead of the three above return the message s content type the returned string is coerced to lower case of the form maintype subtype if there was no content type header in the message the default type as given by get_default_type will be returned since according to rfc 2045 messages always have a default type this will always return a value rfc 2045 defines a message s default type to be text plain unless it appears inside a multipart digest container in which case it would be message rfc822 this should have no parameters rfc 2045 section 5 2 says if its invalid use text plain return the message s main content type this is the maintype part of the string returned by get_content_type returns the message s sub content type this is the subtype part of the string returned by get_content_type return the default content type most messages have a default content type of text plain except for messages that are subparts of multipart digest containers such subparts have a default content type of message rfc822 set the default content type ctype should be either text plain or message rfc822 although this is not enforced the default content type is not stored in the content type header like get_params but preserves the quoting of values baw should this be part of the public interface must have been a bare attribute return the message s content type parameters as a list the elements of the returned list are 2 tuples of key value pairs as split on the sign the left hand side of the is the key while the right hand side is the value if there is no sign in the parameter the value is the empty string the value is as described in the get_param method optional failobj is the object to return if there is no content type header optional header is the header to search instead of content type if unquote is true the value is unquoted return the parameter value if found in the content type header optional failobj is the object to return if there is no content type header or the content type header has no such parameter optional header is the header to search instead of content type parameter keys are always compared case insensitively the return value can either be a string or a 3 tuple if the parameter was rfc 2231 encoded when it s a 3 tuple the elements of the value are of the form charset language value note that both charset and language can be none in which case you should consider value to be encoded in the us ascii charset you can usually ignore language the parameter value either the returned string or the value item in the 3 tuple is always unquoted unless unquote is set to false if your application doesn t care whether the parameter was rfc 2231 encoded it can turn the return value into a string as follows rawparam msg get_param foo param email utils collapse_rfc2231_value rawparam set a parameter in the content type header if the parameter already exists in the header its value will be replaced with the new value if header is content type and has not yet been defined for this message it will be set to text plain and the new parameter and value will be appended as per rfc 2045 an alternate header can be specified in the header argument and all parameters will be quoted as necessary unless requote is false if charset is specified the parameter will be encoded according to rfc 2231 optional language specifies the rfc 2231 language defaulting to the empty string both charset and language should be strings remove the given parameter completely from the content type header the header will be re written in place without the parameter or its value all values will be quoted as necessary unless requote is false optional header specifies an alternative to the content type header set the main type and subtype for the content type header type must be a string in the form maintype subtype otherwise a valueerror is raised this method replaces the content type header keeping all the parameters in place if requote is false this leaves the existing header s quoting as is otherwise the parameters will be quoted the default an alternative header can be specified in the header argument when the content type header is set we ll always also add a mime version header baw should we be strict set the content type you get a mime version skip the first param it s the old type return the filename associated with the payload if present the filename is extracted from the content disposition header s filename parameter and it is unquoted if that header is missing the filename parameter this method falls back to looking for the name parameter return the boundary associated with the payload if present the boundary is extracted from the content type header s boundary parameter and it is unquoted rfc 2046 says that boundaries may begin but not end in w s set the boundary parameter in content type to boundary this is subtly different than deleting the content type header and adding a new one with a new boundary parameter via add_header the main difference is that using the set_boundary method preserves the order of the content type header in the original message headerparseerror is raised if the message has no content type header there was no content type header and we don t know what type to set it to so raise an exception the original content type header had no boundary attribute tack one on the end baw should we raise an exception instead replace the existing content type header with the new value return the charset parameter of the content type header the returned string is always coerced to lower case if there is no content type header or if that header has no charset parameter failobj is returned rfc 2231 encoded so decode it and it better end up as ascii lookuperror will be raised if the charset isn t known to python unicodeerror will be raised if the encoded text contains a character not in the charset charset characters must be in us ascii range rfc 2046 4 1 2 says charsets are not case sensitive return a list containing the charset s used in this message the returned list of items describes the content type headers charset parameter for this message and all the subparts in its payload each item will either be a string the value of the charset parameter in the content type header of that part or the value of the failobj parameter defaults to none if the part does not have a main mime type of text or the charset is not defined the list will contain one string for each part of the message plus one for the container message i e self so that a non multipart message will still return a list of length 1 return the message s content disposition if it exists or none the return values can be either inline attachment or none according to the rfc2183 i e def walk self return the entire formatted message as a string optional unixfrom when true means include the unix from_ envelope header maxheaderlen is retained for backward compatibility with the base message class but defaults to none meaning that the policy value for max_line_length controls the header maximum length policy is passed to the generator instance used to serialize the message if it is not specified the policy associated with the message instance is used return best candidate mime part for display as body of message do a depth first search starting with self looking for the first part matching each of the items in preferencelist and return the part corresponding to the first item that has a match or none if no items have a match if related is not included in preferencelist consider the root part of any multipart related encountered as a candidate match ignore parts with content disposition attachment return an iterator over the non main parts of a multipart skip the first of each occurrence of text plain text html multipart related or multipart alternative in the multipart unless they have a content disposition attachment header and include all remaining subparts in the returned iterator when applied to a multipart related return all parts except the root part return an empty iterator when applied to a multipart alternative or a non multipart certain malformed messages can have content type set to multipart but still have single part body in which case payload copy can fail with attributeerror payload is not a list it is most probably a string for related we treat everything but the root as an attachment the root may be indicated by start if there s no start or we can t find the named start treat the first subpart as the root otherwise we more or less invert the remaining logic in get_body this only really works in edge cases ex non text related or alternatives if the sending agent sets content disposition only skip the first example of each candidate type return an iterator over all immediate subparts of a multipart return an empty iterator for a non multipart there is existing content move it to the first subpart
__all__ = ['Message', 'EmailMessage']

import binascii
import re
import quopri
from io import BytesIO, StringIO


from email import utils
from email import errors
from email._policybase import compat32
from email import charset as _charset
from email._encoded_words import decode_b
Charset = _charset.Charset

SEMISPACE = '; '



tspecials = re.compile(r'[ \(\)<>@,;:\\"/\[\]\?=]')


def _splitparam(param):
    
    
    
    
    a, sep, b = str(param).partition(';')
    if not sep:
        return a.strip(), None
    return a.strip(), b.strip()

def _formatparam(param, value=None, quote=True):
    
    if value is not None and len(value) > 0:
        
        
        
        if isinstance(value, tuple):
            
            param += '*'
            value = utils.encode_rfc2231(value[2], value[0], value[1])
            return '%s=%s' % (param, value)
        else:
            try:
                value.encode('ascii')
            except UnicodeEncodeError:
                param += '*'
                value = utils.encode_rfc2231(value, 'utf-8', '')
                return '%s=%s' % (param, value)
        
        
        if quote or tspecials.search(value):
            return '%s="%s"' % (param, utils.quote(value))
        else:
            return '%s=%s' % (param, value)
    else:
        return param

def _parseparam(s):
    
    s = ';' + str(s)
    plist = []
    while s[:1] == ';':
        s = s[1:]
        end = s.find(';')
        while end > 0 and (s.count('"', 0, end) - s.count('\\"', 0, end)) % 2:
            end = s.find(';', end + 1)
        if end < 0:
            end = len(s)
        f = s[:end]
        if '=' in f:
            i = f.index('=')
            f = f[:i].strip().lower() + '=' + f[i+1:].strip()
        plist.append(f.strip())
        s = s[end:]
    return plist


def _unquotevalue(value):
    
    
    
    
    if isinstance(value, tuple):
        return value[0], value[1], utils.unquote(value[2])
    else:
        return utils.unquote(value)


def _decode_uu(encoded):
    
    decoded_lines = []
    encoded_lines_iter = iter(encoded.splitlines())
    for line in encoded_lines_iter:
        if line.startswith(b"begin "):
            mode, _, path = line.removeprefix(b"begin ").partition(b" ")
            try:
                int(mode, base=8)
            except ValueError:
                continue
            else:
                break
    else:
        raise ValueError("`begin` line not found")
    for line in encoded_lines_iter:
        if not line:
            raise ValueError("Truncated input")
        elif line.strip(b' \t\r\n\f') == b'end':
            break
        try:
            decoded_line = binascii.a2b_uu(line)
        except binascii.Error:
            
            nbytes = (((line[0]-32) & 63) * 4 + 5) // 3
            decoded_line = binascii.a2b_uu(line[:nbytes])
        decoded_lines.append(decoded_line)

    return b''.join(decoded_lines)


class Message:
    
    def __init__(self, policy=compat32):
        self.policy = policy
        self._headers = []
        self._unixfrom = None
        self._payload = None
        self._charset = None
        
        self.preamble = self.epilogue = None
        self.defects = []
        
        self._default_type = 'text/plain'

    def __str__(self):
        
        return self.as_string()

    def as_string(self, unixfrom=False, maxheaderlen=0, policy=None):
        
        from email.generator import Generator
        policy = self.policy if policy is None else policy
        fp = StringIO()
        g = Generator(fp,
                      mangle_from_=False,
                      maxheaderlen=maxheaderlen,
                      policy=policy)
        g.flatten(self, unixfrom=unixfrom)
        return fp.getvalue()

    def __bytes__(self):
        
        return self.as_bytes()

    def as_bytes(self, unixfrom=False, policy=None):
        
        from email.generator import BytesGenerator
        policy = self.policy if policy is None else policy
        fp = BytesIO()
        g = BytesGenerator(fp, mangle_from_=False, policy=policy)
        g.flatten(self, unixfrom=unixfrom)
        return fp.getvalue()

    def is_multipart(self):
        
        return isinstance(self._payload, list)

    
    
    
    def set_unixfrom(self, unixfrom):
        self._unixfrom = unixfrom

    def get_unixfrom(self):
        return self._unixfrom

    
    
    
    def attach(self, payload):
        
        if self._payload is None:
            self._payload = [payload]
        else:
            try:
                self._payload.append(payload)
            except AttributeError:
                raise TypeError("Attach is not valid on a message with a"
                                " non-multipart payload")

    def get_payload(self, i=None, decode=False):
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        if self.is_multipart():
            if decode:
                return None
            if i is None:
                return self._payload
            else:
                return self._payload[i]
        
        
        if i is not None and not isinstance(self._payload, list):
            raise TypeError('Expected list, got %s' % type(self._payload))
        payload = self._payload
        
        cte = str(self.get('content-transfer-encoding', '')).lower()
        
        if isinstance(payload, str):
            if utils._has_surrogates(payload):
                bpayload = payload.encode('ascii', 'surrogateescape')
                if not decode:
                    try:
                        payload = bpayload.decode(self.get_param('charset', 'ascii'), 'replace')
                    except LookupError:
                        payload = bpayload.decode('ascii', 'replace')
            elif decode:
                try:
                    bpayload = payload.encode('ascii')
                except UnicodeError:
                    
                    
                    
                    
                    bpayload = payload.encode('raw-unicode-escape')
        if not decode:
            return payload
        if cte == 'quoted-printable':
            return quopri.decodestring(bpayload)
        elif cte == 'base64':
            
            
            value, defects = decode_b(b''.join(bpayload.splitlines()))
            for defect in defects:
                self.policy.handle_defect(self, defect)
            return value
        elif cte in ('x-uuencode', 'uuencode', 'uue', 'x-uue'):
            try:
                return _decode_uu(bpayload)
            except ValueError:
                
                return bpayload
        if isinstance(payload, str):
            return bpayload
        return payload

    def set_payload(self, payload, charset=None):
        
        if hasattr(payload, 'encode'):
            if charset is None:
                self._payload = payload
                return
            if not isinstance(charset, Charset):
                charset = Charset(charset)
            payload = payload.encode(charset.output_charset)
        if hasattr(payload, 'decode'):
            self._payload = payload.decode('ascii', 'surrogateescape')
        else:
            self._payload = payload
        if charset is not None:
            self.set_charset(charset)

    def set_charset(self, charset):
        
        if charset is None:
            self.del_param('charset')
            self._charset = None
            return
        if not isinstance(charset, Charset):
            charset = Charset(charset)
        self._charset = charset
        if 'MIME-Version' not in self:
            self.add_header('MIME-Version', '1.0')
        if 'Content-Type' not in self:
            self.add_header('Content-Type', 'text/plain',
                            charset=charset.get_output_charset())
        else:
            self.set_param('charset', charset.get_output_charset())
        if charset != charset.get_output_charset():
            self._payload = charset.body_encode(self._payload)
        if 'Content-Transfer-Encoding' not in self:
            cte = charset.get_body_encoding()
            try:
                cte(self)
            except TypeError:
                
                
                
                payload = self._payload
                if payload:
                    try:
                        payload = payload.encode('ascii', 'surrogateescape')
                    except UnicodeError:
                        payload = payload.encode(charset.output_charset)
                self._payload = charset.body_encode(payload)
                self.add_header('Content-Transfer-Encoding', cte)

    def get_charset(self):
        
        return self._charset

    
    
    
    def __len__(self):
        
        return len(self._headers)

    def __getitem__(self, name):
        
        return self.get(name)

    def __setitem__(self, name, val):
        
        max_count = self.policy.header_max_count(name)
        if max_count:
            lname = name.lower()
            found = 0
            for k, v in self._headers:
                if k.lower() == lname:
                    found += 1
                    if found >= max_count:
                        raise ValueError("There may be at most {} {} headers "
                                         "in a message".format(max_count, name))
        self._headers.append(self.policy.header_store_parse(name, val))

    def __delitem__(self, name):
        
        name = name.lower()
        newheaders = []
        for k, v in self._headers:
            if k.lower() != name:
                newheaders.append((k, v))
        self._headers = newheaders

    def __contains__(self, name):
        name_lower = name.lower()
        for k, v in self._headers:
            if name_lower == k.lower():
                return True
        return False

    def __iter__(self):
        for field, value in self._headers:
            yield field

    def keys(self):
        
        return [k for k, v in self._headers]

    def values(self):
        
        return [self.policy.header_fetch_parse(k, v)
                for k, v in self._headers]

    def items(self):
        
        return [(k, self.policy.header_fetch_parse(k, v))
                for k, v in self._headers]

    def get(self, name, failobj=None):
        
        name = name.lower()
        for k, v in self._headers:
            if k.lower() == name:
                return self.policy.header_fetch_parse(k, v)
        return failobj

    
    
    
    

    def set_raw(self, name, value):
        
        self._headers.append((name, value))

    def raw_items(self):
        
        return iter(self._headers.copy())

    
    
    

    def get_all(self, name, failobj=None):
        
        values = []
        name = name.lower()
        for k, v in self._headers:
            if k.lower() == name:
                values.append(self.policy.header_fetch_parse(k, v))
        if not values:
            return failobj
        return values

    def add_header(self, _name, _value, **_params):
        
        parts = []
        for k, v in _params.items():
            if v is None:
                parts.append(k.replace('_', '-'))
            else:
                parts.append(_formatparam(k.replace('_', '-'), v))
        if _value is not None:
            parts.insert(0, _value)
        self[_name] = SEMISPACE.join(parts)

    def replace_header(self, _name, _value):
        
        _name = _name.lower()
        for i, (k, v) in zip(range(len(self._headers)), self._headers):
            if k.lower() == _name:
                self._headers[i] = self.policy.header_store_parse(k, _value)
                break
        else:
            raise KeyError(_name)

    
    
    

    def get_content_type(self):
        
        missing = object()
        value = self.get('content-type', missing)
        if value is missing:
            
            return self.get_default_type()
        ctype = _splitparam(value)[0].lower()
        
        if ctype.count('/') != 1:
            return 'text/plain'
        return ctype

    def get_content_maintype(self):
        
        ctype = self.get_content_type()
        return ctype.split('/')[0]

    def get_content_subtype(self):
        
        ctype = self.get_content_type()
        return ctype.split('/')[1]

    def get_default_type(self):
        
        return self._default_type

    def set_default_type(self, ctype):
        
        self._default_type = ctype

    def _get_params_preserve(self, failobj, header):
        
        
        missing = object()
        value = self.get(header, missing)
        if value is missing:
            return failobj
        params = []
        for p in _parseparam(value):
            try:
                name, val = p.split('=', 1)
                name = name.strip()
                val = val.strip()
            except ValueError:
                
                name = p.strip()
                val = ''
            params.append((name, val))
        params = utils.decode_params(params)
        return params

    def get_params(self, failobj=None, header='content-type', unquote=True):
        
        missing = object()
        params = self._get_params_preserve(missing, header)
        if params is missing:
            return failobj
        if unquote:
            return [(k, _unquotevalue(v)) for k, v in params]
        else:
            return params

    def get_param(self, param, failobj=None, header='content-type',
                  unquote=True):
        
        if header not in self:
            return failobj
        for k, v in self._get_params_preserve(failobj, header):
            if k.lower() == param.lower():
                if unquote:
                    return _unquotevalue(v)
                else:
                    return v
        return failobj

    def set_param(self, param, value, header='Content-Type', requote=True,
                  charset=None, language='', replace=False):
        
        if not isinstance(value, tuple) and charset:
            value = (charset, language, value)

        if header not in self and header.lower() == 'content-type':
            ctype = 'text/plain'
        else:
            ctype = self.get(header)
        if not self.get_param(param, header=header):
            if not ctype:
                ctype = _formatparam(param, value, requote)
            else:
                ctype = SEMISPACE.join(
                    [ctype, _formatparam(param, value, requote)])
        else:
            ctype = ''
            for old_param, old_value in self.get_params(header=header,
                                                        unquote=requote):
                append_param = ''
                if old_param.lower() == param.lower():
                    append_param = _formatparam(param, value, requote)
                else:
                    append_param = _formatparam(old_param, old_value, requote)
                if not ctype:
                    ctype = append_param
                else:
                    ctype = SEMISPACE.join([ctype, append_param])
        if ctype != self.get(header):
            if replace:
                self.replace_header(header, ctype)
            else:
                del self[header]
                self[header] = ctype

    def del_param(self, param, header='content-type', requote=True):
        
        if header not in self:
            return
        new_ctype = ''
        for p, v in self.get_params(header=header, unquote=requote):
            if p.lower() != param.lower():
                if not new_ctype:
                    new_ctype = _formatparam(p, v, requote)
                else:
                    new_ctype = SEMISPACE.join([new_ctype,
                                                _formatparam(p, v, requote)])
        if new_ctype != self.get(header):
            del self[header]
            self[header] = new_ctype

    def set_type(self, type, header='Content-Type', requote=True):
        
        
        if not type.count('/') == 1:
            raise ValueError
        
        if header.lower() == 'content-type':
            del self['mime-version']
            self['MIME-Version'] = '1.0'
        if header not in self:
            self[header] = type
            return
        params = self.get_params(header=header, unquote=requote)
        del self[header]
        self[header] = type
        
        for p, v in params[1:]:
            self.set_param(p, v, header, requote)

    def get_filename(self, failobj=None):
        
        missing = object()
        filename = self.get_param('filename', missing, 'content-disposition')
        if filename is missing:
            filename = self.get_param('name', missing, 'content-type')
        if filename is missing:
            return failobj
        return utils.collapse_rfc2231_value(filename).strip()

    def get_boundary(self, failobj=None):
        
        missing = object()
        boundary = self.get_param('boundary', missing)
        if boundary is missing:
            return failobj
        
        return utils.collapse_rfc2231_value(boundary).rstrip()

    def set_boundary(self, boundary):
        
        missing = object()
        params = self._get_params_preserve(missing, 'content-type')
        if params is missing:
            
            
            raise errors.HeaderParseError('No Content-Type header found')
        newparams = []
        foundp = False
        for pk, pv in params:
            if pk.lower() == 'boundary':
                newparams.append(('boundary', '"%s"' % boundary))
                foundp = True
            else:
                newparams.append((pk, pv))
        if not foundp:
            
            
            
            newparams.append(('boundary', '"%s"' % boundary))
        
        newheaders = []
        for h, v in self._headers:
            if h.lower() == 'content-type':
                parts = []
                for k, v in newparams:
                    if v == '':
                        parts.append(k)
                    else:
                        parts.append('%s=%s' % (k, v))
                val = SEMISPACE.join(parts)
                newheaders.append(self.policy.header_store_parse(h, val))

            else:
                newheaders.append((h, v))
        self._headers = newheaders

    def get_content_charset(self, failobj=None):
        
        missing = object()
        charset = self.get_param('charset', missing)
        if charset is missing:
            return failobj
        if isinstance(charset, tuple):
            
            pcharset = charset[0] or 'us-ascii'
            try:
                
                
                
                as_bytes = charset[2].encode('raw-unicode-escape')
                charset = str(as_bytes, pcharset)
            except (LookupError, UnicodeError):
                charset = charset[2]
        
        try:
            charset.encode('us-ascii')
        except UnicodeError:
            return failobj
        
        return charset.lower()

    def get_charsets(self, failobj=None):
        
        return [part.get_content_charset(failobj) for part in self.walk()]

    def get_content_disposition(self):
        
        value = self.get('content-disposition')
        if value is None:
            return None
        c_d = _splitparam(value)[0].lower()
        return c_d

    
    from email.iterators import walk


class MIMEPart(Message):

    def __init__(self, policy=None):
        if policy is None:
            from email.policy import default
            policy = default
        super().__init__(policy)


    def as_string(self, unixfrom=False, maxheaderlen=None, policy=None):
        
        policy = self.policy if policy is None else policy
        if maxheaderlen is None:
            maxheaderlen = policy.max_line_length
        return super().as_string(unixfrom, maxheaderlen, policy)

    def __str__(self):
        return self.as_string(policy=self.policy.clone(utf8=True))

    def is_attachment(self):
        c_d = self.get('content-disposition')
        return False if c_d is None else c_d.content_disposition == 'attachment'

    def _find_body(self, part, preferencelist):
        if part.is_attachment():
            return
        maintype, subtype = part.get_content_type().split('/')
        if maintype == 'text':
            if subtype in preferencelist:
                yield (preferencelist.index(subtype), part)
            return
        if maintype != 'multipart' or not self.is_multipart():
            return
        if subtype != 'related':
            for subpart in part.iter_parts():
                yield from self._find_body(subpart, preferencelist)
            return
        if 'related' in preferencelist:
            yield (preferencelist.index('related'), part)
        candidate = None
        start = part.get_param('start')
        if start:
            for subpart in part.iter_parts():
                if subpart['content-id'] == start:
                    candidate = subpart
                    break
        if candidate is None:
            subparts = part.get_payload()
            candidate = subparts[0] if subparts else None
        if candidate is not None:
            yield from self._find_body(candidate, preferencelist)

    def get_body(self, preferencelist=('related', 'html', 'plain')):
        
        best_prio = len(preferencelist)
        body = None
        for prio, part in self._find_body(self, preferencelist):
            if prio < best_prio:
                best_prio = prio
                body = part
                if prio == 0:
                    break
        return body

    _body_types = {('text', 'plain'),
                   ('text', 'html'),
                   ('multipart', 'related'),
                   ('multipart', 'alternative')}
    def iter_attachments(self):
        
        maintype, subtype = self.get_content_type().split('/')
        if maintype != 'multipart' or subtype == 'alternative':
            return
        payload = self.get_payload()
        
        
        
        try:
            parts = payload.copy()
        except AttributeError:
            
            return

        if maintype == 'multipart' and subtype == 'related':
            
            
            
            start = self.get_param('start')
            if start:
                found = False
                attachments = []
                for part in parts:
                    if part.get('content-id') == start:
                        found = True
                    else:
                        attachments.append(part)
                if found:
                    yield from attachments
                    return
            parts.pop(0)
            yield from parts
            return
        
        
        
        seen = []   
        for part in parts:
            maintype, subtype = part.get_content_type().split('/')
            if ((maintype, subtype) in self._body_types and
                    not part.is_attachment() and subtype not in seen):
                seen.append(subtype)
                continue
            yield part

    def iter_parts(self):
        
        if self.is_multipart():
            yield from self.get_payload()

    def get_content(self, *args, content_manager=None, **kw):
        if content_manager is None:
            content_manager = self.policy.content_manager
        return content_manager.get_content(self, *args, **kw)

    def set_content(self, *args, content_manager=None, **kw):
        if content_manager is None:
            content_manager = self.policy.content_manager
        content_manager.set_content(self, *args, **kw)

    def _make_multipart(self, subtype, disallowed_subtypes, boundary):
        if self.get_content_maintype() == 'multipart':
            existing_subtype = self.get_content_subtype()
            disallowed_subtypes = disallowed_subtypes + (subtype,)
            if existing_subtype in disallowed_subtypes:
                raise ValueError("Cannot convert {} to {}".format(
                    existing_subtype, subtype))
        keep_headers = []
        part_headers = []
        for name, value in self._headers:
            if name.lower().startswith('content-'):
                part_headers.append((name, value))
            else:
                keep_headers.append((name, value))
        if part_headers:
            
            part = type(self)(policy=self.policy)
            part._headers = part_headers
            part._payload = self._payload
            self._payload = [part]
        else:
            self._payload = []
        self._headers = keep_headers
        self['Content-Type'] = 'multipart/' + subtype
        if boundary is not None:
            self.set_param('boundary', boundary)

    def make_related(self, boundary=None):
        self._make_multipart('related', ('alternative', 'mixed'), boundary)

    def make_alternative(self, boundary=None):
        self._make_multipart('alternative', ('mixed',), boundary)

    def make_mixed(self, boundary=None):
        self._make_multipart('mixed', (), boundary)

    def _add_multipart(self, _subtype, *args, _disp=None, **kw):
        if (self.get_content_maintype() != 'multipart' or
                self.get_content_subtype() != _subtype):
            getattr(self, 'make_' + _subtype)()
        part = type(self)(policy=self.policy)
        part.set_content(*args, **kw)
        if _disp and 'content-disposition' not in part:
            part['Content-Disposition'] = _disp
        self.attach(part)

    def add_related(self, *args, **kw):
        self._add_multipart('related', *args, _disp='inline', **kw)

    def add_alternative(self, *args, **kw):
        self._add_multipart('alternative', *args, **kw)

    def add_attachment(self, *args, **kw):
        self._add_multipart('mixed', *args, _disp='attachment', **kw)

    def clear(self):
        self._headers = []
        self._payload = None

    def clear_content(self):
        self._headers = [(n, v) for n, v in self._headers
                         if not n.lower().startswith('content-')]
        self._payload = None


class EmailMessage(MIMEPart):

    def set_content(self, *args, **kw):
        super().set_content(*args, **kw)
        if 'MIME-Version' not in self:
            self['MIME-Version'] = '1.0'