def _get_detached_message_for_person(self, sender):
        # Return a signed message that contains a detached signature.
        body = dedent("""\
            This is a multi-line body.

            Sincerely,
            Your friendly tester.""")
        to = self.factory.getUniqueEmailAddress()

        msg = MIMEMultipart()
        msg['Message-Id'] = make_msgid('launchpad')
        msg['Date'] = formatdate()
        msg['To'] = to
        msg['From'] = sender.preferredemail.email
        msg['Subject'] = 'Sample'

        body_text = MIMEText(body)
        msg.attach(body_text)
        # A detached signature is calculated on the entire string content of
        # the body message part.
        key = import_secret_test_key()
        gpghandler = getUtility(IGPGHandler)
        signature = gpghandler.signContent(
            canonicalise_line_endings(body_text.as_string()), key, 'test',
            gpgme.SIG_MODE_DETACH)
        gpghandler.resetLocalState()

        attachment = Message()
        attachment.set_payload(signature)
        attachment['Content-Type'] = 'application/pgp-signature'
        msg.attach(attachment)
        self.assertTrue(msg.is_multipart())
        return signed_message_from_string(msg.as_string())
    def test_client_script_user_data(self):
        """
        Verify user-data generates a user-data file.
        """
        from email.mime.multipart import MIMEMultipart

        try:
            output_file = tempfile.mktemp()
            sys.argv = [
                '', 'user-data', '-e', 'https://example.com', '-o', output_file
            ]
            client_script.main()
            with open(output_file, 'r') as f:
                m = MIMEMultipart(f.read())
                self.assertTrue(m.is_multipart())
            filename_count = 0
            for param in m.get_params():
                if param[0] == 'filename':
                    filename_count += 1

            # We should have 3 filenames
            self.assertEquals(3, filename_count)
        finally:
            os.unlink(output_file)
    def test_client_script_user_data(self):
        """
        Verify user-data generates a user-data file.
        """
        from email.mime.multipart import MIMEMultipart

        try:
            output_file = tempfile.mktemp()
            sys.argv = [
                '', 'user-data',
                '-e', 'https://example.com', '-o', output_file]
            client_script.main()
            with open(output_file, 'r') as f:
                m = MIMEMultipart(f.read())
                self.assertTrue(m.is_multipart())
            filename_count = 0
            for param in m.get_params():
                if param[0] == 'filename':
                    filename_count += 1

            # We should have 3 filenames
            self.assertEquals(3, filename_count)
        finally:
            os.unlink(output_file)
