1# Copyright (C) 2006-2020 by the Free Software Foundation, Inc.
2#
3# This file is part of GNU Mailman.
4#
5# GNU Mailman is free software: you can redistribute it and/or modify it under
6# the terms of the GNU General Public License as published by the Free
7# Software Foundation, either version 3 of the License, or (at your option)
8# any later version.
9#
10# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
13# more details.
14#
15# You should have received a copy of the GNU General Public License along with
16# GNU Mailman.  If not, see <https://www.gnu.org/licenses/>.
17
18# XXX This module needs to be refactored to avoid direct access to the
19# config.db global.
20
21"""Mailman LMTP runner (server).
22
23Most mail servers can be configured to deliver local messages via 'LMTP'[1].
24This module is actually an LMTP server rather than a standard runner.
25
26The LMTP runner opens a local TCP port and waits for the mail server to
27connect to it.  The messages it receives over LMTP are very minimally parsed
28for sanity and if they look okay, they are accepted and injected into
29Mailman's incoming queue for normal processing.  If they don't look good, or
30are destined for a bogus sub-address, they are rejected right away, hopefully
31so that the peer mail server can provide better diagnostics.
32
33[1] RFC 2033 Local Mail Transport Protocol
34    http://www.faqs.org/rfcs/rfc2033.html
35"""
36
37import email
38import asyncio
39import logging
40
41from aiosmtpd.controller import Controller
42from aiosmtpd.lmtp import LMTP
43from contextlib import suppress
44from email.utils import parseaddr
45from mailman.config import config
46from mailman.core.runner import Runner
47from mailman.database.transaction import transactional
48from mailman.email.message import Message
49from mailman.interfaces.domain import IDomainManager
50from mailman.interfaces.listmanager import IListManager
51from mailman.interfaces.runner import RunnerInterrupt
52from mailman.utilities.datetime import now
53from mailman.utilities.email import add_message_hash
54from public import public
55from zope.component import getUtility
56
57
58elog = logging.getLogger('mailman.error')
59qlog = logging.getLogger('mailman.runner')
60slog = logging.getLogger('mailman.smtp')
61
62
63# We only care about the listname and the sub-addresses as in listname@ or
64# listname-request@.  This maps user visible subaddress names (which may
65# include aliases) to the internal canonical subaddress name.
66SUBADDRESS_NAMES = dict(
67    bounces='bounces',
68    confirm='confirm',
69    join='join',
70    leave='leave',
71    owner='owner',
72    request='request',
73    subscribe='join',
74    unsubscribe='leave',
75    )
76
77# This maps subaddress canonical name to the destination queue that handles
78# messages sent to that subaddress.
79SUBADDRESS_QUEUES = dict(
80    bounces='bounces',
81    confirm='command',
82    join='command',
83    leave='command',
84    owner='in',
85    request='command',
86    )
87
88DASH = '-'
89CRLF = '\r\n'
90ERR_451 = '451 Requested action aborted: error in processing'
91ERR_501 = '501 Message has defects'
92ERR_502 = '502 Error: command HELO not implemented'
93ERR_550 = '550 Requested action not taken: mailbox unavailable'
94ERR_550_MID = '550 No Message-ID header provided'
95
96
97def split_recipient(address):
98    """Split an address into listname, subaddress and domain parts.
99
100    For example:
101
102    >>> split_recipient('mylist@example.com')
103    ('mylist', None, 'example.com')
104
105    >>> split_recipient('mylist-request@example.com')
106    ('mylist', 'request', 'example.com')
107
108    :param address: The destination address.
109    :return: A 3-tuple of the form (list-shortname, subaddress, domain).
110        subaddress may be None if this is the list's posting address.
111
112    If the domain of the destination address matches an alias_domain of some
113    IDomain Domain, the domain is replaced by the Domain's mail_host.
114    """
115    localpart, domain = address.split('@', 1)
116    domain_manager = getUtility(IDomainManager)
117    for d in domain_manager:
118        if d.alias_domain is not None and domain == d.alias_domain:
119            domain = d.mail_host
120            break
121    localpart = localpart.split(config.mta.verp_delimiter, 1)[0]
122    listname, dash, subaddress = localpart.rpartition('-')
123    if subaddress not in SUBADDRESS_NAMES or listname == '' or dash == '':
124        listname = localpart
125        subaddress = None
126    return listname, subaddress, domain
127
128
129class LMTPHandler:
130    @asyncio.coroutine
131    @transactional
132    def handle_DATA(self, server, session, envelope):
133        try:
134            # Refresh the list of list names every time we process a message
135            # since the set of mailing lists could have changed.
136            listnames = set(getUtility(IListManager).names)
137            # Parse the message data.  If there are any defects in the
138            # message, reject it right away; it's probably spam.
139            msg = email.message_from_bytes(envelope.content, Message)
140        except Exception:
141            elog.exception('LMTP message parsing')
142            config.db.abort()
143            return CRLF.join(ERR_451 for to in envelope.rcpt_tos)
144        # Do basic post-processing of the message, checking it for defects or
145        # other missing information.
146        message_id = msg.get('message-id')
147        if message_id is None:
148            return ERR_550_MID
149        if msg.defects:
150            return ERR_501
151        msg.original_size = len(envelope.content)
152        add_message_hash(msg)
153        msg['X-MailFrom'] = envelope.mail_from
154        # RFC 2033 requires us to return a status code for every recipient.
155        status = []
156        # Now for each address in the recipients, parse the address to first
157        # see if it's destined for a valid mailing list.  If so, then queue
158        # the message to the appropriate place and record a 250 status for
159        # that recipient.  If not, record a failure status for that recipient.
160        received_time = now()
161        for to in envelope.rcpt_tos:
162            try:
163                to = parseaddr(to)[1].lower()
164                local, subaddress, domain = split_recipient(to)
165                if subaddress is not None:
166                    # Check that local-subaddress is not an actual list name.
167                    listname = '{}-{}@{}'.format(local, subaddress, domain)
168                    if listname in listnames:
169                        local = '{}-{}'.format(local, subaddress)
170                        subaddress = None
171                slog.debug('%s to: %s, list: %s, sub: %s, dom: %s',
172                           message_id, to, local, subaddress, domain)
173                listname = '{}@{}'.format(local, domain)
174                if listname not in listnames:
175                    status.append(ERR_550)
176                    continue
177                mlist = getUtility(IListManager).get_by_fqdn(listname)
178                # The recipient is a valid mailing list.  Find the subaddress
179                # if there is one, and set things up to enqueue to the proper
180                # queue.
181                queue = None
182                msgdata = dict(listid=mlist.list_id,
183                               original_size=msg.original_size,
184                               received_time=received_time)
185                canonical_subaddress = SUBADDRESS_NAMES.get(subaddress)
186                queue = SUBADDRESS_QUEUES.get(canonical_subaddress)
187                if subaddress is None:
188                    # The message is destined for the mailing list.
189                    msgdata['to_list'] = True
190                    queue = 'in'
191                elif canonical_subaddress is None:
192                    # The subaddress was bogus.
193                    slog.error('%s unknown sub-address: %s',
194                               message_id, subaddress)
195                    status.append(ERR_550)
196                    continue
197                else:
198                    # A valid subaddress.
199                    msgdata['subaddress'] = canonical_subaddress
200                    if subaddress == 'request':
201                        msgdata['to_request'] = True
202                    if canonical_subaddress == 'owner':
203                        msgdata.update(dict(
204                            to_owner=True,
205                            envsender=config.mailman.site_owner,
206                            ))
207                        queue = 'in'
208                # If we found a valid destination, enqueue the message and add
209                # a success status for this recipient.
210                if queue is not None:
211                    config.switchboards[queue].enqueue(msg, msgdata)
212                    slog.debug('%s subaddress: %s, queue: %s',
213                               message_id, canonical_subaddress, queue)
214                    status.append('250 Ok')
215            except Exception:
216                slog.exception('Queue detection: %s', msg['message-id'])
217                config.db.abort()
218                status.append(ERR_550)
219        # All done; returning this big status string should give the expected
220        # response to the LMTP client.
221        return CRLF.join(status)
222
223
224class LMTPController(Controller):
225    def factory(self):
226        server = LMTP(self.handler)
227        server.__ident__ = 'GNU Mailman LMTP runner 2.0'
228        return server
229
230
231@public
232class LMTPRunner(Runner):
233    # Only __init__ is called on startup. Asyncore is responsible for later
234    # connections from the MTA.  slice and numslices are ignored and are
235    # necessary only to satisfy the API.
236
237    is_queue_runner = False
238
239    def __init__(self, name, slice=None):
240        super().__init__(name, slice)
241        hostname = config.mta.lmtp_host
242        port = int(config.mta.lmtp_port)
243        self.lmtp = LMTPController(LMTPHandler(), hostname=hostname, port=port)
244        qlog.debug('LMTP server listening on %s:%s', hostname, port)
245
246    def run(self):
247        """See `IRunner`."""
248        with suppress(RunnerInterrupt):
249            self.lmtp.start()
250            while not self._stop:
251                self._snooze(0)
252            self.lmtp.stop()
253