Email Versender (Mit Anhängen)

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
myoggradio
User
Beiträge: 15
Registriert: Freitag 20. November 2020, 12:24

Auch wenn es davon schon viele gibt, wollte ich trotzdem meinen Email Versender vorstellen:

Code: Alles auswählen

#!/usr/bin/python3
import smtplib
import sys
ininame = sys.argv[1]
from email.message import EmailMessage
p_subject=""
p_from=""
p_to=[]
p_cc=[]
p_bcc=[]
p_txt=""
p_anhang=[]
with open(ininame) as datei:
    inhalt=datei.read()
    saetze=inhalt.splitlines()
    for satz in saetze:
        worte=satz.split("=")
        if len(worte) >= 2:
            parm=worte[0]
            wert=worte[1]
            if parm=="subject":
                p_subject=wert
            if parm=="from":
                p_from=wert
            if parm=="to":
                x=["x"]
                x[0] = wert
                p_to += x
            if parm=="cc":
                x=["x"]
                x[0] = wert
                p_cc += x
            if parm=="bcc":
                x=["x"]
                x[0] = wert
                p_bcc += x
            if parm=="txt":
                p_txt = wert
            if parm=="anhang":
                x=["x"]
                x[0] = wert
                p_anhang += x
with open(p_txt) as datei:
    p_txt=datei.read()
msg=EmailMessage()
msg.set_content(p_txt)
msg["Subject"]=p_subject
msg["From"]=p_from
msg["To"]=p_to
msg["Cc"]=p_cc
msg["Bcc"]=p_bcc
for file in p_anhang:
    teile=file.split("/")
    with open(file, 'rb') as fp:
        data = fp.read()
    msg.add_attachment(data,maintype='application',subtype='octet',filename=teile[-1])
with smtplib.SMTP("smtps.udag.de",587) as server:
    server.starttls()
    server.login("christian@myoggradio.org","???")
    erg=server.send_message(msg)
    print(erg)
        
Eingelesen wird eine ini Datei der Art:

Code: Alles auswählen

subject=Eine Test Test Mail=
from=christian@myoggradio.org=
to=christian@myoggradio.org=
cc=christian@edv-ehm.de=
bcc=myoggradio@gmail.com
txt=/home/christian/python/PythonMail/pythonmail.txt=
anhang=/home/christian/python/PythonMail/pythonmail.txt=
to, cc, bcc, anhang können mehrfach eingegeben werden
Benutzeravatar
__blackjack__
User
Beiträge: 13132
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@myoggradio: Anmerkungen zum Quelltext:

Die Importe sollten vor dem Code stehen.

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

Namen sollten keine kryptischen Prä- oder Suffixe haben oder abgekürzt werden. Die ganze `p_` haben da nichts zu suchen, und wenn man `parameter` meint, sollte man nicht nur `parm` schreiben.

`anhang` sollte eigentlich `anhaenge` heissen, denn die Liste enthält nicht einen, sondern potentiell mehrere Anhänge.

Beim öffnen von Textdateien sollte man immer die Kodierung mit angeben. Da hier keinerlei Vorkehrungen beim Versenden für Texte gemacht wird die etwas anderes als ASCII enthalten, sollte man die Kodieurng der INI-Datei auch darauf beschränken.

Man muss nicht jedes kleine Zwischenergebnis an einen Namen binden. `inhalt` und `saetze` braucht es beispielsweise nicht, das wäre einfach:

Code: Alles auswählen

    with open(ini_file_path, encoding="ASCII") as datei:
        for satz in datei.read().splitlines():
            ...
Allerdings würde man eher nicht die ganze Datei auf einmal in den Speicher laden, sondern über die Datei iterieren — das Dateiobjekt liefert dann die einzelnen Zeilen.

`satz` und `worte` ist aber auch komisch, weil das nicht das beschreibt was die Werte sind. `satz` ist eigentlich `zeile`, und `worte` ist als Wert problematisch weil einfach an allen "="-Zeichen aufgeteilt wird, wo man eigentlich nur zwei Ergebnisse haben will: alles *vor* dem ersten "=" und alles *nach* dem ersten "=". Da bietet sich die `partition()`-Methode statt `split()` an.

`parm` kann nur einen der vielen Werte annehmen die da geprüft werden, also sollten das nicht alles eigenständige ``if``\s sein, sondern ein ``if`` und dann ``elif``\s.

Wie ist denn bitte der Code zum Anhängen von einzelnen Werten an eine Liste zustande gekommen? Unsinnig umständlicher geht es ja kaum. Statt erst eine Liste mit einem willkürlichen Dummy-Wert zu erzeugen, den dann durch den Wert zu ersetzen, und dann die Liste an die angehängt werden soll um den Inhalt der einelementigen Liste zu erweitern, sollte man einfach das eine Element an die Liste anhängen.

Code: Alles auswählen

                    x = ["x"]
                    x[0] = wert
                    bcc += x
# =>
                    bcc.append(wert)
Der Name `text` ist mal der Dateiname/-pfad und mal der Inhalt der Datei. Das ist verwirrend.

Das was da `file` heisst ist gar keine Datei sondern ein Datei*name*, was zur Folge hat, dass man in der Schleife dann das tatsächliche Dateiobjekt nicht mehr `file` nennen kann und es den schlechten Namen, weil kryptischen und falschen Namen `fp` bekommt.

Pfade sollte man nicht mit Zeichenkettenoperationen bearbeiten. Dafür gibt es das `pathlib`-Modul. `Path`-Objekte haben auch eine sehr praktische Methode eine Datei komplett einzulesen.

Es ist unschön das Zeilen in denen irgendetwas steht, aber kein "=" vorkommt, und Zeilen in denen vor dem "=" etwas unbekanntes steht, einfach ignoriert werden. Da würde es Sinn machen mindestens eine Warnung auszugeben, weil sich der Benutzer da vielleicht einfach nur vertippt hat, und deshalb dann etwas aus der Datei ignoriert wird.

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/python3
import smtplib
import sys
from email.message import EmailMessage
from pathlib import Path


def main():
    ini_file_path = Path(sys.argv[1])
    subject = ""
    from_ = ""
    to = []
    cc = []
    bcc = []
    text_filename = ""
    anhaenge = []
    with ini_file_path.open(encoding="ASCII") as datei:
        for zeilennummer, zeile in enumerate(datei, 1):
            schluessel, trenner, wert = zeile.rstrip().partition("=")
            if trenner:
                if schluessel == "subject":
                    subject = wert
                elif schluessel == "from":
                    from_ = wert
                elif schluessel == "to":
                    to.append(wert)
                elif schluessel == "cc":
                    cc.append(wert)
                elif schluessel == "bcc":
                    bcc.append(wert)
                elif schluessel == "txt":
                    text_filename = wert
                elif schluessel == "anhang":
                    anhaenge.append(wert)
                else:
                    print(
                        f"{ini_file_path}:{zeilennummer}:"
                        f" unbekannter Schlüssel {schluessel!r}"
                    )
            else:
                print(f"{ini_file_path}:{zeilennummer}: kein Trennzeichen")

    message = EmailMessage()
    message["Subject"] = subject
    message["From"] = from_
    message["To"] = to
    message["Cc"] = cc
    message["Bcc"] = bcc
    message.set_content(Path(text_filename).read_text(encoding="ASCII"))
    for file_path in map(Path, anhaenge):
        message.add_attachment(
            file_path.read_bytes(),
            maintype="application",
            subtype="octet",
            filename=file_path.name,
        )

    with smtplib.SMTP("smtps.udag.de", 587) as server:
        server.starttls()
        server.login("christian@myoggradio.org", "???")
        print(server.send_message(message))


if __name__ == "__main__":
    main()
Man könnte da noch ein bisschen verallgemeinern und statt der vielen einzelnen Variablen für die Ini-Datei ein Wörterbuch erstellen. Ungetestet:

Code: Alles auswählen

#!/usr/bin/python3
import smtplib
import sys
from email.message import EmailMessage
from pathlib import Path


def main():
    ini_file_path = Path(sys.argv[1])
    config = {
        "subject": "",
        "from": "",
        "to": [],
        "cc": [],
        "bcc": [],
        "txt": "",  # Filename to load the mail body text from.
        "anhang": [],
    }
    with ini_file_path.open(encoding="ASCII") as datei:
        for zeilennummer, zeile in enumerate(datei, 1):
            schluessel, trenner, wert = zeile.rstrip().partition("=")
            if trenner:
                try:
                    if isinstance(config[schluessel], str):
                        config[schluessel] = wert
                    else:
                        config[schluessel].append(wert)
                except KeyError:
                    print(
                        f"{ini_file_path}:{zeilennummer}:"
                        f" unbekannter Schlüssel {schluessel!r}"
                    )
            else:
                print(f"{ini_file_path}:{zeilennummer}: kein Trennzeichen")

    message = EmailMessage()
    message.set_content(Path(config.pop("txt")).read_text(encoding="ASCII"))
    for file_path in map(Path, config.pop("anhang")):
        message.add_attachment(
            file_path.read_bytes(),
            maintype="application",
            subtype="octet",
            filename=file_path.name,
        )
    for key, value in config.items():
        message[key.capitalize()] = value

    with smtplib.SMTP("smtps.udag.de", 587) as server:
        server.starttls()
        server.login("christian@myoggradio.org", "???")
        print(server.send_message(message))


