|
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"] |
|
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 timeout=socket._GLOBAL_DEFAULT_TIMEOUT): |
|
81 self.host = host |
|
82 self.port = port |
|
83 self.sock = socket.create_connection((host, port), timeout) |
|
84 self.file = self.sock.makefile('rb') |
|
85 self._debugging = 0 |
|
86 self.welcome = self._getresp() |
|
87 |
|
88 |
|
89 def _putline(self, line): |
|
90 if self._debugging > 1: print '*put*', repr(line) |
|
91 self.sock.sendall('%s%s' % (line, CRLF)) |
|
92 |
|
93 |
|
94 # Internal: send one command to the server (through _putline()) |
|
95 |
|
96 def _putcmd(self, line): |
|
97 if self._debugging: print '*cmd*', repr(line) |
|
98 self._putline(line) |
|
99 |
|
100 |
|
101 # Internal: return one line from the server, stripping CRLF. |
|
102 # This is where all the CPU time of this module is consumed. |
|
103 # Raise error_proto('-ERR EOF') if the connection is closed. |
|
104 |
|
105 def _getline(self): |
|
106 line = self.file.readline() |
|
107 if self._debugging > 1: print '*get*', repr(line) |
|
108 if not line: raise error_proto('-ERR EOF') |
|
109 octets = len(line) |
|
110 # server can send any combination of CR & LF |
|
111 # however, 'readline()' returns lines ending in LF |
|
112 # so only possibilities are ...LF, ...CRLF, CR...LF |
|
113 if line[-2:] == CRLF: |
|
114 return line[:-2], octets |
|
115 if line[0] == CR: |
|
116 return line[1:-1], octets |
|
117 return line[:-1], octets |
|
118 |
|
119 |
|
120 # Internal: get a response from the server. |
|
121 # Raise 'error_proto' if the response doesn't start with '+'. |
|
122 |
|
123 def _getresp(self): |
|
124 resp, o = self._getline() |
|
125 if self._debugging > 1: print '*resp*', repr(resp) |
|
126 c = resp[:1] |
|
127 if c != '+': |
|
128 raise error_proto(resp) |
|
129 return resp |
|
130 |
|
131 |
|
132 # Internal: get a response plus following text from the server. |
|
133 |
|
134 def _getlongresp(self): |
|
135 resp = self._getresp() |
|
136 list = []; octets = 0 |
|
137 line, o = self._getline() |
|
138 while line != '.': |
|
139 if line[:2] == '..': |
|
140 o = o-1 |
|
141 line = line[1:] |
|
142 octets = octets + o |
|
143 list.append(line) |
|
144 line, o = self._getline() |
|
145 return resp, list, octets |
|
146 |
|
147 |
|
148 # Internal: send a command and get the response |
|
149 |
|
150 def _shortcmd(self, line): |
|
151 self._putcmd(line) |
|
152 return self._getresp() |
|
153 |
|
154 |
|
155 # Internal: send a command and get the response plus following text |
|
156 |
|
157 def _longcmd(self, line): |
|
158 self._putcmd(line) |
|
159 return self._getlongresp() |
|
160 |
|
161 |
|
162 # These can be useful: |
|
163 |
|
164 def getwelcome(self): |
|
165 return self.welcome |
|
166 |
|
167 |
|
168 def set_debuglevel(self, level): |
|
169 self._debugging = level |
|
170 |
|
171 |
|
172 # Here are all the POP commands: |
|
173 |
|
174 def user(self, user): |
|
175 """Send user name, return response |
|
176 |
|
177 (should indicate password required). |
|
178 """ |
|
179 return self._shortcmd('USER %s' % user) |
|
180 |
|
181 |
|
182 def pass_(self, pswd): |
|
183 """Send password, return response |
|
184 |
|
185 (response includes message count, mailbox size). |
|
186 |
|
187 NB: mailbox is locked by server from here to 'quit()' |
|
188 """ |
|
189 return self._shortcmd('PASS %s' % pswd) |
|
190 |
|
191 |
|
192 def stat(self): |
|
193 """Get mailbox status. |
|
194 |
|
195 Result is tuple of 2 ints (message count, mailbox size) |
|
196 """ |
|
197 retval = self._shortcmd('STAT') |
|
198 rets = retval.split() |
|
199 if self._debugging: print '*stat*', repr(rets) |
|
200 numMessages = int(rets[1]) |
|
201 sizeMessages = int(rets[2]) |
|
202 return (numMessages, sizeMessages) |
|
203 |
|
204 |
|
205 def list(self, which=None): |
|
206 """Request listing, return result. |
|
207 |
|
208 Result without a message number argument is in form |
|
209 ['response', ['mesg_num octets', ...], octets]. |
|
210 |
|
211 Result when a message number argument is given is a |
|
212 single response: the "scan listing" for that message. |
|
213 """ |
|
214 if which is not None: |
|
215 return self._shortcmd('LIST %s' % which) |
|
216 return self._longcmd('LIST') |
|
217 |
|
218 |
|
219 def retr(self, which): |
|
220 """Retrieve whole message number 'which'. |
|
221 |
|
222 Result is in form ['response', ['line', ...], octets]. |
|
223 """ |
|
224 return self._longcmd('RETR %s' % which) |
|
225 |
|
226 |
|
227 def dele(self, which): |
|
228 """Delete message number 'which'. |
|
229 |
|
230 Result is 'response'. |
|
231 """ |
|
232 return self._shortcmd('DELE %s' % which) |
|
233 |
|
234 |
|
235 def noop(self): |
|
236 """Does nothing. |
|
237 |
|
238 One supposes the response indicates the server is alive. |
|
239 """ |
|
240 return self._shortcmd('NOOP') |
|
241 |
|
242 |
|
243 def rset(self): |
|
244 """Unmark all messages marked for deletion.""" |
|
245 return self._shortcmd('RSET') |
|
246 |
|
247 |
|
248 def quit(self): |
|
249 """Signoff: commit changes on server, unlock mailbox, close connection.""" |
|
250 try: |
|
251 resp = self._shortcmd('QUIT') |
|
252 except error_proto, val: |
|
253 resp = val |
|
254 self.file.close() |
|
255 self.sock.close() |
|
256 del self.file, self.sock |
|
257 return resp |
|
258 |
|
259 #__del__ = quit |
|
260 |
|
261 |
|
262 # optional commands: |
|
263 |
|
264 def rpop(self, user): |
|
265 """Not sure what this does.""" |
|
266 return self._shortcmd('RPOP %s' % user) |
|
267 |
|
268 |
|
269 timestamp = re.compile(r'\+OK.*(<[^>]+>)') |
|
270 |
|
271 def apop(self, user, secret): |
|
272 """Authorisation |
|
273 |
|
274 - only possible if server has supplied a timestamp in initial greeting. |
|
275 |
|
276 Args: |
|
277 user - mailbox user; |
|
278 secret - secret shared between client and server. |
|
279 |
|
280 NB: mailbox is locked by server from here to 'quit()' |
|
281 """ |
|
282 m = self.timestamp.match(self.welcome) |
|
283 if not m: |
|
284 raise error_proto('-ERR APOP not supported by server') |
|
285 import hashlib |
|
286 digest = hashlib.md5(m.group(1)+secret).digest() |
|
287 digest = ''.join(map(lambda x:'%02x'%ord(x), digest)) |
|
288 return self._shortcmd('APOP %s %s' % (user, digest)) |
|
289 |
|
290 |
|
291 def top(self, which, howmuch): |
|
292 """Retrieve message header of message number 'which' |
|
293 and first 'howmuch' lines of message body. |
|
294 |
|
295 Result is in form ['response', ['line', ...], octets]. |
|
296 """ |
|
297 return self._longcmd('TOP %s %s' % (which, howmuch)) |
|
298 |
|
299 |
|
300 def uidl(self, which=None): |
|
301 """Return message digest (unique id) list. |
|
302 |
|
303 If 'which', result contains unique id for that message |
|
304 in the form 'response mesgnum uid', otherwise result is |
|
305 the list ['response', ['mesgnum uid', ...], octets] |
|
306 """ |
|
307 if which is not None: |
|
308 return self._shortcmd('UIDL %s' % which) |
|
309 return self._longcmd('UIDL') |
|
310 |
|
311 try: |
|
312 import ssl |
|
313 except ImportError: |
|
314 pass |
|
315 else: |
|
316 |
|
317 class POP3_SSL(POP3): |
|
318 """POP3 client class over SSL connection |
|
319 |
|
320 Instantiate with: POP3_SSL(hostname, port=995, keyfile=None, certfile=None) |
|
321 |
|
322 hostname - the hostname of the pop3 over ssl server |
|
323 port - port number |
|
324 keyfile - PEM formatted file that countains your private key |
|
325 certfile - PEM formatted certificate chain file |
|
326 |
|
327 See the methods of the parent class POP3 for more documentation. |
|
328 """ |
|
329 |
|
330 def __init__(self, host, port = POP3_SSL_PORT, keyfile = None, certfile = None): |
|
331 self.host = host |
|
332 self.port = port |
|
333 self.keyfile = keyfile |
|
334 self.certfile = certfile |
|
335 self.buffer = "" |
|
336 msg = "getaddrinfo returns an empty list" |
|
337 self.sock = None |
|
338 for res in socket.getaddrinfo(self.host, self.port, 0, socket.SOCK_STREAM): |
|
339 af, socktype, proto, canonname, sa = res |
|
340 try: |
|
341 self.sock = socket.socket(af, socktype, proto) |
|
342 self.sock.connect(sa) |
|
343 except socket.error, msg: |
|
344 if self.sock: |
|
345 self.sock.close() |
|
346 self.sock = None |
|
347 continue |
|
348 break |
|
349 if not self.sock: |
|
350 raise socket.error, msg |
|
351 self.file = self.sock.makefile('rb') |
|
352 self.sslobj = ssl.wrap_socket(self.sock, self.keyfile, self.certfile) |
|
353 self._debugging = 0 |
|
354 self.welcome = self._getresp() |
|
355 |
|
356 def _fillBuffer(self): |
|
357 localbuf = self.sslobj.read() |
|
358 if len(localbuf) == 0: |
|
359 raise error_proto('-ERR EOF') |
|
360 self.buffer += localbuf |
|
361 |
|
362 def _getline(self): |
|
363 line = "" |
|
364 renewline = re.compile(r'.*?\n') |
|
365 match = renewline.match(self.buffer) |
|
366 while not match: |
|
367 self._fillBuffer() |
|
368 match = renewline.match(self.buffer) |
|
369 line = match.group(0) |
|
370 self.buffer = renewline.sub('' ,self.buffer, 1) |
|
371 if self._debugging > 1: print '*get*', repr(line) |
|
372 |
|
373 octets = len(line) |
|
374 if line[-2:] == CRLF: |
|
375 return line[:-2], octets |
|
376 if line[0] == CR: |
|
377 return line[1:-1], octets |
|
378 return line[:-1], octets |
|
379 |
|
380 def _putline(self, line): |
|
381 if self._debugging > 1: print '*put*', repr(line) |
|
382 line += CRLF |
|
383 bytes = len(line) |
|
384 while bytes > 0: |
|
385 sent = self.sslobj.write(line) |
|
386 if sent == bytes: |
|
387 break # avoid copy |
|
388 line = line[sent:] |
|
389 bytes = bytes - sent |
|
390 |
|
391 def quit(self): |
|
392 """Signoff: commit changes on server, unlock mailbox, close connection.""" |
|
393 try: |
|
394 resp = self._shortcmd('QUIT') |
|
395 except error_proto, val: |
|
396 resp = val |
|
397 self.sock.close() |
|
398 del self.sslobj, self.sock |
|
399 return resp |
|
400 |
|
401 __all__.append("POP3_SSL") |
|
402 |
|
403 if __name__ == "__main__": |
|
404 import sys |
|
405 a = POP3(sys.argv[1]) |
|
406 print a.getwelcome() |
|
407 a.user(sys.argv[2]) |
|
408 a.pass_(sys.argv[3]) |
|
409 a.list() |
|
410 (numMsgs, totalSize) = a.stat() |
|
411 for i in range(1, numMsgs + 1): |
|
412 (header, msg, octets) = a.retr(i) |
|
413 print "Message %d:" % i |
|
414 for line in msg: |
|
415 print ' ' + line |
|
416 print '-----------------------' |
|
417 a.quit() |