Redemption, CDO und PowerShell

Eigentlich würde ich diese Seite unter Produkte rund um Exchange ansiedeln, da Redemption kommerziell ist, aber die Information für Entwickler wichtig ist, habe ich diese Seite doch bei Programmieren mit Exchange abgelegt.

Am 1. Okt 2022 schaltet Microsoft in Exchange Online "BasicAuth" ab. Es gibt keinen dokumentierten Weg ein OAUTH-Token für Extended Mapi zu bekommen.
Microsoft doesn't expose permissions to generate OAuth tokens for Extended MAPI access to mailboxes
https://docs.microsoft.com/en-us/outlook/troubleshoot/authentication/expose-permissions-issue-with-mapi-oauth-tokens

Es gibt auf der MSXFAQ sehr viele Beschreibungen und Codebeispiele zum Einsatz von Outlook VBA und MAPI/CDO, so dass ich lange auf Redemption verzichtet habe. Aber die Microsoft Schnittstellen kranken doch an einigen Stellen, die den Wunsch nach Drittprodukten aufkommen lassen, z.B.

  • Outlook Sicherheitswarnung
    Die auf Outlook Sicherheitswarnung beschriebenen Probleme kann man mit Redemption umgehen
  • Eingeschränkter CDO Support
    Laut Microsoft funktioniert CDO unter.NET und PowerShell nicht zuverlässig, da sich das Speichermanagement angeblich nicht verträgt. Redemption hingegen scheint dies gelöst zu haben, so dass man hier auch aus .NET heraus mit dem COM-Objekt einfach arbeiten kann.
  • PowerShell Unterstützung
    Meine ersten Gehversuche mit der PowerShell und CDO waren sehr ernüchternd, da die Microsoft CDO keine Typlibrary mitbringt, so dass Tab-Completion der PowerShell nicht funktioniert. Ich konnte mich auch nicht ordentlich an andere Exchange Postfächer anmelden, da die "CDOSessoin.LOGON"-Methode anscheinend aus der PowerShell keine Parameter unterstützt. Mit Redemption gibt es diese Einschränkung nicht, Es gibt sogar weitere Methoden um direkt Exchange Postfächer oder PST-Dateien einzubinden
  • MSG/EML-Export/Import
    Zudem kann man mit Redemption die Mails sehr einfach als Datei exportieren aber auch importieren. Eine Ideale Basis für eigene Migrationslösungen.
  • Erweiterte MAPI-Funktion
    MAPI erlaubt z.B. eine "Synchronisation" von Inhalten, indem man sich den letzten Stand merkt und später einfach diese Info an Exchange sendet, welcher dann die seither geänderten Objekte zurück gibt.
  • Redemption ohne CDO/Outlook
    Der Entwickler hat sogar den Spagat geschafft, die COM-Klassen anzubieten, selbst wenn auf dem System kein Outlook oder CDO installiert ist. Nur die rudimentäre MAPI-Funktion muss vorhanden sein.
  • Profilbearbeitung
    Zudem erlaubt Redemption über die PROFMAN-Library sogar eine Konfiguration der MAPI-Profileinstellungen per Skript. Das was bislang den C++ Entwicklern vorbehalten.

All das kann Redemption lösen und ist sogar mit einer einzigen DLL die auf das Zielsystem kopiert und registriert werden muss, einfach zu installieren. Das Lizenzmodell ist auch sehr interessant. Sie können eine Entwicklungsversion kostenfrei herunter laden, welche aber mit einer Lizenzmeldung darauf hinweist und damit eine vollständige Automatisierung nicht erlaubt. Die Vollversion ist mit 150 Euro für den Entwickler und einer kostenfreie Weitergabe der DLL an davon abgeleitet kommerzielle Produkte ein echtes Schnäppchen.

Redemption und STA/MTA

Bei der Entwicklung von Programmen muss man genau beachten, ob man "Single threaded" oder "Multi Threaded" programmiert. Und genau das kann zu Problemen führen. Bei meinen Tests, einen öffentlichen Ordner zu öffnen, habe ich folgendes Verhalten beobachtet:

Programmiersprache Status

VBScript

Funktioniert

PowerShell

Fehler

C# Console Application (VS2008 Express)

Fehler (default)
Erfolgreich (mit STATHREAD)

C# Windows Forms Application (VS2008 Express)

Erfolgreich

