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
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 *
31 APP_SITE
= 'hayber.us'
34 APP_CFG
= 'Accounts.ini'
40 if pynotify
.init(APP_NAME
):
46 # Options.xml processing
48 rox
.setup_app_options(APP_NAME
, site
=APP_SITE
)
49 Menu
.set_save_name(APP_NAME
, site
=APP_SITE
)
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
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."""
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
91 self
.vertical
= self
.get_panel_orientation() in ('Right', 'Left')
93 self
.set_size_request(8, -1)
95 self
.set_size_request(-1, 8)
98 self
.tooltips
= gtk
.Tooltips()
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
114 rox
.info(_("Problem loading accounts. Launching Account Editor..."))
117 # start the main task
118 Task(self
.check_mail())
121 def load_accounts(self
):
122 """Load all accounts from config file as dictionaries"""
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
):
128 cfg
= ConfigParser
.ConfigParser()
131 for section
in cfg
.sections():
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']:
146 value
= mb
.__dict
__[key
]
147 if isinstance(value
, list):
148 cfg
.set(mb
.name
, key
, ','.join(value
))
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."""
158 dlg
= accounts
.AccountList(self
.mailboxes
)
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
174 def timeout(mailbox
):
175 return mailbox
.blocker
.happened
and isinstance(mailbox
.blocker
, TimeoutBlocker
)
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
:
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))
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"""
208 for box
in self
.mailboxes
:
209 results
+= box
.results
214 results
= _("No Mail")
215 self
.tooltips
.set_tip(self
, results
.strip(), tip_private
=None)
218 self
.pixbuf
= self
.errimg
220 self
.pixbuf
= self
.ismail
222 self
.pixbuf
= self
.nomail
223 self
.resize_image(self
.size
)
225 if mailbox
.unseen
> mailbox
.prev_total
:
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")
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."""
247 """Return the full path of an executable if found on the path"""
248 if (filename
== None) or (filename
== ''):
251 env_path
= os
.getenv('PATH').split(':')
253 if os
.access(p
+'/'+filename
, os
.X_OK
):
254 return p
+'/'+filename
258 rox
.filer
.spawn_rox((which(MAILER
.value
),))
260 rox
.report_exception()
263 def resize(self
, widget
, rectangle
):
264 """Called when the panel sends a size."""
266 size
= rectangle
[2] -2
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
)
280 def play_sound(self
):
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
)
288 def button_press(self
, window
, event
):
289 """Handle mouse clicks by popping up the matching menu."""
290 if event
.button
== 1:
292 elif event
.button
== 2:
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)
305 side
, margin
= pos
.split(',')
308 side
, margin
= None, 2
312 def get_options(self
, widget
=None, rebuild
=False, response
=False):
313 """Used as the notify callback when options change."""
317 def show_options(self
, button
=None):
318 """Open the options edit dialog."""
322 def build_appmenu(self
):
323 """Build the right-click app menu."""
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
):