CSV2EX

CSV2EX ist ein PowerShell-Skript um Kontaktedaten aus einer gelieferten oder erzeugten CSV-Datei in einer Exchange Umgebung (2007 und höher) als Kontakte anzulegen. Eigentlich ist dies ein wenig spektakuläres Thema, welches aber durch viele Sonderfälle doch nichts ganz trivial ist. Der Abgleich von Objekte verfolgt mich immer wieder und es gibt mit MiniSync und dem nie fertig gestellten MiniSync2 Vorgänge. CSV2EX ist insofern aber nun umfangreicher, da das "Matching" auf das Ziel mit zwei Durchläufen auch Umbenennungen erkennt und nicht mehr erst die Zielobjekte löscht und dann mit neuem Namen neu anlegt. Das ist deutlich aufwändiger als die bei MiniSync genutzte Technik zuerst die überzähligen oder nicht mehr zuzuordnenden Objekte im Ziel zu löschen und dann mit der Quelle als Master passend anzulegen. Dabei verlieren die Zielobjekte aber Mitgliedschaften oder anderweitig lokal gepflegte Properties.

Das Skript kann im Prinzip auch mit Exchange Online arbeiten. Knifflig ist die Automatisierung mit MFA. Eine Version, die per Graph als "App" arbeitet, habe ich noch nicht und die Exchange PowerShell V2 kann keine Anmeldung als App. Graph kann Orgwide Contacts aber nur lesen aber nicht schreiben.

Anforderungen

Die Funktion von CSV2EX ist mit wenigen Punkte schnell beschrieben:

  • CSV als Eingabe
    So kann man zwischen dem Export der CSV aus der Quelle und dem Ziel die Datei inspizieren, über verschiedene Wege übertragen, in einer abgeschotteten TestUmgebung nutzen und durchaus unterschiedliche Quellen, z.B. ERP  nutzen.
  • Export der CSV
    Anfangs habe ich die CSV-Datei getrennt erstellen lassen. Das was aber wieder ein weiteres Skript mit einer Fehlerprüfung etc. Mittlerweile sind Export und Import im gleichen Script, wobei der Export einfach nur CSVDE aufruft aber den Errorlevel überprüft.
  • CSV-Datei-Prüfung
    Um zu verhindern, dass falsche oder unvollständige Daten importiert werden, prüft das Skript die Eingabedaten vorab und erwartet auch einen besonderen "letzten" Datensatz.
  • Flexible Authentifizierung
    Viele Administratoren starten die Skripte nicht "als Administrator" und selbst dann ist die "integrierte Authentifizierung" nicht immer nutzbar. Daher sollten alle Wege auch für explizite Angabe der Anmeldedaten nutzbar sein
  • Neue Kontakte anlegen, bestehende Kontakte matchen, Konflikte erkennen
    Wenn im Ziel die Kontakte nicht vorhanden sind, dann muss das Skript diese anlegen. Allerdings muss es gut genug sein, bestehende Kontakte zu erkennen, und je nach Ablageort zu aktualisieren oder zu überspringen. Knifflig ist hier besonders das Erkennen von "Rename"-Aktionen in der Quelle.
  • Orphaned entfernen mit "Sicherung"
    Wenn Kontakte im Ziel vorhanden sind, die in der Quelle fehlen, dann sollen diese gelöscht werden. Als Absicherung soll eine maximale Anzahl der Löschungen pro Durchlauf spezifizierbar sein und das Skript muss sicherstellen, dass die Quelldatenliste komplett ist.
  • Dynamisches Nachladen
    Soweit möglich, reicht der Start des Skripts als "einfache PowerShell". Snapins für Exchange werden bei Bedarf nachgeladen oder eine Remote PowerShell eingebunden und am Ende wieder entfernt.
  • Flexible Konfiguration per XML
    Früher habe ich die Parameter direkt im Skript als Konstanten oder Parameter auf der Kommandozeile übergeben. Das funktioniert nicht sehr gut, wenn es sehr viele und lange Parameter sind. Die Ablage eines "Sync-Profils" als XML-Datei ist viel flexibler, um über eine Kommandozeile einfach nur die XML zu nutzen.
  • Umfangreiches Logging
    Dazu zähle ich eine "Transcript"-Datei pro Durchlauf, wobei die Daten nach einiger Zeit auch wieder aufgeräumt werden. Zudem sollten Fehler und Warnungen ebenso wie Start und Ende im Eventlog protokolliert werden. So wird eine zentrale Überwachung möglich

