示例#1
0
def do_paranoid_envelope_to_validation(to_addrs):
    """Ensure the envelope_to addresses are valid.

    This is extracted from do_paranoid_email_content_validation, so that
    it can be applied to the actual envelope_to addresses, not the
    to header.  The to header and envelope_to addresses may vary
    independently, and the to header cannot break Z3.
    """
    assert (zisinstance(to_addrs, (list, tuple, set))
            and len(to_addrs) > 0), 'Invalid To: %r' % (to_addrs,)
    for addr in to_addrs:
        assert zisinstance(addr, basestring) and bool(addr), \
                'Invalid recipient: %r in %r' % (addr, to_addrs)
        assert '\n' not in addr, (
            "Address contains carriage returns: %r" % (addr,))
示例#2
0
def do_paranoid_envelope_to_validation(to_addrs):
    """Ensure the envelope_to addresses are valid.

    This is extracted from do_paranoid_email_content_validation, so that
    it can be applied to the actual envelope_to addresses, not the
    to header.  The to header and envelope_to addresses may vary
    independently, and the to header cannot break Z3.
    """
    assert (zisinstance(to_addrs, (list, tuple, set))
            and len(to_addrs) > 0), 'Invalid To: %r' % (to_addrs, )
    for addr in to_addrs:
        assert zisinstance(addr, basestring) and bool(addr), \
                'Invalid recipient: %r in %r' % (addr, to_addrs)
        assert '\n' not in addr, ("Address contains carriage returns: %r" %
                                  (addr, ))
示例#3
0
def make_package_branches(factory, series, sourcepackagename, branch_count,
                          official_count=0, owner=None, registrant=None):
    """Make some package branches.

    Make `branch_count` branches, and make `official_count` of those
    official branches.
    """
    if zisinstance(sourcepackagename, basestring):
        sourcepackagename = factory.getOrMakeSourcePackageName(
            sourcepackagename)
    # Make the branches created in the past in order.
    time_gen = time_counter(delta=timedelta(days=-1))
    branch_names = consistent_branch_names()
    branches = [
        factory.makePackageBranch(
            distroseries=series,
            sourcepackagename=sourcepackagename,
            date_created=time_gen.next(),
            name=branch_names.next(), owner=owner, registrant=registrant)
        for i in range(branch_count)]

    official = []
    # Sort the pocket items so RELEASE is last, and thus first popped.
    pockets = sorted(PackagePublishingPocket.items, reverse=True)
    # Since there can be only one link per pocket, max out the number of
    # official branches at the pocket count.
    for i in range(min(official_count, len(pockets))):
        branch = branches.pop()
        pocket = pockets.pop()
        SeriesSourcePackageBranchSet.new(
            series, pocket, sourcepackagename, branch, branch.owner)
        official.append(branch)

    return series, branches, official
示例#4
0
def do_paranoid_email_content_validation(from_addr, to_addrs, subject, body):
    """Validate various bits of the email.

    Extremely paranoid parameter checking is required to ensure we
    raise an exception rather than stick garbage in the mail
    queue. Currently, the Z3 mailer is too forgiving and accepts badly
    formatted emails which the delivery mechanism then can't send.

    An AssertionError will be raised if one of the parameters is
    invalid.
    """
    # XXX StuartBishop 2005-03-19:
    # These checks need to be migrated upstream if this bug
    # still exists in modern Z3.
    assert zisinstance(from_addr, basestring), 'Invalid From: %r' % from_addr
    assert zisinstance(subject, basestring), 'Invalid Subject: %r' % subject
    assert zisinstance(body, basestring), 'Invalid body: %r' % body
