## $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 <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):
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()
syntax highlighted by Code2HTML, v. 0.9.1