From db4037682dcfc2b9426523640b41f59f29ec2979 Mon Sep 17 00:00:00 2001 From: drebs Date: Thu, 29 Sep 2016 19:07:28 -0300 Subject: [refactor] standardize smtp and imap service modules --- src/leap/bitmask/core/mail_services.py | 8 +- src/leap/bitmask/mail/imap/service/__init__.py | 209 +++++++++++++++++++++ src/leap/bitmask/mail/imap/service/imap-server.tac | 4 +- src/leap/bitmask/mail/imap/service/imap.py | 209 --------------------- src/leap/bitmask/mail/smtp/service.py | 73 +++++++ 5 files changed, 288 insertions(+), 215 deletions(-) delete mode 100644 src/leap/bitmask/mail/imap/service/imap.py create mode 100644 src/leap/bitmask/mail/smtp/service.py diff --git a/src/leap/bitmask/core/mail_services.py b/src/leap/bitmask/core/mail_services.py index f971c1a..8e3a35d 100644 --- a/src/leap/bitmask/core/mail_services.py +++ b/src/leap/bitmask/core/mail_services.py @@ -42,10 +42,10 @@ from leap.bitmask.keymanager.errors import KeyNotFound from leap.bitmask.keymanager.validation import ValidationLevels from leap.bitmask.mail.constants import INBOX_NAME from leap.bitmask.mail.mail import Account -from leap.bitmask.mail.imap.service import imap +from leap.bitmask.mail.imap import service as imap_service +from leap.bitmask.mail.smtp import service as smtp_service from leap.bitmask.mail.incoming.service import IncomingMail from leap.bitmask.mail.incoming.service import INCOMING_CHECK_PERIOD -from leap.bitmask.mail import smtp from leap.bitmask.util import get_gpg_bin_path from leap.soledad.client.api import Soledad @@ -585,7 +585,7 @@ class IMAPService(service.Service): def startService(self): log.msg('starting imap service') - port, factory = imap.run_service( + port, factory = imap_service.run_service( self._soledad_sessions, factory=self._factory) self._port = port self._factory = factory @@ -618,7 +618,7 @@ class SMTPService(service.Service): def startService(self): log.msg('starting smtp service') - port, factory = smtp.run_service( + port, factory = smtp_service.run_service( self._soledad_sessions, self._keymanager_sessions, self._sendmail_opts, diff --git a/src/leap/bitmask/mail/imap/service/__init__.py b/src/leap/bitmask/mail/imap/service/__init__.py index e69de29..d9cd335 100644 --- a/src/leap/bitmask/mail/imap/service/__init__.py +++ b/src/leap/bitmask/mail/imap/service/__init__.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +# __init__.py +# Copyright (C) 2013-2015 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 . +""" +IMAP Service Initialization. +""" +import logging +import os + +from collections import defaultdict + +from twisted.cred.portal import Portal, IRealm +from twisted.mail.imap4 import IAccount +from twisted.internet import defer +from twisted.internet import reactor +from twisted.internet.error import CannotListenError +from twisted.internet.protocol import ServerFactory +from twisted.python import log +from zope.interface import implementer + +from leap.common.events import emit_async, catalog +from leap.bitmask.mail.cred import LocalSoledadTokenChecker +from leap.bitmask.mail.imap.account import IMAPAccount +from leap.bitmask.mail.imap.server import LEAPIMAPServer + +# TODO: leave only an implementor of IService in here + +logger = logging.getLogger(__name__) + +DO_MANHOLE = os.environ.get("LEAP_MAIL_MANHOLE", None) +if DO_MANHOLE: + from leap.bitmask.mail.imap.service import manhole + +# The default port in which imap service will run + +IMAP_PORT = 1984 + +# +# Credentials Handling +# + + +@implementer(IRealm) +class LocalSoledadIMAPRealm(object): + + _encoding = 'utf-8' + + def __init__(self, soledad_sessions): + """ + :param soledad_sessions: a dict-like object, containing instances + of a Store (soledad instances), indexed by + userid. + """ + self._soledad_sessions = soledad_sessions + + def requestAvatar(self, avatarId, mind, *interfaces): + if isinstance(avatarId, str): + avatarId = avatarId.decode(self._encoding) + + def gotSoledad(soledad): + for iface in interfaces: + if iface is IAccount: + avatar = IMAPAccount(soledad, avatarId) + return (IAccount, avatar, + getattr(avatar, 'logout', lambda: None)) + raise NotImplementedError(self, interfaces) + + return self.lookupSoledadInstance(avatarId).addCallback(gotSoledad) + + def lookupSoledadInstance(self, userid): + soledad = self._soledad_sessions[userid] + # XXX this should return the instance after whenReady callback + return defer.succeed(soledad) + + +class IMAPTokenChecker(LocalSoledadTokenChecker): + """A credentials checker that will lookup a token for the IMAP service. + For now it will be using the same identifier than SMTPTokenChecker""" + + service = 'mail_auth' + + +class LocalSoledadIMAPServer(LEAPIMAPServer): + + """ + An IMAP Server that authenticates against a LocalSoledad store. + """ + + def __init__(self, soledad_sessions, *args, **kw): + + LEAPIMAPServer.__init__(self, *args, **kw) + + realm = LocalSoledadIMAPRealm(soledad_sessions) + portal = Portal(realm) + checker = IMAPTokenChecker(soledad_sessions) + self.checker = checker + self.portal = portal + portal.registerChecker(checker) + + +class LeapIMAPFactory(ServerFactory): + + """ + Factory for a IMAP4 server with soledad remote sync and gpg-decryption + capabilities. + """ + + protocol = LocalSoledadIMAPServer + + def __init__(self, soledad_sessions): + """ + Initializes the server factory. + + :param soledad_sessions: a dict-like object, containing instances + of a Store (soledad instances), indexed by + userid. + """ + self._soledad_sessions = soledad_sessions + self._connections = defaultdict() + + def buildProtocol(self, addr): + """ + Return a protocol suitable for the job. + + :param addr: remote ip address + :type addr: str + """ + # TODO should reject anything from addr != localhost, + # just in case. + log.msg("Building protocol for connection %s" % addr) + imapProtocol = self.protocol(self._soledad_sessions) + self._connections[addr] = imapProtocol + return imapProtocol + + def stopFactory(self): + # say bye! + for conn, proto in self._connections.items(): + log.msg("Closing connections for %s" % conn) + proto.close_server_connection() + + def doStop(self): + """ + Stops imap service (fetcher, factory and port). + """ + return ServerFactory.doStop(self) + + +def run_service(soledad_sessions, port=IMAP_PORT, factory=None): + """ + Main entry point to run the service from the client. + + :param soledad_sessions: a dict-like object, containing instances + of a Store (soledad instances), indexed by userid. + + :returns: the port as returned by the reactor when starts listening, and + the factory for the protocol. + :rtype: tuple + """ + if not factory: + factory = LeapIMAPFactory(soledad_sessions) + + try: + interface = "localhost" + # don't bind just to localhost if we are running on docker since we + # won't be able to access imap from the host + if os.environ.get("LEAP_DOCKERIZED"): + interface = '' + + # TODO use Endpoints !!! + tport = reactor.listenTCP(port, factory, + interface=interface) + except CannotListenError: + logger.error("IMAP Service failed to start: " + "cannot listen in port %s" % (port,)) + except Exception as exc: + logger.error("Error launching IMAP service: %r" % (exc,)) + else: + # all good. + + if DO_MANHOLE: + # TODO get pass from env var.too. + manhole_factory = manhole.getManholeFactory( + {'f': factory, + 'gm': factory.theAccount.getMailbox}, + "boss", "leap") + # TODO use Endpoints !!! + reactor.listenTCP(manhole.MANHOLE_PORT, manhole_factory, + interface="127.0.0.1") + logger.debug("IMAP4 Server is RUNNING in port %s" % (port,)) + emit_async(catalog.IMAP_SERVICE_STARTED, str(port)) + + # FIXME -- change service signature + return tport, factory + + # not ok, signal error. + emit_async(catalog.IMAP_SERVICE_FAILED_TO_START, str(port)) diff --git a/src/leap/bitmask/mail/imap/service/imap-server.tac b/src/leap/bitmask/mail/imap/service/imap-server.tac index e695630..9460198 100644 --- a/src/leap/bitmask/mail/imap/service/imap-server.tac +++ b/src/leap/bitmask/mail/imap/service/imap-server.tac @@ -38,7 +38,7 @@ from twisted.application import service, internet from leap.bitmask.util import get_gpg_bin_path from leap.bitmask.keymanager import KeyManager -from leap.bitmask.mail.imap.service import imap +from leap.bitmask.mail.imap.service import LeapIMAPFactory from leap.soledad.client import Soledad @@ -136,7 +136,7 @@ keymanager = KeyManager(*km_args, **km_kwargs) def getIMAPService(): soledad_sessions = {userid: soledad} - factory = imap.LeapIMAPFactory(soledad_sessions) + factory = LeapIMAPFactory(soledad_sessions) return internet.TCPServer(port, factory, interface="localhost") diff --git a/src/leap/bitmask/mail/imap/service/imap.py b/src/leap/bitmask/mail/imap/service/imap.py deleted file mode 100644 index 9ccff4a..0000000 --- a/src/leap/bitmask/mail/imap/service/imap.py +++ /dev/null @@ -1,209 +0,0 @@ -# -*- coding: utf-8 -*- -# imap.py -# Copyright (C) 2013-2015 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 . -""" -IMAP Service Initialization. -""" -import logging -import os - -from collections import defaultdict - -from twisted.cred.portal import Portal, IRealm -from twisted.mail.imap4 import IAccount -from twisted.internet import defer -from twisted.internet import reactor -from twisted.internet.error import CannotListenError -from twisted.internet.protocol import ServerFactory -from twisted.python import log -from zope.interface import implementer - -from leap.common.events import emit_async, catalog -from leap.bitmask.mail.cred import LocalSoledadTokenChecker -from leap.bitmask.mail.imap.account import IMAPAccount -from leap.bitmask.mail.imap.server import LEAPIMAPServer - -# TODO: leave only an implementor of IService in here - -logger = logging.getLogger(__name__) - -DO_MANHOLE = os.environ.get("LEAP_MAIL_MANHOLE", None) -if DO_MANHOLE: - from leap.bitmask.mail.imap.service import manhole - -# The default port in which imap service will run - -IMAP_PORT = 1984 - -# -# Credentials Handling -# - - -@implementer(IRealm) -class LocalSoledadIMAPRealm(object): - - _encoding = 'utf-8' - - def __init__(self, soledad_sessions): - """ - :param soledad_sessions: a dict-like object, containing instances - of a Store (soledad instances), indexed by - userid. - """ - self._soledad_sessions = soledad_sessions - - def requestAvatar(self, avatarId, mind, *interfaces): - if isinstance(avatarId, str): - avatarId = avatarId.decode(self._encoding) - - def gotSoledad(soledad): - for iface in interfaces: - if iface is IAccount: - avatar = IMAPAccount(soledad, avatarId) - return (IAccount, avatar, - getattr(avatar, 'logout', lambda: None)) - raise NotImplementedError(self, interfaces) - - return self.lookupSoledadInstance(avatarId).addCallback(gotSoledad) - - def lookupSoledadInstance(self, userid): - soledad = self._soledad_sessions[userid] - # XXX this should return the instance after whenReady callback - return defer.succeed(soledad) - - -class IMAPTokenChecker(LocalSoledadTokenChecker): - """A credentials checker that will lookup a token for the IMAP service. - For now it will be using the same identifier than SMTPTokenChecker""" - - service = 'mail_auth' - - -class LocalSoledadIMAPServer(LEAPIMAPServer): - - """ - An IMAP Server that authenticates against a LocalSoledad store. - """ - - def __init__(self, soledad_sessions, *args, **kw): - - LEAPIMAPServer.__init__(self, *args, **kw) - - realm = LocalSoledadIMAPRealm(soledad_sessions) - portal = Portal(realm) - checker = IMAPTokenChecker(soledad_sessions) - self.checker = checker - self.portal = portal - portal.registerChecker(checker) - - -class LeapIMAPFactory(ServerFactory): - - """ - Factory for a IMAP4 server with soledad remote sync and gpg-decryption - capabilities. - """ - - protocol = LocalSoledadIMAPServer - - def __init__(self, soledad_sessions): - """ - Initializes the server factory. - - :param soledad_sessions: a dict-like object, containing instances - of a Store (soledad instances), indexed by - userid. - """ - self._soledad_sessions = soledad_sessions - self._connections = defaultdict() - - def buildProtocol(self, addr): - """ - Return a protocol suitable for the job. - - :param addr: remote ip address - :type addr: str - """ - # TODO should reject anything from addr != localhost, - # just in case. - log.msg("Building protocol for connection %s" % addr) - imapProtocol = self.protocol(self._soledad_sessions) - self._connections[addr] = imapProtocol - return imapProtocol - - def stopFactory(self): - # say bye! - for conn, proto in self._connections.items(): - log.msg("Closing connections for %s" % conn) - proto.close_server_connection() - - def doStop(self): - """ - Stops imap service (fetcher, factory and port). - """ - return ServerFactory.doStop(self) - - -def run_service(soledad_sessions, port=IMAP_PORT, factory=None): - """ - Main entry point to run the service from the client. - - :param soledad_sessions: a dict-like object, containing instances - of a Store (soledad instances), indexed by userid. - - :returns: the port as returned by the reactor when starts listening, and - the factory for the protocol. - :rtype: tuple - """ - if not factory: - factory = LeapIMAPFactory(soledad_sessions) - - try: - interface = "localhost" - # don't bind just to localhost if we are running on docker since we - # won't be able to access imap from the host - if os.environ.get("LEAP_DOCKERIZED"): - interface = '' - - # TODO use Endpoints !!! - tport = reactor.listenTCP(port, factory, - interface=interface) - except CannotListenError: - logger.error("IMAP Service failed to start: " - "cannot listen in port %s" % (port,)) - except Exception as exc: - logger.error("Error launching IMAP service: %r" % (exc,)) - else: - # all good. - - if DO_MANHOLE: - # TODO get pass from env var.too. - manhole_factory = manhole.getManholeFactory( - {'f': factory, - 'gm': factory.theAccount.getMailbox}, - "boss", "leap") - # TODO use Endpoints !!! - reactor.listenTCP(manhole.MANHOLE_PORT, manhole_factory, - interface="127.0.0.1") - logger.debug("IMAP4 Server is RUNNING in port %s" % (port,)) - emit_async(catalog.IMAP_SERVICE_STARTED, str(port)) - - # FIXME -- change service signature - return tport, factory - - # not ok, signal error. - emit_async(catalog.IMAP_SERVICE_FAILED_TO_START, str(port)) diff --git a/src/leap/bitmask/mail/smtp/service.py b/src/leap/bitmask/mail/smtp/service.py new file mode 100644 index 0000000..07c182d --- /dev/null +++ b/src/leap/bitmask/mail/smtp/service.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# service.py +# Copyright (C) 2013-2016 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 . +""" +SMTP gateway helper function. +""" +import logging +import os + +from twisted.internet import reactor +from twisted.internet.error import CannotListenError + +from leap.common.events import emit_async, catalog +from leap.bitmask.mail.smtp.gateway import SMTPFactory + +logger = logging.getLogger(__name__) + +SMTP_PORT = 2013 + + +def run_service(soledad_sessions, keymanager_sessions, sendmail_opts, + port=SMTP_PORT, factory=None): + """ + Main entry point to run the service from the client. + + :param soledad_sessions: a dict-like object, containing instances + of a Store (soledad instances), indexed by userid. + :param keymanager_sessions: a dict-like object, containing instances + of Keymanager, indexed by userid. + :param sendmail_opts: a dict-like object of sendmailOptions. + :param factory: a factory for the protocol that will listen in the given + port + + :returns: the port as returned by the reactor when starts listening, and + the factory for the protocol. + :rtype: tuple + """ + if not factory: + factory = SMTPFactory(soledad_sessions, keymanager_sessions, + sendmail_opts) + + try: + interface = "localhost" + # don't bind just to localhost if we are running on docker since we + # won't be able to access smtp from the host + if os.environ.get("LEAP_DOCKERIZED"): + interface = '' + + # TODO Use Endpoints instead -------------------------------- + tport = reactor.listenTCP(port, factory, interface=interface) + emit_async(catalog.SMTP_SERVICE_STARTED, str(port)) + + return tport, factory + except CannotListenError: + logger.error("STMP Service failed to start: " + "cannot listen in port %s" % port) + emit_async(catalog.SMTP_SERVICE_FAILED_TO_START, str(port)) + except Exception as exc: + logger.error("Unhandled error while launching smtp gateway service") + logger.exception(exc) -- cgit v1.2.3