blob: 7b13b465718f39886e5c9ca4e2033774832d3a5b [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
Guido van Rossum8d5bef71998-09-14 17:36:51 +0000199 def list(self, which=None):
Guido van Rossum03774bb1998-04-09 13:50:55 +0000200 """Request listing, return result.
Guido van Rossum03774bb1998-04-09 13:50:55 +0000201
Guido van Rossum8d5bef71998-09-14 17:36:51 +0000202 Result without a message number argument is in form
Guido van Rossumf6ae7431998-09-02 14:42:02 +0000203 ['response', ['mesg_num octets', ...]].
204
Guido van Rossum8d5bef71998-09-14 17:36:51 +0000205 Result when a message number argument is given is a
206 single response: the "scan listing" for that message.
Guido van Rossum03774bb1998-04-09 13:50:55 +0000207 """
208 if which:
Guido van Rossumf6ae7431998-09-02 14:42:02 +0000209 return self._shortcmd('LIST %s' % which)
Guido van Rossum03774bb1998-04-09 13:50:55 +0000210 return self._longcmd('LIST')
211
Guido van Rossum484772d1998-04-06 18:27:27 +0000212
213 def retr(self, which):
Guido van Rossum03774bb1998-04-09 13:50:55 +0000214 """Retrieve whole message number 'which'.
215
216 Result is in form ['response', ['line', ...], octets].
217 """
218 return self._longcmd('RETR %s' % which)
219
Guido van Rossum484772d1998-04-06 18:27:27 +0000220
221 def dele(self, which):
Guido van Rossum03774bb1998-04-09 13:50:55 +0000222 """Delete message number 'which'.
223
224 Result is 'response'.
225 """
226 return self._shortcmd('DELE %s' % which)
227
Guido van Rossum484772d1998-04-06 18:27:27 +0000228
229 def noop(self):
Guido van Rossum03774bb1998-04-09 13:50:55 +0000230 """Does nothing.
231
232 One supposes the response indicates the server is alive.
233 """
Guido van Rossum484772d1998-04-06 18:27:27 +0000234 return self._shortcmd('NOOP')
235
Guido van Rossum03774bb1998-04-09 13:50:55 +0000236
Guido van Rossum484772d1998-04-06 18:27:27 +0000237 def rset(self):
Guido van Rossum03774bb1998-04-09 13:50:55 +0000238 """Not sure what this does."""
Guido van Rossum484772d1998-04-06 18:27:27 +0000239 return self._shortcmd('RSET')
240
Guido van Rossum484772d1998-04-06 18:27:27 +0000241
242 def quit(self):
Guido van Rossum03774bb1998-04-09 13:50:55 +0000243 """Signoff: commit changes on server, unlock mailbox, close connection."""
244 try:
245 resp = self._shortcmd('QUIT')
Guido van Rossumde23cb01998-08-06 02:59:07 +0000246 except error_proto, val:
Guido van Rossum03774bb1998-04-09 13:50:55 +0000247 resp = val
Guido van Rossum484772d1998-04-06 18:27:27 +0000248 self.file.close()
249 self.sock.close()
250 del self.file, self.sock
251 return resp
252
Guido van Rossum03774bb1998-04-09 13:50:55 +0000253 #__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 otherwise result is list ['response', ['mesgnum uid', ...], octets]
298 """
299 if which:
300 return self._shortcmd('UIDL %s' % which)
301 return self._longcmd('UIDL')
302
303
Guido van Rossum484772d1998-04-06 18:27:27 +0000304if __name__ == "__main__":
305 a = POP3(TESTSERVER)
306 print a.getwelcome()
307 a.user(TESTACCOUNT)
308 a.pass_(TESTPASSWORD)
309 a.list()
310 (numMsgs, totalSize) = a.stat()
311 for i in range(1, numMsgs + 1):
312 (header, msg, octets) = a.retr(i)
313 print "Message ", `i`, ':'
314 for line in msg:
315 print ' ' + line
316 print '-----------------------'
317 a.quit()