if __name__ == "__main__":
    main()
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis
Benutzeravatar
DeaD_EyE
User
Beiträge: 1023
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Hier könnte man das match Statement verwenden, dass sich dazu eignet Typen zu erkennen (kann auch mehr als nur das).

Code: Alles auswählen

from pathlib import Path

# testdatei
ini_file_path = Path("test.txt")

# dict as keys erstellen und festlegen, dass ein leerer str als Wert gesetzt wird
# ohne Angabe des zweiten Arguments ist es ein None
result = dict.fromkeys(("subject", "from", "txt"), "")
# nun die Zuweisung der listen für die unterschiedlichen keys
# und result wird dann mit den Ergebnissen der Dict-Comprehension vereint.
result |= {key: [] for key in ("to", "cc", "bcc", "anhang")}

print("Vorbereitetes dict:", result)

with ini_file_path.open(encoding="ASCII") as datei:
    for zeilennummer, zeile in enumerate(datei, 1):
        schluessel, trenner, wert = zeile.rstrip().partition("=")

        # zuerst prüfen, ob der Trenner vorhanden ist
        # und überspringen, falls der Trenner leer ist
        if not trenner:
            print(f"{ini_file_path}:{zeilennummer}: kein Trennzeichen")
            continue

        # hier das gleiche mit einem Schlüssel, der nicht im dict vorkommt
        if schluessel not in result:
            print(
                f"{ini_file_path}:{zeilennummer}:"
                f" unbekannter Schlüssel {schluessel!r}"
            )
            continue

        # sieht besser aus, als der isinstance-check
        # afik wurde das mit Python 3.10 eingeführt
        match result[schluessel]:
            case str():
                result[schluessel] = wert
            case list():
                result[schluessel].append(wert)


print("Ergebnis:", result)

sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
Benutzeravatar
__blackjack__
User
Beiträge: 13132
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Ja, ich weiss, es ist ein bisschen übertrieben. Aber immerhin habe ich der Versuchung widerstanden den `ConfigType` auf eine Basisklasse und jeweils eine Klasse für Zeichenketten und Listen aufzuteilen um das noch objektorientierter zu machen. 🤡

Ungetestet:

Code: Alles auswählen

#!/usr/bin/python3
import smtplib
import sys
from email.message import EmailMessage
from pathlib import Path

from attrs import field, frozen


@frozen
class ConfigType:
    name = field()
    full_target_path = field()
    multiple = field()

    @property
    def target_path(self):
        return self.full_target_path[:-1]

    @property
    def target_key(self):
        return self.full_target_path[-1]

    @property
    def default_value(self):
        return [] if self.multiple else ""

    def create_target(self, config):
        target = config
        for key in self.target_path:
            target = target.setdefault(key, {})
        target[self.target_key] = self.default_value
        return target

    def set(self, target, value):
        if self.multiple:
            target[self.target_key].append(value)
        else:
            target[self.target_key] = value


NAME_TO_CONFIG_TYPE = {
    type_.name: type_
    for type_ in [
        ConfigType("subject", ["headers", "Subject"], False),
        ConfigType("from", ["headers", "From"], False),
        ConfigType("to", ["headers", "To"], True),
        ConfigType("cc", ["headers", "Cc"], True),
        ConfigType("bcc", ["headers", "Bcc"], True),
        ConfigType("txt", ["body_text_filename"], False),
        ConfigType("anhang", ["attachment_filenames"], True),
    ]
}


