summaryrefslogtreecommitdiff
path: root/src/leap/mail/smtp/smtprelay.py
blob: 6479873a001fd22b747ed79b646086b42f1d32ef (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
"""
LEAP SMTP encrypted relay.
"""

import re
import gnupg
from zope.interface import implements
from StringIO import StringIO
from twisted.mail import smtp
from twisted.internet.protocol import ServerFactory
from twisted.internet import reactor
from twisted.internet import defer
from twisted.application import internet, service
from twisted.python import log
from email.Header import Header
from leap import soledad


class SMTPInfoNotAvailable(Exception):
    pass


class SMTPFactory(ServerFactory):
    """
    Factory for an SMTP server with encrypted relaying capabilities.
    """

    def __init__(self, soledad, gpg=None):
        self._soledad = soledad
        self._gpg = gpg

    def buildProtocol(self, addr):
        "Return a protocol suitable for the job."
        # TODO: use ESMTP here.
        smtpProtocol = smtp.SMTP(SMTPDelivery(self._soledad, self._gpg))
        smtpProtocol.factory = self
        return smtpProtocol


class SMTPDelivery(object):
    """
    Validate email addresses and handle message delivery.
    """

    implements(smtp.IMessageDelivery)

    def __init__(self, soledad, gpg=None):
        self._soledad = soledad
        if gpg:
            self._gpg = gpg
        else:
            self._gpg = GPGWrapper()

    def receivedHeader(self, helo, origin, recipients):
        myHostname, clientIP = helo
        headerValue = "by %s from %s with ESMTP ; %s" % (
            myHostname, clientIP, smtp.rfc822date())
        # email.Header.Header used for automatic wrapping of long lines
        return "Received: %s" % Header(headerValue)

    def validateTo(self, user):
        """Assert existence of and trust on recipient's GPG public key."""
        # try to find recipient's public key
        try:
            # this will raise an exception if key is not found
            trust = self._gpg.find_key(user.dest.addrstr)['trust']
            # if key is not ultimatelly trusted, then the message will not
            # be encrypted. So, we check for this below
            #if trust != 'u':
            #    raise smtp.SMTPBadRcpt(user)
            log.msg("Accepting mail for %s..." % user.dest)
            return lambda: EncryptedMessage(user, soledad=self._soledad,
                                            gpg=self._gpg)
        except LookupError:
            # if key was not found, check config to see if will send anyway.
            if self.encrypted_only:
                raise smtp.SMTPBadRcpt(user)
            # TODO: send signal to cli/gui that this user's key was not found?
            log.msg("Warning: will send an unencrypted message (because "
                    "encrypted_only' is set to False).")

    def validateFrom(self, helo, originAddress):
        # accept mail from anywhere. To reject an address, raise
        # smtp.SMTPBadSender here.
        return originAddress


class EncryptedMessage():
    """
    Receive plaintext from client, encrypt it and send message to a
    recipient.
    """
    implements(smtp.IMessage)

    SMTP_HOSTNAME = "mail.leap.se"
    SMTP_PORT = 25

    def __init__(self, user, soledad,  gpg=None):
        self.user = user
        self._soledad = soledad
        self.fetchConfig()
        self.lines = []
        if gpg:
            self._gpg = gpg
        else:
            self._gpg = GPGWrapper()

    def lineReceived(self, line):
        """Store email DATA lines as they arrive."""
        self.lines.append(line)

    def eomReceived(self):
        """Encrypt and send message."""
        log.msg("Message data complete.")
        self.lines.append('')  # add a trailing newline
        self.parseMessage()
        try:
            self.encrypt()
            return self.sendMessage()
        except LookupError:
            return None

    def parseMessage(self):
        """Separate message headers from body."""
        sep = self.lines.index('')
        self.headers = self.lines[:sep]
        self.body = self.lines[sep + 1:]

    def connectionLost(self):
        log.msg("Connection lost unexpectedly!")
        log.err()
        # unexpected loss of connection; don't save
        self.lines = []

    def sendSuccess(self, r):
        log.msg(r)

    def sendError(self, e):
        log.msg(e)
        log.err()

    def prepareHeader(self):
        self.headers.insert(1, "From: %s" % self.user.orig.addrstr)
        self.headers.insert(2, "To: %s" % self.user.dest.addrstr)
        self.headers.append('')

    def sendMessage(self):
        self.prepareHeader()
        msg = '\n'.join(self.headers + [self.cyphertext])
        d = defer.Deferred()
        factory = smtp.ESMTPSenderFactory(self.smtp_username,
                                          self.smtp_password,
                                          self.smtp_username,
                                          self.user.dest.addrstr,
                                          StringIO(msg),
                                          d)
        # the next call is TSL-powered!
        reactor.connectTCP(self.SMTP_HOSTNAME, self.SMTP_PORT, factory)
        d.addCallback(self.sendSuccess)
        d.addErrback(self.sendError)
        return d

    def encrypt(self, always_trust=True):
        # TODO: do not "always trust" here.
        try:
            fp = self._gpg.find_key(self.user.dest.addrstr)['fingerprint']
            log.msg("Encrypting to %s" % fp)
            self.cyphertext = str(
                self._gpg.encrypt(
                    '\n'.join(self.body), [fp], always_trust=always_trust))
        except LookupError:
            if self.encrypted_only:
                raise
            log.msg("Warning: sending unencrypted mail (because "
                    "'encrypted_only' is set to False).")

    # this will be replaced by some other mechanism of obtaining credentials
    # for SMTP server.
    def fetchConfig(self):
        # TODO: Soledad/LEAP bootstrap should store the SMTP info on local db,
        # so this relay can load it when it needs.
        if not self._soledad:
            # TODO: uncomment below exception when integration with Soledad is
            # smooth.
            #raise SMTPInfoNotAvailable()
            # TODO: remove dummy settings below when soledad bootstrap is
            # working.
            self.smtp_host = ''
            self.smtp_port = ''
            self.smtp_username = ''
            self.smtp_password = ''
            self.encrypted_only = True
        else:
            self.smtp_config = self._soledad.get_doc('smtp_relay_config')
            for confname in [ 
                'smtp_host', 'smtp_port', 'smtp_username',
                'smtp_password', 'encrypted_only',
            ]:
                setattr(self, confname, doc.content[confname])


class GPGWrapper():
    """
    This is a temporary class for handling GPG requests, and should be
    replaced by a more general class used throughout the project.
    """

    GNUPG_HOME = "~/.config/leap/gnupg"
    GNUPG_BINARY = "/usr/bin/gpg"  # TODO: change this based on OS

    def __init__(self, gpghome=GNUPG_HOME, gpgbinary=GNUPG_BINARY):
        self.gpg = gnupg.GPG(gnupghome=gpghome, gpgbinary=gpgbinary)

    def find_key(self, email):
        """
        Find user's key based on their email.
        """
        for key in self.gpg.list_keys():
            for uid in key['uids']:
                if re.search(email, uid):
                    return key
        raise LookupError("GnuPG public key for %s not found!" % email)

    def encrypt(self, data, recipient, always_trust=True):
        # TODO: do not 'always_trust'.
        return self.gpg.encrypt(data, recipient, always_trust=always_trust)

    def decrypt(self, data):
        return self.gpg.decrypt(data)

    def import_keys(self, data):
        return self.gpg.import_keys(data)


# service configuration
port = 25
user_email = 'user@leap.se' # TODO: replace for real mail from gui/cli

# instantiate soledad for client app storage and sync
s = soledad.Soledad(user_email)
factory = SMTPFactory(s)

# enable the use of this service with twistd
application = service.Application("LEAP SMTP Relay")
service = internet.TCPServer(port, factory)
service.setServiceParent(application)