# Copyright 2002-2004 Nick Mathewson.  See LICENSE for licensing information.
# Id: ClientMain.py,v 1.89 2003/06/05 18:41:40 nickm Exp $

"""mixminion.ClientMain

   Code for Mixminion command-line client.
   """

__all__ = [ 'Address', 'ClientKeyring', 'MixminionClient' ]

import getopt
import os
import sys
import time
from types import IntType, StringType

import mixminion.BuildMessage
import mixminion.ClientUtils
import mixminion.ClientDirectory
import mixminion.Config
import mixminion.Crypto
import mixminion.Filestore
import mixminion.MMTPClient

from mixminion.Common import LOG, Lockfile, LockfileLocked, MixError, \
     MixFatalError, MixProtocolBadAuth, MixProtocolError, STATUS, UIError, \
     UsageError, createPrivateDir, englishSequence, floorDiv, formatTime, \
     isPrintingAscii,\
     isSMTPMailbox, readFile, stringContains, succeedingMidnight, writeFile, \
     previousMidnight
from mixminion.Packet import encodeMailHeaders, ParseError, parseMBOXInfo, \
     parseReplyBlocks, parseSMTPInfo, parseTextEncodedMessages, \
     parseTextReplyBlocks, ReplyBlock, parseMessageAndHeaders, \
     CompressedDataTooLong

from mixminion.ServerInfo import displayServerByRouting, ServerInfo

#----------------------------------------------------------------------
# Global variable; holds an instance of Common.Lockfile used to prevent
# concurrent access to the directory cache, packet queue, or SURB log.
_CLIENT_LOCKFILE = None

def clientLock():
    """Acquire the client lock."""
    assert _CLIENT_LOCKFILE is not None
    pidStr = str(os.getpid())
    try:
        _CLIENT_LOCKFILE.acquire(blocking=0, contents=pidStr)
    except LockfileLocked:
        c = _CLIENT_LOCKFILE.getContents()
        if c:
            LOG.info("Waiting for pid %s", c)
        else:
            LOG.info("Waiting for another process")
        _CLIENT_LOCKFILE.acquire(blocking=1, contents=pidStr)

def clientUnlock():
    """Release the client lock."""
    _CLIENT_LOCKFILE.release()

def configureClientLock(filename):
    """Prepare the client lock for use."""
    global _CLIENT_LOCKFILE
    parent, fname = os.path.split(filename)
    createPrivateDir(parent)
    _CLIENT_LOCKFILE = Lockfile(filename)

class ClientDiskLock:
    """A wrapper around clientLock and clientUnlock to present a lock-like
       interface, and default to blocking locks."""
    def acquire(self):
        clientLock()
    def release(self):
        clientUnlock()

class ClientKeyring:
    """Class to manage storing encrypted keys for a client.  Right now, this
       is limited to a single SURB decryption key.  In the future, we may
       include more SURB keys, as well as end-to-end encryption keys.
    """
    # XXXX Can any more of this class should go into ClientUtils?
    ## Fields
    # keyring: an instance of ClientUtils.Keyring

    # We discard SURB keys after 3 months.
    KEY_LIFETIME = 3*30*24*60*60
    # We don't make new SURBs with any key that will expire in the next
    # month.
    MIN_KEY_LIFETIME_TO_USE = 30*24*60*60

    def __init__(self, keyDir, passwordManager=None):
        """Create a new ClientKeyring, storing its keys in 'keyDir'"""
        if passwordManager is None:
            passwordManager = mixminion.ClientUtils.CLIPasswordManager()
        createPrivateDir(keyDir)

        # XXXX008 remove this; we haven't used the old format since 0.0.5.

        # We used to store our keys in a different format.  At this point,
        # it's easier to change the filename.
        obsoleteFn = os.path.join(keyDir, "keyring")
        if os.path.exists(obsoleteFn):
            LOG.warn("Ignoring obsolete keyring stored in %r",obsoleteFn)
        fn = os.path.join(keyDir, "keyring.txt")

        # Setup the keyring.
        self.keyring = mixminion.ClientUtils.Keyring(fn, passwordManager)

    def getSURBKey(self, name="", create=0, password=None):
        """Return a SURB key for a given identity, asking for passwords and
           loading the keyring if necessary..  Return None on failure.

           name -- the SURB key identity
           create -- If true, create a new key if none is found.
           password -- Optionally, a password for the keyring.
        """
        # If we haven't loaded the keyring yet, try to do so.
        if not self.keyring.isLoaded():
            try:
                self.keyring.load(create=create,password=password)
            except mixminion.ClientUtils.BadPassword:
                LOG.error("Incorrect password")
                return None
            if not self.keyring.isLoaded():
                return None


        try:
            key = self.keyring.getNewestSURBKey(
                name,minLifetime=self.MIN_KEY_LIFETIME_TO_USE)
            if key:
                return key
            elif not create:
                return None
            else:
                # No key, but we're allowed to create a new one.
                LOG.info("Creating new key for identity %r", name)
                return self.keyring.newSURBKey(name,
                                               time.time()+self.KEY_LIFETIME)
        finally:
            # Check whether we changed the keyring, and save it if we did.
            # The keyring may have changed even if we didn't generate any
            # new keys, so this check is always necessary.
            if self.keyring.isDirty():
                self.keyring.save()

        raise AssertionError # Unreached.

    def getSURBKeys(self, password=None):
        """Return the keys for _all_ SURB identities as a list of
           (name,key) tuples."""
        try:
            self.keyring.load(create=0,password=password)
        except mixminion.ClientUtils.BadPassword:
            LOG.error("Incorrect password")
        if not self.keyring.isLoaded(): return []
        if self.keyring.isDirty(): self.keyring.save()
        return self.keyring.getAllSURBKeys()

def installDefaultConfig(fname):
    """Create a default, 'fail-safe' configuration in a given file"""
    LOG.warn("No configuration file found. Installing default file in %s",
                  fname)

    fields = { 'ud_default' : mixminion.Config.DEFAULT_USER_DIR }

    writeFile(os.path.expanduser(fname),
              """\
# This file contains your options for the mixminion client.
[Host]
## Use this option to specify a 'secure remove' command.
#ShredCommand: rm -f
## Use this option to specify a nonstandard entropy source.
#EntropySource: /dev/urandom
## Set this option to 'no' to disable permission checking
#FileParanoia: yes

[DirectoryServers]
DirectoryTimeout: 1 minute
# Other options not yet implemented

[User]
## By default, mixminion puts your files in %(ud_default)s,  You can override
## this directory here.
#UserDir: %(ud_default)s

[Security]
## Address to use by default when generating reply blocks
#SURBAddress: <your address here>
## Deault reply block lifetime
#SURBLifetime: 7 days

### Default paths to use if no path given on command line:
## For forward messages
#ForwardPath: ?,?,?:?,FavoriteExit
## For reply messages
#ReplyPath: ?,?,?,FavoriteSwap
## For reply blocks
#SURBPath: ?,?,?,FavoriteExit

### If there are servers that you never want to include in automatically
### generated paths, you can list them here.
## These servers will never begin a path:
#BlockEntries: example1,example2
## These servers will never end a path:
#BlockExits: example3,example4
## These servers will never appear in a path at all:
#BlockServers: example5,example6,example7

[Network]
Timeout: 2 minutes
""" % fields)

