Compare commits

..
9 Commits
Author SHA1 Message Date
Simon Zeyer 883fc84c02 Enhance securecad message parsing to handle additional table structures for 'Einsatzziel', 'Zusatztext zum Ort', and 'Zusatztext zum Objekt' 2026-08-09 11:10:07 +00:00
Simon Zeyer 399d8e498d Add Divera messaging functionality and API keys to environment configuration 2026-08-09 11:09:51 +00:00
Simon Zeyer 4a965a96a4 update swankt-wendel root ca 2026-08-08 13:31:28 +00:00
Simon Zeyer f64879398f Add sleep delay in run function to prevent tight loop execution 2025-06-16 21:22:55 +00:00
Simon Zeyer 3e33de4606 Fix flag setting method in IMAP message handling 2025-06-09 12:01:03 +00:00
Simon Zeyer 8b4bc74b1d Add PDF generation and printing functionality using Google Chrome 2025-06-09 11:52:44 +00:00
Simon Zeyer 2b331fbd1f Remove redundant idle check logic for existing messages in IMAP connection 2025-06-09 11:37:44 +00:00
Simon Zeyer b528b0a748 Refactor idle check logic in IMAP connection to improve message handling 2025-06-09 11:12:18 +00:00
Simon Zeyer 5127caed03 Add IMAP support and refactor environment configuration
- Introduced `imap_connect.py` for handling IMAP email interactions.
- Created `run.py` to manage execution based on the selected mode (IMAP or EWS).
- Updated Dockerfile and .env.dev to include necessary environment variables.
- Enhanced logging and email processing in `exchange_connect.py`.
- Added `weasyprint` and `imapclient` to requirements.
2025-06-09 10:53:08 +00:00
12 changed files with 381 additions and 231 deletions
+3 -1
View File
@@ -1,3 +1,4 @@
mode="EWS"
username=""
password=""
server=""
@@ -12,4 +13,5 @@ alarminator_token=""
alarminator_zvies_use_PEALGRP="False"
printer=DEFAULT
print_num=0
BASIC_AUTH_PASSWORD=""
DIVERA_MESSANGER_API_KEY=""
DIVERA_ALARMER_API_KEY=""
+5 -4
View File
@@ -1,5 +1,6 @@
FROM python:3.10-bullseye
ENV mode="EWS"
ENV username=
ENV password=
ENV server="exchange.sankt-wendel.de"
@@ -13,8 +14,9 @@ ENV alarminator_zvies_use_PEALGRP="False"
ENV print_num=0
ENV printer="DEFAULT"
ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
ENV DIVERA_MESSANGER_API_KEY=""
ENV DIVERA_ALARMER_API_KEY=""
ENV MAPS_API_KEY=""
ENV BASIC_AUTH_PASSWORD=""
COPY *.deb /
@@ -46,7 +48,6 @@ RUN apt-get --yes --force-yes install ca-certificates cups cups-filters libcups
WORKDIR /usr/src/app
EXPOSE 631
EXPOSE 5000
# Add user and disable sudo password checking
RUN useradd \
--groups=sudo,lp,lpadmin \
@@ -57,6 +58,7 @@ RUN useradd \
print \
&& sed -i '/%sudo[[:space:]]/ s/ALL[[:space:]]*$/NOPASSWD:ALL/' /etc/sudoers
# Print PDF
RUN apt-get update && apt-get install -y \
apt-transport-https \
@@ -76,10 +78,9 @@ RUN groupadd chrome && useradd -g chrome -s /bin/bash -G audio,video chrome \
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install --no-cache-dir waitress
COPY ./app .
COPY *.crt /usr/local/share/ca-certificates/
RUN update-ca-certificates
CMD [ "sh","-c","/etc/init.d/cups start && waitress-serve --port=5000 app:app && python3 exchange_connect.py" ]
CMD [ "sh","-c","/etc/init.d/cups start && python3 /usr/src/app/run.py" ]
@@ -0,0 +1,36 @@
-----BEGIN CERTIFICATE-----
MIIGTDCCBDSgAwIBAgIQOXpmzCdWNi4NqofKbqvjsTANBgkqhkiG9w0BAQwFADBf
MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD
Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw
HhcNMjEwMzIyMDAwMDAwWhcNMzYwMzIxMjM1OTU5WjBgMQswCQYDVQQGEwJHQjEY
MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQDEy5TZWN0aWdvIFB1Ymxp
YyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gQ0EgRFYgUjM2MIIBojANBgkqhkiG9w0B
AQEFAAOCAY8AMIIBigKCAYEAljZf2HIz7+SPUPQCQObZYcrxLTHYdf1ZtMRe7Yeq
RPSwygz16qJ9cAWtWNTcuICc++p8Dct7zNGxCpqmEtqifO7NvuB5dEVexXn9RFFH
12Hm+NtPRQgXIFjx6MSJcNWuVO3XGE57L1mHlcQYj+g4hny90aFh2SCZCDEVkAja
EMMfYPKuCjHuuF+bzHFb/9gV8P9+ekcHENF2nR1efGWSKwnfG5RawlkaQDpRtZTm
M64TIsv/r7cyFO4nSjs1jLdXYdz5q3a4L0NoabZfbdxVb+CUEHfB0bpulZQtH1Rv
38e/lIdP7OTTIlZh6OYL6NhxP8So0/sht/4J9mqIGxRFc0/pC8suja+wcIUna0HB
pXKfXTKpzgis+zmXDL06ASJf5E4A2/m+Hp6b84sfPAwQ766rI65mh50S0Di9E3Pn
2WcaJc+PILsBmYpgtmgWTR9eV9otfKRUBfzHUHcVgarub/XluEpRlTtZudU5xbFN
xx/DgMrXLUAPaI60fZ6wA+PTAgMBAAGjggGBMIIBfTAfBgNVHSMEGDAWgBRWc1hk
lfmSGrASKgRieaFAFYghSTAdBgNVHQ4EFgQUaMASFhgOr872h6YyV6NGUV3LBycw
DgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYI
KwYBBQUHAwEGCCsGAQUFBwMCMBsGA1UdIAQUMBIwBgYEVR0gADAIBgZngQwBAgEw
VAYDVR0fBE0wSzBJoEegRYZDaHR0cDovL2NybC5zZWN0aWdvLmNvbS9TZWN0aWdv
UHVibGljU2VydmVyQXV0aGVudGljYXRpb25Sb290UjQ2LmNybDCBhAYIKwYBBQUH
AQEEeDB2ME8GCCsGAQUFBzAChkNodHRwOi8vY3J0LnNlY3RpZ28uY29tL1NlY3Rp
Z29QdWJsaWNTZXJ2ZXJBdXRoZW50aWNhdGlvblJvb3RSNDYucDdjMCMGCCsGAQUF
BzABhhdodHRwOi8vb2NzcC5zZWN0aWdvLmNvbTANBgkqhkiG9w0BAQwFAAOCAgEA
YtOC9Fy+TqECFw40IospI92kLGgoSZGPOSQXMBqmsGWZUQ7rux7cj1du6d9rD6C8
ze1B2eQjkrGkIL/OF1s7vSmgYVafsRoZd/IHUrkoQvX8FZwUsmPu7amgBfaY3g+d
q1x0jNGKb6I6Bzdl6LgMD9qxp+3i7GQOnd9J8LFSietY6Z4jUBzVoOoz8iAU84OF
h2HhAuiPw1ai0VnY38RTI+8kepGWVfGxfBWzwH9uIjeooIeaosVFvE8cmYUB4TSH
5dUyD0jHct2+8ceKEtIoFU/FfHq/mDaVnvcDCZXtIgitdMFQdMZaVehmObyhRdDD
4NQCs0gaI9AAgFj4L9QtkARzhQLNyRf87Kln+YU0lgCGr9HLg3rGO8q+Y4ppLsOd
unQZ6ZxPNGIfOApbPVf5hCe58EZwiWdHIMn9lPP6+F404y8NNugbQixBber+x536
WrZhFZLjEkhp7fFXf9r32rNPfb74X/U90Bdy4lzp3+X1ukh1BuMxA/EEhDoTOS3l
7ABvc7BYSQubQ2490OcdkIzUh3ZwDrakMVrbaTxUM2p24N6dB+ns2zptWCva6jzW
r8IWKIMxzxLPv5Kt3ePKcUdvkBU/smqujSczTzzSjIoR5QqQA6lN1ZRSnuHIWCvh
JEltkYnTAH41QJ6SAWO66GrrUESwN/cgZzL4JLEqz1Y=
-----END CERTIFICATE-----
@@ -0,0 +1,32 @@
-----BEGIN CERTIFICATE-----
MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBf
MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD
Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw
HhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEY
MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1Ymxp
YyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB
AQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDa
ef0rty2k1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnz
SDBh+oF8HqcIStw+KxwfGExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xf
iOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMPFF1bFOdLvt30yNoDN9HWOaEhUTCDsG3X
ME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vuZDCQOc2TZYEhMbUjUDM3
IuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5QazYw6A3OAS
VYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgE
SJ/AwSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu
+Zd4KKTIRJLpfSYFplhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt
8uaZFURww3y8nDnAtOFr94MlI1fZEoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+L
HaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW6aWWrL3DkJiy4Pmi1KZHQ3xt
zwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWIIUkwDgYDVR0P
AQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c
mTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQ
YKlJfp/imTYpE0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52
gDY9hAaLMyZlbcp+nv4fjFg4exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZA
Fv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M0ejf5lG5Nkc/kLnHvALcWxxPDkjB
JYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI84HxZmduTILA7rpX
DhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9mpFui
TdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5
dHn5HrwdVw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65
LvKRRFHQV80MNNVIIb/bE/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp
0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmmJ1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAY
QqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL
-----END CERTIFICATE-----
-136
View File
@@ -1,136 +0,0 @@
from flask import Flask, flash, request, redirect, send_from_directory, get_flashed_messages
import os
import glob
import securecad_parser
import datetime
from hooks import webhook, alarminator_api, cups_print
from pathlib import Path
from flask_basicauth import BasicAuth
app = Flask(__name__)
app.secret_key = 'super secret key'
app.config['SESSION_TYPE'] = 'filesystem'
app.config['BASIC_AUTH_USERNAME'] = 'admin'
app.config['BASIC_AUTH_PASSWORD'] = os.environ.get('BASIC_AUTH_PASSWORD','password')
basic_auth = BasicAuth(app)
def wrapHtml(innerHtml):
return '''<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>JFW Alarm</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
</head>
<body><div class="container text-center">
''' + flashMessages() +'''
''' + innerHtml + '''
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
</body>
</html>'''
def flashMessages():
messages = get_flashed_messages(with_categories=True)
html = ""
if messages:
for category, message in messages:
html = html + category + ": "+message+"<br>"
return html
def alarmsPath():
dir_path = os.path.dirname(os.path.realpath(__file__))
return dir_path + "/alarms" + os.sep
def getAlarmFiles():
a_list = glob.glob(alarmsPath()+"*.html")
a_list.sort()
return [os.path.basename(f) for f in a_list]
def parseAlarm(f):
f = alarmsPath() + os.sep + f
with open(f, "r") as o:
_r: str = o.read()
now = datetime.datetime.now()
_r = _r.replace('%DATUM%',now.strftime("%d.%m.%Y"))
_r = _r.replace('%UHRZEIT%',now.strftime("%H:%M"))
if _r != '':
return [securecad_parser.parse_securecad_message(_r),_r]
def allowed_file(filename:str):
print(filename.rsplit('.', 1)[1].lower())
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ["html"]
@app.route("/")
@basic_auth.required
def root():
html = ""
file = request.args.get('sendalarm')
if file is not None:
parsed_body, raw = parseAlarm(file)
if parsed_body != None:
if 'ALARMDEPESCHE' in parsed_body:
webhook(parsed_body)
alarminator_api(parsed_body)
cups_print(parsed_body,raw)
flash("Alarm gesendet!")
return redirect('/')
html = html + '<div class="row align-items-start p-3 ">'
i: str
files = getAlarmFiles()
for f in files:
html = html + "<div class=\"col-6\" style='padding: 5px;'><a style='display:block; padding:20px' class=\"btn btn-block btn-danger\" href=\"/?sendalarm="+f+"\">" + f + "</a></div>"
html = html + "</div>"
return wrapHtml(html)
@app.route('/files', methods=['POST'])
@basic_auth.required
def upload_file():
# check if the post request has the file part
print(request.files)
if 'file' not in request.files:
flash('No file part')
return redirect(request.url)
file = request.files['file']
# If the user does not select a file, the browser submits an
# empty file without a filename.
print(file.filename)
if file.filename == '':
flash('No selected file')
return redirect(request.url)
if file and allowed_file(file.filename):
file.save(os.path.join(alarmsPath(), file.filename))
return redirect('/files')
@app.route('/files/<path:filename>', methods=['GET'])
@basic_auth.required
def download(filename):
uploads = alarmsPath()
return send_from_directory(uploads, filename, as_attachment=True)
@app.route("/files", methods=['GET'])
@basic_auth.required
def files():
Path(alarmsPath()).mkdir(parents=True, exist_ok=True)
file = request.args.get('del')
if file is not None:
try:
os.remove(alarmsPath()+os.sep+file)
except:
pass
flash("Datei gelöscht!")
return redirect('/files')
files = getAlarmFiles()
html = '<ul class="list-group">'
for f in files:
html = html + '<li class="list-group-item"><a href="/files/'+f+'">'+f+'</a> <a href="/files?del='+f+'">löschen</a></li>'
html = html + '</ul><form method="post" enctype="multipart/form-data"><input type="file" name="file"><input type=submit value=Upload></form>'
return html
# if __name__ == "__main__":
# app.run(port=5000, host='0.0.0.0')
+55 -54
View File
@@ -21,65 +21,66 @@ Message.register("alarmfax_parser_verarbeitet", alarmfax_parser_verarbeitet)
Message.register("alarmfax_parser_id", alarmfax_parser_id)
#print([f.name for f in Message.FIELDS if f.is_searchable])
threads = {}
def run():
threads = {}
format = "%(asctime)s|%(threadName)s: %(message)s"
logging.basicConfig(format=format, level=logging.INFO,
datefmt="%Y-%m-%d %H:%M:%S")
format = "%(asctime)s|%(threadName)s: %(message)s"
logging.basicConfig(format=format, level=logging.INFO,
datefmt="%Y-%m-%d %H:%M:%S")
def eventHandler(ELEMENT_NAME, item_id, item_changekey):
if (ELEMENT_NAME == 'ModifiedEvent' and IS_DEV) or ELEMENT_NAME == 'NewMailEvent' or ELEMENT_NAME == 'SearchFolderEvent':
logging.info(ELEMENT_NAME + " - get Mail")
m: Message = a.inbox.get(id=item_id, changekey=item_changekey)
if m.alarmfax_parser_verarbeitet and parser_id in ("" if m.alarmfax_parser_id == None else m.alarmfax_parser_id):
logging.info("Mail {} bereits verarbeitet.. ignoriere".format(m.id))
if not IS_DEV:
return
else:
m.alarmfax_parser_verarbeitet = True
m.alarmfax_parser_id = ("" if m.alarmfax_parser_id == None else m.alarmfax_parser_id) + parser_id
m.save(update_fields=["alarmfax_parser_verarbeitet","alarmfax_parser_id"])
logging.info("got Mail {} von {}".format(m.subject, m.sender.email_address))
if m.sender.email_address in filter_from:
parsed_body = parse_securecad_message(m.body)
logging.debug(parsed_body)
if parsed_body != None:
if 'ALARMDEPESCHE' in parsed_body:
logging.info("Alarm für: {}".format(parsed_body['ALARMDEPESCHE']))
webhook(parsed_body)
alarminator_api(parsed_body)
cups_print(parsed_body,m.body)
pass
def eventHandler(ELEMENT_NAME, item_id, item_changekey):
if (ELEMENT_NAME == 'ModifiedEvent' and IS_DEV) or ELEMENT_NAME == 'NewMailEvent' or ELEMENT_NAME == 'SearchFolderEvent':
logging.info(ELEMENT_NAME + " - get Mail")
m: Message = a.inbox.get(id=item_id, changekey=item_changekey)
if m.alarmfax_parser_verarbeitet and parser_id in ("" if m.alarmfax_parser_id == None else m.alarmfax_parser_id):
logging.info("Mail {} bereits verarbeitet.. ignoriere".format(m.id))
if not IS_DEV:
return
else:
m.alarmfax_parser_verarbeitet = True
m.alarmfax_parser_id = ("" if m.alarmfax_parser_id == None else m.alarmfax_parser_id) + parser_id
m.save(update_fields=["alarmfax_parser_verarbeitet","alarmfax_parser_id"])
logging.info("got Mail {} von {}".format(m.subject, m.sender.email_address))
if m.sender.email_address in filter_from:
parsed_body = parse_securecad_message(m.body)
logging.debug(parsed_body)
if parsed_body != None:
if 'ALARMDEPESCHE' in parsed_body:
logging.info("Alarm für: {}".format(parsed_body['ALARMDEPESCHE']))
webhook(parsed_body)
alarminator_api(parsed_body)
cups_print(parsed_body,m.body)
divera_messenger(parsed_body)
pass
def folder_event_subscriber(folder: Folder):
logging.info('folder_event_subscriber startet for Folder: {}'.format(folder.name))
while True:
# filtern des ordners nach mails der letzten 24h, die nicht verarbeitet wurden
now = datetime.datetime.now(a.default_timezone)
folder.all()
folder.all()
filtered_items = folder.filter(
datetime_received__range=(now - datetime.timedelta(days=1), now + datetime.timedelta(days=1))
).exclude(
alarmfax_parser_verarbeitet=True,
alarmfax_parser_id__contains=parser_id
)
cnt = filtered_items.count()
if cnt > 0:
logging.info("{} Mails nicht verarbeitet in den letzten 2 Tagen in ordner: {}".format(cnt, folder.name))
filtered_items = filtered_items.values("id", "changekey")
for m in filtered_items:
t = Thread(target=eventHandler, args=('SearchFolderEvent',m["id"],m["changekey"],),name="eventHandler: SearchFolderEvent ({})".format(m["id"]))
t.start()
# aktives warten auf streaming_events. maximal eine minute lang, dann wird nochmal der ordner durchsucht, falls mails angekommen sind während eines timeout/cooldown.
subscription_id = folder.subscribe_to_streaming()
for notification in folder.get_streaming_events(subscription_id, connection_timeout=1):
for event in notification.events:
if event.item_id != None:
t = Thread(target=eventHandler, args=(event.ELEMENT_NAME,event.item_id.id,event.item_id.changekey,),name="eventHandler: {} ({})".format(event.ELEMENT_NAME, event.item_id.id))
def folder_event_subscriber(folder: Folder):
logging.info('folder_event_subscriber startet for Folder: {}'.format(folder.name))
while True:
# filtern des ordners nach mails der letzten 24h, die nicht verarbeitet wurden
now = datetime.datetime.now(a.default_timezone)
folder.all()
folder.all()
filtered_items = folder.filter(
datetime_received__range=(now - datetime.timedelta(days=1), now + datetime.timedelta(days=1))
).exclude(
alarmfax_parser_verarbeitet=True,
alarmfax_parser_id__contains=parser_id
)
cnt = filtered_items.count()
if cnt > 0:
logging.info("{} Mails nicht verarbeitet in den letzten 2 Tagen in ordner: {}".format(cnt, folder.name))
filtered_items = filtered_items.values("id", "changekey")
for m in filtered_items:
t = Thread(target=eventHandler, args=('SearchFolderEvent',m["id"],m["changekey"],),name="eventHandler: SearchFolderEvent ({})".format(m["id"]))
t.start()
# aktives warten auf streaming_events. maximal eine minute lang, dann wird nochmal der ordner durchsucht, falls mails angekommen sind während eines timeout/cooldown.
subscription_id = folder.subscribe_to_streaming()
for notification in folder.get_streaming_events(subscription_id, connection_timeout=1):
for event in notification.events:
if event.item_id != None:
t = Thread(target=eventHandler, args=(event.ELEMENT_NAME,event.item_id.id,event.item_id.changekey,),name="eventHandler: {} ({})".format(event.ELEMENT_NAME, event.item_id.id))
t.start()
if __name__ == "__main__":
try:
username = os.environ.get('username')
password = os.environ.get('password')
+81 -9
View File
@@ -2,9 +2,12 @@ import os
import logging
import requests
import cups
from weasyprint import HTML
from requests.adapters import Retry
import uuid
import subprocess
import datetime
import time
retries = Retry(total=5,
backoff_factor=0.1,
@@ -68,9 +71,9 @@ def alarminator_api(parsed_body: dict):
gear.append(r['Ressourcen'])
req_string +="&gear={}".format(';'.join(gear))
# req_string +="&district={}".format('district')
# req_string +="&floor={}".format('floor')
# req_string +="&section={}".format('section')
req_string +="&district={}".format('district')
req_string +="&floor={}".format('floor')
req_string +="&section={}".format('section')
req_string +="&keywordRaw={}".format(parsed_body['Einsatzstichwort'])
#req_string +="&keywordId={}".format('keywordId')
req_string +="&keywordCategory={}".format(parsed_body['Einsatzstichwort'].split("(")[0])
@@ -110,6 +113,8 @@ def alarminator_api(parsed_body: dict):
# if False
# req_string +="&lon={}".format() if False
subject = ""
if 'Notfallgeschehen' in parsed_body:
subject = parsed_body['Notfallgeschehen'] + "\n"
if 'Notfallgeschehen' in parsed_body:
subject = parsed_body['Notfallgeschehen'] + "\n"
req_string +="&subject={}".format(subject)
@@ -130,16 +135,17 @@ def generate_pdf(html_body, filename):
os.remove(f)
def cups_print(parsed_body: dict, body: str):
if os.environ.get('IS_DEV') and os.environ.get('IS_DEV') == "True":
generate_pdf(body, "{}.pdf".format(uuid.uuid4()))
fname = "/tmp/{}.pdf".format(uuid.uuid4())
# if os.environ.get('IS_DEV') and os.environ.get('IS_DEV') == "True":
# generate_pdf(body, "{}.pdf".format(uuid.uuid4()))
fname = "{}.pdf".format(uuid.uuid4())
try:
conn = cups.Connection ()
printer_arr = os.environ.get('printer',"DEFAULT").split(";")
print_num = int(os.environ.get('print_num',0))
if printer_arr.__len__() > 0:
generate_pdf(body, fname)
#if printer_arr.__len__() > 0:
generate_pdf(body, fname)
for printer in printer_arr:
print(printer)
if 'ALARMDEPESCHE' in parsed_body:
for i in range(0, print_num):
conn.printFile (printer, fname, "Alarmfax", {})
@@ -147,4 +153,70 @@ def cups_print(parsed_body: dict, body: str):
logging.error("cups_print", e)
finally:
if os.path.exists(fname):
os.remove(fname)
os.remove(fname)
def divera_messenger(parsed_body: dict):
# /api/v2/alarms
# message-channels?accesskey=XXX
try:
api = "https://app.divera247.com/api/v2/"
messenger_api_token = os.environ.get('DIVERA_MESSANGER_API_KEY') if os.environ.get('DIVERA_MESSANGER_API_KEY') else ""
alarmer_api_token = os.environ.get('DIVERA_ALARMER_API_KEY') if os.environ.get('DIVERA_ALARMER_API_KEY') else ""
if messenger_api_token != "":
if 'ALARMDEPESCHE' in parsed_body: # sendAlarm triggern
if 'Einsatzmittelliste' in parsed_body:
zveis = []
message = "Alarmierte Einsatzmittel:\n"
for r in parsed_body['Einsatzmittelliste']:
if r['Typ'] == 'PEALGRP':
zveis.append(r['Ressourcen'])
message +='\n'.join(zveis)
if 'Zusatztext zum Ort' in parsed_body:
message += "\n\nZusatztext zum Ort:\n"+parsed_body['Zusatztext zum Ort']
if 'Zusatztext zum Objekt' in parsed_body:
message += "\n\nZusatztext zum Objekt:\n"+parsed_body['Zusatztext zum Objekt']
logging.info("GET zu {}alarms?accesskey=XXX".format(api))
s = requests.Session()
s.mount('https://', requests.adapters.HTTPAdapter(max_retries=retries))
start = datetime.datetime.now()
message_channel_id = None
alarm_id = None
alarm_text = None
while message_channel_id is None and datetime.datetime.now() < start + datetime.timedelta(minutes=1):
r = s.get("{}alarms?accesskey={}".format(api, messenger_api_token))
r_json = r.json()
if 'items' in r_json.get('data',{}):
for item in r_json.get('data',{})['items']:
if str(r_json['data']['items'][item]['foreign_id']) == str(parsed_body['Auftragsnummer']):
logging.info("EinsatzNr {} in Divera gefunden, sende Nachricht...".format(r_json['data']['items'][item]['foreign_id']))
message_channel_id = r_json['data']['items'][item]['message_channel_id']
alarm_id = r_json['data']['items'][item]['id']
alarm_text = r_json['data']['items'][item]['text']
if message_channel_id is None or alarm_id is None:
time.sleep(20)
if alarmer_api_token != "":
if alarm_text is None:
alarm_text = ""
r = s.put("{}alarms/{}?accesskey={}".format(api, alarm_id, alarmer_api_token),json={
"Alarm": {
"text": alarm_text+"\n\n"+message,
}
})
if message_channel_id:
r = s.post("{}messages?accesskey={}".format(api, messenger_api_token),json={
"Message": {
"message_channel_id": str(message_channel_id),
# "parent_id": 0,
"text": message,
}
})
pass
except Exception as e:
logging.error("divera_messenger", e)
+124
View File
@@ -0,0 +1,124 @@
from datetime import timedelta, datetime
import os
import ssl
import email
import logging
from imapclient import IMAPClient
from threading import Thread
from securecad_parser import parse_securecad_message
from hooks import webhook, alarminator_api, cups_print
import re
from time import sleep
def run():
threads = {}
format = "%(asctime)s|%(threadName)s: %(message)s"
logging.basicConfig(format=format, level=logging.INFO,
datefmt="%Y-%m-%d %H:%M:%S")
def eventHandler(ELEMENT_NAME, uid, message_data, _server: IMAPClient = None):
email_message = email.message_from_bytes(message_data[b'RFC822'])
email_from: list[str] = re.findall(r'([\w\.-]+@[\w\.-]+)', email_message.get('From'))
flags = _server.get_flags(uid)
logging.info(ELEMENT_NAME + " - get Mail")
if 'Processed_{}'.format(parser_id).encode() in flags[uid]:
logging.info("Mail {} bereits verarbeitet.. ignoriere".format(uid))
if not IS_DEV:
return
else:
_server.add_flags(uid, ['\\SEEN','Processed_{}'.format(parser_id)])
logging.info("got Mail {} von {}".format(email_message.get('Subject'), email_from))
if any(mail in filter_from for mail in email_from):
# Get HTML body
html_body = ""
if email_message.is_multipart():
for part in email_message.walk():
if part.get_content_type() == 'text/html':
html_body = part.get_payload(decode=True).decode(part.get_content_charset() or 'utf-8', errors='replace')
break
else:
if email_message.get_content_type() == 'text/html':
html_body = email_message.get_payload(decode=True).decode(email_message.get_content_charset() or 'utf-8', errors='replace')
parsed_body = parse_securecad_message(html_body)
logging.debug(parsed_body)
if parsed_body != None:
if 'ALARMDEPESCHE' in parsed_body:
logging.info("Alarm für: {}".format(parsed_body['ALARMDEPESCHE']))
webhook(parsed_body)
alarminator_api(parsed_body)
cups_print(parsed_body,html_body)
pass
def folder_event_subscriber(folder: str):
logging.info('folder_event_subscriber startet for Folder: {}'.format(folder))
with IMAPClient(server, ssl_context=ssl_context) as _server:
_server.login(username, password)
while True:
# filtern des ordners nach mails der letzten 24h, die nicht verarbeitet wurden
now = datetime.now()
_server.select_folder(folder, readonly=False)
q = ['SENTSINCE', now - timedelta(days=1),'NOT','KEYWORD', 'Processed_{}'.format(parser_id)]
if IS_DEV:
q = ['SENTSINCE', now - timedelta(days=1),'UNSEEN']
q = ['UNSEEN']
messages = _server.search(q)
cnt = messages.__len__()
if cnt > 0:
logging.info("{} Mails nicht verarbeitet in den letzten 2 Tagen in ordner: {}".format(cnt, folder))
for uid, message_data in _server.fetch(messages, 'RFC822').items():
# IMAPClient ist nicht thread-safe, daher wird hier der _server übergeben und kein Thread verwendet.
eventHandler('SearchFolderEvent', uid, message_data, _server)
# t = Thread(target=eventHandler, args=('SearchFolderEvent',uid,message_data,),name="eventHandler: SearchFolderEvent ({})".format(uid))
# t.start()
# aktives warten auf streaming_events. maximal eine minute lang, dann wird nochmal der ordner durchsucht, falls mails angekommen sind während eines timeout/cooldown.
_server.idle()
try:
logging.debug("Idle check for folder: {}".format(folder))
messages = _server.idle_check(timeout=60) # Timeout after 60 seconds
_server.idle_done()
# In den events stehen nur vorhandenen nachrichten. Exists ist nicht die neue Nachricht, sondern eine bereits vorhandene.
# for item in messages:
# if item[1] == b'EXISTS':
# logging.info("New messages in folder: {}".format(folder))
# for uid, message_data in _server.fetch([item[0]], 'RFC822').items():
# if uid:
# eventHandler('NewMailEvent', uid, message_data, _server)
except Exception as e:
logging.error("Error during idle check: {}".format(e))
username = os.environ.get('username')
password = os.environ.get('password')
server = os.environ.get('server')
folders = os.environ.get('folders',"")
parser_id = os.environ.get('alarmfax_parser_id',"")
primary_smtp_address = os.environ.get('primary_smtp_address')
filter_from = os.environ.get('filter_from').split(";") if os.environ.get('filter_from') else []
IS_DEV = True if os.environ.get('IS_DEV') and os.environ.get('IS_DEV') == "True" else False
if IS_DEV:
logging.getLogger().setLevel(logging.INFO)
ssl_context = ssl.create_default_context()
with IMAPClient(server, ssl_context=ssl_context) as _server:
_server.login(username, password)
_server.logout()
folders_to_subscribe = []
for f in folders.split(";"):
if f == "":
folders_to_subscribe.append('INBOX')
else:
folders_to_subscribe.append(f)
while True:
for f in folders_to_subscribe:
if not f in threads or not threads[f].is_alive():
logging.info("folder_event_subscriber for folder \"{}\" not alive, starting".format(f))
t = Thread(target=folder_event_subscriber, args=(f,), daemon=True, name="folder_event_subscriber {}".format(f))
threads[f] = t
t.start()
sleep(1)
+10
View File
@@ -0,0 +1,10 @@
import os
from imap_connect import run as run_imap
from exchange_connect import run as run_ews
mode = os.environ.get('mode')
if mode == 'IMAP':
run_imap()
elif mode == 'EWS':
run_ews()
else:
raise ValueError("Invalid mode specified. Use 'IMAP' or 'EWS'.")
+28 -2
View File
@@ -5,8 +5,10 @@ from numpy import nan
tt_list_alarmdepesche = []
tt_list_infodepesche = []
tt_list_einsatzprotokoll = []
def parse_securecad_message(body_html: str):
body_html = body_html.replace(u'\xa0', u' ')
if 'ALARMDEPESCHE' in body_html:
t_list = {}
tables = pd.read_html(body_html) # Returns list of all tables on page
@@ -25,6 +27,7 @@ def parse_securecad_message(body_html: str):
for r in table_dict:
if table_dict[r][0] is not nan:
t_list[table_dict[r][0].strip(' :')] = table_dict[r][1]
#Einsatzziel
elif t_count > 3 and table_dict[0] is not nan and table_dict[0][0] == 'Einsatzziel' and 'Einsatzziel' not in t_list:
t_list['Einsatzziel'] = {}
for r in table_dict:
@@ -32,6 +35,12 @@ def parse_securecad_message(body_html: str):
t_list['Einsatzziel'][table_dict[r][0].strip(' :')] = table_dict[r][1]
elif t_count > 3 and table_dict[0] is not nan and table_dict[0][0] == 'Einsatzziel' and 'Einsatzziel' in t_list:
continue
elif t_count > 3 and table_dict[0] is not nan and 'Einsatzziel' in t_list and len(t_list['Einsatzziel']) == 0:
#Eigene Tabelle unter 'Einsatzziel'
for r in table_dict:
if table_dict[r][0] is not nan and table_dict[r][0] != 'Einsatzziel':
t_list['Einsatzziel'][table_dict[r][0].strip(' :')] = table_dict[r][1]
#Zusatztext zum Ort
elif t_count > 3 and table_dict[0] is not nan and table_dict[0][0] == 'Zusatztext zum Ort' and 'Zusatztext zum Ort' not in t_list:
t_list['Zusatztext zum Ort'] = []
for r in table_dict:
@@ -39,6 +48,12 @@ def parse_securecad_message(body_html: str):
t_list['Zusatztext zum Ort'].append(table_dict[r][1])
elif t_count > 3 and table_dict[0] is not nan and table_dict[0][0] == 'Zusatztext zum Ort' and 'Zusatztext zum Ort' in t_list:
continue
elif t_count > 3 and table_dict[0] is not nan and 'Zusatztext zum Ort' in t_list and len(t_list['Zusatztext zum Ort']) == 0:
#Eigene Tabelle unter 'Zusatztext zum Ort'
for r in table_dict:
if table_dict[r][0] is not nan and table_dict[r][0] != 'Zusatztext zum Ort':
t_list['Zusatztext zum Ort'].append(table_dict[r][1])
#Zusatztext zum Objekt
elif t_count > 3 and table_dict[0] is not nan and table_dict[0][0] == 'Zusatztext zum Objekt' and 'Zusatztext zum Objekt' not in t_list:
t_list['Zusatztext zum Objekt'] = []
for r in table_dict:
@@ -46,13 +61,24 @@ def parse_securecad_message(body_html: str):
t_list['Zusatztext zum Objekt'].append(table_dict[r][1])
elif t_count > 3 and table_dict[0] is not nan and table_dict[0][0] == 'Zusatztext zum Objekt' and 'Zusatztext zum Objekt' in t_list:
continue
elif t_count > 3 and table_dict[0] is not nan and 'Zusatztext zum Objekt' in t_list and len(t_list['Zusatztext zum Objekt']) == 0:
#Eigene Tabelle unter 'Zusatztext zum Objekt'
for r in table_dict:
if table_dict[r][0] is not nan and table_dict[r][0] != 'Zusatztext zum Objekt':
t_list['Zusatztext zum Objekt'].append(table_dict[r][1])
elif t_count > 3 and table_dict[0] is not nan and table_dict[0][0] == 'Einsatzmittelliste':
# es folgt die Einsatzmittelliste
continue
elif t_count > 3 and table_dict[0] is not nan and table_dict[0][0] == 'Ressourcen'and table_dict[0][1] == 'Typ' and table_dict[0][2] == 'Organisation':
elif t_count > 3 and table_dict[0] is not nan and len(table_dict)>1 and (
# In der ersten Zeile steht "Ressourcen, Typ, Organisation"
(table_dict[0][0] == 'Ressourcen'and table_dict[0][1] == 'Typ' and table_dict[0][2] == 'Organisation' )
or
# In der ersten Zeile steht "Alarmierte Ressourcen", in der zweiten Zeile steht "Ressourcen, Typ, Organisation"
(table_dict[1][0] == 'Ressourcen'and table_dict[1][1] == 'Typ' and table_dict[1][2] == 'Organisation' )
):
t_list['Einsatzmittelliste'] = []
for r in table_dict:
if table_dict[r][0] != "Ressourcen":
if table_dict[r][0] != "Ressourcen" and table_dict[r][0] != 'Alarmierte Ressourcen':
t_list['Einsatzmittelliste'].append({
'Ressourcen': table_dict[r][0] if table_dict[r][0] is not nan else "",
'Typ': table_dict[r][1] if table_dict[r][1] is not nan else "",
+4 -6
View File
@@ -2,13 +2,11 @@ version: "2"
services:
app:
build: ./
#image: gitea.simonzeyer.de/simon/wnd_ils_alarmfax_parser:latest
image: gitea.simonzeyer.de/simon/wnd_ils_alarmfax_parser:latest
restart: always
privileged: true
# ports:
# - 631:631
# - 5000:5000
environment:
- mode=${mode}
- username=${username}
- password=${password}
- server=${server}
@@ -23,8 +21,8 @@ services:
- alarminator_zvies_use_PEALGRP=${alarminator_zvies_use_PEALGRP}
- printer=${printer}
- print_num=${print_num}
- MAPS_API_KEY=${MAPS_API_KEY}
- BASIC_AUTH_PASSWORD=${BASIC_AUTH_PASSWORD}
- DIVERA_MESSANGER_API_KEY=${DIVERA_MESSANGER_API_KEY}
- DIVERA_ALARMER_API_KEY=${DIVERA_ALARMER_API_KEY}
volumes:
- ./cups:/etc/cups
- ./cupsd.conf.txt:/etc/cups/cupsd.conf
+3 -19
View File
@@ -1,35 +1,20 @@
blinker==1.8.2
Brotli==1.1.0
cached-property==1.5.2
certifi==2022.12.7
cffi==1.15.1
charset-normalizer==3.0.1
click==8.1.7
cryptography==39.0.0
cssselect2==0.7.0
defusedxml==0.7.1
dnspython==2.3.0
exchangelib==4.9.0
Flask==3.0.3
Flask-BasicAuth==0.2.0
fonttools==4.54.1
html5lib==1.1
idna==3.4
isodate==0.6.1
itsdangerous==2.2.0
Jinja2==3.1.4
lxml==4.9.2
MarkupSafe==3.0.1
ntlm-auth==1.5.0
numpy==1.24.1
oauthlib==3.2.2
pandas==1.5.2
pillow==10.4.0
pycparser==2.21
pycups==2.0.1
pydyf==0.11.0
Pygments==2.14.0
pyphen==0.16.0
python-dateutil==2.8.2
pytz==2022.7.1
pytz-deprecation-shim==0.1.0.post0
@@ -37,12 +22,11 @@ requests==2.28.2
requests-ntlm==1.1.0
requests-oauthlib==1.3.1
six==1.16.0
tinycss2==1.3.0
tzdata==2022.7
tzlocal==4.2
urllib3==1.26.14
webencodings==0.5.1
Werkzeug==3.0.4
xmltodict==0.12.0
xmltojson==2.0.1
zopfli==0.2.3
pycups==2.0.1
weasyprint
imapclient==3.0.1