poplib.py 7.38 KB
Newer Older
1
"""A POP3 client class.
2

3
Based on the J. Myers POP3 draft, Jan. 96
4 5
"""

6 7 8 9
# Author: David Ascher <david_ascher@brown.edu>
#         [heavily stealing from nntplib.py]
# Updated: Piers Lauder <piers@cs.su.oz.au> [Jul '97]

10 11 12 13 14 15 16 17
# Example (see the test function at the end of this file)

TESTSERVER = "localhost"
TESTACCOUNT = "test"
TESTPASSWORD = "_passwd_"

# Imports

18
import regex, socket, string
19 20

# Exception raised when an error or invalid response is received:
21 22

class error_proto(Exception): pass
23 24 25 26

# Standard Port
POP3_PORT = 110

27 28 29 30
# Line terminators (we always output CRLF, but accept any of CRLF, LFCR, LF)
CR = '\r'
LF = '\n'
CRLF = CR+LF
31 32 33


class POP3:
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61

	"""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
62
		authorization until QUIT, so be sure to get in, suck
63 64 65 66 67 68 69 70 71 72 73 74 75
		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.
	"""


76 77 78 79
	def __init__(self, host, port = POP3_PORT):
		self.host = host
		self.port = port
		self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
Guido van Rossum's avatar
Guido van Rossum committed
80
		self.sock.connect((self.host, self.port))
81 82 83 84
		self.file = self.sock.makefile('rb')
		self._debugging = 0
		self.welcome = self._getresp()

85

86
	def _putline(self, line):
87 88 89
		#if self._debugging > 1: print '*put*', `line`
		self.sock.send('%s%s' % (line, CRLF))

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 96
		self._putline(line)

97

98
	# Internal: return one line from the server, stripping CRLF.
99 100 101
	# This is where all the CPU time of this module is consumed.
	# Raise error_proto('-ERR EOF') if the connection is closed.

102 103
	def _getline(self):
		line = self.file.readline()
104 105 106 107 108 109 110 111 112 113 114 115
		#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

116 117

	# Internal: get a response from the server.
118 119
	# Raise 'error_proto' if the response doesn't start with '+'.

120
	def _getresp(self):
121 122
		resp, o = self._getline()
		#if self._debugging > 1: print '*resp*', `resp`
123 124
		c = resp[:1]
		if c != '+':
125
			raise error_proto(resp)
126 127
		return resp

128

129
	# Internal: get a response plus following text from the server.
130

131 132
	def _getlongresp(self):
		resp = self._getresp()
133 134 135
		list = []; octets = 0
		line, o = self._getline()
		while line != '.':
136 137 138
			if line[:2] == '..':
				o = o-1
				line = line[1:]
139
			octets = octets + o
140
			list.append(line)
141 142 143
			line, o = self._getline()
		return resp, list, octets

144 145

	# Internal: send a command and get the response
146

147 148 149 150
	def _shortcmd(self, line):
		self._putcmd(line)
		return self._getresp()

151

152
	# Internal: send a command and get the response plus following text
153

154 155 156 157
	def _longcmd(self, line):
		self._putcmd(line)
		return self._getlongresp()

158

159 160
	# These can be useful:

161
	def getwelcome(self): 
162 163
		return self.welcome

164

165 166 167
	def set_debuglevel(self, level):
		self._debugging = level

168

169 170 171
	# Here are all the POP commands:

	def user(self, user):
172 173 174 175 176 177
		"""Send user name, return response
		
		(should indicate password required).
		"""
		return self._shortcmd('USER %s' % user)

178 179

	def pass_(self, pswd):
180 181 182 183 184 185 186 187
		"""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)

188 189

	def stat(self):
190 191 192 193
		"""Get mailbox status.
		
		Result is tuple of 2 ints (message count, mailbox size)
		"""
194 195
		retval = self._shortcmd('STAT')
		rets = string.split(retval)
196
		#if self._debugging: print '*stat*', `rets`
197 198 199 200
		numMessages = string.atoi(rets[1])
		sizeMessages = string.atoi(rets[2])
		return (numMessages, sizeMessages)

201

202
	def list(self, which=None):
203 204
		"""Request listing, return result.

205
		Result without a message number argument is in form
206 207
		['response', ['mesg_num octets', ...]].

208 209
		Result when a message number argument is given is a
		single response: the "scan listing" for that message.
210 211
		"""
		if which:
212
			return self._shortcmd('LIST %s' % which)
213 214
		return self._longcmd('LIST')

215 216

	def retr(self, which):
217 218 219 220 221 222
		"""Retrieve whole message number 'which'.

		Result is in form ['response', ['line', ...], octets].
		"""
		return self._longcmd('RETR %s' % which)

223 224

	def dele(self, which):
225 226 227 228 229 230
		"""Delete message number 'which'.

		Result is 'response'.
		"""
		return self._shortcmd('DELE %s' % which)

231 232

	def noop(self):
233 234 235 236
		"""Does nothing.
		
		One supposes the response indicates the server is alive.
		"""
237 238
		return self._shortcmd('NOOP')

239

240
	def rset(self):
241
		"""Not sure what this does."""
242 243 244 245
		return self._shortcmd('RSET')


	def quit(self):
246 247 248
		"""Signoff: commit changes on server, unlock mailbox, close connection."""
		try:
			resp = self._shortcmd('QUIT')
249
		except error_proto, val:
250
			resp = val
251 252 253 254 255
		self.file.close()
		self.sock.close()
		del self.file, self.sock
		return resp

256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
	#__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.

299 300 301
		If 'which', result contains unique id for that message
		in the form 'response mesgnum uid', otherwise result is
		the list ['response', ['mesgnum uid', ...], octets]
302 303 304 305 306 307
		"""
		if which:
			return self._shortcmd('UIDL %s' % which)
		return self._longcmd('UIDL')

				
308 309 310 311 312 313 314 315 316 317 318 319 320 321
if __name__ == "__main__":
	a = POP3(TESTSERVER)
	print a.getwelcome()
	a.user(TESTACCOUNT)
	a.pass_(TESTPASSWORD)
	a.list()
	(numMsgs, totalSize) = a.stat()
	for i in range(1, numMsgs + 1):
		(header, msg, octets) = a.retr(i)
		print "Message ", `i`, ':'
		for line in msg:
			print '   ' + line
		print '-----------------------'
	a.quit()