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"""
207 for box
in self
.mailboxes
:
208 results
+= box
.results
212 results
= _("No Mail")
213 self
.tooltips
.set_tip(self
, results
.strip(), tip_private
=None)
216 self
.pixbuf
= self
.ismail
218 self
.pixbuf
= self
.nomail
219 self
.resize_image(self
.size
)
221 if mailbox
.unseen
> mailbox
.prev_total
:
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")
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."""
243 """Return the full path of an executable if found on the path"""
244 if (filename
== None) or (filename
== ''):
247 env_path
= os
.getenv('PATH').split(':')
249 if os
.access(p
+'/'+filename
, os
.X_OK
):
250 return p
+'/'+filename
254 rox
.filer
.spawn_rox((which(MAILER
.value
),))
256 rox
.report_exception()
259 def resize(self
, widget
, rectangle
):
260 """Called when the panel sends a size."""
262 size
= rectangle
[2] -2
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
)
276 def play_sound(self
):
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
)
284 def button_press(self
, window
, event
):
285 """Handle mouse clicks by popping up the matching menu."""
286 if event
.button
== 1:
288 elif event
.button
== 2:
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)
301 side
, margin
= pos
.split(',')
304 side
, margin
= None, 2
308 def get_options(self
, widget
=None, rebuild
=False, response
=False):
309 """Used as the notify callback when options change."""
313 def show_options(self
, button
=None):
314 """Open the options edit dialog."""
318 def build_appmenu(self
):
319 """Build the right-click app menu."""
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
):