class MixminionClient:
    #XXXX Once ClientAPI is more solid, this class should be folded into it.

    """Access point for client functionality."""
    ## Fields:
    # config: The ClientConfig object with the current configuration
    # prng: A pseudo-random number generator for padding and path selection
    # keys: A ClientKeyring object.
    # queue: A ClientQueue object.
    # surbLogFilename: The filename used by the SURB log.
    def __init__(self, conf, password_fileno=None):
        """Create a new MixminionClient with a given configuration"""
        self.config = conf

        # Make directories
        userdir = self.config['User']['UserDir']
        createPrivateDir(userdir)
        keyDir = os.path.join(userdir, "keys")
        if password_fileno is None:
            self.pwdManager = mixminion.ClientUtils.CLIPasswordManager()
        else:
            self.pwdManager = mixminion.ClientUtils.FDPasswordManager(password_fileno)
        self.keys = ClientKeyring(keyDir, self.pwdManager)
        self.surbLogFilename = os.path.join(userdir, "surbs", "log")

        # Initialize PRNG
        self.prng = mixminion.Crypto.getCommonPRNG()
        self.queue = mixminion.ClientUtils.ClientQueue(os.path.join(userdir, "queue"))
        self.pool = mixminion.ClientUtils.ClientFragmentPool(os.path.join(userdir, "fragments"))

    def _sortPackets(self, packets, shuffle=1):
        """Helper function.  Takes a list of tuples of (packet,
           ServerInfo/routigInforoutingInfo),
           groups packets with the same routingInfos, and returns a list of
           tuples of (routingInfo, [packet list]).

           If 'shuffle' is true, then the packets within each list, and the
           tuples themselves, are returned in a scrambled order.
        """
        d = {}
        for packet, firstHop in packets:
            if isinstance(firstHop, ServerInfo):
                ri = firstHop.getRoutingInfo()
            else:
                assert (isinstance(firstHop, mixminion.Packet.MMTPHostInfo) or
                        isinstance(firstHop, mixminion.Packet.IPV4Info))
                ri = firstHop
            d.setdefault(ri,[]).append(packet)
        result = d.items()
        if shuffle:
            self.prng.shuffle(result)
            for _, pktList in result:
                self.prng.shuffle(pktList)
        return result


    def sendForwardMessage(self, directory, address, pathSpec, message,
                           startAt, endAt, forceQueue=0, forceNoQueue=0,
                           forceNoServerSideFragments=0):
        """Generate and send a forward message.
            directory -- an instance of ClientDirectory; used to generate
               paths.
            address -- an instance of ExitAddress, used to tell where to
               deliver the message.
            pathSpec -- an instance of PathSpec, describing the path to use.
            message -- the contents of the message to send
            startAt, endAt -- an interval over which all servers in the path
               must be valid.
            forceQueue -- if true, do not try to send the message; simply
               queue it and exit.
            forceNoQueue -- if true, do not queue the message even if delivery
               fails.
            forceNoServerSideFragments -- if true, and the message is too
               large to fit in a single packet, deliver fragment packets to
               the eventual recipient rather than having the exit server
               defragment them.
        """
        assert not (forceQueue and forceNoQueue)

        allPackets = self.generateForwardPackets(
            directory, address, pathSpec, message, forceNoServerSideFragments,
            startAt, endAt)

        for routing, packets in self._sortPackets(allPackets):
            if forceQueue:
                self.queuePackets(packets, routing)
            else:
                self.sendPackets(packets, routing, noQueue=forceNoQueue)

    def sendReplyMessage(self, directory, address, pathSpec, surbList, message,
                         startAt, endAt, forceQueue=0,
                         forceNoQueue=0):
        """Generate and send a reply message.
            directory -- an instance of ClientDirectory; used to generate
               paths.
            address -- an instance of ExitAddress, used to tell where to
               deliver the message.
            pathSpec -- an instance of PathSpec, describing the path to use.
            surbList -- a list of SURBs to consider using for the reply.  We
               use the first N that are neither expired nor used, and mark them
               used.
            message -- the contents of the message to send
            startAt, endAt -- an interval over which all servers in the path
               must be valid.
            forceQueue -- if true, do not try to send the message; simply
               queue it and exit.
            forceNoQueue -- if true, do not queue the message even if delivery
               fails.
        """
        #XXXX write unit tests
        allPackets = self.generateReplyPackets(
            directory, address, pathSpec, message, surbList, startAt, endAt)

        for routing, packets in self._sortPackets(allPackets):
            if forceQueue:
                self.queuePackets(packets, routing)
            else:
                self.sendPackets(packets, routing, noQueue=forceNoQueue)

    def generateReplyBlock(self, address, servers, name="", expiryTime=0):
        """Generate an return a new ReplyBlock object.
            address -- the results of a parseAddress call
            servers -- lists of ServerInfos for the reply leg of the path.
            name -- the name of the identity to use for the reply block.
            expiryTime -- if provided, a time at which the replyBlock must
               still be valid, and after which it should not be used.
        """
        #XXXX write unit tests
        key = self.keys.getSURBKey(name=name, create=1)
        if not key:
            raise UIError("unable to get SURB key")
        exitType, exitInfo, _ = address.getRouting()

        block = mixminion.BuildMessage.buildReplyBlock(
            servers, exitType, exitInfo, key, expiryTime)

        return block

    def generateForwardPackets(self, directory, address, pathSpec, message,
                               noSSFragments, startAt, endAt):
        """Generate packets for a forward message, but do not send
           them.  Return a list of tuples of (the packet body, a
           ServerInfo for the first hop.)

            directory -- an instance of ClientDirectory; used to generate
               paths.
            address -- an instance of ExitAddress, used to tell where to
               deliver the message.
            pathSpec -- an instance of PathSpec, describing the path to use.
            message -- the contents of the message to send
            noSSFragments -- if true, and the message is too large to fit in a
               single packet, deliver fragment packets to the eventual
               recipient rather than having the exit server defragment them.
            startAt, endAt -- an interval over which all servers in the path
               must be valid.
            """
        #XXXX we need to factor more of this long-message logic out to the
        #XXXX common code.  For now, this is a temporary measure.

        if noSSFragments:
            fragmentedMessagePrefix = ""
        else:
            fragmentedMessagePrefix = address.getFragmentedMessagePrefix()
        LOG.info("Generating payload(s)...")
        r = []
        if address.hasPayload():
            payloads = mixminion.BuildMessage.encodeMessage(message, 0,
                                fragmentedMessagePrefix)
            if len(payloads) > 1:
                address.setFragmented(not noSSFragments, len(payloads))
            else:
                address.setFragmented(0,1)
        else:
            payloads = [ mixminion.BuildMessage.buildRandomPayload() ]
            address.setFragmented(0,1)
        routingType, routingInfo, _ = address.getRouting()

        directory.validatePath(pathSpec, address, startAt, endAt,
                               warnUnrecommended=0)

        for p, (path1,path2) in zip(payloads, directory.generatePaths(
            len(payloads), pathSpec, address, startAt, endAt)):

            pkt = mixminion.BuildMessage.buildForwardPacket(
                p, routingType, routingInfo, path1, path2,
                self.prng, suppressTag=address.suppressTag())
            r.append( (pkt, path1[0]) )

        return r

    def generateReplyPackets(self, directory, address, pathSpec, message,
                             surbList, startAt, endAt):
        """Generate a reply message, but do not send it.  Returns
           a tuple of (packet body, ServerInfo for the first hop.)

            directory -- an instance of ClientDirectory; used to generate
               paths.
            address -- an instance of ExitAddress, used to tell where to
               deliver the message.
            pathSpec -- an instance of PathSpec, describing the path to use.
            message -- the contents of the message to send
            surbList -- a list of SURBs to consider using for the reply.  We
               use the first N that are neither expired nor used, and mark them
               used.
            startAt, endAt -- an interval over which all servers in the path
               must be valid.
            """
        #XXXX write unit tests
        assert address.isReply

        payloads = mixminion.BuildMessage.encodeMessage(message, 0, "")

        surbLog = self.openSURBLog() # implies lock
        result = []
        try:
            surbs = surbLog.findUnusedSURBs(surbList, len(payloads),
                                           verbose=1, now=startAt)
            if len(surbs) < len(payloads):
                raise UIError("Not enough usable reply blocks found; all were used or expired.")


            for (surb,payload,(path1,path2)) in zip(surbs,payloads,
                  directory.generatePaths(len(payloads),pathSpec, address,
                                          startAt,endAt)):
                assert path1 and not path2
                LOG.info("Generating packet...")
                pkt = mixminion.BuildMessage.buildReplyPacket(
                    payload, path1, surb, self.prng)

                surbLog.markSURBUsed(surb)
                result.append( (pkt, path1[0]) )

        finally:
            surbLog.close() #implies unlock

        return result

    def openSURBLog(self):
        """Return a new, open SURBLog object for this client; it must be closed
           when no longer in use.
        """
        return mixminion.ClientUtils.SURBLog(self.surbLogFilename)

    def pingServer(self, routingInfo):
        """Given an IPV4Info, try to connect to a server and find out if
           it's up.  Returns a boolean and a status message."""
        timeout = self.config.getTimeout()
        try:
            mixminion.MMTPClient.pingServer(routingInfo, timeout)
            return 1, "Server seems to be running"
        except MixProtocolBadAuth:
            return 0, "Server seems to be running, but its key is wrong!"
        except MixProtocolError, e:
            return 0, "Couldn't connect to server: %s" % e

    def sendPackets(self, pktList, routingInfo, noQueue=0, lazyQueue=0,
                    alreadyQueued=0, warnIfLost=1):
        """Given a list of packets and an IPV4Info object, sends the
           packets to the server via MMTP.

           If noQueue is true, do not queue the packets even on failure.
           If lazyQueue is true, only queue the packets on failure.
           XXXX alreadyQueued
           Otherwise, insert the packets in the queue, and remove them on
           success.

           If warnIfLost is true, log a warning if we fail to deliver
           the packets, and we don't queue them.

           XXXX return 1 if all delivered
           """
        #XXXX write unit tests
        timeout = self.config.getTimeout()

        if noQueue or lazyQueue:
            handles = []
        else:
            handles = self.queuePackets(pktList, routingInfo)

        packetsSentByIndex = {}
        def callback(idx, packetsSentByIndex=packetsSentByIndex):
            packetsSentByIndex[idx] = 1

        try:
            # May raise TimeoutError
            LOG.info("Connecting...")
            mixminion.MMTPClient.sendPackets(routingInfo,
                                             pktList,
                                             timeout,
                                             callback=callback)

        except:
            exc = sys.exc_info()
        else:
            exc = None
        nGood = len(packetsSentByIndex)
        nBad = len(pktList)-nGood

        clientLock()
        try:
            if nGood:
                LOG.info("... %s sent", nGood)
                LOG.trace("Removing %s successful packets from queue", nGood)
            for idx in packetsSentByIndex.keys():
                if handles and handles[idx]:
                    self.queue.removePacket(handles[idx])
                elif hasattr(pktList[idx], 'remove'):
                    pktList[idx].remove()
            if nGood:
                try:
                    self.queue.cleanQueue()
                except:
                    e2 = sys.exc_info()
                    LOG.error("Error while cleaning queue: %s",e2[1])

            if nBad and noQueue:
                if warnIfLost:
                    LOG.error("Error with queueing disabled: %s/%s lost",
                              nBad, nGood+nBad)
                elif alreadyQueued:
                    LOG.info("Error while delivering packets; %s/%s left in queue",
                             nBad,nGood+nBad)
            elif nBad and lazyQueue:
                LOG.info("Error while delivering packets; %s/%s left in queue",
                         nBad,nGood+nBad)
                badPackets = [ pktList[idx] for idx in xrange(len(pktList))
                               if not packetsSentByIndex.has_key(idx) ]

                self.queuePackets(badPackets, routingInfo)
            elif nBad:
                assert not (noQueue or lazyQueue)
                LOG.info("Error while delivering packets; leaving %s/%s in queue",
                         nBad, nBad+nGood)
            if exc and not nBad:
                LOG.info("Got error after all packets were delivered.")
            if exc:
                LOG.info("Error was: %s", exc[1])
        finally:
            clientUnlock()

        return nGood

    def flushQueue(self, maxPackets=None, handles=None):
        """Try to send packets in the queue to their destinations.  Do not try
           to send more than maxPackets packets.  If 'handles' is provided,
           try only to send the packets whose handles are listed in 'handles;
           otherwise, choose the ones to try at random.
        """
        #XXXX write unit tests
        class PacketProxy:
            def __init__(self,h,queue):
                self.h = h
                self.queue = queue
            def __str__(self):
                return self.queue.getPacket(self.h)[0]
            def __cmp__(self,other):
                return cmp(id(self),id(other))
            def remove(self):
                self.queue.removePacket(self.h)

        LOG.info("Flushing packet queue")
        clientLock()
        try:
            if handles is None:
                handles = self.queue.getHandles()
                LOG.info("Found %s pending packets", len(handles))
            else:
                LOG.info("Flushing %s packets", len(handles))
            if maxPackets is not None:
                handles = mixminion.Crypto.getCommonPRNG().shuffle(handles,
                                                               maxPackets)
            packets = []
            for h in handles:
                try:
                    routing = self.queue.getRouting(h)
                except mixminion.Filestore.CorruptedFile:
                    continue
                packet = PacketProxy(h,self.queue)
                packets.append((packet,routing))
        finally:
            clientUnlock()

        nPackets = len(packets)
        nSent = 0
        for routing, packets in self._sortPackets(packets):
            LOG.info("Sending %s packets to %s...",
                     len(packets), displayServerByRouting(routing))
            try:
                ok = self.sendPackets(packets, routing, noQueue=1,
                                      warnIfLost=0, alreadyQueued=1)
                nSent += ok
            except MixError, e:
                LOG.error("Can't deliver packets to %s: %s; leaving in queue",
                          displayServerByRouting(routing), str(e))

        if nSent == nPackets:
            LOG.info("Queue flushed")
        elif nSent > 0:
            LOG.info("Queue partially flushed")
        elif nSent == 0:
            LOG.info("No packets delivered")
        else:
            raise MixFatalError("BUG: somehow sent %s/%s packets!"
                                %(nSent,nPackets))

    def cleanQueue(self, handles):
        """Remove all packets older than maxAge seconds from the
           client queue."""
        clientLock()
        try:
            byRouting = self._sortPackets(
                [ (h, self.queue.getRouting(h)) for h in handles ],
                shuffle = 0)
            byName = [ (displayServerByRouting(ri), lst)
                       for ri,lst in byRouting ]
            byName.sort()
            if not byName:
                LOG.info("No packets removed.")
            for name, lst in byName:
                LOG.info("Removing %s packets for %s", len(lst), name)
                for h in lst:
                    self.queue.removePacket(h)
            self.queue.cleanQueue()
        finally:
            clientUnlock()

    def queuePackets(self, pktList, routing):
        """Insert all the packets in pktList into the queue, to be sent
           to the server identified by the IPV4Info or MMTPHostInfo object
           'routing'.
        """
        #XXXX write unit tests
        LOG.trace("Queueing packets")
        handles = []
        clientLock()
        try:
            for pkt in pktList:
                h = self.queue.queuePacket(str(pkt), routing)
                handles.append(h)
        finally:
            clientUnlock()
        if len(pktList) > 1:
            LOG.info("Packets queued")
        else:
            LOG.info("Packet queued")
        return handles

    def decodeMessage(self, s, force=0, isatty=0):
        """Given a string 's' containing one or more text-encoded messages,
           return a list containing the decoded messages.

           Raise ParseError on malformatted messages.  Unless 'force' is
           true, do not uncompress possible zlib bombs.
        """
        #XXXX write unit tests
        results = []
        foundAFragment = 0
        for msg in parseTextEncodedMessages(s, force=force):
            if msg.isOvercompressed() and not force:
                LOG.warn("Message is a possible zlib bomb; not uncompressing")

            if not msg.isEncrypted():
                if msg.isFragment():
                    foundAFragment = 1
                    self.pool.addFragment(msg.getContents(), "---")
                else:
                    results.append(msg.getContents())
            else:
                assert msg.isEncrypted()
                surbKeys = self.keys.getSURBKeys()
                nym = []
                p = mixminion.BuildMessage.decodePayload(msg.getContents(),
                                                         tag=msg.getTag(),
                                                         userKeys=surbKeys,
                                                         retNym=nym)
                if p:
                    if nym == []:
                        nym = "---"
                    elif nym[0] in (None, ""):
                        nym = "default identity"
                    else:
                        nym = nym[0]
                    if p.isSingleton():
                        results.append(p.getUncompressedContents())
                    else:
                        foundAFragment = 1
                        self.pool.addFragment(p,nym)
                else:
                    raise UIError("Unable to decode message")
        if isatty and not force:
            for p in results:
                if not isPrintingAscii(p,allowISO=1):
                    raise UIError("Not writing binary message to terminal: Use -F to do it anyway.")
        if foundAFragment:
            self.pool.process()
        return results

