mhlib.py 32.5 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
"""MH interface -- purely object-oriented (well, almost)

Executive summary:

import mhlib

mh = mhlib.MH()         # use default mailbox directory and profile
mh = mhlib.MH(mailbox)  # override mailbox location (default from profile)
mh = mhlib.MH(mailbox, profile) # override mailbox and profile

mh.error(format, ...)   # print error message -- can be overridden
s = mh.getprofile(key)  # profile entry (None if not set)
path = mh.getpath()     # mailbox pathname
name = mh.getcontext()  # name of current folder
mh.setcontext(name)     # set name of current folder

list = mh.listfolders() # names of top-level folders
list = mh.listallfolders() # names of all folders, including subfolders
list = mh.listsubfolders(name) # direct subfolders of given folder
list = mh.listallsubfolders(name) # all subfolders of given folder

mh.makefolder(name)     # create new folder
mh.deletefolder(name)   # delete folder -- must have no subfolders

f = mh.openfolder(name) # new open folder object

f.error(format, ...)    # same as mh.error(format, ...)
path = f.getfullname()  # folder's full pathname
path = f.getsequencesfilename() # full pathname of folder's sequences file
path = f.getmessagefilename(n)  # full pathname of message n in folder

list = f.listmessages() # list of messages in folder (as numbers)
n = f.getcurrent()      # get current message
f.setcurrent(n)         # set current message
list = f.parsesequence(seq)     # parse msgs syntax into list of messages
n = f.getlast()         # get last message (0 if no messagse)
f.setlast(n)            # set last message (internal use only)

dict = f.getsequences() # dictionary of sequences in folder {name: list}
f.putsequences(dict)    # write sequences back to folder

f.createmessage(n, fp)  # add message from file f as number n
f.removemessages(list)  # remove messages in list from folder
f.refilemessages(list, tofolder) # move messages in list to other folder
f.movemessage(n, tofolder, ton)  # move one message to a given destination
f.copymessage(n, tofolder, ton)  # copy one message to a given destination

m = f.openmessage(n)    # new open message object (costs a file descriptor)
m is a derived class of mimetools.Message(rfc822.Message), with:
s = m.getheadertext()   # text of message's headers
s = m.getheadertext(pred) # text of message's headers, filtered by pred
s = m.getbodytext()     # text of message's body, decoded
s = m.getbodytext(0)    # text of message's body, not decoded
"""

56 57
# XXX To do, functionality:
# - annotate messages
58
# - send messages
59
#
60
# XXX To do, organization:
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
# - move IntSet to separate file
# - move most Message functionality to module mimetools


# Customizable defaults

MH_PROFILE = '~/.mh_profile'
PATH = '~/Mail'
MH_SEQUENCES = '.mh_sequences'
FOLDER_PROTECT = 0700


# Imported modules

import os
76
import sys
77
from stat import ST_NLINK
78
import re
79 80
import mimetools
import multifile
81
import shutil
82
from bisect import bisect
83

84
__all__ = ["MH","Error","Folder","Message"]
85 86 87

# Exported constants

88 89
class Error(Exception):
    pass
90 91 92


class MH:
93 94 95 96 97
    """Class representing a particular collection of folders.
    Optional constructor arguments are the pathname for the directory
    containing the collection, and the MH profile to use.
    If either is omitted or empty a default is used; the default
    directory is taken from the MH profile if it is specified there."""
98

99
    def __init__(self, path = None, profile = None):
100
        """Constructor."""
101 102 103 104 105 106 107 108 109
        if not profile: profile = MH_PROFILE
        self.profile = os.path.expanduser(profile)
        if not path: path = self.getprofile('Path')
        if not path: path = PATH
        if not os.path.isabs(path) and path[0] != '~':
            path = os.path.join('~', path)
        path = os.path.expanduser(path)
        if not os.path.isdir(path): raise Error, 'MH() path not found'
        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 130 131 132
        context = pickline(os.path.join(self.getpath(), 'context'),
                  'Current-Folder')
        if not context: context = 'inbox'
        return context
