einfacher SMTP-Server

Sockets, TCP/IP, (XML-)RPC und ähnliche Themen gehören in dieses Forum
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Ich möchte einen einfachen SMTP-Server schreiben, der E-Mails aus dem lokalen Netzwerk empfängt und dann folgendes macht:

- E-Mails ohne Anhang werden einfach ignoriert (gelöscht)
- E-Mails mit Anhang werden weiter verarbeitet: Anhang extrahieren und als Datei auf der Festplatte speichern; anschließend ursprüngliche E-Mail verwerfen (löschen)

Ich komme mit dem Modul smtpd bzw. der extrem kurzen Dokumentation nicht klar.

Habe hier auch noch ein Beispiel namens SMTP Mailsink gefunden, aber das macht erstens was anderes und zweitens steige ich nicht richtig durch.

Hat jemand ein paar hilfreiche Tipps?
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Ich hab mich durchgewühlt und bin nun ein Stück weiter. Der SMTP-Server steht und empfängt Mails. Die Methode smtpd.SMTPServer.process_message() sieht bei mir erstmal so aus:

Code: Alles auswählen

def process_message(self, peer, mailfrom, rcpttos, data):
	print peer, mailfrom, rcpttos, data
Und sie gibt aus:
D:\#devroot\Python-Tools\SMTPServer>C:\Programme\Python24\python.exe mail2file.p
y
server started
('127.0.0.1', 1428) from@domain.de ['to@domain.de'] Message-ID: <474D2808.
8040707@domain.de>
Date: Wed, 28 Nov 2007 09:34:16 +0100
From: droptix <from@domain.de>
User-Agent: Thunderbird 2.0.0.9 (Windows/20071031)
MIME-Version: 1.0
To: SMTPServer <to@domain.de>
Subject: Betreff
Content-Type: multipart/mixed;
boundary="------------080205020705070304070307"

This is a multi-part message in MIME format.
--------------080205020705070304070307
Content-Type: text/plain; charset=ISO-8859-15; format=flowed
Content-Transfer-Encoding: 7bit

Text

--------------080205020705070304070307
Content-Type: application/x-shockwave-flash;
name="test_attachment.swf"
Content-Transfer-Encoding: base64
Content-Disposition: inline;
filename="test_attachment.swf"

Q1dTBsg9AAB4nN27dVxVzdc4Ovtw6O4SOHR3q0gJ0pLSSHd3KQdUBMSgU0EQkFS64UgoUtLS
[...]
wQ2u/2DK3yEY3ZHWBeDmhWkxvf9fq/sWhA==
--------------080205020705070304070307--
Ich möchte nun den Anhang extrahieren und ihn unter seinem urprünglichen Dateinamen speichern. Außerdem benötige ich die Empfänger-E-Mail-Adresse.

Ideen dazu?
Benutzeravatar
gerold
Python-Forum Veteran
Beiträge: 5555
Registriert: Samstag 28. Februar 2004, 22:04
Wohnort: Oberhofen im Inntal (Tirol)
Kontaktdaten:

Hallo droptix!

Mit dem mail-Modul kannst du Emails auseinander nehmen.

mfg
Gerold
:-)
http://halvar.at | Kleiner Bascom AVR Kurs
Wissen hat eine wunderbare Eigenschaft: Es verdoppelt sich, wenn man es teilt.
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Hat prima funktioniert. Du meintest das Modul email.

Für alle die es interessiert, ich hab es so gelöst:

Code: Alles auswählen

import email

# [...]

class MyClass(smtpd.SMTPServer):

# [...]

	def process_message(self, peer, mailfrom, rcpttos, data):
		print "%s Getting email message from %s to %s" % (time.strftime("%Y/%m/%d %H:%M:%S"), mailfrom, ", ".join(rcpttos))
		# parse email data
		msg = email.message_from_string(data)
		if msg.is_multipart():
			# extract attachments
			for part in msg.walk():
				filename = part.get_filename()
				if filename:
					attachment = part.get_payload()
					# write attachment
					for r in recipients:
						fp = file(filename, "wb")
						fp.write(attachment.decode("base64"))
						fp.close()
