poplib.py 12.2 KB
Newer Older
1
"""A POP3 client class.
2

3
Based on the J. Myers POP3 draft, Jan. 96
4 5
"""

6 7 8
# Author: David Ascher <david_ascher@brown.edu>
#         [heavily stealing from nntplib.py]
# 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 13 14 15
# Example (see the test function at the end of this file)

# Imports

16
import re, socket
17

18
__all__ = ["POP3","error_proto","POP3_SSL"]
19

20
# Exception raised when an error or invalid response is received:
21 22

class error_proto(Exception): pass
23 24 25 26

# Standard Port
POP3_PORT = 110

27 28 29
# POP SSL PORT
POP3_SSL_PORT = 995

30 31 32 33
# Line terminators (we always output CRLF, but accept any of CRLF, LFCR, LF)
CR = '\r'
LF = '\n'
CRLF = CR+LF
34 35 36


class POP3:
37

Tim Peters's avatar
Tim Peters committed
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
    """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'.
60

Tim Peters's avatar
Tim Peters committed
61 62
    Instantiate with:
            POP3(hostname, port=110)
63

Tim Peters's avatar
Tim Peters committed
64 65 66 67
    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.
68

Tim Peters's avatar
Tim Peters committed
69 70 71
            POP is a line-based protocol, which means large mail
            messages consume lots of python cycles reading them
            line-by-line.
72

Tim Peters's avatar
Tim Peters committed
73 74 75 76
            If it's available on your mail server, use IMAP4
            instead, it doesn't suffer from the two problems
            above.
    """
77 78


Tim Peters's avatar
Tim Peters committed
79
    def __init__(self, host, port = POP3_PORT):
80 81
        self.host = host
        self.port = port
82
        msg = "getaddrinfo returns an empty list"
83
        self.sock = None
84 85 86 87 88 89
        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:
90 91
                if self.sock:
                    self.sock.close()
92 93 94 95 96 97 98 99
                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()
100

101

Tim Peters's avatar
Tim Peters committed
102
    def _putline(self, line):
103
        if self._debugging > 1: print '*put*', repr(line)
104
        self.sock.sendall('%s%s' % (line, CRLF))
105

106

Tim Peters's avatar
Tim Peters committed
107
    # Internal: send one command to the server (through _putline())
108

Tim Peters's avatar
Tim Peters committed
109
    def _putcmd(self, line):
110
        if self._debugging: print '*cmd*', repr(line)
Tim Peters's avatar
Tim Peters committed
111
        self._putline(line)
112

113

Tim Peters's avatar
Tim Peters committed
114 115 116
    # 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.
117

Tim Peters's avatar
Tim Peters committed
118 119
    def _getline(self):
        line = self.file.readline()
120
        if self._debugging > 1: print '*get*', repr(line)
Tim Peters's avatar
Tim Peters committed
121 122 123 124 125 126 127 128 129 130
        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
131

132

Tim Peters's avatar
Tim Peters committed
133 134
    # Internal: get a response from the server.
    # Raise 'error_proto' if the response doesn't start with '+'.
135

Tim Peters's avatar
Tim Peters committed
136 137
    def _getresp(self):
        resp, o = self._getline()
138
        if self._debugging > 1: print '*resp*', repr(resp)
Tim Peters's avatar
Tim Peters committed
139 140 141 142
        c = resp[:1]
        if c != '+':
            raise error_proto(resp)
        return resp
143

144

Tim Peters's avatar
Tim Peters committed
145
    # Internal: get a response plus following text from the server.
146

Tim Peters's avatar
Tim Peters committed
147 148 149 150 151 152 153 154 155 156 157 158
    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
159

160

Tim Peters's avatar
Tim Peters committed
161
    # Internal: send a command and get the response
162

Tim Peters's avatar
Tim Peters committed
163 164 165
    def _shortcmd(self, line):
        self._putcmd(line)
        return self._getresp()
166

167

Tim Peters's avatar
Tim Peters committed
168
    # Internal: send a command and get the response plus following text
169

Tim Peters's avatar
Tim Peters committed
170 171 172
    def _longcmd(self, line):
        self._putcmd(line)
        return self._getlongresp()
173

174

Tim Peters's avatar
Tim Peters committed
175
    # These can be useful:
176

Tim Peters's avatar
Tim Peters committed
177 178
    def getwelcome(self):
        return self.welcome
179

180

Tim Peters's avatar
Tim Peters committed
181 182
    def set_debuglevel(self, level):
        self._debugging = level
183

184

Tim Peters's avatar
Tim Peters committed
185
    # Here are all the POP commands:
186

Tim Peters's avatar
Tim Peters committed
187 188
    def user(self, user):
        """Send user name, return response
189

Tim Peters's avatar
Tim Peters committed
190 191 192
        (should indicate password required).
        """
        return self._shortcmd('USER %s' % user)
193

194

