E-Mail PDF Anahg automatisch auslesen und Inhalt per Mail weiterveschicken

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
Sir_Random_Trash
User
Beiträge: 3
Registriert: Montag 4. Februar 2019, 11:42

Moin Zusammen,

erstmal möchte ich sagen, dass ich mich im Grunde nicht mit Python auskenne. Meine Erfahrung im Programmieren durfte ich in zwei Semestern C++ an der Uni sammeln.

Ich möchte hier nicht auftreten im Sinne von "schreibt mal mein Programm fertig", habe derzeit aber leider nicht die Zeit Python von Grund auf neu zu lernen.
Ich freue mich jedoch über Erklärungen/Anmerkungen, weil ich das ganze dann doch am Ende halbwegs verstehen möchte.
Sollte mein Unterfangen zu komplex/arbeitsintensiv sein, um das an dieser Stelle reinzustellen, kann ich mit der Aussage auch leben.

Ich verwende Windows 10 habe Python 3.7.2 installiert, den Code schreibe ich in MS Visual Studio 2017.
Der ausführende PC besitzt jedoch nur Python 3.7 und läuft unter Windows 10.
Fehelende Angaben werde ich natürlich schnellstmöglich nachreichen.

Schonmal besten Dank im Voraus.

Gruß
SRT


Also folgendes soll das Programm tun:

1. Auf einen Mailserver mittels IMAP verbinden
2. Die E-Mail eines bestimmten Absenders abwarten (Diese Mail wird immer einen PDF Anhang haben)
3. Den Inhalt der PDF Datei auslesen (die Formatierung soll möglichst erhalten bleiben)
4. Den ausgelesenen Inhalt in eine Mail kopieren
5. Neue Email, per SMTP, mit dem PDF Inhalt an einen bestimmten Kontakt versenden
6. Das Programm soll das 24/7 tun bis es manuell beendet wird
(7. Ein Log File schreiben/optional ist mir gerade so eingefallen, muss nicht wäre aber cool)

Zu 1.
Dazu verwende ich "imaplib"
Hier wird soweit ich das Verstanden habe ja aber der text der Mail ausgelesen, der ist jedoch uninteressant. Wie kann ich quasi den PDF Anhang zur weiteren Bearbeitung "auswählen"

Code: Alles auswählen

 mail = imaplib.IMAP4_SSL('imap.web.de')
    mail.login('Mailadresse', 'Passwort')
    mail.list()
    mail.select("inbox") 
    result, data = mail.search(None, "ALL")

    ids = data[0] 
    id_list = ids.split() 
    latest_email_id = id_list[-1] 
    result, data = mail.fetch(latest_email_id, "(RFC822)")  
    raw_email = data[0][1]
  
zu 2.
dazu ist mir keine richtige Lösung über den weg gelaufen. Im Moment habe ich die Krücke, dass mein Mail Programm automatisch den Anhang speichert und ich den Ordner überwache.
Außerdem lässt dieser Teil das Programm laufen bis eine Taste gedrückt wird.

Dazu dieser Teil:

Code: Alles auswählen

class ExampleHandler(FileSystemEventHandler):
    def on_created(self, event): # when file is created
        # do something, eg.  call your function to process the image
       time.sleep(3)
       Email()
       print('Ausgeführt')

observer = Observer()
event_handler = ExampleHandler() # create event handler
# set observer to use created handler in directory
observer.schedule(event_handler, path='C:\Test')
observer.start()

# sleep until keyboard interrupt, then stop + rejoin the observer
try:
    while True:
        time.sleep(1)
except KeyboardInterrupt:
    observer.stop()

observer.join()

zu 3.

Hier sieht man noch den Umstand, dass ich immer einfach nur die neuste Datei nehme. Ich würde den Weg aber eben gerne vermeiden. Der Anhang muss eigneltich nicht abgespeichert werden sondern nur Verarbeitet.

Code: Alles auswählen

list_of_files = glob.glob('C:\Test\*.pdf')
    #* means all if need specific format then *.csv
    latest_file = max(list_of_files, key=os.path.getctime)

    pdf_file = latest_file
    read_pdf = PyPDF2.PdfFileReader(pdf_file)
    number_of_pages = read_pdf.getNumPages()
    page = read_pdf.getPage(0)
    page_content = page.extractText()
    #print (page_content)
    
zu 4./5.

Den Punkt fasse ich mal zusammen, hoffentlich ist das richtig.
Der page_content kommt vom auslesen der PDF, leider ist die Formatierung komplett weg.
Versendet wird per SMTP.