def load_config(file_path):
    config = {}
    name_to_target = {
        type_.name: type_.create_target(config)
        for type_ in NAME_TO_CONFIG_TYPE.values()
    }
    with file_path.open(encoding="ASCII") as lines:
        for line_number, line in enumerate(lines, 1):
            key, delimiter, value = line.rstrip().partition("=")
            if delimiter:
                try:
                    type_ = NAME_TO_CONFIG_TYPE[key]
                except KeyError:
                    print(
                        f"{file_path}:{line_number}:"
                        f" unbekannter Schlüssel {key!r}"
                    )
                else:
                    type_.set(name_to_target[type_.name], value)

            else:
                print(f"{file_path}:{line_number}: kein Trennzeichen")

    return config


def create_email_message(config):
    message = EmailMessage()
    for key, value in config["headers"].items():
        message[key] = value

    message.set_content(
        Path(config["body_text_filename"]).read_text(encoding="ASCII")
    )

    for file_path in map(Path, config["attachment_filenames"]):
        message.add_attachment(
            file_path.read_bytes(),
            maintype="application",
            subtype="octet",
            filename=file_path.name,
        )
    
    return message


def main():
    message = create_email_message(load_config(Path(sys.argv[1])))
    with smtplib.SMTP("smtps.udag.de", 587) as server:
        server.starttls()
        server.login("christian@myoggradio.org", "???")
        print(server.send_message(message))


if __name__ == "__main__":
    main()
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis
Benutzeravatar
__blackjack__
User
Beiträge: 13132
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Irgendwie habe ich dann doch der Versuchung nicht widerstanden (ungetestet):

Code: Alles auswählen

#!/usr/bin/python3
import smtplib
import sys
from email.message import EmailMessage
from pathlib import Path

from attrs import field, frozen


@frozen
class BaseConfigType:
    DEFAULT_FACTORY = lambda: None

    name = field()
    convert = field()
    full_target_path = field()

    @property
    def target_path(self):
        return self.full_target_path[:-1]

    @property
    def target_key(self):
        return self.full_target_path[-1]

    def create_target(self, config):
        target = config
        for key in self.target_path:
            target = target.setdefault(key, {})
        target[self.target_key] = self.DEFAULT_FACTORY()
        return target

    def set(self, target, value):
        raise NotImplementedError


@frozen
class SingleConfigType(BaseConfigType):

    def set(self, target, value):
        target[self.target_key] = self.convert(value)


@frozen
class MultiConfigType(BaseConfigType):
    DEFAULT_FACTORY = list

    def set(self, target, value):
        target[self.target_key].append(self.convert(value))


NAME_TO_CONFIG_TYPE = {
    type_.name: type_
    for type_ in [
        SingleConfigType("subject", str, ["headers", "Subject"]),
        SingleConfigType("from", str, ["headers", "From"]),
        MultiConfigType("to", str, ["headers", "To"]),
        MultiConfigType("cc", str, ["headers", "Cc"]),
        MultiConfigType("bcc", str, ["headers", "Bcc"]),
        SingleConfigType("txt", Path, ["body_text_file_path"]),
        MultiConfigType("anhang", Path, ["attachment_file_paths"]),
    ]
}


def load_config(file_path):
    config = {}
    name_to_target = {
        type_.name: type_.create_target(config)
        for type_ in NAME_TO_CONFIG_TYPE.values()
    }
    with file_path.open(encoding="ASCII") as lines:
        for line_number, line in enumerate(lines, 1):
            key, delimiter, value = line.rstrip().partition("=")
            if delimiter:
                try:
                    type_ = NAME_TO_CONFIG_TYPE[key]
                except KeyError:
                    print(
                        f"{file_path}:{line_number}:"
                        f" unbekannter Schlüssel {key!r}"
                    )
                else:
                    type_.set(name_to_target[type_.name], value)

            else:
                print(f"{file_path}:{line_number}: kein Trennzeichen")

    return config


def create_email_message(config):
    message = EmailMessage()
    for key, value in config["headers"].items():
        if value is not None:
            message[key] = value

    body_text_file_path = config["body_text_file_path"]
    if body_text_file_path:
        message.set_content(body_text_file_path.read_text(encoding="ASCII"))

    for file_path in config["attachment_file_paths"]:
        message.add_attachment(
            file_path.read_bytes(),
            maintype="application",
            subtype="octet",
            filename=file_path.name,
        )

    return message


def main():
    message = create_email_message(load_config(Path(sys.argv[1])))
    with smtplib.SMTP("smtps.udag.de", 587) as server:
        server.starttls()
        server.login("christian@myoggradio.org", "???")
        print(server.send_message(message))


if __name__ == "__main__":
    main()
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis
Antworten