EMail Anhang speichern

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
DGUV-V3
User
Beiträge: 24
Registriert: Dienstag 12. April 2016, 09:50

Hallo,
ich habe mir neulich eine Raspberry gekauft und auf diesem einen MySQL Server installiert. Nun möchte ich meine Datenbank mit Hilfe von Mail Anhängen verwalten. Hierfür habe ich mir überlegt den Mail Anhang local zu speichern um ihn anschließend in die Datenbank zu übertragen.
Um den Anhang zu speichern habe ich Folgendes Script erstellt:

Code: Alles auswählen

#!/usr/bin/env python

import poplib
import email
import os
import sys
import string

#
# attsave.py
# Check emails at PROVIDER for attachments and save them to SAVEDIR.
#
#

PROVIDER = "pop3.web.de"
USER = "xy@web.de"
PASSWORD = "xyz"

SAVEDIR = "/home/pi/EmailAnhang"


def saveAttachment(mstring):

    filenames = []
    attachedcontents = []

    msg = email.message_from_string(mstring)

    for part in msg.walk():

        fn = part.get_filename()

        if fn <> None:
            filenames.append(fn)
            attachedcontents.append(part.get_payload())

    for i in range(len(filenames)):
        fp = file(SAVEDIR + "/" + filenames[i], "wb")
        fp.write(attachedcontents[i])
        print 'Found and saved attachment "' + filenames[i] + '".'
        fp.close()

try:
    client = poplib.POP3_SSL(PROVIDER)
except:
    print "Error: Provider not found."
    sys.exit(1)

client.user(USER)
client.pass_(PASSWORD)

anzahl_mails = len(client.list()[1])

for i in range(anzahl_mails):
    lines = client.retr(i + 1)[1]
    mailstring = string.join(lines, "\n")
    saveAttachment(mailstring)

client.quit()
Das Script an sich speichert mir den Anhang auch ab, aber der Inhalt des Anhangs ist nicht identisch zu dem Originalen.
Ich hoffe Ihr könnt mir bei meinem Problem behilflich sein :D
Ich Bedanke mich im vorraus
Zuletzt geändert von Anonymous am Dienstag 12. April 2016, 10:47, insgesamt 1-mal geändert.
Grund: Quelltext in Code-Tags gesetzt.
BlackJack

@DGUV-V3: In wiefern ist der Inhalt nicht identisch? Was hast Du im Original, was erwartest Du, und was bekommst Du stattdessen?

Anmerkungen zum Quelltext: Der Kommentarblock am Anfang würde als Docstring für das Modul Sinn machen.

`os` wird importiert, aber nicht verwenden. Es sollte allerdings verwendet werden, denn Pfadteile setzt man mit `os.path.join()` statt mit Zeichenkettenoperationen zusammen.

Auf Modulebene werden nur Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst, und die so ”geschützt” wird, das sie nur automatisch ausgeführt wird, wenn man das Modul als Programm ausführt und nicht wenn man es importiert. Dann kann man die Bestandteile einfacher testen und wiederverwenden.

Die Ausnahmebehandlung mit dem ”nackten” ``except:`` ohne konkrete Ausnahme(n) ist schlecht. Das behandelt *jede* Ausnahme, auch solche mit denen Du gar nicht rechnest. Und dem Benutzer wird gesagt der E-Mail-Anbieter wurde nicht gefunden. Auch wenn das Problem ein völlig anderes ist. Da Du das Programm sowieso abbrichst, kann man sich diese Art der Nichtbehandlung auch sparen und man bekommt automatisch bessere Informationen was tatsächlich das Problem ist.

Man könnte stattdessen mit einem ``finally`` dafür sorgen, dass am Ende die `quit()`-Methode aufgerufen wird, auch wenn vorher eine Ausnahme auftritt.

Ich würde die Rückgabe von `POP3.list()` nicht verwerfen und dann die Nachrichtennummern selber erzeugen. Du weisst doch gar nicht sicher ob die wirklich von 1 an aufsteigend und lückenlos sind. Das mag bei dem Server den Du getestet hast, bisher zufällig immer so gewesen sein, aber wenn das immer so wäre, dann würde die API von `POP3.list()` ja keinen Sinn machen!

`string.join()` ist veraltet und sollte, wie alle Funktionen aus dem `string`-Modul die es mittlerweile schon sehr lange als Methoden auf `str` gibt, nicht mehr verwendet werden.

