200 lines
9.6 KiB
Python
200 lines
9.6 KiB
Python
"""
|
||
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),))
|