Wäre noch cool, wenn man die Decodierung "base64" automatisch erkennt... obwohl kann es denn sein dass bei Dateianhängen eine andere Codierung in Frage kommt?
Benutzeravatar
gerold
Python-Forum Veteran
Beiträge: 5555
Registriert: Samstag 28. Februar 2004, 22:04
Wohnort: Oberhofen im Inntal (Tirol)
Kontaktdaten:

droptix hat geschrieben:Du meintest das Modul email.
:oops: sorry, die Hand war schneller als das Hirn.

Code: Alles auswählen

attachment.get("Content-Transfer-Encoding")
mfg
Gerold
:-)
http://halvar.at | Kleiner Bascom AVR Kurs
Wissen hat eine wunderbare Eigenschaft: Es verdoppelt sich, wenn man es teilt.
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Ah, danke! Werd ich dann gleich testen...

Nochwas anderes: ich würde das kleine Tool gern als Dienst laufen lassen. Unter Unix muss es also die Parameter "start", "restart" und "stop" kennen. Wenn ich das Tool starte, dann wird es ja nie beendet... weil es via Thread in einem Server-Loop hängt.

Wie könnte ich am besten an diese Problematik rangehen? Gibt's dazu gute Literatur/Tutorials im Netz?
Benutzeravatar
gerold
Python-Forum Veteran
Beiträge: 5555
Registriert: Samstag 28. Februar 2004, 22:04
Wohnort: Oberhofen im Inntal (Tirol)
Kontaktdaten:

droptix hat geschrieben:Unter Unix muss es also die Parameter "start", "restart" und "stop" kennen.
Hallo droptix!

Nein. Nicht dein Programm kennt diese Parameter, sondern das Init-Skript das für dein Programm zuständig ist. Dieses Init-Skript (sieh dir mal /etc/init.d/skeleton an) reagiert dann auf die übergebenen Parameter ("start", "stop",...) und sendet an dein Programm ein Signal. Dein Programm reagiert dann auf dieses Signal. Das übermitteln des Signals übernimmt oft das Programm "start-stop-daemon". Dafür ist es ideal, wenn dein Programm ein PID-File mit der eigenen Prozessnummer hinterlegt. Diese wird dann verwendet um ein Signal an dein Programm zu schicken.

- http://docs.python.org/lib/module-signal.html
- man kill
- man start-stop-daemon

mfg
Gerold
:-)
http://halvar.at | Kleiner Bascom AVR Kurs
Wissen hat eine wunderbare Eigenschaft: Es verdoppelt sich, wenn man es teilt.
Leonidas
Python-Forum Veteran
Beiträge: 16025
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

Die für Daemons wichtigen Signale wären vor allem SIGTERM (exit gracefully) und SIGHUP (reload). Man könnte auch SIGKILL dazuzählen, aber da ein Prozess sowieso nicht auf SIGKILL reagieren kann ist es nicht sonderlich wichtig..
My god, it's full of CARs! | Leonidasvoice vs (former) Modvoice
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

