## $Id: folderops.py,v 1.19 2001/08/08 10:46:33 kjetilja Exp $

## Folder status flags
STATUS_READ   = '-'
STATUS_UNREAD = ' '
STATUS_FORW   = 'f'
STATUS_REPL   = 'r'

import os.path

def build_folder_tree(dir, names, tree):
    for name in names:
        # Don't follow any symlinks
        if os.path.islink(name):
            continue
        
        absname = os.path.join(dir, name)
        # Check if this is a file or directory
        if os.path.isdir(absname) and name[-4:] == '.sbd':
            # Find all files in this dir
            files = os.listdir(absname)
            name = name[:-4]

            # Make sure this folder exists before searching the subfolder
            if os.path.exists(os.path.join(dir, '.' + name + '.idx')):
                tree[name] = {}
                build_folder_tree(absname, files, tree[name])
            #else:
            #    print 'error finding %s' % \
            #          os.path.join(dir, '.' + name + '.idx')
            
        elif name[0] == '.' and name[-4:] == '.idx':
            # Append mail folder to tree
            name = name[1:-4]                
            if not tree.has_key(name):
                tree[name] = None

    return tree

##
## Function get_active_folders (folder path)
##
##    Construct folder list from the disk.
##
##
def get_active_folders(path):
    # Find all folder index files
    try:
        # Find all files in this dir
        files = os.listdir(path)
        ftree = build_folder_tree(path, files, {})
    except:
        return None
    else:
        return ftree

##
## Function move_messages (prefs instance, src fld, dst fld, msg instance)
##
##    Physically move messages from one folder to another.
##
##
def move_messages(src, dst, msgs):
    from marshal import load, dump

    fs = open(src) # Source message file
    fd = open(dst, "a") # Dest message file

    si = get_index_from_pathname(src)
    di = get_index_from_pathname(dst)

    fsi = open(si, "r+") # Source index
    fdi = open(di, "r+") # Dest index

    fldcache = {}
    sh = load(fdi)
    for idx in msgs:
        # Copy the complete message to the dest folder
        start = int(idx[1])
        stop = int(idx[2])
        fs.seek(start, 0)
        pos_start = int(fd.tell())
        fd.write( fs.read(stop-start) )
        pos_end = int(fd.tell())
        fldcache[start] = stop

        # Update the dest folder index for the dest folder in the dest
        # 0-epoch 1-start 2-stop 3-status 4-from 5-subject 6-time
        sh[str(pos_start)] = [ idx[3], str(pos_end), idx[5], idx[4], idx[0] ]

    # Dest index file and folder are now finished
    fdi.seek(0, 0)
    dump(sh, fdi)
    fd.close()
    fdi.close()

    # Record the number of unread messages in the destination folder
    unread_d = unread(sh)
    
    # Update the src folder file, omitting moved messages.
    # We write a new src file but do not include the message
    # boundaries that are listed in 'fldcache'
    newfs = open(src+".new", "w")
    fs.seek(0, 0)
    e = fldcache.keys()
    e.sort()
    cur = 0
    for ent in e:
        newfs.write( fs.read(ent-cur) )
        cur = fldcache[ent]
        fs.seek(cur, 0)
    # Omitting messages completed, now write the rest
    newfs.write( fs.read() )
    newfs.close()
    fs.close()
    # Rename the updated folder with the original folder name
    os.rename(src+".new",  src)

    # Last, update the source index file
    e = fldcache.keys()
    e.sort()
    sh = load(fsi)
    f = map( lambda x: int(x), sh.keys() )
    f.sort()

    # If we are moving all folder entries, just reset the contents of
    # the index file to an empty dictionary
    if e == f:
        fsi.seek(0, 0)
        dump({}, fsi)
        fsi.close()
        return (0, unread_d)

    # Delete cached entries
    for i in e:
        f.remove(i)

    # Rebuild the index by updating relative positions between messages
    newsh = {}
    start = 0
    for i in f:
        offset = int(sh[str(i)][1]) - i
        newsh[str(start)] = sh[str(i)]
        newsh[str(start)][1] = str(start + offset)
        start = start + offset
        
    # Dump the updated index on disk
    fsi.seek(0, 0)
    dump(newsh, fsi)
    fsi.close()

    # Record the number of unread messages in the source folder
    unread_s = unread(newsh)

    # Return a tuple containing the number of unread messages in
    # the source and destination folder
    return (unread_s, unread_d)


