blob: 954ecf65d10553255e6ebf83c89d546f1d9eee26 [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
Tim Peters07e99cb2001-01-14 23:47:14 +000021# Globals
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000022
23CRLF = '\r\n'
24Debug = 0
25IMAP4_PORT = 143
Tim Peters07e99cb2001-01-14 23:47:14 +000026AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000027
Tim Peters07e99cb2001-01-14 23:47:14 +000028# Commands
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000029
30Commands = {
Tim Peters07e99cb2001-01-14 23:47:14 +000031 # name valid states
32 'APPEND': ('AUTH', 'SELECTED'),
33 'AUTHENTICATE': ('NONAUTH',),
34 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
35 'CHECK': ('SELECTED',),
36 'CLOSE': ('SELECTED',),
37 'COPY': ('SELECTED',),
38 'CREATE': ('AUTH', 'SELECTED'),
39 'DELETE': ('AUTH', 'SELECTED'),
40 'EXAMINE': ('AUTH', 'SELECTED'),
41 'EXPUNGE': ('SELECTED',),
42 'FETCH': ('SELECTED',),
43 'LIST': ('AUTH', 'SELECTED'),
44 'LOGIN': ('NONAUTH',),
45 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
46 'LSUB': ('AUTH', 'SELECTED'),
47 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
48 'PARTIAL': ('SELECTED',),
49 'RENAME': ('AUTH', 'SELECTED'),
50 'SEARCH': ('SELECTED',),
51 'SELECT': ('AUTH', 'SELECTED'),
52 'STATUS': ('AUTH', 'SELECTED'),
53 'STORE': ('SELECTED',),
54 'SUBSCRIBE': ('AUTH', 'SELECTED'),
55 'UID': ('SELECTED',),
56 'UNSUBSCRIBE': ('AUTH', 'SELECTED'),
57 }
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000058
Tim Peters07e99cb2001-01-14 23:47:14 +000059# Patterns to match server responses
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000060
Guido van Rossumeda960a1998-06-18 14:24:28 +000061Continuation = re.compile(r'\+( (?P<data>.*))?')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000062Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
63InternalDate = re.compile(r'.*INTERNALDATE "'
Tim Peters07e99cb2001-01-14 23:47:14 +000064 r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
65 r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
66 r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
67 r'"')
Guido van Rossumf36b1822000-02-17 17:12:39 +000068Literal = re.compile(r'.*{(?P<size>\d+)}$')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000069Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
Guido van Rossumeda960a1998-06-18 14:24:28 +000070Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000071Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
72
73
74
75class IMAP4:
76
Tim Peters07e99cb2001-01-14 23:47:14 +000077 """IMAP4 client class.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000078
Tim Peters07e99cb2001-01-14 23:47:14 +000079 Instantiate with: IMAP4([host[, port]])
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000080
Tim Peters07e99cb2001-01-14 23:47:14 +000081 host - host's name (default: localhost);
82 port - port number (default: standard IMAP4 port).
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000083
Tim Peters07e99cb2001-01-14 23:47:14 +000084 All IMAP4rev1 commands are supported by methods of the same
85 name (in lower-case).
Guido van Rossum6884af71998-05-29 13:34:03 +000086
Tim Peters07e99cb2001-01-14 23:47:14 +000087 All arguments to commands are converted to strings, except for
88 AUTHENTICATE, and the last argument to APPEND which is passed as
89 an IMAP4 literal. If necessary (the string contains any
90 non-printing characters or white-space and isn't enclosed with
91 either parentheses or double quotes) each string is quoted.
92 However, the 'password' argument to the LOGIN command is always
93 quoted. If you want to avoid having an argument string quoted
94 (eg: the 'flags' argument to STORE) then enclose the string in
95 parentheses (eg: "(\Deleted)").
Guido van Rossum6884af71998-05-29 13:34:03 +000096
Tim Peters07e99cb2001-01-14 23:47:14 +000097 Each command returns a tuple: (type, [data, ...]) where 'type'
98 is usually 'OK' or 'NO', and 'data' is either the text from the
99 tagged response, or untagged results from command.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000100
Tim Peters07e99cb2001-01-14 23:47:14 +0000101 Errors raise the exception class <instance>.error("<reason>").
102 IMAP4 server errors raise <instance>.abort("<reason>"),
103 which is a sub-class of 'error'. Mailbox status changes
104 from READ-WRITE to READ-ONLY raise the exception class
105 <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
Guido van Rossumeda960a1998-06-18 14:24:28 +0000106
Tim Peters07e99cb2001-01-14 23:47:14 +0000107 "error" exceptions imply a program error.
108 "abort" exceptions imply the connection should be reset, and
109 the command re-tried.
110 "readonly" exceptions imply the command should be re-tried.
Guido van Rossum8c062211999-12-13 23:27:45 +0000111
Tim Peters07e99cb2001-01-14 23:47:14 +0000112 Note: to use this module, you must read the RFCs pertaining
113 to the IMAP4 protocol, as the semantics of the arguments to
114 each IMAP4 command are left to the invoker, not to mention
115 the results.
116 """
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000117
Tim Peters07e99cb2001-01-14 23:47:14 +0000118 class error(Exception): pass # Logical errors - debug required
119 class abort(error): pass # Service errors - close and retry
120 class readonly(abort): pass # Mailbox status changed to READ-ONLY
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000121
Tim Peters07e99cb2001-01-14 23:47:14 +0000122 mustquote = re.compile(r"[^\w!#$%&'*+,.:;<=>?^`|~-]")
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000123
Tim Peters07e99cb2001-01-14 23:47:14 +0000124 def __init__(self, host = '', port = IMAP4_PORT):
125 self.host = host
126 self.port = port
127 self.debug = Debug
128 self.state = 'LOGOUT'
129 self.literal = None # A literal argument to a command
130 self.tagged_commands = {} # Tagged commands awaiting response
131 self.untagged_responses = {} # {typ: [data, ...], ...}
132 self.continuation_response = '' # Last continuation response
133 self.is_readonly = None # READ-ONLY desired state
134 self.tagnum = 0
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000135
Tim Peters07e99cb2001-01-14 23:47:14 +0000136 # Open socket to server.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000137
Tim Peters07e99cb2001-01-14 23:47:14 +0000138 self.open(host, port)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000139
Tim Peters07e99cb2001-01-14 23:47:14 +0000140 # Create unique tag for this session,
141 # and compile tagged response matcher.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000142
Tim Peters07e99cb2001-01-14 23:47:14 +0000143 self.tagpre = Int2AP(random.randint(0, 31999))
144 self.tagre = re.compile(r'(?P<tag>'
145 + self.tagpre
146 + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000147
Tim Peters07e99cb2001-01-14 23:47:14 +0000148 # Get server welcome message,
149 # request and store CAPABILITY response.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000150
Tim Peters07e99cb2001-01-14 23:47:14 +0000151 if __debug__:
152 if self.debug >= 1:
153 _mesg('new IMAP4 connection, tag=%s' % self.tagpre)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000154
Tim Peters07e99cb2001-01-14 23:47:14 +0000155 self.welcome = self._get_response()
156 if self.untagged_responses.has_key('PREAUTH'):
157 self.state = 'AUTH'
158 elif self.untagged_responses.has_key('OK'):
159 self.state = 'NONAUTH'
160 else:
161 raise self.error(self.welcome)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000162
Tim Peters07e99cb2001-01-14 23:47:14 +0000163 cap = 'CAPABILITY'
164 self._simple_command(cap)
165 if not self.untagged_responses.has_key(cap):
166 raise self.error('no CAPABILITY response from server')
167 self.capabilities = tuple(string.split(string.upper(self.untagged_responses[cap][-1])))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000168
Tim Peters07e99cb2001-01-14 23:47:14 +0000169 if __debug__:
170 if self.debug >= 3:
171 _mesg('CAPABILITIES: %s' % `self.capabilities`)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000172
Tim Peters07e99cb2001-01-14 23:47:14 +0000173 for version in AllowedVersions:
174 if not version in self.capabilities:
175 continue
176 self.PROTOCOL_VERSION = version
177 return
Guido van Rossumb1f08121998-06-25 02:22:16 +0000178
Tim Peters07e99cb2001-01-14 23:47:14 +0000179 raise self.error('server not IMAP4 compliant')
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000180
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000181
Tim Peters07e99cb2001-01-14 23:47:14 +0000182 def __getattr__(self, attr):
183 # Allow UPPERCASE variants of IMAP4 command methods.
184 if Commands.has_key(attr):
185 return eval("self.%s" % string.lower(attr))
186 raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
Guido van Rossum26367a01998-09-28 15:34:46 +0000187
188
189
Tim Peters07e99cb2001-01-14 23:47:14 +0000190 # Public methods
Guido van Rossum26367a01998-09-28 15:34:46 +0000191
192
Tim Peters07e99cb2001-01-14 23:47:14 +0000193 def open(self, host, port):
194 """Setup 'self.sock' and 'self.file'."""
195 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
196 self.sock.connect((self.host, self.port))
197 self.file = self.sock.makefile('r')
Guido van Rossumeda960a1998-06-18 14:24:28 +0000198
199
Tim Peters07e99cb2001-01-14 23:47:14 +0000200 def recent(self):
201 """Return most recent 'RECENT' responses if any exist,
202 else prompt server for an update using the 'NOOP' command.
Guido van Rossum26367a01998-09-28 15:34:46 +0000203
Tim Peters07e99cb2001-01-14 23:47:14 +0000204 (typ, [data]) = <instance>.recent()
Guido van Rossum26367a01998-09-28 15:34:46 +0000205
Tim Peters07e99cb2001-01-14 23:47:14 +0000206 'data' is None if no new messages,
207 else list of RECENT responses, most recent last.
208 """
209 name = 'RECENT'
210 typ, dat = self._untagged_response('OK', [None], name)
211 if dat[-1]:
212 return typ, dat
213 typ, dat = self.noop() # Prod server for response
214 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000215
216
Tim Peters07e99cb2001-01-14 23:47:14 +0000217 def response(self, code):
218 """Return data for response 'code' if received, or None.
Guido van Rossum26367a01998-09-28 15:34:46 +0000219
Tim Peters07e99cb2001-01-14 23:47:14 +0000220 Old value for response 'code' is cleared.
Guido van Rossum26367a01998-09-28 15:34:46 +0000221
Tim Peters07e99cb2001-01-14 23:47:14 +0000222 (code, [data]) = <instance>.response(code)
223 """
224 return self._untagged_response(code, [None], string.upper(code))
Guido van Rossum26367a01998-09-28 15:34:46 +0000225
226
Tim Peters07e99cb2001-01-14 23:47:14 +0000227 def socket(self):
228 """Return socket instance used to connect to IMAP4 server.
Guido van Rossum26367a01998-09-28 15:34:46 +0000229
Tim Peters07e99cb2001-01-14 23:47:14 +0000230 socket = <instance>.socket()
231 """
232 return self.sock
Guido van Rossum26367a01998-09-28 15:34:46 +0000233
234
235
Tim Peters07e99cb2001-01-14 23:47:14 +0000236 # IMAP4 commands
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000237
238
Tim Peters07e99cb2001-01-14 23:47:14 +0000239 def append(self, mailbox, flags, date_time, message):
240 """Append message to named mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000241
Tim Peters07e99cb2001-01-14 23:47:14 +0000242 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
Guido van Rossum8c062211999-12-13 23:27:45 +0000243
Tim Peters07e99cb2001-01-14 23:47:14 +0000244 All args except `message' can be None.
245 """
246 name = 'APPEND'
247 if not mailbox:
248 mailbox = 'INBOX'
249 if flags:
250 if (flags[0],flags[-1]) != ('(',')'):
251 flags = '(%s)' % flags
252 else:
253 flags = None
254 if date_time:
255 date_time = Time2Internaldate(date_time)
256 else:
257 date_time = None
258 self.literal = message
259 return self._simple_command(name, mailbox, flags, date_time)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000260
261
Tim Peters07e99cb2001-01-14 23:47:14 +0000262 def authenticate(self, mechanism, authobject):
263 """Authenticate command - requires response processing.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000264
Tim Peters07e99cb2001-01-14 23:47:14 +0000265 'mechanism' specifies which authentication mechanism is to
266 be used - it must appear in <instance>.capabilities in the
267 form AUTH=<mechanism>.
Guido van Rossumeda960a1998-06-18 14:24:28 +0000268
Tim Peters07e99cb2001-01-14 23:47:14 +0000269 'authobject' must be a callable object:
Guido van Rossumeda960a1998-06-18 14:24:28 +0000270
Tim Peters07e99cb2001-01-14 23:47:14 +0000271 data = authobject(response)
Guido van Rossumeda960a1998-06-18 14:24:28 +0000272
Tim Peters07e99cb2001-01-14 23:47:14 +0000273 It will be called to process server continuation responses.
274 It should return data that will be encoded and sent to server.
275 It should return None if the client abort response '*' should
276 be sent instead.
277 """
278 mech = string.upper(mechanism)
279 cap = 'AUTH=%s' % mech
280 if not cap in self.capabilities:
281 raise self.error("Server doesn't allow %s authentication." % mech)
282 self.literal = _Authenticator(authobject).process
283 typ, dat = self._simple_command('AUTHENTICATE', mech)
284 if typ != 'OK':
285 raise self.error(dat[-1])
286 self.state = 'AUTH'
287 return typ, dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000288
289
Tim Peters07e99cb2001-01-14 23:47:14 +0000290 def check(self):
291 """Checkpoint mailbox on server.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000292
Tim Peters07e99cb2001-01-14 23:47:14 +0000293 (typ, [data]) = <instance>.check()
294 """
295 return self._simple_command('CHECK')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000296
297
Tim Peters07e99cb2001-01-14 23:47:14 +0000298 def close(self):
299 """Close currently selected mailbox.
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000300
Tim Peters07e99cb2001-01-14 23:47:14 +0000301 Deleted messages are removed from writable mailbox.
302 This is the recommended command before 'LOGOUT'.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000303
Tim Peters07e99cb2001-01-14 23:47:14 +0000304 (typ, [data]) = <instance>.close()
305 """
306 try:
307 typ, dat = self._simple_command('CLOSE')
308 finally:
309 self.state = 'AUTH'
310 return typ, dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000311
312
Tim Peters07e99cb2001-01-14 23:47:14 +0000313 def copy(self, message_set, new_mailbox):
314 """Copy 'message_set' messages onto end of 'new_mailbox'.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000315
Tim Peters07e99cb2001-01-14 23:47:14 +0000316 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
317 """
318 return self._simple_command('COPY', message_set, new_mailbox)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000319
320
Tim Peters07e99cb2001-01-14 23:47:14 +0000321 def create(self, mailbox):
322 """Create new mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000323
Tim Peters07e99cb2001-01-14 23:47:14 +0000324 (typ, [data]) = <instance>.create(mailbox)
325 """
326 return self._simple_command('CREATE', mailbox)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000327
328
Tim Peters07e99cb2001-01-14 23:47:14 +0000329 def delete(self, mailbox):
330 """Delete old mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000331
Tim Peters07e99cb2001-01-14 23:47:14 +0000332 (typ, [data]) = <instance>.delete(mailbox)
333 """
334 return self._simple_command('DELETE', mailbox)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000335
336
Tim Peters07e99cb2001-01-14 23:47:14 +0000337 def expunge(self):
338 """Permanently remove deleted items from selected mailbox.
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000339
Tim Peters07e99cb2001-01-14 23:47:14 +0000340 Generates 'EXPUNGE' response for each deleted message.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000341
Tim Peters07e99cb2001-01-14 23:47:14 +0000342 (typ, [data]) = <instance>.expunge()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000343
Tim Peters07e99cb2001-01-14 23:47:14 +0000344 'data' is list of 'EXPUNGE'd message numbers in order received.
345 """
346 name = 'EXPUNGE'
347 typ, dat = self._simple_command(name)
348 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000349
350
Tim Peters07e99cb2001-01-14 23:47:14 +0000351 def fetch(self, message_set, message_parts):
352 """Fetch (parts of) messages.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000353
Tim Peters07e99cb2001-01-14 23:47:14 +0000354 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000355
Tim Peters07e99cb2001-01-14 23:47:14 +0000356 'message_parts' should be a string of selected parts
357 enclosed in parentheses, eg: "(UID BODY[TEXT])".
Fred Drakefd267d92000-05-25 03:25:26 +0000358
Tim Peters07e99cb2001-01-14 23:47:14 +0000359 'data' are tuples of message part envelope and data.
360 """
361 name = 'FETCH'
362 typ, dat = self._simple_command(name, message_set, message_parts)
363 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000364
365
Tim Peters07e99cb2001-01-14 23:47:14 +0000366 def list(self, directory='""', pattern='*'):
367 """List mailbox names in directory matching pattern.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000368
Tim Peters07e99cb2001-01-14 23:47:14 +0000369 (typ, [data]) = <instance>.list(directory='""', pattern='*')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000370
Tim Peters07e99cb2001-01-14 23:47:14 +0000371 'data' is list of LIST responses.
372 """
373 name = 'LIST'
374 typ, dat = self._simple_command(name, directory, pattern)
375 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000376
377
Tim Peters07e99cb2001-01-14 23:47:14 +0000378 def login(self, user, password):
379 """Identify client using plaintext password.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000380
Tim Peters07e99cb2001-01-14 23:47:14 +0000381 (typ, [data]) = <instance>.login(user, password)
Guido van Rossum8c062211999-12-13 23:27:45 +0000382
Tim Peters07e99cb2001-01-14 23:47:14 +0000383 NB: 'password' will be quoted.
384 """
385 #if not 'AUTH=LOGIN' in self.capabilities:
386 # raise self.error("Server doesn't allow LOGIN authentication." % mech)
387 typ, dat = self._simple_command('LOGIN', user, self._quote(password))
388 if typ != 'OK':
389 raise self.error(dat[-1])
390 self.state = 'AUTH'
391 return typ, dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000392
393
Tim Peters07e99cb2001-01-14 23:47:14 +0000394 def logout(self):
395 """Shutdown connection to server.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000396
Tim Peters07e99cb2001-01-14 23:47:14 +0000397 (typ, [data]) = <instance>.logout()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000398
Tim Peters07e99cb2001-01-14 23:47:14 +0000399 Returns server 'BYE' response.
400 """
401 self.state = 'LOGOUT'
402 try: typ, dat = self._simple_command('LOGOUT')
403 except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
404 self.file.close()
405 self.sock.close()
406 if self.untagged_responses.has_key('BYE'):
407 return 'BYE', self.untagged_responses['BYE']
408 return typ, dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000409
410
Tim Peters07e99cb2001-01-14 23:47:14 +0000411 def lsub(self, directory='""', pattern='*'):
412 """List 'subscribed' mailbox names in directory matching pattern.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000413
Tim Peters07e99cb2001-01-14 23:47:14 +0000414 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000415
Tim Peters07e99cb2001-01-14 23:47:14 +0000416 'data' are tuples of message part envelope and data.
417 """
418 name = 'LSUB'
419 typ, dat = self._simple_command(name, directory, pattern)
420 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000421
422
Tim Peters07e99cb2001-01-14 23:47:14 +0000423 def noop(self):
424 """Send NOOP command.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000425
Tim Peters07e99cb2001-01-14 23:47:14 +0000426 (typ, data) = <instance>.noop()
427 """
428 if __debug__:
429 if self.debug >= 3:
430 _dump_ur(self.untagged_responses)
431 return self._simple_command('NOOP')
Guido van Rossum6884af71998-05-29 13:34:03 +0000432
433
Tim Peters07e99cb2001-01-14 23:47:14 +0000434 def partial(self, message_num, message_part, start, length):
435 """Fetch truncated part of a message.
Guido van Rossumeda960a1998-06-18 14:24:28 +0000436
Tim Peters07e99cb2001-01-14 23:47:14 +0000437 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
Guido van Rossumeda960a1998-06-18 14:24:28 +0000438
Tim Peters07e99cb2001-01-14 23:47:14 +0000439 'data' is tuple of message part envelope and data.
440 """
441 name = 'PARTIAL'
442 typ, dat = self._simple_command(name, message_num, message_part, start, length)
443 return self._untagged_response(typ, dat, 'FETCH')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000444
445
Tim Peters07e99cb2001-01-14 23:47:14 +0000446 def rename(self, oldmailbox, newmailbox):
447 """Rename old mailbox name to new.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000448
Tim Peters07e99cb2001-01-14 23:47:14 +0000449 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
450 """
451 return self._simple_command('RENAME', oldmailbox, newmailbox)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000452
453
Tim Peters07e99cb2001-01-14 23:47:14 +0000454 def search(self, charset, *criteria):
455 """Search mailbox for matching messages.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000456
Tim Peters07e99cb2001-01-14 23:47:14 +0000457 (typ, [data]) = <instance>.search(charset, criterium, ...)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000458
Tim Peters07e99cb2001-01-14 23:47:14 +0000459 'data' is space separated list of matching message numbers.
460 """
461 name = 'SEARCH'
462 if charset:
463 charset = 'CHARSET ' + charset
464 typ, dat = apply(self._simple_command, (name, charset) + criteria)
465 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000466
467
Tim Peters07e99cb2001-01-14 23:47:14 +0000468 def select(self, mailbox='INBOX', readonly=None):
469 """Select a mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000470
Tim Peters07e99cb2001-01-14 23:47:14 +0000471 Flush all untagged responses.
Guido van Rossum46586821998-05-18 14:39:42 +0000472
Tim Peters07e99cb2001-01-14 23:47:14 +0000473 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000474
Tim Peters07e99cb2001-01-14 23:47:14 +0000475 'data' is count of messages in mailbox ('EXISTS' response).
476 """
477 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
478 self.untagged_responses = {} # Flush old responses.
479 self.is_readonly = readonly
480 if readonly:
481 name = 'EXAMINE'
482 else:
483 name = 'SELECT'
484 typ, dat = self._simple_command(name, mailbox)
485 if typ != 'OK':
486 self.state = 'AUTH' # Might have been 'SELECTED'
487 return typ, dat
488 self.state = 'SELECTED'
489 if self.untagged_responses.has_key('READ-ONLY') \
490 and not readonly:
491 if __debug__:
492 if self.debug >= 1:
493 _dump_ur(self.untagged_responses)
494 raise self.readonly('%s is not writable' % mailbox)
495 return typ, self.untagged_responses.get('EXISTS', [None])
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000496
497
Tim Peters07e99cb2001-01-14 23:47:14 +0000498 def status(self, mailbox, names):
499 """Request named status conditions for mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000500
Tim Peters07e99cb2001-01-14 23:47:14 +0000501 (typ, [data]) = <instance>.status(mailbox, names)
502 """
503 name = 'STATUS'
504 if self.PROTOCOL_VERSION == 'IMAP4':
505 raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
506 typ, dat = self._simple_command(name, mailbox, names)
507 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000508
509
Tim Peters07e99cb2001-01-14 23:47:14 +0000510 def store(self, message_set, command, flags):
511 """Alters flag dispositions for messages in mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000512
Tim Peters07e99cb2001-01-14 23:47:14 +0000513 (typ, [data]) = <instance>.store(message_set, command, flags)
514 """
515 if (flags[0],flags[-1]) != ('(',')'):
516 flags = '(%s)' % flags # Avoid quoting the flags
517 typ, dat = self._simple_command('STORE', message_set, command, flags)
518 return self._untagged_response(typ, dat, 'FETCH')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000519
520
Tim Peters07e99cb2001-01-14 23:47:14 +0000521 def subscribe(self, mailbox):
522 """Subscribe to new mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000523
Tim Peters07e99cb2001-01-14 23:47:14 +0000524 (typ, [data]) = <instance>.subscribe(mailbox)
525 """
526 return self._simple_command('SUBSCRIBE', mailbox)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000527
528
Tim Peters07e99cb2001-01-14 23:47:14 +0000529 def uid(self, command, *args):
530 """Execute "command arg ..." with messages identified by UID,
531 rather than message number.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000532
Tim Peters07e99cb2001-01-14 23:47:14 +0000533 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000534
Tim Peters07e99cb2001-01-14 23:47:14 +0000535 Returns response appropriate to 'command'.
536 """
537 command = string.upper(command)
538 if not Commands.has_key(command):
539 raise self.error("Unknown IMAP4 UID command: %s" % command)
540 if self.state not in Commands[command]:
541 raise self.error('command %s illegal in state %s'
542 % (command, self.state))
543 name = 'UID'
544 typ, dat = apply(self._simple_command, (name, command) + args)
545 if command == 'SEARCH':
546 name = 'SEARCH'
547 else:
548 name = 'FETCH'
549 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000550
551
Tim Peters07e99cb2001-01-14 23:47:14 +0000552 def unsubscribe(self, mailbox):
553 """Unsubscribe from old mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000554
Tim Peters07e99cb2001-01-14 23:47:14 +0000555 (typ, [data]) = <instance>.unsubscribe(mailbox)
556 """
557 return self._simple_command('UNSUBSCRIBE', mailbox)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000558
559
Tim Peters07e99cb2001-01-14 23:47:14 +0000560 def xatom(self, name, *args):
561 """Allow simple extension commands
562 notified by server in CAPABILITY response.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000563
Tim Peters07e99cb2001-01-14 23:47:14 +0000564 (typ, [data]) = <instance>.xatom(name, arg, ...)
565 """
566 if name[0] != 'X' or not name in self.capabilities:
567 raise self.error('unknown extension command: %s' % name)
568 return apply(self._simple_command, (name,) + args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000569
570
571
Tim Peters07e99cb2001-01-14 23:47:14 +0000572 # Private methods
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000573
574
Tim Peters07e99cb2001-01-14 23:47:14 +0000575 def _append_untagged(self, typ, dat):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000576
Tim Peters07e99cb2001-01-14 23:47:14 +0000577 if dat is None: dat = ''
578 ur = self.untagged_responses
579 if __debug__:
580 if self.debug >= 5:
581 _mesg('untagged_responses[%s] %s += ["%s"]' %
582 (typ, len(ur.get(typ,'')), dat))
583 if ur.has_key(typ):
584 ur[typ].append(dat)
585 else:
586 ur[typ] = [dat]
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000587
588
Tim Peters07e99cb2001-01-14 23:47:14 +0000589 def _check_bye(self):
590 bye = self.untagged_responses.get('BYE')
591 if bye:
592 raise self.abort(bye[-1])
Guido van Rossum8c062211999-12-13 23:27:45 +0000593
594
Tim Peters07e99cb2001-01-14 23:47:14 +0000595 def _command(self, name, *args):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000596
Tim Peters07e99cb2001-01-14 23:47:14 +0000597 if self.state not in Commands[name]:
598 self.literal = None
599 raise self.error(
600 'command %s illegal in state %s' % (name, self.state))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000601
Tim Peters07e99cb2001-01-14 23:47:14 +0000602 for typ in ('OK', 'NO', 'BAD'):
603 if self.untagged_responses.has_key(typ):
604 del self.untagged_responses[typ]
Guido van Rossum26367a01998-09-28 15:34:46 +0000605
Tim Peters07e99cb2001-01-14 23:47:14 +0000606 if self.untagged_responses.has_key('READ-ONLY') \
607 and not self.is_readonly:
608 raise self.readonly('mailbox status changed to READ-ONLY')
Guido van Rossumeda960a1998-06-18 14:24:28 +0000609
Tim Peters07e99cb2001-01-14 23:47:14 +0000610 tag = self._new_tag()
611 data = '%s %s' % (tag, name)
612 for arg in args:
613 if arg is None: continue
614 data = '%s %s' % (data, self._checkquote(arg))
Guido van Rossum6884af71998-05-29 13:34:03 +0000615
Tim Peters07e99cb2001-01-14 23:47:14 +0000616 literal = self.literal
617 if literal is not None:
618 self.literal = None
619 if type(literal) is type(self._command):
620 literator = literal
621 else:
622 literator = None
623 data = '%s {%s}' % (data, len(literal))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000624
Tim Peters07e99cb2001-01-14 23:47:14 +0000625 if __debug__:
626 if self.debug >= 4:
627 _mesg('> %s' % data)
628 else:
629 _log('> %s' % data)
Guido van Rossum8c062211999-12-13 23:27:45 +0000630
Tim Peters07e99cb2001-01-14 23:47:14 +0000631 try:
632 self.sock.send('%s%s' % (data, CRLF))
633 except socket.error, val:
634 raise self.abort('socket error: %s' % val)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000635
Tim Peters07e99cb2001-01-14 23:47:14 +0000636 if literal is None:
637 return tag
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000638
Tim Peters07e99cb2001-01-14 23:47:14 +0000639 while 1:
640 # Wait for continuation response
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000641
Tim Peters07e99cb2001-01-14 23:47:14 +0000642 while self._get_response():
643 if self.tagged_commands[tag]: # BAD/NO?
644 return tag
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000645
Tim Peters07e99cb2001-01-14 23:47:14 +0000646 # Send literal
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000647
Tim Peters07e99cb2001-01-14 23:47:14 +0000648 if literator:
649 literal = literator(self.continuation_response)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000650
Tim Peters07e99cb2001-01-14 23:47:14 +0000651 if __debug__:
652 if self.debug >= 4:
653 _mesg('write literal size %s' % len(literal))
Guido van Rossumeda960a1998-06-18 14:24:28 +0000654
Tim Peters07e99cb2001-01-14 23:47:14 +0000655 try:
656 self.sock.send(literal)
657 self.sock.send(CRLF)
658 except socket.error, val:
659 raise self.abort('socket error: %s' % val)
Guido van Rossumeda960a1998-06-18 14:24:28 +0000660
Tim Peters07e99cb2001-01-14 23:47:14 +0000661 if not literator:
662 break
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000663
Tim Peters07e99cb2001-01-14 23:47:14 +0000664 return tag
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000665
666
Tim Peters07e99cb2001-01-14 23:47:14 +0000667 def _command_complete(self, name, tag):
668 self._check_bye()
669 try:
670 typ, data = self._get_tagged_response(tag)
671 except self.abort, val:
672 raise self.abort('command: %s => %s' % (name, val))
673 except self.error, val:
674 raise self.error('command: %s => %s' % (name, val))
675 self._check_bye()
676 if typ == 'BAD':
677 raise self.error('%s command error: %s %s' % (name, typ, data))
678 return typ, data
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000679
680
Tim Peters07e99cb2001-01-14 23:47:14 +0000681 def _get_response(self):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000682
Tim Peters07e99cb2001-01-14 23:47:14 +0000683 # Read response and store.
684 #
685 # Returns None for continuation responses,
686 # otherwise first response line received.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000687
Tim Peters07e99cb2001-01-14 23:47:14 +0000688 resp = self._get_line()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000689
Tim Peters07e99cb2001-01-14 23:47:14 +0000690 # Command completion response?
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000691
Tim Peters07e99cb2001-01-14 23:47:14 +0000692 if self._match(self.tagre, resp):
693 tag = self.mo.group('tag')
694 if not self.tagged_commands.has_key(tag):
695 raise self.abort('unexpected tagged response: %s' % resp)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000696
Tim Peters07e99cb2001-01-14 23:47:14 +0000697 typ = self.mo.group('type')
698 dat = self.mo.group('data')
699 self.tagged_commands[tag] = (typ, [dat])
700 else:
701 dat2 = None
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000702
Tim Peters07e99cb2001-01-14 23:47:14 +0000703 # '*' (untagged) responses?
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000704
Tim Peters07e99cb2001-01-14 23:47:14 +0000705 if not self._match(Untagged_response, resp):
706 if self._match(Untagged_status, resp):
707 dat2 = self.mo.group('data2')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000708
Tim Peters07e99cb2001-01-14 23:47:14 +0000709 if self.mo is None:
710 # Only other possibility is '+' (continuation) response...
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000711
Tim Peters07e99cb2001-01-14 23:47:14 +0000712 if self._match(Continuation, resp):
713 self.continuation_response = self.mo.group('data')
714 return None # NB: indicates continuation
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000715
Tim Peters07e99cb2001-01-14 23:47:14 +0000716 raise self.abort("unexpected response: '%s'" % resp)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000717
Tim Peters07e99cb2001-01-14 23:47:14 +0000718 typ = self.mo.group('type')
719 dat = self.mo.group('data')
720 if dat is None: dat = '' # Null untagged response
721 if dat2: dat = dat + ' ' + dat2
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000722
Tim Peters07e99cb2001-01-14 23:47:14 +0000723 # Is there a literal to come?
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000724
Tim Peters07e99cb2001-01-14 23:47:14 +0000725 while self._match(Literal, dat):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000726
Tim Peters07e99cb2001-01-14 23:47:14 +0000727 # Read literal direct from connection.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000728
Tim Peters07e99cb2001-01-14 23:47:14 +0000729 size = string.atoi(self.mo.group('size'))
730 if __debug__:
731 if self.debug >= 4:
732 _mesg('read literal size %s' % size)
733 data = self.file.read(size)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000734
Tim Peters07e99cb2001-01-14 23:47:14 +0000735 # Store response with literal as tuple
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000736
Tim Peters07e99cb2001-01-14 23:47:14 +0000737 self._append_untagged(typ, (dat, data))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000738
Tim Peters07e99cb2001-01-14 23:47:14 +0000739 # Read trailer - possibly containing another literal
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000740
Tim Peters07e99cb2001-01-14 23:47:14 +0000741 dat = self._get_line()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000742
Tim Peters07e99cb2001-01-14 23:47:14 +0000743 self._append_untagged(typ, dat)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000744
Tim Peters07e99cb2001-01-14 23:47:14 +0000745 # Bracketed response information?
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000746
Tim Peters07e99cb2001-01-14 23:47:14 +0000747 if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
748 self._append_untagged(self.mo.group('type'), self.mo.group('data'))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000749
Tim Peters07e99cb2001-01-14 23:47:14 +0000750 if __debug__:
751 if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
752 _mesg('%s response: %s' % (typ, dat))
Guido van Rossum26367a01998-09-28 15:34:46 +0000753
Tim Peters07e99cb2001-01-14 23:47:14 +0000754 return resp
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000755
756
Tim Peters07e99cb2001-01-14 23:47:14 +0000757 def _get_tagged_response(self, tag):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000758
Tim Peters07e99cb2001-01-14 23:47:14 +0000759 while 1:
760 result = self.tagged_commands[tag]
761 if result is not None:
762 del self.tagged_commands[tag]
763 return result
Guido van Rossumf36b1822000-02-17 17:12:39 +0000764
Tim Peters07e99cb2001-01-14 23:47:14 +0000765 # Some have reported "unexpected response" exceptions.
766 # Note that ignoring them here causes loops.
767 # Instead, send me details of the unexpected response and
768 # I'll update the code in `_get_response()'.
Guido van Rossumf36b1822000-02-17 17:12:39 +0000769
Tim Peters07e99cb2001-01-14 23:47:14 +0000770 try:
771 self._get_response()
772 except self.abort, val:
773 if __debug__:
774 if self.debug >= 1:
775 print_log()
776 raise
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000777
778
Tim Peters07e99cb2001-01-14 23:47:14 +0000779 def _get_line(self):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000780
Tim Peters07e99cb2001-01-14 23:47:14 +0000781 line = self.file.readline()
782 if not line:
783 raise self.abort('socket error: EOF')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000784
Tim Peters07e99cb2001-01-14 23:47:14 +0000785 # Protocol mandates all lines terminated by CRLF
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000786
Tim Peters07e99cb2001-01-14 23:47:14 +0000787 line = line[:-2]
788 if __debug__:
789 if self.debug >= 4:
790 _mesg('< %s' % line)
791 else:
792 _log('< %s' % line)
793 return line
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000794
795
Tim Peters07e99cb2001-01-14 23:47:14 +0000796 def _match(self, cre, s):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000797
Tim Peters07e99cb2001-01-14 23:47:14 +0000798 # Run compiled regular expression match method on 's'.
799 # Save result, return success.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000800
Tim Peters07e99cb2001-01-14 23:47:14 +0000801 self.mo = cre.match(s)
802 if __debug__:
803 if self.mo is not None and self.debug >= 5:
804 _mesg("\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`))
805 return self.mo is not None
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000806
807
Tim Peters07e99cb2001-01-14 23:47:14 +0000808 def _new_tag(self):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000809
Tim Peters07e99cb2001-01-14 23:47:14 +0000810 tag = '%s%s' % (self.tagpre, self.tagnum)
811 self.tagnum = self.tagnum + 1
812 self.tagged_commands[tag] = None
813 return tag
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000814
815
Tim Peters07e99cb2001-01-14 23:47:14 +0000816 def _checkquote(self, arg):
Guido van Rossum8c062211999-12-13 23:27:45 +0000817
Tim Peters07e99cb2001-01-14 23:47:14 +0000818 # Must quote command args if non-alphanumeric chars present,
819 # and not already quoted.
Guido van Rossum8c062211999-12-13 23:27:45 +0000820
Tim Peters07e99cb2001-01-14 23:47:14 +0000821 if type(arg) is not type(''):
822 return arg
823 if (arg[0],arg[-1]) in (('(',')'),('"','"')):
824 return arg
825 if self.mustquote.search(arg) is None:
826 return arg
827 return self._quote(arg)
Guido van Rossum8c062211999-12-13 23:27:45 +0000828
829
Tim Peters07e99cb2001-01-14 23:47:14 +0000830 def _quote(self, arg):
Guido van Rossum8c062211999-12-13 23:27:45 +0000831
Tim Peters07e99cb2001-01-14 23:47:14 +0000832 arg = string.replace(arg, '\\', '\\\\')
833 arg = string.replace(arg, '"', '\\"')
Guido van Rossum8c062211999-12-13 23:27:45 +0000834
Tim Peters07e99cb2001-01-14 23:47:14 +0000835 return '"%s"' % arg
Guido van Rossum8c062211999-12-13 23:27:45 +0000836
837
Tim Peters07e99cb2001-01-14 23:47:14 +0000838 def _simple_command(self, name, *args):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000839
Tim Peters07e99cb2001-01-14 23:47:14 +0000840 return self._command_complete(name, apply(self._command, (name,) + args))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000841
842
Tim Peters07e99cb2001-01-14 23:47:14 +0000843 def _untagged_response(self, typ, dat, name):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000844
Tim Peters07e99cb2001-01-14 23:47:14 +0000845 if typ == 'NO':
846 return typ, dat
847 if not self.untagged_responses.has_key(name):
848 return typ, [None]
849 data = self.untagged_responses[name]
850 if __debug__:
851 if self.debug >= 5:
852 _mesg('untagged_responses[%s] => %s' % (name, data))
853 del self.untagged_responses[name]
854 return typ, data
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000855
856
857
Guido van Rossumeda960a1998-06-18 14:24:28 +0000858class _Authenticator:
859
Tim Peters07e99cb2001-01-14 23:47:14 +0000860 """Private class to provide en/decoding
861 for base64-based authentication conversation.
862 """
Guido van Rossumeda960a1998-06-18 14:24:28 +0000863
Tim Peters07e99cb2001-01-14 23:47:14 +0000864 def __init__(self, mechinst):
865 self.mech = mechinst # Callable object to provide/process data
Guido van Rossumeda960a1998-06-18 14:24:28 +0000866
Tim Peters07e99cb2001-01-14 23:47:14 +0000867 def process(self, data):
868 ret = self.mech(self.decode(data))
869 if ret is None:
870 return '*' # Abort conversation
871 return self.encode(ret)
Guido van Rossumeda960a1998-06-18 14:24:28 +0000872
Tim Peters07e99cb2001-01-14 23:47:14 +0000873 def encode(self, inp):
874 #
875 # Invoke binascii.b2a_base64 iteratively with
876 # short even length buffers, strip the trailing
877 # line feed from the result and append. "Even"
878 # means a number that factors to both 6 and 8,
879 # so when it gets to the end of the 8-bit input
880 # there's no partial 6-bit output.
881 #
882 oup = ''
883 while inp:
884 if len(inp) > 48:
885 t = inp[:48]
886 inp = inp[48:]
887 else:
888 t = inp
889 inp = ''
890 e = binascii.b2a_base64(t)
891 if e:
892 oup = oup + e[:-1]
893 return oup
894
895 def decode(self, inp):
896 if not inp:
897 return ''
898 return binascii.a2b_base64(inp)
899
Guido van Rossumeda960a1998-06-18 14:24:28 +0000900
901
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000902Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
Tim Peters07e99cb2001-01-14 23:47:14 +0000903 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000904
905def Internaldate2tuple(resp):
906
Tim Peters07e99cb2001-01-14 23:47:14 +0000907 """Convert IMAP4 INTERNALDATE to UT.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000908
Tim Peters07e99cb2001-01-14 23:47:14 +0000909 Returns Python time module tuple.
910 """
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000911
Tim Peters07e99cb2001-01-14 23:47:14 +0000912 mo = InternalDate.match(resp)
913 if not mo:
914 return None
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000915
Tim Peters07e99cb2001-01-14 23:47:14 +0000916 mon = Mon2num[mo.group('mon')]
917 zonen = mo.group('zonen')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000918
Tim Peters07e99cb2001-01-14 23:47:14 +0000919 for name in ('day', 'year', 'hour', 'min', 'sec', 'zoneh', 'zonem'):
920 exec "%s = string.atoi(mo.group('%s'))" % (name, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000921
Tim Peters07e99cb2001-01-14 23:47:14 +0000922 # INTERNALDATE timezone must be subtracted to get UT
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000923
Tim Peters07e99cb2001-01-14 23:47:14 +0000924 zone = (zoneh*60 + zonem)*60
925 if zonen == '-':
926 zone = -zone
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000927
Tim Peters07e99cb2001-01-14 23:47:14 +0000928 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000929
Tim Peters07e99cb2001-01-14 23:47:14 +0000930 utc = time.mktime(tt)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000931
Tim Peters07e99cb2001-01-14 23:47:14 +0000932 # Following is necessary because the time module has no 'mkgmtime'.
933 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000934
Tim Peters07e99cb2001-01-14 23:47:14 +0000935 lt = time.localtime(utc)
936 if time.daylight and lt[-1]:
937 zone = zone + time.altzone
938 else:
939 zone = zone + time.timezone
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000940
Tim Peters07e99cb2001-01-14 23:47:14 +0000941 return time.localtime(utc - zone)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000942
943
944
945def Int2AP(num):
946
Tim Peters07e99cb2001-01-14 23:47:14 +0000947 """Convert integer to A-P string representation."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000948
Tim Peters07e99cb2001-01-14 23:47:14 +0000949 val = ''; AP = 'ABCDEFGHIJKLMNOP'
950 num = int(abs(num))
951 while num:
952 num, mod = divmod(num, 16)
953 val = AP[mod] + val
954 return val
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000955
956
957
958def ParseFlags(resp):
959
Tim Peters07e99cb2001-01-14 23:47:14 +0000960 """Convert IMAP4 flags response to python tuple."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000961
Tim Peters07e99cb2001-01-14 23:47:14 +0000962 mo = Flags.match(resp)
963 if not mo:
964 return ()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000965
Tim Peters07e99cb2001-01-14 23:47:14 +0000966 return tuple(string.split(mo.group('flags')))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000967
968
969def Time2Internaldate(date_time):
970
Tim Peters07e99cb2001-01-14 23:47:14 +0000971 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000972
Tim Peters07e99cb2001-01-14 23:47:14 +0000973 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
974 """
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000975
Tim Peters07e99cb2001-01-14 23:47:14 +0000976 dttype = type(date_time)
977 if dttype is type(1) or dttype is type(1.1):
978 tt = time.localtime(date_time)
979 elif dttype is type(()):
980 tt = date_time
981 elif dttype is type(""):
982 return date_time # Assume in correct format
983 else: raise ValueError
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000984
Tim Peters07e99cb2001-01-14 23:47:14 +0000985 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
986 if dt[0] == '0':
987 dt = ' ' + dt[1:]
988 if time.daylight and tt[-1]:
989 zone = -time.altzone
990 else:
991 zone = -time.timezone
992 return '"' + dt + " %+02d%02d" % divmod(zone/60, 60) + '"'
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000993
994
995
Guido van Rossumeda960a1998-06-18 14:24:28 +0000996if __debug__:
997
Tim Peters07e99cb2001-01-14 23:47:14 +0000998 def _mesg(s, secs=None):
999 if secs is None:
1000 secs = time.time()
1001 tm = time.strftime('%M:%S', time.localtime(secs))
1002 sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s))
1003 sys.stderr.flush()
Guido van Rossum26367a01998-09-28 15:34:46 +00001004
Tim Peters07e99cb2001-01-14 23:47:14 +00001005 def _dump_ur(dict):
1006 # Dump untagged responses (in `dict').
1007 l = dict.items()
1008 if not l: return
1009 t = '\n\t\t'
1010 j = string.join
1011 l = map(lambda x,j=j:'%s: "%s"' % (x[0], x[1][0] and j(x[1], '" "') or ''), l)
1012 _mesg('untagged responses dump:%s%s' % (t, j(l, t)))
Guido van Rossumeda960a1998-06-18 14:24:28 +00001013
Tim Peters07e99cb2001-01-14 23:47:14 +00001014 _cmd_log = [] # Last `_cmd_log_len' interactions
1015 _cmd_log_len = 10
Guido van Rossum8c062211999-12-13 23:27:45 +00001016
Tim Peters07e99cb2001-01-14 23:47:14 +00001017 def _log(line):
1018 # Keep log of last `_cmd_log_len' interactions for debugging.
1019 if len(_cmd_log) == _cmd_log_len:
1020 del _cmd_log[0]
1021 _cmd_log.append((time.time(), line))
Guido van Rossum8c062211999-12-13 23:27:45 +00001022
Tim Peters07e99cb2001-01-14 23:47:14 +00001023 def print_log():
1024 _mesg('last %d IMAP4 interactions:' % len(_cmd_log))
1025 for secs,line in _cmd_log:
1026 _mesg(line, secs)
Guido van Rossumeda960a1998-06-18 14:24:28 +00001027
1028
Guido van Rossum8c062211999-12-13 23:27:45 +00001029
1030if __name__ == '__main__':
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001031
Tim Peters07e99cb2001-01-14 23:47:14 +00001032 import getopt, getpass, sys
Guido van Rossumd6596931998-05-29 18:08:48 +00001033
Tim Peters07e99cb2001-01-14 23:47:14 +00001034 try:
1035 optlist, args = getopt.getopt(sys.argv[1:], 'd:')
1036 except getopt.error, val:
1037 pass
Guido van Rossum66d45132000-03-28 20:20:53 +00001038
Tim Peters07e99cb2001-01-14 23:47:14 +00001039 for opt,val in optlist:
1040 if opt == '-d':
1041 Debug = int(val)
Guido van Rossum66d45132000-03-28 20:20:53 +00001042
Tim Peters07e99cb2001-01-14 23:47:14 +00001043 if not args: args = ('',)
Guido van Rossum66d45132000-03-28 20:20:53 +00001044
Tim Peters07e99cb2001-01-14 23:47:14 +00001045 host = args[0]
Guido van Rossumb1f08121998-06-25 02:22:16 +00001046
Tim Peters07e99cb2001-01-14 23:47:14 +00001047 USER = getpass.getuser()
1048 PASSWD = getpass.getpass("IMAP password for %s on %s" % (USER, host or "localhost"))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001049
Tim Peters07e99cb2001-01-14 23:47:14 +00001050 test_mesg = 'From: %s@localhost\nSubject: IMAP4 test\n\ndata...\n' % USER
1051 test_seq1 = (
1052 ('login', (USER, PASSWD)),
1053 ('create', ('/tmp/xxx 1',)),
1054 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1055 ('CREATE', ('/tmp/yyz 2',)),
1056 ('append', ('/tmp/yyz 2', None, None, test_mesg)),
1057 ('list', ('/tmp', 'yy*')),
1058 ('select', ('/tmp/yyz 2',)),
1059 ('search', (None, 'SUBJECT', 'test')),
1060 ('partial', ('1', 'RFC822', 1, 1024)),
1061 ('store', ('1', 'FLAGS', '(\Deleted)')),
1062 ('expunge', ()),
1063 ('recent', ()),
1064 ('close', ()),
1065 )
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001066
Tim Peters07e99cb2001-01-14 23:47:14 +00001067 test_seq2 = (
1068 ('select', ()),
1069 ('response',('UIDVALIDITY',)),
1070 ('uid', ('SEARCH', 'ALL')),
1071 ('response', ('EXISTS',)),
1072 ('append', (None, None, None, test_mesg)),
1073 ('recent', ()),
1074 ('logout', ()),
1075 )
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001076
Tim Peters07e99cb2001-01-14 23:47:14 +00001077 def run(cmd, args):
1078 _mesg('%s %s' % (cmd, args))
1079 typ, dat = apply(eval('M.%s' % cmd), args)
1080 _mesg('%s => %s %s' % (cmd, typ, dat))
1081 return dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001082
Tim Peters07e99cb2001-01-14 23:47:14 +00001083 try:
1084 M = IMAP4(host)
1085 _mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001086
Tim Peters07e99cb2001-01-14 23:47:14 +00001087 for cmd,args in test_seq1:
1088 run(cmd, args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001089
Tim Peters07e99cb2001-01-14 23:47:14 +00001090 for ml in run('list', ('/tmp/', 'yy%')):
1091 mo = re.match(r'.*"([^"]+)"$', ml)
1092 if mo: path = mo.group(1)
1093 else: path = string.split(ml)[-1]
1094 run('delete', (path,))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001095
Tim Peters07e99cb2001-01-14 23:47:14 +00001096 for cmd,args in test_seq2:
1097 dat = run(cmd, args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001098
Tim Peters07e99cb2001-01-14 23:47:14 +00001099 if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
1100 continue
Guido van Rossum38d8f4e1998-04-11 01:22:34 +00001101
Tim Peters07e99cb2001-01-14 23:47:14 +00001102 uid = string.split(dat[-1])
1103 if not uid: continue
1104 run('uid', ('FETCH', '%s' % uid[-1],
1105 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
Guido van Rossum66d45132000-03-28 20:20:53 +00001106
Tim Peters07e99cb2001-01-14 23:47:14 +00001107 print '\nAll tests OK.'
Guido van Rossum66d45132000-03-28 20:20:53 +00001108
Tim Peters07e99cb2001-01-14 23:47:14 +00001109 except:
1110 print '\nTests failed.'
Guido van Rossum66d45132000-03-28 20:20:53 +00001111
Tim Peters07e99cb2001-01-14 23:47:14 +00001112 if not Debug:
1113 print '''
Guido van Rossum66d45132000-03-28 20:20:53 +00001114If you would like to see debugging output,
1115try: %s -d5
1116''' % sys.argv[0]
1117
Tim Peters07e99cb2001-01-14 23:47:14 +00001118 raise