/ src / class_smtpServer.py
class_smtpServer.py
  1  """
  2  SMTP server thread
  3  """
  4  import asyncore
  5  import base64
  6  import email
  7  import logging
  8  import re
  9  import signal
 10  import smtpd
 11  import threading
 12  import time
 13  from email.header import decode_header
 14  from email.parser import Parser
 15  
 16  from . import queues
 17  from .addresses import decodeAddress
 18  from .bmconfigparser import config
 19  from .helper_ackPayload import genAckPayload
 20  from .helper_sql import sqlExecute
 21  from .network.threads import StoppableThread
 22  from .version import softwareVersion
 23  
 24  SMTPDOMAIN = "bmaddr.lan"
 25  LISTENPORT = 8425
 26  
 27  logger = logging.getLogger('default')
 28  # pylint: disable=attribute-defined-outside-init
 29  
 30  
 31  class SmtpServerChannelException(Exception):
 32      """Generic smtp server channel exception."""
 33      pass
 34  
 35  
 36  class smtpServerChannel(smtpd.SMTPChannel):
 37      """Asyncore channel for SMTP protocol (server)"""
 38      def smtp_EHLO(self, arg):
 39          """Process an EHLO"""
 40          if not arg:
 41              self.push('501 Syntax: HELO hostname')
 42              return
 43          self.push('250-PyBitmessage %s' % softwareVersion)
 44          self.push('250 AUTH PLAIN')
 45  
 46      def smtp_AUTH(self, arg):
 47          """Process AUTH"""
 48          if not arg or arg[0:5] not in ["PLAIN"]:
 49              self.push('501 Syntax: AUTH PLAIN')
 50              return
 51          authstring = arg[6:]
 52          try:
 53              decoded = base64.b64decode(authstring)
 54              correctauth = "\x00" + config.safeGet(
 55                  "bitmessagesettings", "smtpdusername", "") + "\x00" + config.safeGet(
 56                      "bitmessagesettings", "smtpdpassword", "")
 57              logger.debug('authstring: %s / %s', correctauth, decoded)
 58              if correctauth == decoded:
 59                  self.auth = True
 60                  self.push('235 2.7.0 Authentication successful')
 61              else:
 62                  raise SmtpServerChannelException("Auth fail")
 63          except:  # noqa:E722
 64              self.push('501 Authentication fail')
 65  
 66      def smtp_DATA(self, arg):
 67          """Process DATA"""
 68          if not hasattr(self, "auth") or not self.auth:
 69              self.push('530 Authentication required')
 70              return
 71          smtpd.SMTPChannel.smtp_DATA(self, arg)
 72  
 73  
 74  class smtpServerPyBitmessage(smtpd.SMTPServer):
 75      """Asyncore SMTP server class"""
 76      def handle_accept(self):
 77          """Accept a connection"""
 78          pair = self.accept()
 79          if pair is not None:
 80              conn, addr = pair
 81              self.channel = smtpServerChannel(self, conn, addr)
 82  
 83      def send(self, fromAddress, toAddress, subject, message):
 84          """Send a bitmessage"""
 85          # pylint: disable=arguments-differ
 86          streamNumber, ripe = decodeAddress(toAddress)[2:]
 87          stealthLevel = config.safeGetInt('bitmessagesettings', 'ackstealthlevel')
 88          ackdata = genAckPayload(streamNumber, stealthLevel)
 89          sqlExecute(
 90              '''INSERT INTO sent VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''',
 91              '',
 92              toAddress,
 93              ripe,
 94              fromAddress,
 95              subject,
 96              message,
 97              ackdata,
 98              int(time.time()),  # sentTime (this will never change)
 99              int(time.time()),  # lastActionTime
100              0,  # sleepTill time. This will get set when the POW gets done.
101              'msgqueued',
102              0,  # retryNumber
103              'sent',  # folder
104              2,  # encodingtype
105              # not necessary to have a TTL higher than 2 days
106              min(config.getint('bitmessagesettings', 'ttl'), 86400 * 2)
107          )
108  
109          queues.workerQueue.put(('sendmessage', toAddress))
110  
111      def decode_header(self, hdr):
112          """Email header decoding"""
113          ret = []
114          for h in decode_header(self.msg_headers[hdr]):
115              if h[1]:
116                  ret.append(h[0].decode(h[1]))
117              else:
118                  ret.append(h[0].decode("utf-8", errors='replace'))
119  
120          return ret
121  
122      def process_message(self, peer, mailfrom, rcpttos, data):
123          """Process an email"""
124          # pylint: disable=too-many-locals, too-many-branches
125          p = re.compile(".*<([^>]+)>")
126          if not hasattr(self.channel, "auth") or not self.channel.auth:
127              logger.error('Missing or invalid auth')
128              return
129          try:
130              self.msg_headers = Parser().parsestr(data)
131          except:  # noqa:E722
132              logger.error('Invalid headers')
133              return
134  
135          try:
136              sender, domain = p.sub(r'\1', mailfrom).split("@")
137              if domain != SMTPDOMAIN:
138                  raise Exception("Bad domain %s" % domain)
139              if sender not in config.addresses():
140                  raise Exception("Nonexisting user %s" % sender)
141          except Exception as err:
142              logger.debug('Bad envelope from %s: %r', mailfrom, err)
143              msg_from = self.decode_header("from")
144              try:
145                  msg_from = p.sub(r'\1', self.decode_header("from")[0])
146                  sender, domain = msg_from.split("@")
147                  if domain != SMTPDOMAIN:
148                      raise Exception("Bad domain %s" % domain)
149                  if sender not in config.addresses():
150                      raise Exception("Nonexisting user %s" % sender)
151              except Exception as err:
152                  logger.error('Bad headers from %s: %r', msg_from, err)
153                  return
154  
155          try:
156              msg_subject = self.decode_header('subject')[0]
157          except:  # noqa:E722
158              msg_subject = "Subject missing..."
159  
160          msg_tmp = email.message_from_string(data)
161          body = ''
162          for part in msg_tmp.walk():
163              if part and part.get_content_type() == "text/plain":
164                  body += part.get_payload(decode=1).decode(part.get_content_charset('utf-8'), errors='replace')
165  
166          for to in rcpttos:
167              try:
168                  rcpt, domain = p.sub(r'\1', to).split("@")
169                  if domain != SMTPDOMAIN:
170                      raise Exception("Bad domain %s" % domain)
171                  logger.debug(
172                      'Sending %s to %s about %s', sender, rcpt, msg_subject)
173                  self.send(sender, rcpt, msg_subject, body)
174                  logger.info('Relayed %s to %s', sender, rcpt)
175              except Exception as err:
176                  logger.error('Bad to %s: %r', to, err)
177                  continue
178          return
179  
180  
181  class smtpServer(StoppableThread):
182      """SMTP server thread"""
183      def __init__(self, _=None):
184          super(smtpServer, self).__init__(name="smtpServerThread")
185          self.server = smtpServerPyBitmessage(('127.0.0.1', LISTENPORT), None)
186  
187      def stopThread(self):
188          super(smtpServer, self).stopThread()
189          self.server.close()
190          return
191  
192      def run(self):
193          asyncore.loop(1)
194  
195  
196  def signals(_, __):
197      """Signal handler"""
198      logger.warning('Got signal, terminating')
199      for thread in threading.enumerate():
200          if thread.isAlive() and isinstance(thread, StoppableThread):
201              thread.stopThread()
202  
203  
204  def runServer():
205      """Run SMTP server as a standalone python process"""
206      logger.warning('Running SMTPd thread')
207      smtpThread = smtpServer()
208      smtpThread.start()
209      signal.signal(signal.SIGINT, signals)
210      signal.signal(signal.SIGTERM, signals)
211      logger.warning('Processing')
212      smtpThread.join()
213      logger.warning('The end')
214  
215  
216  if __name__ == "__main__":
217      runServer()