Added socket timeout so that stuck servers won't hang the applet (5 seconds)
[rox-postal.git] / postal.py
blobdc0f71f5139d75a89d3d06aa161cdace8a47ce5e
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 applet.Applet.__init__(self, id)
76 # load the applet icon
77 self.image = gtk.Image()
78 self.nomail = gtk.gdk.pixbuf_new_from_file(os.path.join(APP_DIR, 'images', 'nomail.svg'))
79 self.errimg = gtk.gdk.pixbuf_new_from_file(os.path.join(APP_DIR, 'images', 'error.svg'))
80 self.ismail = gtk.gdk.pixbuf_new_from_file(os.path.join(APP_DIR, 'images', 'mail.svg'))
81 self.pixbuf = self.nomail
82 self.size = 0
83 self.resize_image(8)
84 self.add(self.image)
86 self.vertical = self.get_panel_orientation() in ('Right', 'Left')
87 if self.vertical:
88 self.set_size_request(8, -1)
89 else:
90 self.set_size_request(-1, 8)
92 # set the tooltip
93 self.tooltips = gtk.Tooltips()
95 # menus
96 self.build_appmenu()
98 # event handling
99 self.add_events(gtk.gdk.BUTTON_PRESS_MASK)
100 self.connect('button-press-event', self.button_press)
101 self.connect('size-allocate', self.resize)
102 self.connect('delete_event', self.quit)
103 rox.app_options.add_notify(self.get_options)
105 # build the mailbox list
106 try:
107 self.load_accounts()
108 except:
109 rox.info(_("Problem loading accounts. Launching Account Editor..."))
110 self.edit_accounts()
112 # start the main task
113 Task(self.check_mail())
116 def load_accounts(self):
117 """Load all accounts from config file as dictionaries"""
118 self.mailboxes = []
120 filename = os.path.join(basedir.save_config_path(APP_SITE, APP_NAME), APP_CFG)
121 if not os.access(filename, os.R_OK or os.W_OK):
122 raise IOError
123 cfg = ConfigParser.ConfigParser()
124 cfg.read(filename)
126 for section in cfg.sections():
127 config = {}
128 for item in cfg.items(section):
129 config[item[0]] = item[1]
130 self.mailboxes.append(CHECKERS[config['protocol']](config))
133 def save_accounts(self):
134 cfg = ConfigParser.ConfigParser()
135 for mb in self.mailboxes:
136 cfg.add_section(mb.name)
137 for key in mb.__dict__:
138 if not key in ['server', 'name', 'protocol', 'folders', 'polltime',
139 'username', 'password', 'port', 'ssl', 'apop', 'filename']:
140 continue
141 value = mb.__dict__[key]
142 if isinstance(value, list):
143 cfg.set(mb.name, key, ','.join(value))
144 else:
145 cfg.set(mb.name, key, value)
146 filename = os.path.join(basedir.save_config_path(APP_SITE, APP_NAME), APP_CFG)
147 cfg.write(open(filename, 'w'))
150 def edit_accounts(self):
151 """Edit the accounts list and save to the config file."""
152 import accounts
153 dlg = accounts.AccountList(self.mailboxes)
154 dlg.run()
155 dlg.destroy()
156 self.save_accounts()
159 def check_mail(self):
161 This is the main task for the applet. It's job is to gather results
162 from each mailbox checker and update the UI. It does this periodically
163 based on the polltime. Each time we wake up from the timeout, we fire
164 all checker tasks and then yield on their blockers. As each blocker
165 triggers, we wake up again and process the results. In some cases more
166 than one blocker may have triggered, so we update the UI for all
167 blocker.happened.
169 def timeout(mailbox):
170 return mailbox.blocker.happened and isinstance(mailbox.blocker, TimeoutBlocker)
172 while True:
173 blockers = []
174 for mailbox in self.mailboxes:
175 if (mailbox.blocker is None) or timeout(mailbox):
176 mailbox.blocker = Blocker()
177 Task(mailbox.check())
178 elif mailbox.blocker.happened:
179 self.update(mailbox)
180 mailbox.blocker = TimeoutBlocker(mailbox.polltime * 60)
181 blockers.append(mailbox.blocker)
183 # in case there are no accounts, sleep for 10 seconds
184 if not len(blockers):
185 blockers.append(TimeoutBlocker(10))
187 yield blockers
190 def force_check(self):
191 """Trigger all pending TimeoutBlockers."""
192 for mailbox in self.mailboxes:
193 if mailbox.blocker and not mailbox.blocker.happened:
194 if isinstance(mailbox.blocker, TimeoutBlocker):
195 mailbox.blocker.trigger()
198 def update(self, mailbox):
199 """Update the display"""
200 unseen = 0
201 results = ""
202 for box in self.mailboxes:
203 results += box.results
204 unseen += box.unseen
206 if not len(results):
207 results = _("No Mail")
208 self.tooltips.set_tip(self, results.strip(), tip_private=None)
210 if unseen:
211 self.pixbuf = self.ismail
212 else:
213 self.pixbuf = self.nomail
214 self.resize_image(self.size)
216 if mailbox.unseen > mailbox.prev_total:
217 if HAVE_NOTIFY:
218 n = pynotify.Notification(_("New Mail has arrived."),
219 mailbox.results.strip(), "mail-message-new")
220 n.add_action("mailer", _("Read Mail"), self.run_it)
221 n.show()
222 if len(SOUND.value):
223 Task(self.play_sound())
225 # don't report the same 'new' mail again
226 mailbox.prev_total = mailbox.unseen
229 def run_it(self, *action):
230 """Run the Mailer command."""
232 def which(filename):
233 """Return the full path of an executable if found on the path"""
234 if (filename == None) or (filename == ''):
235 return None
237 env_path = os.getenv('PATH').split(':')
238 for p in env_path:
239 if os.access(p+'/'+filename, os.X_OK):
240 return p+'/'+filename
241 return None
243 try:
244 rox.filer.spawn_rox((which(MAILER.value),))
245 except:
246 rox.report_exception()
249 def resize(self, widget, rectangle):
250 """Called when the panel sends a size."""
251 if self.vertical:
252 size = rectangle[2] -2
253 else:
254 size = rectangle[3] -2
255 if size != self.size:
256 self.resize_image(size)
259 def resize_image(self, size):
260 """Resize the application image."""
261 scaled_pixbuf = self.pixbuf.scale_simple(size, size, gtk.gdk.INTERP_BILINEAR)
262 self.image.set_from_pixbuf(scaled_pixbuf)
263 self.size = size
266 def play_sound(self):
267 """Play a sound"""
268 process = popen2.Popen3(SOUND.value)
269 # let stuff happen while playing the sound (the command must write to stdout)
270 yield InputBlocker(process.fromchild)
271 process.wait()
274 def button_press(self, window, event):
275 """Handle mouse clicks by popping up the matching menu."""
276 if event.button == 1:
277 self.run_it()
278 elif event.button == 2:
279 self.force_check()
280 elif event.button == 3:
281 self.appmenu.popup(self, event, self.position_menu)
284 def get_panel_orientation(self):
285 """ Return panel orientation and margin for displaying a popup menu.
286 Position in ('Top', 'Bottom', 'Left', 'Right').
288 pos = self.socket.property_get('_ROX_PANEL_MENU_POS', 'STRING', False)
289 if pos: pos = pos[2]
290 if pos:
291 side, margin = pos.split(',')
292 margin = int(margin)
293 else:
294 side, margin = None, 2
295 return side
298 def get_options(self, widget=None, rebuild=False, response=False):
299 """Used as the notify callback when options change."""
300 pass
303 def show_options(self, button=None):
304 """Open the options edit dialog."""
305 rox.edit_options()
308 def build_appmenu(self):
309 """Build the right-click app menu."""
310 items = []
311 items.append(Menu.Action(_('Check mail'), 'force_check', '', gtk.STOCK_REFRESH))
312 items.append(Menu.Action(_('Mail Client'), 'run_it', '', gtk.STOCK_EXECUTE))
313 items.append(Menu.Separator())
314 items.append(Menu.Action(_('Options...'), 'show_options', '', gtk.STOCK_PREFERENCES))
315 items.append(Menu.Action(_('Accounts...'), 'edit_accounts', ''))
316 items.append(Menu.Action(_('Reload...'), 'load_accounts', ''))
317 items.append(Menu.Separator())
318 items.append(Menu.Action(_('Close'), 'quit', '', gtk.STOCK_CLOSE))
319 self.appmenu = Menu.Menu('other', items)
320 self.appmenu.attach(self, self)
323 def quit(self, *args):
324 """Quit applet and close everything."""
326 # TimeoutBlockers won't let the app exit while they are waiting...
327 for mailbox in self.mailboxes:
328 if mailbox.blocker and not mailbox.blocker.happened:
329 if isinstance(mailbox.blocker, TimeoutBlocker):
330 rox.toplevel_unref()
332 self.destroy()