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
36class 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
311try:
312    import ssl
313except ImportError:
314    pass
315else:
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
403if __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()
418