Wednesday, February 23, 2011

Outlook 2010 talking to Exchange 2000 via SMTP.

After some requests in my personal e-mail, I've decided to explain here the steps needed to make Outlook 2010 work with an SMTP Exchange 2000 server.
(Sorry my english, as is not my natural language)

The case goes back to January, when I needed to configure Outlook 2010 to work with the company's server that is based in a different country. This is a little important detail, as the foreign IT department wouldn't be any helpfull. The server was an Exchange 2000 but all the clients are working via SMTP and POP.
If you need to connect O2010 to Exchange 2000 using Exchange's protocol, please stop reading right here.
So, I knew that e-mails could be received, but by all the means, they just won't go out.
I grabbed a copy of Wireshark and started to analyse what's wrong in here.
Long story short:
...the server tells the client what authentication protocols it supports, however Outlook 2010 wants to use DIGEST-MD5.
This is the main difference between Outlook Express (which works great) and Outlook 2010:

Outlook Express:
HELO machine
AUTH LOGIN
username base64 encoded
password base64 encoded
mails go through.
Outlook 2010:
HELO machine
AUTH DIGEST-MD5
response from server
Outlook sends just a *
AUTH LOGIN
password base64 encoded

And it stops right here, no auth, no e-mails to the outside world :)
So, if this is just a little glitch in the matrix, we'll fix it with a python script.
This script, is just a bad re-writen code of a transparent proxy, we're you can mess around with the messages sent between server<->client.
My biggest thanks go to:
Dirk Holtwick (INI reader)
Lobsang (Python Proxy) @ ActiveState
They sure saved me a lot of work by not re-inventing the wheel myself.
So this is the script:

# -*- coding: utf-8 -*-


#############################################################################################################
###  Filipe Polido - Trigenius 2011 - Thanks to: "Dirk Holtwick (INI reader); Lobsang (SMTP Proxy); ActiveState ###
#############################################################################################################


from __future__ import print_function
import re, sys, os, socket, threading, signal
from select import select
import pdb
import ConfigParser
import string


_configuracao = {
    "config.endlocal": "127.0.0.1",
    "config.prtlocal": 25,
    "config.endremoto": "123.123.123.123",
    "config.prtremoto": 25,
    "srv2clt.msgoriginal": "AUTH PLAIN LOGIN",
"srv2clt.msgalterada": "AUTH PLAIN LOGIN",
    "clt2srv.msgoriginal": "MAIL FROM",
"clt2srv.msgalterada": "EMAIL FROM"
    }


CRLF="\r\n"


############################################################################################################
########################## NAO MEXER A PARTIR DAQUI.. #######################################################
############################################################################################################


def LoadConfig(file, config={}):
    config = config.copy()
    cp = ConfigParser.ConfigParser()
    cp.read(file)
    for sec in cp.sections():
        name = string.lower(sec)
        for opt in cp.options(sec):
            config[name + "." + string.lower(opt)] = string.strip(cp.get(sec, opt))
    return config


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(1)


        self.please_die = False


        self.accepted = {}


    def start(self):
        print("#########################################################################")
        print("         Trigenius - Transparent Proxy - Filipe Polido 2011")
        print("#########################################################################")
        print("NAO PODE PARAR ESTE PROCESSO! CASO CONTRARIO O OUTLOOK 2010 NAO FUNCIONA!")
        print("#########################################################################")
        print("mycfg actual:")
        print("Endere├žo local.....: "+mycfg['config.endlocal']+":"+str(mycfg['config.prtlocal']))
        print("Endere├žo remoto....: "+mycfg['config.endremoto']+":"+str(mycfg['config.prtremoto']))
        print("#### Servidor -> Cliente ####")
        print("String original....: "+mycfg['srv2clt.msgoriginal'])
        print("String alterada....: "+mycfg['srv2clt.msgalterada'])
        print("#### Cliente -> Servidor ####")
        print("String original....: "+mycfg['clt2srv.msgoriginal'])
        print("String alterada....: "+mycfg['clt2srv.msgalterada'])
        print("#########################################################################")
        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("ERRO:", err)
                else:
                    #print("Ligado {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):
        print("AVISO: Terminou este processo. O envio de mails no Outlook 2010 deixa de funcionar!!")
        self.please_die = True
        for tc in self.accepted.values():
            tc.die()
            tc.join()


class ThreadClient(threading.Thread):
    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 = []


    def run(self):
        self.remote.connect(self.remote_addr)
        self.remote.setblocking(1)
        while not self.please_die:
            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("ERRO: " + str(self.getName()) + " > " + str(err))
                    break
                else:
                    magiaremote = msg.replace(mycfg['clt2srv.msgoriginal'], mycfg['clt2srv.msgalterada'], 1)
                    self.remote.send(magiaremote)


            # ver se o servidor tem algo a dizer
            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("ERRO: " + str(self.getName()) + " > " + str(err))
                    break
                else:
                    magia = msg.replace(mycfg['srv2clt.msgoriginal'],mycfg['srv2clt.msgalterada'], 1)
                    if magia != "":
                        #print("<< {0}".format(repr(msg)))
                        self.local.send(magia)
                    else:
                        break


        self.remote.close()
        self.local.close()
        self.server.accepted.pop(self.getName())


    def die(self):
        self.please_die = True


####### INICIO
if not os.path.exists('config.ini'):
    print("ERRO: Falta o ficheiro CONFIG.INI")
    sys.exit()
mycfg = LoadConfig("config.ini", _configuracao)
srv = Server((mycfg['config.endlocal'], int(mycfg['config.prtlocal'])), (mycfg['config.endremoto'], int(mycfg['config.prtremoto'])))
def die(signum, frame):
    global srv
    srv.die()


signal.signal(signal.SIGINT, die)
signal.signal(signal.SIGTERM, die)
srv.start()

And this is the config file:

[config]
endlocal=127.0.0.1
prtlocal=25
endremoto=mail.mydomain.pt
prtremoto=25
[srv2clt]
msgoriginal=AUTH PLAIN CRAM-MD5 LOGIN DIGEST-MD5
msgalterada=AUTH PLAIN LOGIN
[clt2srv]
msgoriginal=FOOBAR_SMTP
msgalterada=FOOBAR_SMTP

Sorry, some pieces are in Portuguese, but the code is self-explainatory.
In the config file, you have 2 sections, on srv2clt it processes all the messages from server to client and changes what is need to make Exchange 2000 work with Outlook 2010 (Auth strings)
The clt2srv isn't really needed, but, while I was at it, I thought this could come in handy sometime.
So... now, you have to run the script at localhost or any other server, and tell Outlook to use THAT server as the SMTP server, also, change the config.ini to meet your real SMTP server.
I've also added the port option, just in case.
My costumer uses only windows machines *sigh*, so I've converted python script to a Windows executable and used a tutorial (easy to find online) to make it work like a windows service.
If you have any questions or it doesn't work out for you, just send a e-mail or comment, I'll try to help as soon as possible.