]> git.lizzy.rs Git - plan9front.git/blob - sys/lib/python/poplib.py
dist/mkfile: run binds in subshell
[plan9front.git] / sys / lib / python / poplib.py
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()