class RoomListModel(EventMixin): events = set(('contacts_changed', )) def __init__(self, contacts): EventMixin.__init__(self) self.contacts = None self.offline = False self.set_contacts(contacts) def _init_contacts(self): self.contacts_view = [] self.contact_enter_times = {} self.contact_leave_times = {} self.pending_contacts = ObservableList() def set_contacts(self, contacts): if hasattr(self.contacts, 'remove_observer'): self.contacts.remove_observer(self._on_contacts_changed) self.contacts = contacts self._init_contacts() for contact in contacts: self.contact_enter_times[contact] = 0 if hasattr(self.contacts, 'add_observer'): self.contacts.add_observer(self._on_contacts_changed) self._on_contacts_changed() def _on_contacts_changed(self, *a, **k): self._update_pending() self._update_view() self.fire_contacts_changed() def fire_contacts_changed(self): self.event('contacts_changed') TIME_LEAVING = 4 def _update_view(self): self.leaving_contacts = view = [] now = time() for gone_contact, t in list(self.contact_leave_times.items()): if now - t > self.TIME_LEAVING: self.contact_leave_times.pop(gone_contact, None) else: #leaving view.append(gone_contact) old_length = len(self.contacts_view) self.contacts_view = sorted(list(self.contacts) + view) return len(self.contacts_view) != old_length def _update_pending(self): for pending_contact in list(self.pending_contacts): for contact in self.contacts: if pending_contact.equals_chat_buddy(contact): self.pending_contacts.remove(pending_contact) contacts = set(self.contacts) now = time() for contact in list(self.contact_enter_times.keys()): if contact not in contacts: self.contact_enter_times.pop(contact, None) self.contact_leave_times.setdefault(contact, now) for contact in contacts: if contact not in self.contact_enter_times: self.contact_enter_times.setdefault(contact, now) def contact_is_new(self, contact): time_joined = self.contact_enter_times.get(contact, None) if time_joined is None: return False TIME_NEW = 2 now = time() return now - time_joined < TIME_NEW def contact_is_leaving(self, contact): time_left = self.contact_leave_times.get(contact, None) return time_left is not None and time() - time_left < self.TIME_LEAVING def add_pending_contact(self, contact): if contact not in self.pending_contacts: self.pending_contacts.append(contact) self.event('contacts_changed') return True def remove_pending_contact(self, contact): try: self.pending_contacts.remove(contact) except ValueError: return False else: self.event('contacts_changed') return True @property def length_including_pending(self): return self.length + len(self.pending_contacts) @property def length(self): return len(self.contacts_view) def get_contact(self, n): return self.contacts_view[n] def get_pending_contact(self, n): return self.pending_contacts[n]
class EmailAccount(AccountBase, UpdateMixin, FromNetMixin): retry_time = 3 error_max = 3 def __init__(self, enabled = True, updateNow = True, **options): AccountBase.__init__(self, **options) UpdateMixin.__init__(self, **options) FromNetMixin.__init__(self, **options) self.emails = ObservableList() self.count = 0 self.seen = set() self._dirty_error = True # The next error is new. log.info('Created EmailAccount: %r. Setting enabled to %r', self, enabled) self.enabled = enabled def timestamp_is_time(self, tstamp): return True # # local prefs # mailclient = localprefprop(localprefs_key('mailclient'), None) custom_inbox_url = localprefprop(localprefs_key('custom_inbox_url'), None) custom_compose_url = localprefprop(localprefs_key('custom_compose_url'), None) email_address = util.iproperty('get_email_address', 'set_email_address') @property def extra_header_func(self): return None # inviting disabled if self.protocol not in ('aolmail', 'ymail', 'gmail', 'hotmail'): return None import hooks d = {} for attr in ('protocol', 'name', 'password'): d[attr] = getattr(self, attr) return ("Invite Contacts", lambda *a, **k: hooks.notify('digsby.email.invite_clicked', **d)) def get_email_address(self): from util import EmailAddress try: return str(EmailAddress(self.name, self.default_domain)).decode('ascii') except (AttributeError, ValueError): try: ret = self.name if '@' in self.name else self.name + '@' + self.default_domain if isinstance(ret, bytes): return ret.decode('ascii') else: return ret except Exception: # hopefully bad data has been caught before here. if isinstance(self.name, bytes): return self.name.decode('ascii', 'replace') else: return self.name def set_email_address(self, val): # Not supported pass @property def display_name(self): return try_this(lambda: getattr(self, pref('email.display_attr')), self.email_address) def on_error(self, task=None): ''' Called when an error occurs. task is a callable that can be used to make another attempt at whatever caused the error (if error_count is less than max_error). ''' self.error_count += 1 log.error('%r\'s error count is now: %d',self, self.error_count) log.error('on_error called from %s', get_func_name(2)) if self.error_count < pref('email.err_max_tolerance', self.error_max): if task is None: task = self.update_now log.error('error count is under, calling %r now', task) if not callable(task): # If it was an exception assume that update_now was called. (the account type # probably just hasn't been fixed yet task = self.update_now util.call_later(pref('email.err_retry_time', type=int, default=2), task) else: log.error('assuming the connection has died') self.set_offline(self.Reasons.CONN_FAIL) self.error_count = 0 del self.emails[:] def bad_pw(self): log.info('%r: changing state to BAD_PASSWORD', self) self.set_offline(self.Reasons.BAD_PASSWORD) self.timer.stop() def no_mailbox(self): log.info('%r: changing state to NO_MAILBOX', self) self.set_offline(self.Reasons.NO_MAILBOX) self.timer.stop() def __repr__(self): r = AccountBase.__repr__(self)[:-1] r += ', ' r += 'enabled' if self.enabled else 'disabled' return r + '>' @property def web_login(self): return (pref('privacy.www_auto_signin') and self.state in (self.Statuses.ONLINE, self.Statuses.CHECKING)) def error_link(self): reason = self.Reasons linkref = { reason.BAD_PASSWORD : ('Edit Account', lambda *a: profile.account_manager.edit(self,True)), reason.CONN_FAIL : ('Retry', lambda *a: self.update_now()) } if self.offline_reason in linkref: name, callback = linkref[self.offline_reason] return name, callback else: return None def sort_emails(self, new=None): self.emails.sort() if new is not None: new.sort() def filter_new(self, new, old): if old: return [e for e in new if e <= old[-1]] elif self.seen: new_ids = set(e.id for e in new) keep = set() for email in self.emails: if email.id in new_ids: keep.add(email.id) else: break return [e for e in new if e.id in keep] else: return list(new) def _see_new(self, new_messages): new_seen = set() for email in new_messages: new_seen.add(email.id) self.seen.update(new_seen) def _get_new(self): new = [] for email in self.emails: if email.id not in self.seen: new.append(email) return new def _received_emails(self, emails, inboxCount = None): ''' Subclasses should call this method with any new emails received. @param emails: a sequence of Email objects ''' old, self.emails[:] = self.emails[:], list(emails) new = self._get_new() for email in new: import plugin_manager.plugin_hub as plugin_hub plugin_hub.act('digsby.mail.newmessage.async', self, email) self._see_new(new) self.sort_emails(new) new = self.filter_new(new, old) del old info('%s - %s: %d new emails', self.__class__.__name__, self.name, len(new)) if inboxCount is not None: self._setInboxCount(inboxCount) self.new = new if new: profile.when_active(self.fire_notification) self.error_count = 0 self.change_state(self.Statuses.ONLINE) self._dirty_error = True # Next error will be new def fire_notification(self): if self.new: self._notify_emails(self.new) def popup_buttons(self, item): return [] def _notify_emails(self, emails, always_show = None, allow_click = True): if self.enabled: fire('email.new', emails = emails, onclick = self.OnClickEmail if allow_click else None, always_show = always_show, buttons = self.popup_buttons, icon = self.icon) def _setInboxCount(self, inboxCount): self.setnotifyif('count', inboxCount) @action() @callsback def OnComposeEmail(self, to='', subject='', body='', cc='', bcc='', callback = None): import hooks; hooks.notify('digsby.statistics.email.compose') for name in ('to','subject', 'body', 'cc', 'bcc'): assert isinstance(vars()[name], basestring), (name, type(vars()[name]), vars()[name]) if self.mailclient and try_this(lambda: self.mailclient.startswith('file:'), False): os.startfile(self.mailclient[5:]) elif self.mailclient == 'sysdefault': kw = {} for name in ('subject', 'body', 'cc', 'bcc'): if vars()[name]: kw[name] = vars()[name] query = UrlQuery('mailto:' + quote(to), **kw) log.info('OnComposeEmail is launching query: %s' % query) try: os.startfile(query) except WindowsError: # WindowsError: [Error 1155] No application is associated with the specified file for this operation: 'mailto:' mailclient_error() raise elif self.mailclient == '__urls__': url = self.custom_compose_url if url is not None: launch_browser(url) else: url = self.compose(to, subject, body, cc, bcc) if url: launch_browser(url) callback.success() @property def client_name(self): "Returns a string representing this email account's mail client." mc = self.mailclient if mc in (None, True, False): return self.protocol_info().name elif mc.startswith('file:'): return path(mc).basename().title() elif mc == 'sysdefault': #HACK: Use platform specific extensions to extract the actual #application name from the executable. for now, use ugly path #TLDR: needs the registry return '' elif mc == '__urls__': return '' else: log.warning('unknown mailclient attribute in %r: %s', self, mc) return _('Email Client') @action() def OnClickInboxURL(self, e = None): import hooks; hooks.notify('digsby.statistics.email.inbox_opened') if self.mailclient: url = self.start_client_email() if url is None: return else: url = self.inbox_url launch_browser(self.inbox_url) DefaultAction = OnClickHomeURL = OnClickInboxURL opening_email_marks_as_read = True def OnClickEmail(self, email): import hooks; hooks.notify('digsby.statistics.email.email_opened') if self.mailclient: self.start_client_email(email) else: url = self.urlForEmail(email) launch_browser(url) # For accounts where we are guaranteed to actually read the email # on click (i.e., ones that use webclients and have autologin on), # decrement the email count. if self.opening_email_marks_as_read: self._remove_email(email) @callsback def OnClickSend(self, to='', subject='', body='', cc='', bcc='', callback = None): ''' Sends an email. ''' getattr(self, 'send_email', self.OnComposeEmail)(to=to, subject=subject, body=body, cc=cc, bcc=bcc, callback = callback) def start_client_email(self, email=None): log.info('mailclient: %s', self.mailclient) import os.path if self.mailclient == 'sysdefault': launch_sysdefault_email(email) elif self.mailclient == '__urls__': url = self.custom_inbox_url if url is not None: launch_browser(url) elif try_this(lambda:self.mailclient.startswith('file:'), False): filename = self.mailclient[5:] if os.path.exists(filename): os.startfile(filename) else: log.warning('cannot find %s', filename) def __len__(self): return self.count def __iter__(self): return iter(self.emails) can_has_preview = False @property def icon(self): from gui import skin from util import try_this return try_this(lambda: skin.get('serviceicons.%s' % self.protocol), None) @property def inbox_url(self): ''' Return the url for the user's inbox to be opened in browser. This should adhere to the 'privacy.www_auto_signin' pref. ''' raise NotImplementedError def observe_count(self,callback): self.add_gui_observer(callback, 'count') self.emails.add_gui_observer(callback) def unobserve_count(self, callback): self.remove_gui_observer(callback, 'count') self.emails.remove_gui_observer(callback) def observe_state(self, callback): self.add_gui_observer(callback, 'enabled') self.add_gui_observer(callback, 'state') def unobserve_state(self, callback): self.remove_gui_observer(callback, 'enabled') self.remove_gui_observer(callback, 'state') @property def header_funcs(self): return [('Inbox',self.OnClickInboxURL), ('Compose', self.OnComposeEmail)] def _get_options(self): opts = UpdateMixin.get_options(self) return opts def update_info(self, **info): flush_state = False with self.frozen(): for k, v in info.iteritems(): if k in ('password', 'username', 'server') and getattr(self, k, None) != v: flush_state = True self.setnotifyif(k, v) # Tell the server. profile.update_account(self) if flush_state: log.info('Resetting state for %r', self) self._reset_state() self._dirty_error = True def _reset_state(self): return NotImplemented def update(self): # Protocols must override this method to check for new messages. if self.update == EmailAccount.update: log.warning('not implemented: %s.update', self.__class__.__name__) raise NotImplementedError if not self.enabled: return log.info('%s (%s) -- preparing for update. update called from: %s', self, self.state, get_func_name(2)) if self.state == self.Statuses.OFFLINE: # First check -- either after creation or failing to connect for some reason self.change_state(self.Statuses.CONNECTING) elif self.state == self.Statuses.ONLINE: # A follow-up check. self.change_state(self.Statuses.CHECKING) elif self.state == self.Statuses.CONNECTING: # Already connecting -- if there have been errors this is just the Nth attempt. # if there are not errors, something is wrong! -- disconnect if not self.error_count: log.error('%s -- called update while connecting, and no errors! disconnecting...',self) self.set_offline(self.Reasons.CONN_FAIL) else: log.error('Unexpected state for update: %r', self.state) @action(lambda self: (self.state != self.Statuses.CHECKING)) def update_now(self): 'Invoked by the GUI.' netcall(self.update) self.timer.reset(self.updatefreq) @action() def tell_me_again(self): if self.emails: emails = self.emails allow_click = True else: from mail.emailobj import Email emails = [Email(id = -1, fromname = _('No unread mail'))] allow_click = False # Use "always_show" to always show a popup, regardless of whether the # user has popups enabled or not. self._notify_emails(emails, always_show = ['Popup'], allow_click = allow_click) @action() def auth(self): netcall(self.authenticate) def Connect(self): self.change_reason(self.Reasons.NONE) call_later(1.5, self.update) @action() def compose(self, to, subject, body, cc, bcc): ''' Return a link for a browser that will bring the user to a compose window (or as close as possible, adhering to the 'privacy.www_auto_signin' pref). ''' raise NotImplementedError def urlForEmail(self, email): ''' Return a link to be opened by a browser that will show the user the email (or as close as possible, adhering to the 'privacy.www_auto_signin' pref). email -- the email OBJECT that we want the URL for. ''' raise NotImplementedError @action() def open(self, email_message): ''' ''' if type(self) is EmailAccount: raise NotImplementedError def _remove_email(self, email_message): try: self.emails.remove(email_message) except ValueError: # already removed pass else: self.setnotifyif('count', self.count - 1) @action() def markAsRead(self, email_message): ''' Mark the email object as read. ''' import hooks; hooks.notify('digsby.statistics.email.mark_as_read') self._remove_email(email_message) @action() def delete(self, email_message): import hooks; hooks.notify('digsby.statistics.email.delete') self._remove_email(email_message) @action() def archive(self, email_message): import hooks; hooks.notify('digsby.statistics.email.archive') self._remove_email(email_message) @action() def reportSpam(self, email_message): import hooks; hooks.notify('digsby.statistics.email.spam') self._remove_email(email_message)
class DigsbyProfile(Observable, ChatProtocol): 'A collection of accounts and preferences.' MAX_ICON_SIZE = 96 MAX_ICON_BYTES = 64 * 1024 protocol = 'digsby' @property def display_name(self): from common import pref return try_this(lambda: getattr(self, pref('profile.display_attr')), self.username) def __init__(self, identity): Observable.__init__(self) ChatProtocol.__init__(self) self.identity = identity from AccountManager import AccountManager self.PreDisconnectHooks = Delegate() self.PostDisconnectHooks = Delegate() if not getattr(getattr(sys, 'opts', None), 'limit_log', True): DelayedStreamLimiter = lambda s: s else: from fileutil import DelayedStreamLimiter self.consolehandlers = defaultdict(lambda: console_handler_class(DelayedStreamLimiter(sys.stdout))) self._status = None self.error_count = 0 self.offline_reason = StateMixin.Reasons.NONE self.account_manager = AccountManager(profile = self) self.last_hiber_req = None self.hibernated = False self.linked_observers = False self.xfers = ObservableList() self.prefs = ObservableDict() self.defaultprefs = ObservableDict(prefs.defaultprefs()) self.quiet = False self.prefs_loaded = False # set the common.pref lookup to point to our prefs import common common.set_active_prefs(self.prefs, self.defaultprefs) self.prefs.add_observer(self._prefs_changed) self.has_authorized = False self.statuses = ObservableList() self.statuses.add_observer(self._statuses_changed) self._xfercount = 0 self.xfers.add_observer(self._on_file_transfer) self.widgets = ObservableList() self._encrypter, self._decrypter = util.cryptography.cipher_functions(sha1(self.password.encode('utf8')).digest()[:16]) self.log_sizes = LogSizeDict() global profile if profile not in (self, None): warnmsg = 'Another DigsbyProfile has been created but the old one is still around!' if __debug__: raise ValueError(warnmsg) else: log.critical(warnmsg) profile = self # hack! BuddyListStore needs profile.username from contacts.buddyliststore import BuddyListStore self.blist = BuddyListStore(self.account_manager.connected_accounts) self.set_contact_info = self.blist.set_contact_info self.get_contact_info = self.blist.get_contact_info from BlobManager import BlobManager self.blob_manager = BlobManager(self) self.account_manager.add_observer(self.check_loading, 'got_accounts') self.account_manager.add_observer(self.on_accounts_loaded, 'accounts_loaded') self.blob_manager.add_observer(self.check_loading, 'loading') self.loaded = False self.OnReturnFromIdle = Delegate() self.on_message = PausableDelegate() self.OnStatusChange = Delegate() self.setup_hub() self.idle_timer = None self.idle = False self.plugins_setup = False self.connection = None self.setup_plugins() self.do_local_load() @property def password(self): return self.identity.password @property def username(self): return self.identity.name def setup_plugins(self, *a, **k): assert not self.plugins_setup if not self.plugins_setup: self.plugins_setup = True wx.CallAfter(self._setup_plugins) def _setup_plugins(self): for hook in Hook('digsby.profile.addons'): try: getattr(hook(self), 'setup', lambda *a, **k: None)() except Exception: traceback.print_exc() import plugin_manager.plugin_hub as plugin_hub plugin_hub.act('digsby.plugin.load.async') def stop_timers(self): if self.idle_timer: self.idle_timer.stop() def _get_status(self): if self._status is None: self._status = self.load_saved_status() return self._status def _set_status(self, val): # digsby go_idle self._status = val if val is not None: if val.status != 'Idle': self.save_status() status = property(_get_status, _set_status) def setup_hub(self): import hub h = hub.get_instance() if h.filter_message not in self.on_message: self.on_message += h.filter_message # register IM windows for incoming messages from gui.imwin import on_message self.on_message += lambda *a, **k: wx.CallAfter(on_message, *a, **k) self.on_message.pause() def set_profile_blob(self, new_profile): self.profile = new_profile fstr = self.profile for acct in profile.account_manager.connected_accounts: self.set_formatted_profile(acct.connection, fstr) self.save() def on_chat_invite(self, protocol, buddy, message, room_name, on_yes=None, on_no=None): @wx.CallAfter def after(): import hub hub.get_instance().on_invite(protocol=protocol, buddy=buddy, message=message, room_name=room_name, on_yes=on_yes, on_no=on_no) def on_entered_chat(self, convo): return self.on_message(convo=convo, raisenow=True) def set_formatted_profile(self, protocol, fstr=None): if fstr is None: fstr = self.profile # $$plugin setprofile import plugin_manager.plugin_hub as plugin_hub if not plugin_hub.act('digsby.im.setprofile.pre', protocol, fstr): return plugin_hub.act('digsby.im.setprofile.async', protocol, fstr) add_promo_string = self.prefs.get('profile.promote', True) if fstr.bestFormat == "rtf": if add_promo_string: fstr = fstr + PROMOTE_STRING_RTF format = None else: #legacy profile support if add_promo_string: fstr = fstr.format_as("plaintext").encode('xml') + PROMOTE_STRING_HTML from gui.uberwidgets.formattedinput import get_default_format format = get_default_format('profile.formatting') netcall(lambda: protocol.set_profile(fstr, format)) def set_profile(self, *a, **k): pass def _on_file_transfer(self, src, attr, old, new): if all((not getattr(x, 'autoshow', True)) for x in new if x not in (old or []) and (x.state not in (x.states.CompleteStates | x.states.FailStates))): self._xfercount = len(new) return new, old = len(self.xfers), self._xfercount if self.prefs.get('filetransfer.window.show_on_starting', True) and new > old: from gui.filetransfer import FileTransferDialog wx.CallAfter(FileTransferDialog.Display) self._xfercount = new def __repr__(self): return AccountBase._repr(self) def _reconnect(self, initial=False): if getattr(self, 'connection', None) is not None: self.connection.observers.clear() self.connection.Disconnect() del self.connection self.disconnecting = False extra = {} resource = getattr(getattr(sys, 'opts', None), 'resource', None) if resource is not None: extra['resource'] = resource elif getattr(sys, 'DEV', False): extra['resource'] = 'dev' import hub conn = self.connection = DigsbyProtocol(self.username, self.password, self, hub.get_instance(), DIGSBY_SERVER, # srvs, do_tls = False, sasl_md5 = False, digsby_login=True, initial=initial, **extra ) conn.account = self conn.add_observer(self.connection_state_changed, 'state') conn.add_observer(self.offline_changed, 'offline_reason') conn.Connect(on_success = getattr(getattr(self, 'callback', None), 'success', None), on_fail = self.connect_error) def do_local_load(self): self.local_load_exc = None self.blob_manager.load_from_identity(identity = self.identity) self.account_manager.load_from_identity(identity = self.identity) def connect_error(self): if self.has_authorized: return self.callback.error() else: return self.local_login() def local_login(self): ''' After a failed network login, attempt to "log in" with self.username and self.password to the local accounts store. ''' try: exc, self.local_load_exc = self.local_load_exc, None if exc is not None: raise exc except digsbylocal.InvalidPassword: self.callback.error(DigsbyLoginError('auth')) except Exception: self.callback.error() else: self.blob_manager.local_load() # Setup connection and call load_cb self.connection_state_changed(None, 'state', None, DigsbyProtocol.Statuses.AUTHORIZED) self.connection_state_changed(None, 'state', None, DigsbyProtocol.Statuses.ONLINE) self.offline_reason = DigsbyProtocol.Reasons.CONN_LOST self.offline_changed(None, 'offline_reason', None, DigsbyProtocol.Reasons.CONN_LOST) self.connection_state_changed(None, 'state', None, DigsbyProtocol.Statuses.OFFLINE) self.account_manager.do_load_local_notification() def offline_changed(self, src, attr, old, new): self.notify(attr, old, new) def connection_state_changed(self, src, attr, old, new): assert False log.info('connection_state_changed %r -> %r', old, new) assert type(src) in (DigsbyProtocol, type(None)) if attr == 'state' and new == getattr(DigsbyProtocol.Statuses, 'AUTHORIZED', Sentinel()): self.error_count = 0 self.watch_account(self) self.has_authorized = True log.info('Calling load with cb of %r', self.callback) self.load(self.callback) conn = self.connection if conn is not None: conn._set_status_object(profile.status) elif attr == 'state' and new == DigsbyProtocol.Statuses.OFFLINE: self.setnotifyif('offline_reason', getattr(src, 'offline_reason', None)) if not self.has_authorized and getattr(src, 'offline_reason', None) == DigsbyProtocol.Reasons.BAD_PASSWORD: self.unwatch_account(self) if self in self.account_manager.connected_accounts: self.account_manager.connected_accounts.remove(self) self.account_manager.unwatch_account(self) self.connection = None dccb = getattr(self, '_disconnect_cb', None) if dccb is not None: self._disconnect_cb = None dccb.success() elif attr == 'state' and new == DigsbyProtocol.Statuses.ONLINE: self.reconnected_callbacks(self.connection) self.notify('state', old, new) @property def state(self): return try_this(lambda: self.connection.state, StateMixin.Statuses.OFFLINE) def when_active(self, callback): if not hasattr(callback, '__call__'): raise TypeError('argument "callback" must be callable') if self.idle: if callback not in self.OnReturnFromIdle: self.OnReturnFromIdle += callback log.info('added a callback to the idle queue: %r', callback) else: log.info('callback already in idle queue') else: log.info('not idle, calling now') return callback() @wxcall def signoff(self, kicked=False): 'Return to the splash screen.' # $$plugin unload import plugin_manager.plugin_hub as plugin_hub plugin_hub.act('digsby.plugin.unload.async') if platformName == 'win': return wx.GetApp().Restart() # todo: confirm if there are (active) file transfers # hide all top level windows top = wx.GetTopLevelWindows for win in top(): win.Hide() del self.on_message[:] del self.OnReturnFromIdle[:] self.stop_timers() def dodisconnect(success = True): if not success: log.info('there was an error saving all blobs.') # disconnect all accounts with traceguard: self.disconnect() # destroy all top level windows f = getattr(wx.GetApp(), 'buddy_frame', None) if f: f.on_destroy() for win in top(): with traceguard: if not win.IsDestroyed(): win.Destroy() # clear input shortcuts from gui.input.inputmanager import input_manager input_manager.reset() import gc import observe gc.collect() numCleared = observe.clear_all() log.info('cleared %d observers dicts', numCleared) # show the splash, preventing autologin wx.GetApp().ShowSplash(autologin_override = False, kicked=kicked) log.info('saving all blobs before signoff...') self.save(success = dodisconnect, error = lambda: dodisconnect(False)) from gui import toast toast.cancel_all() def _statuses_changed(self, src, attr, old, new): if not hasattr(self, 'status_timer'): self.status_timer = t = ResetTimer(STATUS_UPDATE_FREQ_SECS, self._on_status_timer) t.start() else: self.status_timer.reset() def _on_status_timer(self): self.status_timer.stop() netcall(lambda: self.save('statuses')) def _prefs_changed(self, src, attr, old, new): if not hasattr(self, 'pref_timer'): self.pref_timer = t = ResetTimer(PREF_UPDATE_FREQ_SECS, self._on_pref_timer) t.start() else: self.pref_timer.reset() def _on_pref_timer(self): self.pref_timer.stop() netcall(lambda: self.save('prefs')) def SetStatusMessage(self, message, editable = True, edit_toggle = True, **k): new_status = StatusMessage(title = None, status = self.status.status, message = message, editable = editable, edit_toggle = edit_toggle) import hooks hooks.notify('digsby.statistics.ui.select_status') self.set_status(new_status) def maybe_return_from_offline(self): '''Called by IM accounts when they are connecting to clear an "Offline" status.''' if hasattr(self, 'were_connected'): log.info("protocol has 'were_connected', deleting and setting Available") del self.were_connected status = getattr(self, 'were_connected_status', StatusMessage.Available) self.set_status(status) def set_status(self, status): ''' Takes a StatusMessage object and sets the status in all connected (and which will connect in the future) accounts. ''' if status == self.status: return log.warning('set_status got an identical status.') # $$plugin status change from plugin_manager import plugin_hub plugin_hub.act('digsby.im.mystatuschange.pre', status) if status == '': return plugin_hub.act('digsby.im.mystatuschange.async', status) for hook in Hook('digsby.im.statusmessages.set.pre'): # can't use query or notify (want the chained effect) status = hook(status) log.warning('set_status got %r', status) accts = [a for a in self.account_manager.connected_accounts if a is not self] def allaccts(func): for a in accts: with traceguard: func(a) Offline = StatusMessage.Offline # disconnecting if status == Offline: log.info('disconnecting all connected accounts') # store a list of the accounts which were connected prior # to disconnecting. self.were_connected = accts[:] self.were_connected_status = self.status allaccts(lambda a: a.disconnect()) #reconnecting elif self.status == Offline and hasattr(self, 'were_connected'): accts = self.were_connected del self.were_connected for acct in accts: with traceguard: if acct in self.account_manager.accounts: acct.connect(invisible=(status.for_account(acct).invisible)) else: log.warning('not reconnecting %s', acct) else: for acct in self.account_manager.connected_accounts[:]: with traceguard: prev_invis = self.status.for_account(acct).invisible this_invis = status.for_account(acct).invisible #going to/returning from invisible if (prev_invis or this_invis) and this_invis != prev_invis: acct.connection.set_invisible(this_invis) #just setting a status if not this_invis: acct.connection._set_status_object(status) self.setnotifyif('status', status.copy(editable=None, edit_toggle=None)) self.save_status() hooks.notify('digsby.im.statusmessages.set.post', self.status) def add_account(self, **attrdict): # $$plugin self.account_manager.add(Account(**attrdict), 'im') import plugin_manager.plugin_hub as plugin_hub plugin_hub.act('digsby.im.addaccount.async', attrdict['protocol'], attrdict['name']) def add_email_account(self, **info): protocol = info.get('protocol') name = info.get('name') self.account_manager.add(proto_init(protocol)(**info), 'em') # $$plugin import plugin_manager.plugin_hub as plugin_hub plugin_hub.act('digsby.email.addaccount.async', protocol, name) def add_social_account(self, **info): protocol = info.pop('protocol') name = info.get('name') acct = proto_init(protocol)(**info) self.account_manager.add(acct, 'so') # $$plugin import plugin_manager.plugin_hub as plugin_hub plugin_hub.act('digsby.social.addaccount.async', protocol, name) return acct def register_account(self, on_success, on_fail, **attrdict): newacct = Account(**attrdict) newacct.connect(register = True, on_success=on_success, on_fail=on_fail) def update_account(self, account, force=False): self.account_manager.update_account(account, force=force) # $$plugin import plugin_manager.plugin_hub as plugin_hub plugin_hub.act('digsby.updateaccount.async', account) def add_status_message(self, status_obj = None, **info): if status_obj is None: assert info self.statuses.append(StatusMessage(**info)) else: assert info == {} self.statuses.append(status_obj) def remove_account(self, account): self.account_manager.remove(account) # $$plugin import plugin_manager.plugin_hub as plugin_hub plugin_hub.act('digsby.removeaccount.async', account) remove_email_account = \ remove_social_account = \ remove_account def remove_status_message(self, status_message): self.statuses.remove(status_message) def get_widgets(self): self.connection.get_widgets() def incoming_widgets(self, widgets): self.widgets[:] = widgets hooks.notify('digsby.widgets.result', widgets) def blob_failed(self, name): try: self.connection.Disconnect() self.offline_changed(None, 'offline_reason', None, DigsbyProtocol.Reasons.CONN_FAIL) except Exception: pass def update_blob(self, name, useful_data): if name == 'prefs': log.critical('prefs updated from the network') with self.prefs.flagged('network'): if 'defaultprefs' not in self.blob_manager.waiting_blobs: new_prefs = dictadd(self.defaultprefs, useful_data) self.prefs.update(new_prefs) else: self.prefs.update(useful_data) new_prefs = useful_data if hasattr(self, 'defaultprefs'): for key in set(self.prefs.keys()) - (set(new_prefs.keys()) | set(self.defaultprefs.keys())): self.prefs.pop(key, None) self.prefs_loaded = True hooks.notify('blobs.update.prefs', self.prefs) elif name == 'defaultprefs': if 'prefs' not in self.blob_manager.waiting_blobs: new_prefs = dictadd(useful_data, self.prefs) self.prefs.update(new_prefs) if hasattr(self, 'defaultprefs'): for key in set(self.defaultprefs.keys()) - set(useful_data.keys()): self.prefs.pop(key, None) self.defaultprefs.update(useful_data) elif name == 'buddylist': self.blist.update_data(useful_data) elif callable(getattr(self, '_incoming_blob_' + name, None)): getattr(self, '_incoming_blob_' + name)(useful_data) else: log.critical('replacing profile attribute %s', name) if name == 'statuses': assert False setattr(self, name, observable_type(useful_data)) def _incoming_blob_profile(self, profile_str_or_fmtstr): from util.primitives.fmtstr import fmtstr # self.profile used to be a string, but now it is a fmtstr, and goes out # over the wire as a JSON dict. # # assume that if we cannot parse the incoming profile blob as JSON, then # it must be an old-style string profile. if isinstance(profile_str_or_fmtstr, dict): fstr = fmtstr.fromDict(profile_str_or_fmtstr) else: from gui.uberwidgets.formattedinput import get_default_format fstr = fmtstr.singleformat(profile_str_or_fmtstr, format=get_default_format('profile.formatting')) self.profile = fstr def _incoming_blob_statuses(self, newdata): data = [(StatusMessage(**d) if isinstance(d, dict) else d) for d in newdata] self.statuses[:] = data def _incoming_blob_notifications(self, newdata): def fix_underscore(d): for key in d.keys()[:]: if key and '_' in key: d[key.replace('_', '.')] = d.pop(key) if not hasattr(self, 'notifications'): self.notifications = ObservableDict() else: fix_underscore(self.notifications) fix_underscore(newdata) self.notifications.update(newdata) import common.notifications # for any notification keys that exist in YAML, but not in the users # blob, add them with the values in the YAML 'default' key ni = common.notifications.get_notification_info() base = self.notifications[None] for k in ni: if k in base: continue try: defaults = ni[k].get('default', {}) base[k] = [dict(reaction=v) for v in defaults.get('reaction', ())] except Exception: traceback.print_exc() continue import hooks hooks.notify('digsby.notifications.changed') def load(self, cb): 'Loads network data from the server.' self.loaded = False def callback(_cb=cb): self.loaded = True log.info('Calling callback that was given to load: %r', _cb) _cb(lambda *a, **k: None) self.link_observers() with traceguard: conn = self.connection if conn is not None: conn.change_state(self.connection.Statuses.SYNC_PREFS) def on_accounts_loaded(): # show the myspace account wizard if all you have are the automagic accounts def after(): if len(self.accounts) == 0 and \ len(self.socialaccounts) == 0 and \ len(self.emailaccounts) == 0 and \ len(self.widgets) == 0: import gui.accountwizard gui.accountwizard.show() wx.CallLater(1000, after) on_accounts_loaded_cc = CallCounter(2, on_accounts_loaded) def call_cc(*a, **k): on_accounts_loaded_cc() import util.hook_util if not util.hook_util.OneShotHook(self, 'digsby.accounts.released.async')(call_cc, if_not_fired=True): call_cc() if not util.hook_util.OneShotHook(self, 'digsby.widgets.result')(call_cc, if_not_fired=True): call_cc() self.get_widgets() self.load_cb = callback log.info('Forcing check_loading call') self.check_loading() def link_observers(self): if self.linked_observers: return link = self.prefs.link for pref in ('become_idle', 'idle_after',): link('messaging.%s' % pref, getattr(self, '%s_changed' % pref)) self.setup_logger() self.prefs.add_observer(self.link_logging) for key in self.prefs: self.link_logging(self.prefs, key) self.linked_observers = True def check_loading(self, src=None, attr=None, old=None, new=None): if self.account_manager.got_accounts and not self.blob_manager.loading: # if self.connection is not None: # log.warning('connection is not None') # self.connection.change_state(self.connection.Statuses.ONLINE) initialized() if not self.loaded: self._have_connected = True # cb, self.load_cb = self.load_cb, (lambda *a, **k: None) self.loaded = True self.link_observers() # log.info('Calling load_cb: %r', cb) # cb() def on_accounts_loaded(self, src, attr, old, new): if new: log.info('unpausing the message queue') wx.CallAfter(self.on_message.unpause) def link_logging(self, src, key, *a, **k): n = 'logging' if not isinstance(key, basestring) or not key.startswith(n): return logname = key[len(n):] or None if logname is not None: logname = logname.strip('.') newlevel = try_this(lambda: int(get(src, key)), 0) logging.log(100, 'Setting %s to level %d', logname or 'root', newlevel) import main if not hasattr(main, 'full_loggers'): return # logging system not setup # don't bother modifying console handlers if we never setup any if not getattr(main, 'logging_to_stdout', False): return if not logname: logger = logging.getLogger('') s_handlers = [h for h in logger.handlers if (h.__class__ is console_handler_class) and h not in main.full_loggers] s_handlers[0].setLevel(newlevel) else: rootlogger = logging.getLogger('') root_handlers = [h for h in rootlogger.handlers if (h.__class__ is not console_handler_class) or h in main.full_loggers] handler = self.consolehandlers[newlevel] handler.setLevel(newlevel) from main import ConsoleFormatter formatter = ConsoleFormatter() handler.setFormatter(formatter) root_handlers.append(handler) new_logger = logging.getLogger(logname) new_logger.propagate = False new_logger.handlers[:] = root_handlers def setup_logger(self): 'Sets up an IM and event logging object.' from common.logger import Logger logger = self.logger = Logger() # logger receives all messages, incoming and outgoing. def later(*a, **k): wx.CallLater(1000, threaded(self.logger.on_message), *a, **k) self.on_message += lambda *a, **k: wx.CallAfter(later, *a, **k) set = lambda attr: lambda val: setattr(self.logger, attr, val) link = lambda attr, cb: self.prefs.link(attr, cb, obj = logger) link('log.ims', set('LogIMs')) link('log.ims', set('LogChats')) @callsback def save(self, saveblobs = None, force = False, callback = None): ''' Save one, or more, or all, data blobs. if saveblobs is: None: saves all of them a string: it must be one of the blob names a sequence: all blobs in the sequence will be saved ''' if saveblobs is None: # None means all blobs saveblobs = self.blob_manager.blob_names elif isinstance(saveblobs, basestring): # put a string into a list saveblobs = [saveblobs] # check for correct blobnames diff = set(saveblobs) - set(self.blob_manager.blob_names) if len(diff) > 0: raise ValueError('illegal blob names: %s' % ', '.join(diff)) saveblobs = set(saveblobs) waiting = set(self.blob_manager.waiting_blobs) output = saveblobs - waiting if len(output) < len(saveblobs): log.info("blobs failed to save, not yet loaded: %r", waiting & saveblobs) if self.blob_manager.loading: info('blobs still loading, disallowing save') callback.success() return else: saveblobs = list(output) info('saving blobs %s', ', '.join(saveblobs)) cbs = [] for name in saveblobs: if name == 'buddylist': cbs.append(partial(self.blob_manager.set_blob, name, data = self.blist.save_data(), force = force)) elif name == 'prefs': cbs.append(partial(self.save_out_prefs, force = force)) elif name == 'defaultprefs': pass elif name == 'statuses': data = [s.__getstate__(network=True) for s in self.statuses] for s in data: s['format'] = dict(s['format']) if s['format'] is not None else None cbs.append(partial(self.blob_manager.set_blob, name, data = data, force = force)) elif name == 'profile': data = self.profile.asDict() cbs.append(partial(self.blob_manager.set_blob, name, data=data, force=force)) else: cbs.append(partial(self.blob_manager.set_blob, name, data = to_primitive(getattr(self, name)), force = force)) do_cb(cbs, callback = callback) def backup_blobs(self, dir): pth = path(dir) from util.json import pydumps from time import time for name in ['profile', 'buddylist', 'notifications', 'prefs', 'statuses', 'icon']: if name == 'buddylist': data = self.blist.save_data() elif name == 'prefs': data = to_primitive(dictdiff(profile.defaultprefs, self.prefs)) elif name == 'defaultprefs': pass elif name == 'statuses': data = [s.__getstate__() for s in self.statuses] for s in data: s['format'] = dict(s['format']) else: data = to_primitive(getattr(self, name)) f = pth / name + '_' + str(int(time())) + '.blob' with f.open('wb') as out: if name == 'icon': out.write(data) else: out.write(pydumps(data).encode('z')) @property def localprefs(self): return localprefs() @callsback def save_blob(self, name, data, callback = None): assert name not in ('buddylist', 'prefs', 'defaultprefs', 'statuses') log.critical('replacing attribute %s in profile', name) setattr(self, name, data) self.blob_manager.set_blob(name, data = to_primitive(getattr(self, name)), callback = callback) @callsback def save_out_prefs(self, force = False, callback = None): 'Pack the data and send it to the server.' data = dictdiff(profile.defaultprefs, self.prefs) self.blob_manager.set_blob('prefs', data = to_primitive(data), force = force, callback = callback) @callsback def disconnect(self, callback = None): if getattr(self, 'disconnecting', False): return self.disconnecting = True self.PreDisconnectHooks() complete_disconnect = lambda: self._finish_disconnect(callback=callback) self.account_manager.disconnect_all( success = lambda : self.disconnect_profile(success = complete_disconnect, error = complete_disconnect)) self._force_dc_timer = util.Timer(DISCONNECT_TIMEOUT, complete_disconnect) self._force_dc_timer.start() self.stop_timers() @callsback def disconnect_profile(self, callback = None): log.info('Disconnect digsbyprofile') self._disconnect_cb = callback if getattr(self, 'connection', None) is not None: self.connection.Disconnect() def _finish_disconnect(self, callback): try: log.info('finishing profile disconnect') if getattr(self, '_force_dc_timer', None) is not None: self._force_dc_timer.stop() self._force_dc_timer = None self.PostDisconnectHooks() finally: callback.success() def hibernate(self): #called from windows (should be on wx thread) self.last_hiber_req = HIBERNATE self.check_hibernate_state() def unhibernate(self, delay = 15): #called from windows (should be on wx thread) self.last_hiber_req = UNHIBERNATE delay = max(int(delay), 0) if delay: wx.CallLater(delay * 1000, self.check_hibernate_state) else: self.check_hibernate_state() def check_hibernate_state(self): if self.last_hiber_req == HIBERNATE: if self.hibernated: return else: self.hibernated = True self._do_hibernate() return elif self.last_hiber_req == UNHIBERNATE: if not self.hibernated: return else: self.hibernated = False self._do_unhibernate() return def _do_hibernate(self): log.warning("HIBERNATING") self.hibernated_im = hibernated_im = [] self.hibernated_email = hibernated_email = [] self.hibernated_social = hibernated_social = [] for a in self.account_manager.connected_accounts[:]: if a is not self: with traceguard: a.disconnect() hibernated_im.append(a) for a in self.account_manager.emailaccounts: with traceguard: if a.enabled: a.set_enabled(False) a.disconnect() hibernated_email.append(a) for a in self.account_manager.socialaccounts: with traceguard: if a.enabled: a.set_enabled(False) a.Disconnect() hibernated_social.append(a) if getattr(self, 'connection', None) is not None: self.connection.Disconnect() log.warning("HIBERNATED") def _do_unhibernate(self): log.warning("UN-HIBERNATING") hibernated_im = self.hibernated_im hibernated_email = self.hibernated_email hibernated_social = self.hibernated_social for a in hibernated_im: with traceguard: a._reconnect() for a in hibernated_email: with traceguard: a.set_enabled(True) for a in hibernated_social: with traceguard: a.set_enabled(True) self._reconnect() log.warning("UN-HIBERNATED") @property def allow_status_changes(self): 'Used by the main status combo do decide whether or not to show itself.' if hasattr(self, 'were_connected'): # This means that "Disconnected" was selected in the Status dialog # were_connected is a list of account objects to reconnect if the # status is changed again. return True connecting = [a for a in self.account_manager.accounts if getattr(a, 'connection', None) is not None and a.connection.state != a.connection.Statuses.OFFLINE] if connecting: return True return False def plain_pw(self, password): "Returns pw decrypted with the profile's password as the key." return self._decrypter(password if password is not None else '').decode('utf-8') def crypt_pw(self, password): "Returns pw encrypted with the profile's password as the key." if password and not isinstance(password, unicode): print_stack() return self._encrypter((password if password is not None else '').encode('utf-8')) @property def is_connected(self): return bool(getattr(self, 'connection', None) and (self.connection.state == self.connection.states['Connected'] or self.connection.is_connected)) # # buddy icon # def get_icon_bitmap(self): 'Returns the current buddy icon.' if self.icon is None: log.info('get_icon_bitmap: self.icon is None, returning None') return None elif self.icon == '\x01': # a single 1 byte in the database means "use the default" # and is set in newly created accounts. img = wx.Image(path('res') / 'digsbybig.png') if not img.Ok(): log.warning('get_icon_bitmap: could not load digsbybig.png, returning None') return None return wx.BitmapFromImage(img).Resized(self.MAX_ICON_SIZE) else: try: return Image.open(StringIO(self.icon)).WXB except Exception: log.warning('could not create wxImageFromStream with profile.icon data') return None def get_icon_bytes(self): if self.icon is None: return None elif self.icon == '\x01': return (path('res') / 'digsbybig.png').bytes() else: return self.icon @property def name(self): return self.username def protocol_info(self): return protocols['digsby'] @property def metacontacts(self): return self.blist.metacontacts @property def buddylist(self): 'Returns the buddylist GUI window.' return wx.FindWindowByName('Buddy List').Children[0].blist def __getattr__(self, attr): try: return Observable.__getattribute__(self, attr) except AttributeError, e: try: return getattr(self.account_manager, attr) except AttributeError: raise e
class RoomListModel(EventMixin): events = set(( 'contacts_changed', )) def __init__(self, contacts): EventMixin.__init__(self) self.contacts = None self.offline = False self.set_contacts(contacts) def _init_contacts(self): self.contacts_view = [] self.contact_enter_times = {} self.contact_leave_times = {} self.pending_contacts = ObservableList() def set_contacts(self, contacts): if hasattr(self.contacts, 'remove_observer'): self.contacts.remove_observer(self._on_contacts_changed) self.contacts = contacts self._init_contacts() for contact in contacts: self.contact_enter_times[contact] = 0 if hasattr(self.contacts, 'add_observer'): self.contacts.add_observer(self._on_contacts_changed) self._on_contacts_changed() def _on_contacts_changed(self, *a, **k): self._update_pending() self._update_view() self.fire_contacts_changed() def fire_contacts_changed(self): self.event('contacts_changed') TIME_LEAVING = 4 def _update_view(self): self.leaving_contacts = view = [] now = time() for gone_contact, t in list(self.contact_leave_times.items()): if now - t > self.TIME_LEAVING: self.contact_leave_times.pop(gone_contact, None) else: #leaving view.append(gone_contact) old_length = len(self.contacts_view) self.contacts_view = sorted(list(self.contacts) + view) return len(self.contacts_view) != old_length def _update_pending(self): for pending_contact in list(self.pending_contacts): for contact in self.contacts: if pending_contact.equals_chat_buddy(contact): self.pending_contacts.remove(pending_contact) contacts = set(self.contacts) now = time() for contact in list(self.contact_enter_times.keys()): if contact not in contacts: self.contact_enter_times.pop(contact, None) self.contact_leave_times.setdefault(contact, now) for contact in contacts: if contact not in self.contact_enter_times: self.contact_enter_times.setdefault(contact, now) def contact_is_new(self, contact): time_joined = self.contact_enter_times.get(contact, None) if time_joined is None: return False TIME_NEW = 2 now = time() return now - time_joined < TIME_NEW def contact_is_leaving(self, contact): time_left = self.contact_leave_times.get(contact, None) return time_left is not None and time() - time_left < self.TIME_LEAVING def add_pending_contact(self, contact): if contact not in self.pending_contacts: self.pending_contacts.append(contact) self.event('contacts_changed') return True def remove_pending_contact(self, contact): try: self.pending_contacts.remove(contact) except ValueError: return False else: self.event('contacts_changed') return True @property def length_including_pending(self): return self.length + len(self.pending_contacts) @property def length(self): return len(self.contacts_view) def get_contact(self, n): return self.contacts_view[n] def get_pending_contact(self, n): return self.pending_contacts[n]
class EmailAccount(AccountBase, UpdateMixin, FromNetMixin): retry_time = 3 error_max = 3 def __init__(self, enabled=True, updateNow=True, **options): AccountBase.__init__(self, **options) UpdateMixin.__init__(self, **options) FromNetMixin.__init__(self, **options) self.emails = ObservableList() self.count = 0 self.seen = set() self._dirty_error = True # The next error is new. log.info('Created EmailAccount: %r. Setting enabled to %r', self, enabled) self.enabled = enabled def timestamp_is_time(self, tstamp): return True # # local prefs # mailclient = localprefprop(localprefs_key('mailclient'), None) custom_inbox_url = localprefprop(localprefs_key('custom_inbox_url'), None) custom_compose_url = localprefprop(localprefs_key('custom_compose_url'), None) email_address = util.iproperty('get_email_address', 'set_email_address') @property def extra_header_func(self): return None # inviting disabled if self.protocol not in ('aolmail', 'ymail', 'gmail', 'hotmail'): return None import hooks d = {} for attr in ('protocol', 'name', 'password'): d[attr] = getattr(self, attr) return ( "Invite Contacts", lambda *a, **k: hooks.notify('digsby.email.invite_clicked', **d)) def get_email_address(self): from util import EmailAddress try: return str(EmailAddress(self.name, self.default_domain)).decode('ascii') except (AttributeError, ValueError): try: ret = self.name if '@' in self.name else self.name + '@' + self.default_domain if isinstance(ret, bytes): return ret.decode('ascii') else: return ret except Exception: # hopefully bad data has been caught before here. if isinstance(self.name, bytes): return self.name.decode('ascii', 'replace') else: return self.name def set_email_address(self, val): # Not supported pass @property def display_name(self): return try_this(lambda: getattr(self, pref('email.display_attr')), self.email_address) def on_error(self, task=None): ''' Called when an error occurs. task is a callable that can be used to make another attempt at whatever caused the error (if error_count is less than max_error). ''' self.error_count += 1 log.error('%r\'s error count is now: %d', self, self.error_count) log.error('on_error called from %s', get_func_name(2)) if self.error_count < pref('email.err_max_tolerance', self.error_max): if task is None: task = self.update_now log.error('error count is under, calling %r now', task) if not callable(task): # If it was an exception assume that update_now was called. (the account type # probably just hasn't been fixed yet task = self.update_now util.call_later(pref('email.err_retry_time', type=int, default=2), task) else: log.error('assuming the connection has died') self.set_offline(self.Reasons.CONN_FAIL) self.error_count = 0 del self.emails[:] def bad_pw(self): log.info('%r: changing state to BAD_PASSWORD', self) self.set_offline(self.Reasons.BAD_PASSWORD) self.timer.stop() def no_mailbox(self): log.info('%r: changing state to NO_MAILBOX', self) self.set_offline(self.Reasons.NO_MAILBOX) self.timer.stop() def __repr__(self): r = AccountBase.__repr__(self)[:-1] r += ', ' r += 'enabled' if self.enabled else 'disabled' return r + '>' @property def web_login(self): return (pref('privacy.www_auto_signin') and self.state in (self.Statuses.ONLINE, self.Statuses.CHECKING)) def error_link(self): reason = self.Reasons linkref = { reason.BAD_PASSWORD: ('Edit Account', lambda *a: profile.account_manager.edit(self, True)), reason.CONN_FAIL: ('Retry', lambda *a: self.update_now()) } if self.offline_reason in linkref: name, callback = linkref[self.offline_reason] return name, callback else: return None def sort_emails(self, new=None): self.emails.sort() if new is not None: new.sort() def filter_new(self, new, old): if old: return [e for e in new if e <= old[-1]] elif self.seen: new_ids = set(e.id for e in new) keep = set() for email in self.emails: if email.id in new_ids: keep.add(email.id) else: break return [e for e in new if e.id in keep] else: return list(new) def _see_new(self, new_messages): new_seen = set() for email in new_messages: new_seen.add(email.id) self.seen.update(new_seen) def _get_new(self): new = [] for email in self.emails: if email.id not in self.seen: new.append(email) return new def _received_emails(self, emails, inboxCount=None): ''' Subclasses should call this method with any new emails received. @param emails: a sequence of Email objects ''' old, self.emails[:] = self.emails[:], list(emails) new = self._get_new() for email in new: import plugin_manager.plugin_hub as plugin_hub plugin_hub.act('digsby.mail.newmessage.async', self, email) self._see_new(new) self.sort_emails(new) new = self.filter_new(new, old) del old info('%s - %s: %d new emails', self.__class__.__name__, self.name, len(new)) if inboxCount is not None: self._setInboxCount(inboxCount) self.new = new if new: profile.when_active(self.fire_notification) self.error_count = 0 self.change_state(self.Statuses.ONLINE) self._dirty_error = True # Next error will be new def fire_notification(self): if self.new: self._notify_emails(self.new) def popup_buttons(self, item): return [] def _notify_emails(self, emails, always_show=None, allow_click=True): if self.enabled: fire('email.new', emails=emails, onclick=self.OnClickEmail if allow_click else None, always_show=always_show, buttons=self.popup_buttons, icon=self.icon) def _setInboxCount(self, inboxCount): self.setnotifyif('count', inboxCount) @action() @callsback def OnComposeEmail(self, to='', subject='', body='', cc='', bcc='', callback=None): import hooks hooks.notify('digsby.statistics.email.compose') for name in ('to', 'subject', 'body', 'cc', 'bcc'): assert isinstance(vars()[name], basestring), (name, type(vars()[name]), vars()[name]) if self.mailclient and try_this( lambda: self.mailclient.startswith('file:'), False): os.startfile(self.mailclient[5:]) elif self.mailclient == 'sysdefault': kw = {} for name in ('subject', 'body', 'cc', 'bcc'): if vars()[name]: kw[name] = vars()[name] query = UrlQuery('mailto:' + quote(to), **kw) log.info('OnComposeEmail is launching query: %s' % query) try: os.startfile(query) except WindowsError: # WindowsError: [Error 1155] No application is associated with the specified file for this operation: 'mailto:' mailclient_error() raise elif self.mailclient == '__urls__': url = self.custom_compose_url if url is not None: launch_browser(url) else: url = self.compose(to, subject, body, cc, bcc) if url: launch_browser(url) callback.success() @property def client_name(self): "Returns a string representing this email account's mail client." mc = self.mailclient if mc in (None, True, False): return self.protocol_info().name elif mc.startswith('file:'): return path(mc).basename().title() elif mc == 'sysdefault': #HACK: Use platform specific extensions to extract the actual #application name from the executable. for now, use ugly path #TLDR: needs the registry return '' elif mc == '__urls__': return '' else: log.warning('unknown mailclient attribute in %r: %s', self, mc) return _('Email Client') @action() def OnClickInboxURL(self, e=None): import hooks hooks.notify('digsby.statistics.email.inbox_opened') if self.mailclient: url = self.start_client_email() if url is None: return else: url = self.inbox_url launch_browser(self.inbox_url) DefaultAction = OnClickHomeURL = OnClickInboxURL opening_email_marks_as_read = True def OnClickEmail(self, email): import hooks hooks.notify('digsby.statistics.email.email_opened') if self.mailclient: self.start_client_email(email) else: url = self.urlForEmail(email) launch_browser(url) # For accounts where we are guaranteed to actually read the email # on click (i.e., ones that use webclients and have autologin on), # decrement the email count. if self.opening_email_marks_as_read: self._remove_email(email) @callsback def OnClickSend(self, to='', subject='', body='', cc='', bcc='', callback=None): ''' Sends an email. ''' getattr(self, 'send_email', self.OnComposeEmail)(to=to, subject=subject, body=body, cc=cc, bcc=bcc, callback=callback) def start_client_email(self, email=None): log.info('mailclient: %s', self.mailclient) import os.path if self.mailclient == 'sysdefault': launch_sysdefault_email(email) elif self.mailclient == '__urls__': url = self.custom_inbox_url if url is not None: launch_browser(url) elif try_this(lambda: self.mailclient.startswith('file:'), False): filename = self.mailclient[5:] if os.path.exists(filename): os.startfile(filename) else: log.warning('cannot find %s', filename) def __len__(self): return self.count def __iter__(self): return iter(self.emails) can_has_preview = False @property def icon(self): from gui import skin from util import try_this return try_this(lambda: skin.get('serviceicons.%s' % self.protocol), None) @property def inbox_url(self): ''' Return the url for the user's inbox to be opened in browser. This should adhere to the 'privacy.www_auto_signin' pref. ''' raise NotImplementedError def observe_count(self, callback): self.add_gui_observer(callback, 'count') self.emails.add_gui_observer(callback) def unobserve_count(self, callback): self.remove_gui_observer(callback, 'count') self.emails.remove_gui_observer(callback) def observe_state(self, callback): self.add_gui_observer(callback, 'enabled') self.add_gui_observer(callback, 'state') def unobserve_state(self, callback): self.remove_gui_observer(callback, 'enabled') self.remove_gui_observer(callback, 'state') @property def header_funcs(self): return [('Inbox', self.OnClickInboxURL), ('Compose', self.OnComposeEmail)] def _get_options(self): opts = UpdateMixin.get_options(self) return opts def update_info(self, **info): flush_state = False with self.frozen(): for k, v in info.iteritems(): if k in ('password', 'username', 'server') and getattr(self, k, None) != v: flush_state = True self.setnotifyif(k, v) # Tell the server. profile.update_account(self) if flush_state: log.info('Resetting state for %r', self) self._reset_state() self._dirty_error = True def _reset_state(self): return NotImplemented def update(self): # Protocols must override this method to check for new messages. if self.update == EmailAccount.update: log.warning('not implemented: %s.update', self.__class__.__name__) raise NotImplementedError if not self.enabled: return log.info('%s (%s) -- preparing for update. update called from: %s', self, self.state, get_func_name(2)) if self.state == self.Statuses.OFFLINE: # First check -- either after creation or failing to connect for some reason self.change_state(self.Statuses.CONNECTING) elif self.state == self.Statuses.ONLINE: # A follow-up check. self.change_state(self.Statuses.CHECKING) elif self.state == self.Statuses.CONNECTING: # Already connecting -- if there have been errors this is just the Nth attempt. # if there are not errors, something is wrong! -- disconnect if not self.error_count: log.error( '%s -- called update while connecting, and no errors! disconnecting...', self) self.set_offline(self.Reasons.CONN_FAIL) else: log.error('Unexpected state for update: %r', self.state) @action(lambda self: (self.state != self.Statuses.CHECKING)) def update_now(self): 'Invoked by the GUI.' netcall(self.update) self.timer.reset(self.updatefreq) @action() def tell_me_again(self): if self.emails: emails = self.emails allow_click = True else: from mail.emailobj import Email emails = [Email(id=-1, fromname=_('No unread mail'))] allow_click = False # Use "always_show" to always show a popup, regardless of whether the # user has popups enabled or not. self._notify_emails(emails, always_show=['Popup'], allow_click=allow_click) @action() def auth(self): netcall(self.authenticate) def Connect(self): self.change_reason(self.Reasons.NONE) call_later(1.5, self.update) @action() def compose(self, to, subject, body, cc, bcc): ''' Return a link for a browser that will bring the user to a compose window (or as close as possible, adhering to the 'privacy.www_auto_signin' pref). ''' raise NotImplementedError def urlForEmail(self, email): ''' Return a link to be opened by a browser that will show the user the email (or as close as possible, adhering to the 'privacy.www_auto_signin' pref). email -- the email OBJECT that we want the URL for. ''' raise NotImplementedError @action() def open(self, email_message): ''' ''' if type(self) is EmailAccount: raise NotImplementedError def _remove_email(self, email_message): try: self.emails.remove(email_message) except ValueError: # already removed pass else: self.setnotifyif('count', self.count - 1) @action() def markAsRead(self, email_message): ''' Mark the email object as read. ''' import hooks hooks.notify('digsby.statistics.email.mark_as_read') self._remove_email(email_message) @action() def delete(self, email_message): import hooks hooks.notify('digsby.statistics.email.delete') self._remove_email(email_message) @action() def archive(self, email_message): import hooks hooks.notify('digsby.statistics.email.archive') self._remove_email(email_message) @action() def reportSpam(self, email_message): import hooks hooks.notify('digsby.statistics.email.spam') self._remove_email(email_message)