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, 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 | self.sock.close()
|
---|
252 |
|
---|
253 |
|
---|
254 | def socket(self):
|
---|
255 | """Return socket instance used to connect to IMAP4 server.
|
---|
256 |
|
---|
257 | socket = <instance>.socket()
|
---|
258 | """
|
---|
259 | return self.sock
|
---|
260 |
|
---|
261 |
|
---|
262 |
|
---|
263 | # Utility methods
|
---|
264 |
|
---|
265 |
|
---|
266 | def recent(self):
|
---|
267 | """Return most recent 'RECENT' responses if any exist,
|
---|
268 | else prompt server for an update using the 'NOOP' command.
|
---|
269 |
|
---|
270 | (typ, [data]) = <instance>.recent()
|
---|
271 |
|
---|
272 | 'data' is None if no new messages,
|
---|
273 | else list of RECENT responses, most recent last.
|
---|
274 | """
|
---|
275 | name = 'RECENT'
|
---|
276 | typ, dat = self._untagged_response('OK', [None], name)
|
---|
277 | if dat[-1]:
|
---|
278 | return typ, dat
|
---|
279 | typ, dat = self.noop() # Prod server for response
|
---|
280 | return self._untagged_response(typ, dat, name)
|
---|
281 |
|
---|
282 |
|
---|
283 | def response(self, code):
|
---|
284 | """Return data for response 'code' if received, or None.
|
---|
285 |
|
---|
286 | Old value for response 'code' is cleared.
|
---|
287 |
|
---|
288 | (code, [data]) = <instance>.response(code)
|
---|
289 | """
|
---|
290 | return self._untagged_response(code, [None], code.upper())
|
---|
291 |
|
---|
292 |
|
---|
293 |
|
---|
294 | # IMAP4 commands
|
---|
295 |
|
---|
296 |
|
---|
297 | def append(self, mailbox, flags, date_time, message):
|
---|
298 | """Append message to named mailbox.
|
---|
299 |
|
---|
300 | (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
|
---|
301 |
|
---|
302 | All args except `message' can be None.
|
---|
303 | """
|
---|
304 | name = 'APPEND'
|
---|
305 | if not mailbox:
|
---|
306 | mailbox = 'INBOX'
|
---|
307 | if flags:
|
---|
308 | if (flags[0],flags[-1]) != ('(',')'):
|
---|
309 | flags = '(%s)' % flags
|
---|
310 | else:
|
---|
311 | flags = None
|
---|
312 | if date_time:
|
---|
313 | date_time = Time2Internaldate(date_time)
|
---|
314 | else:
|
---|
315 | date_time = None
|
---|
316 | self.literal = MapCRLF.sub(CRLF, message)
|
---|
317 | return self._simple_command(name, mailbox, flags, date_time)
|
---|
318 |
|
---|
319 |
|
---|
320 | def authenticate(self, mechanism, authobject):
|
---|
321 | """Authenticate command - requires response processing.
|
---|
322 |
|
---|
323 | 'mechanism' specifies which authentication mechanism is to
|
---|
324 | be used - it must appear in <instance>.capabilities in the
|
---|
325 | form AUTH=<mechanism>.
|
---|
326 |
|
---|
327 | 'authobject' must be a callable object:
|
---|
328 |
|
---|
329 | data = authobject(response)
|
---|
330 |
|
---|
331 | It will be called to process server continuation responses.
|
---|
332 | It should return data that will be encoded and sent to server.
|
---|
333 | It should return None if the client abort response '*' should
|
---|
334 | be sent instead.
|
---|
335 | """
|
---|
336 | mech = mechanism.upper()
|
---|
337 | # XXX: shouldn't this code be removed, not commented out?
|
---|
338 | #cap = 'AUTH=%s' % mech
|
---|
339 | #if not cap in self.capabilities: # Let the server decide!
|
---|
340 | # raise self.error("Server doesn't allow %s authentication." % mech)
|
---|
341 | self.literal = _Authenticator(authobject).process
|
---|
342 | typ, dat = self._simple_command('AUTHENTICATE', mech)
|
---|
343 | if typ != 'OK':
|
---|
344 | raise self.error(dat[-1])
|
---|
345 | self.state = 'AUTH'
|
---|
346 | return typ, dat
|
---|
347 |
|
---|
348 |
|
---|
349 | def capability(self):
|
---|
350 | """(typ, [data]) = <instance>.capability()
|
---|
351 | Fetch capabilities list from server."""
|
---|
352 |
|
---|
353 | name = 'CAPABILITY'
|
---|
354 | typ, dat = self._simple_command(name)
|
---|
355 | return self._untagged_response(typ, dat, name)
|
---|
356 |
|
---|
357 |
|
---|
358 | def check(self):
|
---|
359 | """Checkpoint mailbox on server.
|
---|
360 |
|
---|
361 | (typ, [data]) = <instance>.check()
|
---|
362 | """
|
---|
363 | return self._simple_command('CHECK')
|
---|
364 |
|
---|
365 |
|
---|
366 | def close(self):
|
---|
367 | """Close currently selected mailbox.
|
---|
368 |
|
---|
369 | Deleted messages are removed from writable mailbox.
|
---|
370 | This is the recommended command before 'LOGOUT'.
|
---|
371 |
|
---|
372 | (typ, [data]) = <instance>.close()
|
---|
373 | """
|
---|
374 | try:
|
---|
375 | typ, dat = self._simple_command('CLOSE')
|
---|
376 | finally:
|
---|
377 | self.state = 'AUTH'
|
---|
378 | return typ, dat
|
---|
379 |
|
---|
380 |
|
---|
381 | def copy(self, message_set, new_mailbox):
|
---|
382 | """Copy 'message_set' messages onto end of 'new_mailbox'.
|
---|
383 |
|
---|
384 | (typ, [data]) = <instance>.copy(message_set, new_mailbox)
|
---|
385 | """
|
---|
386 | return self._simple_command('COPY', message_set, new_mailbox)
|
---|
387 |
|
---|
388 |
|
---|
389 | def create(self, mailbox):
|
---|
390 | """Create new mailbox.
|
---|
391 |
|
---|
392 | (typ, [data]) = <instance>.create(mailbox)
|
---|
393 | """
|
---|
394 | return self._simple_command('CREATE', mailbox)
|
---|
395 |
|
---|
396 |
|
---|
397 | def delete(self, mailbox):
|
---|
398 | """Delete old mailbox.
|
---|
399 |
|
---|
400 | (typ, [data]) = <instance>.delete(mailbox)
|
---|
401 | """
|
---|
402 | return self._simple_command('DELETE', mailbox)
|
---|
403 |
|
---|
404 | def deleteacl(self, mailbox, who):
|
---|
405 | """Delete the ACLs (remove any rights) set for who on mailbox.
|
---|
406 |
|
---|
407 | (typ, [data]) = <instance>.deleteacl(mailbox, who)
|
---|
408 | """
|
---|
409 | return self._simple_command('DELETEACL', mailbox, who)
|
---|
410 |
|
---|
411 | def expunge(self):
|
---|
412 | """Permanently remove deleted items from selected mailbox.
|
---|
413 |
|
---|
414 | Generates 'EXPUNGE' response for each deleted message.
|
---|
415 |
|
---|
416 | (typ, [data]) = <instance>.expunge()
|
---|
417 |
|
---|
418 | 'data' is list of 'EXPUNGE'd message numbers in order received.
|
---|
419 | """
|
---|
420 | name = 'EXPUNGE'
|
---|
421 | typ, dat = self._simple_command(name)
|
---|
422 | return self._untagged_response(typ, dat, name)
|
---|
423 |
|
---|
424 |
|
---|
425 | def fetch(self, message_set, message_parts):
|
---|
426 | """Fetch (parts of) messages.
|
---|
427 |
|
---|
428 | (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
|
---|
429 |
|
---|
430 | 'message_parts' should be a string of selected parts
|
---|
431 | enclosed in parentheses, eg: "(UID BODY[TEXT])".
|
---|
432 |
|
---|
433 | 'data' are tuples of message part envelope and data.
|
---|
434 | """
|
---|
435 | name = 'FETCH'
|
---|
436 | typ, dat = self._simple_command(name, message_set, message_parts)
|
---|
437 | return self._untagged_response(typ, dat, name)
|
---|
438 |
|
---|
439 |
|
---|
440 | def getacl(self, mailbox):
|
---|
441 | """Get the ACLs for a mailbox.
|
---|
442 |
|
---|
443 | (typ, [data]) = <instance>.getacl(mailbox)
|
---|
444 | """
|
---|
445 | typ, dat = self._simple_command('GETACL', mailbox)
|
---|
446 | return self._untagged_response(typ, dat, 'ACL')
|
---|
447 |
|
---|
448 |
|
---|
449 | def getannotation(self, mailbox, entry, attribute):
|
---|
450 | """(typ, [data]) = <instance>.getannotation(mailbox, entry, attribute)
|
---|
451 | Retrieve ANNOTATIONs."""
|
---|
452 |
|
---|
453 | typ, dat = self._simple_command('GETANNOTATION', mailbox, entry, attribute)
|
---|
454 | return self._untagged_response(typ, dat, 'ANNOTATION')
|
---|
455 |
|
---|
456 |
|
---|
457 | def getquota(self, root):
|
---|
458 | """Get the quota root's resource usage and limits.
|
---|
459 |
|
---|
460 | Part of the IMAP4 QUOTA extension defined in rfc2087.
|
---|
461 |
|
---|
462 | (typ, [data]) = <instance>.getquota(root)
|
---|
463 | """
|
---|
464 | typ, dat = self._simple_command('GETQUOTA', root)
|
---|
465 | return self._untagged_response(typ, dat, 'QUOTA')
|
---|
466 |
|
---|
467 |
|
---|
468 | def getquotaroot(self, mailbox):
|
---|
469 | """Get the list of quota roots for the named mailbox.
|
---|
470 |
|
---|
471 | (typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox)
|
---|
472 | """
|
---|
473 | typ, dat = self._simple_command('GETQUOTAROOT', mailbox)
|
---|
474 | typ, quota = self._untagged_response(typ, dat, 'QUOTA')
|
---|
475 | typ, quotaroot = self._untagged_response(typ, dat, 'QUOTAROOT')
|
---|
476 | return typ, [quotaroot, quota]
|
---|
477 |
|
---|
478 |
|
---|
479 | def list(self, directory='""', pattern='*'):
|
---|
480 | """List mailbox names in directory matching pattern.
|
---|
481 |
|
---|
482 | (typ, [data]) = <instance>.list(directory='""', pattern='*')
|
---|
483 |
|
---|
484 | 'data' is list of LIST responses.
|
---|
485 | """
|
---|
486 | name = 'LIST'
|
---|
487 | typ, dat = self._simple_command(name, directory, pattern)
|
---|
488 | return self._untagged_response(typ, dat, name)
|
---|
489 |
|
---|
490 |
|
---|
491 | def login(self, user, password):
|
---|
492 | """Identify client using plaintext password.
|
---|
493 |
|
---|
494 | (typ, [data]) = <instance>.login(user, password)
|
---|
495 |
|
---|
496 | NB: 'password' will be quoted.
|
---|
497 | """
|
---|
498 | typ, dat = self._simple_command('LOGIN', user, self._quote(password))
|
---|
499 | if typ != 'OK':
|
---|
500 | raise self.error(dat[-1])
|
---|
501 | self.state = 'AUTH'
|
---|
502 | return typ, dat
|
---|
503 |
|
---|
504 |
|
---|
505 | def login_cram_md5(self, user, password):
|
---|
506 | """ Force use of CRAM-MD5 authentication.
|
---|
507 |
|
---|
508 | (typ, [data]) = <instance>.login_cram_md5(user, password)
|
---|
509 | """
|
---|
510 | self.user, self.password = user, password
|
---|
511 | return self.authenticate('CRAM-MD5', self._CRAM_MD5_AUTH)
|
---|
512 |
|
---|
513 |
|
---|
514 | def _CRAM_MD5_AUTH(self, challenge):
|
---|
515 | """ Authobject to use with CRAM-MD5 authentication. """
|
---|
516 | import hmac
|
---|
517 | return self.user + " " + hmac.HMAC(self.password, challenge).hexdigest()
|
---|
518 |
|
---|
519 |
|
---|
520 | def logout(self):
|
---|
521 | """Shutdown connection to server.
|
---|
522 |
|
---|
523 | (typ, [data]) = <instance>.logout()
|
---|
524 |
|
---|
525 | Returns server 'BYE' response.
|
---|
526 | """
|
---|
527 | self.state = 'LOGOUT'
|
---|
528 | try: typ, dat = self._simple_command('LOGOUT')
|
---|
529 | except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
|
---|
530 | self.shutdown()
|
---|
531 | if 'BYE' in self.untagged_responses:
|
---|
532 | return 'BYE', self.untagged_responses['BYE']
|
---|
533 | return typ, dat
|
---|
534 |
|
---|
535 |
|
---|
536 | def lsub(self, directory='""', pattern='*'):
|
---|
537 | """List 'subscribed' mailbox names in directory matching pattern.
|
---|
538 |
|
---|
539 | (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
|
---|
540 |
|
---|
541 | 'data' are tuples of message part envelope and data.
|
---|
542 | """
|
---|
543 | name = 'LSUB'
|
---|
544 | typ, dat = self._simple_command(name, directory, pattern)
|
---|
545 | return self._untagged_response(typ, dat, name)
|
---|
546 |
|
---|
547 | def myrights(self, mailbox):
|
---|
548 | """Show my ACLs for a mailbox (i.e. the rights that I have on mailbox).
|
---|
549 |
|
---|
550 | (typ, [data]) = <instance>.myrights(mailbox)
|
---|
551 | """
|
---|
552 | typ,dat = self._simple_command('MYRIGHTS', mailbox)
|
---|
553 | return self._untagged_response(typ, dat, 'MYRIGHTS')
|
---|
554 |
|
---|
555 | def namespace(self):
|
---|
556 | """ Returns IMAP namespaces ala rfc2342
|
---|
557 |
|
---|
558 | (typ, [data, ...]) = <instance>.namespace()
|
---|
559 | """
|
---|
560 | name = 'NAMESPACE'
|
---|
561 | typ, dat = self._simple_command(name)
|
---|
562 | return self._untagged_response(typ, dat, name)
|
---|
563 |
|
---|
564 |
|
---|
565 | def noop(self):
|
---|
566 | """Send NOOP command.
|
---|
567 |
|
---|
568 | (typ, [data]) = <instance>.noop()
|
---|
569 | """
|
---|
570 | if __debug__:
|
---|
571 | if self.debug >= 3:
|
---|
572 | self._dump_ur(self.untagged_responses)
|
---|
573 | return self._simple_command('NOOP')
|
---|
574 |
|
---|
575 |
|
---|
576 | def partial(self, message_num, message_part, start, length):
|
---|
577 | """Fetch truncated part of a message.
|
---|
578 |
|
---|
579 | (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
|
---|
580 |
|
---|
581 | 'data' is tuple of message part envelope and data.
|
---|
582 | """
|
---|
583 | name = 'PARTIAL'
|
---|
584 | typ, dat = self._simple_command(name, message_num, message_part, start, length)
|
---|
585 | return self._untagged_response(typ, dat, 'FETCH')
|
---|
586 |
|
---|
587 |
|
---|
588 | def proxyauth(self, user):
|
---|
589 | """Assume authentication as "user".
|
---|
590 |
|
---|
591 | Allows an authorised administrator to proxy into any user's
|
---|
592 | mailbox.
|
---|
593 |
|
---|
594 | (typ, [data]) = <instance>.proxyauth(user)
|
---|
595 | """
|
---|
596 |
|
---|
597 | name = 'PROXYAUTH'
|
---|
598 | return self._simple_command('PROXYAUTH', user)
|
---|
599 |
|
---|
600 |
|
---|
601 | def rename(self, oldmailbox, newmailbox):
|
---|
602 | """Rename old mailbox name to new.
|
---|
603 |
|
---|
604 | (typ, [data]) = <instance>.rename(oldmailbox, newmailbox)
|
---|
605 | """
|
---|
606 | return self._simple_command('RENAME', oldmailbox, newmailbox)
|
---|
607 |
|
---|
608 |
|
---|
609 | def search(self, charset, *criteria):
|
---|
610 | """Search mailbox for matching messages.
|
---|
611 |
|
---|
612 | (typ, [data]) = <instance>.search(charset, criterion, ...)
|
---|
613 |
|
---|
614 | 'data' is space separated list of matching message numbers.
|
---|
615 | """
|
---|
616 | name = 'SEARCH'
|
---|
617 | if charset:
|
---|
618 | typ, dat = self._simple_command(name, 'CHARSET', charset, *criteria)
|
---|
619 | else:
|
---|
620 | typ, dat = self._simple_command(name, *criteria)
|
---|
621 | return self._untagged_response(typ, dat, name)
|
---|
622 |
|
---|
623 |
|
---|
624 | def select(self, mailbox='INBOX', readonly=False):
|
---|
625 | """Select a mailbox.
|
---|
626 |
|
---|
627 | Flush all untagged responses.
|
---|
628 |
|
---|
629 | (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=False)
|
---|
630 |
|
---|
631 | 'data' is count of messages in mailbox ('EXISTS' response).
|
---|
632 |
|
---|
633 | Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so
|
---|
634 | other responses should be obtained via <instance>.response('FLAGS') etc.
|
---|
635 | """
|
---|
636 | self.untagged_responses = {} # Flush old responses.
|
---|
637 | self.is_readonly = readonly
|
---|
638 | if readonly:
|
---|
639 | name = 'EXAMINE'
|
---|
640 | else:
|
---|
641 | name = 'SELECT'
|
---|
642 | typ, dat = self._simple_command(name, mailbox)
|
---|
643 | if typ != 'OK':
|
---|
644 | self.state = 'AUTH' # Might have been 'SELECTED'
|
---|
645 | return typ, dat
|
---|
646 | self.state = 'SELECTED'
|
---|
647 | if 'READ-ONLY' in self.untagged_responses \
|
---|
648 | and not readonly:
|
---|
649 | if __debug__:
|
---|
650 | if self.debug >= 1:
|
---|
651 | self._dump_ur(self.untagged_responses)
|
---|
652 | raise self.readonly('%s is not writable' % mailbox)
|
---|
653 | return typ, self.untagged_responses.get('EXISTS', [None])
|
---|
654 |
|
---|
655 |
|
---|
656 | def setacl(self, mailbox, who, what):
|
---|
657 | """Set a mailbox acl.
|
---|
658 |
|
---|
659 | (typ, [data]) = <instance>.setacl(mailbox, who, what)
|
---|
660 | """
|
---|
661 | return self._simple_command('SETACL', mailbox, who, what)
|
---|
662 |
|
---|
663 |
|
---|
664 | def setannotation(self, *args):
|
---|
665 | """(typ, [data]) = <instance>.setannotation(mailbox[, entry, attribute]+)
|
---|
666 | Set ANNOTATIONs."""
|
---|
667 |
|
---|
668 | typ, dat = self._simple_command('SETANNOTATION', *args)
|
---|
669 | return self._untagged_response(typ, dat, 'ANNOTATION')
|
---|
670 |
|
---|
671 |
|
---|
672 | def setquota(self, root, limits):
|
---|
673 | """Set the quota root's resource limits.
|
---|
674 |
|
---|
675 | (typ, [data]) = <instance>.setquota(root, limits)
|
---|
676 | """
|
---|
677 | typ, dat = self._simple_command('SETQUOTA', root, limits)
|
---|
678 | return self._untagged_response(typ, dat, 'QUOTA')
|
---|
679 |
|
---|
680 |
|
---|
681 | def sort(self, sort_criteria, charset, *search_criteria):
|
---|
682 | """IMAP4rev1 extension SORT command.
|
---|
683 |
|
---|
684 | (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
|
---|
685 | """
|
---|
686 | name = 'SORT'
|
---|
687 | #if not name in self.capabilities: # Let the server decide!
|
---|
688 | # raise self.error('unimplemented extension command: %s' % name)
|
---|
689 | if (sort_criteria[0],sort_criteria[-1]) != ('(',')'):
|
---|
690 | sort_criteria = '(%s)' % sort_criteria
|
---|
691 | typ, dat = self._simple_command(name, sort_criteria, charset, *search_criteria)
|
---|
692 | return self._untagged_response(typ, dat, name)
|
---|
693 |
|
---|
694 |
|
---|
695 | def status(self, mailbox, names):
|
---|
696 | """Request named status conditions for mailbox.
|
---|
697 |
|
---|
698 | (typ, [data]) = <instance>.status(mailbox, names)
|
---|
699 | """
|
---|
700 | name = 'STATUS'
|
---|
701 | #if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide!
|
---|
702 | # raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
|
---|
703 | typ, dat = self._simple_command(name, mailbox, names)
|
---|
704 | return self._untagged_response(typ, dat, name)
|
---|
705 |
|
---|
706 |
|
---|
707 | def store(self, message_set, command, flags):
|
---|
708 | """Alters flag dispositions for messages in mailbox.
|
---|
709 |
|
---|
710 | (typ, [data]) = <instance>.store(message_set, command, flags)
|
---|
711 | """
|
---|
712 | if (flags[0],flags[-1]) != ('(',')'):
|
---|
713 | flags = '(%s)' % flags # Avoid quoting the flags
|
---|
714 | typ, dat = self._simple_command('STORE', message_set, command, flags)
|
---|
715 | return self._untagged_response(typ, dat, 'FETCH')
|
---|
716 |
|
---|
717 |
|
---|
718 | def subscribe(self, mailbox):
|
---|
719 | """Subscribe to new mailbox.
|
---|
720 |
|
---|
721 | (typ, [data]) = <instance>.subscribe(mailbox)
|
---|
722 | """
|
---|
723 | return self._simple_command('SUBSCRIBE', mailbox)
|
---|
724 |
|
---|
725 |
|
---|
726 | def thread(self, threading_algorithm, charset, *search_criteria):
|
---|
727 | """IMAPrev1 extension THREAD command.
|
---|
728 |
|
---|
729 | (type, [data]) = <instance>.thread(threading_algorithm, charset, search_criteria, ...)
|
---|
730 | """
|
---|
731 | name = 'THREAD'
|
---|
732 | typ, dat = self._simple_command(name, threading_algorithm, charset, *search_criteria)
|
---|
733 | return self._untagged_response(typ, dat, name)
|
---|
734 |
|
---|
735 |
|
---|
736 | def uid(self, command, *args):
|
---|
737 | """Execute "command arg ..." with messages identified by UID,
|
---|
738 | rather than message number.
|
---|
739 |
|
---|
740 | (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
|
---|
741 |
|
---|
742 | Returns response appropriate to 'command'.
|
---|
743 | """
|
---|
744 | command = command.upper()
|
---|
745 | if not command in Commands:
|
---|
746 | raise self.error("Unknown IMAP4 UID command: %s" % command)
|
---|
747 | if self.state not in Commands[command]:
|
---|
748 | raise self.error("command %s illegal in state %s, "
|
---|
749 | "only allowed in states %s" %
|
---|
750 | (command, self.state,
|
---|
751 | ', '.join(Commands[command])))
|
---|
752 | name = 'UID'
|
---|
753 | typ, dat = self._simple_command(name, command, *args)
|
---|
754 | if command in ('SEARCH', 'SORT'):
|
---|
755 | name = command
|
---|
756 | else:
|
---|
757 | name = 'FETCH'
|
---|
758 | return self._untagged_response(typ, dat, name)
|
---|
759 |
|
---|
760 |
|
---|
761 | def unsubscribe(self, mailbox):
|
---|
762 | """Unsubscribe from old mailbox.
|
---|
763 |
|
---|
764 | (typ, [data]) = <instance>.unsubscribe(mailbox)
|
---|
765 | """
|
---|
766 | return self._simple_command('UNSUBSCRIBE', mailbox)
|
---|
767 |
|
---|
768 |
|
---|
769 | def xatom(self, name, *args):
|
---|
770 | """Allow simple extension commands
|
---|
771 | notified by server in CAPABILITY response.
|
---|
772 |
|
---|
773 | Assumes command is legal in current state.
|
---|
774 |
|
---|
775 | (typ, [data]) = <instance>.xatom(name, arg, ...)
|
---|
776 |
|
---|
777 | Returns response appropriate to extension command `name'.
|
---|
778 | """
|
---|
779 | name = name.upper()
|
---|
780 | #if not name in self.capabilities: # Let the server decide!
|
---|
781 | # raise self.error('unknown extension command: %s' % name)
|
---|
782 | if not name in Commands:
|
---|
783 | Commands[name] = (self.state,)
|
---|
784 | return self._simple_command(name, *args)
|
---|
785 |
|
---|
786 |
|
---|
787 |
|
---|
788 | # Private methods
|
---|
789 |
|
---|
790 |
|
---|
791 | def _append_untagged(self, typ, dat):
|
---|
792 |
|
---|
793 | if dat is None: dat = ''
|
---|
794 | ur = self.untagged_responses
|
---|
795 | if __debug__:
|
---|
796 | if self.debug >= 5:
|
---|
797 | self._mesg('untagged_responses[%s] %s += ["%s"]' %
|
---|
798 | (typ, len(ur.get(typ,'')), dat))
|
---|
799 | if typ in ur:
|
---|
800 | ur[typ].append(dat)
|
---|
801 | else:
|
---|
802 | ur[typ] = [dat]
|
---|
803 |
|
---|
804 |
|
---|
805 | def _check_bye(self):
|
---|
806 | bye = self.untagged_responses.get('BYE')
|
---|
807 | if bye:
|
---|
808 | raise self.abort(bye[-1])
|
---|
809 |
|
---|
810 |
|
---|
811 | def _command(self, name, *args):
|
---|
812 |
|
---|
813 | if self.state not in Commands[name]:
|
---|
814 | self.literal = None
|
---|
815 | raise self.error("command %s illegal in state %s, "
|
---|
816 | "only allowed in states %s" %
|
---|
817 | (name, self.state,
|
---|
818 | ', '.join(Commands[name])))
|
---|
819 |
|
---|
820 | for typ in ('OK', 'NO', 'BAD'):
|
---|
821 | if typ in self.untagged_responses:
|
---|
822 | del self.untagged_responses[typ]
|
---|
823 |
|
---|
824 | if 'READ-ONLY' in self.untagged_responses \
|
---|
825 | and not self.is_readonly:
|
---|
826 | raise self.readonly('mailbox status changed to READ-ONLY')
|
---|
827 |
|
---|
828 | tag = self._new_tag()
|
---|
829 | data = '%s %s' % (tag, name)
|
---|
830 | for arg in args:
|
---|
831 | if arg is None: continue
|
---|
832 | data = '%s %s' % (data, self._checkquote(arg))
|
---|
833 |
|
---|
834 | literal = self.literal
|
---|
835 | if literal is not None:
|
---|
836 | self.literal = None
|
---|
837 | if type(literal) is type(self._command):
|
---|
838 | literator = literal
|
---|
839 | else:
|
---|
840 | literator = None
|
---|
841 | data = '%s {%s}' % (data, len(literal))
|
---|
842 |
|
---|
843 | if __debug__:
|
---|
844 | if self.debug >= 4:
|
---|
845 | self._mesg('> %s' % data)
|
---|
846 | else:
|
---|
847 | self._log('> %s' % data)
|
---|
848 |
|
---|
849 | try:
|
---|
850 | self.send('%s%s' % (data, CRLF))
|
---|
851 | except (socket.error, OSError), val:
|
---|
852 | raise self.abort('socket error: %s' % val)
|
---|
853 |
|
---|
854 | if literal is None:
|
---|
855 | return tag
|
---|
856 |
|
---|
857 | while 1:
|
---|
858 | # Wait for continuation response
|
---|
859 |
|
---|
860 | while self._get_response():
|
---|
861 | if self.tagged_commands[tag]: # BAD/NO?
|
---|
862 | return tag
|
---|
863 |
|
---|
864 | # Send literal
|
---|
865 |
|
---|
866 | if literator:
|
---|
867 | literal = literator(self.continuation_response)
|
---|
868 |
|
---|
869 | if __debug__:
|
---|
870 | if self.debug >= 4:
|
---|
871 | self._mesg('write literal size %s' % len(literal))
|
---|
872 |
|
---|
873 | try:
|
---|
874 | self.send(literal)
|
---|
875 | self.send(CRLF)
|
---|
876 | except (socket.error, OSError), val:
|
---|
877 | raise self.abort('socket error: %s' % val)
|
---|
878 |
|
---|
879 | if not literator:
|
---|
880 | break
|
---|
881 |
|
---|
882 | return tag
|
---|
883 |
|
---|
884 |
|
---|
885 | def _command_complete(self, name, tag):
|
---|
886 | self._check_bye()
|
---|
887 | try:
|
---|
888 | typ, data = self._get_tagged_response(tag)
|
---|
889 | except self.abort, val:
|
---|
890 | raise self.abort('command: %s => %s' % (name, val))
|
---|
891 | except self.error, val:
|
---|
892 | raise self.error('command: %s => %s' % (name, val))
|
---|
893 | self._check_bye()
|
---|
894 | if typ == 'BAD':
|
---|
895 | raise self.error('%s command error: %s %s' % (name, typ, data))
|
---|
896 | return typ, data
|
---|
897 |
|
---|
898 |
|
---|
899 | def _get_response(self):
|
---|
900 |
|
---|
901 | # Read response and store.
|
---|
902 | #
|
---|
903 | # Returns None for continuation responses,
|
---|
904 | # otherwise first response line received.
|
---|
905 |
|
---|
906 | resp = self._get_line()
|
---|
907 |
|
---|
908 | # Command completion response?
|
---|
909 |
|
---|
910 | if self._match(self.tagre, resp):
|
---|
911 | tag = self.mo.group('tag')
|
---|
912 | if not tag in self.tagged_commands:
|
---|
913 | raise self.abort('unexpected tagged response: %s' % resp)
|
---|
914 |
|
---|
915 | typ = self.mo.group('type')
|
---|
916 | dat = self.mo.group('data')
|
---|
917 | self.tagged_commands[tag] = (typ, [dat])
|
---|
918 | else:
|
---|
919 | dat2 = None
|
---|
920 |
|
---|
921 | # '*' (untagged) responses?
|
---|
922 |
|
---|
923 | if not self._match(Untagged_response, resp):
|
---|
924 | if self._match(Untagged_status, resp):
|
---|
925 | dat2 = self.mo.group('data2')
|
---|
926 |
|
---|
927 | if self.mo is None:
|
---|
928 | # Only other possibility is '+' (continuation) response...
|
---|
929 |
|
---|
930 | if self._match(Continuation, resp):
|
---|
931 | self.continuation_response = self.mo.group('data')
|
---|
932 | return None # NB: indicates continuation
|
---|
933 |
|
---|
934 | raise self.abort("unexpected response: '%s'" % resp)
|
---|
935 |
|
---|
936 | typ = self.mo.group('type')
|
---|
937 | dat = self.mo.group('data')
|
---|
938 | if dat is None: dat = '' # Null untagged response
|
---|
939 | if dat2: dat = dat + ' ' + dat2
|
---|
940 |
|
---|
941 | # Is there a literal to come?
|
---|
942 |
|
---|
943 | while self._match(Literal, dat):
|
---|
944 |
|
---|
945 | # Read literal direct from connection.
|
---|
946 |
|
---|
947 | size = int(self.mo.group('size'))
|
---|
948 | if __debug__:
|
---|
949 | if self.debug >= 4:
|
---|
950 | self._mesg('read literal size %s' % size)
|
---|
951 | data = self.read(size)
|
---|
952 |
|
---|
953 | # Store response with literal as tuple
|
---|
954 |
|
---|
955 | self._append_untagged(typ, (dat, data))
|
---|
956 |
|
---|
957 | # Read trailer - possibly containing another literal
|
---|
958 |
|
---|
959 | dat = self._get_line()
|
---|
960 |
|
---|
961 | self._append_untagged(typ, dat)
|
---|
962 |
|
---|
963 | # Bracketed response information?
|
---|
964 |
|
---|
965 | if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
|
---|
966 | self._append_untagged(self.mo.group('type'), self.mo.group('data'))
|
---|
967 |
|
---|
968 | if __debug__:
|
---|
969 | if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
|
---|
970 | self._mesg('%s response: %s' % (typ, dat))
|
---|
971 |
|
---|
972 | return resp
|
---|
973 |
|
---|
974 |
|
---|
975 | def _get_tagged_response(self, tag):
|
---|
976 |
|
---|
977 | while 1:
|
---|
978 | result = self.tagged_commands[tag]
|
---|
979 | if result is not None:
|
---|
980 | del self.tagged_commands[tag]
|
---|
981 | return result
|
---|
982 |
|
---|
983 | # Some have reported "unexpected response" exceptions.
|
---|
984 | # Note that ignoring them here causes loops.
|
---|
985 | # Instead, send me details of the unexpected response and
|
---|
986 | # I'll update the code in `_get_response()'.
|
---|
987 |
|
---|
988 | try:
|
---|
989 | self._get_response()
|
---|
990 | except self.abort, val:
|
---|
991 | if __debug__:
|
---|
992 | if self.debug >= 1:
|
---|
993 | self.print_log()
|
---|
994 | raise
|
---|
995 |
|
---|
996 |
|
---|
997 | def _get_line(self):
|
---|
998 |
|
---|
999 | line = self.readline()
|
---|
1000 | if not line:
|
---|
1001 | raise self.abort('socket error: EOF')
|
---|
1002 |
|
---|
1003 | # Protocol mandates all lines terminated by CRLF
|
---|
1004 | if not line.endswith('\r\n'):
|
---|
1005 | raise self.abort('socket error: unterminated line')
|
---|
1006 |
|
---|
1007 | line = line[:-2]
|
---|
1008 | if __debug__:
|
---|
1009 | if self.debug >= 4:
|
---|
1010 | self._mesg('< %s' % line)
|
---|
1011 | else:
|
---|
1012 | self._log('< %s' % line)
|
---|
1013 | return line
|
---|
1014 |
|
---|
1015 |
|
---|
1016 | def _match(self, cre, s):
|
---|
1017 |
|
---|
1018 | # Run compiled regular expression match method on 's'.
|
---|
1019 | # Save result, return success.
|
---|
1020 |
|
---|
1021 | self.mo = cre.match(s)
|
---|
1022 | if __debug__:
|
---|
1023 | if self.mo is not None and self.debug >= 5:
|
---|
1024 | self._mesg("\tmatched r'%s' => %r" % (cre.pattern, self.mo.groups()))
|
---|
1025 | return self.mo is not None
|
---|
1026 |
|
---|
1027 |
|
---|
1028 | def _new_tag(self):
|
---|
1029 |
|
---|
1030 | tag = '%s%s' % (self.tagpre, self.tagnum)
|
---|
1031 | self.tagnum = self.tagnum + 1
|
---|
1032 | self.tagged_commands[tag] = None
|
---|
1033 | return tag
|
---|
1034 |
|
---|
1035 |
|
---|
1036 | def _checkquote(self, arg):
|
---|
1037 |
|
---|
1038 | # Must quote command args if non-alphanumeric chars present,
|
---|
1039 | # and not already quoted.
|
---|
1040 |
|
---|
1041 | if type(arg) is not type(''):
|
---|
1042 | return arg
|
---|
1043 | if len(arg) >= 2 and (arg[0],arg[-1]) in (('(',')'),('"','"')):
|
---|
1044 | return arg
|
---|
1045 | if arg and self.mustquote.search(arg) is None:
|
---|
1046 | return arg
|
---|
1047 | return self._quote(arg)
|
---|
1048 |
|
---|
1049 |
|
---|
1050 | def _quote(self, arg):
|
---|
1051 |
|
---|
1052 | arg = arg.replace('\\', '\\\\')
|
---|
1053 | arg = arg.replace('"', '\\"')
|
---|
1054 |
|
---|
1055 | return '"%s"' % arg
|
---|
1056 |
|
---|
1057 |
|
---|
1058 | def _simple_command(self, name, *args):
|
---|
1059 |
|
---|
1060 | return self._command_complete(name, self._command(name, *args))
|
---|
1061 |
|
---|
1062 |
|
---|
1063 | def _untagged_response(self, typ, dat, name):
|
---|
1064 |
|
---|
1065 | if typ == 'NO':
|
---|
1066 | return typ, dat
|
---|
1067 | if not name in self.untagged_responses:
|
---|
1068 | return typ, [None]
|
---|
1069 | data = self.untagged_responses.pop(name)
|
---|
1070 | if __debug__:
|
---|
1071 | if self.debug >= 5:
|
---|
1072 | self._mesg('untagged_responses[%s] => %s' % (name, data))
|
---|
1073 | return typ, data
|
---|
1074 |
|
---|
1075 |
|
---|
1076 | if __debug__:
|
---|
1077 |
|
---|
1078 | def _mesg(self, s, secs=None):
|
---|
1079 | if secs is None:
|
---|
1080 | secs = time.time()
|
---|
1081 | tm = time.strftime('%M:%S', time.localtime(secs))
|
---|
1082 | sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s))
|
---|
1083 | sys.stderr.flush()
|
---|
1084 |
|
---|
1085 | def _dump_ur(self, dict):
|
---|
1086 | # Dump untagged responses (in `dict').
|
---|
1087 | l = dict.items()
|
---|
1088 | if not l: return
|
---|
1089 | t = '\n\t\t'
|
---|
1090 | l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l)
|
---|
1091 | self._mesg('untagged responses dump:%s%s' % (t, t.join(l)))
|
---|
1092 |
|
---|
1093 | def _log(self, line):
|
---|
1094 | # Keep log of last `_cmd_log_len' interactions for debugging.
|
---|
1095 | self._cmd_log[self._cmd_log_idx] = (line, time.time())
|
---|
1096 | self._cmd_log_idx += 1
|
---|
1097 | if self._cmd_log_idx >= self._cmd_log_len:
|
---|
1098 | self._cmd_log_idx = 0
|
---|
1099 |
|
---|
1100 | def print_log(self):
|
---|
1101 | self._mesg('last %d IMAP4 interactions:' % len(self._cmd_log))
|
---|
1102 | i, n = self._cmd_log_idx, self._cmd_log_len
|
---|
1103 | while n:
|
---|
1104 | try:
|
---|
1105 | self._mesg(*self._cmd_log[i])
|
---|
1106 | except:
|
---|
1107 | pass
|
---|
1108 | i += 1
|
---|
1109 | if i >= self._cmd_log_len:
|
---|
1110 | i = 0
|
---|
1111 | n -= 1
|
---|
1112 |
|
---|
1113 |
|
---|
1114 |
|
---|
1115 | try:
|
---|
1116 | import ssl
|
---|
1117 | except ImportError:
|
---|
1118 | pass
|
---|
1119 | else:
|
---|
1120 | class IMAP4_SSL(IMAP4):
|
---|
1121 |
|
---|
1122 | """IMAP4 client class over SSL connection
|
---|
1123 |
|
---|
1124 | Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile]]]])
|
---|
1125 |
|
---|
1126 | host - host's name (default: localhost);
|
---|
1127 | port - port number (default: standard IMAP4 SSL port).
|
---|
1128 | keyfile - PEM formatted file that contains your private key (default: None);
|
---|
1129 | certfile - PEM formatted certificate chain file (default: None);
|
---|
1130 |
|
---|
1131 | for more documentation see the docstring of the parent class IMAP4.
|
---|
1132 | """
|
---|
1133 |
|
---|
1134 |
|
---|
1135 | def __init__(self, host = '', port = IMAP4_SSL_PORT, keyfile = None, certfile = None):
|
---|
1136 | self.keyfile = keyfile
|
---|
1137 | self.certfile = certfile
|
---|
1138 | IMAP4.__init__(self, host, port)
|
---|
1139 |
|
---|
1140 |
|
---|
1141 | def open(self, host = '', port = IMAP4_SSL_PORT):
|
---|
1142 | """Setup connection to remote server on "host:port".
|
---|
1143 | (default: localhost:standard IMAP4 SSL port).
|
---|
1144 | This connection will be used by the routines:
|
---|
1145 | read, readline, send, shutdown.
|
---|
1146 | """
|
---|
1147 | self.host = host
|
---|
1148 | self.port = port
|
---|
1149 | self.sock = socket.create_connection((host, port))
|
---|
1150 | self.sslobj = ssl.wrap_socket(self.sock, self.keyfile, self.certfile)
|
---|
1151 |
|
---|
1152 |
|
---|
1153 | def read(self, size):
|
---|
1154 | """Read 'size' bytes from remote."""
|
---|
1155 | # sslobj.read() sometimes returns < size bytes
|
---|
1156 | chunks = []
|
---|
1157 | read = 0
|
---|
1158 | while read < size:
|
---|
1159 | data = self.sslobj.read(min(size-read, 16384))
|
---|
1160 | read += len(data)
|
---|
1161 | chunks.append(data)
|
---|
1162 |
|
---|
1163 | return ''.join(chunks)
|
---|
1164 |
|
---|
1165 |
|
---|
1166 | def readline(self):
|
---|
1167 | """Read line from remote."""
|
---|
1168 | line = []
|
---|
1169 | while 1:
|
---|
1170 | char = self.sslobj.read(1)
|
---|
1171 | line.append(char)
|
---|
1172 | if char in ("\n", ""): return ''.join(line)
|
---|
1173 |
|
---|
1174 |
|
---|
1175 | def send(self, data):
|
---|
1176 | """Send data to remote."""
|
---|
1177 | bytes = len(data)
|
---|
1178 | while bytes > 0:
|
---|
1179 | sent = self.sslobj.write(data)
|
---|
1180 | if sent == bytes:
|
---|
1181 | break # avoid copy
|
---|
1182 | data = data[sent:]
|
---|
1183 | bytes = bytes - sent
|
---|
1184 |
|
---|
1185 |
|
---|
1186 | def shutdown(self):
|
---|
1187 | """Close I/O established in "open"."""
|
---|
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 | """Convert IMAP4 INTERNALDATE to UT.
|
---|
1315 |
|
---|
1316 | Returns Python time module tuple.
|
---|
1317 | """
|
---|
1318 |
|
---|
1319 | mo = InternalDate.match(resp)
|
---|
1320 | if not mo:
|
---|
1321 | return None
|
---|
1322 |
|
---|
1323 | mon = Mon2num[mo.group('mon')]
|
---|
1324 | zonen = mo.group('zonen')
|
---|
1325 |
|
---|
1326 | day = int(mo.group('day'))
|
---|
1327 | year = int(mo.group('year'))
|
---|
1328 | hour = int(mo.group('hour'))
|
---|
1329 | min = int(mo.group('min'))
|
---|
1330 | sec = int(mo.group('sec'))
|
---|
1331 | zoneh = int(mo.group('zoneh'))
|
---|
1332 | zonem = int(mo.group('zonem'))
|
---|
1333 |
|
---|
1334 | # INTERNALDATE timezone must be subtracted to get UT
|
---|
1335 |
|
---|
1336 | zone = (zoneh*60 + zonem)*60
|
---|
1337 | if zonen == '-':
|
---|
1338 | zone = -zone
|
---|
1339 |
|
---|
1340 | tt = (year, mon, day, hour, min, sec, -1, -1, -1)
|
---|
1341 |
|
---|
1342 | utc = time.mktime(tt)
|
---|
1343 |
|
---|
1344 | # Following is necessary because the time module has no 'mkgmtime'.
|
---|
1345 | # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
|
---|
1346 |
|
---|
1347 | lt = time.localtime(utc)
|
---|
1348 | if time.daylight and lt[-1]:
|
---|
1349 | zone = zone + time.altzone
|
---|
1350 | else:
|
---|
1351 | zone = zone + time.timezone
|
---|
1352 |
|
---|
1353 | return time.localtime(utc - zone)
|
---|
1354 |
|
---|
1355 |
|
---|
1356 |
|
---|
1357 | def Int2AP(num):
|
---|
1358 |
|
---|
1359 | """Convert integer to A-P string representation."""
|
---|
1360 |
|
---|
1361 | val = ''; AP = 'ABCDEFGHIJKLMNOP'
|
---|
1362 | num = int(abs(num))
|
---|
1363 | while num:
|
---|
1364 | num, mod = divmod(num, 16)
|
---|
1365 | val = AP[mod] + val
|
---|
1366 | return val
|
---|
1367 |
|
---|
1368 |
|
---|
1369 |
|
---|
1370 | def ParseFlags(resp):
|
---|
1371 |
|
---|
1372 | """Convert IMAP4 flags response to python tuple."""
|
---|
1373 |
|
---|
1374 | mo = Flags.match(resp)
|
---|
1375 | if not mo:
|
---|
1376 | return ()
|
---|
1377 |
|
---|
1378 | return tuple(mo.group('flags').split())
|
---|
1379 |
|
---|
1380 |
|
---|
1381 | def Time2Internaldate(date_time):
|
---|
1382 |
|
---|
1383 | """Convert 'date_time' to IMAP4 INTERNALDATE representation.
|
---|
1384 |
|
---|
1385 | Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
|
---|
1386 | """
|
---|
1387 |
|
---|
1388 | if isinstance(date_time, (int, float)):
|
---|
1389 | tt = time.localtime(date_time)
|
---|
1390 | elif isinstance(date_time, (tuple, time.struct_time)):
|
---|
1391 | tt = date_time
|
---|
1392 | elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'):
|
---|
1393 | return date_time # Assume in correct format
|
---|
1394 | else:
|
---|
1395 | raise ValueError("date_time not of a known type")
|
---|
1396 |
|
---|
1397 | dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
|
---|
1398 | if dt[0] == '0':
|
---|
1399 | dt = ' ' + dt[1:]
|
---|
1400 | if time.daylight and tt[-1]:
|
---|
1401 | zone = -time.altzone
|
---|
1402 | else:
|
---|
1403 | zone = -time.timezone
|
---|
1404 | return '"' + dt + " %+03d%02d" % divmod(zone//60, 60) + '"'
|
---|
1405 |
|
---|
1406 |
|
---|
1407 |
|
---|
1408 | if __name__ == '__main__':
|
---|
1409 |
|
---|
1410 | # To test: invoke either as 'python imaplib.py [IMAP4_server_hostname]'
|
---|
1411 | # or 'python imaplib.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"'
|
---|
1412 | # to test the IMAP4_stream class
|
---|
1413 |
|
---|
1414 | import getopt, getpass
|
---|
1415 |
|
---|
1416 | try:
|
---|
1417 | optlist, args = getopt.getopt(sys.argv[1:], 'd:s:')
|
---|
1418 | except getopt.error, val:
|
---|
1419 | optlist, args = (), ()
|
---|
1420 |
|
---|
1421 | stream_command = None
|
---|
1422 | for opt,val in optlist:
|
---|
1423 | if opt == '-d':
|
---|
1424 | Debug = int(val)
|
---|
1425 | elif opt == '-s':
|
---|
1426 | stream_command = val
|
---|
1427 | if not args: args = (stream_command,)
|
---|
1428 |
|
---|
1429 | if not args: args = ('',)
|
---|
1430 |
|
---|
1431 | host = args[0]
|
---|
1432 |
|
---|
1433 | USER = getpass.getuser()
|
---|
1434 | PASSWD = getpass.getpass("IMAP password for %s on %s: " % (USER, host or "localhost"))
|
---|
1435 |
|
---|
1436 | test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER, 'lf':'\n'}
|
---|
1437 | test_seq1 = (
|
---|
1438 | ('login', (USER, PASSWD)),
|
---|
1439 | ('create', ('/tmp/xxx 1',)),
|
---|
1440 | ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
|
---|
1441 | ('CREATE', ('/tmp/yyz 2',)),
|
---|
1442 | ('append', ('/tmp/yyz 2', None, None, test_mesg)),
|
---|
1443 | ('list', ('/tmp', 'yy*')),
|
---|
1444 | ('select', ('/tmp/yyz 2',)),
|
---|
1445 | ('search', (None, 'SUBJECT', 'test')),
|
---|
1446 | ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')),
|
---|
1447 | ('store', ('1', 'FLAGS', '(\Deleted)')),
|
---|
1448 | ('namespace', ()),
|
---|
1449 | ('expunge', ()),
|
---|
1450 | ('recent', ()),
|
---|
1451 | ('close', ()),
|
---|
1452 | )
|
---|
1453 |
|
---|
1454 | test_seq2 = (
|
---|
1455 | ('select', ()),
|
---|
1456 | ('response',('UIDVALIDITY',)),
|
---|
1457 | ('uid', ('SEARCH', 'ALL')),
|
---|
1458 | ('response', ('EXISTS',)),
|
---|
1459 | ('append', (None, None, None, test_mesg)),
|
---|
1460 | ('recent', ()),
|
---|
1461 | ('logout', ()),
|
---|
1462 | )
|
---|
1463 |
|
---|
1464 | def run(cmd, args):
|
---|
1465 | M._mesg('%s %s' % (cmd, args))
|
---|
1466 | typ, dat = getattr(M, cmd)(*args)
|
---|
1467 | M._mesg('%s => %s %s' % (cmd, typ, dat))
|
---|
1468 | if typ == 'NO': raise dat[0]
|
---|
1469 | return dat
|
---|
1470 |
|
---|
1471 | try:
|
---|
1472 | if stream_command:
|
---|
1473 | M = IMAP4_stream(stream_command)
|
---|
1474 | else:
|
---|
1475 | M = IMAP4(host)
|
---|
1476 | if M.state == 'AUTH':
|
---|
1477 | test_seq1 = test_seq1[1:] # Login not needed
|
---|
1478 | M._mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
|
---|
1479 | M._mesg('CAPABILITIES = %r' % (M.capabilities,))
|
---|
1480 |
|
---|
1481 | for cmd,args in test_seq1:
|
---|
1482 | run(cmd, args)
|
---|
1483 |
|
---|
1484 | for ml in run('list', ('/tmp/', 'yy%')):
|
---|
1485 | mo = re.match(r'.*"([^"]+)"$', ml)
|
---|
1486 | if mo: path = mo.group(1)
|
---|
1487 | else: path = ml.split()[-1]
|
---|
1488 | run('delete', (path,))
|
---|
1489 |
|
---|
1490 | for cmd,args in test_seq2:
|
---|
1491 | dat = run(cmd, args)
|
---|
1492 |
|
---|
1493 | if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
|
---|
1494 | continue
|
---|
1495 |
|
---|
1496 | uid = dat[-1].split()
|
---|
1497 | if not uid: continue
|
---|
1498 | run('uid', ('FETCH', '%s' % uid[-1],
|
---|
1499 | '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
|
---|
1500 |
|
---|
1501 | print '\nAll tests OK.'
|
---|
1502 |
|
---|
1503 | except:
|
---|
1504 | print '\nTests failed.'
|
---|
1505 |
|
---|
1506 | if not Debug:
|
---|
1507 | print '''
|
---|
1508 | If you would like to see debugging output,
|
---|
1509 | try: %s -d5
|
---|
1510 | ''' % sys.argv[0]
|
---|
1511 |
|
---|
1512 | raise
|
---|