# -*- coding: utf-8 -*- # conductor.py # Copyright (C) 2013 LEAP # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ Mail Services Conductor """ import logging import os from PySide import QtCore from zope.proxy import sameProxiedObjects from leap.bitmask.gui import statemachines from leap.bitmask.services.mail import imap from leap.bitmask.services.mail import connection as mail_connection from leap.bitmask.services.mail.smtpbootstrapper import SMTPBootstrapper from leap.bitmask.services.mail.smtpconfig import SMTPConfig from leap.bitmask.util import is_file from leap.common.check import leap_assert from leap.common.events import register as leap_register from leap.common.events import events_pb2 as leap_events logger = logging.getLogger(__name__) class IMAPControl(object): """ Methods related to IMAP control. """ def __init__(self): """ Initializes smtp variables. """ self.imap_machine = None self.imap_service = None self.imap_port = None self.imap_factory = None self.imap_connection = None leap_register(signal=leap_events.IMAP_SERVICE_STARTED, callback=self._handle_imap_events, reqcbk=lambda req, resp: None) leap_register(signal=leap_events.IMAP_SERVICE_FAILED_TO_START, callback=self._handle_imap_events, reqcbk=lambda req, resp: None) def set_imap_connection(self, imap_connection): """ Sets the imap connection to an initialized connection. :param imap_connection: an initialized imap connection :type imap_connection: IMAPConnection instance. """ self.imap_connection = imap_connection def start_imap_service(self): """ Starts imap service. """ from leap.bitmask.config import flags logger.debug('Starting imap service') leap_assert(sameProxiedObjects(self._soledad, None) is not True, "We need a non-null soledad for initializing imap service") leap_assert(sameProxiedObjects(self._keymanager, None) is not True, "We need a non-null keymanager for initializing imap " "service") offline = flags.OFFLINE self.imap_service, self.imap_port, \ self.imap_factory = imap.start_imap_service( self._soledad, self._keymanager, userid=self.userid, offline=offline) if offline is False: logger.debug("Starting loop") self.imap_service.start_loop() def stop_imap_service(self, cv): """ Stops imap service (fetcher, factory and port). :param cv: A condition variable to which we can signal when imap indeed stops. :type cv: threading.Condition """ self.imap_connection.qtsigs.disconnecting_signal.emit() # TODO We should homogenize both services. if self.imap_service is not None: logger.debug('Stopping imap service.') # Stop the loop call in the fetcher self.imap_service.stop() self.imap_service = None # Stop listening on the IMAP port self.imap_port.stopListening() # Stop the protocol self.imap_factory.theAccount.closed = True self.imap_factory.doStop(cv) else: # main window does not have to wait because there's no service to # be stopped, so we release the condition variable cv.acquire() cv.notify() cv.release() def fetch_incoming_mail(self): """ Fetches incoming mail. """ if self.imap_service: logger.debug('Client connected, fetching mail...') self.imap_service.fetch() # handle events def _handle_imap_events(self, req): """ Callback handler for the IMAP events :param req: Request type :type req: leap.common.events.events_pb2.SignalRequest """ if req.event == leap_events.IMAP_SERVICE_STARTED: self.on_imap_connected() elif req.event == leap_events.IMAP_SERVICE_FAILED_TO_START: self.on_imap_failed() # emit connection signals def on_imap_connecting(self): """ Callback for IMAP connecting state. """ self.imap_connection.qtsigs.connecting_signal.emit() def on_imap_connected(self): """ Callback for IMAP connected state. """ self.imap_connection.qtsigs.connected_signal.emit() def on_imap_failed(self): """ Callback for IMAP failed state. """ self.imap_connection.qtsigs.connetion_aborted_signal.emit() class SMTPControl(object): PORT_KEY = "port" IP_KEY = "ip_address" def __init__(self): """ Initializes smtp variables. """ self.smtp_config = SMTPConfig() self.smtp_connection = None self.smtp_machine = None self._smtp_service = None self._smtp_port = None self.smtp_bootstrapper = SMTPBootstrapper() self.smtp_bootstrapper.download_config.connect( self.smtp_bootstrapped_stage) leap_register(signal=leap_events.SMTP_SERVICE_STARTED, callback=self._handle_smtp_events, reqcbk=lambda req, resp: None) leap_register(signal=leap_events.SMTP_SERVICE_FAILED_TO_START, callback=self._handle_smtp_events, reqcbk=lambda req, resp: None) def set_smtp_connection(self, smtp_connection): """ Sets the smtp connection to an initialized connection. :param smtp_connection: an initialized smtp connection :type smtp_connection: SMTPConnection instance. """ self.smtp_connection = smtp_connection def start_smtp_service(self, host, port, cert): """ Starts the smtp service. :param host: the hostname of the remove SMTP server. :type host: str :param port: the port of the remote SMTP server :type port: str :param cert: the client certificate for authentication :type cert: str """ # TODO Make the encrypted_only configurable # TODO pick local smtp port in a better way # TODO remove hard-coded port and let leap.mail set # the specific default. self.smtp_connection.qtsigs.connecting_signal.emit() from leap.mail.smtp import setup_smtp_gateway self._smtp_service, self._smtp_port = setup_smtp_gateway( port=2013, userid=self.userid, keymanager=self._keymanager, smtp_host=host, smtp_port=port, smtp_cert=cert, smtp_key=cert, encrypted_only=False) def stop_smtp_service(self): """ Stops the smtp service (port and factory). """ self.smtp_connection.qtsigs.disconnecting_signal.emit() # TODO We should homogenize both services. if self._smtp_service is not None: logger.debug('Stopping smtp service.') self._smtp_port.stopListening() self._smtp_service.doStop() @QtCore.Slot() def smtp_bootstrapped_stage(self, data): """ SLOT TRIGGERS: self.smtp_bootstrapper.download_config If there was a problem, displays it, otherwise it does nothing. This is used for intermediate bootstrapping stages, in case they fail. :param data: result from the bootstrapping stage for Soledad :type data: dict """ passed = data[self.smtp_bootstrapper.PASSED_KEY] if not passed: logger.error(data[self.smtp_bootstrapper.ERROR_KEY]) return logger.debug("Done bootstrapping SMTP") self.check_smtp_config() def check_smtp_config(self): """ Checks smtp config and tries to download smtp client cert if needed. Currently called when smtp_bootstrapped_stage has successfuly finished. """ logger.debug("Checking SMTP config...") leap_assert(self.smtp_bootstrapper._provider_config, "smtp bootstrapper does not have a provider_config") provider_config = self.smtp_bootstrapper._provider_config smtp_config = self.smtp_config hosts = smtp_config.get_hosts() # TODO handle more than one host and define how to choose if len(hosts) > 0: hostname = hosts.keys()[0] logger.debug("Using hostname %s for SMTP" % (hostname,)) host = hosts[hostname][self.IP_KEY].encode("utf-8") port = hosts[hostname][self.PORT_KEY] client_cert = smtp_config.get_client_cert_path( provider_config, about_to_download=True) # XXX change this logic! # check_config should be called from within start_service, # and not the other way around. if not is_file(client_cert): self.smtp_bootstrapper._download_client_certificates() if os.path.isfile(client_cert): self.start_smtp_service(host, port, client_cert) else: logger.warning("Tried to download email client " "certificate, but could not find any") else: logger.warning("No smtp hosts configured") # handle smtp events def _handle_smtp_events(self, req): """ Callback handler for the SMTP events. :param req: Request type :type req: leap.common.events.events_pb2.SignalRequest """ if req.event == leap_events.SMTP_SERVICE_STARTED: self.on_smtp_connected() elif req.event == leap_events.SMTP_SERVICE_FAILED_TO_START: self.on_smtp_failed() # emit connection signals def on_smtp_connecting(self): """ Callback for SMTP connecting state. """ self.smtp_connection.qtsigs.connecting_signal.emit() def on_smtp_connected(self): """ Callback for SMTP connected state. """ self.smtp_connection.qtsigs.connected_signal.emit() def on_smtp_failed(self): """ Callback for SMTP failed state. """ self.smtp_connection.qtsigs.connection_aborted_signal.emit() class MailConductor(IMAPControl, SMTPControl): """ This class encapsulates everything related to the initialization and process control for the mail services. Currently, it initializes IMAPConnection and SMPTConnection. """ # XXX We could consider to use composition instead of inheritance here. def __init__(self, soledad, keymanager): """ Initializes the mail conductor. :param soledad: a transparent proxy that eventually will point to a Soledad Instance. :type soledad: zope.proxy.ProxyBase :param keymanager: a transparent proxy that eventually will point to a Keymanager Instance. :type soledad: zope.proxy.ProxyBase """ IMAPControl.__init__(self) SMTPControl.__init__(self) self._soledad = soledad self._keymanager = keymanager self._mail_machine = None self._mail_connection = mail_connection.MailConnection() self._userid = None @property def userid(self): return self._userid @userid.setter def userid(self, userid): """ Sets the user id this conductor is configured for. :param userid: the user id, in the form "user@provider" :type userid: str """ self._userid = userid def start_mail_machine(self, **kwargs): """ Starts mail machine. """ logger.debug("Starting mail state machine...") builder = statemachines.ConnectionMachineBuilder(self._mail_connection) (mail, (imap, smtp)) = builder.make_machine(**kwargs) # we have instantiated the connections while building the composite # machines, and we have to use the qtsigs instantiated there. self.set_imap_connection(imap.conn) self.set_smtp_connection(smtp.conn) self._mail_machine = mail self._mail_machine.start() self._imap_machine = imap self._imap_machine.start() self._smtp_machine = smtp self._smtp_machine.start() def connect_mail_signals(self, widget): """ Connects the mail signals to the mail_status widget slots. :param widget: the widget containing the slots. :type widget: QtCore.QWidget """ qtsigs = self._mail_connection.qtsigs qtsigs.connected_signal.connect(widget.mail_state_connected) qtsigs.connecting_signal.connect(widget.mail_state_connecting) qtsigs.disconnecting_signal.connect(widget.mail_state_disconnecting) qtsigs.disconnected_signal.connect(widget.mail_state_disconnected) qtsigs.soledad_invalid_auth_token.connect(widget.soledad_invalid_auth_token)