""" Overzicht van alle lopende e-mailmachtigingen (Gmail-delegaties) van de school. Google heeft geen lijst "alle delegaties van het domein": je kan ze enkel per mailbox opvragen (users.settings.delegates.list, in naam van de eigenaar). Daarom loopt er een achtergrondtaak die alle actieve (niet-geschorste) gebruikers afgaat, met een paar parallelle verzoeken. Het resultaat komt in de tabel gmail_delegaties en wordt: - volledig vernieuwd met de knop op de pagina, of vanzelf als het ouder is dan VERVERS_UREN; - meteen bijgewerkt als in PIVOT een machtiging toegevoegd of ingetrokken wordt, of als je een mailbox opent. """ import sys import threading from concurrent.futures import ThreadPoolExecutor from datetime import datetime from database import execute_query from tenancy import school_singleton, met_school, huidige_school, in_school VERVERS_UREN = 24 PARALLEL = 8 class DelegatieOverzicht: def __new__(cls): def init(inst): inst._lock = threading.Lock() inst.status = {'bezig': False, 'voortgang': 0, 'totaal': 0, 'fout': None, 'resultaat': None} return school_singleton(cls, init) @staticmethod def ensure_table(): execute_query(""" CREATE TABLE IF NOT EXISTS gmail_delegaties ( Mailbox VARCHAR(255) NOT NULL, Mailbox_naam VARCHAR(255) NULL, Gedelegeerde VARCHAR(255) NOT NULL, Gedelegeerde_naam VARCHAR(255) NULL, Status VARCHAR(20) NULL, Opgehaald DATETIME NOT NULL, PRIMARY KEY (Mailbox, Gedelegeerde), KEY idx_gmail_deleg_gedelegeerde (Gedelegeerde) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci""", commit=True) execute_query("CREATE TABLE IF NOT EXISTS gmail_delegaties_scan (ID TINYINT NOT NULL PRIMARY KEY, " "Opgehaald DATETIME NULL, Mailboxen INT NULL, Fouten INT NULL) ENGINE=InnoDB", commit=True) # ---------------- opvragen ---------------- def alle(self): return execute_query("SELECT * FROM gmail_delegaties ORDER BY Mailbox, Gedelegeerde") or [] def laatst_opgehaald(self): rij = execute_query("SELECT Opgehaald, Mailboxen, Fouten FROM gmail_delegaties_scan WHERE ID = 1") return rij[0] if rij else None def verouderd(self): laatst = self.laatst_opgehaald() return not laatst or not laatst['Opgehaald'] or \ (datetime.now() - laatst['Opgehaald']).total_seconds() > VERVERS_UREN * 3600 # ---------------- één mailbox bijwerken (na toevoegen/intrekken/openen in PIVOT) ---------------- def bewaar_mailbox(self, mailbox, naam, delegates, namen=None): """Vervangt de rijen van één mailbox door de opgegeven gedelegeerden [{'email','status'}].""" try: self.ensure_table() # Namen die we al kennen (uit de volledige ophaling) behouden bekend = {} for r in execute_query("SELECT Mailbox, Mailbox_naam, Gedelegeerde, Gedelegeerde_naam FROM gmail_delegaties") or []: if r['Mailbox_naam']: bekend[r['Mailbox'].lower()] = r['Mailbox_naam'] if r['Gedelegeerde_naam']: bekend[r['Gedelegeerde'].lower()] = r['Gedelegeerde_naam'] namen = {**bekend, **(namen or {})} execute_query("DELETE FROM gmail_delegaties WHERE Mailbox = %s", (mailbox,), commit=True) for d in delegates: execute_query("INSERT INTO gmail_delegaties (Mailbox, Mailbox_naam, Gedelegeerde, Gedelegeerde_naam, " "Status, Opgehaald) VALUES (%s, %s, %s, %s, %s, NOW())", (mailbox, naam, d['email'], namen.get(d['email'].lower()) or d.get('naam'), d.get('status')), commit=True) except Exception as e: sys.stderr.write(f"Delegaties: mailbox {mailbox} niet bewaard: {e}\n") # ---------------- alles ophalen uit Google ---------------- def _gebruikers(self, directory): uit, token = [], None while True: resp = directory.users().list(customer='my_customer', maxResults=500, pageToken=token, fields='nextPageToken,users(primaryEmail,name/fullName,suspended)').execute() for u in resp.get('users', []): uit.append({'email': u['primaryEmail'].lower(), 'naam': (u.get('name') or {}).get('fullName', ''), 'geschorst': bool(u.get('suspended'))}) token = resp.get('nextPageToken') if not token: return uit def sync(self): if not self._lock.acquire(blocking=False): return False, "Er loopt al een ophaling." try: from services.gmail_delegation_service import GmailDelegationService, DelegationError self.status.update(bezig=True, voortgang=0, totaal=0, fout=None, resultaat=None) self.ensure_table() svc = GmailDelegationService() gebruikers = self._gebruikers(svc._directory_api()) namen = {u['email']: u['naam'] for u in gebruikers} actief = [u for u in gebruikers if not u['geschorst']] self.status['totaal'] = len(actief) school = huidige_school() fouten, rechtenfout, gelukt = [], [], [] def een(u): # Krijgt Google telkens dezelfde rechtenfout, dan niet alle gebruikers blijven proberen if len(rechtenfout) >= 3 and not gelukt: self.status['voortgang'] += 1 return u, None with in_school(school): try: delegates = svc.list_delegates(u['email']) gelukt.append(1) return u, delegates except DelegationError as e: tekst = str(e) if 'serviceaccount mist een Gmail-recht' in tekst or 'Gmail API staat nog niet aan' in tekst: rechtenfout.append(tekst) fouten.append(u['email']) return u, None except Exception: fouten.append(u['email']) return u, None finally: self.status['voortgang'] += 1 resultaten = [] with ThreadPoolExecutor(max_workers=PARALLEL) as pool: for u, delegates in pool.map(een, actief): if delegates: resultaten.append((u, delegates)) if rechtenfout and not gelukt: raise RuntimeError(rechtenfout[0]) nu = datetime.now() execute_query("DELETE FROM gmail_delegaties", commit=True) aantal = 0 for u, delegates in resultaten: for d in delegates: execute_query("INSERT INTO gmail_delegaties (Mailbox, Mailbox_naam, Gedelegeerde, Gedelegeerde_naam, " "Status, Opgehaald) VALUES (%s, %s, %s, %s, %s, %s)", (u['email'], u['naam'], d['email'], namen.get(d['email'].lower()), d['status'], nu), commit=True) aantal += 1 execute_query("INSERT INTO gmail_delegaties_scan (ID, Opgehaald, Mailboxen, Fouten) VALUES (1, %s, %s, %s) " "ON DUPLICATE KEY UPDATE Opgehaald = VALUES(Opgehaald), Mailboxen = VALUES(Mailboxen), " "Fouten = VALUES(Fouten)", (nu, len(actief), len(fouten)), commit=True) msg = f"{len(actief)} mailboxen nagekeken: {aantal} machtiging(en) bij {len(resultaten)} mailbox(en)." if fouten: msg += f" {len(fouten)} mailbox(en) konden niet bevraagd worden (bv. zonder Gmail-licentie)." self.status['resultaat'] = msg return True, msg except Exception as e: self.status['fout'] = str(e) return False, f"Ophalen mislukt: {e}" finally: self.status['bezig'] = False self._lock.release() def sync_op_achtergrond(self): if self.status['bezig']: return False self.status.update(bezig=True, voortgang=0, totaal=0, fout=None) threading.Thread(target=met_school(self.sync), daemon=True).start() return True