#!/usr/bin/env python # # Pyne - Python Newsreader and Emailer # # Copyright (c) 2000-2002 Tom Morton # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # Tom Morton # import utils import gtk import gobject import cPickle import time import os import os.path import sys import string import glob import webbrowser from copy import copy # remember location of pyne modules pyne_path = os.path.split( os.path.abspath(sys.argv[0]) )[0] from addressbook import * import pynei18n import boxtypes import boxtypes.superbox import boxtypes.loader import mainwin from pyneheaders import * import pynemsg import personality import userconfig import ptk.misc_widgets class pyne_user: """ Each user of pyne will have one of these objects. They may contain mailboxes, outboxes, NNTPboxes, etc. """ def save(self, save_all=0): """ save_all=1 if you want it to recursively save all folders. """ tosave = {} # Folder tree structure ftree = [] def _add_node(folder, kid_list): this = (folder.uid, []) kid_list.append(this) if save_all: folder.save(self) folder.shutdown(self) if not folder.__dict__.has_key("contents"): return for fol in folder.contents: _add_node(fol, this[1]) for f in self.contents: _add_node(f, ftree) tosave["foldertree"] = ftree def _saveme(key, tosave=tosave, self=self): tosave[key] = self.__dict__[key] # Stuff we want to save _saveme("personalities") _saveme("bodyfont") _saveme("linewrap") _saveme("replyhead") _saveme("addressbook") _saveme("col_text") _saveme("col_quote") _saveme("col_header") _saveme("col_mnormal") _saveme("col_mnobody") _saveme("col_mreplied") _saveme("col_mmarked") _saveme("sort_type") _saveme("opts") _saveme("printer") _saveme("last_expiry") _saveme("tab_pos") _saveme("ui_style") _saveme("default_dir") _saveme("html_parser") _saveme("edit_cmd") _saveme("mime_types") _saveme("window_setup") f = open("newuser.dat", "w") cPickle.dump(ver_stamp, f, 1) cPickle.dump("USER", f, 1) cPickle.dump(tosave, f, 1) f.close() try: # If old user.dat and .bak exist then remove them os.remove("user.dat") os.remove("user.bak") except OSError: pass def load(self): self.__dict__ = {} version = ver_stamp try: f = open("newuser.dat", "r") except IOError: # User does not exist pass else: version = cPickle.load(f) type = cPickle.load(f) self.__dict__ = cPickle.load(f) f.close() def _ifmissing(key, value, self=self): if not self.__dict__.has_key(key): self.__dict__[key] = value # User personalities _ifmissing("personalities", {}) # Font used for message body text _ifmissing("bodyfont", "") _ifmissing("linewrap", 72) _ifmissing("replyhead", "On $DATE, $FROM wrote:") # Address book _ifmissing("addressbook", []) # Texty colours _ifmissing("col_text", '#000000') _ifmissing("col_quote", '#0000cc') _ifmissing("col_header", '#cc0000') # Message view colours _ifmissing("col_mnormal", '#000000') _ifmissing("col_mnobody", '#7f7f7f') _ifmissing("col_mmarked", '#cc0000') _ifmissing("col_mreplied", '#0000cc') # default: sort messages by date, newest to top (2==date) _ifmissing("sort_type", (2, 0)) # user interface style _ifmissing("ui_style", UI_DEFAULT) # various boolean options. see OPT_xx at top _ifmissing("opts", 0) # printer command _ifmissing("printer", 'lpr') # When we last expired stuff _ifmissing("last_expiry", time.localtime(time.time())[:3]) # position of tabs in quickview and composer _ifmissing("tab_pos", int (gtk.POS_TOP)) # Default (attachment load/save, etc) directory _ifmissing("default_dir", "~") # parse html bodies with this: _ifmissing("html_parser", "lynx -dump") # alternative editor command _ifmissing("edit_cmd", "xterm -e vim") # Attachment handlers _ifmissing("mime_types", [ ("image/*", "", 1), ("text/plain", "", 1), ("text/html", "mozilla", 0) ] ) # Window size info (width, height, hpane position, vpane position) _ifmissing("window_setup", []) ###### Temporary stuff self.contents = [] # List of open windows self.windows = {} # List of to-be-deleted temporary files self.tempfiles = [] if self.__dict__.has_key("foldertree"): ftree = self.foldertree def _recurse_load_folders(conts, folder_node): folder = boxtypes.loader.loader(folder_node[0], self) conts.append(folder) if len(folder_node[1]) > 0: # It has children. Load them too for fnode in folder_node[1]: _recurse_load_folders(folder.contents, fnode) for fnode in ftree: _recurse_load_folders(self.contents, fnode) del self.foldertree else: # New user. Give him some cute starting folders to play with :o) # 'special' uids for non-deletable special folders (outbox, a = boxtypes.outbox.outbox(self, "outbox") self.contents.append(a) a = boxtypes.storebox.storebox(self, "drafts") a.name = _("Drafts") self.contents.append(a) a = boxtypes.storebox.storebox(self, "sent") a.name = _("Sent") self.contents.append(a) a = boxtypes.storebox.storebox(self, "saved") a.name = _("Saved") self.contents.append(a) a = boxtypes.storebox.storebox(self, "deleted") a.name = _("Deleted") self.contents.append(a) # For safety... self.save(save_all=1) def recover(self, path): """ Not appropriate to pyne mailboxes... """ pass box.save () def parent_of(self, folder): """ Return the parent object of 'folder'. """ uid = folder.uid def find_parent(folder, uid=uid): if folder.__dict__.has_key("contents"): for x in folder.contents: if x.uid == uid: return folder return utils.recurse_apply( [self], find_parent)[0] def get_folder_by_uid(self, uid): """ Return folder in user.contents with uid==uid :-) """ def get_uid_matches(folder, uid=uid): if folder.uid == uid: return folder folders = utils.recurse_apply(self.contents, get_uid_matches) if len(folders) == 0: return None else: return folders[0] def set_preferences(self, parent_win): """ Allow the user to set his preferences like fonts, etc. """ userconfig.UserConfigWin(self, parent_win) def update(self, update_type=0): """ Update all windows' folder lists. By default update changed stuff only. """ for x in self.windows.keys(): self.windows[x].update(self, update_type) # removed changed markers def remove_changed(folder): if folder.__dict__.has_key("changed"): del folder.changed # from 'utils.py' utils.recurse_apply(self.contents, remove_changed) def expire(self): """ Expire messages collected longer ago than self.expire_after days. """ def expire_msgs(expire_msgs, folder, this_day, expire_after): """ Recursively remove links to msg_id in object. """ # if the object contains messages, remove msg_ids from # them if folder.__dict__.has_key("messages"): messages = copy(folder.messages) for x in messages: msg = folder.load_header(x) # fucked up headers try: len(msg) except TypeError, e: print "*", try: folder.delete_article(x) except ValueError, e: pass continue # test age date_received = msg[HEAD_DATE_RECEIVED] day = int(date_received/86400.0) old = this_day - day if old >= expire_after: # unread messages: fix num_unread if folder.__dict__.has_key ("num_unread") and \ not (msg[HEAD_OPTS] & MSG_ISREAD): folder.num_unread -= 1 # delete it try: folder.delete_article(x) except ValueError, e: pass ############################################################ this_day = int(time.time()/86400.0) # only test for expiry in folders with expire_after def _pre_expire(folder, expire_msgs=expire_msgs, this_day=this_day): if not folder.__dict__.has_key("expire_after"): return if folder.expire_after == None: return print "Pyne: Expiring folder ", folder.name # recurse into folder looking for messages that have # expired. expire_msgs(expire_msgs, folder, this_day, folder.expire_after) utils.recurse_apply(self.contents, _pre_expire) def kill_window(self, num): # First save its pane positions if self.windows[num].__dict__.has_key("vpaned"): size = list(self.window_setup[num]) if not self.windows[num].__dict__.has_key("notebook"): # No panes if in tabbed mode size[VPANE_POS] = self.windows[num].vpaned.get_position() size[HPANE_POS] = self.windows[num].hpaned.get_position() self.window_setup[num] = tuple(size) # Destroy it self.windows[num].destroy() del self.windows[num] # All windows closed: quit if len(self.windows) == 0: gtk.main_quit() return def new_window(self, display_msg = None): """ Open new window, with maximised quickview pane and showing message (folder, msg-id) 'display_msg' if it is != None. """ # Find unused windows number x = 0 while self.windows.has_key(x): x = x + 1 if len(self.window_setup) <= x: self.window_setup.append( (600, 400, 200, 200) ) win = mainwin.pyne_window(self, ver_string, self.window_setup[x], x, display_msg = display_msg) win.update(self, mainwin.UPDATE_ALL) self.windows[x] = win def get_personality_uid(self): i = 0 ids = self.personalities.keys() while 1: if not str(i) in ids: return str(i) i = i + 1 def get_personality(self, uid): return self.personalities[uid] def get_uid(self, prefix): """ Return unique id for new object. """ # build list of current ids def get_uids(folder): return folder.uid uids = utils.recurse_apply(self.contents, get_uids) # return an unused id uid = 0 while 1: if not prefix+str(uid) in uids: return prefix+str(uid) uid = uid + 1 def queue_action(self, act_flag): self.act_flags = self.act_flags | act_flag def register_child (self, pid): """ Stick child pids in here after forking and the timeout function will poll them for exits so we are zombieless """ self.child_pids.append (pid) def timeout_func(self): # Update message/folder views. if self.act_flags & ACT_UPDATE: self.act_flags = self.act_flags & ~ACT_UPDATE self.update() for f in self.timeout_funcs: f (self) for i in xrange (len(self.child_pids)-1, -1, -1): # waitpid returns (pid, status) if self.child_pids[i] == os.waitpid (self.child_pids[i], os.WNOHANG)[0]: del self.child_pids[i] i = i+1 return True def timeout_add (self, some_function): """ Functions should take 1 argument, user. """ if not (some_function in self.timeout_funcs): self.timeout_funcs.append (some_function) def timeout_remove (self, some_function): if some_function in self.timeout_funcs: self.timeout_funcs.remove (some_function) def open_url (self, url): """ I love bacon :o) """ cmd = None for mimehandler in self.mime_types: if mimehandler[0] == "text/html": # pyne internal (which means webbrowser interface) if mimehandler[2] == 1: break cmd = mimehandler[1] break if cmd: pid = os.fork () if pid == 0: try: os.execvp(cmd, (cmd, url)) except OSError: os._exit(0) else: self.register_child (pid) else: pid = os.fork () if pid == 0: webbrowser.open(url, 1) os._exit(0) else: self.register_child (pid) def start(self): """ Just get on with it... """ # Key format "%d%d%d" % (cached, isread, isreplied, ismarked) self.msg_icons = {} self.msg_cols = {} self.act_flags = 0 self.child_pids = [] # just to shut it up with all the damn warnings we make this... # shame on me :-( i = gtk.Image() i.set_from_file(os.path.join (pyne_path,"icons","msg_read.xpm")) self.msg_icons["1100"] = i self.msg_cols["1100"] = "col_mnormal" i = gtk.Image() i.set_from_file(os.path.join (pyne_path,"icons","msg_unread.xpm")) self.msg_icons["1000"] = i self.msg_cols["1000"] = "col_mnormal" i = gtk.Image() i.set_from_file(os.path.join (pyne_path,"icons","msg_read_replied.xpm")) self.msg_icons["1110"] = i self.msg_cols["1110"] = "col_mreplied" i = gtk.Image() i.set_from_file(os.path.join (pyne_path,"icons","msg_uncached.xpm")) self.msg_icons["0000"] = i self.msg_cols["0000"] = "col_mnobody" i = gtk.Image() i.set_from_file(os.path.join (pyne_path,"icons","msg_unread_replied.xpm")) self.msg_icons["1010"] = i self.msg_cols["1010"] = "col_mreplied" i = gtk.Image() i.set_from_file(os.path.join (pyne_path,"icons","msg_read_marked.xpm")) self.msg_icons["1101"] = i self.msg_cols["1101"] = "col_mmarked" i = gtk.Image() i.set_from_file(os.path.join (pyne_path,"icons","msg_unread_marked.xpm")) self.msg_icons["1001"] = i self.msg_cols["1001"] = "col_mmarked" i = gtk.Image() i.set_from_file(os.path.join (pyne_path,"icons","msg_read_replied_marked.xpm")) self.msg_icons["1111"] = i self.msg_cols["1111"] = "col_mmarked" i = gtk.Image() i.set_from_file(os.path.join (pyne_path,"icons","msg_uncached_marked.xpm")) self.msg_icons["0001"] = i self.msg_cols["0001"] = "col_mmarked" i = gtk.Image() i.set_from_file(os.path.join (pyne_path,"icons","msg_unread_replied_marked.xpm")) self.msg_icons["1011"] = i self.msg_cols["1011"] = "col_mmarked" # Start up main window self.new_window() # There are many depraved things we wish to perform self.timeout_funcs = [] gobject.timeout_add(50, self.timeout_func) # Input loop gtk.main() # Trash temporary files for x in self.tempfiles: # delete temporary files try: os.remove(x) except OSError: pass self.tempfiles = [] # expire if not done today if self.last_expiry != time.localtime(time.time())[:3]: self.last_expiry = time.localtime(time.time())[:3] self.expire() # remove stuff we don't want to save del self.msg_icons del self.act_flags # Save datafile self.save(save_all=1) # End print "Pyne exited." if __name__ == '__main__': gtk.threads_init() gtk.threads_enter() # Help if sys.argv[-1] == "--help": print "Usage: pyne [option] [user location]" print "Pyne is a GTK+ Newsreader/Emailer written in Python." print print "User location is optional, and defaults to ~/.pyne" print print " -f, --force Force startup after a crash by removing the lockfile" print print "Report bugs to " sys.exit(0) # get alternative location of .pyne: for arg in sys.argv[1:]: # skip other args if arg[0] == "-": continue user_home = sys.argv[1] break else: user_home = os.path.join(os.environ["HOME"], ".pyne-1.0") # Remember this as location of pyne modules sys.path.append(pyne_path) # Change to the working directory try: os.chdir(user_home) except OSError, e: # It's either non-existant, or not a directory (eek!) try: os.mkdir(user_home) os.chdir(user_home) except OSError, e: print "Error. Cannot create ~/%s/, or file exists with that name. HELP!!! :~{" % user_home sys.exit(0) # other arguments for arg in sys.argv[1:]: if arg == "-f" or arg == "--force": try: os.remove("pyne.lock") except OSError, e: pass # Check for a lock file try: f = open("pyne.lock", "r") except IOError, e: # None. make one. f = open("pyne.lock", "w") f.close() else: if ptk.misc_widgets.ReturnDialog ("Warning!", "There is already an instance of Pyne running or a Pyne\nsession terminated abnormally (crashed :-)\nIf so delete the %s/pyne.lock file." % user_home, ((gtk.STOCK_QUIT,0), ("Ignore",1))) == 0: sys.exit() # Get user. user = pyne_user() user.load() # start main thread: user.start() user.start() # remove the lock file os.remove("pyne.lock") gtk.threads_leave()