sysvoice MS365 Integration Kontaktsync persönliche Kontakte
Information:
Aktuell erfordert die Sysvoice-Integration, dass jeder einzelne Benutzer seine Kontakte manuell für den Administrator freigibt, der die Entra-App autorisiert hat.
Aus meiner Sicht stellt dieser Ablauf sowohl für die Benutzer als auch für den Administrator eine unnötige Belastung dar.
Um den Prozess zu vereinfachen und effizienter zu gestalten, habe ich ein PowerShell-Skript entwickelt, das automatisch alle Benutzer einer bestimmten Gruppe abfragt und die Kontaktfreigabe zentral übernimmt.
Adminkonto benötigt einen Mailbox (mind. ExchangeOnline Plan1) Lizenz!
Schritt-für-Schritt-Anleitung zur automatisierten Kontaktfreigabe
-
Vorbereitung
-
Skript ausführen
-
Starte das PowerShell-Skript.
-
Der Ablauf ist automatisiert – lehn dich zurück und beobachte, wie die Kontakte freigegeben werden.
-
-
Sysvoice aktualisieren
Powershell Skript:
<# =====================================================================
Bulk-Freigabe "Kontakte" für Entra-Sicherheitsgruppe
- Läuft in Windows PowerShell 5.1 (auch ISE) mit Minimal-Import
- Robust gegen leeres $PSScriptRoot, gemischte DirectoryObject-Typen,
fehlendes @odata.type; nutzt typisierte Abfrage & Fallback-Resolve
===================================================================== #>
# -------------------------------
# KONFIGURATION
# -------------------------------
$GroupName = "sysvoice Users" # Entra (Azure AD) Sicherheitsgruppe (DisplayName)
$ReaderIdentity = "admin@kundendomain.onmicrosoft.com" # Konto ODER mailaktivierte Gruppe, die Lesen (Reviewer) soll
$AccessRight = "Reviewer" # Lesen reicht für Yeastar
$WhatIf = $false # $true = Simulation ohne Änderungen
$IncludeTransitiveMembers = $true # $true: Mitglieder verschachtelter Gruppen ebenfalls laden
# Log-Pfade (Fallback, falls in Konsole/ohne Datei gestartet)
$LogFolder = if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path }
$TimeStamp = (Get-Date -Format "yyyyMMdd_HHmmss")
$LogPath = Join-Path $LogFolder "ContactsShare_$($GroupName -replace '\W','_')_$TimeStamp.csv"
# -------------------------------
# HILFSFUNKTIONEN (Ausgaben)
# -------------------------------
function Write-Step($msg) { Write-Host "[STEP] $msg" -ForegroundColor Cyan }
function Write-Info($msg) { Write-Host "[INFO] $msg" -ForegroundColor Gray }
function Write-Ok($msg) { Write-Host "[OK] $msg" -ForegroundColor Green }
function Write-Warn2($msg) { Write-Warning $msg }
function Write-Err2($msg) { Write-Host "[ERROR] $msg" -ForegroundColor Red }
function Write-Debug2($msg) { Write-Host "[DEBUG] $msg" -ForegroundColor Yellow }
# -------------------------------
# 1) Microsoft Graph minimal laden & verbinden
# -------------------------------
Write-Step "Lade Microsoft Graph Teilmodule (Authentication, Users, Groups)…"
$needed = @(
"Microsoft.Graph.Authentication",
"Microsoft.Graph.Users",
"Microsoft.Graph.Groups"
)
foreach ($m in $needed) {
if (-not (Get-Module -ListAvailable -Name $m)) {
Write-Info "Installiere Modul: $m (Scope=CurrentUser)…"
Install-Module $m -Scope CurrentUser -Force -ErrorAction Stop
}
Import-Module $m -ErrorAction Stop
Write-Ok "Modul geladen: $m"
}
Write-Step "Verbinde zu Microsoft Graph (Scopes: Group.Read.All, User.Read.All)…"
try {
Connect-MgGraph -Scopes "Group.Read.All","User.Read.All" -ErrorAction Stop | Out-Null
try {
$ctx = Get-MgContext
Write-Ok "Graph verbunden als: $($ctx.Account) / Tenant: $($ctx.TenantId)"
} catch { Write-Info "Graph-Kontextprüfung nicht verfügbar – fahre fort." }
} catch {
Write-Err2 "Graph-Verbindung fehlgeschlagen: $($_.Exception.Message)"
throw
}
# -------------------------------
# 2) Gruppe finden
# -------------------------------
Write-Step "Suche Entra Gruppe mit DisplayName: '$GroupName'…"
$group = $null
try { $group = Get-MgGroup -All -Filter "displayName eq '$GroupName'" -ErrorAction Stop } catch { }
if (-not $group) {
Write-Info "Wechsle in Suchmodus (ConsistencyLevel eventual)…"
try {
$group = Get-MgGroup -Search ('"'+$GroupName+'"') -ConsistencyLevel eventual -ErrorAction Stop |
Where-Object { $_.DisplayName -eq $GroupName }
} catch { }
}
if (-not $group) {
Write-Info "Hole alle Gruppen und filtere lokal (dies kann dauern)…"
$group = Get-MgGroup -All | Where-Object DisplayName -eq $GroupName
}
if (-not $group) { Write-Err2 "Gruppe '$GroupName' nicht gefunden."; throw "Gruppe '$GroupName' nicht gefunden." }
if ($group.Count -gt 1) { Write-Warn2 "Mehrere Gruppen gefunden – verwende die erste: $($group[0].Id)"; $group = $group[0] }
Write-Ok "Gruppe gefunden: $($group.DisplayName) (Id: $($group.Id))"
# -------------------------------
# 3) Mitglieder (nur Benutzer) laden – robust
# -------------------------------
Write-Step ("Lade {0}Mitglieder der Gruppe…" -f ($(if($IncludeTransitiveMembers){"transitive "}else{""})))
$rawMembers = @()
try {
if ($IncludeTransitiveMembers) {
# TYPISIERT (direkt nur Benutzer)
Write-Info "Versuche typisierte Abfrage: Get-MgGroupTransitiveMemberAsUser…"
$rawMembers = Get-MgGroupTransitiveMemberAsUser -GroupId $group.Id -All -ErrorAction Stop
} else {
Write-Info "Versuche typisierte Abfrage: Get-MgGroupMemberAsUser…"
$rawMembers = Get-MgGroupMemberAsUser -GroupId $group.Id -All -ErrorAction Stop
}
Write-Ok ("Typisierte Benutzer-Mitglieder geladen: {0}" -f $rawMembers.Count)
} catch {
Write-Warn2 "Typisierte Abfrage nicht verfügbar/fehlgeschlagen: $($_.Exception.Message)"
}
# Fallback, wenn typisiert nichts brachte: untypisiert laden und selbst auflösen
if (-not $rawMembers -or $rawMembers.Count -eq 0) {
Write-Info "Fallback: Lade DirectoryObjects und löse pro Id zu User auf…"
try {
if ($IncludeTransitiveMembers) {
$dirObjs = Get-MgGroupTransitiveMember -GroupId $group.Id -All -ErrorAction Stop
} else {
$dirObjs = Get-MgGroupMember -GroupId $group.Id -All -ErrorAction Stop
}
Write-Ok ("DirectoryObjects geladen: {0}" -f $dirObjs.Count)
$rawMembers = foreach ($o in $dirObjs) {
try {
$u2 = Get-MgUser -UserId $o.Id -ErrorAction Stop
$u2 # nur echte Benutzer durchreichen
} catch { }
}
Write-Ok ("Als Benutzer aufgelöst: {0}" -f $rawMembers.Count)
} catch {
Write-Err2 "Mitgliederabfrage fehlgeschlagen: $($_.Exception.Message)"
throw
}
}
if (-not $rawMembers -or $rawMembers.Count -eq 0) {
Write-Err2 "Keine Benutzer-Mitglieder gefunden."
throw "Keine Benutzer-Mitglieder gefunden."
}
# In ein einheitliches Objekt-Set projizieren
Write-Step "Projiziere Benutzerobjekte…"
$users = foreach ($m in $rawMembers) {
# $m kann typisiertes User-Objekt sein
$mail = $null; $upn = $null; $display = $null; $id = $null
try { $mail = $m.Mail } catch {}
try { $upn = $m.UserPrincipalName } catch {}
try { $display = $m.DisplayName } catch {}
try { $id = $m.Id } catch {}
if (-not $display -and $m.AdditionalProperties) { $display = $m.AdditionalProperties.displayName }
if (-not $mail -and $m.AdditionalProperties) { $mail = $m.AdditionalProperties.mail }
if (-not $upn -and $m.AdditionalProperties) { $upn = $m.AdditionalProperties.userPrincipalName }
[pscustomobject]@{
DisplayName = $display
PrimaryAddressHint = if ($mail) { $mail } else { $upn }
UPN = $upn
ObjectId = $id
}
}
Write-Ok ("Gefundene Benutzer: {0}" -f $users.Count)
Write-Debug2 "Beispiel-Eintrag:`n$($users | Select-Object -First 1 | Format-List | Out-String)"
# -------------------------------
# 4) Exchange Online verbinden
# -------------------------------
Write-Step "Verbinde Exchange Online…"
try {
if (-not (Get-Module -ListAvailable -Name ExchangeOnlineManagement)) {
Write-Info "Installiere Modul ExchangeOnlineManagement…"
Install-Module ExchangeOnlineManagement -Scope CurrentUser -Force -ErrorAction Stop
}
Import-Module ExchangeOnlineManagement -ErrorAction Stop
Connect-ExchangeOnline -ShowProgress:$true -ErrorAction Stop | Out-Null
Write-Ok "Exchange Online verbunden."
} catch {
Write-Err2 "EXO-Verbindung fehlgeschlagen: $($_.Exception.Message)"
throw
}
# -------------------------------
# 5) Mailbox-Check (nur UserMailbox zulassen)
# -------------------------------
Write-Step "Prüfe Postfächer für alle Benutzer…"
$mailUsers = foreach ($u in $users) {
$mbx = $null
$probe1 = $u.PrimaryAddressHint
$probe2 = $u.UPN
try {
$mbx = Get-EXOMailbox -Identity $probe1 -ErrorAction Stop
} catch {
try {
$mbx = Get-EXOMailbox -Identity $probe2 -ErrorAction Stop
} catch {
$mbx = $null
}
}
if ($mbx -and $mbx.RecipientTypeDetails -eq "UserMailbox") {
Write-Info "OK: $($u.DisplayName) -> $($mbx.PrimarySmtpAddress)"
[pscustomobject]@{ DisplayName=$u.DisplayName; Smtp=$mbx.PrimarySmtpAddress.ToString() }
} else {
Write-Warn2 "KEIN UserMailbox: $($u.DisplayName) (übersprungen)"
[pscustomobject]@{ DisplayName=$u.DisplayName; Smtp=$null }
}
}
$withMailbox = $mailUsers | Where-Object { $_.Smtp }
$withoutMailbox = $mailUsers | Where-Object { -not $_.Smtp }
Write-Ok ("Mit UserMailbox: {0} | Ohne UserMailbox (skip): {1}" -f $withMailbox.Count, $withoutMailbox.Count)
# -------------------------------
# 6) Kontakte-Ordner ermitteln (lokalisiert) & Rechte setzen
# -------------------------------
function Get-ContactsFolderPath {
param([Parameter(Mandatory)][string]$MailboxSmtp)
$f = Get-MailboxFolderStatistics -Identity $MailboxSmtp -FolderScope Contacts `
| Where-Object { $_.FolderType -eq "Contacts" } `
| Select-Object -First 1
if ($null -eq $f) { return $null }
return "$MailboxSmtp" + ":\$($f.Name)"
}
Write-Step "Setze Ordnerberechtigungen (ReaderIdentity=$ReaderIdentity, Access=$AccessRight)…"
$results = foreach ($m in $withMailbox) {
$path = Get-ContactsFolderPath -MailboxSmtp $m.Smtp
if (-not $path) {
Write-Warn2 "Kein Kontakte-Ordner für $($m.DisplayName) ($($m.Smtp)) – SKIP"
[pscustomobject]@{User=$m.DisplayName; Mail=$m.Smtp; Result="SKIP"; Note="Kein Kontakte-Ordner"}
continue
}
Write-Info "Bearbeite: $($m.DisplayName) | Ordner: $path"
try {
$existing = $null
try { $existing = Get-MailboxFolderPermission -Identity $path -User $ReaderIdentity -ErrorAction Stop } catch {}
if ($existing) {
if ($existing.AccessRights -notcontains $AccessRight) {
if ($WhatIf) {
Write-Info "WOULD-UPDATE -> $AccessRight"
[pscustomobject]@{User=$m.DisplayName; Mail=$m.Smtp; Result="WOULD-UPDATE"; Note="$path -> $AccessRight"}
} else {
Set-MailboxFolderPermission -Identity $path -User $ReaderIdentity -AccessRights $AccessRight -ErrorAction Stop
Write-Ok "UPDATED -> $AccessRight"
[pscustomobject]@{User=$m.DisplayName; Mail=$m.Smtp; Result="UPDATED"; Note="$path -> $AccessRight"}
}
} else {
Write-Ok "OK (bereits $AccessRight)"
[pscustomobject]@{User=$m.DisplayName; Mail=$m.Smtp; Result="OK"; Note="bereits $AccessRight auf $path"}
}
} else {
if ($WhatIf) {
Write-Info "WOULD-ADD -> $AccessRight"
[pscustomobject]@{User=$m.DisplayName; Mail=$m.Smtp; Result="WOULD-ADD"; Note="$path -> $AccessRight"}
} else {
Add-MailboxFolderPermission -Identity $path -User $ReaderIdentity -AccessRights $AccessRight -ErrorAction Stop
Write-Ok "ADDED -> $AccessRight"
[pscustomobject]@{User=$m.DisplayName; Mail=$m.Smtp; Result="ADDED"; Note="$path -> $AccessRight"}
}
}
} catch {
Write-Err2 "Fehler bei $($m.DisplayName): $($_.Exception.Message)"
[pscustomobject]@{User=$m.DisplayName; Mail=$m.Smtp; Result="ERROR"; Note=$_.Exception.Message}
}
}
# -------------------------------
# 7) Ergebnis anzeigen & speichern
# -------------------------------
Write-Step "Ergebnisse zusammenfassen…"
$summary = $results | Group-Object Result | Select-Object Name,Count
$summary | ForEach-Object { Write-Ok ("{0}: {1}" -f $_.Name, $_.Count) }
Write-Step "CSV-Report speichern: $LogPath"
try {
$results | Export-Csv -Path $LogPath -NoTypeInformation -Encoding UTF8
Write-Ok "Report gespeichert."
} catch {
Write-Warn2 "Konnte CSV nicht speichern: $($_.Exception.Message)"
}
Write-Step "Fertig."
$results | Sort-Object Result, User | Format-Table -AutoSize


No Comments