def readConfigFile(configFile):
    """Given a configuration file (possibly none) as specified on the command
       line, return a ClientConfig object.

       Tries to look for the configuration file in the following places:
          - as specified on the command line,
          - as specifed in $MIXMINIONRC
          - in ~/.mixminionrc.
          - in ~/mixminionrc

       If the configuration file is not found in the specified location,
       we create a fresh one.
    """
    if configFile is None:
        configFile = os.environ.get("MIXMINIONRC")
    if configFile is None:
        for candidate in ["~/.mixminionrc", "~/mixminionrc"]:
            if os.path.exists(os.path.expanduser(candidate)):
                configFile = candidate
                break
    if configFile is None:
        if sys.platform == 'win32':
            configFile = "~/mixminionrc"
        else:
            configFile = "~/.mixminionrc"
    if configFile is not None:
        configFile = os.path.expanduser(configFile)

    if not os.path.exists(configFile):
        print >>sys.stderr,"Writing default configuration file to %r"%configFile
        installDefaultConfig(configFile)

    try:
        return mixminion.Config.ClientConfig(fname=configFile)
    except (IOError, OSError), e:
        print >>sys.stderr, "Error reading configuration file %r:"%configFile
        print >>sys.stderr, "   ", str(e)
        sys.exit(1)
    except mixminion.Config.ConfigError, e:
        print >>sys.stderr, "Error in configuration file %r"%configFile
        print >>sys.stderr, "   ", str(e)
        sys.exit(1)
    return None #suppress pychecker warning

