#!/usr/bin/env python ############################################################################ # File: $Id: test-server.py 1363 2007-07-23 07:49:42Z scanner $ # # Copyright (C) 2003-2006 Eric "Scanner" Luce # # Author: Scanner # """Simple server to test a SocketServer based program. It will listen to the IMAP port and attempt to authenticate a user.. spitting what it gets to stdout. """ import sys if sys.hexversion < 0x020400F0: raise "python version 2.4 or higher required." import os import re import string import threading import socket import SocketServer # mhimap imports # import mhimap.IMAPParse from mhimap.Client import TestServerClient as Client from mhimap.IMAPParse import IMAPClientCommand from mhimap.Auth import User from mhimap.Auth import TestAuth from mhimap.IMAPProcess import AuthenticatedIMAPCommandProcessor from mhimap.IMAPProcess import PasswordPreAuthenticatedIMAPCommandProcessor from mhimap.UserMHDir import UserMHDir ############################################################################ ## ## Constants ## # Port to listen on. Set high so we can do this without root. # PORT = 8037 # RE's used by our imap command reader. # re_literal_start = re.compile('{(\d+)}\r\n$') # This dictionary contains a mapping of user name's to instantiated # usermhdir's. The idea is we instantiate a usermhdir once for a given # user while this server is running. We should shutdown the usermhdir # down (for a user) there are no clients connected whose user owns # a given usermhdir instantiation. # usermhdirs = {} # We instantiate the auth system we are going to use once for the # entire server. Each client connection is running in its own thread # and we could instantiate one for each thread, but we want to say there # is one for the entire server. # auth_system = TestAuth() ########################################################################## # class IMAPClientConnection(SocketServer.StreamRequestHandler): """Every 'request' handled by our threading tcp server results in a new thread being spawned. This thread will instantiate one of these objects every time a connection is made to our TCP socket. The 'handle()' method is called once the connection has been established and in there we basically loop reading messages from the client, parsing them, and forking off a thread to process the command. """ ####################################################################### # def setup(self): """Override the default 'setup' function. We need this to create the locker handler for the writing fd so that only one thread will try writing at a time. """ # Be sure to call our parent class's version of this method. We want # what it does to still be done. # SocketServer.StreamRequestHandler.setup(self) # We create a lock for being able to write to the client. only one # thread at a time can write to our client! # self.wlock = threading.Lock() # XXX We need to change this so that when we have an authenticated # XXX conenction we create the client object then (and we use a # XXX different command processor before we have authenticated the # XXX connection # Create our client object and pass it this connection. # self.client = Client(self) self.usermhdir = None # self.logged_out = False ####################################################################### # def shutdown(self): """ A wrapper for other objects to call to shutdown the connection. """ self.wfile.flush() self.wfile.close() self.rfile.close() self.connection.close() ####################################################################### # def write(self, msg): """This encapsulates how you write a message to our attached imap client. This handles the locking of the write socket to make sure that only one thread is writing out at a time. """ self.wlock.acquire() try: self.wfile.write(msg) print "To client: %s" % msg[:-2][:60] finally: self.wlock.release() ####################################################################### # def handle(self): print "connection from", self.client_address # The first thing we do when we get a connection from a # client is send to it our greeting string. # self.write("* OK IMAP4rev1 Service Ready\r\n") full_imap_message = "" # And now begins our loop of reading client messages and processing # them. # reading_string_literal = False string_literal_size = 0 while True: try: # If we are reading a string literal then we need to read # bytes, not just a line. # if reading_string_literal: line = self.rfile.read(string_literal_size) reading_string_literal = False if len(line) < string_literal_size: print "Error - while reading string literal read " \ "%d bytes, not expected %d" % (len(line), string_literal_size) break print "Read literal: %d bytes" % len(line) full_imap_message += line continue else: line = self.rfile.readline() print "Read line: %d bytes" % len(line) except AttributeError: # We should only get here if our readfile was just closed # on us, which should happen when we process the LOGOUT # command break except KeyboardInterrupt: sys.exit(0) # If the line is of zero-length then we got an EOF from our # client. For now we just exit this handler. # if len(line) == 0: break # A single IMAP message may be spread over several lines. See # if this is a partial message. If it is we need to tell our # client to send us more data. # # XXX In this implementation we read the client's entire message # XXX and pass it to our parsing routines. We assume that modern # XXX machines have enough ram to buffer entire messages from # XXX imap clients. It makes stuff simpler. If this is an issue # XXX we will make this fancier and all that guff. # # If the line we have read ends in '{[0-9]+}' then we have # not read in the entire command from the client. Chop off the # , append what we read to our current command and # start this loop over reading more. # m = re_literal_start.search(line) if m is not None: # This will cause us to read another line of input from the # client, append it to our current line so far and then attempt # to see if we can parse the command # reading_string_literal = True string_literal_size = int(m.group(1)) print "Beginning string literal. Need %d more bytes" % \ string_literal_size full_imap_message += line self.write("+ Ready for more input\r\n") continue else: full_imap_message += line # Okay, we get here then we have read the whole message. Try # to parse it and catch parsing errors. # print "Got full imap message, len: %d" % len(full_imap_message) try: imap_command = IMAPClientCommand(full_imap_message) except (mhimap.IMAPParse.NoMatch, mhimap.IMAPParse.UnknownCommand, mhimap.IMAPParse.BadLiteral, mhimap.IMAPParse.BadSyntax, mhimap.IMAPParse.UnknownSearchKey), e: if imap_command.tag is not None: self.write("%s BAD %s\r\n" % (imap_command.tag, str(e))) else: try: self.write("* BAD %s\r\n" % str(e.value)) except: break full_imap_message = "" continue # If the client is authenticated use the authenticated # command processor. Otherwise use the pre-authenticated command # processor # if self.client.authenticated(): # If the client is authenticated but we do not have a usermhdir # look up the proper usermhdir for this user. # if self.usermhdir is None: if self.client.user.name in usermhdirs: self.usermhdir = usermhdirs[self.client.user.name] else: self.usermhdir = UserMHDir(self.client.user.mh) self.usermhdir.resync_all_mailboxes() usermhdirs[self.client.user.name] = self.usermhdir cmd = AuthenticatedIMAPCommandProcessor(self.client, imap_command, self.usermhdir) else: cmd = PasswordPreAuthenticatedIMAPCommandProcessor(self.client, imap_command, auth_system) # And run the command. This will cause its processing to happen # in its own thread. If the command is blocking we will wait # on that thread to exit before we read any more from # the client. # cmd.start() if cmd.blocking: cmd.join() # Before we go back to the top and begin reading input from the # client again, null out the imap message we read last time. # full_imap_message = "" self.client.shutdown() ########################################################################## # class IMAPTCPServer(SocketServer.ThreadingTCPServer): """A threading tcp server that sets some parameters we want """ ###################################################################### # def __init___(self, server_address, RequestHandlerClass): # Let someone else rebind this port immediately when we are done with # it. # self.allow_reuse_address = True SocketServer.ThreadingTCPServer.__init__(self, server_address, RequestHandlerClass) # When the main thread exits we want our handlers to also exit. # self.daemon_threads = True ############################################################################ ## ## And so begins the program that will listen for TCP connections and then ## attempt to parse them. ## def main(): # Create a network server that accepts TCP connections and processes each # TCP connection in its own thread - the root of which is the handle() # method on an insace of the IMAPClientConnection class. # # We listen on port PORT, on all available interfaces. # print "Creating socket. Listening on port %s" % PORT server = IMAPTCPServer(("", PORT), IMAPClientConnection) try: try: print "Beginning actual service." server.serve_forever() except KeyboardInterrupt: print "Exiting due to keyboard interrupt" finally: # All done.. # server.server_close() ########### # # The work starts here # if __name__ == "__main__": main() # # ###########