##
## Function copy_messages (src fld, dst fld, msg instance)
##
##    Physically copy messages from one folder to another.
##
##
def copy_messages(src, dst, msgs):
    from marshal import load, dump

    fs = open(src) # Source message file
    fd = open(dst, "a") # Dest message file
    di = get_index_from_pathname(dst)
    fdi = open(di, "r+") # Dest index

    fldcache = {}
    sh = load(fdi)
    for idx in msgs:
        # Copy the complete message to the dest folder
        start = int(idx[1])
        stop = int(idx[2])
        fs.seek(start, 0)
        pos_start = int(fd.tell())
        fd.write( fs.read(stop-start) )
        pos_end = int(fd.tell())
        fldcache[start] = stop

        # Update the dest folder index for the dest folder in the dest
        # 0-epoch 1-start 2-stop 3-status 4-from 5-subject 6-time
        sh[str(pos_start)] = [ idx[3], str(pos_end), idx[5], idx[4], idx[0] ]

    # Dest index file and folder are now finished
    fdi.seek(0, 0)
    dump(sh, fdi)
    fd.close()
    fdi.close()

    # Record the number of unread messages in the destination folder
    unread_d = unread(sh)
    
    return unread_d


##
## Function del_messages (prefs instance, src fld, dst fld, msg instance)
##
##    Physically delete messages from one folder.
##
##
def del_messages(src, msgs):
    from marshal import load, dump

    fs = open(src)                    # Source message file
    si = get_index_from_pathname(src) # Source index path
    fsi = open(si, "r+")              # Source index file

    fldcache = {}

    for idx in msgs:
        # Find out where the messages starts and stops
        start = int(idx[1])
        stop = int(idx[2])
        fldcache[start] = stop
    
    # Update the src folder file, omitting deleted messages.
    # We write a new src file but do not include the message
    # boundaries that are listed in 'fldcache'
    newfs = open(src+".new", "w")
    fs.seek(0, 0)
    e = fldcache.keys()
    e.sort()
    cur = 0
    for ent in e:
        newfs.write( fs.read(ent-cur) )
        cur = fldcache[ent]
        fs.seek(cur, 0)
        
    # Omitting messages completed, now write the rest
    newfs.write( fs.read() )
    newfs.close()
    fs.close()
    
    # Rename the updated folder with the original folder name
    os.rename(src+".new",  src)

    # Last, update the source index file
    e = fldcache.keys()
    e.sort()
    sh = load(fsi)
    f = map( lambda x: int(x), sh.keys() )
    f.sort()

    # If we are deleting all folder entries, just reset the contents of
    # the index file to an empty dictionary
    if e == f:
        fsi.seek(0, 0)
        dump({}, fsi)
        fsi.close()
        return 0

    # Delete cached entries
    for i in e:
        f.remove(i)

    # Rebuild the index by updating relative positions between messages
    newsh = {}
    start = 0
    for i in f:
        offset = int(sh[str(i)][1]) - i
        newsh[str(start)] = sh[str(i)]
        newsh[str(start)][1] = str(start + offset)
        start = start + offset
        
    # Dump the updated index on disk
    fsi.seek(0, 0)
    dump(newsh, fsi)
    fsi.close()

    # Record the number of unread messages in the source folder
    unread_s = unread(newsh)

    return unread_s

##
## Function create_folder_index (pathname, file start position)
##
##    Create an index file of a mail folder.
##
##
def create_folder_index(pathname, start=0):
    import pygmymailbox, marshal, string

    # Make sure all the dirs exists
    path = '/'
    dirs = string.split(os.path.dirname(pathname), '/')
    for dir in dirs:
        path = os.path.join(path, dir)

        # Do we need to make this dir?
        if not os.path.exists(path):
            #print 'create_folder_index - making dir:', path
            os.mkdir(path)
        
    # Retrieve the folder name information
    f = open(pathname)

    idx = get_index_from_pathname(pathname)

    # Cache the index file in memory to decrease number of disk-writes
    try:
        sh = marshal.load( open(idx) )
    except:
        sh = {}

    mb = pygmymailbox.PygmyMailbox(f, start)
    # Loop over the mailbox, extract header information
    while 1:
        m = mb.next()
        if m is None:
            break
        # Get the From: field (some From headers span more than one line)
        frm = get_from(m)
        # Get the Subject: field
        subject = get_subject(m)
        # Get the Date: field
        date = date_to_epoch(m)
        # Put entries into index file
        sh[str(int(m.fp.start))] = [ STATUS_UNREAD, str(int(m.fp.stop)),
                                     subject, frm, date ]
    f.close()
    # Marshal the index and store it on disk
    marshal.dump(sh, open(idx, "w"))
    # Return the number of unread messages in the folder
    return unread(sh)


##
## Helper functions for the indexer
##
def get_from(message):
    import mimify, string
    # Get the From: field (some From headers span more than one line)
    if message.getheader('from') != None:
        t = string.replace(message.getheader('from'), '\012', '')
        message.__delitem__('from')
        message.__setitem__('from', t)
    name, addr = message.getaddr('from')
    return mimify.mime_decode_header(name or ""), addr


def get_subject(message):
    import mimify, string
    # Get the Subject: field
    subject = mimify.mime_decode_header(message.getheader('subject') or "")
    # Remove problematic characters like newline
    subject = string.replace(subject, '\n', '')
    subject = string.replace(subject, '\r', '')
    return subject


