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 re, socket
     17 
     18 __all__ = ["POP3","error_proto"]
     19 
     20 # Exception raised when an error or invalid response is received:

     21 
     22 class error_proto(Exception): pass
     23 
     24 # Standard Port

     25 POP3_PORT = 110
     26 
     27 # POP SSL PORT

     28 POP3_SSL_PORT = 995
     29 
     30 # Line terminators (we always output CRLF, but accept any of CRLF, LFCR, LF)

     31 CR = '\r'
     32 LF = '\n'
     33 CRLF = CR+LF
     34 
     35 
     36 class 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 
    311 try:
    312     import ssl
    313 except ImportError:
    314     pass
    315 else:
    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 
    403 if __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