gerold hat geschrieben:[Dieses Init-Skript (sieh dir mal /etc/init.d/skeleton an) reagiert dann auf die übergebenen Parameter ("start", "stop",...) und sendet an dein Programm ein Signal. Dein Programm reagiert dann auf dieses Signal.
Hab ein Verständnisproblem: Wie kann denn das "isoliert" laufende Programm vom Init-Skript angesprochen werden und darauf reagieren? Ich hätte mir es jetzt wieder nur via SimpleXMLRPCServer (oder eine Art Server basierend auf Sockets) vorstellen können, aber damit schießt man ja gleich mit Kanonen auf Spatzen... Oder man liest ständig eine Datei ein und erwartet dort drin ein Signal, was die Performance sicher beeinträchtigt.

Kann man sagen: "Schicke Befehl an den Prozess mit der Nummer X"? Und das Programm hinter dem Prozess X kann das Signal empfangen?
monocult
User
Beiträge: 37
Registriert: Donnerstag 31. März 2005, 09:55
Wohnort: hennef
Kontaktdaten:

hab selbst so was gesucht bin aber noch nicht dazu gekommen es zu testen.

demon + init.d script

http://homepage.hispeed.ch/py430/python/daemon.py
http://homepage.hispeed.ch/py430/python/daemon
Benutzeravatar
Rebecca
User
Beiträge: 1662
Registriert: Freitag 3. Februar 2006, 12:28
Wohnort: DN, Heimat: HB
Kontaktdaten:

droptix: http://en.wikipedia.org/wiki/Signal_%28computing%29
Wenn du kill oder ctrl-z benutzt, tust du auch nichts anderes, als Signale versenden.
Offizielles Python-Tutorial (Deutsche Version)

Urheberrecht, Datenschutz, Informationsfreiheit: Piratenpartei
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Rebecca hat geschrieben:Wenn du kill oder ctrl-z benutzt, tust du auch nichts anderes, als Signale versenden.
Ja, aber nur direkt im Programm selber, während es läuft... dort kann ich einen KeyboardInterrupt abfangen. Ich habe quasi einen "Listener" mitlaufen, der ständig danach lauscht, oder?

Aber kann ein zweites Programm z.B. ein Ctrl+C Signal an ein laufendes Programm senden?
Benutzeravatar
Rebecca
User
Beiträge: 1662
Registriert: Freitag 3. Februar 2006, 12:28
Wohnort: DN, Heimat: HB
Kontaktdaten:

Falsch: Wenn du in der Shell Ctrl-C drueckst, sendet der Shell-Prozess an deinen Prozess ein Signal. Du bist in der Shell nie "direkt im Programm selber", sie Shell haengt immer dazwischen. Ja, du kannst den Prozessen, die dir gehoeren, verschiedene Signale von anderen Prozessen aus senden.

Aber wie gesagt, lies dir den Wikipedia-Artikel durch, oder die kill-Manpage, oder Python-Dokumentation zu kill, oder...
Offizielles Python-Tutorial (Deutsche Version)

Urheberrecht, Datenschutz, Informationsfreiheit: Piratenpartei
Benutzeravatar
Rebecca
User
Beiträge: 1662
Registriert: Freitag 3. Februar 2006, 12:28
Wohnort: DN, Heimat: HB
Kontaktdaten:

Nachtrag: Probier mal

Code: Alles auswählen

kill -2 <pid eines python-prozesses>
aus und schau dir an, was das Programm ausgibt. :) Aber fuer deinen urspruenglichen Zweck sind andere Signale nuetzlicher, wie Leonidas schon sagte.
Offizielles Python-Tutorial (Deutsche Version)

Urheberrecht, Datenschutz, Informationsfreiheit: Piratenpartei
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Also ich hab mich ein wenig versucht durchzuwühlen, es scheitert aber an ein paar anschaulichen Beispielen...

Ich kann mir nun vorstellen, dass ein Programm ein Signal an einen Prozess sendet. Nun muss aber mein Prozess das Signal noch empfangen und entsprechend reagieren -> dort scheitert es.

Aus anderen Programmiersprachen kenne ich so etwas wie "listener" oder auch "event listener", die z.B. auf Maus- oder Tastendrücke reagieren. Eine vordefinierte (leere) Methode so einer Listener-Klasse kann man überschreiben und dann sinngemäß etwa sowas hier machen:

Code: Alles auswählen

class Listener():
	
	__signal = None
	
	def listen(self):
		if self.__signal == signal.SIGTERM:
			# close all running stuff and terminate program
			pass
		elif self.__signal == signal.SIGHUP:
			# close and restart
			pass
Auch mit dem Python-Doc-Beispiel komme ich nicht so richtig weiter. Hätte jemand für mich ein anschauliches Beispiel bestehend aus zwei Teilen:

a) Python-Programm, welches in einem Endlos-Loop läuft; bei Signal SIGTERM wird es beendet, bei Signal SIGHUP wird die Endlosschleife verlassen und neu gestartet

