comparison 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
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # SàT plugin for managing smtp server
5 # Copyright (C) 2011 Jérôme Poisson (goffi@goffi.org)
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 from sat.core.i18n import _
21 from sat.core.constants import Const as C
22 from sat.core.log import getLogger
23 log = getLogger(__name__)
24 from twisted.internet import defer
25 from twisted.cred import portal, checkers, credentials
26 from twisted.cred import error as cred_error
27 from twisted.mail import smtp
28 from twisted.python import failure
29 from email.parser import Parser
30 from email.utils import parseaddr
31 from twisted.mail.imap4 import LOGINCredentials, PLAINCredentials
32 from twisted.internet import reactor
33 import sys
34
35 from zope.interface import implements
36
37 PLUGIN_INFO = {
38 C.PI_NAME: "SMTP server Plugin",
39 C.PI_IMPORT_NAME: "SMTP",
40 C.PI_TYPE: "Misc",
41 C.PI_PROTOCOLS: [],
42 C.PI_DEPENDENCIES: ["Maildir"],
43 C.PI_MAIN: "SMTP_server",
44 C.PI_HANDLER: "no",
45 C.PI_DESCRIPTION: _("""Create a SMTP server that you can use to send your "normal" type messages""")
46 }
47
48
49 class SMTP_server(object):
50
51 params = """
52 <params>
53 <general>
54 <category name="Mail Server">
55 <param name="SMTP Port" value="10125" type="int" constraint="1;65535" />
56 </category>
57 </general>
58 </params>
59 """
60
61 def __init__(self, host):
62 log.info(_("Plugin SMTP Server initialization"))
63 self.host = host
64
65 #parameters
66 host.memory.updateParams(self.params)
67
68 port = int(self.host.memory.getParamA("SMTP Port", "Mail Server"))
69 log.info(_("Launching SMTP server on port %d") % port)
70
71 self.server_factory = SmtpServerFactory(self.host)
72 reactor.listenTCP(port, self.server_factory)
73
74
75 class SatSmtpMessage(object):
76 implements(smtp.IMessage)
77
78 def __init__(self, host, profile):
79 self.host = host
80 self.profile = profile
81 self.message = []
82
83 def lineReceived(self, line):
84 """handle another line"""
85 self.message.append(line)
86
87 def eomReceived(self):
88 """handle end of message"""
89 mail = Parser().parsestr("\n".join(self.message))
90 try:
91 self.host._sendMessage(parseaddr(mail['to'].decode('utf-8', 'replace'))[1], mail.get_payload().decode('utf-8', 'replace'), # TODO: manage other charsets
92 subject=mail['subject'].decode('utf-8', 'replace'), mess_type='normal', profile_key=self.profile)
93 except:
94 exc_type, exc_value, exc_traceback = sys.exc_info()
95 log.error(_(u"Can't send message: %s") % exc_value) # The email is invalid or incorreclty parsed
96 return defer.fail()
97 self.message = None
98 return defer.succeed(None)
99
100 def connectionLost(self):
101 """handle message truncated"""
102 raise smtp.SMTPError
103
104
105 class SatSmtpDelivery(object):
106 implements(smtp.IMessageDelivery)
107
108 def __init__(self, host, profile):
109 self.host = host
110 self.profile = profile
111
112 def receivedHeader(self, helo, origin, recipients):
113 """
114 Generate the Received header for a message
115 @param helo: The argument to the HELO command and the client's IP
116 address.
117 @param origin: The address the message is from
118 @param recipients: A list of the addresses for which this message
119 is bound.
120 @return: The full \"Received\" header string.
121 """
122 return "Received:"
123
124 def validateTo(self, user):
125 """
126 Validate the address for which the message is destined.
127 @param user: The address to validate.
128 @return: A Deferred which becomes, or a callable which
129 takes no arguments and returns an object implementing IMessage.
130 This will be called and the returned object used to deliver the
131 message when it arrives.
132 """
133 return lambda: SatSmtpMessage(self.host, self.profile)
134
135 def validateFrom(self, helo, origin):
136 """
137 Validate the address from which the message originates.
138 @param helo: The argument to the HELO command and the client's IP
139 address.
140 @param origin: The address the message is from
141 @return: origin or a Deferred whose callback will be
142 passed origin.
143 """
144 return origin
145
146
147 class SmtpRealm(object):
148 implements(portal.IRealm)
149
150 def __init__(self, host):
151 self.host = host
152
153 def requestAvatar(self, avatarID, mind, *interfaces):
154 log.debug('requestAvatar')
155 profile = avatarID.decode('utf-8')
156 if smtp.IMessageDelivery not in interfaces:
157 raise NotImplementedError
158 return smtp.IMessageDelivery, SatSmtpDelivery(self.host, profile), lambda: None
159
160
161 class SatProfileCredentialChecker(object):
162 """
163 This credential checker check against SàT's profile and associated jabber's password
164 Check if the profile exists, and if the password is OK
165 Return the profile as avatarId
166 """
167 implements(checkers.ICredentialsChecker)
168 credentialInterfaces = (credentials.IUsernamePassword,
169 credentials.IUsernameHashedPassword)
170
171 def __init__(self, host):
172 self.host = host
173
174 def _cbPasswordMatch(self, matched, profile):
175 if matched:
176 return profile.encode('utf-8')
177 else:
178 return failure.Failure(cred_error.UnauthorizedLogin())
179
180 def requestAvatarId(self, credentials):
181 profiles = self.host.memory.getProfilesList()
182 if not credentials.username in profiles:
183 return defer.fail(cred_error.UnauthorizedLogin())
184 d = self.host.memory.asyncGetParamA("Password", "Connection", profile_key=credentials.username)
185 d.addCallback(credentials.checkPassword)
186 d.addCallback(self._cbPasswordMatch, credentials.username)
187 return d
188
189
190 class SmtpServerFactory(smtp.SMTPFactory):
191
192 def __init__(self, host):
193 self.protocol = smtp.ESMTP
194 self.host = host
195 _portal = portal.Portal(SmtpRealm(self.host))
196 _portal.registerChecker(SatProfileCredentialChecker(self.host))
197 smtp.SMTPFactory.__init__(self, _portal)
198
199 def startedConnecting(self, connector):
200 log.debug(_("SMTP server connection started"))
201 smtp.SMTPFactory.startedConnecting(self, connector)
202
203 def clientConnectionLost(self, connector, reason):
204 log.debug(_(u"SMTP server connection lost (reason: %s)"), reason)
205 smtp.SMTPFactory.clientConnectionLost(self, connector, reason)
206
207 def buildProtocol(self, addr):
208 p = smtp.SMTPFactory.buildProtocol(self, addr)
209 # add the challengers from imap4, more secure and complicated challengers are available
210 p.challengers = {"LOGIN": LOGINCredentials, "PLAIN": PLAINCredentials}
211 return p