diff --git a/README.md b/README.md index ddae5f2..624afb1 100644 --- a/README.md +++ b/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 diff --git a/config.yml.example b/config.yml.example deleted file mode 100644 index dfc6526..0000000 --- a/config.yml.example +++ /dev/null @@ -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 diff --git a/flake.lock b/flake.lock index 752db5b..48ce956 100644 --- a/flake.lock +++ b/flake.lock @@ -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": { diff --git a/flake.nix b/flake.nix index 9379539..bbf0a9f 100644 --- a/flake.nix +++ b/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); }; } diff --git a/fragify.py b/fragify.py new file mode 100644 index 0000000..92d4ed5 --- /dev/null +++ b/fragify.py @@ -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() diff --git a/mail-quota-warning.py b/mail-quota-warning.py deleted file mode 100644 index 26fb045..0000000 --- a/mail-quota-warning.py +++ /dev/null @@ -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() diff --git a/module.nix b/module.nix index e9f322b..24635e2 100644 --- a/module.nix +++ b/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 = { diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..8cc621c --- /dev/null +++ b/templates/base.html @@ -0,0 +1,138 @@ + + + + + + {% block title %}Fragify{% endblock %} + + + + + {% block extra_css %}{% endblock %} + + +
+
+ {% block content %}{% endblock %} +
+ + + +
+ + + + + + {% block extra_js %}{% endblock %} + + \ No newline at end of file diff --git a/templates/datenschutz.html b/templates/datenschutz.html new file mode 100644 index 0000000..14e046b --- /dev/null +++ b/templates/datenschutz.html @@ -0,0 +1,100 @@ +{% extends "base.html" %} + +{% block title %}Datenschutz - Fragify{% endblock %} + +{% block content %} +
+

Datenschutzerklärung

+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/impressum.html b/templates/impressum.html new file mode 100644 index 0000000..06a6b1d --- /dev/null +++ b/templates/impressum.html @@ -0,0 +1,47 @@ +{% extends "base.html" %} + +{% block title %}Impressum - Fragify{% endblock %} + +{% block content %} +
+

Impressum

+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..0b5d52a --- /dev/null +++ b/templates/index.html @@ -0,0 +1,138 @@ +{% extends "base.html" %} + +{% block title %}Fragify{% endblock %} + +{% block content %} +
+

Fragify

+

+ Erstelle einfach Links für Anfragen bei dem Portal + FragDenStaat.de, + welche du vorausfüllen und an Freund:innen schicken kannst! +

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ + +
+{% endblock %} + +{% block extra_js %} + +{% endblock %} \ No newline at end of file diff --git a/vm-mail-quota-warning.py b/vm-mail-quota-warning.py deleted file mode 100644 index 93bc22e..0000000 --- a/vm-mail-quota-warning.py +++ /dev/null @@ -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"; - }; - -}