Esempio n. 4
0
class Mdn(object):
    """Class for handling AS2 MDNs. Includes functions for both
    parsing and building them.
    """

    def __init__(self, mdn_mode=None, digest_alg=None, mdn_url=None):
        self.message_id = None
        self.orig_message_id = None
        self.payload = None
        self.mdn_mode = mdn_mode
        self.digest_alg = digest_alg
        self.mdn_url = mdn_url

    @property
    def content(self):
        """Function returns the body of the mdn message as a byte string"""

        if self.payload:
            message_bytes = mime_to_bytes(
                self.payload, 0).replace(b'\n', b'\r\n')
            boundary = b'--' + self.payload.get_boundary().encode('utf-8')
            temp = message_bytes.split(boundary)
            temp.pop(0)
            return boundary + boundary.join(temp)
        else:
            return ''

    @property
    def headers(self):
        if self.payload:
            return dict(self.payload.items())
        else:
            return {}

    @property
    def headers_str(self):
        message_header = ''
        if self.payload:
            for k, v in self.headers.items():
                message_header += '{}: {}\r\n'.format(k, v)
        return message_header.encode('utf-8')

    def build(self, message, status, detailed_status=None):
        """Function builds and signs an AS2 MDN message.

        :param message: The received AS2 message for which this is an MDN.

        :param status: The status of processing of the received AS2 message.

        :param detailed_status:
            The optional detailed status of processing of the received AS2
            message. Used to give additional error info (default "None")

        """

        # Generate message id using UUID 1 as it uses both hostname and time
        self.message_id = email_utils.make_msgid().lstrip('<').rstrip('>')
        self.orig_message_id = message.message_id

        # Set up the message headers
        mdn_headers = {
            'AS2-Version': AS2_VERSION,
            'ediint-features': EDIINT_FEATURES,
            'Message-ID': '<{}>'.format(self.message_id),
            'AS2-From': quote_as2name(message.headers.get('as2-to')),
            'AS2-To': quote_as2name(message.headers.get('as2-from')),
            'Date': email_utils.formatdate(localtime=True),
            'user-agent': 'pyAS2 Open Source AS2 Software'
        }

        # Set the confirmation text message here
        confirmation_text = MDN_CONFIRM_TEXT

        # overwrite with organization specific message
        if message.receiver and message.receiver.mdn_confirm_text:
            confirmation_text = message.receiver.mdn_confirm_text

        # overwrite with partner specific message
        if message.sender and message.sender.mdn_confirm_text:
            confirmation_text = message.sender.mdn_confirm_text

        if status != 'processed':
            confirmation_text = MDN_FAILED_TEXT

        self.payload = MIMEMultipart(
            'report', report_type='disposition-notification')

        # Create and attach the MDN Text Message
        mdn_text = email_message.Message()
        mdn_text.set_payload('%s\n' % confirmation_text)
        mdn_text.set_type('text/plain')
        del mdn_text['MIME-Version']
        encoders.encode_7or8bit(mdn_text)
        self.payload.attach(mdn_text)

        # Create and attache the MDN Report Message
        mdn_base = email_message.Message()
        mdn_base.set_type('message/disposition-notification')
        mdn_report = 'Reporting-UA: pyAS2 Open Source AS2 Software\n'
        mdn_report += 'Original-Recipient: rfc822; {}\n'.format(
            message.headers.get('as2-to'))
        mdn_report += 'Final-Recipient: rfc822; {}\n'.format(
            message.headers.get('as2-to'))
        mdn_report += 'Original-Message-ID: <{}>\n'.format(message.message_id)
        mdn_report += 'Disposition: automatic-action/' \
                      'MDN-sent-automatically; {}'.format(status)
        if detailed_status:
            mdn_report += ': {}'.format(detailed_status)
        mdn_report += '\n'
        if message.mic:
            mdn_report += 'Received-content-MIC: {}, {}\n'.format(
                message.mic.decode(), message.digest_alg)
        mdn_base.set_payload(mdn_report)
        del mdn_base['MIME-Version']
        encoders.encode_7or8bit(mdn_base)
        self.payload.attach(mdn_base)

        # logger.debug('MDN for message %s created:\n%s' % (
        #     message.message_id, mdn_base.as_string()))

        # Sign the MDN if it is requested by the sender
        if message.headers.get('disposition-notification-options') and \
                message.receiver and message.receiver.sign_key:
            self.digest_alg = \
                message.headers['disposition-notification-options'].split(
                    ';')[-1].split(',')[-1].strip().replace('-', '')
            signed_mdn = MIMEMultipart(
                'signed', protocol="application/pkcs7-signature")
            del signed_mdn['MIME-Version']
            signed_mdn.attach(self.payload)

            # Create the signature mime message
            signature = email_message.Message()
            signature.set_type('application/pkcs7-signature')
            signature.set_param('name', 'smime.p7s')
            signature.set_param('smime-type', 'signed-data')
            signature.add_header(
                'Content-Disposition', 'attachment', filename='smime.p7s')
            del signature['MIME-Version']
            signature.set_payload(sign_message(
                canonicalize(self.payload),
                self.digest_alg,
                message.receiver.sign_key
            ))
            encoders.encode_base64(signature)
            # logger.debug(
            #     'Signature for MDN created:\n%s' % signature.as_string())
            signed_mdn.set_param('micalg', self.digest_alg)
            signed_mdn.attach(signature)

            self.payload = signed_mdn

        # Update the headers of the final payload and set message boundary
        for k, v in mdn_headers.items():
            if self.payload.get(k):
                self.payload.replace_header(k, v)
            else:
                self.payload.add_header(k, v)
        if self.payload.is_multipart():
            self.payload.set_boundary(make_mime_boundary())

    def parse(self, raw_content, find_message_cb):
        """Function parses the RAW AS2 MDN, verifies it and extracts the
        processing status of the orginal AS2 message.

        :param raw_content:
            A byte string of the received HTTP headers followed by the body.

        :param find_message_cb:
            A callback the must returns the original Message Object. The
            original message-id and original recipient AS2 ID are passed
            as arguments to it.

        :returns:
            A two element tuple containing (status, detailed_status). The
            status is a string indicating the status of the transaction. The
            optional detailed_status gives additional information about the
            processing status.
        """

        status, detailed_status = None, None
        self.payload = parse_mime(raw_content)
        self.orig_message_id, orig_recipient = self.detect_mdn()

        # Call the find message callback which should return a Message instance
        orig_message = find_message_cb(self.orig_message_id, orig_recipient)

        # Extract the headers and save it
        mdn_headers = {}
        for k, v in self.payload.items():
            k = k.lower()
            if k == 'message-id':
                self.message_id = v.lstrip('<').rstrip('>')
            mdn_headers[k] = v

        if orig_message.receiver.mdn_digest_alg \
                and self.payload.get_content_type() != 'multipart/signed':
            status = 'failed/Failure'
            detailed_status = 'Expected signed MDN but unsigned MDN returned'
            return status, detailed_status

        if self.payload.get_content_type() == 'multipart/signed':
            signature = None
            message_boundary = (
                    '--' + self.payload.get_boundary()).encode('utf-8')
            for part in self.payload.walk():
                if part.get_content_type() == 'application/pkcs7-signature':
                    signature = part.get_payload(decode=True)
                elif part.get_content_type() == 'multipart/report':
                    self.payload = part

            # Verify the message, first using raw message and if it fails
            # then convert to canonical form and try again
            mic_content = extract_first_part(raw_content, message_boundary)
            verify_cert = orig_message.receiver.load_verify_cert()
            try:
                self.digest_alg = verify_message(
                    mic_content, signature, verify_cert)
            except IntegrityError:
                mic_content = canonicalize(self.payload)
                self.digest_alg = verify_message(
                    mic_content, signature, verify_cert)

        for part in self.payload.walk():
            if part.get_content_type() == 'message/disposition-notification':
                # logger.debug('Found MDN report for message %s:\n%s' % (
                #     orig_message.message_id, part.as_string()))

                mdn = part.get_payload()[-1]
                mdn_status = mdn['Disposition'].split(
                    ';').pop().strip().split(':')
                status = mdn_status[0]
                if status == 'processed':
                    mdn_mic = mdn.get('Received-Content-MIC', '').split(',')[0]

                    # TODO: Check MIC for all cases
                    if mdn_mic and orig_message.mic \
                            and mdn_mic != orig_message.mic.decode():
                        status = 'processed/warning'
                        detailed_status = 'Message Integrity check failed.'
                else:
                    detailed_status = ' '.join(mdn_status[1:]).strip()

        return status, detailed_status

    def detect_mdn(self):
        """ Function checks if the received raw message is an AS2 MDN or not.

        :raises MDNNotFound: If the received payload is not an MDN then this
        exception is raised.

        :return:
            A two element tuple containing (message_id, message_recipient). The
            message_id is the original AS2 message id and the message_recipient
            is the original AS2 message recipient.
        """
        mdn_message = None
        if self.payload.get_content_type() == 'multipart/report':
            mdn_message = self.payload
        elif self.payload.get_content_type() == 'multipart/signed':
            for part in self.payload.walk():
                if part.get_content_type() == 'multipart/report':
                    mdn_message = self.payload

        if not mdn_message:
            raise MDNNotFound('No MDN found in the received message')

        message_id, message_recipient = None, None
        for part in mdn_message.walk():
            if part.get_content_type() == 'message/disposition-notification':
                mdn = part.get_payload()[0]
                message_id = mdn.get('Original-Message-ID').strip('<>')
                message_recipient = mdn.get(
                    'Original-Recipient').split(';')[1].strip()
        return message_id, message_recipient
