From e5157b50b3dffea3a8a0c8e8d649db368282db6b Mon Sep 17 00:00:00 2001 From: Chuck Date: Wed, 3 Jul 2013 21:01:11 +0700 Subject: [PATCH] POP3 server semi-functional --- src/bitmessagemain.py | 12 ++ src/class_asyncoreThread.py | 254 +++++++++++++++++++++++++++++++++++- src/class_sqlThread.py | 1 + src/helper_startup.py | 1 + 4 files changed, 264 insertions(+), 4 deletions(-) diff --git a/src/bitmessagemain.py b/src/bitmessagemain.py index c4c2b6db..e8eb749d 100644 --- a/src/bitmessagemain.py +++ b/src/bitmessagemain.py @@ -24,6 +24,7 @@ from class_singleWorker import * from class_outgoingSynSender import * from class_singleListener import * from class_addressGenerator import * +from class_asyncoreThread import * # Helper Functions import helper_startup @@ -731,6 +732,17 @@ if __name__ == "__main__": singleCleanerThread.daemon = True # close the main program even if there are threads left singleCleanerThread.start() + # Start the SMTP server + smtpServer = bitmessageSMTPServer() + + # And the POP3 server... + pop3Server = bitmessagePOP3Server(debug=True) + + # And finally launch asyncore + asyncoreThread = asyncoreThread() + asyncoreThread.daemon = True + asyncoreThread.start() + shared.reloadMyAddressHashes() shared.reloadBroadcastSendersForWhichImWatching() diff --git a/src/class_asyncoreThread.py b/src/class_asyncoreThread.py index 7bd8c34d..210f9f6e 100644 --- a/src/class_asyncoreThread.py +++ b/src/class_asyncoreThread.py @@ -1,12 +1,16 @@ +from collections import deque from pyelliptic.openssl import OpenSSL import asyncore import shared import smtpd +import socket +import sys import threading import time import traceback from addresses import * +import helper_inbox import helper_sent class bitmessageSMTPServer(smtpd.SMTPServer): @@ -41,13 +45,13 @@ class bitmessageSMTPServer(smtpd.SMTPServer): status, addressVersionNumber, streamNumber, fromRipe = decodeAddress(fromAddress) if status != 'success': shared.printLock.acquire() - print 'Error: Could not decode address: ' + toAddress + ' : ' + status + print 'Error: Could not decode address: ' + fromAddress + ' : ' + status if status == 'checksumfailed': - print 'Error: Checksum failed for address: ' + toAddress + print 'Error: Checksum failed for address: ' + fromAddress if status == 'invalidcharacters': - print 'Error: Invalid characters in address: ' + toAddress + print 'Error: Invalid characters in address: ' + fromAddress if status == 'versiontoohigh': - print 'Error: Address version number too high (or zero) in address: ' + toAddress + print 'Error: Address version number too high (or zero) in address: ' + fromAddress shared.printLock.release() raise Exception("Invalid Bitmessage address: {}".format(fromAddress)) #fromAddress = addBMIfNotPresent(fromAddress) # I know there's a BM-, because it's required when using SMTP @@ -125,6 +129,248 @@ class bitmessageSMTPServer(smtpd.SMTPServer): # TODO - what should we do with ackdata.encode('hex') ? +class bitmessagePOP3Connection(asyncore.dispatcher): + END = b"\r\n" + + def __init__(self, sock, peer_address, debug=False): + asyncore.dispatcher.__init__(self, sock) + self.peer_address = peer_address + self.data_buffer = [] + self.commands = deque() + self.debug = debug + + self.dispatch = dict( + USER=self.handleUser, + PASS=self.handlePass, + STAT=self.handleStat, + LIST=self.handleList, + #TOP=self.handleTop, + RETR=self.handleRetr, + DELE=self.handleDele, + NOOP=self.handleNoop, + QUIT=self.handleQuit, + ) + + #self.bitmessage = Bitmessage() + self.messages = None #TODO self.bitmessage.getInboxMessagesByAddress(BITMAIL_ADDRESS) + self.storage_size = 0 + self.address = None + + #for msg in self.messages: + # msg['message'] = base64.b64decode(msg['message'].encode('ascii')) + # msg['subject'] = base64.b64decode(msg['subject'].encode('ascii')) + # msg['size'] = len(msg['message']) + # self.storage_size += msg['size'] + + #print(self.messages) + + self.sendline("+OK Bitmessage POP3 server ready") + + def populateMessageIndex(self): + if self.address is None: + raise Exception("Invalid address: {}".format(self.address)) + + if self.messages is not None: + return + + v = (self.address,) + shared.sqlLock.acquire() + # TODO LENGTH(message) needs to be the byte-length, not the character-length. + shared.sqlSubmitQueue.put('''SELECT msgid, fromaddress, subject, LENGTH(message) FROM inbox WHERE folder='inbox' AND toAddress=?''') + shared.sqlSubmitQueue.put(v) + queryreturn = shared.sqlReturnQueue.get() + shared.sqlLock.release() + + self.storage_size = 0 + self.messages = [] + for row in queryreturn: + msgid, fromAddress, subject, size = row + subject = shared.fixPotentiallyInvalidUTF8Data(subject) + if subject.startswith("': + subject = "" # Reserved, flags. + flags = subject[-21:-1] + # TODO - checksum? + + self.messages.append({ + 'msgid': msgid, + 'fromAddress': fromAddress, + 'subject': subject, + 'size': size, + }) + + self.storage_size += size + + + def getMessageContent(self, msgid): + if self.address is None: + raise Exception("Invalid address: {}".format(self.address)) + + v = (msgid,) + shared.sqlLock.acquire() + shared.sqlSubmitQueue.put('''SELECT fromaddress, received, message, encodingtype FROM inbox WHERE msgid=?''') + shared.sqlSubmitQueue.put(v) + queryreturn = shared.sqlReturnQueue.get() + shared.sqlLock.release() + + for row in queryreturn: + fromAddress, received, message, encodingtype = row + message = shared.fixPotentiallyInvalidUTF8Data(message) + return { + 'fromAddress': fromAddress, + 'received': received, + 'message': message, + 'encodingtype': encodingtype + } + + def trashMessage(self, msgid): + # TODO - how to determine if the update succeeded? + if not self.debug: + helper_inbox.trash(msgid) + return True + + def sendline(self, data, END=END): + if self.debug: + shared.printLock.acquire() + sys.stdout.write("sending ") + sys.stdout.write(data) + sys.stdout.write("\n") + shared.printLock.release() + data = data + END + while len(data) > 4096: + self.send(data[:4096]) + data = data[4096:] + if len(data): + self.send(data) + + def handle_read(self): + chunk = self.recv(4096) + + while bitmessagePOP3Connection.END in chunk: + # Join all the data up to the END and throw it in commands + command = b''.join(self.data_buffer) + chunk[:chunk.index(bitmessagePOP3Connection.END)] + chunk = chunk[chunk.index(bitmessagePOP3Connection.END)+2:] + self.data_buffer = [] + self.commands.append(command) + + if len(chunk): + self.data_buffer.append(chunk) + + if self.debug: + shared.printLock.acquire() + print('data_buffer', self.data_buffer) + print('commands', self.commands) + print('-') + shared.printLock.release() + + while len(self.commands): + line = self.commands.popleft() + + if b' ' in line: + cmd, data = line.split(b' ', 1) + else: + cmd, data = line, b'' + + try: + cmd = self.dispatch[cmd.decode('ascii').upper()] + except KeyError: + self.sendline('-ERR unknown command') + continue + + for response in cmd(data): + self.sendline(response) + + if cmd is self.handleQuit: + self.close() + break + + def handleUser(self, data): + self.address = data + + status, addressVersionNumber, streamNumber, ripe = decodeAddress(self.address) + if status != 'success': + shared.printLock.acquire() + print 'Error: Could not decode address: ' + self.address + ' : ' + status + if status == 'checksumfailed': + print 'Error: Checksum failed for address: ' + self.address + if status == 'invalidcharacters': + print 'Error: Invalid characters in address: ' + self.address + if status == 'versiontoohigh': + print 'Error: Address version number too high (or zero) in address: ' + self.address + shared.printLock.release() + raise Exception("Invalid Bitmessage address: {}".format(self.address)) + + return ["+OK user accepted"] + + def handlePass(self, data): + # TODO + return ["+OK pass accepted"] + + def handleStat(self, data): + self.populateMessageIndex() + return ["+OK {} {}".format(len(self.messages), self.storage_size)] + + def handleList(self, data): + self.populateMessageIndex() + yield "+OK {} messages ({} octets)".format(len(self.messages), self.storage_size) + for i, msg in enumerate(self.messages): + yield "{} {}".format(i + 1, msg['size']) + yield "." + + #def handleTop(self, data): + # cmd, num, lines = data.split() + # assert num == "1", "unknown message number: {}".format(num) + # lines = int(lines) + # text = msg.top + "\r\n\r\n" + "\r\n".join(msg.bot[:lines]) + # return "+OK top of message follows\r\n{}\r\n.".format(text) + + def handleRetr(self, data): + index = int(data.decode('ascii')) - 1 + assert index >= 0 + self.populateMessageIndex() + msg = self.messages[index] + content = self.getMessageContent(msg['msgid']) + if self.debug: + shared.printLock.acquire() + sys.stdout.write(str(msg) + ": " + str(content)) + shared.printLock.release() + yield "+OK {} octets".format(msg['size']) + yield content['message'] + yield '.' + + def handleDele(self, data): + index = int(data.decode('ascii')) - 1 + assert index >= 0 + self.populateMessageIndex() + msg = self.messages[index] + if self.trashMessage(msg['msgid']): + return ["+OK deleted"] + else: + return ["-ERR internal error"] + + def handleNoop(self, data): + return ["+OK"] + + def handleQuit(self, data): + return ["+OK Bitmessage POP3 server signing off"] + +class bitmessagePOP3Server(asyncore.dispatcher): + def __init__(self, debug=False): + asyncore.dispatcher.__init__(self) + self.debug = debug + + pop3port = shared.config.getint('bitmessagesettings', 'pop3port') + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.bind(('127.0.0.1', pop3port)) + self.listen(10) + + shared.printLock.acquire() + print "POP3 server started" + shared.printLock.release() + + def handle_accept(self): + sock, peer_address = self.accept() + _ = bitmessagePOP3Connection(sock, peer_address, debug=self.debug) + class asyncoreThread(threading.Thread): def __init__(self): diff --git a/src/class_sqlThread.py b/src/class_sqlThread.py index ebe6a7ff..7cfa5da5 100644 --- a/src/class_sqlThread.py +++ b/src/class_sqlThread.py @@ -268,6 +268,7 @@ class sqlThread(threading.Thread): item) + '" Here are the parameters; you might want to censor this data with asterisks (***) as it can contain private information: ' + str(repr(parameters)) + '\nHere is the actual error message thrown by the sqlThread: ' + str(err) + '\n') sys.stderr.write('This program shall now abruptly exit!\n') shared.printLock.release() + raw_input('Press enter to exit...') os._exit(0) shared.sqlReturnQueue.put(self.cur.fetchall()) diff --git a/src/helper_startup.py b/src/helper_startup.py index e6590b0e..58df926b 100644 --- a/src/helper_startup.py +++ b/src/helper_startup.py @@ -28,6 +28,7 @@ def loadConfig(): shared.config.add_section('bitmessagesettings') shared.config.set('bitmessagesettings', 'settingsversion', '6') shared.config.set('bitmessagesettings', 'port', '8444') + shared.config.set('bitmessagesettings', 'smtpport', '10025') shared.config.set( 'bitmessagesettings', 'timeformat', '%%a, %%d %%b %%Y %%I:%%M %%p') shared.config.set('bitmessagesettings', 'blackwhitelist', 'black')