1 """A POP3 client class.
3 Based on the J. Myers POP3 draft, Jan. 96
5 Author: David Ascher <david_ascher@brown.edu>
6 [heavily stealing from nntplib.py]
7 Updated: Piers Lauder <piers@cs.su.oz.au> [Jul '97]
10 # Example (see the test function at the end of this file)
12 TESTSERVER
= "localhost"
14 TESTPASSWORD
= "_passwd_"
18 import regex
, socket
, string
20 # Exception raised when an error or invalid response is received:
22 class error_proto(Exception): pass
27 # Line terminators (we always output CRLF, but accept any of CRLF, LFCR, LF)
35 """This class supports both the minimal and optional command sets.
36 Arguments can be strings or integers (where appropriate)
37 (e.g.: retr(1) and retr('1') both work equally well.
41 PASS string pass_(string)
43 LIST [msg] list(msg = None)
50 Optional Commands (some servers support these):
52 APOP name digest apop(name, digest)
54 UIDL [msg] uidl(msg = None)
56 Raises one exception: 'error_proto'.
59 POP3(hostname, port=110)
61 NB: the POP protocol locks the mailbox from user
62 authorisation until QUIT, so be sure to get in, suck
63 the messages, and quit, each time you access the
66 POP is a line-based protocol, which means large mail
67 messages consume lots of python cycles reading them
70 If it's available on your mail server, use IMAP4
71 instead, it doesn't suffer from the two problems
76 def __init__(self
, host
, port
= POP3_PORT
):
79 self
.sock
= socket
.socket(socket
.AF_INET
, socket
.SOCK_STREAM
)
80 self
.sock
.connect(self
.host
, self
.port
)
81 self
.file = self
.sock
.makefile('rb')
83 self
.welcome
= self
._getresp
()
86 def _putline(self
, line
):
87 #if self._debugging > 1: print '*put*', `line`
88 self
.sock
.send('%s%s' % (line
, CRLF
))
91 # Internal: send one command to the server (through _putline())
93 def _putcmd(self
, line
):
94 #if self._debugging: print '*cmd*', `line`
98 # Internal: return one line from the server, stripping CRLF.
99 # This is where all the CPU time of this module is consumed.
100 # Raise error_proto('-ERR EOF') if the connection is closed.
103 line
= self
.file.readline()
104 #if self._debugging > 1: print '*get*', `line`
105 if not line
: raise error_proto('-ERR EOF')
107 # server can send any combination of CR & LF
108 # however, 'readline()' returns lines ending in LF
109 # so only possibilities are ...LF, ...CRLF, CR...LF
110 if line
[-2:] == CRLF
:
111 return line
[:-2], octets
113 return line
[1:-1], octets
114 return line
[:-1], octets
117 # Internal: get a response from the server.
118 # Raise 'error_proto' if the response doesn't start with '+'.
121 resp
, o
= self
._getline
()
122 #if self._debugging > 1: print '*resp*', `resp`
125 raise error_proto(resp
)
129 # Internal: get a response plus following text from the server.
131 def _getlongresp(self
):
132 resp
= self
._getresp
()
133 list = []; octets
= 0
134 line
, o
= self
._getline
()
138 line
, o
= self
._getline
()
139 return resp
, list, octets
142 # Internal: send a command and get the response
144 def _shortcmd(self
, line
):
146 return self
._getresp
()
149 # Internal: send a command and get the response plus following text
151 def _longcmd(self
, line
):
153 return self
._getlongresp
()
156 # These can be useful:
158 def getwelcome(self
):
162 def set_debuglevel(self
, level
):
163 self
._debugging
= level
166 # Here are all the POP commands:
168 def user(self
, user
):
169 """Send user name, return response
171 (should indicate password required).
173 return self
._shortcmd
('USER %s' % user
)
176 def pass_(self
, pswd
):
177 """Send password, return response
179 (response includes message count, mailbox size).
181 NB: mailbox is locked by server from here to 'quit()'
183 return self
._shortcmd
('PASS %s' % pswd
)
187 """Get mailbox status.
189 Result is tuple of 2 ints (message count, mailbox size)
191 retval
= self
._shortcmd
('STAT')
192 rets
= string
.split(retval
)
193 #if self._debugging: print '*stat*', `rets`
194 numMessages
= string
.atoi(rets
[1])
195 sizeMessages
= string
.atoi(rets
[2])
196 return (numMessages
, sizeMessages
)
199 def list(self
, which
=None):
200 """Request listing, return result.
202 Result without a message number argument is in form
203 ['response', ['mesg_num octets', ...]].
205 Result when a message number argument is given is a
206 single response: the "scan listing" for that message.
209 return self
._shortcmd
('LIST %s' % which
)
210 return self
._longcmd
('LIST')
213 def retr(self
, which
):
214 """Retrieve whole message number 'which'.
216 Result is in form ['response', ['line', ...], octets].
218 return self
._longcmd
('RETR %s' % which
)
221 def dele(self
, which
):
222 """Delete message number 'which'.
224 Result is 'response'.
226 return self
._shortcmd
('DELE %s' % which
)
232 One supposes the response indicates the server is alive.
234 return self
._shortcmd
('NOOP')
238 """Not sure what this does."""
239 return self
._shortcmd
('RSET')
243 """Signoff: commit changes on server, unlock mailbox, close connection."""
245 resp
= self
._shortcmd
('QUIT')
246 except error_proto
, val
:
250 del self
.file, self
.sock
258 def rpop(self
, user
):
259 """Not sure what this does."""
260 return self
._shortcmd
('RPOP %s' % user
)
263 timestamp
= regex
.compile('\+OK.*\(<[^>]+>\)')
265 def apop(self
, user
, secret
):
268 - only possible if server has supplied a timestamp in initial greeting.
272 secret - secret shared between client and server.
274 NB: mailbox is locked by server from here to 'quit()'
276 if self
.timestamp
.match(self
.welcome
) <= 0:
277 raise error_proto('-ERR APOP not supported by server')
279 digest
= md5
.new(self
.timestamp
.group(1)+secret
).digest()
280 digest
= string
.join(map(lambda x
:'%02x'%ord(x
), digest
), '')
281 return self
._shortcmd
('APOP %s %s' % (user
, digest
))
284 def top(self
, which
, howmuch
):
285 """Retrieve message header of message number 'which'
286 and first 'howmuch' lines of message body.
288 Result is in form ['response', ['line', ...], octets].
290 return self
._longcmd
('TOP %s %s' % (which
, howmuch
))
293 def uidl(self
, which
=None):
294 """Return message digest (unique id) list.
296 If 'which', result contains unique id for that message,
297 otherwise result is list ['response', ['mesgnum uid', ...], octets]
300 return self
._shortcmd
('UIDL %s' % which
)
301 return self
._longcmd
('UIDL')
304 if __name__
== "__main__":
308 a
.pass_(TESTPASSWORD
)
310 (numMsgs
, totalSize
) = a
.stat()
311 for i
in range(1, numMsgs
+ 1):
312 (header
, msg
, octets
) = a
.retr(i
)
313 print "Message ", `i`
, ':'
316 print '-----------------------'