poplib.py revision 361c048b8fbd1c57597bcd8ee08c2cc0fc361253
1"""A POP3 client class.
2
3Based on the J. Myers POP3 draft, Jan. 96
4
5Author: David Ascher <david_ascher@brown.edu>
6        [heavily stealing from nntplib.py]
7Updated: Piers Lauder <piers@cs.su.oz.au> [Jul '97]
8"""
9
10# Example (see the test function at the end of this file)
11
12TESTSERVER = "localhost"
13TESTACCOUNT = "test"
14TESTPASSWORD = "_passwd_"
15
16# Imports
17
18import regex, socket, string
19
20# Exception raised when an error or invalid response is received:
21
22class error_proto(Exception): pass
23
24# Standard Port
25POP3_PORT = 110
26
27# Line terminators (we always output CRLF, but accept any of CRLF, LFCR, LF)
28CR = '\r'
29LF = '\n'
30CRLF = CR+LF
31
32
33class POP3:
34
35	"""This class supports both the minimal and optional command sets.
36	Arguments can be strings or integers (where appropriate)
37	(e.g.: retr(1) and retr('1') both work equally well.
38
39	Minimal Command Set:
40		USER name		user(name)
41		PASS string		pass_(string)
42		STAT			stat()
43		LIST [msg]		list(msg = None)
44		RETR msg		retr(msg)
45		DELE msg		dele(msg)
46		NOOP			noop()
47		RSET			rset()
48		QUIT			quit()
49
50	Optional Commands (some servers support these):
51		RPOP name		rpop(name)
52		APOP name digest	apop(name, digest)
53		TOP msg n		top(msg, n)
54		UIDL [msg]		uidl(msg = None)
55
56	Raises one exception: 'error_proto'.
57
58	Instantiate with:
59		POP3(hostname, port=110)
60
61	NB:	the POP protocol locks the mailbox from user
62		authorisation until QUIT, so be sure to get in, suck
63		the messages, and quit, each time you access the
64		mailbox.
65
66		POP is a line-based protocol, which means large mail
67		messages consume lots of python cycles reading them
68		line-by-line.
69
70		If it's available on your mail server, use IMAP4
71		instead, it doesn't suffer from the two problems
72		above.
73	"""
74
75
76	def __init__(self, host, port = POP3_PORT):
77		self.host = host
78		self.port = port
79		self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
80		self.sock.connect(self.host, self.port)
81		self.file = self.sock.makefile('rb')
82		self._debugging = 0
83		self.welcome = self._getresp()
84
85
86	def _putline(self, line):
87		#if self._debugging > 1: print '*put*', `line`
88		self.sock.send('%s%s' % (line, CRLF))
89
90
91	# Internal: send one command to the server (through _putline())
92
93	def _putcmd(self, line):
94		#if self._debugging: print '*cmd*', `line`
95		self._putline(line)
96
97
98	# Internal: return one line from the server, stripping CRLF.
99	# This is where all the CPU time of this module is consumed.
100	# Raise error_proto('-ERR EOF') if the connection is closed.
101
102	def _getline(self):
103		line = self.file.readline()
104		#if self._debugging > 1: print '*get*', `line`
105		if not line: raise error_proto('-ERR EOF')
106		octets = len(line)
107		# server can send any combination of CR & LF
108		# however, 'readline()' returns lines ending in LF
109		# so only possibilities are ...LF, ...CRLF, CR...LF
110		if line[-2:] == CRLF:
111			return line[:-2], octets
112		if line[0] == CR:
113			return line[1:-1], octets
114		return line[:-1], octets
115
116
117	# Internal: get a response from the server.
118	# Raise 'error_proto' if the response doesn't start with '+'.
119
120	def _getresp(self):
121		resp, o = self._getline()
122		#if self._debugging > 1: print '*resp*', `resp`
123		c = resp[:1]
124		if c != '+':
125			raise error_proto(resp)
126		return resp
127
128
129	# Internal: get a response plus following text from the server.
130
131	def _getlongresp(self):
132		resp = self._getresp()
133		list = []; octets = 0
134		line, o = self._getline()
135		while line != '.':
136			octets = octets + o
137			list.append(line)
138			line, o = self._getline()
139		return resp, list, octets
140
141
142	# Internal: send a command and get the response
143
144	def _shortcmd(self, line):
145		self._putcmd(line)
146		return self._getresp()
147
148
149	# Internal: send a command and get the response plus following text
150
151	def _longcmd(self, line):
152		self._putcmd(line)
153		return self._getlongresp()
154
155
156	# These can be useful:
157
158	def getwelcome(self):
159		return self.welcome
160
161
162	def set_debuglevel(self, level):
163		self._debugging = level
164
165
166	# Here are all the POP commands:
167
168	def user(self, user):
169		"""Send user name, return response
170
171		(should indicate password required).
172		"""
173		return self._shortcmd('USER %s' % user)
174
175
176	def pass_(self, pswd):
177		"""Send password, return response
178
179		(response includes message count, mailbox size).
180
181		NB: mailbox is locked by server from here to 'quit()'
182		"""
183		return self._shortcmd('PASS %s' % pswd)
184
185
186	def stat(self):
187		"""Get mailbox status.
188
189		Result is tuple of 2 ints (message count, mailbox size)
190		"""
191		retval = self._shortcmd('STAT')
192		rets = string.split(retval)
193		#if self._debugging: print '*stat*', `rets`
194		numMessages = string.atoi(rets[1])
195		sizeMessages = string.atoi(rets[2])
196		return (numMessages, sizeMessages)
197
198
199	def list(self, which=None):
200		"""Request listing, return result.
201
202		Result without a message number argument is in form
203		['response', ['mesg_num octets', ...]].
204
205		Result when a message number argument is given is a
206		single response: the "scan listing" for that message.
207		"""
208		if which:
209			return self._shortcmd('LIST %s' % which)
210		return self._longcmd('LIST')
211
212
213	def retr(self, which):
214		"""Retrieve whole message number 'which'.
215
216		Result is in form ['response', ['line', ...], octets].
217		"""
218		return self._longcmd('RETR %s' % which)
219
220
221	def dele(self, which):
222		"""Delete message number 'which'.
223
224		Result is 'response'.
225		"""
226		return self._shortcmd('DELE %s' % which)
227
228
229	def noop(self):
230		"""Does nothing.
231
232		One supposes the response indicates the server is alive.
233		"""
234		return self._shortcmd('NOOP')
235
236
237	def rset(self):
238		"""Not sure what this does."""
239		return self._shortcmd('RSET')
240
241
242	def quit(self):
243		"""Signoff: commit changes on server, unlock mailbox, close connection."""
244		try:
245			resp = self._shortcmd('QUIT')
246		except error_proto, val:
247			resp = val
248		self.file.close()
249		self.sock.close()
250		del self.file, self.sock
251		return resp
252
253	#__del__ = quit
254
255
256	# optional commands:
257
258	def rpop(self, user):
259		"""Not sure what this does."""
260		return self._shortcmd('RPOP %s' % user)
261
262
263	timestamp = regex.compile('\+OK.*\(<[^>]+>\)')
264
265	def apop(self, user, secret):
266		"""Authorisation
267
268		- only possible if server has supplied a timestamp in initial greeting.
269
270		Args:
271			user	- mailbox user;
272			secret	- secret shared between client and server.
273
274		NB: mailbox is locked by server from here to 'quit()'
275		"""
276		if self.timestamp.match(self.welcome) <= 0:
277			raise error_proto('-ERR APOP not supported by server')
278		import md5
279		digest = md5.new(self.timestamp.group(1)+secret).digest()
280		digest = string.join(map(lambda x:'%02x'%ord(x), digest), '')
281		return self._shortcmd('APOP %s %s' % (user, digest))
282
283
284	def top(self, which, howmuch):
285		"""Retrieve message header of message number 'which'
286		and first 'howmuch' lines of message body.
287
288		Result is in form ['response', ['line', ...], octets].
289		"""
290		return self._longcmd('TOP %s %s' % (which, howmuch))
291
292
293	def uidl(self, which=None):
294		"""Return message digest (unique id) list.
295
296		If 'which', result contains unique id for that message
297		in the form 'response mesgnum uid', otherwise result is
298		the list ['response', ['mesgnum uid', ...], octets]
299		"""
300		if which:
301			return self._shortcmd('UIDL %s' % which)
302		return self._longcmd('UIDL')
303
304
305if __name__ == "__main__":
306	a = POP3(TESTSERVER)
307	print a.getwelcome()
308	a.user(TESTACCOUNT)
309	a.pass_(TESTPASSWORD)
310	a.list()
311	(numMsgs, totalSize) = a.stat()
312	for i in range(1, numMsgs + 1):
313		(header, msg, octets) = a.retr(i)
314		print "Message ", `i`, ':'
315		for line in msg:
316			print '   ' + line
317		print '-----------------------'
318	a.quit()
319