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