[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[minion-cvs] Full SMTP/POP3 proxy for sending and receiving mixminio...



Update of /home/minion/cvsroot/src/minion/etc
In directory moria.mit.edu:/tmp/cvs-serv12817

Modified Files:
	minionSMTP.py pop3d.py 
Added Files:
	IMAPproxy.py minionProxy.py mmUtils.py 
Log Message:
Full SMTP/POP3 proxy for sending and receiving mixminion messages.
Documentation to follow soon.



--- NEW FILE: IMAPproxy.py ---
import pop3d

import mmUtils


import sys
import os
import pwd
import errno
import getopt
import time
import socket
import asyncore
import asynchat

import re
import copy
import md5
import base64
import getpass
import imaplib
import cPickle
import email

def md5hash(m):
    md = md5.new()
    md.update(m)
    md_res = (base64.encodestring(md.digest()))
    return md_res[:22]

class IMAPproxy(pop3d.POP3Server):
    """ An IMAP to POP3 proxy that fetches anonymous mail

    It then delivers it to the POP3 mail box.
    """
    def __init__(self, localaddr, ser, passwd):
        "Server and Mixminion must be specified"
        self.__server = ser
        print 'IMAP login on %s' % (ser)
        self.__pass = passwd
        pop3d.POP3Server.__init__(self,localaddr)
        self.__cachedIDs = []
        
    # API for "doing something useful with the message"
    def get_pop3_messages(self, user, passd):

        # Get the IMAP messages
        try:
            M = imaplib.IMAP4(self.__server)
            M.login(user, passd)
            M.select()
            # Filter for anonymous messages 
            typ, data = M.search(None, '(HEADER "X-Anonymous" "yes")')
            ms = []
            for num in data[0].split():
                typ, data = M.fetch(num, '(RFC822)')
                ms = ms + [data[0][1]]
            M.logout()
        except M.error, inst:
            print "IMAP exception:",inst
            # TODO: Should really include an "error" message here.
            return []


        # Implement a list of previously seen messages.
        # The list of messages confirmed by the mail client
        if 'seen_files.dat' in os.listdir('.'):
            print 'loading seen file'
            seenlist = cPickle.load(file('seen_files.dat','r'))
        else:
            seenlist = []

        # Filter out messages that are already seen.
        ms = filter(lambda x:not md5hash(x) in seenlist,ms)
        self.__cachedIDs = map(lambda x: md5hash(x), ms)

        # Decodes the anonymous messages

        # The decode routine does not like '\r\n' so
        # I need to transform everything to '\n'
        ms = map(lambda x:re.sub('\r\n','\n',x),ms)

        # How to recognise a SURB:
        surbPat = re.compile('(?:- )?(-----BEGIN TYPE III REPLY BLOCK-----)([^\-]*)(?:- )?(-----END TYPE III REPLY BLOCK-----)',re.S)

         # The list of surbs cached by the client.
        if 'surb_file.dat' in os.listdir('.'):
            surb_file = cPickle.load(file('surb_file.dat','r'))
        else:
            surb_file = {}

        ms2 = []
        for m in ms:
            msg = email.message_from_string(m)
            # Decode the body of the message
            bx = mmUtils.decode(msg.get_payload(),passd)

            # By default allow no reply.
            reply_addrs = '%s@nym.taz' % 'anonymous'

            # Extract any SURBs and store them.
            rs = surbPat.findall(bx)
            rs = map(lambda (x,y,z): "%s%s%s" % (x,y,z),rs)

            if len(rs) > 0:
                bx = surbPat.sub('',bx)
                surb_file[md5hash(rs[0])[:10]] = rs
                reply_addrs = '%s@nym.taz' % md5hash(rs[0])[:10]

            # Set the reply addresses with none@nym.taz or the SURB IDs.
            del msg['Return-Path']
            msg['Return-Path'] = reply_addrs
            new_from = re.sub('([^<]*@[^>]*)',reply_addrs,msg['From'])
            del msg['From']
            msg['From'] = new_from
            msg.set_payload(bx)
            ms2 += [msg.as_string()]

        # Add '\r\n' back at the end of each line!
        m2 = map(lambda x:re.sub('\n','\r\n',x),ms2)

        # Save the SURBs
        cPickle.dump(surb_file,file('surb_file.dat','w'))

        return m2

    def set_pop3_messages(self, user, msgs):
        # Loads the list of seen (by the client) messages 
        seenlist = []
        if 'seen_files.dat' in os.listdir('.'):
            seenlist = cPickle.load(file('seen_files.dat','r'))
        else:
            seenlist = []

        # Stores the IDs of seen messages
        for (i,(d,m)) in zip(range(len(msgs)),msgs):
            if d:
                seenlist += [self.__cachedIDs[i]]

        cPickle.dump(seenlist,file('seen_files.dat','w'))
        self.__cachedIDs =[]
        return None # No errors


if __name__ == '__main__':
    import __main__
    proxy = IMAPproxy(('127.0.0.1', 20110),'imap.hermes.cam.ac.uk',getpass.getpass())

    try:
        asyncore.loop()
    except KeyboardInterupt:
        pass

--- NEW FILE: minionProxy.py ---
from IMAPproxy import *
from minionSMTP import *
import getpass

import asyncore

imap_address = 'imap.hermes.cam.ac.uk'
local_host = '127.0.0.1'
smtp_port = 20025
imap_port = 20110

if __name__ == '__main__':
    import __main__
    print 'Mixminion password:'
    mm_Pass = getpass.getpass()
    proxy1 = IMAPproxy((local_host, imap_port),imap_address,mm_Pass)
    proxy2 = minionSMTP((local_host,smtp_port),mm_Pass)
    
    try:
        asyncore.loop()
    except KeyboardInterupt:
        pass

--- NEW FILE: mmUtils.py ---
# Implmentes the mixminion interface.
import os, sys
import re

# Give it a list of ommands and what should go in the std input
# it returns what appeared in the std output.
# PRIVATE: DO NOT CALL FROM OUTSIDE THIS MODULE!!!
def mm_command(cmd, in_str = None, show_stderr = 1):
    c = cmd

    if show_stderr == 1:
        (sout,sin) = os.popen4(c)
    else:
        (sout,sin,serr) = os.popen3(c)

    if in_str != None:
        sout.write(in_str+'\n')
        sout.close()

    result = sin.read()
    return result

# provides a single use reply block
# If an error occus it return an empty list '[]'
def getSURB(addrs,login,passwd):
    rs = mm_command(['mixminion','generate-surb','--identity=%s'%login,'-t',addrs], passwd)
    surbPat = re.compile('-----BEGIN TYPE III REPLY BLOCK-----[^\-]*-----END TYPE III REPLY BLOCK-----',re.S)
    rs = surbPat.findall(rs)
    return rs

# routine to decode a received mixminion message
# If there is an error the empty string is returned.
def decode(msg,passwd):
    decPat = re.compile('-----BEGIN TYPE III ANONYMOUS MESSAGE-----\r?\nMessage-type: (plaintext|encrypted)(.*)-----END TYPE III ANONYMOUS MESSAGE-----\r?\n',re.S)
    mtc = decPat.search(msg)
    if mtc != None:
        f = open('__tempMM','w')
        f.write(mtc.group(0))
        f.close()
        rs = mm_command(['mixminion','decode','-i','__tempMM'], passwd, 0)
        # os.remove('__tempMM')
    rs.strip('\n')
    return rs+'\n'
    # Delete file!

# Simply sends a message
def send(msg,addrs,cmd):
    f = open('__msgMM','w')
    f.write(msg)
    f.close()

    rs = mm_command(['mixminion','send','-i','__msgMM','-t',addrs]+cmd, None)
    os.remove('__msgMM')
    return rs

# routine to send a message using mixminion.
def reply(msg,surb,cmd):
    f = open('__msgMM','w')
    f.write(msg)
    f.close()

    f = open('__surbMM','w')
    f.write(surb)
    f.close()

    rs = mm_command(['mixminion','send','-i','__msgMM','-R','__surbMM']+cmd, None)
    os.remove('__msgMM')
    os.remove('__surbMM')
    return rs
    # Delete files !!

# Old debugging information
if __name__ == '__main__':
    import getpass
    sb = getSURB('gd216@cl.cam.ac.uk',getpass.getpass())
    reply('Hello world\nThis is my message\n',sb[0])

# print rs

Index: minionSMTP.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/etc/minionSMTP.py,v
retrieving revision 1.2
retrieving revision 1.3
diff -u -d -r1.2 -r1.3
--- minionSMTP.py	18 Jan 2004 08:16:21 -0000	1.2
+++ minionSMTP.py	5 Mar 2004 17:42:31 -0000	1.3
@@ -39,12 +39,17 @@
 import email
 import re
 
+import mmUtils
+import getpass
+import cPickle
+
 program = sys.argv[0]
 __version__ = 'Mixminion SMTP proxy - 0.0.1'
 
 class minionSMTP(smtpd.SMTPServer):
 
-    def __init__(self, localaddr):
+    def __init__(self, localaddr,passwd):
+        self.__passwd = passwd
         smtpd.SMTPServer.__init__(self,localaddr,localaddr)
         print '%s started at %s\n\tLocal addr: %s\n\t' % (
             self.__class__.__name__, time.ctime(time.time()),
@@ -60,6 +65,8 @@
         and nickname contained in the headers are extracted and sent along. 
         """
 
+        # print peer,mailfrom,rcpttos,data
+
         # Use the email package to extract headers and body.
         msg = email.message_from_string(data)
 
@@ -69,20 +76,18 @@
         else:
             subject = ''
 
-        # Extract "from" field nickname
+        # Extract "from" field nickname and return address
 	import re
+        nickname = ''
+        retaddrs = None
         if 'from' in msg:
-            m = re.search("(.*)<", msg['from'])
-            try:
-                nickname = m.group(1).strip();
-                nickname = nickname.strip('\"')
-                if nickname.find('@') != -1:
-                    nickname = ''
-            except AttributeError:
-                # The string must be empty
-                nickname = ''
-        else:
-            nickname = ''
+            m = re.search('^([^@<]*)', msg['from'])
+            if m != None:
+                nickname = m.group(1).strip()
+
+            m = re.search('([^< ]*@[^> ]*)', msg['from'])
+            if m != None:
+                retaddrs = m.group(1).strip()
 
         print "Started sending"
 
@@ -102,34 +107,63 @@
             print "No body found - make sure you send some text/plain"
             return "501 no text/plain body found"
 
+        if retaddrs != None:
+            surb = mmUtils.getSURB(retaddrs,nickname,self.__passwd)
+            body = body +'\n'+surb[0]
+
         # Base mixminion command
-        cmd = ['mixminion', 'send']
+        cmd = []
         
         # Augment the command with a nickname
         if nickname != '':
-            cmd.append('--from=%s'%nickname)
+            cmd.append('--from=%s' % nickname)
 
         if subject != '':
-            cmd.append('--subject=%s'%subject)
+            cmd.append('--subject=%s' % subject)
 
         for address in rcpttos:
-            # For each address it sends the message using mixminion.
-            cmdFull = cmd + ['-t', address]
-            (sout,sin) = os.popen2(cmdFull)
-            print cmdFull
-            print body
-            sout.write(body)
-            sout.close()
+            taz = re.findall('([^@]*)@nym.taz',address)
 
-            # Check that mixminion confirms sending otherwise returns
-            # an 502 error code.
-            result = sin.read()
-            m = re.search("... message sent", result)
-            if m != None:
-                return "502 Mixminion did not confirm sending"
+            # Reply to anonymous sender case.
+            if len(taz) > 0:
+                surb_id = taz[0]
+                if surb_id == 'anonymous':
+                    # TODO: send back an error message
+                    print 'Cannot send to anonymous'
+                    continue
+                surb_file = {}
+                if 'surb_file.dat' in os.listdir('.'):
+                    surb_file = cPickle.load(file('surb_file.dat','r'))
+
+                if surb_file.has_key(surb_id):
+                    surb_list = surb_file[surb_id]
+                    if len(surb_list) == 0:
+                        # Send back an error message
+                        print 'No more SURBs available'
+                        del surb_file[surb_id]
+                    else:
+                        result = mmUtils.reply(body,surb_list[0],cmd)
+                        print result
+                        m = re.search("sent", result)
+            
+                        if m == None:
+                            return "502 Mixminion did not confirm sending"
+                        else:
+                            surb_file[surb_id] = surb_list[1:]
+                            print "Done"
+
+                    cPickle.dump(surb_file,file('surb_file.dat','w'))
+                else:
+                    print 'No address known for: %s@nym.taz'%taz[0]
             else:
-                pass
-            print "Done"
+                # For each address it sends the message using mixminion.
+                result = mmUtils.send(body,address,cmd)
+                m = re.search("sent", result)
+            
+                if m == None:
+                    return "502 Mixminion did not confirm sending"
+                else:
+                    print "Done"
         # raise UnimplementedError
 
 if __name__ == '__main__':
@@ -167,7 +201,7 @@
     except ValueError:
         print 'Bad local port: %s' % localspec
 
-    proxy = minionSMTP((localhost,localport))
+    proxy = minionSMTP((localhost,localport),getpass.getpass())
     # proxy = smtpd.DebuggingServer(('127.0.0.1',20025),None)
     try:
         asyncore.loop()

Index: pop3d.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/etc/pop3d.py,v
retrieving revision 1.1
retrieving revision 1.2
diff -u -d -r1.1 -r1.2
--- pop3d.py	28 Feb 2004 20:54:52 -0000	1.1
+++ pop3d.py	5 Mar 2004 17:42:31 -0000	1.2
@@ -23,6 +23,7 @@
 # - Refactor UIDL + LIST
 # - Refactor TOP + RETR
 # - Unify the checks for command parameters
+# - Make a command line interface
 
 import sys
 import os
@@ -39,6 +40,7 @@
 import md5
 import base64
 
+
 class Devnull:
     def write(self, msg): pass
     def flush(self): pass
@@ -335,7 +337,22 @@
             return
         
         if self.__state == self.TRAN:
-            status = self.__server.set_pop3_messages(self.__username, self.__messages)
+            # We will do something more smart than just returning the
+            # remaining messages. We will return tuple (0|1,m) indicating
+            # that a message has been deleted or not.
+
+            # the remaining messages
+            m = map(lambda (x,y): y, self.__messages)
+
+            # The list we will return
+            ret = []
+            for (x,msg) in self.__oldmessages:
+                if msg in m:
+                    ret += [(0,msg)]
+                else:
+                    ret += [(1,msg)]
+            
+            status = self.__server.set_pop3_messages(self.__username, ret)
             if status == None:
                 self.push('+OK Bye')
             else:
@@ -481,8 +498,8 @@
         If an empty sequence in returned there are no messages.
 
         """
-        # Sample implemtation always provides one message.
-        return ['from: x@cl.cam.ac.uk\nTo: gd216@cam.ac.uk\nSubject: Hello\n\nWhat\ncan\nI\ndo\nfor\nyou']
+        return []
+    
         raise UnimplementedError
 
     def set_pop3_messages(self, user, msgs):
@@ -492,15 +509,9 @@
         the messages left, otherwise it should return an error string.
 
         """
+      
         # sample implementation simply prints messages
         print 'New box', user, msgs
         return None # No errors
 
-if __name__ == '__main__':
-    import __main__
-    proxy = POP3Server(('127.0.0.1', 20110))
 
-    try:
-        asyncore.loop()
-    except KeyboardInterupt:
-        pass