class CLIArgumentParser:
    """Helper class to parse common command line arguments.

       The following arguments are recognized:
          COMMON
             -h | --help : print usage and exit.
             -f | --config : specify a configuration file.
             -v | --verbose : run verbosely.
          DIRECTORY ONLY
             -D | --download-directory : force/disable directory downloading.
          PATH-RELATED
             -t | --to : specify an exit address
             -R | --reply-block | --reply-block-fd : specify a reply block
             -P | --path : specify a literal path.
          REPLY PATH ONLY
             --lifetime : Required lifetime of new reply blocks.
          MESSAGE-SENDING ONLY:
             --queue | --no-queue : force/disable queueing.

         The class's constructor parses command line options, as required.
         The .init() method initializes a config file, logging, a
           MixminionClient object, or the ClientDirectory object as requested.
         The parsePath method parses the path as given.

         DOCDOC --status-fd
    """
    ##Fields:
    #  want*: as given as arguments to __init__
    # [CALL "init()" before using these.
    #  config: ClientConfig, or None.
    #  directory: ClientDirectory, or None.
    #  client: MixminionClient, or None.
    #  keyring: ClientKeyring, or None.
    # [As specified on command line"
    #  path: path string, or None.
    #  nHops: number of hops, or None.
    #  address: exit address, or None.
    #  lifetime: SURB lifetime, or None.
    #  replyBlockSources: list of SURB filenames (string), or SURB fds (int).
    #  configFile: Filename of configuration file, or None.
    #  forceQueue: true if "--queue" is set.
    #  forceNoQueue: true if "--no-queue" is set.
    #  verbose: true if verbose mode is set.
    #  download: 1 if the user told us to download the directory, 0 if
    #    they told us not to download it, and None if they didn't say.
    # [Not public]
    #  path1, path2 -- path as generated by parsePath.

    def __init__(self, opts,
                 wantConfig=0, wantClientDirectory=0, wantClient=0, wantLog=0,
                 wantDownload=0, wantForwardPath=0, wantReplyPath=0,
                 ignoreOptions=[]):
        """Parse the command line options 'opts' as returned by getopt.getopt.

           wantConfig -- If true, accept options pertaining to the config file,
              and generate a ClientConfig object when self.init() is called.
           wantClientDiredctory -- If true, accept options pertaining to the
              client directory, and generate a ClientDirectory object when
              self.init() is called.
           wantClient -- If true, generate a MixminionClient when self.init()
              is called.
           wantLog -- If true, configure logging.
           wantDownload -- If true, accept options pertaining to downloading
              a new directory, and download the directory as required.
           wantForawrdPath -- If true, accept options to specify a forward
              path (for forward or reply messages), and enable self.parsePath.
           wantReplyPath -- If true, accept options to specify a path for
              a reply block, and enable self.parsePath.
        """
        self.config = None
        self.directory = None
        self.client = None
        self.path1 = None
        self.path2 = None

        if wantForwardPath: wantClientDirectory = 1
        if wantReplyPath: wantClientDirectory = 1
        if wantDownload: wantClientDirectory = 1
        if wantClientDirectory: wantConfig = 1
        if wantClient: wantConfig = 1

        self.wantConfig = wantConfig
        self.wantClientDirectory = wantClientDirectory
        self.wantClient = wantClient
        self.wantLog = wantLog
        self.wantDownload = wantDownload
        self.wantForwardPath = wantForwardPath
        self.wantReplyPath = wantReplyPath

        self.configFile = None
        self.verbose = self.quiet = 0
        self.download = None
        self.password_fileno = None

        self.path = None
        self.exitAddress = None
        self.lifetime = None
        self.replyBlockSources = []

        self.forceQueue = None
        self.forceNoQueue = None

        for o,v in opts:
            if o in ignoreOptions: continue
            if o in ('-h', '--help'):
                raise UsageError()
            elif o in ('-f', '--config'):
                self.configFile = v
            elif o in ('-v', '--verbose'):
                self.verbose = 1
            elif o in ('-Q', '--quiet'):
                self.quiet = 1
            elif o in ('-D', '--download-directory'):
                assert wantDownload
                download = v.lower()
                if download in ('0','no','false','n','f'):
                    dl = 0
                elif download in ('1','yes','true','y','t','force'):
                    dl = 1
                else:
                    raise UIError(
                        "Unrecognized value for %s. Expected 'yes' or 'no'"%o)
                if self.download not in (None, dl):
                    raise UIError(
                        "Value of %s for %s conflicts with earlier value" %
                        (v, o))
                self.download = dl
            elif o in ('-t', '--to'):
                #assert wantForwardPath or wantReplyPath
                #XXXX008 reenable, sanely.
                if self.exitAddress is not None:
                    raise UIError("Multiple addresses specified.")
                try:
                    self.exitAddress = mixminion.ClientDirectory.parseAddress(v)
                except ParseError, e:
                    raise UsageError(str(e))
            elif o in ('-R', '--reply-block'):
                #assert wantForwardPath #XXXX008 re-enable, sanely
                self.replyBlockSources.append(v)
            elif o == '--reply-block-fd':
                try:
                    self.replyBlockSources.append(int(v))
                except ValueError:
                    raise UIError("%s expects an integer"%o)
            elif o in ('-H', '--hops'):
                raise UIError("The %s flag is deprecated; use -P '*%s' instead"
                              %(o,v))
            elif o in ('-P', '--path'):
                assert wantForwardPath or wantReplyPath
                if self.path is not None:
                    raise UIError("Multiple paths specified")
                self.path = v
            elif o in ('--lifetime',):
                assert wantReplyPath
                if self.lifetime is not None:
                    raise UIError("Multiple --lifetime arguments specified")
                try:
                    self.lifetime = int(v)
                except ValueError:
                    raise UsageError("%s expects an integer"%o)
            elif o in ('--passphrase-fd',):
                try:
                    self.password_fileno = int(v)
                except ValueError:
                    raise UsageError("%s expects an integer"%o)
            elif o in ('--queue',):
                self.forceQueue = 1
            elif o in ('--no-queue',):
                self.forceNoQueue = 1
            elif o in ('--status-fd',):
                try:
                    STATUS.setFD(int(v))
                except ValueError:
                    raise UsageError("%s expects an integer"%o)

        if self.quiet and self.verbose:
            raise UsageError("I can't be quiet and verbose at the same time.")

    def init(self):
        """Configure objects and initialize subsystems as specified by the
           command line."""
        if self.verbose:
            severity = "TRACE"
        elif self.quiet:
            severity = "ERROR"
        else:
            severity = "INFO"

        if self.wantConfig:
            self.config = readConfigFile(self.configFile)
            if self.wantLog:
                LOG.configure(self.config)
                LOG.setMinSeverity(severity)
            mixminion.Common.configureShredCommand(self.config)
            mixminion.Common.configureFileParanoia(self.config)
            if not self.verbose:
                try:
                    LOG.setMinSeverity("WARN")
                    mixminion.Crypto.init_crypto(self.config)
                finally:
                    LOG.setMinSeverity(severity)
            else:
                mixminion.Crypto.init_crypto(self.config)

            userdir = self.config['User']['UserDir']
            configureClientLock(os.path.join(userdir, "lock"))
        else:
            if self.wantLog:
                LOG.setMinSeverity(severity)
            userdir = None

        if self.wantClient:
            assert self.wantConfig
            LOG.debug("Configuring client")
            self.client = MixminionClient(self.config, self.password_fileno)

        if self.wantClientDirectory:
            assert self.wantConfig
            assert _CLIENT_LOCKFILE
            LOG.debug("Configuring server list")
            self.directory = mixminion.ClientDirectory.ClientDirectory(
                config=self.config, diskLock=ClientDiskLock())
            self.directory._installAsKeyIDResolver()

        if self.wantDownload:
            assert self.wantClientDirectory
            if self.download != 0:
                clientLock()
                try:
                    self.directory.update(force=self.download)
                finally:
                    clientUnlock()

        if self.wantClientDirectory or self.wantDownload:
            self.directory.checkSoftwareVersion(client=1)

    def parsePath(self):
        # Sets: exitAddress, pathSpec.
        if self.wantReplyPath and self.exitAddress is None:
            address = self.config['Security'].get('SURBAddress')
            if address is None:
                raise UIError("No recipient specified; exiting.  (Try "
                              "using -t <your-address>)")
            try:
                self.exitAddress = mixminion.ClientDirectory.parseAddress(address)
            except ParseError, e:
                raise UIError("Error in SURBAddress: %s" % e)
        elif self.exitAddress is None and self.replyBlockSources == []:
            raise UIError("No recipients specified; exiting. (Try using "
                          "-t <recipient-address>")
        elif self.exitAddress is not None and self.replyBlockSources:
            raise UIError("Cannot use both a recipient and a reply block")
        elif self.replyBlockSources:
            useRB = 1
            surbs = []
            for fn in self.replyBlockSources:
                if isinstance(fn, IntType):
                    f = os.fdopen(fn, 'rb')
                    try:
                        s = f.read()
                    finally:
                        f.close()
                elif fn == '-':
                    s = sys.stdin.read()
                else:
                    assert isinstance(fn, StringType)
                    s = readFile(fn, 1)
                try:
                    if stringContains(s,
                                      "-----BEGIN TYPE III REPLY BLOCK-----"):
                        surbs.extend(parseTextReplyBlocks(s))
                    else:
                        surbs.extend(parseReplyBlocks(s))
                except ParseError, e:
                        raise UIError("Error parsing %s: %s" % (fn, e))
            self.surbList = surbs
            self.exitAddress = mixminion.ClientDirectory.ExitAddress(isReply=1)
        else:
            assert self.exitAddress is not None
            useRB = 0

        isSURB = isReply = 0
        if self.wantReplyPath:
            p = 'SURBPath'; isSURB = 1
        elif useRB:
            p = 'ReplyPath'; isReply = 1
        else:
            p = 'ForwardPath'
        if self.path is None:
            self.path = self.config['Security'].get(p, "~5")

        if isSURB:
            if self.lifetime is not None:
                duration = self.lifetime * 24*60*60
            else:
                duration = int(self.config['Security']['SURBLifetime'])
        else:
            duration = 24*60*60

        self.startAt = time.time()
        self.endAt = previousMidnight(self.startAt+duration)

        self.pathSpec = mixminion.ClientDirectory.parsePath(
            self.config, self.path, isReply=isReply, isSURB=isSURB)
        self.directory.validatePath(self.pathSpec, self.exitAddress,
                                    self.startAt, self.endAt)

    def generatePaths(self, n):
        return self.directory.generatePaths(n,self.pathSpec,self.exitAddress,
                                            self.startAt,self.endAt)

