From 03774bb5ea61022d2a76cc4a53a7dd24f61398d4 Mon Sep 17 00:00:00 2001
From: Guido van Rossum <guido@python.org>
Date: Thu, 9 Apr 1998 13:50:55 +0000
Subject: Version with docstrings and some other changes, by Piers Lauder.
 (Adapted by Just, I believe.)

---
 Lib/poplib.py | 289 ++++++++++++++++++++++++++++++++++++++++------------------
 1 file changed, 201 insertions(+), 88 deletions(-)

diff --git a/Lib/poplib.py b/Lib/poplib.py
index 7b406d9..c7b1679 100644
--- a/Lib/poplib.py
+++ b/Lib/poplib.py
@@ -1,12 +1,12 @@
-"""A POP3 client class.  Based on the J. Myers POP3 draft, Jan. 96
+"""A POP3 client class.
 
-Author: David Ascher <david_ascher@brown.edu> [heavily stealing from
-nntplib.py]
+Based on the J. Myers POP3 draft, Jan. 96
 
+Author: David Ascher <david_ascher@brown.edu>
+        [heavily stealing from nntplib.py]
+Updated: Piers Lauder <piers@cs.su.oz.au> [Jul '97]
 """
 
-__version__ = "0.01a - Feb 1, 1996 (with formatting changes by GvR)"
-
 # Example (see the test function at the end of this file)
 
 TESTSERVER = "localhost"
@@ -15,43 +15,64 @@ TESTPASSWORD = "_passwd_"
 
 # Imports
 
-from types import StringType
-import regex
-import socket
-import string
+import regex, socket, string
 
 # Exception raised when an error or invalid response is received:
-error_proto = 'pop3.error_proto'	 # response does not begin with +
+
+class error_proto(Exception): pass
 
 # Standard Port
 POP3_PORT = 110
 
-# Line terminators (we always output CRLF, but accept any of CRLF, CR, LF)
-CRLF = '\r\n'
-
-# This library supports both the minimal and optional command sets:
-# Arguments can be strings or integers (where appropriate)
-# (e.g.: retr(1) and retr('1') both work equally well.
-#
-# Minimal Command Set:
-#	 USER name				user(name)
-#	 PASS string				pass_(string)
-#	 STAT					stat()
-#	 LIST [msg]				list(msg = None)
-#	 RETR msg				retr(msg)
-#	 DELE msg				dele(msg)
-#	 NOOP					noop()
-#	 RSET					rset()
-#	 QUIT					quit()
-#
-# Optional Commands (some servers support these)
-#	 APOP name digest			apop(name, digest)
-#	 TOP msg n				top(msg, n)
-#	 UIDL [msg]				uidl(msg = None)
-#
+# Line terminators (we always output CRLF, but accept any of CRLF, LFCR, LF)
+CR = '\r'
+LF = '\n'
+CRLF = CR+LF
 
 
 class POP3:
+
+	"""This class supports both the minimal and optional command sets.
+	Arguments can be strings or integers (where appropriate)
+	(e.g.: retr(1) and retr('1') both work equally well.
+
+	Minimal Command Set:
+		USER name		user(name)
+		PASS string		pass_(string)
+		STAT			stat()
+		LIST [msg]		list(msg = None)
+		RETR msg		retr(msg)
+		DELE msg		dele(msg)
+		NOOP			noop()
+		RSET			rset()
+		QUIT			quit()
+
+	Optional Commands (some servers support these):
+		RPOP name		rpop(name)
+		APOP name digest	apop(name, digest)
+		TOP msg n		top(msg, n)
+		UIDL [msg]		uidl(msg = None)
+
+	Raises one exception: 'error_proto'.
+
+	Instantiate with:
+		POP3(hostname, port=110)
+
+	NB:	the POP protocol locks the mailbox from user
+		authorisation until QUIT, so be sure to get in, suck
+		the messages, and quit, each time you access the
+		mailbox.
+
+		POP is a line-based protocol, which means large mail
+		messages consume lots of python cycles reading them
+		line-by-line.
+
+		If it's available on your mail server, use IMAP4
+		instead, it doesn't suffer from the two problems
+		above.
+	"""
+
+
 	def __init__(self, host, port = POP3_PORT):
 		self.host = host
 		self.port = port
@@ -61,130 +82,222 @@ class POP3:
 		self._debugging = 0
 		self.welcome = self._getresp()
 
+
 	def _putline(self, line):
-		line = line + CRLF
-		if self._debugging > 1: print '*put*', `line`
-		self.sock.send(line)
+		#if self._debugging > 1: print '*put*', `line`
+		self.sock.send('%s%s' % (line, CRLF))
+
 
 	# Internal: send one command to the server (through _putline())
+
 	def _putcmd(self, line):
-		if self._debugging: print '*cmd*', `line`
+		#if self._debugging: print '*cmd*', `line`
 		self._putline(line)
 
+
 	# Internal: return one line from the server, stripping CRLF.
-	# Raise EOFError if the connection is closed
+	# This is where all the CPU time of this module is consumed.
+	# Raise error_proto('-ERR EOF') if the connection is closed.
+
 	def _getline(self):
 		line = self.file.readline()
-		if self._debugging > 1:
-			print '*get*', `line`
-		if not line: raise EOFError
-		if line[-2:] == CRLF: line = line[:-2]
-		elif line[-1:] in CRLF: line = line[:-1]
-		return line
+		#if self._debugging > 1: print '*get*', `line`
+		if not line: raise error_proto('-ERR EOF')
+		octets = len(line)
+		# server can send any combination of CR & LF
+		# however, 'readline()' returns lines ending in LF
+		# so only possibilities are ...LF, ...CRLF, CR...LF
+		if line[-2:] == CRLF:
+			return line[:-2], octets
+		if line[0] == CR:
+			return line[1:-1], octets
+		return line[:-1], octets
+
 
 	# Internal: get a response from the server.
-	# Raise various errors if the response indicates an error
+	# Raise 'error_proto' if the response doesn't start with '+'.
+
 	def _getresp(self):
-		resp = self._getline()
-		if self._debugging > 1: print '*resp*', `resp`
+		resp, o = self._getline()
+		#if self._debugging > 1: print '*resp*', `resp`
 		c = resp[:1]
 		if c != '+':
-			raise error_proto, resp
+			raise error_proto(resp)
 		return resp
 
+
 	# Internal: get a response plus following text from the server.
-	# Raise various errors if the response indicates an error
+
 	def _getlongresp(self):
 		resp = self._getresp()
-		list = []
-		while 1:
-			line = self._getline()
-			if line == '.':
-				break
+		list = []; octets = 0
+		line, o = self._getline()
+		while line != '.':
+			octets = octets + o
 			list.append(line)
-		return resp, list
+			line, o = self._getline()
+		return resp, list, octets
+
 
 	# Internal: send a command and get the response
+
 	def _shortcmd(self, line):
 		self._putcmd(line)
 		return self._getresp()
 
+
 	# Internal: send a command and get the response plus following text
+
 	def _longcmd(self, line):
 		self._putcmd(line)
 		return self._getlongresp()
 
+
 	# These can be useful:
 
-	def getwelcome(self):
+	def getwelcome(self): 
 		return self.welcome
 
+
 	def set_debuglevel(self, level):
 		self._debugging = level
 
+
 	# Here are all the POP commands:
 
 	def user(self, user):
-		user = str(user)
-		return self._shortcmd('USER ' + user)
+		"""Send user name, return response
+		
+		(should indicate password required).
+		"""
+		return self._shortcmd('USER %s' % user)
+
 
 	def pass_(self, pswd):
-		pswd = str(pswd)
-		return self._shortcmd('PASS ' + pswd)
+		"""Send password, return response
+		
+		(response includes message count, mailbox size).
+
+		NB: mailbox is locked by server from here to 'quit()'
+		"""
+		return self._shortcmd('PASS %s' % pswd)
+
 
 	def stat(self):
+		"""Get mailbox status.
+		
+		Result is tuple of 2 ints (message count, mailbox size)
+		"""
 		retval = self._shortcmd('STAT')
 		rets = string.split(retval)
+		#if self._debugging: print '*stat*', `rets`
 		numMessages = string.atoi(rets[1])
 		sizeMessages = string.atoi(rets[2])
 		return (numMessages, sizeMessages)
 
-	def list(self, msg=None):
-		if msg:
-			msg = str(msg)
-			return self._longcmd('LIST ' + msg)
-		else:
-			return self._longcmd('LIST')
+
+	def list(self, which=None):
+		"""Request listing, return result.
+		Result is in form ['response', ['mesg_num octets', ...]].
+
+		Unsure what the optional 'msg' arg does.
+		"""
+		if which:
+			return self._longcmd('LIST %s' % which)
+		return self._longcmd('LIST')
+
 
 	def retr(self, which):
-		which = str(which)
-		return self._longcmd('RETR ' + which)
+		"""Retrieve whole message number 'which'.
+
+		Result is in form ['response', ['line', ...], octets].
+		"""
+		return self._longcmd('RETR %s' % which)
+
 
 	def dele(self, which):
-		which = str(which)
-		return self._shortcmd('DELE ' + which)
+		"""Delete message number 'which'.
+
+		Result is 'response'.
+		"""
+		return self._shortcmd('DELE %s' % which)
+
 
 	def noop(self):
+		"""Does nothing.
+		
+		One supposes the response indicates the server is alive.
+		"""
 		return self._shortcmd('NOOP')
 
+
 	def rset(self):
+		"""Not sure what this does."""
 		return self._shortcmd('RSET')
 
-	# optional commands:
-
-	def apop(self, digest):
-		digest = str(digest)
-		return self._shortcmd('APOP ' + digest)
-
-	def top(self, which, howmuch):
-		which = str(which)
-		howmuch = str(howmuch)
-		return self._longcmd('TOP ' + which + ' ' + howmuch)
-
-	def uidl(self, which = None):
-		if which:
-			which = str(which)
-			return self._longcmd('UIDL ' + which)
-		else:
-			return self._longcmd('UIDL')
 
 	def quit(self):
-		resp = self._shortcmd('QUIT')
+		"""Signoff: commit changes on server, unlock mailbox, close connection."""
+		try:
+			resp = self._shortcmd('QUIT')
+		except error_proto(val):
+			resp = val
 		self.file.close()
 		self.sock.close()
 		del self.file, self.sock
 		return resp
 
+	#__del__ = quit
+
+
+	# optional commands:
+
+	def rpop(self, user):
+		"""Not sure what this does."""
+		return self._shortcmd('RPOP %s' % user)
+
+
+	timestamp = regex.compile('\+OK.*\(<[^>]+>\)')
+
+	def apop(self, user, secret):
+		"""Authorisation
+		
+		- only possible if server has supplied a timestamp in initial greeting.
+
+		Args:
+			user	- mailbox user;
+			secret	- secret shared between client and server.
+
+		NB: mailbox is locked by server from here to 'quit()'
+		"""
+		if self.timestamp.match(self.welcome) <= 0:
+			raise error_proto('-ERR APOP not supported by server')
+		import md5
+		digest = md5.new(self.timestamp.group(1)+secret).digest()
+		digest = string.join(map(lambda x:'%02x'%ord(x), digest), '')
+		return self._shortcmd('APOP %s %s' % (user, digest))
+
+
+	def top(self, which, howmuch):
+		"""Retrieve message header of message number 'which'
+		and first 'howmuch' lines of message body.
+
+		Result is in form ['response', ['line', ...], octets].
+		"""
+		return self._longcmd('TOP %s %s' % (which, howmuch))
+
+
+	def uidl(self, which=None):
+		"""Return message digest (unique id) list.
+
+		If 'which', result contains unique id for that message,
+		otherwise result is list ['response', ['mesgnum uid', ...], octets]
+		"""
+		if which:
+			return self._shortcmd('UIDL %s' % which)
+		return self._longcmd('UIDL')
+
+				
 if __name__ == "__main__":
 	a = POP3(TESTSERVER)
 	print a.getwelcome()
-- 
cgit v0.12

