blob: 990cd9a003c1c9c1ce6417b3947b348574b02926 [file] [log] [blame]
Guido van Rossum56013131994-06-23 12:06:02 +00001# MH interface -- purely object-oriented (well, almost)
2#
3# Executive summary:
4#
5# import mhlib
6#
7# mh = mhlib.MH() # use default mailbox directory and profile
8# mh = mhlib.MH(mailbox) # override mailbox location (default from profile)
9# mh = mhlib.MH(mailbox, profile) # override mailbox and profile
10#
11# mh.error(format, ...) # print error message -- can be overridden
12# s = mh.getprofile(key) # profile entry (None if not set)
13# path = mh.getpath() # mailbox pathname
14# name = mh.getcontext() # name of current folder
Guido van Rossum508a0921996-05-28 22:59:37 +000015# mh.setcontext(name) # set name of current folder
Guido van Rossum56013131994-06-23 12:06:02 +000016#
17# list = mh.listfolders() # names of top-level folders
18# list = mh.listallfolders() # names of all folders, including subfolders
19# list = mh.listsubfolders(name) # direct subfolders of given folder
20# list = mh.listallsubfolders(name) # all subfolders of given folder
21#
22# mh.makefolder(name) # create new folder
23# mh.deletefolder(name) # delete folder -- must have no subfolders
24#
25# f = mh.openfolder(name) # new open folder object
26#
27# f.error(format, ...) # same as mh.error(format, ...)
28# path = f.getfullname() # folder's full pathname
29# path = f.getsequencesfilename() # full pathname of folder's sequences file
30# path = f.getmessagefilename(n) # full pathname of message n in folder
31#
32# list = f.listmessages() # list of messages in folder (as numbers)
33# n = f.getcurrent() # get current message
34# f.setcurrent(n) # set current message
Guido van Rossum508a0921996-05-28 22:59:37 +000035# list = f.parsesequence(seq) # parse msgs syntax into list of messages
Guido van Rossum40b2cfb1995-01-02 18:38:23 +000036# n = f.getlast() # get last message (0 if no messagse)
37# f.setlast(n) # set last message (internal use only)
Guido van Rossum56013131994-06-23 12:06:02 +000038#
39# dict = f.getsequences() # dictionary of sequences in folder {name: list}
40# f.putsequences(dict) # write sequences back to folder
41#
Guido van Rossum40b2cfb1995-01-02 18:38:23 +000042# f.removemessages(list) # remove messages in list from folder
43# f.refilemessages(list, tofolder) # move messages in list to other folder
44# f.movemessage(n, tofolder, ton) # move one message to a given destination
45# f.copymessage(n, tofolder, ton) # copy one message to a given destination
46#
Guido van Rossum56013131994-06-23 12:06:02 +000047# m = f.openmessage(n) # new open message object (costs a file descriptor)
Guido van Rossum85347411994-09-09 11:10:15 +000048# m is a derived class of mimetools.Message(rfc822.Message), with:
Guido van Rossum56013131994-06-23 12:06:02 +000049# s = m.getheadertext() # text of message's headers
50# s = m.getheadertext(pred) # text of message's headers, filtered by pred
51# s = m.getbodytext() # text of message's body, decoded
52# s = m.getbodytext(0) # text of message's body, not decoded
53#
54# XXX To do, functionality:
Guido van Rossum56013131994-06-23 12:06:02 +000055# - annotate messages
56# - create, send messages
57#
Guido van Rossum40b2cfb1995-01-02 18:38:23 +000058# XXX To do, organization:
Guido van Rossum56013131994-06-23 12:06:02 +000059# - move IntSet to separate file
60# - move most Message functionality to module mimetools
61
62
63# Customizable defaults
64
65MH_PROFILE = '~/.mh_profile'
66PATH = '~/Mail'
67MH_SEQUENCES = '.mh_sequences'
68FOLDER_PROTECT = 0700
69
70
71# Imported modules
72
73import os
Guido van Rossum508a0921996-05-28 22:59:37 +000074import sys
Guido van Rossum56013131994-06-23 12:06:02 +000075from stat import ST_NLINK
76import regex
77import string
78import mimetools
79import multifile
Guido van Rossum40b2cfb1995-01-02 18:38:23 +000080import shutil
Guido van Rossum56013131994-06-23 12:06:02 +000081
82
83# Exported constants
84
85Error = 'mhlib.Error'
86
87
88# Class representing a particular collection of folders.
89# Optional constructor arguments are the pathname for the directory
90# containing the collection, and the MH profile to use.
91# If either is omitted or empty a default is used; the default
92# directory is taken from the MH profile if it is specified there.
93
94class MH:
95
96 # Constructor
97 def __init__(self, path = None, profile = None):
98 if not profile: profile = MH_PROFILE
99 self.profile = os.path.expanduser(profile)
100 if not path: path = self.getprofile('Path')
101 if not path: path = PATH
102 if not os.path.isabs(path) and path[0] != '~':
103 path = os.path.join('~', path)
104 path = os.path.expanduser(path)
105 if not os.path.isdir(path): raise Error, 'MH() path not found'
106 self.path = path
107
108 # String representation
109 def __repr__(self):
110 return 'MH(%s, %s)' % (`self.path`, `self.profile`)
111
112 # Routine to print an error. May be overridden by a derived class
113 def error(self, msg, *args):
Guido van Rossum508a0921996-05-28 22:59:37 +0000114 sys.stderr.write('MH error: %s\n' % (msg % args))
Guido van Rossum56013131994-06-23 12:06:02 +0000115
116 # Return a profile entry, None if not found
117 def getprofile(self, key):
118 return pickline(self.profile, key)
119
120 # Return the path (the name of the collection's directory)
121 def getpath(self):
122 return self.path
123
124 # Return the name of the current folder
125 def getcontext(self):
126 context = pickline(os.path.join(self.getpath(), 'context'),
127 'Current-Folder')
128 if not context: context = 'inbox'
129 return context
130
Guido van Rossum508a0921996-05-28 22:59:37 +0000131 # Set the name of the current folder
132 def setcontext(self, context):
133 fn = os.path.join(self.getpath(), 'context')
134 f = open(fn, "w")
135 f.write("Current-Folder: %s\n" % context)
136 f.close()
137
Guido van Rossum56013131994-06-23 12:06:02 +0000138 # Return the names of the top-level folders
139 def listfolders(self):
140 folders = []
141 path = self.getpath()
142 for name in os.listdir(path):
Guido van Rossum56013131994-06-23 12:06:02 +0000143 fullname = os.path.join(path, name)
144 if os.path.isdir(fullname):
145 folders.append(name)
146 folders.sort()
147 return folders
148
149 # Return the names of the subfolders in a given folder
150 # (prefixed with the given folder name)
151 def listsubfolders(self, name):
152 fullname = os.path.join(self.path, name)
153 # Get the link count so we can avoid listing folders
154 # that have no subfolders.
155 st = os.stat(fullname)
156 nlinks = st[ST_NLINK]
157 if nlinks <= 2:
158 return []
159 subfolders = []
160 subnames = os.listdir(fullname)
161 for subname in subnames:
Guido van Rossum56013131994-06-23 12:06:02 +0000162 fullsubname = os.path.join(fullname, subname)
163 if os.path.isdir(fullsubname):
164 name_subname = os.path.join(name, subname)
165 subfolders.append(name_subname)
166 # Stop looking for subfolders when
167 # we've seen them all
168 nlinks = nlinks - 1
169 if nlinks <= 2:
170 break
171 subfolders.sort()
172 return subfolders
173
174 # Return the names of all folders, including subfolders, recursively
175 def listallfolders(self):
176 return self.listallsubfolders('')
177
178 # Return the names of subfolders in a given folder, recursively
179 def listallsubfolders(self, name):
180 fullname = os.path.join(self.path, name)
181 # Get the link count so we can avoid listing folders
182 # that have no subfolders.
183 st = os.stat(fullname)
184 nlinks = st[ST_NLINK]
185 if nlinks <= 2:
186 return []
187 subfolders = []
188 subnames = os.listdir(fullname)
189 for subname in subnames:
Guido van Rossum56013131994-06-23 12:06:02 +0000190 if subname[0] == ',' or isnumeric(subname): continue
191 fullsubname = os.path.join(fullname, subname)
192 if os.path.isdir(fullsubname):
193 name_subname = os.path.join(name, subname)
194 subfolders.append(name_subname)
195 if not os.path.islink(fullsubname):
196 subsubfolders = self.listallsubfolders(
197 name_subname)
198 subfolders = subfolders + subsubfolders
199 # Stop looking for subfolders when
200 # we've seen them all
201 nlinks = nlinks - 1
202 if nlinks <= 2:
203 break
204 subfolders.sort()
205 return subfolders
206
207 # Return a new Folder object for the named folder
208 def openfolder(self, name):
209 return Folder(self, name)
210
211 # Create a new folder. This raises os.error if the folder
212 # cannot be created
213 def makefolder(self, name):
214 protect = pickline(self.profile, 'Folder-Protect')
215 if protect and isnumeric(protect):
Guido van Rossum508a0921996-05-28 22:59:37 +0000216 mode = string.atoi(protect, 8)
Guido van Rossum56013131994-06-23 12:06:02 +0000217 else:
218 mode = FOLDER_PROTECT
219 os.mkdir(os.path.join(self.getpath(), name), mode)
220
221 # Delete a folder. This removes files in the folder but not
222 # subdirectories. If deleting the folder itself fails it
223 # raises os.error
224 def deletefolder(self, name):
225 fullname = os.path.join(self.getpath(), name)
226 for subname in os.listdir(fullname):
Guido van Rossum56013131994-06-23 12:06:02 +0000227 fullsubname = os.path.join(fullname, subname)
228 try:
229 os.unlink(fullsubname)
230 except os.error:
231 self.error('%s not deleted, continuing...' %
232 fullsubname)
233 os.rmdir(fullname)
234
235
236# Class representing a particular folder
237
Guido van Rossum659a3b51997-04-02 01:18:30 +0000238numericprog = regex.compile('^[1-9][0-9]*$')
Guido van Rossum56013131994-06-23 12:06:02 +0000239def isnumeric(str):
Guido van Rossum659a3b51997-04-02 01:18:30 +0000240 return numericprog.match(str) >= 0
Guido van Rossum56013131994-06-23 12:06:02 +0000241
242class Folder:
243
244 # Constructor
245 def __init__(self, mh, name):
246 self.mh = mh
247 self.name = name
248 if not os.path.isdir(self.getfullname()):
249 raise Error, 'no folder %s' % name
250
251 # String representation
252 def __repr__(self):
253 return 'Folder(%s, %s)' % (`self.mh`, `self.name`)
254
255 # Error message handler
256 def error(self, *args):
257 apply(self.mh.error, args)
258
259 # Return the full pathname of the folder
260 def getfullname(self):
261 return os.path.join(self.mh.path, self.name)
262
263 # Return the full pathname of the folder's sequences file
264 def getsequencesfilename(self):
265 return os.path.join(self.getfullname(), MH_SEQUENCES)
266
267 # Return the full pathname of a message in the folder
268 def getmessagefilename(self, n):
269 return os.path.join(self.getfullname(), str(n))
270
271 # Return list of direct subfolders
272 def listsubfolders(self):
273 return self.mh.listsubfolders(self.name)
274
275 # Return list of all subfolders
276 def listallsubfolders(self):
277 return self.mh.listallsubfolders(self.name)
278
279 # Return the list of messages currently present in the folder.
280 # As a side effect, set self.last to the last message (or 0)
281 def listmessages(self):
282 messages = []
Guido van Rossum659a3b51997-04-02 01:18:30 +0000283 match = numericprog.match
284 append = messages.append
Guido van Rossum56013131994-06-23 12:06:02 +0000285 for name in os.listdir(self.getfullname()):
Guido van Rossum659a3b51997-04-02 01:18:30 +0000286 if match(name) >= 0:
287 append(name)
288 messages = map(string.atoi, messages)
Guido van Rossum56013131994-06-23 12:06:02 +0000289 messages.sort()
290 if messages:
Guido van Rossum659a3b51997-04-02 01:18:30 +0000291 self.last = messages[-1]
Guido van Rossum56013131994-06-23 12:06:02 +0000292 else:
293 self.last = 0
294 return messages
295
296 # Return the set of sequences for the folder
297 def getsequences(self):
298 sequences = {}
299 fullname = self.getsequencesfilename()
300 try:
301 f = open(fullname, 'r')
302 except IOError:
303 return sequences
304 while 1:
305 line = f.readline()
306 if not line: break
307 fields = string.splitfields(line, ':')
308 if len(fields) <> 2:
309 self.error('bad sequence in %s: %s' %
310 (fullname, string.strip(line)))
311 key = string.strip(fields[0])
312 value = IntSet(string.strip(fields[1]), ' ').tolist()
313 sequences[key] = value
314 return sequences
315
316 # Write the set of sequences back to the folder
317 def putsequences(self, sequences):
318 fullname = self.getsequencesfilename()
Guido van Rossum85347411994-09-09 11:10:15 +0000319 f = None
Guido van Rossum56013131994-06-23 12:06:02 +0000320 for key in sequences.keys():
321 s = IntSet('', ' ')
322 s.fromlist(sequences[key])
Guido van Rossum85347411994-09-09 11:10:15 +0000323 if not f: f = open(fullname, 'w')
Guido van Rossum56013131994-06-23 12:06:02 +0000324 f.write('%s: %s\n' % (key, s.tostring()))
Guido van Rossum85347411994-09-09 11:10:15 +0000325 if not f:
326 try:
327 os.unlink(fullname)
328 except os.error:
329 pass
330 else:
331 f.close()
Guido van Rossum56013131994-06-23 12:06:02 +0000332
333 # Return the current message. Raise KeyError when there is none
334 def getcurrent(self):
335 return min(self.getsequences()['cur'])
336
337 # Set the current message
338 def setcurrent(self, n):
339 updateline(self.getsequencesfilename(), 'cur', str(n), 0)
340
Guido van Rossum508a0921996-05-28 22:59:37 +0000341 # Parse an MH sequence specification into a message list.
342 def parsesequence(self, seq):
343 if seq == "all":
344 return self.listmessages()
345 try:
346 n = string.atoi(seq, 10)
347 except string.atoi_error:
348 n = 0
349 if n > 0:
350 return [n]
351 if regex.match("^last:", seq) >= 0:
352 try:
353 n = string.atoi(seq[5:])
354 except string.atoi_error:
355 n = 0
356 if n > 0:
357 return self.listmessages()[-n:]
358 if regex.match("^first:", seq) >= 0:
359 try:
360 n = string.atoi(seq[6:])
361 except string.atoi_error:
362 n = 0
363 if n > 0:
364 return self.listmessages()[:n]
365 dict = self.getsequences()
366 if dict.has_key(seq):
367 return dict[seq]
368 context = self.mh.getcontext()
369 # Complex syntax -- use pick
370 pipe = os.popen("pick +%s %s 2>/dev/null" % (self.name, seq))
371 data = pipe.read()
372 sts = pipe.close()
373 self.mh.setcontext(context)
374 if sts:
375 raise Error, "unparseable sequence %s" % `seq`
376 items = string.split(data)
377 return map(string.atoi, items)
378
Guido van Rossum85347411994-09-09 11:10:15 +0000379 # Open a message -- returns a Message object
Guido van Rossum56013131994-06-23 12:06:02 +0000380 def openmessage(self, n):
Guido van Rossum56013131994-06-23 12:06:02 +0000381 return Message(self, n)
382
383 # Remove one or more messages -- may raise os.error
384 def removemessages(self, list):
385 errors = []
386 deleted = []
387 for n in list:
388 path = self.getmessagefilename(n)
389 commapath = self.getmessagefilename(',' + str(n))
390 try:
391 os.unlink(commapath)
392 except os.error:
393 pass
394 try:
395 os.rename(path, commapath)
396 except os.error, msg:
397 errors.append(msg)
398 else:
399 deleted.append(n)
400 if deleted:
401 self.removefromallsequences(deleted)
402 if errors:
403 if len(errors) == 1:
404 raise os.error, errors[0]
405 else:
406 raise os.error, ('multiple errors:', errors)
407
408 # Refile one or more messages -- may raise os.error.
409 # 'tofolder' is an open folder object
Guido van Rossum6d6a15b1996-07-21 02:18:22 +0000410 def refilemessages(self, list, tofolder, keepsequences=0):
Guido van Rossum56013131994-06-23 12:06:02 +0000411 errors = []
Guido van Rossum6d6a15b1996-07-21 02:18:22 +0000412 refiled = {}
Guido van Rossum56013131994-06-23 12:06:02 +0000413 for n in list:
414 ton = tofolder.getlast() + 1
415 path = self.getmessagefilename(n)
416 topath = tofolder.getmessagefilename(ton)
417 try:
418 os.rename(path, topath)
Guido van Rossum40b2cfb1995-01-02 18:38:23 +0000419 except os.error:
420 # Try copying
421 try:
422 shutil.copy2(path, topath)
423 os.unlink(path)
424 except (IOError, os.error), msg:
425 errors.append(msg)
426 try:
427 os.unlink(topath)
428 except os.error:
429 pass
430 continue
431 tofolder.setlast(ton)
Guido van Rossum6d6a15b1996-07-21 02:18:22 +0000432 refiled[n] = ton
Guido van Rossum56013131994-06-23 12:06:02 +0000433 if refiled:
Guido van Rossum6d6a15b1996-07-21 02:18:22 +0000434 if keepsequences:
435 tofolder._copysequences(self, refiled.items())
436 self.removefromallsequences(refiled.keys())
Guido van Rossum56013131994-06-23 12:06:02 +0000437 if errors:
438 if len(errors) == 1:
439 raise os.error, errors[0]
440 else:
441 raise os.error, ('multiple errors:', errors)
442
Guido van Rossum6d6a15b1996-07-21 02:18:22 +0000443 # Helper for refilemessages() to copy sequences
444 def _copysequences(self, fromfolder, refileditems):
445 fromsequences = fromfolder.getsequences()
446 tosequences = self.getsequences()
447 changed = 0
448 for name, seq in fromsequences.items():
449 try:
450 toseq = tosequences[name]
451 new = 0
452 except:
453 toseq = []
454 new = 1
455 for fromn, ton in refileditems:
456 if fromn in seq:
457 toseq.append(ton)
458 changed = 1
459 if new and toseq:
460 tosequences[name] = toseq
461 if changed:
462 self.putsequences(tosequences)
463
Guido van Rossum40b2cfb1995-01-02 18:38:23 +0000464 # Move one message over a specific destination message,
465 # which may or may not already exist.
466 def movemessage(self, n, tofolder, ton):
467 path = self.getmessagefilename(n)
468 # Open it to check that it exists
469 f = open(path)
470 f.close()
471 del f
472 topath = tofolder.getmessagefilename(ton)
473 backuptopath = tofolder.getmessagefilename(',%d' % ton)
474 try:
475 os.rename(topath, backuptopath)
476 except os.error:
477 pass
478 try:
479 os.rename(path, topath)
480 except os.error:
481 # Try copying
482 ok = 0
483 try:
484 tofolder.setlast(None)
485 shutil.copy2(path, topath)
486 ok = 1
487 finally:
488 if not ok:
489 try:
490 os.unlink(topath)
491 except os.error:
492 pass
493 os.unlink(path)
494 self.removefromallsequences([n])
495
496 # Copy one message over a specific destination message,
497 # which may or may not already exist.
498 def copymessage(self, n, tofolder, ton):
499 path = self.getmessagefilename(n)
500 # Open it to check that it exists
501 f = open(path)
502 f.close()
503 del f
504 topath = tofolder.getmessagefilename(ton)
505 backuptopath = tofolder.getmessagefilename(',%d' % ton)
506 try:
507 os.rename(topath, backuptopath)
508 except os.error:
509 pass
510 ok = 0
511 try:
512 tofolder.setlast(None)
513 shutil.copy2(path, topath)
514 ok = 1
515 finally:
516 if not ok:
517 try:
518 os.unlink(topath)
519 except os.error:
520 pass
521
Guido van Rossum56013131994-06-23 12:06:02 +0000522 # Remove one or more messages from all sequeuces (including last)
Guido van Rossum3508d601996-11-12 04:15:47 +0000523 # -- but not from 'cur'!!!
Guido van Rossum56013131994-06-23 12:06:02 +0000524 def removefromallsequences(self, list):
525 if hasattr(self, 'last') and self.last in list:
526 del self.last
527 sequences = self.getsequences()
528 changed = 0
529 for name, seq in sequences.items():
Guido van Rossum3508d601996-11-12 04:15:47 +0000530 if name == 'cur':
531 continue
Guido van Rossum56013131994-06-23 12:06:02 +0000532 for n in list:
533 if n in seq:
534 seq.remove(n)
535 changed = 1
536 if not seq:
537 del sequences[name]
538 if changed:
Guido van Rossum5f47e571994-07-14 14:01:00 +0000539 self.putsequences(sequences)
Guido van Rossum56013131994-06-23 12:06:02 +0000540
541 # Return the last message number
542 def getlast(self):
543 if not hasattr(self, 'last'):
544 messages = self.listmessages()
545 return self.last
546
547 # Set the last message number
548 def setlast(self, last):
549 if last is None:
550 if hasattr(self, 'last'):
551 del self.last
552 else:
553 self.last = last
554
555class Message(mimetools.Message):
556
557 # Constructor
558 def __init__(self, f, n, fp = None):
559 self.folder = f
560 self.number = n
561 if not fp:
562 path = f.getmessagefilename(n)
563 fp = open(path, 'r')
564 mimetools.Message.__init__(self, fp)
565
566 # String representation
567 def __repr__(self):
568 return 'Message(%s, %s)' % (repr(self.folder), self.number)
569
570 # Return the message's header text as a string. If an
571 # argument is specified, it is used as a filter predicate to
572 # decide which headers to return (its argument is the header
573 # name converted to lower case).
574 def getheadertext(self, pred = None):
575 if not pred:
576 return string.joinfields(self.headers, '')
577 headers = []
578 hit = 0
579 for line in self.headers:
580 if line[0] not in string.whitespace:
581 i = string.find(line, ':')
582 if i > 0:
583 hit = pred(string.lower(line[:i]))
584 if hit: headers.append(line)
585 return string.joinfields(headers, '')
586
587 # Return the message's body text as string. This undoes a
588 # Content-Transfer-Encoding, but does not interpret other MIME
589 # features (e.g. multipart messages). To suppress to
590 # decoding, pass a 0 as argument
591 def getbodytext(self, decode = 1):
592 self.fp.seek(self.startofbody)
593 encoding = self.getencoding()
594 if not decode or encoding in ('7bit', '8bit', 'binary'):
595 return self.fp.read()
596 from StringIO import StringIO
597 output = StringIO()
598 mimetools.decode(self.fp, output, encoding)
599 return output.getvalue()
600
601 # Only for multipart messages: return the message's body as a
602 # list of SubMessage objects. Each submessage object behaves
603 # (almost) as a Message object.
604 def getbodyparts(self):
605 if self.getmaintype() != 'multipart':
606 raise Error, \
607 'Content-Type is not multipart/*'
608 bdry = self.getparam('boundary')
609 if not bdry:
610 raise Error, 'multipart/* without boundary param'
611 self.fp.seek(self.startofbody)
612 mf = multifile.MultiFile(self.fp)
613 mf.push(bdry)
614 parts = []
615 while mf.next():
616 n = str(self.number) + '.' + `1 + len(parts)`
617 part = SubMessage(self.folder, n, mf)
618 parts.append(part)
619 mf.pop()
620 return parts
621
622 # Return body, either a string or a list of messages
623 def getbody(self):
624 if self.getmaintype() == 'multipart':
625 return self.getbodyparts()
626 else:
627 return self.getbodytext()
628
629
630class SubMessage(Message):
631
632 # Constructor
633 def __init__(self, f, n, fp):
634 Message.__init__(self, f, n, fp)
635 if self.getmaintype() == 'multipart':
636 self.body = Message.getbodyparts(self)
637 else:
638 self.body = Message.getbodytext(self)
639 # XXX If this is big, should remember file pointers
640
641 # String representation
642 def __repr__(self):
643 f, n, fp = self.folder, self.number, self.fp
644 return 'SubMessage(%s, %s, %s)' % (f, n, fp)
645
646 def getbodytext(self):
647 if type(self.body) == type(''):
648 return self.body
649
650 def getbodyparts(self):
651 if type(self.body) == type([]):
652 return self.body
653
654 def getbody(self):
655 return self.body
656
657
658# Class implementing sets of integers.
659#
660# This is an efficient representation for sets consisting of several
661# continuous ranges, e.g. 1-100,200-400,402-1000 is represented
662# internally as a list of three pairs: [(1,100), (200,400),
663# (402,1000)]. The internal representation is always kept normalized.
664#
665# The constructor has up to three arguments:
666# - the string used to initialize the set (default ''),
667# - the separator between ranges (default ',')
668# - the separator between begin and end of a range (default '-')
669# The separators may be regular expressions and should be different.
670#
671# The tostring() function yields a string that can be passed to another
672# IntSet constructor; __repr__() is a valid IntSet constructor itself.
673#
674# XXX The default begin/end separator means that negative numbers are
675# not supported very well.
676#
677# XXX There are currently no operations to remove set elements.
678
679class IntSet:
680
681 def __init__(self, data = None, sep = ',', rng = '-'):
682 self.pairs = []
683 self.sep = sep
684 self.rng = rng
685 if data: self.fromstring(data)
686
687 def reset(self):
688 self.pairs = []
689
690 def __cmp__(self, other):
691 return cmp(self.pairs, other.pairs)
692
693 def __hash__(self):
694 return hash(self.pairs)
695
696 def __repr__(self):
697 return 'IntSet(%s, %s, %s)' % (`self.tostring()`,
698 `self.sep`, `self.rng`)
699
700 def normalize(self):
701 self.pairs.sort()
702 i = 1
703 while i < len(self.pairs):
704 alo, ahi = self.pairs[i-1]
705 blo, bhi = self.pairs[i]
706 if ahi >= blo-1:
707 self.pairs[i-1:i+1] = [
708 (alo, max(ahi, bhi))]
709 else:
710 i = i+1
711
712 def tostring(self):
713 s = ''
714 for lo, hi in self.pairs:
715 if lo == hi: t = `lo`
716 else: t = `lo` + self.rng + `hi`
717 if s: s = s + (self.sep + t)
718 else: s = t
719 return s
720
721 def tolist(self):
722 l = []
723 for lo, hi in self.pairs:
724 m = range(lo, hi+1)
725 l = l + m
726 return l
727
728 def fromlist(self, list):
729 for i in list:
730 self.append(i)
731
732 def clone(self):
733 new = IntSet()
734 new.pairs = self.pairs[:]
735 return new
736
737 def min(self):
738 return self.pairs[0][0]
739
740 def max(self):
741 return self.pairs[-1][-1]
742
743 def contains(self, x):
744 for lo, hi in self.pairs:
745 if lo <= x <= hi: return 1
746 return 0
747
748 def append(self, x):
749 for i in range(len(self.pairs)):
750 lo, hi = self.pairs[i]
751 if x < lo: # Need to insert before
752 if x+1 == lo:
753 self.pairs[i] = (x, hi)
754 else:
755 self.pairs.insert(i, (x, x))
756 if i > 0 and x-1 == self.pairs[i-1][1]:
757 # Merge with previous
758 self.pairs[i-1:i+1] = [
759 (self.pairs[i-1][0],
760 self.pairs[i][1])
761 ]
762 return
763 if x <= hi: # Already in set
764 return
765 i = len(self.pairs) - 1
766 if i >= 0:
767 lo, hi = self.pairs[i]
768 if x-1 == hi:
769 self.pairs[i] = lo, x
770 return
771 self.pairs.append((x, x))
772
773 def addpair(self, xlo, xhi):
774 if xlo > xhi: return
775 self.pairs.append((xlo, xhi))
776 self.normalize()
777
778 def fromstring(self, data):
779 import string, regsub
780 new = []
781 for part in regsub.split(data, self.sep):
782 list = []
783 for subp in regsub.split(part, self.rng):
784 s = string.strip(subp)
785 list.append(string.atoi(s))
786 if len(list) == 1:
787 new.append((list[0], list[0]))
788 elif len(list) == 2 and list[0] <= list[1]:
789 new.append((list[0], list[1]))
790 else:
791 raise ValueError, 'bad data passed to IntSet'
792 self.pairs = self.pairs + new
793 self.normalize()
794
795
796# Subroutines to read/write entries in .mh_profile and .mh_sequences
797
798def pickline(file, key, casefold = 1):
799 try:
800 f = open(file, 'r')
801 except IOError:
802 return None
803 pat = key + ':'
804 if casefold:
805 prog = regex.compile(pat, regex.casefold)
806 else:
807 prog = regex.compile(pat)
808 while 1:
809 line = f.readline()
810 if not line: break
Guido van Rossumea8ee1d1995-01-26 00:45:20 +0000811 if prog.match(line) >= 0:
Guido van Rossum56013131994-06-23 12:06:02 +0000812 text = line[len(key)+1:]
813 while 1:
814 line = f.readline()
815 if not line or \
816 line[0] not in string.whitespace:
817 break
818 text = text + line
819 return string.strip(text)
820 return None
821
822def updateline(file, key, value, casefold = 1):
823 try:
824 f = open(file, 'r')
825 lines = f.readlines()
826 f.close()
827 except IOError:
828 lines = []
829 pat = key + ':\(.*\)\n'
830 if casefold:
831 prog = regex.compile(pat, regex.casefold)
832 else:
833 prog = regex.compile(pat)
834 if value is None:
835 newline = None
836 else:
Guido van Rossum508a0921996-05-28 22:59:37 +0000837 newline = '%s: %s\n' % (key, value)
Guido van Rossum56013131994-06-23 12:06:02 +0000838 for i in range(len(lines)):
839 line = lines[i]
840 if prog.match(line) == len(line):
841 if newline is None:
842 del lines[i]
843 else:
844 lines[i] = newline
845 break
846 else:
847 if newline is not None:
848 lines.append(newline)
Guido van Rossum508a0921996-05-28 22:59:37 +0000849 tempfile = file + "~"
Guido van Rossum56013131994-06-23 12:06:02 +0000850 f = open(tempfile, 'w')
851 for line in lines:
852 f.write(line)
853 f.close()
Guido van Rossum508a0921996-05-28 22:59:37 +0000854 os.rename(tempfile, file)
Guido van Rossum56013131994-06-23 12:06:02 +0000855
856
857# Test program
858
859def test():
860 global mh, f
861 os.system('rm -rf $HOME/Mail/@test')
862 mh = MH()
863 def do(s): print s; print eval(s)
864 do('mh.listfolders()')
865 do('mh.listallfolders()')
866 testfolders = ['@test', '@test/test1', '@test/test2',
867 '@test/test1/test11', '@test/test1/test12',
868 '@test/test1/test11/test111']
869 for t in testfolders: do('mh.makefolder(%s)' % `t`)
870 do('mh.listsubfolders(\'@test\')')
871 do('mh.listallsubfolders(\'@test\')')
872 f = mh.openfolder('@test')
873 do('f.listsubfolders()')
874 do('f.listallsubfolders()')
875 do('f.getsequences()')
876 seqs = f.getsequences()
877 seqs['foo'] = IntSet('1-10 12-20', ' ').tolist()
878 print seqs
879 f.putsequences(seqs)
880 do('f.getsequences()')
881 testfolders.reverse()
882 for t in testfolders: do('mh.deletefolder(%s)' % `t`)
883 do('mh.getcontext()')
884 context = mh.getcontext()
885 f = mh.openfolder(context)
886 do('f.listmessages()')
887 do('f.getcurrent()')
888
889
890if __name__ == '__main__':
891 test()