# DOCDOC
def getOptions(args, shortOpts="", longOpts=(), dir=0, reply=0, path=0,
               headers=0, argsOK=0, dest=0, input=0, output=0, passphrase=0):
    longOpts = list(longOpts)
    shortOpts += "hvQf:"
    longOpts += ["help", "verbose", "quiet", "config=", "status-fd="]
    if dir:
        shortOpts += "D:"
        longOpts += ["download-directory="]
    if reply:
        shortOpts += "R:"
        longOpts += ["reply-block=", "reply-block-fd="]
    if path:
        shortOpts += "P:H:"
        longOpts += ["path=","hops="]
    if headers:
        longOpts += ["subject=", "from=", "in-reply-to=", "references="]
    if dest:
        shortOpts += "t:"
        longOpts += ["to="]
    if input:
        shortOpts += "i:"
        longOpts += ["input="]
    if output:
        shortOpts += "o:"
        longOpts += ["output="]
    if passphrase:
        longOpts += ["passphrase-fd="]
    o, a = getopt.getopt(args, shortOpts, longOpts)
    if a and not argsOK:
        raise UsageError("No arguments expected")
    return o, a

_SEND_USAGE = """\
Usage: %(cmd)s [options] <-t address>|<--to=address>|
                          <-R reply-block>|<--reply-block=reply-block>
Options:
  -h, --help                 Print this usage message and exit.
  -v, --verbose              Display extra debugging messages.
  -D <yes|no>, --download-directory=<yes|no>
                             Force the client to download/not to download a
                               fresh directory.
  -f <file>, --config=<file> Use a configuration file other than ~/.mixminionrc
                               (You can also use MIXMINIONRC=FILE)
  -i <file>, --input=<file>  Read the message from <file>. (Defaults to stdin.)
  -P <path>, --path=<path>   Specify an explicit message path.
  -t address, --to=address   Specify the recipient's address.
  -R <file>, --reply-block=<file>
                             %(Send)s the message to a reply block in <file>,
                             or '-' for a reply block read from stdin.
  --subject=<str>, --from=<str>, --in-reply-to=<str>, --references=<str>
                             Specify an email header for the exiting message.
  --deliver-fragments        If the message is too long to fit in a single
                             packet, then deliver multiple fragmented packets
                             to the recipient instead of having the server
                             reassemble the message.
  --reply-block-fd=<N>       Read reply blocks from file descriptor <N>.
%(extra)s

EXAMPLES:
  %(Send)s a message contained in a file <data> to user@domain.
      %(cmd)s -t user@domain -i data
  As above, but force 6 hops.
      %(cmd)s -t user@domain -i data -P '*6'
  As above, but use a random number of hops (about 6).
      %(cmd)s -t user@domain -i data -P '~6'
  As above, but use the server nicknamed Foo for the first hop and the server
  whose descriptor is stored in bar/baz for the last hop.
      %(cmd)s -t user@domain -i data -P 'Foo,~4,bar/baz'
  As above, but switch legs of the path after the second hop.
      %(cmd)s -t user@domain -i data -P 'Foo,?:~3,bar/baz'
  Specify an explicit path
      %(cmd)s -t user@domain -i data -P 'Foo,Bar,Baz,Quux,Fee,Fie,Foe'
  Specify an explicit path with a swap point
      %(cmd)s -t user@domain -i data -P 'Foo,Bar,Baz,Quux:Fee,Fie,Foe'
  %(Send)s the message to a reply block stored in 'FredsBlocks', using a
  randomly chosen first leg.
      %(cmd)s -t user@domain -i data -R FredsBlocks
  %(Send)s the message to a reply block stored in 'FredsBlocks', specifying
  the first leg.
      %(cmd)s -t user@domain -i data -R FredsBlocks -P 'Foo,Bar,Baz'
  Read the message from standard input.
      %(cmd)s -t user@domain
  Force a fresh directory download
      %(cmd)s -D yes
  %(Send)s a message without downloading a new directory, even if the current
  directory is out of date.
      %(cmd)s -D no -t user@domain -i data
""".strip()

def sendUsageAndExit(cmd, error=None):
    """Print a usage message for the mixminion send command (and family)
       and exit."""
    if error:
        print >>sys.stderr, "ERROR: %s"%error
        print >>sys.stderr, "For usage, run 'mixminion send --help'"
        sys.exit(1)
    if cmd.endswith(" queue"):
        print _SEND_USAGE % { 'cmd' : cmd, 'send' : 'queue', 'Send': 'Queue',
                              'extra' : '' }
    else:
        print _SEND_USAGE % { 'cmd' : cmd, 'send' : 'send', 'Send': 'Send',
                              'extra' : """\
  --queue                    Queue the message; don't send it.
  --no-queue                 Do not attempt to queue the message.""" }
    sys.exit(0)

if sys.platform == 'win32':
    EOF_STR = "Ctrl-Z, Return"
else:
    EOF_STR = "Ctrl-D"

def runClient(cmd, args):
    """[Entry point]  Generate an outgoing mixminion message and possibly
       send it.  Implements 'mixminion send' and 'mixminion queue'."""

    # Are we queueing?
    queueMode = 0
    if cmd.endswith(" queue"):
        queueMode = 1

    ###
    # Parse and validate our options.
    options, args = getOptions(args, "",
                               ["queue", "no-queue", "deliver-fragments"],
                               dir=1,reply=1,path=1,headers=1,dest=1,input=1)

    if not options:
        sendUsageAndExit(cmd)

    inFile = '-'
    h_subject = h_from = h_irt = h_references = None
    no_ss_fragment = 0
    for opt,val in options:
        if opt in ('-i', '--input'):
            inFile = val
        elif opt == '--subject':
            h_subject = val
        elif opt == '--from':
            h_from = val
        elif opt == '--in-reply-to':
            h_irt = val
        elif opt == '--references':
            h_references = val
        elif opt == '--deliver-fragments':
            no_ss_fragment = 1

    try:
        parser = CLIArgumentParser(options, wantConfig=1,wantClientDirectory=1,
                                   wantClient=1, wantLog=1, wantDownload=1,
                                   wantForwardPath=1)
        if queueMode and parser.forceNoQueue:
            raise UsageError("Can't use --no-queue option with queue command")
        if parser.forceQueue and parser.forceNoQueue:
            raise UsageError("Can't use both --queue and --no-queue")
    except UsageError, e:
        e.dump()
        sendUsageAndExit(cmd)

    # Encode the headers early so that we die before reading the message if
    # they won't work.
    try:
        headerStr = encodeMailHeaders(subject=h_subject, fromAddr=h_from,
                                      inReplyTo=h_irt, references=h_references)
    except MixError, e:
        raise UIError("Invalid headers: %s"%e)
    if no_ss_fragment:
        if headerStr != '\n':
            raise UIError("Can't use --deliver-fragments with message headers")
        else:
            # suppress intial newline.
            headerStr = ""

    if inFile == '-' and '-' in parser.replyBlockSources:
        raise UIError(
            "Can't read both message and reply block from stdin")

    # FFFF Make queueing configurable from .mixminionrc
    forceQueue = queueMode or parser.forceQueue
    forceNoQueue = parser.forceNoQueue

    parser.init()
    client = parser.client
    parser.parsePath()
    address = parser.exitAddress
    # Tell the address about the headers, so it knows to pick an exit
    # that supports them.
    address.setHeaders(parseMessageAndHeaders(headerStr+"\n")[1])

    # Get our surb, if any.
    if address.isReply and inFile == '-':
        # We check to make sure that we have a valid SURB before reading
        # from stdin.
        surblog = client.openSURBLog()
        try:
            s = surblog.findUnusedSURBs(parser.surbList)
            if s is None:
                raise UIError("No unused and unexpired reply blocks found.")
        finally:
            surblog.close()

    # Read the message.
    # XXXX Clean up this ugly control structure.
    if address and inFile == '-' and not address.hasPayload():
        message = None
        LOG.info("Sending dummy message")
    else:
        if address and not address.hasPayload():
            raise UIError("Cannot send a message in a DROP packet")

        try:
            if inFile == '-':
                if os.isatty(sys.stdin.fileno()):
                    print "Enter your message.  Type %s when you are done."%(
                        EOF_STR)
                message = sys.stdin.read()
            else:
                message = readFile(inFile)
        except KeyboardInterrupt:
            print "Interrupted.  Message not sent."
            sys.exit(1)

        message = "%s%s" % (headerStr, message)

        address.setExitSize(len(message))

    if parser.exitAddress.isReply:
        client.sendReplyMessage(
            parser.directory, parser.exitAddress, parser.pathSpec,
            parser.surbList, message,
            parser.startAt, parser.endAt, forceQueue, forceNoQueue)
    else:
        client.sendForwardMessage(
            parser.directory, parser.exitAddress, parser.pathSpec,
            message, parser.startAt, parser.endAt, forceQueue, forceNoQueue,
            forceNoServerSideFragments=no_ss_fragment)