Die Schreibweise von `saveAttachment()` entspricht nicht dem Style Guide for Python Code und ist ausserdem inhaltlich nicht ganz korrekt, denn es wird nicht *eine* Anlage gespeichert, sondern *alle*. Es sollte also `save_attachments()` heissen.

Namen sollten nicht kryptisch abgekürzt, oder mit Prä- oder Suffixen versehen werden, sondern klar vermitteln was der Wert dahinter im Kontext des Programms bedeutet. An Wortgrenzen innerhalb eines namens macht ein Unterstrich das lesen/erfassen deutlich einfacher.

Anstatt paarweise zusammengehörende Daten in zwei ”paralellen” Listen zu speichern und am Ende über den Umweg eines Laufindexes wieder zusammen zu führen, speichert man die zusammengehörenden Daten besser *zusammen* in *einer* Liste. Zum Beispiel in dem man sie zu Tupeln zusammenfasst.

Ich bin schon ziemlich lange bei Python dabei, aber den ``<>``-Operator habe ich vielleicht zwei oder drei mal gesehen. Der ist veraltet und wird als ``!=`` geschrieben. Da leere Dateinamen nicht erlaubt sind, braucht man hier aber eigentlich auch gar keinen expliziten Test, sondern kann einfach den Dateinamen selbst als Wahrheitswert verwenden.

`file` war nicht zum öffnen von Dateien vorgesehen sondern als Datentyp um eigene Untertypen vom Dateityp abzuleiten. Dateien werden mit `open()` geöffnet. Und falls möglich zusammen mit der ``with``-Anweisung um den Code robuster und sicherer zu machen was Probleme zwischen dem öffnen und dem schliessen der Datei betrifft, die sonst dazu führen könnten, das die Datei nicht geschlossen wird.

Zeichenketten und Werte mit ``+`` zusammen zu stückeln ist eher BASIC als Python. Python kennt Zeichenkettenformatierung.

Ich komme dann ungefähr bei so etwas heraus:

Code: Alles auswählen

#!/usr/bin/env python
"""
attsave.py

Check emails at :const:`PROVIDER` for attachments and save them to
:const:`SAVEDIR`.
"""
from __future__ import absolute_import, division, print_function
import email
import os
import poplib

PROVIDER = 'pop3.web.de'
USER = 'xy@web.de'
PASSWORD = 'xyz'

SAVE_DIR = '/home/pi/EmailAnhang'


def save_attachments(mail_string):
    attachments = list()
    for part in email.message_from_string(mail_string).walk():
        filename = part.get_filename()
        if filename:
            attachments.append((filename, part.get_payload()))

    for filename, content in attachments:
        with open(os.path.join(SAVE_DIR, filename), 'wb') as attachment_file:
            attachment_file.write(content)
        print('Found and saved attachment {0!r}.'.format(filename))


def main():
    try:
        client = poplib.POP3_SSL(PROVIDER)
        client.user(USER)
        client.pass_(PASSWORD)
        for message_number in client.list()[1]:
            save_attachments('\n'.join(client.retr(message_number)[1]))
    finally:
        client.quit()


if __name__ == '__main__':
    main()
Der Umweg über das Dateisystem sollte allerdings am Ende nicht notwendig sein, und so wie das jetzt programmiert ist, ist es ausserdem noch sehr unsicher, denn man sollte den tatsächlichen Zielpfad überprüfen ob der auch wirklich innerhalb des gewünschten Zielverzeichnisses liegt. Sonst kann ein Angreifer nämlich E-Mails so präparieren dass er Dir gezielt Dateien überschreibt und sich so Zugang zu Deinem Rechner verschaffen kann.
DGUV-V3
User
Beiträge: 24
Registriert: Dienstag 12. April 2016, 09:50

Danke für deine Rücksendung.

Meine Erwartung an das Script war folgender:
Ich hatte in einer Testmail eine txt-Datei angehangen.
In dieser stand ein kleiner Text (Dies ist ein Test ).
Nach dem ich das Script ausgeführt habe, wurde der Anhang mit Folgendem Inhalt gespeichert ( PD)waHANCi.....und so weiter)

Bezüglich der Unsicherheit durch die Dateiablage, diese würde gar nicht bestehen wenn ich eine direkte Möglichkeit wüsste oder hätte, die XML-Datei (welche sich im Anhang einer Email mit bestimmten Betreff befindet) direkt weiter verarbeiten könnte ( in eine vorhandenen DB schreiben).