133 134

    def setcontext(self, context):
135
        """Set the name of the current folder."""
136 137 138 139
        fn = os.path.join(self.getpath(), 'context')
        f = open(fn, "w")
        f.write("Current-Folder: %s\n" % context)
        f.close()
140 141

    def listfolders(self):
142
        """Return the names of the top-level folders."""
143 144 145 146 147 148 149 150
        folders = []
        path = self.getpath()
        for name in os.listdir(path):
            fullname = os.path.join(path, name)
            if os.path.isdir(fullname):
                folders.append(name)
        folders.sort()
        return folders
151 152

    def listsubfolders(self, name):
153 154
        """Return the names of the subfolders in a given folder
        (prefixed with the given folder name)."""
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
        fullname = os.path.join(self.path, name)
        # Get the link count so we can avoid listing folders
        # that have no subfolders.
        st = os.stat(fullname)
        nlinks = st[ST_NLINK]
        if nlinks <= 2:
            return []
        subfolders = []
        subnames = os.listdir(fullname)
        for subname in subnames:
            fullsubname = os.path.join(fullname, subname)
            if os.path.isdir(fullsubname):
                name_subname = os.path.join(name, subname)
                subfolders.append(name_subname)
                # Stop looking for subfolders when
                # we've seen them all
                nlinks = nlinks - 1
                if nlinks <= 2:
                    break
        subfolders.sort()
        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 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
        fullname = os.path.join(self.path, name)
        # Get the link count so we can avoid listing folders
        # that have no subfolders.
        st = os.stat(fullname)
        nlinks = st[ST_NLINK]
        if nlinks <= 2:
            return []
        subfolders = []
        subnames = os.listdir(fullname)
        for subname in subnames:
            if subname[0] == ',' or isnumeric(subname): continue
            fullsubname = os.path.join(fullname, subname)
            if os.path.isdir(fullsubname):
                name_subname = os.path.join(name, subname)
                subfolders.append(name_subname)
                if not os.path.islink(fullsubname):
                    subsubfolders = self.listallsubfolders(
                              name_subname)
                    subfolders = subfolders + subsubfolders
                # Stop looking for subfolders when
                # we've seen them all
                nlinks = nlinks - 1
                if nlinks <= 2:
                    break
        subfolders.sort()
        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 217
        protect = pickline(self.profile, 'Folder-Protect')
        if protect and isnumeric(protect):
218
            mode = int(protect, 8)
219 220 221
        else:
            mode = FOLDER_PROTECT
        os.mkdir(os.path.join(self.getpath(), name), mode)
222 223

    def deletefolder(self, name):
224 225
        """Delete a folder.  This removes files in the folder but not
        subdirectories.  Raise os.error if deleting the folder itself fails."""
226 227 228 229 230 231 232 233 234
        fullname = os.path.join(self.getpath(), name)
        for subname in os.listdir(fullname):
            fullsubname = os.path.join(fullname, subname)
            try:
                os.unlink(fullsubname)
            except os.error:
                self.error('%s not deleted, continuing...' %
                          fullsubname)
        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 247 248 249
        self.mh = mh
        self.name = name
        if not os.path.isdir(self.getfullname()):
            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 263

    def getsequencesfilename(self):
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 281
        """Return the list of messages currently present in the folder.
        As a side effect, set self.last to the last message (or 0)."""
282 283 284 285
        messages = []
        match = numericprog.match
        append = messages.append
        for name in os.listdir(self.getfullname()):
286
            if match(name):
287
                append(name)
288
        messages = map(int, messages)
289 290 291 292 293 294
        messages.sort()
        if messages:
            self.last = messages[-1]
        else:
            self.last = 0
        return messages
295

296
    def getsequences(self):
297
        """Return the set of sequences for the folder."""
298 299 300 301 302 303 304 305 306
        sequences = {}
        fullname = self.getsequencesfilename()
        try:
            f = open(fullname, 'r')
        except IOError:
            return sequences
        while 1:
            line = f.readline()
            if not line: break
