## $Id: folder.py,v 1.156 2001/10/15 12:41:45 kjetilja Exp $

## System modules
import rfc822, time, os, FCNTL, string, re, mimify, marshal
import signal
from fcntl import flock

try:
    from gtk import *
    from gnome.ui import *
except ImportError:
    print "Searched, but could not find the gnome-python modules in:"
    import sys
    for locs in sys.path:
        print locs
    print "\nYou must install gnome-python in order to use Pygmy.\nYou can download gnome-python at ftp.gnome.org.  Or, if you're using Debian: apt-get install python-gnome"""
    import sys
    raise SystemExit
from GDK import _2BUTTON_PRESS, BUTTON_PRESS, BUTTON1_MASK, KEY_PRESS, KEY_RELEASE, Up, Down, Return

## Local modules
import edit, prefs, msg, folderops, addresslist, newmail
import headers, mime, filter, pygmymailbox, sendmail, privacy
from filter import ENTRY_TX, ENTRY_IN, ENTRY_AC, ENTRY_TO, ENTRY_RE
from folderops import STATUS_UNREAD, STATUS_READ
from prefs import WIN_SIZE, FLD_SIZE, MSG_SIZE

## Error messages from the GUI
err1 = """Error opening destination file."""
err2 = """The message you tried to view cannot be parsed!"""
err3 = """The message you tried to reply/forward cannot be parsed!"""
err6 = """You cannot create a new folder in the default folder hierarchy!

Select a folder in the "User Folders" hierarchy, or the 
"User Folders" folder itself if you have not added any
folders previously."""
err7 = """The folder %s does not exist!

You need to update your filter settings
before running filters again.
"""

## Information messages from the GUI
info1 = """Cannot complete operation while
other folder operations are running!

Wait until the current folder operations
are finished and retry.
"""
info2 = "You appear to be editing mail.  Quitting now may cause\n"\
        "the mails you are editing to be lost.\n\n"\
        "Are you sure you want to quit?"
info3 = "Really empty the trash folder?"

## Search types
SEARCH_BODY    = 1
SEARCH_SUBJECT = 2
SEARCH_FROM    = 4


