blob: 6aca2992c5b9e18debc434666c1705e385157520 [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.
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +000016# String method conversion by ESR, February 2001.
Guido van Rossum98d9fd32000-02-28 15:12:25 +000017
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +000018__version__ = "2.40"
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000019
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +000020import binascii, re, socket, time, random, sys
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000021
Barry Warsawf4493912001-01-24 04:16:09 +000022__all__ = ["IMAP4", "Internaldate2tuple",
23 "Int2AP", "ParseFlags", "Time2Internaldate"]
Skip Montanaro2dd42762001-01-23 15:35:05 +000024
Tim Peters07e99cb2001-01-14 23:47:14 +000025# Globals
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000026
27CRLF = '\r\n'
28Debug = 0
29IMAP4_PORT = 143
Tim Peters07e99cb2001-01-14 23:47:14 +000030AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000031
Tim Peters07e99cb2001-01-14 23:47:14 +000032# Commands
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000033
34Commands = {
Tim Peters07e99cb2001-01-14 23:47:14 +000035 # name valid states
36 'APPEND': ('AUTH', 'SELECTED'),
37 'AUTHENTICATE': ('NONAUTH',),
38 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
39 'CHECK': ('SELECTED',),
40 'CLOSE': ('SELECTED',),
41 'COPY': ('SELECTED',),
42 'CREATE': ('AUTH', 'SELECTED'),
43 'DELETE': ('AUTH', 'SELECTED'),
44 'EXAMINE': ('AUTH', 'SELECTED'),
45 'EXPUNGE': ('SELECTED',),
46 'FETCH': ('SELECTED',),
47 'LIST': ('AUTH', 'SELECTED'),
48 'LOGIN': ('NONAUTH',),
49 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
50 'LSUB': ('AUTH', 'SELECTED'),
51 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
52 'PARTIAL': ('SELECTED',),
53 'RENAME': ('AUTH', 'SELECTED'),
54 'SEARCH': ('SELECTED',),
55 'SELECT': ('AUTH', 'SELECTED'),
56 'STATUS': ('AUTH', 'SELECTED'),
57 'STORE': ('SELECTED',),
58 'SUBSCRIBE': ('AUTH', 'SELECTED'),
59 'UID': ('SELECTED',),
60 'UNSUBSCRIBE': ('AUTH', 'SELECTED'),
Guido van Rossuma85e2c82001-06-17 13:31:25 +000061 'NAMESPACE': ('AUTH', 'SELECTED'),
Tim Peters07e99cb2001-01-14 23:47:14 +000062 }
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000063
Tim Peters07e99cb2001-01-14 23:47:14 +000064# Patterns to match server responses
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000065
Guido van Rossumeda960a1998-06-18 14:24:28 +000066Continuation = re.compile(r'\+( (?P<data>.*))?')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000067Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
68InternalDate = re.compile(r'.*INTERNALDATE "'
Tim Peters07e99cb2001-01-14 23:47:14 +000069 r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
70 r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
71 r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
72 r'"')
Guido van Rossumf36b1822000-02-17 17:12:39 +000073Literal = re.compile(r'.*{(?P<size>\d+)}$')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000074Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
Guido van Rossumeda960a1998-06-18 14:24:28 +000075Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000076Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
77
78
79
80class IMAP4:
81
Tim Peters07e99cb2001-01-14 23:47:14 +000082 """IMAP4 client class.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000083
Tim Peters07e99cb2001-01-14 23:47:14 +000084 Instantiate with: IMAP4([host[, port]])
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000085
Tim Peters07e99cb2001-01-14 23:47:14 +000086 host - host's name (default: localhost);
87 port - port number (default: standard IMAP4 port).
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000088
Tim Peters07e99cb2001-01-14 23:47:14 +000089 All IMAP4rev1 commands are supported by methods of the same
90 name (in lower-case).
Guido van Rossum6884af71998-05-29 13:34:03 +000091
Tim Peters07e99cb2001-01-14 23:47:14 +000092 All arguments to commands are converted to strings, except for
93 AUTHENTICATE, and the last argument to APPEND which is passed as
94 an IMAP4 literal. If necessary (the string contains any
95 non-printing characters or white-space and isn't enclosed with
96 either parentheses or double quotes) each string is quoted.
97 However, the 'password' argument to the LOGIN command is always
98 quoted. If you want to avoid having an argument string quoted
99 (eg: the 'flags' argument to STORE) then enclose the string in
100 parentheses (eg: "(\Deleted)").
Guido van Rossum6884af71998-05-29 13:34:03 +0000101
Tim Peters07e99cb2001-01-14 23:47:14 +0000102 Each command returns a tuple: (type, [data, ...]) where 'type'
103 is usually 'OK' or 'NO', and 'data' is either the text from the
104 tagged response, or untagged results from command.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000105
Tim Peters07e99cb2001-01-14 23:47:14 +0000106 Errors raise the exception class <instance>.error("<reason>").
107 IMAP4 server errors raise <instance>.abort("<reason>"),
108 which is a sub-class of 'error'. Mailbox status changes
109 from READ-WRITE to READ-ONLY raise the exception class
110 <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
Guido van Rossumeda960a1998-06-18 14:24:28 +0000111
Tim Peters07e99cb2001-01-14 23:47:14 +0000112 "error" exceptions imply a program error.
113 "abort" exceptions imply the connection should be reset, and
114 the command re-tried.
115 "readonly" exceptions imply the command should be re-tried.
Guido van Rossum8c062211999-12-13 23:27:45 +0000116
Tim Peters07e99cb2001-01-14 23:47:14 +0000117 Note: to use this module, you must read the RFCs pertaining
118 to the IMAP4 protocol, as the semantics of the arguments to
119 each IMAP4 command are left to the invoker, not to mention
120 the results.
121 """
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000122
Tim Peters07e99cb2001-01-14 23:47:14 +0000123 class error(Exception): pass # Logical errors - debug required
124 class abort(error): pass # Service errors - close and retry
125 class readonly(abort): pass # Mailbox status changed to READ-ONLY
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000126
Tim Peters07e99cb2001-01-14 23:47:14 +0000127 mustquote = re.compile(r"[^\w!#$%&'*+,.:;<=>?^`|~-]")
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000128
Tim Peters07e99cb2001-01-14 23:47:14 +0000129 def __init__(self, host = '', port = IMAP4_PORT):
130 self.host = host
131 self.port = port
132 self.debug = Debug
133 self.state = 'LOGOUT'
134 self.literal = None # A literal argument to a command
135 self.tagged_commands = {} # Tagged commands awaiting response
136 self.untagged_responses = {} # {typ: [data, ...], ...}
137 self.continuation_response = '' # Last continuation response
138 self.is_readonly = None # READ-ONLY desired state
139 self.tagnum = 0
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000140
Tim Peters07e99cb2001-01-14 23:47:14 +0000141 # Open socket to server.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000142
Tim Peters07e99cb2001-01-14 23:47:14 +0000143 self.open(host, port)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000144
Tim Peters07e99cb2001-01-14 23:47:14 +0000145 # Create unique tag for this session,
146 # and compile tagged response matcher.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000147
Tim Peters07e99cb2001-01-14 23:47:14 +0000148 self.tagpre = Int2AP(random.randint(0, 31999))
149 self.tagre = re.compile(r'(?P<tag>'
150 + self.tagpre
151 + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000152
Tim Peters07e99cb2001-01-14 23:47:14 +0000153 # Get server welcome message,
154 # request and store CAPABILITY response.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000155
Tim Peters07e99cb2001-01-14 23:47:14 +0000156 if __debug__:
157 if self.debug >= 1:
158 _mesg('new IMAP4 connection, tag=%s' % self.tagpre)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000159
Tim Peters07e99cb2001-01-14 23:47:14 +0000160 self.welcome = self._get_response()
161 if self.untagged_responses.has_key('PREAUTH'):
162 self.state = 'AUTH'
163 elif self.untagged_responses.has_key('OK'):
164 self.state = 'NONAUTH'
165 else:
166 raise self.error(self.welcome)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000167
Tim Peters07e99cb2001-01-14 23:47:14 +0000168 cap = 'CAPABILITY'
169 self._simple_command(cap)
170 if not self.untagged_responses.has_key(cap):
171 raise self.error('no CAPABILITY response from server')
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +0000172 self.capabilities = tuple(self.untagged_responses[cap][-1].upper().split())
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000173
Tim Peters07e99cb2001-01-14 23:47:14 +0000174 if __debug__:
175 if self.debug >= 3:
176 _mesg('CAPABILITIES: %s' % `self.capabilities`)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000177
Tim Peters07e99cb2001-01-14 23:47:14 +0000178 for version in AllowedVersions:
179 if not version in self.capabilities:
180 continue
181 self.PROTOCOL_VERSION = version
182 return
Guido van Rossumb1f08121998-06-25 02:22:16 +0000183
Tim Peters07e99cb2001-01-14 23:47:14 +0000184 raise self.error('server not IMAP4 compliant')
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000185
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000186
Tim Peters07e99cb2001-01-14 23:47:14 +0000187 def __getattr__(self, attr):
188 # Allow UPPERCASE variants of IMAP4 command methods.
189 if Commands.has_key(attr):
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +0000190 return eval("self.%s" % attr.lower())
Tim Peters07e99cb2001-01-14 23:47:14 +0000191 raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
Guido van Rossum26367a01998-09-28 15:34:46 +0000192
193
194
Tim Peters07e99cb2001-01-14 23:47:14 +0000195 # Public methods
Guido van Rossum26367a01998-09-28 15:34:46 +0000196
197
Tim Peters07e99cb2001-01-14 23:47:14 +0000198 def open(self, host, port):
199 """Setup 'self.sock' and 'self.file'."""
200 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
201 self.sock.connect((self.host, self.port))
202 self.file = self.sock.makefile('r')
Guido van Rossumeda960a1998-06-18 14:24:28 +0000203
204
Tim Peters07e99cb2001-01-14 23:47:14 +0000205 def recent(self):
206 """Return most recent 'RECENT' responses if any exist,
207 else prompt server for an update using the 'NOOP' command.
Guido van Rossum26367a01998-09-28 15:34:46 +0000208
Tim Peters07e99cb2001-01-14 23:47:14 +0000209 (typ, [data]) = <instance>.recent()
Guido van Rossum26367a01998-09-28 15:34:46 +0000210
Tim Peters07e99cb2001-01-14 23:47:14 +0000211 'data' is None if no new messages,
212 else list of RECENT responses, most recent last.
213 """
214 name = 'RECENT'
215 typ, dat = self._untagged_response('OK', [None], name)
216 if dat[-1]:
217 return typ, dat
218 typ, dat = self.noop() # Prod server for response
219 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000220
221
Tim Peters07e99cb2001-01-14 23:47:14 +0000222 def response(self, code):
223 """Return data for response 'code' if received, or None.
Guido van Rossum26367a01998-09-28 15:34:46 +0000224
Tim Peters07e99cb2001-01-14 23:47:14 +0000225 Old value for response 'code' is cleared.
Guido van Rossum26367a01998-09-28 15:34:46 +0000226
Tim Peters07e99cb2001-01-14 23:47:14 +0000227 (code, [data]) = <instance>.response(code)
228 """
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +0000229 return self._untagged_response(code, [None], code.upper())
Guido van Rossum26367a01998-09-28 15:34:46 +0000230
231
Tim Peters07e99cb2001-01-14 23:47:14 +0000232 def socket(self):
233 """Return socket instance used to connect to IMAP4 server.
Guido van Rossum26367a01998-09-28 15:34:46 +0000234
Tim Peters07e99cb2001-01-14 23:47:14 +0000235 socket = <instance>.socket()
236 """
237 return self.sock
Guido van Rossum26367a01998-09-28 15:34:46 +0000238
239
240
Tim Peters07e99cb2001-01-14 23:47:14 +0000241 # IMAP4 commands
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000242
243
Tim Peters07e99cb2001-01-14 23:47:14 +0000244 def append(self, mailbox, flags, date_time, message):
245 """Append message to named mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000246
Tim Peters07e99cb2001-01-14 23:47:14 +0000247 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
Guido van Rossum8c062211999-12-13 23:27:45 +0000248
Tim Peters07e99cb2001-01-14 23:47:14 +0000249 All args except `message' can be None.
250 """
251 name = 'APPEND'
252 if not mailbox:
253 mailbox = 'INBOX'
254 if flags:
255 if (flags[0],flags[-1]) != ('(',')'):
256 flags = '(%s)' % flags
257 else:
258 flags = None
259 if date_time:
260 date_time = Time2Internaldate(date_time)
261 else:
262 date_time = None
263 self.literal = message
264 return self._simple_command(name, mailbox, flags, date_time)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000265
266
Tim Peters07e99cb2001-01-14 23:47:14 +0000267 def authenticate(self, mechanism, authobject):
268 """Authenticate command - requires response processing.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000269
Tim Peters07e99cb2001-01-14 23:47:14 +0000270 'mechanism' specifies which authentication mechanism is to
271 be used - it must appear in <instance>.capabilities in the
272 form AUTH=<mechanism>.
Guido van Rossumeda960a1998-06-18 14:24:28 +0000273
Tim Peters07e99cb2001-01-14 23:47:14 +0000274 'authobject' must be a callable object:
Guido van Rossumeda960a1998-06-18 14:24:28 +0000275
Tim Peters07e99cb2001-01-14 23:47:14 +0000276 data = authobject(response)
Guido van Rossumeda960a1998-06-18 14:24:28 +0000277
Tim Peters07e99cb2001-01-14 23:47:14 +0000278 It will be called to process server continuation responses.
279 It should return data that will be encoded and sent to server.
280 It should return None if the client abort response '*' should
281 be sent instead.
282 """
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +0000283 mech = mechanism.upper()
Tim Peters07e99cb2001-01-14 23:47:14 +0000284 cap = 'AUTH=%s' % mech
285 if not cap in self.capabilities:
286 raise self.error("Server doesn't allow %s authentication." % mech)
287 self.literal = _Authenticator(authobject).process
288 typ, dat = self._simple_command('AUTHENTICATE', mech)
289 if typ != 'OK':
290 raise self.error(dat[-1])
291 self.state = 'AUTH'
292 return typ, dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000293
294
Tim Peters07e99cb2001-01-14 23:47:14 +0000295 def check(self):
296 """Checkpoint mailbox on server.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000297
Tim Peters07e99cb2001-01-14 23:47:14 +0000298 (typ, [data]) = <instance>.check()
299 """
300 return self._simple_command('CHECK')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000301
302
Tim Peters07e99cb2001-01-14 23:47:14 +0000303 def close(self):
304 """Close currently selected mailbox.
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000305
Tim Peters07e99cb2001-01-14 23:47:14 +0000306 Deleted messages are removed from writable mailbox.
307 This is the recommended command before 'LOGOUT'.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000308
Tim Peters07e99cb2001-01-14 23:47:14 +0000309 (typ, [data]) = <instance>.close()
310 """
311 try:
312 typ, dat = self._simple_command('CLOSE')
313 finally:
314 self.state = 'AUTH'
315 return typ, dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000316
317
Tim Peters07e99cb2001-01-14 23:47:14 +0000318 def copy(self, message_set, new_mailbox):
319 """Copy 'message_set' messages onto end of 'new_mailbox'.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000320
Tim Peters07e99cb2001-01-14 23:47:14 +0000321 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
322 """
323 return self._simple_command('COPY', message_set, new_mailbox)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000324
325
Tim Peters07e99cb2001-01-14 23:47:14 +0000326 def create(self, mailbox):
327 """Create new mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000328
Tim Peters07e99cb2001-01-14 23:47:14 +0000329 (typ, [data]) = <instance>.create(mailbox)
330 """
331 return self._simple_command('CREATE', mailbox)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000332
333
Tim Peters07e99cb2001-01-14 23:47:14 +0000334 def delete(self, mailbox):
335 """Delete old mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000336
Tim Peters07e99cb2001-01-14 23:47:14 +0000337 (typ, [data]) = <instance>.delete(mailbox)
338 """
339 return self._simple_command('DELETE', mailbox)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000340
341
Tim Peters07e99cb2001-01-14 23:47:14 +0000342 def expunge(self):
343 """Permanently remove deleted items from selected mailbox.
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000344
Tim Peters07e99cb2001-01-14 23:47:14 +0000345 Generates 'EXPUNGE' response for each deleted message.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000346
Tim Peters07e99cb2001-01-14 23:47:14 +0000347 (typ, [data]) = <instance>.expunge()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000348
Tim Peters07e99cb2001-01-14 23:47:14 +0000349 'data' is list of 'EXPUNGE'd message numbers in order received.
350 """
351 name = 'EXPUNGE'
352 typ, dat = self._simple_command(name)
353 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000354
355
Tim Peters07e99cb2001-01-14 23:47:14 +0000356 def fetch(self, message_set, message_parts):
357 """Fetch (parts of) messages.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000358
Tim Peters07e99cb2001-01-14 23:47:14 +0000359 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000360
Tim Peters07e99cb2001-01-14 23:47:14 +0000361 'message_parts' should be a string of selected parts
362 enclosed in parentheses, eg: "(UID BODY[TEXT])".
Fred Drakefd267d92000-05-25 03:25:26 +0000363
Tim Peters07e99cb2001-01-14 23:47:14 +0000364 'data' are tuples of message part envelope and data.
365 """
366 name = 'FETCH'
367 typ, dat = self._simple_command(name, message_set, message_parts)
368 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000369
370
Tim Peters07e99cb2001-01-14 23:47:14 +0000371 def list(self, directory='""', pattern='*'):
372 """List mailbox names in directory matching pattern.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000373
Tim Peters07e99cb2001-01-14 23:47:14 +0000374 (typ, [data]) = <instance>.list(directory='""', pattern='*')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000375
Tim Peters07e99cb2001-01-14 23:47:14 +0000376 'data' is list of LIST responses.
377 """
378 name = 'LIST'
379 typ, dat = self._simple_command(name, directory, pattern)
380 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000381
382
Tim Peters07e99cb2001-01-14 23:47:14 +0000383 def login(self, user, password):
384 """Identify client using plaintext password.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000385
Tim Peters07e99cb2001-01-14 23:47:14 +0000386 (typ, [data]) = <instance>.login(user, password)
Guido van Rossum8c062211999-12-13 23:27:45 +0000387
Tim Peters07e99cb2001-01-14 23:47:14 +0000388 NB: 'password' will be quoted.
389 """
390 #if not 'AUTH=LOGIN' in self.capabilities:
391 # raise self.error("Server doesn't allow LOGIN authentication." % mech)
392 typ, dat = self._simple_command('LOGIN', user, self._quote(password))
393 if typ != 'OK':
394 raise self.error(dat[-1])
395 self.state = 'AUTH'
396 return typ, dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000397
398
Tim Peters07e99cb2001-01-14 23:47:14 +0000399 def logout(self):
400 """Shutdown connection to server.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000401
Tim Peters07e99cb2001-01-14 23:47:14 +0000402 (typ, [data]) = <instance>.logout()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000403
Tim Peters07e99cb2001-01-14 23:47:14 +0000404 Returns server 'BYE' response.
405 """
406 self.state = 'LOGOUT'
407 try: typ, dat = self._simple_command('LOGOUT')
408 except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
409 self.file.close()
410 self.sock.close()
411 if self.untagged_responses.has_key('BYE'):
412 return 'BYE', self.untagged_responses['BYE']
413 return typ, dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000414
415
Tim Peters07e99cb2001-01-14 23:47:14 +0000416 def lsub(self, directory='""', pattern='*'):
417 """List 'subscribed' mailbox names in directory matching pattern.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000418
Tim Peters07e99cb2001-01-14 23:47:14 +0000419 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000420
Tim Peters07e99cb2001-01-14 23:47:14 +0000421 'data' are tuples of message part envelope and data.
422 """
423 name = 'LSUB'
424 typ, dat = self._simple_command(name, directory, pattern)
425 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000426
427
Tim Peters07e99cb2001-01-14 23:47:14 +0000428 def noop(self):
429 """Send NOOP command.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000430
Tim Peters07e99cb2001-01-14 23:47:14 +0000431 (typ, data) = <instance>.noop()
432 """
433 if __debug__:
434 if self.debug >= 3:
435 _dump_ur(self.untagged_responses)
436 return self._simple_command('NOOP')
Guido van Rossum6884af71998-05-29 13:34:03 +0000437
438
Tim Peters07e99cb2001-01-14 23:47:14 +0000439 def partial(self, message_num, message_part, start, length):
440 """Fetch truncated part of a message.
Guido van Rossumeda960a1998-06-18 14:24:28 +0000441
Tim Peters07e99cb2001-01-14 23:47:14 +0000442 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
Guido van Rossumeda960a1998-06-18 14:24:28 +0000443
Tim Peters07e99cb2001-01-14 23:47:14 +0000444 'data' is tuple of message part envelope and data.
445 """
446 name = 'PARTIAL'
447 typ, dat = self._simple_command(name, message_num, message_part, start, length)
448 return self._untagged_response(typ, dat, 'FETCH')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000449
450
Tim Peters07e99cb2001-01-14 23:47:14 +0000451 def rename(self, oldmailbox, newmailbox):
452 """Rename old mailbox name to new.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000453
Tim Peters07e99cb2001-01-14 23:47:14 +0000454 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
455 """
456 return self._simple_command('RENAME', oldmailbox, newmailbox)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000457
458
Tim Peters07e99cb2001-01-14 23:47:14 +0000459 def search(self, charset, *criteria):
460 """Search mailbox for matching messages.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000461
Tim Peters07e99cb2001-01-14 23:47:14 +0000462 (typ, [data]) = <instance>.search(charset, criterium, ...)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000463
Tim Peters07e99cb2001-01-14 23:47:14 +0000464 'data' is space separated list of matching message numbers.
465 """
466 name = 'SEARCH'
467 if charset:
468 charset = 'CHARSET ' + charset
469 typ, dat = apply(self._simple_command, (name, charset) + criteria)
470 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000471
472
Tim Peters07e99cb2001-01-14 23:47:14 +0000473 def select(self, mailbox='INBOX', readonly=None):
474 """Select a mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000475
Tim Peters07e99cb2001-01-14 23:47:14 +0000476 Flush all untagged responses.
Guido van Rossum46586821998-05-18 14:39:42 +0000477
Tim Peters07e99cb2001-01-14 23:47:14 +0000478 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000479
Tim Peters07e99cb2001-01-14 23:47:14 +0000480 'data' is count of messages in mailbox ('EXISTS' response).
481 """
482 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
483 self.untagged_responses = {} # Flush old responses.
484 self.is_readonly = readonly
485 if readonly:
486 name = 'EXAMINE'
487 else:
488 name = 'SELECT'
489 typ, dat = self._simple_command(name, mailbox)
490 if typ != 'OK':
491 self.state = 'AUTH' # Might have been 'SELECTED'
492 return typ, dat
493 self.state = 'SELECTED'
494 if self.untagged_responses.has_key('READ-ONLY') \
495 and not readonly:
496 if __debug__:
497 if self.debug >= 1:
498 _dump_ur(self.untagged_responses)
499 raise self.readonly('%s is not writable' % mailbox)
500 return typ, self.untagged_responses.get('EXISTS', [None])
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000501
502
Tim Peters07e99cb2001-01-14 23:47:14 +0000503 def status(self, mailbox, names):
504 """Request named status conditions for mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000505
Tim Peters07e99cb2001-01-14 23:47:14 +0000506 (typ, [data]) = <instance>.status(mailbox, names)
507 """
508 name = 'STATUS'
509 if self.PROTOCOL_VERSION == 'IMAP4':
510 raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
511 typ, dat = self._simple_command(name, mailbox, names)
512 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000513
514
Tim Peters07e99cb2001-01-14 23:47:14 +0000515 def store(self, message_set, command, flags):
516 """Alters flag dispositions for messages in mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000517
Tim Peters07e99cb2001-01-14 23:47:14 +0000518 (typ, [data]) = <instance>.store(message_set, command, flags)
519 """
520 if (flags[0],flags[-1]) != ('(',')'):
521 flags = '(%s)' % flags # Avoid quoting the flags
522 typ, dat = self._simple_command('STORE', message_set, command, flags)
523 return self._untagged_response(typ, dat, 'FETCH')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000524
525
Tim Peters07e99cb2001-01-14 23:47:14 +0000526 def subscribe(self, mailbox):
527 """Subscribe to new mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000528
Tim Peters07e99cb2001-01-14 23:47:14 +0000529 (typ, [data]) = <instance>.subscribe(mailbox)
530 """
531 return self._simple_command('SUBSCRIBE', mailbox)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000532
533
Tim Peters07e99cb2001-01-14 23:47:14 +0000534 def uid(self, command, *args):
535 """Execute "command arg ..." with messages identified by UID,
536 rather than message number.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000537
Tim Peters07e99cb2001-01-14 23:47:14 +0000538 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000539
Tim Peters07e99cb2001-01-14 23:47:14 +0000540 Returns response appropriate to 'command'.
541 """
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +0000542 command = command.upper()
Tim Peters07e99cb2001-01-14 23:47:14 +0000543 if not Commands.has_key(command):
544 raise self.error("Unknown IMAP4 UID command: %s" % command)
545 if self.state not in Commands[command]:
546 raise self.error('command %s illegal in state %s'
547 % (command, self.state))
548 name = 'UID'
549 typ, dat = apply(self._simple_command, (name, command) + args)
550 if command == 'SEARCH':
551 name = 'SEARCH'
552 else:
553 name = 'FETCH'
554 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000555
556
Tim Peters07e99cb2001-01-14 23:47:14 +0000557 def unsubscribe(self, mailbox):
558 """Unsubscribe from old mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000559
Tim Peters07e99cb2001-01-14 23:47:14 +0000560 (typ, [data]) = <instance>.unsubscribe(mailbox)
561 """
562 return self._simple_command('UNSUBSCRIBE', mailbox)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000563
564
Tim Peters07e99cb2001-01-14 23:47:14 +0000565 def xatom(self, name, *args):
566 """Allow simple extension commands
567 notified by server in CAPABILITY response.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000568
Tim Peters07e99cb2001-01-14 23:47:14 +0000569 (typ, [data]) = <instance>.xatom(name, arg, ...)
570 """
571 if name[0] != 'X' or not name in self.capabilities:
572 raise self.error('unknown extension command: %s' % name)
573 return apply(self._simple_command, (name,) + args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000574
Guido van Rossuma85e2c82001-06-17 13:31:25 +0000575 def namespace(self):
Tim Petersff0a2bb2001-06-18 23:56:36 +0000576 """ Returns IMAP namespaces ala rfc2342
577 """
578 name = 'NAMESPACE'
579 typ, dat = self._simple_command(name)
580 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000581
582
Tim Peters07e99cb2001-01-14 23:47:14 +0000583 # Private methods
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000584
585
Tim Peters07e99cb2001-01-14 23:47:14 +0000586 def _append_untagged(self, typ, dat):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000587
Tim Peters07e99cb2001-01-14 23:47:14 +0000588 if dat is None: dat = ''
589 ur = self.untagged_responses
590 if __debug__:
591 if self.debug >= 5:
592 _mesg('untagged_responses[%s] %s += ["%s"]' %
593 (typ, len(ur.get(typ,'')), dat))
594 if ur.has_key(typ):
595 ur[typ].append(dat)
596 else:
597 ur[typ] = [dat]
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000598
599
Tim Peters07e99cb2001-01-14 23:47:14 +0000600 def _check_bye(self):
601 bye = self.untagged_responses.get('BYE')
602 if bye:
603 raise self.abort(bye[-1])
Guido van Rossum8c062211999-12-13 23:27:45 +0000604
605
Tim Peters07e99cb2001-01-14 23:47:14 +0000606 def _command(self, name, *args):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000607
Tim Peters07e99cb2001-01-14 23:47:14 +0000608 if self.state not in Commands[name]:
609 self.literal = None
610 raise self.error(
611 'command %s illegal in state %s' % (name, self.state))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000612
Tim Peters07e99cb2001-01-14 23:47:14 +0000613 for typ in ('OK', 'NO', 'BAD'):
614 if self.untagged_responses.has_key(typ):
615 del self.untagged_responses[typ]
Guido van Rossum26367a01998-09-28 15:34:46 +0000616
Tim Peters07e99cb2001-01-14 23:47:14 +0000617 if self.untagged_responses.has_key('READ-ONLY') \
618 and not self.is_readonly:
619 raise self.readonly('mailbox status changed to READ-ONLY')
Guido van Rossumeda960a1998-06-18 14:24:28 +0000620
Tim Peters07e99cb2001-01-14 23:47:14 +0000621 tag = self._new_tag()
622 data = '%s %s' % (tag, name)
623 for arg in args:
624 if arg is None: continue
625 data = '%s %s' % (data, self._checkquote(arg))
Guido van Rossum6884af71998-05-29 13:34:03 +0000626
Tim Peters07e99cb2001-01-14 23:47:14 +0000627 literal = self.literal
628 if literal is not None:
629 self.literal = None
630 if type(literal) is type(self._command):
631 literator = literal
632 else:
633 literator = None
634 data = '%s {%s}' % (data, len(literal))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000635
Tim Peters07e99cb2001-01-14 23:47:14 +0000636 if __debug__:
637 if self.debug >= 4:
638 _mesg('> %s' % data)
639 else:
640 _log('> %s' % data)
Guido van Rossum8c062211999-12-13 23:27:45 +0000641
Tim Peters07e99cb2001-01-14 23:47:14 +0000642 try:
643 self.sock.send('%s%s' % (data, CRLF))
644 except socket.error, val:
645 raise self.abort('socket error: %s' % val)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000646
Tim Peters07e99cb2001-01-14 23:47:14 +0000647 if literal is None:
648 return tag
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000649
Tim Peters07e99cb2001-01-14 23:47:14 +0000650 while 1:
651 # Wait for continuation response
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000652
Tim Peters07e99cb2001-01-14 23:47:14 +0000653 while self._get_response():
654 if self.tagged_commands[tag]: # BAD/NO?
655 return tag
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000656
Tim Peters07e99cb2001-01-14 23:47:14 +0000657 # Send literal
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000658
Tim Peters07e99cb2001-01-14 23:47:14 +0000659 if literator:
660 literal = literator(self.continuation_response)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000661
Tim Peters07e99cb2001-01-14 23:47:14 +0000662 if __debug__:
663 if self.debug >= 4:
664 _mesg('write literal size %s' % len(literal))
Guido van Rossumeda960a1998-06-18 14:24:28 +0000665
Tim Peters07e99cb2001-01-14 23:47:14 +0000666 try:
667 self.sock.send(literal)
668 self.sock.send(CRLF)
669 except socket.error, val:
670 raise self.abort('socket error: %s' % val)
Guido van Rossumeda960a1998-06-18 14:24:28 +0000671
Tim Peters07e99cb2001-01-14 23:47:14 +0000672 if not literator:
673 break
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000674
Tim Peters07e99cb2001-01-14 23:47:14 +0000675 return tag
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000676
677
Tim Peters07e99cb2001-01-14 23:47:14 +0000678 def _command_complete(self, name, tag):
679 self._check_bye()
680 try:
681 typ, data = self._get_tagged_response(tag)
682 except self.abort, val:
683 raise self.abort('command: %s => %s' % (name, val))
684 except self.error, val:
685 raise self.error('command: %s => %s' % (name, val))
686 self._check_bye()
687 if typ == 'BAD':
688 raise self.error('%s command error: %s %s' % (name, typ, data))
689 return typ, data
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000690
691
Tim Peters07e99cb2001-01-14 23:47:14 +0000692 def _get_response(self):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000693
Tim Peters07e99cb2001-01-14 23:47:14 +0000694 # Read response and store.
695 #
696 # Returns None for continuation responses,
697 # otherwise first response line received.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000698
Tim Peters07e99cb2001-01-14 23:47:14 +0000699 resp = self._get_line()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000700
Tim Peters07e99cb2001-01-14 23:47:14 +0000701 # Command completion response?
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000702
Tim Peters07e99cb2001-01-14 23:47:14 +0000703 if self._match(self.tagre, resp):
704 tag = self.mo.group('tag')
705 if not self.tagged_commands.has_key(tag):
706 raise self.abort('unexpected tagged response: %s' % resp)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000707
Tim Peters07e99cb2001-01-14 23:47:14 +0000708 typ = self.mo.group('type')
709 dat = self.mo.group('data')
710 self.tagged_commands[tag] = (typ, [dat])
711 else:
712 dat2 = None
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000713
Tim Peters07e99cb2001-01-14 23:47:14 +0000714 # '*' (untagged) responses?
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000715
Tim Peters07e99cb2001-01-14 23:47:14 +0000716 if not self._match(Untagged_response, resp):
717 if self._match(Untagged_status, resp):
718 dat2 = self.mo.group('data2')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000719
Tim Peters07e99cb2001-01-14 23:47:14 +0000720 if self.mo is None:
721 # Only other possibility is '+' (continuation) response...
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000722
Tim Peters07e99cb2001-01-14 23:47:14 +0000723 if self._match(Continuation, resp):
724 self.continuation_response = self.mo.group('data')
725 return None # NB: indicates continuation
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000726
Tim Peters07e99cb2001-01-14 23:47:14 +0000727 raise self.abort("unexpected response: '%s'" % resp)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000728
Tim Peters07e99cb2001-01-14 23:47:14 +0000729 typ = self.mo.group('type')
730 dat = self.mo.group('data')
731 if dat is None: dat = '' # Null untagged response
732 if dat2: dat = dat + ' ' + dat2
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000733
Tim Peters07e99cb2001-01-14 23:47:14 +0000734 # Is there a literal to come?
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000735
Tim Peters07e99cb2001-01-14 23:47:14 +0000736 while self._match(Literal, dat):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000737
Tim Peters07e99cb2001-01-14 23:47:14 +0000738 # Read literal direct from connection.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000739
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +0000740 size = int(self.mo.group('size'))
Tim Peters07e99cb2001-01-14 23:47:14 +0000741 if __debug__:
742 if self.debug >= 4:
743 _mesg('read literal size %s' % size)
744 data = self.file.read(size)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000745
Tim Peters07e99cb2001-01-14 23:47:14 +0000746 # Store response with literal as tuple
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000747
Tim Peters07e99cb2001-01-14 23:47:14 +0000748 self._append_untagged(typ, (dat, data))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000749
Tim Peters07e99cb2001-01-14 23:47:14 +0000750 # Read trailer - possibly containing another literal
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000751
Tim Peters07e99cb2001-01-14 23:47:14 +0000752 dat = self._get_line()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000753
Tim Peters07e99cb2001-01-14 23:47:14 +0000754 self._append_untagged(typ, dat)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000755
Tim Peters07e99cb2001-01-14 23:47:14 +0000756 # Bracketed response information?
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000757
Tim Peters07e99cb2001-01-14 23:47:14 +0000758 if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
759 self._append_untagged(self.mo.group('type'), self.mo.group('data'))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000760
Tim Peters07e99cb2001-01-14 23:47:14 +0000761 if __debug__:
762 if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
763 _mesg('%s response: %s' % (typ, dat))
Guido van Rossum26367a01998-09-28 15:34:46 +0000764
Tim Peters07e99cb2001-01-14 23:47:14 +0000765 return resp
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000766
767
Tim Peters07e99cb2001-01-14 23:47:14 +0000768 def _get_tagged_response(self, tag):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000769
Tim Peters07e99cb2001-01-14 23:47:14 +0000770 while 1:
771 result = self.tagged_commands[tag]
772 if result is not None:
773 del self.tagged_commands[tag]
774 return result
Guido van Rossumf36b1822000-02-17 17:12:39 +0000775
Tim Peters07e99cb2001-01-14 23:47:14 +0000776 # Some have reported "unexpected response" exceptions.
777 # Note that ignoring them here causes loops.
778 # Instead, send me details of the unexpected response and
779 # I'll update the code in `_get_response()'.
Guido van Rossumf36b1822000-02-17 17:12:39 +0000780
Tim Peters07e99cb2001-01-14 23:47:14 +0000781 try:
782 self._get_response()
783 except self.abort, val:
784 if __debug__:
785 if self.debug >= 1:
786 print_log()
787 raise
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000788
789
Tim Peters07e99cb2001-01-14 23:47:14 +0000790 def _get_line(self):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000791
Tim Peters07e99cb2001-01-14 23:47:14 +0000792 line = self.file.readline()
793 if not line:
794 raise self.abort('socket error: EOF')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000795
Tim Peters07e99cb2001-01-14 23:47:14 +0000796 # Protocol mandates all lines terminated by CRLF
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000797
Tim Peters07e99cb2001-01-14 23:47:14 +0000798 line = line[:-2]
799 if __debug__:
800 if self.debug >= 4:
801 _mesg('< %s' % line)
802 else:
803 _log('< %s' % line)
804 return line
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000805
806
Tim Peters07e99cb2001-01-14 23:47:14 +0000807 def _match(self, cre, s):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000808
Tim Peters07e99cb2001-01-14 23:47:14 +0000809 # Run compiled regular expression match method on 's'.
810 # Save result, return success.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000811
Tim Peters07e99cb2001-01-14 23:47:14 +0000812 self.mo = cre.match(s)
813 if __debug__:
814 if self.mo is not None and self.debug >= 5:
815 _mesg("\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`))
816 return self.mo is not None
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000817
818
Tim Peters07e99cb2001-01-14 23:47:14 +0000819 def _new_tag(self):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000820
Tim Peters07e99cb2001-01-14 23:47:14 +0000821 tag = '%s%s' % (self.tagpre, self.tagnum)
822 self.tagnum = self.tagnum + 1
823 self.tagged_commands[tag] = None
824 return tag
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000825
826
Tim Peters07e99cb2001-01-14 23:47:14 +0000827 def _checkquote(self, arg):
Guido van Rossum8c062211999-12-13 23:27:45 +0000828
Tim Peters07e99cb2001-01-14 23:47:14 +0000829 # Must quote command args if non-alphanumeric chars present,
830 # and not already quoted.
Guido van Rossum8c062211999-12-13 23:27:45 +0000831
Tim Peters07e99cb2001-01-14 23:47:14 +0000832 if type(arg) is not type(''):
833 return arg
834 if (arg[0],arg[-1]) in (('(',')'),('"','"')):
835 return arg
836 if self.mustquote.search(arg) is None:
837 return arg
838 return self._quote(arg)
Guido van Rossum8c062211999-12-13 23:27:45 +0000839
840
Tim Peters07e99cb2001-01-14 23:47:14 +0000841 def _quote(self, arg):
Guido van Rossum8c062211999-12-13 23:27:45 +0000842
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +0000843 arg = arg.replace('\\', '\\\\')
844 arg = arg.replace('"', '\\"')
Guido van Rossum8c062211999-12-13 23:27:45 +0000845
Tim Peters07e99cb2001-01-14 23:47:14 +0000846 return '"%s"' % arg
Guido van Rossum8c062211999-12-13 23:27:45 +0000847
848
Tim Peters07e99cb2001-01-14 23:47:14 +0000849 def _simple_command(self, name, *args):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000850
Tim Peters07e99cb2001-01-14 23:47:14 +0000851 return self._command_complete(name, apply(self._command, (name,) + args))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000852
853
Tim Peters07e99cb2001-01-14 23:47:14 +0000854 def _untagged_response(self, typ, dat, name):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000855
Tim Peters07e99cb2001-01-14 23:47:14 +0000856 if typ == 'NO':
857 return typ, dat
858 if not self.untagged_responses.has_key(name):
859 return typ, [None]
860 data = self.untagged_responses[name]
861 if __debug__:
862 if self.debug >= 5:
863 _mesg('untagged_responses[%s] => %s' % (name, data))
864 del self.untagged_responses[name]
865 return typ, data
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000866
867
868
Guido van Rossumeda960a1998-06-18 14:24:28 +0000869class _Authenticator:
870
Tim Peters07e99cb2001-01-14 23:47:14 +0000871 """Private class to provide en/decoding
872 for base64-based authentication conversation.
873 """
Guido van Rossumeda960a1998-06-18 14:24:28 +0000874
Tim Peters07e99cb2001-01-14 23:47:14 +0000875 def __init__(self, mechinst):
876 self.mech = mechinst # Callable object to provide/process data
Guido van Rossumeda960a1998-06-18 14:24:28 +0000877
Tim Peters07e99cb2001-01-14 23:47:14 +0000878 def process(self, data):
879 ret = self.mech(self.decode(data))
880 if ret is None:
881 return '*' # Abort conversation
882 return self.encode(ret)
Guido van Rossumeda960a1998-06-18 14:24:28 +0000883
Tim Peters07e99cb2001-01-14 23:47:14 +0000884 def encode(self, inp):
885 #
886 # Invoke binascii.b2a_base64 iteratively with
887 # short even length buffers, strip the trailing
888 # line feed from the result and append. "Even"
889 # means a number that factors to both 6 and 8,
890 # so when it gets to the end of the 8-bit input
891 # there's no partial 6-bit output.
892 #
893 oup = ''
894 while inp:
895 if len(inp) > 48:
896 t = inp[:48]
897 inp = inp[48:]
898 else:
899 t = inp
900 inp = ''
901 e = binascii.b2a_base64(t)
902 if e:
903 oup = oup + e[:-1]
904 return oup
905
906 def decode(self, inp):
907 if not inp:
908 return ''
909 return binascii.a2b_base64(inp)
910
Guido van Rossumeda960a1998-06-18 14:24:28 +0000911
912
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000913Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
Tim Peters07e99cb2001-01-14 23:47:14 +0000914 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000915
916def Internaldate2tuple(resp):
Tim Peters07e99cb2001-01-14 23:47:14 +0000917 """Convert IMAP4 INTERNALDATE to UT.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000918
Tim Peters07e99cb2001-01-14 23:47:14 +0000919 Returns Python time module tuple.
920 """
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000921
Tim Peters07e99cb2001-01-14 23:47:14 +0000922 mo = InternalDate.match(resp)
923 if not mo:
924 return None
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000925
Tim Peters07e99cb2001-01-14 23:47:14 +0000926 mon = Mon2num[mo.group('mon')]
927 zonen = mo.group('zonen')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000928
Jeremy Hyltonf5d3ea02001-02-22 13:24:27 +0000929 day = int(mo.group('day'))
930 year = int(mo.group('year'))
931 hour = int(mo.group('hour'))
932 min = int(mo.group('min'))
933 sec = int(mo.group('sec'))
934 zoneh = int(mo.group('zoneh'))
935 zonem = int(mo.group('zonem'))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000936
Tim Peters07e99cb2001-01-14 23:47:14 +0000937 # INTERNALDATE timezone must be subtracted to get UT
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000938
Tim Peters07e99cb2001-01-14 23:47:14 +0000939 zone = (zoneh*60 + zonem)*60
940 if zonen == '-':
941 zone = -zone
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000942
Tim Peters07e99cb2001-01-14 23:47:14 +0000943 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000944
Tim Peters07e99cb2001-01-14 23:47:14 +0000945 utc = time.mktime(tt)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000946
Tim Peters07e99cb2001-01-14 23:47:14 +0000947 # Following is necessary because the time module has no 'mkgmtime'.
948 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000949
Tim Peters07e99cb2001-01-14 23:47:14 +0000950 lt = time.localtime(utc)
951 if time.daylight and lt[-1]:
952 zone = zone + time.altzone
953 else:
954 zone = zone + time.timezone
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000955
Tim Peters07e99cb2001-01-14 23:47:14 +0000956 return time.localtime(utc - zone)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000957
958
959
960def Int2AP(num):
961
Tim Peters07e99cb2001-01-14 23:47:14 +0000962 """Convert integer to A-P string representation."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000963
Tim Peters07e99cb2001-01-14 23:47:14 +0000964 val = ''; AP = 'ABCDEFGHIJKLMNOP'
965 num = int(abs(num))
966 while num:
967 num, mod = divmod(num, 16)
968 val = AP[mod] + val
969 return val
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000970
971
972
973def ParseFlags(resp):
974
Tim Peters07e99cb2001-01-14 23:47:14 +0000975 """Convert IMAP4 flags response to python tuple."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000976
Tim Peters07e99cb2001-01-14 23:47:14 +0000977 mo = Flags.match(resp)
978 if not mo:
979 return ()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000980
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +0000981 return tuple(mo.group('flags').split())
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000982
983
984def Time2Internaldate(date_time):
985
Tim Peters07e99cb2001-01-14 23:47:14 +0000986 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000987
Tim Peters07e99cb2001-01-14 23:47:14 +0000988 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
989 """
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000990
Tim Peters07e99cb2001-01-14 23:47:14 +0000991 dttype = type(date_time)
992 if dttype is type(1) or dttype is type(1.1):
993 tt = time.localtime(date_time)
994 elif dttype is type(()):
995 tt = date_time
996 elif dttype is type(""):
997 return date_time # Assume in correct format
998 else: raise ValueError
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000999
Tim Peters07e99cb2001-01-14 23:47:14 +00001000 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
1001 if dt[0] == '0':
1002 dt = ' ' + dt[1:]
1003 if time.daylight and tt[-1]:
1004 zone = -time.altzone
1005 else:
1006 zone = -time.timezone
1007 return '"' + dt + " %+02d%02d" % divmod(zone/60, 60) + '"'
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001008
1009
1010
Guido van Rossumeda960a1998-06-18 14:24:28 +00001011if __debug__:
1012
Tim Peters07e99cb2001-01-14 23:47:14 +00001013 def _mesg(s, secs=None):
1014 if secs is None:
1015 secs = time.time()
1016 tm = time.strftime('%M:%S', time.localtime(secs))
1017 sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s))
1018 sys.stderr.flush()
Guido van Rossum26367a01998-09-28 15:34:46 +00001019
Tim Peters07e99cb2001-01-14 23:47:14 +00001020 def _dump_ur(dict):
1021 # Dump untagged responses (in `dict').
1022 l = dict.items()
1023 if not l: return
1024 t = '\n\t\t'
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +00001025 l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l)
Tim Peters07e99cb2001-01-14 23:47:14 +00001026 _mesg('untagged responses dump:%s%s' % (t, j(l, t)))
Guido van Rossumeda960a1998-06-18 14:24:28 +00001027
Tim Peters07e99cb2001-01-14 23:47:14 +00001028 _cmd_log = [] # Last `_cmd_log_len' interactions
1029 _cmd_log_len = 10
Guido van Rossum8c062211999-12-13 23:27:45 +00001030
Tim Peters07e99cb2001-01-14 23:47:14 +00001031 def _log(line):
1032 # Keep log of last `_cmd_log_len' interactions for debugging.
1033 if len(_cmd_log) == _cmd_log_len:
1034 del _cmd_log[0]
1035 _cmd_log.append((time.time(), line))
Guido van Rossum8c062211999-12-13 23:27:45 +00001036
Tim Peters07e99cb2001-01-14 23:47:14 +00001037 def print_log():
1038 _mesg('last %d IMAP4 interactions:' % len(_cmd_log))
1039 for secs,line in _cmd_log:
1040 _mesg(line, secs)
Guido van Rossumeda960a1998-06-18 14:24:28 +00001041
1042
Guido van Rossum8c062211999-12-13 23:27:45 +00001043
1044if __name__ == '__main__':
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001045
Tim Peters07e99cb2001-01-14 23:47:14 +00001046 import getopt, getpass, sys
Guido van Rossumd6596931998-05-29 18:08:48 +00001047
Tim Peters07e99cb2001-01-14 23:47:14 +00001048 try:
1049 optlist, args = getopt.getopt(sys.argv[1:], 'd:')
1050 except getopt.error, val:
1051 pass
Guido van Rossum66d45132000-03-28 20:20:53 +00001052
Tim Peters07e99cb2001-01-14 23:47:14 +00001053 for opt,val in optlist:
1054 if opt == '-d':
1055 Debug = int(val)
Guido van Rossum66d45132000-03-28 20:20:53 +00001056
Tim Peters07e99cb2001-01-14 23:47:14 +00001057 if not args: args = ('',)
Guido van Rossum66d45132000-03-28 20:20:53 +00001058
Tim Peters07e99cb2001-01-14 23:47:14 +00001059 host = args[0]
Guido van Rossumb1f08121998-06-25 02:22:16 +00001060
Tim Peters07e99cb2001-01-14 23:47:14 +00001061 USER = getpass.getuser()
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +00001062 PASSWD = getpass.getpass("IMAP password for %s on %s:" % (USER, host or "localhost"))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001063
Tim Peters07e99cb2001-01-14 23:47:14 +00001064 test_mesg = 'From: %s@localhost\nSubject: IMAP4 test\n\ndata...\n' % USER
1065 test_seq1 = (
1066 ('login', (USER, PASSWD)),
1067 ('create', ('/tmp/xxx 1',)),
1068 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1069 ('CREATE', ('/tmp/yyz 2',)),
1070 ('append', ('/tmp/yyz 2', None, None, test_mesg)),
1071 ('list', ('/tmp', 'yy*')),
1072 ('select', ('/tmp/yyz 2',)),
1073 ('search', (None, 'SUBJECT', 'test')),
1074 ('partial', ('1', 'RFC822', 1, 1024)),
1075 ('store', ('1', 'FLAGS', '(\Deleted)')),
1076 ('expunge', ()),
1077 ('recent', ()),
1078 ('close', ()),
1079 )
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001080
Tim Peters07e99cb2001-01-14 23:47:14 +00001081 test_seq2 = (
1082 ('select', ()),
1083 ('response',('UIDVALIDITY',)),
1084 ('uid', ('SEARCH', 'ALL')),
1085 ('response', ('EXISTS',)),
1086 ('append', (None, None, None, test_mesg)),
1087 ('recent', ()),
1088 ('logout', ()),
1089 )
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001090
Tim Peters07e99cb2001-01-14 23:47:14 +00001091 def run(cmd, args):
1092 _mesg('%s %s' % (cmd, args))
1093 typ, dat = apply(eval('M.%s' % cmd), args)
1094 _mesg('%s => %s %s' % (cmd, typ, dat))
1095 return dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001096
Tim Peters07e99cb2001-01-14 23:47:14 +00001097 try:
1098 M = IMAP4(host)
1099 _mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001100
Tim Peters07e99cb2001-01-14 23:47:14 +00001101 for cmd,args in test_seq1:
1102 run(cmd, args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001103
Tim Peters07e99cb2001-01-14 23:47:14 +00001104 for ml in run('list', ('/tmp/', 'yy%')):
1105 mo = re.match(r'.*"([^"]+)"$', ml)
1106 if mo: path = mo.group(1)
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +00001107 else: path = ml.split()[-1]
Tim Peters07e99cb2001-01-14 23:47:14 +00001108 run('delete', (path,))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001109
Tim Peters07e99cb2001-01-14 23:47:14 +00001110 for cmd,args in test_seq2:
1111 dat = run(cmd, args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001112
Tim Peters07e99cb2001-01-14 23:47:14 +00001113 if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
1114 continue
Guido van Rossum38d8f4e1998-04-11 01:22:34 +00001115
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +00001116 uid = dat[-1].split()
Tim Peters07e99cb2001-01-14 23:47:14 +00001117 if not uid: continue
1118 run('uid', ('FETCH', '%s' % uid[-1],
1119 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
Guido van Rossum66d45132000-03-28 20:20:53 +00001120
Tim Peters07e99cb2001-01-14 23:47:14 +00001121 print '\nAll tests OK.'
Guido van Rossum66d45132000-03-28 20:20:53 +00001122
Tim Peters07e99cb2001-01-14 23:47:14 +00001123 except:
1124 print '\nTests failed.'
Guido van Rossum66d45132000-03-28 20:20:53 +00001125
Tim Peters07e99cb2001-01-14 23:47:14 +00001126 if not Debug:
1127 print '''
Guido van Rossum66d45132000-03-28 20:20:53 +00001128If you would like to see debugging output,
1129try: %s -d5
1130''' % sys.argv[0]
1131
Tim Peters07e99cb2001-01-14 23:47:14 +00001132 raise