This smtp proxy can be used to process any part of the message (header and body). It is also possible to process all the body part just before it is send to the MTA.
The aim of this proxy is to allow a modification of the message on the fly. It had been tested with the postfix before queue content filter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | #!/usr/bin/python2.6
# -*-coding:UTF-8 -*
#import smtpd, smtplib, asyncore
from __future__ import print_function
import re, sys, os, socket, threading, signal
from select import select
import pdb
CRLF="\r\n"
class Server:
def __init__(self, listen_addr, remote_addr):
self.local_addr = listen_addr
self.remote_addr = remote_addr
self.srv_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.srv_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.srv_socket.bind(listen_addr)
self.srv_socket.setblocking(0)
self.please_die = False
self.accepted = {}
def start(self):
self.srv_socket.listen(5)
while not self.please_die:
try:
ready_to_read, ready_to_write, in_error = select([self.srv_socket], [], [], 0.1)
except Exception as err:
pass
if len(ready_to_read) > 0:
try:
client_socket, client_addr = self.srv_socket.accept()
except Exception as err:
print("Problem:", err)
else:
print("Connection from {0}:{1}".format(client_addr[0], client_addr[1]))
tclient = ThreadClient(self, client_socket, self.remote_addr)
tclient.start()
self.accepted[tclient.getName()] = tclient
def die(self):
"""Used to kill the server (joining threads, etc...)
"""
print("Killing all client threads...")
self.please_die = True
for tc in self.accepted.values():
tc.die()
tc.join()
class ThreadClient(threading.Thread):
"""This class is used to manage a 'client to final SMTP server' connection.
It is the 'Proxy' part of the program
"""
(MAIL_DIALOG, MSG_HEADER, MSG_BODY) = range(3)
def __init__(self, serv, conn, remote_addr):
threading.Thread.__init__(self)
self.server = serv
self.local = conn
self.remote_addr = remote_addr
self.remote = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.please_die = False
self.mbuffer = []
self.msg_state = ThreadClient.MAIL_DIALOG
def recv_body_line(self, line):
"""Each line of the body is received here and can be processed, one by one.
A typical behaviour should be to send it immediatly... or keep all the
body until it reaches the end of it, and then process it and finally,
send it.
Body example:
Hello foo !
blabla
"""
mline = "{0}{1}".format(line, CRLF)
print("B>", line)
self.mbuffer.append(line)
def flush_body(self):
"""This method is called when the end of body (matched with a single
dot (.) on en empty line is encountered. This method is useful if you
want to process the whole body.
"""
for line in self.mbuffer:
mline = "{0}{1}".format(line, CRLF)
print("~B>", mline)
self.remote.send(mline.encode())
# Append example:
#toto = "---{0}{0}Un peu de pub{0}".format(CRLF)
#self.remote.send(toto.encode())
def recv_header_line(self, line):
"""All header lines (subject, date, mailer, ...) are processed here.
"""
mline = "{0}{1}".format(line, CRLF)
print("H>", line)
self.remote.send(mline.encode())
def recv_server_dialog_line(self, line):
"""All 'dialog' lines (which are mail commands send by the mail client
to the MTA) are processed here.
Dialog example:
MAIL FROM: foo@bar.tld
"""
mline = "{0}{1}".format(line, CRLF)
print(">>", line)
self.remote.send(mline.encode())
def run(self):
"""Here is the core of the proxy side of this script:
For each line sent by the Mail client to the MTA, split it on the CRLF
character, and then:
If it is a DOT on an empty line, call the 'flush_body()' method
else, if it matches 'DATA' begin to process the body of the message,
else:
if we're processing the header, give each line to the
'recv_header_line()' method,
else if we're processing the 'MAIL DIALOG' give the line to the
'recv_server_dialog_line()' method.
else, consider that we're processing the body and give each line
to the 'recv_body_line()' method,
"""
self.remote.connect(self.remote_addr)
self.remote.setblocking(0)
while not self.please_die:
# Check if the client side has something to say:
ready_to_read, ready_to_write, in_error = select([self.local], [], [], 0.1)
if len(ready_to_read) > 0:
try:
msg = self.local.recv(1024)
except Exception as err:
print(str(self.getName()) + " > " + str(err))
break
else:
dmsg = msg.decode()
if dmsg != "":
dmsg = dmsg.strip(CRLF)
for line in dmsg.split(CRLF):
mline = "{0}{1}".format(line,CRLF)
if line != "":
if line == "DATA":
# the 'DATA' string means: 'BEGINNING of the # MESSAGE { HEADER + BODY }
self.msg_state = ThreadClient.MSG_HEADER
self.remote.send(mline.encode())
elif line == ".":
# a signle dot means 'END OF MESSAGE { HEADER+BODY }'
self.msg_state = ThreadClient.MAIL_DIALOG
self.flush_body()
self.remote.send(mline.encode())
else:
# else, the line can be anything and its
# signification depend on the part of the
# whole dialog we're processing.
if self.msg_state == ThreadClient.MSG_HEADER:
self.recv_header_line(line)
elif self.msg_state == ThreadClient.MAIL_DIALOG:
self.recv_server_dialog_line(line)
else:
self.recv_body_line(line)
else:
# Probably the most important: An empty line
# inside the { HEADER + BODY } part of the
# message means we're done with the 'HEADER'
# part and we're beginning the BODY part.
self.msg_state = ThreadClient.MSG_BODY
else:
break
# Check if the server side has something to say:
ready_to_read, ready_to_write, in_error = select([self.remote], [], [], 0.1)
if len(ready_to_read) > 0:
try:
msg = self.remote.recv(1024)
except Exception as err:
print(str(self.getName()) + " > " + str(err))
break
else:
dmsg = msg.decode()
if dmsg != "":
print("<< {0}".format(repr(msg.decode())))
self.local.send(dmsg.encode())
else:
break
self.remote.close()
self.local.close()
self.server.accepted.pop(self.getName())
def die(self):
self.please_die = True
srv = Server(("127.0.0.1", 10025), ("127.0.0.1", 10026))
def die(signum, frame):
global srv
srv.die()
signal.signal(signal.SIGINT, die)
signal.signal(signal.SIGTERM, die)
srv.start()
|
Hello,
I would like to test this script with postfix MTA. Could you pleace help me set up the postfix configuration part in order to redirect the incomming emails to your python script?
I found this information about how to redirect mails to an script : http://www.iredmail.org/docs/pipe.incoming.email.for.certain.user.to.external.script.html
Still one more question: Which python commands should I use to refuse emails after scanning and which command to use to let the email fellow his normal postfix way.
Thank you in advance for your assistance ;-)