##
## Main folder window class
##
##
class FolderWindow:

    ##
    ## Method __init__ (self, prefs instance)
    ##
    ##    Folder widget constructor.
    ##
    ##
    def __init__(self, prefs=None, ftree=None, args=[]):
	self.win = GnomeApp('Pygmy', ':Pygmy')
	self.win.set_wmclass('pygmy', ':Pygmy')
	self.win.connect('delete_event', self.quit)
	self.win.connect('destroy', self.quit)
        self.win.connect_after('size_allocate', self.resize)
	self.win.set_border_width(5)
        apply(self.win.set_default_size, tuple(prefs.size[WIN_SIZE]))
        self.win.set_policy(1, 1, 0)

    	self.vbox = GtkVBox()
	self.vbox.set_spacing(5)
        self.appbar = GnomeAppBar()
        self.appbar.show()

        # Create the security stuff
	self.privacy = privacy.Privacy(self.appbar, self.win)

        # Use supplied preferences if instance supplied
        if prefs == None:  self.prefs = prefs.Preferences()
        else:  self.prefs = prefs

        # Check for command line arguments
        self.args = args
        if len(self.args) > 1:
            self.hidemain = 1
            self.composeaddr = args[1]
            self.oldmailnotify = self.prefs.mailnotify
            self.prefs.mailnotify = 0
        else:
            self.hidemain = 0
            self.composeaddr = ''

        # Set some instance variables
        self.sortcolumn = 0
        self.mbox = 'inbox'
        self.active_folder = 'inbox'
        self.row_count = 0
        self.needs_update = []
        self.invoked_external = 0
        self.button_pressed = 0
        self.key_updown_pressed = 0
        self.adrlist_active = 0
        self.filters_active = 0
        self.unsafe = 0
        self.new_mail_win = 1
        self.ftree = ftree
	self.search_row = 0
        self.search_idx = -1 
        self.search_option = SEARCH_BODY | SEARCH_SUBJECT # Default
        self.row = 0
        self.msgwin = None
        self.unread = {}
        self.total = {}
        self.num_edits = 0        

        # Initialize font settings
        self.init_fonts()        
        
        # Init compiled filters
        self.filters_re = {}
        try:
            filters = marshal.load(open(self.prefs.flistfile))
        except:
            filters = []
        self.set_filters(filters)
        
        # Create widgets and fill them
	self.win.create_menus( self.create_menu() )
	self.win.create_toolbar( self.create_toolbar() )
        self.searchbar = self.create_search()
        self.win.add_docked(self.searchbar, 'Search', DOCK_ITEM_BEH_NORMAL,
                            DOCK_TOP, 2, 0, 0)
        self.win.set_statusbar(self.appbar)
        self.init_total_and_unread()

        # Make some space and panes
        c = GtkVBox()
        c.set_border_width(2)
        c.show()
        self.vbox.pack_start(c, expand=FALSE)
        self.hpaned = GtkHPaned()
        self.vbox.pack_start(self.hpaned)
        self.vpaned = GtkVPaned()
        self.hpaned.add2(self.vpaned)
       
        self.init_pixmaps()
	self.init_folderview()
        self.init_foldertree()

        # Termination handlers - catch signals
        signal.signal(signal.SIGTERM, self.quit)
        signal.signal(signal.SIGINT, self.quit)

        # Start the show
	self.vbox.show()
        self.hpaned.show()
        self.vpaned.show()
        self.win.set_contents(self.vbox)
        if self.hidemain == 0:
            self.win.show()
        else:
            edtwin = edit.EditWindow(self, edit.MAILTO, mailto=self.composeaddr)
            edtwin.setup_widgets()
            self.update_foldertree()

	# Ensure the window is up before we start fetching new mail
        # otherwise the progressbar won't update itself
        self.init_mail_check()


    ## Initialize fonts
    def init_fonts(self):
	# Make bold folder font
	tmp = string.split(self.prefs.fld_font, '-')
	tmp[3] = 'bold'
	bf = string.join(tmp, '-')
	try:
            load_font(bf)
	except:
            tmp[3] = 'medium'
            bf = string.join(tmp, '-')
        self.fld_nfont = load_font(self.prefs.fld_font)
	self.fld_bfont = load_font(bf)

	# Make bold folder font
	tmp = string.split(self.prefs.sub_font, '-')
	tmp[3] = 'bold'
	bf = string.join(tmp, '-')
	try:
            load_font(bf)
	except:
            tmp[3] = 'medium'
            bf = string.join(tmp, '-')

	self.sub_nfont = load_font(self.prefs.sub_font)
	self.sub_bfont = load_font(bf)
        self.comp_nfont = load_font(self.prefs.compose_font)


    ## Unsupported features should use this function to notify users
    def unsupported(self, msg):
        w = GnomeOkDialog(msg)
        w.set_parent(self.win)
        w.show()


    # Create search dock band
    def create_search(self):
        menues = [('Body and subject', SEARCH_BODY | SEARCH_SUBJECT), 
                  ('Body', SEARCH_BODY), 
                  ('Subject', SEARCH_SUBJECT), 
                  ('From', SEARCH_FROM)]
        h = GtkHBox()
        h.set_border_width(2)
        l = GtkLabel(' Search ')
        l.show()
        h.pack_start(l, expand=FALSE)
        c = GtkOptionMenu()
        c.show()
        m = GtkMenu()

        for menu in menues:
            i = GtkMenuItem(menu[0] + ' contains:')
            i.show()
            i.connect('activate', self.search_option_cb, menu[1])
            m.append(i)

        c.set_menu(m)
        h.pack_start(c, expand=FALSE)
        e = GnomeEntry()
        e.show()
        e.set_history_id('search')
        e.load_history()
        self.search_entry = e.gtk_entry()
        self.search_entry.connect('activate', self.search)
        h.pack_start(e, expand=TRUE)
        h.show() 
        return h

        
    # Called when the user selects a specific search option
    def search_option_cb(self, w=None, option=None):
        self.search_option = option 

    def search_check_subject(self, m, text, row):
        subject = self.fdisp.get_text(row, 5)
        return string.find(string.lower(subject), text)
        
    def search_check_from(self, m, text, row):
        _from = self.fdisp.get_text(row, 4)
        return string.find(string.lower(_from), text)
        
    def search_check_body(self, m, text, row, offset):
        if m.getmaintype() == 'multipart':
            body = ''
            try:
                p = msg.find_all_parts(m.getbodyparts(), new_parts=[])
                for sm in p:
                    type = sm.gettype()
                    # Display some types directly
                    if type == 'text/plain' or type == 'message/rfc822':
                        # Plain text
                        body = body + sm.getbodytext()
            except:
                pass # Just ignore mail errors
        else:
            # Single, plain message
            m.fp.seek(m.startofbody)
            body = m.fp.read(int(m.stop)-m.startofbody)
        return string.find(string.lower(body), text, offset)

    def __search(self, button=None, row=0, offset=0):
        result = 0

        text = self.search_entry.get_text()
        if not text:
            return
        text = string.lower(text) 
        self.appbar.set_status('Searching for %s (please wait ...)' % text)
        
        # Open folder and seek to the start of the message
        pathname = folderops.get_folder_pathname(self.prefs.folders, self.mbox)
        f = open(pathname)

        # Check all mails in folder
        for row in range(self.numrows)[row:]:
            # First find the message boundary in the folder
            start = int(self.fdisp.get_text(row,1))
            stop = int(self.fdisp.get_text(row,2))

            f.seek(start)

            # Need to instantiate a mime message here to capture attachments
            m = mime.Message(f, start, stop)

            # Update progress bar
            self.appbar.set_progress(float(row)/float(max(self.numrows-1, 1)))
            
            # Update GUI so we can watch the progressbar. 
            while events_pending():
                mainiteration(FALSE)
            
            result = 0
            msg = []
            o = self.search_option
            if o & SEARCH_BODY:
                idx =  self.search_check_body(m, text, row, offset)
                if idx != -1:
                    result = result | SEARCH_BODY
                    msg.append('body')
            if o & SEARCH_SUBJECT:
                sub_pos = self.search_check_subject(m, text, row)
                if sub_pos != -1:
                    result = result | SEARCH_SUBJECT
                    msg.append('subject')
            if o & SEARCH_FROM:
                hit = self.search_check_from(m, text, row)
                if hit != -1:
                    result = result | SEARCH_FROM
                    msg.append('from')
            if result:
                self.fdisp.unselect_row(self.row, 0)
                self.fdisp.select_row(row, 0)
                self.fdisp.moveto(row=row)
                self.view_mail(row, inline=1)
                if self.msgwin.gtkhtml != 1:
                    self.msgwin.msg_text.select_region(0,0)
                while events_pending():
                    mainiteration(FALSE)
                # Check if we should hightlight some body text
                if result & SEARCH_BODY:
                    if self.msgwin.gtkhtml != 1:
                        self.msgwin.msg_text.set_position(idx+len(text))
                        self.msgwin.msg_text.select_region(idx, idx+len(text))    
                    self.search_idx = idx
                # If the hit is in the subject only, then we should move on
                if ((result & SEARCH_SUBJECT) or (result & SEARCH_FROM)) and \
                   not (result & SEARCH_BODY):
                    self.search_row = row+1
                else:
                    self.search_row = row
                    
                self.appbar.set_status('Found match in %s' % string.join(msg, ' and '))
                break
                
            # Reset offset, so we search from start of body again
            offset = 0
            if self.msgwin:
                if self.msgwin.gtkhtml != 1:
                    self.msgwin.msg_text.select_region(0, 0)    
                
            self.search_row = self.search_row + 1
            self.search_idx = -1
            
        if not result:        
            if self.search_row:
                self.appbar.set_status('No more hits')
            else:
                self.appbar.set_status('No hits')
        f.close()


    def search(self, button=None):
        # Check if we need to wrap around
        if self.search_row == self.numrows:
            self.search_row = 0
        
        self.__search(row=self.search_row, offset=self.search_idx+1) 

    
    ##
    ## Method init_pixmaps (self)
    ##
    ##    Open treeview icons.
    ##
    ##
    def init_pixmaps(self):
        from prefix import PYGMY_ICONDIR
        # Create a pixmaps
        self.open_dir, self.open_dir_mask = create_pixmap_from_xpm(self.win, None,
                                                                   PYGMY_ICONDIR+"/dir-open.xpm")
        self.close_dir, self.close_dir_mask = create_pixmap_from_xpm(self.win, None,
                                                                     PYGMY_ICONDIR+"/dir-close.xpm")
        self.trash, self.trash_mask = create_pixmap_from_xpm(self.win, None,
                                                             PYGMY_ICONDIR+"/trash.xpm")
        self.inbox, self.inbox_mask = create_pixmap_from_xpm(self.win, None,
                                                             PYGMY_ICONDIR+"/inbox.xpm")
        self.outbox, self.outbox_mask = create_pixmap_from_xpm(self.win, None,
                                                               PYGMY_ICONDIR+"/outbox.xpm")
        self.empty, self.empty_mask = create_pixmap_from_xpm(self.win, None,
                                                             PYGMY_ICONDIR+"/tray_empty.xpm")
        self.full, self.full_mask = create_pixmap_from_xpm(self.win, None,
                                                           PYGMY_ICONDIR+"/tray_full.xpm")

    ##
    ## Method create_menu (self)
    ##
    ##    Create the menu elements.
    ##
    ##
    def create_menu(self):
        from prefix import PYGMY_ICONDIR
        
        file_menu = [
            UIINFO_ITEM_STOCK('Save As...', None, self.saveas, STOCK_MENU_SAVE_AS),
            UIINFO_SEPARATOR,
            UIINFO_ITEM_STOCK('Get New Messages', None, self.mail_check,
                              STOCK_MENU_MAIL),
            UIINFO_SEPARATOR,
            UIINFO_ITEM_STOCK('Quit', None, self.quit, STOCK_MENU_QUIT)
            ]
        mark_menu = [
            UIINFO_ITEM_STOCK('Mark Read', None, self.mark_read,
                              STOCK_MENU_BLANK),
            UIINFO_ITEM_STOCK('Mark All Read', None, self.mark_all_read,
                              STOCK_MENU_BLANK),
            UIINFO_ITEM_STOCK('Mark Unread', None, self.remove_marks,
                              STOCK_MENU_BLANK)
            ]        
        edit_menu = [
            UIINFO_ITEM_STOCK('Select All', None, self.sel_all, STOCK_MENU_BLANK),
            UIINFO_ITEM_STOCK('Unselect All', None, self.usel_all, STOCK_MENU_BLANK),
            UIINFO_SEPARATOR,
            UIINFO_ITEM_STOCK('Preferences', None, self.set_prefs, STOCK_MENU_PREF)
            ]
        folder_menu = [
            UIINFO_ITEM_STOCK('New', None, self.add_folder_callback,
                              STOCK_MENU_NEW),
            UIINFO_ITEM_STOCK('Rename', None, self.rename_folder_callback,
                              STOCK_MENU_BLANK),
            UIINFO_ITEM_STOCK('Delete', None, self.delete_folder_callback,
                              STOCK_MENU_CUT),
            UIINFO_SEPARATOR,
            UIINFO_ITEM_STOCK('Run Filters', None, self.do_run_filters,
                              STOCK_MENU_EXEC),
            UIINFO_ITEM_STOCK('Rebuild Index', None, self.rebuild_index,
                              STOCK_MENU_EXEC),
            UIINFO_ITEM_STOCK('Empty Trash', None, self.empty_trash,
                              STOCK_MENU_TRASH)
            ]
        msg_menu = [
            UIINFO_ITEM_STOCK('Compose', None, self.new_callback,
                              STOCK_MENU_MAIL_NEW),
            UIINFO_ITEM_STOCK('Reply', None, self.reply_callback,
                              STOCK_MENU_MAIL_RPL),
            UIINFO_ITEM_STOCK('Reply All', None, self.replyall_callback,
                              PYGMY_ICONDIR+"/reply_to_all_menu.xpm"),
            UIINFO_ITEM_STOCK('Forward', None, self.forward_callback,
                              STOCK_MENU_MAIL_FWD),
            UIINFO_ITEM_STOCK('Delete', None, self.trash_msg,
                              STOCK_MENU_TRASH), 
            UIINFO_ITEM_STOCK('View All Headers', None, self.view_hdr,
                              STOCK_MENU_OPEN),
            UIINFO_SEPARATOR,
            UIINFO_ITEM_STOCK('Move', None, self.move_msg,
                              PYGMY_ICONDIR+"/move_message.xpm"),
            UIINFO_SEPARATOR,
            UIINFO_ITEM_STOCK('Next', None, self.next_msg_callback,
                              STOCK_MENU_FORWARD), 
            UIINFO_ITEM_STOCK('Prev', None, self.prev_msg_callback,
                              STOCK_MENU_BACK), 
            UIINFO_SEPARATOR,
            UIINFO_SUBTREE_STOCK('Mark', mark_menu, PYGMY_ICONDIR+"/mark.xpm")
           ]
        tools_menu = [
            UIINFO_ITEM_STOCK('Address List', None, self.addresslist, STOCK_MENU_BLANK),
            UIINFO_ITEM_STOCK('Filter Editor', None, self.filters, STOCK_MENU_BLANK),
            ]
        help_menu = [
            UIINFO_ITEM_STOCK('A_bout', None, self.about, STOCK_MENU_ABOUT),
            ]
        menu_info = [
            UIINFO_SUBTREE('File', file_menu),
            UIINFO_SUBTREE('Edit', edit_menu),
            UIINFO_SUBTREE('Folder', folder_menu),
            UIINFO_SUBTREE('Message', msg_menu),
            UIINFO_SUBTREE('Tools', tools_menu),
            UIINFO_SUBTREE('Help', help_menu),
            ]

        return menu_info


    ##
    ## Method create_toolbar (self)
    ##
    ##    Create the toolbar elements.
    ##
    ##
    def create_toolbar(self):
        from prefix import PYGMY_ICONDIR

        toolbar_info = [
            UIINFO_ITEM_STOCK('Get Mail', None, self.mail_check,
                              STOCK_PIXMAP_MAIL),
            UIINFO_SEPARATOR,
            UIINFO_ITEM_STOCK('Compose', None, self.new_callback,
                              STOCK_PIXMAP_MAIL_NEW),
            UIINFO_ITEM_STOCK('Reply', None, self.reply_callback,
                              STOCK_PIXMAP_MAIL_RPL),
            UIINFO_ITEM_STOCK('Reply All', None, self.replyall_callback,
                              PYGMY_ICONDIR+"/reply_to_all.xpm"),
            UIINFO_ITEM_STOCK('Forward', None, self.forward_callback,
                              STOCK_PIXMAP_MAIL_FWD),
            UIINFO_SEPARATOR,
            UIINFO_ITEM_STOCK('Prev', None, self.prev_msg_callback,
                              STOCK_PIXMAP_BACK),
            UIINFO_ITEM_STOCK('Next', None, self.next_msg_callback,
                              STOCK_PIXMAP_FORWARD),
            UIINFO_SEPARATOR,
            UIINFO_ITEM_STOCK('Delete', None, self.trash_msg,
                              STOCK_PIXMAP_TRASH),
            ]
        return toolbar_info


    ## Callback handling for empty trash stuff
    def handle_trash_cb(self, no):
        if no == 0:
            # Empty trash folder file
            folderops.empty_trash(self.prefs)
            # Reset number of unread/total messages to 0
            self.unread['trash'] = self.total['trash'] = 0
            # Update the display
            self.update_folderview()
            self.update_foldertree()


    ## Empty trash confirmation dialog
    def empty_trash(self, b=None, a=None):
        if not self.issafe():
            return
        v = GnomeQuestionDialog(info3, self.handle_trash_cb, self.win)
        v.show()


    ## Race conditions may occor on lengthy folder ops, so these methods
    ## should be used to ensure safety
    def set_unsafe(self):
        if self.unsafe == 0:
            self.unsafe = 1
        else:
            print 'tried to set_unsafe when already set'

    def clear_unsafe(self):
        if self.unsafe == 1:
            self.unsafe = 0
        else:
            print 'tried to clear_unsafe when already set'

    def issafe(self):
        if self.unsafe == 1:
            w = GnomeOkDialog(info1)
            w.set_parent(self.win)
            w.show()
            return 0
        else:
            return 1

    ## If there are still open edit windows, get exit confimation from user
    def query_quit(self):
        v = GnomeQuestionDialog(info2, self.handle_query_quit, self.win)
        v.show()

    def handle_query_quit(self, no):
        if no == 0:
            # Pressed yes
            self.num_edits = 0
            self.quit()
        
               
    ##
    ## Function quit (self)
    ##
    ##    Quit window callback.
    ##
    ##
    def quit(self, b=None, a=None):
        # Do not quit if folder ops are running since it may cause
        # folder corruption
        if not self.issafe():
            return
        
        # Check if there are open edit windows
        if self.num_edits > 0:
            # Popup a request
            self.query_quit()
            return
        
        # Close address list window
        if self.adrlist_active:
            self.adrlist.destroy()

        # Trash messages
        if self.prefs.trash == 1:
            folderops.empty_trash(self.prefs)

        # Close message viewers and cleanup
        if self.msgwin != None:
            self.msgwin.destroy()

        # Get the size values for the folder listing
        fsize = [self.fdisp.get_column_width(3),
                 self.fdisp.get_column_width(4),
                 self.fdisp.get_column_width(5),
                 self.fdisp.get_column_width(6),
                 self.fdisp.get_column_width(7)]
        self.prefs.size = self.prefs.size[:3] + [fsize]

        # Save the preferences (to save resize)
        self.prefs.save()

        # Crank out for real
        self.win.destroy()
        mainquit()


    ##
    ## Method resize (self)
    ##
    ##    Upon resize, store the size of the folder view and main window
    ##
    ##
    def resize(self, w=None, a=None, c=None):
        size = w.get_allocation()
        if w == self.win:
            # Main window resize
            self.prefs.size[WIN_SIZE] = size[2:4]
        else:
            # Folder window resize
            self.prefs.size[FLD_SIZE] = size[2:4]


    ##
    ## Method view_hdr (self)
    ##
    ##    View all headers.
    ##
    ##
    def view_hdr(self, b=None):
        if not self.msgwin:
            return
        headers.HeaderWindow(self.msgwin.hdr, self.win)


    ##
    ## Method saveas (self, b)
    ##
    ##    Save one or more messages to disk.
    ##
    ##
    def saveas(self, b):
        # Use this to store multiple save objects, mostly needed by
        # the folder window multiple selection stuff
        savecache = {}

        # Do a context check on which window is open
        # Find whether the user has selected some messages to save
        if self.fdisp.selection == []:
            return None
        name = self.mbox
        # Now, get the message boundaries and make a copy to save
        for row in self.fdisp.selection:
            savecache[ int(self.fdisp.get_text(row, 1)) ] =\
                       int(self.fdisp.get_text(row, 2))

        # Loop through all the messages that should be saved
        for s in savecache.keys():
            # Get the file name from the user
            import fileops
            fname = fileops.getfilename( self.prefs.filepaths, '', 'Save message to file...', 'save-message' )
            if fname == None:  return
            try:
                f = open(fname, 'w')
            except:
                # Unable to open file, show error
                w = GnomeErrorDialog(err1)
                w.set_parent(self.win)
                w.show()
            else:
                # Blast the message into the file
		pathname = folderops.get_folder_pathname(self.prefs.folders, self.mbox)
		m = open(pathname)
                m.seek(s)
                f.write(m.read(savecache[s]-s))            
                m.close()
                f.close()
    

    ##
    ## Method set_prefs (self, b)
    ##
    ##    Invoke preferences window.
    ##
    ##
    def set_prefs(self, b):
        pwin = prefs.PreferencesWindow(self.prefs, self.win)
        pwin.mainloop()


    ##
    ## Method {sel|usel}_all (self, b)
    ##
    ##    Select/Unselect all functions.
    ##
    ##
    def sel_all(self, b=None):
        if self.numrows > 0:
            # GTK segfaults on zero entries
            self.fdisp.select_all()

    def usel_all(self, b=None):
        if self.numrows > 0:
            # GTK segfaults on zero entries
            self.fdisp.unselect_all()

    ##
    ## Method about (self, *item)
    ##
    ##    The infamous About callback.
    ##
    ##
    def about(self, *item):
        aboutwin = GnomeAbout('Pygmy', 'v'+self.prefs.version,
                              'Copyright (C), 1999-2001',
                              ['Kjetil Jacobsen <kjetilja@cs.uit.no>',
                               'Dag Brattli <dag@brattli.net>',
                               'Jason Hildebrand <jason@peaceworks.ca>'],
                              'GNOME Mail Client.\n\
                               \nPygmy Homepage:\n\
        http://pygmy.sourceforge.net/')
        aboutwin.set_parent(self.win)
        aboutwin.show()


    ##
    ## Method __init_total_and_unread ()
    ##
    def __init_total_and_unread(self, path, ftree):
        # Make entries in the 'total' and 'unread' dictionary
        for fname in ftree.keys():
            folder = os.path.join(path, fname)
            pathname = folderops.get_folder_pathname(self.prefs.folders,
                                                     folder)
            self.unread[folder] = folderops.num_unread(pathname)
            self.total[folder]  = folderops.num_msgs(pathname)

            if ftree[fname] != None:
                next_path = os.path.join(path, fname)
                self.__init_total_and_unread(next_path, ftree[fname])

    ##
    ## Method init_total_and_unread ()
    ##
    def init_total_and_unread(self):
        from posixpath import split
        # Fetch active folders files
        ftree = folderops.get_active_folders(self.prefs.folders)

        self.__init_total_and_unread('', ftree)


    ##
    ## Method init_foldertree ()
    ##
    def init_foldertree(self):
        self.column_headers = ['Name', 'Total', 'Unread']
        defsizes = [155, 30, 37]
        self.tdisp = GtkCTree(3, 0, self.column_headers)
        self.tdisp.connect('select_row', self.tree_row_doubleclick)
        self.tdisp.connect_after('tree-expand', self.tree_expand)
	self.tdisp.connect_after('tree-collapse', self.tree_collapse)
        self.tdisp.connect('drag_data_received', self.tree_drag_data_received)
        self.tdisp.connect_after('tree_select_row', self.tree_row_selected)
        self.tdisp.drag_dest_set(DEST_DEFAULT_ALL,
                                 [('application/x-pygmy', 0, -1)],
                                 GDK.ACTION_COPY|GDK.ACTION_MOVE)
        
        self.tdisp.set_line_style(CTREE_LINES_DOTTED)
        self.tdisp.column_titles_hide()
        
        # Uncomment these if you want to see Total/Unread columns
        self.tdisp.set_column_visibility(1, 0)
        self.tdisp.set_column_visibility(2, 0)
        for i in range(len(self.column_headers)):
            self.tdisp.set_column_width(i, defsizes[i])
        
        self.tdisp.show()

        swin = GtkScrolledWindow()
        apply(swin.set_usize, tuple(self.prefs.size[FLD_SIZE]))
        swin.connect_after('size_allocate', self.resize)
        swin.set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC)
        swin.add(self.tdisp)
        swin.show()
        self.hpaned.add1(swin)

        self.update_foldertree()


    ##
    ## Method init_folderview ()
    ##
    def init_folderview(self):
        # Create hidden columns to hold some extra sort info
        self.fdisp = GtkCList(9, ['', '', '', 'S', 'From', 'Subject', 'Date',
                                  'Size', ''])
        self.fdisp.connect('click_column', self.clickcolumn)
        self.fdisp.connect('select_row', self.folderview_select_row)
        self.fdisp.connect('button_press_event', self.folderview_button_press)
        self.fdisp.connect('drag_data_get', self.folderview_drag_data_get)
        self.fdisp.connect('key_press_event', self.folderview_key_event)
        self.fdisp.connect('key_release_event', self.folderview_key_event)
        self.fdisp.drag_source_set(BUTTON1_MASK,
                                   [('application/x-pygmy', 0, 1)],
                                   GDK.ACTION_COPY|GDK.ACTION_MOVE)
        # Widget attributes
        self.fdisp.set_selection_mode(SELECTION_EXTENDED)
        # Set widget size according to the saved values
        if len(self.prefs.size) == 4:
            apply(self.fdisp.set_column_width, (3, self.prefs.size[prefs.LST_SIZE][0]))
            apply(self.fdisp.set_column_width, (4, self.prefs.size[prefs.LST_SIZE][1]))
            apply(self.fdisp.set_column_width, (5, self.prefs.size[prefs.LST_SIZE][2]))
            apply(self.fdisp.set_column_width, (6, self.prefs.size[prefs.LST_SIZE][3]))
            if len(self.prefs.size[prefs.LST_SIZE]) > 4:
                # This is a new option which is upgradable
                apply(self.fdisp.set_column_width, (7, self.prefs.size[prefs.LST_SIZE][4]))
        self.fdisp.set_column_visibility(0, 0)
        self.fdisp.set_column_visibility(1, 0)
        self.fdisp.set_column_visibility(2, 0)
        self.fdisp.set_column_visibility(8, 0)
        # Add vertical scrollbar
        self.swin = GtkScrolledWindow()
        self.swin.set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC)
        self.vpaned.pack1(self.swin)
        self.swin.add(self.fdisp)
        self.swin.show()
        self.fdisp.show()

        # Fill the view with folder contents
        self.update_folderview()


    ##
    ## Method init_mail_check (self)
    ##
    ##    Initialize mail checking
    ##
    ##
    def init_mail_check(self):
        # Store last modification date of the inbox
        self.inboxtime = os.stat(self.prefs.folders+'/inbox')[8]
        # Do a check on the inbox
        self.mail_check(startup=not self.prefs.invoke_at_startup)


    ##
    ## Method invoke_external (self)
    ##
    ##    Invoke external command (for fetching mail and so on)
    ##
    ##
    def invoke_external(self):
        if self.prefs.external_cmd == '' or self.invoked_external:
            return

        # We don't want more that one going at a time, set to indicate
        self.invoked_external = 1
        
        # Fork a client to fetch the stuff and update progress on window
        pid = os.fork()
        if pid == 0:
            os._exit(os.system(self.prefs.external_cmd))
        else:
            self.appbar.set_status('Checking for new mail...')
            pbar = self.appbar.get_progress()
            pbar.set_activity_mode(1)
            round = 0.0
            while 1:
                round = round + 0.01
                if round >= 1.0: round = 0.0
                self.appbar.set_progress(round)
                while events_pending():
                    mainiteration(FALSE)
                (ret, status) = os.waitpid(pid, os.WNOHANG)
                if ret > 0:
                    self.appbar.set_progress(0.0)
                    self.appbar.set_status('Done')
                    pbar.set_activity_mode(0)
                    break
                # We don't want to steal all the CPU
                time.sleep(0.1)
            # Ready for another round
            self.invoked_external = 0


    ##
    ## Method mail_check ()
    ##
    def mail_check(self, button=None, startup=0):
        ## Invoke external command to check for mail
        if not startup:
            self.invoke_external()

        # Determine whether to start running filters from the start of the
        # inbox folder or from the offset of the last seen message
	statinfo = os.stat(self.prefs.folders+'/inbox')
        newmod, filter_start = statinfo[8], statinfo[6]
        indexname = folderops.get_index_from_pathname(self.prefs.folders+'/inbox')
        indexmod =  os.stat(indexname)[8]
        if indexmod < newmod or self.prefs.filter_start != 1:
            # Reset if forced by the configuration or if the inbox file is
            # not consistent with its index file
            filter_start = 0
	    
        ## Local stuff from either the spool file or the inbox folder file
        if self.prefs.spoolfile != None:
            # Check in spool file
            try:  statinfo = os.stat(self.prefs.spoolfile)
            except:
                # The spool does not exist, just bail out
                return TRUE
            newmod, size = statinfo[8], statinfo[6]

        if size > 0:
            if self.prefs.spoolfile != None:
                # Copy spool file messages into the inbox folder
                sp = open(self.prefs.spoolfile, 'r+')
                fo = open(self.prefs.folders+'/inbox', 'a')
                r1 = flock( sp.fileno(), FCNTL.LOCK_EX | FCNTL.LOCK_NB )
                # If we cannot lock, just do nothing
                if r1 == -1:
                    flock( sp.fileno(), FCNTL.LOCK_UN )
                    fo.close()
                    sp.close()
                    return TRUE
                # Got locks -- proceed
                fo.write( sp.read() )
                fo.close()
                # Truncate the spool file
                sp.seek(0, 0)
                sp.truncate()
                flock( sp.fileno(), FCNTL.LOCK_UN )
                sp.close()
                # Have to reupdate mod-time to the last write
                self.inboxtime = os.stat(self.prefs.spoolfile)[8]
            else:
                # Using the previous update time suffices here
                self.inboxtime = newmod

            # Need to update the index to reflect the new messages
            pathname = folderops.get_folder_pathname(self.prefs.folders,
                                                     'inbox')
            self.unread['inbox'] = folderops.update_folder_index(pathname)
            self.total['inbox'] = folderops.num_msgs(pathname)

            # Run filter on inbox
            msg = self.run_filters('inbox', filter_start)

            # If the currently visible folder is the inbox, redisplay
            if self.active_folder == 'inbox':
                self.update_folderview('inbox')
            # Redisplay the folder tree if no filters were run
            if not msg:
                self.update_foldertree()
            # Bring up a notify window to the user
            if self.new_mail_win and self.prefs.mailnotify:
                w = newmail.NewMailWindow(self, msg)
                self.new_mail_win = 0
        else:
            # If the button was pressed and no new mail, give a reply
            if button != None:
                self.appbar.set_status('No new mail!')
        return TRUE


    ##
    ## Method mainloop ()
    ##
    def mainloop(self):
        mainloop()
            

    ##
    ## Method addresslist (self, item)
    ##
    ##    Callback for the address list window.
    ##
    ##
    def addresslist(self, item, edit=None, parent=None):
        if self.adrlist_active:
            return
        self.adrlist_active = 1
        self.adrlist = addresslist.AddressListWindow(self, edit, parent)


    ##
    ## Method filters (self, item)
    ##
    ##    Callback for the filter list window.
    ##
    ##
    def filters(self, item):
        if self.filters_active:
            return
        self.filters_active = 1
        self.filteredit = filter.FilterWindow(self)


    ##
    ## Method new_active_folder (self, button, foldername)
    ##
    ##    Callback for the folder select menu.
    ##
    ##
    def new_active_folder(self, button, foldername):
        self.active_folder = foldername

        if not self.active_folder:
            return

        # If no messages are selected, we just switch directly to
        # the target folder
        if self.fdisp.selection == []:
            self.switch_to_folder()

        self.appbar.set_status('Current folder: %s (%d / %d)'\
                               % (foldername, self.unread[foldername],
                                  self.total[foldername]))

        # Reset search state
        self.search_row, self.search_idx = (0,-1)
        self.appbar.set_progress(0.0)

    ## This is just one to avoid a messy lambda callback
    def switch_to_folder(self, button=None):
        self.update_folderview(self.active_folder)

    ## Another callback shortcut for trashing messages
    def trash_msg(self, button):
        if not self.issafe():
            return

        tmp = self.active_folder

        if self.fdisp.selection != []:
            nextmsg = self.fdisp.selection[-1]
        else:
            nextmsg = 0
            
        self.active_folder = 'trash'
        self.move_msg()
    
        # We only need to update these two nodes in the folder tree view,
        # and we must do it before update_folderview()
        self.update_foldertree_nodes([self.mbox, self.active_folder])
        self.active_folder = tmp
        self.update_folderview(self.mbox)
        self.update_statusbar()

        # View the next message in the line
        if self.numrows > 0:
            row = min(nextmsg, self.numrows-1)
            self.fdisp.select_row(row, 0)
            self.fdisp.moveto(row=row)
            self.view_mail(row, inline=1)
            self.fdisp.grab_focus()


    ##
    ## Method move_msg (self, button)
    ##
    ##    Move messages from viewed folder to active folder.
    ##
    ##
    def move_msg(self, button=None):
        if self.mbox == self.active_folder:
            return

        if not self.issafe():
            return
        self.set_unsafe()

        # Make an array with the selected rows' start and end of
        # message positions
        msg = []
        for row in self.fdisp.selection:
            start, idx = self.fdisp.get_row_data(row)
            # Build the format used by folderops ('time' will not be used)
            msg.append([idx[4], start, idx[1], idx[0], idx[3], idx[2], 'time'])

        # Get the real path of these folders
        src = folderops.get_folder_pathname(self.prefs.folders, self.mbox)
        dst = folderops.get_folder_pathname(self.prefs.folders, self.active_folder)

        # Now, commit the move by updating index files and folder files
        (nus, nud) = folderops.move_messages(src, dst, msg)
        self.unread[self.mbox] = nus
        self.unread[self.active_folder] = nud
        self.total[self.mbox] = self.total[self.mbox] - len(msg)
        self.total[self.active_folder] = self.total[self.active_folder] + len(msg)

        if self.total[self.mbox] < 0:
            self.total[self.mbox] = 0

        # Update the registered index and mbox modification times
        if self.active_folder == 'inbox' or self.mbox == 'inbox':
            self.inboxtime = os.stat(self.prefs.folders+'/inbox')[8]

        self.update_folderview(self.mbox, sync=0)

        # Redisplay the folder tree if invoked by the "Move" button
        if button != None:
            self.update_foldertree_nodes([self.active_folder, self.mbox])
            self.update_statusbar()

        self.clear_unsafe()


    ##
    ##
    ## Methods for signal callbacks of folder contents list widget.
    ##
    ##
    def clickcolumn(self, clist, c):
        self.fdisp.freeze()
        if c == 6:
            # Dates (numbers) are sorted descending
            self.fdisp.set_sort_column(0)
            self.fdisp.set_sort_type(SORT_DESCENDING)
            self.sortcolumn = 0
        elif c == 7:
            # Sizes (numbers) are sorted descending
            self.fdisp.set_sort_column(8)
            self.fdisp.set_sort_type(SORT_DESCENDING)
            self.sortcolumn = 8
        else:
            # And the rest (strings) sorted ascending
            self.fdisp.set_sort_column(c)
            self.fdisp.set_sort_type(SORT_ASCENDING)
            self.sortcolumn = c
        self.fdisp.sort()
        self.fdisp.thaw()


    ##
    ## Method folderview_select_row ()
    ##
    def folderview_select_row(self, clist, r, c, event):
	# remember which row was selected, so we can view the message
	# if the user hits the return key
	self.selected_row = r

        # Cursor movement event has no type attribute
	# Check for double clicks
        if hasattr(event, 'type') and event.type == _2BUTTON_PRESS:
	    if self.mbox == 'drafts':
		# Special case for the outbox -- we launch an 'edit'
		# window here where the user can edit an existing
		# mail message in the outbox
		self.reply_forward(':Pygmy - Edit Message', edit.EDIT)
		# Delete the message from the outbox
		self.trash_msg(None)
	    else:
		# Just view the mail in a separate window
		self.view_mail(r, inline=0)


	# if the keyboard was used or the button clicked to make a selection,
	# update the inline display, except if the user is doing a multi-select
	elif ( self.button_pressed or self.key_updown_pressed ) \
	and len( self.fdisp.selection ) <= 1:
	    # View mail inline
	    self.view_mail(r, inline=1, msgwin=self.msgwin)
	    # Start next search from this row
	    self.search_row = self.row
	    self.search_idx = -1
	    # Make sure we don't do this for every select event
	    self.button_pressed = 0
	    self.key_updown_pressed = 0


    ##
    ## Method tree_row_selected ()
    ##
    def tree_row_selected(self, tree, node, col):
        folder = tree.node_get_row_data(node)
        self.new_active_folder(None, folder)


    ##
    ## Method tree_row_doubleclick ()
    ##
    def tree_row_doubleclick(self, tree, row, col, event):
	if self.active_folder == '':
    	    return

        if hasattr(event, 'type'):
            # Check for double clicks
            if event.type == _2BUTTON_PRESS:
                node = self.tdisp.node_nth(row)
                data = self.tdisp.node_get_row_data(node)
                folder = self.active_folder
                self.update_folderview(folder)
                self.appbar.set_status('Current folder: %s (%d / %d)'\
                                       % (folder, self.unread[folder],
                                          self.total[folder]))

    def tree_expand(self, tree, node):
        # Update status for expanded nodes
        self.update_foldertree_nodes()
        folder = tree.node_get_row_data(node)
	
	# Mark this folder as expanded
	folderops.expand_folder(self.prefs.folders, folder, 1)

    def tree_collapse(self, tree, node):
        folder = tree.node_get_row_data(node)

	# Mark this folder as collapsed
	folderops.expand_folder(self.prefs.folders, folder, 0)

    def tree_drag_data_received(self, widget, context, x, y, data, info, time):
        msg = data.data
        try:
            row, col = self.tdisp.get_selection_info(x, y)
        except TypeError:
            # Tried to drag something weird, just ignore it
            return
        n = self.tdisp.node_nth(row)
        if n:
            folder = self.tdisp.node_get_row_data(n)
            tmp = self.active_folder
            self.active_folder = folder
            self.move_msg()
            # We only need to update these two nodes in the folder tree view,
            # and we must do it before update_folderview()
            self.update_foldertree_nodes([self.mbox, self.active_folder])
            self.active_folder = tmp
            self.update_folderview(self.mbox)
            self.update_statusbar()


    def folderview_key_event(self, clist, event):
	# if the user used the up/down arrows, set a flag so that 
	# the message will be viewed inline
	if event.type == KEY_PRESS:
	    if event.keyval == Up or event.keyval == Down:
		self.key_updown_pressed = 1
	elif event.type == KEY_RELEASE:
	    # if the user hit the return key, view the message 
	    # in a separate window
	    if event.keyval == Return:
		self.view_mail(self.selected_row, inline=0)
	    

    ##
    ## Method folderview_button_press ()
    ##
    def folderview_button_press(self, clist, event):
        if event.button == 1:
            # Since it's hard to know the row that got pressed, we notify
            # folderview_select_row() to do the work for us
            self.button_pressed = 1
        elif event.button == 3:
            # Do nothing if nothing is selected
            if self.fdisp.selection == []:
                return
            # Build a little popup (use arrays to maintain order)
            options = ( ('View', self.rb_viewmail),
                        ('Save', self.rb_save),
                        ('Headers', self.rb_allheaders),
                        (None, lambda *x:None),
                        ('Reply', self.rb_reply),
                        ('Reply all', self.rb_replyall),
                        ('Forward', self.rb_forward),
                        ('Delete', self.rb_delete),
                        (None, lambda *x:None),
                        ('Mark Read',  self.mark_read),
                        ('Mark All Read', self.mark_all_read),
                        ('Mark Unread', self.remove_marks),
                        )
            menu = GtkMenu()
            for i in options:
                menuitem = GtkMenuItem(i[0])
                menuitem.connect('activate', i[1])
                menu.append(menuitem)
                menuitem.show()
            menu.popup(None, None, None, event.button, event.time)
            menu.show()

    def folderview_drag_data_get(self, widget, context, data, info, time):
        msg = ''
        for row in self.fdisp.selection:
            start, idx = self.fdisp.get_row_data(row)
            msg = msg + '%s, %s, %s\n' % (self.mbox, start, idx[1])

        data.set(data.target, 1, msg)        

    ## Callback for right button 'view all headers'
    def rb_allheaders(self, button):
        row = self.fdisp.selection[0]
        start = int(self.fdisp.get_text(row,1))
        stop = int(self.fdisp.get_text(row,2))

        # Open folder and seek to the start of the message
        pathname = folderops.get_folder_pathname(self.prefs.folders, self.mbox)
        f = open(pathname)
        f.seek(start)

        # Make a message object and get the headers
        m = rfc822.Message(f)
        f.close()
        h = headers.HeaderWindow(m.headers, self.win)
        del m

    ## Callback for right button 'save'
    def rb_save(self, button):
        self.saveas(button)
            
    ## Callback for right button 'view mail'
    def rb_viewmail(self, button):
        self.view_mail(self.fdisp.selection[0])
        
    ## Callback for right button 'view headers'
    def rb_viewheaders(self, button):
        self.rb_allheaders(None)

    ## Callback for right button 'reply'
    def rb_reply(self, button):
        self.reply_callback(None)

    ## Callback for right button 'reply all'
    def rb_replyall(self, button):
        self.replyall_callback(None)

    ## Callback for right button 'forward'
    def rb_forward(self, button):
        self.forward_callback(None)

    ## Callback for right button 'delete'
    def rb_delete(self, button):
        self.trash_msg(None)


    ##
    ## Method view_mail ()
    ##
    def view_mail(self, row, inline=0, msgwin=None):

	# Reset status
	self.appbar.set_status('')
	
        # First find the message boundary in the folder
        start = int(self.fdisp.get_text(row,1))
        stop = int(self.fdisp.get_text(row,2))

        # Get the current status field of the message
        status = self.fdisp.get_text(row, 3)

        # Store a reference to the row number to ease further navigation
        self.row = row

        # Open folder and seek to the start of the message
        pathname = folderops.get_folder_pathname(self.prefs.folders, self.mbox)
        f = open(pathname)
        f.seek(start)

        # Need to instantiate a mime message here to capture attachments
        m = mime.Message(f, start, stop)
        m.mbox = self.mbox

        # Check if we have to update the status in the index file
        m.msg_unread = (status == folderops.STATUS_UNREAD)
        # This message is no longer unread
        if m.msg_unread:
            self.unread[self.mbox] = self.unread[self.mbox] - 1

        # Check if we should just reuse the old Msg window
        if inline and self.msgwin:
            # Recycle the inline msg window
            self.msgwin.msg = m
            self.msgwin.hdr = m.headers
            self.msgwin.init_contents()
        elif msgwin and not inline:
            # Recycle the external msg window
            msgwin.msg = m
            msgwin.hdr = m.headers
            msgwin.init_contents()
        else:
            # Need to make new one
            msgwin = msg.MsgWindow(self, m, inline)
            if inline:
                self.msgwin = msgwin

        # Redisplay since the message status may have changed
        self.update_foldertree_nodes([self.mbox])


    ##
    ## Method update_folderview ()
    ##
    def update_folderview(self, folder='inbox', sync=1):
	if folder == '':
            return
        # Insert folder contents into the list
        self.fdisp.freeze()
        self.fdisp.clear()

        # Find the real path of this folder
        pathname = folderops.get_folder_pathname(self.prefs.folders, folder)
        f = folderops.fetch_folder_index(pathname)

        self.numrows = len(f.keys())
        for e in f.keys():
            frm = self.make_from_string(f[e][3])
            datestr = f[e][4] or 0
            try:
                l_time = time.localtime(int(datestr))
            except:
                try:
                    l_time = time.localtime(float(datestr))
                except:
                    print 'error: unable to convert time', {0:datestr}
                    l_time = 0
            date = time.strftime("%d/%m/%y %H:%M", l_time)
            stat = f[e][0]
            size = float(f[e][1]) - int(e)
            if size > 1048576:
                out = "%.1fM" % (size / 1048576)
            elif size > 1024:
                out = "%.1fK" % (size / 1024)
            else:
                out = "%dB" % int(size)
            size = "%8d" % (int(f[e][1]) - int(e))
            # Add row to display
            d_s = "%10s" % int(float(f[e][4] or '0'))
            if d_s[0] == ' ': d_s = "\1" + d_s[1:]
            pos = self.fdisp.append((d_s, e, f[e][1], stat, frm,
                                     f[e][2], date, out, size))
            # Insert index data as row data
            self.fdisp.set_row_data(pos, (e, f[e]))
            style = self.fdisp.get_style().copy()
            if stat == STATUS_UNREAD:
                style.font = self.sub_bfont
            else:
                style.font = self.sub_nfont 
            self.fdisp.set_row_style(pos, style)

        # Sort columns
        self.fdisp.set_sort_column(self.sortcolumn)
        if self.sortcolumn == 0:
            self.fdisp.set_sort_type(SORT_DESCENDING)
        else:
            self.fdisp.set_sort_type(SORT_ASCENDING)
        self.fdisp.sort()
        self.fdisp.thaw()

        if sync:
            # Update currently active mailbox for later reference
            self.mbox = folder
            self.active_folder = folder

    ##
    ## Method update_foldertree_nodes ()
    ##
    def update_foldertree_nodes(self, folders=[]):
        # Add folders to list of nodes that needs to be updated
        for folder in folders:
            # Make sure we don't add same folder twice
            if folder not in self.needs_update:
                self.needs_update.append(folder)
                
        for row in range(self.row_count):
            n = self.tdisp.node_nth(row)
            if not n:
                break
        
            f = self.tdisp.node_get_row_data(n)
            if f in self.needs_update:
                pixtext = self.tdisp.node_get_pixtext(n, 0)
                fname = os.path.basename(f)
                unread = self.unread[f]
                total = self.total[f]
                if total > 0 and fname in ['sent-mail', 'drafts', 'trash']:
                    text = '%s (%d)' % (fname, total)
                elif unread > 0:
                    text = '%s (%d)' % (fname, unread)
                else:
                    text = fname
                icons = self.get_folder_icons(f)
                self.tdisp.node_set_pixtext(n, 0, text, pixtext[1],
                                            pixmap = icons[0],
                                            mask = icons[1])
                style = self.tdisp.get_style().copy()
                if (unread > 0 and fname != 'sent-mail') or \
                   (total > 0 and fname == 'drafts'):
                    style.font = self.fld_bfont
                else:
                    style.font = self.fld_nfont
                self.tdisp.node_set_row_style(n, style)

                # Updated, so remove it from list
                del self.needs_update[self.needs_update.index(f)]
            
    ##
    ## Method update_statusbar ()
    ##
    def update_statusbar(self, folder=None):
        if not folder:
            folder = self.mbox
            
        # Update status bar as well since 'unread' may have changed
        self.appbar.set_status('Current folder: %s (%d / %d)'\
                               % (folder,
                                  self.unread[folder],
                                  self.total[folder]))

    ##
    ## Method get_folder_icons ()
    ##
    def get_folder_icons(self, folder=None):
        if not folder:
            folder = self.mbox

        unread = self.unread[folder]
        total = self.total[folder]
            
        # Select which icons to use
        if folder == 'trash':
            close = self.trash
            open = self.trash
            close_mask = self.trash_mask
            open_mask = self.trash_mask
        elif folder == 'inbox':
            close = self.inbox
            open = self.inbox
            close_mask = self.inbox_mask
            open_mask = self.inbox_mask
        elif folder == 'drafts':
            close = self.outbox
            open = self.outbox
            close_mask = self.outbox_mask
            open_mask = self.outbox_mask
        else:
            # Other folders
            if total > 0:
                close = self.full
                open = self.full
                close_mask = self.full_mask
                open_mask = self.full_mask
            else:
                close = self.empty
                open = self.empty
                close_mask = self.empty_mask
                open_mask = self.empty_mask

        return (close, close_mask, open, open_mask)

    ##
    ## Method __update_foldertree ()
    ##
    def __update_foldertree(self, ftree, path='', node=None):
        # Sort entries -- system folders we order otherwise
        if ftree.has_key('__order__'):
            k = ftree['__order__']
        else:
            k = ftree.keys()
            k.sort()

        # Traverse the sorted entry list
        for fname in k:
            folder = os.path.join(path, fname)
            unread = self.unread[folder]
            total = self.total[folder]

            icons = self.get_folder_icons(folder)
            if total > 0 and fname in ['sent-mail', 'drafts', 'trash']:
                text = '%s (%d)' % (fname, total)
            elif unread > 0:
                text = '%s (%d)' % (fname, unread)
            else:
                text = fname

            subtree = ftree[fname]
            if subtree:
                leaf = FALSE
            else:
                leaf = TRUE
	    
	    state = folderops.folder_expanded(self.prefs.folders, folder)
            n = self.tdisp.insert_node(node, None, [text, str(total),
                                                    str(unread)],
                                       is_leaf = leaf,
                                       expanded = state,
                                       pixmap_closed = icons[0],
                                       pixmap_opened = icons[2],
                                       mask_closed = icons[1],
                                       mask_opened = icons[3])
            
            self.row_count = self.row_count + 1
            style = self.tdisp.get_style().copy()
            if (unread > 0 and folder != 'sent-mail') or \
               (total > 0 and folder == 'drafts'):
                style.font = self.fld_bfont
            else:
                style.font = self.fld_nfont
            self.tdisp.node_set_row_style(n, style)
            
            # Must be done before self.tdisp.select() below, since the select
            # will trigger an "select_row" event, and tree_row_selected()
            # depends upon this data being set ;-)
            self.tdisp.node_set_row_data(n, folder)

            # Select the active folder
            if fname == self.active_folder:
                # Notice that the following line will trigger an event
                self.tdisp.select(n)
            # Build subtree if any (recursively)
            if subtree:
                self.__update_foldertree(subtree, folder, n)

    ##
    ## Method update_foldertree ()
    ##
    def update_foldertree(self):
        stree = {}
        utree = {}

        self.row_count = 0
        ftree = self.ftree

        for fname in ftree.keys():
            if fname in self.prefs.default_folders:
                stree[fname] = ftree[fname]
                # System folder are not ordered alphabetically
                stree['__order__'] = self.prefs.default_folders
            else:
                utree[fname] = ftree[fname]

        self.tdisp.freeze()
        self.tdisp.clear()
        system = self.tdisp.insert_node(None, None, ['Pygmy', '', ''],
                                        is_leaf = FALSE,
                                        expanded = TRUE,
                                        pixmap_closed = self.close_dir,
                                        pixmap_opened = self.open_dir,
                                        mask_closed = self.close_dir_mask,
                                        mask_opened = self.open_dir_mask)
        self.tdisp.node_set_row_data(system, '')        
        style = self.tdisp.get_style().copy()
        style.font = self.fld_nfont
        self.tdisp.node_set_row_style(system, style)
        self.row_count = self.row_count + 1
        self.__update_foldertree(stree, '', system)
	state = folderops.folder_expanded(self.prefs.folders, '')
        user = self.tdisp.insert_node(None, None, ['User Folders', '', ''],
                                      is_leaf=FALSE,
                                      expanded = state,
                                      pixmap_closed = self.close_dir,
                                      pixmap_opened = self.open_dir,
                                      mask_closed = self.close_dir_mask,
                                      mask_opened = self.open_dir_mask)
        self.tdisp.node_set_row_data(user, '')
        style = self.tdisp.get_style().copy()
        style.font = self.fld_nfont
        self.tdisp.node_set_row_style(user, style)
        self.row_count = self.row_count + 1
        self.__update_foldertree(utree, '', user)
        
        self.tdisp.thaw()

    ##
    ## Method next_msg_callback ()
    ##
    def next_msg_callback(self, button):
        # This is to be able to browse from folder view window
        if self.fdisp.selection == []:
            if self.numrows >= 1:  self.row = -1
            else:  return
        else:
            self.row = self.fdisp.selection[0]
        if self.row+1 < self.numrows:
            self.row = self.row + 1
            self.fdisp.unselect_row(self.row-1, 0)
            self.fdisp.select_row(self.row, 0)
            self.fdisp.moveto(row=self.row)
            self.view_mail(self.row, inline=1)
            self.msgwin.msg_text.grab_focus()


    ##
    ## Method prev_msg_callback (self, button)
    ##
    ##    View previous message callback.
    ##
    ##
    def prev_msg_callback(self, button):
        # This is to be able to browse from folder view window
        if self.fdisp.selection == []:
            return
        self.row = self.fdisp.selection[0]
        if self.row-1 >= 0:
            self.row = self.row - 1
            self.fdisp.unselect_row(self.row+1, 0)
            self.fdisp.select_row(self.row, 0)
            self.fdisp.moveto(row=self.row)
            self.view_mail(self.row, inline=1)
            self.msgwin.msg_text.grab_focus()


    ##
    ## Method new_callback (self, button)
    ##
    ##    Callback for New (mail) button.
    ##
    ##
    def new_callback(self, button):
        edtwin = edit.EditWindow(self, edit.NEW)
        edtwin.setup_widgets()
        self.update_foldertree()


    ##
    ## Method reply_forward (self, title, action)
    ##
    ##    Orchestrate reply and forward message.
    ##
    ##
    def reply_forward(self, title, action):
        # Sanity checks
        if not self.issafe():
            # Do not update the folder index while filters are running
            return
        if self.fdisp.selection == []:
            # No message selected for forwarding or replying
            return
        row = self.fdisp.selection[0]

        # First find the message boundary in the folder
        start = int(self.fdisp.get_text(row,1))
        stop = int(self.fdisp.get_text(row,2))

        # This message is no longer unread
        status = self.fdisp.get_text(row,3)
        if status == folderops.STATUS_UNREAD:
            self.unread[self.mbox] = self.unread[self.mbox] - 1

        # Store reference to current row to allow navigation
        self.row = row

        # Insert header information
        pathname = folderops.get_folder_pathname(self.prefs.folders,
                                                 self.mbox)
        f = open(pathname)
        f.seek(start)
        m = mime.Message(f, start, stop)

        try:
	    edtwin = edit.EditWindow(self, action, m)
	    edtwin.setup_widgets()
	    edtwin.win.set_title(title)
        except:
            # An exception may occur for invalid messages
            w = GnomeErrorDialog(err3)
            w.set_parent(self.win)
            w.show()
            try:  edtwin.destroy()
            except:  pass
        del m
        
        # Redisplay the folder tree
        self.update_foldertree()
        

    ## Callback for Reply button
    def reply_callback(self, button):
        # Reply to message
        self.reply_forward(':Pygmy - Reply to Message', edit.REPLY)


    ## Callback for Forward button
    def forward_callback(self, button):
        # Forward message
        self.reply_forward(':Pygmy - Forward Message', edit.FORW)


    ## Callback for Reply All menu option
    def replyall_callback(self, button):
        # Reply all to message
        self.reply_forward(':Pygmy - Reply to Message', edit.REPLYALL)


    ##
    ## Method set_filters ()
    ##
    def set_filters(self, filters):
        if not self.issafe():
            return
        self.filters_re = []
        for fi in filters:
            # Append compiled regexp of the text at end of tuple
            self.filters_re.append(fi + (re.compile(fi[ENTRY_TX], re.I),))


    ##
    ## Method do_run_filters ()
    ##
    def do_run_filters(self, button=None):
        self.run_filters(self.mbox)


    ##
    ## Method run_filters ()
    ##
    def run_filters(self, folder, start=0):
        if not self.issafe():
            return

        self.set_unsafe()
        msg = [] 

        # Minimize the penalty for users with no filters installed
        if self.filters_re == []:
            self.clear_unsafe()
            return msg
        todo = {}

        # Need to run filters on specified folder
        fname = folderops.get_folder_pathname(self.prefs.folders, folder)
        f = open(fname, 'r')
        mb = pygmymailbox.PygmyMailbox(f, start)
        total = os.path.getsize(fname)
        self.appbar.set_status('Running filter on %s' % folder)
        self.appbar.set_progress(0.0)

        # Loop over the mailbox, extract header information
        while 1:
            m = mb.next()
            if m is None:
                break
            # Get the From: field
            name, addr = m.getaddr('from')
            frm = mimify.mime_decode_header(name), addr
        
            if frm[0] == '':
                name = frm[1]
            else:
                name = '%s <%s>' % (frm[0], frm[1])
                
            # Get the Subject: field
            subject = mimify.mime_decode_header(m.getheader('subject') or "")
            # Remove problematic characters like newline
            subject = string.replace(subject, '\n', '')
            subject = string.replace(subject, '\r', '')

            # Get the Date: field
            date = m.getdate('date') or ""
            if date != "":
                # Convert to epoch value
                try:
                    date = time.mktime(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
            date = str(int(date))

            # Run all filters on this message
            for fi in self.filters_re:
                _in = fi[ENTRY_IN][:-1]
                if _in[:3] == 'Any':
                    data = (m.getheader('To') or '') + \
                           (m.getheader('Cc') or '')
                elif _in == 'Body':
                    print 'body filtering is currently not impl.'
                else:
                    data = m.getheader(_in)
                if (not data) or (data and fi[ENTRY_RE].search(data) == None):
                    continue # Try next filter
                
                # Which filter op
                action = fi[ENTRY_AC]
                idx = [date, m.fp.start, m.fp.stop, STATUS_UNREAD, frm, subject]
                to = fi[ENTRY_TO]
                if action == filter.ACTION_DELETE:
                    # Make sure we don't try to move mails to ourself
                    if to == folder:  break # Go to next message
                    try:
                        todo['trash'].append(idx)
                    except:
                        todo['trash'] = [idx]
                        
                    break # Don't run any more filters on this msg
                elif action == filter.ACTION_MOVE:
                    # Make sure we don't try to move mails to ourself
                    if to == folder:  break # Go to next message
                    try:
                        todo[to].append(idx)
                    except:
                        todo[to] = [idx]

                    break # Go to next message
                elif action == filter.ACTION_FORWARD:
                    # Need to change the To: field of the message
                    m.__setitem__('To', edit.mime_encode(to))
                    message = str(m) + "\n" + m.fp.read(m.fp.stop - m.startofbody)
                    if self.prefs.usemailserver:
                        if not sendmail.sendmail_server(self.prefs.serverentry, name, to, 
                                                    message):
                            del message
                            continue # Go to next filter
                    elif self.prefs.usesendmail:
                        if not sendmail.sendmail_cmd(self.prefs.sendmailentry, message):
                            del message
                            continue # Go to next filter
                    del message
                elif action == filter.ACTION_NONE:
                    break # Go to next message
                else:
                    print "folder -- got unimplemented filter operation"

            # Update progress bar
            self.appbar.set_progress(float(m.fp.stop)/float(total))
            
            # Update GUI so we can watch the progressbar. 
            while events_pending():
                mainiteration(FALSE)

        # Process todo list for any mails that needs to be moved. But we
        # cannot really move them since that will make our recorded file
        # marks inconsistent while we process the map. Our solution is
        # first to copy all the messages, and then delete them in one single
        # operation, when _all_ the copying is finished.
        todel = []
        src = folderops.get_folder_pathname(self.prefs.folders, folder)

        # First copy the messages that should be moved to the same folder
        for to in todo.keys():
            dst = folderops.get_folder_pathname(self.prefs.folders, to)
            try:
                nud = folderops.copy_messages(src, dst, todo[to])
            except IOError:
                w = GnomeErrorDialog(err7 % to)
                w.set_parent(self.win)
                w.show()
                self.appbar.set_status('Done')
                self.appbar.set_progress(0.0)
                self.clear_unsafe()
                return            
            todel = todel + todo[to]
            self.unread[to] = nud
            self.total[to] = self.total[to] + len(todo[to])
            
            # Generate report
            msg.append((len(todo[to]), to))

        # Then delete in a single operation (very important)
        nus = folderops.del_messages(src, todel)
        self.unread[folder] = nus
        self.total[folder] = self.total[folder] - len(todel)
        if self.total[folder] < 0:
            self.total[folder] = 0
        self.update_foldertree_nodes(todo.keys() + [folder])

        # Update folderview if necessary
        if todo != {}:
            self.update_folderview(self.mbox)
            self.update_statusbar()

        self.appbar.set_status('Done')
        self.appbar.set_progress(0.0)
            
        f.close()

        self.clear_unsafe()
        return msg

        
    ##
    ## Method rebuild_index ()
    ##
    def rebuild_index(self, button=None):
        if not self.issafe():
            return
        self.set_unsafe()

        pathname = folderops.get_folder_pathname(self.prefs.folders,
                                                 self.mbox)
        folderops.create_folder_index(pathname)
        self.update_folderview(self.mbox)

        # Just clear the status field since it might be corrupted anyway.
        self.unread[self.mbox] = self.total[self.mbox]
        self.update_foldertree()

        self.clear_unsafe()


    ##
    ## Method make_from ()
    ##
    def make_from_string(self, frm):
        # Check for frm = None
        if not frm:
            frm = ('', '')

        # Check for frm = (None, 'email), or ('', 'email')
        if not frm[0]:
            frm = frm[1]
        else:
            frm = frm[0]

        # Check for frm = (None, None)
        if not frm:
            frm = ''
            
        return frm


    ##
    ## Method add_folder_callback ()
    ##
    def add_folder_callback(self, button=None):
        # Check that we are not trying to add a folder in the
        # default folder hierarchy
        if self.active_folder in self.prefs.default_folders:
            w = GnomeErrorDialog(err6)
            w.set_parent(self.win)
            w.show()
            return
        l = GtkLabel("Enter name of new folder:")
	l.show()
	self.addentry = GtkEntry()
        self.addentry.show()
        v = GnomeDialog('Add Subfolder ', 'Ok', 'Cancel')
        v.set_parent(self.win)
        v.vbox.pack_start(l)
        v.vbox.pack_start(self.addentry)
        v.connect('clicked', self.do_add_folder)
        v.show()
        self.addentry.grab_focus()


    ##
    ## Method do_add_folder ()
    ##
    def do_add_folder(self, button, no):
        if no == 0:
            # Check that we are not trying to add a folder in the
            # default folder hierarchy
            if self.active_folder in self.prefs.default_folders:
                w = GnomeErrorDialog(err6)
                w.set_parent(self.win)
                w.show()
                button.destroy()
                return
                
            # Ok button
            name = self.addentry.get_text()

            # Prepend the name of the parent folder(s)
            folder = os.path.join(self.active_folder, name)

            if folder in self.total.keys() or name == '':
                # User has input the name of an existing folder, do nothing
                button.destroy()
                return

            if folder in self.prefs.default_folders:
                # User has input the name of a default folder
                button.destroy()
                return

            # Append to folders tree
            if not self.active_folder:
                self.ftree[name] = None
            else:
                # Walk down the tree until we find the right spot to insert
                tree = self.ftree
                for f in string.split(self.active_folder, '/'):
                    if f != '':
                        subtree = tree
                        tree = tree[f]
                        
                if tree == None:
                    # Insert the new node
                    subtree[f] = { name : None }
                else:
                    tree[name] = None

            # Get the real path of this folder
            pathname = folderops.get_folder_pathname(self.prefs.folders,
                                                     folder)
            if os.path.isfile(pathname):
                # Folder exists but lacks index, create a folder index
                folderops.create_folder_index(pathname)
            else:
                # 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.isdir(path):
                        os.mkdir(path)
                
                # Create a zero folder file
                f = open(pathname, "w")
                f.close()
                # Dump an empty dict in the index file
                index = folderops.get_index_from_pathname(pathname)
                marshal.dump({}, open(index, "w") )

            # Init total and unread
            self.unread[folder] = folderops.num_unread(pathname)
            self.total[folder] = folderops.num_msgs(pathname)

            # Redraw foldertree
            self.update_foldertree()
            button.destroy()
        elif no == 1:
            # Cancel button
            button.destroy()


    ##
    ## Method rename_folder_callback ()
    ##
    def rename_folder_callback(self, button=None):
        if self.tdisp.selection == []:
            return
        folder = os.path.basename(self.active_folder)
	l = GtkLabel("Enter new name of folder '"+folder+"':")
	l.show()
	self.renentry = GtkEntry()
        self.renentry.set_text(folder)
        self.renentry.show()
        v = GnomeDialog('Rename Folder', 'Ok', 'Cancel')
        v.set_parent(self.win)
        v.vbox.pack_start(l)
        v.vbox.pack_start(self.renentry)
        v.connect('clicked', self.do_rename_folder)
        v.show()
        self.renentry.grab_focus()


    ##
    ## Method do_rename_folder ()
    ##
    def do_rename_folder(self, button=None, no=None):
        # Ok button
        if no == 0:
            if self.renentry.get_text() == '':
                # No name is not good
                button.destroy()
                return

            name = os.path.join(os.path.dirname(self.active_folder),
                                self.renentry.get_text())

            if name in self.total.keys() or name == '':
                # User has input the name of an existing folder, do nothing
                button.destroy()
                return
            if name in self.prefs.default_folders:
                # User has input the name of a default folder
                button.destroy()
                return

            oldname = os.path.basename(self.active_folder)
            newname = os.path.basename(name)

            # Walk down the tree until we find the right spot to rename
            tree = self.ftree
            for f in string.split(self.active_folder, '/'):
                if f == oldname:
                    tree[newname] = tree[f]
                    del tree[f]
                    break
                else:
                    tree = tree[f]

            oldpathname = folderops.get_folder_pathname(self.prefs.folders,
                                                        self.active_folder)
            newpathname = folderops.get_folder_pathname(self.prefs.folders,
                                                        name)

            # Rename folder file            
            os.rename(oldpathname, newpathname)
            
            # Rename index file
            oldidx = folderops.get_index_from_pathname(oldpathname)
            newidx = folderops.get_index_from_pathname(newpathname)
            os.rename(oldidx, newidx)

            # Rename path to subfolder if any
            if os.path.isdir(oldpathname + '.sbd'):
                os.rename(oldpathname + '.sbd', newpathname + '.sbd')
                # Just reinit the total and unread counts since the renamed
                # folder have a subfolder, and this is the easiest way to
                # change the path for all of the subfolders within this folder
                self.total = {}
                self.unread = {}
                self.init_total_and_unread()
            else:
                # Update the unread messages index
                self.unread[name] = self.unread[self.active_folder]
                self.total[name] = self.total[self.active_folder]

                del self.unread[self.active_folder]
                del self.total[self.active_folder]

            # Update the foldertree datastructure
            self.ftree = folderops.get_active_folders(self.prefs.folders)
            
            # Update display
            self.update_foldertree()

            # Set active folder to the renamed folder
            self.active_folder = name
            button.destroy()

        # Cancel button
        elif no == 1:
            button.destroy()

    ##
    ## Method delete_folder_callback ()
    ##
    def delete_folder_callback(self, button=None):
        if self.tdisp.selection == []:
            return

	l = GtkLabel("Are you sure you want to \ndelete folder '"+\
                     self.active_folder+"' ? ")
	l.show()
        v = GnomeDialog('Delete Folder', 'Ok', 'Cancel')
        v.set_parent(self.win)
        v.vbox.pack_start(l)
        v.connect('clicked', self.do_delete_folder)
        v.show()


    ##
    ## Method do_delete_folder ()
    ##
    def do_delete_folder(self, button, no):
        if no == 0: # Ok button

            # Don't delete any of the default folders
            if self.active_folder in self.prefs.default_folders:
                # User has input the name of a default folder
                button.destroy()
                return
            
            pathname = folderops.get_folder_pathname(self.prefs.folders,
                                                     self.active_folder)
            # Folder file
            os.remove(pathname)
            # Index file
            idx = folderops.get_index_from_pathname(pathname)
            os.remove(idx)
            
            name = os.path.basename(self.active_folder)
            
            # Walk down the tree until we find the right spot to delete
            tree = self.ftree
            for f in string.split(self.active_folder, '/'):
                if f == name:
                    del tree[f]
                    break
                else:
                    tree = tree[f]
        
            # Update the unread messages index
            del self.unread[self.active_folder]
            del self.total[self.active_folder]
                    
            # Just switch to the inbox as a default action
            self.active_folder = 'inbox'
            self.mbox = 'inbox'
            
            # Redraw display
            self.update_foldertree() 
            self.update_folderview()

            button.destroy()
        elif no == 1:
            # Cancel button
            button.destroy()


    ##
    ## Method mark_all_read ()
    ##
    def mark_all_read(self, button=None):
        self.sel_all()
        self.mark_read()
        self.usel_all()


    ##
    ## Method mark_read ()
    ##
    def mark_read(self, button=None):
        if not self.issafe():
            return
        self.set_unsafe()
        
        pathname = folderops.get_folder_pathname(self.prefs.folders,
                                                 self.mbox)
        msg = []
        self.fdisp.freeze()
        for row in self.fdisp.selection:
            status = self.fdisp.get_text(row, 3)
            start, idx = self.fdisp.get_row_data(row)

            if status == STATUS_UNREAD:
                self.fdisp.set_text(row, 3, STATUS_READ)
                style = self.fdisp.get_style().copy()
                style.font = self.sub_nfont
                self.fdisp.set_row_style(row, style)
                msg.append((start, STATUS_READ))
                # Update row data
                start, idx = self.fdisp.get_row_data(row)
                idx[0] = STATUS_READ
                self.fdisp.set_row_data(row, (start, idx))
                self.unread[self.mbox] = self.unread[self.mbox] - 1
                
        folderops.update_folder_index_status_multiple(pathname, msg)
        self.fdisp.thaw()

        # Redisplay foldertree so that the unread info is updated
        self.update_foldertree_nodes([self.mbox])

        self.clear_unsafe()


    
    ##
    ## Method remove_marks ()
    ##
    def remove_marks(self, button=None):
        if not self.issafe():
            return
        self.set_unsafe()

        pathname = folderops.get_folder_pathname(self.prefs.folders,
                                                 self.mbox)
        
        # Make an array with the selected rows' start and end of message
        # positions
        msg = []
        self.fdisp.freeze()
        for row in self.fdisp.selection:
            status = self.fdisp.get_text(row, 3)
            start, idx = self.fdisp.get_row_data(row)

            if status != STATUS_UNREAD:
                self.fdisp.set_text(row, 3, STATUS_UNREAD)
                style = self.fdisp.get_style().copy()
                style.font = self.sub_bfont
                self.fdisp.set_row_style(row, style)
                msg.append((start, STATUS_UNREAD))
                # Update row data
                start, idx = self.fdisp.get_row_data(row)
                idx[0] = STATUS_UNREAD
                self.fdisp.set_row_data(row, (start, idx))
                self.unread[self.mbox] = self.unread[self.mbox] + 1
                
        folderops.update_folder_index_status_multiple(pathname, msg)
        self.fdisp.thaw()

        # Redisplay foldertree so that the unread info is updated
        self.update_foldertree_nodes([self.mbox])

        self.clear_unsafe()