示例#5
0
def do_paranoid_email_content_validation(from_addr, to_addrs, subject, body):
    """Validate various bits of the email.

    Extremely paranoid parameter checking is required to ensure we
    raise an exception rather than stick garbage in the mail
    queue. Currently, the Z3 mailer is too forgiving and accepts badly
    formatted emails which the delivery mechanism then can't send.

    An AssertionError will be raised if one of the parameters is
    invalid.
    """
    # XXX StuartBishop 2005-03-19:
    # These checks need to be migrated upstream if this bug
    # still exists in modern Z3.
    assert zisinstance(from_addr, basestring), 'Invalid From: %r' % from_addr
    assert zisinstance(subject, basestring), 'Invalid Subject: %r' % subject
    assert zisinstance(body, basestring), 'Invalid body: %r' % body
示例#6
0
 def __contains__(self, obj):
     if zisinstance(obj, Storm):
         found_obj = IStore(self._table).find(
             self._table,
             self._table.name == obj.name, *self._clauses).one()
         return found_obj is not None and found_obj == obj
     else:
         found_obj = IStore(self._table).find(
             self._table, self._table.name == obj, *self._clauses).one()
         return found_obj is not None
示例#7
0
    def restoreRequestFromSession(self):
        """Get the OpenIDRequest from our session."""
        session = self.getSession()
        cache = get_property_cache(self)
        try:
            cache.openid_parameters = session[OPENID_REQUEST_SESSION_KEY]
        except KeyError:
            raise UnexpectedFormData("No OpenID request in session")

        # Decode the request parameters and create the request object.
        self.openid_request = self.openid_server.decodeRequest(
            self.openid_parameters)
        assert zisinstance(self.openid_request, CheckIDRequest), (
            'Invalid OpenIDRequest in session')
示例#8
0
 def __init__(self, from_addr, to_addrs, subject, body, headers=None,
              envelope_to=None, bulk=True):
     self.from_addr = from_addr
     if zisinstance(to_addrs, basestring):
         to_addrs = [to_addrs]
     self.to_addrs = to_addrs
     self.envelope_to = envelope_to
     self.subject = subject
     self.body = body
     if headers is None:
         headers = {}
     self.headers = headers
     self.bulk = bulk
     self.attachments = []
示例#9
0
    def restoreRequestFromSession(self):
        """Get the OpenIDRequest from our session."""
        session = self.getSession()
        cache = get_property_cache(self)
        try:
            cache.openid_parameters = session[OPENID_REQUEST_SESSION_KEY]
        except KeyError:
            raise UnexpectedFormData("No OpenID request in session")

        # Decode the request parameters and create the request object.
        self.openid_request = self.openid_server.decodeRequest(
            self.openid_parameters)
        assert zisinstance(
            self.openid_request,
            CheckIDRequest), ('Invalid OpenIDRequest in session')
示例#10
0
    def makeMessage(self):
        # It's the caller's responsibility to either encode the address fields
        # to ASCII strings or pass in Unicode strings.

        # Using the maxlinelen for the Headers as we have paranoid checks to
        # make sure that we have no carriage returns in the to or from email
        # addresses.  We use nice email addresses like 'Launchpad Community
        # Help Rotation team <*****@*****.**>' that
        # get broken over two lines in the header.  RFC 5322 specified that
        # the lines MUST be no more than 998, so we use that as our maximum.
        from_addr = Header(self.from_addr, maxlinelen=998).encode()
        to_addrs = [
            Header(address, maxlinelen=998).encode()
            for address in list(self.to_addrs)
        ]

        for address in [from_addr] + to_addrs:
            if not isinstance(address, str) or not is_ascii_only(address):
                raise AssertionError('Expected an ASCII str object, got: %r' %
                                     address)

        do_paranoid_email_content_validation(from_addr=from_addr,
                                             to_addrs=to_addrs,
                                             subject=self.subject,
                                             body=self.body)
        if len(self.attachments) == 0:
            msg = MIMEText(self.body.encode('utf-8'), 'plain', 'utf-8')
        else:
            msg = MIMEMultipart()
            body_part = MIMEText(self.body.encode('utf-8'), 'plain', 'utf-8')
            msg.attach(body_part)
            for attachment in self.attachments:
                msg.attach(attachment)

        # The header_body_values may be a list or tuple of values, so we will
        # add a header once for each value provided for that header.
        # (X-Launchpad-Bug, for example, may often be set more than once for a
        # bugmail.)
        for header, header_body_values in self.headers.items():
            if not zisinstance(header_body_values, (list, tuple)):
                header_body_values = [header_body_values]
            for header_body_value in header_body_values:
                msg[header] = header_body_value
        msg['To'] = ','.join(to_addrs)
        msg['From'] = from_addr
        msg['Subject'] = self.subject
        return msg