Bei dem testen deines verbesserten Scriptes ist bei mir Folgende Fehlermeldung aufgetreten aus der ich nicht schlau werde, vielleicht kannst du mir ja da weiter helfen.
Fehler:
Traceback (most recent call last):
File "EmpfMailTest.py", line 45, in <module>
main()
File "EmpfMailTest.py", line 39, in main
save_attachments('\n'.join(client.retr(message_number)[1]))
File "/usr/lib/python2.7/poplib.py", line 232, in retr
return self._longcmd('RETR %s' % which)
File "/usr/lib/python2.7/poplib.py", line 167, in _longcmd
return self._getlongresp()
File "/usr/lib/python2.7/poplib.py", line 143, in _getlongresp
resp = self._getresp()
File "/usr/lib/python2.7/poplib.py", line 136, in _getresp
raise error_proto(resp)
poplib.error_proto: -ERR invalid sequence number: "1 63056"

Danke schon mal im vorraus
BlackJack

@DGUV-V3: Also ist der Inhalt kodiert. Schau Dir dazu mal die Dokumentation zur `get_payload()`-Methode an.

Bezüglich des Fehlers, da habe ich nicht nachgeschaut was die Elemente im Rückgabewert von `POP3.list()` darstellen. Da ist nicht nur die Nachrichtennummer sondern auch die Grösse der Nachricht kodiert. Das muss man also noch auseinandernehmen:

Code: Alles auswählen

def main():
    try:
        client = poplib.POP3_SSL(PROVIDER)
        client.user(USER)
        client.pass_(PASSWORD)
        message_numbers = (int(s.split()[0]) for s in client.list()[1])
        for message_number in message_numbers:
            save_attachments('\n'.join(client.retr(message_number)[1]))
    finally:
        client.quit()
DGUV-V3
User
Beiträge: 24
Registriert: Dienstag 12. April 2016, 09:50

@BlackJack vielen dank für den Tipp mit dem kodieren.
Die Lösung für mein Problem in meinem Script war so simpel und einfach das ich nicht drauf gekommen bin.
Ich musste lediglich in die Klammer von get_payload(decode = True) reinschreiben und schon ging es.
Benutzeravatar
/me
User
Beiträge: 3555
Registriert: Donnerstag 25. Juni 2009, 14:40
Wohnort: Bonn

DGUV-V3 hat geschrieben:Ich musste lediglich in die Klammer von get_payload(decode = True) reinschreiben und schon ging es.
Das klingt so ... frickelig. Formulieren wir es wie folgt: Du musstest der Funktion get_payload den Keyword-Parameter decode mit dem Wert True mitgeben.
DGUV-V3
User
Beiträge: 24
Registriert: Dienstag 12. April 2016, 09:50

Hallo zusammen,

da das Script soweit läuft würde ich gerne eine Verbesserung vornehmen, allerdings hapert es an der Umsetzung daher folgende Frage:
Weiß einer wie ich in dem von mir im ersten Post geposteten Script eine Bedingung einzubauen die mir nur den Anhang von bestimmten Email mit dem Betreff xy abspeichert.
Ich Bedanke mich schon einmal im voraus bei euch
Jade
User
Beiträge: 1
Registriert: Freitag 29. Juli 2016, 14:03

Den Code aus dem ersten Beitrag hatte ich jetzt auch schon zwei Wochen in der Mangel, ohne Erfolgserlebnisse.
Den hatte ich, wie auch DGUV-V3, im Netz gefunden und arglos benutzt.
(Bin natürlich Anfänger)

Faszinierend, wie anders das Teil nach der Modernisierung durch BlackJack aussieht. Vielen Dank dafür! Wie lernt man das bloß?

Nun die Fragen (OffTopic):
1. Wie erkenne ich veralteten Code, wenn ich etwas suche/finde. Wo sollte ich generell (nicht) suchen? Es ist frustrierend wenn man nach viel Zeit und Mühen liest, dass das veraltet und eh schlechter Stil ist.
2. Welches ist das "richtige" Buch für den Einstieg? Ich habe Dutzende Tipps im Forum "Allgemeine Frage" gelesen. Z.B. die deutschen Bücher seien alle schlecht. Ich habe wenigstens schon entschieden, dass ich python3 auf dem PI nehmen will.
Antworten