_COUNT_PACKETS_USAGE = """\
Usage: mixminion count-packets [options] <-t address>|<--to=address>|
                              <-R>|<--reply>
Options:
  -h, --help                 Print this usage message and exit.
  -v, --verbose              Display extra debugging messages.
  -f <file>, --config=<file> Use a configuration file other than ~/.mixminionrc
                               (You can also use MIXMINIONRC=FILE)
  -i <file>, --input=<file>  Read the message from <file>. (Defaults to stdin.)
  --subject=<str>, --from=<str>, --in-reply-to=<str>, --references=<str>
                             Specify an email header for the exiting message.
  --deliver-fragments        If the message is too long to fit in a single
                             packet, then deliver multiple fragmented packets
                             to the recipient instead of having the server
                             reassemble the message.
  --reply                    Read reply blocks from file descriptor <N>.
  DOCDOC not right anymore
"""
def countPackets(cmd,args):
    options, args = getOptions(args, "R", ["reply"], dir=1, dest=1, path=1,
                               input=1, headers=1)

    inFile = '-'
    h_subject = h_from = h_irt = h_references = None
    no_ss_fragment = 0
    reply = 0
    for opt,val in options:
        if opt in ('-i', '--input'):
            inFile = val
        elif opt == '--subject':
            h_subject = val
        elif opt == '--from':
            h_from = val
        elif opt == '--in-reply-to':
            h_irt = val
        elif opt == '--references':
            h_references = val
        elif opt == '--deliver-fragments':
            no_ss_fragment = 1
        elif opt in ('-R', '--reply'):
            reply = 1
    try:
        parser = CLIArgumentParser(options, wantConfig=1,wantClientDirectory=1,
                                   wantLog=1)
    except UsageError, e:
        e.dump()
        print _COUNT_PACKETS_USAGE
        sys.exit()

    # Encode the headers early so that we die before reading the message if
    # they won't work.
    try:
        headerStr = encodeMailHeaders(subject=h_subject, fromAddr=h_from,
                                      inReplyTo=h_irt, references=h_references)
    except MixError, e:
        raise UIError("Invalid headers: %s"%e)
    if no_ss_fragment:
        if headerStr != '\n':
            raise UIError("Can't use --deliver-fragments with message headers")
        else:
            # suppress intial newline.
            headerStr = ""

    if inFile == '-' and '-' in parser.replyBlockSources:
        raise UIError(
            "Can't read both message and reply block from stdin")

    parser.init()
    address = parser.exitAddress
    if address and not reply:
        address.setHeaders(parseMessageAndHeaders(headerStr+"\n")[1])
    elif reply and address:
        raise UsageError("Cannot use both a recipient and a reply block")
    elif not reply and not address:
        raise UsageError("Must specify a recipient, or --reply for a reply")
    else:
        assert reply and not address
        address = mixminion.ClientDirectory.exitAddress(isReply=1)

    if address and inFile == '-' and not address.hasPayload():
        print "1 packet needed"
        STATUS.log("COUNT_PACKETS", "1")
        return
    else:
        if address and not address.hasPayload():
            raise UIError("Cannot send a message in a DROP packet")
        try:
            if inFile == '-':
                if os.isatty(sys.stdin.fileno()):
                    print "Enter your message.  Type %s when you are done."%(
                        EOF_STR)
                message = sys.stdin.read()
            else:
                message = readFile(inFile)
        except KeyboardInterrupt:
            print "Interrupted."
            return

        message = "%s%s"%(headerStr,message)
        address.setExitSize(len(message))

        if no_ss_fragment:
            prefix=""
        else:
            prefix=address.getFragmentedMessagePrefix()

        n = mixminion.BuildMessage.getNPacketsToEncode(message, 0, prefix)
        print "%d packets needed" % n
        STATUS.log("COUNT_PACKETS", str(n))

_PING_USAGE = """\
Usage: mixminion ping [options] serverName
Options
  -h, --help:             Print this usage message and exit.
  -v, --verbose           Display extra debugging messages.
  -f FILE, --config=FILE  Use a configuration file other than ~/.mixminionrc
  -D <yes|no>, --download-directory=<yes|no>
                          Force the client to download/not to download a
                            fresh directory.
"""
def runPing(cmd, args):
    """[Entry point] Send link padding to servers to see if they're up."""
    if len(args) == 1 and args[0] in ('-h', '--help'):
        print _PING_USAGE
        sys.exit(0)

    options, args = getOptions(args, dir=1, argsOK=1)

    print "==========================================================="
    print "WARNING: Pinging a server is potentially dangerous, since"
    print "      it might alert people that you plan to use the server"
    print "      for your messages.  Even if you ping *all* the servers,"
    print "      an attacker can see _when_ you pinged the servers and"
    print "      use this information to help a traffic analysis attack."
    print
    print "      This command is for testing only, and will go away before"
    print "      Mixminion 1.0.  By then, all listed servers will be"
    print "      reliable anyway.  <wink>"
    print "==========================================================="

    parser = CLIArgumentParser(options, wantConfig=1,
                               wantClientDirectory=1, wantClient=1,
                               wantLog=1, wantDownload=1)

    parser.init()

    directory = parser.directory
    client = parser.client

    for arg in args:
        if '.' in arg and not os.path.exists(arg):
            addrport = arg.split(":",1)
            if len(addrport) == 2:
                try:
                    port = int(addrport[1])
                except ValueError:
                    raise UIError("Invalid port: %r"%addrport[1])

            else:
                arg = "%s:48099"%arg
                port = 48099
            addr = addrport[0]
            routing = mixminion.Packet.MMTPHostInfo(addr,port,"\x00"*20)
            name = arg
        else:
            info = directory.getServerInfo(arg,
                                       startAt=time.time(), endAt=time.time(),
                                       strict=1)
            routing = info.getRoutingInfo()
            name = info.getNickname()

        ok, status = client.pingServer(routing)
        print ">>>", status
        print name, (ok and "is up" or "is down")

_IMPORT_SERVER_USAGE = """\
Usage: %(cmd)s [options] <filename> ...
Options:
   -h, --help:             Print this usage message and exit.
   -v, --verbose           Display extra debugging messages.
   -f FILE, --config=FILE  Use a configuration file other than ~/.mixminionrc

EXAMPLES:
  Import a ServerInfo from the file MyServer into our local directory.
      %(cmd)s MyServer
""".strip()

def importServer(cmd, args):
    """[Entry point] Manually add a server to the client directory."""
    options, args = getOptions(args, dir=1, argsOK=1)

    try:
        parser = CLIArgumentParser(options, wantConfig=1,wantClientDirectory=1,
                                   wantLog=1)
    except UsageError, e:
        e.dump()
        print _IMPORT_SERVER_USAGE % { 'cmd' : cmd }
        sys.exit(1)

    parser.init()
    directory = parser.directory

    clientLock()
    try:
        for filename in args:
            print "Importing from", filename
            try:
                directory.importFromFile(filename)
            except MixError, e:
                print "Error while importing %s: %s" % (filename, e)
    finally:
        clientUnlock()

    print "Done."

_LIST_SERVERS_USAGE = """\
Usage: %(cmd)s [options] [server names]
Options:
  -h, --help:                Print this usage message and exit.
  -v, --verbose              Display extra debugging messages.
  -f <file>, --config=<file> Use a configuration file other than ~/.mixminionrc
  -D <yes|no>, --download-directory=<yes|no>
                             Force the client to download/not to download a
                               fresh directory.

  -r, --recommended          Only display recommended servers.
  -T, --with-time            Display validity intervals for server descriptors.
  --no-collapse              Don't combine descriptors with adjacent times.
  -s <str>,--separator=<str> Separate features with <str> instead of tab.
  -c, --cascade              Pretty-print results, cascading by descriptors.
  -C, --cascade-features     Pretty-print results, cascading by features.
  -J, --justify              Justify features into columns
  -F <name>,--feature=<name> Select which server features to list.
  --list-features            Display a list of all recognized features.

EXAMPLES:
  List all currently known servers.
      %(cmd)s
  Same as above, but explicitly name the features to be listed.
      %(cmd)s -F caps -F status
""".strip()