307
            fields = line.split(':')
308
            if len(fields) != 2:
309
                self.error('bad sequence in %s: %s' %
310 311 312
                          (fullname, line.strip()))
            key = fields[0].strip()
            value = IntSet(fields[1].strip(), ' ').tolist()
313 314
            sequences[key] = value
        return sequences
315 316

    def putsequences(self, sequences):
317
        """Write the set of sequences back to the folder."""
318 319 320 321 322 323 324 325 326 327 328 329 330 331
        fullname = self.getsequencesfilename()
        f = None
        for key in sequences.keys():
            s = IntSet('', ' ')
            s.fromlist(sequences[key])
            if not f: f = open(fullname, 'w')
            f.write('%s: %s\n' % (key, s.tostring()))
        if not f:
            try:
                os.unlink(fullname)
            except os.error:
                pass
        else:
            f.close()
332

333
    def getcurrent(self):
334
        """Return the current message.  Raise Error when there is none."""
335 336 337 338 339
        seqs = self.getsequences()
        try:
            return max(seqs['cur'])
        except (ValueError, KeyError):
            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 347 348 349
        """Parse an MH sequence specification into a message list.
        Attempt to mimic mh-sequence(5) as close as possible.
        Also attempt to mimic observed behavior regarding which
        conditions cause which error messages."""
350 351 352 353 354 355 356 357 358 359 360 361
        # XXX Still not complete (see mh-format(5)).
        # Missing are:
        # - 'prev', 'next' as count
        # - Sequence-Negation option
        all = self.listmessages()
        # Observed behavior: test for empty folder is done first
        if not all:
            raise Error, "no messages in %s" % self.name
        # Common case first: all is frequently the default
        if seq == 'all':
            return all
        # Test for X:Y before X-Y because 'seq:-n' matches both
362
        i = seq.find(':')
363 364 365 366 367 368 369
        if i >= 0:
            head, dir, tail = seq[:i], '', seq[i+1:]
            if tail[:1] in '-+':
                dir, tail = tail[:1], tail[1:]
            if not isnumeric(tail):
                raise Error, "bad message list %s" % seq
            try:
370
                count = int(tail)
371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399
            except (ValueError, OverflowError):
                # Can't use sys.maxint because of i+count below
                count = len(all)
            try:
                anchor = self._parseindex(head, all)
            except Error, msg:
                seqs = self.getsequences()
                if not seqs.has_key(head):
                    if not msg:
                        msg = "bad message list %s" % seq
                    raise Error, msg, sys.exc_info()[2]
                msgs = seqs[head]
                if not msgs:
                    raise Error, "sequence %s empty" % head
                if dir == '-':
                    return msgs[-count:]
                else:
                    return msgs[:count]
            else:
                if not dir:
                    if head in ('prev', 'last'):
                        dir = '-'
                if dir == '-':
                    i = bisect(all, anchor)
                    return all[max(0, i-count):i]
                else:
                    i = bisect(all, anchor-1)
                    return all[i:i+count]
        # Test for X-Y next
400
        i = seq.find('-')
401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427
        if i >= 0:
            begin = self._parseindex(seq[:i], all)
            end = self._parseindex(seq[i+1:], all)
            i = bisect(all, begin-1)
            j = bisect(all, end)
            r = all[i:j]
            if not r:
                raise Error, "bad message list %s" % seq
            return r
        # Neither X:Y nor X-Y; must be a number or a (pseudo-)sequence
        try:
            n = self._parseindex(seq, all)
        except Error, msg:
            seqs = self.getsequences()
            if not seqs.has_key(seq):
                if not msg:
                    msg = "bad message list %s" % seq
                raise Error, msg
            return seqs[seq]
        else:
            if n not in all:
                if isnumeric(seq):
                    raise Error, "message %d doesn't exist" % n
                else:
                    raise Error, "no %s message" % seq
            else:
                return [n]
428 429

    def _parseindex(self, seq, all):
