## $Id: folder.py,v 1.164 2002/05/24 13:56:34 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 "\nError: You must install gnome-python in order to use Pygmy!\n\nYou can download gnome-python at ftp.gnome.org.\nOr, 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?" info4 = "Seleted Messages are in TRASH folder already, do you\n"\ "want to delete them PERMANENTLY ?" ## Search types SEARCH_BODY = 1 SEARCH_SUBJECT = 2 SEARCH_FROM = 4 ## Enable transparent pixmaps if not already set rcstring = """ style "trans" { engine "pixmap" {} } class "GtkWindow" style "trans" """ rc_parse_string(rcstring) ## ## 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) ] move_menu = [ UIINFO_ITEM_STOCK('Inbox', None, self.move_msg_to_inbox, STOCK_MENU_BLANK), UIINFO_ITEM_STOCK('Sent-Mail', None, self.move_msg_to_sent_mail, STOCK_MENU_BLANK), UIINFO_ITEM_STOCK('Drafts', None, self.move_msg_to_drafts, STOCK_MENU_BLANK), UIINFO_ITEM_STOCK('Trash', None, self.move_msg_to_trash, STOCK_MENU_BLANK) ] 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_SUBTREE_STOCK('Move To...', move_menu, 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 ', 'Dag Brattli ', 'Jason Hildebrand '], '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): self.move_selected_msgs_and_update_view ('trash') ## same as above def move_msg_to_trash(self, button): self.move_selected_msgs_and_update_view ('trash') def move_msg_to_inbox(self, button): self.move_selected_msgs_and_update_view ('inbox') def move_msg_to_drafts(self, button): self.move_selected_msgs_and_update_view ('drafts') def move_msg_to_sent_mail(self, button): self.move_selected_msgs_and_update_view ('sent-mail') ## ## Method popup_moveto_cb (self, button): ## ## Callback function for the "Move to" in the popup menu ## def popup_moveto_cb (self, button): self.move_selected_msgs_and_update_view ( button.get_name() ) ## ## Method move_selected_msgs_and_update_view (self, dst): ## ## Call folderops.move_messages(), then update view ## ## def move_selected_msgs_and_update_view (self, dst): if not self.issafe(): return self.set_unsafe() if dst == self.mbox: if dst == 'trash': # delete msgs in trash, ask before # delete permanently v = GnomeQuestionDialog(info4, self.del_selected_trash_msgs_and_update_view, self.win) v.show() self.clear_unsafe() return if self.fdisp.selection != []: nextmsg = self.fdisp.selection[-1] else: nextmsg = 0 # 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 srcPath = folderops.get_folder_pathname(self.prefs.folders, self.mbox) dstPath = folderops.get_folder_pathname(self.prefs.folders, dst) # Now, commit the move by updating index files and folder files (nus, nud) = folderops.move_messages(srcPath, dstPath, msg) self.unread[self.mbox] = nus self.unread[dst] = nud self.total[self.mbox] = self.total[self.mbox] - len(msg) self.total[dst] = self.total[dst] + len(msg) if self.total[self.mbox] < 0: self.total[self.mbox] = 0 # Update the registered index and mbox modification times if dst == 'inbox' or self.mbox == 'inbox': self.inboxtime = os.stat(self.prefs.folders+'/inbox')[8] # 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, dst]) self.update_folderview(self.mbox, sync=0) self.update_statusbar() self.clear_unsafe() # 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 del_selected_trash_msgs_ann_update_view (self, no) ## ## Adapted from above move_selected_msg_and_update_view(), ## Will call folderops.del_messages(msg) . ## ## def del_selected_trash_msgs_and_update_view (self, no): if not self.issafe(): return self.set_unsafe() if no != 0: return if self.mbox != 'trash': return # only msg in trash can be deleted # permanently if self.fdisp.selection != []: nextmsg = self.fdisp.selection[-1] else: nextmsg = 0 # 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']) trashPath = folderops.get_folder_pathname(self.prefs.folders, 'trash') # Now, commit the move by updating index files and folder files nus = folderops.del_messages (trashPath, msg) self.unread[self.mbox] = nus self.total[self.mbox] = self.total[self.mbox] - len(msg) if self.total[self.mbox] < 0: self.total[self.mbox] = 0 self.update_folderview(self.mbox, sync=0) # and we must do it before update_folderview() self.update_foldertree_nodes([self.mbox]) self.update_folderview(self.mbox, sync=0) self.update_statusbar() self.clear_unsafe() # 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() ## ## ## 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) self.move_selected_msgs_and_update_view(folder) 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() #generate move_to submenu allFolders = folderops.create_foldernames (self.ftree) moveto_submenu=GtkMenu() for f in self.prefs.default_folders: menuitem = GtkMenuItem(f) menuitem.set_name(f) menuitem.connect('activate', self.popup_moveto_cb) moveto_submenu.append(menuitem) menuitem.show() allFolders.sort() menuitem = GtkMenuItem(None) menuitem.connect('activate', lambda *x:None) moveto_submenu.append(menuitem) menuitem.show() for f in allFolders: if f not in self.prefs.default_folders: menuitem = GtkMenuItem(f) menuitem.set_name(f) menuitem.connect('activate', self.popup_moveto_cb) moveto_submenu.append(menuitem) menuitem.show() #add move_to submenu to popup menu menuitem = GtkMenuItem('Move to') menuitem.set_submenu(moveto_submenu) menu.insert(menuitem, 7) 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') try: frm = mimify.mime_decode_header(name), addr except: print "** Error decoding From: field -- (%s, %s, %s)" %\ (name, addr, folder) frm = ['Failed', 'failed@error.com'] 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) smtpserver, localsendmail = \ self.prefs.accounts [self.prefs.defacc][4:6] if smtpserver != '': # Use smtp server if not sendmail.sendmail_server(smtpserver, name, to, message): del message continue # Go to next filter else: # Use local sendmail if not sendmail.sendmail_cmd(localsendmail, 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()