Tim Peters's avatar
Tim Peters committed
195 196
    def pass_(self, pswd):
        """Send password, return response
197

Tim Peters's avatar
Tim Peters committed
198
        (response includes message count, mailbox size).
199

Tim Peters's avatar
Tim Peters committed
200 201 202
        NB: mailbox is locked by server from here to 'quit()'
        """
        return self._shortcmd('PASS %s' % pswd)
203

204

Tim Peters's avatar
Tim Peters committed
205 206
    def stat(self):
        """Get mailbox status.
207

Tim Peters's avatar
Tim Peters committed
208 209 210
        Result is tuple of 2 ints (message count, mailbox size)
        """
        retval = self._shortcmd('STAT')
211
        rets = retval.split()
212
        if self._debugging: print '*stat*', repr(rets)
213 214
        numMessages = int(rets[1])
        sizeMessages = int(rets[2])
Tim Peters's avatar
Tim Peters committed
215
        return (numMessages, sizeMessages)
216

217

Tim Peters's avatar
Tim Peters committed
218 219
    def list(self, which=None):
        """Request listing, return result.
220

Tim Peters's avatar
Tim Peters committed
221
        Result without a message number argument is in form
222
        ['response', ['mesg_num octets', ...], octets].
223

Tim Peters's avatar
Tim Peters committed
224 225 226
        Result when a message number argument is given is a
        single response: the "scan listing" for that message.
        """
227
        if which is not None:
Tim Peters's avatar
Tim Peters committed
228 229
            return self._shortcmd('LIST %s' % which)
        return self._longcmd('LIST')
230

231

Tim Peters's avatar
Tim Peters committed
232 233
    def retr(self, which):
        """Retrieve whole message number 'which'.
234

Tim Peters's avatar
Tim Peters committed
235 236 237
        Result is in form ['response', ['line', ...], octets].
        """
        return self._longcmd('RETR %s' % which)
238

239

Tim Peters's avatar
Tim Peters committed
240 241
    def dele(self, which):
        """Delete message number 'which'.
242

Tim Peters's avatar
Tim Peters committed
243 244 245
        Result is 'response'.
        """
        return self._shortcmd('DELE %s' % which)
246

247

Tim Peters's avatar
Tim Peters committed
248 249
    def noop(self):
        """Does nothing.
250

Tim Peters's avatar
Tim Peters committed
251 252 253
        One supposes the response indicates the server is alive.
        """
        return self._shortcmd('NOOP')
254

255

Tim Peters's avatar
Tim Peters committed
256 257 258
    def rset(self):
        """Not sure what this does."""
        return self._shortcmd('RSET')
259 260


Tim Peters's avatar
Tim Peters committed
261 262 263 264 265 266 267 268 269 270
    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
271

Tim Peters's avatar
Tim Peters committed
272
    #__del__ = quit
273 274


Tim Peters's avatar
Tim Peters committed
275
    # optional commands:
276

Tim Peters's avatar
Tim Peters committed
277 278 279
    def rpop(self, user):
        """Not sure what this does."""
        return self._shortcmd('RPOP %s' % user)
280 281


282
    timestamp = re.compile(r'\+OK.*(<[^>]+>)')
283

Tim Peters's avatar
Tim Peters committed
284 285
    def apop(self, user, secret):
        """Authorisation
286

Tim Peters's avatar
Tim Peters committed
287
        - only possible if server has supplied a timestamp in initial greeting.
288

Tim Peters's avatar
Tim Peters committed
289 290 291
        Args:
                user    - mailbox user;
                secret  - secret shared between client and server.
292

Tim Peters's avatar
Tim Peters committed
293 294
        NB: mailbox is locked by server from here to 'quit()'
        """
295 296
        m = self.timestamp.match(self.welcome)
        if not m:
Tim Peters's avatar
Tim Peters committed
297
            raise error_proto('-ERR APOP not supported by server')
298 299
        import hashlib
        digest = hashlib.md5(m.group(1)+secret).digest()
300
        digest = ''.join(map(lambda x:'%02x'%ord(x), digest))
Tim Peters's avatar
Tim Peters committed
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319
        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]
        """
320
        if which is not None:
Tim Peters's avatar
Tim Peters committed
321 322
            return self._shortcmd('UIDL %s' % which)
        return self._longcmd('UIDL')
323

324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377
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 = socket.ssl(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)
378
        if self._debugging > 1: print '*get*', repr(line)
379 380 381 382 383 384 385 386 387

        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):
388
        if self._debugging > 1: print '*put*', repr(line)
389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407
        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

408

409
if __name__ == "__main__":
410 411
    import sys
    a = POP3(sys.argv[1])
Tim Peters's avatar
Tim Peters committed
412
    print a.getwelcome()
413 414
    a.user(sys.argv[2])
    a.pass_(sys.argv[3])
Tim Peters's avatar
Tim Peters committed
415 416 417 418
    a.list()
    (numMsgs, totalSize) = a.stat()
    for i in range(1, numMsgs + 1):
        (header, msg, octets) = a.retr(i)
419
        print "Message %d:" % i
Tim Peters's avatar
Tim Peters committed
420 421 422 423
        for line in msg:
            print '   ' + line
        print '-----------------------'
    a.quit()