Überlegungen

Basierend auf den Anforderungen bis ich bei einem PowerShell-Skript gelandet, welches Export der Daten aus der Quelle als auch Import und Update der Kontakte im Ziel in einem Skript enthält, aber per Parameter bzw. Konfigurationsdatei dies gesteuert werden kann. Dies ist insbesondere hilfreich, um zwischen Export und Import die Datei zu kontrollieren oder etwas anzupassen.

Dann muss ich im Skript natürlich die "TargetAddress" ermitteln. Ich habe mich dazu entschieden, dass ich dazu eine Mailadresse aus dem Feld "ProxyAddresses" aus der Quelle oder alternativ das Feld "Mail" nutze. Per Konfiguration wird dazu aber die SMTP-Domäne bestimmt die als "Routingdomäne" fungieren soll.

Zu Sicherheit darf das Skript im Ziel nur in genau einer OU arbeiten, die dann aber auch exklusiv für Objekte dieses Abgleichs bestimmt sein muss. Sonst kann das Skript ja keine "überzähligen" Objekte erkennen und löschen.

In der OU werden die Kontakte dann einfach per LDAP angelegt und die Felder gefüllt. Natürlich könnte das auch ein "New-MailContact" gleicht mitmachen, aber es gibt Felder, die nicht durch Exchange Commandlets zu setzen sind. Vielleicht stelle ich das später doch einmal auf "Exchange Only" um, wobei dann die Einschränkung bestünde, dass zwingend Exchange vorhanden sein muss. So könnte CSV2EX auch Ziele ohne Exchange bedienen, wenn die Exchange befehle einfach auskommentiert werden.

Wenn jedoch Exchange im Ziel vorhanden ist, dann nutze ich natürlich die Exchange Remote PowerShell, um die Kontakte anzulegen und zu aktualisieren

Das Skript soll trotzdem einfach bleiben und kann daher keinen vollwertigen DirSync ersetzen. Daher gibt es einige Einschränkungen

  • Unidirektional, Ziel ist "Slave"
    Es ist also keine "Multi Master"-Replikation in beide Richtungen oder sogar bezogen auf einzelne Felder. Sie können natürlich durch zwei Sync-Profile die Postfächer einer Seite auf der anderen Seite als Kontakte anlegen lassen, wenn Sie die Kontakte der Gegenrichtung aus dem Export heraus halten. Das ist mit dem passenden LDAP-Filter sehr einfach.
  • Keine Gruppenmitgliedschaften
    Das Skript bekommt eine CSV-Datei mit Mail-Empfängern". Da können natürlich auch Verteiler enthalten sein aber sind als solche nicht erkenntlich. Selbst wenn ist es komplex, Gruppen im Ziel anzulegen, denn dann muss z.B. sichergestellt sein, dass auch alle Mitglieder in der Quelle auch im Ziel als Objekt bestehen. Ansonsten würden Mails nicht an wirklich alle Empfänger zugestellt. Wer diese Anforderungen hat, ist mit CSV2EX nicht passend bedient.

Export, Import, Funktionsweise

Weil es bei Kunden immer zu Verwirrungen kommt.

  • Export...
    .. beschreibt den Prozess die Daten aus der Quelle in eine CSV-Datei zu exportieren. Dazu nutze ich NICHT PowerShell o.ä. sondern verwende einfach CSVDE.
  • Import ...
    Beschreibt den Prozess die CSV-Datei einzulesen und im Ziel entsprechenden Kontakte zu verwalten.

Die Sichtweise ist also nicht aus der Perspektive des Programms sondern der Quelle und dem Ziel zu sehen. CSV2EX ist ja kein richtiges "MetaDirectory" mit einem eigenen Datenbestand, sondern die CSV-Datei ist nur temporär ein Mittel zum Zweck bzw. ein Transportmedium, wenn der Export und Import mangels transparenter Netzwerkkopplung nicht ausgeführt werden kann.

