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
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 *
32 APP_SITE
= 'hayber.us'
35 APP_CFG
= 'Accounts.ini'
41 if pynotify
.init(APP_NAME
):
47 # Options.xml processing
49 rox
.setup_app_options(APP_NAME
, site
=APP_SITE
)
50 Menu
.set_save_name(APP_NAME
, site
=APP_SITE
)
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
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."""
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
89 self
.vertical
= self
.get_panel_orientation() in ('Right', 'Left')
91 self
.set_size_request(8, -1)
93 self
.set_size_request(-1, 8)
96 self
.tooltips
= gtk
.Tooltips()
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
112 rox
.info(_("Problem loading accounts. Launching Account Editor..."))
115 # start the main task
116 Task(self
.check_mail())
119 def load_accounts(self
):
120 """Load all accounts from config file as dictionaries"""
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
):
126 cfg
= ConfigParser
.ConfigParser()
129 for section
in cfg
.sections():
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']:
144 value
= mb
.__dict
__[key
]
145 if isinstance(value
, list):
146 cfg
.set(mb
.name
, key
, ','.join(value
))
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."""
156 dlg
= accounts
.AccountList(self
.mailboxes
)
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
172 def timeout(mailbox
):
173 return mailbox
.blocker
.happened
and isinstance(mailbox
.blocker
, TimeoutBlocker
)
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
:
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))
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"""
205 for box
in self
.mailboxes
:
206 results
+= box
.results
210 results
= _("No Mail")
211 self
.tooltips
.set_tip(self
, results
.strip(), tip_private
=None)
214 self
.pixbuf
= self
.ismail
216 self
.pixbuf
= self
.nomail
217 self
.resize_image(self
.size
)
219 if mailbox
.unseen
> mailbox
.prev_total
:
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")
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."""
238 """Return the full path of an executable if found on the path"""
239 if (filename
== None) or (filename
== ''):
242 env_path
= os
.getenv('PATH').split(':')
244 if os
.access(p
+'/'+filename
, os
.X_OK
):
245 return p
+'/'+filename
249 rox
.filer
.spawn_rox((which(MAILER
.value
),))
251 rox
.report_exception()
254 def resize(self
, widget
, rectangle
):
255 """Called when the panel sends a size."""
257 size
= rectangle
[2] -2
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
)
271 def play_sound(self
):
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
)
279 def button_press(self
, window
, event
):
280 """Handle mouse clicks by popping up the matching menu."""
281 if event
.button
== 1:
283 elif event
.button
== 2:
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)
296 side
, margin
= pos
.split(',')
299 side
, margin
= None, 2
303 def get_options(self
, widget
=None, rebuild
=False, response
=False):
304 """Used as the notify callback when options change."""
308 def show_options(self
, button
=None):
309 """Open the options edit dialog."""
313 def build_appmenu(self
):
314 """Build the right-click app menu."""
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
):