Set 'error' icon when there are any account errors
[rox-postal.git] / postal.py
blob43ed8f9f0a83bf3b259a0f291b0cf1e283cc83a3
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 socket
24 import sys, os, time, gtk, gobject, rox, getpass, popen2, ConfigParser
25 from rox import applet, filer, tasks, basedir
26 from rox.options import Option
27 from rox.tasks import *
29 # globals
30 APP_NAME = 'Postal'
31 APP_SITE = 'hayber.us'
32 APP_DIR = rox.app_dir
33 APP_SIZE = [28, 28]
34 APP_CFG = 'Accounts.ini'
37 HAVE_NOTIFY = False
38 try:
39 import pynotify
40 if pynotify.init(APP_NAME):
41 HAVE_NOTIFY = True
42 except:
43 pass
46 # Options.xml processing
47 from rox import Menu
48 rox.setup_app_options(APP_NAME, site=APP_SITE)
49 Menu.set_save_name(APP_NAME, site=APP_SITE)
51 #Options go here
52 MAILER = Option('mailer', 'thunderbird')
53 SOUND = Option('sound', '')
54 TIMEOUT = Option('timeout', 30)
56 #Enable notification of options changes
57 rox.app_options.notify()
59 socket.setdefaulttimeout(TIMEOUT.int_value) #set a timeout for imaplib & poplib
61 #Configure mailbox handling
62 import imap_check, pop_check, mbox_check
63 CHECKERS = {
64 'IMAP':imap_check.IMAPChecker,
65 'POP':pop_check.POPChecker,
66 'MBOX':mbox_check.MBOXChecker,
70 class Postal(applet.Applet):
71 """A Mail Checking Applet"""
73 def __init__(self, id):
74 """Initialize applet."""
75 if id == -1:
76 return # to support --accounts option
78 applet.Applet.__init__(self, id)
80 # load the applet icon
81 self.image = gtk.Image()
82 self.nomail = gtk.gdk.pixbuf_new_from_file(os.path.join(APP_DIR, 'images', 'nomail.svg'))
83 self.errimg = gtk.gdk.pixbuf_new_from_file(os.path.join(APP_DIR, 'images', 'error.svg'))
84 self.ismail = gtk.gdk.pixbuf_new_from_file(os.path.join(APP_DIR, 'images', 'mail.svg'))
85 self.pixbuf = self.nomail
86 self.size = 0
87 self.resize_image(8)
88 self.add(self.image)
89 self.notify = None
91 self.vertical = self.get_panel_orientation() in ('Right', 'Left')
92 if self.vertical:
93 self.set_size_request(8, -1)
94 else:
95 self.set_size_request(-1, 8)
97 # set the tooltip
98 self.tooltips = gtk.Tooltips()
100 # menus
101 self.build_appmenu()
103 # event handling
104 self.add_events(gtk.gdk.BUTTON_PRESS_MASK)
105 self.connect('button-press-event', self.button_press)
106 self.connect('size-allocate', self.resize)
107 self.connect('delete_event', self.quit)
108 rox.app_options.add_notify(self.get_options)
110 # build the mailbox list
111 try:
112 self.load_accounts()
113 except:
114 rox.info(_("Problem loading accounts. Launching Account Editor..."))
115 self.edit_accounts()
117 # start the main task
118 Task(self.check_mail())
121 def load_accounts(self):
122 """Load all accounts from config file as dictionaries"""
123 self.mailboxes = []
125 filename = os.path.join(basedir.save_config_path(APP_SITE, APP_NAME), APP_CFG)
126 if not os.access(filename, os.R_OK or os.W_OK):
127 raise IOError
128 cfg = ConfigParser.ConfigParser()
129 cfg.read(filename)
131 for section in cfg.sections():
132 config = {}
133 for item in cfg.items(section):
134 config[item[0]] = item[1]
135 self.mailboxes.append(CHECKERS[config['protocol']](config))
138 def save_accounts(self):
139 cfg = ConfigParser.ConfigParser()
140 for mb in self.mailboxes:
141 cfg.add_section(mb.name)
142 for key in mb.__dict__:
143 if not key in ['server', 'name', 'protocol', 'folders', 'polltime',
144 'username', 'password', 'port', 'ssl', 'apop', 'filename']:
145 continue
146 value = mb.__dict__[key]
147 if isinstance(value, list):
148 cfg.set(mb.name, key, ','.join(value))
149 else:
150 cfg.set(mb.name, key, value)
151 filename = os.path.join(basedir.save_config_path(APP_SITE, APP_NAME), APP_CFG)
152 cfg.write(open(filename, 'w'))
155 def edit_accounts(self):
156 """Edit the accounts list and save to the config file."""
157 import accounts
158 dlg = accounts.AccountList(self.mailboxes)
159 dlg.run()
160 dlg.destroy()
161 self.save_accounts()
164 def check_mail(self):
166 This is the main task for the applet. It's job is to gather results
167 from each mailbox checker and update the UI. It does this periodically
168 based on the polltime. Each time we wake up from the timeout, we fire
169 all checker tasks and then yield on their blockers. As each blocker
170 triggers, we wake up again and process the results. In some cases more
171 than one blocker may have triggered, so we update the UI for all
172 blocker.happened.
174 def timeout(mailbox):
175 return mailbox.blocker.happened and isinstance(mailbox.blocker, TimeoutBlocker)
177 while True:
178 blockers = []
179 for mailbox in self.mailboxes:
180 if (mailbox.blocker is None) or timeout(mailbox):
181 mailbox.blocker = Blocker()
182 Task(mailbox.check())
183 elif mailbox.blocker.happened:
184 self.update(mailbox)
185 mailbox.blocker = TimeoutBlocker(mailbox.polltime * 60)
186 blockers.append(mailbox.blocker)
188 # in case there are no accounts, sleep for 10 seconds
189 if not len(blockers):
190 blockers.append(TimeoutBlocker(10))
192 yield blockers
195 def force_check(self):
196 """Trigger all pending TimeoutBlockers."""
197 for mailbox in self.mailboxes:
198 if mailbox.blocker and not mailbox.blocker.happened:
199 if isinstance(mailbox.blocker, TimeoutBlocker):
200 mailbox.blocker.trigger()
203 def update(self, mailbox):
204 """Update the display"""
205 unseen = 0
206 errors = 0
207 results = ""
208 for box in self.mailboxes:
209 results += box.results
210 unseen += box.unseen
211 errors += box.errors
213 if not len(results):
214 results = _("No Mail")
215 self.tooltips.set_tip(self, results.strip(), tip_private=None)
217 if errors:
218 self.pixbuf = self.errimg
219 elif unseen:
220 self.pixbuf = self.ismail
221 else:
222 self.pixbuf = self.nomail
223 self.resize_image(self.size)
225 if mailbox.unseen > mailbox.prev_total:
226 if HAVE_NOTIFY:
227 if self.notify:
228 self.notify.close()
229 n = pynotify.Notification(_("New Mail has arrived."),
230 results.strip(), "mail-message-new")
231 n.add_action("mailer", _("Read Mail"), self.run_it)
232 n.attach_to_widget(self)
233 n.set_category("email.arrived")
234 n.show()
235 self.notify = n
236 if len(SOUND.value):
237 Task(self.play_sound())
239 # don't report the same 'new' mail again
240 mailbox.prev_total = mailbox.unseen
243 def run_it(self, *action):
244 """Run the Mailer command."""
246 def which(filename):
247 """Return the full path of an executable if found on the path"""
248 if (filename == None) or (filename == ''):
249 return None
251 env_path = os.getenv('PATH').split(':')
252 for p in env_path:
253 if os.access(p+'/'+filename, os.X_OK):
254 return p+'/'+filename
255 return None
257 try:
258 rox.filer.spawn_rox((which(MAILER.value),))
259 except:
260 rox.report_exception()
263 def resize(self, widget, rectangle):
264 """Called when the panel sends a size."""
265 if self.vertical:
266 size = rectangle[2] -2
267 else:
268 size = rectangle[3] -2
269 if size != self.size:
270 self.resize_image(size)
273 def resize_image(self, size):
274 """Resize the application image."""
275 scaled_pixbuf = self.pixbuf.scale_simple(size, size, gtk.gdk.INTERP_BILINEAR)
276 self.image.set_from_pixbuf(scaled_pixbuf)
277 self.size = size
280 def play_sound(self):
281 """Play a sound"""
282 process = popen2.Popen3(SOUND.value)
283 # let stuff happen while playing the sound (the command must write to stdout)
284 yield InputBlocker(process.fromchild)
285 process.wait()
288 def button_press(self, window, event):
289 """Handle mouse clicks by popping up the matching menu."""
290 if event.button == 1:
291 self.run_it()
292 elif event.button == 2:
293 self.force_check()
294 elif event.button == 3:
295 self.appmenu.popup(self, event, self.position_menu)
298 def get_panel_orientation(self):
299 """ Return panel orientation and margin for displaying a popup menu.
300 Position in ('Top', 'Bottom', 'Left', 'Right').
302 pos = self.socket.property_get('_ROX_PANEL_MENU_POS', 'STRING', False)
303 if pos: pos = pos[2]
304 if pos:
305 side, margin = pos.split(',')
306 margin = int(margin)
307 else:
308 side, margin = None, 2
309 return side
312 def get_options(self, widget=None, rebuild=False, response=False):
313 """Used as the notify callback when options change."""
314 pass
317 def show_options(self, button=None):
318 """Open the options edit dialog."""
319 rox.edit_options()
322 def build_appmenu(self):
323 """Build the right-click app menu."""
324 items = []
325 items.append(Menu.Action(_('Check mail'), 'force_check', '', gtk.STOCK_REFRESH))
326 items.append(Menu.Action(_('Mail Client'), 'run_it', '', gtk.STOCK_EXECUTE))
327 items.append(Menu.Separator())
328 items.append(Menu.Action(_('Options...'), 'show_options', '', gtk.STOCK_PREFERENCES))
329 items.append(Menu.Action(_('Accounts...'), 'edit_accounts', ''))
330 items.append(Menu.Action(_('Reload...'), 'load_accounts', ''))
331 items.append(Menu.Separator())
332 items.append(Menu.Action(_('Close'), 'quit', '', gtk.STOCK_CLOSE))
333 self.appmenu = Menu.Menu('other', items)
334 self.appmenu.attach(self, self)
337 def quit(self, *args):
338 """Quit applet and close everything."""
340 # TimeoutBlockers won't let the app exit while they are waiting...
341 for mailbox in self.mailboxes:
342 if mailbox.blocker and not mailbox.blocker.happened:
343 if isinstance(mailbox.blocker, TimeoutBlocker):
344 rox.toplevel_unref()
346 self.destroy()