blob: dc07415f8f52302fa00bdefededd6ba1c0600d35 [file] [log] [blame]
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001"""IMAP4 client.
2
3Based on RFC 2060.
4
Tim Peters07e99cb2001-01-14 23:47:14 +00005Public class: IMAP4
6Public variable: Debug
7Public functions: Internaldate2tuple
8 Int2AP
9 ParseFlags
10 Time2Internaldate
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000011"""
Guido van Rossumb1f08121998-06-25 02:22:16 +000012
Guido van Rossum98d9fd32000-02-28 15:12:25 +000013# Author: Piers Lauder <piers@cs.su.oz.au> December 1997.
Tim Peters07e99cb2001-01-14 23:47:14 +000014#
Guido van Rossum98d9fd32000-02-28 15:12:25 +000015# Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
16
Fred Drakefd267d92000-05-25 03:25:26 +000017__version__ = "2.39"
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000018
Guido van Rossum26367a01998-09-28 15:34:46 +000019import binascii, re, socket, string, time, random, sys
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000020
Barry Warsawf4493912001-01-24 04:16:09 +000021__all__ = ["IMAP4", "Internaldate2tuple",
22 "Int2AP", "ParseFlags", "Time2Internaldate"]
Skip Montanaro2dd42762001-01-23 15:35:05 +000023
Tim Peters07e99cb2001-01-14 23:47:14 +000024# Globals
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000025
26CRLF = '\r\n'
27Debug = 0
28IMAP4_PORT = 143
Tim Peters07e99cb2001-01-14 23:47:14 +000029AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000030
Tim Peters07e99cb2001-01-14 23:47:14 +000031# Commands
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000032
33Commands = {
Tim Peters07e99cb2001-01-14 23:47:14 +000034 # name valid states
35 'APPEND': ('AUTH', 'SELECTED'),
36 'AUTHENTICATE': ('NONAUTH',),
37 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
38 'CHECK': ('SELECTED',),
39 'CLOSE': ('SELECTED',),
40 'COPY': ('SELECTED',),
41 'CREATE': ('AUTH', 'SELECTED'),
42 'DELETE': ('AUTH', 'SELECTED'),
43 'EXAMINE': ('AUTH', 'SELECTED'),
44 'EXPUNGE': ('SELECTED',),
45 'FETCH': ('SELECTED',),
46 'LIST': ('AUTH', 'SELECTED'),
47 'LOGIN': ('NONAUTH',),
48 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
49 'LSUB': ('AUTH', 'SELECTED'),
50 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
51 'PARTIAL': ('SELECTED',),
52 'RENAME': ('AUTH', 'SELECTED'),
53 'SEARCH': ('SELECTED',),
54 'SELECT': ('AUTH', 'SELECTED'),
55 'STATUS': ('AUTH', 'SELECTED'),
56 'STORE': ('SELECTED',),
57 'SUBSCRIBE': ('AUTH', 'SELECTED'),
58 'UID': ('SELECTED',),
59 'UNSUBSCRIBE': ('AUTH', 'SELECTED'),
60 }
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000061
Tim Peters07e99cb2001-01-14 23:47:14 +000062# Patterns to match server responses
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000063
Guido van Rossumeda960a1998-06-18 14:24:28 +000064Continuation = re.compile(r'\+( (?P<data>.*))?')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000065Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
66InternalDate = re.compile(r'.*INTERNALDATE "'
Tim Peters07e99cb2001-01-14 23:47:14 +000067 r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
68 r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
69 r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
70 r'"')
Guido van Rossumf36b1822000-02-17 17:12:39 +000071Literal = re.compile(r'.*{(?P<size>\d+)}$')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000072Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
Guido van Rossumeda960a1998-06-18 14:24:28 +000073Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000074Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
75
76
77
78class IMAP4:
79
Tim Peters07e99cb2001-01-14 23:47:14 +000080 """IMAP4 client class.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000081
Tim Peters07e99cb2001-01-14 23:47:14 +000082 Instantiate with: IMAP4([host[, port]])
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000083
Tim Peters07e99cb2001-01-14 23:47:14 +000084 host - host's name (default: localhost);
85 port - port number (default: standard IMAP4 port).
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000086
Tim Peters07e99cb2001-01-14 23:47:14 +000087 All IMAP4rev1 commands are supported by methods of the same
88 name (in lower-case).
Guido van Rossum6884af71998-05-29 13:34:03 +000089
Tim Peters07e99cb2001-01-14 23:47:14 +000090 All arguments to commands are converted to strings, except for
91 AUTHENTICATE, and the last argument to APPEND which is passed as
92 an IMAP4 literal. If necessary (the string contains any
93 non-printing characters or white-space and isn't enclosed with
94 either parentheses or double quotes) each string is quoted.
95 However, the 'password' argument to the LOGIN command is always
96 quoted. If you want to avoid having an argument string quoted
97 (eg: the 'flags' argument to STORE) then enclose the string in
98 parentheses (eg: "(\Deleted)").
Guido van Rossum6884af71998-05-29 13:34:03 +000099
Tim Peters07e99cb2001-01-14 23:47:14 +0000100 Each command returns a tuple: (type, [data, ...]) where 'type'
101 is usually 'OK' or 'NO', and 'data' is either the text from the
102 tagged response, or untagged results from command.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000103
Tim Peters07e99cb2001-01-14 23:47:14 +0000104 Errors raise the exception class <instance>.error("<reason>").
105 IMAP4 server errors raise <instance>.abort("<reason>"),
106 which is a sub-class of 'error'. Mailbox status changes
107 from READ-WRITE to READ-ONLY raise the exception class
108 <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
Guido van Rossumeda960a1998-06-18 14:24:28 +0000109
Tim Peters07e99cb2001-01-14 23:47:14 +0000110 "error" exceptions imply a program error.
111 "abort" exceptions imply the connection should be reset, and
112 the command re-tried.
113 "readonly" exceptions imply the command should be re-tried.
Guido van Rossum8c062211999-12-13 23:27:45 +0000114
Tim Peters07e99cb2001-01-14 23:47:14 +0000115 Note: to use this module, you must read the RFCs pertaining
116 to the IMAP4 protocol, as the semantics of the arguments to
117 each IMAP4 command are left to the invoker, not to mention
118 the results.
119 """
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000120
Tim Peters07e99cb2001-01-14 23:47:14 +0000121 class error(Exception): pass # Logical errors - debug required
122 class abort(error): pass # Service errors - close and retry
123 class readonly(abort): pass # Mailbox status changed to READ-ONLY
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000124
Tim Peters07e99cb2001-01-14 23:47:14 +0000125 mustquote = re.compile(r"[^\w!#$%&'*+,.:;<=>?^`|~-]")
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000126
Tim Peters07e99cb2001-01-14 23:47:14 +0000127 def __init__(self, host = '', port = IMAP4_PORT):
128 self.host = host
129 self.port = port
130 self.debug = Debug
131 self.state = 'LOGOUT'
132 self.literal = None # A literal argument to a command
133 self.tagged_commands = {} # Tagged commands awaiting response
134 self.untagged_responses = {} # {typ: [data, ...], ...}
135 self.continuation_response = '' # Last continuation response
136 self.is_readonly = None # READ-ONLY desired state
137 self.tagnum = 0
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000138
Tim Peters07e99cb2001-01-14 23:47:14 +0000139 # Open socket to server.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000140
Tim Peters07e99cb2001-01-14 23:47:14 +0000141 self.open(host, port)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000142
Tim Peters07e99cb2001-01-14 23:47:14 +0000143 # Create unique tag for this session,
144 # and compile tagged response matcher.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000145
Tim Peters07e99cb2001-01-14 23:47:14 +0000146 self.tagpre = Int2AP(random.randint(0, 31999))
147 self.tagre = re.compile(r'(?P<tag>'
148 + self.tagpre
149 + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000150
Tim Peters07e99cb2001-01-14 23:47:14 +0000151 # Get server welcome message,
152 # request and store CAPABILITY response.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000153
Tim Peters07e99cb2001-01-14 23:47:14 +0000154 if __debug__:
155 if self.debug >= 1:
156 _mesg('new IMAP4 connection, tag=%s' % self.tagpre)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000157
Tim Peters07e99cb2001-01-14 23:47:14 +0000158 self.welcome = self._get_response()
159 if self.untagged_responses.has_key('PREAUTH'):
160 self.state = 'AUTH'
161 elif self.untagged_responses.has_key('OK'):
162 self.state = 'NONAUTH'
163 else:
164 raise self.error(self.welcome)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000165
Tim Peters07e99cb2001-01-14 23:47:14 +0000166 cap = 'CAPABILITY'
167 self._simple_command(cap)
168 if not self.untagged_responses.has_key(cap):
169 raise self.error('no CAPABILITY response from server')
170 self.capabilities = tuple(string.split(string.upper(self.untagged_responses[cap][-1])))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000171
Tim Peters07e99cb2001-01-14 23:47:14 +0000172 if __debug__:
173 if self.debug >= 3:
174 _mesg('CAPABILITIES: %s' % `self.capabilities`)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000175
Tim Peters07e99cb2001-01-14 23:47:14 +0000176 for version in AllowedVersions:
177 if not version in self.capabilities:
178 continue
179 self.PROTOCOL_VERSION = version
180 return
Guido van Rossumb1f08121998-06-25 02:22:16 +0000181
Tim Peters07e99cb2001-01-14 23:47:14 +0000182 raise self.error('server not IMAP4 compliant')
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000183
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000184
Tim Peters07e99cb2001-01-14 23:47:14 +0000185 def __getattr__(self, attr):
186 # Allow UPPERCASE variants of IMAP4 command methods.
187 if Commands.has_key(attr):
188 return eval("self.%s" % string.lower(attr))
189 raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
Guido van Rossum26367a01998-09-28 15:34:46 +0000190
191
192
Tim Peters07e99cb2001-01-14 23:47:14 +0000193 # Public methods
Guido van Rossum26367a01998-09-28 15:34:46 +0000194
195
Tim Peters07e99cb2001-01-14 23:47:14 +0000196 def open(self, host, port):
197 """Setup 'self.sock' and 'self.file'."""
198 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
199 self.sock.connect((self.host, self.port))
200 self.file = self.sock.makefile('r')
Guido van Rossumeda960a1998-06-18 14:24:28 +0000201
202
Tim Peters07e99cb2001-01-14 23:47:14 +0000203 def recent(self):
204 """Return most recent 'RECENT' responses if any exist,
205 else prompt server for an update using the 'NOOP' command.
Guido van Rossum26367a01998-09-28 15:34:46 +0000206
Tim Peters07e99cb2001-01-14 23:47:14 +0000207 (typ, [data]) = <instance>.recent()
Guido van Rossum26367a01998-09-28 15:34:46 +0000208
Tim Peters07e99cb2001-01-14 23:47:14 +0000209 'data' is None if no new messages,
210 else list of RECENT responses, most recent last.
211 """
212 name = 'RECENT'
213 typ, dat = self._untagged_response('OK', [None], name)
214 if dat[-1]:
215 return typ, dat
216 typ, dat = self.noop() # Prod server for response
217 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000218
219
Tim Peters07e99cb2001-01-14 23:47:14 +0000220 def response(self, code):
221 """Return data for response 'code' if received, or None.
Guido van Rossum26367a01998-09-28 15:34:46 +0000222
Tim Peters07e99cb2001-01-14 23:47:14 +0000223 Old value for response 'code' is cleared.
Guido van Rossum26367a01998-09-28 15:34:46 +0000224
Tim Peters07e99cb2001-01-14 23:47:14 +0000225 (code, [data]) = <instance>.response(code)
226 """
227 return self._untagged_response(code, [None], string.upper(code))
Guido van Rossum26367a01998-09-28 15:34:46 +0000228
229
Tim Peters07e99cb2001-01-14 23:47:14 +0000230 def socket(self):
231 """Return socket instance used to connect to IMAP4 server.
Guido van Rossum26367a01998-09-28 15:34:46 +0000232
Tim Peters07e99cb2001-01-14 23:47:14 +0000233 socket = <instance>.socket()
234 """
235 return self.sock
Guido van Rossum26367a01998-09-28 15:34:46 +0000236
237
238
Tim Peters07e99cb2001-01-14 23:47:14 +0000239 # IMAP4 commands
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000240
241
Tim Peters07e99cb2001-01-14 23:47:14 +0000242 def append(self, mailbox, flags, date_time, message):
243 """Append message to named mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000244
Tim Peters07e99cb2001-01-14 23:47:14 +0000245 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
Guido van Rossum8c062211999-12-13 23:27:45 +0000246
Tim Peters07e99cb2001-01-14 23:47:14 +0000247 All args except `message' can be None.
248 """
249 name = 'APPEND'
250 if not mailbox:
251 mailbox = 'INBOX'
252 if flags:
253 if (flags[0],flags[-1]) != ('(',')'):
254 flags = '(%s)' % flags
255 else:
256 flags = None
257 if date_time:
258 date_time = Time2Internaldate(date_time)
259 else:
260 date_time = None
261 self.literal = message
262 return self._simple_command(name, mailbox, flags, date_time)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000263
264
Tim Peters07e99cb2001-01-14 23:47:14 +0000265 def authenticate(self, mechanism, authobject):
266 """Authenticate command - requires response processing.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000267
Tim Peters07e99cb2001-01-14 23:47:14 +0000268 'mechanism' specifies which authentication mechanism is to
269 be used - it must appear in <instance>.capabilities in the
270 form AUTH=<mechanism>.
Guido van Rossumeda960a1998-06-18 14:24:28 +0000271
Tim Peters07e99cb2001-01-14 23:47:14 +0000272 'authobject' must be a callable object:
Guido van Rossumeda960a1998-06-18 14:24:28 +0000273
Tim Peters07e99cb2001-01-14 23:47:14 +0000274 data = authobject(response)
Guido van Rossumeda960a1998-06-18 14:24:28 +0000275
Tim Peters07e99cb2001-01-14 23:47:14 +0000276 It will be called to process server continuation responses.
277 It should return data that will be encoded and sent to server.
278 It should return None if the client abort response '*' should
279 be sent instead.
280 """
281 mech = string.upper(mechanism)
282 cap = 'AUTH=%s' % mech
283 if not cap in self.capabilities:
284 raise self.error("Server doesn't allow %s authentication." % mech)
285 self.literal = _Authenticator(authobject).process
286 typ, dat = self._simple_command('AUTHENTICATE', mech)
287 if typ != 'OK':
288 raise self.error(dat[-1])
289 self.state = 'AUTH'
290 return typ, dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000291
292
Tim Peters07e99cb2001-01-14 23:47:14 +0000293 def check(self):
294 """Checkpoint mailbox on server.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000295
Tim Peters07e99cb2001-01-14 23:47:14 +0000296 (typ, [data]) = <instance>.check()
297 """
298 return self._simple_command('CHECK')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000299
300
Tim Peters07e99cb2001-01-14 23:47:14 +0000301 def close(self):
302 """Close currently selected mailbox.
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000303
Tim Peters07e99cb2001-01-14 23:47:14 +0000304 Deleted messages are removed from writable mailbox.
305 This is the recommended command before 'LOGOUT'.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000306
Tim Peters07e99cb2001-01-14 23:47:14 +0000307 (typ, [data]) = <instance>.close()
308 """
309 try:
310 typ, dat = self._simple_command('CLOSE')
311 finally:
312 self.state = 'AUTH'
313 return typ, dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000314
315
Tim Peters07e99cb2001-01-14 23:47:14 +0000316 def copy(self, message_set, new_mailbox):
317 """Copy 'message_set' messages onto end of 'new_mailbox'.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000318
Tim Peters07e99cb2001-01-14 23:47:14 +0000319 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
320 """
321 return self._simple_command('COPY', message_set, new_mailbox)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000322
323
Tim Peters07e99cb2001-01-14 23:47:14 +0000324 def create(self, mailbox):
325 """Create new mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000326
Tim Peters07e99cb2001-01-14 23:47:14 +0000327 (typ, [data]) = <instance>.create(mailbox)
328 """
329 return self._simple_command('CREATE', mailbox)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000330
331
Tim Peters07e99cb2001-01-14 23:47:14 +0000332 def delete(self, mailbox):
333 """Delete old mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000334
Tim Peters07e99cb2001-01-14 23:47:14 +0000335 (typ, [data]) = <instance>.delete(mailbox)
336 """
337 return self._simple_command('DELETE', mailbox)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000338
339
Tim Peters07e99cb2001-01-14 23:47:14 +0000340 def expunge(self):
341 """Permanently remove deleted items from selected mailbox.
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000342
Tim Peters07e99cb2001-01-14 23:47:14 +0000343 Generates 'EXPUNGE' response for each deleted message.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000344
Tim Peters07e99cb2001-01-14 23:47:14 +0000345 (typ, [data]) = <instance>.expunge()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000346
Tim Peters07e99cb2001-01-14 23:47:14 +0000347 'data' is list of 'EXPUNGE'd message numbers in order received.
348 """
349 name = 'EXPUNGE'
350 typ, dat = self._simple_command(name)
351 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000352
353
Tim Peters07e99cb2001-01-14 23:47:14 +0000354 def fetch(self, message_set, message_parts):
355 """Fetch (parts of) messages.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000356
Tim Peters07e99cb2001-01-14 23:47:14 +0000357 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000358
Tim Peters07e99cb2001-01-14 23:47:14 +0000359 'message_parts' should be a string of selected parts
360 enclosed in parentheses, eg: "(UID BODY[TEXT])".
Fred Drakefd267d92000-05-25 03:25:26 +0000361
Tim Peters07e99cb2001-01-14 23:47:14 +0000362 'data' are tuples of message part envelope and data.
363 """
364 name = 'FETCH'
365 typ, dat = self._simple_command(name, message_set, message_parts)
366 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000367
368
Tim Peters07e99cb2001-01-14 23:47:14 +0000369 def list(self, directory='""', pattern='*'):
370 """List mailbox names in directory matching pattern.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000371
Tim Peters07e99cb2001-01-14 23:47:14 +0000372 (typ, [data]) = <instance>.list(directory='""', pattern='*')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000373
Tim Peters07e99cb2001-01-14 23:47:14 +0000374 'data' is list of LIST responses.
375 """
376 name = 'LIST'
377 typ, dat = self._simple_command(name, directory, pattern)
378 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000379
380
Tim Peters07e99cb2001-01-14 23:47:14 +0000381 def login(self, user, password):
382 """Identify client using plaintext password.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000383
Tim Peters07e99cb2001-01-14 23:47:14 +0000384 (typ, [data]) = <instance>.login(user, password)
Guido van Rossum8c062211999-12-13 23:27:45 +0000385
Tim Peters07e99cb2001-01-14 23:47:14 +0000386 NB: 'password' will be quoted.
387 """
388 #if not 'AUTH=LOGIN' in self.capabilities:
389 # raise self.error("Server doesn't allow LOGIN authentication." % mech)
390 typ, dat = self._simple_command('LOGIN', user, self._quote(password))
391 if typ != 'OK':
392 raise self.error(dat[-1])
393 self.state = 'AUTH'
394 return typ, dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000395
396
Tim Peters07e99cb2001-01-14 23:47:14 +0000397 def logout(self):
398 """Shutdown connection to server.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000399
Tim Peters07e99cb2001-01-14 23:47:14 +0000400 (typ, [data]) = <instance>.logout()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000401
Tim Peters07e99cb2001-01-14 23:47:14 +0000402 Returns server 'BYE' response.
403 """
404 self.state = 'LOGOUT'
405 try: typ, dat = self._simple_command('LOGOUT')
406 except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
407 self.file.close()
408 self.sock.close()
409 if self.untagged_responses.has_key('BYE'):
410 return 'BYE', self.untagged_responses['BYE']
411 return typ, dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000412
413
Tim Peters07e99cb2001-01-14 23:47:14 +0000414 def lsub(self, directory='""', pattern='*'):
415 """List 'subscribed' mailbox names in directory matching pattern.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000416
Tim Peters07e99cb2001-01-14 23:47:14 +0000417 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000418
Tim Peters07e99cb2001-01-14 23:47:14 +0000419 'data' are tuples of message part envelope and data.
420 """
421 name = 'LSUB'
422 typ, dat = self._simple_command(name, directory, pattern)
423 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000424
425
Tim Peters07e99cb2001-01-14 23:47:14 +0000426 def noop(self):
427 """Send NOOP command.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000428
Tim Peters07e99cb2001-01-14 23:47:14 +0000429 (typ, data) = <instance>.noop()
430 """
431 if __debug__:
432 if self.debug >= 3:
433 _dump_ur(self.untagged_responses)
434 return self._simple_command('NOOP')
Guido van Rossum6884af71998-05-29 13:34:03 +0000435
436
Tim Peters07e99cb2001-01-14 23:47:14 +0000437 def partial(self, message_num, message_part, start, length):
438 """Fetch truncated part of a message.
Guido van Rossumeda960a1998-06-18 14:24:28 +0000439
Tim Peters07e99cb2001-01-14 23:47:14 +0000440 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
Guido van Rossumeda960a1998-06-18 14:24:28 +0000441
Tim Peters07e99cb2001-01-14 23:47:14 +0000442 'data' is tuple of message part envelope and data.
443 """
444 name = 'PARTIAL'
445 typ, dat = self._simple_command(name, message_num, message_part, start, length)
446 return self._untagged_response(typ, dat, 'FETCH')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000447
448
Tim Peters07e99cb2001-01-14 23:47:14 +0000449 def rename(self, oldmailbox, newmailbox):
450 """Rename old mailbox name to new.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000451
Tim Peters07e99cb2001-01-14 23:47:14 +0000452 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
453 """
454 return self._simple_command('RENAME', oldmailbox, newmailbox)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000455
456
Tim Peters07e99cb2001-01-14 23:47:14 +0000457 def search(self, charset, *criteria):
458 """Search mailbox for matching messages.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000459
Tim Peters07e99cb2001-01-14 23:47:14 +0000460 (typ, [data]) = <instance>.search(charset, criterium, ...)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000461
Tim Peters07e99cb2001-01-14 23:47:14 +0000462 'data' is space separated list of matching message numbers.
463 """
464 name = 'SEARCH'
465 if charset:
466 charset = 'CHARSET ' + charset
467 typ, dat = apply(self._simple_command, (name, charset) + criteria)
468 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000469
470
Tim Peters07e99cb2001-01-14 23:47:14 +0000471 def select(self, mailbox='INBOX', readonly=None):
472 """Select a mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000473
Tim Peters07e99cb2001-01-14 23:47:14 +0000474 Flush all untagged responses.
Guido van Rossum46586821998-05-18 14:39:42 +0000475
Tim Peters07e99cb2001-01-14 23:47:14 +0000476 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000477
Tim Peters07e99cb2001-01-14 23:47:14 +0000478 'data' is count of messages in mailbox ('EXISTS' response).
479 """
480 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
481 self.untagged_responses = {} # Flush old responses.
482 self.is_readonly = readonly
483 if readonly:
484 name = 'EXAMINE'
485 else:
486 name = 'SELECT'
487 typ, dat = self._simple_command(name, mailbox)
488 if typ != 'OK':
489 self.state = 'AUTH' # Might have been 'SELECTED'
490 return typ, dat
491 self.state = 'SELECTED'
492 if self.untagged_responses.has_key('READ-ONLY') \
493 and not readonly:
494 if __debug__:
495 if self.debug >= 1:
496 _dump_ur(self.untagged_responses)
497 raise self.readonly('%s is not writable' % mailbox)
498 return typ, self.untagged_responses.get('EXISTS', [None])
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000499
500
Tim Peters07e99cb2001-01-14 23:47:14 +0000501 def status(self, mailbox, names):
502 """Request named status conditions for mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000503
Tim Peters07e99cb2001-01-14 23:47:14 +0000504 (typ, [data]) = <instance>.status(mailbox, names)
505 """
506 name = 'STATUS'
507 if self.PROTOCOL_VERSION == 'IMAP4':
508 raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
509 typ, dat = self._simple_command(name, mailbox, names)
510 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000511
512
Tim Peters07e99cb2001-01-14 23:47:14 +0000513 def store(self, message_set, command, flags):
514 """Alters flag dispositions for messages in mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000515
Tim Peters07e99cb2001-01-14 23:47:14 +0000516 (typ, [data]) = <instance>.store(message_set, command, flags)
517 """
518 if (flags[0],flags[-1]) != ('(',')'):
519 flags = '(%s)' % flags # Avoid quoting the flags
520 typ, dat = self._simple_command('STORE', message_set, command, flags)
521 return self._untagged_response(typ, dat, 'FETCH')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000522
523
Tim Peters07e99cb2001-01-14 23:47:14 +0000524 def subscribe(self, mailbox):
525 """Subscribe to new mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000526
Tim Peters07e99cb2001-01-14 23:47:14 +0000527 (typ, [data]) = <instance>.subscribe(mailbox)
528 """
529 return self._simple_command('SUBSCRIBE', mailbox)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000530
531
Tim Peters07e99cb2001-01-14 23:47:14 +0000532 def uid(self, command, *args):
533 """Execute "command arg ..." with messages identified by UID,
534 rather than message number.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000535
Tim Peters07e99cb2001-01-14 23:47:14 +0000536 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000537
Tim Peters07e99cb2001-01-14 23:47:14 +0000538 Returns response appropriate to 'command'.
539 """
540 command = string.upper(command)
541 if not Commands.has_key(command):
542 raise self.error("Unknown IMAP4 UID command: %s" % command)
543 if self.state not in Commands[command]:
544 raise self.error('command %s illegal in state %s'
545 % (command, self.state))
546 name = 'UID'
547 typ, dat = apply(self._simple_command, (name, command) + args)
548 if command == 'SEARCH':
549 name = 'SEARCH'
550 else:
551 name = 'FETCH'
552 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000553
554
Tim Peters07e99cb2001-01-14 23:47:14 +0000555 def unsubscribe(self, mailbox):
556 """Unsubscribe from old mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000557
Tim Peters07e99cb2001-01-14 23:47:14 +0000558 (typ, [data]) = <instance>.unsubscribe(mailbox)
559 """
560 return self._simple_command('UNSUBSCRIBE', mailbox)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000561
562
Tim Peters07e99cb2001-01-14 23:47:14 +0000563 def xatom(self, name, *args):
564 """Allow simple extension commands
565 notified by server in CAPABILITY response.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000566
Tim Peters07e99cb2001-01-14 23:47:14 +0000567 (typ, [data]) = <instance>.xatom(name, arg, ...)
568 """
569 if name[0] != 'X' or not name in self.capabilities:
570 raise self.error('unknown extension command: %s' % name)
571 return apply(self._simple_command, (name,) + args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000572
573
574
Tim Peters07e99cb2001-01-14 23:47:14 +0000575 # Private methods
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000576
577
Tim Peters07e99cb2001-01-14 23:47:14 +0000578 def _append_untagged(self, typ, dat):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000579
Tim Peters07e99cb2001-01-14 23:47:14 +0000580 if dat is None: dat = ''
581 ur = self.untagged_responses
582 if __debug__:
583 if self.debug >= 5:
584 _mesg('untagged_responses[%s] %s += ["%s"]' %
585 (typ, len(ur.get(typ,'')), dat))
586 if ur.has_key(typ):
587 ur[typ].append(dat)
588 else:
589 ur[typ] = [dat]
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000590
591
Tim Peters07e99cb2001-01-14 23:47:14 +0000592 def _check_bye(self):
593 bye = self.untagged_responses.get('BYE')
594 if bye:
595 raise self.abort(bye[-1])
Guido van Rossum8c062211999-12-13 23:27:45 +0000596
597
Tim Peters07e99cb2001-01-14 23:47:14 +0000598 def _command(self, name, *args):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000599
Tim Peters07e99cb2001-01-14 23:47:14 +0000600 if self.state not in Commands[name]:
601 self.literal = None
602 raise self.error(
603 'command %s illegal in state %s' % (name, self.state))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000604
Tim Peters07e99cb2001-01-14 23:47:14 +0000605 for typ in ('OK', 'NO', 'BAD'):
606 if self.untagged_responses.has_key(typ):
607 del self.untagged_responses[typ]
Guido van Rossum26367a01998-09-28 15:34:46 +0000608
Tim Peters07e99cb2001-01-14 23:47:14 +0000609 if self.untagged_responses.has_key('READ-ONLY') \
610 and not self.is_readonly:
611 raise self.readonly('mailbox status changed to READ-ONLY')
Guido van Rossumeda960a1998-06-18 14:24:28 +0000612
Tim Peters07e99cb2001-01-14 23:47:14 +0000613 tag = self._new_tag()
614 data = '%s %s' % (tag, name)
615 for arg in args:
616 if arg is None: continue
617 data = '%s %s' % (data, self._checkquote(arg))
Guido van Rossum6884af71998-05-29 13:34:03 +0000618
Tim Peters07e99cb2001-01-14 23:47:14 +0000619 literal = self.literal
620 if literal is not None:
621 self.literal = None
622 if type(literal) is type(self._command):
623 literator = literal
624 else:
625 literator = None
626 data = '%s {%s}' % (data, len(literal))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000627
Tim Peters07e99cb2001-01-14 23:47:14 +0000628 if __debug__:
629 if self.debug >= 4:
630 _mesg('> %s' % data)
631 else:
632 _log('> %s' % data)
Guido van Rossum8c062211999-12-13 23:27:45 +0000633
Tim Peters07e99cb2001-01-14 23:47:14 +0000634 try:
635 self.sock.send('%s%s' % (data, CRLF))
636 except socket.error, val:
637 raise self.abort('socket error: %s' % val)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000638
Tim Peters07e99cb2001-01-14 23:47:14 +0000639 if literal is None:
640 return tag
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000641
Tim Peters07e99cb2001-01-14 23:47:14 +0000642 while 1:
643 # Wait for continuation response
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000644
Tim Peters07e99cb2001-01-14 23:47:14 +0000645 while self._get_response():
646 if self.tagged_commands[tag]: # BAD/NO?
647 return tag
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000648
Tim Peters07e99cb2001-01-14 23:47:14 +0000649 # Send literal
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000650
Tim Peters07e99cb2001-01-14 23:47:14 +0000651 if literator:
652 literal = literator(self.continuation_response)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000653
Tim Peters07e99cb2001-01-14 23:47:14 +0000654 if __debug__:
655 if self.debug >= 4:
656 _mesg('write literal size %s' % len(literal))
Guido van Rossumeda960a1998-06-18 14:24:28 +0000657
Tim Peters07e99cb2001-01-14 23:47:14 +0000658 try:
659 self.sock.send(literal)
660 self.sock.send(CRLF)
661 except socket.error, val:
662 raise self.abort('socket error: %s' % val)
Guido van Rossumeda960a1998-06-18 14:24:28 +0000663
Tim Peters07e99cb2001-01-14 23:47:14 +0000664 if not literator:
665 break
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000666
Tim Peters07e99cb2001-01-14 23:47:14 +0000667 return tag
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000668
669
Tim Peters07e99cb2001-01-14 23:47:14 +0000670 def _command_complete(self, name, tag):
671 self._check_bye()
672 try:
673 typ, data = self._get_tagged_response(tag)
674 except self.abort, val:
675 raise self.abort('command: %s => %s' % (name, val))
676 except self.error, val:
677 raise self.error('command: %s => %s' % (name, val))
678 self._check_bye()
679 if typ == 'BAD':
680 raise self.error('%s command error: %s %s' % (name, typ, data))
681 return typ, data
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000682
683
Tim Peters07e99cb2001-01-14 23:47:14 +0000684 def _get_response(self):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000685
Tim Peters07e99cb2001-01-14 23:47:14 +0000686 # Read response and store.
687 #
688 # Returns None for continuation responses,
689 # otherwise first response line received.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000690
Tim Peters07e99cb2001-01-14 23:47:14 +0000691 resp = self._get_line()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000692
Tim Peters07e99cb2001-01-14 23:47:14 +0000693 # Command completion response?
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000694
Tim Peters07e99cb2001-01-14 23:47:14 +0000695 if self._match(self.tagre, resp):
696 tag = self.mo.group('tag')
697 if not self.tagged_commands.has_key(tag):
698 raise self.abort('unexpected tagged response: %s' % resp)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000699
Tim Peters07e99cb2001-01-14 23:47:14 +0000700 typ = self.mo.group('type')
701 dat = self.mo.group('data')
702 self.tagged_commands[tag] = (typ, [dat])
703 else:
704 dat2 = None
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000705
Tim Peters07e99cb2001-01-14 23:47:14 +0000706 # '*' (untagged) responses?
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000707
Tim Peters07e99cb2001-01-14 23:47:14 +0000708 if not self._match(Untagged_response, resp):
709 if self._match(Untagged_status, resp):
710 dat2 = self.mo.group('data2')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000711
Tim Peters07e99cb2001-01-14 23:47:14 +0000712 if self.mo is None:
713 # Only other possibility is '+' (continuation) response...
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000714
Tim Peters07e99cb2001-01-14 23:47:14 +0000715 if self._match(Continuation, resp):
716 self.continuation_response = self.mo.group('data')
717 return None # NB: indicates continuation
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000718
Tim Peters07e99cb2001-01-14 23:47:14 +0000719 raise self.abort("unexpected response: '%s'" % resp)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000720
Tim Peters07e99cb2001-01-14 23:47:14 +0000721 typ = self.mo.group('type')
722 dat = self.mo.group('data')
723 if dat is None: dat = '' # Null untagged response
724 if dat2: dat = dat + ' ' + dat2
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000725
Tim Peters07e99cb2001-01-14 23:47:14 +0000726 # Is there a literal to come?
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000727
Tim Peters07e99cb2001-01-14 23:47:14 +0000728 while self._match(Literal, dat):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000729
Tim Peters07e99cb2001-01-14 23:47:14 +0000730 # Read literal direct from connection.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000731
Tim Peters07e99cb2001-01-14 23:47:14 +0000732 size = string.atoi(self.mo.group('size'))
733 if __debug__:
734 if self.debug >= 4:
735 _mesg('read literal size %s' % size)
736 data = self.file.read(size)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000737
Tim Peters07e99cb2001-01-14 23:47:14 +0000738 # Store response with literal as tuple
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000739
Tim Peters07e99cb2001-01-14 23:47:14 +0000740 self._append_untagged(typ, (dat, data))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000741
Tim Peters07e99cb2001-01-14 23:47:14 +0000742 # Read trailer - possibly containing another literal
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000743
Tim Peters07e99cb2001-01-14 23:47:14 +0000744 dat = self._get_line()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000745
Tim Peters07e99cb2001-01-14 23:47:14 +0000746 self._append_untagged(typ, dat)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000747
Tim Peters07e99cb2001-01-14 23:47:14 +0000748 # Bracketed response information?
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000749
Tim Peters07e99cb2001-01-14 23:47:14 +0000750 if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
751 self._append_untagged(self.mo.group('type'), self.mo.group('data'))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000752
Tim Peters07e99cb2001-01-14 23:47:14 +0000753 if __debug__:
754 if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
755 _mesg('%s response: %s' % (typ, dat))
Guido van Rossum26367a01998-09-28 15:34:46 +0000756
Tim Peters07e99cb2001-01-14 23:47:14 +0000757 return resp
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000758
759
Tim Peters07e99cb2001-01-14 23:47:14 +0000760 def _get_tagged_response(self, tag):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000761
Tim Peters07e99cb2001-01-14 23:47:14 +0000762 while 1:
763 result = self.tagged_commands[tag]
764 if result is not None:
765 del self.tagged_commands[tag]
766 return result
Guido van Rossumf36b1822000-02-17 17:12:39 +0000767
Tim Peters07e99cb2001-01-14 23:47:14 +0000768 # Some have reported "unexpected response" exceptions.
769 # Note that ignoring them here causes loops.
770 # Instead, send me details of the unexpected response and
771 # I'll update the code in `_get_response()'.
Guido van Rossumf36b1822000-02-17 17:12:39 +0000772
Tim Peters07e99cb2001-01-14 23:47:14 +0000773 try:
774 self._get_response()
775 except self.abort, val:
776 if __debug__:
777 if self.debug >= 1:
778 print_log()
779 raise
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000780
781
Tim Peters07e99cb2001-01-14 23:47:14 +0000782 def _get_line(self):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000783
Tim Peters07e99cb2001-01-14 23:47:14 +0000784 line = self.file.readline()
785 if not line:
786 raise self.abort('socket error: EOF')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000787
Tim Peters07e99cb2001-01-14 23:47:14 +0000788 # Protocol mandates all lines terminated by CRLF
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000789
Tim Peters07e99cb2001-01-14 23:47:14 +0000790 line = line[:-2]
791 if __debug__:
792 if self.debug >= 4:
793 _mesg('< %s' % line)
794 else:
795 _log('< %s' % line)
796 return line
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000797
798
Tim Peters07e99cb2001-01-14 23:47:14 +0000799 def _match(self, cre, s):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000800
Tim Peters07e99cb2001-01-14 23:47:14 +0000801 # Run compiled regular expression match method on 's'.
802 # Save result, return success.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000803
Tim Peters07e99cb2001-01-14 23:47:14 +0000804 self.mo = cre.match(s)
805 if __debug__:
806 if self.mo is not None and self.debug >= 5:
807 _mesg("\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`))
808 return self.mo is not None
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000809
810
Tim Peters07e99cb2001-01-14 23:47:14 +0000811 def _new_tag(self):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000812
Tim Peters07e99cb2001-01-14 23:47:14 +0000813 tag = '%s%s' % (self.tagpre, self.tagnum)
814 self.tagnum = self.tagnum + 1
815 self.tagged_commands[tag] = None
816 return tag
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000817
818
Tim Peters07e99cb2001-01-14 23:47:14 +0000819 def _checkquote(self, arg):
Guido van Rossum8c062211999-12-13 23:27:45 +0000820
Tim Peters07e99cb2001-01-14 23:47:14 +0000821 # Must quote command args if non-alphanumeric chars present,
822 # and not already quoted.
Guido van Rossum8c062211999-12-13 23:27:45 +0000823
Tim Peters07e99cb2001-01-14 23:47:14 +0000824 if type(arg) is not type(''):
825 return arg
826 if (arg[0],arg[-1]) in (('(',')'),('"','"')):
827 return arg
828 if self.mustquote.search(arg) is None:
829 return arg
830 return self._quote(arg)
Guido van Rossum8c062211999-12-13 23:27:45 +0000831
832
Tim Peters07e99cb2001-01-14 23:47:14 +0000833 def _quote(self, arg):
Guido van Rossum8c062211999-12-13 23:27:45 +0000834
Tim Peters07e99cb2001-01-14 23:47:14 +0000835 arg = string.replace(arg, '\\', '\\\\')
836 arg = string.replace(arg, '"', '\\"')
Guido van Rossum8c062211999-12-13 23:27:45 +0000837
Tim Peters07e99cb2001-01-14 23:47:14 +0000838 return '"%s"' % arg
Guido van Rossum8c062211999-12-13 23:27:45 +0000839
840
Tim Peters07e99cb2001-01-14 23:47:14 +0000841 def _simple_command(self, name, *args):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000842
Tim Peters07e99cb2001-01-14 23:47:14 +0000843 return self._command_complete(name, apply(self._command, (name,) + args))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000844
845
Tim Peters07e99cb2001-01-14 23:47:14 +0000846 def _untagged_response(self, typ, dat, name):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000847
Tim Peters07e99cb2001-01-14 23:47:14 +0000848 if typ == 'NO':
849 return typ, dat
850 if not self.untagged_responses.has_key(name):
851 return typ, [None]
852 data = self.untagged_responses[name]
853 if __debug__:
854 if self.debug >= 5:
855 _mesg('untagged_responses[%s] => %s' % (name, data))
856 del self.untagged_responses[name]
857 return typ, data
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000858
859
860
Guido van Rossumeda960a1998-06-18 14:24:28 +0000861class _Authenticator:
862
Tim Peters07e99cb2001-01-14 23:47:14 +0000863 """Private class to provide en/decoding
864 for base64-based authentication conversation.
865 """
Guido van Rossumeda960a1998-06-18 14:24:28 +0000866
Tim Peters07e99cb2001-01-14 23:47:14 +0000867 def __init__(self, mechinst):
868 self.mech = mechinst # Callable object to provide/process data
Guido van Rossumeda960a1998-06-18 14:24:28 +0000869
Tim Peters07e99cb2001-01-14 23:47:14 +0000870 def process(self, data):
871 ret = self.mech(self.decode(data))
872 if ret is None:
873 return '*' # Abort conversation
874 return self.encode(ret)
Guido van Rossumeda960a1998-06-18 14:24:28 +0000875
Tim Peters07e99cb2001-01-14 23:47:14 +0000876 def encode(self, inp):
877 #
878 # Invoke binascii.b2a_base64 iteratively with
879 # short even length buffers, strip the trailing
880 # line feed from the result and append. "Even"
881 # means a number that factors to both 6 and 8,
882 # so when it gets to the end of the 8-bit input
883 # there's no partial 6-bit output.
884 #
885 oup = ''
886 while inp:
887 if len(inp) > 48:
888 t = inp[:48]
889 inp = inp[48:]
890 else:
891 t = inp
892 inp = ''
893 e = binascii.b2a_base64(t)
894 if e:
895 oup = oup + e[:-1]
896 return oup
897
898 def decode(self, inp):
899 if not inp:
900 return ''
901 return binascii.a2b_base64(inp)
902
Guido van Rossumeda960a1998-06-18 14:24:28 +0000903
904
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000905Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
Tim Peters07e99cb2001-01-14 23:47:14 +0000906 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000907
908def Internaldate2tuple(resp):
909
Tim Peters07e99cb2001-01-14 23:47:14 +0000910 """Convert IMAP4 INTERNALDATE to UT.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000911
Tim Peters07e99cb2001-01-14 23:47:14 +0000912 Returns Python time module tuple.
913 """
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000914
Tim Peters07e99cb2001-01-14 23:47:14 +0000915 mo = InternalDate.match(resp)
916 if not mo:
917 return None
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000918
Tim Peters07e99cb2001-01-14 23:47:14 +0000919 mon = Mon2num[mo.group('mon')]
920 zonen = mo.group('zonen')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000921
Tim Peters07e99cb2001-01-14 23:47:14 +0000922 for name in ('day', 'year', 'hour', 'min', 'sec', 'zoneh', 'zonem'):
923 exec "%s = string.atoi(mo.group('%s'))" % (name, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000924
Tim Peters07e99cb2001-01-14 23:47:14 +0000925 # INTERNALDATE timezone must be subtracted to get UT
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000926
Tim Peters07e99cb2001-01-14 23:47:14 +0000927 zone = (zoneh*60 + zonem)*60
928 if zonen == '-':
929 zone = -zone
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000930
Tim Peters07e99cb2001-01-14 23:47:14 +0000931 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000932
Tim Peters07e99cb2001-01-14 23:47:14 +0000933 utc = time.mktime(tt)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000934
Tim Peters07e99cb2001-01-14 23:47:14 +0000935 # Following is necessary because the time module has no 'mkgmtime'.
936 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000937
Tim Peters07e99cb2001-01-14 23:47:14 +0000938 lt = time.localtime(utc)
939 if time.daylight and lt[-1]:
940 zone = zone + time.altzone
941 else:
942 zone = zone + time.timezone
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000943
Tim Peters07e99cb2001-01-14 23:47:14 +0000944 return time.localtime(utc - zone)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000945
946
947
948def Int2AP(num):
949
Tim Peters07e99cb2001-01-14 23:47:14 +0000950 """Convert integer to A-P string representation."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000951
Tim Peters07e99cb2001-01-14 23:47:14 +0000952 val = ''; AP = 'ABCDEFGHIJKLMNOP'
953 num = int(abs(num))
954 while num:
955 num, mod = divmod(num, 16)
956 val = AP[mod] + val
957 return val
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000958
959
960
961def ParseFlags(resp):
962
Tim Peters07e99cb2001-01-14 23:47:14 +0000963 """Convert IMAP4 flags response to python tuple."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000964
Tim Peters07e99cb2001-01-14 23:47:14 +0000965 mo = Flags.match(resp)
966 if not mo:
967 return ()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000968
Tim Peters07e99cb2001-01-14 23:47:14 +0000969 return tuple(string.split(mo.group('flags')))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000970
971
972def Time2Internaldate(date_time):
973
Tim Peters07e99cb2001-01-14 23:47:14 +0000974 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000975
Tim Peters07e99cb2001-01-14 23:47:14 +0000976 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
977 """
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000978
Tim Peters07e99cb2001-01-14 23:47:14 +0000979 dttype = type(date_time)
980 if dttype is type(1) or dttype is type(1.1):
981 tt = time.localtime(date_time)
982 elif dttype is type(()):
983 tt = date_time
984 elif dttype is type(""):
985 return date_time # Assume in correct format
986 else: raise ValueError
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000987
Tim Peters07e99cb2001-01-14 23:47:14 +0000988 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
989 if dt[0] == '0':
990 dt = ' ' + dt[1:]
991 if time.daylight and tt[-1]:
992 zone = -time.altzone
993 else:
994 zone = -time.timezone
995 return '"' + dt + " %+02d%02d" % divmod(zone/60, 60) + '"'
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000996
997
998
Guido van Rossumeda960a1998-06-18 14:24:28 +0000999if __debug__:
1000
Tim Peters07e99cb2001-01-14 23:47:14 +00001001 def _mesg(s, secs=None):
1002 if secs is None:
1003 secs = time.time()
1004 tm = time.strftime('%M:%S', time.localtime(secs))
1005 sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s))
1006 sys.stderr.flush()
Guido van Rossum26367a01998-09-28 15:34:46 +00001007
Tim Peters07e99cb2001-01-14 23:47:14 +00001008 def _dump_ur(dict):
1009 # Dump untagged responses (in `dict').
1010 l = dict.items()
1011 if not l: return
1012 t = '\n\t\t'
1013 j = string.join
1014 l = map(lambda x,j=j:'%s: "%s"' % (x[0], x[1][0] and j(x[1], '" "') or ''), l)
1015 _mesg('untagged responses dump:%s%s' % (t, j(l, t)))
Guido van Rossumeda960a1998-06-18 14:24:28 +00001016
Tim Peters07e99cb2001-01-14 23:47:14 +00001017 _cmd_log = [] # Last `_cmd_log_len' interactions
1018 _cmd_log_len = 10
Guido van Rossum8c062211999-12-13 23:27:45 +00001019
Tim Peters07e99cb2001-01-14 23:47:14 +00001020 def _log(line):
1021 # Keep log of last `_cmd_log_len' interactions for debugging.
1022 if len(_cmd_log) == _cmd_log_len:
1023 del _cmd_log[0]
1024 _cmd_log.append((time.time(), line))
Guido van Rossum8c062211999-12-13 23:27:45 +00001025
Tim Peters07e99cb2001-01-14 23:47:14 +00001026 def print_log():
1027 _mesg('last %d IMAP4 interactions:' % len(_cmd_log))
1028 for secs,line in _cmd_log:
1029 _mesg(line, secs)
Guido van Rossumeda960a1998-06-18 14:24:28 +00001030
1031
Guido van Rossum8c062211999-12-13 23:27:45 +00001032
1033if __name__ == '__main__':
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001034
Tim Peters07e99cb2001-01-14 23:47:14 +00001035 import getopt, getpass, sys
Guido van Rossumd6596931998-05-29 18:08:48 +00001036
Tim Peters07e99cb2001-01-14 23:47:14 +00001037 try:
1038 optlist, args = getopt.getopt(sys.argv[1:], 'd:')
1039 except getopt.error, val:
1040 pass
Guido van Rossum66d45132000-03-28 20:20:53 +00001041
Tim Peters07e99cb2001-01-14 23:47:14 +00001042 for opt,val in optlist:
1043 if opt == '-d':
1044 Debug = int(val)
Guido van Rossum66d45132000-03-28 20:20:53 +00001045
Tim Peters07e99cb2001-01-14 23:47:14 +00001046 if not args: args = ('',)
Guido van Rossum66d45132000-03-28 20:20:53 +00001047
Tim Peters07e99cb2001-01-14 23:47:14 +00001048 host = args[0]
Guido van Rossumb1f08121998-06-25 02:22:16 +00001049
Tim Peters07e99cb2001-01-14 23:47:14 +00001050 USER = getpass.getuser()
1051 PASSWD = getpass.getpass("IMAP password for %s on %s" % (USER, host or "localhost"))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001052
Tim Peters07e99cb2001-01-14 23:47:14 +00001053 test_mesg = 'From: %s@localhost\nSubject: IMAP4 test\n\ndata...\n' % USER
1054 test_seq1 = (
1055 ('login', (USER, PASSWD)),
1056 ('create', ('/tmp/xxx 1',)),
1057 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1058 ('CREATE', ('/tmp/yyz 2',)),
1059 ('append', ('/tmp/yyz 2', None, None, test_mesg)),
1060 ('list', ('/tmp', 'yy*')),
1061 ('select', ('/tmp/yyz 2',)),
1062 ('search', (None, 'SUBJECT', 'test')),
1063 ('partial', ('1', 'RFC822', 1, 1024)),
1064 ('store', ('1', 'FLAGS', '(\Deleted)')),
1065 ('expunge', ()),
1066 ('recent', ()),
1067 ('close', ()),
1068 )
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001069
Tim Peters07e99cb2001-01-14 23:47:14 +00001070 test_seq2 = (
1071 ('select', ()),
1072 ('response',('UIDVALIDITY',)),
1073 ('uid', ('SEARCH', 'ALL')),
1074 ('response', ('EXISTS',)),
1075 ('append', (None, None, None, test_mesg)),
1076 ('recent', ()),
1077 ('logout', ()),
1078 )
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001079
Tim Peters07e99cb2001-01-14 23:47:14 +00001080 def run(cmd, args):
1081 _mesg('%s %s' % (cmd, args))
1082 typ, dat = apply(eval('M.%s' % cmd), args)
1083 _mesg('%s => %s %s' % (cmd, typ, dat))
1084 return dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001085
Tim Peters07e99cb2001-01-14 23:47:14 +00001086 try:
1087 M = IMAP4(host)
1088 _mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001089
Tim Peters07e99cb2001-01-14 23:47:14 +00001090 for cmd,args in test_seq1:
1091 run(cmd, args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001092
Tim Peters07e99cb2001-01-14 23:47:14 +00001093 for ml in run('list', ('/tmp/', 'yy%')):
1094 mo = re.match(r'.*"([^"]+)"$', ml)
1095 if mo: path = mo.group(1)
1096 else: path = string.split(ml)[-1]
1097 run('delete', (path,))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001098
Tim Peters07e99cb2001-01-14 23:47:14 +00001099 for cmd,args in test_seq2:
1100 dat = run(cmd, args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001101
Tim Peters07e99cb2001-01-14 23:47:14 +00001102 if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
1103 continue
Guido van Rossum38d8f4e1998-04-11 01:22:34 +00001104
Tim Peters07e99cb2001-01-14 23:47:14 +00001105 uid = string.split(dat[-1])
1106 if not uid: continue
1107 run('uid', ('FETCH', '%s' % uid[-1],
1108 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
Guido van Rossum66d45132000-03-28 20:20:53 +00001109
Tim Peters07e99cb2001-01-14 23:47:14 +00001110 print '\nAll tests OK.'
Guido van Rossum66d45132000-03-28 20:20:53 +00001111
Tim Peters07e99cb2001-01-14 23:47:14 +00001112 except:
1113 print '\nTests failed.'
Guido van Rossum66d45132000-03-28 20:20:53 +00001114
Tim Peters07e99cb2001-01-14 23:47:14 +00001115 if not Debug:
1116 print '''
Guido van Rossum66d45132000-03-28 20:20:53 +00001117If you would like to see debugging output,
1118try: %s -d5
1119''' % sys.argv[0]
1120
Tim Peters07e99cb2001-01-14 23:47:14 +00001121 raise