|
1 """A POP3 client class. |
|
2 |
|
3 Based on the J. Myers POP3 draft, Jan. 96 |
|
4 """ |
|
5 |
|
6 # Author: David Ascher <david_ascher@brown.edu> |
|
7 # [heavily stealing from nntplib.py] |
|
8 # Updated: Piers Lauder <piers@cs.su.oz.au> [Jul '97] |
|
9 # String method conversion and test jig improvements by ESR, February 2001. |
|
10 # Added the POP3_SSL class. Methods loosely based on IMAP_SSL. Hector Urtubia <urtubia@mrbook.org> Aug 2003 |
|
11 |
|
12 # Example (see the test function at the end of this file) |
|
13 |
|
14 # Imports |
|
15 |
|
16 import re, socket |
|
17 |
|
18 __all__ = ["POP3","error_proto","POP3_SSL"] |
|
19 |
|
20 # Exception raised when an error or invalid response is received: |
|
21 |
|
22 class error_proto(Exception): pass |
|
23 |
|
24 # Standard Port |
|
25 POP3_PORT = 110 |
|
26 |
|
27 # POP SSL PORT |
|
28 POP3_SSL_PORT = 995 |
|
29 |
|
30 # Line terminators (we always output CRLF, but accept any of CRLF, LFCR, LF) |
|
31 CR = '\r' |
|
32 LF = '\n' |
|
33 CRLF = CR+LF |
|
34 |
|
35 |
|
36 class POP3: |
|
37 |
|
38 """This class supports both the minimal and optional command sets. |
|
39 Arguments can be strings or integers (where appropriate) |
|
40 (e.g.: retr(1) and retr('1') both work equally well. |
|
41 |
|
42 Minimal Command Set: |
|
43 USER name user(name) |
|
44 PASS string pass_(string) |
|
45 STAT stat() |
|
46 LIST [msg] list(msg = None) |
|
47 RETR msg retr(msg) |
|
48 DELE msg dele(msg) |
|
49 NOOP noop() |
|
50 RSET rset() |
|
51 QUIT quit() |
|
52 |
|
53 Optional Commands (some servers support these): |
|
54 RPOP name rpop(name) |
|
55 APOP name digest apop(name, digest) |
|
56 TOP msg n top(msg, n) |
|
57 UIDL [msg] uidl(msg = None) |
|
58 |
|
59 Raises one exception: 'error_proto'. |
|
60 |
|
61 Instantiate with: |
|
62 POP3(hostname, port=110) |
|
63 |
|
64 NB: the POP protocol locks the mailbox from user |
|
65 authorization until QUIT, so be sure to get in, suck |
|
66 the messages, and quit, each time you access the |
|
67 mailbox. |
|
68 |
|
69 POP is a line-based protocol, which means large mail |
|
70 messages consume lots of python cycles reading them |
|
71 line-by-line. |
|
72 |
|
73 If it's available on your mail server, use IMAP4 |
|
74 instead, it doesn't suffer from the two problems |
|
75 above. |
|
76 """ |
|
77 |
|
78 |
|
79 def __init__(self, host, port = POP3_PORT): |
|
80 self.host = host |
|
81 self.port = port |
|
82 msg = "getaddrinfo returns an empty list" |
|
83 self.sock = None |
|
84 for res in socket.getaddrinfo(self.host, self.port, 0, socket.SOCK_STREAM): |
|
85 af, socktype, proto, canonname, sa = res |
|
86 try: |
|
87 self.sock = socket.socket(af, socktype, proto) |
|
88 self.sock.connect(sa) |
|
89 except socket.error, msg: |
|
90 if self.sock: |
|
91 self.sock.close() |
|
92 self.sock = None |
|
93 continue |
|
94 break |
|
95 if not self.sock: |
|
96 raise socket.error, msg |
|
97 self.file = self.sock.makefile('rb') |
|
98 self._debugging = 0 |
|
99 self.welcome = self._getresp() |
|
100 |
|
101 |
|
102 def _putline(self, line): |
|
103 if self._debugging > 1: print '*put*', repr(line) |
|
104 self.sock.sendall('%s%s' % (line, CRLF)) |
|
105 |
|
106 |
|
107 # Internal: send one command to the server (through _putline()) |
|
108 |
|
109 def _putcmd(self, line): |
|
110 if self._debugging: print '*cmd*', repr(line) |
|
111 self._putline(line) |
|
112 |
|
113 |
|
114 # Internal: return one line from the server, stripping CRLF. |
|
115 # This is where all the CPU time of this module is consumed. |
|
116 # Raise error_proto('-ERR EOF') if the connection is closed. |
|
117 |
|
118 def _getline(self): |
|
119 line = self.file.readline() |
|
120 if self._debugging > 1: print '*get*', repr(line) |
|
121 if not line: raise error_proto('-ERR EOF') |
|
122 octets = len(line) |
|
123 # server can send any combination of CR & LF |
|
124 # however, 'readline()' returns lines ending in LF |
|
125 # so only possibilities are ...LF, ...CRLF, CR...LF |
|
126 if line[-2:] == CRLF: |
|
127 return line[:-2], octets |
|
128 if line[0] == CR: |
|
129 return line[1:-1], octets |
|
130 return line[:-1], octets |
|
131 |
|
132 |
|
133 # Internal: get a response from the server. |
|
134 # Raise 'error_proto' if the response doesn't start with '+'. |
|
135 |
|
136 def _getresp(self): |
|
137 resp, o = self._getline() |
|
138 if self._debugging > 1: print '*resp*', repr(resp) |
|
139 c = resp[:1] |
|
140 if c != '+': |
|
141 raise error_proto(resp) |
|
142 return resp |
|
143 |
|
144 |
|
145 # Internal: get a response plus following text from the server. |
|
146 |
|
147 def _getlongresp(self): |
|
148 resp = self._getresp() |
|
149 list = []; octets = 0 |
|
150 line, o = self._getline() |
|
151 while line != '.': |
|
152 if line[:2] == '..': |
|
153 o = o-1 |
|
154 line = line[1:] |
|
155 octets = octets + o |
|
156 list.append(line) |
|
157 line, o = self._getline() |
|
158 return resp, list, octets |
|
159 |
|
160 |
|
161 # Internal: send a command and get the response |
|
162 |
|
163 def _shortcmd(self, line): |
|
164 self._putcmd(line) |
|
165 return self._getresp() |
|
166 |
|
167 |
|
168 # Internal: send a command and get the response plus following text |
|
169 |
|
170 def _longcmd(self, line): |
|
171 self._putcmd(line) |
|
172 return self._getlongresp() |
|
173 |
|
174 |
|
175 # These can be useful: |
|
176 |
|
177 def getwelcome(self): |
|
178 return self.welcome |
|
179 |
|
180 |
|
181 def set_debuglevel(self, level): |
|
182 self._debugging = level |
|
183 |
|
184 |
|
185 # Here are all the POP commands: |
|
186 |
|
187 def user(self, user): |
|
188 """Send user name, return response |
|
189 |
|
190 (should indicate password required). |
|
191 """ |
|
192 return self._shortcmd('USER %s' % user) |
|
193 |
|
194 |
|
195 def pass_(self, pswd): |
|
196 """Send password, return response |
|
197 |
|
198 (response includes message count, mailbox size). |
|
199 |
|
200 NB: mailbox is locked by server from here to 'quit()' |
|
201 """ |
|
202 return self._shortcmd('PASS %s' % pswd) |
|
203 |
|
204 |
|
205 def stat(self): |
|
206 """Get mailbox status. |
|
207 |
|
208 Result is tuple of 2 ints (message count, mailbox size) |
|
209 """ |
|
210 retval = self._shortcmd('STAT') |
|
211 rets = retval.split() |
|
212 if self._debugging: print '*stat*', repr(rets) |
|
213 numMessages = int(rets[1]) |
|
214 sizeMessages = int(rets[2]) |
|
215 return (numMessages, sizeMessages) |
|
216 |
|
217 |
|
218 def list(self, which=None): |
|
219 """Request listing, return result. |
|
220 |
|
221 Result without a message number argument is in form |
|
222 ['response', ['mesg_num octets', ...], octets]. |
|
223 |
|
224 Result when a message number argument is given is a |
|
225 single response: the "scan listing" for that message. |
|
226 """ |
|
227 if which is not None: |
|
228 return self._shortcmd('LIST %s' % which) |
|
229 return self._longcmd('LIST') |
|
230 |
|
231 |
|
232 def retr(self, which): |
|
233 """Retrieve whole message number 'which'. |
|
234 |
|
235 Result is in form ['response', ['line', ...], octets]. |
|
236 """ |
|
237 return self._longcmd('RETR %s' % which) |
|
238 |
|
239 |
|
240 def dele(self, which): |
|
241 """Delete message number 'which'. |
|
242 |
|
243 Result is 'response'. |
|
244 """ |
|
245 return self._shortcmd('DELE %s' % which) |
|
246 |
|
247 |
|
248 def noop(self): |
|
249 """Does nothing. |
|
250 |
|
251 One supposes the response indicates the server is alive. |
|
252 """ |
|
253 return self._shortcmd('NOOP') |
|
254 |
|
255 |
|
256 def rset(self): |
|
257 """Not sure what this does.""" |
|
258 return self._shortcmd('RSET') |
|
259 |
|
260 |
|
261 def quit(self): |
|
262 """Signoff: commit changes on server, unlock mailbox, close connection.""" |
|
263 try: |
|
264 resp = self._shortcmd('QUIT') |
|
265 except error_proto, val: |
|
266 resp = val |
|
267 self.file.close() |
|
268 self.sock.close() |
|
269 del self.file, self.sock |
|
270 return resp |
|
271 |
|
272 #__del__ = quit |
|
273 |
|
274 |
|
275 # optional commands: |
|
276 |
|
277 def rpop(self, user): |
|
278 """Not sure what this does.""" |
|
279 return self._shortcmd('RPOP %s' % user) |
|
280 |
|
281 |
|
282 timestamp = re.compile(r'\+OK.*(<[^>]+>)') |
|
283 |
|
284 def apop(self, user, secret): |
|
285 """Authorisation |
|
286 |
|
287 - only possible if server has supplied a timestamp in initial greeting. |
|
288 |
|
289 Args: |
|
290 user - mailbox user; |
|
291 secret - secret shared between client and server. |
|
292 |
|
293 NB: mailbox is locked by server from here to 'quit()' |
|
294 """ |
|
295 m = self.timestamp.match(self.welcome) |
|
296 if not m: |
|
297 raise error_proto('-ERR APOP not supported by server') |
|
298 import hashlib |
|
299 digest = hashlib.md5(m.group(1)+secret).digest() |
|
300 digest = ''.join(map(lambda x:'%02x'%ord(x), digest)) |
|
301 return self._shortcmd('APOP %s %s' % (user, digest)) |
|
302 |
|
303 |
|
304 def top(self, which, howmuch): |
|
305 """Retrieve message header of message number 'which' |
|
306 and first 'howmuch' lines of message body. |
|
307 |
|
308 Result is in form ['response', ['line', ...], octets]. |
|
309 """ |
|
310 return self._longcmd('TOP %s %s' % (which, howmuch)) |
|
311 |
|
312 |
|
313 def uidl(self, which=None): |
|
314 """Return message digest (unique id) list. |
|
315 |
|
316 If 'which', result contains unique id for that message |
|
317 in the form 'response mesgnum uid', otherwise result is |
|
318 the list ['response', ['mesgnum uid', ...], octets] |
|
319 """ |
|
320 if which is not None: |
|
321 return self._shortcmd('UIDL %s' % which) |
|
322 return self._longcmd('UIDL') |
|
323 |
|
324 class POP3_SSL(POP3): |
|
325 """POP3 client class over SSL connection |
|
326 |
|
327 Instantiate with: POP3_SSL(hostname, port=995, keyfile=None, certfile=None) |
|
328 |
|
329 hostname - the hostname of the pop3 over ssl server |
|
330 port - port number |
|
331 keyfile - PEM formatted file that countains your private key |
|
332 certfile - PEM formatted certificate chain file |
|
333 |
|
334 See the methods of the parent class POP3 for more documentation. |
|
335 """ |
|
336 |
|
337 def __init__(self, host, port = POP3_SSL_PORT, keyfile = None, certfile = None): |
|
338 self.host = host |
|
339 self.port = port |
|
340 self.keyfile = keyfile |
|
341 self.certfile = certfile |
|
342 self.buffer = "" |
|
343 msg = "getaddrinfo returns an empty list" |
|
344 self.sock = None |
|
345 for res in socket.getaddrinfo(self.host, self.port, 0, socket.SOCK_STREAM): |
|
346 af, socktype, proto, canonname, sa = res |
|
347 try: |
|
348 self.sock = socket.socket(af, socktype, proto) |
|
349 self.sock.connect(sa) |
|
350 except socket.error, msg: |
|
351 if self.sock: |
|
352 self.sock.close() |
|
353 self.sock = None |
|
354 continue |
|
355 break |
|
356 if not self.sock: |
|
357 raise socket.error, msg |
|
358 self.file = self.sock.makefile('rb') |
|
359 self.sslobj = socket.ssl(self.sock, self.keyfile, self.certfile) |
|
360 self._debugging = 0 |
|
361 self.welcome = self._getresp() |
|
362 |
|
363 def _fillBuffer(self): |
|
364 localbuf = self.sslobj.read() |
|
365 if len(localbuf) == 0: |
|
366 raise error_proto('-ERR EOF') |
|
367 self.buffer += localbuf |
|
368 |
|
369 def _getline(self): |
|
370 line = "" |
|
371 renewline = re.compile(r'.*?\n') |
|
372 match = renewline.match(self.buffer) |
|
373 while not match: |
|
374 self._fillBuffer() |
|
375 match = renewline.match(self.buffer) |
|
376 line = match.group(0) |
|
377 self.buffer = renewline.sub('' ,self.buffer, 1) |
|
378 if self._debugging > 1: print '*get*', repr(line) |
|
379 |
|
380 octets = len(line) |
|
381 if line[-2:] == CRLF: |
|
382 return line[:-2], octets |
|
383 if line[0] == CR: |
|
384 return line[1:-1], octets |
|
385 return line[:-1], octets |
|
386 |
|
387 def _putline(self, line): |
|
388 if self._debugging > 1: print '*put*', repr(line) |
|
389 line += CRLF |
|
390 bytes = len(line) |
|
391 while bytes > 0: |
|
392 sent = self.sslobj.write(line) |
|
393 if sent == bytes: |
|
394 break # avoid copy |
|
395 line = line[sent:] |
|
396 bytes = bytes - sent |
|
397 |
|
398 def quit(self): |
|
399 """Signoff: commit changes on server, unlock mailbox, close connection.""" |
|
400 try: |
|
401 resp = self._shortcmd('QUIT') |
|
402 except error_proto, val: |
|
403 resp = val |
|
404 self.sock.close() |
|
405 del self.sslobj, self.sock |
|
406 return resp |
|
407 |
|
408 |
|
409 if __name__ == "__main__": |
|
410 import sys |
|
411 a = POP3(sys.argv[1]) |
|
412 print a.getwelcome() |
|
413 a.user(sys.argv[2]) |
|
414 a.pass_(sys.argv[3]) |
|
415 a.list() |
|
416 (numMsgs, totalSize) = a.stat() |
|
417 for i in range(1, numMsgs + 1): |
|
418 (header, msg, octets) = a.retr(i) |
|
419 print "Message %d:" % i |
|
420 for line in msg: |
|
421 print ' ' + line |
|
422 print '-----------------------' |
|
423 a.quit() |