Das war natürlich erst einmal etwas verwirrend und zusammen mit Dmitry Streblechenko und der PowerShell Newsgroup haben wir dann doch die Ursache gefunden. War war nicht das Fehlen der Windows Message Pump bei Console Anwendungen (Keine Formulare) sondern die Funktion, das PowerShell als auch Console Anwendungen anscheinend "Muti Threading" nutzen. Die Console Anwendnug konnte man mit einem "[STATHREAD]" zwingen, einen Single Thread zu nutzen und damit die Funktion zu erhalten. Die Betriebsart STA kennt aber PowerShell 1.0 noch nicht. (siehe auch http://blogs.msdn.com/PowerShell/archive/2007/03/23/thread-apartmentstate-and-PowerShell-execution-thread.aspx). Erst PowerShell 2 kann man mit der Option "-STA" in einen Single Thread Mode zwingen.

Redemption und PowerShell

Achtung mit PowerShell 1
Diese Beispiele funktionieren nur unter Einschränkungen zuverlässig, da PowerShell 1 per Default MultiThreaded arbeitet.

Je mehr Lösungen ich mit PowerShell entwickle, desto schmerzlicher war die Einschränkungen, dass ich damit kein CDO nutzen konnte. Also habe ich mich umgeschaut und bin dann doch bei Redemption gelandet, da ich zum einen per PowerShell nicht wirklich einfach die Exchange Webservices nutzen wollte (die mich dann eh auf E2007 beschränkt hätten) und ich das CDO Objektmodell doch schon recht gut kenne. Und zusammen mit der PowerShell kann man all das schön "interaktiv" machen.

Hier sieht man schon mal den Aufbaue einer RDO-Session mit einer expliziten Anmeldung am Exchange Server, ohne vorher erst ein Profil anlegen zu müssen:

PS> $redemptionsession = New-Object -com redemption.rdosession
PS> $redemptionsession.LogonExchangeMailbox("fcarius","nawsv002")
PS> $redemptionsession


ProfileName                  : {D83E61D0-0764-4852-A8D1-EE03492CCF61}
LoggedOn                     : True
MAPIOBJECT                   : System.__ComObject
Stores                       : {Postfach - Carius, Frank}
AddressBook                  : System.__ComObject
CurrentUser                  : System.__ComObject
Accounts                     : {}
AuthKey                      :
TimeZones                    : {(GMT-12:00) Internationale Datumsgrenze (Westen), (GMT-11:00) Midway-Inseln, Samoa, (GM
                               T-10:00) Hawaii, (GMT-09:00) Alaska...}
Profiles                     : {CDO_Admin@msxfaq.local_00002BD8_00000001, CDO_Administrator@msxfaq.local_00002B7C_00000
                               001, CDO_Administrator@msxfaq.local_00003D3C_00000001, CDO_Administrator@msxfaq.local_00
                               003E10_00000001...}
ExchangeConnectionMode       : 800
ExchangeMailboxServerName    : NAWSV002
ExchangeMailboxServerVersion : 8.1.291.1
OutlookVersion               : 12.0.6316.5000
CurrentWindowsUser           : System.__ComObject

Die Ausgabe zeigt mir, dass ich angemeldet bin (LoggedOn : True) und ich auch ein Postfach habe. Ich kann mir nun auch direkt einen Ordner binden. Dazu verwende ich eine der folgenden Konstanten:

$olAppointmentItem = 1;
$olFolderDeletedItems = 3;
$olFolderOutbox = 4;
$olFolderSentMail = 5;
$olFolderInbox = 6;
$olFolderCalendar = 9;
$olFolderContacts = 10;
$olFolderJournal = 11;
$olFolderNotes = 12;
$olFolderTasks = 13;
$olFolderDrafts = 16;
$olPublicFoldersAllPublicFolders = 18;
$olFolderConflicts = 19;
$olFolderSyncIssues = 20;
$olFolderLocalFailures = 21;
$olFolderServerFailures = 22;
$olFolderJunk = 23;

Und hole mir z.B. den Posteingang in eine neue Variable:

PS > $folder = $redemptionsession.Stores.GetDefaultFolder($olFolderInbox)
PS > $folder


MAPIOBJECT             : System.__ComObject
Session                : System.__ComObject
DefaultMessageClass    : IPM.Note
Description            :
EntryID                : 000000007C317BB2CD33D011AFC9008029638ABA0100F1A817713F07D011AF9E008029638ABA00000001713F0000
Name                   : Posteingang
Parent                 : System.__ComObject
StoreID                : 0000000038A1BB1005E5101AA1BB08002B2A56C20000454D534D44422E444C4C00000000000000001B55FA20AA6611
                         CD9BC800AA002FC45A0C0000004E41575356303032002F6F3D4E657420617420576F726B20476D62482F6F753D5061
                         646572626F726E2F636E3D526563697069656E74732F636E3D6663617269757300 unReadItemCount        : 58
Items                  : {Carius, Frank, Carius, Frank, , Carius, Frank...}
Folders                : {_MSXFAQ ToDo, _Spam, _test, Aktuelle Vorgänge...}
HiddenItems            : {, , , ...}
Store                  : System.__ComObject
AddressBookName        : Posteingang
ShowAsOutlookAB        : False
DefaultItemType        : 0
WebViewAllowNavigation : True
WebViewOn              : False
WebViewURL             :
DeletedItems           : {Carius, Frank, Carius, Frank, Carius, Frank, Carius, Frank...}
FolderKind             : 1
ACL                    : {Standard, Anonym}
FolderPath             : \\Postfach - Carius, Frank\Posteingang
FolderFields           : {}
IsInPFFavorites        : False
DeletedFolders         : {}
ExchangeSynchonizer    : System.__ComObject
ShowItemCount          : 1
ExchangeSynchronizer   : System.__ComObject

Man sieht direkt, dass ich zu dem Zeitpunkt 58 "ungelesene" Elemente hatte und welche unterordner es gibt. Ich kann natürlich nicht nur lesen, sondern auch bestimmte Properties schreiben. Wenn ich nun die Mails in dem Ordner ansprechen will, dann kann ich mir über "Items" einfach die Auflistung holen, welche ich entweder als Collection durchlaufen kann, oder mit GetFirst/GetNext abarbeiten kann. Hier mal die erste Mail. (Ausgabe gekürzt)

PS> $mail = $folder.Items.GetFirst()
PS> $mail

MAPIOBJECT                        : System.__ComObject
Session                           : System.__ComObject
EntryID                           : 000000007C317BB2CD33D011AFC9008029638ABA0700F1A817713F07D011AF9E008029638ABA0000000
                                    1713F0000EAB40024B97AD74587304851424699D60013B095B2D90000
Subject                           : Zugestellt: Accepted: Team Meeting
AlternateRecipientAllowed         : False
AutoForwarded                     : False
BCC                               :
BillingInformation                :
Body                              : Ihre Nachricht wurde den folgenden Empfängern zugestellt: User2 <mailto:user2@msxfaq.de>
                                    Betreff: Accepted: Team Meeting
                                    ________________________________
                                    Mit Microsoft Exchange Server 2007 gesendet
BodyFormat                        : 2
CreationTime                      : 01.10.2008 12:17:47
HTMLBody                          : <html>
                                    <Head></head><body>
                                    <p><b><font color="#000066" size="3" face="Arial">Ihre Nachricht wurde den folgende
                                    n Empfängern zugestellt:</font></b></p>
...
                                    </body>
                                    </html>
Importance                        : 1
MessageClass                      : REPORT.IPM.Schedule.Meeting.Resp.Pos.DR
ReceivedTime                      : 01.10.2008 12:17:47
Size                              : 998
Submitted                         : False
To                                : Ulbrich, uwe unRead                            : True
RTFBody                           : {\rtf1\ansi\ansicpg1252\fromhtml1 \fbidis \deff0{\fonttbl
                                    {\f0\fswiss Arial;}
                                    {\f1\fmodern Courier New;}
                                    {\f2\fnil\fcharset2 Symbol;}</html>}}
Attachments                       : {}
Recipients                        : {Ulbrich, uwe}
HidePaperClip                     : False
Sender                            : System.__ComObject
SentOnBehalfOf                    : System.__ComObject
Store                             : System.__ComObject
Parent                            : System.__ComObject
Actions                           : {Reply, Reply to All, Forward, Reply to Folder}
Conflicts                         : {} UserProperties                    : {}
Modified                          : False
ReportText                        : Your message
                                          To: Ulbrich, uwe
                                          Subject:    Accepted: Team Meeting
                                          Sent:    01.10.2008 12:17
                                    was delivered to the following recipient(s): Ulbrich, uwe on 01.10.2008 12:17

Noch mal als Zusammenfassung. Es hat mich also gerade mal 5 Zeilen gekostet, um ohne Outlook in einen Exchange Store zuzugreifen.

PS> $redemptionsession = New-Object -com redemption.rdosession
PS> $redemptionsession.LogonExchangeMailbox("fcarius","nawsv002")
PS> $folder = $redemptionsession.Stores.GetDefaultFolder($olFolderInbox)
PS> $mail = $folder.Items.GetFirst()

Sie können sich sicher vorstellen, dass es nicht sehr viel schwerer ist, um diese Zeilen eine Schleife zu bauen, um mehrere Elemente, Ordner oder sogar alle Postfächer zu durchlaufen.

Weitere Links

$outlook = New-Object -COM Outlook.Application 
$redemtionOutlook = New-Object -COM Redemption.RDOSession 
$redemtionOutlook.Logon() 
$redemtionOutlook.MAPIOBJECT = $outlook.Session.MAPIOBJECT 
$msg = $redemtionOutlook.GetDefaultFolder(6).Items.Add(0) 
$msg.Import("c:\testmail.msg", 3) 
$msg.Save() 
$redemtionOutlook.Logoff()