imaplib: copy mit uid

Sockets, TCP/IP, (XML-)RPC und ähnliche Themen gehören in dieses Forum
Antworten
mm96
User
Beiträge: 30
Registriert: Donnerstag 26. November 2020, 23:24

Hallo zusammen,

ich möchte Mails, die bestimmte Kriterien erfüllen, in Gmail in den Papierkorb verschieben, in diesem Fall z. B. die Sicherheitswarnung, die durch das Einloggen in Gmail im Browser kommt.

Ich erhalte allerdings am Ende beim Befehl "copy" den Fehler

Code: Alles auswählen

imaplib.IMAP4.error: UID command error: BAD [b'Could not parse command']
Der print-Befehl hat als Beispiel die Liste - [b'11012'] - ausgegeben.

Die gleiche UID erhalte ich, wenn ich wie hier https://stackoverflow.com/questions/352 ... nd-imaplib den Befehl "fetch" verwende, das müsste also passen.
In diesem Fall https://stackoverflow.com/questions/497 ... se-command war z. B. das Problem, dass die UID nicht als string übergeben wurde, das müsste bei mir eigentlich passen, drum weiß ich nicht, an was es liegen könnte.

Hier noch der Code:

Code: Alles auswählen

import imaplib

mailadress = 'beispiel@gmail.com'
pw = 'beispiel'

filter = ['(SUBJECT "Sicherheitswarnung")']

M = imaplib.IMAP4_SSL('imap.gmail.com')
M.login(mailadress, pw)

for obj in M.list()[1]:
    folder = obj.decode('utf-8').partition('"/"')[-1].lstrip()
    status = M.select(folder)[0]
    if status == "OK":
        data = []
        for rej in filter:
            data = M.uid('SEARCH', None, rej)[1]
            data = data[0].split()
            print(data)
            for id in data:
                id = id.decode('utf-8')
                M.uid('COPY', id, '[Google Mail]/Papierkorb')
    else:
        continue

M.close()
M.logout()
LG
mm96
User
Beiträge: 30
Registriert: Donnerstag 26. November 2020, 23:24

Ich hab mal noch ChatGPT gefragt, damit hats dann geklappt. Die letzte Zeile muss

Code: Alles auswählen

M.uid('COPY', id, '"[Google Mail]/Papierkorb"')
heißen.

Ich verstehs allerdings nicht so ganz, warum braucht man hier jetzt extra Anführungszeichen?

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

@mm96 Weil der Server sonst nicht weiss, dass der Mailboxname bei dem Leerzeichen nicht zuende ist. Oder es sind schon die eckigen Klammern die ein Quoting erforderlich machen. Aber das Leerzeichen auf jeden Fall. Wenn Du es genau wissen willst, musst Du im RFC nachlesen: https://www.ietf.org/rfc/rfc9051.html

``.decode("utf-8")`` ist bei Mailboxnamen falsch. Das klappt hier anscheinend weil nichts ausserhalb von ASCII vorkommt.

Ich würde mich mit diesen Low-Level-IMAP-Protokoll-Details gar nicht selbst herum schlagen wollen, sondern `IMAPClient` verwenden.

Namen sollten keine kryptischen Abkürzungen enthalten, oder gar nur daraus bestehen. `M` ist zudem auch noch gross geschrieben als wäre es eine Konstante. `obj` ist zu generisch. Bei `pw` kann man ja noch erraten wofür es stehen soll, aber bei `rej`‽

`filter` ist der Name einer eingebauten Funktion, den sollte man nicht an etwas anderes binden. Das diese Liste nur einen Eintrag hat ist auch komisch. `id` ist ebenfalls schon von einer eingebauten Funktion belegt.

Die leere Liste die `data` zugewiesen wird, wird nirgends verwendet. Das ist also überflüssig.

``continue`` sollte man vermeiden, weil das ein unbedingter Sprung ist, der nicht an der Struktur des Codes erkennbar ist, und weil das Probleme beim weiterentwickeln von Code machen kann. Zudem ist das an der Stelle wo es steht auch noch total überflüssig, denn da passiert ohne das ``continue`` genau das gleiche als mit dem ``continue``.

Die beiden inneren Schleifen sind ineffizient. Man würde mehrere Bedingungen nicht einzeln verarbeiten, sondern *eine* verknüpfte Suchanfrage durchführen, und dann nicht jede Nachricht einzeln kopieren, sondern alle auf einmal.

Falls die äussere Schleife über alle Mailboxordner geht, dann ja auch über den Papierkorb, und da werden dann unsinnigerweise die Nachrichten aus dem Papierkorb in den Papierkorb kopiert.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
mm96
User
Beiträge: 30
Registriert: Donnerstag 26. November 2020, 23:24

Danke für die Hilfe!

Ich hab mal versucht alles umzusetzen, ich hoffe ich hab nichts übersehen. 'ImapClient' hab ich mir jetzt noch nicht angeschaut, das werde ich noch tun.
Für die Suche habe ich noch eine Funktion gemacht, die aus einer Liste einen entsprechenden Suchstring erstellt und die Listeneinträge mit einer Oderverknüpfung verbindet.
Das ist wahrscheinlich ziemlich roh und unbeholfen, aber es funktioniert schonmal.

Das Ganze soll jetzt ein einfacher Spamfilter sein, in dem man in der Liste eingibt, was man gelöscht haben möchte und das Skript löscht dann die entsprechenden Mails.

Code: Alles auswählen

import imaplib

def get_uids(mail_connection, filters):
    data = mail_connection.uid('SEARCH', None, filters)[1][0].split()
    uids = ','.join([num.decode('ascii') for num in data if num])
    return uids

def copy(mail_connection, filters, dest_folder):
     for raw_folder in mail_connection.list()[1]:
        folder = raw_folder.decode('ascii').partition('"/"')[-1].lstrip()
        status = mail_connection.select(folder)[0]
        if status == "OK" and folder != dest_folder:
            uids = get_uids(mail_connection, filters)
            if uids:                                   
                mail_connection.uid('COPY', uids, dest_folder)

def delete(mail_connection, filters):
    mail.select('"[Google Mail]/Trash"')
    uids = get_uids(mail_connection, filters)
    if uids:
        mail.uid('STORE', uids, '+FLAGS', '\DELETED')
    mail.expunge()

def create_search_string(filters=None):
    if len(filters) == 1:
        return filters.pop()
    search_string = 'OR ' + filters.pop() + ' ' + filters.pop()
    while filters:
        search_string = 'OR (' + search_string + ') ' + filters.pop()
    return search_string

if __name__ == '__main__':

    mailadress = 'beispiel@gmail.com'
    password = 'beispiel'
    spam = ['SUBJECT "abcdefg"', 'SUBJECT "defgh"', 'SUBJECT "sldkfh"', 'SUBJECT "alkjkljkj"']
    
    with imaplib.IMAP4_SSL('imap.gmail.com') as mail:
        mail.login(mailadress, password)

        spam_converted = create_search_string(spam)
        copy(mail, spam_converted, '"[Google Mail]/Trash"')
        delete(mail, spam_converted)

        mail.close()
LG
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Was ist denn der Sinn davon Nachrichten in den Papierkorb zu kopieren und diese Kopien dann zu löschen?

`create_search_string()` hat zwei Probleme: Defaultargumente machen nur Sinn wenn man das Argument beim Aufruf dann auch tatsächlich weg lassen kann. Und die übergebene Liste hier mit den ganzen `pop()`-Aufrufen geleert, was für den Aufrufer sehr überraschend ist.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
mm96
User
Beiträge: 30
Registriert: Donnerstag 26. November 2020, 23:24

Ups, da wollte ich eigentlich noch eine kleine Fehlerabfrage einbauen:

Code: Alles auswählen

def create_search_string(filters):
    if not filters:
        msg = '"filters" is empty. Use \'(key1 "..." key2 "..." etc.)\' for AND connections.'
        raise ValueError(msg)
    if len(filters) == 1:
        return filters.pop()
    search_string = 'OR ' + filters.pop() + ' ' + filters.pop()
    while filters:
        search_string = 'OR (' + search_string + ') ' + filters.pop()
    return search_string
Zum zweiten Problem mit den pop()-Aufrufen:
Ist das ein Problem, weil man dann die Liste im späteren Programmverlauf nicht mehr verwenden könnte?
In dem Fall würde ich die Liste einfach innerhalb der Parsefunktion kopieren:

Code: Alles auswählen

    def create_search_string(filters):
        filter_to_work_with = filters
        ...
Wäre das dann besser?

Das Verschieben und Löschen brauchts irgendwie bei Gmail, weil alle Nachrichten, die nicht aus dem Papierkorb, sondern direkt aus anderen Ordnern gelöscht werden, dann trotzdem noch in "Alle Nachrichten" liegen.
Dann würde der Code z. B. die entsprechenden Nachrichten in "Alle Nachrichten" finden, mit der delete-Flag versehen und expungen, aber sie wären danach gleich wieder da.

Edit: Die copy-Funktion verschiebt die Nachrichten, sie sind nachher im ursprünglichen Ordner nicht mehr vorhanden.

LG
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Ein `filter_to_work_with = filters` macht überhaupt nichts, außer die selbe Liste unter einem anderen Namen verfügbar zu machen.
Es gibt vier verschiedene Anführungszeichen, um literale Strings zu schreiben, Anführungszeichen zu escapen ist daher fast nie nötig.

Statt dass man das, was man übergeben bekommt, zu verändern, sollte man einfach den Code so schreiben, dass das gar nicht erst nötig ist.

Code: Alles auswählen

def create_search_string(filters):
    if not filters:
        raise ValueError(
            '''"filters" is empty. Use '(key1 "..." key2 "..." etc.)' for AND connections.'''
        )
    filters = reversed(filters)
    search_string = next(filters)
    for filter in filters:
        search_string = f'OR ({search_string}) {filter}'
    return search_string
mm96
User
Beiträge: 30
Registriert: Donnerstag 26. November 2020, 23:24

Verdammt, den Fehler mach ich immer: Es hätte filter_to_work_with = filters.copy() hei0en sollen...

Ändert aber nix daran, dass die iterator-Lösung viel ressourcenschonender und eleganter ist, danke!
Antworten