Auch wenn das Skript eigentlich sowohl die Quelle also auch das Ziel direkt per LDAP ansprechen könnte, erfolgt der Export und Import-getrennt und hintereinander. So muss ich beim Import nicht den Sonderfall berücksichtigen, wenn der Export ein Problem hat. Viel wichtiger ist aber, dass man nach dem Export erst einmal pausieren könnte um die CSV-Datei zu inspizieren und gegebenenfalls zu ändern. Zudem kann der Import so vorab alle Daten auf Gültigkeit prüfen, ehe man schon Elemente geändert oder womöglich gelöscht hat.

Eingabe CSV

Die einfachste Version einer CSV-Datei sieht etwas so aus (gekürzt):

DN,sn,givenName,proxyAddresses,mail
"CN=testUser1,CN=Users,DC=source,DC=de",TesUser1surname,TestUser1givenname,smtp:test1@source.de;smtp:test1b@source.de,Test1@source.de
"EOF"

Es ist das fast klassische Format eines Export mit CSVDE, welches die Überschriften in der ersten Zeile enthält und die weiteren Zeilen die Datensätze sind Es gibt aber zwei Abweichungen.

  • UNICODE
    Ich nutze nicht ANSI sondern UNICODE als Dateiformat, da damit auch Sonderzeichen etc. problemlos übertragen werden und nicht gesondert behandelt werden müssen. Ansonsten haben Sie nämlich z.B.: sowas in der CSV-Datei. Nicht, dass es nicht auch lösbar wäre aber ich bin hier erst mal den einfacheren Weg gegangen.
X'5363686cc3bc746572', = Schlüter
  • EOF-Marke
    Beim DirSync ist es zwingend erforderlich die Vollständigkeit der Daten zu prüfen. Daher habe ich abweichend vom CSVDE-Standard die Forderung, dass die letzte Zeile nur aus dem String "EOF" besteht. ein Export-Prozess sollte daher diese Zeichenkette einfach hinten anhängen, damit CSV2EX die Datei als "vollständig" ansieht

Der zweite Aspekt bezieht sich auf die zwingend erforderlichen Felder. Das Skript importiert diese CSV-Datei und nutzt diese Felder um erste Plausibilitätsprüfungen durchzuführen. So muss am Ende das EOF, die folgenden Felder müssen vorhanden sind und natürlich muss es mindestens zwei Datensätze (Header und EOF) geben.

  • DN
    Das Feld DN muss vorhanden sein. Es wird zum Import nicht wirklich gebraucht, aber in jedem CSVDE-Export ist es vorhanden und wenn die letzte Zeile ein "EOF" enthält, dann frage ich genau dies als Validierung im Skript ab. Hier der Auszug:
elseif ($csvlist[$totalsource-1].dn -ne "EOF") {
   exitonerror -eventID 6 -Message ("csv2ex:ImportCSV:Incomplete CSVFile missing trailing EOF in DN"+$error) $true
}
  • MAIL
    Das Feld MAIL ist besonders wichtig, da das Skript diesen Wert als "weltweit eindeutig" ansieht und daraus den DN im Ziel bildet. Ein Verzeichnisabgleich ohne Mailadresse ist mit CSV2EX nicht vorgesehen. Das Skript prüft im Rahmen der Validierung auch, ob das Feld in der CSV-Datei wirklich eindeutig ist und auch im Ziel wird so geprüft, ob es das Objekt schon gibt. Mail ist ein universelles "Match"-Property
    Dieses Feld wird auch genutzt, um im Ziel die "TargetAddress" zu formen, wenn diese nicht im optionalen Feld "ProxyAddresses" mit gegeben wird.

