def convert_encrypted_mime_message(crypto_message, ciphertext, from_user, to_user): ''' Convert a MIME message that has been signed or encrypted, and creating a new plain text message with the payload the encrypted/signed original message. This reduces the metadata someone can collect, but it does require the receiving end decrypt the message and create a new readable message from the original message. ''' def copy_item_from_original(msg, keyword): value = crypto_message.get_email_message().get_header(keyword) if value is not None: msg.__setitem__(keyword, value) if ciphertext is not None and len(ciphertext) > 0: # set up the body parts parts = [] parts.append( MIMEApplication(mime_constants.PGP_MIME_VERSION_FIELD, mime_constants.PGP_SUB_TYPE, encode_7or8bit)) parts.append( MIMEApplication(ciphertext, mime_constants.OCTET_STREAM_SUB_TYPE, encode_7or8bit)) boundary = 'Part{}{}--'.format(random(), random()) charset, __ = get_charset( crypto_message.get_email_message().get_message()) params = { mime_constants.PROTOCOL_KEYWORD: mime_constants.PGP_TYPE, mime_constants.CHARSET_KEYWORD: charset, } msg = MIMEMultipart(mime_constants.ENCRYPTED_SUB_TYPE, boundary, parts, **params) log_message("part's content type: {}".format(msg.get_content_type())) # configure the header msg.__setitem__(mime_constants.FROM_KEYWORD, from_user) msg.__setitem__(mime_constants.TO_KEYWORD, to_user) msg.__setitem__(constants.PGP_ENCRYPTED_CONTENT_TYPE, mime_constants.MULTIPART_MIXED_TYPE) copy_item_from_original(msg, mime_constants.MESSAGE_ID_KEYWORD) copy_item_from_original(msg, mime_constants.SUBJECT_KEYWORD) copy_item_from_original(msg, mime_constants.DATE_KEYWORD) crypto_message.set_email_message(EmailMessage(msg)) crypto_message.add_public_key_to_header(from_user) crypto_message.set_filtered(True) crypto_message.set_crypted(True)
def bind(self, param_values, **kw_param_values): """Bind the definition to parameter values, creating a document. :return: A 2-tuple (media_type, document). """ definition = self.resolve_definition() params = definition.params(self.resource) validated_values = self.validate_param_values(params, param_values, **kw_param_values) media_type = self.media_type if media_type == 'application/x-www-form-urlencoded': doc = urlencode(sorted(validated_values.items())) elif media_type == 'multipart/form-data': outer = MIMEMultipart() outer.set_type('multipart/form-data') missing = object() for param in params: value = validated_values.get(param.name, missing) if value is not missing: if param.type == 'binary': maintype, subtype = 'application', 'octet-stream' params = {} else: maintype, subtype = 'text', 'plain' params = {'charset': 'utf-8'} inner = MIMENonMultipart(maintype, subtype, **params) inner.set_payload(value) inner['Content-Disposition'] = ('form-data; name="%s"' % param.name) outer.attach(inner) doc = str(outer) # Chop off the 'From' line, which only makes sense in an # email. (Python 3 does not include it.) if doc.startswith('From '): doc = doc[doc.find('\n') + 1:] media_type = (outer.get_content_type() + '; boundary="%s"' % outer.get_boundary()) elif media_type == 'application/json': doc = json.dumps(validated_values) else: raise ValueError("Unsupported media type: '%s'" % media_type) return media_type, doc
def bind(self, param_values, **kw_param_values): """Bind the definition to parameter values, creating a document. :return: A 2-tuple (media_type, document). """ definition = self.resolve_definition() params = definition.params(self.resource) validated_values = self.validate_param_values( params, param_values, **kw_param_values) media_type = self.media_type if media_type == 'application/x-www-form-urlencoded': doc = urlencode(sorted(validated_values.items())) elif media_type == 'multipart/form-data': outer = MIMEMultipart() outer.set_type('multipart/form-data') missing = object() for param in params: value = validated_values.get(param.name, missing) if value is not missing: if param.type == 'binary': maintype, subtype = 'application', 'octet-stream' params = {} else: maintype, subtype = 'text', 'plain' params = {'charset' : 'utf-8'} inner = MIMENonMultipart(maintype, subtype, **params) inner.set_payload(value) inner['Content-Disposition'] = ( 'form-data; name="%s"' % param.name) outer.attach(inner) doc = str(outer) # Chop off the 'From' line, which only makes sense in an # email. (Python 3 does not include it.) if doc.startswith('From '): doc = doc[doc.find('\n')+1:] media_type = (outer.get_content_type() + '; boundary="%s"' % outer.get_boundary()) elif media_type == 'application/json': doc = json.dumps(validated_values) else: raise ValueError("Unsupported media type: '%s'" % media_type) return media_type, doc
def convert_encrypted_mime_message(crypto_message, ciphertext, from_user, to_user): ''' Convert a MIME message that has been signed or encrypted, and creating a new plain text message with the payload the encrypted/signed original message. This reduces the metadata someone can collect, but it does require the receiving end decrypt the message and create a new readable message from the original message. ''' def copy_item_from_original(msg, keyword): value = crypto_message.get_email_message().get_header(keyword) if value is not None: msg.__setitem__(keyword, value) if ciphertext is not None and len(ciphertext) > 0: # set up the body parts parts = [] parts.append( MIMEApplication( mime_constants.PGP_MIME_VERSION_FIELD, mime_constants.PGP_SUB_TYPE, encode_7or8bit)) parts.append( MIMEApplication(ciphertext, mime_constants.OCTET_STREAM_SUB_TYPE, encode_7or8bit)) boundary = 'Part{}{}--'.format(random(), random()) charset, __ = get_charset(crypto_message.get_email_message().get_message()) params = {mime_constants.PROTOCOL_KEYWORD:mime_constants.PGP_TYPE, mime_constants.CHARSET_KEYWORD:charset,} msg = MIMEMultipart(mime_constants.ENCRYPTED_SUB_TYPE, boundary, parts, **params) log_message("part's content type: {}".format(msg.get_content_type())) # configure the header msg.__setitem__(mime_constants.FROM_KEYWORD, from_user) msg.__setitem__(mime_constants.TO_KEYWORD, to_user) msg.__setitem__(constants.PGP_ENCRYPTED_CONTENT_TYPE, mime_constants.MULTIPART_MIXED_TYPE) copy_item_from_original(msg, mime_constants.MESSAGE_ID_KEYWORD) copy_item_from_original(msg, mime_constants.SUBJECT_KEYWORD) copy_item_from_original(msg, mime_constants.DATE_KEYWORD) crypto_message.set_email_message(EmailMessage(msg)) crypto_message.add_public_key_to_header(from_user) crypto_message.set_filtered(True) crypto_message.set_crypted(True)
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 Mdn: """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 is not None: message_bytes = mime_to_bytes(self.payload) boundary = b"--" + self.payload.get_boundary().encode("utf-8") temp = message_bytes.split(boundary) temp.pop(0) return boundary + boundary.join(temp) return "" @property def headers(self): """Return the headers in the payload as a dictionary.""" if self.payload: return dict(self.payload.items()) return {} @property def headers_str(self): """Return the headers in the payload as a string.""" message_header = "" if self.payload: for k, v in self.headers.items(): message_header += f"{k}: {v}\r\n" return message_header.encode("utf-8") def build( self, message, status, detailed_status=None, confirmation_text=MDN_CONFIRM_TEXT, failed_text=MDN_FAILED_TEXT, ): """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") :param confirmation_text: The confirmation message sent in the first part of the MDN. :param failed_text: The failure message sent in the first part of the failed MDN. """ # 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": f"<{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 # 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 = 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(f"{confirmation_text}\r\n") 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\r\n" mdn_report += f'Original-Recipient: rfc822; {message.headers.get("as2-to")}\r\n' mdn_report += f'Final-Recipient: rfc822; {message.headers.get("as2-to")}\r\n' mdn_report += f"Original-Message-ID: <{message.message_id}>\r\n" mdn_report += f"Disposition: automatic-action/MDN-sent-automatically; {status}" if detailed_status: mdn_report += f": {detailed_status}" mdn_report += "\r\n" if message.mic: mdn_report += f"Received-content-MIC: {message.mic.decode()}, {message.digest_alg}\r\n" mdn_base.set_payload(mdn_report) del mdn_base["MIME-Version"] encoders.encode_7or8bit(mdn_base) self.payload.attach(mdn_base) logger.debug( f"MDN report for message {message.message_id} created:\n{mime_to_bytes(mdn_base)}" ) # 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"] signed_data = sign_message(canonicalize(self.payload), self.digest_alg, message.receiver.sign_key) signature.set_payload(signed_data) encoders.encode_base64(signature) signed_mdn.set_param("micalg", self.digest_alg) signed_mdn.attach(signature) self.payload = signed_mdn logger.debug(f"Signing the MDN for message {message.message_id}") # 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) self.payload.set_boundary(make_mime_boundary()) logger.debug(f"MDN generated for message {message.message_id} with " f"content:\n {mime_to_bytes(self.payload)}") 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 try: 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": logger.debug( f"Verifying signed MDN: \n{mime_to_bytes(self.payload)}") message_boundary = ( "--" + self.payload.get_boundary()).encode("utf-8") # Extract the signature and the signed payload signature = None signature_types = [ "application/pkcs7-signature", "application/x-pkcs7-signature", ] for part in self.payload.walk(): if part.get_content_type() in signature_types: 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( f"MDN report for message {orig_message.message_id}:\n{part.as_string()}" ) mdn = part.get_payload()[-1] mdn_status = mdn["Disposition"].split( ";").pop().strip().split(":") status = mdn_status[0] if status == "processed": # Compare the original mic with the received mic mdn_mic = mdn.get("Received-Content-MIC", "").split(",")[0] 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() except MDNNotFound: status = "failed/Failure" detailed_status = "mdn-not-found" except Exception as e: # pylint: disable=W0703 status = "failed/Failure" detailed_status = f"Failed to parse received MDN. {e}" logger.error( f"Failed to parse AS2 MDN\n: {traceback.format_exc()}") 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 = None if "Original-Recipient" in mdn: message_recipient = mdn["Original-Recipient"].split( ";")[1].strip() elif "Final-Recipient" in mdn: message_recipient = mdn["Final-Recipient"].split( ";")[1].strip() return message_id, message_recipient
html_msg = MIMEText('<strong>hello!</strong>', 'html') with open("s/python_logo.png","rb") as fb: image_msg = MIMEImage(fb.read()) msg.attach(text_msg) msg.attach(html_msg) # walk() method which allows you to move through all of the messages parts # and subparts. ''' srv = smtplib.SMTP("smtp.gmail.com", 587) srv.ehlo() srv.starttls() srv.login("*****@*****.**", "23688mfb999705949") srv.sendmail(msg["From"], msg["To"],msg.as_string()) srv.quit() ''' print(msg.as_string()) print("*" * 30, "\n") print(text_msg.as_string()) print("*" * 30, "\n") print(html_msg.as_string()) print("*" * 30, "\n") print(msg.get_content_type()) print(text_msg.get_content_type()) print(html_msg.get_content_type())
text_msg = MIMEText('hello!', 'plain') html_msg = MIMEText('<strong>hello!</strong>', 'html') with open("s/python_logo.png", "rb") as fb: image_msg = MIMEImage(fb.read()) msg.attach(text_msg) msg.attach(html_msg) # walk() method which allows you to move through all of the messages parts # and subparts. ''' srv = smtplib.SMTP("smtp.gmail.com", 587) srv.ehlo() srv.starttls() srv.login("*****@*****.**", "23688mfb999705949") srv.sendmail(msg["From"], msg["To"],msg.as_string()) srv.quit() ''' print(msg.as_string()) print("*" * 30, "\n") print(text_msg.as_string()) print("*" * 30, "\n") print(html_msg.as_string()) print("*" * 30, "\n") print(msg.get_content_type()) print(text_msg.get_content_type()) print(html_msg.get_content_type())