示例#11
0
 def __contains__(self, obj):
     # Sometimes this method is called with an SQLBase instance, but
     # z3 form machinery sends through integer ids. This might be due
     # to a bug somewhere.
     if zisinstance(obj, SQLBase):
         clause = self._table.q.id == obj.id
         if self._filter:
             # XXX kiko 2007-01-16: this code is untested.
             clause = AND(clause, self._filter)
         found_obj = self._table.selectOne(clause)
         return found_obj is not None and found_obj == obj
     else:
         clause = self._table.q.id == int(obj)
         if self._filter:
             # XXX kiko 2007-01-16: this code is untested.
             clause = AND(clause, self._filter)
         found_obj = self._table.selectOne(clause)
         return found_obj is not None
示例#12
0
 def __contains__(self, obj):
     # Sometimes this method is called with an SQLBase instance, but
     # z3 form machinery sends through integer ids. This might be due
     # to a bug somewhere.
     if zisinstance(obj, SQLBase):
         clause = self._table.q.id == obj.id
         if self._filter:
             # XXX kiko 2007-01-16: this code is untested.
             clause = AND(clause, self._filter)
         found_obj = self._table.selectOne(clause)
         return found_obj is not None and found_obj == obj
     else:
         clause = self._table.q.id == int(obj)
         if self._filter:
             # XXX kiko 2007-01-16: this code is untested.
             clause = AND(clause, self._filter)
         found_obj = self._table.selectOne(clause)
         return found_obj is not None
示例#13
0
 def __contains__(self, obj):
     # Sometimes this method is called with a Storm instance, but z3 form
     # machinery sends through integer ids.  This might be due to a bug
     # somewhere.
     if zisinstance(obj, Storm):
         clauses = [self._table.id == obj.id]
         if self._clauses:
             # XXX kiko 2007-01-16: this code is untested.
             clauses.extend(self._clauses)
         found_obj = IStore(self._table).find(self._table, *clauses).one()
         return found_obj is not None and found_obj == obj
     else:
         clauses = [self._table.id == int(obj)]
         if self._clauses:
             # XXX kiko 2007-01-16: this code is untested.
             clauses.extend(self._clauses)
         found_obj = IStore(self._table).find(self._table, *clauses).one()
         return found_obj is not None
示例#14
0
    def makeMessage(self):
        # It's the caller's responsibility to either encode the address fields
        # to ASCII strings or pass in Unicode strings.

        # Using the maxlinelen for the Headers as we have paranoid checks to
        # make sure that we have no carriage returns in the to or from email
        # addresses.  We use nice email addresses like 'Launchpad Community
        # Help Rotation team <*****@*****.**>' that
        # get broken over two lines in the header.  RFC 5322 specified that
        # the lines MUST be no more than 998, so we use that as our maximum.
        from_addr = Header(self.from_addr, maxlinelen=998).encode()
        to_addrs = [Header(address, maxlinelen=998).encode()
            for address in list(self.to_addrs)]

        for address in [from_addr] + to_addrs:
            if not isinstance(address, str) or not is_ascii_only(address):
                raise AssertionError(
                    'Expected an ASCII str object, got: %r' % address)

        do_paranoid_email_content_validation(
            from_addr=from_addr, to_addrs=to_addrs,
            subject=self.subject, body=self.body)
        if len(self.attachments) == 0:
            msg = MIMEText(self.body.encode('utf-8'), 'plain', 'utf-8')
        else:
            msg = MIMEMultipart()
            body_part = MIMEText(self.body.encode('utf-8'), 'plain', 'utf-8')
            msg.attach(body_part)
            for attachment in self.attachments:
                msg.attach(attachment)

        # The header_body_values may be a list or tuple of values, so we will
        # add a header once for each value provided for that header.
        # (X-Launchpad-Bug, for example, may often be set more than once for a
        # bugmail.)
        for header, header_body_values in self.headers.items():
            if not zisinstance(header_body_values, (list, tuple)):
                header_body_values = [header_body_values]
            for header_body_value in header_body_values:
                msg[header] = header_body_value
        msg['To'] = ','.join(to_addrs)
        msg['From'] = from_addr
        msg['Subject'] = self.subject
        return msg
