123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465 |
- #!/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 <reminder@freifunk-dortmund.de>" 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( "<ERROR WHILE PRINTING VALUE>")
- #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)
- kome = 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)
|