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