示例#15
0
 def __init__(self,
              from_addr,
              to_addrs,
              subject,
              body,
              headers=None,
              envelope_to=None,
              bulk=True):
     self.from_addr = from_addr
     if zisinstance(to_addrs, basestring):
         to_addrs = [to_addrs]
     self.to_addrs = to_addrs
     self.envelope_to = envelope_to
     self.subject = subject
     self.body = body
     if headers is None:
         headers = {}
     self.headers = headers
     self.bulk = bulk
     self.attachments = []
示例#16
0
    def getTerm(self, value):
        # Short circuit. There is probably a design problem here since
        # we sometimes get the id and sometimes an SQLBase instance.
        if zisinstance(value, SQLBase):
            return self.toTerm(value)

        try:
            value = int(value)
        except ValueError:
            raise LookupError(value)

        clause = self._table.q.id == value
        if self._filter:
            clause = AND(clause, self._filter)
        try:
            obj = self._table.selectOne(clause)
        except ValueError:
            raise LookupError(value)

        if obj is None:
            raise LookupError(value)

        return self.toTerm(obj)
示例#17
0
    def getTerm(self, value):
        # Short circuit.  There is probably a design problem here since we
        # sometimes get the id and sometimes a Storm instance.
        if zisinstance(value, Storm):
            return self.toTerm(value)

        try:
            value = int(value)
        except ValueError:
            raise LookupError(value)

        clauses = [self._table.id == value]
        if self._clauses:
            clauses.extend(self._clauses)
        try:
            obj = IStore(self._table).find(self._table, *clauses).one()
        except ValueError:
            raise LookupError(value)

        if obj is None:
            raise LookupError(value)

        return self.toTerm(obj)
示例#18
0
    def getTerm(self, value):
        # Short circuit. There is probably a design problem here since
        # we sometimes get the id and sometimes an SQLBase instance.
        if zisinstance(value, SQLBase):
            return self.toTerm(value)

        try:
            value = int(value)
        except ValueError:
            raise LookupError(value)

        clause = self._table.q.id == value
        if self._filter:
            clause = AND(clause, self._filter)
        try:
            obj = self._table.selectOne(clause)
        except ValueError:
            raise LookupError(value)

        if obj is None:
            raise LookupError(value)

        return self.toTerm(obj)
示例#19
0
def make_package_branches(
    factory, series, sourcepackagename, branch_count, official_count=0, owner=None, registrant=None
):
    """Make some package branches.

    Make `branch_count` branches, and make `official_count` of those
    official branches.
    """
    if zisinstance(sourcepackagename, basestring):
        sourcepackagename = factory.getOrMakeSourcePackageName(sourcepackagename)
    # Make the branches created in the past in order.
    time_gen = time_counter(delta=timedelta(days=-1))
    branch_names = consistent_branch_names()
    branches = [
        factory.makePackageBranch(
            distroseries=series,
            sourcepackagename=sourcepackagename,
            date_created=time_gen.next(),
            name=branch_names.next(),
            owner=owner,
            registrant=registrant,
        )
        for i in range(branch_count)
    ]

    official = []
    # Sort the pocket items so RELEASE is last, and thus first popped.
    pockets = sorted(PackagePublishingPocket.items, reverse=True)
    # Since there can be only one link per pocket, max out the number of
    # official branches at the pocket count.
    for i in range(min(official_count, len(pockets))):
        branch = branches.pop()
        pocket = pockets.pop()
        SeriesSourcePackageBranchSet.new(series, pocket, sourcepackagename, branch, branch.owner)
        official.append(branch)

    return series, branches, official
