def test_prepend_headerfields(): # we can inject headerfields msg = Parser(policy=default_policy).parsestr( "To: foo\nSubject: bar\n\nMeet at dawni\n") msg.add_header("X-Foo", "baz") result = pgp.prepend_header_fields(msg, [("To", "foo"), ("From", "bar")]) assert result.keys() == ["From", "To", "Subject", "X-Foo"]
def header(self): """ Returns the message header, if it exists. Otherwise it will generate one. """ try: return self._header except AttributeError: headerText = self._getStringStream('__substg1.0_007D') if headerText is not None: self._header = EmailParser().parsestr(headerText) self._header['date'] = self.date else: logger.info( 'Header is empty or was not found. Header will be generated from other streams.' ) header = EmailParser().parsestr('') header.add_header('Date', self.date) header.add_header('From', self.sender) header.add_header('To', self.to) header.add_header('Cc', self.cc) header.add_header('Message-Id', self.message_id) # TODO find authentication results outside of header header.add_header('Authentication-Results', None) self._header = header return self._header
def load_mail_from_file(mail_file, enforceUniqueMessageId=False): mailset_dir = pkg_resources.resource_filename('test.unit.fixtures', 'mailset') mail_file = os.path.join(mailset_dir, 'new', mail_file) with open(mail_file) as f: mail = Parser().parse(f) if enforceUniqueMessageId: mail.add_header('Message-Id', make_msgid()) return mail
def testStripLeapHeaders(self): ENC_HEADER = "fake encryption header" SIG_HEADER = "fake signature header" message = Parser().parsestr(self.EMAIL) message.add_header("X-Leap-Encryption", ENC_HEADER) message.add_header("X-Leap-Signature", SIG_HEADER) self.fetcher._add_message_locally = Mock() def check_headers(_): self.assertTrue(self.fetcher._add_message_locally.called, "The message was not added to soledad") _, data = self.fetcher._add_message_locally.call_args[0][0] msg = Parser().parsestr(data) self.assertNotEqual(msg.get('X-Leap-Encryption', ''), ENC_HEADER) self.assertNotEqual(msg.get('X-Leap-Signature', ''), SIG_HEADER) d = self._do_fetch(message.as_string()) d.addCallback(check_headers) return d
def testExtractOpenPGPHeaderInvalidUrl(self): """ Test the OpenPGP header key extraction """ KEYURL = "https://someotherdomain.com/key.txt" OpenPGP = "id=12345678; url=\"%s\"; preference=signencrypt" % (KEYURL,) message = Parser().parsestr(self.EMAIL) message.add_header("OpenPGP", OpenPGP) self.fetcher._keymanager.fetch_key = Mock() def fetch_key_called(ret): self.assertFalse(self.fetcher._keymanager.fetch_key.called) d = self._create_incoming_email(message.as_string()) d.addCallback( lambda email: self._mock_soledad_get_from_index(fields.JUST_MAIL_IDX, [email])) d.addCallback(lambda _: self.fetcher.fetch()) d.addCallback(fetch_key_called) return d
def _handleTcpStream(self, tcp): """Parser call-back for processing data streams.""" # Collect time and IP metadata ((src, sport), (dst, dport)) = tcp.addr # Grab data for every SMTP session if tcp.nids_state == nids.NIDS_JUST_EST: if sport == 25 or dport == 25: tcp.client.collect = 1 tcp.server.collect = 1 # Wait until the SMTP session completes before processing elif tcp.nids_state == nids.NIDS_DATA: tcp.discard(0) # Process SMTP session data elif tcp.nids_state in self.end_states: # We're only interested in the server side of the traffic server_data = tcp.server.data[:tcp.server.count] # Python's email parser module doesn't seem to like ESMTP # formatted messages, so we'll skip the EHLO -> DATA header, # parse from the first 'Received:' value, and add the ESMTP # header values back in later. f = server_data.find("Received:") message = Parser().parsestr(server_data[f:]) # Add ESMTP headers back into the email message object esmtp_start = server_data.find("EHLO") esmtp_end = server_data.find("DATA") if esmtp_start >= 0: esmtp_headers = server_data[esmtp_start:esmtp_end] for line in esmtp_headers.splitlines(): if "EHLO" not in line: (name, value) = line.split(":") message.add_header(name, value) self.messages.append(message)
def testExtractOpenPGPHeader(self): """ Test the OpenPGP header key extraction """ KEYURL = "https://leap.se/key.txt" OpenPGP = "id=12345678; url=\"%s\"; preference=signencrypt" % (KEYURL,) message = Parser().parsestr(self.EMAIL) message.add_header("OpenPGP", OpenPGP) self.fetcher._keymanager.fetch_key = Mock( return_value=defer.succeed(None)) def fetch_key_called(ret): self.fetcher._keymanager.fetch_key.assert_called_once_with( ADDRESS_2, KEYURL, OpenPGPKey) d = self._create_incoming_email(message.as_string()) d.addCallback( lambda email: self._mock_soledad_get_from_index(fields.JUST_MAIL_IDX, [email])) d.addCallback(lambda _: self.fetcher.fetch()) d.addCallback(fetch_key_called) return d
def header(self): """ Returns the message header, if it exists. Otherwise it will generate one. """ try: return self._header except AttributeError: headerText = self._getStringStream('__substg1.0_007D') if headerText is not None: self._header = EmailParser().parsestr(headerText) self._header['date'] = self.date else: logger.info('Header is empty or was not found. Header will be generated from other streams.') header = EmailParser().parsestr('') header.add_header('Date', self.date) header.add_header('From', self.sender) header.add_header('To', self.to) header.add_header('Cc', self.cc) header.add_header('Message-Id', self.message_id) # TODO find authentication results outside of header header.add_header('Authentication-Results', None) self._header = header return self._header
def execute(*args, **kw): if not os.path.isdir(mybasepath): os.makedirs(mybasepath) for stage in ['incoming', 'ACCEPT' ]: if not os.path.isdir(os.path.join(mybasepath, stage)): os.makedirs(os.path.join(mybasepath, stage)) # TODO: Test for correct call. filepath = args[0] if kw.has_key('stage'): log.debug(_("Issuing callback after processing to stage %s") % (kw['stage']), level=8) log.debug(_("Testing cb_action_%s()") % (kw['stage']), level=8) if hasattr(modules, 'cb_action_%s' % (kw['stage'])): log.debug(_("Attempting to execute cb_action_%s()") % (kw['stage']), level=8) exec('modules.cb_action_%s(%r, %r)' % (kw['stage'],'optout',filepath)) return log.debug(_("Executing module footer for %r, %r") % (args, kw), level=8) new_filepath = os.path.join('/var/spool/pykolab/wallace/footer/incoming', os.path.basename(filepath)) os.rename(filepath, new_filepath) filepath = new_filepath # parse message headers # @TODO: make sure we can use True as the 2nd argument here message = Parser().parse(open(filepath, 'r'), True) # Possible footer answers are limited to ACCEPT only answers = [ 'ACCEPT' ] footer = {} footer_html_file = conf.get('wallace', 'footer_html') footer_text_file = conf.get('wallace', 'footer_text') if not os.path.isfile(footer_text_file) and not os.path.isfile(footer_html_file): exec('modules.cb_action_%s(%r, %r)' % ('ACCEPT','footer', filepath)) return if os.path.isfile(footer_text_file): footer['plain'] = open(footer_text_file, 'r').read() if not os.path.isfile(footer_html_file): footer['html'] = '<p>' + self.footer['plain'] + '</p>' else: footer['html'] = open(footer_html_file, 'r').read() if footer['html'] == "": footer['html'] = '<p>' + self.footer['plain'] + '</p>' if footer['plain'] == "" and footer['html'] == "<p></p>": exec('modules.cb_action_%s(%r, %r)' % ('ACCEPT','footer', filepath)) return footer_added = False try: _footer_added = message.get("X-Wallace-Footer") except: pass if _footer_added == "YES": exec('modules.cb_action_%s(%r, %r)' % ('ACCEPT','footer', filepath)) return if message.is_multipart(): if message.get_content_type() == "multipart/alternative": log.debug("The message content type is multipart/alternative.") for part in message.walk(): disposition = None try: content_type = part.get_content_type() except: continue try: disposition = part.get("Content-Disposition") except: pass if not disposition == None: continue if content_type == "text/plain": content = part.get_payload() content += "\n\n--\n%s" % (footer['plain']) part.set_payload(content) footer_added = True elif content_type == "text/html": content = part.get_payload() content += "\n<!-- footer appended by Wallace -->\n" content += "\n<html><body><hr />%s</body></html>\n" % (footer['html']) part.set_payload(content) footer_added = True else: # Check the main content-type. if message.get_content_type() == "text/html": content = message.get_payload() content += "\n<!-- footer appended by Wallace -->\n" content += "\n<html><body><hr />%s</body></html>\n" % (footer['html']) message.set_payload(content) footer_added = True else: content = message.get_payload() content += "\n\n--\n%s" % (footer['plain']) message.set_payload(content) footer_added = True if footer_added: log.debug("Footer attached.") message.add_header("X-Wallace-Footer", "YES") (fp, new_filepath) = tempfile.mkstemp(dir="/var/spool/pykolab/wallace/footer/ACCEPT") os.write(fp, message.as_string()) os.close(fp) os.unlink(filepath) exec('modules.cb_action_%s(%r, %r)' % ('ACCEPT','footer', new_filepath))
def execute(*args, **kw): if not os.path.isdir(mybasepath): os.makedirs(mybasepath) for stage in ['incoming', 'ACCEPT']: if not os.path.isdir(os.path.join(mybasepath, stage)): os.makedirs(os.path.join(mybasepath, stage)) # TODO: Test for correct call. filepath = args[0] if kw.has_key('stage'): log.debug(_("Issuing callback after processing to stage %s") % (kw['stage']), level=8) log.debug(_("Testing cb_action_%s()") % (kw['stage']), level=8) if hasattr(modules, 'cb_action_%s' % (kw['stage'])): log.debug(_("Attempting to execute cb_action_%s()") % (kw['stage']), level=8) exec('modules.cb_action_%s(%r, %r)' % (kw['stage'], 'gpgencrypt', filepath)) log.debug(_("Executing module gpgencrypt for %r, %r") % (args, kw), level=8) new_filepath = os.path.join( '/var/spool/pykolab/wallace/gpgencrypt/incoming', os.path.basename(filepath)) if not filepath == new_filepath: log.debug("Renaming %r to %r" % (filepath, new_filepath)) os.rename(filepath, new_filepath) filepath = new_filepath # parse message headers # @TODO: make sure we can use True as the 2nd argument here message = Parser().parse(open(filepath, 'r'), True) # Possible gpgencrypt answers are limited to ACCEPT only answers = ['ACCEPT'] # from Mail::GnuPG.is_encrypted # #sub is_encrypted { # my ($self,$entity) = @_; # return 1 # if (($entity->effective_type =~ m!multipart/encrypted!) # || # ($entity->as_string =~ m!^-----BEGIN PGP MESSAGE-----!m)); # return 0; #} message_already_encrypted = False for part in message.walk(): if part.get_content_type() in ["application/pgp-encrypted"]: message_already_encrypted = True log.debug( _("Message is already encrypted (app/pgp-enc content-type)"), level=8) if message.get_content_type() in ["multipart/encrypted"]: message_already_encrypted = True log.debug(_("Message already encrypted by main content-type header"), level=8) if message_already_encrypted: return filepath try: # What are recipient addresses to encrypt to (bitmask)? # 1 - organization key # 2 - envelope to # 4 - to # 8 - cc # 16 - resent-to # 32 - resent-cc encrypt_to_rcpts = conf.get('wallace', 'gpgencrypt_to_rcpts') if encrypt_to_rcpts == None: encrypt_to_rcpts = 14 else: encrypt_to_rcpts = (int)(encrypt_to_rcpts) # Only encrypt to keys that are trusted strict_crypt = conf.get('wallace', 'gpgencrypt_strict') if strict_crypt == None: strict_crypt = False # Organization key ID if encrypt_to_rcpts & 1: encrypt_to_org = conf.get('wallace', 'gpgencrypt_to_org_key') if encrypt_to_org == None and encrypt_to_rcpts & 1: if strict_crypt: log.error( _("Configured to encrypt to a key not configured, and strict policy enabled. Bailing out." )) modules.cb_action_REJECT('gpgencrypt', filepath) else: log.error( _("Configured to encrypt to a key not configured, but continuing anyway (see 'gpgencrypt_strict')." )) else: encrypt_to_org = [] # Bounce the message if encryption fails? force_crypt = conf.get('wallace', 'gpgencrypt_force') if force_crypt == None: force_crypt = False # Retrieve keys from remote server(s) automatically? retrieve_keys = conf.get('wallace', 'gpgencrypt_retrieve_keys') if retrieve_keys == None: retrieve_keys = False if retrieve_keys: gpgserver = conf.get('wallace', 'gpgencrypt_server') if gpgserver == None: gpgserver = 'pgp.mit.edu' encrypt_to = [] if encrypt_to_rcpts & 2: encrypt_to.extend(message.get_all('X-Kolab-To', [])) if encrypt_to_rcpts & 4: encrypt_to.extend(message.get_all('to', [])) if encrypt_to_rcpts & 8: encrypt_to.extend(message.get_all('cc', [])) if encrypt_to_rcpts & 16: encrypt_to.extend(message.get_all('resent-to', [])) if encrypt_to_rcpts & 32: encrypt_to.extend(message.get_all('resent-cc', [])) recipients = [ address for displayname, address in getaddresses(encrypt_to) ] log.debug(_("Recipients: %r") % (recipients)) # Split between recipients we can encrypt for/to, and ones we can not encrypt_rcpts = [] nocrypt_rcpts = [] gpg = gnupg.GPG(gnupghome='/var/lib/kolab/.gnupg', verbose=conf.debuglevel > 8) gpg.encoding = 'utf-8' local_keys = gpg.list_keys() log.debug(_("Current keys: %r") % (local_keys), level=8) for recipient in recipients: key_local = False log.debug(_("Retrieving key for recipient: %r") % (recipient)) for key in local_keys: for address in [ x for x in [ address for displayname, address in getaddresses( key['uids']) ] if x == recipient ]: log.debug(_("Found matching address %r") % (address)) key_local = key['keyid'] if key_local == False: if retrieve_keys: remote_keys = gpg.search_keys(recipient, gpgserver) if len(remote_keys) == 1: for address in [ x for x in [ address for displayname, address in getaddresses(remote_keys[0]['uids']) ] if x == recipient ]: log.debug( _("Found matching address %r in remote keys") % (address)) gpg.recv_keys(gpgserver, remote_keys[0]['keyid']) local_keys = gpg.list_keys() else: nocrypt_rcpts.append(recipient) for key in local_keys: for address in [ x for x in [ address for displayname, address in getaddresses( key['uids']) ] if x == recipient ]: log.debug(_("Found matching address %r") % (address)) key_local = key['keyid'] if not key_local == False: encrypt_rcpts.append(key_local) payload = message.get_payload() #print "payload:", payload if len(encrypt_rcpts) < 1: return filepath if "multipart" in message.get_content_type(): log.debug(_( "Mime Message - we need to build multipart/encrypted structure" ), level=8) msg = message enc_mime_message = pgp_mime(msg, encrypt_rcpts) message = enc_mime_message else: log.debug(_("No Mime Message - encypt plain"), level=8) encrypted_data = gpg.encrypt(payload, encrypt_rcpts, always_trust=True) encrypted_string = str(encrypted_data) message.set_payload(encrypted_string) message.add_header('X-wallace-gpg-encrypted', 'true') (fp, new_filepath) = tempfile.mkstemp( dir="/var/spool/pykolab/wallace/gpgencrypt/ACCEPT") os.write(fp, message.as_string()) os.close(fp) os.unlink(filepath) exec('modules.cb_action_%s(%r, %r)' % ('ACCEPT', 'gpgencrypt', new_filepath)) except Exception, errmsg: log.error(_("An error occurred: %r") % (errmsg)) if conf.debuglevel > 8: import traceback traceback.print_exc()
mail.attach(part) os.remove(f) mail_files[0] = (mail_files[0][0], mail) else: print "Can't decide how to bundle the %s non-mail files among the %s mails. Skip the folder..." % (len(non_mail_files), len(mail_files)) continue parent_folder_name = os.path.split(path)[-1] parent_folders = path.split(os.sep)[::-1] for (filepath, mail) in mail_files: # The mail was saved from an IMP account if mail.get('X-Mailer-Version', None) == 'v3.57 (r)' and not mail.get('From', None) and not mail.get('Date', None): mail.add_header('From', '*****@*****.**') # Get the main body of the mail mail_body = None if mail.is_multipart(): for payload in mail.get_payload(): if isinstance(payload, email.mime.text.MIMEText): mail_body = payload.get_payload() break else: mail_body = mail.get_payload() assert type(mail_body) == type('') # Find the hard coded date date = None # Normalize line endings mail_body = mail_body.replace('\r\n', '\n') cleaned_body = ''
mail_files[0] = (mail_files[0][0], mail) else: print "Can't decide how to bundle the %s non-mail files among the %s mails. Skip the folder..." % ( len(non_mail_files), len(mail_files)) continue parent_folder_name = os.path.split(path)[-1] parent_folders = path.split(os.sep)[::-1] for (filepath, mail) in mail_files: # The mail was saved from an IMP account if mail.get( 'X-Mailer-Version', None) == 'v3.57 (r)' and not mail.get( 'From', None) and not mail.get('Date', None): mail.add_header('From', '*****@*****.**') # Get the main body of the mail mail_body = None if mail.is_multipart(): for payload in mail.get_payload(): if isinstance(payload, email.mime.text.MIMEText): mail_body = payload.get_payload() break else: mail_body = mail.get_payload() assert type(mail_body) == type('') # Find the hard coded date date = None # Normalize line endings mail_body = mail_body.replace('\r\n', '\n') cleaned_body = ''
def header_check (db, cl, nodeid, new_values) : """ Check header of new messages and determine original customer from that header -- only if sender is the support special account (any account with system status). If send_to_customer flag is set *and* account is not a system account, munge the headers and add X-ROUNDUP-TO and X-ROUNDUP-CC headers. """ send_to_customer = False # Be sure to alway set send_to_customer to False! if 'send_to_customer' in new_values : send_to_customer = new_values ['send_to_customer'] new_values ['send_to_customer'] = False newmsgs = new_values.get ('messages') if not newmsgs : return newmsgs = set (newmsgs) if nodeid : oldmsgs = set (cl.get (nodeid, 'messages')) else : oldmsgs = set () system = db.user_status.lookup ('system') cemail = db.contact_type.lookup ('Email') for msgid in newmsgs.difference (oldmsgs) : msg = db.msg.getnode (msgid) h = None if msg.header : h = Parser ().parsestr (msg.header, headersonly = True) else : h = Message () if db.user.get (msg.author, 'status') == system : frm = fix_emails (h.get_all ('From')) subj = header_utf8 (h.get_all ('Subject') [0]) if ( frm and 'customer' not in new_values and 'emails' not in new_values ) : cc = {} if not nodeid : # use only first 'From' address (there shouldn't be more) rn, mail = getaddresses (frm) [0] # the *to* address in this mail is the support user we # want as a from-address for future mails *to* this user autad = None hto = fix_emails (h.get_all ('To')) if hto : torn, autad = getaddresses (hto) [0] if not autad.startswith ('support') : autad = None c = find_or_create_contact \ (db, mail, rn, frm = autad, subject = subj) cust = new_values ['customer'] = \ db.contact.get (c, 'customer') new_values ['emails'] = [c] else : supi = cl.getnode (nodeid) cust = supi.customer new_values ['emails'] = supi.emails new_values ['cc_emails'] = supi.cc_emails if supi.cc : cc = dict.fromkeys \ (x.strip ().lower () for x in supi.cc.split (',')) # Parse To and CC headers to find more customer email # addresses. Check if these contain the same domain # part as the From. ccs = h.get_all ('CC') or [] tos = h.get_all ('To') or [] if nodeid : tos.extend (frm) cfrm = db.customer.get (cust, 'fromaddress') alltocc = dict.fromkeys (new_values ['emails']) if 'cc_emails' in new_values : alltocc.update (dict.fromkeys (new_values ['cc_emails'])) for addrs, field in ((tos, 'emails'), (ccs, 'cc_emails')) : addrs = fix_emails (addrs) for rn, mail in getaddresses (addrs) : if mail == cfrm : continue c = find_or_create_contact \ (db, mail, rn, customer = cust) if c : if field not in new_values : new_values [field] = [] if c not in alltocc : new_values [field].append (c) alltocc [c] = 1 elif uidFromAddress (db, (rn, mail), create = 0) : # ignore internal addresses pass else : cc [mail.lower ()] = 1 if cc : new_values ['cc'] = ', '.join (cc.keys ()) else : if send_to_customer : mails = [] cc = [] if 'emails' in new_values : mails = new_values ['emails'] elif nodeid : mails = cl.get (nodeid, 'emails') if 'cc_emails' in new_values : mcc = new_values ['cc_emails'] elif nodeid : mcc = cl.get (nodeid, 'cc_emails') mails = mails or [] mcc = mcc or [] mails = (db.contact.get (x, 'contact') for x in mails) mcc = (db.contact.get (x, 'contact') for x in mcc) if 'cc' in new_values : cc = new_values ['cc'] elif nodeid : cc = cl.get (nodeid, 'cc') m = ','.join (mails) mc = ','.join (mcc) if mc : if cc : mc = ','.join ((mc, cc)) else : mc = cc if not m and not mc : raise Reject, \ _ ("Trying to send to customer with empty CC and " "without configured contact-email for customer" ) if m : h.add_header ('X-ROUNDUP-TO', m) if mc : h.add_header ('X-ROUNDUP-CC', mc) if 'bcc' in new_values : bcc = new_values ['bcc'] elif nodeid : bcc = cl.get (nodeid, 'bcc') if bcc : h.add_header ('X-ROUNDUP-BCC', bcc) # Time-Stamp of first reply to customer if not nodeid or not cl.get (nodeid, 'first_reply') : new_values ['first_reply'] = Date ('.') h = h.as_string () if h != '\n' and h != msg.header : db.msg.set (msgid, header = h)
def construct(cls, raw_headers=None, from_email=None, to=None, cc=None, subject=None, headers=None, text=None, text_charset='utf-8', html=None, html_charset='utf-8', attachments=None): """ Returns a new AnymailInboundMessage constructed from params. This is designed to handle the sorts of email fields typically present in ESP parsed inbound messages. (It's not a generalized MIME message constructor.) :param raw_headers: {str|None} base (or complete) message headers as a single string :param from_email: {str|None} value for From header :param to: {str|None} value for To header :param cc: {str|None} value for Cc header :param subject: {str|None} value for Subject header :param headers: {sequence[(str, str)]|mapping|None} additional headers :param text: {str|None} plaintext body :param text_charset: {str} charset of plaintext body; default utf-8 :param html: {str|None} html body :param html_charset: {str} charset of html body; default utf-8 :param attachments: {list[MIMEBase]|None} as returned by construct_attachment :return: {AnymailInboundMessage} """ if raw_headers is not None: msg = Parser(cls, policy=default_policy).parsestr(raw_headers, headersonly=True) msg.set_payload(None) # headersonly forces an empty string payload, which breaks things later else: msg = cls() if from_email is not None: del msg['From'] # override raw_headers value, if any msg['From'] = from_email if to is not None: del msg['To'] msg['To'] = to if cc is not None: del msg['Cc'] msg['Cc'] = cc if subject is not None: del msg['Subject'] msg['Subject'] = subject if headers is not None: try: header_items = headers.items() # mapping except AttributeError: header_items = headers # sequence of (key, value) for name, value in header_items: msg.add_header(name, value) # For simplicity, we always build a MIME structure that could support plaintext/html # alternative bodies, inline attachments for the body(ies), and message attachments. # This may be overkill for simpler messages, but the structure is never incorrect. del msg['MIME-Version'] # override raw_headers values, if any del msg['Content-Type'] msg['MIME-Version'] = '1.0' msg['Content-Type'] = 'multipart/mixed' related = cls() # container for alternative bodies and inline attachments related['Content-Type'] = 'multipart/related' msg.attach(related) alternatives = cls() # container for text and html bodies alternatives['Content-Type'] = 'multipart/alternative' related.attach(alternatives) if text is not None: part = cls() part['Content-Type'] = 'text/plain' part.set_payload(text, charset=text_charset) alternatives.attach(part) if html is not None: part = cls() part['Content-Type'] = 'text/html' part.set_payload(html, charset=html_charset) alternatives.attach(part) if attachments is not None: for attachment in attachments: if attachment.is_inline_attachment(): related.attach(attachment) else: msg.attach(attachment) return msg