diff options
Diffstat (limited to 'src/leap/bitmask/services/mail/conductor.py')
-rw-r--r-- | src/leap/bitmask/services/mail/conductor.py | 383 |
1 files changed, 383 insertions, 0 deletions
diff --git a/src/leap/bitmask/services/mail/conductor.py b/src/leap/bitmask/services/mail/conductor.py new file mode 100644 index 00000000..3a3e16a3 --- /dev/null +++ b/src/leap/bitmask/services/mail/conductor.py @@ -0,0 +1,383 @@ +# -*- 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 <http://www.gnu.org/licenses/>. +""" +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_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. + """ + 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") + + if self.imap_service is None: + # first time. + self.imap_service = imap.start_imap_service( + self._soledad, + self._keymanager) + else: + # we have the fetcher. just start it. + self.imap_service.start_loop() + + def stop_imap_service(self): + """ + Stops imap service. + + There is a subtle difference here: + we are just stopping the fetcher here, + but in the smtp case we are stopping the factory. + """ + 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.') + self.imap_service.stop() + + def fetch_incoming_mail(self): + """ + Fetches incoming mail. + """ + # TODO have a mutex over fetch operation. + 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_relay + self._smtp_service, self._smtp_port = setup_smtp_relay( + port=2013, + 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. + + There is a subtle difference here: + we are stopping the factory for the smtp service here, + but in the imap case we are just stopping the fetcher. + """ + 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() + + 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. + # XXX we could probably use a proxy here too to make the thing + # transparent. + self.set_imap_connection(imap.conn) + self.set_smtp_connection(smtp.conn) + + self._mail_machine = mail + # XXX ------------------- + # need to keep a reference? + #self._mail_events = mail.events + 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) |