blob: 2a0eeb6ff46448050633bc942e91ae622b9f1819 [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'),
61 }
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000062
Tim Peters07e99cb2001-01-14 23:47:14 +000063# Patterns to match server responses
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000064
Guido van Rossumeda960a1998-06-18 14:24:28 +000065Continuation = re.compile(r'\+( (?P<data>.*))?')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000066Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
67InternalDate = re.compile(r'.*INTERNALDATE "'
Tim Peters07e99cb2001-01-14 23:47:14 +000068 r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
69 r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
70 r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
71 r'"')
Guido van Rossumf36b1822000-02-17 17:12:39 +000072Literal = re.compile(r'.*{(?P<size>\d+)}$')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000073Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
Guido van Rossumeda960a1998-06-18 14:24:28 +000074Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000075Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
76
77
78
79class IMAP4:
80
Tim Peters07e99cb2001-01-14 23:47:14 +000081 """IMAP4 client class.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000082
Tim Peters07e99cb2001-01-14 23:47:14 +000083 Instantiate with: IMAP4([host[, port]])
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000084
Tim Peters07e99cb2001-01-14 23:47:14 +000085 host - host's name (default: localhost);
86 port - port number (default: standard IMAP4 port).
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000087
Tim Peters07e99cb2001-01-14 23:47:14 +000088 All IMAP4rev1 commands are supported by methods of the same
89 name (in lower-case).
Guido van Rossum6884af71998-05-29 13:34:03 +000090
Tim Peters07e99cb2001-01-14 23:47:14 +000091 All arguments to commands are converted to strings, except for
92 AUTHENTICATE, and the last argument to APPEND which is passed as
93 an IMAP4 literal. If necessary (the string contains any
94 non-printing characters or white-space and isn't enclosed with
95 either parentheses or double quotes) each string is quoted.
96 However, the 'password' argument to the LOGIN command is always
97 quoted. If you want to avoid having an argument string quoted
98 (eg: the 'flags' argument to STORE) then enclose the string in
99 parentheses (eg: "(\Deleted)").
Guido van Rossum6884af71998-05-29 13:34:03 +0000100
Tim Peters07e99cb2001-01-14 23:47:14 +0000101 Each command returns a tuple: (type, [data, ...]) where 'type'
102 is usually 'OK' or 'NO', and 'data' is either the text from the
103 tagged response, or untagged results from command.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000104
Tim Peters07e99cb2001-01-14 23:47:14 +0000105 Errors raise the exception class <instance>.error("<reason>").
106 IMAP4 server errors raise <instance>.abort("<reason>"),
107 which is a sub-class of 'error'. Mailbox status changes
108 from READ-WRITE to READ-ONLY raise the exception class
109 <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
Guido van Rossumeda960a1998-06-18 14:24:28 +0000110
Tim Peters07e99cb2001-01-14 23:47:14 +0000111 "error" exceptions imply a program error.
112 "abort" exceptions imply the connection should be reset, and
113 the command re-tried.
114 "readonly" exceptions imply the command should be re-tried.
Guido van Rossum8c062211999-12-13 23:27:45 +0000115
Tim Peters07e99cb2001-01-14 23:47:14 +0000116 Note: to use this module, you must read the RFCs pertaining
117 to the IMAP4 protocol, as the semantics of the arguments to
118 each IMAP4 command are left to the invoker, not to mention
119 the results.
120 """
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000121
Tim Peters07e99cb2001-01-14 23:47:14 +0000122 class error(Exception): pass # Logical errors - debug required
123 class abort(error): pass # Service errors - close and retry
124 class readonly(abort): pass # Mailbox status changed to READ-ONLY
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000125
Tim Peters07e99cb2001-01-14 23:47:14 +0000126 mustquote = re.compile(r"[^\w!#$%&'*+,.:;<=>?^`|~-]")
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000127
Tim Peters07e99cb2001-01-14 23:47:14 +0000128 def __init__(self, host = '', port = IMAP4_PORT):
129 self.host = host
130 self.port = port
131 self.debug = Debug
132 self.state = 'LOGOUT'
133 self.literal = None # A literal argument to a command
134 self.tagged_commands = {} # Tagged commands awaiting response
135 self.untagged_responses = {} # {typ: [data, ...], ...}
136 self.continuation_response = '' # Last continuation response
137 self.is_readonly = None # READ-ONLY desired state
138 self.tagnum = 0
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000139
Tim Peters07e99cb2001-01-14 23:47:14 +0000140 # Open socket to server.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000141
Tim Peters07e99cb2001-01-14 23:47:14 +0000142 self.open(host, port)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000143
Tim Peters07e99cb2001-01-14 23:47:14 +0000144 # Create unique tag for this session,
145 # and compile tagged response matcher.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000146
Tim Peters07e99cb2001-01-14 23:47:14 +0000147 self.tagpre = Int2AP(random.randint(0, 31999))
148 self.tagre = re.compile(r'(?P<tag>'
149 + self.tagpre
150 + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000151
Tim Peters07e99cb2001-01-14 23:47:14 +0000152 # Get server welcome message,
153 # request and store CAPABILITY response.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000154
Tim Peters07e99cb2001-01-14 23:47:14 +0000155 if __debug__:
156 if self.debug >= 1:
157 _mesg('new IMAP4 connection, tag=%s' % self.tagpre)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000158
Tim Peters07e99cb2001-01-14 23:47:14 +0000159 self.welcome = self._get_response()
160 if self.untagged_responses.has_key('PREAUTH'):
161 self.state = 'AUTH'
162 elif self.untagged_responses.has_key('OK'):
163 self.state = 'NONAUTH'
164 else:
165 raise self.error(self.welcome)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000166
Tim Peters07e99cb2001-01-14 23:47:14 +0000167 cap = 'CAPABILITY'
168 self._simple_command(cap)
169 if not self.untagged_responses.has_key(cap):
170 raise self.error('no CAPABILITY response from server')
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +0000171 self.capabilities = tuple(self.untagged_responses[cap][-1].upper().split())
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000172
Tim Peters07e99cb2001-01-14 23:47:14 +0000173 if __debug__:
174 if self.debug >= 3:
175 _mesg('CAPABILITIES: %s' % `self.capabilities`)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000176
Tim Peters07e99cb2001-01-14 23:47:14 +0000177 for version in AllowedVersions:
178 if not version in self.capabilities:
179 continue
180 self.PROTOCOL_VERSION = version
181 return
Guido van Rossumb1f08121998-06-25 02:22:16 +0000182
Tim Peters07e99cb2001-01-14 23:47:14 +0000183 raise self.error('server not IMAP4 compliant')
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000184
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000185
Tim Peters07e99cb2001-01-14 23:47:14 +0000186 def __getattr__(self, attr):
187 # Allow UPPERCASE variants of IMAP4 command methods.
188 if Commands.has_key(attr):
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +0000189 return eval("self.%s" % attr.lower())
Tim Peters07e99cb2001-01-14 23:47:14 +0000190 raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
Guido van Rossum26367a01998-09-28 15:34:46 +0000191
192
193
Tim Peters07e99cb2001-01-14 23:47:14 +0000194 # Public methods
Guido van Rossum26367a01998-09-28 15:34:46 +0000195
196
Tim Peters07e99cb2001-01-14 23:47:14 +0000197 def open(self, host, port):
198 """Setup 'self.sock' and 'self.file'."""
199 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
200 self.sock.connect((self.host, self.port))
201 self.file = self.sock.makefile('r')
Guido van Rossumeda960a1998-06-18 14:24:28 +0000202
203
Tim Peters07e99cb2001-01-14 23:47:14 +0000204 def recent(self):
205 """Return most recent 'RECENT' responses if any exist,
206 else prompt server for an update using the 'NOOP' command.
Guido van Rossum26367a01998-09-28 15:34:46 +0000207
Tim Peters07e99cb2001-01-14 23:47:14 +0000208 (typ, [data]) = <instance>.recent()
Guido van Rossum26367a01998-09-28 15:34:46 +0000209
Tim Peters07e99cb2001-01-14 23:47:14 +0000210 'data' is None if no new messages,
211 else list of RECENT responses, most recent last.
212 """
213 name = 'RECENT'
214 typ, dat = self._untagged_response('OK', [None], name)
215 if dat[-1]:
216 return typ, dat
217 typ, dat = self.noop() # Prod server for response
218 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000219
220
Tim Peters07e99cb2001-01-14 23:47:14 +0000221 def response(self, code):
222 """Return data for response 'code' if received, or None.
Guido van Rossum26367a01998-09-28 15:34:46 +0000223
Tim Peters07e99cb2001-01-14 23:47:14 +0000224 Old value for response 'code' is cleared.
Guido van Rossum26367a01998-09-28 15:34:46 +0000225
Tim Peters07e99cb2001-01-14 23:47:14 +0000226 (code, [data]) = <instance>.response(code)
227 """
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +0000228 return self._untagged_response(code, [None], code.upper())
Guido van Rossum26367a01998-09-28 15:34:46 +0000229
230
Tim Peters07e99cb2001-01-14 23:47:14 +0000231 def socket(self):
232 """Return socket instance used to connect to IMAP4 server.
Guido van Rossum26367a01998-09-28 15:34:46 +0000233
Tim Peters07e99cb2001-01-14 23:47:14 +0000234 socket = <instance>.socket()
235 """
236 return self.sock
Guido van Rossum26367a01998-09-28 15:34:46 +0000237
238
239
Tim Peters07e99cb2001-01-14 23:47:14 +0000240 # IMAP4 commands
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000241
242
Tim Peters07e99cb2001-01-14 23:47:14 +0000243 def append(self, mailbox, flags, date_time, message):
244 """Append message to named mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000245
Tim Peters07e99cb2001-01-14 23:47:14 +0000246 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
Guido van Rossum8c062211999-12-13 23:27:45 +0000247
Tim Peters07e99cb2001-01-14 23:47:14 +0000248 All args except `message' can be None.
249 """
250 name = 'APPEND'
251 if not mailbox:
252 mailbox = 'INBOX'
253 if flags:
254 if (flags[0],flags[-1]) != ('(',')'):
255 flags = '(%s)' % flags
256 else:
257 flags = None
258 if date_time:
259 date_time = Time2Internaldate(date_time)
260 else:
261 date_time = None
262 self.literal = message
263 return self._simple_command(name, mailbox, flags, date_time)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000264
265
Tim Peters07e99cb2001-01-14 23:47:14 +0000266 def authenticate(self, mechanism, authobject):
267 """Authenticate command - requires response processing.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000268
Tim Peters07e99cb2001-01-14 23:47:14 +0000269 'mechanism' specifies which authentication mechanism is to
270 be used - it must appear in <instance>.capabilities in the
271 form AUTH=<mechanism>.
Guido van Rossumeda960a1998-06-18 14:24:28 +0000272
Tim Peters07e99cb2001-01-14 23:47:14 +0000273 'authobject' must be a callable object:
Guido van Rossumeda960a1998-06-18 14:24:28 +0000274
Tim Peters07e99cb2001-01-14 23:47:14 +0000275 data = authobject(response)
Guido van Rossumeda960a1998-06-18 14:24:28 +0000276
Tim Peters07e99cb2001-01-14 23:47:14 +0000277 It will be called to process server continuation responses.
278 It should return data that will be encoded and sent to server.
279 It should return None if the client abort response '*' should
280 be sent instead.
281 """
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +0000282 mech = mechanism.upper()
Tim Peters07e99cb2001-01-14 23:47:14 +0000283 cap = 'AUTH=%s' % mech
284 if not cap in self.capabilities:
285 raise self.error("Server doesn't allow %s authentication." % mech)
286 self.literal = _Authenticator(authobject).process
287 typ, dat = self._simple_command('AUTHENTICATE', mech)
288 if typ != 'OK':
289 raise self.error(dat[-1])
290 self.state = 'AUTH'
291 return typ, dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000292
293
Tim Peters07e99cb2001-01-14 23:47:14 +0000294 def check(self):
295 """Checkpoint mailbox on server.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000296
Tim Peters07e99cb2001-01-14 23:47:14 +0000297 (typ, [data]) = <instance>.check()
298 """
299 return self._simple_command('CHECK')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000300
301
Tim Peters07e99cb2001-01-14 23:47:14 +0000302 def close(self):
303 """Close currently selected mailbox.
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000304
Tim Peters07e99cb2001-01-14 23:47:14 +0000305 Deleted messages are removed from writable mailbox.
306 This is the recommended command before 'LOGOUT'.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000307
Tim Peters07e99cb2001-01-14 23:47:14 +0000308 (typ, [data]) = <instance>.close()
309 """
310 try:
311 typ, dat = self._simple_command('CLOSE')
312 finally:
313 self.state = 'AUTH'
314 return typ, dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000315
316
Tim Peters07e99cb2001-01-14 23:47:14 +0000317 def copy(self, message_set, new_mailbox):
318 """Copy 'message_set' messages onto end of 'new_mailbox'.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000319
Tim Peters07e99cb2001-01-14 23:47:14 +0000320 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
321 """
322 return self._simple_command('COPY', message_set, new_mailbox)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000323
324
Tim Peters07e99cb2001-01-14 23:47:14 +0000325 def create(self, mailbox):
326 """Create new mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000327
Tim Peters07e99cb2001-01-14 23:47:14 +0000328 (typ, [data]) = <instance>.create(mailbox)
329 """
330 return self._simple_command('CREATE', mailbox)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000331
332
Tim Peters07e99cb2001-01-14 23:47:14 +0000333 def delete(self, mailbox):
334 """Delete old mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000335
Tim Peters07e99cb2001-01-14 23:47:14 +0000336 (typ, [data]) = <instance>.delete(mailbox)
337 """
338 return self._simple_command('DELETE', mailbox)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000339
340
Tim Peters07e99cb2001-01-14 23:47:14 +0000341 def expunge(self):
342 """Permanently remove deleted items from selected mailbox.
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000343
Tim Peters07e99cb2001-01-14 23:47:14 +0000344 Generates 'EXPUNGE' response for each deleted message.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000345
Tim Peters07e99cb2001-01-14 23:47:14 +0000346 (typ, [data]) = <instance>.expunge()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000347
Tim Peters07e99cb2001-01-14 23:47:14 +0000348 'data' is list of 'EXPUNGE'd message numbers in order received.
349 """
350 name = 'EXPUNGE'
351 typ, dat = self._simple_command(name)
352 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000353
354
Tim Peters07e99cb2001-01-14 23:47:14 +0000355 def fetch(self, message_set, message_parts):
356 """Fetch (parts of) messages.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000357
Tim Peters07e99cb2001-01-14 23:47:14 +0000358 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000359
Tim Peters07e99cb2001-01-14 23:47:14 +0000360 'message_parts' should be a string of selected parts
361 enclosed in parentheses, eg: "(UID BODY[TEXT])".
Fred Drakefd267d92000-05-25 03:25:26 +0000362
Tim Peters07e99cb2001-01-14 23:47:14 +0000363 'data' are tuples of message part envelope and data.
364 """
365 name = 'FETCH'
366 typ, dat = self._simple_command(name, message_set, message_parts)
367 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000368
369
Tim Peters07e99cb2001-01-14 23:47:14 +0000370 def list(self, directory='""', pattern='*'):
371 """List mailbox names in directory matching pattern.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000372
Tim Peters07e99cb2001-01-14 23:47:14 +0000373 (typ, [data]) = <instance>.list(directory='""', pattern='*')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000374
Tim Peters07e99cb2001-01-14 23:47:14 +0000375 'data' is list of LIST responses.
376 """
377 name = 'LIST'
378 typ, dat = self._simple_command(name, directory, pattern)
379 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000380
381
Tim Peters07e99cb2001-01-14 23:47:14 +0000382 def login(self, user, password):
383 """Identify client using plaintext password.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000384
Tim Peters07e99cb2001-01-14 23:47:14 +0000385 (typ, [data]) = <instance>.login(user, password)
Guido van Rossum8c062211999-12-13 23:27:45 +0000386
Tim Peters07e99cb2001-01-14 23:47:14 +0000387 NB: 'password' will be quoted.
388 """
389 #if not 'AUTH=LOGIN' in self.capabilities:
390 # raise self.error("Server doesn't allow LOGIN authentication." % mech)
391 typ, dat = self._simple_command('LOGIN', user, self._quote(password))
392 if typ != 'OK':
393 raise self.error(dat[-1])
394 self.state = 'AUTH'
395 return typ, dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000396
397
Tim Peters07e99cb2001-01-14 23:47:14 +0000398 def logout(self):
399 """Shutdown connection to server.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000400
Tim Peters07e99cb2001-01-14 23:47:14 +0000401 (typ, [data]) = <instance>.logout()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000402
Tim Peters07e99cb2001-01-14 23:47:14 +0000403 Returns server 'BYE' response.
404 """
405 self.state = 'LOGOUT'
406 try: typ, dat = self._simple_command('LOGOUT')
407 except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
408 self.file.close()
409 self.sock.close()
410 if self.untagged_responses.has_key('BYE'):
411 return 'BYE', self.untagged_responses['BYE']
412 return typ, dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000413
414
Tim Peters07e99cb2001-01-14 23:47:14 +0000415 def lsub(self, directory='""', pattern='*'):
416 """List 'subscribed' mailbox names in directory matching pattern.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000417
Tim Peters07e99cb2001-01-14 23:47:14 +0000418 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000419
Tim Peters07e99cb2001-01-14 23:47:14 +0000420 'data' are tuples of message part envelope and data.
421 """
422 name = 'LSUB'
423 typ, dat = self._simple_command(name, directory, pattern)
424 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000425
426
Tim Peters07e99cb2001-01-14 23:47:14 +0000427 def noop(self):
428 """Send NOOP command.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000429
Tim Peters07e99cb2001-01-14 23:47:14 +0000430 (typ, data) = <instance>.noop()
431 """
432 if __debug__:
433 if self.debug >= 3:
434 _dump_ur(self.untagged_responses)
435 return self._simple_command('NOOP')
Guido van Rossum6884af71998-05-29 13:34:03 +0000436
437
Tim Peters07e99cb2001-01-14 23:47:14 +0000438 def partial(self, message_num, message_part, start, length):
439 """Fetch truncated part of a message.
Guido van Rossumeda960a1998-06-18 14:24:28 +0000440
Tim Peters07e99cb2001-01-14 23:47:14 +0000441 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
Guido van Rossumeda960a1998-06-18 14:24:28 +0000442
Tim Peters07e99cb2001-01-14 23:47:14 +0000443 'data' is tuple of message part envelope and data.
444 """
445 name = 'PARTIAL'
446 typ, dat = self._simple_command(name, message_num, message_part, start, length)
447 return self._untagged_response(typ, dat, 'FETCH')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000448
449
Tim Peters07e99cb2001-01-14 23:47:14 +0000450 def rename(self, oldmailbox, newmailbox):
451 """Rename old mailbox name to new.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000452
Tim Peters07e99cb2001-01-14 23:47:14 +0000453 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
454 """
455 return self._simple_command('RENAME', oldmailbox, newmailbox)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000456
457
Tim Peters07e99cb2001-01-14 23:47:14 +0000458 def search(self, charset, *criteria):
459 """Search mailbox for matching messages.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000460
Tim Peters07e99cb2001-01-14 23:47:14 +0000461 (typ, [data]) = <instance>.search(charset, criterium, ...)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000462
Tim Peters07e99cb2001-01-14 23:47:14 +0000463 'data' is space separated list of matching message numbers.
464 """
465 name = 'SEARCH'
466 if charset:
467 charset = 'CHARSET ' + charset
468 typ, dat = apply(self._simple_command, (name, charset) + criteria)
469 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000470
471
Tim Peters07e99cb2001-01-14 23:47:14 +0000472 def select(self, mailbox='INBOX', readonly=None):
473 """Select a mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000474
Tim Peters07e99cb2001-01-14 23:47:14 +0000475 Flush all untagged responses.
Guido van Rossum46586821998-05-18 14:39:42 +0000476
Tim Peters07e99cb2001-01-14 23:47:14 +0000477 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000478
Tim Peters07e99cb2001-01-14 23:47:14 +0000479 'data' is count of messages in mailbox ('EXISTS' response).
480 """
481 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
482 self.untagged_responses = {} # Flush old responses.
483 self.is_readonly = readonly
484 if readonly:
485 name = 'EXAMINE'
486 else:
487 name = 'SELECT'
488 typ, dat = self._simple_command(name, mailbox)
489 if typ != 'OK':
490 self.state = 'AUTH' # Might have been 'SELECTED'
491 return typ, dat
492 self.state = 'SELECTED'
493 if self.untagged_responses.has_key('READ-ONLY') \
494 and not readonly:
495 if __debug__:
496 if self.debug >= 1:
497 _dump_ur(self.untagged_responses)
498 raise self.readonly('%s is not writable' % mailbox)
499 return typ, self.untagged_responses.get('EXISTS', [None])
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000500
501
Tim Peters07e99cb2001-01-14 23:47:14 +0000502 def status(self, mailbox, names):
503 """Request named status conditions for mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000504
Tim Peters07e99cb2001-01-14 23:47:14 +0000505 (typ, [data]) = <instance>.status(mailbox, names)
506 """
507 name = 'STATUS'
508 if self.PROTOCOL_VERSION == 'IMAP4':
509 raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
510 typ, dat = self._simple_command(name, mailbox, names)
511 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000512
513
Tim Peters07e99cb2001-01-14 23:47:14 +0000514 def store(self, message_set, command, flags):
515 """Alters flag dispositions for messages in mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000516
Tim Peters07e99cb2001-01-14 23:47:14 +0000517 (typ, [data]) = <instance>.store(message_set, command, flags)
518 """
519 if (flags[0],flags[-1]) != ('(',')'):
520 flags = '(%s)' % flags # Avoid quoting the flags
521 typ, dat = self._simple_command('STORE', message_set, command, flags)
522 return self._untagged_response(typ, dat, 'FETCH')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000523
524
Tim Peters07e99cb2001-01-14 23:47:14 +0000525 def subscribe(self, mailbox):
526 """Subscribe to new mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000527
Tim Peters07e99cb2001-01-14 23:47:14 +0000528 (typ, [data]) = <instance>.subscribe(mailbox)
529 """
530 return self._simple_command('SUBSCRIBE', mailbox)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000531
532
Tim Peters07e99cb2001-01-14 23:47:14 +0000533 def uid(self, command, *args):
534 """Execute "command arg ..." with messages identified by UID,
535 rather than message number.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000536
Tim Peters07e99cb2001-01-14 23:47:14 +0000537 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000538
Tim Peters07e99cb2001-01-14 23:47:14 +0000539 Returns response appropriate to 'command'.
540 """
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +0000541 command = command.upper()
Tim Peters07e99cb2001-01-14 23:47:14 +0000542 if not Commands.has_key(command):
543 raise self.error("Unknown IMAP4 UID command: %s" % command)
544 if self.state not in Commands[command]:
545 raise self.error('command %s illegal in state %s'
546 % (command, self.state))
547 name = 'UID'
548 typ, dat = apply(self._simple_command, (name, command) + args)
549 if command == 'SEARCH':
550 name = 'SEARCH'
551 else:
552 name = 'FETCH'
553 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000554
555
Tim Peters07e99cb2001-01-14 23:47:14 +0000556 def unsubscribe(self, mailbox):
557 """Unsubscribe from old mailbox.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000558
Tim Peters07e99cb2001-01-14 23:47:14 +0000559 (typ, [data]) = <instance>.unsubscribe(mailbox)
560 """
561 return self._simple_command('UNSUBSCRIBE', mailbox)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000562
563
Tim Peters07e99cb2001-01-14 23:47:14 +0000564 def xatom(self, name, *args):
565 """Allow simple extension commands
566 notified by server in CAPABILITY response.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000567
Tim Peters07e99cb2001-01-14 23:47:14 +0000568 (typ, [data]) = <instance>.xatom(name, arg, ...)
569 """
570 if name[0] != 'X' or not name in self.capabilities:
571 raise self.error('unknown extension command: %s' % name)
572 return apply(self._simple_command, (name,) + args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000573
574
575
Tim Peters07e99cb2001-01-14 23:47:14 +0000576 # Private methods
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000577
578
Tim Peters07e99cb2001-01-14 23:47:14 +0000579 def _append_untagged(self, typ, dat):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000580
Tim Peters07e99cb2001-01-14 23:47:14 +0000581 if dat is None: dat = ''
582 ur = self.untagged_responses
583 if __debug__:
584 if self.debug >= 5:
585 _mesg('untagged_responses[%s] %s += ["%s"]' %
586 (typ, len(ur.get(typ,'')), dat))
587 if ur.has_key(typ):
588 ur[typ].append(dat)
589 else:
590 ur[typ] = [dat]
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000591
592
Tim Peters07e99cb2001-01-14 23:47:14 +0000593 def _check_bye(self):
594 bye = self.untagged_responses.get('BYE')
595 if bye:
596 raise self.abort(bye[-1])
Guido van Rossum8c062211999-12-13 23:27:45 +0000597
598
Tim Peters07e99cb2001-01-14 23:47:14 +0000599 def _command(self, name, *args):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000600
Tim Peters07e99cb2001-01-14 23:47:14 +0000601 if self.state not in Commands[name]:
602 self.literal = None
603 raise self.error(
604 'command %s illegal in state %s' % (name, self.state))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000605
Tim Peters07e99cb2001-01-14 23:47:14 +0000606 for typ in ('OK', 'NO', 'BAD'):
607 if self.untagged_responses.has_key(typ):
608 del self.untagged_responses[typ]
Guido van Rossum26367a01998-09-28 15:34:46 +0000609
Tim Peters07e99cb2001-01-14 23:47:14 +0000610 if self.untagged_responses.has_key('READ-ONLY') \
611 and not self.is_readonly:
612 raise self.readonly('mailbox status changed to READ-ONLY')
Guido van Rossumeda960a1998-06-18 14:24:28 +0000613
Tim Peters07e99cb2001-01-14 23:47:14 +0000614 tag = self._new_tag()
615 data = '%s %s' % (tag, name)
616 for arg in args:
617 if arg is None: continue
618 data = '%s %s' % (data, self._checkquote(arg))
Guido van Rossum6884af71998-05-29 13:34:03 +0000619
Tim Peters07e99cb2001-01-14 23:47:14 +0000620 literal = self.literal
621 if literal is not None:
622 self.literal = None
623 if type(literal) is type(self._command):
624 literator = literal
625 else:
626 literator = None
627 data = '%s {%s}' % (data, len(literal))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000628
Tim Peters07e99cb2001-01-14 23:47:14 +0000629 if __debug__:
630 if self.debug >= 4:
631 _mesg('> %s' % data)
632 else:
633 _log('> %s' % data)
Guido van Rossum8c062211999-12-13 23:27:45 +0000634
Tim Peters07e99cb2001-01-14 23:47:14 +0000635 try:
636 self.sock.send('%s%s' % (data, CRLF))
637 except socket.error, val:
638 raise self.abort('socket error: %s' % val)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000639
Tim Peters07e99cb2001-01-14 23:47:14 +0000640 if literal is None:
641 return tag
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000642
Tim Peters07e99cb2001-01-14 23:47:14 +0000643 while 1:
644 # Wait for continuation response
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000645
Tim Peters07e99cb2001-01-14 23:47:14 +0000646 while self._get_response():
647 if self.tagged_commands[tag]: # BAD/NO?
648 return tag
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000649
Tim Peters07e99cb2001-01-14 23:47:14 +0000650 # Send literal
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000651
Tim Peters07e99cb2001-01-14 23:47:14 +0000652 if literator:
653 literal = literator(self.continuation_response)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000654
Tim Peters07e99cb2001-01-14 23:47:14 +0000655 if __debug__:
656 if self.debug >= 4:
657 _mesg('write literal size %s' % len(literal))
Guido van Rossumeda960a1998-06-18 14:24:28 +0000658
Tim Peters07e99cb2001-01-14 23:47:14 +0000659 try:
660 self.sock.send(literal)
661 self.sock.send(CRLF)
662 except socket.error, val:
663 raise self.abort('socket error: %s' % val)
Guido van Rossumeda960a1998-06-18 14:24:28 +0000664
Tim Peters07e99cb2001-01-14 23:47:14 +0000665 if not literator:
666 break
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000667
Tim Peters07e99cb2001-01-14 23:47:14 +0000668 return tag
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000669
670
Tim Peters07e99cb2001-01-14 23:47:14 +0000671 def _command_complete(self, name, tag):
672 self._check_bye()
673 try:
674 typ, data = self._get_tagged_response(tag)
675 except self.abort, val:
676 raise self.abort('command: %s => %s' % (name, val))
677 except self.error, val:
678 raise self.error('command: %s => %s' % (name, val))
679 self._check_bye()
680 if typ == 'BAD':
681 raise self.error('%s command error: %s %s' % (name, typ, data))
682 return typ, data
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000683
684
Tim Peters07e99cb2001-01-14 23:47:14 +0000685 def _get_response(self):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000686
Tim Peters07e99cb2001-01-14 23:47:14 +0000687 # Read response and store.
688 #
689 # Returns None for continuation responses,
690 # otherwise first response line received.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000691
Tim Peters07e99cb2001-01-14 23:47:14 +0000692 resp = self._get_line()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000693
Tim Peters07e99cb2001-01-14 23:47:14 +0000694 # Command completion response?
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000695
Tim Peters07e99cb2001-01-14 23:47:14 +0000696 if self._match(self.tagre, resp):
697 tag = self.mo.group('tag')
698 if not self.tagged_commands.has_key(tag):
699 raise self.abort('unexpected tagged response: %s' % resp)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000700
Tim Peters07e99cb2001-01-14 23:47:14 +0000701 typ = self.mo.group('type')
702 dat = self.mo.group('data')
703 self.tagged_commands[tag] = (typ, [dat])
704 else:
705 dat2 = None
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000706
Tim Peters07e99cb2001-01-14 23:47:14 +0000707 # '*' (untagged) responses?
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000708
Tim Peters07e99cb2001-01-14 23:47:14 +0000709 if not self._match(Untagged_response, resp):
710 if self._match(Untagged_status, resp):
711 dat2 = self.mo.group('data2')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000712
Tim Peters07e99cb2001-01-14 23:47:14 +0000713 if self.mo is None:
714 # Only other possibility is '+' (continuation) response...
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000715
Tim Peters07e99cb2001-01-14 23:47:14 +0000716 if self._match(Continuation, resp):
717 self.continuation_response = self.mo.group('data')
718 return None # NB: indicates continuation
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000719
Tim Peters07e99cb2001-01-14 23:47:14 +0000720 raise self.abort("unexpected response: '%s'" % resp)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000721
Tim Peters07e99cb2001-01-14 23:47:14 +0000722 typ = self.mo.group('type')
723 dat = self.mo.group('data')
724 if dat is None: dat = '' # Null untagged response
725 if dat2: dat = dat + ' ' + dat2
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000726
Tim Peters07e99cb2001-01-14 23:47:14 +0000727 # Is there a literal to come?
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000728
Tim Peters07e99cb2001-01-14 23:47:14 +0000729 while self._match(Literal, dat):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000730
Tim Peters07e99cb2001-01-14 23:47:14 +0000731 # Read literal direct from connection.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000732
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +0000733 size = int(self.mo.group('size'))
Tim Peters07e99cb2001-01-14 23:47:14 +0000734 if __debug__:
735 if self.debug >= 4:
736 _mesg('read literal size %s' % size)
737 data = self.file.read(size)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000738
Tim Peters07e99cb2001-01-14 23:47:14 +0000739 # Store response with literal as tuple
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000740
Tim Peters07e99cb2001-01-14 23:47:14 +0000741 self._append_untagged(typ, (dat, data))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000742
Tim Peters07e99cb2001-01-14 23:47:14 +0000743 # Read trailer - possibly containing another literal
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000744
Tim Peters07e99cb2001-01-14 23:47:14 +0000745 dat = self._get_line()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000746
Tim Peters07e99cb2001-01-14 23:47:14 +0000747 self._append_untagged(typ, dat)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000748
Tim Peters07e99cb2001-01-14 23:47:14 +0000749 # Bracketed response information?
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000750
Tim Peters07e99cb2001-01-14 23:47:14 +0000751 if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
752 self._append_untagged(self.mo.group('type'), self.mo.group('data'))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000753
Tim Peters07e99cb2001-01-14 23:47:14 +0000754 if __debug__:
755 if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
756 _mesg('%s response: %s' % (typ, dat))
Guido van Rossum26367a01998-09-28 15:34:46 +0000757
Tim Peters07e99cb2001-01-14 23:47:14 +0000758 return resp
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000759
760
Tim Peters07e99cb2001-01-14 23:47:14 +0000761 def _get_tagged_response(self, tag):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000762
Tim Peters07e99cb2001-01-14 23:47:14 +0000763 while 1:
764 result = self.tagged_commands[tag]
765 if result is not None:
766 del self.tagged_commands[tag]
767 return result
Guido van Rossumf36b1822000-02-17 17:12:39 +0000768
Tim Peters07e99cb2001-01-14 23:47:14 +0000769 # Some have reported "unexpected response" exceptions.
770 # Note that ignoring them here causes loops.
771 # Instead, send me details of the unexpected response and
772 # I'll update the code in `_get_response()'.
Guido van Rossumf36b1822000-02-17 17:12:39 +0000773
Tim Peters07e99cb2001-01-14 23:47:14 +0000774 try:
775 self._get_response()
776 except self.abort, val:
777 if __debug__:
778 if self.debug >= 1:
779 print_log()
780 raise
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000781
782
Tim Peters07e99cb2001-01-14 23:47:14 +0000783 def _get_line(self):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000784
Tim Peters07e99cb2001-01-14 23:47:14 +0000785 line = self.file.readline()
786 if not line:
787 raise self.abort('socket error: EOF')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000788
Tim Peters07e99cb2001-01-14 23:47:14 +0000789 # Protocol mandates all lines terminated by CRLF
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000790
Tim Peters07e99cb2001-01-14 23:47:14 +0000791 line = line[:-2]
792 if __debug__:
793 if self.debug >= 4:
794 _mesg('< %s' % line)
795 else:
796 _log('< %s' % line)
797 return line
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000798
799
Tim Peters07e99cb2001-01-14 23:47:14 +0000800 def _match(self, cre, s):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000801
Tim Peters07e99cb2001-01-14 23:47:14 +0000802 # Run compiled regular expression match method on 's'.
803 # Save result, return success.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000804
Tim Peters07e99cb2001-01-14 23:47:14 +0000805 self.mo = cre.match(s)
806 if __debug__:
807 if self.mo is not None and self.debug >= 5:
808 _mesg("\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`))
809 return self.mo is not None
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000810
811
Tim Peters07e99cb2001-01-14 23:47:14 +0000812 def _new_tag(self):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000813
Tim Peters07e99cb2001-01-14 23:47:14 +0000814 tag = '%s%s' % (self.tagpre, self.tagnum)
815 self.tagnum = self.tagnum + 1
816 self.tagged_commands[tag] = None
817 return tag
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000818
819
Tim Peters07e99cb2001-01-14 23:47:14 +0000820 def _checkquote(self, arg):
Guido van Rossum8c062211999-12-13 23:27:45 +0000821
Tim Peters07e99cb2001-01-14 23:47:14 +0000822 # Must quote command args if non-alphanumeric chars present,
823 # and not already quoted.
Guido van Rossum8c062211999-12-13 23:27:45 +0000824
Tim Peters07e99cb2001-01-14 23:47:14 +0000825 if type(arg) is not type(''):
826 return arg
827 if (arg[0],arg[-1]) in (('(',')'),('"','"')):
828 return arg
829 if self.mustquote.search(arg) is None:
830 return arg
831 return self._quote(arg)
Guido van Rossum8c062211999-12-13 23:27:45 +0000832
833
Tim Peters07e99cb2001-01-14 23:47:14 +0000834 def _quote(self, arg):
Guido van Rossum8c062211999-12-13 23:27:45 +0000835
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +0000836 arg = arg.replace('\\', '\\\\')
837 arg = arg.replace('"', '\\"')
Guido van Rossum8c062211999-12-13 23:27:45 +0000838
Tim Peters07e99cb2001-01-14 23:47:14 +0000839 return '"%s"' % arg
Guido van Rossum8c062211999-12-13 23:27:45 +0000840
841
Tim Peters07e99cb2001-01-14 23:47:14 +0000842 def _simple_command(self, name, *args):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000843
Tim Peters07e99cb2001-01-14 23:47:14 +0000844 return self._command_complete(name, apply(self._command, (name,) + args))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000845
846
Tim Peters07e99cb2001-01-14 23:47:14 +0000847 def _untagged_response(self, typ, dat, name):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000848
Tim Peters07e99cb2001-01-14 23:47:14 +0000849 if typ == 'NO':
850 return typ, dat
851 if not self.untagged_responses.has_key(name):
852 return typ, [None]
853 data = self.untagged_responses[name]
854 if __debug__:
855 if self.debug >= 5:
856 _mesg('untagged_responses[%s] => %s' % (name, data))
857 del self.untagged_responses[name]
858 return typ, data
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000859
860
861
Guido van Rossumeda960a1998-06-18 14:24:28 +0000862class _Authenticator:
863
Tim Peters07e99cb2001-01-14 23:47:14 +0000864 """Private class to provide en/decoding
865 for base64-based authentication conversation.
866 """
Guido van Rossumeda960a1998-06-18 14:24:28 +0000867
Tim Peters07e99cb2001-01-14 23:47:14 +0000868 def __init__(self, mechinst):
869 self.mech = mechinst # Callable object to provide/process data
Guido van Rossumeda960a1998-06-18 14:24:28 +0000870
Tim Peters07e99cb2001-01-14 23:47:14 +0000871 def process(self, data):
872 ret = self.mech(self.decode(data))
873 if ret is None:
874 return '*' # Abort conversation
875 return self.encode(ret)
Guido van Rossumeda960a1998-06-18 14:24:28 +0000876
Tim Peters07e99cb2001-01-14 23:47:14 +0000877 def encode(self, inp):
878 #
879 # Invoke binascii.b2a_base64 iteratively with
880 # short even length buffers, strip the trailing
881 # line feed from the result and append. "Even"
882 # means a number that factors to both 6 and 8,
883 # so when it gets to the end of the 8-bit input
884 # there's no partial 6-bit output.
885 #
886 oup = ''
887 while inp:
888 if len(inp) > 48:
889 t = inp[:48]
890 inp = inp[48:]
891 else:
892 t = inp
893 inp = ''
894 e = binascii.b2a_base64(t)
895 if e:
896 oup = oup + e[:-1]
897 return oup
898
899 def decode(self, inp):
900 if not inp:
901 return ''
902 return binascii.a2b_base64(inp)
903
Guido van Rossumeda960a1998-06-18 14:24:28 +0000904
905
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000906Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
Tim Peters07e99cb2001-01-14 23:47:14 +0000907 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000908
909def Internaldate2tuple(resp):
Tim Peters07e99cb2001-01-14 23:47:14 +0000910 """Convert IMAP4 INTERNALDATE to UT.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000911
Tim Peters07e99cb2001-01-14 23:47:14 +0000912 Returns Python time module tuple.
913 """
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000914
Tim Peters07e99cb2001-01-14 23:47:14 +0000915 mo = InternalDate.match(resp)
916 if not mo:
917 return None
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000918
Tim Peters07e99cb2001-01-14 23:47:14 +0000919 mon = Mon2num[mo.group('mon')]
920 zonen = mo.group('zonen')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000921
Jeremy Hyltonf5d3ea02001-02-22 13:24:27 +0000922 day = int(mo.group('day'))
923 year = int(mo.group('year'))
924 hour = int(mo.group('hour'))
925 min = int(mo.group('min'))
926 sec = int(mo.group('sec'))
927 zoneh = int(mo.group('zoneh'))
928 zonem = int(mo.group('zonem'))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000929
Tim Peters07e99cb2001-01-14 23:47:14 +0000930 # INTERNALDATE timezone must be subtracted to get UT
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000931
Tim Peters07e99cb2001-01-14 23:47:14 +0000932 zone = (zoneh*60 + zonem)*60
933 if zonen == '-':
934 zone = -zone
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000935
Tim Peters07e99cb2001-01-14 23:47:14 +0000936 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000937
Tim Peters07e99cb2001-01-14 23:47:14 +0000938 utc = time.mktime(tt)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000939
Tim Peters07e99cb2001-01-14 23:47:14 +0000940 # Following is necessary because the time module has no 'mkgmtime'.
941 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000942
Tim Peters07e99cb2001-01-14 23:47:14 +0000943 lt = time.localtime(utc)
944 if time.daylight and lt[-1]:
945 zone = zone + time.altzone
946 else:
947 zone = zone + time.timezone
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000948
Tim Peters07e99cb2001-01-14 23:47:14 +0000949 return time.localtime(utc - zone)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000950
951
952
953def Int2AP(num):
954
Tim Peters07e99cb2001-01-14 23:47:14 +0000955 """Convert integer to A-P string representation."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000956
Tim Peters07e99cb2001-01-14 23:47:14 +0000957 val = ''; AP = 'ABCDEFGHIJKLMNOP'
958 num = int(abs(num))
959 while num:
960 num, mod = divmod(num, 16)
961 val = AP[mod] + val
962 return val
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000963
964
965
966def ParseFlags(resp):
967
Tim Peters07e99cb2001-01-14 23:47:14 +0000968 """Convert IMAP4 flags response to python tuple."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000969
Tim Peters07e99cb2001-01-14 23:47:14 +0000970 mo = Flags.match(resp)
971 if not mo:
972 return ()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000973
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +0000974 return tuple(mo.group('flags').split())
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000975
976
977def Time2Internaldate(date_time):
978
Tim Peters07e99cb2001-01-14 23:47:14 +0000979 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000980
Tim Peters07e99cb2001-01-14 23:47:14 +0000981 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
982 """
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000983
Tim Peters07e99cb2001-01-14 23:47:14 +0000984 dttype = type(date_time)
985 if dttype is type(1) or dttype is type(1.1):
986 tt = time.localtime(date_time)
987 elif dttype is type(()):
988 tt = date_time
989 elif dttype is type(""):
990 return date_time # Assume in correct format
991 else: raise ValueError
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000992
Tim Peters07e99cb2001-01-14 23:47:14 +0000993 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
994 if dt[0] == '0':
995 dt = ' ' + dt[1:]
996 if time.daylight and tt[-1]:
997 zone = -time.altzone
998 else:
999 zone = -time.timezone
1000 return '"' + dt + " %+02d%02d" % divmod(zone/60, 60) + '"'
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001001
1002
1003
Guido van Rossumeda960a1998-06-18 14:24:28 +00001004if __debug__:
1005
Tim Peters07e99cb2001-01-14 23:47:14 +00001006 def _mesg(s, secs=None):
1007 if secs is None:
1008 secs = time.time()
1009 tm = time.strftime('%M:%S', time.localtime(secs))
1010 sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s))
1011 sys.stderr.flush()
Guido van Rossum26367a01998-09-28 15:34:46 +00001012
Tim Peters07e99cb2001-01-14 23:47:14 +00001013 def _dump_ur(dict):
1014 # Dump untagged responses (in `dict').
1015 l = dict.items()
1016 if not l: return
1017 t = '\n\t\t'
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +00001018 l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l)
Tim Peters07e99cb2001-01-14 23:47:14 +00001019 _mesg('untagged responses dump:%s%s' % (t, j(l, t)))
Guido van Rossumeda960a1998-06-18 14:24:28 +00001020
Tim Peters07e99cb2001-01-14 23:47:14 +00001021 _cmd_log = [] # Last `_cmd_log_len' interactions
1022 _cmd_log_len = 10
Guido van Rossum8c062211999-12-13 23:27:45 +00001023
Tim Peters07e99cb2001-01-14 23:47:14 +00001024 def _log(line):
1025 # Keep log of last `_cmd_log_len' interactions for debugging.
1026 if len(_cmd_log) == _cmd_log_len:
1027 del _cmd_log[0]
1028 _cmd_log.append((time.time(), line))
Guido van Rossum8c062211999-12-13 23:27:45 +00001029
Tim Peters07e99cb2001-01-14 23:47:14 +00001030 def print_log():
1031 _mesg('last %d IMAP4 interactions:' % len(_cmd_log))
1032 for secs,line in _cmd_log:
1033 _mesg(line, secs)
Guido van Rossumeda960a1998-06-18 14:24:28 +00001034
1035
Guido van Rossum8c062211999-12-13 23:27:45 +00001036
1037if __name__ == '__main__':
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001038
Tim Peters07e99cb2001-01-14 23:47:14 +00001039 import getopt, getpass, sys
Guido van Rossumd6596931998-05-29 18:08:48 +00001040
Tim Peters07e99cb2001-01-14 23:47:14 +00001041 try:
1042 optlist, args = getopt.getopt(sys.argv[1:], 'd:')
1043 except getopt.error, val:
1044 pass
Guido van Rossum66d45132000-03-28 20:20:53 +00001045
Tim Peters07e99cb2001-01-14 23:47:14 +00001046 for opt,val in optlist:
1047 if opt == '-d':
1048 Debug = int(val)
Guido van Rossum66d45132000-03-28 20:20:53 +00001049
Tim Peters07e99cb2001-01-14 23:47:14 +00001050 if not args: args = ('',)
Guido van Rossum66d45132000-03-28 20:20:53 +00001051
Tim Peters07e99cb2001-01-14 23:47:14 +00001052 host = args[0]
Guido van Rossumb1f08121998-06-25 02:22:16 +00001053
Tim Peters07e99cb2001-01-14 23:47:14 +00001054 USER = getpass.getuser()
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +00001055 PASSWD = getpass.getpass("IMAP password for %s on %s:" % (USER, host or "localhost"))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001056
Tim Peters07e99cb2001-01-14 23:47:14 +00001057 test_mesg = 'From: %s@localhost\nSubject: IMAP4 test\n\ndata...\n' % USER
1058 test_seq1 = (
1059 ('login', (USER, PASSWD)),
1060 ('create', ('/tmp/xxx 1',)),
1061 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1062 ('CREATE', ('/tmp/yyz 2',)),
1063 ('append', ('/tmp/yyz 2', None, None, test_mesg)),
1064 ('list', ('/tmp', 'yy*')),
1065 ('select', ('/tmp/yyz 2',)),
1066 ('search', (None, 'SUBJECT', 'test')),
1067 ('partial', ('1', 'RFC822', 1, 1024)),
1068 ('store', ('1', 'FLAGS', '(\Deleted)')),
1069 ('expunge', ()),
1070 ('recent', ()),
1071 ('close', ()),
1072 )
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001073
Tim Peters07e99cb2001-01-14 23:47:14 +00001074 test_seq2 = (
1075 ('select', ()),
1076 ('response',('UIDVALIDITY',)),
1077 ('uid', ('SEARCH', 'ALL')),
1078 ('response', ('EXISTS',)),
1079 ('append', (None, None, None, test_mesg)),
1080 ('recent', ()),
1081 ('logout', ()),
1082 )
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001083
Tim Peters07e99cb2001-01-14 23:47:14 +00001084 def run(cmd, args):
1085 _mesg('%s %s' % (cmd, args))
1086 typ, dat = apply(eval('M.%s' % cmd), args)
1087 _mesg('%s => %s %s' % (cmd, typ, dat))
1088 return dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001089
Tim Peters07e99cb2001-01-14 23:47:14 +00001090 try:
1091 M = IMAP4(host)
1092 _mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001093
Tim Peters07e99cb2001-01-14 23:47:14 +00001094 for cmd,args in test_seq1:
1095 run(cmd, args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001096
Tim Peters07e99cb2001-01-14 23:47:14 +00001097 for ml in run('list', ('/tmp/', 'yy%')):
1098 mo = re.match(r'.*"([^"]+)"$', ml)
1099 if mo: path = mo.group(1)
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +00001100 else: path = ml.split()[-1]
Tim Peters07e99cb2001-01-14 23:47:14 +00001101 run('delete', (path,))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001102
Tim Peters07e99cb2001-01-14 23:47:14 +00001103 for cmd,args in test_seq2:
1104 dat = run(cmd, args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001105
Tim Peters07e99cb2001-01-14 23:47:14 +00001106 if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
1107 continue
Guido van Rossum38d8f4e1998-04-11 01:22:34 +00001108
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +00001109 uid = dat[-1].split()
Tim Peters07e99cb2001-01-14 23:47:14 +00001110 if not uid: continue
1111 run('uid', ('FETCH', '%s' % uid[-1],
1112 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
Guido van Rossum66d45132000-03-28 20:20:53 +00001113
Tim Peters07e99cb2001-01-14 23:47:14 +00001114 print '\nAll tests OK.'
Guido van Rossum66d45132000-03-28 20:20:53 +00001115
Tim Peters07e99cb2001-01-14 23:47:14 +00001116 except:
1117 print '\nTests failed.'
Guido van Rossum66d45132000-03-28 20:20:53 +00001118
Tim Peters07e99cb2001-01-14 23:47:14 +00001119 if not Debug:
1120 print '''
Guido van Rossum66d45132000-03-28 20:20:53 +00001121If you would like to see debugging output,
1122try: %s -d5
1123''' % sys.argv[0]
1124
Tim Peters07e99cb2001-01-14 23:47:14 +00001125 raise