Changed fixed socket timeout to an Option
[rox-postal.git] / postal.py
blobda17dd79a15a46ec704340bd7465a321f5e6d4ad
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 results = ""
207 for box in self.mailboxes:
208 results += box.results
209 unseen += box.unseen
211 if not len(results):
212 results = _("No Mail")
213 self.tooltips.set_tip(self, results.strip(), tip_private=None)
215 if unseen:
216 self.pixbuf = self.ismail
217 else:
218 self.pixbuf = self.nomail
219 self.resize_image(self.size)
221 if mailbox.unseen > mailbox.prev_total:
222 if HAVE_NOTIFY:
223 if self.notify:
224 self.notify.close()
225 n = pynotify.Notification(_("New Mail has arrived."),
226 results.strip(), "mail-message-new")
227 n.add_action("mailer", _("Read Mail"), self.run_it)
228 n.attach_to_widget(self)
229 n.set_category("email.arrived")
230 n.show()
231 self.notify = n
232 if len(SOUND.value):
233 Task(self.play_sound())
235 # don't report the same 'new' mail again
236 mailbox.prev_total = mailbox.unseen
239 def run_it(self, *action):
240 """Run the Mailer command."""
242 def which(filename):
243 """Return the full path of an executable if found on the path"""
244 if (filename == None) or (filename == ''):
245 return None
247 env_path = os.getenv('PATH').split(':')
248 for p in env_path:
249 if os.access(p+'/'+filename, os.X_OK):
250 return p+'/'+filename
251 return None
253 try:
254 rox.filer.spawn_rox((which(MAILER.value),))
255 except:
256 rox.report_exception()
259 def resize(self, widget, rectangle):
260 """Called when the panel sends a size."""
261 if self.vertical:
262 size = rectangle[2] -2
263 else:
264 size = rectangle[3] -2
265 if size != self.size:
266 self.resize_image(size)
269 def resize_image(self, size):
270 """Resize the application image."""
271 scaled_pixbuf = self.pixbuf.scale_simple(size, size, gtk.gdk.INTERP_BILINEAR)
272 self.image.set_from_pixbuf(scaled_pixbuf)
273 self.size = size
276 def play_sound(self):
277 """Play a sound"""
278 process = popen2.Popen3(SOUND.value)
279 # let stuff happen while playing the sound (the command must write to stdout)
280 yield InputBlocker(process.fromchild)
281 process.wait()
284 def button_press(self, window, event):
285 """Handle mouse clicks by popping up the matching menu."""
286 if event.button == 1:
287 self.run_it()
288 elif event.button == 2:
289 self.force_check()
290 elif event.button == 3:
291 self.appmenu.popup(self, event, self.position_menu)
294 def get_panel_orientation(self):
295 """ Return panel orientation and margin for displaying a popup menu.
296 Position in ('Top', 'Bottom', 'Left', 'Right').
298 pos = self.socket.property_get('_ROX_PANEL_MENU_POS', 'STRING', False)
299 if pos: pos = pos[2]
300 if pos:
301 side, margin = pos.split(',')
302 margin = int(margin)
303 else:
304 side, margin = None, 2
305 return side
308 def get_options(self, widget=None, rebuild=False, response=False):
309 """Used as the notify callback when options change."""
310 pass
313 def show_options(self, button=None):
314 """Open the options edit dialog."""
315 rox.edit_options()
318 def build_appmenu(self):
319 """Build the right-click app menu."""
320 items = []
321 items.append(Menu.Action(_('Check mail'), 'force_check', '', gtk.STOCK_REFRESH))
322 items.append(Menu.Action(_('Mail Client'), 'run_it', '', gtk.STOCK_EXECUTE))
323 items.append(Menu.Separator())
324 items.append(Menu.Action(_('Options...'), 'show_options', '', gtk.STOCK_PREFERENCES))
325 items.append(Menu.Action(_('Accounts...'), 'edit_accounts', ''))
326 items.append(Menu.Action(_('Reload...'), 'load_accounts', ''))
327 items.append(Menu.Separator())
328 items.append(Menu.Action(_('Close'), 'quit', '', gtk.STOCK_CLOSE))
329 self.appmenu = Menu.Menu('other', items)
330 self.appmenu.attach(self, self)
333 def quit(self, *args):
334 """Quit applet and close everything."""
336 # TimeoutBlockers won't let the app exit while they are waiting...
337 for mailbox in self.mailboxes:
338 if mailbox.blocker and not mailbox.blocker.happened:
339 if isinstance(mailbox.blocker, TimeoutBlocker):
340 rox.toplevel_unref()
342 self.destroy()