
#!/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)
