""" Gmail-delegatie: andere gebruikers toegang geven tot een mailbox. Werkt via de Gmail API (users.settings.delegates). Het service-account handelt in naam van de EIGENAAR van de mailbox (de delegator). Vereist (eenmalig): 1. Admin-console › Beveiliging › API-beheer › Domeinbrede delegatie: scope toevoegen aan het service-account: https://www.googleapis.com/auth/gmail.settings.sharing 2. Admin-console › Apps › Google Workspace › Gmail › Gebruikersinstellingen: 'E-maildelegatie' inschakelen voor de betrokken organisatie-eenheden. Beperkingen van Google: - de gedelegeerde moet in dezelfde Google Workspace-organisatie zitten - max. 25 gedelegeerden per mailbox, max. 10 mailboxen per gedelegeerde - het kan tot 1 minuut duren voor de toegang actief is """ import sys from googleapiclient.discovery import build from googleapiclient.errors import HttpError from config import Config from database import execute_query from tenancy import google_credentials # Toevoegen/verwijderen van gedelegeerden: gmail.settings.sharing. # Opvragen (delegates.list/get): gmail.settings.basic (sharing volstaat daar NIET). GMAIL_SCOPE = 'https://www.googleapis.com/auth/gmail.settings.sharing' GMAIL_LEES_SCOPE = 'https://www.googleapis.com/auth/gmail.settings.basic' MAX_DELEGATES = 25 STATUS_LABELS = { 'accepted': 'Actief', 'pending': 'In afwachting', 'rejected': 'Geweigerd', 'expired': 'Verlopen', } class DelegationError(Exception): pass class GmailDelegationService: def __init__(self, directory_service=None): # Bestaande Directory-client hergebruiken om adressen/aliassen op te zoeken self._directory = directory_service # ---------- clients ---------- def _gmail_for(self, mailbox, scope=GMAIL_SCOPE): creds = google_credentials([scope], subject=mailbox) try: # meegeleverde API-beschrijving: geen extra verzoek naar Google per mailbox return build('gmail', 'v1', credentials=creds, static_discovery=True, cache_discovery=False) except Exception: return build('gmail', 'v1', credentials=creds, static_discovery=False, cache_discovery=False) def _directory_api(self): if self._directory is None: from services.google_service import GoogleAdminService self._directory = GoogleAdminService().service return self._directory # ---------- hulpfuncties ---------- @staticmethod def _friendly_error(e, mailbox=None, delegate=None): msg = str(e) status = getattr(getattr(e, 'resp', None), 'status', None) if 'unauthorized_client' in msg or 'access_denied' in msg or 'insufficient authentication scopes' in msg.lower(): return ("Google weigert de toegang: het serviceaccount mist een Gmail-recht. Voeg in admin.google.com › " "Beveiliging › Toegangs- en gegevensbeheer › API-beheer › Domeinbrede delegatie bij het serviceaccount " f"deze twee bereiken toe: {GMAIL_LEES_SCOPE} en {GMAIL_SCOPE}. Het kan enkele minuten duren voor dit werkt.") if status == 409: return f"{delegate} heeft al toegang tot {mailbox}." laag = msg.lower() reden = msg.split('returned', 1)[-1].strip(' ">')[:200] if 'returned' in msg else msg[:200] if 'has not been used' in laag or 'is disabled' in laag or 'accessnotconfigured' in laag: return ("De Gmail API staat nog niet aan in het Google Cloud-project van het serviceaccount " "(console.cloud.google.com › API's en services › Bibliotheek › Gmail API › Inschakelen). " f"Google: {reden}") if 'mail service not enabled' in laag or 'failedprecondition' in laag or 'precondition' in laag: return (f"{mailbox or 'Deze gebruiker'} heeft (nog) geen werkende Gmail-mailbox: controleer of Gmail aanstaat " "en of de gebruiker een licentie heeft. Een net aangemaakt account kan tot 24 uur nodig hebben. " f"Google: {reden}") if 'delegation denied' in laag or 'not allowed' in laag or 'delegation is not allowed' in laag: return ("Delegatie is niet toegestaan voor deze gebruiker. Controleer of 'E-maildelegatie' aanstaat " "(Admin-console › Apps › Google Workspace › Gmail › Gebruikersinstellingen) voor de OU van de " "mailbox én van de gedelegeerde. Een gewijzigde instelling kan tot 24 uur nodig hebben. " f"Google: {reden}") if status == 403: return f"Google weigert deze actie (403). Google: {reden}" if status == 400 and 'limit' in msg.lower(): return "Limiet bereikt: max. 25 gedelegeerden per mailbox en max. 10 mailboxen per gedelegeerde." if status == 400: return f"Google weigert deze delegatie: {getattr(e, 'reason', '') or msg}" if status == 404: return "Gebruiker of delegatie niet gevonden." return msg def resolve_user(self, email): """Zoekt de gebruiker op in Google en geeft het PRIMAIRE adres terug (aliassen werken niet bij delegatie).""" email = (email or '').strip().lower() if not email or '@' not in email: raise DelegationError("Geef een volledig e-mailadres op.") try: u = self._directory_api().users().get(userKey=email).execute() except HttpError as e: if getattr(e.resp, 'status', None) == 404: raise DelegationError(f"{email} bestaat niet in jullie Google Workspace. " "Delegeren naar externe (bv. @gmail.com) adressen kan niet.") raise DelegationError(self._friendly_error(e)) return { 'email': u['primaryEmail'].lower(), 'naam': (u.get('name') or {}).get('fullName', ''), 'ou': u.get('orgUnitPath', ''), 'geschorst': bool(u.get('suspended')), 'is_alias': u['primaryEmail'].lower() != email, } def search_users(self, query): """Gebruikers zoeken op naam of adres (voor de zoekbox).""" q = query.strip() try: resp = self._directory_api().users().list( customer='my_customer', query=q, maxResults=15, orderBy='email').execute() except Exception as e: sys.stderr.write(f"Zoeken mislukt: {e}\n") return [] return [{'email': u['primaryEmail'], 'naam': (u.get('name') or {}).get('fullName', ''), 'ou': u.get('orgUnitPath', '')} for u in resp.get('users', [])] # ---------- delegaties ---------- def list_delegates(self, mailbox): try: resp = self._gmail_for(mailbox, GMAIL_LEES_SCOPE).users().settings().delegates().list(userId='me').execute() except HttpError as e: raise DelegationError(self._friendly_error(e, mailbox)) except Exception as e: raise DelegationError(self._friendly_error(e, mailbox)) result = [] for d in resp.get('delegates', []): status = d.get('verificationStatus', '') result.append({'email': d.get('delegateEmail', ''), 'status': status, 'status_label': STATUS_LABELS.get(status, status)}) result.sort(key=lambda d: d['email']) return result def add_delegate(self, mailbox, delegate): if mailbox == delegate: raise DelegationError("Een mailbox kan niet aan zichzelf gedelegeerd worden.") try: self._gmail_for(mailbox).users().settings().delegates().create( userId='me', body={'delegateEmail': delegate}).execute() except HttpError as e: raise DelegationError(self._friendly_error(e, mailbox, delegate)) except Exception as e: raise DelegationError(self._friendly_error(e, mailbox, delegate)) def remove_delegate(self, mailbox, delegate): try: self._gmail_for(mailbox).users().settings().delegates().delete( userId='me', delegateEmail=delegate).execute() except HttpError as e: raise DelegationError(self._friendly_error(e, mailbox, delegate)) except Exception as e: raise DelegationError(self._friendly_error(e, mailbox, delegate)) # ---------- logboek ---------- @staticmethod def ensure_table(): execute_query(""" CREATE TABLE IF NOT EXISTS gmail_delegatie_log ( ID INT AUTO_INCREMENT PRIMARY KEY, Tijdstip DATETIME NOT NULL, Gebruiker VARCHAR(100) NULL, Actie VARCHAR(20) NOT NULL, Mailbox VARCHAR(255) NOT NULL, Gedelegeerde VARCHAR(255) NOT NULL, INDEX idx_mailbox (Mailbox), INDEX idx_tijdstip (Tijdstip) )""", commit=True) @staticmethod def log(gebruiker, actie, mailbox, gedelegeerde): execute_query( "INSERT INTO gmail_delegatie_log (Tijdstip, Gebruiker, Actie, Mailbox, Gedelegeerde) " "VALUES (NOW(), %s, %s, %s, %s)", (gebruiker, actie, mailbox, gedelegeerde), commit=True) @staticmethod def history(mailbox=None, limit=25): if mailbox: return execute_query( "SELECT Tijdstip, Gebruiker, Actie, Mailbox, Gedelegeerde FROM gmail_delegatie_log " "WHERE Mailbox = %s OR Gedelegeerde = %s ORDER BY Tijdstip DESC LIMIT %s", (mailbox, mailbox, int(limit))) return execute_query( "SELECT Tijdstip, Gebruiker, Actie, Mailbox, Gedelegeerde FROM gmail_delegatie_log " "ORDER BY Tijdstip DESC LIMIT %s", (int(limit),))