This repository has been archived on 2025-02-02. You can view files and clone it, but cannot push or open issues or pull requests.
PyBitmessage-2025-02-02/src/class_smtpServer.py

482 lines
19 KiB
Python
Raw Normal View History

from cStringIO import StringIO
2013-07-03 16:09:53 +02:00
from pyelliptic.openssl import OpenSSL
import asynchat
import base64
2013-07-10 13:10:54 +02:00
from email import parser, generator, utils
import errno
2013-07-03 16:09:53 +02:00
import shared
import smtpd
import socket
import ssl
2013-07-03 16:09:53 +02:00
import time
from addresses import *
import helper_sent
# This is copied from Python's smtpd module and modified to support basic SMTP AUTH.
class bitmessageSMTPChannel(asynchat.async_chat):
COMMAND = 0
DATA = 1
def __init__(self, server, conn, addr):
asynchat.async_chat.__init__(self, conn)
self.__server = server
self.__conn = conn
self.__addr = addr
self.__line = []
self.__state = self.COMMAND
2013-07-05 11:19:38 +02:00
self.__greeting = None
self.__mailfrom = None
self.__rcpttos = []
self.__data = ''
self.__fqdn = socket.getfqdn()
self.__version = 'Python SMTP proxy version 0.2a'
self.__invalid_command_count = 0
self.logged_in = False
try:
self.__peer = conn.getpeername()
except socket.error, err:
# a race condition may occur if the other end is closing
# before we can get the peername
self.close()
if err[0] != errno.ENOTCONN:
raise
return
2013-07-10 12:02:45 +02:00
with shared.printLock:
print >> smtpd.DEBUGSTREAM, 'Peer:', repr(self.__peer)
self.push('220 %s %s' % (self.__fqdn, self.__version))
self.set_terminator('\r\n')
# Overrides base class for convenience
def push(self, msg):
asynchat.async_chat.push(self, msg + '\r\n')
# Implementation of base class abstract method
def collect_incoming_data(self, data):
self.__line.append(data)
# Close the connection after enough errors
def invalid_command(self, msg):
self.__invalid_command_count += 1
if self.__invalid_command_count >= 10:
self.push(msg + " (closing connection)")
self.close_when_done()
else:
self.push(msg)
# Implementation of base class abstract method
def found_terminator(self):
line = ''.join(self.__line)
2013-07-10 12:02:45 +02:00
with shared.printLock:
print >> smtpd.DEBUGSTREAM, 'Data:', repr(line)
self.__line = []
if self.__state == self.COMMAND:
if not line:
self.invalid_command('500 Error: bad syntax')
return
method = None
i = line.find(' ')
if i < 0:
command = line.upper()
arg = None
else:
command = line[:i].upper()
arg = line[i+1:].strip()
method = getattr(self, 'smtp_' + command, None)
if not method:
self.invalid_command('502 Error: command "%s" not implemented' % command)
return
method(arg)
return
else:
if self.__state != self.DATA:
self.invalid_command('451 Internal confusion')
return
# Remove extraneous carriage returns and de-transparency according
# to RFC 821, Section 4.5.2.
data = []
for text in line.split('\r\n'):
if text and text[0] == '.':
data.append(text[1:])
else:
data.append(text)
self.__data = '\n'.join(data)
try:
status = self.__server.process_message(self.__peer,
self.address,
self.__rcpttos,
self.__data)
except Exception, e:
status = '554 Requested transaction failed: {}'.format(str(e))
self.__rcpttos = []
self.__mailfrom = None
self.__state = self.COMMAND
self.set_terminator('\r\n')
if not status:
self.push('250 Ok')
else:
self.invalid_command(status)
# SMTP and ESMTP commands
2013-07-05 09:35:38 +02:00
def smtp_HELP(self, arg):
self.push('214 HELP HELO EHLO AUTH NOOP QUIT MAIL RCPT RSET DATA')
# TODO - detailed help for all commands.
return
def smtp_EHLO(self, arg):
if not arg:
self.invalid_command('501 Syntax: EHLO hostname')
return
2013-07-05 11:19:38 +02:00
if self.__greeting is not None:
self.invalid_command('503 Duplicate HELO/EHLO')
return
else:
self.__greeting = arg
self.push('250-%s offers:' % self.__fqdn)
self.push('250 AUTH PLAIN')
def smtp_HELO(self, arg):
if not arg:
self.invalid_command('501 Syntax: HELO hostname')
return
2013-07-05 11:19:38 +02:00
if self.__greeting is not None:
self.invalid_command('503 Duplicate HELO/EHLO')
else:
self.__greeting = arg
self.push('250 %s' % self.__fqdn)
def smtp_AUTH(self, arg):
2013-07-05 11:19:38 +02:00
if self.__greeting is None:
self.invalid_command('503 Error: required EHLO')
return
try:
encoding, pw = arg.split(' ')
except ValueError:
self.invalid_command('501 Syntax: AUTH PLAIN auth')
return
if encoding != 'PLAIN':
self.invalid_command('502 method not understood')
return
2013-07-05 09:35:38 +02:00
try:
z, username, pw = base64.b64decode(pw).split('\x00')
except:
z = 'error'
if z != '':
self.invalid_command('501 authorization not understood')
return
if '@' in username:
username, _ = username.split('@', 1)
self.address = username
with shared.printLock:
print 'Login request from {}'.format(self.address)
status, addressVersionNumber, streamNumber, ripe = decodeAddress(self.address)
if status != 'success':
2013-07-06 13:45:51 +02:00
with shared.printLock:
print 'Error: Could not decode address: ' + self.address + ' : ' + status
if status == 'checksumfailed':
print 'Error: Checksum failed for address: ' + self.address
if status == 'invalidcharacters':
print 'Error: Invalid characters in address: ' + self.address
if status == 'versiontoohigh':
print 'Error: Address version number too high (or zero) in address: ' + self.address
raise Exception("Invalid Bitmessage address: {}".format(self.address))
# Each identity must be enabled independly by setting the smtppop3password for the identity
# If no password is set, then the identity is not available for SMTP/POP3 access.
try:
2013-07-03 18:21:04 +02:00
if shared.config.getboolean(self.address, "enabled"):
self.pw = shared.config.get(self.address, "smtppop3password")
if pw == self.pw:
self.push('235 Authentication successful. Proceed.')
self.logged_in = True
return
except:
pass
2013-07-03 18:21:04 +02:00
self.invalid_command('530 Access denied.')
def smtp_NOOP(self, arg):
if arg:
self.invalid_command('501 Syntax: NOOP')
else:
self.push('250 Ok')
def smtp_QUIT(self, arg):
# args is ignored
self.push('221 Bye')
self.close_when_done()
# factored
def __getaddr(self, keyword, arg):
address = None
keylen = len(keyword)
if arg[:keylen].upper() == keyword:
address = arg[keylen:].strip()
if not address:
pass
elif address[0] == '<' and address[-1] == '>' and address != '<>':
# Addresses can be in the form <person@dom.com> but watch out
# for null address, e.g. <>
address = address[1:-1]
return address
def smtp_MAIL(self, arg):
2013-07-10 12:02:45 +02:00
with shared.printLock:
print >> smtpd.DEBUGSTREAM, '===> MAIL', arg
address = self.__getaddr('FROM:', arg) if arg else None
if not address or '@' not in address:
self.invalid_command('501 Syntax: MAIL FROM: <address>')
return
if self.__mailfrom:
self.invalid_command('503 Error: nested MAIL command')
return
if not self.logged_in:
self.invalid_command('503 Not authenticated.')
return
localPart, _ = address.split('@', 1)
if self.address != localPart:
self.invalid_command('530 Access denied: address must be the same as the authorized account')
2013-07-04 08:33:04 +02:00
return
self.__mailfrom = address
2013-07-10 12:02:45 +02:00
with shared.printLock:
print >> smtpd.DEBUGSTREAM, 'sender:', self.__mailfrom
self.push('250 Ok')
def smtp_RCPT(self, arg):
2013-07-10 12:02:45 +02:00
with shared.printLock:
print >> smtpd.DEBUGSTREAM, '===> RCPT', arg
if not self.__mailfrom:
self.invalid_command('503 Error: need MAIL command')
return
if not self.logged_in:
# This will never happen. :)
self.invalid_command('503 Not authenticated.')
return
address = self.__getaddr('TO:', arg) if arg else None
if not address:
self.invalid_command('501 Syntax: RCPT TO: <user@address>')
return
try:
localPart, dom = address.split('@', 1)
except ValueError:
self.invalid_command('501 Syntax: RCPT TO: <user@address>')
return
status, addressVersionNumber, streamNumber, fromRipe = decodeAddress(localPart)
if status != 'success':
self.invalid_command('501 Bitmessage address is incorrect: {}'.format(status))
return
self.__rcpttos.append(address)
2013-07-10 12:02:45 +02:00
with shared.printLock:
print >> smtpd.DEBUGSTREAM, 'recips:', self.__rcpttos
self.push('250 Ok')
def smtp_RSET(self, arg):
if arg:
self.invalid_command('501 Syntax: RSET')
return
# Resets the sender, recipients, and data, but not the greeting
self.__mailfrom = None
self.__rcpttos = []
self.__data = ''
self.__state = self.COMMAND
self.push('250 Ok')
def smtp_DATA(self, arg):
if not self.logged_in:
self.invalid_command('503 Not authenticated.')
return
if not self.__rcpttos:
self.invalid_command('503 Error: need RCPT command')
return
if arg:
self.invalid_command('501 Syntax: DATA')
return
self.__state = self.DATA
self.set_terminator('\r\n.\r\n')
self.push('354 End data with <CR><LF>.<CR><LF>')
2013-07-03 16:09:53 +02:00
class bitmessageSMTPServer(smtpd.SMTPServer):
def __init__(self):
smtpport = shared.config.getint('bitmessagesettings', 'smtpport')
self.ssl = shared.config.getboolean('bitmessagesettings', 'smtpssl')
if self.ssl:
self.keyfile = shared.config.get('bitmessagesettings', 'keyfile')
self.certfile = shared.config.get('bitmessagesettings', 'certfile')
try:
bindAddress = shared.config.get('bitmessagesettings', 'smtpaddress')
except:
bindAddress = '127.0.0.1'
smtpd.SMTPServer.__init__(self, (bindAddress, smtpport), None)
2013-07-06 13:45:51 +02:00
with shared.printLock:
print "SMTP server started: SSL enabled={}".format(str(self.ssl))
2013-07-03 16:09:53 +02:00
def handle_accept(self):
# Override SMTPServer's handle_accept so that we can start an SSL connection.
sock, peer_address = self.accept()
if self.ssl:
sock = ssl.wrap_socket(sock, server_side=True, certfile=self.certfile, keyfile=self.keyfile, ssl_version=ssl.PROTOCOL_SSLv23)
2013-07-03 19:48:29 +02:00
bitmessageSMTPChannel(self, sock, peer_address)
@staticmethod
def stripMessageHeaders(message):
# Always convert Date header into GMT
2013-07-10 13:10:54 +02:00
if 'Date' in message:
oldDate = message['Date']
del message['Date']
message['Date'] = utils.formatdate(utils.mktime_tz(utils.parsedate_tz(oldDate)), localtime=False)
try:
if not shared.config.getboolean('bitmessagesettings', 'stripmessageheadersenable'):
return
except:
pass
try:
headersToStrip = shared.config.get('bitmessagesettings', 'stripmessageheaders')
except:
2013-07-10 13:10:54 +02:00
headersToStrip = "User-Agent"
headersToStrip = [x.strip() for x in headersToStrip.split(',') if len(x.strip()) > 0]
for h in headersToStrip:
if h in message:
del message[h]
def process_message(self, peer, fromAddress, rcpttos, data):
2013-07-03 16:09:53 +02:00
#print("Peer", peer)
#print("Mail From", fromAddress)
2013-07-03 16:09:53 +02:00
#print("Rcpt To", rcpttos)
#print("Data")
#print(data)
#print('--------')
#print(type(fromAddress))
2013-07-03 16:09:53 +02:00
message = parser.Parser().parsestr(data)
message['X-Bitmessage-Sending-Version'] = shared.softwareVersion
message['X-Bitmessage-Flags'] = '0'
bitmessageSMTPServer.stripMessageHeaders(message)
fp = StringIO()
gen = generator.Generator(fp, mangle_from_=False, maxheaderlen=128)
gen.flatten(message)
message_as_text = fp.getvalue()
2013-07-10 13:10:54 +02:00
with shared.printLock:
2013-07-10 12:02:45 +02:00
print(message_as_text)
checksum = hashlib.sha256(message_as_text).digest()[:2]
checksum = (ord(checksum[0]) << 8) | ord(checksum[1])
2013-07-03 16:09:53 +02:00
# Determine the fromAddress and make sure it's an owned identity
if not (fromAddress.startswith('BM-') and '.' not in fromAddress):
raise Exception("From Address must be a Bitmessage address.")
else:
status, addressVersionNumber, streamNumber, fromRipe = decodeAddress(fromAddress)
if status != 'success':
2013-07-06 13:45:51 +02:00
with shared.printLock:
print 'Error: Could not decode address: ' + fromAddress + ' : ' + status
if status == 'checksumfailed':
print 'Error: Checksum failed for address: ' + fromAddress
if status == 'invalidcharacters':
print 'Error: Invalid characters in address: ' + fromAddress
if status == 'versiontoohigh':
print 'Error: Address version number too high (or zero) in address: ' + fromAddress
2013-07-03 16:09:53 +02:00
raise Exception("Invalid Bitmessage address: {}".format(fromAddress))
#fromAddress = addBMIfNotPresent(fromAddress) # I know there's a BM-, because it's required when using SMTP
try:
fromAddressEnabled = shared.config.getboolean(fromAddress, 'enabled')
except:
2013-07-06 13:45:51 +02:00
with shared.printLock:
print 'Error: Could not find your fromAddress in the keys.dat file.'
2013-07-03 16:09:53 +02:00
raise Exception("Could not find address in keys.dat: {}".format(fromAddress))
if not fromAddressEnabled:
2013-07-06 13:45:51 +02:00
with shared.printLock:
print 'Error: Your fromAddress is disabled. Cannot send.'
2013-07-03 16:09:53 +02:00
raise Exception("The fromAddress is disabled: {}".format(fromAddress))
for recipient in rcpttos:
toAddress, _ = recipient.split('@', 1)
2013-07-03 16:09:53 +02:00
if not (toAddress.startswith('BM-') and '.' not in toAddress):
2013-07-04 08:33:04 +02:00
# TODO - deliver message to another SMTP server..
# I think this feature would urge adoption: the ability to use the same bitmessage address
# for delivering standard E-mail as well bitmessages.
2013-07-03 16:09:53 +02:00
raise Exception("Cannot yet handle normal E-mail addresses.")
else:
2013-07-04 08:33:04 +02:00
# This is now the 3rd copy of this message delivery code.
# There's one in the API, there's another copy in __init__ for
# the UI. Yet another exists here. It needs to be refactored
2013-07-03 16:09:53 +02:00
# into a utility func!
status, addressVersionNumber, streamNumber, toRipe = decodeAddress(toAddress)
if status != 'success':
2013-07-06 13:45:51 +02:00
with shared.printLock:
print 'Error: Could not decode address: ' + toAddress + ' : ' + status
if status == 'checksumfailed':
print 'Error: Checksum failed for address: ' + toAddress
if status == 'invalidcharacters':
print 'Error: Invalid characters in address: ' + toAddress
if status == 'versiontoohigh':
print 'Error: Address version number too high (or zero) in address: ' + toAddress
2013-07-03 16:09:53 +02:00
raise Exception("Invalid Bitmessage address: {}".format(toAddress))
toAddressIsOK = False
try:
shared.config.get(toAddress, 'enabled')
2013-07-04 08:33:04 +02:00
except:
toAddressIsOK = True
if not toAddressIsOK:
2013-07-03 16:09:53 +02:00
# The toAddress is one owned by me. We cannot send
# messages to ourselves without significant changes
# to the codebase.
2013-07-06 13:45:51 +02:00
with shared.printLock:
print "Error: One of the addresses to which you are sending a message, {}, is yours. Unfortunately the Bitmessage client cannot process its own messages. Please try running a second client on a different computer or within a VM.".format(toAddress)
raise Exception("An address that you are sending a message to, {}, is yours. Unfortunately the Bitmessage client cannot process its own messages. Please try running a second client on a different computer or within a VM.".format(toAddress))
2013-07-03 16:09:53 +02:00
# The subject is specially formatted to identify it from non-E-mail messages.
2013-07-04 08:33:04 +02:00
# TODO - The bitfield will be used to convey things like external attachments, etc.
# Last 2 bytes are two bytes of the sha256 checksum of message
if 'Subject' in message:
subject = message['Subject']
else:
subject = ''
2013-07-03 16:09:53 +02:00
ackdata = OpenSSL.rand(32)
t = ('', toAddress, toRipe, fromAddress, subject, message_as_text, ackdata, int(time.time()), 'msgqueued', 1, 1, 'sent', 2)
2013-07-03 16:09:53 +02:00
helper_sent.insert(t)
toLabel = ''
t = (toAddress,)
shared.sqlLock.acquire()
2013-07-04 08:33:04 +02:00
shared.sqlSubmitQueue.put('''SELECT label FROM addressbook WHERE address=?''')
2013-07-03 16:09:53 +02:00
shared.sqlSubmitQueue.put(t)
queryreturn = shared.sqlReturnQueue.get()
shared.sqlLock.release()
if queryreturn != []:
for row in queryreturn:
toLabel, = row
shared.UISignalQueue.put(('displayNewSentMessage', (toAddress, toLabel, fromAddress, subject, message_as_text, ackdata)))
2013-07-03 16:09:53 +02:00
shared.workerQueue.put(('sendmessage', toAddress))
# TODO - what should we do with ackdata.encode('hex') ?
import sys
smtpd.DEBUGSTREAM = sys.stdout