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."""
74 applet
.Applet
.__init
__(self
, id)
76 # load the applet icon
77 self
.image
= gtk
.Image()
78 self
.nomail
= gtk
.gdk
.pixbuf_new_from_file(os
.path
.join(APP_DIR
, 'images', 'nomail.svg'))
79 self
.errimg
= gtk
.gdk
.pixbuf_new_from_file(os
.path
.join(APP_DIR
, 'images', 'error.svg'))
80 self
.ismail
= gtk
.gdk
.pixbuf_new_from_file(os
.path
.join(APP_DIR
, 'images', 'mail.svg'))
81 self
.pixbuf
= self
.nomail
86 self
.vertical
= self
.get_panel_orientation() in ('Right', 'Left')
88 self
.set_size_request(8, -1)
90 self
.set_size_request(-1, 8)
93 self
.tooltips
= gtk
.Tooltips()
99 self
.add_events(gtk
.gdk
.BUTTON_PRESS_MASK
)
100 self
.connect('button-press-event', self
.button_press
)
101 self
.connect('size-allocate', self
.resize
)
102 self
.connect('delete_event', self
.quit
)
103 rox
.app_options
.add_notify(self
.get_options
)
105 # build the mailbox list
109 rox
.info(_("Problem loading accounts. Launching Account Editor..."))
112 # start the main task
113 Task(self
.check_mail())
116 def load_accounts(self
):
117 """Load all accounts from config file as dictionaries"""
120 filename
= os
.path
.join(basedir
.save_config_path(APP_SITE
, APP_NAME
), APP_CFG
)
121 if not os
.access(filename
, os
.R_OK
or os
.W_OK
):
123 cfg
= ConfigParser
.ConfigParser()
126 for section
in cfg
.sections():
128 for item
in cfg
.items(section
):
129 config
[item
[0]] = item
[1]
130 self
.mailboxes
.append(CHECKERS
[config
['protocol']](config
))
133 def save_accounts(self
):
134 cfg
= ConfigParser
.ConfigParser()
135 for mb
in self
.mailboxes
:
136 cfg
.add_section(mb
.name
)
137 for key
in mb
.__dict
__:
138 if not key
in ['server', 'name', 'protocol', 'folders', 'polltime',
139 'username', 'password', 'port', 'ssl', 'apop', 'filename']:
141 value
= mb
.__dict
__[key
]
142 if isinstance(value
, list):
143 cfg
.set(mb
.name
, key
, ','.join(value
))
145 cfg
.set(mb
.name
, key
, value
)
146 filename
= os
.path
.join(basedir
.save_config_path(APP_SITE
, APP_NAME
), APP_CFG
)
147 cfg
.write(open(filename
, 'w'))
150 def edit_accounts(self
):
151 """Edit the accounts list and save to the config file."""
153 dlg
= accounts
.AccountList(self
.mailboxes
)
159 def check_mail(self
):
161 This is the main task for the applet. It's job is to gather results
162 from each mailbox checker and update the UI. It does this periodically
163 based on the polltime. Each time we wake up from the timeout, we fire
164 all checker tasks and then yield on their blockers. As each blocker
165 triggers, we wake up again and process the results. In some cases more
166 than one blocker may have triggered, so we update the UI for all
169 def timeout(mailbox
):
170 return mailbox
.blocker
.happened
and isinstance(mailbox
.blocker
, TimeoutBlocker
)
174 for mailbox
in self
.mailboxes
:
175 if (mailbox
.blocker
is None) or timeout(mailbox
):
176 mailbox
.blocker
= Blocker()
177 Task(mailbox
.check())
178 elif mailbox
.blocker
.happened
:
180 mailbox
.blocker
= TimeoutBlocker(mailbox
.polltime
* 60)
181 blockers
.append(mailbox
.blocker
)
183 # in case there are no accounts, sleep for 10 seconds
184 if not len(blockers
):
185 blockers
.append(TimeoutBlocker(10))
190 def force_check(self
):
191 """Trigger all pending TimeoutBlockers."""
192 for mailbox
in self
.mailboxes
:
193 if mailbox
.blocker
and not mailbox
.blocker
.happened
:
194 if isinstance(mailbox
.blocker
, TimeoutBlocker
):
195 mailbox
.blocker
.trigger()
198 def update(self
, mailbox
):
199 """Update the display"""
202 for box
in self
.mailboxes
:
203 results
+= box
.results
207 results
= _("No Mail")
208 self
.tooltips
.set_tip(self
, results
.strip(), tip_private
=None)
211 self
.pixbuf
= self
.ismail
213 self
.pixbuf
= self
.nomail
214 self
.resize_image(self
.size
)
216 if mailbox
.unseen
> mailbox
.prev_total
:
218 n
= pynotify
.Notification(_("New Mail has arrived."),
219 mailbox
.results
.strip(), "mail-message-new")
220 n
.add_action("mailer", _("Read Mail"), self
.run_it
)
223 Task(self
.play_sound())
225 # don't report the same 'new' mail again
226 mailbox
.prev_total
= mailbox
.unseen
229 def run_it(self
, *action
):
230 """Run the Mailer command."""
233 """Return the full path of an executable if found on the path"""
234 if (filename
== None) or (filename
== ''):
237 env_path
= os
.getenv('PATH').split(':')
239 if os
.access(p
+'/'+filename
, os
.X_OK
):
240 return p
+'/'+filename
244 rox
.filer
.spawn_rox((which(MAILER
.value
),))
246 rox
.report_exception()
249 def resize(self
, widget
, rectangle
):
250 """Called when the panel sends a size."""
252 size
= rectangle
[2] -2
254 size
= rectangle
[3] -2
255 if size
!= self
.size
:
256 self
.resize_image(size
)
259 def resize_image(self
, size
):
260 """Resize the application image."""
261 scaled_pixbuf
= self
.pixbuf
.scale_simple(size
, size
, gtk
.gdk
.INTERP_BILINEAR
)
262 self
.image
.set_from_pixbuf(scaled_pixbuf
)
266 def play_sound(self
):
268 process
= popen2
.Popen3(SOUND
.value
)
269 # let stuff happen while playing the sound (the command must write to stdout)
270 yield InputBlocker(process
.fromchild
)
274 def button_press(self
, window
, event
):
275 """Handle mouse clicks by popping up the matching menu."""
276 if event
.button
== 1:
278 elif event
.button
== 2:
280 elif event
.button
== 3:
281 self
.appmenu
.popup(self
, event
, self
.position_menu
)
284 def get_panel_orientation(self
):
285 """ Return panel orientation and margin for displaying a popup menu.
286 Position in ('Top', 'Bottom', 'Left', 'Right').
288 pos
= self
.socket
.property_get('_ROX_PANEL_MENU_POS', 'STRING', False)
291 side
, margin
= pos
.split(',')
294 side
, margin
= None, 2
298 def get_options(self
, widget
=None, rebuild
=False, response
=False):
299 """Used as the notify callback when options change."""
303 def show_options(self
, button
=None):
304 """Open the options edit dialog."""
308 def build_appmenu(self
):
309 """Build the right-click app menu."""
311 items
.append(Menu
.Action(_('Check mail'), 'force_check', '', gtk
.STOCK_REFRESH
))
312 items
.append(Menu
.Action(_('Mail Client'), 'run_it', '', gtk
.STOCK_EXECUTE
))
313 items
.append(Menu
.Separator())
314 items
.append(Menu
.Action(_('Options...'), 'show_options', '', gtk
.STOCK_PREFERENCES
))
315 items
.append(Menu
.Action(_('Accounts...'), 'edit_accounts', ''))
316 items
.append(Menu
.Action(_('Reload...'), 'load_accounts', ''))
317 items
.append(Menu
.Separator())
318 items
.append(Menu
.Action(_('Close'), 'quit', '', gtk
.STOCK_CLOSE
))
319 self
.appmenu
= Menu
.Menu('other', items
)
320 self
.appmenu
.attach(self
, self
)
323 def quit(self
, *args
):
324 """Quit applet and close everything."""
326 # TimeoutBlockers won't let the app exit while they are waiting...
327 for mailbox
in self
.mailboxes
:
328 if mailbox
.blocker
and not mailbox
.blocker
.happened
:
329 if isinstance(mailbox
.blocker
, TimeoutBlocker
):