blob: 2539892e64a6b0c5ba12eeb0f81f573cd2092aa7 [file] [log] [blame]
Guido van Rossum03774bb1998-04-09 13:50:55 +00001"""A POP3 client class.
Guido van Rossum484772d1998-04-06 18:27:27 +00002
Guido van Rossum03774bb1998-04-09 13:50:55 +00003Based on the J. Myers POP3 draft, Jan. 96
Guido van Rossum484772d1998-04-06 18:27:27 +00004
Guido van Rossum03774bb1998-04-09 13:50:55 +00005Author: David Ascher <david_ascher@brown.edu>
6 [heavily stealing from nntplib.py]
7Updated: Piers Lauder <piers@cs.su.oz.au> [Jul '97]
Guido van Rossum484772d1998-04-06 18:27:27 +00008"""
9
Guido van Rossum484772d1998-04-06 18:27:27 +000010# Example (see the test function at the end of this file)
11
12TESTSERVER = "localhost"
13TESTACCOUNT = "test"
14TESTPASSWORD = "_passwd_"
15
16# Imports
17
Guido van Rossum03774bb1998-04-09 13:50:55 +000018import regex, socket, string
Guido van Rossum484772d1998-04-06 18:27:27 +000019
20# Exception raised when an error or invalid response is received:
Guido van Rossum03774bb1998-04-09 13:50:55 +000021
22class error_proto(Exception): pass
Guido van Rossum484772d1998-04-06 18:27:27 +000023
24# Standard Port
25POP3_PORT = 110
26
Guido van Rossum03774bb1998-04-09 13:50:55 +000027# Line terminators (we always output CRLF, but accept any of CRLF, LFCR, LF)
28CR = '\r'
29LF = '\n'
30CRLF = CR+LF
Guido van Rossum484772d1998-04-06 18:27:27 +000031
32
33class POP3:
Guido van Rossum03774bb1998-04-09 13:50:55 +000034
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
Guido van Rossum484772d1998-04-06 18:27:27 +000076 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
Guido van Rossum03774bb1998-04-09 13:50:55 +000085
Guido van Rossum484772d1998-04-06 18:27:27 +000086 def _putline(self, line):
Guido van Rossum03774bb1998-04-09 13:50:55 +000087 #if self._debugging > 1: print '*put*', `line`
88 self.sock.send('%s%s' % (line, CRLF))
89
Guido van Rossum484772d1998-04-06 18:27:27 +000090
91 # Internal: send one command to the server (through _putline())
Guido van Rossum03774bb1998-04-09 13:50:55 +000092
Guido van Rossum484772d1998-04-06 18:27:27 +000093 def _putcmd(self, line):
Guido van Rossum03774bb1998-04-09 13:50:55 +000094 #if self._debugging: print '*cmd*', `line`
Guido van Rossum484772d1998-04-06 18:27:27 +000095 self._putline(line)
96
Guido van Rossum03774bb1998-04-09 13:50:55 +000097
Guido van Rossum484772d1998-04-06 18:27:27 +000098 # Internal: return one line from the server, stripping CRLF.
Guido van Rossum03774bb1998-04-09 13:50:55 +000099 # This is where all the CPU time of this module is consumed.
100 # Raise error_proto('-ERR EOF') if the connection is closed.
101
Guido van Rossum484772d1998-04-06 18:27:27 +0000102 def _getline(self):
103 line = self.file.readline()
Guido van Rossum03774bb1998-04-09 13:50:55 +0000104 #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
Guido van Rossum484772d1998-04-06 18:27:27 +0000116
117 # Internal: get a response from the server.
Guido van Rossum03774bb1998-04-09 13:50:55 +0000118 # Raise 'error_proto' if the response doesn't start with '+'.
119
Guido van Rossum484772d1998-04-06 18:27:27 +0000120 def _getresp(self):
Guido van Rossum03774bb1998-04-09 13:50:55 +0000121 resp, o = self._getline()
122 #if self._debugging > 1: print '*resp*', `resp`
Guido van Rossum484772d1998-04-06 18:27:27 +0000123 c = resp[:1]
124 if c != '+':
Guido van Rossum03774bb1998-04-09 13:50:55 +0000125 raise error_proto(resp)
Guido van Rossum484772d1998-04-06 18:27:27 +0000126 return resp
127
Guido van Rossum03774bb1998-04-09 13:50:55 +0000128
Guido van Rossum484772d1998-04-06 18:27:27 +0000129 # Internal: get a response plus following text from the server.
Guido van Rossum03774bb1998-04-09 13:50:55 +0000130
Guido van Rossum484772d1998-04-06 18:27:27 +0000131 def _getlongresp(self):
132 resp = self._getresp()
Guido van Rossum03774bb1998-04-09 13:50:55 +0000133 list = []; octets = 0
134 line, o = self._getline()
135 while line != '.':
136 octets = octets + o
Guido van Rossum484772d1998-04-06 18:27:27 +0000137 list.append(line)
Guido van Rossum03774bb1998-04-09 13:50:55 +0000138 line, o = self._getline()
139 return resp, list, octets
140
Guido van Rossum484772d1998-04-06 18:27:27 +0000141
142 # Internal: send a command and get the response
Guido van Rossum03774bb1998-04-09 13:50:55 +0000143
Guido van Rossum484772d1998-04-06 18:27:27 +0000144 def _shortcmd(self, line):
145 self._putcmd(line)
146 return self._getresp()
147
Guido van Rossum03774bb1998-04-09 13:50:55 +0000148
Guido van Rossum484772d1998-04-06 18:27:27 +0000149 # Internal: send a command and get the response plus following text
Guido van Rossum03774bb1998-04-09 13:50:55 +0000150
Guido van Rossum484772d1998-04-06 18:27:27 +0000151 def _longcmd(self, line):
152 self._putcmd(line)
153 return self._getlongresp()
154
Guido van Rossum03774bb1998-04-09 13:50:55 +0000155
Guido van Rossum484772d1998-04-06 18:27:27 +0000156 # These can be useful:
157
Guido van Rossum03774bb1998-04-09 13:50:55 +0000158 def getwelcome(self):
Guido van Rossum484772d1998-04-06 18:27:27 +0000159 return self.welcome
160
Guido van Rossum03774bb1998-04-09 13:50:55 +0000161
Guido van Rossum484772d1998-04-06 18:27:27 +0000162 def set_debuglevel(self, level):
163 self._debugging = level
164
Guido van Rossum03774bb1998-04-09 13:50:55 +0000165
Guido van Rossum484772d1998-04-06 18:27:27 +0000166 # Here are all the POP commands:
167
168 def user(self, user):
Guido van Rossum03774bb1998-04-09 13:50:55 +0000169 """Send user name, return response
170
171 (should indicate password required).
172 """
173 return self._shortcmd('USER %s' % user)
174
Guido van Rossum484772d1998-04-06 18:27:27 +0000175
176 def pass_(self, pswd):
Guido van Rossum03774bb1998-04-09 13:50:55 +0000177 """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
Guido van Rossum484772d1998-04-06 18:27:27 +0000185
186 def stat(self):
Guido van Rossum03774bb1998-04-09 13:50:55 +0000187 """Get mailbox status.
188
189 Result is tuple of 2 ints (message count, mailbox size)
190 """
Guido van Rossum484772d1998-04-06 18:27:27 +0000191 retval = self._shortcmd('STAT')
192 rets = string.split(retval)
Guido van Rossum03774bb1998-04-09 13:50:55 +0000193 #if self._debugging: print '*stat*', `rets`
Guido van Rossum484772d1998-04-06 18:27:27 +0000194 numMessages = string.atoi(rets[1])
195 sizeMessages = string.atoi(rets[2])
196 return (numMessages, sizeMessages)
197
Guido van Rossum03774bb1998-04-09 13:50:55 +0000198
199 def list(self, which=None):
200 """Request listing, return result.
201 Result is in form ['response', ['mesg_num octets', ...]].
202
203 Unsure what the optional 'msg' arg does.
204 """
205 if which:
206 return self._longcmd('LIST %s' % which)
207 return self._longcmd('LIST')
208
Guido van Rossum484772d1998-04-06 18:27:27 +0000209
210 def retr(self, which):
Guido van Rossum03774bb1998-04-09 13:50:55 +0000211 """Retrieve whole message number 'which'.
212
213 Result is in form ['response', ['line', ...], octets].
214 """
215 return self._longcmd('RETR %s' % which)
216
Guido van Rossum484772d1998-04-06 18:27:27 +0000217
218 def dele(self, which):
Guido van Rossum03774bb1998-04-09 13:50:55 +0000219 """Delete message number 'which'.
220
221 Result is 'response'.
222 """
223 return self._shortcmd('DELE %s' % which)
224
Guido van Rossum484772d1998-04-06 18:27:27 +0000225
226 def noop(self):
Guido van Rossum03774bb1998-04-09 13:50:55 +0000227 """Does nothing.
228
229 One supposes the response indicates the server is alive.
230 """
Guido van Rossum484772d1998-04-06 18:27:27 +0000231 return self._shortcmd('NOOP')
232
Guido van Rossum03774bb1998-04-09 13:50:55 +0000233
Guido van Rossum484772d1998-04-06 18:27:27 +0000234 def rset(self):
Guido van Rossum03774bb1998-04-09 13:50:55 +0000235 """Not sure what this does."""
Guido van Rossum484772d1998-04-06 18:27:27 +0000236 return self._shortcmd('RSET')
237
Guido van Rossum484772d1998-04-06 18:27:27 +0000238
239 def quit(self):
Guido van Rossum03774bb1998-04-09 13:50:55 +0000240 """Signoff: commit changes on server, unlock mailbox, close connection."""
241 try:
242 resp = self._shortcmd('QUIT')
Guido van Rossumde23cb01998-08-06 02:59:07 +0000243 except error_proto, val:
Guido van Rossum03774bb1998-04-09 13:50:55 +0000244 resp = val
Guido van Rossum484772d1998-04-06 18:27:27 +0000245 self.file.close()
246 self.sock.close()
247 del self.file, self.sock
248 return resp
249
Guido van Rossum03774bb1998-04-09 13:50:55 +0000250 #__del__ = quit
251
252
253 # optional commands:
254
255 def rpop(self, user):
256 """Not sure what this does."""
257 return self._shortcmd('RPOP %s' % user)
258
259
260 timestamp = regex.compile('\+OK.*\(<[^>]+>\)')
261
262 def apop(self, user, secret):
263 """Authorisation
264
265 - only possible if server has supplied a timestamp in initial greeting.
266
267 Args:
268 user - mailbox user;
269 secret - secret shared between client and server.
270
271 NB: mailbox is locked by server from here to 'quit()'
272 """
273 if self.timestamp.match(self.welcome) <= 0:
274 raise error_proto('-ERR APOP not supported by server')
275 import md5
276 digest = md5.new(self.timestamp.group(1)+secret).digest()
277 digest = string.join(map(lambda x:'%02x'%ord(x), digest), '')
278 return self._shortcmd('APOP %s %s' % (user, digest))
279
280
281 def top(self, which, howmuch):
282 """Retrieve message header of message number 'which'
283 and first 'howmuch' lines of message body.
284
285 Result is in form ['response', ['line', ...], octets].
286 """
287 return self._longcmd('TOP %s %s' % (which, howmuch))
288
289
290 def uidl(self, which=None):
291 """Return message digest (unique id) list.
292
293 If 'which', result contains unique id for that message,
294 otherwise result is list ['response', ['mesgnum uid', ...], octets]
295 """
296 if which:
297 return self._shortcmd('UIDL %s' % which)
298 return self._longcmd('UIDL')
299
300
Guido van Rossum484772d1998-04-06 18:27:27 +0000301if __name__ == "__main__":
302 a = POP3(TESTSERVER)
303 print a.getwelcome()
304 a.user(TESTACCOUNT)
305 a.pass_(TESTPASSWORD)
306 a.list()
307 (numMsgs, totalSize) = a.stat()
308 for i in range(1, numMsgs + 1):
309 (header, msg, octets) = a.retr(i)
310 print "Message ", `i`, ':'
311 for line in msg:
312 print ' ' + line
313 print '-----------------------'
314 a.quit()