poplib.py revision 2ad2569c721c6179667a16d1a6f2df4f93606636
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
11# Example (see the test function at the end of this file)
12
13# Imports
14
15import re, socket
16
17__all__ = ["POP3","error_proto"]
18
19# Exception raised when an error or invalid response is received:
20
21class error_proto(Exception): pass
22
23# Standard Port
24POP3_PORT = 110
25
26# Line terminators (we always output CRLF, but accept any of CRLF, LFCR, LF)
27CR = '\r'
28LF = '\n'
29CRLF = CR+LF
30
31
32class POP3:
33
34    """This class supports both the minimal and optional command sets.
35    Arguments can be strings or integers (where appropriate)
36    (e.g.: retr(1) and retr('1') both work equally well.
37
38    Minimal Command Set:
39            USER name               user(name)
40            PASS string             pass_(string)
41            STAT                    stat()
42            LIST [msg]              list(msg = None)
43            RETR msg                retr(msg)
44            DELE msg                dele(msg)
45            NOOP                    noop()
46            RSET                    rset()
47            QUIT                    quit()
48
49    Optional Commands (some servers support these):
50            RPOP name               rpop(name)
51            APOP name digest        apop(name, digest)
52            TOP msg n               top(msg, n)
53            UIDL [msg]              uidl(msg = None)
54
55    Raises one exception: 'error_proto'.
56
57    Instantiate with:
58            POP3(hostname, port=110)
59
60    NB:     the POP protocol locks the mailbox from user
61            authorization until QUIT, so be sure to get in, suck
62            the messages, and quit, each time you access the
63            mailbox.
64
65            POP is a line-based protocol, which means large mail
66            messages consume lots of python cycles reading them
67            line-by-line.
68
69            If it's available on your mail server, use IMAP4
70            instead, it doesn't suffer from the two problems
71            above.
72    """
73
74
75    def __init__(self, host, port = POP3_PORT):
76        self.host = host
77        self.port = port
78        msg = "getaddrinfo returns an empty list"
79        for res in socket.getaddrinfo(self.host, self.port, 0, socket.SOCK_STREAM):
80            af, socktype, proto, canonname, sa = res
81            try:
82                self.sock = socket.socket(af, socktype, proto)
83                self.sock.connect(sa)
84            except socket.error, msg:
85                self.sock.close()
86                self.sock = None
87                continue
88            break
89        if not self.sock:
90            raise socket.error, msg
91        self.file = self.sock.makefile('rb')
92        self._debugging = 0
93        self.welcome = self._getresp()
94
95
96    def _putline(self, line):
97        #if self._debugging > 1: print '*put*', `line`
98        self.sock.send('%s%s' % (line, CRLF))
99
100
101    # Internal: send one command to the server (through _putline())
102
103    def _putcmd(self, line):
104        #if self._debugging: print '*cmd*', `line`
105        self._putline(line)
106
107
108    # Internal: return one line from the server, stripping CRLF.
109    # This is where all the CPU time of this module is consumed.
110    # Raise error_proto('-ERR EOF') if the connection is closed.
111
112    def _getline(self):
113        line = self.file.readline()
114        #if self._debugging > 1: print '*get*', `line`
115        if not line: raise error_proto('-ERR EOF')
116        octets = len(line)
117        # server can send any combination of CR & LF
118        # however, 'readline()' returns lines ending in LF
119        # so only possibilities are ...LF, ...CRLF, CR...LF
120        if line[-2:] == CRLF:
121            return line[:-2], octets
122        if line[0] == CR:
123            return line[1:-1], octets
124        return line[:-1], octets
125
126
127    # Internal: get a response from the server.
128    # Raise 'error_proto' if the response doesn't start with '+'.
129
130    def _getresp(self):
131        resp, o = self._getline()
132        #if self._debugging > 1: print '*resp*', `resp`
133        c = resp[:1]
134        if c != '+':
135            raise error_proto(resp)
136        return resp
137
138
139    # Internal: get a response plus following text from the server.
140
141    def _getlongresp(self):
142        resp = self._getresp()
143        list = []; octets = 0
144        line, o = self._getline()
145        while line != '.':
146            if line[:2] == '..':
147                o = o-1
148                line = line[1:]
149            octets = octets + o
150            list.append(line)
151            line, o = self._getline()
152        return resp, list, octets
153
154
155    # Internal: send a command and get the response
156
157    def _shortcmd(self, line):
158        self._putcmd(line)
159        return self._getresp()
160
161
162    # Internal: send a command and get the response plus following text
163
164    def _longcmd(self, line):
165        self._putcmd(line)
166        return self._getlongresp()
167
168
169    # These can be useful:
170
171    def getwelcome(self):
172        return self.welcome
173
174
175    def set_debuglevel(self, level):
176        self._debugging = level
177
178
179    # Here are all the POP commands:
180
181    def user(self, user):
182        """Send user name, return response
183
184        (should indicate password required).
185        """
186        return self._shortcmd('USER %s' % user)
187
188
189    def pass_(self, pswd):
190        """Send password, return response
191
192        (response includes message count, mailbox size).
193
194        NB: mailbox is locked by server from here to 'quit()'
195        """
196        return self._shortcmd('PASS %s' % pswd)
197
198
199    def stat(self):
200        """Get mailbox status.
201
202        Result is tuple of 2 ints (message count, mailbox size)
203        """
204        retval = self._shortcmd('STAT')
205        rets = retval.split()
206        #if self._debugging: print '*stat*', `rets`
207        numMessages = int(rets[1])
208        sizeMessages = int(rets[2])
209        return (numMessages, sizeMessages)
210
211
212    def list(self, which=None):
213        """Request listing, return result.
214
215        Result without a message number argument is in form
216        ['response', ['mesg_num octets', ...]].
217
218        Result when a message number argument is given is a
219        single response: the "scan listing" for that message.
220        """
221        if which:
222            return self._shortcmd('LIST %s' % which)
223        return self._longcmd('LIST')
224
225
226    def retr(self, which):
227        """Retrieve whole message number 'which'.
228
229        Result is in form ['response', ['line', ...], octets].
230        """
231        return self._longcmd('RETR %s' % which)
232
233
234    def dele(self, which):
235        """Delete message number 'which'.
236
237        Result is 'response'.
238        """
239        return self._shortcmd('DELE %s' % which)
240
241
242    def noop(self):
243        """Does nothing.
244
245        One supposes the response indicates the server is alive.
246        """
247        return self._shortcmd('NOOP')
248
249
250    def rset(self):
251        """Not sure what this does."""
252        return self._shortcmd('RSET')
253
254
255    def quit(self):
256        """Signoff: commit changes on server, unlock mailbox, close connection."""
257        try:
258            resp = self._shortcmd('QUIT')
259        except error_proto, val:
260            resp = val
261        self.file.close()
262        self.sock.close()
263        del self.file, self.sock
264        return resp
265
266    #__del__ = quit
267
268
269    # optional commands:
270
271    def rpop(self, user):
272        """Not sure what this does."""
273        return self._shortcmd('RPOP %s' % user)
274
275
276    timestamp = re.compile(r'\+OK.*(<[^>]+>)')
277
278    def apop(self, user, secret):
279        """Authorisation
280
281        - only possible if server has supplied a timestamp in initial greeting.
282
283        Args:
284                user    - mailbox user;
285                secret  - secret shared between client and server.
286
287        NB: mailbox is locked by server from here to 'quit()'
288        """
289        m = self.timestamp.match(self.welcome)
290        if not m:
291            raise error_proto('-ERR APOP not supported by server')
292        import md5
293        digest = md5.new(m.group(1)+secret).digest()
294        digest = ''.join(map(lambda x:'%02x'%ord(x), digest))
295        return self._shortcmd('APOP %s %s' % (user, digest))
296
297
298    def top(self, which, howmuch):
299        """Retrieve message header of message number 'which'
300        and first 'howmuch' lines of message body.
301
302        Result is in form ['response', ['line', ...], octets].
303        """
304        return self._longcmd('TOP %s %s' % (which, howmuch))
305
306
307    def uidl(self, which=None):
308        """Return message digest (unique id) list.
309
310        If 'which', result contains unique id for that message
311        in the form 'response mesgnum uid', otherwise result is
312        the list ['response', ['mesgnum uid', ...], octets]
313        """
314        if which:
315            return self._shortcmd('UIDL %s' % which)
316        return self._longcmd('UIDL')
317
318
319if __name__ == "__main__":
320    import sys
321    a = POP3(sys.argv[1])
322    print a.getwelcome()
323    a.user(sys.argv[2])
324    a.pass_(sys.argv[3])
325    a.list()
326    (numMsgs, totalSize) = a.stat()
327    for i in range(1, numMsgs + 1):
328        (header, msg, octets) = a.retr(i)
329        print "Message ", `i`, ':'
330        for line in msg:
331            print '   ' + line
332        print '-----------------------'
333    a.quit()
334