Welcome, guest | Sign In | My Account | Store | Cart

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

Python, 205 lines
  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()

1 comment

Dieter 6 years, 3 months ago  # | flag

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 ;-)