📄 sb_server.py
字号:
y = 1/x self.serverSocket.push(self.request + '\r\n') if self.request.strip() == '': # Someone just hit the Enter key. self.command = '' self.args = [] else: # A proper command. splitCommand = self.request.strip().split() self.command = splitCommand[0].upper() self.args = splitCommand[1:] self.startTime = time.time() self.request = '' def onResponse(self): # There are some features, tested by clients using CAPA, # that we don't support. We strip them from the CAPA # response here, so that the client won't use them. for unsupported in ['PIPELINING', 'STLS', ]: unsupportedLine = r'(?im)^%s[^\n]*\n' % (unsupported,) self.response = re.sub(unsupportedLine, '', self.response) # Pass the request and the raw response to the subclass and # send back the cooked response. if self.response: cooked = self.onTransaction(self.command, self.args, self.response) self.push(cooked) # If onServerLine() decided that the server has closed its # socket, close this one when the response has been sent. if self.isClosing: self.close_when_done() # Reset. self.command = '' self.args = [] self.isClosing = False self.seenAllHeaders = Falseclass BayesProxyListener(Dibbler.Listener): """Listens for incoming email client connections and spins off BayesProxy objects to serve them. """ def __init__(self, serverName, serverPort, proxyPort, ssl=False): proxyArgs = (serverName, serverPort, ssl) Dibbler.Listener.__init__(self, proxyPort, BayesProxy, proxyArgs) print 'Listener on port %s is proxying %s:%d' % \ (_addressPortStr(proxyPort), serverName, serverPort)class BayesProxy(POP3ProxyBase): """Proxies between an email client and a POP3 server, inserting judgement headers. It acts on the following POP3 commands: o STAT: o Adds the size of all the judgement headers to the maildrop size. o LIST: o With no message number: adds the size of an judgement header to the message size for each message in the scan listing. o With a message number: adds the size of an judgement header to the message size. o RETR: o Adds the judgement header based on the raw headers and body of the message. o TOP: o Adds the judgement header based on the raw headers and as much of the body as the TOP command retrieves. This can mean that the header might have a different value for different calls to TOP, or for calls to TOP vs. calls to RETR. I'm assuming that the email client will either not make multiple calls, or will cope with the headers being different. o USER: o Does no processing based on the USER command itself, but expires any old messages in the three caches. """ def __init__(self, clientSocket, serverName, serverPort, ssl=False): POP3ProxyBase.__init__(self, clientSocket, serverName, serverPort, ssl) self.handlers = {'STAT': self.onStat, 'LIST': self.onList, 'RETR': self.onRetr, 'TOP': self.onTop, 'USER': self.onUser} state.totalSessions += 1 state.activeSessions += 1 self.isClosed = False def send(self, data): """Logs the data to the log file.""" if options["globals", "verbose"]: state.logFile.write(data) state.logFile.flush() try: return POP3ProxyBase.send(self, data) except socket.error: # The email client has closed the connection - 40tude Dialog # does this immediately after issuing a QUIT command, # without waiting for the response. self.close() def recv(self, size): """Logs the data to the log file.""" data = POP3ProxyBase.recv(self, size) if options["globals", "verbose"]: state.logFile.write(data) state.logFile.flush() return data def close(self): # This can be called multiple times by async. if not self.isClosed: self.isClosed = True state.activeSessions -= 1 POP3ProxyBase.close(self) def onTransaction(self, command, args, response): """Takes the raw request and response, and returns the (possibly processed) response to pass back to the email client. """ handler = self.handlers.get(command, self.onUnknown) return handler(command, args, response) def onStat(self, command, args, response): """Adds the size of all the judgement headers to the maildrop size.""" match = re.search(r'^\+OK\s+(\d+)\s+(\d+)(.*)\r\n', response) if match: count = int(match.group(1)) size = int(match.group(2)) + HEADER_SIZE_FUDGE_FACTOR * count return '+OK %d %d%s\r\n' % (count, size, match.group(3)) else: return response def onList(self, command, args, response): """Adds the size of an judgement header to the message size(s).""" if response.count('\r\n') > 1: # Multiline: all lines but the first contain a message size. lines = response.split('\r\n') outputLines = [lines[0]] for line in lines[1:]: match = re.search(r'^(\d+)\s+(\d+)', line) if match: number = int(match.group(1)) size = int(match.group(2)) + HEADER_SIZE_FUDGE_FACTOR line = "%d %d" % (number, size) outputLines.append(line) return '\r\n'.join(outputLines) else: # Single line. match = re.search(r'^\+OK\s+(\d+)\s+(\d+)(.*)\r\n', response) if match: messageNumber = match.group(1) size = int(match.group(2)) + HEADER_SIZE_FUDGE_FACTOR trailer = match.group(3) return "+OK %s %s%s\r\n" % (messageNumber, size, trailer) else: return response def onRetr(self, command, args, response): """Adds the judgement header based on the raw headers and body of the message.""" # Previously, we used '\n\r?\n' to detect the end of the headers in # case of broken emails that don't use the proper line separators, # and if we couldn't find it, then we assumed that the response was # and error response and passed it unfiltered. However, if the # message doesn't contain the separator (malformed mail), then this # would mean the message was passed straight through the proxy. # Since all the content is then in the headers, this probably # doesn't do a spammer much good, but, just in case, we now just # check for "+OK" and assume no error response will be given if # that is (which seems reasonable). # Remove the trailing .\r\n before passing to the email parser. # Thanks to Scott Schlesier for this fix. terminatingDotPresent = (response[-4:] == '\n.\r\n') if terminatingDotPresent: response = response[:-3] # Break off the first line, which will be '+OK'. statusLine, messageText = response.split('\n', 1) statusData = statusLine.split() ok = statusData[0] if ok.strip().upper() != "+OK": # Must be an error response. Return unproxied. return response try: msg = email.message_from_string(messageText, _class=spambayes.message.SBHeaderMessage) msg.setId(state.getNewMessageName()) # Now find the spam disposition and add the header. (prob, clues) = state.bayes.spamprob(msg.tokenize(),\ evidence=True) msg.addSBHeaders(prob, clues) # Check for "RETR" or "TOP N 99999999" - fetchmail without # the 'fetchall' option uses the latter to retrieve messages. if (command == 'RETR' or (command == 'TOP' and len(args) == 2 and args[1] == '99999999')): cls = msg.GetClassification() state.RecordClassification(cls, prob) # Suppress caching of "Precedence: bulk" or # "Precedence: list" ham if the options say so. isSuppressedBulkHam = \ (cls == options["Headers", "header_ham_string"] and options["Storage", "no_cache_bulk_ham"] and msg.get('precedence') in ['bulk', 'list']) # Suppress large messages if the options say so. size_limit = options["Storage", "no_cache_large_messages"] isTooBig = size_limit > 0 and \ len(messageText) > size_limit # Cache the message. Don't pollute the cache with test # messages or suppressed bulk ham. if (not state.isTest and options["Storage", "cache_messages"] and not isSuppressedBulkHam and not isTooBig): # Write the message into the Unknown cache. makeMessage = state.unknownCorpus.makeMessage message = makeMessage(msg.getId(), msg.as_string()) state.unknownCorpus.addMessage(message) # We'll return the message with the headers added. We take # all the headers from the SBHeaderMessage, but take the body # directly from the POP3 conversation, because the # SBHeaderMessage might have "fixed" a partial message by # appending a closing boundary separator. Remember we can # be dealing with partial message here because of the timeout # code in onServerLine. headers = [] for name, value in msg.items(): header = "%s: %s" % (name, value) headers.append(re.sub(r'\r?\n', '\r\n', header)) try: body = re.split(r'\n\r?\n', messageText, 1)[1] except IndexError: # No separator, so no body. Bad message, but proxy it # through anyway (adding the missing separator). messageText = "\r\n".join(headers) + "\r\n\r\n" else: messageText = "\r\n".join(headers) + "\r\n\r\n" + body except: # Something nasty happened while parsing or classifying - # report the exception in a hand-appended header and recover. # This is one case where an unqualified 'except' is OK, 'cos # anything's better than destroying people's email... messageText, details = spambayes.message.\ insert_exception_header(messageText) # Print the exception and a traceback. print >>sys.stderr, details # Restore the +OK and the POP3 .\r\n terminator if there was one. retval = ok + "\n" + messageText if terminatingDotPresent: retval += '.\r\n' return retval def onTop(self, command, args, response): """Adds the judgement header based on the raw headers and as much of the body as the TOP command retrieves.""" # Easy (but see the caveat in BayesProxy.__doc__). return self.onRetr(command, args, response) def onUser(self, command, args, response): """Spins off three separate threads that expires any old messages in the three caches, but does not do any processing of the USER command itself.""" start_new_thread(state.spamCorpus.removeExpiredMessages, ()) start_new_thread(state.hamCorpus.removeExpiredMessages, ()) start_new_thread(state.unknownCorpus.removeExpiredMessages, ()) return response def onUnknown(self, command, args, response): """Default handler; returns the server's response verbatim.""" return response# Implementations of a mutex or other resource which can prevent# multiple servers starting at once. Platform specific as no reasonable# cross-platform solution exists (however, an old trick is to use a# directory for a mutex, as a "create/test" atomic API generally exists).# Will return a handle to be later closed, or may throw AlreadyRunningExceptiondef open_platform_mutex(mutex_name="SpamBayesServer"): if sys.platform.startswith("win"): try: import win32event, win32api, winerror, win32con import pywintypes, ntsecuritycon # ideally, the mutex name could include either the username, # or the munged path to the INI file - this would mean we # would allow multiple starts so long as they weren't for # the same user. However, as of now, the service version # is likely to start as a different user, so a single mutex # is best for now. # XXX - even if we do get clever with another mutex name, we # should consider still creating a non-exclusive # "SpamBayesServer" mutex, if for no better reason than so # an installer can check if we are running try: hmutex = win32event.CreateMutex(None, True, mutex_name) except win32event.error, details: # If another user has the mutex open, we get an "access denied" # error - this is still telling us what we need to know. if details[0] != winerror.ERROR_ACCESS_DENIED: raise raise AlreadyRunningException # mutex opened - now check if we actually created it. if win32api.GetLastError()==winerror.ERROR_ALREADY_EXISTS: win32api.CloseHandle(hmutex) raise AlreadyRunningException return hmutex except ImportError: # no win32all - no worries, just start pass return Nonedef close_platform_mutex(mutex): if sys.platform.startswith("win"): if mutex is not None: mutex.Close()# This keeps the global state of the module - the command-line options,# statistics like how many mails have been classified, the handle of the# log file, the Classifier and FileCorpus objects, and so on.class State: def __init__(self): """Initialises the State object that holds the state of the app. The default settings are read from Options.py and bayescustomize.ini and are then overridden by the command-line processing code in the __main__ code below.""" self.logFile = None self.bayes = None self.platform_mutex = None self.prepared = False self.can_stop = True self.init() # Load up the other settings from Option.py / bayescustomize.ini self.uiPort = options["html_ui", "port"] self.launchUI = options["html_ui", "launch_browser"] self.gzipCache = options["Storage", "cache_use_gzip"] self.cacheExpiryDays = options["Storage", "cache_expiry_days"] self.runTestServer = False self.isTest = False def init(self): assert not self.prepared, "init after prepare, but before close" # Load the environment for translation. self.lang_manager = i18n.LanguageManager() # Set the system user default language. self.lang_manager.set_language(\ self.lang_manager.locale_default_lang()) # Set interface to use the user language in the configuration file. for language in reversed(options["globals", "language"]): # We leave the default in there as the last option, to fall # back on if necessary. self.lang_manager.add_language(language)
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -