~malept/ubuntu/lucid/python2.6/dev-dependency-fix

« back to all changes in this revision

Viewing changes to Lib/mhlib.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2009-02-13 12:51:00 UTC
  • Revision ID: james.westby@ubuntu.com-20090213125100-uufgcb9yeqzujpqw
Tags: upstream-2.6.1
ImportĀ upstreamĀ versionĀ 2.6.1

Show diffs side-by-side

added added

removed removed

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