#!/usr/bin/python2 # mkc: module doc string must go first in file """ NAME popdaemon -- rfc1725 implementation. Expects to find all messages in a directory associted with each user, one message per file. This can be extended arbitrarily. SYNOPSIS After editing the user specific portions, 'nohup popdaemon &' can be added to rc.local. README This program is a full implementation of rfc 1725, with an adjustment made to unsplit header lines so that Netscape Communicator will not drop the connection when it gets a message-id that is too long. All configuration is done within the code, which means that it is open to being tied to the database(s) of your choice. COPYRIGHT Copyright (c) 1999 David Nicol . License is granted to modify and install as needed, with the expectation that this copyright notice will remain. This sample Python translation by Mike Coleman """ version = "Kansas City POP Daemon Version 0.06"; # #=pod # #David's Pop Daemon # 2 november 1999 # 13 December 1999 # 5 December 2000 -- changed multiline end from .CRLF to CRLF.CRLF MLEM="\r\n.\r\n" #Multi Line End Marker # thanks to Mark Lipscombe's frustrating experiments with Microsoft # Outlook which is more demanding in such things than Mozilla. # #implementation goals: # # be in standard perl 5 # implement rfc1725 # contain usernames, passwords and directories in the source code # run standalone (from inetd can be done by crippling this easily) # # read mail out of a directory where is has been placed one # message per file (such as a MailDir) # # Delete mail directly from the directory # # #Discussion: # # Qmail isn't the only MTA that can write to a directory; # # There should be another perl program called "PreSpool" # which can be used in a sendmail aliases file like so: # # djb: "|/usr/local/kcpm/prespool.pl /usr/users/home/bernstein/MailDir/new" # # and that will cause sendmail to deliver all incoming mails into that # directory, with unique file names even. # # #=cut # import fcntl import os import re import signal import socket import sys import time import SocketServer #####USER CONFIGURATION PORTION: # changing this to read from an external file # would not be difficult, but you'd still have to # edit something -- the external file -- what's the win? # Add a Passwd and a Directory entry for each user. Passwd = {} Directory = {} Passwd['djb'] = 'Qtips' Directory['djb'] = '/usr/users/home/bernstein/MailDir/new' Passwd['lw'] = 'chiXlyd' Directory['lw'] = '/usr/users/home/wall/MailDir/new' Passwd['mkc'] = 'mkc' Directory['mkc'] = '/tmp/new' ######################################################### def SockData(handler): address, port = handler.client_address hostname, aliases, ipaddrlist = socket.gethostbyaddr(address) return "%s %s" % (hostname, address) def response(message, prefix): global client message = re.sub(r'\s+', ' ', message).rstrip() s = "%s %s\r\n" % (prefix, message) client.wfile.write(s) sys.stdout.write("S: %s" % s) signal.alarm(timeout) def OK(message): response(message, '+OK') def ERR(message): response(message, '-ERR') def AUTHORIZATION(client_): global client client = client_ Name = '' strikes = 0 OK("TipJar POP3 Daemon %s %s %s" % (version, SockData(client), time.asctime(time.localtime(time.time())))) while 1: Data = client.rfile.readline() print "C:%s" % Data if re.search('^quit', Data, re.IGNORECASE): OK("whatever") sys.exit("client quit"); Name_match = re.search('^user (\w+)', Data, re.IGNORECASE) if not Name_match: ERR("The itsy bitsy spider walked up the water spout") strikes += 1 if strikes > 5: sys.exit("struck out") continue Name = Name_match.group(1) OK("User name (%s) ok. Password, please." % Name) Data = client.rfile.readline() print "C:%s" % Data if re.search('^quit', Data, re.IGNORECASE): OK("whatever") sys.exit("client quit"); Pass_match = re.search('^pass (.*)', Data, re.IGNORECASE) if not Pass_match or Passwd[Name] != Pass_match.group(1).rstrip(): ERR("Down came the rain and washed the spider out") strikes += 1 if strikes > 5: sys.exit("struck out") continue MailDir = Directory[Name] # mkc: we're following symlinks here (we could choose not to) try: os.chdir(MailDir) global Messages Messages = filter(lambda s: s.find('PopDaemonLock') < 0, filter(os.path.isfile, os.listdir('.'))) except OSError, e: ERR("%s does not appear to be a readable directory (%s)" % (MailDir, e)) continue # Lock the maildrop LOCK = open('.PopDaemonLock', 'w+') try: fcntl.flock(LOCK.fileno(), fcntl.LOCK_EX|fcntl.LOCK_NB) except OSError, e: ERR("Maildrop contains %s but it is already locked:" " perhaps we are still deleting? Please try again" " in a few minutes" % len(Messages)) continue OK("%s has %s messages" % (Name, len(Messages))) break def TRANSACTION(): # mkc: global variables (yuck, but do it to show we can) global deletia, Data, client deletia = {} while 1: Data = client.rfile.readline() if not Data: sys.exit("Client closed connection") print "C:%s" % Data if re.search('^quit', Data, re.IGNORECASE): OK("Thanks for flying sneaker express") return if re.search('^STAT', Data, re.IGNORECASE): STAT(); continue; if re.search('^LIST', Data, re.IGNORECASE): LIST(); continue; if re.search('^RETR', Data, re.IGNORECASE): RETR(); continue; if re.search('^DELE', Data, re.IGNORECASE): DELE(); continue; if re.search('^NOOP', Data, re.IGNORECASE): NOOP(); continue; if re.search('^RSET', Data, re.IGNORECASE): RSET(); continue; # optional commands (rfc 1725) if re.search('^TOP', Data, re.IGNORECASE): TOP(); continue; if re.search('^UIDL', Data, re.IGNORECASE): UIDL(); continue; ERR("I'm from Missouri") continue def STAT(): signal.alarm(0) #who knows how long reading the dir will take? nn = len(Messages) mm = reduce(lambda x,y: x+y, map(os.path.getsize, Messages), 0) OK("%s %s" % (nn, mm)) def List(index): M = Messages[index-1] if deletia.has_key(M): return message = "%s %s.\r\n" % (M, os.path.getsize(M)) client.wfile.write(message) print "S:", message signal.alarm(timeout) def LIST(): global Data, Messages, deletia m = re.search(r'(\d+)', Data) if m: d = int(m.group(1)) # we "know" m is numeric if not 0 < d <= len(Messages): ERR("no message number %s" % d) return M = Messages[d-1] if deletia.has_key(M): ERR("message %s deleted" % d) return OK("Listing %s" % d) List(d) return OK("Listing") for d in xrange(1, len(Messages)+1): List(d) client.wfile.write(".\r\n") def RETR(): global Data, Messages, client, deletia, MLEM m = re.search(r'(\d+)', Data) if not m: ERR("message number required") return d = int(m.group(1)) # we "know" m is numeric if not 0 < d <= len(Messages): ERR("no message %s" % d) return M = Messages[d-1] if deletia.has_key(M): ERR("message %s deleted already" % d) return OK("Here comes %s bytes" % os.path.getsize(M)) signal.alarm(0) MESSAGE = open(M, 'r') while 1: l = MESSAGE.readline() if not l: break if re.search(r'^\.\s*\Z', l): client.wfile.write('.') client.wfile.write(l) client.wfile.write(MLEM) # pads message with final CRLF. So what. signal.alarm(timeout) def DELE(): global Data, Messages, client, deletia m = re.search(r'(\d+)', Data) if not m: ERR("message number required") return d = int(m.group(1)) # we "know" m is numeric if not 0 < d <= len(Messages): ERR("no message %s" % d) return M = Messages[d-1] if deletia.has_key(M): ERR("message %s deleted already" % d) return deletia[M] = 1 OK("message %s (%s) marked" % (d, M)) def NOOP(): OK("whatever") def RSET(): global deletia deletia = {} OK("biz buzz") def TOP(): global Data, Messages, client, deletia m = re.search(r'(\d+) (\d+)', Data) if not m: ERR("RFC1725 says TWO numbers here") return d = int(m.group(1)) n = int(m.group(2)) if not 0 < d <= len(Messages): ERR("no message %s" % d) return M = Messages[d-1] if deletia.has_key(M): ERR("message %s deleted already" % d) return OK("Here come headers for message %s (%s)" % (d, M)) signal.alarm(0) MESSAGE = open(M) # defaults to readonly counter = -1 while 1: # mkc: python doesn't have -- operator nor can = be used in an # expression line = MESSAGE.readline() counter -= 1 if not line or not counter: break # escape single dots if re.search(r'^\.\s*\Z', line): client.wfile.write('.') # mush first line of oversplit header (for mozilla) m = re.search(r'^(\S+\:)\s+\Z', line) if m: HB = m.group(1) line = MESSAGE.readline().lstrip() line = "%s %s" % (HB, line) client.wfile.write(line) if counter < 0 and not re.search(r'\w', line): counter = n client.wfile.write(MLEM) signal.alarm(timeout) def UIDL(): global Data, Messages, deletia m = re.search(r'(\d+)', Data) if m: d = int(m.group(1)) # we "know" m is numeric if not 0 < d <= len(Messages): ERR("no message number %s" % d) return M = Messages[d-1] if deletia.has_key(M): ERR("message %s deleted" % d) return OK(str(d)) return OK("Listing file names") signal.alarm(0) for d in xrange(1, len(Messages)+1): client.wfile.write("%s %s\r\n" % (d, Messages[d-1])) signal.alarm(timeout) client.wfile.write(".\r\n") # already have a leading CRLF from # the OK or the last message line. def UPDATE(): for Target in deletia.keys(): print "Trying to unlink %s" % Target if not os.path.isfile(Target): print "<%s> is not a file" % Target else: try: os.unlink(Target) print "unlinked %s" % Target except OSError: pass # just ignore failure PortNumber = 1100 # so can test w/o root #PortNumber = 110 timeout = 60 signal.signal(signal.SIGALRM, lambda signum, frame: sys.exit("alarm or timeout")) class PopHandler(SocketServer.StreamRequestHandler): def handle(self): try: pid = os.getpid() print pid, "New Connection", SockData(self) AUTHORIZATION(self) print pid, "authorized" TRANSACTION() print pid, "proceeding to update" UPDATE() print pid, "done" except SystemExit, e: print e class myForkingTCPServer(SocketServer.ForkingTCPServer): # just override some defaults request_queue_size = 100 allow_reuse_address = 1 server = myForkingTCPServer(("", PortNumber), PopHandler) server.serve_forever()