📄 sb_server.py
字号:
#!/usr/bin/env python"""The primary server for SpamBayes.Currently serves the web interface, and any configured POP3 and SMTPproxies.The POP3 proxy works with classifier.py, and adds a simpleX-Spambayes-Classification header (ham/spam/unsure) to each incomingemail. You point the proxy at your POP3 server, and configure youremail client to collect mail from the proxy then filter on the addedheader. Usage: sb_server.py [options] [<server> [<server port>]] <server> is the name of your real POP3 server <port> is the port number of your real POP3 server, which defaults to 110. options: -h : Displays this help message. -d FILE : use the named DBM database file -p FILE : the the named Pickle database file -l port : proxy listens on this port number (default 110) -u port : User interface listens on this port number (default 8880; Browse http://localhost:8880/) -b : Launch a web browser showing the user interface. -o section:option:value : set [section, option] in the options database to value All command line arguments and switches take their default values from the [pop3proxy] and [html_ui] sections of bayescustomize.ini.For safety, and to help debugging, the whole POP3 conversation iswritten out to _pop3proxy.log for each run, ifoptions["globals", "verbose"] is True.To make rebuilding the database easier, uploaded messages are appendedto _pop3proxyham.mbox and _pop3proxyspam.mbox."""# This module is part of the spambayes project, which is Copyright 2002# The Python Software Foundation and is covered by the Python Software# Foundation license.__author__ = "Richie Hindle <richie@entrian.com>"__credits__ = "Tim Peters, Neale Pickett, Tim Stone, all the Spambayes folk."try: True, Falseexcept NameError: # Maintain compatibility with Python 2.2 True, False = 1, 0try: reversedexcept NameError: # Maintain compatibility with Python 2.2 and 2.3 def reversed(seq): seq = list(seq[:]) seq.reverse() return iter(seq)todo = """Web training interface:User interface improvements: o Once the pieces are on separate pages, make the paste box bigger. o Deployment: Windows executable? atlaxwin and ctypes? Or just webbrowser? o "Reload database" button.New features: o Online manual. o Links to project homepage, mailing list, etc. o List of words with stats (it would have to be paged!) a la SpamSieve.Code quality: o Cope with the email client timing out and closing the connection.Info: o Slightly-wordy index page; intro paragraph for each page. o In both stats and training results, report nham and nspam. o "Links" section (on homepage?) to project homepage, mailing list, etc.Gimmicks: o Classify a web page given a URL. o Graphs. Of something. Who cares what? o NNTP proxy."""import os, sys, re, errno, getopt, time, traceback, socket, cStringIO, emailfrom thread import start_new_threadfrom email.Header import Headerimport spambayes.messagefrom spambayes import i18nfrom spambayes import Statsfrom spambayes import Dibblerfrom spambayes import storagefrom spambayes.FileCorpus import FileCorpus, ExpiryFileCorpusfrom spambayes.FileCorpus import FileMessageFactory, GzipFileMessageFactoryfrom spambayes.Options import options, get_pathname_option, _from spambayes.UserInterface import UserInterfaceServerfrom spambayes.ProxyUI import ProxyUserInterfacefrom spambayes.Version import get_current_version# Increase the stack size on MacOS X. Stolen from Lib/test/regrtest.pyif sys.platform == 'darwin': try: import resource except ImportError: pass else: soft, hard = resource.getrlimit(resource.RLIMIT_STACK) newsoft = min(hard, max(soft, 1024*2048)) resource.setrlimit(resource.RLIMIT_STACK, (newsoft, hard))# exception may be raised if we are already running and check such things.class AlreadyRunningException(Exception): pass# number to add to STAT length for each msg to fudge for spambayes headersHEADER_SIZE_FUDGE_FACTOR = 512class ServerLineReader(Dibbler.BrighterAsyncChat): """An async socket that reads lines from a remote server and simply calls a callback with the data. The BayesProxy object can't connect to the real POP3 server and talk to it synchronously, because that would block the process.""" def __init__(self, serverName, serverPort, lineCallback, ssl=False, map=None): Dibbler.BrighterAsyncChat.__init__(self, map=map) self.lineCallback = lineCallback self.handled_exception = False self.request = '' self.set_terminator('\r\n') self.create_socket(socket.AF_INET, socket.SOCK_STREAM) # create_socket creates a non-blocking socket. This is not great, # because then socket.connect() will return errno 10035, because # connect takes time. We then don't know if the connect call # succeeded or not. With Python 2.4, this means that we will move # into asyncore.loop(), and if the connect does fail, have a # loop something like 'while True: log(error)', which fills up # stdout very fast. Non-blocking is also a problem for ssl sockets. self.socket.setblocking(1) try: self.connect((serverName, serverPort)) except socket.error, e: error = "Can't connect to %s:%d: %s" % (serverName, serverPort, e) # Some people have their system setup to check mail very # frequently, but without being clever enough to check whether # the network is available. If we continually print the # "can't connect" error, we use up lots of CPU and disk space. # To avoid this, if not verbose only print each distinct error # once per hour. # See also: [ 1113863 ] sb_tray eats all cpu time now = time.time() then = time.time() - 3600 if error not in state.reported_errors or \ options["globals", "verbose"] or \ state.reported_errors[error] < then: print >>sys.stderr, error # Record this error in the list of ones we have seen this # session. state.reported_errors[error] = now self.lineCallback('-ERR %s\r\n' % error) self.lineCallback('') # "The socket's been closed." self.close() else: if ssl: try: self.ssl_socket = socket.ssl(self.socket) except socket.sslerror, why: if why[0] == 1: # error:140770FC:SSL routines:SSL23_GET_SERVER_HELLO:unknown protocol' # Probably not SSL after all. print >>sys.stderr, "Can't use SSL" else: raise else: self.send = self.send_ssl self.recv = self.recv_ssl self.socket.setblocking(0) def send_ssl(self, data): return self.ssl_socket.write(data) def handle_expt(self): # Python 2.4's system of continuously pumping error messages # is stupid. Print an error once, and then ignore. if not self.handled_exception: print >> sys.stderr, "Unhandled exception in ServerLineReader" self.handled_exception = True def recv_ssl(self, buffer_size): try: data = self.ssl_socket.read(buffer_size) if not data: # a closed connection is indicated by signaling # a read condition, and having recv() return 0. self.handle_close() return '' else: return data except socket.sslerror, why: if why[0] == 6: # 'TLS/SSL connection has been closed' self.handle_close() return '' elif why[0] == 2: # 'The operation did not complete (read)' return '' else: raise def collect_incoming_data(self, data): self.request = self.request + data def found_terminator(self): self.lineCallback(self.request + '\r\n') self.request = '' def handle_close(self): self.lineCallback('') self.close() try: del self.ssl_socket except AttributeError: passclass POP3ProxyBase(Dibbler.BrighterAsyncChat): """An async dispatcher that understands POP3 and proxies to a POP3 server, calling `self.onTransaction(request, response)` for each transaction. Responses are not un-byte-stuffed before reaching self.onTransaction() (they probably should be for a totally generic POP3ProxyBase class, but BayesProxy doesn't need it and it would mean re-stuffing them afterwards). self.onTransaction() should return the response to pass back to the email client - the response can be the verbatim response or a processed version of it. The special command 'KILL' kills it (passing a 'QUIT' command to the server). """ def __init__(self, clientSocket, serverName, serverPort, ssl=False, map=Dibbler._defaultContext._map): Dibbler.BrighterAsyncChat.__init__(self, clientSocket) self.request = '' self.response = '' self.set_terminator('\r\n') self.command = '' # The POP3 command being processed... self.args = [] # ...and its arguments self.isClosing = False # Has the server closed the socket? self.seenAllHeaders = False # For the current RETR or TOP self.startTime = 0 # (ditto) if not self.onIncomingConnection(clientSocket): # We must refuse this connection, so pass an error back # to the mail client. self.push("-ERR Connection not allowed\r\n") self.close_when_done() return self.serverSocket = ServerLineReader(serverName, serverPort, self.onServerLine, ssl, map) def onIncomingConnection(self, clientSocket): """Checks the security settings.""" # Stolen from UserInterface.py remoteIP = clientSocket.getpeername()[0] trustedIPs = options["pop3proxy", "allow_remote_connections"] if trustedIPs == "*" or remoteIP == clientSocket.getsockname()[0]: return True trustedIPs = trustedIPs.replace('.', '\.').replace('*', '([01]?\d\d?|2[0-4]\d|25[0-5])') for trusted in trustedIPs.split(','): if re.search("^" + trusted + "$", remoteIP): return True return False def onTransaction(self, command, args, response): """Overide this. Takes the raw request and the response, and returns the (possibly processed) response to pass back to the email client. """ raise NotImplementedError def onServerLine(self, line): """A line of response has been received from the POP3 server.""" isFirstLine = not self.response self.response = self.response + line # Is this the line that terminates a set of headers? self.seenAllHeaders = self.seenAllHeaders or line in ['\r\n', '\n'] # Has the server closed its end of the socket? if not line: self.isClosing = True # If we're not processing a command, just echo the response. if not self.command: self.push(self.response) self.response = '' # Time out after some seconds (30 by default) for message-retrieval # commands if all the headers are down. The rest of the message # will proxy straight through. # See also [ 870524 ] Make the message-proxy timeout configurable if self.command in ['TOP', 'RETR'] and \ self.seenAllHeaders and time.time() > \ self.startTime + options["pop3proxy", "retrieval_timeout"]: self.onResponse() self.response = '' # If that's a complete response, handle it. elif not self.isMultiline() or line == '.\r\n' or \ (isFirstLine and line.startswith('-ERR')): self.onResponse() self.response = '' def isMultiline(self): """Returns True if the request should get a multiline response (assuming the response is positive). """ if self.command in ['USER', 'PASS', 'APOP', 'QUIT', 'STAT', 'DELE', 'NOOP', 'RSET', 'KILL']: return False elif self.command in ['RETR', 'TOP', 'CAPA']: return True elif self.command in ['LIST', 'UIDL']: return len(self.args) == 0 else: # Assume that an unknown command will get a single-line # response. This should work for errors and for POP-AUTH, # and is harmless even for multiline responses - the first # line will be passed to onTransaction and ignored, then the # rest will be proxied straight through. return False def collect_incoming_data(self, data): """Asynchat override.""" self.request = self.request + data def found_terminator(self): """Asynchat override.""" verb = self.request.strip().upper() if verb == 'KILL': self.socket.shutdown(2) self.close() raise SystemExit elif verb == 'CRASH': # For testing x = 0
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -