| """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. |
| # Added the POP3_SSL class. Methods loosely based on IMAP_SSL. Hector Urtubia <urtubia@mrbook.org> Aug 2003 |
| |
| # 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 |
| |
| # POP SSL PORT |
| POP3_SSL_PORT = 995 |
| |
| # 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, |
| timeout=socket._GLOBAL_DEFAULT_TIMEOUT): |
| self.host = host |
| self.port = port |
| self.sock = socket.create_connection((host, port), timeout) |
| self.file = self.sock.makefile('rb') |
| self._debugging = 0 |
| self.welcome = self._getresp() |
| |
| |
| def _putline(self, line): |
| if self._debugging > 1: print '*put*', repr(line) |
| self.sock.sendall('%s%s' % (line, CRLF)) |
| |
| |
| # Internal: send one command to the server (through _putline()) |
| |
| def _putcmd(self, line): |
| if self._debugging: print '*cmd*', repr(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*', repr(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*', repr(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*', repr(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', ...], octets]. |
| |
| Result when a message number argument is given is a |
| single response: the "scan listing" for that message. |
| """ |
| if which is not None: |
| 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): |
| """Unmark all messages marked for deletion.""" |
| 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 hashlib |
| digest = hashlib.md5(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 is not None: |
| return self._shortcmd('UIDL %s' % which) |
| return self._longcmd('UIDL') |
| |
| try: |
| import ssl |
| except ImportError: |
| pass |
| else: |
| |
| class POP3_SSL(POP3): |
| """POP3 client class over SSL connection |
| |
| Instantiate with: POP3_SSL(hostname, port=995, keyfile=None, certfile=None) |
| |
| hostname - the hostname of the pop3 over ssl server |
| port - port number |
| keyfile - PEM formatted file that countains your private key |
| certfile - PEM formatted certificate chain file |
| |
| See the methods of the parent class POP3 for more documentation. |
| """ |
| |
| def __init__(self, host, port = POP3_SSL_PORT, keyfile = None, certfile = None): |
| self.host = host |
| self.port = port |
| self.keyfile = keyfile |
| self.certfile = certfile |
| self.buffer = "" |
| msg = "getaddrinfo returns an empty list" |
| self.sock = None |
| 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: |
| if self.sock: |
| self.sock.close() |
| self.sock = None |
| continue |
| break |
| if not self.sock: |
| raise socket.error, msg |
| self.file = self.sock.makefile('rb') |
| self.sslobj = ssl.wrap_socket(self.sock, self.keyfile, self.certfile) |
| self._debugging = 0 |
| self.welcome = self._getresp() |
| |
| def _fillBuffer(self): |
| localbuf = self.sslobj.read() |
| if len(localbuf) == 0: |
| raise error_proto('-ERR EOF') |
| self.buffer += localbuf |
| |
| def _getline(self): |
| line = "" |
| renewline = re.compile(r'.*?\n') |
| match = renewline.match(self.buffer) |
| while not match: |
| self._fillBuffer() |
| match = renewline.match(self.buffer) |
| line = match.group(0) |
| self.buffer = renewline.sub('' ,self.buffer, 1) |
| if self._debugging > 1: print '*get*', repr(line) |
| |
| octets = len(line) |
| if line[-2:] == CRLF: |
| return line[:-2], octets |
| if line[0] == CR: |
| return line[1:-1], octets |
| return line[:-1], octets |
| |
| def _putline(self, line): |
| if self._debugging > 1: print '*put*', repr(line) |
| line += CRLF |
| bytes = len(line) |
| while bytes > 0: |
| sent = self.sslobj.write(line) |
| if sent == bytes: |
| break # avoid copy |
| line = line[sent:] |
| bytes = bytes - sent |
| |
| def quit(self): |
| """Signoff: commit changes on server, unlock mailbox, close connection.""" |
| try: |
| resp = self._shortcmd('QUIT') |
| except error_proto, val: |
| resp = val |
| self.sock.close() |
| del self.sslobj, self.sock |
| return resp |
| |
| __all__.append("POP3_SSL") |
| |
| 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 %d:" % i |
| for line in msg: |
| print ' ' + line |
| print '-----------------------' |
| a.quit() |