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)
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
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()