from __future__ import annotations from govoplan_core.core.modules import with_documentation_structured_translations from govoplan_addresses.backend.german_structured_documentation import GERMAN_STRUCTURED_TRANSLATIONS from dataclasses import replace from pathlib import Path from sqlalchemy import inspect from govoplan_addresses.backend.capabilities import ( CAPABILITY_ADDRESSES_CONTACT_WRITER, CAPABILITY_ADDRESSES_LOOKUP, CAPABILITY_ADDRESSES_RECIPIENT_SOURCE, ) from govoplan_addresses.backend.db import models as addresses_models # noqa: F401 - populate address ORM metadata from govoplan_core.core.access import ( CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER, ) from govoplan_core.core.contact_points import ( CAPABILITY_ADDRESSES_CONTACT_POINT_RESOLUTION, ) from govoplan_core.core.module_guards import ( drop_table_retirement_provider, persistent_table_uninstall_guard, ) from govoplan_core.core.people import CAPABILITY_ADDRESSES_PEOPLE_SEARCH from govoplan_core.core.distribution_lists import CAPABILITY_RECIPIENT_CHANNEL_FACTS from govoplan_core.core.modules import ( CapabilityDocumentation, DocumentationCondition, DocumentationTopic, FrontendModule, FrontendRoute, MigrationSpec, ModuleContext, ModuleInterfaceProvider, ModuleManifest, NavItem, PermissionDefinition, ProductAreaContribution, RoleTemplate, ) from govoplan_core.core.provider_governance import ( ExternalProviderDeclaration, ExternalProviderStateProviderRegistration, ProviderBehaviorDeclaration, ProviderObjectDeclaration, declared_module_architecture, ) from govoplan_core.core.views import ViewSurface from govoplan_core.db.base import Base from govoplan_addresses.backend.dsar_provider import ADDRESSES_DSAR_CAPABILITY from govoplan_addresses.backend.provider_state import ( CARDDAV_PROVIDER_ID, LDAP_PROVIDER_ID, carddav_provider_states, ldap_provider_states, ) def _addresses_dsar_provider(context: ModuleContext) -> object: del context from govoplan_addresses.backend.dsar_provider import AddressesDsarProvider return AddressesDsarProvider() _addresses_table_retirement_provider = drop_table_retirement_provider( addresses_models.AddressImportRun, addresses_models.AddressImportProfile, addresses_models.ContactFieldProvenance, addresses_models.ContactRedirect, addresses_models.ContactMergeRecord, addresses_models.ContactPointQualityDecision, addresses_models.ContactPointSnapshot, addresses_models.AddressSyncDiagnostic, addresses_models.AddressSyncConflict, addresses_models.AddressSyncTombstone, addresses_models.AddressSyncSource, addresses_models.AddressListEntry, addresses_models.AddressList, addresses_models.ContactPostalAddress, addresses_models.ContactPhone, addresses_models.ContactEmail, addresses_models.ContactChannelRule, addresses_models.Contact, addresses_models.AddressBook, label="Addresses", ) def _addresses_retirement_provider(session: object | None, module_id: str): plan = _addresses_table_retirement_provider(session, module_id) base_executor = plan.destroy_data_executor if base_executor is None: return plan def executor(execute_session: object, execute_module_id: str) -> None: if not hasattr(execute_session, "get_bind") or not hasattr( execute_session, "query" ): raise RuntimeError( "No database session is available for Addresses credential retirement." ) if inspect(execute_session.get_bind()).has_table( addresses_models.AddressSyncSource.__tablename__ ): from govoplan_addresses.backend.service import ( audit_address_credentials_for_retirement, ) audit_address_credentials_for_retirement(execute_session) base_executor(execute_session, execute_module_id) return replace( plan, destroy_data_warnings=( *plan.destroy_data_warnings, "Addresses-owned encrypted connector credentials are audited and deleted with the sync-source table.", ), destroy_data_executor=executor, ) def _permission(scope: str, label: str, description: str) -> PermissionDefinition: module_id, resource, action = scope.split(":", 2) return PermissionDefinition( scope=scope, label=label, description=description, category="Addresses", level="tenant", module_id=module_id, resource=resource, action=action, ) PERMISSIONS = ( _permission( "addresses:address_book:read", "View address books", "List address books visible to the current principal.", ), _permission( "addresses:address_book:write", "Manage address books", "Create and edit local address books.", ), _permission( "addresses:address_book:delete", "Delete address books", "Soft-delete local address books.", ), _permission( "addresses:address_book:admin", "Administer address books", "Manage system-scoped address books and future sync sources.", ), _permission( "addresses:address_list:read", "View address lists", "List reusable address lists and their entries.", ), _permission( "addresses:address_list:write", "Manage address lists", "Create and edit reusable address lists.", ), _permission( "addresses:address_list:delete", "Delete address lists", "Soft-delete reusable address lists.", ), _permission( "addresses:contact:read", "View contacts", "List and lookup contacts in visible address books.", ), _permission( "addresses:contact:write", "Manage contacts", "Create and edit local contacts." ), _permission( "addresses:contact:delete", "Delete contacts", "Soft-delete local contacts." ), _permission( "addresses:governance:read", "View communication governance", "Inspect effective-dated consent, suppression, and channel-preference facts.", ), _permission( "addresses:governance:write", "Manage communication governance", "Record and end consent, suppression, and channel-preference facts.", ), _permission( "addresses:sync:read", "View address sync", "Inspect address sync sources, conflicts, tombstones, and diagnostics.", ), _permission( "addresses:sync:write", "Manage address sync", "Bind address books to external sources and record sync state.", ), _permission( "addresses:sync:admin", "Administer address sync", "Administer address sync connectors and future destructive sync operations.", ), ) ROLE_TEMPLATES = ( RoleTemplate( slug="address_book_manager", name="Address book manager", description="Manage visible local address books and contacts.", permissions=( "addresses:address_book:read", "addresses:address_book:write", "addresses:address_book:delete", "addresses:address_list:read", "addresses:address_list:write", "addresses:address_list:delete", "addresses:contact:read", "addresses:contact:write", "addresses:contact:delete", "addresses:governance:read", "addresses:governance:write", "addresses:sync:read", "addresses:sync:write", ), ), RoleTemplate( slug="address_book_reader", name="Address book reader", description="Read visible address books and contacts.", permissions=( "addresses:address_book:read", "addresses:address_list:read", "addresses:contact:read", "addresses:governance:read", "addresses:sync:read", ), ), ) def _tenant_summary(session, tenant_id: str) -> dict[str, int]: from govoplan_addresses.backend.db.models import ( AddressBook, AddressImportProfile, AddressImportRun, AddressList, AddressSyncSource, Contact, ContactMergeRecord, ContactPointQualityDecision, ContactPointSnapshot, ) return { "address_books": session.query(AddressBook) .filter(AddressBook.tenant_id == tenant_id, AddressBook.deleted_at.is_(None)) .count(), "address_lists": session.query(AddressList) .filter(AddressList.tenant_id == tenant_id, AddressList.deleted_at.is_(None)) .count(), "contacts": session.query(Contact) .filter(Contact.tenant_id == tenant_id, Contact.deleted_at.is_(None)) .count(), "active_contact_merges": session.query(ContactMergeRecord) .filter( ContactMergeRecord.tenant_id == tenant_id, ContactMergeRecord.status == "active", ) .count(), "contact_quality_decisions": session.query(ContactPointQualityDecision) .filter(ContactPointQualityDecision.tenant_id == tenant_id) .count(), "contact_point_snapshots": session.query(ContactPointSnapshot) .filter(ContactPointSnapshot.tenant_id == tenant_id) .count(), "sync_sources": session.query(AddressSyncSource) .filter( AddressSyncSource.tenant_id == tenant_id, AddressSyncSource.enabled.is_(True), ) .count(), "address_import_profiles": session.query(AddressImportProfile) .filter( AddressImportProfile.tenant_id == tenant_id, AddressImportProfile.is_current.is_(True), ) .count(), "address_import_runs": session.query(AddressImportRun) .filter(AddressImportRun.tenant_id == tenant_id) .count(), } def _addresses_router(_context: ModuleContext): from govoplan_addresses.backend.router import router return router CARDDAV_PROVIDER = ExternalProviderDeclaration( id=CARDDAV_PROVIDER_ID, module_id="addresses", label="CardDAV address-book synchronization", maturity="synchronize", operations=("discover", "read", "write", "delete", "synchronize", "preview"), objects=( ProviderObjectDeclaration( object_type="address_book", field_groups=("identity", "display", "sync_state"), authority_modes=( "external_authoritative", "external_mirror", "governed_sync", ), default_authority_mode="external_mirror", ), ProviderObjectDeclaration( object_type="contact", field_groups=( "identity", "name", "postal", "email", "phone", "source_metadata", ), authority_modes=( "external_authoritative", "external_mirror", "governed_sync", ), default_authority_mode="governed_sync", ), ), behavior=ProviderBehaviorDeclaration( revision_tokens="CardDAV sync tokens, resource hrefs, and ETags are retained.", concurrency="Conditional writes reject stale ETags and preserve explicit conflicts.", freshness="Last attempt, last success, source status, and sync token are recorded.", health="Transport failures, diagnostics, and unresolved conflicts are projected separately.", max_read_items=5000, idempotency="Stable source, href, UID, and ETag facts prevent duplicate contact effects.", retry="Only a new governed sync attempt retries failed transport operations.", timeout_seconds=30, conflicts="Local and remote values remain in an explicit conflict record until resolved.", outcome_unknown="Timed-out writes require a subsequent CardDAV read before correction or retry.", outcome_unknown_supported=True, evidence="Sync diagnostics, tombstones, conflicts, source revisions, and contact provenance are retained.", audit_event_types=( "addresses.sync.started", "addresses.sync.finished", "addresses.sync.conflict_recorded", ), correction="A resolved conflict or later synchronized revision corrects state without rewriting prior evidence.", rollback="Remote writes are not assumed to be transactionally reversible.", compensation="A reconciled update or tombstone can compensate after the remote outcome is known.", reconciliation="Read by resource href and compare ETag, UID, and local revision before applying changes.", outage="Existing local contacts remain available with stale or unknown freshness.", classifications=("personal", "confidential"), purposes=("address-book synchronization", "governed recipient resolution"), retention="Address-book and audit retention policies apply independently.", secret_handling="Only credential references and sanitized authentication metadata are persisted in sync state.", ), capability_names=(CAPABILITY_ADDRESSES_LOOKUP, CAPABILITY_ADDRESSES_CONTACT_WRITER), documentation_topic_ids=("addresses.boundary",), ) LDAP_PROVIDER = ExternalProviderDeclaration( id=LDAP_PROVIDER_ID, module_id="addresses", label="Read-only LDAP and Active Directory contacts", maturity="synchronize", operations=("discover", "read", "preview", "synchronize"), objects=( ProviderObjectDeclaration( object_type="contact", field_groups=( "identity", "name", "organization", "postal", "email", "phone", "source_metadata", ), authority_modes=("external_authoritative", "external_mirror"), default_authority_mode="external_authoritative", ), ), behavior=ProviderBehaviorDeclaration( revision_tokens="Stable LDAP source keys plus modifyTimestamp, uSNChanged, entryCSN, or a deterministic attribute digest are retained.", concurrency="LDAP is authoritative and read-only; local projections are replaced only from a complete reviewed plan.", freshness="Last attempt, last success, remote revision, and stale provider health remain visible.", health="TLS, bind, discovery, paging, mapping, truncation, and malformed-entry failures are separate diagnostics.", max_read_items=10000, idempotency="The source binding, stable key, and revision prevent duplicate contact projections.", retry="Failed reads are retried only by a new operator or scheduled sync attempt with bounded timeouts.", timeout_seconds=120, conflicts="Duplicate source keys, malformed mappings, and locally changed projections block or require a fresh plan.", outcome_unknown="Read failures never infer external deletions and retain prior local projections as stale.", outcome_unknown_supported=True, evidence="Source keys, revisions, mapping configuration, diagnostics, tombstones, and normalized field provenance are retained.", audit_event_types=( "addresses.sync_source_created", "addresses.sync_previewed", "addresses.sync_completed", ), correction="Correct the directory or mapping, then run a new full preview and synchronization.", rollback="Prior projections remain reconstructable from source revision and contact change evidence; external LDAP is never mutated.", compensation="A later authoritative refresh restores corrected projections.", reconciliation="Only a complete paged search may infer an absent source object and create a local tombstone.", outage="Existing contacts remain available and visibly stale; an unavailable directory never causes deletes.", classifications=("personal", "confidential", "restricted"), purposes=( "directory projection", "recipient resolution", "identity-linked contact discovery", ), retention="Address, audit, and records policies govern local projections and tombstone evidence.", secret_handling="Bind secrets remain in reusable credential envelopes; URLs, previews, and diagnostics contain no credentials.", ), capability_names=(CAPABILITY_ADDRESSES_LOOKUP, CAPABILITY_ADDRESSES_CONTACT_WRITER), documentation_topic_ids=("addresses.ldap-directory",), ) manifest = ModuleManifest( id="addresses", name="Addresses", version="0.1.23", required_capabilities=( CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR, ), optional_dependencies=( "campaigns", "mail", "forms", "reporting", "portal", "postbox", "connectors", ), provides_interfaces=( ModuleInterfaceProvider(name=CAPABILITY_ADDRESSES_LOOKUP, version="0.1.8"), ModuleInterfaceProvider( name=CAPABILITY_ADDRESSES_PEOPLE_SEARCH, version="0.1.0" ), ModuleInterfaceProvider( name=CAPABILITY_ADDRESSES_RECIPIENT_SOURCE, version="0.1.9" ), ModuleInterfaceProvider( name=CAPABILITY_ADDRESSES_CONTACT_POINT_RESOLUTION, version="1.0.0" ), ModuleInterfaceProvider( name=CAPABILITY_ADDRESSES_CONTACT_WRITER, version="0.1.8" ), ModuleInterfaceProvider( name=CAPABILITY_RECIPIENT_CHANNEL_FACTS, version="0.1.0" ), ModuleInterfaceProvider(name=ADDRESSES_DSAR_CAPABILITY, version="0.1.0"), ), permissions=PERMISSIONS, route_factory=_addresses_router, role_templates=ROLE_TEMPLATES, tenant_summary_providers=(_tenant_summary,), nav_items=( NavItem( path="/address-book", label="Address Book", icon="book-user", required_any=("addresses:contact:read",), order=80, ), ), frontend=FrontendModule( module_id="addresses", package_name="@govoplan/addresses-webui", routes=( FrontendRoute( path="/address-book", component="AddressBookPage", required_any=("addresses:contact:read",), order=80, ), ), nav_items=( NavItem( path="/address-book", label="Address Book", icon="book-user", required_any=("addresses:contact:read",), order=80, ), ), product_areas=( ProductAreaContribution( id="people-responsibility", module_id="addresses", label="i18n:govoplan-core.product_area.people_responsibility", icon="users", description="i18n:govoplan-core.product_area.people_responsibility_description", surface_ids=( "addresses.nav.address.book", "addresses.route.address.book", ), order=70, ), ), view_surfaces=( ViewSurface( id="addresses.page", module_id="addresses", kind="route", label="Address Book", order=80, ), ViewSurface( id="addresses.sources", module_id="addresses", kind="section", label="Address sources", order=10, ), ViewSurface( id="addresses.contacts", module_id="addresses", kind="section", label="Contacts", order=20, ), ViewSurface( id="addresses.detail", module_id="addresses", kind="section", label="Contact detail", order=30, ), ViewSurface( id="addresses.governance", module_id="addresses", kind="action", label="Communication governance", order=40, ), ViewSurface( id="addresses.sync", module_id="addresses", kind="action", label="Address synchronization", order=50, ), ), ), migration_spec=MigrationSpec( module_id="addresses", metadata=Base.metadata, script_location=str(Path(__file__).with_name("migrations") / "versions"), retirement_supported=True, retirement_provider=_addresses_retirement_provider, retirement_notes="Destructive retirement drops address-owned database tables after the installer captures a database snapshot.", ), capability_factories={ CAPABILITY_ADDRESSES_LOOKUP: lambda context: __import__( "govoplan_addresses.backend.capabilities", fromlist=["lookup_capability"] ).lookup_capability(context), CAPABILITY_ADDRESSES_PEOPLE_SEARCH: lambda context: __import__( "govoplan_addresses.backend.capabilities", fromlist=["people_search_capability"], ).people_search_capability(context), CAPABILITY_ADDRESSES_RECIPIENT_SOURCE: lambda context: __import__( "govoplan_addresses.backend.capabilities", fromlist=["recipient_source_capability"], ).recipient_source_capability(context), CAPABILITY_ADDRESSES_CONTACT_WRITER: lambda context: __import__( "govoplan_addresses.backend.capabilities", fromlist=["contact_writer_capability"], ).contact_writer_capability(context), CAPABILITY_RECIPIENT_CHANNEL_FACTS: lambda context: __import__( "govoplan_addresses.backend.capabilities", fromlist=["channel_facts_capability"], ).channel_facts_capability(context), CAPABILITY_ADDRESSES_CONTACT_POINT_RESOLUTION: lambda context: __import__( "govoplan_addresses.backend.capabilities", fromlist=["contact_point_resolution_capability"], ).contact_point_resolution_capability(context), ADDRESSES_DSAR_CAPABILITY: _addresses_dsar_provider, }, capability_documentation={ ADDRESSES_DSAR_CAPABILITY: CapabilityDocumentation( label="Addresses data-subject request provider", summary=( "Finds bounded contact, contact-point, address-list, governance, " "provenance, synchronization, and operator-attribution data without " "exporting raw source payloads, connector state, or opaque evidence." ), contract_version="0.1.0", documentation_types=("admin",), audience=("privacy_officer", "addresses_admin", "records_manager"), ), }, uninstall_guard_providers=( persistent_table_uninstall_guard( addresses_models.AddressImportRun, addresses_models.AddressImportProfile, addresses_models.AddressSyncDiagnostic, addresses_models.AddressSyncConflict, addresses_models.AddressSyncTombstone, addresses_models.AddressSyncSource, addresses_models.AddressListEntry, addresses_models.AddressList, addresses_models.ContactFieldProvenance, addresses_models.ContactRedirect, addresses_models.ContactMergeRecord, addresses_models.ContactPointQualityDecision, addresses_models.ContactPointSnapshot, addresses_models.AddressBook, addresses_models.Contact, addresses_models.ContactEmail, addresses_models.ContactPhone, addresses_models.ContactPostalAddress, addresses_models.ContactChannelRule, label="Addresses", ), ), documentation=( DocumentationTopic( id="addresses.privacy.data-subject-requests", title="Review Addresses data in a data-subject request", summary=( "Collect tenant-scoped contact data while preserving shared address, " "recipient, synchronization, and provenance evidence." ), body=( "Addresses searches corroborated email and account selectors plus " "namespaced contact and contact-point references. A matching contact " "exports bounded identity, email, telephone, and postal values together " "with its address-list use and minimized governance, quality, provenance, " "merge, redirect, and synchronization evidence. Account matches add only " "minimized operator attribution for governed configuration and evidence. " "The provider excludes raw imported or synchronized source payloads, " "connector tokens and revisions that could act as credentials, opaque " "metadata, snapshot request and resolution payloads, import plans, merge " "before/after payloads, unrelated contacts, and other tenants. Quality, " "governance, provenance, merge, redirect, synchronization, import, " "snapshot, and operator evidence is retained with an explicit reason. " "Because reusable contacts can be shared, synchronized, merged, or " "referenced by immutable recipient snapshots, the DSAR provider never " "deletes them automatically. An authorized operator must review " "dependencies and use the normal Addresses correction, archive, source, " "merge, or governance workflow." ), layer="static", documentation_types=("admin",), audience=( "privacy_officer", "addresses_admin", "records_manager", "operator", ), related_modules=( "access", "audit", "campaigns", "dist_lists", "records", ), order=29, translations={ "de": { "title": "Addresses-Daten in einer Betroffenenanfrage prüfen", "summary": ( "Mandantenbezogene Kontaktdaten erfassen und dabei gemeinsame Adress-, Empfänger-, Synchronisations- und " "Herkunftsnachweise bewahren." ), "body": ( "Addresses durchsucht bestätigte E-Mail- und Kontoselektoren sowie namensraumgebundene Kontakt- und " "Kontaktpunktverweise. Zu einem passenden Kontakt werden begrenzte Identitäts-, E-Mail-, Telefon- und Postwerte " "einschließlich seiner Adresslistennutzung und minimierter Nachweise zu Governance, Qualität, Herkunft, Zusammenführung, " "Weiterleitung und Synchronisation exportiert. Kontotreffer ergänzen nur minimierte Zuordnungen von Betriebspersonen zu " "gesteuerter Konfiguration und Nachweisen. Ausgeschlossen sind rohe importierte oder synchronisierte Quelldaten, " "Connector-Token und Revisionen mit Zugangsdatencharakter, undurchsichtige Metadaten, Anfrage- und Auflösungsnutzdaten " "von Snapshots, Importpläne, Vorher-/Nachher-Daten von Zusammenführungen, unbeteiligte Kontakte und andere Mandanten. " "Nachweise zu Qualität, Governance, Herkunft, Zusammenführung, Weiterleitung, Synchronisation, Import, Snapshot und " "Betriebszuordnung werden mit ausdrücklicher Begründung aufbewahrt. Weil wiederverwendbare Kontakte geteilt, " "synchronisiert, zusammengeführt oder von unveränderlichen Empfänger-Snapshots referenziert sein können, löscht der " "DSAR-Provider sie niemals automatisch. Eine berechtigte Betriebsperson muss Abhängigkeiten prüfen und den regulären " "Addresses-Ablauf für Korrektur, Archivierung, Quelle, Zusammenführung oder Governance verwenden." ), } }, metadata={ "seed": True, "help_contexts": [ "addresses.contacts", "addresses.governance", "addresses.action.archive", ], }, ), DocumentationTopic( id="addresses.boundary", title="Reusable address ownership", summary="Reusable person, organization, household, postal, and email recipient sources belong to the addresses module.", body=( "Open the book beside Address books for documentation of the address workspace. " "Campaigns may keep immutable campaign-local recipient snapshots, but durable address directories, " "recipient-source definitions, consent metadata, provenance, deduplication, and import/export workflows " "are owned by govoplan-addresses. The Address Book workspace keeps Reload directly " "before Add address book at the upper right. Import / export, Connections, and Address " "quality open labelled, scoped tools; Manage applies to the selected book or list and " "separates archive actions from editing. Export version is chosen in Import / export. " "Contact creation remains beside the contact list. Folder icons alone expand or collapse " "the tree; labels select a group, book, or list without changing expansion. A selected " "group is navigation, not an aggregate contact book. Reload preserves collapsed " "branches. All permission and read-only reasons, confirmations, import safeguards, " "and contact-to-list drag and drop still apply." ), layer="configured", documentation_types=("admin", "user"), audience=("tenant_admin", "operator", "module_admin"), related_modules=( "campaigns", "mail", "forms", "reporting", "portal", "postbox", ), order=30, translations={ "de": { "title": "Zuständigkeit für wiederverwendbare Adressen", "summary": ( "Wiederverwendbare Personen-, Organisations-, Haushalts-, Post- und E-Mail-Empfängerquellen gehören dem " "Addresses-Modul." ), "body": ( "Öffnen Sie das Buch neben Adressbücher für die Dokumentation des Adressarbeitsbereichs. " "Campaigns darf unveränderliche campaignlokale Empfänger-Snapshots halten. Dauerhafte Adressverzeichnisse, " "Empfängerquellendefinitionen, Einwilligungsmetadaten, Herkunft, Dublettenbereinigung sowie Import- und Exportabläufe " "gehören jedoch govoplan-addresses. Im Adressbuch steht Neuladen oben rechts unmittelbar vor " "Adressbuch hinzufügen. Import / Export, Verbindungen und Adressqualität öffnen beschriftete, " "kontextbezogene Werkzeuge. Verwalten bezieht sich auf das ausgewählte Adressbuch oder die Liste " "und trennt Archivieren vom Bearbeiten. Die Exportversion wird unter Import / Export gewählt. " "Kontakte werden weiterhin direkt neben der Kontaktliste angelegt. Nur Ordnersymbole klappen " "den Baum auf oder zu; Beschriftungen wählen eine Gruppe, ein Adressbuch oder eine Liste aus, " "ohne die Aufklappstellung zu ändern. Eine ausgewählte Gruppe dient der Navigation und ist " "kein zusammengefasstes Adressbuch. Neuladen bewahrt zugeklappte Zweige. Berechtigungs- und " "Schreibschutzgründe, Bestätigungen, Importsicherungen und das Ziehen von Kontakten in Listen gelten unverändert." ), } }, metadata={ "seed": True, "help_contexts": [ "addresses.page", "addresses.explorer.transfer", "addresses.sources", "addresses.contacts", "addresses.detail", "addresses.state.read-only", ], }, ), DocumentationTopic( id="addresses.contact-point-resolution", title="Contact-point resolution and snapshots", summary="Resolve purpose-aware channel targets and freeze immutable recipient evidence.", body=( "Addresses exposes a versioned contact-point capability for email, postal, internal-mail, and portal targets. " "Callers can request an effective date, communication purpose, address purpose, fallback rule, locale, and " "postal format. Bounded previews remain live; frozen snapshots retain the resolved values, exclusions, " "source and governance revisions, provenance, and a deterministic evidence hash even after contacts change." ), layer="configured", documentation_types=("admin", "user"), audience=("tenant_admin", "operator", "module_admin"), related_modules=("dist_lists", "campaigns", "policy", "templates"), order=31, translations={ "de": { "title": "Kontaktpunktauflösung und Snapshots", "summary": "Zweckbezogene Kanalziele auflösen und unveränderliche Empfängernachweise einfrieren.", "body": ( "Addresses stellt eine versionierte Kontaktpunktfähigkeit für E-Mail-, Post-, Hauspost- und Portalziele bereit. " "Aufrufende können Wirksamkeitsdatum, Kommunikationszweck, Adresszweck, Rückfallregel, Spracheinstellung und Postformat " "angeben. Begrenzte Vorschauen bleiben aktuell; eingefrorene Snapshots bewahren aufgelöste Werte, Ausschlüsse, Quellen- " "und Governance-Revisionen, Herkunft und einen deterministischen Nachweishash auch nach späteren Kontaktänderungen." ), } }, metadata={ "seed": True, "help_contexts": [ "addresses.governance", "addresses.field.channel", "addresses.field.contact-point", "addresses.field.communication-purpose", "addresses.field.effective-period", ], }, ), DocumentationTopic( id="addresses.tabular-imports", title="CSV, XLSX, and LDIF contact imports", summary="Preview and apply reusable, versioned contact mappings without silent row or entry loss.", body=( "CSV, XLSX, and LDIF files can be mapped with scoped, reusable profile versions. Each preview validates headers or attributes, " "encodings, source keys, duplicates, blank values, format limits, and contact identity before any mutation. " "The reviewed input hash and plan hash are retained with row-level effects and diagnostics. Apply is idempotent, " "rejects contacts changed after preview, and records sufficient evidence for a guarded rollback. Each new update " "uses version-2 before-images for editable contact values, source metadata, complete contact-point identities, " "original and normalized values, provenance, order and timestamps, and prior deletion state: rolling back an " "import that restored a deleted contact archives it again. Older update runs without complete before-images require " "manual reconciliation; automatic rollback stops before changing any contacts. Created-contact identities and " "after-hashes are persisted with the applied plan and required for rollback; missing older guards or contacts " "moved to another book also require reconciliation. Point-evidence edits are included in change guards. " "Unchanged point values retain their identities and original evidence. Removing or replacing a point referenced " "by an address list or point-specific consent/quality decision requires explicit reconciliation, including when " "a new reference would otherwise be detached by rollback. Current book visibility and change " "guards still apply. Preview source lookups and contact collections are loaded in bounded batches, without changing " "duplicate policies, reviewed hashes, or apply-time validation. A persisted run " "can be reopened with its run link after navigation or reload; previewed, applied, rolled-back, expired, and " "unavailable states remain explicit. Both apply and rollback submit the reviewed plan hash. Missing, expired, " "hidden, and cross-tenant runs disclose no source payload. XLSX formulas, macros, and legacy workbook formats " "are never executed or imported. LDIF folded lines, UTF-8 and base64 text, repeated attributes, and comments are parsed; " "binary and URL values are never projected or fetched. Change records default to rejected diagnostics and may only be ignored " "or treat add records as static entries through an explicit profile policy." ), layer="configured", documentation_types=("admin", "user"), audience=("tenant_admin", "module_admin", "power_user"), conditions=( DocumentationCondition( required_modules=("addresses",), any_scopes=("addresses:contact:write", "addresses:sync:write"), ), ), related_modules=("connectors", "datasources", "dataflow", "files", "audit"), order=33, translations={ "de": { "title": "Kontakte aus CSV, XLSX und LDIF importieren", "summary": ( "Wiederverwendbare, versionierte Kontaktzuordnungen vorprüfen und anwenden, ohne Zeilen oder Einträge " "stillschweigend zu verlieren." ), "body": ( "CSV-, XLSX- und LDIF-Dateien lassen sich mit bereichsgebundenen, wiederverwendbaren Profilversionen zuordnen. Jede " "Vorschau prüft Überschriften oder Attribute, Kodierung, Quellschlüssel, Dubletten, Leerwerte, Formatgrenzen und " "Kontaktidentität vor jeder Änderung. Der geprüfte Eingabe- und Planhash wird mit zeilenbezogenen Wirkungen und Diagnosen " "aufbewahrt. Die Anwendung ist idempotent, verwirft seit der Vorschau geänderte Kontakte und zeichnet ausreichende " "Nachweise für eine gesicherte Rücknahme auf. Ein gespeicherter Lauf kann nach Navigation oder Neuladen über seinen Link " "erneut geöffnet werden. Vorher-Bilder der Version 2 enthalten bearbeitbare Kontaktwerte, Quellmetadaten, " "vollständige Kontaktpunktkennungen, Original- und normalisierte Werte, Herkunft, Reihenfolge, Zeitstempel und den " "vorherigen Löschzustand: Die Rücknahme archiviert einen durch den Import wiederhergestellten Kontakt erneut. " "Ältere Änderungsläufe ohne vollständige Vorher-Bilder erfordern einen manuellen Abgleich; die automatische " "Rücknahme stoppt vor jeder Kontaktänderung. Kennungen neu angelegter Kontakte und Nachher-Hashes werden mit dem " "angewendeten Plan gespeichert und sind für die Rücknahme erforderlich; fehlende ältere Sicherungen oder in ein " "anderes Buch verschobene Kontakte erfordern ebenfalls einen Abgleich. Änderungen an Punktnachweisen werden vom " "Änderungsschutz erfasst. Unveränderte Punktwerte behalten Kennung und Originalnachweise. Das Entfernen oder " "Ersetzen eines in Adresslisten oder punktspezifischen Einwilligungs-/Qualitätsentscheidungen referenzierten " "Punkts erfordert einen ausdrücklichen Abgleich; dies gilt auch für neue Referenzen, die eine Rücknahme sonst " "lösen würde. Aktuelle Adressbuchsichtbarkeit und " "Änderungsschutz bleiben wirksam. " "Quellzuordnungen und Kontaktpunkte werden für die Vorschau in begrenzten Stapeln geladen, ohne Dublettenregeln, " "geprüfte Hashes oder die erneute Prüfung bei Anwendung zu ändern. Ein gespeicherter Lauf kann über seinen Link " "erneut geöffnet werden; Vorschau-, Anwendungs-, Rücknahme-, Ablauf- und Nichtverfügbarkeitszustände bleiben eindeutig. " "Anwendung und Rücknahme übermitteln den geprüften Planhash. Fehlende, abgelaufene, verborgene und mandantenfremde Läufe " "legen keine Quelldaten offen. XLSX-Formeln, Makros und ältere Arbeitsmappenformate werden niemals ausgeführt oder " "importiert. Gefaltete LDIF-Zeilen, UTF-8- und Base64-Text, wiederholte Attribute und Kommentare werden verarbeitet; " "Binär- und URL-Werte werden weder projiziert noch abgerufen. Änderungsdatensätze gelten standardmäßig als abgelehnte " "Diagnose und dürfen nur über eine ausdrückliche Profilrichtlinie ignoriert oder bei Add-Einträgen als statische Daten " "behandelt werden." ), } }, metadata={ "kind": "workflow", "help_contexts": [ "addresses.action.import", "addresses.contacts", "addresses.sources", ], }, ), DocumentationTopic( id="addresses.vcard-batches", title="Selective vCard batch import and export", summary="Preview multiple vCard files, choose each card's effect, and export deterministic scoped files.", body=( "One or more UTF-8 .vcf files are parsed into a persisted, non-mutating preview with bounded diagnostics, " "duplicate suggestions, an input hash, a parser version, and a deterministic plan hash. Operators choose " "create, update, or ignore only where the reviewed plan permits it. Apply rejects stale contact targets and " "is idempotent for the same selection; a different retry is rejected. Pending runs can be reloaded or cancelled " "without changing contacts. Upload size, file count, card count, line count, and unfolded-line length are bounded. " "Exports can target a complete address book, one address list, or explicit contacts; vCard 3.0 or 4.0 is selected " "explicitly and contacts use deterministic display-name and stable-ID ordering. Export and import evidence records " "hashes and counts, while diagnostics never disclose raw contact payloads. Large previews remain persisted and expose " "their batch execution mode so a runtime job capability can execute them asynchronously when available." ), layer="configured", documentation_types=("admin", "user"), audience=("tenant_admin", "operator", "module_admin", "power_user"), related_modules=("files", "audit", "connectors"), order=34, translations={ "de": { "title": "vCard-Stapel selektiv importieren und exportieren", "summary": ( "Mehrere vCard-Dateien vorprüfen, die Wirkung jeder Karte wählen und deterministische bereichsgebundene Dateien exportieren." ), "body": ( "Eine oder mehrere UTF-8-.vcf-Dateien werden in eine gespeicherte, nicht verändernde Vorschau mit begrenzten Diagnosen, " "Dublettenhinweisen, Eingabehash, Parser-Version und deterministischem Planhash eingelesen. Betriebspersonen wählen " "Anlegen, Aktualisieren oder Ignorieren nur dort, wo der geprüfte Plan es erlaubt. Die Anwendung verwirft veraltete " "Kontaktziele und ist für dieselbe Auswahl idempotent; eine abweichende Wiederholung wird abgelehnt. Ausstehende Läufe " "lassen sich neu laden oder abbrechen, ohne Kontakte zu verändern. Uploadgröße, Datei- und Kartenanzahl, Zeilenanzahl und " "Länge entfalteter Zeilen sind begrenzt. Exporte können ein vollständiges Adressbuch, eine Adressliste oder ausgewählte " "Kontakte umfassen; vCard 3.0 oder 4.0 wird ausdrücklich gewählt und Kontakte werden deterministisch nach Anzeigename und " "stabiler Kennung sortiert. Export- und Importnachweise speichern Hashes und Anzahlen, während Diagnosen niemals rohe " "Kontaktdaten offenlegen. Große Vorschauen bleiben gespeichert und geben ihren Stapelausführungsmodus an, sodass eine " "Laufzeit-Jobfähigkeit sie bei Verfügbarkeit asynchron ausführen kann." ), } }, metadata={ "seed": True, "help_contexts": [ "addresses.action.import", "addresses.contacts", "addresses.sources", ], }, ), DocumentationTopic( id="addresses.ldap-directory", title="LDAP and Active Directory address sources", summary="Project authoritative directory contacts through a bounded, read-only synchronization source.", body=( "LDAP sources use LDAPS or StartTLS and reusable credential envelopes. Discovery finds available base DNs; " "the source profile then controls a bounded paged filter and explicit attribute mapping. Preview never mutates " "contacts. A complete successful read may create, update, or tombstone local projections; truncated or failed " "reads suppress absence-based deletes and mark the source stale. Stable source keys, revisions, normalized fields, " "and provenance remain attached to every retained contact." ), layer="configured", documentation_types=("admin", "user"), audience=("tenant_admin", "operator", "module_admin"), related_modules=("connectors", "idm", "access", "policy", "audit"), order=35, translations={ "de": { "title": "LDAP- und Active-Directory-Adressquellen", "summary": ( "Maßgebliche Verzeichniskontakte über eine begrenzte, schreibgeschützte Synchronisationsquelle projizieren." ), "body": ( "LDAP-Quellen verwenden LDAPS oder StartTLS und wiederverwendbare Zugangsdatenhüllen. Die Ermittlung findet verfügbare " "Basis-DNs; anschließend steuert das Quellprofil einen begrenzten seitenweisen Filter und eine ausdrückliche " "Attributzuordnung. Die Vorschau verändert niemals Kontakte. Ein vollständiger erfolgreicher Lesevorgang darf lokale " "Projektionen anlegen, aktualisieren oder als entfernt markieren; abgeschnittene oder fehlgeschlagene Lesevorgänge " "unterdrücken Löschungen aufgrund von Abwesenheit und markieren die Quelle als veraltet. Stabile Quellschlüssel, " "Revisionen, normalisierte Felder und Herkunft bleiben mit jedem erhaltenen Kontakt verbunden." ), } }, ), DocumentationTopic( id="addresses.quality-and-merge", title="Contact quality, duplicates, and reversible merges", summary="Review address quality and duplicate suggestions without losing source evidence.", body=( "Addresses preserves original and normalized contact-point values, records field-level provenance, " "and projects invalid, returned, stale, or undeliverable states into recipient resolution with stable " "reason codes. Duplicate suggestions are bounded and explain their matching features. An operator can " "choose the surviving values, merge contact points, and later undo or split the merge while the recorded " "post-merge evidence still matches. Contact redirects keep stored references resolvable, and address-list " "memberships are repaired transactionally. Audit remains an optional integration; the Addresses change " "sequence and merge evidence are always retained." ), layer="configured", documentation_types=("admin", "user"), audience=("tenant_admin", "operator", "module_admin"), related_modules=("campaigns", "dist_lists", "policy", "audit"), order=32, translations={ "de": { "title": "Kontaktqualität, Dubletten und umkehrbare Zusammenführungen", "summary": "Adressqualität und Dublettenvorschläge prüfen, ohne Quellnachweise zu verlieren.", "body": ( "Addresses bewahrt ursprüngliche und normalisierte Kontaktpunktwerte, zeichnet die Herkunft je Feld auf und überführt " "ungültige, zurückgesandte, veraltete oder unzustellbare Zustände mit stabilen Grundcodes in die Empfängerauflösung. " "Dublettenvorschläge sind begrenzt und erklären ihre Übereinstimmungsmerkmale. Eine Betriebsperson kann die zu erhaltenden " "Werte wählen, Kontaktpunkte zusammenführen und die Zusammenführung später rückgängig machen oder aufteilen, solange der " "aufgezeichnete Nachweis nach der Zusammenführung noch übereinstimmt. Kontaktweiterleitungen halten gespeicherte Verweise " "auflösbar, und Mitgliedschaften in Adresslisten werden transaktional repariert. Audit bleibt eine optionale Integration; " "die Änderungsfolge und Zusammenführungsnachweise von Addresses werden stets aufbewahrt." ), } }, ), DocumentationTopic( id="addresses.reference.fields-and-consequences", title="Address fields, scope, and action consequences", summary="Scope, source authority, contact points, list membership, archival, synchronization, and merge consequences.", body=( "Address books are scoped to a user, group, tenant, or authorized system context. Inherited and externally authoritative " "books may remain visible but read-only. Contacts own reusable name, organization, electronic, phone, postal, tag, note, " "quality, and provenance facts; address lists reference contact points from the same book and do not replace Distribution " "Lists. Archival hides a book, list, or contact from ordinary selection while preserving governed history and references. " "CardDAV and LDAP sources expose their direction, authority, freshness, diagnostics, conflict, and stale-state behavior. " "Imports and synchronization require preview before mutation. Contact merges select a survivor and field provenance, repair " "list references transactionally, and retain redirects and evidence so a matching merge can be undone or split." ), layer="configured", documentation_types=("admin", "user"), audience=("tenant_admin", "operator", "module_admin", "power_user"), related_modules=( "dist_lists", "connectors", "datasources", "campaigns", "policy", "audit", ), order=36, translations={ "de": { "title": "Adressfelder, Geltungsbereiche und Folgen von Aktionen", "summary": ( "Geltungsbereich, Quellenhoheit, Kontaktpunkte, Listenmitgliedschaft sowie Folgen von Archivierung, Synchronisation und " "Zusammenführung." ), "body": ( "Adressbücher sind einer Person, Gruppe, einem Mandanten oder einem berechtigten Systemkontext zugeordnet. Geerbte und " "extern maßgebliche Bücher können sichtbar, aber schreibgeschützt bleiben. Kontakte besitzen wiederverwendbare Angaben " "zu Name, Organisation, elektronischen und telefonischen Kontaktpunkten, Postanschrift, Schlagwörtern, Notizen, Qualität " "und Herkunft. Adresslisten verweisen auf Kontaktpunkte desselben Buchs und ersetzen keine Distribution Lists. Eine " "Archivierung entfernt Buch, Liste oder Kontakt aus der gewöhnlichen Auswahl, bewahrt aber gesteuerte Historie und " "Verweise. CardDAV- und LDAP-Quellen zeigen Richtung, Hoheit, Aktualität, Diagnosen, Konflikte und Verhalten bei " "veraltetem Zustand. Import und Synchronisation erfordern vor jeder Änderung eine Vorschau. Kontaktzusammenführungen " "wählen überlebenden Kontakt und Feldherkunft, reparieren Listenverweise transaktional und bewahren Weiterleitungen und " "Nachweise, sodass eine passende Zusammenführung rückgängig gemacht oder aufgeteilt werden kann." ), } }, metadata={ "kind": "reference", "seed": True, "help_contexts": [ "addresses.field.book-scope", "addresses.field.contact-identity", "addresses.field.organization", "addresses.field.contact-point", "addresses.action.archive", "addresses.action.import", "addresses.action.sync", "addresses.action.merge", ], "consequence_classes": { "archive": "Removes the object from ordinary selection while retaining governed history and references.", "import_or_sync": "Applies only a reviewed bounded plan and retains source revision, diagnostics, and provenance.", "merge": "Repoints governed references to a survivor and retains reversible redirect and provenance evidence.", "governance_fact": "Adds or ends an effective-dated communication decision without erasing prior facts.", }, }, ), ), external_providers=(CARDDAV_PROVIDER, LDAP_PROVIDER), external_provider_state_providers=( ExternalProviderStateProviderRegistration( module_id="addresses", provider_id=CARDDAV_PROVIDER_ID, provider=carddav_provider_states, ), ExternalProviderStateProviderRegistration( module_id="addresses", provider_id=LDAP_PROVIDER_ID, provider=ldap_provider_states, ), ), architecture=declared_module_architecture( layer="communication_participation", kind="domain", maturity="vertical_slice", documentation_ref="docs/ADDRESS_MODULE_ARCHITECTURE.md", test_ref="tests/test_addresses_service.py", known_limits=( "External address-book synchronization remains a bounded connector slice rather than a supported provider profile.", ), supported_authority_modes=( "native_authoritative", "external_authoritative", "external_mirror", "governed_sync", ), owned_concepts=( "contact point", "address book", "contact consent", "recipient source", ), non_owned_concepts=( "identity", "organization", "campaign recipient snapshot", "procedure party", ), target_tested_providers=(CARDDAV_PROVIDER_ID,), security_docs=("docs/ADDRESS_MODULE_ARCHITECTURE.md",), operations_docs=("README.md",), ), ) manifest = with_documentation_structured_translations( manifest, locale="de", translations=GERMAN_STRUCTURED_TRANSLATIONS ) def get_manifest() -> ModuleManifest: return manifest