[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[minion-cvs] Start of server impl, module manager, serverinfo genera...



Update of /home/minion/cvsroot/src/minion/lib/mixminion
In directory moria.seul.org:/tmp/cvs-serv21049/minion/lib/mixminion

Modified Files:
	BuildMessage.py Config.py MMTPClient.py MMTPServer.py 
	Modules.py Packet.py Queue.py ServerInfo.py test.py 
Added Files:
	ServerMain.py 
Log Message:
Start of server impl, module manager, serverinfo generation, and much more.

ServerMain:
	New file to hold main loop and state for server.  Will need to be
		refactored.

BuildMessage, Packet, test:
	Rename Local to MBOX

Config:
	Support for loadable modules

MMTPClient, MMTPServer:
	Change magic strings to match spec changes

MMTPServer:
	Blow up more intelligently on bad protocol list.
	Handle new padding format
	Add message to sent callback
	Add first cut of generic MMTPServer class

Modules:
	Add new module manager code, and an example MBOX module.

Queue:
	Log queue creation.
	Worry more about directory permissions

ServerInfo:
	Deal with spec changes; move MBox into a module

crypt.c:
	Accept (but ignore) bits argument on DH generation

tls.c:
	Add separate 'server mode' for sockets to allow incoming cipher
		suites that we'd otherwise reject.



--- NEW FILE: ServerMain.py ---
# Copyright 2002 Nick Mathewson.  See LICENSE for licensing information.
# $Id: ServerMain.py,v 1.1 2002/08/06 16:09:21 nickm Exp $

"""mixminion.ServerMain

   The main loop and related functionality for a Mixminion server

   BUG: No support for public key encryption"""

from mixminion.Common import getLog, MixFatalError, MixError

# Directory layout:
#     MINION_HOME/work/queues/incoming/
#                             mix/
#                             outgoing/
#                             deliver/mbox/
#                      tls/dhparam
#                      hashlogs/hash_1 ...
#                 log
#                 keys/key_1/ServerDesc
#                            mix.key
#                            mmtp.key
#                            mmtp.cert
#                      key_2/...
#                 conf/miniond.conf 
#                       ....

def createDir(d):
    if not os.path.exists(d):
        try:
            os.mkdir(d, 0700)
        except OSError, e:
            getLog().fatal("Unable to create directory %s"%d)
            raise MixFatalError()
    elif not os.path.isdir(d):
        getLog().fatal("%s is not a directory"%d)
        raise MixFatalError()
    else:
        m = os.stat(d)[stat.ST_MODE]
        # check permissions
        if m & 0077:
            getLog().fatal("Directory %s must be mode 0700" %d)
            raise MixFatalError()

class ServerState:
    # config
    # log
    # homedir
    def __init__(self, config):
        self.config = config

        #XXXX DOCDOC
        # set up directory structure.
        c = self.config
        self.homedir = c['Server']['Homedir']
        createDir(self.homedir)
        getLog()._configure() # ????
        
        w = os.path.join(self.homeDir, "work")
        q = os.path.join(w, "queues")
        self.incomingDir = os.path.join(q, "incoming")
        self.mixDir = os.path.join(q, "mix")
        self.outgoingDir = os.path.join(q, "outgoing")
        self.deliverDir = os.path.join(q, "deliver")
        self.deliverMBOXDir = os.path.join(self.deliverDir, "mbox")

        tlsDir = os.path.join(w, "tls")
        self.hashlogsDir = os.path.join(w, "hashlogs")
        self.keysDir = os.path.join(self.homeDir, "keys")
        self.confDir = os.path.join(self.homeDir, "conf")
        
        for d in [self.homeDir, w, q, self.incomingDir, self.mixDir,
                  self.outgoingDir, self.deliverDir, tlsDir,
                  self.hashlogsDir, self.keysDir, self.confDir]:
            createDir(d)

        for name in ("incoming", "mix", "outgoing", "deliverMBOX"):
            loc = getattr(self, name+"Dir")
            queue = mixminion.Queue.Queue(loc, create=1, scrub=1)
            setattr(self, name+"Queue", queue)

        self.dhFile = os.path.join(tlsDir, "dhparam")

        self.checkKeys()

    def getDHFile(self):
        if not os.path.exists(self.dhFile):
            getLog().info("Generating Diffie-Helman parameters for TLS...")
            mixminion._minionlib.generate_dh_parameters(self.dhFile, verbose=0)
            getLog().info("...done")

        return self.dhFile

    def checkKeys(self):
        self.keyIntervals = [] # list of start, end, keysetname
        for dirname in os.listdir(self.keysDir):
            if not dirname.startswith('key_'):
                continue
            keysetname = dirname[4:]
            
            d = os.path.join(self.keysDir, dirname)
            si = os.path.join(self.keysDir, "ServerDesc")
            if os.path.exists(si):
                inf = mixminion.ServerInfo.ServerInfo(fname=si, assumeValid=1)
                t1 = inf['Server']['Valid-After']
                t2 = inf['Server']['Valid-Until']
                self.keyIntervals.append( (t1, t2, keysetname) ) 

        self.keyIntervals.sort()

    def removeDeadKeys(self):
        now = time.time()
        cutoff = now - config['Server']['PublicKeySloppiness']
        names = [ os.path.join(self.keyDir,"key_"+name)
                  for va, vu, name in self.keyIntervals if vu < cutoff ]
        # XXXX DELETE KEYS
        
    def _getLiveKey(self):
        now = time.time()
        idx = bisect.bisect_left(self.keyIntervals, (now, None, None))
        return self.keyIntervals[idx]

    def getNextKeyRotation(self):
        return self._getLiveKey()[1]

    def getServerKeys(self):
        keyset = self._getLiveKey()[2]
        sk = mixminion.ServerInfo.ServerKeys(self.keyDir, keyset,
                                             self.hashlogsDir)
        sk.load()
        return sk

    def getTLSContext(self):
        # XXXX NO SUPPORT FOR ROTATION
        keys = self.getServerKeys()
        return mixminion._minionlib.TLSContext_new(keys.certFile,
                                                   keys.mmtpKey,
                                                   self.dhFile)
    
    def getPacketHandler(self):
        keys = self.getServerKeys()
        return mixminion.PacketHandler.PacketHandler(keys.packetKey,
                                                     keys.hashlogFile)

    def getIncomingQueues(self):
        return self.incomingQueue

    def getOutgoingQueue(self):
        return self.outgoingQueue

    def getMixQueue(self):
        return self.mixQueue

    def getDeliverMBOXQueue(self, which):
        return self.deliverMBOXQueue
        

class _Server(MMTPServer):
    def __init__(self, config, serverState):
        self.incomingQueue = serverState.getIncomingQueue()
        self.outgoingQueue = serverState.getOutgoingQueue()
        MMTPServer.__init__(self, config)
        
    def onMessageReceived(self, msg):
        self.incomingQueue.queueMessage(msg)

    def onMessageSent(self, msg):
        self.outgoingQueue.remove
    

def runServer(config):
    s = ServerState(config)
    packetHandler = s.getPacketHandler()
    context = s.getTLSContext()

    shouldProcess = len(os.listdir(s.incomingDir))
    shouldSend = len(os.listdir(s.outgoingDir))
    shouldMBox = len(os.listdir(s.deliverMBOXDir))
    # XXXX Make these configurable; make mixing OO.
    mixInterval = 60
    mixPoolMinSize = 5
    mixPoolMaxRate = 5 

    nextMixTime = time.time() + mixInterval

    server = mixminion.MMTPServer.MMTPServer(config)

    while 1:
        
        

Index: BuildMessage.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/BuildMessage.py,v
retrieving revision 1.10
retrieving revision 1.11
diff -u -d -r1.10 -r1.11
--- BuildMessage.py	26 Jul 2002 20:52:17 -0000	1.10
+++ BuildMessage.py	6 Aug 2002 16:09:21 -0000	1.11
@@ -70,7 +70,7 @@
                   path: a list of ServerInfo objects
                   user: the user's username/email address
                   userKey: an AES key to encrypt the seed, or None.
-                  email: If true, delivers via SMTP; else delivers via LOCAL.
+                  email: If true, delivers via SMTP; else delivers via MBOX
        """
     #XXXX Out of sync with the spec.
     if email and userKey:
@@ -86,8 +86,8 @@
         exitType = Modules.SMTP_TYPE
         exitInfo = SMTPInfo(user, "RTRN"+tag).pack()
     else:
-        exitType = Modules.LOCAL_TYPE
-        exitInfo = LocalInfo(user, "RTRN"+tag).pack()
+        exitType = Modules.MBOX_TYPE
+        exitInfo = MBOXInfo(user, "RTRN"+tag).pack()
 
     prng = Crypto.AESCounterPRNG(seed)
     return buildReplyBlock(path, exitType, exitInfo, expiryTime, prng)[0]

Index: Config.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Config.py,v
retrieving revision 1.6
retrieving revision 1.7
diff -u -d -r1.6 -r1.7
--- Config.py	28 Jul 2002 22:42:33 -0000	1.6
+++ Config.py	6 Aug 2002 16:09:21 -0000	1.7
@@ -28,7 +28,6 @@
    Key3 = value3
    # A comment
    Key4=value4
-
    [Section2]
    Key5 value5
       value5 value5 value5
@@ -53,6 +52,7 @@
 import re
 import binascii
 import time
+import copy
 from cStringIO import StringIO
 
 import mixminion.Common
@@ -422,6 +422,7 @@
     #  _sections: A map from secname->key->value.
     #  _sectionEntries: A  map from secname->[ (key, value) ] inorder.
     #  _sectionNames: An inorder list of secnames.
+    #  _callbacks: XXXX DOC
     #
     # Fields to be set by a subclass:
     #     _syntax is map from sec->{key:
@@ -451,6 +452,9 @@
            steps.  (Use this to load a file that's already been checked as
            valid.)"""
         assert filename is None or string is None
+        if not hasattr(self, '_callbacks'):
+            self._callbacks = {}
+
         self.assumeValid = assumeValid
         self.fname = filename
         if filename:
@@ -568,6 +572,10 @@
                         assert rule == 'ALLOW*'
                         section[k] = map(parseFn,default)
 
+            cb = self._callbacks.get(secName, None)
+            if cb:
+                cb(section, sectionEntries)
+
         # Check for missing required sections, setting any missing
         # allowed sections to {}.
         for secName, secConfig in self._syntax.items():
@@ -595,6 +603,11 @@
         self._sectionEntries = self_sectionEntries
         self._sectionNames = self_sectionNames
 
+    def _addCallback(self, section, cb):
+        if not hasattr(self, '_callbacks'):
+            self._callbacks = {}
+        self._callbacks[section] = cb
+
     def validate(self, sections, sectionEntries, entryLines,
 		 fileContents):
         """Check additional semantic properties of a set of configuration
@@ -652,9 +665,8 @@
         #XXXX Write this
         pass
 
-class ServerConfig(_ConfigFile):
-    _restrictFormat = 0
-    _syntax = {
+
+SERVER_SYNTAX =  {
         'Host' : ClientConfig._syntax['Host'],
         'Server' : { '__SECTION__' : ('REQUIRE', None, None),
                      'Homedir' : ('ALLOW', None, "/var/spool/minion"),
@@ -666,11 +678,13 @@
                                             "30 days"),
                      'PublicKeySloppiness': ('ALLOW', _parseInterval,
                                              "5 minutes"),
-                     'EncryptPublicKey' : ('REQUIRE', _parseBoolean, "no"),
+                     'EncryptPrivateKey' : ('REQUIRE', _parseBoolean, "no"),
                      'Mode' : ('REQUIRE', _parseServerMode, "local"),
                      'Nickname': ('ALLOW', None, None),
                      'Contact-Email': ('ALLOW', None, None),
                      'Comments': ('ALLOW', None, None),
+                     'ModulePath': ('ALLOW', None, None),
+                     'Module': ('ALLOW*', None, None),
                      },
         'DirectoryServers' : { 'ServerURL' : ('ALLOW*', None, None),
                                'Publish' : ('ALLOW', _parseBoolean, "no"),
@@ -684,18 +698,33 @@
         'Outgoing/MMTP' : { 'Enabled' : ('REQUIRE', _parseBoolean, "no"),
                             'Allow' : ('ALLOW*', _parseAddressSet_allow, None),
                             'Deny' : ('ALLOW*', _parseAddressSet_deny, None) },
-        'Delivery/MBOX' : { 'Enabled' : ('REQUIRE',  _parseBoolean, "no"),
-                            'AddressFile' : ('ALLOW', None, None),
-                            'Command' : ('ALLOW', _parseCommand, "sendmail") },
         }
+
+class ServerConfig(_ConfigFile):
+    _restrictFormat = 0
     # XXXX Missing: Queue-Size / Queue config options
     # XXXX         timeout options
     def __init__(self, fname=None, string=None):
+        self._syntax = SERVER_SYNTAX.copy()
+
+        import mixminion.Modules
+        self.moduleManager = mixminion.Modules.ModuleManager()
+        self._addCallback("Server", self.loadModules)    
+
         _ConfigFile.__init__(self, fname, string)
 
     def validate(self, sections, entries, lines, contents):
         #XXXX write this.
-        pass
+        self.moduleManager.validate(sections, entries, lines, contents)
+
+    def loadModules(self, section, sectionEntries):
+        self.moduleManager.setPath(section.get('ModulePath', None))
+        for mod in section.get('Module', []):
+            self.moduleManager.loadExtModule(mod)
+
+        self._syntax.update(self.moduleManager.getConfigSyntax())
+    
+        
     
 ##         if sections['Server']['PublicKeyLifeTime'][2] < 24*60*60:
 ##             raise ConfigError("PublicKeyLifetime must be at least 1 day.")

Index: MMTPClient.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/MMTPClient.py,v
retrieving revision 1.5
retrieving revision 1.6
diff -u -d -r1.5 -r1.6
--- MMTPClient.py	25 Jul 2002 15:52:57 -0000	1.5
+++ MMTPClient.py	6 Aug 2002 16:09:21 -0000	1.6
@@ -52,9 +52,9 @@
         ####
         # Protocol negotiation
         # For now, we only support 1.0
-        self.tls.write("PROTOCOL 1.0\r\n")
+        self.tls.write("MMTP 1.0\r\n")
         inp = self.tls.read(len("PROTOCOL 1.0\r\n"))
-        if inp != "PROTOCOL 1.0\r\n":
+        if inp != "MMTP 1.0\r\n":
             raise MixProtocolError("Protocol negotiation failed")
         
     def sendPacket(self, packet):

Index: MMTPServer.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/MMTPServer.py,v
retrieving revision 1.7
retrieving revision 1.8
diff -u -d -r1.7 -r1.8
--- MMTPServer.py	25 Jul 2002 15:52:57 -0000	1.7
+++ MMTPServer.py	6 Aug 2002 16:09:21 -0000	1.8
@@ -398,11 +398,11 @@
         return self.__con.get_peer_cert_pk()
     
 #----------------------------------------------------------------------
-# XXXX Need to support future protos.
-PROTOCOL_STRING      = "PROTOCOL 1.0\r\n"
-PROTOCOL_RE = re.compile("PROTOCOL ([^\s\r\n]+)\r\n")
-SEND_CONTROL         = "SEND\r\n" #XXXX Not as in spec
-RECEIVED_CONTROL     = "RECEIVED\r\n" #XXXX Not as in spec
+PROTOCOL_STRING      = "MMTP 1.0\r\n"
+PROTOCOL_RE = re.compile("MMTP ([^\s\r\n]+)\r\n")
+SEND_CONTROL         = "SEND\r\n"
+JUNK_CONTROL         = "JUNK\r\n"
+RECEIVED_CONTROL     = "RECEIVED\r\n"
 SEND_CONTROL_LEN     = len(SEND_CONTROL)
 RECEIVED_CONTROL_LEN = len(RECEIVED_CONTROL)
 SEND_RECORD_LEN      = len(SEND_CONTROL) + MESSAGE_LEN + DIGEST_LEN
@@ -429,7 +429,11 @@
         """
         trace("done w/ client sendproto")
         inp = self.getInput()
-        m = PROTOCOL_RE.match(inp)
+        m =PROTOCOL_RE.match(inp)
+
+        if not m:
+            warn("Bad protocol list.  Closing connection.")
+            self.shutdown(err=1)
         protocols = m.group(1).split(",")
         if "1.0" not in protocols:
             warn("Unsupported protocol list.  Closing connection.")
@@ -454,14 +458,24 @@
         msg = data[SEND_CONTROL_LEN:-DIGEST_LEN]
         digest = data[-DIGEST_LEN:]
 
-        if (not (data.startswith(SEND_CONTROL) and
-                 sha1(msg+"SEND") == digest)):
+        if data.startswith(JUNK_CONTROL):
+            expectedDigest = sha1(msg+"JUNK")
+            replyDigest = sha1(msg+"RECEIVED JUNK")
+        elif data.startswith(SEND_CONTROL):
+            expectedDigest = sha1(msg+"SEND")
+            replyDigest = sha1(msg+"RECEIVED")
+        else:
+            warn("Unrecognized command.  Closing connection.")
+            self.shutdown(err=1)
+            return
+        if expectedDigest != digest:
             warn("Invalid checksum. Closing connection.")
             self.shutdown(err=1)
+            return
         else:
             debug("Packet received; Checksum valid.")
             self.finished = self.__sentAck
-            self.beginWrite(RECEIVED_CONTROL+sha1(msg+"RECEIVED"))
+            self.beginWrite(RECEIVED_CONTROL+replyDigest)
             self.messageConsumer(msg)
 
     def __sentAck(self):
@@ -553,7 +567,7 @@
        """Called when we're done reading the ACK.  If the ACK is bad,
           closes the connection.  If the ACK is correct, removes the
           just-sent message from the connection's internal queue, and
-          calls sentCallback.
+          calls sentCallback with the sent message.
 
           If there are more messages to send, begins sending the next.
           Otherwise, begins shutting down.
@@ -566,9 +580,39 @@
            return
 
        debug("Received valid ACK for message.")
+       justSent = self.messageList[0]
        del self.messageList[0]
        if self.sentCallback is not None:
-           self.sentCallback()
+           self.sentCallback(justSent)
 
        self.beginNextMessage()
 
+class MMTPServer(AsyncServer):
+    "XXXX"
+    def __init__(self, config):
+        self.context = config.getTLSContext(server=1)
+        self.listener = ListenConnection("127.0.0.1",
+                                         config['Outgoing/MMTP']['Port']
+                                         10, self._newMMTPConnection)
+        self.config = config
+        self.listener.register(self)
+
+    def _newMMTPConnection(self, sock):
+        "XXXX"
+        # XXXX Check whether incoming IP is valid XXXX
+        tls = self.context.sock(sock, serverMode=1)
+        sock.setblocking(0)
+        con = MMTPServerConnection(sock, tls, self.onMessageReceived)
+        con.register(self)
+        
+    def sendMessages(self, ip, port, keyID, messages):
+        con = MMTPClientConnection(ip, port, keyID, messages,
+                                   self.onMessageSent)
+        con.register(self)
+
+    def onMessageReceived(self, msg):
+        pass
+
+    def onMessageSent(self, msg):
+        pass
+    

Index: Modules.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Modules.py,v
retrieving revision 1.2
retrieving revision 1.3
diff -u -d -r1.2 -r1.3
--- Modules.py	2 Jun 2002 06:11:16 -0000	1.2
+++ Modules.py	6 Aug 2002 16:09:21 -0000	1.3
@@ -5,6 +5,19 @@
 
    Type codes and dispatch functions for routing functionality."""
 
+#__all__ = [ 'ModuleManager' ]
+
+import os
+
+import mixminion.Config
+import mixminion.Packet
+from mixminion.Config import ConfigError, _parseBoolean, _parseCommand
+from mixminion.Common import getLog
+
+DELIVER_OK = 1
+DELIVER_FAIL_RETRY = 2
+DELIVER_FAIL_NORETRY = 3
+
 # Numerically first exit type.
 MIN_EXIT_TYPE  = 0x0100
 
@@ -15,4 +28,261 @@
 
 # Exit types
 SMTP_TYPE      = 0x0100  # Mail the message
-LOCAL_TYPE     = 0x0101  # Store the message for local delivery.
+MBOX_TYPE      = 0x0101  # Send the message to one of a fixed list of addresses
+
+
+class DeliveryModule:
+    "XXXX DOCME"
+    def __init__(self):
+        pass
+
+    def getConfigSyntax(self):
+        pass
+
+    def validateConfig(self, sections, entries, lines, contents):
+        pass
+
+    def configure(self, config, manager):
+        pass
+
+    def getServerInfoBlock(self):
+        pass
+
+    def getName(self):
+        pass
+
+    def getExitTypes(self):
+        pass
+
+    def processMessage(self, message, exitType, exitInfo):
+        pass
+
+class ModuleManager:
+    def __init__(self):
+        self.syntax = {}
+        self.modules = []
+        self.typeToModule = {}
+        
+        self.registerModule(MBoxModule())
+        self.registerModule(DropModule())
+
+    def getConfigSyntax(self):
+        return self.syntax
+
+    def registerModule(self, module):
+        self.modules.append(module)
+        syn = module.getConfigSyntax()
+        for sec, rules in syn.items():
+            if self.syntax.has_key(sec):
+                raise ConfigError("Multiple modules want to define [%s]"% sec)
+        self.syntax.update(syn)
+
+    def setPath(self, path):
+        self.path = path
+
+    def loadExtModule(self, className):
+        # CHECK! XXXX Handle errors
+        ids = className.split(".")
+        pyPkg = ".".join(ids[:-1])
+        pyClassName = ids[-1]
+        try:
+            orig_path = sys.path[:]
+            sys.path.extend(self.path)
+            m = __import__(pyPkg, {}, {}, [])
+        finally:
+            sys.path = orig_path
+        pyClass = getattr(pyPkg, pyClassname)
+        self.registerModule(pyClass())
+
+    def validate(self, sections, entries, lines, contents):
+        for m in self.modules:
+            m.validateConfig(sections, entries, lines, contents)
+
+    def configure(self, config):
+        for m in self.modules:
+            m.configure(config, self)
+
+    def enableModule(self, module):
+        for t in module.getExitTypes():
+            self.typeToModule[t] = module
+
+    def disableModule(self, module):
+        for t in module.getExitTypes():
+            if self.typeToModule.has_key(t):
+                del self.typeToModule[t]
+
+    def processMessage(self, message, exitType, exitInfo):
+        mod = self.typeToModule.get(exitType, None)
+        if mod is not None:
+            return mod.processMessage(message, exitType, exitInfo)
+        else:
+            getLog().error("Unable to deliver message with unknown type %s",
+                           exitType)
+            return DELIVER_FAIL_NORETRY
+
+    def getServerInfoBlocks(self):
+        return [ m.getServerInfoBlock() for m in self.modules ]
+
+class DropModule(DeliveryModule):
+    def __init__(self):
+        DeliveryModule.__init__(self)
+
+    def getConfigSyntax(self):
+        return { }
+
+    def validateConfig(self, sections, entries, lines, contents):
+        pass
+
+    def configure(self, config, moduleManager):
+        pass
+
+    def getServerInfoBlock(self):
+        return ""
+    
+    def getName(self):
+        return "DROP module"
+
+    def getExitTypes(self):
+        return [ DROP_TYPE ]
+
+    def processMessage(self, message, exitType, exitInfo):
+        getLog().info("Dropping padding message")
+        return DELIVER_OK
+
+#----------------------------------------------------------------------
+class MBoxModule(DeliveryModule):
+    def __init__(self):
+        DeliveryModule.__init__(self)
+        self.command = None
+        self.enabled = 0
+        self.addresses = {}
+
+    def getConfigSyntax(self):
+        return { "Delivery/MBOX" :
+                 { 'Enabled' : ('REQUIRE',  _parseBoolean, "no"),
+                   'AddressFile' : ('ALLOW', None, None),
+                   'ReturnAddress' : ('ALLOW', None, None),
+                   'RemoveContact' : ('ALLOW', None, None),
+                   'Command' : ('ALLOW', _parseCommand, "sendmail") }
+                 }
+
+    def validateConfig(self, sections, entries, lines, contents):
+        # XXXX write this.  Parse address file.
+        pass
+
+    def configure(self, config, moduleManager):
+        # XXXX Check this.  error handling
+        self.enabled = config['Delivery/MBOX'].get("Enabled", 0)
+        self.command = config['Delivery/MBOX']['Command']
+        self.addressFile = config['Delivery/MBOX']['AddressFile']
+        self.returnAddress = config['Delivery/MBOX']['ReturnAddress']
+        self.contact = config['Delivery/MBOX']['RemoveContact']
+        if self.enabled:
+            if not self.addressFile:
+                raise ConfigError("Missing AddressFile field in Delivery/MBOX")
+            if not self.returnAddress:
+                raise ConfigError("Missing ReturnAddress field "+
+                                  "in Delivery/MBOX")
+            if not self.contact:
+                raise ConfigError("Missing RemoveContact field "+
+                                  "in Delivery/MBOX")
+        
+        self.nickname = config['Server']['Nickname']
+        if not self.nickname:
+            self.nickname = socket.gethostname()
+        self.addr = config['Server'].get('IP', "<Unknown host>")
+
+        if self.command != ('sendmail', []):
+            getLog().warn("Ignoring mail command in version 0.0.1")
+
+        f = open(self.addressfile)
+        addresses = f.read()
+        f.close()
+
+        addresses = mixminion.Config._readConfigFile(addresses)
+        assert len(addresses) > 1
+        assert not addresses.has_key('Addresses')
+
+        self.addresses = {}
+        for k, v, line in addresses[0][1]:
+            if self.addresses.has_key(k):
+                raise ConfigError("Duplicate MBOX user %s"%k)
+            self.addresses[k] = v
+
+        if enabled:
+            moduleManager.enableModule(self)
+        else:
+            moduleManager.disableModule(self)
+
+    def getServerInfoBlock(self):
+        return """\
+                  [Delivery/MBOX]
+                  Version: 1.0
+               """
+    
+    def getName(self):
+        return "MBOX module"
+
+    def getExitTypes(self):
+        return [ MBOX_TYPE ]
+
+    def processMessage(self, message, exitType, exitInfo):
+        assert exitType == MBOX_TYPE
+        getLog().trace("Received MBOX message")
+        info = mixminion.packet.parseMBOXInfo(exitInfo)
+        if not addresses.has_key(info.user):
+            getLog.warn("Unknown MBOX user %r", info.user)
+            return 
+        msg = _escapeMessageForEmail(message)
+
+        fields = { 'user': addresses[info.user],
+                   'return': self.returnAddr,
+                   'nickname': self.nickname,
+                   'addr': self.addr,
+                   'contact': self.contact,
+                   'msg': msg }
+        msg = """
+To: %(user)s
+From: %(return)s
+Subject: Anonymous Mixminion message
+
+THIS IS AN ANONYMOUS MESSAGE.  The mixminion server '%(nickname)s' at
+%(addr)s has been configured to deliver messages to your address.  If you
+do not want to receive messages in the future, contact %(contact)s and you
+will be removed.  (XXXX Need real boilerplate)
+
+%(msg)s
+""" % fields
+
+        f = os.popen("sendmail -i -t", 'w')
+        f.write(msg)
+        status = f.close()
+        if status != 0:
+            getLog().error("Unsuccessful sendmail")
+            return DELIVER_FAIL_RETRY
+
+        return DELIVER_OK
+
+#----------------------------------------------------------------------
+
+
+_allChars = "".join(map(chr, range(256)))
+_nonprinting = "".join(map(chr, range(0x00, 0x07)+range(0x0E, 0x20)))
+def _escapeMessageForEmail(msg):
+    printable = msg.translate(_allChars, _nonprinting)
+    if msg[len(printable):] == '\x00'*(len(msg)-len(printable)):
+        msg = msg[len(printable)]
+        return """\
+============ ANONYMOUS MESSAGE BEGINS
+%s
+============ ANONYMOUS MESSAGE ENDS\n""" %msg
+    else:
+        msg = base64.encodestring(msg)
+        return """\
+This message is encoded in Base64 because it contains some nonprintable
+characters.  It's possible that this message is a non-text object, that
+it was sent to using a reply block, that it was corrupted on its way to
+you, or that it's just plain junk.
+============ BASE-64 ENCODED ANONYMOUS MESSAGE BEGINS
+%s
+============ BASE-64 ENCODED ANONYMOUS MESSAGE ENDS\n""" % msg

Index: Packet.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Packet.py,v
retrieving revision 1.5
retrieving revision 1.6
diff -u -d -r1.5 -r1.6
--- Packet.py	1 Jul 2002 18:03:05 -0000	1.5
+++ Packet.py	6 Aug 2002 16:09:21 -0000	1.6
@@ -8,8 +8,8 @@
 __all__ = [ 'ParseError', 'Message', 'Header', 'Subheader',
             'parseMessage', 'parseHeader', 'parseSubheader',
             'getTotalBlocksForRoutingInfoLen', 'ReplyBlock',
-            'IPV4Info', 'SMTPInfo', 'LocalInfo', 'parseIPV4Info',
-            'parseSMTPInfo', 'parseLocalInfo', 'ReplyBlock',
+            'IPV4Info', 'SMTPInfo', 'MBOXInfo', 'parseIPV4Info',
+            'parseSMTPInfo', 'parseMBOXInfo', 'ReplyBlock',
             'parseReplyBlock', 'ENC_SUBHEADER_LEN',
             'HEADER_LEN', 'PAYLOAD_LEN', 'MAJOR_NO', 'MINOR_NO',
             'SECRET_LEN']
@@ -348,17 +348,17 @@
         else:
             return self.email
 
-def parseLocalInfo(s):
-    """Convert the encoding of an LOCAL routinginfo into an LocalInfo
+def parseMBOXInfo(s):
+    """Convert the encoding of an MBOX routinginfo into an MBOXInfo
        object."""
     lst = s.split("\000",1)
     if len(lst) == 1:
-        return LocalInfo(s,None)
+        return MBOXInfo(s,None)
     else:
-        return LocalInfo(lst[0], lst[1])
+        return MBOXInfo(lst[0], lst[1])
 
-class LocalInfo:
-    """Represents the routinginfo for a LOCAL hop.
+class MBOXInfo:
+    """Represents the routinginfo for an MBOX hop.
 
        Fields: user (a user identifier), tag (an arbitrary tag, optional)."""
     def __init__(self, user, tag):

Index: Queue.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Queue.py,v
retrieving revision 1.6
retrieving revision 1.7
diff -u -d -r1.6 -r1.7
--- Queue.py	25 Jul 2002 15:52:57 -0000	1.6
+++ Queue.py	6 Aug 2002 16:09:21 -0000	1.7
@@ -75,6 +75,7 @@
 
         if not os.path.exists(location):
             if create:
+                getLog().info("Trying to create queue %s", location)
                 os.mkdir(location, 0700)
             else:
                 raise MixFatalError("No directory for queue %s" % location)
@@ -82,7 +83,7 @@
         # Check permissions
         mode = os.stat(location)[stat.ST_MODE]
         if mode & 0077:
-            # FFFF be more Draconian.
+            # XXXX be more Draconian.
             getLog().warn("Worrisome more %o on directory %s", mode, location)
 
         if scrub:

Index: ServerInfo.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ServerInfo.py,v
retrieving revision 1.7
retrieving revision 1.8
diff -u -d -r1.7 -r1.8
--- ServerInfo.py	28 Jul 2002 22:42:33 -0000	1.7
+++ ServerInfo.py	6 Aug 2002 16:09:21 -0000	1.8
@@ -13,6 +13,7 @@
 import time
 import os
 import binascii
+import socket
 
 from mixminion.Modules import SWAP_FWD_TYPE, FWD_TYPE
 from mixminion.Packet import IPV4Info
@@ -62,16 +63,16 @@
                      "Allow": ("ALLOW*", C._parseAddressSet_allow, None),
                      "Deny": ("ALLOW*", C._parseAddressSet_deny, None),
 		     },