def listServers(cmd, args):
    """[Entry point] Print info about servers in the directory, or on
       the command line."""
    options, args = getOptions(args, "F:JTrRs:cC",
                               ['feature=', 'justify',
                                'with-time', "no-collapse", "recommended",
                                "separator=", "cascade","cascade-features",
                                'list-features'],
                               dir=1, argsOK=1)
    try:
        parser = CLIArgumentParser(options, wantConfig=1,
                                   wantClientDirectory=1,
                                   wantLog=1, wantDownload=1,
                                   ignoreOptions=['-R'])
    except UsageError, e:
        e.dump()
        print _LIST_SERVERS_USAGE % {'cmd' : cmd}
        sys.exit(1)
    features = []
    cascade = 0
    showTime = 0
    goodOnly = 0
    separator = None
    justify = 0
    listFeatures = 0
    for opt,val in options:
        if opt in ('-F', '--feature'):
            features.extend(val.split(","))
        elif opt in ('-T', '--with-time'):
            showTime = 1
        elif opt == ('--no-collapse'):
            showTime = 2
        elif opt in ('-r', '--recommended'):
            goodOnly = 1
        elif opt == '-R':
            #XXXX009 remove; deprecated since 0.0.7
            LOG.warn("The -R option is deprecated. Please say -r instead.")
            goodOnly = 1
        elif opt in ('-s', '--separator'):
            separator = val
        elif opt in ('-c', '--cascade'):
            cascade = 1
        elif opt in ('-C', '--cascade-features'):
            cascade = 2
        elif opt in ('-J', '--justify'):
            justify = 1
        elif opt == ('--list-features'):
            listFeatures = 1

    if listFeatures:
        features = mixminion.Config.getFeatureList(ServerInfo)
        features.append(("caps",))
        features.append(("status",))
        for f in features:
            if len(f)>1:
                print "%-30s (abbreviate as %s)" % (
                    f[0], englishSequence(f[1:],compound="or"))
            else:
                print f[0]
        return

    if not features:
        if goodOnly:
            features = [ 'caps' ]
        else:
            features = [ 'caps', 'status' ]

    if separator is None:
        if justify:
            separator = ' '
        else:
            separator = '\t'

    parser.init()
    directory = parser.directory

    # Look up features in directory.
    featureMap = directory.getFeatureMap(features,goodOnly=goodOnly)

    # If any servers are listed on the command line, restrict to those
    # servers.
    if args:
        lcargs = [ arg.lower() for arg in args ]
        lcfound = {}
        restrictedMap = {}
        for nn,v in featureMap.items():
            if nn.lower() in lcargs:
                restrictedMap[nn] = v
                lcfound[nn.lower()] = 1
        for arg in args:
            if not lcfound.has_key(arg.lower()):
                if goodOnly:
                    raise UIError("No recommended descriptors found for %s"%
                                  arg)
                else:
                    raise UIError("No descriptors found for %s"%arg)
        featureMap = restrictedMap

    # Collapse consecutive server descriptors with matching features.
    if showTime < 2:
        featureMap = mixminion.ClientDirectory.compressFeatureMap(
            featureMap, ignoreGaps=(not showTime), terse=(not showTime))

    # Now display the result.
    for line in mixminion.ClientDirectory.formatFeatureMap(
        features,featureMap,showTime,cascade,separator,justify):
        print line


_UPDATE_SERVERS_USAGE = """\
Usage: %(cmd)s [options]
Options:
  -h, --help:                Print this usage message and exit.
  -v, --verbose              Display extra debugging messages.
  -f <file>, --config=<file> Use a configuration file other than ~/.mixminionrc
                             (You can also use MIXMINIONRC=FILE)

EXAMPLES:
  Download a new list of servers.  (Note that the 'mixminion send' and
  the 'mixminion generate-surbs' commands do this by default.)
      %(cmd)s
""".strip()

def updateServers(cmd, args):
    options, args = getOptions(args)

    try:
        parser = CLIArgumentParser(options, wantConfig=1, wantClientDirectory=1,
                                   wantLog=1)
    except UsageError, e:
        e.dump()
        print _UPDATE_SERVERS_USAGE % { 'cmd' : cmd }
        sys.exit(1)

    parser.init()
    directory = parser.directory
    clientLock()
    try:
        directory.update(force=1)
    finally:
        clientUnlock()
    print "Directory updated"

_CLIENT_DECODE_USAGE = """\
Usage: %(cmd)s [options] -i <file>|--input=<file>
Options:
  -h, --help:                Print this usage message and exit.
  -v, --verbose              Display extra debugging messages.
  -f <file>, --config=<file> Use a configuration file other than ~/.mixminionrc
                             (You can also use MIXMINIONRC=FILE)
  -F, --force:               Decode the input files, even if they seem
                             overcompressed.
  -o <file>, --output=<file> Write the results to <file> rather than stdout.
  -i <file>, --input=<file>  Read the results from <file>.
  --passphrase-fd=<N>        Read passphrase from file descriptor N instead
                               of asking on the console.

EXAMPLES:
  Decode message(s) stored in 'NewMail', writing the result to stdout.
      %(cmd)s -i NewMail
  Decode message(s) stored in 'NewMail', writing the result to 'Decoded'.
      %(cmd)s -i NewMail -o  Decoded
""".strip()

def clientDecode(cmd, args):
    """[Entry point] Decode a message."""
    options, args = getOptions(args, "F", ["--force"], input=1, output=1,
                               passphrase=1)

    outputFile = '-'
    inputFile = '-'
    force = 0
    for o,v in options:
        if o in ('-o', '--output'):
            outputFile = v
        elif o in ('-F', '--force'):
            force = 1
        elif o in ('-i', '--input'):
            inputFile = v

    try:
        parser = CLIArgumentParser(options, wantConfig=1, wantClient=1,
                                   wantLog=1)
    except UsageError, e:
        e.dump()
        print _CLIENT_DECODE_USAGE % { 'cmd' : cmd }
        sys.exit(1)

    if args:
        msg = "Unexpected arguments."
        if len(args) == 1:
            msg += " (Did you mean '-i %s'?)" % args[0]
        raise UIError(msg)

    parser.init()
    client = parser.client

    if outputFile == '-':
        out = sys.stdout
    else:
        # ???? Should we sometimes open this in text mode?
        out = open(outputFile, 'wb')

    tty = os.isatty(out.fileno())

    if inputFile == '-':
        s = sys.stdin.read()
    else:
        try:
            s = readFile(inputFile)
        except OSError, e:
            LOG.error("Could not read file %s: %s", inputFile, e)
    try:
        res = client.decodeMessage(s, force=force, isatty=tty)
    except ParseError, e:
        raise UIError("Couldn't parse message: %s"%e)

    for r in res:
        out.write(r)
    out.close()

_GENERATE_SURB_USAGE = """\
Usage: %(cmd)s [options]
Options:
  -h, --help                 Print this usage message and exit.
  -v, --verbose              Display extra debugging messages.
  -D <yes|no>, --download-directory=<yes|no>
                             Force the client to download/not to download a
                               fresh directory.
  -f <file>, --config=<file> Use a configuration file other than ~/.mixminionrc
                               (You can also use MIXMINIONRC=FILE)
  -P <path>, --path=<path>   Specify an explicit path.
  -t address, --to=address   Specify the block's address. (Defaults to value
                               in configuration file.)
  -o <file>, --output=<file> Write the reply blocks to <file> instead of
                               stdout.
  -b, --binary               Write the reply blocks in binary mode instead
                               of ascii mode.
  -n <N>, --count=<N>        Generate <N> reply blocks. (Defaults to 1.)
  --identity=<name>          Specify a pseudonymous identity.
  --passphrase-fd=<N>        Read passphrase from file descriptor N instead
                               of asking on the console.

EXAMPLES:
  Generate a reply block to deliver messages to the address given in
  ~/.mixminiond.conf; choose a path at random; write the block to stdout.
      %(cmd)s
  As above, but force change address to deliver to user@domain.
      %(cmd)s -t user@domain
  As above, but force a 6-hop path.
      %(cmd)s -t user@domain -P '*6'
  As above, but force the first hop to be 'Foo' and the last to be 'Bar'.
      %(cmd)s -t user@domain 'Foo,*4,Bar'
  As above, but write the reply block to the file 'MyBlocks'.
      %(cmd)s -t user@domain -P 'Foo,*4,Bar' -o MyBlocks
  As above, but write the reply block in binary mode.
      %(cmd)s -t user@domain -P 'Foo,*4,Bar' -o MyBlocks -b
  As above, but generate 100 reply blocks.
      %(cmd)s -t user@domain -P 'Foo,*4,Bar' -o MyBlocks -b -n 100
  Specify an explicit path.
      %(cmd)s -P 'Foo,Bar,Baz,Quux'
  Generate 10 reply blocks without downloading a new directory, even if the
  current directory is out of date.
      %(cmd)s -D no -n 10
""".strip()

def generateSURB(cmd, args):
    options, args = getOptions(args,
                               "bn:", ["binary", "count=", "identity="],
                               dir=1, dest=1, path=1, passphrase=1, output=1)

    outputFile = '-'
    binary = 0
    count = 1
    identity = ""
    for o,v in options:
        if o in ('-o', '--output'):
            outputFile = v
        elif o in ('-b', '--binary'):
            binary = 1
        elif o in ('-n', '--count'):
            try:
                count = int(v)
            except ValueError:
                print "ERROR: %s expects an integer" % o
                sys.exit(1)
        elif o in ('--identity',):
            identity = v
    try:
        parser = CLIArgumentParser(options, wantConfig=1, wantClient=1,
                                   wantLog=1, wantClientDirectory=1,
                                   wantDownload=1, wantReplyPath=1)
    except UsageError, e:
        e.dump()
        print _GENERATE_SURB_USAGE % { 'cmd' : cmd }
        sys.exit(0)

    if args:
        print >>sys.stderr, "ERROR: Unexpected arguments"
        print _GENERATE_SURB_USAGE % { 'cmd' : cmd }
        sys.exit(0)

    parser.init()

    client = parser.client

    parser.parsePath()

    if outputFile == '-':
        out = sys.stdout
    elif binary:
        out = open(outputFile, 'wb')
    else:
        out = open(outputFile, 'w')

    for path1,path2 in parser.generatePaths(count):
        assert path2 and not path1
        surb = client.generateReplyBlock(parser.exitAddress, path2,
                                         name=identity,
                                         expiryTime=parser.endAt)
        if binary:
            out.write(surb.pack())
        else:
            out.write(surb.packAsText())

    if outputFile != '-':
        out.close()

