Vega strike Python Modules doc  0.5.1
Documentation of the " Modules " folder of Vega strike
 All Data Structures Namespaces Files Functions Variables
mhlib.py
Go to the documentation of this file.
1 """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
15 mh.setcontext(name) # set name of current folder
16 
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
35 list = f.parsesequence(seq) # parse msgs syntax into list of messages
36 n = f.getlast() # get last message (0 if no messagse)
37 f.setlast(n) # set last message (internal use only)
38 
39 dict = f.getsequences() # dictionary of sequences in folder {name: list}
40 f.putsequences(dict) # write sequences back to folder
41 
42 f.createmessage(n, fp) # add message from file f as number n
43 f.removemessages(list) # remove messages in list from folder
44 f.refilemessages(list, tofolder) # move messages in list to other folder
45 f.movemessage(n, tofolder, ton) # move one message to a given destination
46 f.copymessage(n, tofolder, ton) # copy one message to a given destination
47 
48 m = f.openmessage(n) # new open message object (costs a file descriptor)
49 m is a derived class of mimetools.Message(rfc822.Message), with:
50 s = m.getheadertext() # text of message's headers
51 s = m.getheadertext(pred) # text of message's headers, filtered by pred
52 s = m.getbodytext() # text of message's body, decoded
53 s = m.getbodytext(0) # text of message's body, not decoded
54 """
55 
56 # XXX To do, functionality:
57 # - annotate messages
58 # - send messages
59 #
60 # XXX To do, organization:
61 # - move IntSet to separate file
62 # - move most Message functionality to module mimetools
63 
64 
65 # Customizable defaults
66 
67 MH_PROFILE = '~/.mh_profile'
68 PATH = '~/Mail'
69 MH_SEQUENCES = '.mh_sequences'
70 FOLDER_PROTECT = 0700
71 
72 
73 # Imported modules
74 
75 import os
76 import sys
77 from stat import ST_NLINK
78 import re
79 import mimetools
80 import multifile
81 import shutil
82 from bisect import bisect
83 
84 __all__ = ["MH","Error","Folder","Message"]
85 
86 # Exported constants
87 
88 class Error(Exception):
89  pass
90 
91 
92 class MH:
93  """Class representing a particular collection of folders.
94  Optional constructor arguments are the pathname for the directory
95  containing the collection, and the MH profile to use.
96  If either is omitted or empty a default is used; the default
97  directory is taken from the MH profile if it is specified there."""
98 
99  def __init__(self, path = None, profile = None):
100  """Constructor."""
101  if not profile: profile = MH_PROFILE
102  self.profile = os.path.expanduser(profile)
103  if not path: path = self.getprofile('Path')
104  if not path: path = PATH
105  if not os.path.isabs(path) and path[0] != '~':
106  path = os.path.join('~', path)
107  path = os.path.expanduser(path)
108  if not os.path.isdir(path): raise Error, 'MH() path not found'
109  self.path = path
110 
111  def __repr__(self):
112  """String representation."""
113  return 'MH(%s, %s)' % (`self.path`, `self.profile`)
114 
115  def error(self, msg, *args):
116  """Routine to print an error. May be overridden by a derived class."""
117  sys.stderr.write('MH error: %s\n' % (msg % args))
118 
119  def getprofile(self, key):
120  """Return a profile entry, None if not found."""
121  return pickline(self.profile, key)
122 
123  def getpath(self):
124  """Return the path (the name of the collection's directory)."""
125  return self.path
126 
127  def getcontext(self):
128  """Return the name of the current folder."""
129  context = pickline(os.path.join(self.getpath(), 'context'),
130  'Current-Folder')
131  if not context: context = 'inbox'
132  return context
133 
134  def setcontext(self, context):
135  """Set the name of the current folder."""
136  fn = os.path.join(self.getpath(), 'context')
137  f = open(fn, "w")
138  f.write("Current-Folder: %s\n" % context)
139  f.close()
140 
141  def listfolders(self):
142  """Return the names of the top-level folders."""
143  folders = []
144  path = self.getpath()
145  for name in os.listdir(path):
146  fullname = os.path.join(path, name)
147  if os.path.isdir(fullname):
148  folders.append(name)
149  folders.sort()
150  return folders
151 
152  def listsubfolders(self, name):
153  """Return the names of the subfolders in a given folder
154  (prefixed with the given folder name)."""
155  fullname = os.path.join(self.path, name)
156  # Get the link count so we can avoid listing folders
157  # that have no subfolders.
158  st = os.stat(fullname)
159  nlinks = st[ST_NLINK]
160  if nlinks <= 2:
161  return []
162  subfolders = []
163  subnames = os.listdir(fullname)
164  for subname in subnames:
165  fullsubname = os.path.join(fullname, subname)
166  if os.path.isdir(fullsubname):
167  name_subname = os.path.join(name, subname)
168  subfolders.append(name_subname)
169  # Stop looking for subfolders when
170  # we've seen them all
171  nlinks = nlinks - 1
172  if nlinks <= 2:
173  break
174  subfolders.sort()
175  return subfolders
176 
177  def listallfolders(self):
178  """Return the names of all folders and subfolders, recursively."""
179  return self.listallsubfolders('')
180 
181  def listallsubfolders(self, name):
182  """Return the names of subfolders in a given folder, recursively."""
183  fullname = os.path.join(self.path, name)
184  # Get the link count so we can avoid listing folders
185  # that have no subfolders.
186  st = os.stat(fullname)
187  nlinks = st[ST_NLINK]
188  if nlinks <= 2:
189  return []
190  subfolders = []
191  subnames = os.listdir(fullname)
192  for subname in subnames:
193  if subname[0] == ',' or isnumeric(subname): continue
194  fullsubname = os.path.join(fullname, subname)
195  if os.path.isdir(fullsubname):
196  name_subname = os.path.join(name, subname)
197  subfolders.append(name_subname)
198  if not os.path.islink(fullsubname):
199  subsubfolders = self.listallsubfolders(
200  name_subname)
201  subfolders = subfolders + subsubfolders
202  # Stop looking for subfolders when
203  # we've seen them all
204  nlinks = nlinks - 1
205  if nlinks <= 2:
206  break
207  subfolders.sort()
208  return subfolders
209 
210  def openfolder(self, name):
211  """Return a new Folder object for the named folder."""
212  return Folder(self, name)
213 
214  def makefolder(self, name):
215  """Create a new folder (or raise os.error if it cannot be created)."""
216  protect = pickline(self.profile, 'Folder-Protect')
217  if protect and isnumeric(protect):
218  mode = int(protect, 8)
219  else:
220  mode = FOLDER_PROTECT
221  os.mkdir(os.path.join(self.getpath(), name), mode)
222 
223  def deletefolder(self, name):
224  """Delete a folder. This removes files in the folder but not
225  subdirectories. Raise os.error if deleting the folder itself fails."""
226  fullname = os.path.join(self.getpath(), name)
227  for subname in os.listdir(fullname):
228  fullsubname = os.path.join(fullname, subname)
229  try:
230  os.unlink(fullsubname)
231  except os.error:
232  self.error('%s not deleted, continuing...' %
233  fullsubname)
234  os.rmdir(fullname)
235 
236 
237 numericprog = re.compile('^[1-9][0-9]*$')
238 def isnumeric(str):
239  return numericprog.match(str) is not None
240 
241 class Folder:
242  """Class representing a particular folder."""
243 
244  def __init__(self, mh, name):
245  """Constructor."""
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  def __repr__(self):
252  """String representation."""
253  return 'Folder(%s, %s)' % (`self.mh`, `self.name`)
254 
255  def error(self, *args):
256  """Error message handler."""
257  apply(self.mh.error, args)
258 
259  def getfullname(self):
260  """Return the full pathname of the folder."""
261  return os.path.join(self.mh.path, self.name)
262 
264  """Return the full pathname of the folder's sequences file."""
265  return os.path.join(self.getfullname(), MH_SEQUENCES)
266 
267  def getmessagefilename(self, n):
268  """Return the full pathname of a message in the folder."""
269  return os.path.join(self.getfullname(), str(n))
270 
271  def listsubfolders(self):
272  """Return list of direct subfolders."""
273  return self.mh.listsubfolders(self.name)
274 
275  def listallsubfolders(self):
276  """Return list of all subfolders."""
277  return self.mh.listallsubfolders(self.name)
278 
279  def listmessages(self):
280  """Return the list of messages currently present in the folder.
281  As a side effect, set self.last to the last message (or 0)."""
282  messages = []
283  match = numericprog.match
284  append = messages.append
285  for name in os.listdir(self.getfullname()):
286  if match(name):
287  append(name)
288  messages = map(int, messages)
289  messages.sort()
290  if messages:
291  self.last = messages[-1]
292  else:
293  self.last = 0
294  return messages
295 
296  def getsequences(self):
297  """Return the set of sequences for the folder."""
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 = line.split(':')
308  if len(fields) != 2:
309  self.error('bad sequence in %s: %s' %
310  (fullname, line.strip()))
311  key = fields[0].strip()
312  value = IntSet(fields[1].strip(), ' ').tolist()
313  sequences[key] = value
314  return sequences
315 
316  def putsequences(self, sequences):
317  """Write the set of sequences back to the folder."""
318  fullname = self.getsequencesfilename()
319  f = None
320  for key in sequences.keys():
321  s = IntSet('', ' ')
322  s.fromlist(sequences[key])
323  if not f: f = open(fullname, 'w')
324  f.write('%s: %s\n' % (key, s.tostring()))
325  if not f:
326  try:
327  os.unlink(fullname)
328  except os.error:
329  pass
330  else:
331  f.close()
332 
333  def getcurrent(self):
334  """Return the current message. Raise Error when there is none."""
335  seqs = self.getsequences()
336  try:
337  return max(seqs['cur'])
338  except (ValueError, KeyError):
339  raise Error, "no cur message"
340 
341  def setcurrent(self, n):
342  """Set the current message."""
343  updateline(self.getsequencesfilename(), 'cur', str(n), 0)
344 
345  def parsesequence(self, seq):
346  """Parse an MH sequence specification into a message list.
347  Attempt to mimic mh-sequence(5) as close as possible.
348  Also attempt to mimic observed behavior regarding which
349  conditions cause which error messages."""
350  # XXX Still not complete (see mh-format(5)).
351  # Missing are:
352  # - 'prev', 'next' as count
353  # - Sequence-Negation option
354  all = self.listmessages()
355  # Observed behavior: test for empty folder is done first
356  if not all:
357  raise Error, "no messages in %s" % self.name
358  # Common case first: all is frequently the default
359  if seq == 'all':
360  return all
361  # Test for X:Y before X-Y because 'seq:-n' matches both
362  i = seq.find(':')
363  if i >= 0:
364  head, dir, tail = seq[:i], '', seq[i+1:]
365  if tail[:1] in '-+':
366  dir, tail = tail[:1], tail[1:]
367  if not isnumeric(tail):
368  raise Error, "bad message list %s" % seq
369  try:
370  count = int(tail)
371  except (ValueError, OverflowError):
372  # Can't use sys.maxint because of i+count below
373  count = len(all)
374  try:
375  anchor = self._parseindex(head, all)
376  except Error, msg:
377  seqs = self.getsequences()
378  if not seqs.has_key(head):
379  if not msg:
380  msg = "bad message list %s" % seq
381  raise Error, msg, sys.exc_info()[2]
382  msgs = seqs[head]
383  if not msgs:
384  raise Error, "sequence %s empty" % head
385  if dir == '-':
386  return msgs[-count:]
387  else:
388  return msgs[:count]
389  else:
390  if not dir:
391  if head in ('prev', 'last'):
392  dir = '-'
393  if dir == '-':
394  i = bisect(all, anchor)
395  return all[max(0, i-count):i]
396  else:
397  i = bisect(all, anchor-1)
398  return all[i:i+count]
399  # Test for X-Y next
400  i = seq.find('-')
401  if i >= 0:
402  begin = self._parseindex(seq[:i], all)
403  end = self._parseindex(seq[i+1:], all)
404  i = bisect(all, begin-1)
405  j = bisect(all, end)
406  r = all[i:j]
407  if not r:
408  raise Error, "bad message list %s" % seq
409  return r
410  # Neither X:Y nor X-Y; must be a number or a (pseudo-)sequence
411  try:
412  n = self._parseindex(seq, all)
413  except Error, msg:
414  seqs = self.getsequences()
415  if not seqs.has_key(seq):
416  if not msg:
417  msg = "bad message list %s" % seq
418  raise Error, msg
419  return seqs[seq]
420  else:
421  if n not in all:
422  if isnumeric(seq):
423  raise Error, "message %d doesn't exist" % n
424  else:
425  raise Error, "no %s message" % seq
426  else:
427  return [n]
428 
429  def _parseindex(self, seq, all):
430  """Internal: parse a message number (or cur, first, etc.)."""
431  if isnumeric(seq):
432  try:
433  return int(seq)
434  except (OverflowError, ValueError):
435  return sys.maxint
436  if seq in ('cur', '.'):
437  return self.getcurrent()
438  if seq == 'first':
439  return all[0]
440  if seq == 'last':
441  return all[-1]
442  if seq == 'next':
443  n = self.getcurrent()
444  i = bisect(all, n)
445  try:
446  return all[i]
447  except IndexError:
448  raise Error, "no next message"
449  if seq == 'prev':
450  n = self.getcurrent()
451  i = bisect(all, n-1)
452  if i == 0:
453  raise Error, "no prev message"
454  try:
455  return all[i-1]
456  except IndexError:
457  raise Error, "no prev message"
458  raise Error, None
459 
460  def openmessage(self, n):
461  """Open a message -- returns a Message object."""
462  return Message(self, n)
463 
464  def removemessages(self, list):
465  """Remove one or more messages -- may raise os.error."""
466  errors = []
467  deleted = []
468  for n in list:
469  path = self.getmessagefilename(n)
470  commapath = self.getmessagefilename(',' + str(n))
471  try:
472  os.unlink(commapath)
473  except os.error:
474  pass
475  try:
476  os.rename(path, commapath)
477  except os.error, msg:
478  errors.append(msg)
479  else:
480  deleted.append(n)
481  if deleted:
482  self.removefromallsequences(deleted)
483  if errors:
484  if len(errors) == 1:
485  raise os.error, errors[0]
486  else:
487  raise os.error, ('multiple errors:', errors)
488 
489  def refilemessages(self, list, tofolder, keepsequences=0):
490  """Refile one or more messages -- may raise os.error.
491  'tofolder' is an open folder object."""
492  errors = []
493  refiled = {}
494  for n in list:
495  ton = tofolder.getlast() + 1
496  path = self.getmessagefilename(n)
497  topath = tofolder.getmessagefilename(ton)
498  try:
499  os.rename(path, topath)
500  except os.error:
501  # Try copying
502  try:
503  shutil.copy2(path, topath)
504  os.unlink(path)
505  except (IOError, os.error), msg:
506  errors.append(msg)
507  try:
508  os.unlink(topath)
509  except os.error:
510  pass
511  continue
512  tofolder.setlast(ton)
513  refiled[n] = ton
514  if refiled:
515  if keepsequences:
516  tofolder._copysequences(self, refiled.items())
517  self.removefromallsequences(refiled.keys())
518  if errors:
519  if len(errors) == 1:
520  raise os.error, errors[0]
521  else:
522  raise os.error, ('multiple errors:', errors)
523 
524  def _copysequences(self, fromfolder, refileditems):
525  """Helper for refilemessages() to copy sequences."""
526  fromsequences = fromfolder.getsequences()
527  tosequences = self.getsequences()
528  changed = 0
529  for name, seq in fromsequences.items():
530  try:
531  toseq = tosequences[name]
532  new = 0
533  except KeyError:
534  toseq = []
535  new = 1
536  for fromn, ton in refileditems:
537  if fromn in seq:
538  toseq.append(ton)
539  changed = 1
540  if new and toseq:
541  tosequences[name] = toseq
542  if changed:
543  self.putsequences(tosequences)
544 
545  def movemessage(self, n, tofolder, ton):
546  """Move one message over a specific destination message,
547  which may or may not already exist."""
548  path = self.getmessagefilename(n)
549  # Open it to check that it exists
550  f = open(path)
551  f.close()
552  del f
553  topath = tofolder.getmessagefilename(ton)
554  backuptopath = tofolder.getmessagefilename(',%d' % ton)
555  try:
556  os.rename(topath, backuptopath)
557  except os.error:
558  pass
559  try:
560  os.rename(path, topath)
561  except os.error:
562  # Try copying
563  ok = 0
564  try:
565  tofolder.setlast(None)
566  shutil.copy2(path, topath)
567  ok = 1
568  finally:
569  if not ok:
570  try:
571  os.unlink(topath)
572  except os.error:
573  pass
574  os.unlink(path)
575  self.removefromallsequences([n])
576 
577  def copymessage(self, n, tofolder, ton):
578  """Copy one message over a specific destination message,
579  which may or may not already exist."""
580  path = self.getmessagefilename(n)
581  # Open it to check that it exists
582  f = open(path)
583  f.close()
584  del f
585  topath = tofolder.getmessagefilename(ton)
586  backuptopath = tofolder.getmessagefilename(',%d' % ton)
587  try:
588  os.rename(topath, backuptopath)
589  except os.error:
590  pass
591  ok = 0
592  try:
593  tofolder.setlast(None)
594  shutil.copy2(path, topath)
595  ok = 1
596  finally:
597  if not ok:
598  try:
599  os.unlink(topath)
600  except os.error:
601  pass
602 
603  def createmessage(self, n, txt):
604  """Create a message, with text from the open file txt."""
605  path = self.getmessagefilename(n)
606  backuppath = self.getmessagefilename(',%d' % n)
607  try:
608  os.rename(path, backuppath)
609  except os.error:
610  pass
611  ok = 0
612  BUFSIZE = 16*1024
613  try:
614  f = open(path, "w")
615  while 1:
616  buf = txt.read(BUFSIZE)
617  if not buf:
618  break
619  f.write(buf)
620  f.close()
621  ok = 1
622  finally:
623  if not ok:
624  try:
625  os.unlink(path)
626  except os.error:
627  pass
628 
629  def removefromallsequences(self, list):
630  """Remove one or more messages from all sequences (including last)
631  -- but not from 'cur'!!!"""
632  if hasattr(self, 'last') and self.last in list:
633  del self.last
634  sequences = self.getsequences()
635  changed = 0
636  for name, seq in sequences.items():
637  if name == 'cur':
638  continue
639  for n in list:
640  if n in seq:
641  seq.remove(n)
642  changed = 1
643  if not seq:
644  del sequences[name]
645  if changed:
646  self.putsequences(sequences)
647 
648  def getlast(self):
649  """Return the last message number."""
650  if not hasattr(self, 'last'):
651  self.listmessages() # Set self.last
652  return self.last
653 
654  def setlast(self, last):
655  """Set the last message number."""
656  if last is None:
657  if hasattr(self, 'last'):
658  del self.last
659  else:
660  self.last = last
661 
663 
664  def __init__(self, f, n, fp = None):
665  """Constructor."""
666  self.folder = f
667  self.number = n
668  if not fp:
669  path = f.getmessagefilename(n)
670  fp = open(path, 'r')
672 
673  def __repr__(self):
674  """String representation."""
675  return 'Message(%s, %s)' % (repr(self.folder), self.number)
676 
677  def getheadertext(self, pred = None):
678  """Return the message's header text as a string. If an
679  argument is specified, it is used as a filter predicate to
680  decide which headers to return (its argument is the header
681  name converted to lower case)."""
682  if not pred:
683  return ''.join(self.headers)
684  headers = []
685  hit = 0
686  for line in self.headers:
687  if not line[0].isspace():
688  i = line.find(':')
689  if i > 0:
690  hit = pred(line[:i].lower())
691  if hit: headers.append(line)
692  return ''.join(headers)
693 
694  def getbodytext(self, decode = 1):
695  """Return the message's body text as string. This undoes a
696  Content-Transfer-Encoding, but does not interpret other MIME
697  features (e.g. multipart messages). To suppress decoding,
698  pass 0 as an argument."""
699  self.fp.seek(self.startofbody)
700  encoding = self.getencoding()
701  if not decode or encoding in ('', '7bit', '8bit', 'binary'):
702  return self.fp.read()
703  from StringIO import StringIO
704  output = StringIO()
705  mimetools.decode(self.fp, output, encoding)
706  return output.getvalue()
707 
708  def getbodyparts(self):
709  """Only for multipart messages: return the message's body as a
710  list of SubMessage objects. Each submessage object behaves
711  (almost) as a Message object."""
712  if self.getmaintype() != 'multipart':
713  raise Error, 'Content-Type is not multipart/*'
714  bdry = self.getparam('boundary')
715  if not bdry:
716  raise Error, 'multipart/* without boundary param'
717  self.fp.seek(self.startofbody)
718  mf = multifile.MultiFile(self.fp)
719  mf.push(bdry)
720  parts = []
721  while mf.next():
722  n = str(self.number) + '.' + `1 + len(parts)`
723  part = SubMessage(self.folder, n, mf)
724  parts.append(part)
725  mf.pop()
726  return parts
727 
728  def getbody(self):
729  """Return body, either a string or a list of messages."""
730  if self.getmaintype() == 'multipart':
731  return self.getbodyparts()
732  else:
733  return self.getbodytext()
734 
735 
737 
738  def __init__(self, f, n, fp):
739  """Constructor."""
740  Message.__init__(self, f, n, fp)
741  if self.getmaintype() == 'multipart':
742  self.body = Message.getbodyparts(self)
743  else:
744  self.body = Message.getbodytext(self)
745  self.bodyencoded = Message.getbodytext(self, decode=0)
746  # XXX If this is big, should remember file pointers
747 
748  def __repr__(self):
749  """String representation."""
750  f, n, fp = self.folder, self.number, self.fp
751  return 'SubMessage(%s, %s, %s)' % (f, n, fp)
752 
753  def getbodytext(self, decode = 1):
754  if not decode:
755  return self.bodyencoded
756  if type(self.body) == type(''):
757  return self.body
758 
759  def getbodyparts(self):
760  if type(self.body) == type([]):
761  return self.body
762 
763  def getbody(self):
764  return self.body
765 
766 
767 class IntSet:
768  """Class implementing sets of integers.
769 
770  This is an efficient representation for sets consisting of several
771  continuous ranges, e.g. 1-100,200-400,402-1000 is represented
772  internally as a list of three pairs: [(1,100), (200,400),
773  (402,1000)]. The internal representation is always kept normalized.
774 
775  The constructor has up to three arguments:
776  - the string used to initialize the set (default ''),
777  - the separator between ranges (default ',')
778  - the separator between begin and end of a range (default '-')
779  The separators must be strings (not regexprs) and should be different.
780 
781  The tostring() function yields a string that can be passed to another
782  IntSet constructor; __repr__() is a valid IntSet constructor itself.
783  """
784 
785  # XXX The default begin/end separator means that negative numbers are
786  # not supported very well.
787  #
788  # XXX There are currently no operations to remove set elements.
789 
790  def __init__(self, data = None, sep = ',', rng = '-'):
791  self.pairs = []
792  self.sep = sep
793  self.rng = rng
794  if data: self.fromstring(data)
795 
796  def reset(self):
797  self.pairs = []
798 
799  def __cmp__(self, other):
800  return cmp(self.pairs, other.pairs)
801 
802  def __hash__(self):
803  return hash(self.pairs)
804 
805  def __repr__(self):
806  return 'IntSet(%s, %s, %s)' % (`self.tostring()`,
807  `self.sep`, `self.rng`)
808 
809  def normalize(self):
810  self.pairs.sort()
811  i = 1
812  while i < len(self.pairs):
813  alo, ahi = self.pairs[i-1]
814  blo, bhi = self.pairs[i]
815  if ahi >= blo-1:
816  self.pairs[i-1:i+1] = [(alo, max(ahi, bhi))]
817  else:
818  i = i+1
819 
820  def tostring(self):
821  s = ''
822  for lo, hi in self.pairs:
823  if lo == hi: t = `lo`
824  else: t = `lo` + self.rng + `hi`
825  if s: s = s + (self.sep + t)
826  else: s = t
827  return s
828 
829  def tolist(self):
830  l = []
831  for lo, hi in self.pairs:
832  m = range(lo, hi+1)
833  l = l + m
834  return l
835 
836  def fromlist(self, list):
837  for i in list:
838  self.append(i)
839 
840  def clone(self):
841  new = IntSet()
842  new.pairs = self.pairs[:]
843  return new
844 
845  def min(self):
846  return self.pairs[0][0]
847 
848  def max(self):
849  return self.pairs[-1][-1]
850 
851  def contains(self, x):
852  for lo, hi in self.pairs:
853  if lo <= x <= hi: return 1
854  return 0
855 
856  def append(self, x):
857  for i in range(len(self.pairs)):
858  lo, hi = self.pairs[i]
859  if x < lo: # Need to insert before
860  if x+1 == lo:
861  self.pairs[i] = (x, hi)
862  else:
863  self.pairs.insert(i, (x, x))
864  if i > 0 and x-1 == self.pairs[i-1][1]:
865  # Merge with previous
866  self.pairs[i-1:i+1] = [
867  (self.pairs[i-1][0],
868  self.pairs[i][1])
869  ]
870  return
871  if x <= hi: # Already in set
872  return
873  i = len(self.pairs) - 1
874  if i >= 0:
875  lo, hi = self.pairs[i]
876  if x-1 == hi:
877  self.pairs[i] = lo, x
878  return
879  self.pairs.append((x, x))
880 
881  def addpair(self, xlo, xhi):
882  if xlo > xhi: return
883  self.pairs.append((xlo, xhi))
884  self.normalize()
885 
886  def fromstring(self, data):
887  new = []
888  for part in data.split(self.sep):
889  list = []
890  for subp in part.split(self.rng):
891  s = subp.strip()
892  list.append(int(s))
893  if len(list) == 1:
894  new.append((list[0], list[0]))
895  elif len(list) == 2 and list[0] <= list[1]:
896  new.append((list[0], list[1]))
897  else:
898  raise ValueError, 'bad data passed to IntSet'
899  self.pairs = self.pairs + new
900  self.normalize()
901 
902 
903 # Subroutines to read/write entries in .mh_profile and .mh_sequences
904 
905 def pickline(file, key, casefold = 1):
906  try:
907  f = open(file, 'r')
908  except IOError:
909  return None
910  pat = re.escape(key) + ':'
911  prog = re.compile(pat, casefold and re.IGNORECASE)
912  while 1:
913  line = f.readline()
914  if not line: break
915  if prog.match(line):
916  text = line[len(key)+1:]
917  while 1:
918  line = f.readline()
919  if not line or not line[0].isspace():
920  break
921  text = text + line
922  return text.strip()
923  return None
924 
925 def updateline(file, key, value, casefold = 1):
926  try:
927  f = open(file, 'r')
928  lines = f.readlines()
929  f.close()
930  except IOError:
931  lines = []
932  pat = re.escape(key) + ':(.*)\n'
933  prog = re.compile(pat, casefold and re.IGNORECASE)
934  if value is None:
935  newline = None
936  else:
937  newline = '%s: %s\n' % (key, value)
938  for i in range(len(lines)):
939  line = lines[i]
940  if prog.match(line):
941  if newline is None:
942  del lines[i]
943  else:
944  lines[i] = newline
945  break
946  else:
947  if newline is not None:
948  lines.append(newline)
949  tempfile = file + "~"
950  f = open(tempfile, 'w')
951  for line in lines:
952  f.write(line)
953  f.close()
954  os.rename(tempfile, file)
955 
956 
957 # Test program
958 
959 def test():
960  global mh, f
961  os.system('rm -rf $HOME/Mail/@test')
962  mh = MH()
963  def do(s): print s; print eval(s)
964  do('mh.listfolders()')
965  do('mh.listallfolders()')
966  testfolders = ['@test', '@test/test1', '@test/test2',
967  '@test/test1/test11', '@test/test1/test12',
968  '@test/test1/test11/test111']
969  for t in testfolders: do('mh.makefolder(%s)' % `t`)
970  do('mh.listsubfolders(\'@test\')')
971  do('mh.listallsubfolders(\'@test\')')
972  f = mh.openfolder('@test')
973  do('f.listsubfolders()')
974  do('f.listallsubfolders()')
975  do('f.getsequences()')
976  seqs = f.getsequences()
977  seqs['foo'] = IntSet('1-10 12-20', ' ').tolist()
978  print seqs
979  f.putsequences(seqs)
980  do('f.getsequences()')
981  testfolders.reverse()
982  for t in testfolders: do('mh.deletefolder(%s)' % `t`)
983  do('mh.getcontext()')
984  context = mh.getcontext()
985  f = mh.openfolder(context)
986  do('f.getcurrent()')
987  for seq in ['first', 'last', 'cur', '.', 'prev', 'next',
988  'first:3', 'last:3', 'cur:3', 'cur:-3',
989  'prev:3', 'next:3',
990  '1:3', '1:-3', '100:3', '100:-3', '10000:3', '10000:-3',
991  'all']:
992  try:
993  do('f.parsesequence(%s)' % `seq`)
994  except Error, msg:
995  print "Error:", msg
996  stuff = os.popen("pick %s 2>/dev/null" % `seq`).read()
997  list = map(int, stuff.split())
998  print list, "<-- pick"
999  do('f.listmessages()')
1000 
1001 
1002 if __name__ == '__main__':
1003  test()