Alle anderen Felder sind "Optional", bzw. werden zueinander zusammengebaut.

  • sn, givenname, Displayname
    Es ist natürlich ratsam, dass z.B. Vorname und Nachname als auch der Displayname vorhanden und gefüllt sind, da das Script ansonsten im Minimalfall die Mailadresse als Displayname setzt
  • ProxyAddresses
    Dieses Feld können die Exchange Administratoren und bei einem Verzeichnisabgleich sollte man diese Informationen auch übertragen. Nur so kann man im Ziel einen Benutzer auch mit seinen alternativen Adressen erreichen. Zudem kann das Script, wenn man ihm die RoutingDomain mitteilt, aus den ProxyAdressen eine alternative "TargetAddress" ermitteln. Zuletzt kann über diese Adressliste auch erkannt werden, wenn ein Benutzer umbenannt wurde. Er hat dann in der Regel die alte Mailadresse noch als ProxyAdresse, so das im Ziel nicht irrtümlich ein Objekte gelöscht und aus allen Verteilern entfernt wird, und dann wieder mit dem neuen Namen angelegt wird.
  • weitere Felder
    Es ist relativ einfach, den Code um eigene Felder zu erweitern oder Felder anzupassen, d.h. den Displayname um einen Suffix zu erweitern.
  • Felder löschen
    Ohne Meta-Directory ist es nicht möglich zu erkennen, ob Daten im Export nicht mehr enthalten sind. Wenn also im Export ein Feld "leer" ist, dann kann ich es beim Import auch leeren, aber wenn ein Feld im Export gar nicht mehr vorhanden ist, dann wird das Ziel-Feld nicht verändert.

Steuerung per XML

Anfangs hatte ich die Parameter direkt im Code quasi als Konstanten hinterlegt und sehr schnell dann als Parameter implementiert. Das ist in PowerShell ja sehr einfach. Aber es wurden immer mehr und mehr Parameter und der Aufruf des Skripts wurde immer länger. Zudem habe ich gemerkt, dass es immer mehr "Paarungen" gibt und so war der Weg vorgezeichnet, dass ich die Konfiguration einer Synchronisationspartnerschaft vielleicht komplett als XML-Datei auslagere und beim Aufruf von CSV2EX nur noch die Parameter-XML-Datei angeben muss. Dies ist auch der aktuelle Stand:

Sie können schon sehen, dass es mittlerweile eine ganze Menge an Parameter geworden sind.

Ermittlung TargetAddress

Zum Verständnis dieses Abschnitts sollten Sie auch die Seite TargetAddress gelesen haben. für einen Kontakt in einem Ziel ist die Angabe der eigentlichen Zieladresse notwendig, um ein effektives Mailrouting zu erreichen. Hier gibt es zwei Szenarien:

  • Getrennter Adressraum
    Wenn QuellUmgebung und ZielUmgebung unterschiedliche SMTP-Domänen nutzen, dann kann ein Kontakt im Ziel einfach auf die primäre Mailadresse in der Quelle verweisen. Das Quellfeld "Mail" ist hier optimal als "Target-Address" in der anderen Organisation. Alternativ kann man sich die primäre SMTP-Adresse aus den ProxyAdresses auslesen.
  • Shared Addressraum
    Nutzen beide Welten einen gemeinsamen Adressraum, dann könnte auch die primäre Adresse genutzt werden, wenn man denn einen Send-Connector mit der gemeinsamen SMTP-Domäne als Adressraum zur anderen Seite konfiguriert. Aber schön ist das nicht, da es zu Mailloops kommen kann und das Messagetracking erschwert ist. Ganz unbrauchbar wird die Nutzung einer solchen Domäne, wenn drei oder mehr Systeme die gleiche Mailadresse nutzen. Dann sollten Sie immer mit "Routingdomänen" arbeiten.

Um für beide Fälle gerüstet zu sein, erwartet das Script eine SMTP-Domäne als Parameter die zur Bestimmung der SMTP-Adresse zum Routing genutzt wird. Folgende Logik kommt hier zum Einsatz:

Das Flussdiagramm sieht etwas verwirrend aus aber ich musste verschiedene Optionen und Parameter unterbringen. Das Bild ist nach der Codierung entstanden und man sieht gut, dass es durchaus "bessere" Pfade gegeben hätte. Bei nächster Gelegenheit möchte ich den Code optimieren und dann das Bild anpassen. Aber so ist die aktuelle Funktion und entsprechend ist es dokumentiert.