-	"Modules/MMTP" : {
+	"Outgoing/MMTP" : {
  	             "Version": ("REQUIRE", None, None),
 		     "Protocols": ("REQUIRE", None, None),
                      "Allow": ("ALLOW*", C._parseAddressSet_allow, None),
                      "Deny": ("ALLOW*", C._parseAddressSet_deny, None),
 		     },
-	"Modules/MBOX" : {
+	"Delivery/MBOX" : {
    	             "Version": ("REQUIRE", None, None),
 		     },
-	"Modules/SMTP" : {
+	"Delivery/SMTP" : {
            	     "Version": ("REQUIRE", None, None),
 		     }
 	}
@@ -241,7 +242,9 @@
 
     nickname = config['Server']['Nickname']
     if not nickname:
-        nickname = config['Incoming/MMTP'].get('IP', "<Unnamed server>")
+        nickname = socket.gethostname()
+        if not nickname or nickname.lower().startswith("localhost"):
+            nickname = config['Incoming/MMTP'].get('IP', "<Unknown host>")
     contact = config['Server']['Contact-Email']
     comments = config['Server']['Comments']
     if not validAt:
@@ -305,7 +308,7 @@
 
     if config["Outgoing/MMTP"].get("Enabled", 0):
 	info += """\
-            [Modules/MMTP]
+            [Outgoing/MMTP]
 	    Version: 1.0
             Protocols: 1.0
             """
@@ -314,12 +317,8 @@
                 continue
             info += "%s: %s" % (k, _rule(k=='Allow',v))
 
-    if config["Delivery/MBOX"].get("Enabled", 0):
-	info += """\
-            [Modules/MBOX]
-            Version: 1.0
-            """
-	
+    info += "".join(config.moduleManager.getServerInfoBlocks())
+
     # Remove extra (leading) whitespace.
     lines = [ line.strip() for line in info.split("\n") ]
     # Remove empty lines

Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.15
retrieving revision 1.16
diff -u -d -r1.15 -r1.16
--- test.py	28 Jul 2002 22:42:33 -0000	1.15
+++ test.py	6 Aug 2002 16:09:21 -0000	1.16
@@ -523,9 +523,9 @@
         self.failUnlessRaises(ParseError, parseIPV4Info, ri+"x")
     
 
-    def test_smtpinfolocalinfo(self):
+    def test_smtpinfomboxinfo(self):
         for _class, _parse, _key in ((SMTPInfo, parseSMTPInfo, 'email'),
-                                     (LocalInfo, parseLocalInfo, 'user')):
+                                     (MBOXInfo, parseMBOXInfo, 'user')):
             ri = "no-such-user@wangafu.net\x00xyzzy"
             inf = _parse(ri)
             self.assertEquals(getattr(inf,_key), "no-such-user@wangafu.net")
@@ -776,7 +776,7 @@
         longStr2 = longStr * 2
 
         def getLongRoutingInfo(longStr2=longStr2):
-            return LocalInfo("fred",longStr2)
+            return MBOXInfo("fred",longStr2)
 
         server4 = FakeServerInfo("127.0.0.1", 1, self.pk1, "X"*20)
         server4.getRoutingInfo = getLongRoutingInfo
@@ -961,7 +961,7 @@
                      "fred", "Galaxy Far Away.", 0)
 
         sec,(loc,) = self.do_header_test(reply.header, pks_1, None,
-                            (FWD_TYPE,FWD_TYPE,FWD_TYPE,FWD_TYPE,LOCAL_TYPE),
+                            (FWD_TYPE,FWD_TYPE,FWD_TYPE,FWD_TYPE,MBOX_TYPE),
                             infos+(None,))
         s = "fred\x00RTRN"
         self.assert_(loc.startswith(s))
