Forum Programmation.python Un prototype de relais SMTP vers MAPI en python

Posté par (page perso) .
Tags : aucun
1
3
avr.
2009

J'ai voulu écrire ce code suite à la limitation, à mon boulot, de l'envoi de mail via SMTP : c'est restreint aux adresses internes depuis quelques mois. Pour envoyer des mails à l'extérieur, on doit passer par MAPI (et donc Outlook).



C'est un premier jet, qui fonctionne, mais est très limité (un seul destinataire, le corps du message est le source du mail d'origine)



Pour arriver à mes fins, j'ai utilisé un serveur smtp en python et extensible : http://www.hare.demon.co.uk/pysmtp.html



Pour le code de création du message et de l'envoi, j'ai consulté la doc des objets OLE idoines sur MSDN : http://msdn.microsoft.com/fr-fr/library/ms268731(VS.80).aspx



# (c)2009 David SPORN

#
# A brain-dead Python SMTP to MAPI relay server based on the smtps, smtplib and mapi
# packages.
#
# DISCLAIMER
# You are free to use this code in any way you like, subject to the
# Python disclaimers & copyrights. I make no representations about the
# suitability of this software for any purpose. It is provided "AS-IS"
# without warranty of any kind, either express or implied. So there.
#
# Sources
# http://code.activestate.com/recipes/149461/
# smtps.py

"""
smtp2mapi.py -- A very simple & dumb Python SMTP to MAPI relay server. It uses
smtps for the server side and MAPI for the client side. This is
intended for use as a proxy to an Exchange server that is restricted to MAPI
(e.g. for 'security purpose').

This blocks, waiting for RFC821 messages from clients on the given
port. When a complete SMTP message is received, it connect to the
Exchange server using the Outlook OLE component, convert the
message and send it. Obviously, Outlook must be present.

All processing is single threaded. It generally handles errors
badly. It fails especially badly if DNS or the resolved mail host
hangs. DNS or mailhost failures are not propagated back to the client,
which is bad news.

The mail address 'shutdown@shutdown.now' is interpreted
specially. This gets around a Python 1.5/Windows/WINSOCK bug that
prevents this script from being interrupted.
"""

import sys, smtps, string, smtplib, rfc822, StringIO, win32com.client.dynamic, re

#Default Mapi profile
DEFAULT_MAPI_PROFILE = "Outlook"
#


#
# This extends the smtps.SMTPServerInterface and specializes it to
# proxy requests onwards. It uses DNS to resolve each RCPT TO:
# address, then uses smtplib to forward the client mail on the
# resolved mailhost.
#

class SMTPService(smtps.SMTPServerInterface):
def initMapi(self):
self.outlook = win32com.client.dynamic.Dispatch("Outlook.Application")
self.mapi = self.outlook.GetNamespace("MAPI")
self.mapi.Logon(DEFAULT_MAPI_PROFILE)

def quitMapi(self):
self.mapi.Logoff()
self.mapi = None

def __init__(self):
self.savedTo = []
self.savedMailFrom = ''
self.shutdown = 0
self.initMapi()

def mailFrom(self, args):
# Stash who its from for later
self.savedMailFrom = smtps.stripAddress(args)

def rcptTo(self, args):
# Stashes multiple RCPT TO: addresses
self.savedTo.append(args)

def data(self, args):
data = args
sys.stdout.write('Creating message...\n\r')
subject=self.getSubject(args)
message = self.outlook.CreateItem(0)
message.Body = data
message.Subject = subject
for addressee in self.savedTo:
toHost, toFull = smtps.splitTo(addressee)
# Treat this TO address speciallt. All because of a
# WINSOCK bug!
if toFull == 'shutdown@shutdown.now':
self.shutdown = 1
return
sys.stdout.write('Adding recipient ' + toFull + '...')
message.To = toFull
sys.stdout.write('Sending message...\n\r')
message.Send()
self.savedTo = []

def quit(self, args):
if self.shutdown:
print 'Shutdown at user request\n\r'
sys.exit(0)

def frobData(self, data):
hend = string.find(data, '\n\r')
if hend != -1:
rv = data[:hend]
else:
rv = data[:]
rv = rv + 'X-Sporniket: Python SMTP to MAPI Relay'
rv = rv + data[hend:]
return rv

def getSubject(self, data):
hstart = string.find(data, 'Subject: ')
if hstart != -1:
rv = data[hstart+9:]
else:
return ''
hend = string.find(rv, '\n\r')
if hend != -1:
rv = rv[:hend]
else:
rv = rv[:]
return rv