Die Überlegung dahinter ist, dass im Regelfall in der Quelle auch die ProxyAddresses gepflegt sind, in denen die eindeutige Routingadresse als zusätzliche Adresse hinterlegt ist. Über den Parameter der RoutingDomain kann das Skript dann korrekt eine eindeutige Routingadresse für das Feld "TargetAddress" ermitteln. Nur wenn dies nicht möglich ist unddie Funktion "Fallback" aktiviert ist, dann wird das Feld "Mail" herangezogen. Aber auch das könnte leer sein und muss dann zu einem Fehler führen.

Verarbeitung mit Konflikt und Rename Ermittlung

Das Skript darf natürlich nicht einfach das Feld "Mail" nehmen und dann einen Kontakt im Ziel anlegen. Es kann ja schon bestehende Kontakte aus früheren Läufen geben. Gerade in Firmen ist es nicht unüblich, dass Objekte früher schon im Ziel angelegt wurden. Entsprechend muss das Skript im Ziel erst mal nach den Objekten suchen und schauen. Diese können in der Ziel-OU sein, und werden dann verwaltet oder sie sind in einer anderen OU und werden übersprungen und als Warnung ausgegeben.

Nun kann es auch passieren, dass ein Objekt in der Quelle umbenannt wird und sich entsprechend auch das Feld "Mail" ändert. Ohne besondere Logik würde das Skript das nun verwaiste Objekt im Ziel löschen und ein neues Objekt anlegen. Das ist unschön, weil so z.B. Gruppenmitgliedschaften im Ziel verloren gehen und der Nicknamecache auf den Clients ungültig wird. Daher durchläuft das Skript mehrere Iterationen.

  • Lauf 1: Direkt Match
    Zuerst werden anhand der gebildeten DN-Adresse im Ziel die übereinstimmenden Objekte gesucht und sowohl in der Quelle als auch im Ziel gemerkt (Hashtable). Bei normalen Lösung werden hier ein Großteil der Objekte gefunden
  • Lauf 2: Proxy Address
    Dann werden die ProxyAdressen der im Ziel noch nicht zugeordneten Objekte in der Quelle anhand der Proxyadressen dort gesucht. So finden wir auch umbenannte Objekte. Beim Ziel muss bei diesen Objekten dann der DN geändert werden (rename), damit er dem neuen DN entsprechen und zukünftig wieder im Lauf 1 gefunden werden.
  • Lauf 3 Orphaned
    Dann werden alle Objekte im Ziel, die nicht zu einem Objekt in der Quelle zugeordnet werden konnten, gelöscht. Das Löschen vorab verhindert, dass später bei der Neuanlage Konflikte mit anderen Objekten auftreten können.
  • Lauf 4: Update/Create
    Erst jetzt werden anhand der Tabelle der Quelle im Ziel die gefundenen Objekte mit den Exchange Commandlets angelegt und aktualisiert. Felder die nicht durch Exchange aktualisiert werden, werden per ADSI direkt geschrieben.

So ist sichergestellt, dass alle Änderungen möglichst konfliktfrei im Ziel angewendet werden. Als Langform hier noch mal ein Flussdiagramm.

Das ganze noch mal in Kurzform

  1. Import der Quellen aus der CSV
    Ich lade die Daten in eine Hashtabelle, welche ich noch um ein Feld zur Speicherung des DN des Zielobjekts erweitere.
  2. Matching mit dem Ziel
    Dann suche ich anhand der Daten das Objekt im Ziel mit zwei Läufen. Zuerst nutze ich ein primäres Feld, z.B. Personalnummer oder Mail-Adresse. Wer dann noch nicht gematched ist, wird in einem zweiten Lauf mit einer "Ähnlichkeit" gesucht, z.B. indem man die ProxyAddresses der Quelle mit den ProxyAddresses im Ziel vergleicht. So werden auch Namensänderungen (Heirat etc.) gefunden und das Objekt aktualisiert.
  3. Delete Orphaned
    Dann werden im Ziel alle Objekte bereinigt (Gelöscht oder zumindest deaktiviert), damit die Adressen frei werden.
  4. Add/Update
    Zuletzt werden die fehlenden Objekte neu angelegt und dann  die bestehenden Objekte aktualisiert.
    Damit möchte ich erst mal "readOnly" die Objekte im Ziel anhand der Daten suchen und matchen.

