1"""A POP3 client class. 2 3Based on the J. Myers POP3 draft, Jan. 96 4""" 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 2003 11 12# Example (see the test function at the end of this file) 13 14# Imports 15 16import re, socket 17 18__all__ = ["POP3","error_proto"] 19 20# Exception raised when an error or invalid response is received: 21 22class error_proto(Exception): pass 23 24# Standard Port 25POP3_PORT = 110 26 27# POP SSL PORT 28POP3_SSL_PORT = 995 29 30# Line terminators (we always output CRLF, but accept any of CRLF, LFCR, LF) 31CR = '\r' 32LF = '\n' 33CRLF = CR+LF 34 35 36class 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 311try: 312 import ssl 313except ImportError: 314 pass 315else: 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 403if __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