430
        """Internal: parse a message number (or cur, first, etc.)."""
431 432
        if isnumeric(seq):
            try:
433
                return int(seq)
434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458
            except (OverflowError, ValueError):
                return sys.maxint
        if seq in ('cur', '.'):
            return self.getcurrent()
        if seq == 'first':
            return all[0]
        if seq == 'last':
            return all[-1]
        if seq == 'next':
            n = self.getcurrent()
            i = bisect(all, n)
            try:
                return all[i]
            except IndexError:
                raise Error, "no next message"
        if seq == 'prev':
            n = self.getcurrent()
            i = bisect(all, n-1)
            if i == 0:
                raise Error, "no prev message"
            try:
                return all[i-1]
            except IndexError:
                raise Error, "no prev message"
        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 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487
        errors = []
        deleted = []
        for n in list:
            path = self.getmessagefilename(n)
            commapath = self.getmessagefilename(',' + str(n))
            try:
                os.unlink(commapath)
            except os.error:
                pass
            try:
                os.rename(path, commapath)
            except os.error, msg:
                errors.append(msg)
            else:
                deleted.append(n)
        if deleted:
            self.removefromallsequences(deleted)
        if errors:
            if len(errors) == 1:
                raise os.error, errors[0]
            else:
                raise os.error, ('multiple errors:', errors)
488 489

    def refilemessages(self, list, tofolder, keepsequences=0):
490 491
        """Refile one or more messages -- may raise os.error.
        'tofolder' is an open folder object."""
492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522
        errors = []
        refiled = {}
        for n in list:
            ton = tofolder.getlast() + 1
            path = self.getmessagefilename(n)
            topath = tofolder.getmessagefilename(ton)
            try:
                os.rename(path, topath)
            except os.error:
                # Try copying
                try:
                    shutil.copy2(path, topath)
                    os.unlink(path)
                except (IOError, os.error), msg:
                    errors.append(msg)
                    try:
                        os.unlink(topath)
                    except os.error:
                        pass
                    continue
            tofolder.setlast(ton)
            refiled[n] = ton
        if refiled:
            if keepsequences:
                tofolder._copysequences(self, refiled.items())
            self.removefromallsequences(refiled.keys())
        if errors:
            if len(errors) == 1:
                raise os.error, errors[0]
            else:
                raise os.error, ('multiple errors:', errors)
523 524

    def _copysequences(self, fromfolder, refileditems):
525
        """Helper for refilemessages() to copy sequences."""
526 527 528 529 530 531 532
        fromsequences = fromfolder.getsequences()
        tosequences = self.getsequences()
        changed = 0
        for name, seq in fromsequences.items():
            try:
                toseq = tosequences[name]
                new = 0
533
            except KeyError:
534 535 536 537 538 539 540 541 542 543
                toseq = []
                new = 1
            for fromn, ton in refileditems:
                if fromn in seq:
                    toseq.append(ton)
                    changed = 1
            if new and toseq:
                tosequences[name] = toseq
        if changed:
            self.putsequences(tosequences)
544 545

    def movemessage(self, n, tofolder, ton):
546 547
        """Move one message over a specific destination message,
        which may or may not already exist."""
548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575
        path = self.getmessagefilename(n)
        # Open it to check that it exists
        f = open(path)
        f.close()
        del f
        topath = tofolder.getmessagefilename(ton)
        backuptopath = tofolder.getmessagefilename(',%d' % ton)
        try:
            os.rename(topath, backuptopath)
        except os.error:
            pass
        try:
            os.rename(path, topath)
        except os.error:
            # Try copying
            ok = 0
            try:
                tofolder.setlast(None)
                shutil.copy2(path, topath)
                ok = 1
            finally:
                if not ok:
                    try:
                        os.unlink(topath)
                    except os.error:
                        pass
            os.unlink(path)
        self.removefromallsequences([n])
576 577

    def copymessage(self, n, tofolder, ton):
578 579
        """Copy one message over a specific destination message,
        which may or may not already exist."""