Diese Vorgehensweise unterscheidet sich von meinen früheren Projekten wie CSVSync - Framework und MiniSync, bei denen ich erst danach aufgeräumt habe.

Monitoring

CSV2EX kann zwar interaktiv durch einen Administrator regelmäßig ausgeführt werden, aber sehr schnell werden Sie einen "geplanten Task" einrichten, der ihnen das Skript immer mal wieder ausführt. Und genau darauf habe ich im Code Rücksicht genommen, indem hoffentlich alle Fehlerfälle kontrolliert abgefangen werden (Try/Catch oder $error-Abfragen) und entsprechend bewertet und gemeldet werden. Das Skript nutze dazu

  • Transcript-Logfile
    Alles, was das Skript tut, wird mittels "Start-Transcript" in Textdateien protokolliert, deren Dateinamen den Startzeitpunkt des Skripts enthalten und nach einer in der Konfiguration einstellbaren Zeit in Tagen wieder gelöscht werden.
  • Eventlog
    Zudem generiert das Skript Eventlogs für den Start, das Ende aber auch sonstige Warnungen und Fehler, so dass sie sehr einfach diese Funktion überwachen können. Das Skript nutzt dazu eine eigene "Source". die einmalig als Administrator angelegt werden muss, zumindest wenn das Skript selbst kein lokaler Administrator ist. Die folgenden Zeilen sind dazu auszuführen:
if (![System.Diagnostics.EventLog]::SourceExists("csv2ex")) {
   new-eventlog -logname 'Application' -Source 'csv2ex'
}

Rechte und Umgebungsbedingungen

Das Skript tut bei verschiedenen Kunden mittlerweile seinen Dienst und gleich Adressen aus Exchange Organisationen untereinander aus und auch fremde Datenquelle können als Kontakte recht einfach über die CSV-Datei importiert und gepflegt werden. Aber ein paar Anmerkungen:

  • ADSI-Rechte
    Das Konto muss zum Export in der Quelle alle relevante Objekte lesen können. In der Regel reicht es dazu Mitglied in "DomainUser" zu sein. Im Ziel werden aber Objekte in der einen OU auch per ADSI erzeugt, geändert und gelöscht. In dieser Ziel-OU muss das Dienstkonto die entsprechenden rechte für Objekte vom Typ "Kontakt" haben. Es muss zudem wie in der Quelle im Ziel über den globalen Katalog nach Objekten suchen können, um Konflikte zu erkennen.
  • Exchange PowerShell
    Das Skript nutzt die Exchange Remote PowerShell per HTTP und bindet sich diese als PSSession ein. Das bedeutet natürlich, dass das ausführend Konto dies auch darf.
    • enable-psremoting auf dem Client
      Der ausführende Computer muss natürlich "PS-Remoting nutzen" dürfen. Dazu zählen beim Einsatz von HTTPS natürlich auch die entsprechenden Einstellungen im Proxy und Zertifikate
    • enable-psremoting auf dem Exchange Server
      Auch der Exchange Server muss den Zugriff der Clients aus der Ferne zulassen und muss entsprechend konfiguriert sein. Siehe auch PS Remote und PSExRemote
    • RBAC-Rollen
      Das Konto muss die Rechte haben, per RBAC in der ZielOU Exchange Kontakte zu verwalten. Das Konto sollte daher "View Only Org Management" Rechte haben und z.B. "Recipient Admin" in der Ziel-OU.
  • Verteiler
    Sie können in der Quelle natürlich auch Mailverteiler  in die CSV-Datei exportieren und entsprechend im Ziel anlegen. Sie erscheinen aber nur als "Kontakte". Im Ziel ist nicht erkennbar, dass der Empfänger ein Verteiler ist. Schon gar nicht werden Mitgliedschaften repliziert. Dies ist kein trivialer Prozess und kann CSV2EX nicht leisten. Mail an einen gemischten Verteiler werden vom Ziel also erst an die Quelle gesendet, die dann die Mail an die Mitglieder aufteilt und ggfls. wieder an Empfänger im ursprünglichen Mailsystem zurück sendet.
  • PublicFolder
    Sie können in der Quelle natürlich auch die email-aktivierten öffentlichen Ordner in die CSV-Datei exportieren und entsprechend im Ziel anlegen. Sie erscheinen aber nur als "Kontakte". Im Ziel ist nicht erkennbar, dass der Empfänger ein Ordner ist
  • Postfach darf keine RoutingDomain in der "falschen" Site haben.
    Informieren Sie sich genau über das Konzept der Routingdomäne. Die Routingdomäne ist ein gutes Kriterium, um z.B. Empfänger in der Quelle zu exportieren. Im Ziel darf es aber keinen Empfänger hierzu geben, damit keine Schleifen entstehen.
  • AD-Replikation
    Um Probleme mit der Latenzzeit zu umgehen ist man gut damit beraten, immer den gleichen DC für alle Aktionen zu nutzen. Dazu muss man bei ADSI den DC direkt mit angeben aber auch bei den Exchange Commandlets. Es scheint dennoch manchmal zu passieren, dass Exchange den angegebenen DC nicht nutzt oder vielleicht den lokalen AD-Cache bemüht, der noch nicht aktualisiert ist. So kann es passieren, dass per ADSI ein AD-Kontakt angelegt wird aber der Enable-Mailcontact das Objekt noch nicht aktivieren kann. Das ist unschön aber ich möchte nicht bei jedem Objekt eine Verzögerung einbauen. Die fehlende Aktion wird beim nächsten Durchlauf aber nachgeholt.

