def send_message (self, email, envelope = None, mail_opts = (), rcpt_opts = None) : assert isinstance (email, message.Message) if envelope is None : envelope = email to_addrs = envelope ["To"] to = set (t.strip () for t in to_addrs.split (",")) for k in "cc", "bcc", "dcc" : for h in envelope.get_all (k, []) : if h : to.update (t.strip () for t in h.split (",")) if k != "cc" : del email [k] if "Date" not in email : email ["Date"] = formatdate () if "Content-Type" not in email : charset = self.charset email ["Content-Type"] = \ """text/plain; charset="%s" """ % (charset, ) else : charset = email.get_charset () if not email.is_multipart () : encoders.encode_7or8bit (email) for k in "Subject", "To", "From", "CC", "BCC" : vs = email.get_all (k) if vs : del email [k] for v in vs : email [k] = self.header (v, charset, header_name = k) ### In Python 3, `email.as_string` is useless because it returns a ### base64 encoded body if there are any non-ASCII characters ### (setting Content-Transfer-Encoding to "8bit" does *not* help) self.send \ ( envelope ["From"], list (to), pyk.email_as_bytes (email) , mail_opts, rcpt_opts )
def _multipart(self, depth, atchs, encts): base_msg = MIMEMultipart() for atch_id, encode_type in zip(atchs, encts): ctypes, encoding = mimetypes.guess_type( self.cfg[ATCH][atch_id]['file']) if not ctypes or encoding: ctypes = 'application/octet-stream' maintype, subtype = ctypes.split('/', 1) attach = MIMEBase(maintype, subtype) attach.set_payload(self.atch_files[atch_id]) if encode_type == 'base64': encoders.encode_base64(attach) elif encode_type == 'qp': encoders.encode_quopri(attach) else: encoders.encode_7or8bit(attach) attach.add_header('Content-Disposition', 'attachment', filename=self.cfg[ATCH][atch_id]['file']) base_msg.attach(attach) outer = [] outer.append(base_msg) for i in range(depth): outer.append(MIMEMultipart()) outer[-1].attach(outer[-2]) return outer[-1]
def render_template(self, render_data): template_name = render_data.get("name", "") template = self.templates.get(template_name, None) if not template: raise TemplateDoesNotExist("Invalid Template Name.") template_data = render_data.get("data") try: template_data = json.loads(template_data) except ValueError: raise InvalidRenderingParameterException( "Template rendering data is invalid") var, are_variables_present = are_all_variables_present( template, template_data) if not are_variables_present: raise MissingRenderingAttributeException(var) subject_part = template["subject_part"] text_part = template["text_part"] html_part = template["html_part"] for key, value in template_data.items(): subject_part = str.replace(str(subject_part), "{{%s}}" % key, value) text_part = str.replace(str(text_part), "{{%s}}" % key, value) html_part = str.replace(str(html_part), "{{%s}}" % key, value) email = MIMEMultipart("alternative") mime_text = MIMEBase("text", "plain;charset=UTF-8") mime_text.set_payload(text_part.encode("utf-8")) encode_7or8bit(mime_text) email.attach(mime_text) mime_html = MIMEBase("text", "html;charset=UTF-8") mime_html.set_payload(html_part.encode("utf-8")) encode_7or8bit(mime_html) email.attach(mime_html) now = datetime.datetime.now().isoformat() rendered_template = "Date: %s\r\nSubject: %s\r\n%s" % ( now, subject_part, email.as_string(), ) return rendered_template
def mbox(self): from email.mime.nonmultipart import MIMENonMultipart from email.encoders import encode_7or8bit body = '' if self.description: body += self.description.content + '\n' body += self.content mbox = MIMENonMultipart('text', 'plain', charset='utf-8') mbox['Subject'] = ": ".join([t.name for t in self.tags] + [self.name.strip().capitalize()]) mbox['From'] = '%s <%s>' % (self.submitter.name, self.submitter.email) mbox['Message-Id'] = self.msgid mbox.set_payload(body.encode('utf-8')) encode_7or8bit(mbox) return mbox.as_string()
def mbox(self): from email.mime.nonmultipart import MIMENonMultipart from email.encoders import encode_7or8bit body = '' if self.comments[0].msgid == self.msgid: body += self.comments[0].content + '\n' body += self.content mbox = MIMENonMultipart('text', 'plain', charset='utf-8') mbox['Subject'] = self.name mbox['From'] = '%s <%s>' % (self.submitter.name, self.submitter.email) mbox['Message-Id'] = self.msgid mbox.set_payload(body.encode('utf-8')) encode_7or8bit(mbox) return mbox.as_string()
def extract_payload(self, mail): if mail.body == None: return # only None, '' is still ok ctype, ctype_params = mail.content_encoding['Content-Type'] cdisp, cdisp_params = mail.content_encoding['Content-Disposition'] assert ctype, "Extract payload requires that mail.content_encoding have a valid Content-Type." if ctype.startswith("text/"): self.add_text(mail.body) else: if cdisp: # replicate the content-disposition settings self.add_header('Content-Disposition', cdisp, **cdisp_params) self.set_payload(mail.body) if ctype == "message/rfc822": encoders.encode_7or8bit(self) else: encoders.encode_base64(self)
def __init__(self, _text): MIMENonMultipart.__init__(self, 'text', 'plain', **{'charset': self.patch_charset}) self.set_payload(_text.encode(self.patch_charset)) encode_7or8bit(self)
def apply_mtom(headers, envelope, params, paramvals): ''' Apply MTOM to a SOAP envelope, separating attachments into a MIME multipart message. References: XOP http://www.w3.org/TR/xop10/ MTOM http://www.w3.org/TR/soap12-mtom/ http://www.w3.org/Submission/soap11mtom10/ @param headers Headers dictionary of the SOAP message that would originally be sent. @param envelope SOAP envelope string that would have originally been sent. @param params params attribute from the Message object used for the SOAP @param paramvals values of the params, passed to Message.to_xml @return tuple of length 2 with dictionary of headers and string of body that can be sent with HTTPConnection ''' # grab the XML element of the message in the SOAP body soapmsg = StringIO(envelope) soaptree = ElementTree.parse(soapmsg) soapns = soaptree.getroot().tag.split('}')[0].strip('{') soapbody = soaptree.getroot().find("{%s}Body" % soapns) message = None for child in list(soapbody): if child.tag != "%sFault" % (soapns, ): message = child break # Get additional parameters from original Content-Type ctarray = [] for n, v in headers.items(): if n.lower() == 'content-type': ctarray = v.split(';') break roottype = ctarray[0].strip() rootparams = {} for ctparam in ctarray[1:]: n, v = ctparam.strip().split('=') rootparams[n] = v.strip("\"'") # Set up initial MIME parts mtompkg = MIMEMultipart('related', boundary='?//<><>soaplib_MIME_boundary<>') rootpkg = None try: rootpkg = MIMEApplication(envelope, 'xop+xml', encode_7or8bit) except NameError: rootpkg = MIMENonMultipart("application", "xop+xml") rootpkg.set_payload(envelope) encode_7or8bit(rootpkg) # Set up multipart headers. del(mtompkg['mime-version']) mtompkg.set_param('start-info', roottype) mtompkg.set_param('start', '<soaplibEnvelope>') if 'SOAPAction' in headers: mtompkg.add_header('SOAPAction', headers.get('SOAPAction')) # Set up root SOAP part headers. del(rootpkg['mime-version']) rootpkg.add_header('Content-ID', '<soaplibEnvelope>') for n, v in rootparams.items(): rootpkg.set_param(n, v) rootpkg.set_param('type', roottype) mtompkg.attach(rootpkg) # Extract attachments from SOAP envelope. for i in range(len(params)): name, typ = params[i] if typ == Attachment: id = "soaplibAttachment_%s" % (len(mtompkg.get_payload()), ) param = message[i] param.text = "" incl = create_xml_subelement(param, "{http://www.w3.org/2004/08/xop/include}Include") incl.attrib["href"] = "cid:%s" % id if paramvals[i].fileName and not paramvals[i].data: paramvals[i].load_from_file() data = paramvals[i].data attachment = None try: attachment = MIMEApplication(data, _encoder=encode_7or8bit) except NameError: attachment = MIMENonMultipart("application", "octet-stream") attachment.set_payload(data) encode_7or8bit(attachment) del(attachment['mime-version']) attachment.add_header('Content-ID', '<%s>' % (id, )) mtompkg.attach(attachment) # Update SOAP envelope. soapmsg.close() soapmsg = StringIO() soaptree.write(soapmsg) rootpkg.set_payload(soapmsg.getvalue()) soapmsg.close() # extract body string from MIMEMultipart message bound = '--%s' % (mtompkg.get_boundary(), ) marray = mtompkg.as_string().split(bound) mtombody = bound mtombody += bound.join(marray[1:]) # set Content-Length mtompkg.add_header("Content-Length", str(len(mtombody))) # extract dictionary of headers from MIMEMultipart message mtomheaders = {} for name, value in mtompkg.items(): mtomheaders[name] = value if len(mtompkg.get_payload()) <= 1: return (headers, envelope) return (mtomheaders, mtombody)
encode_quopri(part) else: encode_base64(part) else: part = MIMEBase(main_type, sub_type) part.set_payload(data) encode_base64(part) else: part = MIMEBase(main_type, sub_type) part.set_payload(data) if encoding == "base64": encode_base64(part) elif encoding == "quoted-printable": encode_quopri(part) elif encoding == "7or8bit": encode_7or8bit(part) else: # elif encoding == "noop": encode_noop(part) for key, value in attachment.get("replace_header_list", []): part.replace_header(key, value) for key, value in attachment.get("header_dict", {}).items(): # adds headers, does not replace or set part.add_header(key, value) for key, value in attachment.get("add_header_list", []): part.add_header(key, value) if attachment.get("filename", None) is not None: part.add_header("Content-Disposition", "attachment", attachment["filename"]) outer.attach(part) #return outer.as_string() return formatMultipartMessageToRFC2822String(outer)
def Compile(self): """Check message for error and compile it""" # self._Message = Message() self._Message = MIMEMultipart() m = self._Message if len(self._Body) == 0: raise EmptyBody("Please set Body") if len(self._Recipients['To']) + len(self._Recipients['Cc']) + len(self._Recipients['Bcc']) == 0: raise EmptyAddr("Recipients address not set") # if len(self._AltBody) > 0: # self.ContentType = "multipart/alternative" if len(self._MessageDate) != 0: m.add_header("Date", self._MessageDate) else: m.add_header("Date", time.strftime("%a, %d %b %Y %T %z")) if len(self._ReturnPath) != 0: m.add_header("Return-path", self._ReturnPath) elif len(self._Sender) != 0: m.add_header("Return-path", self._Sender) else: m.add_header("Return-path", self._From) self._Sender = self._From for rcpt in self._Recipients.keys(): if len(self._Recipients[rcpt]) > 0: for each in self._Recipients[rcpt]: if len(each['text']) > 0: m.add_header(rcpt, '"%s" <%s>' % (str(Header(each['text'], 'utf-8')), each['email'])) else: m.add_header(rcpt, '<%s>' % (each['email'])) if m.get("To") in (None, ''): m.add_header("To", 'undisclosed-recipients:;') m.add_header("From", self._From) if self._ReplyTo != '': reply_to = self._ReplyTo if len(reply_to['text']) > 0: m.add_header("Reply-To", '<%s>' (reply_to['email'])) else: m.add_header("Reply-To", '"%s" <%s>' % (str(Header(reply_to['text'], 'utf-8')), reply_to['email'])) if len(self._MessageID) != 0: m.add_header("Message-ID", self._MessageID) else: m.add_header("Message-ID", '<%s@%s>' % (self.uniqid(), self._Hostname)) m.add_header('X-Priority', str(self._Priority)) m.add_header("X-Mailer", self._XMailer) m.add_header("Subject", str(Header(self._Subject, 'utf-8'))) if len(self._AltBody) > 0: # alt_body = MIMEText(self._AltBody, ) # alt_body.set_type(self._AltBodyType) alt_body = MIMEBase(self._AltBodyType.split('/')[0], self._AltBodyType.split('/')[1]) alt_body.set_payload(self._AltBody) if self._Encoding == 'base64': encoders.encode_base64(alt_body) elif self._Encoding == 'quoted': encoders.encode_quopri(alt_body) elif self._Encoding in ('8bit', '7bit'): encoders.encode_7or8bit(alt_body) alt_body.set_charset(self._CharSet) # body = MIMEText(self._Body) # body.set_type(self._BodyType) body = MIMEBase(self._BodyType.split('/')[0], self._BodyType.split('/')[1]) body.set_payload(self._Body) if self._Encoding == 'base64': encoders.encode_base64(body) elif self._Encoding == 'quoted': encoders.encode_quopri(body) elif self._Encoding in ('8bit', '7bit'): encoders.encode_7or8bit(body) body.set_charset(self._CharSet) m.attach(alt_body) m.attach(body) else: # body = MIMEText(self._Body) # body.set_type(self._BodyType) body = MIMEBase(self._BodyType.split('/')[0], self._BodyType.split('/')[1]) body.set_payload(self._Body) encoders.encode_base64(body) body.set_charset(self._CharSet) m.attach(body) # m.set_charset(self._CharSet) m.set_type('multipart/alternative') self._isCompile = True
def apply_mtom(headers, envelope, params, paramvals): ''' Apply MTOM to a SOAP envelope, separating attachments into a MIME multipart message. References: XOP http://www.w3.org/TR/xop10/ MTOM http://www.w3.org/TR/soap12-mtom/ http://www.w3.org/Submission/soap11mtom10/ @param headers Headers dictionary of the SOAP message that would originally be sent. @param envelope SOAP envelope string that would have originally been sent. @param params params attribute from the Message object used for the SOAP @param paramvals values of the params, passed to Message.to_xml @return tuple of length 2 with dictionary of headers and string of body that can be sent with HTTPConnection ''' # grab the XML element of the message in the SOAP body soapmsg = StringIO(envelope) soaptree = ElementTree.parse(soapmsg) soapns = soaptree.getroot().tag.split('}')[0].strip('{') soapbody = soaptree.getroot().find("{%s}Body" % soapns) message = None for child in list(soapbody): if child.tag != "%sFault" % (soapns, ): message = child break # Get additional parameters from original Content-Type ctarray = [] for n, v in headers.items(): if n.lower() == 'content-type': ctarray = v.split(';') break roottype = ctarray[0].strip() rootparams = {} for ctparam in ctarray[1:]: n, v = ctparam.strip().split('=') rootparams[n] = v.strip("\"'") # Set up initial MIME parts mtompkg = MIMEMultipart('related', boundary='?//<><>soaplib_MIME_boundary<>') rootpkg = None try: rootpkg = MIMEApplication(envelope, 'xop+xml', encode_7or8bit) except NameError: rootpkg = MIMENonMultipart("application", "xop+xml") rootpkg.set_payload(envelope) encode_7or8bit(rootpkg) # Set up multipart headers. del (mtompkg['mime-version']) mtompkg.set_param('start-info', roottype) mtompkg.set_param('start', '<soaplibEnvelope>') if 'SOAPAction' in headers: mtompkg.add_header('SOAPAction', headers.get('SOAPAction')) # Set up root SOAP part headers. del (rootpkg['mime-version']) rootpkg.add_header('Content-ID', '<soaplibEnvelope>') for n, v in rootparams.items(): rootpkg.set_param(n, v) rootpkg.set_param('type', roottype) mtompkg.attach(rootpkg) # Extract attachments from SOAP envelope. for i in range(len(params)): name, typ = params[i] if typ == Attachment: id = "soaplibAttachment_%s" % (len(mtompkg.get_payload()), ) param = message[i] param.text = "" incl = create_xml_subelement( param, "{http://www.w3.org/2004/08/xop/include}Include") incl.attrib["href"] = "cid:%s" % id if paramvals[i].fileName and not paramvals[i].data: paramvals[i].load_from_file() data = paramvals[i].data attachment = None try: attachment = MIMEApplication(data, _encoder=encode_7or8bit) except NameError: attachment = MIMENonMultipart("application", "octet-stream") attachment.set_payload(data) encode_7or8bit(attachment) del (attachment['mime-version']) attachment.add_header('Content-ID', '<%s>' % (id, )) mtompkg.attach(attachment) # Update SOAP envelope. soapmsg.close() soapmsg = StringIO() soaptree.write(soapmsg) rootpkg.set_payload(soapmsg.getvalue()) soapmsg.close() # extract body string from MIMEMultipart message bound = '--%s' % (mtompkg.get_boundary(), ) marray = mtompkg.as_string().split(bound) mtombody = bound mtombody += bound.join(marray[1:]) # set Content-Length mtompkg.add_header("Content-Length", str(len(mtombody))) # extract dictionary of headers from MIMEMultipart message mtomheaders = {} for name, value in mtompkg.items(): mtomheaders[name] = value if len(mtompkg.get_payload()) <= 1: return (headers, envelope) return (mtomheaders, mtombody)
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 build(self, data, filename=None, subject='AS2 Message', content_type='application/edi-consent', additional_headers=None): """Function builds the AS2 message. Compresses, signs and encrypts the payload if applicable. :param data: A byte string of the data to be transmitted. :param filename: Optional filename to be included in the Content-disposition header. :param subject: The subject for the AS2 message, used by some AS2 servers for additional routing of messages. (default "AS2 Message") :param content_type: The content type for the AS2 message, to be used in the MIME header. (default "application/edi-consent") :param additional_headers: Any additional headers to be included as part of the AS2 message. """ # Validations assert type(data) is byte_cls, \ 'Parameter data must be of type {}'.format(byte_cls) additional_headers = additional_headers if additional_headers else {} assert type(additional_headers) is dict if self.receiver.sign and not self.sender.sign_key: raise ImproperlyConfigured( 'Signing of messages is enabled but sign key is not set ' 'for the sender.') if self.receiver.encrypt and not self.receiver.encrypt_cert: raise ImproperlyConfigured( 'Encryption of messages is enabled but encrypt key is not set ' 'for the receiver.') # Generate message id using UUID 1 as it uses both hostname and time self.message_id = email_utils.make_msgid().lstrip('<').rstrip('>') # Set up the message headers as2_headers = { 'AS2-Version': AS2_VERSION, 'ediint-features': EDIINT_FEATURES, 'Message-ID': '<{}>'.format(self.message_id), 'AS2-From': quote_as2name(self.sender.as2_name), 'AS2-To': quote_as2name(self.receiver.as2_name), 'Subject': subject, 'Date': email_utils.formatdate(localtime=True), # 'recipient-address': message.partner.target_url, } as2_headers.update(additional_headers) # Read the input and convert to bytes if value is unicode/str # using utf-8 encoding and finally Canonicalize the payload self.payload = email_message.Message() self.payload.set_payload(data) self.payload.set_type(content_type) encoders.encode_7or8bit(self.payload) if filename: self.payload.add_header( 'Content-Disposition', 'attachment', filename=filename) del self.payload['MIME-Version'] if self.receiver.compress: self.compressed = True compressed_message = email_message.Message() compressed_message.set_type('application/pkcs7-mime') compressed_message.set_param('name', 'smime.p7z') compressed_message.set_param('smime-type', 'compressed-data') compressed_message.add_header( 'Content-Disposition', 'attachment', filename='smime.p7z') compressed_message.set_payload( compress_message(canonicalize(self.payload))) encoders.encode_base64(compressed_message) self.payload = compressed_message # logger.debug(b'Compressed message %s payload as:\n%s' % ( # self.message_id, self.payload.as_string())) if self.receiver.sign: self.signed, self.digest_alg = True, self.receiver.digest_alg signed_message = MIMEMultipart( 'signed', protocol="application/pkcs7-signature") del signed_message['MIME-Version'] signed_message.attach(self.payload) # Calculate the MIC Hash of the message to be verified mic_content = canonicalize(self.payload) digest_func = hashlib.new(self.digest_alg) digest_func.update(mic_content) self.mic = binascii.b2a_base64(digest_func.digest()).strip() # 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( mic_content, self.digest_alg, self.sender.sign_key)) encoders.encode_base64(signature) signed_message.set_param('micalg', self.digest_alg) signed_message.attach(signature) self.payload = signed_message # logger.debug(b'Signed message %s payload as:\n%s' % ( # self.message_id, self.payload.as_string())) if self.receiver.encrypt: self.encrypted, self.enc_alg = True, self.receiver.enc_alg encrypted_message = email_message.Message() encrypted_message.set_type('application/pkcs7-mime') encrypted_message.set_param('name', 'smime.p7m') encrypted_message.set_param('smime-type', 'enveloped-data') encrypted_message.add_header( 'Content-Disposition', 'attachment', filename='smime.p7m') encrypt_cert = self.receiver.load_encrypt_cert() encrypted_message.set_payload(encrypt_message( canonicalize(self.payload), self.enc_alg, encrypt_cert )) encoders.encode_base64(encrypted_message) self.payload = encrypted_message # logger.debug(b'Encrypted message %s payload as:\n%s' % ( # self.message_id, self.payload.as_string())) if self.receiver.mdn_mode: as2_headers['disposition-notification-to'] = '*****@*****.**' if self.receiver.mdn_digest_alg: as2_headers['disposition-notification-options'] = \ 'signed-receipt-protocol=required, pkcs7-signature; ' \ 'signed-receipt-micalg=optional, {}'.format( self.receiver.mdn_digest_alg) if self.receiver.mdn_mode == 'ASYNC': if not self.sender.mdn_url: raise ImproperlyConfigured( 'MDN URL must be set in the organization when MDN mode ' 'is set to ASYNC') as2_headers['receipt-delivery-option'] = self.sender.mdn_url # Update the headers of the final payload and set its boundary for k, v in as2_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 __init__(self, _text): MIMENonMultipart.__init__(self, "text", "plain", **{"charset": self.patch_charset}) self.set_payload(_text.encode(self.patch_charset)) encode_7or8bit(self)
def replace(self, find, replace, trash_folder, callback=None): """Performs a body-wide string search and replace Note that this search-and-replace is pretty dumb, and will fail in, for example, HTML messages where HTML tags would alter the search string. Args: find -- the search term to look for as a string, or a tuple of items to replace with corresponding items in the replace tuple replace -- the string to replace instances of the "find" term with, or a tuple of terms to replace the corresponding strings in the find tuple trash_folder -- the name of the folder / label that is, in the current account, the trash container Returns: True on success, and in all other instances an error object """ def _set_content_transfer_encoding(part, encoding): try: del part['Content-Transfer-Encoding'] except: "" part.add_header('Content-Transfer-Encoding', encoding) valid_content_types = ('plain', 'html') for valid_type in valid_content_types: for part in typed_subpart_iterator(self.raw, 'text', valid_type): section_encoding = part['Content-Transfer-Encoding'] # If the message section doesn't advertise an encoding, # then default to quoted printable. Otherwise the module # will default to base64, which can cause problems if not section_encoding: section_encoding = "quoted-printable" else: section_encoding = section_encoding.lower() section_charset = message_part_charset(part, self.raw) new_payload_section = utf8_encode_message_part( part, self.raw, section_charset) if is_encoding_error(new_payload_section): self.encoding_error = new_payload_section return _cmd(callback, self.encoding_error) if isinstance(find, tuple) or isinstance(find, list): for i in range(0, len(find)): new_payload_section = new_payload_section.replace( find[i], replace[i]) else: new_payload_section = new_payload_section.replace( find, replace) new_payload_section = new_payload_section.encode( part._orig_charset, errors="replace") if section_encoding == "quoted-printable": new_payload_section = encodestring(new_payload_section, quotetabs=0) part.set_payload(new_payload_section, part._orig_charset) _set_content_transfer_encoding(part, "quoted-printable") elif section_encoding == "base64": part.set_payload(new_payload_section, part._orig_charset) ENC.encode_base64(part) _set_content_transfer_encoding(part, "base64") elif section_encoding in ('7bit', '8bit'): part.set_payload(new_payload_section, part._orig_charset) ENC.encode_7or8bit(part) _set_content_transfer_encoding(part, section_encoding) elif section_encoding == "binary": part.set_payload(new_payload_section, part._orig_charset) part['Content-Transfer-Encoding'] = 'binary' _set_content_transfer_encoding(part, 'binary') del part._normalized del part._orig_charset def _on_save(was_success): return _cmd(callback, was_success) return _cmd_cb(self.save, _on_save, bool(callback), trash_folder)
def apply_mtom(headers, envelope, params, paramvals): """ Apply MTOM to a SOAP envelope, separating attachments into a MIME multipart message. References: XOP http://www.w3.org/TR/xop10/ MTOM http://www.w3.org/TR/soap12-mtom/ http://www.w3.org/Submission/soap11mtom10/ @param headers Headers dictionary of the SOAP message that would originally be sent. @param envelope SOAP envelope string that would have originally been sent. @param params params attribute from the Message object used for the SOAP @param paramvals values of the params, passed to Message.to_xml @return tuple of length 2 with dictionary of headers and string of body that can be sent with HTTPConnection """ # grab the XML element of the message in the SOAP body soaptree = etree.fromstring(envelope) soapbody = soaptree.find("{%s}Body" % soaplib.ns_soap_env) message = None for child in list(soapbody): if child.tag != "%sFault" % (soaplib.ns_soap_env,): message = child break # Get additional parameters from original Content-Type ctarray = [] for n, v in headers.items(): if n.lower() == "content-type": ctarray = v.split(";") break roottype = ctarray[0].strip() rootparams = {} for ctparam in ctarray[1:]: n, v = ctparam.strip().split("=") rootparams[n] = v.strip("\"'") # Set up initial MIME parts. mtompkg = MIMEMultipart("related", boundary="?//<><>soaplib_MIME_boundary<>") rootpkg = None try: rootpkg = MIMEApplication(envelope, "xop+xml", encode_7or8bit) except NameError: rootpkg = MIMENonMultipart("application", "xop+xml") rootpkg.set_payload(envelope) encode_7or8bit(rootpkg) # Set up multipart headers. del (mtompkg["mime-version"]) mtompkg.set_param("start-info", roottype) mtompkg.set_param("start", "<soaplibEnvelope>") if "SOAPAction" in headers: mtompkg.add_header("SOAPAction", headers.get("SOAPAction")) # Set up root SOAP part headers. del (rootpkg["mime-version"]) rootpkg.add_header("Content-ID", "<soaplibEnvelope>") for n, v in rootparams.items(): rootpkg.set_param(n, v) rootpkg.set_param("type", roottype) mtompkg.attach(rootpkg) # Extract attachments from SOAP envelope. for i in range(len(params)): name, typ = params[i] if typ == Attachment: id = "soaplibAttachment_%s" % (len(mtompkg.get_payload()),) param = message[i] param.text = "" incl = etree.SubElement(param, "{%s}Include" % soaplib.ns_xop) incl.attrib["href"] = "cid:%s" % id if paramvals[i].fileName and not paramvals[i].data: paramvals[i].load_from_file() data = paramvals[i].data attachment = None try: attachment = MIMEApplication(data, _encoder=encode_7or8bit) except NameError: attachment = MIMENonMultipart("application", "octet-stream") attachment.set_payload(data) encode_7or8bit(attachment) del (attachment["mime-version"]) attachment.add_header("Content-ID", "<%s>" % (id,)) mtompkg.attach(attachment) # Update SOAP envelope. rootpkg.set_payload(etree.tostring(soaptree)) # extract body string from MIMEMultipart message bound = "--%s" % (mtompkg.get_boundary(),) marray = mtompkg.as_string().split(bound) mtombody = bound mtombody += bound.join(marray[1:]) # set Content-Length mtompkg.add_header("Content-Length", str(len(mtombody))) # extract dictionary of headers from MIMEMultipart message mtomheaders = {} for name, value in mtompkg.items(): mtomheaders[name] = value if len(mtompkg.get_payload()) <= 1: return (headers, envelope) return (mtomheaders, mtombody)
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 build( self, data, filename=None, subject="AS2 Message", content_type="application/edi-consent", additional_headers=None, disposition_notification_to="*****@*****.**", ): """Function builds the AS2 message. Compresses, signs and encrypts the payload if applicable. :param data: A byte string of the data to be transmitted. :param filename: Optional filename to be included in the Content-disposition header. :param subject: The subject for the AS2 message, used by some AS2 servers for additional routing of messages. (default "AS2 Message") :param content_type: The content type for the AS2 message, to be used in the MIME header. (default "application/edi-consent") :param additional_headers: Any additional headers to be included as part of the AS2 message. :param disposition_notification_to: Email address for disposition-notification-to header entry. (default "*****@*****.**") """ # Validations assert isinstance(data, bytes), "Parameter data must be of bytes type." additional_headers = additional_headers if additional_headers else {} assert isinstance(additional_headers, dict) if self.receiver.sign and not self.sender.sign_key: raise ImproperlyConfigured( "Signing of messages is enabled but sign key is not set for the sender." ) if self.receiver.encrypt and not self.receiver.encrypt_cert: raise ImproperlyConfigured( "Encryption of messages is enabled but encrypt key is not set for the receiver." ) # Generate message id using UUID 1 as it uses both hostname and time self.message_id = email_utils.make_msgid().lstrip("<").rstrip(">") # Set up the message headers as2_headers = { "AS2-Version": AS2_VERSION, "ediint-features": EDIINT_FEATURES, "Message-ID": f"<{self.message_id}>", "AS2-From": quote_as2name(self.sender.as2_name), "AS2-To": quote_as2name(self.receiver.as2_name), "Subject": subject, "Date": email_utils.formatdate(localtime=True), } as2_headers.update(additional_headers) # Read the input and convert to bytes if value is unicode/str # using utf-8 encoding and finally Canonicalize the payload self.payload = email_message.Message() self.payload.set_payload(data) self.payload.set_type(content_type) if content_type.lower().startswith("application/octet-stream"): self.payload["Content-Transfer-Encoding"] = "binary" else: encoders.encode_7or8bit(self.payload) if filename: self.payload.add_header("Content-Disposition", "attachment", filename=filename) del self.payload["MIME-Version"] if self.receiver.compress: self.compressed = True compressed_message = email_message.Message() compressed_message.set_type("application/pkcs7-mime") compressed_message.set_param("name", "smime.p7z") compressed_message.set_param("smime-type", "compressed-data") compressed_message.add_header("Content-Disposition", "attachment", filename="smime.p7z") compressed_message.add_header("Content-Transfer-Encoding", "binary") compressed_message.set_payload( compress_message(mime_to_bytes(self.payload))) self.payload = compressed_message logger.debug( f"Compressed message {self.message_id} payload as:\n{mime_to_bytes(self.payload)}" ) if self.receiver.sign: self.signed, self.digest_alg = True, self.receiver.digest_alg signed_message = MIMEMultipart( "signed", protocol="application/pkcs7-signature") del signed_message["MIME-Version"] signed_message.attach(self.payload) # Calculate the MIC Hash of the message to be verified mic_content = canonicalize(self.payload) digest_func = hashlib.new(self.digest_alg) digest_func.update(mic_content) self.mic = binascii.b2a_base64(digest_func.digest()).strip() # 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_data = sign_message(mic_content, self.digest_alg, self.sender.sign_key) signature.set_payload(signature_data) encoders.encode_base64(signature) signed_message.set_param("micalg", self.digest_alg) signed_message.attach(signature) self.payload = signed_message logger.debug( f"Signed message {self.message_id} payload as:\n{mime_to_bytes(self.payload)}" ) if self.receiver.encrypt: self.encrypted, self.enc_alg = True, self.receiver.enc_alg encrypted_message = email_message.Message() encrypted_message.set_type("application/pkcs7-mime") encrypted_message.set_param("name", "smime.p7m") encrypted_message.set_param("smime-type", "enveloped-data") encrypted_message.add_header("Content-Disposition", "attachment", filename="smime.p7m") encrypted_message.add_header("Content-Transfer-Encoding", "binary") encrypt_cert = self.receiver.load_encrypt_cert() encrypted_data = encrypt_message(mime_to_bytes(self.payload), self.enc_alg, encrypt_cert) encrypted_message.set_payload(encrypted_data) self.payload = encrypted_message logger.debug( f"Encrypted message {self.message_id} payload as:\n{mime_to_bytes(self.payload)}" ) if self.receiver.mdn_mode: as2_headers[ "disposition-notification-to"] = disposition_notification_to if self.receiver.mdn_digest_alg: as2_headers["disposition-notification-options"] = ( f"signed-receipt-protocol=required, pkcs7-signature; " f"signed-receipt-micalg=optional, {self.receiver.mdn_digest_alg}" ) if self.receiver.mdn_mode == "ASYNC": if not self.sender.mdn_url: raise ImproperlyConfigured( "MDN URL must be set in the organization when MDN mode is set to ASYNC" ) as2_headers["receipt-delivery-option"] = self.sender.mdn_url # Update the headers of the final payload and set its boundary for k, v in as2_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())