2019-11-04 15:51:54 +01:00
|
|
|
"""
|
|
|
|
SMTP server thread
|
|
|
|
"""
|
2016-07-22 08:10:56 +02:00
|
|
|
import asyncore
|
2016-07-19 13:57:54 +02:00
|
|
|
import base64
|
|
|
|
import email
|
2019-08-06 13:04:33 +02:00
|
|
|
import logging
|
2016-07-19 13:57:54 +02:00
|
|
|
import re
|
|
|
|
import signal
|
|
|
|
import smtpd
|
|
|
|
import threading
|
|
|
|
import time
|
2019-08-06 13:04:33 +02:00
|
|
|
from email.header import decode_header
|
|
|
|
from email.parser import Parser
|
2016-07-19 13:57:54 +02:00
|
|
|
|
2019-08-06 13:04:33 +02:00
|
|
|
import queues
|
2016-07-19 13:57:54 +02:00
|
|
|
from addresses import decodeAddress
|
2022-01-28 13:55:23 +01:00
|
|
|
from bmconfigparser import config
|
2017-09-30 11:19:44 +02:00
|
|
|
from helper_ackPayload import genAckPayload
|
2019-08-06 13:04:33 +02:00
|
|
|
from helper_sql import sqlExecute
|
|
|
|
from network.threads import StoppableThread
|
2017-01-11 14:27:19 +01:00
|
|
|
from version import softwareVersion
|
2016-07-19 13:57:54 +02:00
|
|
|
|
|
|
|
SMTPDOMAIN = "bmaddr.lan"
|
|
|
|
LISTENPORT = 8425
|
|
|
|
|
2019-08-06 13:04:33 +02:00
|
|
|
logger = logging.getLogger('default')
|
2019-11-04 15:51:54 +01:00
|
|
|
# pylint: disable=attribute-defined-outside-init
|
2019-08-06 13:04:33 +02:00
|
|
|
|
2019-10-12 16:12:19 +02:00
|
|
|
|
2021-08-31 18:53:37 +02:00
|
|
|
class SmtpServerChannelException(Exception):
|
|
|
|
"""Generic smtp server channel exception."""
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2016-07-19 13:57:54 +02:00
|
|
|
class smtpServerChannel(smtpd.SMTPChannel):
|
2019-11-04 15:51:54 +01:00
|
|
|
"""Asyncore channel for SMTP protocol (server)"""
|
2016-07-19 13:57:54 +02:00
|
|
|
def smtp_EHLO(self, arg):
|
2019-11-04 15:51:54 +01:00
|
|
|
"""Process an EHLO"""
|
2016-07-19 13:57:54 +02:00
|
|
|
if not arg:
|
|
|
|
self.push('501 Syntax: HELO hostname')
|
|
|
|
return
|
2017-01-11 14:27:19 +01:00
|
|
|
self.push('250-PyBitmessage %s' % softwareVersion)
|
2016-07-19 13:57:54 +02:00
|
|
|
self.push('250 AUTH PLAIN')
|
|
|
|
|
|
|
|
def smtp_AUTH(self, arg):
|
2019-11-04 15:51:54 +01:00
|
|
|
"""Process AUTH"""
|
2016-07-19 13:57:54 +02:00
|
|
|
if not arg or arg[0:5] not in ["PLAIN"]:
|
|
|
|
self.push('501 Syntax: AUTH PLAIN')
|
|
|
|
return
|
|
|
|
authstring = arg[6:]
|
|
|
|
try:
|
|
|
|
decoded = base64.b64decode(authstring)
|
2022-01-28 13:55:23 +01:00
|
|
|
correctauth = "\x00" + config.safeGet(
|
|
|
|
"bitmessagesettings", "smtpdusername", "") + "\x00" + config.safeGet(
|
2019-11-04 15:51:54 +01:00
|
|
|
"bitmessagesettings", "smtpdpassword", "")
|
2019-08-06 13:04:33 +02:00
|
|
|
logger.debug('authstring: %s / %s', correctauth, decoded)
|
2016-07-19 13:57:54 +02:00
|
|
|
if correctauth == decoded:
|
|
|
|
self.auth = True
|
|
|
|
self.push('235 2.7.0 Authentication successful')
|
|
|
|
else:
|
2021-08-31 18:53:37 +02:00
|
|
|
raise SmtpServerChannelException("Auth fail")
|
2021-08-27 18:01:23 +02:00
|
|
|
except: # noqa:E722
|
2016-07-19 13:57:54 +02:00
|
|
|
self.push('501 Authentication fail')
|
|
|
|
|
|
|
|
def smtp_DATA(self, arg):
|
2019-11-04 15:51:54 +01:00
|
|
|
"""Process DATA"""
|
2016-07-19 13:57:54 +02:00
|
|
|
if not hasattr(self, "auth") or not self.auth:
|
2019-08-06 13:04:33 +02:00
|
|
|
self.push('530 Authentication required')
|
2016-07-19 13:57:54 +02:00
|
|
|
return
|
|
|
|
smtpd.SMTPChannel.smtp_DATA(self, arg)
|
|
|
|
|
|
|
|
|
|
|
|
class smtpServerPyBitmessage(smtpd.SMTPServer):
|
2019-11-04 15:51:54 +01:00
|
|
|
"""Asyncore SMTP server class"""
|
2016-07-19 13:57:54 +02:00
|
|
|
def handle_accept(self):
|
2019-11-04 15:51:54 +01:00
|
|
|
"""Accept a connection"""
|
2016-07-19 13:57:54 +02:00
|
|
|
pair = self.accept()
|
|
|
|
if pair is not None:
|
|
|
|
conn, addr = pair
|
|
|
|
self.channel = smtpServerChannel(self, conn, addr)
|
|
|
|
|
2020-01-15 11:47:26 +01:00
|
|
|
def send(self, fromAddress, toAddress, subject, message):
|
2019-11-04 15:51:54 +01:00
|
|
|
"""Send a bitmessage"""
|
2020-01-15 11:47:26 +01:00
|
|
|
# pylint: disable=arguments-differ
|
2019-11-04 15:51:54 +01:00
|
|
|
streamNumber, ripe = decodeAddress(toAddress)[2:]
|
2022-01-28 13:55:23 +01:00
|
|
|
stealthLevel = config.safeGetInt('bitmessagesettings', 'ackstealthlevel')
|
2017-09-30 11:19:44 +02:00
|
|
|
ackdata = genAckPayload(streamNumber, stealthLevel)
|
2016-07-19 13:57:54 +02:00
|
|
|
sqlExecute(
|
|
|
|
'''INSERT INTO sent VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''',
|
|
|
|
'',
|
|
|
|
toAddress,
|
|
|
|
ripe,
|
|
|
|
fromAddress,
|
|
|
|
subject,
|
|
|
|
message,
|
|
|
|
ackdata,
|
2019-11-04 15:51:54 +01:00
|
|
|
int(time.time()), # sentTime (this will never change)
|
|
|
|
int(time.time()), # lastActionTime
|
|
|
|
0, # sleepTill time. This will get set when the POW gets done.
|
2016-07-19 13:57:54 +02:00
|
|
|
'msgqueued',
|
2019-11-04 15:51:54 +01:00
|
|
|
0, # retryNumber
|
|
|
|
'sent', # folder
|
|
|
|
2, # encodingtype
|
|
|
|
# not necessary to have a TTL higher than 2 days
|
2022-01-28 13:55:23 +01:00
|
|
|
min(config.getint('bitmessagesettings', 'ttl'), 86400 * 2)
|
2016-07-19 13:57:54 +02:00
|
|
|
)
|
|
|
|
|
2017-02-08 13:41:56 +01:00
|
|
|
queues.workerQueue.put(('sendmessage', toAddress))
|
2016-07-19 13:57:54 +02:00
|
|
|
|
|
|
|
def decode_header(self, hdr):
|
2019-11-04 15:51:54 +01:00
|
|
|
"""Email header decoding"""
|
2016-07-19 13:57:54 +02:00
|
|
|
ret = []
|
|
|
|
for h in decode_header(self.msg_headers[hdr]):
|
|
|
|
if h[1]:
|
2021-08-27 18:01:23 +02:00
|
|
|
ret.append(h[0].decode(h[1]))
|
2016-07-19 13:57:54 +02:00
|
|
|
else:
|
|
|
|
ret.append(h[0].decode("utf-8", errors='replace'))
|
2018-05-02 17:29:55 +02:00
|
|
|
|
2016-07-19 13:57:54 +02:00
|
|
|
return ret
|
|
|
|
|
2020-01-15 11:47:26 +01:00
|
|
|
def process_message(self, peer, mailfrom, rcpttos, data):
|
2019-11-04 15:51:54 +01:00
|
|
|
"""Process an email"""
|
2020-01-15 11:47:26 +01:00
|
|
|
# pylint: disable=too-many-locals, too-many-branches
|
2016-07-19 13:57:54 +02:00
|
|
|
p = re.compile(".*<([^>]+)>")
|
|
|
|
if not hasattr(self.channel, "auth") or not self.channel.auth:
|
2019-08-06 13:04:33 +02:00
|
|
|
logger.error('Missing or invalid auth')
|
2016-07-19 13:57:54 +02:00
|
|
|
return
|
|
|
|
try:
|
|
|
|
self.msg_headers = Parser().parsestr(data)
|
2021-08-27 18:01:23 +02:00
|
|
|
except: # noqa:E722
|
2019-08-06 13:04:33 +02:00
|
|
|
logger.error('Invalid headers')
|
2016-07-19 13:57:54 +02:00
|
|
|
return
|
|
|
|
|
|
|
|
try:
|
|
|
|
sender, domain = p.sub(r'\1', mailfrom).split("@")
|
|
|
|
if domain != SMTPDOMAIN:
|
2019-10-12 16:12:19 +02:00
|
|
|
raise Exception("Bad domain %s" % domain)
|
2022-01-28 13:55:23 +01:00
|
|
|
if sender not in config.addresses():
|
2019-10-12 16:12:19 +02:00
|
|
|
raise Exception("Nonexisting user %s" % sender)
|
2016-07-19 13:57:54 +02:00
|
|
|
except Exception as err:
|
2019-08-06 13:04:33 +02:00
|
|
|
logger.debug('Bad envelope from %s: %r', mailfrom, err)
|
2016-07-19 13:57:54 +02:00
|
|
|
msg_from = self.decode_header("from")
|
|
|
|
try:
|
|
|
|
msg_from = p.sub(r'\1', self.decode_header("from")[0])
|
|
|
|
sender, domain = msg_from.split("@")
|
|
|
|
if domain != SMTPDOMAIN:
|
2019-10-12 16:12:19 +02:00
|
|
|
raise Exception("Bad domain %s" % domain)
|
2022-01-28 13:55:23 +01:00
|
|
|
if sender not in config.addresses():
|
2019-10-12 16:12:19 +02:00
|
|
|
raise Exception("Nonexisting user %s" % sender)
|
2016-07-19 13:57:54 +02:00
|
|
|
except Exception as err:
|
2019-08-06 13:04:33 +02:00
|
|
|
logger.error('Bad headers from %s: %r', msg_from, err)
|
2016-07-19 13:57:54 +02:00
|
|
|
return
|
|
|
|
|
|
|
|
try:
|
|
|
|
msg_subject = self.decode_header('subject')[0]
|
2021-08-27 18:01:23 +02:00
|
|
|
except: # noqa:E722
|
2016-07-19 13:57:54 +02:00
|
|
|
msg_subject = "Subject missing..."
|
|
|
|
|
|
|
|
msg_tmp = email.message_from_string(data)
|
|
|
|
body = u''
|
|
|
|
for part in msg_tmp.walk():
|
|
|
|
if part and part.get_content_type() == "text/plain":
|
|
|
|
body += part.get_payload(decode=1).decode(part.get_content_charset('utf-8'), errors='replace')
|
|
|
|
|
|
|
|
for to in rcpttos:
|
|
|
|
try:
|
|
|
|
rcpt, domain = p.sub(r'\1', to).split("@")
|
|
|
|
if domain != SMTPDOMAIN:
|
2019-10-12 16:12:19 +02:00
|
|
|
raise Exception("Bad domain %s" % domain)
|
2019-08-06 13:04:33 +02:00
|
|
|
logger.debug(
|
|
|
|
'Sending %s to %s about %s', sender, rcpt, msg_subject)
|
2016-07-19 13:57:54 +02:00
|
|
|
self.send(sender, rcpt, msg_subject, body)
|
2019-08-06 13:04:33 +02:00
|
|
|
logger.info('Relayed %s to %s', sender, rcpt)
|
2016-07-19 13:57:54 +02:00
|
|
|
except Exception as err:
|
2019-08-06 13:04:33 +02:00
|
|
|
logger.error('Bad to %s: %r', to, err)
|
2016-07-19 13:57:54 +02:00
|
|
|
continue
|
|
|
|
return
|
|
|
|
|
2019-08-01 13:37:26 +02:00
|
|
|
|
|
|
|
class smtpServer(StoppableThread):
|
2019-11-04 15:51:54 +01:00
|
|
|
"""SMTP server thread"""
|
|
|
|
def __init__(self, _=None):
|
2019-08-01 13:37:26 +02:00
|
|
|
super(smtpServer, self).__init__(name="smtpServerThread")
|
2016-07-19 13:57:54 +02:00
|
|
|
self.server = smtpServerPyBitmessage(('127.0.0.1', LISTENPORT), None)
|
2018-05-02 17:29:55 +02:00
|
|
|
|
2016-07-19 13:57:54 +02:00
|
|
|
def stopThread(self):
|
|
|
|
super(smtpServer, self).stopThread()
|
|
|
|
self.server.close()
|
|
|
|
return
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
asyncore.loop(1)
|
|
|
|
|
2019-08-06 13:04:33 +02:00
|
|
|
|
2019-11-04 15:51:54 +01:00
|
|
|
def signals(_, __):
|
|
|
|
"""Signal handler"""
|
2019-08-06 13:04:33 +02:00
|
|
|
logger.warning('Got signal, terminating')
|
2016-07-19 13:57:54 +02:00
|
|
|
for thread in threading.enumerate():
|
|
|
|
if thread.isAlive() and isinstance(thread, StoppableThread):
|
|
|
|
thread.stopThread()
|
|
|
|
|
2019-08-06 13:04:33 +02:00
|
|
|
|
2016-07-19 13:57:54 +02:00
|
|
|
def runServer():
|
2019-11-04 15:51:54 +01:00
|
|
|
"""Run SMTP server as a standalone python process"""
|
2019-08-06 13:04:33 +02:00
|
|
|
logger.warning('Running SMTPd thread')
|
2016-07-19 13:57:54 +02:00
|
|
|
smtpThread = smtpServer()
|
|
|
|
smtpThread.start()
|
|
|
|
signal.signal(signal.SIGINT, signals)
|
|
|
|
signal.signal(signal.SIGTERM, signals)
|
2019-08-06 13:04:33 +02:00
|
|
|
logger.warning('Processing')
|
2016-07-19 13:57:54 +02:00
|
|
|
smtpThread.join()
|
2019-08-06 13:04:33 +02:00
|
|
|
logger.warning('The end')
|
|
|
|
|
2016-07-19 13:57:54 +02:00
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
runServer()
|