def date_to_epoch(message):
    import time, rfc822
    # Get the Date: field
    date = message.getdate_tz('date') or ""
    if date != "":
        # Convert to epoch value
        try:
            date = rfc822.mktime_tz(date)
        except:
            date = time.time()
    else:
        # If we cannot parse the date, insert current epoch
        date = time.time()
    # Make an integer since the float conversion is locale specific
    return str(int(date))


##
## Function num_unread (foldername)
##
##    Number of messages in a folder (typed numbers).
##
##
def unread(fld):
    num = 0
    for n in fld.keys():
        if fld[n][0] == STATUS_UNREAD:  
            num = num + 1
    return num
    
def num_unread(pathname):
    sh = fetch_folder_index(pathname)
    return unread(sh)

def num_readunread(pathname):
    sh = fetch_folder_index(pathname)
    return len(sh.keys()), unread(sh)

def num_msgs(pathname):
    sh = fetch_folder_index(pathname)
    return len(sh.keys())


##
## Function update_folder_index (path, foldername)
##
##    New messages need to be indexed in an existing folder.
##
##
def update_folder_index(pathname):
    sh = fetch_folder_index(pathname)
    f = map(lambda x: int(x), sh.keys())
    f.sort()
    # Need this one in case the dict is empty at startup
    if len(f) == 0:
        start = 0
    else:
        start = int( sh[str(f[-1])][1] )
    # Recreate the folder index at the given start position
    return create_folder_index(pathname, start)    



##
## Function update_folder_index_status (path, foldername,
##                                      file start, status)
##
##    Update the status field in an index file.
##
##
def update_folder_index_status(pathname, start, update=STATUS_READ):
    import marshal

    # Read folder index from disk
    index = get_index_from_pathname(pathname)
    fi = open(index, "r+")
    sh = marshal.load( fi )
    sh[str(start)][0] = update
    fi.seek(0, 0)
    marshal.dump(sh, fi)
    fi.close()

    return unread(sh)


##
## Function update_folder_index_status_multiple (path, msg)
##
##    Update the status field for multiple messages in an index file.
##
##
def update_folder_index_status_multiple(path, msg):
    import marshal

    # Read folder index from disk
    index = get_index_from_pathname(path)
    fi = open(index, "r+")
    sh = marshal.load( fi )
    for start, update in msg:
        sh[str(start)][0] = update
    fi.seek(0, 0)
    marshal.dump(sh, fi)
    fi.close()

    return unread(sh)


##
## Function fetch_folder_index (path, foldername)
##
##    Fetch folder index file contents.
##
##
def fetch_folder_index(pathname):
    import marshal

    # Read folder index from disk and unmarshal it
    index = get_index_from_pathname(pathname)
    sh = marshal.load(open(index))
    # Return the result
    return sh


##
## Function check_index_consistency (prefs instance)
##
##    Make index files consistent with folder contents.
##
##
def check_index_consistency(path, ftree):
    from posix import stat

    # Ensure that the index files are updated
    for fname in ftree.keys():
        tf = stat(path+"/"+fname)[8]
        ti = stat(path+"/."+fname+".idx")[8]
        # Check if the message folder is newer than the index
        if tf > ti:
            # Remove old index
            os.remove(path+"/."+fname+".idx")
            # Create new index -- will lose status but may save the msgs
            create_folder_index(os.path.join(path, fname))
            
        # Check subtree if any
        subtree = ftree[fname]
        if subtree:
            check_index_consistency(os.path.join(path, fname + '.sbd'),
                                    subtree)
        

##
## Function empty_trash (prefs instance)
##
##    Empty the 'trash' folder.
##
##
def empty_trash(p):
    import marshal

    # Zero the folder file
    open(p.folders+"/trash", "w").close()
    # Dump an empty dict in the index file
    marshal.dump({}, open(p.folders+"/.trash.idx", "w") )


##
## Method get_folder_path ()
##
def get_folder_path(path, folder):
    import string
    
    real_path = ''
    folders = os.path.dirname(folder)
    for f in string.split(folders, '/'):
        if f != '':
            real_path = os.path.join(real_path, f + '.sbd')
        real_path = os.path.join(path, real_path)

    return real_path


def get_folder_pathname(path, folder):
    real_path = get_folder_path(path, folder)
    name = os.path.basename(folder)
    pathname = os.path.join(real_path, name)

    return pathname


def get_index_from_pathname(pathname):
    path = os.path.dirname(pathname)
    name = os.path.basename(pathname)

    index = os.path.join(path, '.' + name + '.idx')
    
    return index

def folder_expanded(path, folder):
    realpath = get_folder_path(path, os.path.join(folder, ''))
    if os.path.isfile(os.path.join(realpath, '.expanded')):
	return 1
    
    return 0

def expand_folder(path, folder, state=1):
    realpath = get_folder_path(path, os.path.join(folder, ''))
    pathname = os.path.join(realpath, '.expanded')
    if state:
	# Touch file
	f = open(pathname, 'w')
	f.close()
    else:
	# Remove file
	try:
	    os.unlink(pathname)
	except: pass
    