580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601
        path = self.getmessagefilename(n)
        # Open it to check that it exists
        f = open(path)
        f.close()
        del f
        topath = tofolder.getmessagefilename(ton)
        backuptopath = tofolder.getmessagefilename(',%d' % ton)
        try:
            os.rename(topath, backuptopath)
        except os.error:
            pass
        ok = 0
        try:
            tofolder.setlast(None)
            shutil.copy2(path, topath)
            ok = 1
        finally:
            if not ok:
                try:
                    os.unlink(topath)
                except os.error:
                    pass
602

603
    def createmessage(self, n, txt):
604
        """Create a message, with text from the open file txt."""
605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627
        path = self.getmessagefilename(n)
        backuppath = self.getmessagefilename(',%d' % n)
        try:
            os.rename(path, backuppath)
        except os.error:
            pass
        ok = 0
        BUFSIZE = 16*1024
        try:
            f = open(path, "w")
            while 1:
                buf = txt.read(BUFSIZE)
                if not buf:
                    break
                f.write(buf)
            f.close()
            ok = 1
        finally:
            if not ok:
                try:
                    os.unlink(path)
                except os.error:
                    pass
628

629
    def removefromallsequences(self, list):
630
        """Remove one or more messages from all sequences (including last)
631
        -- but not from 'cur'!!!"""
632 633 634 635 636 637 638 639 640 641 642 643 644 645 646
        if hasattr(self, 'last') and self.last in list:
            del self.last
        sequences = self.getsequences()
        changed = 0
        for name, seq in sequences.items():
            if name == 'cur':
                continue
            for n in list:
                if n in seq:
                    seq.remove(n)
                    changed = 1
                    if not seq:
                        del sequences[name]
        if changed:
            self.putsequences(sequences)
647 648

    def getlast(self):
649
        """Return the last message number."""
650 651 652
        if not hasattr(self, 'last'):
            messages = self.listmessages()
        return self.last
653 654

    def setlast(self, last):
655
        """Set the last message number."""
656 657 658 659 660
        if last is None:
            if hasattr(self, 'last'):
                del self.last
        else:
            self.last = last
661 662 663

class Message(mimetools.Message):

664
    def __init__(self, f, n, fp = None):
665
        """Constructor."""
666 667 668 669 670 671
        self.folder = f
        self.number = n
        if not fp:
            path = f.getmessagefilename(n)
            fp = open(path, 'r')
        mimetools.Message.__init__(self, fp)
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 679 680 681
        """Return the message's header text as a string.  If an
        argument is specified, it is used as a filter predicate to
        decide which headers to return (its argument is the header
        name converted to lower case)."""
682
        if not pred:
683
            return ''.join(self.headers)
684 685 686
        headers = []
        hit = 0
        for line in self.headers:
Eric S. Raymond's avatar
Eric S. Raymond committed
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 696 697 698
        """Return the message's body text as string.  This undoes a
        Content-Transfer-Encoding, but does not interpret other MIME
        features (e.g. multipart messages).  To suppress decoding,
        pass 0 as an argument."""
699 700
        self.fp.seek(self.startofbody)
        encoding = self.getencoding()
701
        if not decode or encoding in ('', '7bit', '8bit', 'binary'):
702 703 704 705 706
            return self.fp.read()
        from StringIO import StringIO
        output = StringIO()
        mimetools.decode(self.fp, output, encoding)
        return output.getvalue()
707 708

    def getbodyparts(self):
709 710 711
        """Only for multipart messages: return the message's body as a
        list of SubMessage objects.  Each submessage object behaves
        (almost) as a Message object."""
712 713 714 715 716 717 718 719 720 721 722 723 724 725 726
        if self.getmaintype() != 'multipart':
            raise Error, 'Content-Type is not multipart/*'
        bdry = self.getparam('boundary')
        if not bdry:
            raise Error, 'multipart/* without boundary param'
        self.fp.seek(self.startofbody)
        mf = multifile.MultiFile(self.fp)
        mf.push(bdry)
        parts = []
        while mf.next():
            n = str(self.number) + '.' + `1 + len(parts)`
            part = SubMessage(self.folder, n, mf)
            parts.append(part)
        mf.pop()
        return parts
