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