2 postal.py - An imap folder checker panel applet for ROX
4 Copyright 2005-2006 Kenneth Hayber <ken@hayber.us>,
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
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 *
29 APP_SITE
= 'hayber.us'
32 APP_CFG
= 'Accounts.ini'
38 if pynotify
.init(APP_NAME
):
44 # Options.xml processing
46 rox
.setup_app_options(APP_NAME
, site
=APP_SITE
)
47 Menu
.set_save_name(APP_NAME
, site
=APP_SITE
)
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
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
83 self
.vertical
= self
.get_panel_orientation() in ('Right', 'Left')
85 self
.set_size_request(8, -1)
87 self
.set_size_request(-1, 8)
90 self
.tooltips
= gtk
.Tooltips()
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
106 rox
.info(_("Problem loading accounts. Launching Account Editor..."))
109 # start the main task
110 Task(self
.check_mail())
113 def load_accounts(self
):
114 """Load all accounts from config file as dictionaries"""
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
):
120 cfg
= ConfigParser
.ConfigParser()
123 for section
in cfg
.sections():
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
148 def timeout(mailbox
):
149 return mailbox
.blocker
.happened
and isinstance(mailbox
.blocker
, TimeoutBlocker
)
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
:
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))
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"""
181 for box
in self
.mailboxes
:
182 results
+= box
.results
186 results
= _("No Mail")
187 self
.tooltips
.set_tip(self
, results
.strip(), tip_private
=None)
190 self
.pixbuf
= self
.ismail
192 self
.pixbuf
= self
.nomail
193 self
.resize_image(self
.size
)
195 if mailbox
.unseen
> mailbox
.prev_total
:
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
)
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."""
212 """Return the full path of an executable if found on the path"""
213 if (filename
== None) or (filename
== ''):
216 env_path
= os
.getenv('PATH').split(':')
218 if os
.access(p
+'/'+filename
, os
.X_OK
):
219 return p
+'/'+filename
223 rox
.filer
.spawn_rox((which(MAILER
.value
),))
225 rox
.report_exception()
228 def resize(self
, widget
, rectangle
):
229 """Called when the panel sends a size."""
231 size
= rectangle
[2] -2
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
)
245 def play_sound(self
):
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
)
253 def button_press(self
, window
, event
):
254 """Handle mouse clicks by popping up the matching menu."""
255 if event
.button
== 1:
257 elif event
.button
== 2:
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)
270 side
, margin
= pos
.split(',')
273 side
, margin
= None, 2
277 def get_options(self
, widget
=None, rebuild
=False, response
=False):
278 """Used as the notify callback when options change."""
282 def show_options(self
, button
=None):
283 """Open the options edit dialog."""
287 def build_appmenu(self
):
288 """Build the right-click app menu."""
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
):