Minor fixes
[rox-postal.git] / postal.py
blob95472d362031d26d8656153e0973a58e94b3726b
1 """
2 postal.py - An imap folder checker panel applet for ROX
4 Copyright 2005-2006 Kenneth Hayber <ken@hayber.us>,
5 All rights reserved.
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation; either version 2 of the License.
11 This program is distributed in the hope that it will be useful
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 """
21 # standard library modules
22 import sys, os, time, gtk, gobject, rox, getpass, popen2, ConfigParser
23 from rox import applet, filer, tasks, basedir
24 from rox.options import Option
25 from rox.tasks import *
27 # globals
28 APP_NAME = 'Postal'
29 APP_SITE = 'hayber.us'
30 APP_DIR = rox.app_dir
31 APP_SIZE = [28, 28]
32 APP_CFG = 'Accounts.ini'
35 HAVE_NOTIFY = False
36 try:
37 import pynotify
38 if pynotify.init(APP_NAME):
39 HAVE_NOTIFY = True
40 except:
41 pass
44 # Options.xml processing
45 from rox import Menu
46 rox.setup_app_options(APP_NAME, site=APP_SITE)
47 Menu.set_save_name(APP_NAME, site=APP_SITE)
49 #Options go here
50 MAILER = Option('mailer', 'thunderbird')
51 SOUND = Option('sound', '')
53 #Enable notification of options changes
54 rox.app_options.notify()
57 #Configure mailbox handling
58 import imap_check, pop_check, mbox_check
59 CHECKERS = {
60 'IMAP':imap_check.IMAPChecker,
61 'POP':pop_check.POPChecker,
62 'MBOX':mbox_check.MBOXChecker,
66 class Postal(applet.Applet):
67 """An Applet (no, really)"""
69 def __init__(self, id):
70 """Initialize applet."""
71 applet.Applet.__init__(self, id)
73 # load the applet icon
74 self.image = gtk.Image()
75 self.nomail = gtk.gdk.pixbuf_new_from_file(os.path.join(APP_DIR, 'images', 'nomail.svg'))
76 self.errimg = gtk.gdk.pixbuf_new_from_file(os.path.join(APP_DIR, 'images', 'error.svg'))
77 self.ismail = gtk.gdk.pixbuf_new_from_file(os.path.join(APP_DIR, 'images', 'mail.svg'))
78 self.pixbuf = self.nomail
79 self.size = 0
80 self.resize_image(8)
81 self.add(self.image)
83 self.vertical = self.get_panel_orientation() in ('Right', 'Left')
84 if self.vertical:
85 self.set_size_request(8, -1)
86 else:
87 self.set_size_request(-1, 8)
89 # set the tooltip
90 self.tooltips = gtk.Tooltips()
92 # menus
93 self.build_appmenu()
95 # event handling
96 self.add_events(gtk.gdk.BUTTON_PRESS_MASK)
97 self.connect('button-press-event', self.button_press)
98 self.connect('size-allocate', self.resize)
99 self.connect('delete_event', self.quit)
100 rox.app_options.add_notify(self.get_options)
102 # build the mailbox list
103 try:
104 self.load_accounts()
105 except:
106 rox.info(_("Problem loading accounts. Launching Account Editor..."))
107 self.edit_accounts()
109 # start the main task
110 Task(self.check_mail())
113 def load_accounts(self):
114 """Load all accounts from config file as dictionaries"""
115 self.mailboxes = []
117 filename = os.path.join(basedir.save_config_path(APP_SITE, APP_NAME), APP_CFG)
118 if not os.access(filename, os.R_OK or os.W_OK):
119 raise IOError
120 cfg = ConfigParser.ConfigParser()
121 cfg.read(filename)
123 for section in cfg.sections():
124 config = {}
125 for item in cfg.items(section):
126 config[item[0]] = item[1]
127 self.mailboxes.append(CHECKERS[config['protocol']](config))
130 def edit_accounts(self):
131 """Edit the accounts config file (just a text editor for now)"""
132 filename = os.path.join(basedir.save_config_path(APP_SITE, APP_NAME), APP_CFG)
133 if not os.access(filename, os.R_OK or os.W_OK):
134 os.system("cp %s %s" % (os.path.join(APP_DIR, APP_CFG), filename))
135 rox.filer.spawn_rox((filename,))
138 def check_mail(self):
140 This is the main task for the applet. It's job is to gather results
141 from each mailbox checker and update the UI. It does this periodically
142 based on the polltime. Each time we wake up from the timeout, we fire
143 all checker tasks and then yield on their blockers. As each blocker
144 triggers, we wake up again and process the results. In some cases more
145 than one blocker may have triggered, so we update the UI for all
146 blocker.happened.
148 def timeout(mailbox):
149 return mailbox.blocker.happened and isinstance(mailbox.blocker, TimeoutBlocker)
151 while True:
152 blockers = []
153 for mailbox in self.mailboxes:
154 if (mailbox.blocker is None) or timeout(mailbox):
155 mailbox.blocker = Blocker()
156 Task(mailbox.check())
157 elif mailbox.blocker.happened:
158 self.update(mailbox)
159 mailbox.blocker = TimeoutBlocker(mailbox.polltime * 60)
160 blockers.append(mailbox.blocker)
162 # in case there are no accounts, sleep for 10 seconds
163 if not len(blockers):
164 blockers.append(TimeoutBlocker(10))
166 yield blockers
169 def force_check(self):
170 """Trigger all pending TimeoutBlockers."""
171 for mailbox in self.mailboxes:
172 if mailbox.blocker and not mailbox.blocker.happened:
173 if isinstance(mailbox.blocker, TimeoutBlocker):
174 mailbox.blocker.trigger()
177 def update(self, mailbox):
178 """Update the display"""
179 unseen = 0
180 results = ""
181 for box in self.mailboxes:
182 results += box.results
183 unseen += box.unseen
185 if not len(results):
186 results = _("No Mail")
187 self.tooltips.set_tip(self, results.strip(), tip_private=None)
189 if unseen:
190 self.pixbuf = self.ismail
191 else:
192 self.pixbuf = self.nomail
193 self.resize_image(self.size)
195 if mailbox.unseen > mailbox.prev_total:
196 if HAVE_NOTIFY:
197 n = pynotify.Notification(_("New Mail has arrived."),
198 mailbox.results.strip(), "mail-message-new")
199 n.add_action("mailer", _("Read Mail"), self.run_it)
200 n.show()
201 if len(SOUND.value):
202 Task(self.play_sound())
204 # don't report the same 'new' mail again
205 mailbox.prev_total = mailbox.unseen
208 def run_it(self, *action):
209 """Run the Mailer command."""
211 def which(filename):
212 """Return the full path of an executable if found on the path"""
213 if (filename == None) or (filename == ''):
214 return None
216 env_path = os.getenv('PATH').split(':')
217 for p in env_path:
218 if os.access(p+'/'+filename, os.X_OK):
219 return p+'/'+filename
220 return None
222 try:
223 rox.filer.spawn_rox((which(MAILER.value),))
224 except:
225 rox.report_exception()
228 def resize(self, widget, rectangle):
229 """Called when the panel sends a size."""
230 if self.vertical:
231 size = rectangle[2] -2
232 else:
233 size = rectangle[3] -2
234 if size != self.size:
235 self.resize_image(size)
238 def resize_image(self, size):
239 """Resize the application image."""
240 scaled_pixbuf = self.pixbuf.scale_simple(size, size, gtk.gdk.INTERP_BILINEAR)
241 self.image.set_from_pixbuf(scaled_pixbuf)
242 self.size = size
245 def play_sound(self):
246 """Play a sound"""
247 process = popen2.Popen3(SOUND.value)
248 # let stuff happen while playing the sound (the command must write to stdout)
249 yield InputBlocker(process.fromchild)
250 process.wait()
253 def button_press(self, window, event):
254 """Handle mouse clicks by popping up the matching menu."""
255 if event.button == 1:
256 self.run_it()
257 elif event.button == 2:
258 self.force_check()
259 elif event.button == 3:
260 self.appmenu.popup(self, event, self.position_menu)
263 def get_panel_orientation(self):
264 """ Return panel orientation and margin for displaying a popup menu.
265 Position in ('Top', 'Bottom', 'Left', 'Right').
267 pos = self.socket.property_get('_ROX_PANEL_MENU_POS', 'STRING', False)
268 if pos: pos = pos[2]
269 if pos:
270 side, margin = pos.split(',')
271 margin = int(margin)
272 else:
273 side, margin = None, 2
274 return side
277 def get_options(self, widget=None, rebuild=False, response=False):
278 """Used as the notify callback when options change."""
279 pass
282 def show_options(self, button=None):
283 """Open the options edit dialog."""
284 rox.edit_options()
287 def build_appmenu(self):
288 """Build the right-click app menu."""
289 items = []
290 items.append(Menu.Action(_('Check mail'), 'force_check', '', gtk.STOCK_REFRESH))
291 items.append(Menu.Action(_('Mail Client'), 'run_it', '', gtk.STOCK_EXECUTE))
292 items.append(Menu.Separator())
293 items.append(Menu.Action(_('Options...'), 'show_options', '', gtk.STOCK_PREFERENCES))
294 items.append(Menu.Action(_('Accounts...'), 'edit_accounts', ''))
295 items.append(Menu.Action(_('Reload...'), 'load_accounts', ''))
296 items.append(Menu.Separator())
297 items.append(Menu.Action(_('Close'), 'quit', '', gtk.STOCK_CLOSE))
298 self.appmenu = Menu.Menu('other', items)
299 self.appmenu.attach(self, self)
302 def quit(self, *args):
303 """Quit applet and close everything."""
305 # TimeoutBlockers won't let the app exit while they are waiting...
306 for mailbox in self.mailboxes:
307 if mailbox.blocker and not mailbox.blocker.happened:
308 if isinstance(mailbox.blocker, TimeoutBlocker):
309 rox.toplevel_unref()
311 self.destroy()