blob: b7acf553999ef82f0adb159dfe183dd97f4c01f3 [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 Rossum7cfd31e1997-04-16 02:45:08 +000081from bisect import bisect
Guido van Rossum56013131994-06-23 12:06:02 +000082
83
84# Exported constants
85
86Error = 'mhlib.Error'
87
88
89# Class representing a particular collection of folders.
90# Optional constructor arguments are the pathname for the directory
91# containing the collection, and the MH profile to use.
92# If either is omitted or empty a default is used; the default
93# directory is taken from the MH profile if it is specified there.
94
95class MH:
96
97 # Constructor
98 def __init__(self, path = None, profile = None):
99 if not profile: profile = MH_PROFILE
100 self.profile = os.path.expanduser(profile)
101 if not path: path = self.getprofile('Path')
102 if not path: path = PATH
103 if not os.path.isabs(path) and path[0] != '~':
104 path = os.path.join('~', path)
105 path = os.path.expanduser(path)
106 if not os.path.isdir(path): raise Error, 'MH() path not found'
107 self.path = path
108
109 # String representation
110 def __repr__(self):
111 return 'MH(%s, %s)' % (`self.path`, `self.profile`)
112
113 # Routine to print an error. May be overridden by a derived class
114 def error(self, msg, *args):
Guido van Rossum508a0921996-05-28 22:59:37 +0000115 sys.stderr.write('MH error: %s\n' % (msg % args))
Guido van Rossum56013131994-06-23 12:06:02 +0000116
117 # Return a profile entry, None if not found
118 def getprofile(self, key):
119 return pickline(self.profile, key)
120
121 # Return the path (the name of the collection's directory)
122 def getpath(self):
123 return self.path
124
125 # Return the name of the current folder
126 def getcontext(self):
127 context = pickline(os.path.join(self.getpath(), 'context'),
128 'Current-Folder')
129 if not context: context = 'inbox'
130 return context
131
Guido van Rossum508a0921996-05-28 22:59:37 +0000132 # Set the name of the current folder
133 def setcontext(self, context):
134 fn = os.path.join(self.getpath(), 'context')
135 f = open(fn, "w")
136 f.write("Current-Folder: %s\n" % context)
137 f.close()
138
Guido van Rossum56013131994-06-23 12:06:02 +0000139 # Return the names of the top-level folders
140 def listfolders(self):
141 folders = []
142 path = self.getpath()
143 for name in os.listdir(path):
Guido van Rossum56013131994-06-23 12:06:02 +0000144 fullname = os.path.join(path, name)
145 if os.path.isdir(fullname):
146 folders.append(name)
147 folders.sort()
148 return folders
149
150 # Return the names of the subfolders in a given folder
151 # (prefixed with the given folder name)
152 def listsubfolders(self, name):
153 fullname = os.path.join(self.path, name)
154 # Get the link count so we can avoid listing folders
155 # that have no subfolders.
156 st = os.stat(fullname)
157 nlinks = st[ST_NLINK]
158 if nlinks <= 2:
159 return []
160 subfolders = []
161 subnames = os.listdir(fullname)
162 for subname in subnames:
Guido van Rossum56013131994-06-23 12:06:02 +0000163 fullsubname = os.path.join(fullname, subname)
164 if os.path.isdir(fullsubname):
165 name_subname = os.path.join(name, subname)
166 subfolders.append(name_subname)
167 # Stop looking for subfolders when
168 # we've seen them all
169 nlinks = nlinks - 1
170 if nlinks <= 2:
171 break
172 subfolders.sort()
173 return subfolders
174
175 # Return the names of all folders, including subfolders, recursively
176 def listallfolders(self):
177 return self.listallsubfolders('')
178
179 # Return the names of subfolders in a given folder, recursively
180 def listallsubfolders(self, name):
181 fullname = os.path.join(self.path, name)
182 # Get the link count so we can avoid listing folders
183 # that have no subfolders.
184 st = os.stat(fullname)
185 nlinks = st[ST_NLINK]
186 if nlinks <= 2:
187 return []
188 subfolders = []
189 subnames = os.listdir(fullname)
190 for subname in subnames:
Guido van Rossum56013131994-06-23 12:06:02 +0000191 if subname[0] == ',' or isnumeric(subname): continue
192 fullsubname = os.path.join(fullname, subname)
193 if os.path.isdir(fullsubname):
194 name_subname = os.path.join(name, subname)
195 subfolders.append(name_subname)
196 if not os.path.islink(fullsubname):
197 subsubfolders = self.listallsubfolders(
198 name_subname)
199 subfolders = subfolders + subsubfolders
200 # Stop looking for subfolders when
201 # we've seen them all
202 nlinks = nlinks - 1
203 if nlinks <= 2:
204 break
205 subfolders.sort()
206 return subfolders
207
208 # Return a new Folder object for the named folder
209 def openfolder(self, name):
210 return Folder(self, name)
211
212 # Create a new folder. This raises os.error if the folder
213 # cannot be created
214 def makefolder(self, name):
215 protect = pickline(self.profile, 'Folder-Protect')
216 if protect and isnumeric(protect):
Guido van Rossum508a0921996-05-28 22:59:37 +0000217 mode = string.atoi(protect, 8)
Guido van Rossum56013131994-06-23 12:06:02 +0000218 else:
219 mode = FOLDER_PROTECT
220 os.mkdir(os.path.join(self.getpath(), name), mode)
221
222 # Delete a folder. This removes files in the folder but not
223 # subdirectories. If deleting the folder itself fails it
224 # raises os.error
225 def deletefolder(self, name):
226 fullname = os.path.join(self.getpath(), name)
227 for subname in os.listdir(fullname):
Guido van Rossum56013131994-06-23 12:06:02 +0000228 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# Class representing a particular folder
238
Guido van Rossum659a3b51997-04-02 01:18:30 +0000239numericprog = regex.compile('^[1-9][0-9]*$')
Guido van Rossum56013131994-06-23 12:06:02 +0000240def isnumeric(str):
Guido van Rossum659a3b51997-04-02 01:18:30 +0000241 return numericprog.match(str) >= 0
Guido van Rossum56013131994-06-23 12:06:02 +0000242
243class Folder:
244
245 # Constructor
246 def __init__(self, mh, name):
247 self.mh = mh
248 self.name = name
249 if not os.path.isdir(self.getfullname()):
250 raise Error, 'no folder %s' % name
251
252 # String representation
253 def __repr__(self):
254 return 'Folder(%s, %s)' % (`self.mh`, `self.name`)
255
256 # Error message handler
257 def error(self, *args):
258 apply(self.mh.error, args)
259
260 # Return the full pathname of the folder
261 def getfullname(self):
262 return os.path.join(self.mh.path, self.name)
263
264 # Return the full pathname of the folder's sequences file
265 def getsequencesfilename(self):
266 return os.path.join(self.getfullname(), MH_SEQUENCES)
267
268 # Return the full pathname of a message in the folder
269 def getmessagefilename(self, n):
270 return os.path.join(self.getfullname(), str(n))
271
272 # Return list of direct subfolders
273 def listsubfolders(self):
274 return self.mh.listsubfolders(self.name)
275
276 # Return list of all subfolders
277 def listallsubfolders(self):
278 return self.mh.listallsubfolders(self.name)
279
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 def listmessages(self):
283 messages = []
Guido van Rossum659a3b51997-04-02 01:18:30 +0000284 match = numericprog.match
285 append = messages.append
Guido van Rossum56013131994-06-23 12:06:02 +0000286 for name in os.listdir(self.getfullname()):
Guido van Rossum659a3b51997-04-02 01:18:30 +0000287 if match(name) >= 0:
288 append(name)
289 messages = map(string.atoi, messages)
Guido van Rossum56013131994-06-23 12:06:02 +0000290 messages.sort()
291 if messages:
Guido van Rossum659a3b51997-04-02 01:18:30 +0000292 self.last = messages[-1]
Guido van Rossum56013131994-06-23 12:06:02 +0000293 else:
294 self.last = 0
295 return messages
296
297 # Return the set of sequences for the folder
298 def getsequences(self):
299 sequences = {}
300 fullname = self.getsequencesfilename()
301 try:
302 f = open(fullname, 'r')
303 except IOError:
304 return sequences
305 while 1:
306 line = f.readline()
307 if not line: break
308 fields = string.splitfields(line, ':')
309 if len(fields) <> 2:
310 self.error('bad sequence in %s: %s' %
311 (fullname, string.strip(line)))
312 key = string.strip(fields[0])
313 value = IntSet(string.strip(fields[1]), ' ').tolist()
314 sequences[key] = value
315 return sequences
316
317 # Write the set of sequences back to the folder
318 def putsequences(self, sequences):
319 fullname = self.getsequencesfilename()
Guido van Rossum85347411994-09-09 11:10:15 +0000320 f = None
Guido van Rossum56013131994-06-23 12:06:02 +0000321 for key in sequences.keys():
322 s = IntSet('', ' ')
323 s.fromlist(sequences[key])
Guido van Rossum85347411994-09-09 11:10:15 +0000324 if not f: f = open(fullname, 'w')
Guido van Rossum56013131994-06-23 12:06:02 +0000325 f.write('%s: %s\n' % (key, s.tostring()))
Guido van Rossum85347411994-09-09 11:10:15 +0000326 if not f:
327 try:
328 os.unlink(fullname)
329 except os.error:
330 pass
331 else:
332 f.close()
Guido van Rossum56013131994-06-23 12:06:02 +0000333
334 # Return the current message. Raise KeyError when there is none
335 def getcurrent(self):
Guido van Rossum7cfd31e1997-04-16 02:45:08 +0000336 seqs = self.getsequences()
337 try:
338 return max(seqs['cur'])
339 except (ValueError, KeyError):
340 raise Error, "no cur message"
Guido van Rossum56013131994-06-23 12:06:02 +0000341
342 # Set the current message
343 def setcurrent(self, n):
344 updateline(self.getsequencesfilename(), 'cur', str(n), 0)
345
Guido van Rossum508a0921996-05-28 22:59:37 +0000346 # Parse an MH sequence specification into a message list.
Guido van Rossum7cfd31e1997-04-16 02:45:08 +0000347 # 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
Guido van Rossum508a0921996-05-28 22:59:37 +0000350 def parsesequence(self, seq):
Guido van Rossum7cfd31e1997-04-16 02:45:08 +0000351 # XXX Still not complete (see mh-format(5)).
352 # Missing are:
353 # - 'prev', 'next' as count
354 # - Sequence-Negation option
355 all = self.listmessages()
356 # Observed behavior: test for empty folder is done first
357 if not all:
358 raise Error, "no messages in %s" % self.name
359 # Common case first: all is frequently the default
360 if seq == 'all':
361 return all
362 # Test for X:Y before X-Y because 'seq:-n' matches both
363 i = string.find(seq, ':')
364 if i >= 0:
365 head, dir, tail = seq[:i], '', seq[i+1:]
366 if tail[:1] in '-+':
367 dir, tail = tail[:1], tail[1:]
368 if not isnumeric(tail):
369 raise Error, "bad message list %s" % seq
370 try:
371 count = string.atoi(tail)
372 except (ValueError, OverflowError):
373 # Can't use sys.maxint because of i+count below
374 count = len(all)
375 try:
376 anchor = self._parseindex(head, all)
377 except Error, msg:
378 seqs = self.getsequences()
379 if not seqs.has_key(head):
380 if not msg:
381 msg = "bad message list %s" % seq
382 raise Error, msg, sys.exc_traceback
383 msgs = seqs[head]
384 if not msgs:
385 raise Error, "sequence %s empty" % head
386 if dir == '-':
387 return msgs[-count:]
388 else:
389 return msgs[:count]
390 else:
391 if not dir:
392 if head in ('prev', 'last'):
393 dir = '-'
394 if dir == '-':
395 i = bisect(all, anchor)
396 return all[max(0, i-count):i]
397 else:
398 i = bisect(all, anchor-1)
399 return all[i:i+count]
400 # Test for X-Y next
401 i = string.find(seq, '-')
402 if i >= 0:
403 begin = self._parseindex(seq[:i], all)
404 end = self._parseindex(seq[i+1:], all)
405 i = bisect(all, begin-1)
406 j = bisect(all, end)
407 r = all[i:j]
408 if not r:
409 raise Error, "bad message list %s" % seq
410 return r
411 # Neither X:Y nor X-Y; must be a number or a (pseudo-)sequence
Guido van Rossum508a0921996-05-28 22:59:37 +0000412 try:
Guido van Rossum7cfd31e1997-04-16 02:45:08 +0000413 n = self._parseindex(seq, all)
414 except Error, msg:
415 seqs = self.getsequences()
416 if not seqs.has_key(seq):
417 if not msg:
418 msg = "bad message list %s" % seq
419 raise Error, msg
420 return seqs[seq]
421 else:
422 if n not in all:
423 if isnumeric(seq):
424 raise Error, \
425 "message %d doesn't exist" % n
426 else:
427 raise Error, "no %s message" % seq
428 else:
429 return [n]
430
431 # Internal: parse a message number (or cur, first, etc.)
432 def _parseindex(self, seq, all):
433 if isnumeric(seq):
434 try:
435 return string.atoi(seq)
436 except (OverflowError, ValueError):
437 return sys.maxint
438 if seq in ('cur', '.'):
439 return self.getcurrent()
440 if seq == 'first':
441 return all[0]
442 if seq == 'last':
443 return all[-1]
444 if seq == 'next':
445 n = self.getcurrent()
446 i = bisect(all, n)
447 try:
448 return all[i]
449 except IndexError:
450 raise Error, "no next message"
451 if seq == 'prev':
452 n = self.getcurrent()
453 i = bisect(all, n-1)
454 if i == 0:
455 raise Error, "no prev message"
456 try:
457 return all[i-1]
458 except IndexError:
459 raise Error, "no prev message"
460 raise Error, None
Guido van Rossum508a0921996-05-28 22:59:37 +0000461
Guido van Rossum85347411994-09-09 11:10:15 +0000462 # Open a message -- returns a Message object
Guido van Rossum56013131994-06-23 12:06:02 +0000463 def openmessage(self, n):
Guido van Rossum56013131994-06-23 12:06:02 +0000464 return Message(self, n)
465
466 # Remove one or more messages -- may raise os.error
467 def removemessages(self, list):
468 errors = []
469 deleted = []
470 for n in list:
471 path = self.getmessagefilename(n)
472 commapath = self.getmessagefilename(',' + str(n))
473 try:
474 os.unlink(commapath)
475 except os.error:
476 pass
477 try:
478 os.rename(path, commapath)
479 except os.error, msg:
480 errors.append(msg)
481 else:
482 deleted.append(n)
483 if deleted:
484 self.removefromallsequences(deleted)
485 if errors:
486 if len(errors) == 1:
487 raise os.error, errors[0]
488 else:
489 raise os.error, ('multiple errors:', errors)
490
491 # Refile one or more messages -- may raise os.error.
492 # 'tofolder' is an open folder object
Guido van Rossum6d6a15b1996-07-21 02:18:22 +0000493 def refilemessages(self, list, tofolder, keepsequences=0):
Guido van Rossum56013131994-06-23 12:06:02 +0000494 errors = []
Guido van Rossum6d6a15b1996-07-21 02:18:22 +0000495 refiled = {}
Guido van Rossum56013131994-06-23 12:06:02 +0000496 for n in list:
497 ton = tofolder.getlast() + 1
498 path = self.getmessagefilename(n)
499 topath = tofolder.getmessagefilename(ton)
500 try:
501 os.rename(path, topath)
Guido van Rossum40b2cfb1995-01-02 18:38:23 +0000502 except os.error:
503 # Try copying
504 try:
505 shutil.copy2(path, topath)
506 os.unlink(path)
507 except (IOError, os.error), msg:
508 errors.append(msg)
509 try:
510 os.unlink(topath)
511 except os.error:
512 pass
513 continue
514 tofolder.setlast(ton)
Guido van Rossum6d6a15b1996-07-21 02:18:22 +0000515 refiled[n] = ton
Guido van Rossum56013131994-06-23 12:06:02 +0000516 if refiled:
Guido van Rossum6d6a15b1996-07-21 02:18:22 +0000517 if keepsequences:
518 tofolder._copysequences(self, refiled.items())
519 self.removefromallsequences(refiled.keys())
Guido van Rossum56013131994-06-23 12:06:02 +0000520 if errors:
521 if len(errors) == 1:
522 raise os.error, errors[0]
523 else:
524 raise os.error, ('multiple errors:', errors)
525
Guido van Rossum6d6a15b1996-07-21 02:18:22 +0000526 # Helper for refilemessages() to copy sequences
527 def _copysequences(self, fromfolder, refileditems):
528 fromsequences = fromfolder.getsequences()
529 tosequences = self.getsequences()
530 changed = 0
531 for name, seq in fromsequences.items():
532 try:
533 toseq = tosequences[name]
534 new = 0
535 except:
536 toseq = []
537 new = 1
538 for fromn, ton in refileditems:
539 if fromn in seq:
540 toseq.append(ton)
541 changed = 1
542 if new and toseq:
543 tosequences[name] = toseq
544 if changed:
545 self.putsequences(tosequences)
546
Guido van Rossum40b2cfb1995-01-02 18:38:23 +0000547 # Move one message over a specific destination message,
548 # which may or may not already exist.
549 def movemessage(self, n, tofolder, ton):
550 path = self.getmessagefilename(n)
551 # Open it to check that it exists
552 f = open(path)
553 f.close()
554 del f
555 topath = tofolder.getmessagefilename(ton)
556 backuptopath = tofolder.getmessagefilename(',%d' % ton)
557 try:
558 os.rename(topath, backuptopath)
559 except os.error:
560 pass
561 try:
562 os.rename(path, topath)
563 except os.error:
564 # Try copying
565 ok = 0
566 try:
567 tofolder.setlast(None)
568 shutil.copy2(path, topath)
569 ok = 1
570 finally:
571 if not ok:
572 try:
573 os.unlink(topath)
574 except os.error:
575 pass
576 os.unlink(path)
577 self.removefromallsequences([n])
578
579 # Copy one message over a specific destination message,
580 # which may or may not already exist.
581 def copymessage(self, n, tofolder, ton):
582 path = self.getmessagefilename(n)
583 # Open it to check that it exists
584 f = open(path)
585 f.close()
586 del f
587 topath = tofolder.getmessagefilename(ton)
588 backuptopath = tofolder.getmessagefilename(',%d' % ton)
589 try:
590 os.rename(topath, backuptopath)
591 except os.error:
592 pass
593 ok = 0
594 try:
595 tofolder.setlast(None)
596 shutil.copy2(path, topath)
597 ok = 1
598 finally:
599 if not ok:
600 try:
601 os.unlink(topath)
602 except os.error:
603 pass
604
Guido van Rossum56013131994-06-23 12:06:02 +0000605 # Remove one or more messages from all sequeuces (including last)
Guido van Rossum3508d601996-11-12 04:15:47 +0000606 # -- but not from 'cur'!!!
Guido van Rossum56013131994-06-23 12:06:02 +0000607 def removefromallsequences(self, list):
608 if hasattr(self, 'last') and self.last in list:
609 del self.last
610 sequences = self.getsequences()
611 changed = 0
612 for name, seq in sequences.items():
Guido van Rossum3508d601996-11-12 04:15:47 +0000613 if name == 'cur':
614 continue
Guido van Rossum56013131994-06-23 12:06:02 +0000615 for n in list:
616 if n in seq:
617 seq.remove(n)
618 changed = 1
619 if not seq:
620 del sequences[name]
621 if changed:
Guido van Rossum5f47e571994-07-14 14:01:00 +0000622 self.putsequences(sequences)
Guido van Rossum56013131994-06-23 12:06:02 +0000623
624 # Return the last message number
625 def getlast(self):
626 if not hasattr(self, 'last'):
627 messages = self.listmessages()
628 return self.last
629
630 # Set the last message number
631 def setlast(self, last):
632 if last is None:
633 if hasattr(self, 'last'):
634 del self.last
635 else:
636 self.last = last
637
638class Message(mimetools.Message):
639
640 # Constructor
641 def __init__(self, f, n, fp = None):
642 self.folder = f
643 self.number = n
644 if not fp:
645 path = f.getmessagefilename(n)
646 fp = open(path, 'r')
647 mimetools.Message.__init__(self, fp)
648
649 # String representation
650 def __repr__(self):
651 return 'Message(%s, %s)' % (repr(self.folder), self.number)
652
653 # Return the message's header text as a string. If an
654 # argument is specified, it is used as a filter predicate to
655 # decide which headers to return (its argument is the header
656 # name converted to lower case).
657 def getheadertext(self, pred = None):
658 if not pred:
659 return string.joinfields(self.headers, '')
660 headers = []
661 hit = 0
662 for line in self.headers:
663 if line[0] not in string.whitespace:
664 i = string.find(line, ':')
665 if i > 0:
666 hit = pred(string.lower(line[:i]))
667 if hit: headers.append(line)
668 return string.joinfields(headers, '')
669
670 # Return the message's body text as string. This undoes a
671 # Content-Transfer-Encoding, but does not interpret other MIME
672 # features (e.g. multipart messages). To suppress to
673 # decoding, pass a 0 as argument
674 def getbodytext(self, decode = 1):
675 self.fp.seek(self.startofbody)
676 encoding = self.getencoding()
677 if not decode or encoding in ('7bit', '8bit', 'binary'):
678 return self.fp.read()
679 from StringIO import StringIO
680 output = StringIO()
681 mimetools.decode(self.fp, output, encoding)
682 return output.getvalue()
683
684 # Only for multipart messages: return the message's body as a
685 # list of SubMessage objects. Each submessage object behaves
686 # (almost) as a Message object.
687 def getbodyparts(self):
688 if self.getmaintype() != 'multipart':
689 raise Error, \
690 'Content-Type is not multipart/*'
691 bdry = self.getparam('boundary')
692 if not bdry:
693 raise Error, 'multipart/* without boundary param'
694 self.fp.seek(self.startofbody)
695 mf = multifile.MultiFile(self.fp)
696 mf.push(bdry)
697 parts = []
698 while mf.next():
699 n = str(self.number) + '.' + `1 + len(parts)`
700 part = SubMessage(self.folder, n, mf)
701 parts.append(part)
702 mf.pop()
703 return parts
704
705 # Return body, either a string or a list of messages
706 def getbody(self):
707 if self.getmaintype() == 'multipart':
708 return self.getbodyparts()
709 else:
710 return self.getbodytext()
711
712
713class SubMessage(Message):
714
715 # Constructor
716 def __init__(self, f, n, fp):
717 Message.__init__(self, f, n, fp)
718 if self.getmaintype() == 'multipart':
719 self.body = Message.getbodyparts(self)
720 else:
721 self.body = Message.getbodytext(self)
722 # XXX If this is big, should remember file pointers
723
724 # String representation
725 def __repr__(self):
726 f, n, fp = self.folder, self.number, self.fp
727 return 'SubMessage(%s, %s, %s)' % (f, n, fp)
728
729 def getbodytext(self):
730 if type(self.body) == type(''):
731 return self.body
732
733 def getbodyparts(self):
734 if type(self.body) == type([]):
735 return self.body
736
737 def getbody(self):
738 return self.body
739
740
741# Class implementing sets of integers.
742#
743# This is an efficient representation for sets consisting of several
744# continuous ranges, e.g. 1-100,200-400,402-1000 is represented
745# internally as a list of three pairs: [(1,100), (200,400),
746# (402,1000)]. The internal representation is always kept normalized.
747#
748# The constructor has up to three arguments:
749# - the string used to initialize the set (default ''),
750# - the separator between ranges (default ',')
751# - the separator between begin and end of a range (default '-')
752# The separators may be regular expressions and should be different.
753#
754# The tostring() function yields a string that can be passed to another
755# IntSet constructor; __repr__() is a valid IntSet constructor itself.
756#
757# XXX The default begin/end separator means that negative numbers are
758# not supported very well.
759#
760# XXX There are currently no operations to remove set elements.
761
762class IntSet:
763
764 def __init__(self, data = None, sep = ',', rng = '-'):
765 self.pairs = []
766 self.sep = sep
767 self.rng = rng
768 if data: self.fromstring(data)
769
770 def reset(self):
771 self.pairs = []
772
773 def __cmp__(self, other):
774 return cmp(self.pairs, other.pairs)
775
776 def __hash__(self):
777 return hash(self.pairs)
778
779 def __repr__(self):
780 return 'IntSet(%s, %s, %s)' % (`self.tostring()`,
781 `self.sep`, `self.rng`)
782
783 def normalize(self):
784 self.pairs.sort()
785 i = 1
786 while i < len(self.pairs):
787 alo, ahi = self.pairs[i-1]
788 blo, bhi = self.pairs[i]
789 if ahi >= blo-1:
Guido van Rossum7cfd31e1997-04-16 02:45:08 +0000790 self.pairs[i-1:i+1] = [(alo, max(ahi, bhi))]
Guido van Rossum56013131994-06-23 12:06:02 +0000791 else:
792 i = i+1
793
794 def tostring(self):
795 s = ''
796 for lo, hi in self.pairs:
797 if lo == hi: t = `lo`
798 else: t = `lo` + self.rng + `hi`
799 if s: s = s + (self.sep + t)
800 else: s = t
801 return s
802
803 def tolist(self):
804 l = []
805 for lo, hi in self.pairs:
806 m = range(lo, hi+1)
807 l = l + m
808 return l
809
810 def fromlist(self, list):
811 for i in list:
812 self.append(i)
813
814 def clone(self):
815 new = IntSet()
816 new.pairs = self.pairs[:]
817 return new
818
819 def min(self):
820 return self.pairs[0][0]
821
822 def max(self):
823 return self.pairs[-1][-1]
824
825 def contains(self, x):
826 for lo, hi in self.pairs:
827 if lo <= x <= hi: return 1
828 return 0
829
830 def append(self, x):
831 for i in range(len(self.pairs)):
832 lo, hi = self.pairs[i]
833 if x < lo: # Need to insert before
834 if x+1 == lo:
835 self.pairs[i] = (x, hi)
836 else:
837 self.pairs.insert(i, (x, x))
838 if i > 0 and x-1 == self.pairs[i-1][1]:
839 # Merge with previous
840 self.pairs[i-1:i+1] = [
841 (self.pairs[i-1][0],
842 self.pairs[i][1])
843 ]
844 return
845 if x <= hi: # Already in set
846 return
847 i = len(self.pairs) - 1
848 if i >= 0:
849 lo, hi = self.pairs[i]
850 if x-1 == hi:
851 self.pairs[i] = lo, x
852 return
853 self.pairs.append((x, x))
854
855 def addpair(self, xlo, xhi):
856 if xlo > xhi: return
857 self.pairs.append((xlo, xhi))
858 self.normalize()
859
860 def fromstring(self, data):
861 import string, regsub
862 new = []
863 for part in regsub.split(data, self.sep):
864 list = []
865 for subp in regsub.split(part, self.rng):
866 s = string.strip(subp)
867 list.append(string.atoi(s))
868 if len(list) == 1:
869 new.append((list[0], list[0]))
870 elif len(list) == 2 and list[0] <= list[1]:
871 new.append((list[0], list[1]))
872 else:
873 raise ValueError, 'bad data passed to IntSet'
874 self.pairs = self.pairs + new
875 self.normalize()
876
877
878# Subroutines to read/write entries in .mh_profile and .mh_sequences
879
880def pickline(file, key, casefold = 1):
881 try:
882 f = open(file, 'r')
883 except IOError:
884 return None
885 pat = key + ':'
886 if casefold:
887 prog = regex.compile(pat, regex.casefold)
888 else:
889 prog = regex.compile(pat)
890 while 1:
891 line = f.readline()
892 if not line: break
Guido van Rossumea8ee1d1995-01-26 00:45:20 +0000893 if prog.match(line) >= 0:
Guido van Rossum56013131994-06-23 12:06:02 +0000894 text = line[len(key)+1:]
895 while 1:
896 line = f.readline()
897 if not line or \
898 line[0] not in string.whitespace:
899 break
900 text = text + line
901 return string.strip(text)
902 return None
903
904def updateline(file, key, value, casefold = 1):
905 try:
906 f = open(file, 'r')
907 lines = f.readlines()
908 f.close()
909 except IOError:
910 lines = []
911 pat = key + ':\(.*\)\n'
912 if casefold:
913 prog = regex.compile(pat, regex.casefold)
914 else:
915 prog = regex.compile(pat)
916 if value is None:
917 newline = None
918 else:
Guido van Rossum508a0921996-05-28 22:59:37 +0000919 newline = '%s: %s\n' % (key, value)
Guido van Rossum56013131994-06-23 12:06:02 +0000920 for i in range(len(lines)):
921 line = lines[i]
922 if prog.match(line) == len(line):
923 if newline is None:
924 del lines[i]
925 else:
926 lines[i] = newline
927 break
928 else:
929 if newline is not None:
930 lines.append(newline)
Guido van Rossum508a0921996-05-28 22:59:37 +0000931 tempfile = file + "~"
Guido van Rossum56013131994-06-23 12:06:02 +0000932 f = open(tempfile, 'w')
933 for line in lines:
934 f.write(line)
935 f.close()
Guido van Rossum508a0921996-05-28 22:59:37 +0000936 os.rename(tempfile, file)
Guido van Rossum56013131994-06-23 12:06:02 +0000937
938
939# Test program
940
941def test():
942 global mh, f
943 os.system('rm -rf $HOME/Mail/@test')
944 mh = MH()
945 def do(s): print s; print eval(s)
946 do('mh.listfolders()')
947 do('mh.listallfolders()')
948 testfolders = ['@test', '@test/test1', '@test/test2',
949 '@test/test1/test11', '@test/test1/test12',
950 '@test/test1/test11/test111']
951 for t in testfolders: do('mh.makefolder(%s)' % `t`)
952 do('mh.listsubfolders(\'@test\')')
953 do('mh.listallsubfolders(\'@test\')')
954 f = mh.openfolder('@test')
955 do('f.listsubfolders()')
956 do('f.listallsubfolders()')
957 do('f.getsequences()')
958 seqs = f.getsequences()
959 seqs['foo'] = IntSet('1-10 12-20', ' ').tolist()
960 print seqs
961 f.putsequences(seqs)
962 do('f.getsequences()')
963 testfolders.reverse()
964 for t in testfolders: do('mh.deletefolder(%s)' % `t`)
965 do('mh.getcontext()')
966 context = mh.getcontext()
967 f = mh.openfolder(context)
Guido van Rossum56013131994-06-23 12:06:02 +0000968 do('f.getcurrent()')
Guido van Rossum7cfd31e1997-04-16 02:45:08 +0000969 for seq in ['first', 'last', 'cur', '.', 'prev', 'next',
970 'first:3', 'last:3', 'cur:3', 'cur:-3',
971 'prev:3', 'next:3',
972 '1:3', '1:-3', '100:3', '100:-3', '10000:3', '10000:-3',
973 'all']:
974 try:
975 do('f.parsesequence(%s)' % `seq`)
976 except Error, msg:
977 print "Error:", msg
978 stuff = os.popen("pick %s 2>/dev/null" % `seq`).read()
979 list = map(string.atoi, string.split(stuff))
980 print list, "<-- pick"
981 do('f.listmessages()')
Guido van Rossum56013131994-06-23 12:06:02 +0000982
983
984if __name__ == '__main__':
985 test()