summaryrefslogtreecommitdiff
path: root/service/pixelated/adapter/services/mail_sender.py
blob: b7b5bfe682a555588e4741faa38c6b268b55c7d6 (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
#
# Copyright (c) 2014 ThoughtWorks, Inc.
#
# Pixelated 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.
#
# Pixelated 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 Pixelated. If not, see <http://www.gnu.org/licenses/>.
from StringIO import StringIO
from email.utils import parseaddr
from copy import deepcopy
from leap.mail.outgoing.service import OutgoingMail

from twisted.internet.defer import Deferred, fail
from twisted.mail.smtp import SMTPSenderFactory
from twisted.internet import reactor, defer
from pixelated.support.functional import flatten
from twisted.mail.smtp import User


class SMTPDownException(Exception):
    def __init__(self):
        Exception.__init__(self, "Couldn't send mail now, try again later.")


NOT_NEEDED = None


class MailSenderException(Exception):

    def __init__(self, message, email_error_map):
        super(MailSenderException, self).__init__(message, email_error_map)
        self.email_error_map = email_error_map


class MailSender(object):

    def __init__(self, smtp_config, keymanager):
        self._smtp_config = smtp_config
        self._keymanager = keymanager

    @defer.inlineCallbacks
    def sendmail(self, mail):
        # message is changed in sending, but should be saved unaltered
        mail = deepcopy(mail)

        recipients = flatten([mail.to, mail.cc, mail.bcc])

        results = yield self._send_mail_to_all_recipients(mail, recipients)
        all_succeeded = reduce(lambda a, b: a and b, [r[0] for r in results])

        if not all_succeeded:
            error_map = self._build_error_map(recipients, results)
            raise MailSenderException('Failed to send mail to all recipients', error_map)

        defer.returnValue(all_succeeded)

    def _send_mail_to_all_recipients(self, mail, recipients):
        outgoing_mail = self._create_outgoing_mail()
        bccs = mail.bcc
        deferreds = []

        for recipient in recipients:
            self._define_bcc_field(mail, recipient, bccs)
            smtp_recipient = self._create_twisted_smtp_recipient(recipient)
            deferreds.append(outgoing_mail.send_message(mail.to_smtp_format(), smtp_recipient))

        return defer.DeferredList(deferreds, fireOnOneErrback=False, consumeErrors=True)

    def _define_bcc_field(self, mail, recipient, bccs):
        if recipient in bccs:
            mail.headers['Bcc'] = [recipient]
        else:
            mail.headers['Bcc'] = []

    def _build_error_map(self, recipients, results):
        error_map = {}
        for email, error in [(recipients[idx], r[1]) for idx, r in enumerate(results)]:
            error_map[email] = error
        return error_map

    def _create_outgoing_mail(self):
        return OutgoingMail(str(self._smtp_config.account_email),
                            self._keymanager,
                            self._smtp_config.cert_path,
                            self._smtp_config.cert_path,
                            str(self._smtp_config.remote_smtp_host),
                            int(self._smtp_config.remote_smtp_port))

    def _create_twisted_smtp_recipient(self, recipient):
        # TODO: Better is fix Twisted instead
        return User(str(recipient), NOT_NEEDED, NOT_NEEDED, NOT_NEEDED)