Commit | Line | Data |
---|---|---|
920dae64 AT |
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 md5 | |
299 | digest = md5.new(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() |