Home | History | Annotate | Download | only in Lib
      1 """A POP3 client class.
      2 
      3 Based on the J. Myers POP3 draft, Jan. 96
      4 """
      5 
      6 # Author: David Ascher <david_ascher (at] brown.edu>
      7 #         [heavily stealing from nntplib.py]
      8 # Updated: Piers Lauder <piers (at] 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 (at] mrbook.org> Aug 2003
     11 
     12 # Example (see the test function at the end of this file)
     13 
     14 # Imports
     15 
     16 import errno
     17 import re
     18 import socket
     19 
     20 try:
     21     import ssl
     22     HAVE_SSL = True
     23 except ImportError:
     24     HAVE_SSL = False
     25 
     26 __all__ = ["POP3","error_proto"]
     27 
     28 # Exception raised when an error or invalid response is received:
     29 
     30 class error_proto(Exception): pass
     31 
     32 # Standard Port
     33 POP3_PORT = 110
     34 
     35 # POP SSL PORT
     36 POP3_SSL_PORT = 995
     37 
     38 # Line terminators (we always output CRLF, but accept any of CRLF, LFCR, LF)
     39 CR = b'\r'
     40 LF = b'\n'
     41 CRLF = 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 
     50 class 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 
    412 if 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 
    464 if __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