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