Files
Pivot/services/gmail_delegation_service.py
T
2026-10-14 13:34:37 +02:00

200 lines
9.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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),))