#!/usr/local/bin/python
from socket import *
from string import strip, split
from sys import argv, exit
import re, signal, md5
Timeout = 'Timeout'
def timeout(sig, frame) :
raise Timeout
OK_LIST = re.compile(r"\+OK ([0-9]+) ([0-9]+).*")
POP_ERROR = 'POP_ERROR'
POP_TIMEOUT = 'POP_TIMEOUT'
POP_USER = 'POP_USER'
class POP :
"The POP class is a clientside version of the POP3 protocol"
def __init__(self, HOST = '', PORT = 110, DEBUG = 0) :
"Initialize instance of Pop using (optional) HOST and PORT"
# Setup a handler for timeouts
signal.signal(signal.SIGALRM, timeout)
# Initate variables for socket use
self.socket = 0 # The socket, not yet
self.username = ''
self.password = ''
self.DEBUG = DEBUG
if HOST == '' :
return
self.connect(HOST, PORT)
def __del__(self) :
"Make sure that we close down before entering oblivion"
if self.DEBUG :
print "Entering __del__"
if self.socket :
if self.username :
self.socket.send("RSET\r\n")
data = self.socket.recv(1024)
self.socket.send("QUIT\r\n")
data = self.socket.recv(1024)
self.username = ''
self.socket.close()
def __getitem__(self, id) :
"Same as self.retrieve(ID)"
return self.retrieve(id)
def __delitem__(self, id) :
"Same as self.delete(ID)"
self.delete(id)
def __len__(self) :
"Return the number of messages at POP-server"
return self.status()[0]
def triad(self) :
"Return hostname, user and password on a connection"
if not self.username :
raise POP_ERROR, "User is not logged in"
return (self.host, self.username, self.password)
def connect(self, HOST, PORT = 110) :
"Connect to HOST using (optional) PORT"
if self.DEBUG :
print "Entering connect to host %s on port %d" % ( HOST, PORT)
if self.socket :
raise POP_ERROR, "Already connected"
signal.alarm(30)
try :
self.socket = socket(AF_INET, SOCK_STREAM)
self.socket.connect((HOST, PORT))
self.host = HOST
self.port = PORT
except Timeout :
raise POP_TIMEOUT, 'Timed out when trying to connect to %s' % HOST
signal.alarm(120)
try :
data = self.socket.recv(1024)
except Timeout :
raise POP_TIMEOUT, 'Timed out when getting initial message'
signal.alarm(0)
if data[0:3] != '+OK' :
self.socket.shutdown(2)
raise POP_ERROR, \
'Failed to get an OK initial message from %s' % HOST
answer = strip(data[4:])
if self.DEBUG :
print "Initial message was: '%s'" % answer
m = re.match(r".*(<[^>]*>).*\r\n", answer)
if m :
self.timestamp = m.group(1)
if self.DEBUG :
print "Timestamp was: %s" % self.timestamp
else :
self.timestamp = ''
if self.DEBUG :
print "No timestamp was given"
def close(self, rset = 1) :
"Close a connection, and maybe (optional) RESET first"
if self.DEBUG :
if rset :
print "Entering close, with reset"
else :
print "Entering close, without reset"
if self.username :
try :
if rset :
data = self.command("RSET")
data = self.command("QUIT", 10)
except POP_TIMEOUT :
raise POP_TIMEOUT, "QUIT timed out, indeterminded behavior"
self.username = ''
self.socket.close()
self.socket = 0
def reconnect(self) :
"Reset the current connection by closing and reopening it"
if self.socket :
self.close(1)
self.connect(self.host, self.port)
def multiline(self, data = '', ALARM = 10) :
"""Get a multiline answer. Ending with a line only a dot '.' which
will not be included. Allowing lines to be separated by only
<LF> instead of the required <CRLF>. Returning a list of lines"""
if self.DEBUG :
print "Entering multiline"
try :
result = []
while data[-5:] != '\r\n.\r\n' :
data = data + self.socket.recv(1024)
lines = split(data, '\n')
for line in lines :
if line and line[0:2] == '.\r' :
if len(line) == 2 :
signal.alarm(0)
return result
else :
result.append(line[1:])
else :
result.append(line)
raise POP_ERROR, "Unexpected end of a multiline answer"
except Timeout :
raise POP_TIMEOUT, "multiline timed out when getting data"
def command(self, COMMAND, ALARM = 5) :
"""Send a COMMAND and return the answer"""
if self.DEBUG :
print "Entering command with command %s" % COMMAND
if COMMAND[-1] != '\n' :
COMMAND = COMMAND + '\r\n'
try :
signal.alarm(ALARM)
if not self.socket :
if self.host and self.port :
self.connect(self.host, self.port)
signal.alarm(ALARM)
else :
signal.alarm(0)
raise POP_ERROR, \
"Unable to invoke command '%s', no connection" \
% COMMAND
if self.DEBUG :
print "Sending %s" % COMMAND
self.socket.send(COMMAND)
signal.alarm(ALARM)
if self.DEBUG :
print "Reciving data. . .",
data = self.socket.recv(1024)
if self.DEBUG :
m = re.match(r"(.*)\r\n", data)
if m :
print "'%s'" % m.group(1)
else :
print data
signal.alarm(0)
return data
except Timeout :
raise POP_TIMEOUT, "Timed out on command '%s'" % COMMAND
def user(self, NAME, PASSWORD) :
"Try log in USER with PASSWORD, first trying with apop command"
if self.DEBUG :
print "Entering user with %s and %s" % (NAME, PASSWORD)
if self.username :
raise POP_ERROR, "User %s is already in" % self.username
# Try APOP first to avoid sendig uncrypted password, if we can
if self.timestamp :
m = md5.new(self.timestamp + PASSWORD)
l = map(lambda x : hex(ord(x))[2:], m.digest())
digest = reduce(lambda x, y : x+y,
map(lambda x : hex(ord(x))[2:], m.digest()),
"")
data = self.command("APOP %s %s" % (NAME, digest))
if data[0:3] == '+OK' :
self.username = NAME
self.password = PASSWORD
return
# This is to handle a protocol-error in (at least) qpop
# which disconnects after a failed APOP instead of staying
# in authorization state
self.reconnect()
# End of protocol-error handling
data = self.command("USER %s" % NAME)
if data[0:3] != '+OK' :
raise POP_USER, "Username %s was not accepted, %s" % (NAME, data)
data = self.command("PASS %s" % PASSWORD, 10) # An extended timeout
if data[0:3] != '+OK' :
raise POP_USER, "Password was not accepted, %s" % data
self.username = NAME # We are in
self.password = PASSWORD
def quit(self) :
"""Close shop in a nice way"""
if self.DEBUG :
print "Entering quit"
self.close(0)
def list(self, msg = 0) :
"""List all messages, or just one MESSAGE. Returns an id, size pair
or a list of them"""
if msg == 0 :
return self.list_all()
if not self.username :
raise POP_ERROR, 'May only invoke list when user is active'
data = self.command("LIST %d" % msg)
m = OK_LIST.match(data)
if m :
return (int(m.group(1)), int(m.group(2)))
raise POP_ERROR, "LIST %d failed, %s" % (msg, data)
def list_all(self) :
"Auxiliary function for list"
if not self.username :
raise POP_ERROR, 'May only invoke list when user is active'
data = self.command("LIST")
if data[0:3] == '+OK' :
lines = self.multiline() # Get a multiline answer
result = []
for line in lines :
m = re.match(r"([0-9]+) ([0-9]+).*", line)
if m :
result.append((int(m.group(1)), int(m.group(2))))
elif self.DEBUG :
print "List: ignoring '%s'" % line
return result
raise POP_ERROR, "LIST failed, %s" % data
def uidl(self, msg = 0) :
"""List unique-id for all messages, or just one MESSAGE. Returns an
id, uid par or a list of them."""
if msg == 0 :
return self.uidl_all()
if not self.username :
raise POP_ERROR, 'May only invoke uidl when user is active'
data = self.command("UIDL %d" % msg)
m = re.match(r"\+OK ([0-9]+) ([!-~]+).*", data)
if m :
return (int(m.group(1)), m.group(2))
raise POP_ERROR, "UIDL %d failed, %s" % (msg, data)
def uidl_all(self) :
"Auxiliary function for uidl"
if not self.username :
raise POP_ERROR, 'May only invoke uidl when user is active'
data = self.command("UIDL")
if data[0:3] == '+OK' :
lines = self.multiline() # Get a multiline answer
result = []
for line in lines :
m = re.match(r"([0-9]+) ([!-~]+).*", line)
if m :
result.append((int(m.group(1)), m.group(2)))
elif self.DEBUG :
print "Uidl: ignoring '%s'" % line
return result
raise POP_ERROR, "UIDL failed, %s" % data
def noop(self) :
"Pass the time (maybe to keep a connection alive)"
if not self.username :
raise POP_ERROR, 'May only invoke noop when user is active'
data = self.command("NOOP")
if data[0:3] != '+OK' :
raise POP_ERROR, "NOOP failed, %s" % data
def retrieve(self, msg, delete = 0) :
"Get a MESSAGE, and optionally DELETE it afterwards."
if not self.username :
raise POP_ERROR, 'May only invoke retrieve when user is active'
data = self.command("RETR %d" % msg)
if data[0:3] != '+OK' :
raise POP_ERROR, "RETR %d failed, %s" % (msg, data)
(junk, data) = split(data, '\n', 1)
result = self.multiline(data)
if delete :
self.delete(msg)
return result
def reset(self) :
"Reset the state of this connection, does not log out the user"
if not self.username :
raise POP_ERROR, 'May only invoke reset when user is active'
data = self.command("RSET")
if data[0:3] != '+OK' :
raise POP_ERROR, "RSET failed, %s" % data
def delete(self, msg) :
"Delete a MESSAGE"
if not self.username :
raise POP_ERROR, 'May only invoke delete when user is active'
data = self.command("DELE %d" % msg)
if data[0:3] != '+OK' :
raise POP_ERROR, "DELE failed, %s" % data
def top(self, msg, lines = 0) :
"From MESSAGE get headers and optionally some LINES of the letter"
if not self.username :
raise POP_ERROR, 'May only invoke top when user is active'
data = self.command("TOP %d %d" % (msg, lines))
if data[0:3] != '+OK' :
raise POP_ERROR, "TOP %d %d failed, %s" % (msg, lines, data)
return self.multiline()
def status(self) :
"Returns the number of messages, and the total size of them"
if not self.username :
raise POP_ERROR, 'May only invoke status when user is active'
data = self.command("STAT")
m = re.match(r"\+OK ([0-9]+) ([0-9]+).*", data)
if m :
return (int(m.group(1)), int(m.group(2)))
else :
raise POP_ERROR, "STAT failed, %s" % data
##
# Functions that uses POP
##
def kill_large(pop, the_size = 209715152, verbose = 0) :
"""Connect to HOST use USER and PASSWORD to enter, and remove all
messages larger than (optional) SIZE, and be TALKATIVE.
Returns the number of remaing messages, and their size."""
(msgs, total) = pop.status()
if verbose :
print "Total of %d kbytes in %d messages" % \
((total+512) / 1024 , msgs)
for (n, size) in pop.list() :
if size > the_size :
if verbose :
print "Deleting %d because of size (%d)" % (n, size)
pop.delete(n)
(msgs, total) = pop.status()
if verbose :
print "Total size of %d remaining messages %d kbytes" % \
(msgs, (total + 512) / 1024)
return (msgs, (total + 512) / 1024)
def fetch_mail(host, user, password, mailfile) :
"Poll mail on HOST for USER with PASSWORD and write it to MAILFILE"
pop = POP(host)
pop.user(user, password)
triad = pop.triad()
(N, size) = pop.status()
if N == 0 :
pop.quit()
return triad
# Start by copying the old file
from tempfile import mktemp
from time import asctime, localtime, time
import re, os
eol = '\n'
filename = mktemp()
outfile = open(filename, "w")
try :
infile = open(mailfile, "r")
except IOError :
infile = None
if infile :
line = infile.readline()
while line :
outfile.write(line)
line = infile.readline()
infile.close()
returnpath = re.compile(r"^[Rr][Ee][Tt][Uu][Rr][Nn]-[Pp][Aa][Tt][Hh]:[\t ]*(.*)")
for id in range(1, N+1) :
message = pop.retrieve(id)
sender = "<>"
for line in message :
m = returnpath.match(line)
if m :
sender = m.group(1)
break
elif line == '' :
break
outfile.write("From %s %s\n" % (sender, asctime(localtime(time()))))
for line in message :
outfile.write("%s%s" % (line, eol))
if id < N :
outfile.write(eol)
outfile.close()
try :
os.rename(filename, mailfile)
except os.error : # Rename failed, try to copy
outfile = open(mailfile, "w")
infile = open(filename, "r")
line = infile.readline()
while line :
outfile.write(line)
line = infile.readline()
infile.close()
outfile.close()
os.remove(filename)
# Mailbox is updated, delete mail
for id in range(1, N+1) :
pop.delete(id)
pop.quit()
return triad
def poll_mail(host, user, password, mailfile, delay = 120) :
"Go and fetch the mail, again and again"
from time import sleep
while 1 :
(host, user, password) = fetch_mail(host, user, password, mailfile)
sleep(delay)
if __name__ == '__main__' :
import re, sys, os
host = user = password = ''
for i in range(1, len(sys.argv)) :
m = re.match(r"([a-zA-Z0-9]+)@([-.a-zA-Z0-9]+)", sys.argv[i])
if m :
user = m.group(1)
host = m.group(2)
else :
password = sys.argv[i]
mailfile = os.environ['MAIL']
if mailfile and host and user and password :
# fetch_mail(host, user, password, '/tmp/mail')
poll_mail(host, user, password, mailfile)
else :
print "usage: %s <user>@<maildrop> <password or secret phrase>" % \
argv[0]