## $Id: folderops.py,v 1.21 2001/11/07 11:26:48 kjetilja Exp $ ## Folder status flags STATUS_READ = '-' STATUS_UNREAD = ' ' STATUS_FORW = 'f' STATUS_REPL = 'r' import os.path, marshal 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 create_foldernames(ftree): ## ## This is to change FolderTree list (such as self.ftree) to FolderNames, ## i.e. add up parentDir to childDir as: "parentDir/childDir" ## def create_foldernames(ftree): if len(ftree)==0: return keys = ftree.keys() foldernames = keys [:] # [:] is to strong copy list for k in keys: if ftree[k]: # is a directory, not mail folder tmp = create_foldernames (ftree[k]) for i in range(len(tmp)): tmp[i] = '%s/%s' % (k, tmp[i]) foldernames = foldernames + tmp return foldernames ## ## 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): unread_d = copy_messages (src, dst, msgs) unread_s = del_messages (src, msgs) # 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): 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 = marshal.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) marshal.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): 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 = marshal.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) marshal.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) marshal.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, 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) frm = message.getheader('from') if frm != None: t = string.replace(frm, '\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): # 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): # 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): # 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): # 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