codekingpro/portable-devtools
114k
1"""A POP3 client class.2 3Based on the J. Myers POP3 draft, Jan. 964"""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 200311 12# Example (see the test function at the end of this file)13 14# Imports15 16import errno17import re18import socket19import sys20 21try:22 import ssl23 HAVE_SSL = True24except ImportError:25 HAVE_SSL = False26 27__all__ = ["POP3","error_proto"]28 29# Exception raised when an error or invalid response is received:30 31class error_proto(Exception): pass32 33# Standard Port34POP3_PORT = 11035 36# POP SSL PORT37POP3_SSL_PORT = 99538 39# Line terminators (we always output CRLF, but accept any of CRLF, LFCR, LF)40CR = b'\r'41LF = b'\n'42CRLF = CR+LF43 44# maximal line length when calling readline(). This is to prevent45# reading arbitrary length lines. RFC 1939 limits POP3 line length to46# 512 characters, including CRLF. We have selected 2048 just to be on47# the safe side.48_MAXLINE = 204849 50 51class POP3:52 53 """This class supports both the minimal and optional command sets.54 Arguments can be strings or integers (where appropriate)55 (e.g.: retr(1) and retr('1') both work equally well.56 57 Minimal Command Set:58 USER name user(name)59 PASS string pass_(string)60 STAT stat()61 LIST [msg] list(msg = None)62 RETR msg retr(msg)63 DELE msg dele(msg)64 NOOP noop()65 RSET rset()66 QUIT quit()67 68 Optional Commands (some servers support these):69 RPOP name rpop(name)70 APOP name digest apop(name, digest)71 TOP msg n top(msg, n)72 UIDL [msg] uidl(msg = None)73 CAPA capa()74 STLS stls()75 UTF8 utf8()76 77 Raises one exception: 'error_proto'.78 79 Instantiate with:80 POP3(hostname, port=110)81 82 NB: the POP protocol locks the mailbox from user83 authorization until QUIT, so be sure to get in, suck84 the messages, and quit, each time you access the85 mailbox.86 87 POP is a line-based protocol, which means large mail88 messages consume lots of python cycles reading them89 line-by-line.90 91 If it's available on your mail server, use IMAP492 instead, it doesn't suffer from the two problems93 above.94 """95 96 encoding = 'UTF-8'97 98 def __init__(self, host, port=POP3_PORT,99 timeout=socket._GLOBAL_DEFAULT_TIMEOUT):100 self.host = host101 self.port = port102 self._tls_established = False103 sys.audit("poplib.connect", self, host, port)104 self.sock = self._create_socket(timeout)105 self.file = self.sock.makefile('rb')106 self._debugging = 0107 self.welcome = self._getresp()108 109 def _create_socket(self, timeout):110 if timeout is not None and not timeout:111 raise ValueError('Non-blocking socket (timeout=0) is not supported')112 return socket.create_connection((self.host, self.port), timeout)113 114 def _putline(self, line):115 if self._debugging > 1: print('*put*', repr(line))116 sys.audit("poplib.putline", self, line)117 self.sock.sendall(line + CRLF)118 119 120 # Internal: send one command to the server (through _putline())121 122 def _putcmd(self, line):123 if self._debugging: print('*cmd*', repr(line))124 line = bytes(line, self.encoding)125 self._putline(line)126 127 128 # Internal: return one line from the server, stripping CRLF.129 # This is where all the CPU time of this module is consumed.130 # Raise error_proto('-ERR EOF') if the connection is closed.131 132 def _getline(self):133 line = self.file.readline(_MAXLINE + 1)134 if len(line) > _MAXLINE:135 raise error_proto('line too long')136 137 if self._debugging > 1: print('*get*', repr(line))138 if not line: raise error_proto('-ERR EOF')139 octets = len(line)140 # server can send any combination of CR & LF141 # however, 'readline()' returns lines ending in LF142 # so only possibilities are ...LF, ...CRLF, CR...LF143 if line[-2:] == CRLF:144 return line[:-2], octets145 if line[:1] == CR:146 return line[1:-1], octets147 return line[:-1], octets148 149 150 # Internal: get a response from the server.151 # Raise 'error_proto' if the response doesn't start with '+'.152 153 def _getresp(self):154 resp, o = self._getline()155 if self._debugging > 1: print('*resp*', repr(resp))156 if not resp.startswith(b'+'):157 raise error_proto(resp)158 return resp159 160 161 # Internal: get a response plus following text from the server.162 163 def _getlongresp(self):164 resp = self._getresp()165 list = []; octets = 0166 line, o = self._getline()167 while line != b'.':168 if line.startswith(b'..'):169 o = o-1170 line = line[1:]171 octets = octets + o172 list.append(line)173 line, o = self._getline()174 return resp, list, octets175 176 177 # Internal: send a command and get the response178 179 def _shortcmd(self, line):180 self._putcmd(line)181 return self._getresp()182 183 184 # Internal: send a command and get the response plus following text185 186 def _longcmd(self, line):187 self._putcmd(line)188 return self._getlongresp()189 190 191 # These can be useful:192 193 def getwelcome(self):194 return self.welcome195 196 197 def set_debuglevel(self, level):198 self._debugging = level199 200 201 # Here are all the POP commands:202 203 def user(self, user):204 """Send user name, return response205 206 (should indicate password required).207 """208 return self._shortcmd('USER %s' % user)209 210 211 def pass_(self, pswd):212 """Send password, return response213 214 (response includes message count, mailbox size).215 216 NB: mailbox is locked by server from here to 'quit()'217 """218 return self._shortcmd('PASS %s' % pswd)219 220 221 def stat(self):222 """Get mailbox status.223 224 Result is tuple of 2 ints (message count, mailbox size)225 """226 retval = self._shortcmd('STAT')227 rets = retval.split()228 if self._debugging: print('*stat*', repr(rets))229 230 # Check if the response has enough elements231 # RFC 1939 requires at least 3 elements (+OK, message count, mailbox size)232 # but allows additional data after the required fields233 if len(rets) < 3:234 raise error_proto("Invalid STAT response format")235 236 try:237 numMessages = int(rets[1])238 sizeMessages = int(rets[2])239 except ValueError:240 raise error_proto("Invalid STAT response data: non-numeric values")241 242 return (numMessages, sizeMessages)243 244 245 def list(self, which=None):246 """Request listing, return result.247 248 Result without a message number argument is in form249 ['response', ['mesg_num octets', ...], octets].250 251 Result when a message number argument is given is a252 single response: the "scan listing" for that message.253 """254 if which is not None:255 return self._shortcmd('LIST %s' % which)256 return self._longcmd('LIST')257 258 259 def retr(self, which):260 """Retrieve whole message number 'which'.261 262 Result is in form ['response', ['line', ...], octets].263 """264 return self._longcmd('RETR %s' % which)265 266 267 def dele(self, which):268 """Delete message number 'which'.269 270 Result is 'response'.271 """272 return self._shortcmd('DELE %s' % which)273 274 275 def noop(self):276 """Does nothing.277 278 One supposes the response indicates the server is alive.279 """280 return self._shortcmd('NOOP')281 282 283 def rset(self):284 """Unmark all messages marked for deletion."""285 return self._shortcmd('RSET')286 287 288 def quit(self):289 """Signoff: commit changes on server, unlock mailbox, close connection."""290 resp = self._shortcmd('QUIT')291 self.close()292 return resp293 294 def close(self):295 """Close the connection without assuming anything about it."""296 try:297 file = self.file298 self.file = None299 if file is not None:300 file.close()301 finally:302 sock = self.sock303 self.sock = None304 if sock is not None:305 try:306 sock.shutdown(socket.SHUT_RDWR)307 except OSError as exc:308 # The server might already have closed the connection.309 # On Windows, this may result in WSAEINVAL (error 10022):310 # An invalid operation was attempted.311 if (exc.errno != errno.ENOTCONN312 and getattr(exc, 'winerror', 0) != 10022):313 raise314 finally:315 sock.close()316 317 #__del__ = quit318 319 320 # optional commands:321 322 def rpop(self, user):323 """Send RPOP command to access the mailbox with an alternate user."""324 return self._shortcmd('RPOP %s' % user)325 326 327 timestamp = re.compile(br'\+OK.[^<]*(<.*>)')328 329 def apop(self, user, password):330 """Authorisation331 332 - only possible if server has supplied a timestamp in initial greeting.333 334 Args:335 user - mailbox user;336 password - mailbox password.337 338 NB: mailbox is locked by server from here to 'quit()'339 """340 secret = bytes(password, self.encoding)341 m = self.timestamp.match(self.welcome)342 if not m:343 raise error_proto('-ERR APOP not supported by server')344 import hashlib345 digest = m.group(1)+secret346 digest = hashlib.md5(digest).hexdigest()347 return self._shortcmd('APOP %s %s' % (user, digest))348 349 350 def top(self, which, howmuch):351 """Retrieve message header of message number 'which'352 and first 'howmuch' lines of message body.353 354 Result is in form ['response', ['line', ...], octets].355 """356 return self._longcmd('TOP %s %s' % (which, howmuch))357 358 359 def uidl(self, which=None):360 """Return message digest (unique id) list.361 362 If 'which', result contains unique id for that message363 in the form 'response mesgnum uid', otherwise result is364 the list ['response', ['mesgnum uid', ...], octets]365 """366 if which is not None:367 return self._shortcmd('UIDL %s' % which)368 return self._longcmd('UIDL')369 370 371 def utf8(self):372 """Try to enter UTF-8 mode (see RFC 6856). Returns server response.373 """374 return self._shortcmd('UTF8')375 376 377 def capa(self):378 """Return server capabilities (RFC 2449) as a dictionary379 >>> c=poplib.POP3('localhost')380 >>> c.capa()381 {'IMPLEMENTATION': ['Cyrus', 'POP3', 'server', 'v2.2.12'],382 'TOP': [], 'LOGIN-DELAY': ['0'], 'AUTH-RESP-CODE': [],383 'EXPIRE': ['NEVER'], 'USER': [], 'STLS': [], 'PIPELINING': [],384 'UIDL': [], 'RESP-CODES': []}385 >>>386 387 Really, according to RFC 2449, the cyrus folks should avoid388 having the implementation split into multiple arguments...389 """390 def _parsecap(line):391 lst = line.decode('ascii').split()392 return lst[0], lst[1:]393 394 caps = {}395 try:396 resp = self._longcmd('CAPA')397 rawcaps = resp[1]398 for capline in rawcaps:399 capnm, capargs = _parsecap(capline)400 caps[capnm] = capargs401 except error_proto:402 raise error_proto('-ERR CAPA not supported by server')403 return caps404 405 406 def stls(self, context=None):407 """Start a TLS session on the active connection as specified in RFC 2595.408 409 context - a ssl.SSLContext410 """411 if not HAVE_SSL:412 raise error_proto('-ERR TLS support missing')413 if self._tls_established:414 raise error_proto('-ERR TLS session already established')415 caps = self.capa()416 if not 'STLS' in caps:417 raise error_proto('-ERR STLS not supported by server')418 if context is None:419 context = ssl._create_stdlib_context()420 resp = self._shortcmd('STLS')421 self.sock = context.wrap_socket(self.sock,422 server_hostname=self.host)423 self.file = self.sock.makefile('rb')424 self._tls_established = True425 return resp426 427 428if HAVE_SSL:429 430 class POP3_SSL(POP3):431 """POP3 client class over SSL connection432 433 Instantiate with: POP3_SSL(hostname, port=995, context=None)434 435 hostname - the hostname of the pop3 over ssl server436 port - port number437 context - a ssl.SSLContext438 439 See the methods of the parent class POP3 for more documentation.440 """441 442 def __init__(self, host, port=POP3_SSL_PORT,443 *, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, context=None):444 if context is None:445 context = ssl._create_stdlib_context()446 self.context = context447 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 sock454 455 def stls(self, context=None):456 """The method unconditionally raises an exception since the457 STLS command doesn't make any sense on an already established458 SSL/TLS session.459 """460 raise error_proto('-ERR TLS session already established')461 462 __all__.append("POP3_SSL")463 464if __name__ == "__main__":465 a = POP3(sys.argv[1])466 print(a.getwelcome())467 a.user(sys.argv[2])468 a.pass_(sys.argv[3])469 a.list()470 (numMsgs, totalSize) = a.stat()471 for i in range(1, numMsgs + 1):472 (header, msg, octets) = a.retr(i)473 print("Message %d:" % i)474 for line in msg:475 print(' ' + line)476 print('-----------------------')477 a.quit()478 