1 | """IMAP4 client.
|
---|
2 |
|
---|
3 | Based on RFC 2060.
|
---|
4 |
|
---|
5 | Public class: IMAP4
|
---|
6 | Public variable: Debug
|
---|
7 | Public functions: Internaldate2tuple
|
---|
8 | Int2AP
|
---|
9 | ParseFlags
|
---|
10 | Time2Internaldate
|
---|
11 | """
|
---|
12 |
|
---|
13 | # Author: Piers Lauder <piers@cs.su.oz.au> December 1997.
|
---|
14 | #
|
---|
15 | # Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
|
---|
16 | # String method conversion by ESR, February 2001.
|
---|
17 | # GET/SETACL contributed by Anthony Baxter <anthony@interlink.com.au> April 2001.
|
---|
18 | # IMAP4_SSL contributed by Tino Lange <Tino.Lange@isg.de> March 2002.
|
---|
19 | # GET/SETQUOTA contributed by Andreas Zeidler <az@kreativkombinat.de> June 2002.
|
---|
20 | # PROXYAUTH contributed by Rick Holbert <holbert.13@osu.edu> November 2002.
|
---|
21 | # GET/SETANNOTATION contributed by Tomas Lindroos <skitta@abo.fi> June 2005.
|
---|
22 |
|
---|
23 | __version__ = "2.58"
|
---|
24 |
|
---|
25 | import binascii, errno, random, re, socket, subprocess, sys, time
|
---|
26 |
|
---|
27 | __all__ = ["IMAP4", "IMAP4_stream", "Internaldate2tuple",
|
---|
28 | "Int2AP", "ParseFlags", "Time2Internaldate"]
|
---|
29 |
|
---|
30 | # Globals
|
---|
31 |
|
---|
32 | CRLF = '\r\n'
|
---|
33 | Debug = 0
|
---|
34 | IMAP4_PORT = 143
|
---|
35 | IMAP4_SSL_PORT = 993
|
---|
36 | AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first
|
---|
37 |
|
---|
38 | # Commands
|
---|
39 |
|
---|
40 | Commands = {
|
---|
41 | # name valid states
|
---|
42 | 'APPEND': ('AUTH', 'SELECTED'),
|
---|
43 | 'AUTHENTICATE': ('NONAUTH',),
|
---|
44 | 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
|
---|
45 | 'CHECK': ('SELECTED',),
|
---|
46 | 'CLOSE': ('SELECTED',),
|
---|
47 | 'COPY': ('SELECTED',),
|
---|
48 | 'CREATE': ('AUTH', 'SELECTED'),
|
---|
49 | 'DELETE': ('AUTH', 'SELECTED'),
|
---|
50 | 'DELETEACL': ('AUTH', 'SELECTED'),
|
---|
51 | 'EXAMINE': ('AUTH', 'SELECTED'),
|
---|
52 | 'EXPUNGE': ('SELECTED',),
|
---|
53 | 'FETCH': ('SELECTED',),
|
---|
54 | 'GETACL': ('AUTH', 'SELECTED'),
|
---|
55 | 'GETANNOTATION':('AUTH', 'SELECTED'),
|
---|
56 | 'GETQUOTA': ('AUTH', 'SELECTED'),
|
---|
57 | 'GETQUOTAROOT': ('AUTH', 'SELECTED'),
|
---|
58 | 'MYRIGHTS': ('AUTH', 'SELECTED'),
|
---|
59 | 'LIST': ('AUTH', 'SELECTED'),
|
---|
60 | 'LOGIN': ('NONAUTH',),
|
---|
61 | 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
|
---|
62 | 'LSUB': ('AUTH', 'SELECTED'),
|
---|
63 | 'NAMESPACE': ('AUTH', 'SELECTED'),
|
---|
64 | 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
|
---|
65 | 'PARTIAL': ('SELECTED',), # NB: obsolete
|
---|
66 | 'PROXYAUTH': ('AUTH',),
|
---|
67 | 'RENAME': ('AUTH', 'SELECTED'),
|
---|
68 | 'SEARCH': ('SELECTED',),
|
---|
69 | 'SELECT': ('AUTH', 'SELECTED'),
|
---|
70 | 'SETACL': ('AUTH', 'SELECTED'),
|
---|
71 | 'SETANNOTATION':('AUTH', 'SELECTED'),
|
---|
72 | 'SETQUOTA': ('AUTH', 'SELECTED'),
|
---|
73 | 'SORT': ('SELECTED',),
|
---|
74 | 'STATUS': ('AUTH', 'SELECTED'),
|
---|
75 | 'STORE': ('SELECTED',),
|
---|
76 | 'SUBSCRIBE': ('AUTH', 'SELECTED'),
|
---|
77 | 'THREAD': ('SELECTED',),
|
---|
78 | 'UID': ('SELECTED',),
|
---|
79 | 'UNSUBSCRIBE': ('AUTH', 'SELECTED'),
|
---|
80 | }
|
---|
81 |
|
---|
82 | # Patterns to match server responses
|
---|
83 |
|
---|
84 | Continuation = re.compile(r'\+( (?P<data>.*))?')
|
---|
85 | Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
|
---|
86 | InternalDate = re.compile(r'.*INTERNALDATE "'
|
---|
87 | r'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
|
---|
88 | r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
|
---|
89 | r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
|
---|
90 | r'"')
|
---|
91 | Literal = re.compile(r'.*{(?P<size>\d+)}$')
|
---|
92 | MapCRLF = re.compile(r'\r\n|\r|\n')
|
---|
93 | Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
|
---|
94 | Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
|
---|
95 | Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
|
---|
96 |
|
---|
97 |
|
---|
98 |
|
---|
99 | class IMAP4:
|
---|
100 |
|
---|
101 | """IMAP4 client class.
|
---|
102 |
|
---|
103 | Instantiate with: IMAP4([host[, port]])
|
---|
104 |
|
---|
105 | host - host's name (default: localhost);
|
---|
106 | port - port number (default: standard IMAP4 port).
|
---|
107 |
|
---|
108 | All IMAP4rev1 commands are supported by methods of the same
|
---|
109 | name (in lower-case).
|
---|
110 |
|
---|
111 | All arguments to commands are converted to strings, except for
|
---|
112 | AUTHENTICATE, and the last argument to APPEND which is passed as
|
---|
113 | an IMAP4 literal. If necessary (the string contains any
|
---|
114 | non-printing characters or white-space and isn't enclosed with
|
---|
115 | either parentheses or double quotes) each string is quoted.
|
---|
116 | However, the 'password' argument to the LOGIN command is always
|
---|
117 | quoted. If you want to avoid having an argument string quoted
|
---|
118 | (eg: the 'flags' argument to STORE) then enclose the string in
|
---|
119 | parentheses (eg: "(\Deleted)").
|
---|
120 |
|
---|
121 | Each command returns a tuple: (type, [data, ...]) where 'type'
|
---|
122 | is usually 'OK' or 'NO', and 'data' is either the text from the
|
---|
123 | tagged response, or untagged results from command. Each 'data'
|
---|
124 | is either a string, or a tuple. If a tuple, then the first part
|
---|
125 | is the header of the response, and the second part contains
|
---|
126 | the data (ie: 'literal' value).
|
---|
127 |
|
---|
128 | Errors raise the exception class <instance>.error("<reason>").
|
---|
129 | IMAP4 server errors raise <instance>.abort("<reason>"),
|
---|
130 | which is a sub-class of 'error'. Mailbox status changes
|
---|
131 | from READ-WRITE to READ-ONLY raise the exception class
|
---|
132 | <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
|
---|
133 |
|
---|
134 | "error" exceptions imply a program error.
|
---|
135 | "abort" exceptions imply the connection should be reset, and
|
---|
136 | the command re-tried.
|
---|
137 | "readonly" exceptions imply the command should be re-tried.
|
---|
138 |
|
---|
139 | Note: to use this module, you must read the RFCs pertaining to the
|
---|
140 | IMAP4 protocol, as the semantics of the arguments to each IMAP4
|
---|
141 | command are left to the invoker, not to mention the results. Also,
|
---|
142 | most IMAP servers implement a sub-set of the commands available here.
|
---|
143 | """
|
---|
144 |
|
---|
145 | class error(Exception): pass # Logical errors - debug required
|
---|
146 | class abort(error): pass # Service errors - close and retry
|
---|
147 | class readonly(abort): pass # Mailbox status changed to READ-ONLY
|
---|
148 |
|
---|
149 | mustquote = re.compile(r"[^\w!#$%&'*+,.:;<=>?^`|~-]")
|
---|
150 |
|
---|
151 | def __init__(self, host = '', port = IMAP4_PORT):
|
---|
152 | self.debug = Debug
|
---|
153 | self.state = 'LOGOUT'
|
---|
154 | self.literal = None # A literal argument to a command
|
---|
155 | self.tagged_commands = {} # Tagged commands awaiting response
|
---|
156 | self.untagged_responses = {} # {typ: [data, ...], ...}
|
---|
157 | self.continuation_response = '' # Last continuation response
|
---|
158 | self.is_readonly = False # READ-ONLY desired state
|
---|
159 | self.tagnum = 0
|
---|
160 |
|
---|
161 | # Open socket to server.
|
---|
162 |
|
---|
163 | self.open(host, port)
|
---|
164 |
|
---|
165 | # Create unique tag for this session,
|
---|
166 | # and compile tagged response matcher.
|
---|
167 |
|
---|
168 | self.tagpre = Int2AP(random.randint(4096, 65535))
|
---|
169 | self.tagre = re.compile(r'(?P<tag>'
|
---|
170 | + self.tagpre
|
---|
171 | + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
|
---|
172 |
|
---|
173 | # Get server welcome message,
|
---|
174 | # request and store CAPABILITY response.
|
---|
175 |
|
---|
176 | if __debug__:
|
---|
177 | self._cmd_log_len = 10
|
---|
178 | self._cmd_log_idx = 0
|
---|
179 | self._cmd_log = {} # Last `_cmd_log_len' interactions
|
---|
180 | if self.debug >= 1:
|
---|
181 | self._mesg('imaplib version %s' % __version__)
|
---|
182 | self._mesg('new IMAP4 connection, tag=%s' % self.tagpre)
|
---|
183 |
|
---|
184 | self.welcome = self._get_response()
|
---|
185 | if 'PREAUTH' in self.untagged_responses:
|
---|
186 | self.state = 'AUTH'
|
---|
187 | elif 'OK' in self.untagged_responses:
|
---|
188 | self.state = 'NONAUTH'
|
---|
189 | else:
|
---|
190 | raise self.error(self.welcome)
|
---|
191 |
|
---|
192 | typ, dat = self.capability()
|
---|
193 | if dat == [None]:
|
---|
194 | raise self.error('no CAPABILITY response from server')
|
---|
195 | self.capabilities = tuple(dat[-1].upper().split())
|
---|
196 |
|
---|
197 | if __debug__:
|
---|
198 | if self.debug >= 3:
|
---|
199 | self._mesg('CAPABILITIES: %r' % (self.capabilities,))
|
---|
200 |
|
---|
201 | for version in AllowedVersions:
|
---|
202 | if not version in self.capabilities:
|
---|
203 | continue
|
---|
204 | self.PROTOCOL_VERSION = version
|
---|
205 | return
|
---|
206 |
|
---|
207 | raise self.error('server not IMAP4 compliant')
|
---|
208 |
|
---|
209 |
|
---|
210 | def __getattr__(self, attr):
|
---|
211 | # Allow UPPERCASE variants of IMAP4 command methods.
|
---|
212 | if attr in Commands:
|
---|
213 | return getattr(self, attr.lower())
|
---|
214 | raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
|
---|
215 |
|
---|
216 |
|
---|
217 |
|
---|
218 | # Overridable methods
|
---|
219 |
|
---|
220 |
|
---|
221 | def open(self, host = '', port = IMAP4_PORT):
|
---|
222 | """Setup connection to remote server on "host:port"
|
---|
223 | (default: localhost:standard IMAP4 port).
|
---|
224 | This connection will be used by the routines:
|
---|
225 | read, readline, send, shutdown.
|
---|
226 | """
|
---|
227 | self.host = host
|
---|
228 | self.port = port
|
---|
229 | self.sock = socket.create_connection((host, port))
|
---|
230 | self.file = self.sock.makefile('rb')
|
---|
231 |
|
---|
232 |
|
---|
233 | def read(self, size):
|
---|
234 | """Read 'size' bytes from remote."""
|
---|
235 | return self.file.read(size)
|
---|
236 |
|
---|
237 |
|
---|
238 | def readline(self):
|
---|
239 | """Read line from remote."""
|
---|
240 | return self.file.readline()
|
---|
241 |
|
---|
242 |
|
---|
243 | def send(self, data):
|
---|
244 | """Send data to remote."""
|
---|
245 | self.sock.sendall(data)
|
---|
246 |
|
---|
247 |
|
---|
248 | def shutdown(self):
|
---|
249 | """Close I/O established in "open"."""
|
---|
250 | self.file.close()
|
---|
251 | try:
|
---|
252 | self.sock.shutdown(socket.SHUT_RDWR)
|
---|
253 | except socket.error as e:
|
---|
254 | # The server might already have closed the connection
|
---|
255 | if e.errno != errno.ENOTCONN:
|
---|
256 | raise
|
---|
257 | finally:
|
---|
258 | self.sock.close()
|
---|
259 |
|
---|
260 |
|
---|
261 | def socket(self):
|
---|
262 | """Return socket instance used to connect to IMAP4 server.
|
---|
263 |
|
---|
264 | socket = <instance>.socket()
|
---|
265 | """
|
---|
266 | return self.sock
|
---|
267 |
|
---|
268 |
|
---|
269 |
|
---|
270 | # Utility methods
|
---|
271 |
|
---|
272 |
|
---|
273 | def recent(self):
|
---|
274 | """Return most recent 'RECENT' responses if any exist,
|
---|
275 | else prompt server for an update using the 'NOOP' command.
|
---|
276 |
|
---|
277 | (typ, [data]) = <instance>.recent()
|
---|
278 |
|
---|
279 | 'data' is None if no new messages,
|
---|
280 | else list of RECENT responses, most recent last.
|
---|
281 | """
|
---|
282 | name = 'RECENT'
|
---|
283 | typ, dat = self._untagged_response('OK', [None], name)
|
---|
284 | if dat[-1]:
|
---|
285 | return typ, dat
|
---|
286 | typ, dat = self.noop() # Prod server for response
|
---|
287 | return self._untagged_response(typ, dat, name)
|
---|
288 |
|
---|
289 |
|
---|
290 | def response(self, code):
|
---|
291 | """Return data for response 'code' if received, or None.
|
---|
292 |
|
---|
293 | Old value for response 'code' is cleared.
|
---|
294 |
|
---|
295 | (code, [data]) = <instance>.response(code)
|
---|
296 | """
|
---|
297 | return self._untagged_response(code, [None], code.upper())
|
---|
298 |
|
---|
299 |
|
---|
300 |
|
---|
301 | # IMAP4 commands
|
---|
302 |
|
---|
303 |
|
---|
304 | def append(self, mailbox, flags, date_time, message):
|
---|
305 | """Append message to named mailbox.
|
---|
306 |
|
---|
307 | (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
|
---|
308 |
|
---|
309 | All args except `message' can be None.
|
---|
310 | """
|
---|
311 | name = 'APPEND'
|
---|
312 | if not mailbox:
|
---|
313 | mailbox = 'INBOX'
|
---|
314 | if flags:
|
---|
315 | if (flags[0],flags[-1]) != ('(',')'):
|
---|
316 | flags = '(%s)' % flags
|
---|
317 | else:
|
---|
318 | flags = None
|
---|
319 | if date_time:
|
---|
320 | date_time = Time2Internaldate(date_time)
|
---|
321 | else:
|
---|
322 | date_time = None
|
---|
323 | self.literal = MapCRLF.sub(CRLF, message)
|
---|
324 | return self._simple_command(name, mailbox, flags, date_time)
|
---|
325 |
|
---|
326 |
|
---|
327 | def authenticate(self, mechanism, authobject):
|
---|
328 | """Authenticate command - requires response processing.
|
---|
329 |
|
---|
330 | 'mechanism' specifies which authentication mechanism is to
|
---|
331 | be used - it must appear in <instance>.capabilities in the
|
---|
332 | form AUTH=<mechanism>.
|
---|
333 |
|
---|
334 | 'authobject' must be a callable object:
|
---|
335 |
|
---|
336 | data = authobject(response)
|
---|
337 |
|
---|
338 | It will be called to process server continuation responses.
|
---|
339 | It should return data that will be encoded and sent to server.
|
---|
340 | It should return None if the client abort response '*' should
|
---|
341 | be sent instead.
|
---|
342 | """
|
---|
343 | mech = mechanism.upper()
|
---|
344 | # XXX: shouldn't this code be removed, not commented out?
|
---|
345 | #cap = 'AUTH=%s' % mech
|
---|
346 | #if not cap in self.capabilities: # Let the server decide!
|
---|
347 | # raise self.error("Server doesn't allow %s authentication." % mech)
|
---|
348 | self.literal = _Authenticator(authobject).process
|
---|
349 | typ, dat = self._simple_command('AUTHENTICATE', mech)
|
---|
350 | if typ != 'OK':
|
---|
351 | raise self.error(dat[-1])
|
---|
352 | self.state = 'AUTH'
|
---|
353 | return typ, dat
|
---|
354 |
|
---|
355 |
|
---|
356 | def capability(self):
|
---|
357 | """(typ, [data]) = <instance>.capability()
|
---|
358 | Fetch capabilities list from server."""
|
---|
359 |
|
---|
360 | name = 'CAPABILITY'
|
---|
361 | typ, dat = self._simple_command(name)
|
---|
362 | return self._untagged_response(typ, dat, name)
|
---|
363 |
|
---|
364 |
|
---|
365 | def check(self):
|
---|
366 | """Checkpoint mailbox on server.
|
---|
367 |
|
---|
368 | (typ, [data]) = <instance>.check()
|
---|
369 | """
|
---|
370 | return self._simple_command('CHECK')
|
---|
371 |
|
---|
372 |
|
---|
373 | def close(self):
|
---|
374 | """Close currently selected mailbox.
|
---|
375 |
|
---|
376 | Deleted messages are removed from writable mailbox.
|
---|
377 | This is the recommended command before 'LOGOUT'.
|
---|
378 |
|
---|
379 | (typ, [data]) = <instance>.close()
|
---|
380 | """
|
---|
381 | try:
|
---|
382 | typ, dat = self._simple_command('CLOSE')
|
---|
383 | finally:
|
---|
384 | self.state = 'AUTH'
|
---|
385 | return typ, dat
|
---|
386 |
|
---|
387 |
|
---|
388 | def copy(self, message_set, new_mailbox):
|
---|
389 | """Copy 'message_set' messages onto end of 'new_mailbox'.
|
---|
390 |
|
---|
391 | (typ, [data]) = <instance>.copy(message_set, new_mailbox)
|
---|
392 | """
|
---|
393 | return self._simple_command('COPY', message_set, new_mailbox)
|
---|
394 |
|
---|
395 |
|
---|
396 | def create(self, mailbox):
|
---|
397 | """Create new mailbox.
|
---|
398 |
|
---|
399 | (typ, [data]) = <instance>.create(mailbox)
|
---|
400 | """
|
---|
401 | return self._simple_command('CREATE', mailbox)
|
---|
402 |
|
---|
403 |
|
---|
404 | def delete(self, mailbox):
|
---|
405 | """Delete old mailbox.
|
---|
406 |
|
---|
407 | (typ, [data]) = <instance>.delete(mailbox)
|
---|
408 | """
|
---|
409 | return self._simple_command('DELETE', mailbox)
|
---|
410 |
|
---|
411 | def deleteacl(self, mailbox, who):
|
---|
412 | """Delete the ACLs (remove any rights) set for who on mailbox.
|
---|
413 |
|
---|
414 | (typ, [data]) = <instance>.deleteacl(mailbox, who)
|
---|
415 | """
|
---|
416 | return self._simple_command('DELETEACL', mailbox, who)
|
---|
417 |
|
---|
418 | def expunge(self):
|
---|
419 | """Permanently remove deleted items from selected mailbox.
|
---|
420 |
|
---|
421 | Generates 'EXPUNGE' response for each deleted message.
|
---|
422 |
|
---|
423 | (typ, [data]) = <instance>.expunge()
|
---|
424 |
|
---|
425 | 'data' is list of 'EXPUNGE'd message numbers in order received.
|
---|
426 | """
|
---|
427 | name = 'EXPUNGE'
|
---|
428 | typ, dat = self._simple_command(name)
|
---|
429 | return self._untagged_response(typ, dat, name)
|
---|
430 |
|
---|
431 |
|
---|
432 | def fetch(self, message_set, message_parts):
|
---|
433 | """Fetch (parts of) messages.
|
---|
434 |
|
---|
435 | (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
|
---|
436 |
|
---|
437 | 'message_parts' should be a string of selected parts
|
---|
438 | enclosed in parentheses, eg: "(UID BODY[TEXT])".
|
---|
439 |
|
---|
440 | 'data' are tuples of message part envelope and data.
|
---|
441 | """
|
---|
442 | name = 'FETCH'
|
---|
443 | typ, dat = self._simple_command(name, message_set, message_parts)
|
---|
444 | return self._untagged_response(typ, dat, name)
|
---|
445 |
|
---|
446 |
|
---|
447 | def getacl(self, mailbox):
|
---|
448 | """Get the ACLs for a mailbox.
|
---|
449 |
|
---|
450 | (typ, [data]) = <instance>.getacl(mailbox)
|
---|
451 | """
|
---|
452 | typ, dat = self._simple_command('GETACL', mailbox)
|
---|
453 | return self._untagged_response(typ, dat, 'ACL')
|
---|
454 |
|
---|
455 |
|
---|
456 | def getannotation(self, mailbox, entry, attribute):
|
---|
457 | """(typ, [data]) = <instance>.getannotation(mailbox, entry, attribute)
|
---|
458 | Retrieve ANNOTATIONs."""
|
---|
459 |
|
---|
460 | typ, dat = self._simple_command('GETANNOTATION', mailbox, entry, attribute)
|
---|
461 | return self._untagged_response(typ, dat, 'ANNOTATION')
|
---|
462 |
|
---|
463 |
|
---|
464 | def getquota(self, root):
|
---|
465 | """Get the quota root's resource usage and limits.
|
---|
466 |
|
---|
467 | Part of the IMAP4 QUOTA extension defined in rfc2087.
|
---|
468 |
|
---|
469 | (typ, [data]) = <instance>.getquota(root)
|
---|
470 | """
|
---|
471 | typ, dat = self._simple_command('GETQUOTA', root)
|
---|
472 | return self._untagged_response(typ, dat, 'QUOTA')
|
---|
473 |
|
---|
474 |
|
---|
475 | def getquotaroot(self, mailbox):
|
---|
476 | """Get the list of quota roots for the named mailbox.
|
---|
477 |
|
---|
478 | (typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox)
|
---|
479 | """
|
---|
480 | typ, dat = self._simple_command('GETQUOTAROOT', mailbox)
|
---|
481 | typ, quota = self._untagged_response(typ, dat, 'QUOTA')
|
---|
482 | typ, quotaroot = self._untagged_response(typ, dat, 'QUOTAROOT')
|
---|
483 | return typ, [quotaroot, quota]
|
---|
484 |
|
---|
485 |
|
---|
486 | def list(self, directory='""', pattern='*'):
|
---|
487 | """List mailbox names in directory matching pattern.
|
---|
488 |
|
---|
489 | (typ, [data]) = <instance>.list(directory='""', pattern='*')
|
---|
490 |
|
---|
491 | 'data' is list of LIST responses.
|
---|
492 | """
|
---|
493 | name = 'LIST'
|
---|
494 | typ, dat = self._simple_command(name, directory, pattern)
|
---|
495 | return self._untagged_response(typ, dat, name)
|
---|
496 |
|
---|
497 |
|
---|
498 | def login(self, user, password):
|
---|
499 | """Identify client using plaintext password.
|
---|
500 |
|
---|
501 | (typ, [data]) = <instance>.login(user, password)
|
---|
502 |
|
---|
503 | NB: 'password' will be quoted.
|
---|
504 | """
|
---|
505 | typ, dat = self._simple_command('LOGIN', user, self._quote(password))
|
---|
506 | if typ != 'OK':
|
---|
507 | raise self.error(dat[-1])
|
---|
508 | self.state = 'AUTH'
|
---|
509 | return typ, dat
|
---|
510 |
|
---|
511 |
|
---|
512 | def login_cram_md5(self, user, password):
|
---|
513 | """ Force use of CRAM-MD5 authentication.
|
---|
514 |
|
---|
515 | (typ, [data]) = <instance>.login_cram_md5(user, password)
|
---|
516 | """
|
---|
517 | self.user, self.password = user, password
|
---|
518 | return self.authenticate('CRAM-MD5', self._CRAM_MD5_AUTH)
|
---|
519 |
|
---|
520 |
|
---|
521 | def _CRAM_MD5_AUTH(self, challenge):
|
---|
522 | """ Authobject to use with CRAM-MD5 authentication. """
|
---|
523 | import hmac
|
---|
524 | return self.user + " " + hmac.HMAC(self.password, challenge).hexdigest()
|
---|
525 |
|
---|
526 |
|
---|
527 | def logout(self):
|
---|
528 | """Shutdown connection to server.
|
---|
529 |
|
---|
530 | (typ, [data]) = <instance>.logout()
|
---|
531 |
|
---|
532 | Returns server 'BYE' response.
|
---|
533 | """
|
---|
534 | self.state = 'LOGOUT'
|
---|
535 | try: typ, dat = self._simple_command('LOGOUT')
|
---|
536 | except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
|
---|
537 | self.shutdown()
|
---|
538 | if 'BYE' in self.untagged_responses:
|
---|
539 | return 'BYE', self.untagged_responses['BYE']
|
---|
540 | return typ, dat
|
---|
541 |
|
---|
542 |
|
---|
543 | def lsub(self, directory='""', pattern='*'):
|
---|
544 | """List 'subscribed' mailbox names in directory matching pattern.
|
---|
545 |
|
---|
546 | (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
|
---|
547 |
|
---|
548 | 'data' are tuples of message part envelope and data.
|
---|
549 | """
|
---|
550 | name = 'LSUB'
|
---|
551 | typ, dat = self._simple_command(name, directory, pattern)
|
---|
552 | return self._untagged_response(typ, dat, name)
|
---|
553 |
|
---|
554 | def myrights(self, mailbox):
|
---|
555 | """Show my ACLs for a mailbox (i.e. the rights that I have on mailbox).
|
---|
556 |
|
---|
557 | (typ, [data]) = <instance>.myrights(mailbox)
|
---|
558 | """
|
---|
559 | typ,dat = self._simple_command('MYRIGHTS', mailbox)
|
---|
560 | return self._untagged_response(typ, dat, 'MYRIGHTS')
|
---|
561 |
|
---|
562 | def namespace(self):
|
---|
563 | """ Returns IMAP namespaces ala rfc2342
|
---|
564 |
|
---|
565 | (typ, [data, ...]) = <instance>.namespace()
|
---|
566 | """
|
---|
567 | name = 'NAMESPACE'
|
---|
568 | typ, dat = self._simple_command(name)
|
---|
569 | return self._untagged_response(typ, dat, name)
|
---|
570 |
|
---|
571 |
|
---|
572 | def noop(self):
|
---|
573 | """Send NOOP command.
|
---|
574 |
|
---|
575 | (typ, [data]) = <instance>.noop()
|
---|
576 | """
|
---|
577 | if __debug__:
|
---|
578 | if self.debug >= 3:
|
---|
579 | self._dump_ur(self.untagged_responses)
|
---|
580 | return self._simple_command('NOOP')
|
---|
581 |
|
---|
582 |
|
---|
583 | def partial(self, message_num, message_part, start, length):
|
---|
584 | """Fetch truncated part of a message.
|
---|
585 |
|
---|
586 | (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
|
---|
587 |
|
---|
588 | 'data' is tuple of message part envelope and data.
|
---|
589 | """
|
---|
590 | name = 'PARTIAL'
|
---|
591 | typ, dat = self._simple_command(name, message_num, message_part, start, length)
|
---|
592 | return self._untagged_response(typ, dat, 'FETCH')
|
---|
593 |
|
---|
594 |
|
---|
595 | def proxyauth(self, user):
|
---|
596 | """Assume authentication as "user".
|
---|
597 |
|
---|
598 | Allows an authorised administrator to proxy into any user's
|
---|
599 | mailbox.
|
---|
600 |
|
---|
601 | (typ, [data]) = <instance>.proxyauth(user)
|
---|
602 | """
|
---|
603 |
|
---|
604 | name = 'PROXYAUTH'
|
---|
605 | return self._simple_command('PROXYAUTH', user)
|
---|
606 |
|
---|
607 |
|
---|
608 | def rename(self, oldmailbox, newmailbox):
|
---|
609 | """Rename old mailbox name to new.
|
---|
610 |
|
---|
611 | (typ, [data]) = <instance>.rename(oldmailbox, newmailbox)
|
---|
612 | """
|
---|
613 | return self._simple_command('RENAME', oldmailbox, newmailbox)
|
---|
614 |
|
---|
615 |
|
---|
616 | def search(self, charset, *criteria):
|
---|
617 | """Search mailbox for matching messages.
|
---|
618 |
|
---|
619 | (typ, [data]) = <instance>.search(charset, criterion, ...)
|
---|
620 |
|
---|
621 | 'data' is space separated list of matching message numbers.
|
---|
622 | """
|
---|
623 | name = 'SEARCH'
|
---|
624 | if charset:
|
---|
625 | typ, dat = self._simple_command(name, 'CHARSET', charset, *criteria)
|
---|
626 | else:
|
---|
627 | typ, dat = self._simple_command(name, *criteria)
|
---|
628 | return self._untagged_response(typ, dat, name)
|
---|
629 |
|
---|
630 |
|
---|
631 | def select(self, mailbox='INBOX', readonly=False):
|
---|
632 | """Select a mailbox.
|
---|
633 |
|
---|
634 | Flush all untagged responses.
|
---|
635 |
|
---|
636 | (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=False)
|
---|
637 |
|
---|
638 | 'data' is count of messages in mailbox ('EXISTS' response).
|
---|
639 |
|
---|
640 | Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so
|
---|
641 | other responses should be obtained via <instance>.response('FLAGS') etc.
|
---|
642 | """
|
---|
643 | self.untagged_responses = {} # Flush old responses.
|
---|
644 | self.is_readonly = readonly
|
---|
645 | if readonly:
|
---|
646 | name = 'EXAMINE'
|
---|
647 | else:
|
---|
648 | name = 'SELECT'
|
---|
649 | typ, dat = self._simple_command(name, mailbox)
|
---|
650 | if typ != 'OK':
|
---|
651 | self.state = 'AUTH' # Might have been 'SELECTED'
|
---|
652 | return typ, dat
|
---|
653 | self.state = 'SELECTED'
|
---|
654 | if 'READ-ONLY' in self.untagged_responses \
|
---|
655 | and not readonly:
|
---|
656 | if __debug__:
|
---|
657 | if self.debug >= 1:
|
---|
658 | self._dump_ur(self.untagged_responses)
|
---|
659 | raise self.readonly('%s is not writable' % mailbox)
|
---|
660 | return typ, self.untagged_responses.get('EXISTS', [None])
|
---|
661 |
|
---|
662 |
|
---|
663 | def setacl(self, mailbox, who, what):
|
---|
664 | """Set a mailbox acl.
|
---|
665 |
|
---|
666 | (typ, [data]) = <instance>.setacl(mailbox, who, what)
|
---|
667 | """
|
---|
668 | return self._simple_command('SETACL', mailbox, who, what)
|
---|
669 |
|
---|
670 |
|
---|
671 | def setannotation(self, *args):
|
---|
672 | """(typ, [data]) = <instance>.setannotation(mailbox[, entry, attribute]+)
|
---|
673 | Set ANNOTATIONs."""
|
---|
674 |
|
---|
675 | typ, dat = self._simple_command('SETANNOTATION', *args)
|
---|
676 | return self._untagged_response(typ, dat, 'ANNOTATION')
|
---|
677 |
|
---|
678 |
|
---|
679 | def setquota(self, root, limits):
|
---|
680 | """Set the quota root's resource limits.
|
---|
681 |
|
---|
682 | (typ, [data]) = <instance>.setquota(root, limits)
|
---|
683 | """
|
---|
684 | typ, dat = self._simple_command('SETQUOTA', root, limits)
|
---|
685 | return self._untagged_response(typ, dat, 'QUOTA')
|
---|
686 |
|
---|
687 |
|
---|
688 | def sort(self, sort_criteria, charset, *search_criteria):
|
---|
689 | """IMAP4rev1 extension SORT command.
|
---|
690 |
|
---|
691 | (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
|
---|
692 | """
|
---|
693 | name = 'SORT'
|
---|
694 | #if not name in self.capabilities: # Let the server decide!
|
---|
695 | # raise self.error('unimplemented extension command: %s' % name)
|
---|
696 | if (sort_criteria[0],sort_criteria[-1]) != ('(',')'):
|
---|
697 | sort_criteria = '(%s)' % sort_criteria
|
---|
698 | typ, dat = self._simple_command(name, sort_criteria, charset, *search_criteria)
|
---|
699 | return self._untagged_response(typ, dat, name)
|
---|
700 |
|
---|
701 |
|
---|
702 | def status(self, mailbox, names):
|
---|
703 | """Request named status conditions for mailbox.
|
---|
704 |
|
---|
705 | (typ, [data]) = <instance>.status(mailbox, names)
|
---|
706 | """
|
---|
707 | name = 'STATUS'
|
---|
708 | #if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide!
|
---|
709 | # raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
|
---|
710 | typ, dat = self._simple_command(name, mailbox, names)
|
---|
711 | return self._untagged_response(typ, dat, name)
|
---|
712 |
|
---|
713 |
|
---|
714 | def store(self, message_set, command, flags):
|
---|
715 | """Alters flag dispositions for messages in mailbox.
|
---|
716 |
|
---|
717 | (typ, [data]) = <instance>.store(message_set, command, flags)
|
---|
718 | """
|
---|
719 | if (flags[0],flags[-1]) != ('(',')'):
|
---|
720 | flags = '(%s)' % flags # Avoid quoting the flags
|
---|
721 | typ, dat = self._simple_command('STORE', message_set, command, flags)
|
---|
722 | return self._untagged_response(typ, dat, 'FETCH')
|
---|
723 |
|
---|
724 |
|
---|
725 | def subscribe(self, mailbox):
|
---|
726 | """Subscribe to new mailbox.
|
---|
727 |
|
---|
728 | (typ, [data]) = <instance>.subscribe(mailbox)
|
---|
729 | """
|
---|
730 | return self._simple_command('SUBSCRIBE', mailbox)
|
---|
731 |
|
---|
732 |
|
---|
733 | def thread(self, threading_algorithm, charset, *search_criteria):
|
---|
734 | """IMAPrev1 extension THREAD command.
|
---|
735 |
|
---|
736 | (type, [data]) = <instance>.thread(threading_algorithm, charset, search_criteria, ...)
|
---|
737 | """
|
---|
738 | name = 'THREAD'
|
---|
739 | typ, dat = self._simple_command(name, threading_algorithm, charset, *search_criteria)
|
---|
740 | return self._untagged_response(typ, dat, name)
|
---|
741 |
|
---|
742 |
|
---|
743 | def uid(self, command, *args):
|
---|
744 | """Execute "command arg ..." with messages identified by UID,
|
---|
745 | rather than message number.
|
---|
746 |
|
---|
747 | (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
|
---|
748 |
|
---|
749 | Returns response appropriate to 'command'.
|
---|
750 | """
|
---|
751 | command = command.upper()
|
---|
752 | if not command in Commands:
|
---|
753 | raise self.error("Unknown IMAP4 UID command: %s" % command)
|
---|
754 | if self.state not in Commands[command]:
|
---|
755 | raise self.error("command %s illegal in state %s, "
|
---|
756 | "only allowed in states %s" %
|
---|
757 | (command, self.state,
|
---|
758 | ', '.join(Commands[command])))
|
---|
759 | name = 'UID'
|
---|
760 | typ, dat = self._simple_command(name, command, *args)
|
---|
761 | if command in ('SEARCH', 'SORT', 'THREAD'):
|
---|
762 | name = command
|
---|
763 | else:
|
---|
764 | name = 'FETCH'
|
---|
765 | return self._untagged_response(typ, dat, name)
|
---|
766 |
|
---|
767 |
|
---|
768 | def unsubscribe(self, mailbox):
|
---|
769 | """Unsubscribe from old mailbox.
|
---|
770 |
|
---|
771 | (typ, [data]) = <instance>.unsubscribe(mailbox)
|
---|
772 | """
|
---|
773 | return self._simple_command('UNSUBSCRIBE', mailbox)
|
---|
774 |
|
---|
775 |
|
---|
776 | def xatom(self, name, *args):
|
---|
777 | """Allow simple extension commands
|
---|
778 | notified by server in CAPABILITY response.
|
---|
779 |
|
---|
780 | Assumes command is legal in current state.
|
---|
781 |
|
---|
782 | (typ, [data]) = <instance>.xatom(name, arg, ...)
|
---|
783 |
|
---|
784 | Returns response appropriate to extension command `name'.
|
---|
785 | """
|
---|
786 | name = name.upper()
|
---|
787 | #if not name in self.capabilities: # Let the server decide!
|
---|
788 | # raise self.error('unknown extension command: %s' % name)
|
---|
789 | if not name in Commands:
|
---|
790 | Commands[name] = (self.state,)
|
---|
791 | return self._simple_command(name, *args)
|
---|
792 |
|
---|
793 |
|
---|
794 |
|
---|
795 | # Private methods
|
---|
796 |
|
---|
797 |
|
---|
798 | def _append_untagged(self, typ, dat):
|
---|
799 |
|
---|
800 | if dat is None: dat = ''
|
---|
801 | ur = self.untagged_responses
|
---|
802 | if __debug__:
|
---|
803 | if self.debug >= 5:
|
---|
804 | self._mesg('untagged_responses[%s] %s += ["%s"]' %
|
---|
805 | (typ, len(ur.get(typ,'')), dat))
|
---|
806 | if typ in ur:
|
---|
807 | ur[typ].append(dat)
|
---|
808 | else:
|
---|
809 | ur[typ] = [dat]
|
---|
810 |
|
---|
811 |
|
---|
812 | def _check_bye(self):
|
---|
813 | bye = self.untagged_responses.get('BYE')
|
---|
814 | if bye:
|
---|
815 | raise self.abort(bye[-1])
|
---|
816 |
|
---|
817 |
|
---|
818 | def _command(self, name, *args):
|
---|
819 |
|
---|
820 | if self.state not in Commands[name]:
|
---|
821 | self.literal = None
|
---|
822 | raise self.error("command %s illegal in state %s, "
|
---|
823 | "only allowed in states %s" %
|
---|
824 | (name, self.state,
|
---|
825 | ', '.join(Commands[name])))
|
---|
826 |
|
---|
827 | for typ in ('OK', 'NO', 'BAD'):
|
---|
828 | if typ in self.untagged_responses:
|
---|
829 | del self.untagged_responses[typ]
|
---|
830 |
|
---|
831 | if 'READ-ONLY' in self.untagged_responses \
|
---|
832 | and not self.is_readonly:
|
---|
833 | raise self.readonly('mailbox status changed to READ-ONLY')
|
---|
834 |
|
---|
835 | tag = self._new_tag()
|
---|
836 | data = '%s %s' % (tag, name)
|
---|
837 | for arg in args:
|
---|
838 | if arg is None: continue
|
---|
839 | data = '%s %s' % (data, self._checkquote(arg))
|
---|
840 |
|
---|
841 | literal = self.literal
|
---|
842 | if literal is not None:
|
---|
843 | self.literal = None
|
---|
844 | if type(literal) is type(self._command):
|
---|
845 | literator = literal
|
---|
846 | else:
|
---|
847 | literator = None
|
---|
848 | data = '%s {%s}' % (data, len(literal))
|
---|
849 |
|
---|
850 | if __debug__:
|
---|
851 | if self.debug >= 4:
|
---|
852 | self._mesg('> %s' % data)
|
---|
853 | else:
|
---|
854 | self._log('> %s' % data)
|
---|
855 |
|
---|
856 | try:
|
---|
857 | self.send('%s%s' % (data, CRLF))
|
---|
858 | except (socket.error, OSError), val:
|
---|
859 | raise self.abort('socket error: %s' % val)
|
---|
860 |
|
---|
861 | if literal is None:
|
---|
862 | return tag
|
---|
863 |
|
---|
864 | while 1:
|
---|
865 | # Wait for continuation response
|
---|
866 |
|
---|
867 | while self._get_response():
|
---|
868 | if self.tagged_commands[tag]: # BAD/NO?
|
---|
869 | return tag
|
---|
870 |
|
---|
871 | # Send literal
|
---|
872 |
|
---|
873 | if literator:
|
---|
874 | literal = literator(self.continuation_response)
|
---|
875 |
|
---|
876 | if __debug__:
|
---|
877 | if self.debug >= 4:
|
---|
878 | self._mesg('write literal size %s' % len(literal))
|
---|
879 |
|
---|
880 | try:
|
---|
881 | self.send(literal)
|
---|
882 | self.send(CRLF)
|
---|
883 | except (socket.error, OSError), val:
|
---|
884 | raise self.abort('socket error: %s' % val)
|
---|
885 |
|
---|
886 | if not literator:
|
---|
887 | break
|
---|
888 |
|
---|
889 | return tag
|
---|
890 |
|
---|
891 |
|
---|
892 | def _command_complete(self, name, tag):
|
---|
893 | # BYE is expected after LOGOUT
|
---|
894 | if name != 'LOGOUT':
|
---|
895 | self._check_bye()
|
---|
896 | try:
|
---|
897 | typ, data = self._get_tagged_response(tag)
|
---|
898 | except self.abort, val:
|
---|
899 | raise self.abort('command: %s => %s' % (name, val))
|
---|
900 | except self.error, val:
|
---|
901 | raise self.error('command: %s => %s' % (name, val))
|
---|
902 | if name != 'LOGOUT':
|
---|
903 | self._check_bye()
|
---|
904 | if typ == 'BAD':
|
---|
905 | raise self.error('%s command error: %s %s' % (name, typ, data))
|
---|
906 | return typ, data
|
---|
907 |
|
---|
908 |
|
---|
909 | def _get_response(self):
|
---|
910 |
|
---|
911 | # Read response and store.
|
---|
912 | #
|
---|
913 | # Returns None for continuation responses,
|
---|
914 | # otherwise first response line received.
|
---|
915 |
|
---|
916 | resp = self._get_line()
|
---|
917 |
|
---|
918 | # Command completion response?
|
---|
919 |
|
---|
920 | if self._match(self.tagre, resp):
|
---|
921 | tag = self.mo.group('tag')
|
---|
922 | if not tag in self.tagged_commands:
|
---|
923 | raise self.abort('unexpected tagged response: %s' % resp)
|
---|
924 |
|
---|
925 | typ = self.mo.group('type')
|
---|
926 | dat = self.mo.group('data')
|
---|
927 | self.tagged_commands[tag] = (typ, [dat])
|
---|
928 | else:
|
---|
929 | dat2 = None
|
---|
930 |
|
---|
931 | # '*' (untagged) responses?
|
---|
932 |
|
---|
933 | if not self._match(Untagged_response, resp):
|
---|
934 | if self._match(Untagged_status, resp):
|
---|
935 | dat2 = self.mo.group('data2')
|
---|
936 |
|
---|
937 | if self.mo is None:
|
---|
938 | # Only other possibility is '+' (continuation) response...
|
---|
939 |
|
---|
940 | if self._match(Continuation, resp):
|
---|
941 | self.continuation_response = self.mo.group('data')
|
---|
942 | return None # NB: indicates continuation
|
---|
943 |
|
---|
944 | raise self.abort("unexpected response: '%s'" % resp)
|
---|
945 |
|
---|
946 | typ = self.mo.group('type')
|
---|
947 | dat = self.mo.group('data')
|
---|
948 | if dat is None: dat = '' # Null untagged response
|
---|
949 | if dat2: dat = dat + ' ' + dat2
|
---|
950 |
|
---|
951 | # Is there a literal to come?
|
---|
952 |
|
---|
953 | while self._match(Literal, dat):
|
---|
954 |
|
---|
955 | # Read literal direct from connection.
|
---|
956 |
|
---|
957 | size = int(self.mo.group('size'))
|
---|
958 | if __debug__:
|
---|
959 | if self.debug >= 4:
|
---|
960 | self._mesg('read literal size %s' % size)
|
---|
961 | data = self.read(size)
|
---|
962 |
|
---|
963 | # Store response with literal as tuple
|
---|
964 |
|
---|
965 | self._append_untagged(typ, (dat, data))
|
---|
966 |
|
---|
967 | # Read trailer - possibly containing another literal
|
---|
968 |
|
---|
969 | dat = self._get_line()
|
---|
970 |
|
---|
971 | self._append_untagged(typ, dat)
|
---|
972 |
|
---|
973 | # Bracketed response information?
|
---|
974 |
|
---|
975 | if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
|
---|
976 | self._append_untagged(self.mo.group('type'), self.mo.group('data'))
|
---|
977 |
|
---|
978 | if __debug__:
|
---|
979 | if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
|
---|
980 | self._mesg('%s response: %s' % (typ, dat))
|
---|
981 |
|
---|
982 | return resp
|
---|
983 |
|
---|
984 |
|
---|
985 | def _get_tagged_response(self, tag):
|
---|
986 |
|
---|
987 | while 1:
|
---|
988 | result = self.tagged_commands[tag]
|
---|
989 | if result is not None:
|
---|
990 | del self.tagged_commands[tag]
|
---|
991 | return result
|
---|
992 |
|
---|
993 | # Some have reported "unexpected response" exceptions.
|
---|
994 | # Note that ignoring them here causes loops.
|
---|
995 | # Instead, send me details of the unexpected response and
|
---|
996 | # I'll update the code in `_get_response()'.
|
---|
997 |
|
---|
998 | try:
|
---|
999 | self._get_response()
|
---|
1000 | except self.abort, val:
|
---|
1001 | if __debug__:
|
---|
1002 | if self.debug >= 1:
|
---|
1003 | self.print_log()
|
---|
1004 | raise
|
---|
1005 |
|
---|
1006 |
|
---|
1007 | def _get_line(self):
|
---|
1008 |
|
---|
1009 | line = self.readline()
|
---|
1010 | if not line:
|
---|
1011 | raise self.abort('socket error: EOF')
|
---|
1012 |
|
---|
1013 | # Protocol mandates all lines terminated by CRLF
|
---|
1014 | if not line.endswith('\r\n'):
|
---|
1015 | raise self.abort('socket error: unterminated line')
|
---|
1016 |
|
---|
1017 | line = line[:-2]
|
---|
1018 | if __debug__:
|
---|
1019 | if self.debug >= 4:
|
---|
1020 | self._mesg('< %s' % line)
|
---|
1021 | else:
|
---|
1022 | self._log('< %s' % line)
|
---|
1023 | return line
|
---|
1024 |
|
---|
1025 |
|
---|
1026 | def _match(self, cre, s):
|
---|
1027 |
|
---|
1028 | # Run compiled regular expression match method on 's'.
|
---|
1029 | # Save result, return success.
|
---|
1030 |
|
---|
1031 | self.mo = cre.match(s)
|
---|
1032 | if __debug__:
|
---|
1033 | if self.mo is not None and self.debug >= 5:
|
---|
1034 | self._mesg("\tmatched r'%s' => %r" % (cre.pattern, self.mo.groups()))
|
---|
1035 | return self.mo is not None
|
---|
1036 |
|
---|
1037 |
|
---|
1038 | def _new_tag(self):
|
---|
1039 |
|
---|
1040 | tag = '%s%s' % (self.tagpre, self.tagnum)
|
---|
1041 | self.tagnum = self.tagnum + 1
|
---|
1042 | self.tagged_commands[tag] = None
|
---|
1043 | return tag
|
---|
1044 |
|
---|
1045 |
|
---|
1046 | def _checkquote(self, arg):
|
---|
1047 |
|
---|
1048 | # Must quote command args if non-alphanumeric chars present,
|
---|
1049 | # and not already quoted.
|
---|
1050 |
|
---|
1051 | if type(arg) is not type(''):
|
---|
1052 | return arg
|
---|
1053 | if len(arg) >= 2 and (arg[0],arg[-1]) in (('(',')'),('"','"')):
|
---|
1054 | return arg
|
---|
1055 | if arg and self.mustquote.search(arg) is None:
|
---|
1056 | return arg
|
---|
1057 | return self._quote(arg)
|
---|
1058 |
|
---|
1059 |
|
---|
1060 | def _quote(self, arg):
|
---|
1061 |
|
---|
1062 | arg = arg.replace('\\', '\\\\')
|
---|
1063 | arg = arg.replace('"', '\\"')
|
---|
1064 |
|
---|
1065 | return '"%s"' % arg
|
---|
1066 |
|
---|
1067 |
|
---|
1068 | def _simple_command(self, name, *args):
|
---|
1069 |
|
---|
1070 | return self._command_complete(name, self._command(name, *args))
|
---|
1071 |
|
---|
1072 |
|
---|
1073 | def _untagged_response(self, typ, dat, name):
|
---|
1074 |
|
---|
1075 | if typ == 'NO':
|
---|
1076 | return typ, dat
|
---|
1077 | if not name in self.untagged_responses:
|
---|
1078 | return typ, [None]
|
---|
1079 | data = self.untagged_responses.pop(name)
|
---|
1080 | if __debug__:
|
---|
1081 | if self.debug >= 5:
|
---|
1082 | self._mesg('untagged_responses[%s] => %s' % (name, data))
|
---|
1083 | return typ, data
|
---|
1084 |
|
---|
1085 |
|
---|
1086 | if __debug__:
|
---|
1087 |
|
---|
1088 | def _mesg(self, s, secs=None):
|
---|
1089 | if secs is None:
|
---|
1090 | secs = time.time()
|
---|
1091 | tm = time.strftime('%M:%S', time.localtime(secs))
|
---|
1092 | sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s))
|
---|
1093 | sys.stderr.flush()
|
---|
1094 |
|
---|
1095 | def _dump_ur(self, dict):
|
---|
1096 | # Dump untagged responses (in `dict').
|
---|
1097 | l = dict.items()
|
---|
1098 | if not l: return
|
---|
1099 | t = '\n\t\t'
|
---|
1100 | l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l)
|
---|
1101 | self._mesg('untagged responses dump:%s%s' % (t, t.join(l)))
|
---|
1102 |
|
---|
1103 | def _log(self, line):
|
---|
1104 | # Keep log of last `_cmd_log_len' interactions for debugging.
|
---|
1105 | self._cmd_log[self._cmd_log_idx] = (line, time.time())
|
---|
1106 | self._cmd_log_idx += 1
|
---|
1107 | if self._cmd_log_idx >= self._cmd_log_len:
|
---|
1108 | self._cmd_log_idx = 0
|
---|
1109 |
|
---|
1110 | def print_log(self):
|
---|
1111 | self._mesg('last %d IMAP4 interactions:' % len(self._cmd_log))
|
---|
1112 | i, n = self._cmd_log_idx, self._cmd_log_len
|
---|
1113 | while n:
|
---|
1114 | try:
|
---|
1115 | self._mesg(*self._cmd_log[i])
|
---|
1116 | except:
|
---|
1117 | pass
|
---|
1118 | i += 1
|
---|
1119 | if i >= self._cmd_log_len:
|
---|
1120 | i = 0
|
---|
1121 | n -= 1
|
---|
1122 |
|
---|
1123 |
|
---|
1124 |
|
---|
1125 | try:
|
---|
1126 | import ssl
|
---|
1127 | except ImportError:
|
---|
1128 | pass
|
---|
1129 | else:
|
---|
1130 | class IMAP4_SSL(IMAP4):
|
---|
1131 |
|
---|
1132 | """IMAP4 client class over SSL connection
|
---|
1133 |
|
---|
1134 | Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile]]]])
|
---|
1135 |
|
---|
1136 | host - host's name (default: localhost);
|
---|
1137 | port - port number (default: standard IMAP4 SSL port).
|
---|
1138 | keyfile - PEM formatted file that contains your private key (default: None);
|
---|
1139 | certfile - PEM formatted certificate chain file (default: None);
|
---|
1140 |
|
---|
1141 | for more documentation see the docstring of the parent class IMAP4.
|
---|
1142 | """
|
---|
1143 |
|
---|
1144 |
|
---|
1145 | def __init__(self, host = '', port = IMAP4_SSL_PORT, keyfile = None, certfile = None):
|
---|
1146 | self.keyfile = keyfile
|
---|
1147 | self.certfile = certfile
|
---|
1148 | IMAP4.__init__(self, host, port)
|
---|
1149 |
|
---|
1150 |
|
---|
1151 | def open(self, host = '', port = IMAP4_SSL_PORT):
|
---|
1152 | """Setup connection to remote server on "host:port".
|
---|
1153 | (default: localhost:standard IMAP4 SSL port).
|
---|
1154 | This connection will be used by the routines:
|
---|
1155 | read, readline, send, shutdown.
|
---|
1156 | """
|
---|
1157 | self.host = host
|
---|
1158 | self.port = port
|
---|
1159 | self.sock = socket.create_connection((host, port))
|
---|
1160 | self.sslobj = ssl.wrap_socket(self.sock, self.keyfile, self.certfile)
|
---|
1161 | self.file = self.sslobj.makefile('rb')
|
---|
1162 |
|
---|
1163 |
|
---|
1164 | def read(self, size):
|
---|
1165 | """Read 'size' bytes from remote."""
|
---|
1166 | return self.file.read(size)
|
---|
1167 |
|
---|
1168 |
|
---|
1169 | def readline(self):
|
---|
1170 | """Read line from remote."""
|
---|
1171 | return self.file.readline()
|
---|
1172 |
|
---|
1173 |
|
---|
1174 | def send(self, data):
|
---|
1175 | """Send data to remote."""
|
---|
1176 | bytes = len(data)
|
---|
1177 | while bytes > 0:
|
---|
1178 | sent = self.sslobj.write(data)
|
---|
1179 | if sent == bytes:
|
---|
1180 | break # avoid copy
|
---|
1181 | data = data[sent:]
|
---|
1182 | bytes = bytes - sent
|
---|
1183 |
|
---|
1184 |
|
---|
1185 | def shutdown(self):
|
---|
1186 | """Close I/O established in "open"."""
|
---|
1187 | self.file.close()
|
---|
1188 | self.sock.close()
|
---|
1189 |
|
---|
1190 |
|
---|
1191 | def socket(self):
|
---|
1192 | """Return socket instance used to connect to IMAP4 server.
|
---|
1193 |
|
---|
1194 | socket = <instance>.socket()
|
---|
1195 | """
|
---|
1196 | return self.sock
|
---|
1197 |
|
---|
1198 |
|
---|
1199 | def ssl(self):
|
---|
1200 | """Return SSLObject instance used to communicate with the IMAP4 server.
|
---|
1201 |
|
---|
1202 | ssl = ssl.wrap_socket(<instance>.socket)
|
---|
1203 | """
|
---|
1204 | return self.sslobj
|
---|
1205 |
|
---|
1206 | __all__.append("IMAP4_SSL")
|
---|
1207 |
|
---|
1208 |
|
---|
1209 | class IMAP4_stream(IMAP4):
|
---|
1210 |
|
---|
1211 | """IMAP4 client class over a stream
|
---|
1212 |
|
---|
1213 | Instantiate with: IMAP4_stream(command)
|
---|
1214 |
|
---|
1215 | where "command" is a string that can be passed to subprocess.Popen()
|
---|
1216 |
|
---|
1217 | for more documentation see the docstring of the parent class IMAP4.
|
---|
1218 | """
|
---|
1219 |
|
---|
1220 |
|
---|
1221 | def __init__(self, command):
|
---|
1222 | self.command = command
|
---|
1223 | IMAP4.__init__(self)
|
---|
1224 |
|
---|
1225 |
|
---|
1226 | def open(self, host = None, port = None):
|
---|
1227 | """Setup a stream connection.
|
---|
1228 | This connection will be used by the routines:
|
---|
1229 | read, readline, send, shutdown.
|
---|
1230 | """
|
---|
1231 | self.host = None # For compatibility with parent class
|
---|
1232 | self.port = None
|
---|
1233 | self.sock = None
|
---|
1234 | self.file = None
|
---|
1235 | self.process = subprocess.Popen(self.command,
|
---|
1236 | stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
---|
1237 | shell=True, close_fds=True)
|
---|
1238 | self.writefile = self.process.stdin
|
---|
1239 | self.readfile = self.process.stdout
|
---|
1240 |
|
---|
1241 |
|
---|
1242 | def read(self, size):
|
---|
1243 | """Read 'size' bytes from remote."""
|
---|
1244 | return self.readfile.read(size)
|
---|
1245 |
|
---|
1246 |
|
---|
1247 | def readline(self):
|
---|
1248 | """Read line from remote."""
|
---|
1249 | return self.readfile.readline()
|
---|
1250 |
|
---|
1251 |
|
---|
1252 | def send(self, data):
|
---|
1253 | """Send data to remote."""
|
---|
1254 | self.writefile.write(data)
|
---|
1255 | self.writefile.flush()
|
---|
1256 |
|
---|
1257 |
|
---|
1258 | def shutdown(self):
|
---|
1259 | """Close I/O established in "open"."""
|
---|
1260 | self.readfile.close()
|
---|
1261 | self.writefile.close()
|
---|
1262 | self.process.wait()
|
---|
1263 |
|
---|
1264 |
|
---|
1265 |
|
---|
1266 | class _Authenticator:
|
---|
1267 |
|
---|
1268 | """Private class to provide en/decoding
|
---|
1269 | for base64-based authentication conversation.
|
---|
1270 | """
|
---|
1271 |
|
---|
1272 | def __init__(self, mechinst):
|
---|
1273 | self.mech = mechinst # Callable object to provide/process data
|
---|
1274 |
|
---|
1275 | def process(self, data):
|
---|
1276 | ret = self.mech(self.decode(data))
|
---|
1277 | if ret is None:
|
---|
1278 | return '*' # Abort conversation
|
---|
1279 | return self.encode(ret)
|
---|
1280 |
|
---|
1281 | def encode(self, inp):
|
---|
1282 | #
|
---|
1283 | # Invoke binascii.b2a_base64 iteratively with
|
---|
1284 | # short even length buffers, strip the trailing
|
---|
1285 | # line feed from the result and append. "Even"
|
---|
1286 | # means a number that factors to both 6 and 8,
|
---|
1287 | # so when it gets to the end of the 8-bit input
|
---|
1288 | # there's no partial 6-bit output.
|
---|
1289 | #
|
---|
1290 | oup = ''
|
---|
1291 | while inp:
|
---|
1292 | if len(inp) > 48:
|
---|
1293 | t = inp[:48]
|
---|
1294 | inp = inp[48:]
|
---|
1295 | else:
|
---|
1296 | t = inp
|
---|
1297 | inp = ''
|
---|
1298 | e = binascii.b2a_base64(t)
|
---|
1299 | if e:
|
---|
1300 | oup = oup + e[:-1]
|
---|
1301 | return oup
|
---|
1302 |
|
---|
1303 | def decode(self, inp):
|
---|
1304 | if not inp:
|
---|
1305 | return ''
|
---|
1306 | return binascii.a2b_base64(inp)
|
---|
1307 |
|
---|
1308 |
|
---|
1309 |
|
---|
1310 | Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
|
---|
1311 | 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
|
---|
1312 |
|
---|
1313 | def Internaldate2tuple(resp):
|
---|
1314 | """Parse an IMAP4 INTERNALDATE string.
|
---|
1315 |
|
---|
1316 | Return corresponding local time. The return value is a
|
---|
1317 | time.struct_time instance or None if the string has wrong format.
|
---|
1318 | """
|
---|
1319 |
|
---|
1320 | mo = InternalDate.match(resp)
|
---|
1321 | if not mo:
|
---|
1322 | return None
|
---|
1323 |
|
---|
1324 | mon = Mon2num[mo.group('mon')]
|
---|
1325 | zonen = mo.group('zonen')
|
---|
1326 |
|
---|
1327 | day = int(mo.group('day'))
|
---|
1328 | year = int(mo.group('year'))
|
---|
1329 | hour = int(mo.group('hour'))
|
---|
1330 | min = int(mo.group('min'))
|
---|
1331 | sec = int(mo.group('sec'))
|
---|
1332 | zoneh = int(mo.group('zoneh'))
|
---|
1333 | zonem = int(mo.group('zonem'))
|
---|
1334 |
|
---|
1335 | # INTERNALDATE timezone must be subtracted to get UT
|
---|
1336 |
|
---|
1337 | zone = (zoneh*60 + zonem)*60
|
---|
1338 | if zonen == '-':
|
---|
1339 | zone = -zone
|
---|
1340 |
|
---|
1341 | tt = (year, mon, day, hour, min, sec, -1, -1, -1)
|
---|
1342 |
|
---|
1343 | utc = time.mktime(tt)
|
---|
1344 |
|
---|
1345 | # Following is necessary because the time module has no 'mkgmtime'.
|
---|
1346 | # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
|
---|
1347 |
|
---|
1348 | lt = time.localtime(utc)
|
---|
1349 | if time.daylight and lt[-1]:
|
---|
1350 | zone = zone + time.altzone
|
---|
1351 | else:
|
---|
1352 | zone = zone + time.timezone
|
---|
1353 |
|
---|
1354 | return time.localtime(utc - zone)
|
---|
1355 |
|
---|
1356 |
|
---|
1357 |
|
---|
1358 | def Int2AP(num):
|
---|
1359 |
|
---|
1360 | """Convert integer to A-P string representation."""
|
---|
1361 |
|
---|
1362 | val = ''; AP = 'ABCDEFGHIJKLMNOP'
|
---|
1363 | num = int(abs(num))
|
---|
1364 | while num:
|
---|
1365 | num, mod = divmod(num, 16)
|
---|
1366 | val = AP[mod] + val
|
---|
1367 | return val
|
---|
1368 |
|
---|
1369 |
|
---|
1370 |
|
---|
1371 | def ParseFlags(resp):
|
---|
1372 |
|
---|
1373 | """Convert IMAP4 flags response to python tuple."""
|
---|
1374 |
|
---|
1375 | mo = Flags.match(resp)
|
---|
1376 | if not mo:
|
---|
1377 | return ()
|
---|
1378 |
|
---|
1379 | return tuple(mo.group('flags').split())
|
---|
1380 |
|
---|
1381 |
|
---|
1382 | def Time2Internaldate(date_time):
|
---|
1383 |
|
---|
1384 | """Convert date_time to IMAP4 INTERNALDATE representation.
|
---|
1385 |
|
---|
1386 | Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'. The
|
---|
1387 | date_time argument can be a number (int or float) representing
|
---|
1388 | seconds since epoch (as returned by time.time()), a 9-tuple
|
---|
1389 | representing local time (as returned by time.localtime()), or a
|
---|
1390 | double-quoted string. In the last case, it is assumed to already
|
---|
1391 | be in the correct format.
|
---|
1392 | """
|
---|
1393 |
|
---|
1394 | if isinstance(date_time, (int, float)):
|
---|
1395 | tt = time.localtime(date_time)
|
---|
1396 | elif isinstance(date_time, (tuple, time.struct_time)):
|
---|
1397 | tt = date_time
|
---|
1398 | elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'):
|
---|
1399 | return date_time # Assume in correct format
|
---|
1400 | else:
|
---|
1401 | raise ValueError("date_time not of a known type")
|
---|
1402 |
|
---|
1403 | dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
|
---|
1404 | if dt[0] == '0':
|
---|
1405 | dt = ' ' + dt[1:]
|
---|
1406 | if time.daylight and tt[-1]:
|
---|
1407 | zone = -time.altzone
|
---|
1408 | else:
|
---|
1409 | zone = -time.timezone
|
---|
1410 | return '"' + dt + " %+03d%02d" % divmod(zone//60, 60) + '"'
|
---|
1411 |
|
---|
1412 |
|
---|
1413 |
|
---|
1414 | if __name__ == '__main__':
|
---|
1415 |
|
---|
1416 | # To test: invoke either as 'python imaplib.py [IMAP4_server_hostname]'
|
---|
1417 | # or 'python imaplib.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"'
|
---|
1418 | # to test the IMAP4_stream class
|
---|
1419 |
|
---|
1420 | import getopt, getpass
|
---|
1421 |
|
---|
1422 | try:
|
---|
1423 | optlist, args = getopt.getopt(sys.argv[1:], 'd:s:')
|
---|
1424 | except getopt.error, val:
|
---|
1425 | optlist, args = (), ()
|
---|
1426 |
|
---|
1427 | stream_command = None
|
---|
1428 | for opt,val in optlist:
|
---|
1429 | if opt == '-d':
|
---|
1430 | Debug = int(val)
|
---|
1431 | elif opt == '-s':
|
---|
1432 | stream_command = val
|
---|
1433 | if not args: args = (stream_command,)
|
---|
1434 |
|
---|
1435 | if not args: args = ('',)
|
---|
1436 |
|
---|
1437 | host = args[0]
|
---|
1438 |
|
---|
1439 | USER = getpass.getuser()
|
---|
1440 | PASSWD = getpass.getpass("IMAP password for %s on %s: " % (USER, host or "localhost"))
|
---|
1441 |
|
---|
1442 | test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER, 'lf':'\n'}
|
---|
1443 | test_seq1 = (
|
---|
1444 | ('login', (USER, PASSWD)),
|
---|
1445 | ('create', ('/tmp/xxx 1',)),
|
---|
1446 | ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
|
---|
1447 | ('CREATE', ('/tmp/yyz 2',)),
|
---|
1448 | ('append', ('/tmp/yyz 2', None, None, test_mesg)),
|
---|
1449 | ('list', ('/tmp', 'yy*')),
|
---|
1450 | ('select', ('/tmp/yyz 2',)),
|
---|
1451 | ('search', (None, 'SUBJECT', 'test')),
|
---|
1452 | ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')),
|
---|
1453 | ('store', ('1', 'FLAGS', '(\Deleted)')),
|
---|
1454 | ('namespace', ()),
|
---|
1455 | ('expunge', ()),
|
---|
1456 | ('recent', ()),
|
---|
1457 | ('close', ()),
|
---|
1458 | )
|
---|
1459 |
|
---|
1460 | test_seq2 = (
|
---|
1461 | ('select', ()),
|
---|
1462 | ('response',('UIDVALIDITY',)),
|
---|
1463 | ('uid', ('SEARCH', 'ALL')),
|
---|
1464 | ('response', ('EXISTS',)),
|
---|
1465 | ('append', (None, None, None, test_mesg)),
|
---|
1466 | ('recent', ()),
|
---|
1467 | ('logout', ()),
|
---|
1468 | )
|
---|
1469 |
|
---|
1470 | def run(cmd, args):
|
---|
1471 | M._mesg('%s %s' % (cmd, args))
|
---|
1472 | typ, dat = getattr(M, cmd)(*args)
|
---|
1473 | M._mesg('%s => %s %s' % (cmd, typ, dat))
|
---|
1474 | if typ == 'NO': raise dat[0]
|
---|
1475 | return dat
|
---|
1476 |
|
---|
1477 | try:
|
---|
1478 | if stream_command:
|
---|
1479 | M = IMAP4_stream(stream_command)
|
---|
1480 | else:
|
---|
1481 | M = IMAP4(host)
|
---|
1482 | if M.state == 'AUTH':
|
---|
1483 | test_seq1 = test_seq1[1:] # Login not needed
|
---|
1484 | M._mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
|
---|
1485 | M._mesg('CAPABILITIES = %r' % (M.capabilities,))
|
---|
1486 |
|
---|
1487 | for cmd,args in test_seq1:
|
---|
1488 | run(cmd, args)
|
---|
1489 |
|
---|
1490 | for ml in run('list', ('/tmp/', 'yy%')):
|
---|
1491 | mo = re.match(r'.*"([^"]+)"$', ml)
|
---|
1492 | if mo: path = mo.group(1)
|
---|
1493 | else: path = ml.split()[-1]
|
---|
1494 | run('delete', (path,))
|
---|
1495 |
|
---|
1496 | for cmd,args in test_seq2:
|
---|
1497 | dat = run(cmd, args)
|
---|
1498 |
|
---|
1499 | if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
|
---|
1500 | continue
|
---|
1501 |
|
---|
1502 | uid = dat[-1].split()
|
---|
1503 | if not uid: continue
|
---|
1504 | run('uid', ('FETCH', '%s' % uid[-1],
|
---|
1505 | '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
|
---|
1506 |
|
---|
1507 | print '\nAll tests OK.'
|
---|
1508 |
|
---|
1509 | except:
|
---|
1510 | print '\nTests failed.'
|
---|
1511 |
|
---|
1512 | if not Debug:
|
---|
1513 | print '''
|
---|
1514 | If you would like to see debugging output,
|
---|
1515 | try: %s -d5
|
---|
1516 | ''' % sys.argv[0]
|
---|
1517 |
|
---|
1518 | raise
|
---|