def Usage():
print """Usage pyspy.py port
Where:
port = Client SMTP Port number (ie 25)"""
sys.exit(1)

if __name__ == '__main__':
if len(sys.argv) != 2:
Usage()
port = int(sys.argv[1])
service = SMTPService()
server = smtps.SMTPServer(port)
print 'Python SMTP to MAPI Relay Ready. (c)2009 David SPORN\n\r'
server.serve(service)
  • # 2eme essais

    Posté par (page perso) . Évalué à 1.

    J'ai eu le temps d'améliorer un peu le prototype, en utilisant le package email notamment pour convertir les données brutes du mail en objet facilement manipulable. Concernant la classe de base, j'ai rajouté le comportement correct pour la commande smtp "EHLO" : on renvoie la réponse 502, ce qui indique à Thunderbird que mon serveur est un serveur smtp et non esmtp (Google a été mon ami sur ce coup là, mais j'ai oublié de noter l'url) L'extrait de code, à rajouter après la section traitant la commande "HELO" :
            elif cmd == "EHLO":
                return("502 Command not implemented", keep)
    
    Et voici le code de mon serveur. Celui-ci gère maintenant les destinataire (to, cc, bcc), par contre, pour les mails en multiple partie (pièces jointes, texte html etc) je ne fais que concaténer les partie dont le type de contenu contient "text/". Quoiqu'il en soit, c'est maintenant utilisable pour le besoin de base, l'envoi d'un message ou d'une réponse à plusieurs.
    # (c)2009 David SPORN
    #
    # A brain-dead Python SMTP to MAPI relay server based on the smtps, smtplib and mapi
    # packages.
    #
    # DISCLAIMER
    # You are free to use this code in any way you like, subject to the
    # Python disclaimers & copyrights. I make no representations about the
    # suitability of this software for any purpose. It is provided "AS-IS"
    # without warranty of any kind, either express or implied. So there.
    #
    # Sources 
    # http://code.activestate.com/recipes/149461/
    # smtps.py
    
    """
    smtp2mapi.py -- A very simple & dumb Python SMTP to MAPI relay server. It uses
    smtps for the server side and MAPI for the client side. This is
    intended for use as a proxy to an Exchange server that is restricted to MAPI
    (e.g. for 'security purpose').
    
    This blocks, waiting for RFC821 messages from clients on the given
    port. When a complete SMTP message is received, it connect to the 
    Exchange server using the Outlook OLE component, convert the
    message and send it. Obviously, Outlook must be present.
    
    All processing is single threaded. It generally handles errors
    badly. It fails especially badly if DNS or the resolved mail host
    hangs. DNS or mailhost failures are not propagated back to the client,
    which is bad news.
    
    The mail address 'shutdown@shutdown.now' is interpreted
    specially. This gets around a Python 1.5/Windows/WINSOCK bug that
    prevents this script from being interrupted.
    """
    
    import sys
    import smtps
    import string
    import smtplib
    import email
    import StringIO
    import win32com.client.dynamic
    import re
    
    from email.feedparser import FeedParser
    
    #Default Mapi profile
    DEFAULT_MAPI_PROFILE = "Outlook"
    #
    
    
    #
    # This extends the smtps.SMTPServerInterface and specializes it to
    # proxy requests onwards. It uses DNS to resolve each RCPT TO:
    # address, then uses smtplib to forward the client mail on the
    # resolved mailhost.
    #
    
    class SMTPService(smtps.SMTPServerInterface):
        def initMapi(self):
    	self.outlook = win32com.client.dynamic.Dispatch("Outlook.Application")
    	self.mapi = self.outlook.GetNamespace("MAPI")
    	self.mapi.Logon(DEFAULT_MAPI_PROFILE)
    	    
        def quitMapi(self):
    	self.mapi.Logoff()
    	self.mapi = None
    	    
        def __init__(self):
            self.savedTo = []
            self.savedMailFrom = ''
            self.shutdown = 0
    	self.initMapi()
            
        def mailFrom(self, args):
            # Stash who its from for later
            self.savedMailFrom = smtps.stripAddress(args)
            
        def rcptTo(self, args):
            # Stashes multiple RCPT TO: addresses
            self.savedTo.append(args)
            
        def data(self, args):
            data = args
    	#check for shutdown command
            for addressee in self.savedTo:
                toHost, toFull = smtps.splitTo(addressee)
                # Treat this TO address speciallt. All because of a
                # WINSOCK bug!
                if toFull == 'shutdown@shutdown.now':
                    self.shutdown = 1
                    return
    	    #sys.stdout.write('Adding recipient ' + toFull + '...\n\r')
    	    #recipients.Add(toFull)
    	#
    	#
            message = self.outlook.CreateItem(0)
    	#
    	#data parsing...
    	e_mail = self.getEmailMessage(data)
    	header = self.getMessageRawHeader(args)
            subject=e_mail['Subject']
    	dest_to = e_mail['To']
    	dest_cc = e_mail['Cc']
    	dest_bcc = e_mail['Bcc']
    	#
    	#recipientprocessing
    	recipients = message.Recipients
    	if (None != dest_to):
    	    #print "To :\n\r"+dest_to
    	    self.appendRecipient(recipients, dest_to, 1)
    	if (None != dest_cc):
    	    #print "CC :\n\r"+dest_cc
    	    self.appendRecipient(recipients, dest_cc, 2)
    	if (None != dest_bcc):
    	    #print "BCC :\n\r"+dest_bcc
    	    self.appendRecipient(recipients, dest_bcc, 3)
    	#
    	#subject processing
    	if (None != subject):
    	    message.Subject = subject
    	else:
    	    message.Subject = ""
    	#
    	#message processing
    	if (e_mail.is_multipart()):
    	    #attachements = message.Attachements
    	    #attache each textual parts to the messsage
    	    the_body = ""
    	    for part in e_mail.walk():
    		if (-1 != string.find(part.get_content_type(), 'text/')):
    		    the_body = the_body + part.get_payload()
    	    message.Body = the_body
    	else:
    	    message.Body = e_mail.get_payload()
            sys.stdout.write('Sending message...\n\r')
    	message.Send()
            self.savedTo = []
            
        def quit(self, args):
            if self.shutdown:
                print 'Shutdown at user request\n\r'
                sys.exit(0)
    
        def frobData(self, data):
            hend = string.find(data, '\n\r')
            if hend != -1:
                rv = data[:hend]
            else:
                rv = data[:]
            rv = rv + 'X-Sporniket: Python SMTP to MAPI Relay'
            rv = rv + data[hend:]
            return rv
    
        def getSubject(self, data):
    	hstart =  string.find(data, 'Subject: ')
            if hstart != -1:
                rv = data[hstart+9:]
            else:
                return ''
            hend = string.find(rv, '\n')
            if hend != -1:
                rv = rv[:hend]
            else:
                rv = rv[:]
            return rv
    
        def getMessageRawHeader(self, data):
    	hend =  string.find(data, '\n\r')
            if hend != -1:
                return data[:hend]
            else:
                return ''
    
        def getMessageRawBody(self, data):
    	hstart =  string.find(data, '\n\r')
            if hstart != -1:
                return data[hstart+2:]
            else:
                return ''
    
        def getEmailMessage(self, data):
    	"""
    	instanciate a email.Message from the source data
    	"""
    	parser =  FeedParser()
    	parser.feed(data)
    	return parser.close()
    
        def appendRecipient(self, recipientObject, dataList, recipientType):
    	"""
    	split dataList and add items to recipientObject
    	"""
    	for dest in dataList.split(','):
    	    dest = dest.strip()
    	    recipient = recipientObject.Add(dest.strip())
    	    recipient.Type = recipientType
    	    
    
    
    def Usage():
        print """Usage pyspy.py port
    Where:
      port = Client SMTP Port number (ie 25)"""
        sys.exit(1)
        
    if __name__ == '__main__':
        if len(sys.argv) != 2:
            Usage()
        port = int(sys.argv[1])
        service = SMTPService()
        server = smtps.SMTPServer(port)
        print 'Python SMTP to MAPI Relay Ready. (c)2009 David SPORN\n\r'
        server.serve(service)
    
  • # 3eme version

    Posté par (page perso) . Évalué à 1.

    Cette fois, on a quelque chose de beaucoup plus utilisable : toutes les pièces jointes sont sauvées dans un dossier temporaire à définir, puis réintégrées dans le mail. Le message texte brut et html sont supportés. Le problème est que les images inclus dans le message HTML (disposant d'un Content-ID et sans nom de fichier) ne peuvent pas être embarquée de la même façon, et sont donc jointe comme des fichiers normaux. (J'ai trouvé des exemples de code sur le net, mais les versions récentes des objets COM d'outlook ont l'air d'exposer moins de propriétés, en particulier l'attribut Fields de la classe Attachment, donc ça m'a l'air mort... Un autre code passe par l'objet COM "MAPI.Session" mais comme Thunderbird est mon client mail par défaut, forcément ça ne marche pas.) Deuxième problème, le titre du message n'est pas décodé, et je n'ai pas trouvé encore de solution. Ce n'est pas grave si le message doit aller à l'extérieur, car les destinataire verront le titre correcte, mais c'est gênant lorsque le mail restera sur la messagerie interne.
    # (c)2009 David SPORN
    #
    # A brain-dead Python SMTP to MAPI relay server based on the smtps, smtplib and mapi
    # packages.
    #
    # DISCLAIMER
    # You are free to use this code in any way you like, subject to the
    # Python disclaimers & copyrights. I make no representations about the
    # suitability of this software for any purpose. It is provided "AS-IS"
    # without warranty of any kind, either express or implied. So there.
    #
    # Sources 
    # http://code.activestate.com/recipes/149461/
    # http://www.outlookcode.com/d/code/htmlimg.htm
    # smtps.py
    
    """
    smtp2mapi.py -- A very simple & dumb Python SMTP to MAPI relay server. It uses
    smtps for the server side and MAPI for the client side. This is
    intended for use as a proxy to an Exchange server that is restricted to MAPI
    (e.g. for 'security purpose').
    
    This blocks, waiting for RFC821 messages from clients on the given
    port. When a complete SMTP message is received, it connect to the 
    Exchange server using the Outlook OLE component, convert the
    message and send it. Obviously, Outlook must be present.
    
    All processing is single threaded. It generally handles errors
    badly. It fails especially badly if DNS or the resolved mail host
    hangs. DNS or mailhost failures are not propagated back to the client,
    which is bad news.
    
    The mail address 'shutdown@shutdown.now' is interpreted
    specially. This gets around a Python 1.5/Windows/WINSOCK bug that
    prevents this script from being interrupted.
    """
    
    import os
    import sys
    import errno
    import mimetypes
    import smtps
    import string
    import smtplib
    import email
    import StringIO
    #http://www.developpez.net/forums/d676074/autres-langages/pyt(...)
    import win32com.client.dynamic
    import win32com.client
    from win32com.gen_py import *
    import win32com.gen_py
    
    import re
    import marshal
    import pickle
    
    from email.feedparser import FeedParser
    
    #Default Mapi profile
    DEFAULT_MAPI_PROFILE = "Outlook"
    TEMPORARY_FULL_PATH = 'C:\\dsporn\\software\\smtp\\tmp\\'
    #
    
    
    #
    # This extends the smtps.SMTPServerInterface and specializes it to
    # proxy requests onwards. It uses DNS to resolve each RCPT TO:
    # address, then uses smtplib to forward the client mail on the
    # resolved mailhost.
    #
    
    class SMTPService(smtps.SMTPServerInterface):
        def initMapi(self):
    	self.outlook = win32com.client.dynamic.Dispatch("Outlook.Application")
    	self.mapi = self.outlook.GetNamespace("MAPI")
    	self.mapi_session = self.mapi.Logon(DEFAULT_MAPI_PROFILE)
    	    
        def quitMapi(self):
    	self.mapi.Logoff()
    	self.mapi = None
    	    
        def __init__(self):
            self.savedTo = []
            self.savedMailFrom = ''
            self.shutdown = 0
    	self.initMapi()
            
        def mailFrom(self, args):
            # Stash who its from for later
            self.savedMailFrom = smtps.stripAddress(args)
            
        def rcptTo(self, args):
            # Stashes multiple RCPT TO: addresses
            self.savedTo.append(args)
            
        def data(self, args):
            data = args
    	#check for shutdown command
            for addressee in self.savedTo:
                toHost, toFull = smtps.splitTo(addressee)
                # Treat this TO address speciallt. All because of a
                # WINSOCK bug!
                if toFull == 'shutdown@shutdown.now':
                    self.shutdown = 1
                    return
    	    #sys.stdout.write('Adding recipient ' + toFull + '...\n\r')
    	    #recipients.Add(toFull)
    	#
    	#
            message = self.outlook.CreateItem(0)
    	#
    	#data parsing...
    	e_mail = self.getEmailMessage(data)
    	#header = self.getMessageRawHeader(args)
            subject=e_mail['Subject']
    	dest_to = e_mail['To']
    	dest_cc = e_mail['Cc']
    	dest_bcc = e_mail['Bcc']
    	#
    	#recipientprocessing
    	recipients = message.Recipients
    	if (None != dest_to):
    	    #print "To :\n\r"+dest_to
    	    self.appendRecipient(recipients, dest_to, 1)
    	if (None != dest_cc):
    	    #print "CC :\n\r"+dest_cc
    	    self.appendRecipient(recipients, dest_cc, 2)
    	if (None != dest_bcc):
    	    #print "BCC :\n\r"+dest_bcc
    	    self.appendRecipient(recipients, dest_bcc, 3)
    	#
    	#subject processing
    	if (None != subject):
    	    message.Subject = subject
    	else:
    	    message.Subject = ""
    	#
    	#message processing
    	if (e_mail.is_multipart()):
    	    #attachements = message.Attachements
    	    #attache each textual parts to the messsage
    	    the_body = ""
    	    counter = 1
    	    has_body = False
    	    has_html_body = False
    	    the_html_body = ""
    	    for part in e_mail.walk():
    		if part.get_content_maintype() == 'multipart':
    		    continue
    		filename = part.get_filename()
    		save_part = True
    		has_content_id = False
    		the_content_id = ""
    		#
    		# handle part that have no filename : it might
    		# be the content of the message body and inlined pictures...
    		if not filename:
    		    #
    		    # is it the plain text message body ?
    		    if (False == has_body) and (part.get_content_type() == 'text/plain'):
    			the_body = part.get_payload(decode=True)
    			has_body = True
    			save_part = False
    		    #
    		    # is it the html text message body ?
    		    elif (False == has_html_body) and (part.get_content_type() == 'text/html'):
    			the_html_body = part.get_payload(decode=True)
    			has_html_body = True
    			save_part = False
    		    #
    		    # maybe some media files (or something else)
    		    else:
    			#
    			# Retrieve the content id if any
    			#
    			# Seems to be useless, as the com object seems to not 
    			# expose the Fields attribute of the Attachment class...
    			if (None != part.has_key('Content-ID')):
    			    has_content_id = True
    			    the_content_id = self.getStripedContentId(part['Content-ID'])
    			#
    			# Guess extension
    			ext = mimetypes.guess_extension(part.get_content_type())
    			if not ext:
    		            # Use a generic bag-of-bits extension
    			    ext = '.bin'
    			filename = 'part-%03d%s' % (counter, ext)
    		if (save_part):
    		    sys.stdout.write('Save part ['+filename+']...\n\r')
    		    fp = open(os.path.join('.\\tmp', filename), 'wb')
    		    fp.write(part.get_payload(decode=True))
    		    fp.close()
    		    attached_file = message.Attachments.Add(TEMPORARY_FULL_PATH+filename)
    		    counter += 1
    	    message.Body = the_body
    	    if (has_html_body):
    		message.HTMLBody = the_html_body
    	else:
    	    message.Body = e_mail.get_payload(decode=True)
            sys.stdout.write('Sending message...\n\r')
    	message.Send()
            self.savedTo = []
            
        def quit(self, args):
            if self.shutdown:
                print 'Shutdown at user request\n\r'
                sys.exit(0)
    
        def getEmailMessage(self, data):
    	"""
    	instanciate a email.Message from the source data
    	"""
    	parser =  FeedParser()
    	parser.feed(data)
    	return parser.close()
    
        def appendRecipient(self, recipientObject, dataList, recipientType):
    	"""
    	split dataList and add items to recipientObject
    	"""
    	for dest in dataList.split(','):
    	    dest = dest.strip()
    	    recipient = recipientObject.Add(dest.strip())
    	    recipient.Type = recipientType
    	    
        def getStripedContentId(self, data):
    	hstart =  string.find(data, '<')
            if hstart != -1:
                rv = data[hstart+1:]
            else:
                rv = data[:]
            hend = string.find(rv, '>')
            if hend != -1:
                rv = rv[:hend]
            else:
                rv = rv[:]
            return rv
    
    
    
    def Usage():
        print """Usage pyspy.py port
    Where:
      port = Client SMTP Port number (ie 25)"""
        sys.exit(1)
        
    if __name__ == '__main__':
        if len(sys.argv) != 2:
            Usage()
        port = int(sys.argv[1])
        service = SMTPService()
        server = smtps.SMTPServer(port)
        print 'Python SMTP to MAPI Relay Ready. (c)2009 David SPORN\n\r'
        server.serve(service)
    

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.