_INSPECT_SURBS_USAGE = """\
Usage: %(cmd)s [options] <files>
  -h, --help                 Print this usage message and exit.
  -v, --verbose              Display extra debugging messages.
  -f <file>, --config=<file> Use a configuration file other than ~/.mixminionrc
                               (You can also use MIXMINIONRC=FILE)

EXAMPLES:
  Examine properties of reply blocks stored in 'FredsBlocks'.
      %(cmd)s FredsBlocks
""".strip()

def inspectSURBs(cmd, args):
    options, args = getOptions(args, argsOK=1)

    try:
        parser = CLIArgumentParser(options, wantConfig=1, wantLog=1,
                                   wantClient=1)
    except UsageError, e:
        e.dump()
        print _INSPECT_SURBS_USAGE % { 'cmd' : cmd }
        sys.exit(1)

    parser.init()

    surblog = parser.client.openSURBLog()

    try:
        for fn in args:
            s = readFile(fn, 1)
            print "==== %s"%fn
            try:
                if stringContains(s, "-----BEGIN TYPE III REPLY BLOCK-----"):
                    surbs = parseTextReplyBlocks(s)
                else:
                    surbs = parseReplyBlocks(s)

                for surb in surbs:
                    used = surblog.isSURBUsed(surb) and "yes" or "no"
                    print surb.format()
                    print "Used:", used
                    STATUS.log("INSPECT_SURB",
                               "%s %s %s"%(surb.getHexDigest(),
                                           surb.timestamp,
                                   surblog.isSURBUsed(surb) and "1" or "0"))
            except ParseError, e:
                print "Error while parsing: %s"%e
    finally:
        surblog.close()

_FLUSH_QUEUE_USAGE = """\
Usage: %(cmd)s [options] [servername] ...
  -h, --help                 Print this usage message and exit.
  -v, --verbose              Display extra debugging messages.
  -f <file>, --config=<file> Use a configuration file other than ~/.mixminionrc
                               (You can also use MIXMINIONRC=FILE)
  -n <n>, --count=<n>        Send no more than <n> packets from the queue.

EXAMPLES:
  Try to send all currently queued packets.
      %(cmd)s
  Try to send at most 10 currently queued packets, chosen at random.
      %(cmd)s -n 10
  Try to send all currently queued packets for the server named 'Example1', or
  for the server whose hostname is 'minion.example.com'.
      %(cmd)s Example1 minion.example.com

""".strip()

def flushQueue(cmd, args):
    options, args = getOptions(args, "", ["count="], argsOK=1)
    count=None
    for o,v in options:
        if o in ('-n','--count'):
            try:
                count = int(v)
            except ValueError:
                print "ERROR: %s expects an integer" % o
                sys.exit(1)
    try:
        parser = CLIArgumentParser(options, wantConfig=1, wantLog=1,
                                   wantClient=1,
                                   wantClientDirectory=len(args))
    except UsageError, e:
        e.dump()
        print _FLUSH_QUEUE_USAGE % { 'cmd' : cmd }
        sys.exit(1)

    parser.init()
    client = parser.client

    if args:
        handles = parser.client.queue.getHandlesByDestAndAge(
            args, parser.directory, notAfter=None)
    else:
        handles = None

    client.flushQueue(count, handles)

_CLEAN_QUEUE_USAGE = """\
Usage: %(cmd)s  [options] [servername...]
  -h, --help                 Print this usage message and exit.
  -v, --verbose              Display extra debugging messages.
  -f <file>, --config=<file> Use a configuration file other than ~/.mixminionrc
                               (You can also use MIXMINIONRC=FILE)
  -d <n>, --days=<n>         Remove all packets older than <n> days old.

EXAMPLES:
  Remove all pending packets older than one week.
      %(cmd)s -d 7
  Remove all pending packets for the server 'Example1'.
      %(cmd)s Example1
""".strip()

def cleanQueue(cmd, args):
    options, args = getOptions(args, "d:", ["days"])
    days = 60
    for o,v in options:
        if o in ('-d','--days'):
            try:
                days = int(v)
            except ValueError:
                print "ERROR: %s expects an integer" % o
                sys.exit(1)
    try:
        parser = CLIArgumentParser(options, wantConfig=1, wantLog=1,
                                   wantClient=1,
                                   wantClientDirectory=len(args))
    except UsageError, e:
        e.dump()
        print _CLEAN_QUEUE_USAGE % { 'cmd' : cmd }
        sys.exit(1)

    parser.init()
    client = parser.client
    notAfter = time.time() - days*24*60*60
    if args:
        handles = parser.client.queue.getHandlesByDestAndAge(
            args, parser.directory, notAfter)
    else:
        handles = parser.client.queue.getHandlesByAge(notAfter)

    client.cleanQueue(handles)

_LIST_QUEUE_USAGE = """\
Usage: %(cmd)s [options]
  -h, --help                 Print this usage message and exit.
  -v, --verbose              Display extra debugging messages.
  -f <file>, --config=<file> Use a configuration file other than ~/.mixminionrc
                               (You can also use MIXMINIONRC=FILE)

EXAMPLES:
  Describe the current contents of the queue.
      %(cmd)s
""".strip()

def listQueue(cmd, args):
    options, args = getOptions(args, dir=1)
    try:
        parser = CLIArgumentParser(options, wantConfig=1, wantLog=1,
                                   wantClient=1, wantClientDirectory=1)
    except UsageError, e:
        e.dump()
        print _LIST_QUEUE_USAGE % { 'cmd' : cmd }
        sys.exit(1)

    parser.init()
    client = parser.client

    clientLock()
    try:
        res = client.queue.inspectQueue()
    finally:
        clientUnlock()

    if not res:
        print "(No packets in queue)"
        return

    res_items = [ (displayServerByRouting(ri),count,date)
                  for ri,(count,date) in res.items() ]
    res_items.sort()
    now = time.time()
    for server, count, date in res_items:
        days = floorDiv(now-date, 24*60*60)
        if days < 1:
            days = "<1"
        print "%2d packets for %s (oldest is %s days old)"%(
            count, server, days)

_LIST_FRAGMENTS_USAGE = """\
Usage: %(cmd)s [options]
  -h, --help                 Print this usage message and exit.
  -v, --verbose              Display extra debugging messages.
  -f <file>, --config=<file> Use a configuration file other than ~/.mixminionrc
                               (You can also use MIXMINIONRC=FILE)

EXAMPLES:
  Describe the state of fragmented messages currently being reassembled.
      %(cmd)s
""".strip()

def listFragments(cmd, args):
    options, args = getOptions(args, dir=1)
    try:
        parser = CLIArgumentParser(options, wantConfig=1, wantLog=1,
                                   wantClient=1, wantClientDirectory=1)
    except UsageError, e:
        e.dump()
        print _LIST_FRAGMENTS_USAGE % { 'cmd' : cmd }
        sys.exit(1)

    parser.init()
    client = parser.client

    clientLock()
    try:
        res = client.pool.formatMessageList()
    finally:
        clientUnlock()

    if not res:
        print "(No fragments being reassembled)"
        return

    for line in res:
        print line


_REASSEMBLE_USAGE = """\
Usage: %(cmd)s [options] <message-id> ...
  -h, --help                 Print this usage message and exit.
  -v, --verbose              Display extra debugging messages.
  -f <file>, --config=<file> Use a configuration file other than ~/.mixminionrc
                               (You can also use MIXMINIONRC=FILE)
""".strip()

def reassemble(cmd, args):
    options, args = getOptions(args, "PF", ["purge", "force"],
                               output=1, argsOK=1)
    reassemble = 1
    if cmd.endswith("purge-fragments") or cmd.endswith("purge-fragment"):
        reassemble = 0
    try:
        parser = CLIArgumentParser(options, wantConfig=1, wantLog=1,
                                   wantClient=1, wantClientDirectory=1)
    except UsageError, e:
        e.dump()
        print _REASSEMBLE_USAGE % { 'cmd' : cmd }
        if reassemble:
            print """\
  -P, --purge                Remove the message from the pool.
  -F, --force:               Decode the message, even if it seems
                             overcompressed.
  -o <file>, --output=<file> Write the message to a file instead of stdout
""".strip()
        sys.exit(1)
    purge = not reassemble
    outfilename = None
    force = 0
    for o,v in options:
        if o in ('-o', '--output'):
            outfilename = v
        elif o in ("-P", "--purge"):
            purge = 1
        elif o in ("-F", "--force"):
            force = 1

    if not args:
        print "No message IDs provided."
        return

    parser.init()
    client = parser.client

    closeoutfile = 0
    out = None
    clientLock()
    try:
        removed = []
        for msgid in args:
            if reassemble:
                try:
                    msg = client.pool.getMessage(msgid, force=force)
                except CompressedDataTooLong:
                    raise UIError("Can't reassemble message %s: possible zlib bomb.")
                if out == None:
                    if outfilename in ('-',None):
                        out = sys.stdout
                    else:
                        out = open(outfilename, 'wb')
                        closeoutfile = 1
                out.write(msg)
            if purge:
                removed.append(msgid)
        client.pool.removeMessages(removed)
    finally:
        if closeoutfile:
            out.close()
        clientUnlock()
