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 errno
17import re
18import socket
19import sys
20
21try:
22    import ssl
23    HAVE_SSL = True
24except ImportError:
25    HAVE_SSL = False
26
27__all__ = ["POP3","error_proto"]
28
29# Exception raised when an error or invalid response is received:
30
31class error_proto(Exception): pass
32
33# Standard Port
34POP3_PORT = 110
35
36# POP SSL PORT
37POP3_SSL_PORT = 995
38
39# Line terminators (we always output CRLF, but accept any of CRLF, LFCR, LF)
40CR = b'\r'
41LF = b'\n'
42CRLF = CR+LF
43
44# maximal line length when calling readline(). This is to prevent
45# reading arbitrary length lines. RFC 1939 limits POP3 line length to
46# 512 characters, including CRLF. We have selected 2048 just to be on
47# the safe side.
48_MAXLINE = 2048
49
50
51class POP3:
52
53    """This class supports both the minimal and optional command sets.
54    Arguments can be strings or integers (where appropriate)
55    (e.g.: retr(1) and retr('1') both work equally well.
56
57    Minimal Command Set:
58            USER name               user(name)
59            PASS string             pass_(string)
60            STAT                    stat()
61            LIST [msg]              list(msg = None)
62            RETR msg                retr(msg)
63            DELE msg                dele(msg)
64            NOOP                    noop()
65            RSET                    rset()
66            QUIT                    quit()
67
68    Optional Commands (some servers support these):
69            RPOP name               rpop(name)
70            APOP name digest        apop(name, digest)
71            TOP msg n               top(msg, n)
72            UIDL [msg]              uidl(msg = None)
73            CAPA                    capa()
74            STLS                    stls()
75            UTF8                    utf8()
76
77    Raises one exception: 'error_proto'.
78
79    Instantiate with:
80            POP3(hostname, port=110)
81
82    NB:     the POP protocol locks the mailbox from user
83            authorization until QUIT, so be sure to get in, suck
84            the messages, and quit, each time you access the
85            mailbox.
86
87            POP is a line-based protocol, which means large mail
88            messages consume lots of python cycles reading them
89            line-by-line.
90
91            If it's available on your mail server, use IMAP4
92            instead, it doesn't suffer from the two problems
93            above.
94    """
95
96    encoding = 'UTF-8'
97
98    def __init__(self, host, port=POP3_PORT,
99                 timeout=socket._GLOBAL_DEFAULT_TIMEOUT):
100        self.host = host
101        self.port = port
102        self._tls_established = False
103        sys.audit("poplib.connect", self, host, port)
104        self.sock = self._create_socket(timeout)
105        self.file = self.sock.makefile('rb')
106        self._debugging = 0
107        self.welcome = self._getresp()
108
109    def _create_socket(self, timeout):
110        return socket.create_connection((self.host, self.port), timeout)
111
112    def _putline(self, line):
113        if self._debugging > 1: print('*put*', repr(line))
114        sys.audit("poplib.putline", self, line)
115        self.sock.sendall(line + CRLF)
116
117
118    # Internal: send one command to the server (through _putline())
119
120    def _putcmd(self, line):
121        if self._debugging: print('*cmd*', repr(line))
122        line = bytes(line, self.encoding)
123        self._putline(line)
124
125
126    # Internal: return one line from the server, stripping CRLF.
127    # This is where all the CPU time of this module is consumed.
128    # Raise error_proto('-ERR EOF') if the connection is closed.
129
130    def _getline(self):
131        line = self.file.readline(_MAXLINE + 1)
132        if len(line) > _MAXLINE:
133            raise error_proto('line too long')
134
135        if self._debugging > 1: print('*get*', repr(line))
136        if not line: raise error_proto('-ERR EOF')
137        octets = len(line)
138        # server can send any combination of CR & LF
139        # however, 'readline()' returns lines ending in LF
140        # so only possibilities are ...LF, ...CRLF, CR...LF
141        if line[-2:] == CRLF:
142            return line[:-2], octets
143        if line[:1] == CR:
144            return line[1:-1], octets
145        return line[:-1], octets
146
147
148    # Internal: get a response from the server.
149    # Raise 'error_proto' if the response doesn't start with '+'.
150
151    def _getresp(self):
152        resp, o = self._getline()
153        if self._debugging > 1: print('*resp*', repr(resp))
154        if not resp.startswith(b'+'):
155            raise error_proto(resp)
156        return resp
157
158
159    # Internal: get a response plus following text from the server.
160
161    def _getlongresp(self):
162        resp = self._getresp()
163        list = []; octets = 0
164        line, o = self._getline()
165        while line != b'.':
166            if line.startswith(b'..'):
167                o = o-1
168                line = line[1:]
169            octets = octets + o
170            list.append(line)
171            line, o = self._getline()
172        return resp, list, octets
173
174
175    # Internal: send a command and get the response
176
177    def _shortcmd(self, line):
178        self._putcmd(line)
179        return self._getresp()
180
181
182    # Internal: send a command and get the response plus following text
183
184    def _longcmd(self, line):
185        self._putcmd(line)
186        return self._getlongresp()
187
188
189    # These can be useful:
190
191    def getwelcome(self):
192        return self.welcome
193
194
195    def set_debuglevel(self, level):
196        self._debugging = level
197
198
199    # Here are all the POP commands:
200
201    def user(self, user):
202        """Send user name, return response
203
204        (should indicate password required).
205        """
206        return self._shortcmd('USER %s' % user)
207
208
209    def pass_(self, pswd):
210        """Send password, return response
211
212        (response includes message count, mailbox size).
213
214        NB: mailbox is locked by server from here to 'quit()'
215        """
216        return self._shortcmd('PASS %s' % pswd)
217
218
219    def stat(self):
220        """Get mailbox status.
221
222        Result is tuple of 2 ints (message count, mailbox size)
223        """
224        retval = self._shortcmd('STAT')
225        rets = retval.split()
226        if self._debugging: print('*stat*', repr(rets))
227        numMessages = int(rets[1])
228        sizeMessages = int(rets[2])
229        return (numMessages, sizeMessages)
230
231
232    def list(self, which=None):
233        """Request listing, return result.
234
235        Result without a message number argument is in form
236        ['response', ['mesg_num octets', ...], octets].
237
238        Result when a message number argument is given is a
239        single response: the "scan listing" for that message.
240        """
241        if which is not None:
242            return self._shortcmd('LIST %s' % which)
243        return self._longcmd('LIST')
244
245
246    def retr(self, which):
247        """Retrieve whole message number 'which'.
248
249        Result is in form ['response', ['line', ...], octets].
250        """
251        return self._longcmd('RETR %s' % which)
252
253
254    def dele(self, which):
255        """Delete message number 'which'.
256
257        Result is 'response'.
258        """
259        return self._shortcmd('DELE %s' % which)
260
261
262    def noop(self):
263        """Does nothing.
264
265        One supposes the response indicates the server is alive.
266        """
267        return self._shortcmd('NOOP')
268
269
270    def rset(self):
271        """Unmark all messages marked for deletion."""
272        return self._shortcmd('RSET')
273
274
275    def quit(self):
276        """Signoff: commit changes on server, unlock mailbox, close connection."""
277        resp = self._shortcmd('QUIT')
278        self.close()
279        return resp
280
281    def close(self):
282        """Close the connection without assuming anything about it."""
283        try:
284            file = self.file
285            self.file = None
286            if file is not None:
287                file.close()
288        finally:
289            sock = self.sock
290            self.sock = None
291            if sock is not None:
292                try:
293                    sock.shutdown(socket.SHUT_RDWR)
294                except OSError as exc:
295                    # The server might already have closed the connection.
296                    # On Windows, this may result in WSAEINVAL (error 10022):
297                    # An invalid operation was attempted.
298                    if (exc.errno != errno.ENOTCONN
299                       and getattr(exc, 'winerror', 0) != 10022):
300                        raise
301                finally:
302                    sock.close()
303
304    #__del__ = quit
305
306
307    # optional commands:
308
309    def rpop(self, user):
310        """Not sure what this does."""
311        return self._shortcmd('RPOP %s' % user)
312
313
314    timestamp = re.compile(br'\+OK.[^<]*(<.*>)')
315
316    def apop(self, user, password):
317        """Authorisation
318
319        - only possible if server has supplied a timestamp in initial greeting.
320
321        Args:
322                user     - mailbox user;
323                password - mailbox password.
324
325        NB: mailbox is locked by server from here to 'quit()'
326        """
327        secret = bytes(password, self.encoding)
328        m = self.timestamp.match(self.welcome)
329        if not m:
330            raise error_proto('-ERR APOP not supported by server')
331        import hashlib
332        digest = m.group(1)+secret
333        digest = hashlib.md5(digest).hexdigest()
334        return self._shortcmd('APOP %s %s' % (user, digest))
335
336
337    def top(self, which, howmuch):
338        """Retrieve message header of message number 'which'
339        and first 'howmuch' lines of message body.
340
341        Result is in form ['response', ['line', ...], octets].
342        """
343        return self._longcmd('TOP %s %s' % (which, howmuch))
344
345
346    def uidl(self, which=None):
347        """Return message digest (unique id) list.
348
349        If 'which', result contains unique id for that message
350        in the form 'response mesgnum uid', otherwise result is
351        the list ['response', ['mesgnum uid', ...], octets]
352        """
353        if which is not None:
354            return self._shortcmd('UIDL %s' % which)
355        return self._longcmd('UIDL')
356
357
358    def utf8(self):
359        """Try to enter UTF-8 mode (see RFC 6856). Returns server response.
360        """
361        return self._shortcmd('UTF8')
362
363
364    def capa(self):
365        """Return server capabilities (RFC 2449) as a dictionary
366        >>> c=poplib.POP3('localhost')
367        >>> c.capa()
368        {'IMPLEMENTATION': ['Cyrus', 'POP3', 'server', 'v2.2.12'],
369         'TOP': [], 'LOGIN-DELAY': ['0'], 'AUTH-RESP-CODE': [],
370         'EXPIRE': ['NEVER'], 'USER': [], 'STLS': [], 'PIPELINING': [],
371         'UIDL': [], 'RESP-CODES': []}
372        >>>
373
374        Really, according to RFC 2449, the cyrus folks should avoid
375        having the implementation split into multiple arguments...
376        """
377        def _parsecap(line):
378            lst = line.decode('ascii').split()
379            return lst[0], lst[1:]
380
381        caps = {}
382        try:
383            resp = self._longcmd('CAPA')
384            rawcaps = resp[1]
385            for capline in rawcaps:
386                capnm, capargs = _parsecap(capline)
387                caps[capnm] = capargs
388        except error_proto as _err:
389            raise error_proto('-ERR CAPA not supported by server')
390        return caps
391
392
393    def stls(self, context=None):
394        """Start a TLS session on the active connection as specified in RFC 2595.
395
396                context - a ssl.SSLContext
397        """
398        if not HAVE_SSL:
399            raise error_proto('-ERR TLS support missing')
400        if self._tls_established:
401            raise error_proto('-ERR TLS session already established')
402        caps = self.capa()
403        if not 'STLS' in caps:
404            raise error_proto('-ERR STLS not supported by server')
405        if context is None:
406            context = ssl._create_stdlib_context()
407        resp = self._shortcmd('STLS')
408        self.sock = context.wrap_socket(self.sock,
409                                        server_hostname=self.host)
410        self.file = self.sock.makefile('rb')
411        self._tls_established = True
412        return resp
413
414
415if HAVE_SSL:
416
417    class POP3_SSL(POP3):
418        """POP3 client class over SSL connection
419
420        Instantiate with: POP3_SSL(hostname, port=995, keyfile=None, certfile=None,
421                                   context=None)
422
423               hostname - the hostname of the pop3 over ssl server
424               port - port number
425               keyfile - PEM formatted file that contains your private key
426               certfile - PEM formatted certificate chain file
427               context - a ssl.SSLContext
428
429        See the methods of the parent class POP3 for more documentation.
430        """
431
432        def __init__(self, host, port=POP3_SSL_PORT, keyfile=None, certfile=None,
433                     timeout=socket._GLOBAL_DEFAULT_TIMEOUT, context=None):
434            if context is not None and keyfile is not None:
435                raise ValueError("context and keyfile arguments are mutually "
436                                 "exclusive")
437            if context is not None and certfile is not None:
438                raise ValueError("context and certfile arguments are mutually "
439                                 "exclusive")
440            if keyfile is not None or certfile is not None:
441                import warnings
442                warnings.warn("keyfile and certfile are deprecated, use a "
443                              "custom context instead", DeprecationWarning, 2)
444            self.keyfile = keyfile
445            self.certfile = certfile
446            if context is None:
447                context = ssl._create_stdlib_context(certfile=certfile,
448                                                     keyfile=keyfile)
449            self.context = context
450            POP3.__init__(self, host, port, timeout)
451
452        def _create_socket(self, timeout):
453            sock = POP3._create_socket(self, timeout)
454            sock = self.context.wrap_socket(sock,
455                                            server_hostname=self.host)
456            return sock
457
458        def stls(self, keyfile=None, certfile=None, context=None):
459            """The method unconditionally raises an exception since the
460            STLS command doesn't make any sense on an already established
461            SSL/TLS session.
462            """
463            raise error_proto('-ERR TLS session already established')
464
465    __all__.append("POP3_SSL")
466
467if __name__ == "__main__":
468    import sys
469    a = POP3(sys.argv[1])
470    print(a.getwelcome())
471    a.user(sys.argv[2])
472    a.pass_(sys.argv[3])
473    a.list()
474    (numMsgs, totalSize) = a.stat()
475    for i in range(1, numMsgs + 1):
476        (header, msg, octets) = a.retr(i)
477        print("Message %d:" % i)
478        for line in msg:
479            print('   ' + line)
480        print('-----------------------')
481    a.quit()
482