| """A POP3 client class. | 
 |  | 
 | Based on the J. Myers POP3 draft, Jan. 96 | 
 | """ | 
 |  | 
 | # Author: David Ascher <david_ascher@brown.edu> | 
 | #         [heavily stealing from nntplib.py] | 
 | # Updated: Piers Lauder <piers@cs.su.oz.au> [Jul '97] | 
 | # String method conversion and test jig improvements by ESR, February 2001. | 
 |  | 
 | # Example (see the test function at the end of this file) | 
 |  | 
 | # Imports | 
 |  | 
 | import re, socket | 
 |  | 
 | __all__ = ["POP3","error_proto"] | 
 |  | 
 | # Exception raised when an error or invalid response is received: | 
 |  | 
 | class error_proto(Exception): pass | 
 |  | 
 | # Standard Port | 
 | POP3_PORT = 110 | 
 |  | 
 | # Line terminators (we always output CRLF, but accept any of CRLF, LFCR, LF) | 
 | CR = '\r' | 
 | LF = '\n' | 
 | CRLF = CR+LF | 
 |  | 
 |  | 
 | class POP3: | 
 |  | 
 |     """This class supports both the minimal and optional command sets. | 
 |     Arguments can be strings or integers (where appropriate) | 
 |     (e.g.: retr(1) and retr('1') both work equally well. | 
 |  | 
 |     Minimal Command Set: | 
 |             USER name               user(name) | 
 |             PASS string             pass_(string) | 
 |             STAT                    stat() | 
 |             LIST [msg]              list(msg = None) | 
 |             RETR msg                retr(msg) | 
 |             DELE msg                dele(msg) | 
 |             NOOP                    noop() | 
 |             RSET                    rset() | 
 |             QUIT                    quit() | 
 |  | 
 |     Optional Commands (some servers support these): | 
 |             RPOP name               rpop(name) | 
 |             APOP name digest        apop(name, digest) | 
 |             TOP msg n               top(msg, n) | 
 |             UIDL [msg]              uidl(msg = None) | 
 |  | 
 |     Raises one exception: 'error_proto'. | 
 |  | 
 |     Instantiate with: | 
 |             POP3(hostname, port=110) | 
 |  | 
 |     NB:     the POP protocol locks the mailbox from user | 
 |             authorization until QUIT, so be sure to get in, suck | 
 |             the messages, and quit, each time you access the | 
 |             mailbox. | 
 |  | 
 |             POP is a line-based protocol, which means large mail | 
 |             messages consume lots of python cycles reading them | 
 |             line-by-line. | 
 |  | 
 |             If it's available on your mail server, use IMAP4 | 
 |             instead, it doesn't suffer from the two problems | 
 |             above. | 
 |     """ | 
 |  | 
 |  | 
 |     def __init__(self, host, port = POP3_PORT): | 
 |         self.host = host | 
 |         self.port = port | 
 |         msg = "getaddrinfo returns an empty list" | 
 |         for res in socket.getaddrinfo(self.host, self.port, 0, socket.SOCK_STREAM): | 
 |             af, socktype, proto, canonname, sa = res | 
 |             try: | 
 |                 self.sock = socket.socket(af, socktype, proto) | 
 |                 self.sock.connect(sa) | 
 |             except socket.error, msg: | 
 |                 self.sock.close() | 
 |                 self.sock = None | 
 |                 continue | 
 |             break | 
 |         if not self.sock: | 
 |             raise socket.error, msg | 
 |         self.file = self.sock.makefile('rb') | 
 |         self._debugging = 0 | 
 |         self.welcome = self._getresp() | 
 |  | 
 |  | 
 |     def _putline(self, line): | 
 |         #if self._debugging > 1: print '*put*', `line` | 
 |         self.sock.send('%s%s' % (line, CRLF)) | 
 |  | 
 |  | 
 |     # Internal: send one command to the server (through _putline()) | 
 |  | 
 |     def _putcmd(self, line): | 
 |         #if self._debugging: print '*cmd*', `line` | 
 |         self._putline(line) | 
 |  | 
 |  | 
 |     # Internal: return one line from the server, stripping CRLF. | 
 |     # This is where all the CPU time of this module is consumed. | 
 |     # Raise error_proto('-ERR EOF') if the connection is closed. | 
 |  | 
 |     def _getline(self): | 
 |         line = self.file.readline() | 
 |         #if self._debugging > 1: print '*get*', `line` | 
 |         if not line: raise error_proto('-ERR EOF') | 
 |         octets = len(line) | 
 |         # server can send any combination of CR & LF | 
 |         # however, 'readline()' returns lines ending in LF | 
 |         # so only possibilities are ...LF, ...CRLF, CR...LF | 
 |         if line[-2:] == CRLF: | 
 |             return line[:-2], octets | 
 |         if line[0] == CR: | 
 |             return line[1:-1], octets | 
 |         return line[:-1], octets | 
 |  | 
 |  | 
 |     # Internal: get a response from the server. | 
 |     # Raise 'error_proto' if the response doesn't start with '+'. | 
 |  | 
 |     def _getresp(self): | 
 |         resp, o = self._getline() | 
 |         #if self._debugging > 1: print '*resp*', `resp` | 
 |         c = resp[:1] | 
 |         if c != '+': | 
 |             raise error_proto(resp) | 
 |         return resp | 
 |  | 
 |  | 
 |     # Internal: get a response plus following text from the server. | 
 |  | 
 |     def _getlongresp(self): | 
 |         resp = self._getresp() | 
 |         list = []; octets = 0 | 
 |         line, o = self._getline() | 
 |         while line != '.': | 
 |             if line[:2] == '..': | 
 |                 o = o-1 | 
 |                 line = line[1:] | 
 |             octets = octets + o | 
 |             list.append(line) | 
 |             line, o = self._getline() | 
 |         return resp, list, octets | 
 |  | 
 |  | 
 |     # Internal: send a command and get the response | 
 |  | 
 |     def _shortcmd(self, line): | 
 |         self._putcmd(line) | 
 |         return self._getresp() | 
 |  | 
 |  | 
 |     # Internal: send a command and get the response plus following text | 
 |  | 
 |     def _longcmd(self, line): | 
 |         self._putcmd(line) | 
 |         return self._getlongresp() | 
 |  | 
 |  | 
 |     # These can be useful: | 
 |  | 
 |     def getwelcome(self): | 
 |         return self.welcome | 
 |  | 
 |  | 
 |     def set_debuglevel(self, level): | 
 |         self._debugging = level | 
 |  | 
 |  | 
 |     # Here are all the POP commands: | 
 |  | 
 |     def user(self, user): | 
 |         """Send user name, return response | 
 |  | 
 |         (should indicate password required). | 
 |         """ | 
 |         return self._shortcmd('USER %s' % user) | 
 |  | 
 |  | 
 |     def pass_(self, pswd): | 
 |         """Send password, return response | 
 |  | 
 |         (response includes message count, mailbox size). | 
 |  | 
 |         NB: mailbox is locked by server from here to 'quit()' | 
 |         """ | 
 |         return self._shortcmd('PASS %s' % pswd) | 
 |  | 
 |  | 
 |     def stat(self): | 
 |         """Get mailbox status. | 
 |  | 
 |         Result is tuple of 2 ints (message count, mailbox size) | 
 |         """ | 
 |         retval = self._shortcmd('STAT') | 
 |         rets = retval.split() | 
 |         #if self._debugging: print '*stat*', `rets` | 
 |         numMessages = int(rets[1]) | 
 |         sizeMessages = int(rets[2]) | 
 |         return (numMessages, sizeMessages) | 
 |  | 
 |  | 
 |     def list(self, which=None): | 
 |         """Request listing, return result. | 
 |  | 
 |         Result without a message number argument is in form | 
 |         ['response', ['mesg_num octets', ...]]. | 
 |  | 
 |         Result when a message number argument is given is a | 
 |         single response: the "scan listing" for that message. | 
 |         """ | 
 |         if which: | 
 |             return self._shortcmd('LIST %s' % which) | 
 |         return self._longcmd('LIST') | 
 |  | 
 |  | 
 |     def retr(self, which): | 
 |         """Retrieve whole message number 'which'. | 
 |  | 
 |         Result is in form ['response', ['line', ...], octets]. | 
 |         """ | 
 |         return self._longcmd('RETR %s' % which) | 
 |  | 
 |  | 
 |     def dele(self, which): | 
 |         """Delete message number 'which'. | 
 |  | 
 |         Result is 'response'. | 
 |         """ | 
 |         return self._shortcmd('DELE %s' % which) | 
 |  | 
 |  | 
 |     def noop(self): | 
 |         """Does nothing. | 
 |  | 
 |         One supposes the response indicates the server is alive. | 
 |         """ | 
 |         return self._shortcmd('NOOP') | 
 |  | 
 |  | 
 |     def rset(self): | 
 |         """Not sure what this does.""" | 
 |         return self._shortcmd('RSET') | 
 |  | 
 |  | 
 |     def quit(self): | 
 |         """Signoff: commit changes on server, unlock mailbox, close connection.""" | 
 |         try: | 
 |             resp = self._shortcmd('QUIT') | 
 |         except error_proto, val: | 
 |             resp = val | 
 |         self.file.close() | 
 |         self.sock.close() | 
 |         del self.file, self.sock | 
 |         return resp | 
 |  | 
 |     #__del__ = quit | 
 |  | 
 |  | 
 |     # optional commands: | 
 |  | 
 |     def rpop(self, user): | 
 |         """Not sure what this does.""" | 
 |         return self._shortcmd('RPOP %s' % user) | 
 |  | 
 |  | 
 |     timestamp = re.compile(r'\+OK.*(<[^>]+>)') | 
 |  | 
 |     def apop(self, user, secret): | 
 |         """Authorisation | 
 |  | 
 |         - only possible if server has supplied a timestamp in initial greeting. | 
 |  | 
 |         Args: | 
 |                 user    - mailbox user; | 
 |                 secret  - secret shared between client and server. | 
 |  | 
 |         NB: mailbox is locked by server from here to 'quit()' | 
 |         """ | 
 |         m = self.timestamp.match(self.welcome) | 
 |         if not m: | 
 |             raise error_proto('-ERR APOP not supported by server') | 
 |         import md5 | 
 |         digest = md5.new(m.group(1)+secret).digest() | 
 |         digest = ''.join(map(lambda x:'%02x'%ord(x), digest)) | 
 |         return self._shortcmd('APOP %s %s' % (user, digest)) | 
 |  | 
 |  | 
 |     def top(self, which, howmuch): | 
 |         """Retrieve message header of message number 'which' | 
 |         and first 'howmuch' lines of message body. | 
 |  | 
 |         Result is in form ['response', ['line', ...], octets]. | 
 |         """ | 
 |         return self._longcmd('TOP %s %s' % (which, howmuch)) | 
 |  | 
 |  | 
 |     def uidl(self, which=None): | 
 |         """Return message digest (unique id) list. | 
 |  | 
 |         If 'which', result contains unique id for that message | 
 |         in the form 'response mesgnum uid', otherwise result is | 
 |         the list ['response', ['mesgnum uid', ...], octets] | 
 |         """ | 
 |         if which: | 
 |             return self._shortcmd('UIDL %s' % which) | 
 |         return self._longcmd('UIDL') | 
 |  | 
 |  | 
 | if __name__ == "__main__": | 
 |     import sys | 
 |     a = POP3(sys.argv[1]) | 
 |     print a.getwelcome() | 
 |     a.user(sys.argv[2]) | 
 |     a.pass_(sys.argv[3]) | 
 |     a.list() | 
 |     (numMsgs, totalSize) = a.stat() | 
 |     for i in range(1, numMsgs + 1): | 
 |         (header, msg, octets) = a.retr(i) | 
 |         print "Message ", `i`, ':' | 
 |         for line in msg: | 
 |             print '   ' + line | 
 |         print '-----------------------' | 
 |     a.quit() |