Code: Alles auswählen

 fromaddr = "Adresse1"
    toaddr = "Adresse2"
    msg = MIMEMultipart()
    msg['From'] = fromaddr
    msg['To'] = toaddr
    msg['Subject'] = "TEST"
 
    #body = page_content
    msg.attach(MIMEText(page_content, 'plain'))
 
    server = smtplib.SMTP('smtp.web.de', 587)
    server.starttls()
    server.login(fromaddr, "Passwort")
    text = msg.as_string()
    server.sendmail(fromaddr, toaddr, text)
    server.quit()
    
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich verstehe das mit den Dateien nicht. Was hindert dich daran, einfach regelmäßig die IMAP-Verbindung zu öffnen, und nach neuen Mails zu suchen? Spannend wäre dann noch die Frage, wie man nachhält, welche Mail schon bearbeitet wurde. AFAIK haben die immer eindeutige IDS, die könntest du dir merken.

Was die Formatierung angeht - da hast du denke ich Pech. Text kann halt vieles nicht, sonst gäbe es ja kein PDF (oder HTML etc). Eine Informationsreduktion wird also immer Verluste mit sich bringen.
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Sir_Random_Trash: Ad 1: Ich verwende für den Zugriff auf IMAP die externe Bibliothek `imapclient`. Die baut auf `imaplib` aus der Standardbibliothek auf, nimmt einem aber eine Menge arbeit ab, da `imaplib` im Grund nur das nötigste stellt um das IMAP-Protokoll zu ”sprechen”.

Du bekommst bei Deinem gezeigten Quelltext nicht nur den Text der E-Mail, sondern die gesamte E-Mail als ”Quelltext” im wie das in RFC822 beschrieben ist. Um das in Objekte zu parsen mit denen man dann arbeiten kann, also Beispielsweise aus einer Multipart-E-Mail die PDF-Anhänge heraus zu filtern, gibt es in der Standardbibliothek das `email`-Package und da als ersten Schritt `email.message_from_bytes()`.

Wenn Du nur E-Meils von einem bestimmten Absender verarbeiten möchtest, würde es Sinn machen den `search()`-Aufruf entsprechend mit einem Filter zu machen – momentan sucht/findet der ja alle E-Mails.

Ad 2. Man kann sich vom IMAP-Server benachrichtigen lassen wenn sich in einer Mailbox etwas geändert hat. Die `imapclient`-Dokumentation hat da was zu. Stichwort `idle()`/`idle_check()`.

Bei `on_created()` drei Sekunden warten und hoffen, das die Datei dann vollständig geschrieben ist, wäre mir nicht robust genug. Man kann doch auch auf das Schliessen der Datei warten, dann sollte sie ja fertig geschrieben sein.

Namenskonvention bei Python ist klein_mit_unterstrichen für alles ausser Konstanten (KOMPLETT_GROSS) und Klassen (MixedCase). `Email()` sollte wahrscheinlich `email()` heissen. Allerdings wäre das dann wieder ungünstig wenn man ein Objekt das eine `email` repräsentiert so nennen will (oder das `email`-Modul importiert hat). Funktionen und Methoden benennt man aber normalerweise ja auch nach der Tätigkeit die sie durchführen.

Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

Ad 3: `read_pdf` wäre zum Beispiel ein guter Name für eine Funktion. Aber ein schlechter für einen `PdfReader` – das wäre eher `pdf_reader`.

Grunddatentypen haben in Namen nichts verloren. `list_of_files` würde besser `filenames` heissen. `files` impliziert das die Objekt tatsächlich Dateiobjekte sind, also zum Beispiel eine `read()`- und/oder `write()`-Methode besitzen. Die Umbenennung von `latest_file` zu `pdf_file` macht keinen Sinn. Da hätte man auch gleich den Namen verwenden können unter dem man das Objekt weiterverwenden möchte.

Wenn das alles im Speicher passieren soll, kann man die PDF-Dateidaten, die man aus der E-Mail holt, in ein `io.BytesIO`-Objekt verpacken und an den `PdfReader` übergeben.

Ad 4./5.: Was genau meinst Du mit Formatierung? PDFs enthalten im Grunde Zeichenoperationen die beschreiben welche Zeichen wo auf einer Seite ”gemalt” werden sollen, wobei das ”wo” sozusagen in physikalischen Einheiten geschieht, also beispielsweise in Inch oder Millimeter. So stellt sich das zumindest für Software die PDFs erstellt in der Regel dar. Und die Texte die da gezeichnet werden, müssen auch nicht in der Reihenfolge gezeichnet werden in der sie räumlich hinterher auf einer Seite im PDF erscheinen. Eine Sofware kann also beispielsweise als erstes unten auf der Seite die Seitennummer zeichnen. Oder Tabellen müssen nicht von links oben nach rechts unten zellenweise gezeichnet werden. Mehrspaltiger Text kann von der erzeugenden Software spaltenweise auf das virtuelle Papier gebracht werden, oder aber auch erste die erste Zeile von allen Spalten, dann die Zweite, die Dritte, und so weiter. Textextraktion aus PDFs ist also nicht einfach und kann auch Probleme bereiten/schief gehen. Für Menschen einfache Beobachtungen wie: „Hey, der Text ist in einer Tabelle mit Spalten angeordnet“ können für Software schwierig bis unmöglich sein.

