diff sat/plugins/plugin_misc_smtp.py @ 2562:26edcf3a30eb

core, setup: huge cleaning: - moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention - move twisted directory to root - removed all hacks from setup.py, and added missing dependencies, it is now clean - use https URL for website in setup.py - removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed - renamed sat.sh to sat and fixed its installation - added python_requires to specify Python version needed - replaced glib2reactor which use deprecated code by gtk3reactor sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author Goffi <goffi@goffi.org>
date Mon, 02 Apr 2018 19:44:50 +0200
parents src/plugins/plugin_misc_smtp.py@33c8c4973743
children 56f94936df1e
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sat/plugins/plugin_misc_smtp.py	Mon Apr 02 19:44:50 2018 +0200
@@ -0,0 +1,211 @@
+#!/usr/bin/env python2
+# -*- coding: utf-8 -*-
+
+# SàT plugin for managing smtp server
+# Copyright (C) 2011  Jérôme Poisson (goffi@goffi.org)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero 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 Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from sat.core.i18n import _
+from sat.core.constants import Const as C
+from sat.core.log import getLogger
+log = getLogger(__name__)
+from twisted.internet import defer
+from twisted.cred import portal, checkers, credentials
+from twisted.cred import error as cred_error
+from twisted.mail import smtp
+from twisted.python import failure
+from email.parser import Parser
+from email.utils import parseaddr
+from twisted.mail.imap4 import LOGINCredentials, PLAINCredentials
+from twisted.internet import reactor
+import sys
+
+from zope.interface import implements
+
+PLUGIN_INFO = {
+    C.PI_NAME: "SMTP server Plugin",
+    C.PI_IMPORT_NAME: "SMTP",
+    C.PI_TYPE: "Misc",
+    C.PI_PROTOCOLS: [],
+    C.PI_DEPENDENCIES: ["Maildir"],
+    C.PI_MAIN: "SMTP_server",
+    C.PI_HANDLER: "no",
+    C.PI_DESCRIPTION: _("""Create a SMTP server that you can use to send your "normal" type messages""")
+}
+
+
+class SMTP_server(object):
+
+    params = """
+    <params>
+    <general>
+    <category name="Mail Server">
+        <param name="SMTP Port" value="10125" type="int" constraint="1;65535" />
+    </category>
+    </general>
+    </params>
+    """
+
+    def __init__(self, host):
+        log.info(_("Plugin SMTP Server initialization"))
+        self.host = host
+
+        #parameters
+        host.memory.updateParams(self.params)
+
+        port = int(self.host.memory.getParamA("SMTP Port", "Mail Server"))
+        log.info(_("Launching SMTP server on port %d") % port)
+
+        self.server_factory = SmtpServerFactory(self.host)
+        reactor.listenTCP(port, self.server_factory)
+
+
+class SatSmtpMessage(object):
+    implements(smtp.IMessage)
+
+    def __init__(self, host, profile):
+        self.host = host
+        self.profile = profile
+        self.message = []
+
+    def lineReceived(self, line):
+        """handle another line"""
+        self.message.append(line)
+
+    def eomReceived(self):
+        """handle end of message"""
+        mail = Parser().parsestr("\n".join(self.message))
+        try:
+            self.host._sendMessage(parseaddr(mail['to'].decode('utf-8', 'replace'))[1], mail.get_payload().decode('utf-8', 'replace'),  # TODO: manage other charsets
+                                  subject=mail['subject'].decode('utf-8', 'replace'), mess_type='normal', profile_key=self.profile)
+        except:
+            exc_type, exc_value, exc_traceback = sys.exc_info()
+            log.error(_(u"Can't send message: %s") % exc_value)  # The email is invalid or incorreclty parsed
+            return defer.fail()
+        self.message = None
+        return defer.succeed(None)
+
+    def connectionLost(self):
+        """handle message truncated"""
+        raise smtp.SMTPError
+
+
+class SatSmtpDelivery(object):
+    implements(smtp.IMessageDelivery)
+
+    def __init__(self, host, profile):
+        self.host = host
+        self.profile = profile
+
+    def receivedHeader(self, helo, origin, recipients):
+        """
+        Generate the Received header for a message
+        @param helo: The argument to the HELO command and the client's IP
+        address.
+        @param origin: The address the message is from
+        @param recipients: A list of the addresses for which this message
+        is bound.
+        @return: The full \"Received\" header string.
+        """
+        return "Received:"
+
+    def validateTo(self, user):
+        """
+        Validate the address for which the message is destined.
+        @param user: The address to validate.
+        @return: A Deferred which becomes, or a callable which
+        takes no arguments and returns an object implementing IMessage.
+        This will be called and the returned object used to deliver the
+        message when it arrives.
+        """
+        return lambda: SatSmtpMessage(self.host, self.profile)
+
+    def validateFrom(self, helo, origin):
+        """
+        Validate the address from which the message originates.
+        @param helo: The argument to the HELO command and the client's IP
+        address.
+        @param origin: The address the message is from
+        @return: origin or a Deferred whose callback will be
+        passed origin.
+        """
+        return origin
+
+
+class SmtpRealm(object):
+    implements(portal.IRealm)
+
+    def __init__(self, host):
+        self.host = host
+
+    def requestAvatar(self, avatarID, mind, *interfaces):
+        log.debug('requestAvatar')
+        profile = avatarID.decode('utf-8')
+        if smtp.IMessageDelivery not in interfaces:
+            raise NotImplementedError
+        return smtp.IMessageDelivery, SatSmtpDelivery(self.host, profile), lambda: None
+
+
+class SatProfileCredentialChecker(object):
+    """
+    This credential checker check against SàT's profile and associated jabber's password
+    Check if the profile exists, and if the password is OK
+    Return the profile as avatarId
+    """
+    implements(checkers.ICredentialsChecker)
+    credentialInterfaces = (credentials.IUsernamePassword,
+                            credentials.IUsernameHashedPassword)
+
+    def __init__(self, host):
+        self.host = host
+
+    def _cbPasswordMatch(self, matched, profile):
+        if matched:
+            return profile.encode('utf-8')
+        else:
+            return failure.Failure(cred_error.UnauthorizedLogin())
+
+    def requestAvatarId(self, credentials):
+        profiles = self.host.memory.getProfilesList()
+        if not credentials.username in profiles:
+            return defer.fail(cred_error.UnauthorizedLogin())
+        d = self.host.memory.asyncGetParamA("Password", "Connection", profile_key=credentials.username)
+        d.addCallback(credentials.checkPassword)
+        d.addCallback(self._cbPasswordMatch, credentials.username)
+        return d
+
+
+class SmtpServerFactory(smtp.SMTPFactory):
+
+    def __init__(self, host):
+        self.protocol = smtp.ESMTP
+        self.host = host
+        _portal = portal.Portal(SmtpRealm(self.host))
+        _portal.registerChecker(SatProfileCredentialChecker(self.host))
+        smtp.SMTPFactory.__init__(self, _portal)
+
+    def startedConnecting(self, connector):
+        log.debug(_("SMTP server connection started"))
+        smtp.SMTPFactory.startedConnecting(self, connector)
+
+    def clientConnectionLost(self, connector, reason):
+        log.debug(_(u"SMTP server connection lost (reason: %s)"), reason)
+        smtp.SMTPFactory.clientConnectionLost(self, connector, reason)
+
+    def buildProtocol(self, addr):
+        p = smtp.SMTPFactory.buildProtocol(self, addr)
+        # add the challengers from imap4, more secure and complicated challengers are available
+        p.challengers = {"LOGIN": LOGINCredentials, "PLAIN": PLAINCredentials}
+        return p