727 728

    def getbody(self):
729
        """Return body, either a string or a list of messages."""
730 731 732 733
        if self.getmaintype() == 'multipart':
            return self.getbodyparts()
        else:
            return self.getbodytext()
734 735 736 737


class SubMessage(Message):

738
    def __init__(self, f, n, fp):
739
        """Constructor."""
740 741 742 743 744
        Message.__init__(self, f, n, fp)
        if self.getmaintype() == 'multipart':
            self.body = Message.getbodyparts(self)
        else:
            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 751
        f, n, fp = self.folder, self.number, self.fp
        return 'SubMessage(%s, %s, %s)' % (f, n, fp)
752

753 754 755
    def getbodytext(self, decode = 1):
        if not decode:
            return self.bodyencoded
756 757
        if type(self.body) == type(''):
            return self.body
758

759
    def getbodyparts(self):
760 761
        if type(self.body) == type([]):
            return self.body
762

763
    def getbody(self):
764
        return self.body
765 766 767


class IntSet:
768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788
    """Class implementing sets of integers.

    This is an efficient representation for sets consisting of several
    continuous ranges, e.g. 1-100,200-400,402-1000 is represented
    internally as a list of three pairs: [(1,100), (200,400),
    (402,1000)].  The internal representation is always kept normalized.

    The constructor has up to three arguments:
    - the string used to initialize the set (default ''),
    - the separator between ranges (default ',')
    - the separator between begin and end of a range (default '-')
    The separators must be strings (not regexprs) and should be different.

    The tostring() function yields a string that can be passed to another
    IntSet constructor; __repr__() is a valid IntSet constructor itself.
    """

    # XXX The default begin/end separator means that negative numbers are
    #     not supported very well.
    #
    # XXX There are currently no operations to remove set elements.
789

790
    def __init__(self, data = None, sep = ',', rng = '-'):
791 792 793 794
        self.pairs = []
        self.sep = sep
        self.rng = rng
        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 807
        return 'IntSet(%s, %s, %s)' % (`self.tostring()`,
                  `self.sep`, `self.rng`)
808 809

    def normalize(self):
810 811 812 813 814 815 816 817 818
        self.pairs.sort()
        i = 1
        while i < len(self.pairs):
            alo, ahi = self.pairs[i-1]
            blo, bhi = self.pairs[i]
            if ahi >= blo-1:
                self.pairs[i-1:i+1] = [(alo, max(ahi, bhi))]
            else:
                i = i+1
819 820

    def tostring(self):
821 822 823 824 825 826 827
        s = ''
        for lo, hi in self.pairs:
            if lo == hi: t = `lo`
            else: t = `lo` + self.rng + `hi`
            if s: s = s + (self.sep + t)
            else: s = t
        return s
828 829

    def tolist(self):
830 831 832 833 834
        l = []
        for lo, hi in self.pairs:
            m = range(lo, hi+1)
            l = l + m
        return l
835 836

    def fromlist(self, list):
837 838
        for i in list:
            self.append(i)
839 840

    def clone(self):
841 842 843
        new = IntSet()
        new.pairs = self.pairs[:]
        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 853 854
        for lo, hi in self.pairs:
            if lo <= x <= hi: return 1
        return 0
855 856

    def append(self, x):
857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879
        for i in range(len(self.pairs)):
            lo, hi = self.pairs[i]
            if x < lo: # Need to insert before
                if x+1 == lo:
                    self.pairs[i] = (x, hi)
                else:
                    self.pairs.insert(i, (x, x))
                if i > 0 and x-1 == self.pairs[i-1][1]:
                    # Merge with previous
                    self.pairs[i-1:i+1] = [
                            (self.pairs[i-1][0],
                             self.pairs[i][1])
                          ]
                return
            if x <= hi: # Already in set
                return
        i = len(self.pairs) - 1
        if i >= 0:
            lo, hi = self.pairs[i]
            if x-1 == hi:
                self.pairs[i] = lo, x
                return
        self.pairs.append((x, x))