b) ein Python-Programm welches ein Signal an a) sendet; ich nehme an b) muss dazu erst a) starten und sich die Prozess-Id merken
Benutzeravatar
gerold
Python-Forum Veteran
Beiträge: 5555
Registriert: Samstag 28. Februar 2004, 22:04
Wohnort: Oberhofen im Inntal (Tirol)
Kontaktdaten:

Hallo droptix!

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: iso-8859-15 -*-

import signal
import time


class Signals(object):
    canceled = False
    reload_it = False
signals = Signals()


def signal_handler(signum, frame):
    print 'Signal handler called with signal', signum
    if signum == signal.SIGTERM:
        signals.canceled = True
    elif signum == signal.SIGHUP:
        signals.reload_it = True


def main():
    signal.signal(signal.SIGTERM, signal_handler)
    signal.signal(signal.SIGHUP, signal_handler)
    
    while True:
        if signals.canceled:
            print "Canceled"
            break
        if signals.reload_it:
            signals.reload_it = False
            print "Reloaded"
            continue
        time.sleep(1)
        print "."


if __name__ == "__main__":
    main()
mfg
Gerold
:-)
http://halvar.at | Kleiner Bascom AVR Kurs
Wissen hat eine wunderbare Eigenschaft: Es verdoppelt sich, wenn man es teilt.
Leonidas
Python-Forum Veteran
Beiträge: 16025
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

Also a) war wirklich nicht so schwer.

Code: Alles auswählen

import signal, time

class Listener(object):
    def complain(self, signum, frame):
        self.loop = False
    
    def run(self):
        signal.signal(signal.SIGHUP, self.complain)
        runs = 0
        while True:
            runs += 1
            print "Starting run %d" % runs
            self.loop = True
            while self.loop:
                time.sleep(1)
        
            
Listener().run()
Den Handler zu SIGTERM habe ich mir gespart, da das Programm beendet wird, auch wenn man nicht darauf reagiert. Wenn man vor dem Beenden Code ausführen will, würde ich eher auf `atexit` zurückgreifen.

b) habe ich mir gespart, stattdessen kill -s HUP `pidof python` verwendet.
My god, it's full of CARs! | Leonidasvoice vs (former) Modvoice
Benutzeravatar
HWK
User
Beiträge: 1295
Registriert: Mittwoch 7. Juni 2006, 20:44

Hallo, Gerold!
In Deinem Beispiel leuchtet mir die Objektorientierung nicht ein. Wenn man eine Klasse Signals schafft, sollte man dann nicht signal_handler als Methode dieser Klasse konstruieren anstatt als globale Funktion? Wenn man aber doch die globale Funktion verwendet, kann man dann nicht auf Signals verzichten und die Klassenattribute als globale Variablen definieren? Man hat ja mit signals sowieso eine globale Variable?
MfG
HWK
Benutzeravatar
gerold
Python-Forum Veteran
Beiträge: 5555
Registriert: Samstag 28. Februar 2004, 22:04
Wohnort: Oberhofen im Inntal (Tirol)
Kontaktdaten:

HWK hat geschrieben:In Deinem Beispiel leuchtet mir die Objektorientierung nicht ein.
Hallo HWK!

Mein Beispiel hat mit OO nichts am Hut. Und globale Variablen? Kenn ich nicht! :twisted:

lg
Gerold
:-)
http://halvar.at | Kleiner Bascom AVR Kurs
Wissen hat eine wunderbare Eigenschaft: Es verdoppelt sich, wenn man es teilt.
Benutzeravatar
veers
User
Beiträge: 1219
Registriert: Mittwoch 28. Februar 2007, 20:01
Wohnort: Zürich (CH)
Kontaktdaten:

Zum Glück ist signals keine globale Variable! *duck*
[url=http://29a.ch/]My Website - 29a.ch[/url]
"If privacy is outlawed, only outlaws will have privacy." - Phil Zimmermann
Antworten