blob: 4bb741f2aa6045e66650bfdafb540c07d327dae5 [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):
910
Tim Peters07e99cb2001-01-14 23:47:14 +0000911 """Convert IMAP4 INTERNALDATE to UT.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000912
Tim Peters07e99cb2001-01-14 23:47:14 +0000913 Returns Python time module tuple.
914 """
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000915
Tim Peters07e99cb2001-01-14 23:47:14 +0000916 mo = InternalDate.match(resp)
917 if not mo:
918 return None
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000919
Tim Peters07e99cb2001-01-14 23:47:14 +0000920 mon = Mon2num[mo.group('mon')]
921 zonen = mo.group('zonen')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000922
Tim Peters07e99cb2001-01-14 23:47:14 +0000923 for name in ('day', 'year', 'hour', 'min', 'sec', 'zoneh', 'zonem'):
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +0000924 exec "%s = int(mo.group('%s'))" % (name, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000925
Tim Peters07e99cb2001-01-14 23:47:14 +0000926 # INTERNALDATE timezone must be subtracted to get UT
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000927
Tim Peters07e99cb2001-01-14 23:47:14 +0000928 zone = (zoneh*60 + zonem)*60
929 if zonen == '-':
930 zone = -zone
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000931
Tim Peters07e99cb2001-01-14 23:47:14 +0000932 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000933
Tim Peters07e99cb2001-01-14 23:47:14 +0000934 utc = time.mktime(tt)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000935
Tim Peters07e99cb2001-01-14 23:47:14 +0000936 # Following is necessary because the time module has no 'mkgmtime'.
937 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000938
Tim Peters07e99cb2001-01-14 23:47:14 +0000939 lt = time.localtime(utc)
940 if time.daylight and lt[-1]:
941 zone = zone + time.altzone
942 else:
943 zone = zone + time.timezone
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000944
Tim Peters07e99cb2001-01-14 23:47:14 +0000945 return time.localtime(utc - zone)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000946
947
948
949def Int2AP(num):
950
Tim Peters07e99cb2001-01-14 23:47:14 +0000951 """Convert integer to A-P string representation."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000952
Tim Peters07e99cb2001-01-14 23:47:14 +0000953 val = ''; AP = 'ABCDEFGHIJKLMNOP'
954 num = int(abs(num))
955 while num:
956 num, mod = divmod(num, 16)
957 val = AP[mod] + val
958 return val
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000959
960
961
962def ParseFlags(resp):
963
Tim Peters07e99cb2001-01-14 23:47:14 +0000964 """Convert IMAP4 flags response to python tuple."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000965
Tim Peters07e99cb2001-01-14 23:47:14 +0000966 mo = Flags.match(resp)
967 if not mo:
968 return ()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000969
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +0000970 return tuple(mo.group('flags').split())
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000971
972
973def Time2Internaldate(date_time):
974
Tim Peters07e99cb2001-01-14 23:47:14 +0000975 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000976
Tim Peters07e99cb2001-01-14 23:47:14 +0000977 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
978 """
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000979
Tim Peters07e99cb2001-01-14 23:47:14 +0000980 dttype = type(date_time)
981 if dttype is type(1) or dttype is type(1.1):
982 tt = time.localtime(date_time)
983 elif dttype is type(()):
984 tt = date_time
985 elif dttype is type(""):
986 return date_time # Assume in correct format
987 else: raise ValueError
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000988
Tim Peters07e99cb2001-01-14 23:47:14 +0000989 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
990 if dt[0] == '0':
991 dt = ' ' + dt[1:]
992 if time.daylight and tt[-1]:
993 zone = -time.altzone
994 else:
995 zone = -time.timezone
996 return '"' + dt + " %+02d%02d" % divmod(zone/60, 60) + '"'
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000997
998
999
Guido van Rossumeda960a1998-06-18 14:24:28 +00001000if __debug__:
1001
Tim Peters07e99cb2001-01-14 23:47:14 +00001002 def _mesg(s, secs=None):
1003 if secs is None:
1004 secs = time.time()
1005 tm = time.strftime('%M:%S', time.localtime(secs))
1006 sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s))
1007 sys.stderr.flush()
Guido van Rossum26367a01998-09-28 15:34:46 +00001008
Tim Peters07e99cb2001-01-14 23:47:14 +00001009 def _dump_ur(dict):
1010 # Dump untagged responses (in `dict').
1011 l = dict.items()
1012 if not l: return
1013 t = '\n\t\t'
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +00001014 l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l)
Tim Peters07e99cb2001-01-14 23:47:14 +00001015 _mesg('untagged responses dump:%s%s' % (t, j(l, t)))
Guido van Rossumeda960a1998-06-18 14:24:28 +00001016
Tim Peters07e99cb2001-01-14 23:47:14 +00001017 _cmd_log = [] # Last `_cmd_log_len' interactions
1018 _cmd_log_len = 10
Guido van Rossum8c062211999-12-13 23:27:45 +00001019
Tim Peters07e99cb2001-01-14 23:47:14 +00001020 def _log(line):
1021 # Keep log of last `_cmd_log_len' interactions for debugging.
1022 if len(_cmd_log) == _cmd_log_len:
1023 del _cmd_log[0]
1024 _cmd_log.append((time.time(), line))
Guido van Rossum8c062211999-12-13 23:27:45 +00001025
Tim Peters07e99cb2001-01-14 23:47:14 +00001026 def print_log():
1027 _mesg('last %d IMAP4 interactions:' % len(_cmd_log))
1028 for secs,line in _cmd_log:
1029 _mesg(line, secs)
Guido van Rossumeda960a1998-06-18 14:24:28 +00001030
1031
Guido van Rossum8c062211999-12-13 23:27:45 +00001032
1033if __name__ == '__main__':
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001034
Tim Peters07e99cb2001-01-14 23:47:14 +00001035 import getopt, getpass, sys
Guido van Rossumd6596931998-05-29 18:08:48 +00001036
Tim Peters07e99cb2001-01-14 23:47:14 +00001037 try:
1038 optlist, args = getopt.getopt(sys.argv[1:], 'd:')
1039 except getopt.error, val:
1040 pass
Guido van Rossum66d45132000-03-28 20:20:53 +00001041
Tim Peters07e99cb2001-01-14 23:47:14 +00001042 for opt,val in optlist:
1043 if opt == '-d':
1044 Debug = int(val)
Guido van Rossum66d45132000-03-28 20:20:53 +00001045
Tim Peters07e99cb2001-01-14 23:47:14 +00001046 if not args: args = ('',)
Guido van Rossum66d45132000-03-28 20:20:53 +00001047
Tim Peters07e99cb2001-01-14 23:47:14 +00001048 host = args[0]
Guido van Rossumb1f08121998-06-25 02:22:16 +00001049
Tim Peters07e99cb2001-01-14 23:47:14 +00001050 USER = getpass.getuser()
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +00001051 PASSWD = getpass.getpass("IMAP password for %s on %s:" % (USER, host or "localhost"))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001052
Tim Peters07e99cb2001-01-14 23:47:14 +00001053 test_mesg = 'From: %s@localhost\nSubject: IMAP4 test\n\ndata...\n' % USER
1054 test_seq1 = (
1055 ('login', (USER, PASSWD)),
1056 ('create', ('/tmp/xxx 1',)),
1057 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1058 ('CREATE', ('/tmp/yyz 2',)),
1059 ('append', ('/tmp/yyz 2', None, None, test_mesg)),
1060 ('list', ('/tmp', 'yy*')),
1061 ('select', ('/tmp/yyz 2',)),
1062 ('search', (None, 'SUBJECT', 'test')),
1063 ('partial', ('1', 'RFC822', 1, 1024)),
1064 ('store', ('1', 'FLAGS', '(\Deleted)')),
1065 ('expunge', ()),
1066 ('recent', ()),
1067 ('close', ()),
1068 )
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001069
Tim Peters07e99cb2001-01-14 23:47:14 +00001070 test_seq2 = (
1071 ('select', ()),
1072 ('response',('UIDVALIDITY',)),
1073 ('uid', ('SEARCH', 'ALL')),
1074 ('response', ('EXISTS',)),
1075 ('append', (None, None, None, test_mesg)),
1076 ('recent', ()),
1077 ('logout', ()),
1078 )
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001079
Tim Peters07e99cb2001-01-14 23:47:14 +00001080 def run(cmd, args):
1081 _mesg('%s %s' % (cmd, args))
1082 typ, dat = apply(eval('M.%s' % cmd), args)
1083 _mesg('%s => %s %s' % (cmd, typ, dat))
1084 return dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001085
Tim Peters07e99cb2001-01-14 23:47:14 +00001086 try:
1087 M = IMAP4(host)
1088 _mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001089
Tim Peters07e99cb2001-01-14 23:47:14 +00001090 for cmd,args in test_seq1:
1091 run(cmd, args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001092
Tim Peters07e99cb2001-01-14 23:47:14 +00001093 for ml in run('list', ('/tmp/', 'yy%')):
1094 mo = re.match(r'.*"([^"]+)"$', ml)
1095 if mo: path = mo.group(1)
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +00001096 else: path = ml.split()[-1]
Tim Peters07e99cb2001-01-14 23:47:14 +00001097 run('delete', (path,))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001098
Tim Peters07e99cb2001-01-14 23:47:14 +00001099 for cmd,args in test_seq2:
1100 dat = run(cmd, args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001101
Tim Peters07e99cb2001-01-14 23:47:14 +00001102 if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
1103 continue
Guido van Rossum38d8f4e1998-04-11 01:22:34 +00001104
Eric S. Raymond25a0cbc2001-02-09 06:50:21 +00001105 uid = dat[-1].split()
Tim Peters07e99cb2001-01-14 23:47:14 +00001106 if not uid: continue
1107 run('uid', ('FETCH', '%s' % uid[-1],
1108 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
Guido van Rossum66d45132000-03-28 20:20:53 +00001109
Tim Peters07e99cb2001-01-14 23:47:14 +00001110 print '\nAll tests OK.'
Guido van Rossum66d45132000-03-28 20:20:53 +00001111
Tim Peters07e99cb2001-01-14 23:47:14 +00001112 except:
1113 print '\nTests failed.'
Guido van Rossum66d45132000-03-28 20:20:53 +00001114
Tim Peters07e99cb2001-01-14 23:47:14 +00001115 if not Debug:
1116 print '''
Guido van Rossum66d45132000-03-28 20:20:53 +00001117If you would like to see debugging output,
1118try: %s -d5
1119''' % sys.argv[0]
1120
Tim Peters07e99cb2001-01-14 23:47:14 +00001121 raise