880 881

    def addpair(self, xlo, xhi):
882 883 884
        if xlo > xhi: return
        self.pairs.append((xlo, xhi))
        self.normalize()
885 886

    def fromstring(self, data):
887
        new = []
888
        for part in data.split(self.sep):
889
            list = []
890 891 892
            for subp in part.split(self.rng):
                s = subp.strip()
                list.append(int(s))
893 894 895 896 897 898 899 900
            if len(list) == 1:
                new.append((list[0], list[0]))
            elif len(list) == 2 and list[0] <= list[1]:
                new.append((list[0], list[1]))
            else:
                raise ValueError, 'bad data passed to IntSet'
        self.pairs = self.pairs + new
        self.normalize()
901 902 903 904 905


# Subroutines to read/write entries in .mh_profile and .mh_sequences

def pickline(file, key, casefold = 1):
906
    try:
907
        f = open(file, 'r')
908
    except IOError:
909
        return None
910 911
    pat = re.escape(key) + ':'
    prog = re.compile(pat, casefold and re.IGNORECASE)
912
    while 1:
913 914 915 916 917 918
        line = f.readline()
        if not line: break
        if prog.match(line):
            text = line[len(key)+1:]
            while 1:
                line = f.readline()
Eric S. Raymond's avatar
Eric S. Raymond committed
919
                if not line or not line[0].isspace():
920 921
                    break
                text = text + line
922
            return text.strip()
923
    return None
924 925

def updateline(file, key, value, casefold = 1):
926
    try:
927 928 929
        f = open(file, 'r')
        lines = f.readlines()
        f.close()
930
    except IOError:
931
        lines = []
932 933
    pat = re.escape(key) + ':(.*)\n'
    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 940 941 942 943 944 945
        line = lines[i]
        if prog.match(line):
            if newline is None:
                del lines[i]
            else:
                lines[i] = newline
            break
946
    else:
947 948
        if newline is not None:
            lines.append(newline)
949 950 951
    tempfile = file + "~"
    f = open(tempfile, 'w')
    for line in lines:
952
        f.write(line)
953 954
    f.close()
    os.rename(tempfile, file)
955 956 957 958 959


# Test program

def test():
960 961 962 963 964 965 966
    global mh, f
    os.system('rm -rf $HOME/Mail/@test')
    mh = MH()
    def do(s): print s; print eval(s)
    do('mh.listfolders()')
    do('mh.listallfolders()')
    testfolders = ['@test', '@test/test1', '@test/test2',
967 968
                   '@test/test1/test11', '@test/test1/test12',
                   '@test/test1/test11/test111']
969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987
    for t in testfolders: do('mh.makefolder(%s)' % `t`)
    do('mh.listsubfolders(\'@test\')')
    do('mh.listallsubfolders(\'@test\')')
    f = mh.openfolder('@test')
    do('f.listsubfolders()')
    do('f.listallsubfolders()')
    do('f.getsequences()')
    seqs = f.getsequences()
    seqs['foo'] = IntSet('1-10 12-20', ' ').tolist()
    print seqs
    f.putsequences(seqs)
    do('f.getsequences()')
    testfolders.reverse()
    for t in testfolders: do('mh.deletefolder(%s)' % `t`)
    do('mh.getcontext()')
    context = mh.getcontext()
    f = mh.openfolder(context)
    do('f.getcurrent()')
    for seq in ['first', 'last', 'cur', '.', 'prev', 'next',
988 989 990 991 992 993 994 995 996
                'first:3', 'last:3', 'cur:3', 'cur:-3',
                'prev:3', 'next:3',
                '1:3', '1:-3', '100:3', '100:-3', '10000:3', '10000:-3',
                'all']:
        try:
            do('f.parsesequence(%s)' % `seq`)
        except Error, msg:
            print "Error:", msg
        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()