Esempio n. 5
0
class HttpMessage(object):
    '''
    generate and parse message in MIME multipart/related (RFC2387) structure.
    Its core variable is a MIMEMultipart Object
    '''
    DEFAULT_MAIN_CONTENT_ID_HEADER_VALUE = 'Main'

    def __init__(self, multipart = None):
        '''
        If parameter 'multipart' is None, then create an empty MIMEMultipart, whose body parts
        need to be added by invoking methods addMainpart() and add().
        Otherwise reuse the exising multipart Object (this has been used by parseMIMEmessage() method.)
        '''
        if multipart==None:
            self.multipart = MIMEMultipart('related')
        else:
            self.multipart = multipart
         
    def addMainpart(self, mainPartString, mainPartContentType='text/turtle'):
        self.multipart.set_param('type', mainPartContentType)
        self.multipart.set_param('start', self.DEFAULT_MAIN_CONTENT_ID_HEADER_VALUE)
        
        self.add(self.DEFAULT_MAIN_CONTENT_ID_HEADER_VALUE, mainPartString, mainPartContentType)
    
    def add(self, partId, partString, partContentType='text/turtle'):
        [mainType, subType]=partContentType.split('/')
        if mainType.lower()=='text':
            part = MIMEText(partString, subType)
        elif mainType.lower() == 'application':
            part = MIMEApplication(partString, subType, email.encoders.encode_7or8bit)
        
        if part is not None:
            part.add_header('Content-ID', partId)
            #mime package automatically add 'Content-Transfer-Encoding' header. We do not need it.
            #part.__delitem__('Content-Transfer-Encoding')
            self.multipart.attach(part)
    
    def getBody(self):
        '''
        print out the whole multipart message as a String (including Content-Type header)
        @return string
        '''       
        return self.multipart.as_string()
         
    def getParts(self):
        '''
        return the body parts as a list of MIME Object. Each body part includes body string and headers
        '''
        payload = self.multipart.get_payload()
        return payload
    
    def getPart(self, partId):
        '''
        return the body part whose Content-ID value is partId. Return only the body part string, no headers
        @return: string
        '''
        payload = self.multipart.get_payload()
        for part in payload:
            if partId == part.get('Content-ID'):
                return part.get_payload()
        
        return None
    
    def getMainPart(self):
        '''
        return the body part of "Main" part. No headers.
        '''
        return self.getPart(self.DEFAULT_MAIN_CONTENT_ID_HEADER_VALUE)
    
    def getNonMainPartsAsDict(self):
        '''
        return all those non "Main" parts as a dictionary (key is Content-ID, value is the body string). No headers
        '''
        rt = {}
        payload = self.multipart.get_payload()
        for part in payload:
            if part.get('Content-ID')!= self.DEFAULT_MAIN_CONTENT_ID_HEADER_VALUE:
                rt[part.get('Content-ID')] = part.get_payload()
        
        return rt
    
    def getContentType(self):
        '''
        return the Content-Type header value, including its parameters.
        '''
        return self.multipart.get('Content-Type')
    
    def isMultipart(self):
        return self.multipart.is_multipart()
    
    def asString(self):
        return self.getBody()