#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ efbeff 2024-04-16 Dieses script läuft z.Zt. produktiv auf Duesseldorf blech02 Freifunk Dortmund Terminerinnerungen aus dem Kalender erzeugen und über die Mailingliste(n) verschicken. Die Erinnerung für das Monatstreffen wird 5 Tage vorher verschickt, die restlichen (FF@HOME, ... 2 Tage vorher ) Unterschieden wird es durch den ical tag SUMMARY. Aus der WEBseite lassen sich die Termine leider nur monatsweise lesen, deshalb werden 2 Monatskalender gelesen, falls das Prüfintervall über eine Monatsgrenze geht. Das alte script von FFdo als Basis erheblich überarbeitet Dies ist die zweite Überarbeitung mit erstellen von *.ics Anhängen Dies ist die dritte Überarbeitung mit Entfernen HTML Kommentar Es darf nur 1mal am Tag laufen, sonst gibt es mehrfache Erinnerungen Der Absender reminder@freifunk-dortmund.de ist keine echte mailadresse, sondern nur als Nichtmitglied aber sendeberechtigt zugelassen in der DEFAULT_LISTE, VEREIN_LISTE und INFRA_LISTE Dafür in der Listenadministration jeweils unter Abo-regeln und Adressfilter Absender-filter Anti-Spam-filter reminder@freifunk-dortmund.de eintragen. Mails an reminder@freifunk-dortmund.de werden abgewiesen. """ # testmode 0 setzen für Produktion # testmode 1 nur aufbereiten und in log schreiben # testmode 2 über smarthost an einzelnen Empfänger schicken # setzt eventuell username PW und port voraus # ACHTUNG PW im Klartext testmode = 0 # bei testmode 1 wird keine mail versandt, die Erinnerungen werden # nur aufbereitet und ins log geschrieben import logging import logging.handlers import sys import traceback import os import shutil import smtplib import requests from email.message import EmailMessage from datetime import datetime, timedelta, date ########from icalendar import Calendar, Event import re ABSFILE=os.path.abspath(__file__) CALURL_TEMPLATE='http://www.freifunk-dortmund.de/termine/{}-{:02d}/?ical=1' # empfängerlisten DEFAULT_LISTE="freifunk-do@list.free.de" INFRA_LISTE="freifunk-do-infra@list.free.de" VEREIN_LISTE="freifunk-do-verein@list.free.de" # Absender MAIL_FROM="FF-DO Termine " if testmode < 2 else\ "fbeythien@gmx.de" MAIL_SUBJECT="Monatstreffen" # wird angepasst if testmode < 2: MAIL_HOST="list.free.de" MAIL_STARTTLS=False MAIL_USER="" MAIL_PASSWORD="" MAIL_PORT="25" else: MAIL_HOST="mail.gmx.net" MAIL_STARTTLS=True MAIL_USER="xxxx" MAIL_PASSWORD="xxxx" MAIL_PORT="587" DEFAULT_LISTE="efbe@prima.de" INFRA_LISTE=DEFAULT_LISTE VEREIN_LISTE=DEFAULT_LISTE # Tagesordnungspunkte Monatstreffen TOP_LINK="https://wiki.ffdo.de/Community/TOPs" TOP_LINK_PAGE= TOP_LINK +".page" WOTAGE = ("Montag","Dienstag","Mittwoch","Donnerstag","Freitag", "Samstag","Sonntag") REM_TREFFEN_MONAT = 4 # Tage vorher erinnern Monatstreffen REM_TREFFEN_TOPS = REM_TREFFEN_MONAT + 2 # Tage vorher erinnern TOP ergänzen REM_STANDARD = 2 # Tage vorher erinnern FF@home, ... REM_INTERVALL = 10 # heute + x Tage Termine prüfen # logging init logging.basicConfig(level=logging.DEBUG) mylog = logging.getLogger("remind") sysl = logging.handlers.SysLogHandler(address='/dev/log') #formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s') sysl.setFormatter(formatter) mylog.addHandler(sysl) def print_exc_plus(): """ Print the usual traceback information, followed by a listing of all the local variables in each frame. """ tb = sys.exc_info()[2] while 1: if not tb.tb_next: break tb = tb.tb_next stack = [] f = tb.tb_frame while f: stack.append(f) f = f.f_back stack.reverse() traceback.print_exc() print("Locals by frame, innermost last") for frame in stack: print() print("Frame %s in %s at line %s" % (frame.f_code.co_name, frame.f_code.co_filename, frame.f_lineno)) for key, value in frame.f_locals.items(): print("\t%20s =" % key,end=" ") #We have to be careful not to cause a new error in our error #printer! Calling str() on an unknown object could cause an #error we don't want. try: print(value) except: print( "") #falls remind nicht erfolgreich beim cron.daily Lauf (z.B. temporäre Netzproblem) #dann versuche es in cron.hourly bis es klappt def falls_daily_versuche_hourly(progfile): (pfad, datei) = os.path.split(progfile) if pfad[-6:] == ".daily": hpfad = pfad[:-5] + "hourly/" shutil.copy2(progfile, hpfad, follow_symlinks=False) mylog.info("remind nach cron.hourly kopiert.") # evtl remind aus hourly wieder entfernen nach erfolgreichem Lauf def falls_hourly_loe(progfile): if "hourly" in progfile: os.remove(progfile) mylog.info("remind in cron.hourly gelöscht") # alle gefundenen Erinnerungen versenden wenn kein testmode gesetzt def send(msgs, sender, server, username=None, password=None): if testmode == 0: s = smtplib.SMTP(server, port=MAIL_PORT) # s.set_debuglevel(2) for m in msgs: s.send_message(m) mylog.info(str(m)) # print('\n', str(m), '\n') s.quit() elif testmode == 1: for m in msgs: mylog.info(str(m)) # print('\n', str(m), '\n') elif testmode == 2: with smtplib.SMTP(MAIL_HOST, port=MAIL_PORT) as s: s.set_debuglevel(2) s.starttls() s.login(MAIL_USER, MAIL_PASSWORD, initial_response_ok=True) for m in msgs: s.send_message(m) mylog.info(str(m)) # print('\n', str(m), '\n') s.close() else: mylog.info(" testmode nicht 0 - 2 ungültig.") # Erinnerung für allgemeinen Termin aufbereiten def gen_mail_allg(sender, rcpt, subject, description, vorspann, event, dtstart): body = "DESCRIPTION " + description + " " + "\n\nDein freundlicher FFDO Autoreminder\n.\n" msg = EmailMessage() msg["From"]=sender msg["To"]=rcpt msg["Subject"]="Reminder " + subject msg.set_content(body) att = vorspann + event + '\r\nEND:VCALENDAR' evdat = dtstart.date() msg.add_attachment(att, subtype="ics", filename=str(evdat)+".ics") return msg # die mmarkdown Markierungen teilweise umsetzen # --- bis ... markdown vorspann löschen # führende Leerstellen erhalten # dies ist rudimentär, kein markdown interpreter!! def zeilen_aufbereiten(tops): # html kommentar entfernen # beispiel für spamschutz entfernen # email = "tony@tiremove_thisger.net" # m = re.search("remove_this", email) # email[:m.start()] + email[m.end():] # Ergebnis: 'tony@tiger.net' koma = re.search("", tops) topn = tops if koma != None and kome != None: if koma.end() < kome.start(): topn = tops[:koma.start()] + tops[kome.end():] topsp = topn.partition("...") if topsp[2]: lines = topsp[2].split("\n") else: lines = topsp[0].split("\n") body = "" for l in lines: # print(l) if len(l.strip()) > 0: if l.strip()[0] == "*": prefix = l.partition("*")[0] body += prefix + " - " + l.strip(' *\n') + "\n" elif l.strip()[0] == "#": continue elif l.strip()[0] == "-": prefix = l.partition("-")[0] body += prefix + "- " + l.strip(' -\n') + "\n" else: body += l.rstrip(" \n") + "\n" return body # Erinnerung für TOPs einstellen zum Monatstreffen aufbereiten def gen_mail_top(sender, rcpt, subject, tops, vorspann, event, dtstart): body = "Hallo zusammen!\n\nIn zwei Tagen wird die Erinnerung für das nächste Monatstreffen verschickt. Bisher sind folgende TOPs eingetragen:\n\n" body += zeilen_aufbereiten(tops) body += "\nFalls du noch ein weiteres Thema besprechen möchtest, trag es bitte hier ein: " + TOP_LINK + "\n\nDein freundlicher FFDO Auto-Reminder\n.\n" msg = EmailMessage() msg["From"]=sender msg["To"]=rcpt msg.set_content(body) att = vorspann + event + '\r\nEND:VCALENDAR' evdat = dtstart.date() msg.add_attachment(att, subtype="ics", filename=str(evdat)+".ics") msg["Subject"]="Reminder TOPs für " + subject return msg # Erinnerung für Monatstreffen aufbereiten def gen_mail_monat(sender, rcpt, subject, tops, vorspann, event, dtstart): wotag = WOTAGE[dtstart.weekday()] stzeit = str(dtstart.time())[:5] body = "Hallo zusammen! \n\nAm kommenden " + wotag + " um " + stzeit + " Uhr steht das nächste Monatstreffen auf dem Plan. Bisher sind folgende TOPs eingetragen: \n\n" body += zeilen_aufbereiten(tops) body += "\nFalls du noch ein weiteres Thema besprechen möchtest, trag es bitte hier ein: " + TOP_LINK + " \n\nDein freundlicher Auto-Reminder\n.\n" msg = EmailMessage() msg["From"]=sender msg["To"]=rcpt msg.set_content(body) att = vorspann + event + '\r\nEND:VCALENDAR' evdat=dtstart.date() msg.add_attachment(att, subtype="ics", filename=str(evdat)+".ics") msg["Subject"]="Reminder " + subject return msg # key in string suchen ende mit crlf def s_in_event(string,such): ergebnis = '' inda=string.find(such) if inda > -1: inda += len(such) inde=string[inda:].find('\r\n') ergebnis=string[inda:inda + inde] return ergebnis def main(): start = date.today() # Intervall für prüfungen festlegen end = start + timedelta(days=REM_INTERVALL) if testmode: # bei test auch 2 vorige Tage prüfen start -= timedelta(2) mylog.info("Start remind testmode: " + str(testmode)) remstandard = start + timedelta(days=REM_STANDARD) remmontreffen = start + timedelta(days=REM_TREFFEN_MONAT) remmontops = start + timedelta(days=REM_TREFFEN_TOPS) # jetzt die Kalenderdaten einsammeln und zerlegen kals = [] msgs = [] msguids = [] tops = None icss = [] calurls = [] # termin URL für lfd Monat erzeugen calurls.append(CALURL_TEMPLATE.format(start.year,start.month)) if start.month != end.month: # termin URL für nächsten Monat erzeugen calurls.append(CALURL_TEMPLATE.format(end.year,end.month)) # Termindaten holen (1 oder 2 Monate) und in Kalenderformat bringen # und auch als ics daten behalten for calurl in calurls: try: ics = requests.get(calurl) # gibt es Termine für den lfd / nächsten Monat if ics: icss.append(ics.text) except ConnectionError as ce: mylog.error("Fehler bei Holen Kalender: " + str(ce)) continue # Events isolieren und auswerten veanf=0 voreins=True icswk=ics.text while veanf > -1: veanf=icswk.find('BEGIN:VEVENT') if veanf == -1: break # keine events mehr Ende while if voreins: # Kalendervorspann retten voreins=False vorspann=icswk[:veanf] if testmode > 1: print(len(vorspann)) print(vorspann) veend=icswk.find('END:VEVENT') vevent=icswk[veanf:veend+len('END:VEVENT')] #icswk setzen damit ein continue später klappt icswk=icswk[veend+len('END:VEVENT'):] vstdat=vevent.find('DTSTART') vstdat1=vevent[vstdat:].find(':') vdatum=vevent[vstdat+vstdat1+1:vstdat+vstdat1+16] dtstart=datetime.strptime(vdatum, '%Y%m%dT%H%M%S') if testmode > 1: print(len(vevent)) print(vevent) print(vdatum) print(dtstart) if dtstart.date() >= start and ( dtstart.date() <= end or testmode): if testmode > 1: print('\n\n selektiert, im Intervall\n\n') subject = s_in_event(vevent,'SUMMARY:') subject += ' ' +str(dtstart) description = s_in_event(vevent,'DESCRIPTION:') for such in ('\\n', '\\,'): ix = description.find(such) while ix > 0: description = description[:ix] + description[ix+2] ix = description.find(such) location = s_in_event(vevent,'LOCATTION:') url = s_in_event(vevent,'URL:') description += '\n' + location + '\n' + url uid = s_in_event(vevent,'UID:') if uid in msguids: if testmode: print("doppelt ", uid) continue # an FF-DO liste per default liste = DEFAULT_LISTE # subject/SUMMARY für Ermittlung der Vorwarnzeit prüfen if "Freifunktreffen" in subject or\ "Monatstreffen" in subject or\ "Orga-Treff" in subject: if testmode or dtstart.date() == remmontops: # Aufforderung TOPs einzustellen try: tops = requests.get(TOP_LINK_PAGE) except ConnectionError as ce: mylog.error("Fehler holen TOPs mit requests.get(): " + str(ce)) continue m = gen_mail_top(MAIL_FROM, liste, subject, tops.text, vorspann, vevent, dtstart) msgs.append(m) msguids.append(uid) elif testmode or dtstart.date() == remmontreffen: # Tagesordnungspunkte für nächstes Monatstreffen try: tops = requests.get(TOP_LINK_PAGE) except ConnectionError as ce: mylog.error("Fehler holen TOPs mit requests.get(): " + str(ce)) continue m = gen_mail_monat(MAIL_FROM, liste, subject, tops.text, vorspann, vevent, dtstart) msgs.append(m) msguids.append(uid) elif "FF@home" in subject: if testmode or dtstart.date() == remstandard: liste = INFRA_LISTE m = gen_mail_allg(MAIL_FROM, liste, subject, description, vorspann, vevent, dtstart) msgs.append(m) msguids.append(uid) elif "Öffentlichkeitsarbeit" in subject: if testmode or dtstart.date() == remstandard: liste = VEREIN_LISTE m = gen_mail_allg(MAIL_FROM, liste, subject, description, vorspann, vevent, dtstart) msgs.append(m) msguids.append(uid) else: if testmode or dtstart.date() == remstandard: m = gen_mail_allg(MAIL_FROM, liste, subject, description, vorspann, vevent, dtstart) msgs.append(m) msguids.append(uid) ### willi=willi ### für exception test # Erinnerungen verschicken wenn kein testmode oder testmode 2 if len(msgs): send(msgs, MAIL_FROM, MAIL_HOST, MAIL_USER, MAIL_PASSWORD) # print("\n-------", len(msgs), "Erinnerung(en)\n") mylog.info("Ende " + str(len(msgs)) + " Erinnerungen") if __name__ == '__main__': try: main() except: falls_daily_versuche_hourly(ABSFILE) traceback.print_exc() #### print_exc_plus() #für bessere Fehlersuche aktivieren mylog.error(traceback.format_exc()) else: falls_hourly_loe(ABSFILE) finally: logging.shutdown() exit(0)