@@ -974,7 +974,7 @@
                       self.server1, self.server3],
                      "fred", None)
         sec,(loc,) = self.do_header_test(reply.header, pks_1, None,
-                            (FWD_TYPE,FWD_TYPE,FWD_TYPE,FWD_TYPE,LOCAL_TYPE),
+                            (FWD_TYPE,FWD_TYPE,FWD_TYPE,FWD_TYPE,MBOX_TYPE),
                                          infos+(None,))
         self.assert_(loc.startswith(s))
         seed = loc[len(s):]
@@ -1099,7 +1099,7 @@
             def pack(self): return "x"*200
         server1X.getRoutingInfo = lambda _packable=_packable: _packable()
 
-        m = bfm("Z", LOCAL_TYPE, "hello\000bye",
+        m = bfm("Z", MBOX_TYPE, "hello\000bye",
                 [self.server2, server1X, self.server3],
                 [server1X, self.server2, self.server3])
         self.failUnlessRaises(ContentError, self.sp2.processMessage, m)
@@ -1123,7 +1123,7 @@
         prng = AESCounterPRNG(" "*16)
         reply1,s = brb([self.server1], SMTP_TYPE, "fred@invalid",0,prng)
         prng = AESCounterPRNG(" "*16)
-        reply2,s = brb([self.server2], LOCAL_TYPE, "foo",0,prng)
+        reply2,s = brb([self.server2], MBOX_TYPE, "foo",0,prng)
         m = brm("Y", [self.server3], reply1)
         m2 = brm("Y", [self.server3], reply2)
         q, (a,m) = self.sp3.processMessage(m)
@@ -1174,7 +1174,7 @@
 
         # Subhead that claims to be impossibly long: exit case
         subh = parseSubheader(subh_real)
-        subh.routingtype = LOCAL_TYPE
+        subh.routingtype = MBOX_TYPE
         subh.setRoutingInfo("X"*10000)
         m_x = pk_encrypt(subh.pack(), self.pk1)+m[128:]
         self.failUnlessRaises(ContentError, self.sp1.processMessage, m_x)
@@ -1192,7 +1192,7 @@
         self.failUnlessRaises(ContentError, self.sp1.processMessage, m_x)
 
         # Corrupt payload
-        m = bfm("Z", LOCAL_TYPE, "Z", [self.server1, self.server2],
+        m = bfm("Z", MBOX_TYPE, "Z", [self.server1, self.server2],
                 [self.server3])
         m_x = m[:-30] + " "*30
         assert len(m_x) == len(m)
@@ -1439,7 +1439,7 @@
             m.append(pkt)
         def conFactory(sock, context=_getTLSContext(1),
                        receiveMessage=receivedHook):
-            tls = context.sock(sock)
+            tls = context.sock(sock, serverMode=1)
             sock.setblocking(0)
             return mixminion.MMTPServer.MMTPServerConnection(sock,tls,
                                                              receiveMessage)
@@ -1467,7 +1467,7 @@
                     self.server.process(0.1)
                     count = count + 1
 
-    def ___testBlockingTransmission(self):
+    def testBlockingTransmission(self):
         self.doTest(self._testBlockingTransmission)
 
     def testNonblockingTransmission(self):
@@ -1542,8 +1542,6 @@
             while not clientcon.isShutdown():
                 async.process(2)
             
-
-
         severity = getLog().getMinSeverity()
         getLog().setMinSeverity("ERROR") #suppress warning
         try:
@@ -1807,7 +1805,7 @@
 [Server]
 EncryptIdentityKey: no
 PublicKeyLifetime: 10 days
-EncryptPublicKey: no
+EncryptPrivateKey: no
 Mode: relay
 Nickname: The Server
 Contact-Email: a@b.c
@@ -1826,14 +1824,15 @@
 Allow: *
 
 [Delivery/MBOX]
-Enabled: yes
+Enabled: no
+
 """
 
 SERVER_CONFIG_SHORT = """
 [Server]
 EncryptIdentityKey: no
 PublicKeyLifetime: 10 days
-EncryptPublicKey: no
+EncryptPrivateKey: no
 Mode: relay
 """
 
@@ -1870,8 +1869,8 @@
         eq(info['Incoming/MMTP']['Version'], "1.0")
         eq(info['Incoming/MMTP']['Port'], 48099)
         eq(info['Incoming/MMTP']['Protocols'], "1.0")
-        eq(info['Modules/MMTP']['Version'], "1.0")
-        eq(info['Modules/MMTP']['Protocols'], "1.0")
+        eq(info['Outgoing/MMTP']['Version'], "1.0")
+        eq(info['Outgoing/MMTP']['Protocols'], "1.0")
         eq(info['Incoming/MMTP']['Allow'], [("192.168.0.16", "255.255.255.255",
                                             1,1024),
                                            ("0.0.0.0", "0.0.0.0",
@@ -1879,7 +1878,7 @@
         eq(info['Incoming/MMTP']['Deny'], [("192.168.0.16", "255.255.255.255",
                                             0,65535),
                                            ])
-        eq(info['Modules/MBOX']['Version'], "1.0")
+        eq(info['Delivery/MBOX']['Version'], "1.0")
 
         # Now make sure everything was saved properly
         keydir = os.path.join(d, "key_key1")