Ad 7. Es gibt das `logging`-Modul in der Python-Standardbibliothek.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Sir_Random_Trash
User
Beiträge: 3
Registriert: Montag 4. Februar 2019, 11:42

Erstmal vielen Dank für die schnellen Antworten. Dann werde ich mich mal schauen, ob ich das umsetzen kann.
Sir_Random_Trash
User
Beiträge: 3
Registriert: Montag 4. Februar 2019, 11:42

Ich habe versucht das Programm so zu ordnen wie ich es verstanden habe:
Ich bin mir aber nicht ganz sicher, ob vieles von dem was jetzt in den def XYZ steht nicht doch komplett in den main() Teil kommen müsste.
Zu den anderen Ideen habe ich auch schon etwas gefunden, ich will nur ertsmal die Struktur vernünftig haben sonst schaffe ich mir unter Umständen nur unnötige Probleme.

Code: Alles auswählen


def server_check(): [...]

def mail_empfang(): [...]

def read_pdf():[...]

def mail_versenden():[...]

def main():[...]

main()

Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Sir_Random_Trash: Besser zu viele Funktionen als zu grosse Monsterfunktionen. Eine Funktion sollte eine in sich geschlossene Aufgabe erledigen und so geschrieben sein, dass sie leicht isoliert testbar ist. Wenn alles läuft und man findet man hat Funktionen die zu wenig Substanz enthalten, kann man die immer noch ”inlinen”. Aus einer zu gross gewordenen Funktion einzelne Teile in eigene Funktionen heraus zu ziehen ist üblicherweise mehr Arbeit.

Wobei die Struktur doch relativ linear ist. Ich würde da nicht vorher die ganzen Funktionen planen, sondern ausgehend vom Anfang Schritt für Schritt die Transformation zum Ergebnis implementieren. Ausgangspunkt sind IMAP-Zugangsdaten und ein Absender. Da kann man eine Funktion schreiben, die diese Werte entgegen nimmt und E-Mails von diesem Absender als Iterator/Generator liefert. Dann kann man eine Funktion schreiben, die diesen Strom von E-Mails entgegen nimmt und einen Strom von PDF-Dateidaten daraus macht. Dazu kann man eine Hilfsfunktion gebrauchen, die aus einer E-Mail einen PDF-Anhang extrahiert. Dabei würde ich dann auch kein Problem darin sehen wenn die Funktion die den Strom von E-Mail-Rohdaten in PDF-Dateidaten umwandelt nur aus einer einzigen Zeile besteht. Die erste Funktion ist eine Datenquelle, dann hast Du ein paar Transformationsschritte, und am Ende dann eine Datensenke die die Antwort-Emails verschickt. Also zum Beispiel grob so:

Code: Alles auswählen

def iter_emails(imap_host, user, password, sender):
    """Return an iterable of mails by `sender`."""
    # ...


def extract_pdf(message):
    """Return PDF data from `message` or `None` if `message` has no
    PDF attachment.
    """
    # ...


def iter_pdfs(emails):
    return filter(bool, map(extract_pdf, map(email.message_from_bytes, emails)))


# ...


def send_email(smpt_host, user, password, recipient, email):
    """Send one E-Mail."""
    # ...


def send_emails(smpt_host, user, password, recipient, emails):
    for email in emails:
        send_email(smpt_host, user, password, recipient, email)



def main():
    incoming_emails = iter_emails(IMAP_HOST, IMAP_USER, IMAP_PASSWORD, SENDER)
    pdfs = iter_pdfs(incoming_emails)
    
    # ...
    
    outgoing_emails = ...
    send_emails(SMPT_HOST, SMTP_USER, SMPT_PASSWORD, RECIPIENT, outgoing_emails)
Wichtig sind die Funktionen die jeweils ein einzelnes Element des Datenstroms bearbeiten, denn auf die Weise kann man einen einzelnen Schritt einfach und isoliert testen. Das führt dann in der Regel dazu das die Funktionen die den Strom an sich bearbeiten, wie `iter_pdfs()` oder `send_emails()` in der Regel sehr kurz sind. Das macht aber nichts! Keine Angst vor kurzen Funktionen. :-)

Wenn man sich das jetzt so anschaut, sieht man, das es vielleicht Sinn machen könnte sich mit `collections.namedtuple()` einen Datentyp für Serverzugangsdaten zu erstellen der Host, Benutzername, und Password zu einem Wert zusammenfasst.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Antworten