示例#20
0
    def fromEmail(self, email_message, owner=None, filealias=None,
                  parsed_message=None, create_missing_persons=False,
                  fallback_parent=None, date_created=None, restricted=False):
        """See IMessageSet.fromEmail."""
        # It does not make sense to handle Unicode strings, as email
        # messages may contain chunks encoded in differing character sets.
        # Passing Unicode in here indicates a bug.
        if not zisinstance(email_message, str):
            raise TypeError(
                'email_message must be a normal string.  Got: %r'
                % email_message)

        # Parse the raw message into an email.Message.Message instance,
        # if we haven't been given one already.
        if parsed_message is None:
            parsed_message = email.message_from_string(email_message)

        # We could easily generate a default, but a missing message-id
        # almost certainly means a developer is using this method when
        # they shouldn't (by creating emails by hand and passing them here),
        # which is broken because they will almost certainly have Unicode
        # errors.
        rfc822msgid = parsed_message.get('message-id')
        if not rfc822msgid:
            raise InvalidEmailMessage('Missing Message-Id')

        # Over-long messages are checked for at the handle_on_message level.

        # If it's a restricted mail (IE: for a private bug), or it hasn't been
        # uploaded, do so now.
        from lp.services.mail.helpers import save_mail_to_librarian
        if restricted or filealias is None:
            raw_email_message = save_mail_to_librarian(
                email_message, restricted=restricted)
        else:
            raw_email_message = filealias

        # Find the message subject
        subject = self._decode_header(parsed_message.get('subject', ''))
        subject = subject.strip()

        if owner is None:
            # Try and determine the owner. We raise a NotFoundError
            # if the sender does not exist, unless we were asked to
            # create_missing_persons.
            person_set = getUtility(IPersonSet)
            from_hdr = self._decode_header(
                parsed_message.get('from', '')).strip()
            replyto_hdr = self._decode_header(
                parsed_message.get('reply-to', '')).strip()
            from_addrs = [from_hdr, replyto_hdr]
            from_addrs = [parseaddr(addr) for addr in from_addrs if addr]
            if len(from_addrs) == 0:
                raise InvalidEmailMessage('No From: or Reply-To: header')
            for from_addr in from_addrs:
                owner = person_set.getByEmail(from_addr[1].lower().strip())
                if owner is not None:
                    break
            if owner is None:
                if not create_missing_persons:
                    raise UnknownSender(from_addrs[0][1])
                # autocreate a person
                sendername = ensure_unicode(from_addrs[0][0].strip())
                senderemail = from_addrs[0][1].lower().strip()
                # XXX: Guilherme Salgado 2006-08-31 bug=62344:
                # It's hard to define what rationale to use here, and to
                # make things worst, it's almost impossible to provide a
                # meaningful comment having only the email message.
                owner = person_set.ensurePerson(
                    senderemail, sendername,
                    PersonCreationRationale.FROMEMAILMESSAGE)
                if owner is None:
                    raise UnknownSender(senderemail)

        # Get the parent of the message, if available in the db. We'll
        # go through all the message's parents until we find one that's
        # in the db.
        parent = None
        for parent_msgid in reversed(get_parent_msgids(parsed_message)):
            try:
                # we assume it's the first matching message
                parent = self.get(parent_msgid)[0]
                break
            except NotFoundError:
                pass
        else:
            parent = fallback_parent

        # Figure out the date of the message.
        if date_created is not None:
            datecreated = date_created
        else:
            datecreated = utcdatetime_from_field(parsed_message['date'])

        # Make sure we don't create an email with a datecreated in the
        # distant past or future.
        now = datetime.now(pytz.timezone('UTC'))
        thedistantpast = datetime(1990, 1, 1, tzinfo=pytz.timezone('UTC'))
        if datecreated < thedistantpast or datecreated > now:
            datecreated = UTC_NOW

        message = Message(subject=subject, owner=owner,
            rfc822msgid=rfc822msgid, parent=parent,
            raw=raw_email_message, datecreated=datecreated)

        sequence = 1

        # Don't store the preamble or epilogue -- they are only there
        # to give hints to non-MIME aware clients
        #
        # Determine the encoding to use for non-multipart messages, and the
        # preamble and epilogue of multipart messages. We default to
        # iso-8859-1 as it seems fairly harmless to cope with old, broken
        # mail clients (The RFCs state US-ASCII as the default character
        # set).
        # default_charset = (parsed_message.get_content_charset() or
        #                    'iso-8859-1')
        #
        # XXX: kiko 2005-09-23: Is default_charset only useful here?
        #
        # if getattr(parsed_message, 'preamble', None):
        #     # We strip a leading and trailing newline - the email parser
        #     # seems to arbitrarily add them :-/
        #     preamble = parsed_message.preamble.decode(
        #             default_charset, 'replace')
        #     if preamble.strip():
        #         if preamble[0] == '\n':
        #             preamble = preamble[1:]
        #         if preamble[-1] == '\n':
        #             preamble = preamble[:-1]
        #         MessageChunk(
        #             message=message, sequence=sequence, content=preamble
        #             )
        #         sequence += 1

        for part in parsed_message.walk():
            mime_type = part.get_content_type()

            # Skip the multipart section that walk gives us. This part
            # is the entire message.
            if part.is_multipart():
                continue

            # Decode the content of this part.
            content = part.get_payload(decode=True)

            # Store the part as a MessageChunk
            #
            # We want only the content type text/plain as "main content".
            # Exceptions to this rule:
            # - if the content disposition header explicitly says that
            #   this part is an attachment, text/plain content is stored
            #   as a blob,
            # - if the content-disposition header provides a filename,
            #   text/plain content is stored as a blob.
            content_disposition = part.get('Content-disposition', '').lower()
            no_attachment = not content_disposition.startswith('attachment')
            if (mime_type == 'text/plain' and no_attachment
                and part.get_filename() is None):

                # Get the charset for the message part. If one isn't
                # specified, default to latin-1 to prevent
                # UnicodeDecodeErrors.
                charset = part.get_content_charset()
                if charset is None or str(charset).lower() == 'x-unknown':
                    charset = 'latin-1'
                content = self.decode(content, charset)

                if content.strip():
                    MessageChunk(
                        message=message, sequence=sequence, content=content)
                    sequence += 1
            else:
                filename = part.get_filename() or 'unnamed'
                # Strip off any path information.
                filename = os.path.basename(filename)
                # Note we use the Content-Type header instead of
                # part.get_content_type() here to ensure we keep
                # parameters as sent. If Content-Type is None we default
                # to application/octet-stream.
                if part['content-type'] is None:
                    content_type = 'application/octet-stream'
                else:
                    content_type = part['content-type']

                if len(content) > 0:
                    blob = getUtility(ILibraryFileAliasSet).create(
                        name=filename, size=len(content),
                        file=cStringIO(content), contentType=content_type,
                        restricted=restricted)
                    MessageChunk(message=message, sequence=sequence, blob=blob)
                    sequence += 1

        # Don't store the epilogue
        # if getattr(parsed_message, 'epilogue', None):
        #     epilogue = parsed_message.epilogue.decode(
        #             default_charset, 'replace')
        #     if epilogue.strip():
        #         if epilogue[0] == '\n':
        #             epilogue = epilogue[1:]
        #         if epilogue[-1] == '\n':
        #             epilogue = epilogue[:-1]
        #         MessageChunk(
        #             message=message, sequence=sequence, content=epilogue
        #             )

        # XXX 2008-05-27 jamesh:
        # Ensure that BugMessages get flushed in same order as they
        # are created.
        Store.of(message).flush()
        return message