Falle bei Enable-Contact

Zuerst wollte ich komplett auf ADSI verzichten und alles über die Exchange remote PowerShell per HTTPS machen. Das hätte den Vorteil gehabt, dass man wie in der Cloud nur noch Port 443 nutzen müsste und komplett ADSI entfallen kann. Office 365 nutzt Exchange aber auch eigene MSOL-Commandlets für die AD-Verwaltung, die es On-Prem nicht gibt. Bei der Entwicklung des Skripts ist aber ein unerwarteter Effekt bekannt geworden, der diese Überlegung torpediert hat:

Wenn man mit "Enable-Contact" über den Parameter "-emailaddresses" die Mailadressen ändert, dann schreibt/ersetzt es wie erwartet bestehende MailAdressen, aber ändert auch das Feld Mail. Es ändert aber nicht das Feld "TargetAddress". Daher muss man zuerst das Feld "TargetAddress" setzen und dann die Emailaddresses" kopieren. Schaltet man hingegen im Ziel dann die Pflege durch Empfängerrichtlinien auf dem Objekte an, dann wird auch das Feld "Mail" nicht mehr gesetzt, wenn man die ProxyAddresses aktualisiert. Das Commandlet behauptet, es hätte sich nichts geändert.

Aktuell wird ein Kontakt also wie folgt aktiviert:

# Kontakt anlegen 
$adsicontact = $targetou.psbase.Children.Find(("cn="+$targetcn.replace(",","\,")))

# Feld Mail füllen Update-property ([ref]$adsicontact) "mail" $mail
$adsicontact.setinfo()

Später wird der Kontakt zum Exchange Mailcontact:

$excontact = enable-mailcontact `
   -identity $contactdn `
   -alias $alias `
   -ExternalEmailAddress $targetaddress `
   -Displayname $Displayname `
   -PrimarySMTPAddress $mail  `
   -ErrorAction "continue" `
   -domaincontroller $config.dcname

Und danach noch mal aktualisiert:

set-mailcontact `
   -identity $contactdn `
   -ErrorAction "continue" `
   -EmailAddressPolicyEnabled $false `
   -EmailAddresses $proxyaddresses `
   -Displayname $Displayname `
   -ExternalEmailAddress $targetaddress  `
   -domaincontroller $config.dcname

Download

Bitte haben Sie Verständnis, dass sehr leistungsfähige Skripte, die weit über eine Funktion als Muster" hinausgehen, nicht öffentlich verfügbar sind. Solche Skripte sind in kleinen Umgebungen nur bedingt bis gar nicht sinnvoll einsetzbar. In größeren Umgebungen hingegen sind zu oft individuelle Anpassungen erforderlich. Sie erhalten diese nur im Rahmen von Support oder Dienstleistung.
Informationen, warum diese Skripte nicht öffentlich sind, finden Sie auf nicht public.

Weitere Links