Commits

Greg Ward committed 8070356

Control Exim directly via pipes so we can ditch the netcat hack.

Comments (0)

Files changed (1)

src/eximunit/smtp.py

+import sys
 import re
 import os
 import time
         """Open the connection and say HELO (implicit for now).
         Theoretically you could override this method to sort out a Python
         SMTP session to any server, not just Exim."""
-        self.smtp = EximDebugSMTP(from_host=self.from_ip, debug=self.debug)
+        self.smtp = run_exim(self.from_ip, self.debug)
         self.smtp.helo()        # TODO optional support for ESMTP?
 
     def close(self):
     return results
 
 
+def run_exim(from_ip, debug_level):
+    cmd = ['/usr/sbin/exim4', '-bhc', from_ip]
+    if debug_level > 0:
+        print('Spawning exim test server: %s' % ' '.join(cmd))
+    err_log = open('exim-stderr.log', 'w')
+    try:
+        child = subprocess.Popen(
+            cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=err_log)
+
+        smtp = PipeSMTP(child.stdin, FilteredFile(child.stdout))
+        smtp.set_debuglevel(debug_level)
+        return smtp
+    finally:
+        err_log.close()
+
+
+class PipeSMTP(smtplib.SMTP):
+    """SMTP client class that communicates over a pipe to a child process
+    (exim -bh) rather than a TCP socket."""
+    def __init__(self, child_stdin, child_stdout):
+        self.child_stdin = child_stdin
+        self.child_stdout = child_stdout
+        self.file = self.child_stdout   # to fool getreply()
+
+        smtplib.SMTP.__init__(self)
+
+        # copied from superclass __init__()
+        (code, msg) = self.connect()
+        if code != 220:
+            raise smtplib.SMTPConnectError(code, msg)
+
+    def connect(self):
+        # copied from superclass connect()
+        (code, msg) = self.getreply()
+        if self.debuglevel > 0:
+            print >> stderr, "connect:", msg
+        return (code, msg)
+
+    def close(self):
+        self.child_stdin.close()
+        self.child_stdout.close()
+        self.file = None
+
+    def send(self, str):
+        if self.debuglevel > 0:
+            print >> sys.stderr, 'send:', repr(str)
+        try:
+            self.child_stdin.write(str)
+        except IOError as err:
+            raise smtplib.SMTPServerDisconnected(
+                "error writing to server: %s" % (err,))
+
+
+class FilteredFile(object):
+    """A readable file-like object that suppresses certain input
+    patterns, namely the non-SMTP strings that "exim -bh" writes to
+    stdout. These are there for a human user and just confuse smtplib."""
+    def __init__(self, file):
+        self.file = file
+        self.close = self.file.close
+
+    def readline(self):
+        line = self.file.readline()
+        while line in ('\n', '\r\n') or line.startswith('**** '):
+            line = self.file.readline()
+        return line
+
+
 class EximDebugSMTP(smtplib.SMTP):
     """SMTP client which is backed by a fake Exim SMTP session."""
 
             pass
         s.recv(2)               # two newlines after 'not for real!'
         return s
+
+if __name__ == '__main__':
+    smtp = run_exim('127.0.0.1', 1)
+    smtp.helo('localhost')
+    smtp.mail('foo@bar')
+    smtp.rcpt('ding@dong')
+    smtp.rset()
+    smtp.quit()