2013-07-04 08:57:39 +02:00
from cStringIO import StringIO
2013-07-03 16:09:53 +02:00
from pyelliptic . openssl import OpenSSL
2013-07-03 18:15:42 +02:00
import asynchat
import base64
2013-07-10 13:10:54 +02:00
from email import parser , generator , utils
2013-07-03 18:15:42 +02:00
import errno
2013-07-03 16:09:53 +02:00
import shared
import smtpd
2013-07-03 18:15:42 +02:00
import socket
2013-07-03 17:30:06 +02:00
import ssl
2013-07-03 16:09:53 +02:00
import time
from addresses import *
import helper_sent
2013-07-03 18:15:42 +02:00
# 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
2013-07-03 18:15:42 +02:00
self . __mailfrom = None
self . __rcpttos = [ ]
self . __data = ' '
self . __fqdn = socket . getfqdn ( )
self . __version = ' Python SMTP proxy version 0.2a '
2013-07-05 09:42:41 +02:00
self . __invalid_command_count = 0
2013-07-03 18:15:42 +02:00
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 )
2013-07-03 18:15:42 +02:00
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 )
2013-07-05 09:42:41 +02:00
# 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 )
2013-07-03 18:15:42 +02:00
# 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 )
2013-07-03 18:15:42 +02:00
self . __line = [ ]
if self . __state == self . COMMAND :
if not line :
2013-07-05 09:42:41 +02:00
self . invalid_command ( ' 500 Error: bad syntax ' )
2013-07-03 18:15:42 +02:00
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 :
2013-07-05 09:42:41 +02:00
self . invalid_command ( ' 502 Error: command " %s " not implemented ' % command )
2013-07-03 18:15:42 +02:00
return
method ( arg )
return
else :
if self . __state != self . DATA :
2013-07-05 09:42:41 +02:00
self . invalid_command ( ' 451 Internal confusion ' )
2013-07-03 18:15:42 +02:00
return
2013-07-05 11:02:00 +02:00
2013-07-03 18:15:42 +02:00
# 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 )
2013-07-05 11:02:00 +02:00
2013-07-03 18:15:42 +02:00
self . __data = ' \n ' . join ( data )
2013-07-05 11:02:00 +02:00
try :
status = self . __server . process_message ( self . __peer ,
self . address ,
self . __rcpttos ,
self . __data )
except Exception , e :
2013-07-05 13:22:52 +02:00
status = ' 554 Requested transaction failed: {} ' . format ( str ( e ) )
2013-07-05 11:02:00 +02:00
2013-07-03 18:15:42 +02:00
self . __rcpttos = [ ]
self . __mailfrom = None
self . __state = self . COMMAND
self . set_terminator ( ' \r \n ' )
if not status :
self . push ( ' 250 Ok ' )
else :
2013-07-05 11:02:00 +02:00
self . invalid_command ( status )
2013-07-03 18:15:42 +02:00
# 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
2013-07-03 18:15:42 +02:00
def smtp_EHLO ( self , arg ) :
if not arg :
2013-07-05 09:42:41 +02:00
self . invalid_command ( ' 501 Syntax: EHLO hostname ' )
2013-07-03 18:15:42 +02:00
return
2013-07-05 11:19:38 +02:00
if self . __greeting is not None :
2013-07-05 09:42:41 +02:00
self . invalid_command ( ' 503 Duplicate HELO/EHLO ' )
return
2013-07-03 18:15:42 +02:00
else :
self . __greeting = arg
self . push ( ' 250- %s offers: ' % self . __fqdn )
self . push ( ' 250 AUTH PLAIN ' )
def smtp_HELO ( self , arg ) :
if not arg :
2013-07-05 09:42:41 +02:00
self . invalid_command ( ' 501 Syntax: HELO hostname ' )
2013-07-03 18:15:42 +02:00
return
2013-07-05 11:19:38 +02:00
if self . __greeting is not None :
2013-07-05 09:42:41 +02:00
self . invalid_command ( ' 503 Duplicate HELO/EHLO ' )
2013-07-03 18:15:42 +02:00
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
2013-07-05 10:11:38 +02:00
try :
encoding , pw = arg . split ( ' ' )
except ValueError :
self . invalid_command ( ' 501 Syntax: AUTH PLAIN auth ' )
return
2013-07-03 18:15:42 +02:00
if encoding != ' PLAIN ' :
2013-07-05 13:22:52 +02:00
self . invalid_command ( ' 502 method not understood ' )
2013-07-03 18:15:42 +02:00
return
2013-07-04 11:51:24 +02:00
2013-07-05 09:35:38 +02:00
try :
z , username , pw = base64 . b64decode ( pw ) . split ( ' \x00 ' )
except :
z = ' error '
2013-07-03 18:15:42 +02:00
if z != ' ' :
2013-07-05 13:22:52 +02:00
self . invalid_command ( ' 501 authorization not understood ' )
2013-07-03 18:15:42 +02:00
return
2013-07-12 07:09:50 +02:00
if ' @ ' in username :
username , _ = username . split ( ' @ ' , 1 )
2013-07-04 21:41:07 +02:00
2013-07-12 07:09:50 +02:00
self . address = username
with shared . printLock :
print ' Login request from {} ' . format ( self . address )
2013-07-03 18:15:42 +02:00
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 ) )
2013-07-03 18:15:42 +02:00
# 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
2013-07-03 18:15:42 +02:00
except :
pass
2013-07-03 18:21:04 +02:00
2013-07-05 09:42:41 +02:00
self . invalid_command ( ' 530 Access denied. ' )
2013-07-03 18:15:42 +02:00
def smtp_NOOP ( self , arg ) :
if arg :
2013-07-05 09:42:41 +02:00
self . invalid_command ( ' 501 Syntax: NOOP ' )
2013-07-03 18:15:42 +02:00
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
2013-07-05 09:28:52 +02:00
2013-07-03 18:15:42 +02:00
address = self . __getaddr ( ' FROM: ' , arg ) if arg else None
2013-07-12 07:09:50 +02:00
if not address or ' @ ' not in address :
2013-07-05 09:42:41 +02:00
self . invalid_command ( ' 501 Syntax: MAIL FROM: <address> ' )
2013-07-03 18:15:42 +02:00
return
2013-07-05 09:28:52 +02:00
2013-07-03 18:15:42 +02:00
if self . __mailfrom :
2013-07-05 09:42:41 +02:00
self . invalid_command ( ' 503 Error: nested MAIL command ' )
2013-07-03 18:15:42 +02:00
return
2013-07-05 09:28:52 +02:00
if not self . logged_in :
2013-07-05 09:42:41 +02:00
self . invalid_command ( ' 503 Not authenticated. ' )
2013-07-05 09:28:52 +02:00
return
2013-07-12 07:09:50 +02:00
localPart , _ = address . split ( ' @ ' , 1 )
if self . address != localPart :
2013-07-05 13:22:52 +02:00
self . invalid_command ( ' 530 Access denied: address must be the same as the authorized account ' )
2013-07-04 08:33:04 +02:00
return
2013-07-05 09:28:52 +02:00
2013-07-03 18:15:42 +02:00
self . __mailfrom = address
2013-07-10 12:02:45 +02:00
with shared . printLock :
print >> smtpd . DEBUGSTREAM , ' sender: ' , self . __mailfrom
2013-07-03 18:15:42 +02:00
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
2013-07-03 18:15:42 +02:00
if not self . __mailfrom :
2013-07-05 09:42:41 +02:00
self . invalid_command ( ' 503 Error: need MAIL command ' )
2013-07-03 18:15:42 +02:00
return
2013-07-05 09:28:52 +02:00
if not self . logged_in :
# This will never happen. :)
2013-07-05 09:42:41 +02:00
self . invalid_command ( ' 503 Not authenticated. ' )
2013-07-05 09:28:52 +02:00
return
2013-07-03 18:15:42 +02:00
address = self . __getaddr ( ' TO: ' , arg ) if arg else None
if not address :
2013-07-05 13:22:52 +02:00
self . invalid_command ( ' 501 Syntax: RCPT TO: <user@address> ' )
return
try :
2013-07-12 07:09:50 +02:00
localPart , dom = address . split ( ' @ ' , 1 )
2013-07-05 13:22:52 +02:00
except ValueError :
self . invalid_command ( ' 501 Syntax: RCPT TO: <user@address> ' )
2013-07-03 18:15:42 +02:00
return
2013-07-12 07:09:50 +02:00
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
2013-07-03 18:15:42 +02:00
self . push ( ' 250 Ok ' )
def smtp_RSET ( self , arg ) :
if arg :
2013-07-05 09:42:41 +02:00
self . invalid_command ( ' 501 Syntax: RSET ' )
2013-07-03 18:15:42 +02:00
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 :
2013-07-05 09:42:41 +02:00
self . invalid_command ( ' 503 Not authenticated. ' )
2013-07-03 18:15:42 +02:00
return
if not self . __rcpttos :
2013-07-05 09:42:41 +02:00
self . invalid_command ( ' 503 Error: need RCPT command ' )
2013-07-03 18:15:42 +02:00
return
if arg :
2013-07-05 09:42:41 +02:00
self . invalid_command ( ' 501 Syntax: DATA ' )
2013-07-03 18:15:42 +02:00
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 ' )
2013-07-03 17:30:06 +02:00
self . ssl = shared . config . getboolean ( ' bitmessagesettings ' , ' smtpssl ' )
if self . ssl :
self . keyfile = shared . config . get ( ' bitmessagesettings ' , ' keyfile ' )
self . certfile = shared . config . get ( ' bitmessagesettings ' , ' certfile ' )
2013-07-06 13:19:36 +02:00
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
2013-07-03 17:30:06 +02:00
def handle_accept ( self ) :
# Override SMTPServer's handle_accept so that we can start an SSL connection.
sock , peer_address = self . accept ( )
2013-07-03 18:15:42 +02:00
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 )
2013-07-03 17:30:06 +02:00
2013-07-10 12:00:26 +02:00
@staticmethod
def stripMessageHeaders ( message ) :
2013-07-12 07:09:50 +02:00
# 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 )
2013-07-10 12:00:26 +02:00
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 "
2013-07-10 12:00:26 +02:00
headersToStrip = [ x . strip ( ) for x in headersToStrip . split ( ' , ' ) if len ( x . strip ( ) ) > 0 ]
for h in headersToStrip :
if h in message :
del message [ h ]
2013-07-12 07:09:50 +02:00
def process_message ( self , peer , fromAddress , rcpttos , data ) :
2013-07-03 16:09:53 +02:00
#print("Peer", peer)
2013-07-12 07:09:50 +02:00
#print("Mail From", fromAddress)
2013-07-03 16:09:53 +02:00
#print("Rcpt To", rcpttos)
#print("Data")
#print(data)
#print('--------')
2013-07-12 07:09:50 +02:00
#print(type(fromAddress))
2013-07-03 16:09:53 +02:00
2013-07-04 08:57:39 +02:00
message = parser . Parser ( ) . parsestr ( data )
2013-07-08 17:07:59 +02:00
message [ ' X-Bitmessage-Sending-Version ' ] = shared . softwareVersion
2013-07-12 07:09:50 +02:00
message [ ' X-Bitmessage-Flags ' ] = ' 0 '
2013-07-04 08:57:39 +02:00
2013-07-10 12:00:26 +02:00
bitmessageSMTPServer . stripMessageHeaders ( message )
2013-07-04 08:57:39 +02:00
fp = StringIO ( )
gen = generator . Generator ( fp , mangle_from_ = False , maxheaderlen = 128 )
gen . flatten ( message )
2013-07-12 07:09:50 +02:00
2013-07-04 08:57:39 +02:00
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 )
2013-07-04 08:57:39 +02:00
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 :
2013-07-12 07:09:50 +02:00
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 )
2013-07-05 11:02:00 +02:00
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.
2013-07-04 08:57:39 +02:00
# Last 2 bytes are two bytes of the sha256 checksum of message
2013-07-12 07:09:50 +02:00
if ' Subject ' in message :
subject = message [ ' Subject ' ]
else :
subject = ' '
2013-07-03 16:09:53 +02:00
ackdata = OpenSSL . rand ( 32 )
2013-07-04 08:57:39 +02:00
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
2013-07-04 08:57:39 +02:00
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') ?
2013-07-03 18:15:42 +02:00
import sys
smtpd . DEBUGSTREAM = sys . stdout