示例#21
0
    def fromEmail(self,
                  email_message,
                  owner=None,
                  filealias=None,
                  parsed_message=None,
                  create_missing_persons=False,
                  fallback_parent=None,
                  date_created=None,
                  restricted=False):
        """See IMessageSet.fromEmail."""
        # It does not make sense to handle Unicode strings, as email
        # messages may contain chunks encoded in differing character sets.
        # Passing Unicode in here indicates a bug.
        if not zisinstance(email_message, str):
            raise TypeError('email_message must be a normal string.  Got: %r' %
                            email_message)

        # Parse the raw message into an email.message.Message instance,
        # if we haven't been given one already.
        if parsed_message is None:
            parsed_message = email.message_from_string(email_message)

        # We could easily generate a default, but a missing message-id
        # almost certainly means a developer is using this method when
        # they shouldn't (by creating emails by hand and passing them here),
        # which is broken because they will almost certainly have Unicode
        # errors.
        rfc822msgid = parsed_message.get('message-id')
        if not rfc822msgid:
            raise InvalidEmailMessage('Missing Message-Id')

        # Over-long messages are checked for at the handle_on_message level.

        # If it's a restricted mail (IE: for a private bug), or it hasn't been
        # uploaded, do so now.
        from lp.services.mail.helpers import save_mail_to_librarian
        if restricted or filealias is None:
            raw_email_message = save_mail_to_librarian(email_message,
                                                       restricted=restricted)
        else:
            raw_email_message = filealias

        # Find the message subject
        subject = self._decode_header(parsed_message.get('subject', ''))
        subject = subject.strip()

        if owner is None:
            # Try and determine the owner. We raise a NotFoundError
            # if the sender does not exist, unless we were asked to
            # create_missing_persons.
            person_set = getUtility(IPersonSet)
            from_hdr = self._decode_header(parsed_message.get('from',
                                                              '')).strip()
            replyto_hdr = self._decode_header(
                parsed_message.get('reply-to', '')).strip()
            from_addrs = [from_hdr, replyto_hdr]
            from_addrs = [parseaddr(addr) for addr in from_addrs if addr]
            if len(from_addrs) == 0:
                raise InvalidEmailMessage('No From: or Reply-To: header')
            for from_addr in from_addrs:
                owner = person_set.getByEmail(from_addr[1].lower().strip())
                if owner is not None:
                    break
            if owner is None:
                if not create_missing_persons:
                    raise UnknownSender(from_addrs[0][1])
                # autocreate a person
                sendername = ensure_unicode(from_addrs[0][0].strip())
                senderemail = from_addrs[0][1].lower().strip()
                # XXX: Guilherme Salgado 2006-08-31 bug=62344:
                # It's hard to define what rationale to use here, and to
                # make things worst, it's almost impossible to provide a
                # meaningful comment having only the email message.
                owner = person_set.ensurePerson(
                    senderemail, sendername,
                    PersonCreationRationale.FROMEMAILMESSAGE)
                if owner is None:
                    raise UnknownSender(senderemail)

        # Get the parent of the message, if available in the db. We'll
        # go through all the message's parents until we find one that's
        # in the db.
        parent = None
        for parent_msgid in reversed(get_parent_msgids(parsed_message)):
            try:
                # we assume it's the first matching message
                parent = self.get(parent_msgid)[0]
                break
            except NotFoundError:
                pass
        else:
            parent = fallback_parent

        # Figure out the date of the message.
        if date_created is not None:
            datecreated = date_created
        else:
            datecreated = utcdatetime_from_field(parsed_message['date'])

        # Make sure we don't create an email with a datecreated in the
        # distant past or future.
        now = datetime.now(pytz.timezone('UTC'))
        thedistantpast = datetime(1990, 1, 1, tzinfo=pytz.timezone('UTC'))
        if datecreated < thedistantpast or datecreated > now:
            datecreated = UTC_NOW

        message = Message(subject=subject,
                          owner=owner,
                          rfc822msgid=rfc822msgid,
                          parent=parent,
                          raw=raw_email_message,
                          datecreated=datecreated)

        sequence = 1

        # Don't store the preamble or epilogue -- they are only there
        # to give hints to non-MIME aware clients
        #
        # Determine the encoding to use for non-multipart messages, and the
        # preamble and epilogue of multipart messages. We default to
        # iso-8859-1 as it seems fairly harmless to cope with old, broken
        # mail clients (The RFCs state US-ASCII as the default character
        # set).
        # default_charset = (parsed_message.get_content_charset() or
        #                    'iso-8859-1')
        #
        # XXX: kiko 2005-09-23: Is default_charset only useful here?
        #
        # if getattr(parsed_message, 'preamble', None):
        #     # We strip a leading and trailing newline - the email parser
        #     # seems to arbitrarily add them :-/
        #     preamble = parsed_message.preamble.decode(
        #             default_charset, 'replace')
        #     if preamble.strip():
        #         if preamble[0] == '\n':
        #             preamble = preamble[1:]
        #         if preamble[-1] == '\n':
        #             preamble = preamble[:-1]
        #         MessageChunk(
        #             message=message, sequence=sequence, content=preamble
        #             )
        #         sequence += 1

        for part in parsed_message.walk():
            mime_type = part.get_content_type()

            # Skip the multipart section that walk gives us. This part
            # is the entire message.
            if part.is_multipart():
                continue

            # Decode the content of this part.
            content = part.get_payload(decode=True)

            # Store the part as a MessageChunk
            #
            # We want only the content type text/plain as "main content".
            # Exceptions to this rule:
            # - if the content disposition header explicitly says that
            #   this part is an attachment, text/plain content is stored
            #   as a blob,
            # - if the content-disposition header provides a filename,
            #   text/plain content is stored as a blob.
            content_disposition = part.get('Content-disposition', '').lower()
            no_attachment = not content_disposition.startswith('attachment')
            if (mime_type == 'text/plain' and no_attachment
                    and part.get_filename() is None):

                # Get the charset for the message part. If one isn't
                # specified, default to latin-1 to prevent
                # UnicodeDecodeErrors.
                charset = part.get_content_charset()
                if charset is None or str(charset).lower() == 'x-unknown':
                    charset = 'latin-1'
                content = self.decode(content, charset)

                if content.strip():
                    MessageChunk(message=message,
                                 sequence=sequence,
                                 content=content)
                    sequence += 1
            else:
                filename = part.get_filename() or 'unnamed'
                # Strip off any path information.
                filename = os.path.basename(filename)
                # Note we use the Content-Type header instead of
                # part.get_content_type() here to ensure we keep
                # parameters as sent. If Content-Type is None we default
                # to application/octet-stream.
                if part['content-type'] is None:
                    content_type = 'application/octet-stream'
                else:
                    content_type = part['content-type']

                if len(content) > 0:
                    blob = getUtility(ILibraryFileAliasSet).create(
                        name=filename,
                        size=len(content),
                        file=cStringIO(content),
                        contentType=content_type,
                        restricted=restricted)
                    MessageChunk(message=message, sequence=sequence, blob=blob)
                    sequence += 1

        # Don't store the epilogue
        # if getattr(parsed_message, 'epilogue', None):
        #     epilogue = parsed_message.epilogue.decode(
        #             default_charset, 'replace')
        #     if epilogue.strip():
        #         if epilogue[0] == '\n':
        #             epilogue = epilogue[1:]
        #         if epilogue[-1] == '\n':
        #             epilogue = epilogue[:-1]
        #         MessageChunk(
        #             message=message, sequence=sequence, content=epilogue
        #             )

        # XXX 2008-05-27 jamesh:
        # Ensure that BugMessages get flushed in same order as they
        # are created.
        Store.of(message).flush()
        return message