init project
This commit is contained in:
parent
3b4176fa0a
commit
b66a9d0d2c
12 changed files with 721 additions and 492 deletions
122
README.md
122
README.md
|
|
@ -1,18 +1,26 @@
|
|||
# mail-quota-warning
|
||||
# Fragify
|
||||
|
||||
Small script to check a configured list of IMAP accounts for mailbox quota and send
|
||||
a warning mail in case a specific threashold is exceeded.
|
||||
Eine einfache Web-Anwendung, um vorausgefüllte Links für Anfragen bei [FragDenStaat.de](https://fragdenstaat.de) zu generieren, die du an Freund:innen schicken kannst.
|
||||
|
||||
## Was ist Fragify?
|
||||
|
||||
Fragify ist ein webbasiertes Tool, das es dir ermöglicht, schnell und einfach Anfragen bei deutschen Behörden über das Informationsfreiheitsportal FragDenStaat.de zu erstellen. Du kannst:
|
||||
|
||||
- Nach Behörden suchen und auswählen
|
||||
- Betreff und Inhalt der Anfrage vorausfüllen
|
||||
- Einen fertigen Link generieren, der alle Informationen enthält
|
||||
- Den Link mit anderen teilen, die dann nur noch auf "Senden" klicken müssen
|
||||
|
||||
## Installation
|
||||
|
||||
### NixOS
|
||||
|
||||
Add the module to your `flake.nix`:
|
||||
Füge das Modul zu deiner `flake.nix` hinzu:
|
||||
|
||||
```nix
|
||||
{
|
||||
inputs = {
|
||||
mail-quota-warning.url = "git+https://git.project-insanity.org/onny/mail-quota-warning.git";
|
||||
fragify.url = "git+https://git.project-insanity.org/onny/fragify.git";
|
||||
[...]
|
||||
};
|
||||
|
||||
|
|
@ -22,12 +30,12 @@ Add the module to your `flake.nix`:
|
|||
system = "x86_64-linux";
|
||||
specialArgs.inputs = inputs;
|
||||
modules = [
|
||||
inputs.mail-quota-warning.nixosModule
|
||||
inputs.fragify.nixosModule
|
||||
|
||||
({ pkgs, ... }:{
|
||||
|
||||
nixpkgs.overlays = [
|
||||
inputs.mail-quota-warning.overlay
|
||||
inputs.fragify.overlay
|
||||
];
|
||||
|
||||
})
|
||||
|
|
@ -40,52 +48,78 @@ Add the module to your `flake.nix`:
|
|||
}
|
||||
```
|
||||
|
||||
Add this to your `configuration.nix` file
|
||||
Füge dies zu deiner `configuration.nix` hinzu:
|
||||
|
||||
```nix
|
||||
environment.etc."mail-quota-warning-secrets.yml".text = ''
|
||||
accounts:
|
||||
- name: Sales
|
||||
imap_server: mail.example.com
|
||||
imap_port: 993
|
||||
username: sales@example.com
|
||||
password: secret
|
||||
|
||||
- name: Support
|
||||
imap_server: mail.example.com
|
||||
imap_port: 993
|
||||
username: support@example.com
|
||||
password: secret
|
||||
|
||||
mail:
|
||||
smtp_server: mail.example.com
|
||||
smtp_port: 587
|
||||
smtp_username: monitoring@example.com
|
||||
smtp_password: secret
|
||||
from_address: monitoring@example.com
|
||||
recipients:
|
||||
- admin1@example.com
|
||||
- admin2@example.com
|
||||
'';
|
||||
|
||||
services.mail-quota-warning = {
|
||||
services.fragify = {
|
||||
enable = true;
|
||||
settings = {
|
||||
CHECK_INTERVAL_DAYS = 7;
|
||||
QUOTA_WARNING_THRESHOLD_PERCENT = 80;
|
||||
# Konfiguration hier
|
||||
};
|
||||
secretFile = "/etc/mail-quota-warning-secrets.yml";
|
||||
};
|
||||
```
|
||||
|
||||
Replace setting variables according to your setup.
|
||||
### Von der Quelle
|
||||
|
||||
### From source
|
||||
|
||||
```
|
||||
cd mail-quota-warning
|
||||
```bash
|
||||
cd fragify
|
||||
nix develop
|
||||
export CHECK_INTERVAL_DAYS=7
|
||||
export QUOTA_WARNING_THRESHOLD_PERCENT=80
|
||||
nix run
|
||||
```
|
||||
|
||||
Öffne dann deinen Browser und navigiere zu: http://localhost:8000
|
||||
|
||||
## Verwendung
|
||||
|
||||
1. **Behörde auswählen**: Suche und wähle die gewünschte Behörde aus dem Dropdown-Menü
|
||||
2. **Betreff eingeben**: Gib einen aussagekräftigen Betreff für deine Anfrage ein
|
||||
3. **Anfrage beschreiben**: Beschreibe detailliert, welche Dokumente oder Informationen du anfragen möchtest
|
||||
4. **Link generieren**: Klicke auf "Anfrage Link generieren"
|
||||
5. **Link teilen**: Kopiere den generierten Link und teile ihn mit anderen
|
||||
|
||||
## Technische Details
|
||||
|
||||
- **Framework**: Falcon (Python)
|
||||
- **Frontend**: Bootstrap 5 mit modernem Design
|
||||
- **API**: Integration mit der FragDenStaat.de API
|
||||
- **Styling**: Responsive Design mit Gradient-Hintergrund
|
||||
|
||||
## API-Integration
|
||||
|
||||
Fragify nutzt die offizielle [FragDenStaat.de API](https://fragdenstaat.de/api/) um:
|
||||
|
||||
- Behörden zu durchsuchen
|
||||
- Links zu generieren, die das Anfrage-Formular vorausfüllen
|
||||
- Die korrekte URL-Struktur von FragDenStaat.de zu verwenden
|
||||
|
||||
## Entwicklung
|
||||
|
||||
### Lokale Entwicklung
|
||||
|
||||
```bash
|
||||
# Entwicklungsumgebung starten
|
||||
nix develop
|
||||
|
||||
# Anwendung starten
|
||||
python fragify.py
|
||||
```
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
- Python 3.8+
|
||||
- Falcon (Web-Framework)
|
||||
- Requests (HTTP-Client)
|
||||
|
||||
## Lizenz
|
||||
|
||||
Dieses Projekt steht unter der gleichen Lizenz wie FragDenStaat.de.
|
||||
|
||||
## Beitragen
|
||||
|
||||
Beiträge sind willkommen! Bitte erstelle einen Pull Request oder öffne ein Issue.
|
||||
|
||||
## Links
|
||||
|
||||
- [FragDenStaat.de](https://fragdenstaat.de) - Das Hauptportal
|
||||
- [FragDenStaat API](https://fragdenstaat.de/api/) - API-Dokumentation
|
||||
- [Informationsfreiheitsgesetz](https://fragdenstaat.de/informationsfreiheit/) - Rechtliche Grundlagen
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
check_interval_days: 7 # Minimum days between warnings for same account
|
||||
quota_warning_threshold_percent: 80
|
||||
|
||||
accounts:
|
||||
- name: Sales
|
||||
imap_server: mail.example.com
|
||||
imap_port: 993
|
||||
username: sales@example.com
|
||||
password: secret
|
||||
|
||||
- name: Support
|
||||
imap_server: mail.example.com
|
||||
imap_port: 993
|
||||
username: support@example.com
|
||||
password: secret
|
||||
|
||||
mail:
|
||||
smtp_server: mail.example.com
|
||||
smtp_port: 587
|
||||
smtp_username: monitoring@example.com
|
||||
smtp_password: secret
|
||||
from_address: monitoring@example.com
|
||||
recipients:
|
||||
- admin1@example.com
|
||||
- admin2@example.com
|
||||
6
flake.lock
generated
6
flake.lock
generated
|
|
@ -2,11 +2,11 @@
|
|||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1754563854,
|
||||
"narHash": "sha256-YzNTExe3kMY9lYs23mZR7jsVHe5TWnpwNrsPOpFs/b8=",
|
||||
"lastModified": 1755471983,
|
||||
"narHash": "sha256-axUoWcm4cNQ36jOlnkD9D40LTfSQgk8ExfHSRm3rTtg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e728d7ae4bb6394bbd19eec52b7358526a44c414",
|
||||
"rev": "48f4c982de68d966421d2b6f1ddbeb6227cc5ceb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
|||
23
flake.nix
23
flake.nix
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
description = "mail-quota-warning package and service";
|
||||
description = "fragify package and service";
|
||||
|
||||
inputs.nixpkgs.url = "nixpkgs/nixos-25.05";
|
||||
|
||||
|
|
@ -16,8 +16,8 @@
|
|||
);
|
||||
in {
|
||||
overlay = final: prev: {
|
||||
mail-quota-warning = with final; python3Packages.buildPythonApplication {
|
||||
pname = "mail-quota-warning";
|
||||
fragify = with final; python3Packages.buildPythonApplication {
|
||||
pname = "fragify";
|
||||
version = "0.0.1";
|
||||
format = "other";
|
||||
|
||||
|
|
@ -25,23 +25,26 @@
|
|||
|
||||
dependencies = with python3Packages; [
|
||||
python
|
||||
pyyaml
|
||||
imaplib2
|
||||
falcon
|
||||
requests
|
||||
jinja2
|
||||
];
|
||||
|
||||
installPhase = ''
|
||||
install -Dm755 ${./mail-quota-warning.py} $out/bin/mail-quota-warning
|
||||
install -Dm755 ${./fragify.py} $out/bin/fragify
|
||||
mkdir -p $out/share/fragify
|
||||
cp -r ${./templates} $out/share/fragify/
|
||||
'';
|
||||
|
||||
meta.mainProgram = "mail-quota-warning";
|
||||
meta.mainProgram = "fragify";
|
||||
};
|
||||
};
|
||||
|
||||
packages = forAllSystems (system: {
|
||||
inherit (nixpkgsFor.${system}) mail-quota-warning;
|
||||
inherit (nixpkgsFor.${system}) fragify;
|
||||
});
|
||||
|
||||
defaultPackage = forAllSystems (system: self.packages.${system}.mail-quota-warning);
|
||||
defaultPackage = forAllSystems (system: self.packages.${system}.fragify);
|
||||
|
||||
devShells = forAllSystems (system: let
|
||||
pkgs = import nixpkgs { inherit system; overlays = [ self.overlay ]; };
|
||||
|
|
@ -53,7 +56,7 @@
|
|||
];
|
||||
});
|
||||
|
||||
# mail-quota-warning service module
|
||||
# fragify service module
|
||||
nixosModule = (import ./module.nix);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
185
fragify.py
Normal file
185
fragify.py
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Fragify - A web application to generate prefilled FragDenStaat.de request links
|
||||
"""
|
||||
|
||||
import falcon
|
||||
import json
|
||||
import requests
|
||||
from urllib.parse import urlencode
|
||||
import os
|
||||
import sys
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
class BaseTemplateResource:
|
||||
"""Base class for resources that need template rendering"""
|
||||
|
||||
def _get_template_dir(self):
|
||||
"""Get the template directory path, handling both development and installed environments"""
|
||||
# Get the directory where this script is located
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# Try development templates first
|
||||
dev_template_dir = os.path.join(script_dir, 'templates')
|
||||
if os.path.exists(dev_template_dir):
|
||||
return dev_template_dir
|
||||
|
||||
# Try to find templates relative to the executable
|
||||
try:
|
||||
# If we're running from a Nix store, look for templates in share/fragify
|
||||
if '/nix/store/' in script_dir:
|
||||
# Go up from bin to share/fragify/templates
|
||||
share_dir = os.path.join(script_dir, '..', 'share', 'fragify', 'templates')
|
||||
if os.path.exists(share_dir):
|
||||
return share_dir
|
||||
|
||||
# Alternative: look for templates in the same store path
|
||||
store_root = script_dir.split('/nix/store/')[1].split('/')[0]
|
||||
store_path = f"/nix/store/{store_root}"
|
||||
alt_share_dir = os.path.join(store_path, 'share', 'fragify', 'templates')
|
||||
if os.path.exists(alt_share_dir):
|
||||
return alt_share_dir
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Last resort: try to find any templates directory
|
||||
for root, dirs, files in os.walk('/nix/store'):
|
||||
if 'templates' in dirs and 'index.html' in os.listdir(os.path.join(root, 'templates')):
|
||||
return os.path.join(root, 'templates')
|
||||
|
||||
# Fallback to current directory
|
||||
return dev_template_dir
|
||||
|
||||
class FragifyApp(BaseTemplateResource):
|
||||
def __init__(self):
|
||||
self.fragdenstaat_api = "https://fragdenstaat.de/api/v1"
|
||||
# Setup Jinja2 template environment
|
||||
template_dir = self._get_template_dir()
|
||||
print(f"Using template directory: {template_dir}")
|
||||
self.jinja_env = Environment(loader=FileSystemLoader(template_dir))
|
||||
|
||||
def on_get(self, req, resp):
|
||||
"""Serve the main page"""
|
||||
template = self.jinja_env.get_template('index.html')
|
||||
resp.content_type = 'text/html; charset=utf-8'
|
||||
resp.text = template.render()
|
||||
|
||||
def on_post(self, req, resp):
|
||||
"""Handle form submission and generate link"""
|
||||
try:
|
||||
# Parse form data
|
||||
form_data = req.get_media()
|
||||
publicbody_id = form_data.get('publicbody_id', '')
|
||||
subject = form_data.get('subject', '')
|
||||
body = form_data.get('body', '')
|
||||
|
||||
# Generate FragDenStaat.de link
|
||||
base_url = "https://fragdenstaat.de/anfrage-stellen/"
|
||||
if publicbody_id:
|
||||
base_url += f"an/{publicbody_id}/"
|
||||
|
||||
params = {}
|
||||
if subject:
|
||||
params['subject'] = subject
|
||||
if body:
|
||||
params['body'] = body
|
||||
|
||||
if params:
|
||||
base_url += "?" + urlencode(params)
|
||||
|
||||
resp.content_type = 'application/json'
|
||||
resp.text = json.dumps({
|
||||
'success': True,
|
||||
'link': base_url
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
resp.status = falcon.HTTP_500
|
||||
resp.content_type = 'application/json'
|
||||
resp.text = json.dumps({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
})
|
||||
|
||||
class ImpressumResource(BaseTemplateResource):
|
||||
def __init__(self):
|
||||
template_dir = self._get_template_dir()
|
||||
self.jinja_env = Environment(loader=FileSystemLoader(template_dir))
|
||||
|
||||
def on_get(self, req, resp):
|
||||
"""Serve the Impressum page"""
|
||||
template = self.jinja_env.get_template('impressum.html')
|
||||
resp.content_type = 'text/html; charset=utf-8'
|
||||
resp.text = template.render()
|
||||
|
||||
class DatenschutzResource(BaseTemplateResource):
|
||||
def __init__(self):
|
||||
template_dir = self._get_template_dir()
|
||||
self.jinja_env = Environment(loader=FileSystemLoader(template_dir))
|
||||
|
||||
def on_get(self, req, resp):
|
||||
"""Serve the Datenschutz page"""
|
||||
template = self.jinja_env.get_template('datenschutz.html')
|
||||
resp.content_type = 'text/html; charset=utf-8'
|
||||
resp.text = template.render()
|
||||
|
||||
class PublicBodiesResource:
|
||||
def __init__(self):
|
||||
self.fragdenstaat_api = "https://fragdenstaat.de/api/v1"
|
||||
|
||||
def on_get(self, req, resp):
|
||||
"""API endpoint to search public bodies"""
|
||||
try:
|
||||
search = req.get_param('search', default='')
|
||||
page = req.get_param('page', default=1)
|
||||
|
||||
# Build API URL
|
||||
url = f"{self.fragdenstaat_api}/publicbody/"
|
||||
params = {
|
||||
'limit': 20,
|
||||
'offset': (int(page) - 1) * 20
|
||||
}
|
||||
|
||||
if search:
|
||||
params['q'] = search
|
||||
|
||||
# Make request to FragDenStaat API
|
||||
response = requests.get(url, params=params, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
|
||||
resp.content_type = 'application/json'
|
||||
resp.text = json.dumps(data)
|
||||
|
||||
except Exception as e:
|
||||
resp.status = falcon.HTTP_500
|
||||
resp.content_type = 'application/json'
|
||||
resp.text = json.dumps({
|
||||
'error': str(e),
|
||||
'results': [],
|
||||
'next': None
|
||||
})
|
||||
|
||||
# Create Falcon application
|
||||
app = falcon.App()
|
||||
|
||||
# Add routes
|
||||
fragify = FragifyApp()
|
||||
impressum = ImpressumResource()
|
||||
datenschutz = DatenschutzResource()
|
||||
publicbodies = PublicBodiesResource()
|
||||
|
||||
app.add_route('/', fragify)
|
||||
app.add_route('/impressum', impressum)
|
||||
app.add_route('/datenschutz', datenschutz)
|
||||
app.add_route('/api/publicbodies', publicbodies)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import wsgiref.simple_server
|
||||
|
||||
print("Starting Fragify web application...")
|
||||
print("Open your browser and navigate to: http://localhost:8000")
|
||||
|
||||
httpd = wsgiref.simple_server.make_server('localhost', 8000, app)
|
||||
httpd.serve_forever()
|
||||
|
|
@ -1,295 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
import imaplib2
|
||||
import smtplib
|
||||
import yaml
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import argparse
|
||||
from email.mime.text import MIMEText
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
CONFIG_FILE = "config.yml"
|
||||
STATE_FILE = "quota_state.json"
|
||||
|
||||
# TODO
|
||||
# - load config from file
|
||||
# - override with env vars
|
||||
|
||||
def get_config_value(config, env_var, config_key, default_value, value_type=int):
|
||||
"""Get configuration value from environment variable or config file, with fallback to default"""
|
||||
env_value = os.environ.get(env_var)
|
||||
if env_value is not None:
|
||||
try:
|
||||
return value_type(env_value)
|
||||
except ValueError:
|
||||
log(f"Invalid value for {env_var}: {env_value}, using config/default")
|
||||
|
||||
return config.get(config_key, default_value)
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description="Email quota monitoring script")
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
default="config.yml",
|
||||
help="Path to config.yml file (default: config.yml in current directory)"
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
def load_config(config_file):
|
||||
if not os.path.exists(config_file):
|
||||
log(f"Config file not found: {config_file}")
|
||||
raise FileNotFoundError(f"Config file not found: {config_file}")
|
||||
|
||||
with open(config_file, "r") as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
def load_state():
|
||||
state_file = "quota_state.json"
|
||||
if os.path.exists(state_file):
|
||||
with open(state_file, "r") as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
def save_state(state):
|
||||
state_file = "quota_state.json"
|
||||
with open(state_file, "w") as f:
|
||||
json.dump(state, f, indent=2)
|
||||
|
||||
def log(msg):
|
||||
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {msg}")
|
||||
|
||||
def format_bytes(kb):
|
||||
"""Convert KB to human readable format"""
|
||||
if kb >= 1024 * 1024: # GB
|
||||
return f"{kb / (1024 * 1024):.1f} GB"
|
||||
elif kb >= 1024: # MB
|
||||
return f"{kb / 1024:.1f} MB"
|
||||
else: # KB
|
||||
return f"{kb} KB"
|
||||
|
||||
def check_quota(account):
|
||||
"""
|
||||
Uses IMAP QUOTA command (RFC 2087) to get mailbox usage with imaplib2.
|
||||
Tries multiple approaches to get quota information.
|
||||
Returns dict with quota info or None if not supported.
|
||||
"""
|
||||
try:
|
||||
log(f"Checking quota for account: {account['name']} ({account['username']})")
|
||||
|
||||
# Create imaplib2 connection
|
||||
mail = imaplib2.IMAP4_SSL(account['imap_server'], account['imap_port'])
|
||||
|
||||
# Login
|
||||
typ, data = mail.login(account['username'], account['password'])
|
||||
if typ != 'OK':
|
||||
log(f"Login failed for {account['name']}: {data}")
|
||||
return None
|
||||
|
||||
# Try different quota roots (quietly)
|
||||
quota_roots = ["INBOX", account['username'], f"user/{account['username']}", f"user.{account['username']}"]
|
||||
|
||||
for quota_root in quota_roots:
|
||||
try:
|
||||
typ, data = mail.getquota(quota_root)
|
||||
|
||||
if typ == "OK" and data and data[0]:
|
||||
# Parse quota response
|
||||
quota_info = data[0].decode() if isinstance(data[0], bytes) else str(data[0])
|
||||
|
||||
# Handle different response formats
|
||||
# Format 1: (STORAGE used limit) or (MESSAGE used limit)
|
||||
if quota_info.startswith('('):
|
||||
parts = quota_info.replace("(", "").replace(")", "").split()
|
||||
if len(parts) >= 3:
|
||||
resource_type = parts[0] # STORAGE or MESSAGE
|
||||
used = int(parts[1])
|
||||
limit = int(parts[2])
|
||||
|
||||
if limit > 0 and resource_type == "STORAGE":
|
||||
percent_used = (used / limit) * 100
|
||||
log(f"Quota usage: {percent_used:.1f}% ({format_bytes(used)} of {format_bytes(limit)})")
|
||||
mail.logout()
|
||||
return {
|
||||
'percent_used': percent_used,
|
||||
'used_kb': used,
|
||||
'limit_kb': limit
|
||||
}
|
||||
|
||||
# Format 2: Multiple resources in one response
|
||||
elif "STORAGE" in quota_info:
|
||||
import re
|
||||
storage_match = re.search(r'STORAGE\s+(\d+)\s+(\d+)', quota_info)
|
||||
if storage_match:
|
||||
used = int(storage_match.group(1))
|
||||
limit = int(storage_match.group(2))
|
||||
if limit > 0:
|
||||
percent_used = (used / limit) * 100
|
||||
log(f"Quota usage: {percent_used:.1f}% ({format_bytes(used)} of {format_bytes(limit)})")
|
||||
mail.logout()
|
||||
return {
|
||||
'percent_used': percent_used,
|
||||
'used_kb': used,
|
||||
'limit_kb': limit
|
||||
}
|
||||
|
||||
except imaplib2.IMAP4.error:
|
||||
# Silently try next quota root
|
||||
continue
|
||||
|
||||
# Try GETQUOTAROOT command as alternative
|
||||
try:
|
||||
typ, data = mail.getquotaroot("INBOX")
|
||||
if typ == "OK" and data:
|
||||
# Parse quotaroot response which might give us quota info
|
||||
for item in data:
|
||||
item_str = item.decode() if isinstance(item, bytes) else str(item)
|
||||
if "STORAGE" in item_str:
|
||||
import re
|
||||
storage_match = re.search(r'STORAGE\s+(\d+)\s+(\d+)', item_str)
|
||||
if storage_match:
|
||||
used = int(storage_match.group(1))
|
||||
limit = int(storage_match.group(2))
|
||||
if limit > 0:
|
||||
percent_used = (used / limit) * 100
|
||||
log(f"Quota usage: {percent_used:.1f}% ({format_bytes(used)} of {format_bytes(limit)})")
|
||||
mail.logout()
|
||||
return {
|
||||
'percent_used': percent_used,
|
||||
'used_kb': used,
|
||||
'limit_kb': limit
|
||||
}
|
||||
except imaplib2.IMAP4.error:
|
||||
pass
|
||||
|
||||
mail.logout()
|
||||
log(f"No quota data available for {account['name']}")
|
||||
return None
|
||||
|
||||
except imaplib2.IMAP4.error as e:
|
||||
log(f"IMAP error checking quota for {account['name']}: {e}")
|
||||
except Exception as e:
|
||||
log(f"Error checking quota for {account['name']}: {e}")
|
||||
|
||||
return None
|
||||
|
||||
def should_send_warning(state, account_name, interval_days):
|
||||
last_sent_str = state.get(account_name)
|
||||
if not last_sent_str:
|
||||
return True
|
||||
|
||||
last_sent = datetime.fromisoformat(last_sent_str)
|
||||
return datetime.now() - last_sent >= timedelta(days=interval_days)
|
||||
|
||||
def send_warning(config, triggered_accounts, all_quotas, threshold):
|
||||
mail_cfg = config["mail"]
|
||||
|
||||
# Create subject based on number of accounts
|
||||
if len(triggered_accounts) == 1:
|
||||
account_name, quota_info = list(triggered_accounts.items())[0]
|
||||
subject = f"[Quota Warning] {account_name} mailbox usage at {quota_info['percent_used']:.1f}%"
|
||||
else:
|
||||
subject = f"[Quota Warning] {len(triggered_accounts)} mailbox(es) over threshold"
|
||||
|
||||
# Build email body
|
||||
body_lines = []
|
||||
|
||||
if len(triggered_accounts) == 1:
|
||||
account_name, quota_info = list(triggered_accounts.items())[0]
|
||||
body_lines.append(f"The mailbox for account '{account_name}' has reached {quota_info['percent_used']:.1f}% of its quota.")
|
||||
body_lines.append(f"Usage: {format_bytes(quota_info['used_kb'])} of {format_bytes(quota_info['limit_kb'])}")
|
||||
else:
|
||||
body_lines.append(f"The following mailboxes have exceeded the quota threshold ({threshold}%):")
|
||||
body_lines.append("")
|
||||
for account_name, quota_info in triggered_accounts.items():
|
||||
body_lines.append(f"• {account_name}: {quota_info['percent_used']:.1f}% ({format_bytes(quota_info['used_kb'])} of {format_bytes(quota_info['limit_kb'])})")
|
||||
|
||||
body_lines.append("")
|
||||
body_lines.append("--- All Account Summary ---")
|
||||
|
||||
# Sort accounts by usage percentage (descending)
|
||||
sorted_quotas = sorted(all_quotas.items(), key=lambda x: x[1]['percent_used'] if x[1] else 0, reverse=True)
|
||||
|
||||
for account_name, quota_info in sorted_quotas:
|
||||
if quota_info:
|
||||
status = "⚠️ " if account_name in triggered_accounts else "✓ "
|
||||
body_lines.append(f"{status}{account_name}: {quota_info['percent_used']:.1f}% ({format_bytes(quota_info['used_kb'])} of {format_bytes(quota_info['limit_kb'])})")
|
||||
else:
|
||||
body_lines.append(f"? {account_name}: Quota info unavailable")
|
||||
|
||||
body_lines.append("")
|
||||
body_lines.append("Please take action to free up space for accounts over the threshold.")
|
||||
|
||||
body = "\n".join(body_lines)
|
||||
|
||||
msg = MIMEText(body)
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = mail_cfg["from_address"]
|
||||
msg["To"] = ", ".join(mail_cfg["recipients"])
|
||||
|
||||
try:
|
||||
server = smtplib.SMTP(mail_cfg["smtp_server"], mail_cfg["smtp_port"])
|
||||
server.starttls()
|
||||
server.login(mail_cfg["smtp_username"], mail_cfg["smtp_password"])
|
||||
server.sendmail(mail_cfg["from_address"], mail_cfg["recipients"], msg.as_string())
|
||||
server.quit()
|
||||
account_names = ", ".join(triggered_accounts.keys())
|
||||
log(f"Warning email sent for: {account_names}")
|
||||
except Exception as e:
|
||||
log(f"Error sending warning email: {e}")
|
||||
|
||||
def check_account_quota(account, config, state, threshold, interval_days):
|
||||
"""
|
||||
Check quota for a single account (can be run in thread)
|
||||
"""
|
||||
quota_info = check_quota(account)
|
||||
if quota_info is None:
|
||||
log(f"Quota info not available for {account['name']}")
|
||||
return None, None
|
||||
|
||||
percent_used = quota_info['percent_used']
|
||||
|
||||
if percent_used >= threshold:
|
||||
if should_send_warning(state, account["name"], interval_days):
|
||||
return account["name"], quota_info
|
||||
else:
|
||||
log(f"Warning already sent recently for {account['name']}, skipping.")
|
||||
else:
|
||||
log(f"Quota usage for {account['name']} is below threshold ({percent_used:.1f}%).")
|
||||
|
||||
return None, quota_info
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
config = load_config(args.config)
|
||||
state = load_state()
|
||||
interval_days = get_config_value(config, "CHECK_INTERVAL_DAYS", "check_interval_days", 7, int)
|
||||
threshold = get_config_value(config, "QUOTA_WARNING_THRESHOLD_PERCENT", "quota_warning_threshold_percent", 80, int)
|
||||
|
||||
# For thread-safe state updates
|
||||
state_lock = threading.Lock()
|
||||
|
||||
# Track all accounts and those that need warnings
|
||||
triggered_accounts = {} # account_name: quota_info
|
||||
all_quotas = {} # account_name: quota_info (or None)
|
||||
|
||||
for account in config["accounts"]:
|
||||
warning_result, quota_info = check_account_quota(account, config, state, threshold, interval_days)
|
||||
|
||||
# Store quota info for summary
|
||||
all_quotas[account["name"]] = quota_info
|
||||
|
||||
# Track accounts that need warnings
|
||||
if warning_result:
|
||||
triggered_accounts[account["name"]] = quota_info
|
||||
with state_lock:
|
||||
state[account["name"]] = datetime.now().isoformat()
|
||||
|
||||
# Send consolidated warning email if any accounts triggered
|
||||
if triggered_accounts:
|
||||
send_warning(config, triggered_accounts, all_quotas, threshold)
|
||||
|
||||
save_state(state)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
96
module.nix
96
module.nix
|
|
@ -12,71 +12,13 @@ in
|
|||
{
|
||||
|
||||
options = {
|
||||
services.mail-quota-warning = {
|
||||
services.fragify = {
|
||||
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Enable mail-quota-warning daemon.
|
||||
'';
|
||||
};
|
||||
|
||||
settings = lib.mkOption {
|
||||
type = lib.types.submodule {
|
||||
freeformType = with lib.types; attrsOf anything;
|
||||
options = {
|
||||
CHECK_INTERVAL_DAYS = lib.mkOption {
|
||||
default = 7;
|
||||
type = lib.types.int;
|
||||
description = ''
|
||||
Interval of days in which a warning message will be
|
||||
delivered.
|
||||
'';
|
||||
};
|
||||
QUOTA_WARNING_THRESHOLD_PERCENT = lib.mkOption {
|
||||
default = 80;
|
||||
type = lib.types.int;
|
||||
description = ''
|
||||
Threshold of used mailbox space in percent after which
|
||||
a warning message will be delivered.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
default = { };
|
||||
description = ''
|
||||
Extra options which should be used by the mailbox quota warning script.
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
CHECK_INTERVAL_DAYS = 7;
|
||||
QUOTA_WARNING_THRESHOLD_PERCENT = 80;
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
secretFile = lib.mkOption {
|
||||
type = lib.types.nullOr (lib.types.pathWith {
|
||||
inStore = false;
|
||||
absolute = true;
|
||||
});
|
||||
default = null;
|
||||
example = "/run/keys/mail-quota-warning-secrets";
|
||||
description = ''
|
||||
A YAML file containing secrets, see example config file
|
||||
in the repository.
|
||||
'';
|
||||
};
|
||||
|
||||
interval = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "*:00,30:00";
|
||||
description = ''
|
||||
How often we run the sync. Default is half an hour.
|
||||
|
||||
The format is described in
|
||||
{manpage}`systemd.time(7)`.
|
||||
Enable fragify web application.
|
||||
'';
|
||||
};
|
||||
|
||||
|
|
@ -85,31 +27,32 @@ in
|
|||
|
||||
config = lib.mkIf cfg.enable {
|
||||
|
||||
systemd.services."mail-quota-warning" = {
|
||||
description = "mail-quota-warning script";
|
||||
systemd.services."fragify" = {
|
||||
description = "fragify web application";
|
||||
after = [ "network.target" ];
|
||||
wants = [ "network-online.target" ];
|
||||
environment = {
|
||||
PYTHONUNBUFFERED = "1";
|
||||
}
|
||||
// lib.mapAttrs (_: v: toString v) cfg.settings;
|
||||
};
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
ExecStart = "${lib.getExe pkgs.mail-quota-warning}${lib.optionalString (cfg.secretFile != null) " --config ${cfg.secretFile}"}";
|
||||
WorkingDirectory = "%S/mail-quota-warning";
|
||||
StateDirectory = "mail-quota-warning";
|
||||
ExecStart = "${lib.getExe pkgs.fragify}";
|
||||
WorkingDirectory = "%S/fragify";
|
||||
StateDirectory = "fragify";
|
||||
User = "fragify";
|
||||
Group = "fragify";
|
||||
|
||||
# hardening
|
||||
AmbientCapabilities = "";
|
||||
CapabilityBoundingSet = "";
|
||||
DevicePolicy = "closed";
|
||||
DynamicUser = true;
|
||||
DynamicUser = false;
|
||||
LockPersonality = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
NoNewPrivileges = true;
|
||||
PrivateDevices = true;
|
||||
PrivateTmp = true;
|
||||
PrivateUsers = true;
|
||||
PrivateUsers = false;
|
||||
ProcSubset = "pid";
|
||||
ProtectClock = true;
|
||||
ProtectControlGroups = true;
|
||||
|
|
@ -137,16 +80,15 @@ in
|
|||
};
|
||||
};
|
||||
|
||||
systemd.timers.mail-quota-warning = {
|
||||
timerConfig = {
|
||||
OnCalendar = [
|
||||
""
|
||||
cfg.interval
|
||||
];
|
||||
};
|
||||
wantedBy = [ "timers.target" ];
|
||||
# Create fragify user and group
|
||||
users.users.fragify = {
|
||||
isSystemUser = true;
|
||||
group = "fragify";
|
||||
description = "fragify web application user";
|
||||
};
|
||||
|
||||
users.groups.fragify = {};
|
||||
|
||||
};
|
||||
|
||||
meta = {
|
||||
|
|
|
|||
138
templates/base.html
Normal file
138
templates/base.html
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Fragify{% endblock %}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
.main-container {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||
padding: 3rem;
|
||||
margin: 2rem auto;
|
||||
max-width: 800px;
|
||||
}
|
||||
.title {
|
||||
color: #2c3e50;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.description {
|
||||
color: #7f8c8d;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 2.5rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.form-control, .form-select {
|
||||
border-radius: 10px;
|
||||
border: 2px solid #e9ecef;
|
||||
padding: 0.75rem 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.form-control:focus, .form-select:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
|
||||
}
|
||||
.btn-primary {
|
||||
background: linear-gradient(45deg, #667eea, #764ba2);
|
||||
border: none;
|
||||
border-radius: 15px;
|
||||
padding: 1rem 2rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
.result-link {
|
||||
background: #f8f9fa;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
.loading {
|
||||
display: none;
|
||||
}
|
||||
.footer {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
.footer-links a {
|
||||
font-size: 0.9rem;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
.footer-links a:hover {
|
||||
color: #667eea !important;
|
||||
}
|
||||
.footer-text {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.footer-text a:hover {
|
||||
color: #667eea !important;
|
||||
}
|
||||
.legal-content {
|
||||
text-align: left;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.legal-content h2 {
|
||||
color: #2c3e50;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.legal-content p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.legal-content ul {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="main-container">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer mt-5 pt-4 border-top">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 text-center">
|
||||
<div class="footer-links mb-2">
|
||||
<a href="/impressum" class="text-muted text-decoration-none me-3">Impressum</a>
|
||||
<a href="/datenschutz" class="text-muted text-decoration-none me-3">Datenschutz</a>
|
||||
<a href="https://git.project-insanity.org/onny/fragify" class="text-muted text-decoration-none" target="_blank">Source</a>
|
||||
</div>
|
||||
<div class="footer-text">
|
||||
<small class="text-muted">
|
||||
Projekt von <a href="https://project-insanity.org" class="text-muted text-decoration-none" target="_blank">Project-Insanity.org</a>,
|
||||
follow us on <a href="https://social.project-insanity.org/@pi_crew" class="text-muted text-decoration-none" target="_blank">Mastodon</a> :)
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
100
templates/datenschutz.html
Normal file
100
templates/datenschutz.html
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Datenschutz - Fragify{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="text-center">
|
||||
<h1 class="title display-4">Datenschutzerklärung</h1>
|
||||
</div>
|
||||
|
||||
<div class="legal-content">
|
||||
<h2>1. Datenschutz auf einen Blick</h2>
|
||||
|
||||
<h3>Allgemeine Hinweise</h3>
|
||||
<p>
|
||||
Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit Ihren personenbezogenen Daten passiert, wenn Sie diese Website besuchen. Personenbezogene Daten sind alle Daten, mit denen Sie persönlich identifiziert werden können.
|
||||
</p>
|
||||
|
||||
<h2>2. Datenerfassung auf dieser Website</h2>
|
||||
|
||||
<h3>Wer ist verantwortlich für die Datenerfassung auf dieser Website?</h3>
|
||||
<p>
|
||||
Die Datenverarbeitung auf dieser Website erfolgt durch den Websitebetreiber. Dessen Kontaktdaten können Sie dem Abschnitt „Hinweis zur Verantwortlichen Stelle" in dieser Datenschutzerklärung entnehmen.
|
||||
</p>
|
||||
|
||||
<h3>Wie erfassen wir Ihre Daten?</h3>
|
||||
<p>
|
||||
Ihre Daten werden zum einen dadurch erhoben, dass Sie uns diese mitteilen. Hierbei kann es sich z. B. um Daten handeln, die Sie in ein Kontaktformular eingeben.
|
||||
</p>
|
||||
<p>
|
||||
Andere Daten werden automatisch oder nach Ihrer Einwilligung beim Besuch der Website durch unsere IT-Systeme erfasst. Das sind vor allem technische Daten (z. B. Internetbrowser, Betriebssystem oder Uhrzeit des Seitenaufrufs).
|
||||
</p>
|
||||
|
||||
<h3>Wofür nutzen wir Ihre Daten?</h3>
|
||||
<p>
|
||||
Ein Teil der Daten wird erhoben, um eine fehlerfreie Bereitstellung der Website zu gewährleisten. Andere Daten können zur Analyse Ihres Nutzerverhaltens verwendet werden.
|
||||
</p>
|
||||
|
||||
<h3>Welche Rechte haben Sie bezüglich Ihrer Daten?</h3>
|
||||
<p>
|
||||
Sie haben jederzeit das Recht, unentgeltlich Auskunft über Herkunft, Empfänger und Zweck Ihrer gespeicherten personenbezogenen Daten zu erhalten. Sie haben außerdem ein Recht, die Berichtigung oder Löschung dieser Daten zu verlangen. Wenn Sie eine Einwilligung zur Datenverarbeitung erteilt haben, können Sie diese Einwilligung jederzeit für die Zukunft widerrufen. Außerdem haben Sie das Recht, unter bestimmten Umständen die Einschränkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen.
|
||||
</p>
|
||||
|
||||
<h2>3. Hosting</h2>
|
||||
<p>
|
||||
Wir hosten unsere Website bei uns selbst. Es werden keine Daten an externe Hosting-Dienste weitergegeben.
|
||||
</p>
|
||||
|
||||
<h2>4. Allgemeine Hinweise und Pflichtinformationen</h2>
|
||||
|
||||
<h3>Datenschutz</h3>
|
||||
<p>
|
||||
Die Betreiber dieser Seiten nehmen den Schutz Ihrer persönlichen Daten sehr ernst. Wir behandeln Ihre personenbezogenen Daten vertraulich und entsprechend den gesetzlichen Datenschutzvorschriften sowie dieser Datenschutzerklärung.
|
||||
</p>
|
||||
|
||||
<h2>5. Datenerfassung auf dieser Website</h2>
|
||||
|
||||
<h3>Cookies</h3>
|
||||
<p>
|
||||
<strong>Diese Website verwendet keine Cookies.</strong> Es werden keine Tracking-Cookies oder Analyse-Cookies gesetzt. Die Website funktioniert vollständig ohne Cookie-Speicherung.
|
||||
</p>
|
||||
|
||||
<h3>Server-Log-Dateien</h3>
|
||||
<p>
|
||||
Der Provider der Seiten erhebt und speichert automatisch Informationen in so genannten Server-Log-Dateien, die Ihr Browser automatisch an uns übermittelt. Dies sind:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Browsertyp und Browserversion</li>
|
||||
<li>verwendetes Betriebssystem</li>
|
||||
<li>Referrer URL</li>
|
||||
<li>Hostname des zugreifenden Rechners</li>
|
||||
<li>Uhrzeit der Serveranfrage</li>
|
||||
<li>IP-Adresse</li>
|
||||
</ul>
|
||||
<p>
|
||||
Eine Zusammenführung dieser Daten mit anderen Datenquellen wird nicht vorgenommen.
|
||||
</p>
|
||||
|
||||
<h2>6. API-Nutzung</h2>
|
||||
<p>
|
||||
Diese Website nutzt die öffentliche API von FragDenStaat.de, um Behördeninformationen abzurufen. Bei der Nutzung der Suchfunktion werden Ihre Suchanfragen an die FragDenStaat.de API weitergeleitet. Es werden keine persönlichen Daten an FragDenStaat.de übertragen, außer den reinen Suchbegriffen.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Wichtig:</strong> Wir speichern keine Ihrer Suchanfragen oder generierten Links auf unseren Servern. Alle Daten werden nur temporär im Browser verarbeitet und nicht dauerhaft gespeichert.
|
||||
</p>
|
||||
|
||||
<h2>7. Kontakt</h2>
|
||||
<p>
|
||||
Bei Fragen zur Erhebung, Verarbeitung oder Nutzung Ihrer personenbezogenen Daten wenden Sie sich bitte an:
|
||||
</p>
|
||||
<p>
|
||||
Jonas Heinrich<br>
|
||||
E-Mail: <a href="mailto:onny@project-insanity.org">onny@project-insanity.org</a>
|
||||
</p>
|
||||
|
||||
<h2>8. Änderungen</h2>
|
||||
<p>
|
||||
Wir behalten uns vor, diese Datenschutzerklärung anzupassen, damit sie stets den aktuellen rechtlichen Anforderungen entspricht oder um Änderungen unserer Leistungen in der Datenschutzerklärung umzusetzen, z. B. bei der Einführung neuer Services.
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
47
templates/impressum.html
Normal file
47
templates/impressum.html
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Impressum - Fragify{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="text-center">
|
||||
<h1 class="title display-4">Impressum</h1>
|
||||
</div>
|
||||
|
||||
<div class="legal-content">
|
||||
<h2>Angaben gemäß § 5 TMG</h2>
|
||||
<p>
|
||||
<strong>Jonas Heinrich</strong><br>
|
||||
Erzbergerstraße 9<br>
|
||||
76133 Karlsruhe<br>
|
||||
Deutschland
|
||||
</p>
|
||||
|
||||
<h2>Kontakt</h2>
|
||||
<p>
|
||||
E-Mail: <a href="mailto:onny@project-insanity.org">onny@project-insanity.org</a>
|
||||
</p>
|
||||
|
||||
<h2>Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV</h2>
|
||||
<p>
|
||||
Jonas Heinrich<br>
|
||||
Erzbergerstraße 9<br>
|
||||
76133 Karlsruhe
|
||||
</p>
|
||||
|
||||
<h2>Haftungsausschluss</h2>
|
||||
<h3>Haftung für Inhalte</h3>
|
||||
<p>
|
||||
Die Inhalte unserer Seiten wurden mit größter Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte können wir jedoch keine Gewähr übernehmen.
|
||||
</p>
|
||||
|
||||
<h3>Haftung für Links</h3>
|
||||
<p>
|
||||
Unser Angebot enthält Links zu externen Webseiten Dritter, auf deren Inhalte wir keinen Einfluss haben. Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich.
|
||||
</p>
|
||||
|
||||
<h3>Urheberrecht</h3>
|
||||
<p>
|
||||
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers.
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
138
templates/index.html
Normal file
138
templates/index.html
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Fragify{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="text-center">
|
||||
<h1 class="title display-4">Fragify</h1>
|
||||
<p class="description">
|
||||
Erstelle einfach Links für Anfragen bei dem Portal
|
||||
<a href="https://fragdenstaat.de" target="_blank" class="text-decoration-none">FragDenStaat.de</a>,
|
||||
welche du vorausfüllen und an Freund:innen schicken kannst!
|
||||
</p>
|
||||
|
||||
<form id="fragifyForm" class="text-start">
|
||||
<div class="mb-4">
|
||||
<label for="publicbody" class="form-label fw-bold">Behörde</label>
|
||||
<select class="form-select" id="publicbody" name="publicbody_id" required>
|
||||
<option value="">Behörde auswählen...</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="subject" class="form-label fw-bold">Betreff</label>
|
||||
<input type="text" class="form-control" id="subject" name="subject"
|
||||
placeholder="Betreff der Anfrage" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="body" class="form-label fw-bold">Dokumente anfragen:</label>
|
||||
<textarea class="form-control" id="body" name="body" rows="5"
|
||||
placeholder="Beschreibe hier, welche Dokumente oder Informationen du anfragen möchtest..." required></textarea>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
<span class="btn-text">Anfrage Link generieren</span>
|
||||
<span class="loading spinner-border spinner-border-sm ms-2" role="status"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="result" class="mt-4" style="display: none;">
|
||||
<h5 class="text-success mb-3">Link erfolgreich generiert!</h5>
|
||||
<div class="result-link">
|
||||
<a href="" id="generatedLink" target="_blank"></a>
|
||||
</div>
|
||||
<button class="btn btn-outline-primary mt-3" onclick="copyToClipboard()">
|
||||
Link kopieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize Select2 for public bodies
|
||||
$('#publicbody').select2({
|
||||
theme: 'bootstrap-5',
|
||||
placeholder: 'Behörde auswählen...',
|
||||
allowClear: true,
|
||||
ajax: {
|
||||
url: '/api/publicbodies',
|
||||
dataType: 'json',
|
||||
delay: 250,
|
||||
data: function(params) {
|
||||
return {
|
||||
search: params.term,
|
||||
page: params.page || 1
|
||||
};
|
||||
},
|
||||
processResults: function(data, params) {
|
||||
params.page = params.page || 1;
|
||||
return {
|
||||
results: data.results.map(function(item) {
|
||||
return {
|
||||
id: item.id,
|
||||
text: item.name + ' (' + item.jurisdiction + ')'
|
||||
};
|
||||
}),
|
||||
pagination: {
|
||||
more: data.next !== null
|
||||
}
|
||||
};
|
||||
},
|
||||
cache: true
|
||||
}
|
||||
});
|
||||
|
||||
// Handle form submission
|
||||
$('#fragifyForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
const submitBtn = $('button[type="submit"]');
|
||||
const btnText = submitBtn.find('.btn-text');
|
||||
const loading = submitBtn.find('.loading');
|
||||
|
||||
// Show loading state
|
||||
btnText.hide();
|
||||
loading.show();
|
||||
submitBtn.prop('disabled', true);
|
||||
|
||||
fetch('/', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
$('#generatedLink').attr('href', data.link).text(data.link);
|
||||
$('#result').show();
|
||||
$('#fragifyForm')[0].reset();
|
||||
$('#publicbody').val(null).trigger('change');
|
||||
} else {
|
||||
alert('Fehler: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Fehler beim Generieren des Links: ' + error);
|
||||
})
|
||||
.finally(() => {
|
||||
// Hide loading state
|
||||
btnText.show();
|
||||
loading.hide();
|
||||
submitBtn.prop('disabled', false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function copyToClipboard() {
|
||||
const link = document.getElementById('generatedLink').href;
|
||||
navigator.clipboard.writeText(link).then(function() {
|
||||
alert('Link wurde in die Zwischenablage kopiert!');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
{ pkgs, ... }:
|
||||
{
|
||||
|
||||
environment.etc."mail-quota-warning-secrets.yml".text = ''
|
||||
accounts:
|
||||
- name: Sales
|
||||
imap_server: mail.example.com
|
||||
imap_port: 993
|
||||
username: sales@example.com
|
||||
password: secret
|
||||
|
||||
- name: Support
|
||||
imap_server: mail.example.com
|
||||
imap_port: 993
|
||||
username: support@example.com
|
||||
password: secret
|
||||
|
||||
mail:
|
||||
smtp_server: mail.example.com
|
||||
smtp_port: 587
|
||||
smtp_username: monitoring@example.com
|
||||
smtp_password: secret
|
||||
from_address: monitoring@example.com
|
||||
recipients:
|
||||
- admin1@example.com
|
||||
- admin2@example.com
|
||||
'';
|
||||
|
||||
services.mail-quota-warning = {
|
||||
enable = true;
|
||||
settings = {
|
||||
CHECK_INTERVAL_DAYS = 7;
|
||||
QUOTA_WARNING_THRESHOLD_PERCENT = 80;
|
||||
};
|
||||
secretFile = "/etc/mail-quota-warning-secrets.yml";
|
||||
};
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue