MGGraph Mail

Mit Microsoft Graph gibt es eine sehr leistungsfähige API, mit der auch Elemente im Exchange Online Postfach bearbeitet werden können. Andere APIs wie EWS, CDO, MAPI, IMAP sind in Exchange Online entweder als "Legacy" oder "Depreciated" gekennzeichnet oder nicht leistungsfähig genug. Graph erlaubt den Zugriff durch den Anwender (Delegated) als auch durch eine Applikation. Auf dieser Seite zeige ich, wie ich mittels der Graph PowerShell mich mit meinem Postfach verbinde und Elemente abrufe, verschiebe und lösche. Es ist daher zukünftig ein sehr guter Weg zur Automatisierung.

Installation

Die Graph PowerShell ist normalerweise nicht auf ihrem Computer. Sie müssen diese zuerst installieren. Die Installation der Graph PowerShell habe ich schon beschrieben. Sie müssen für diese Sete aber nicht das komplett 600+MB-Paket installieren. Uns reicht das Modul für Mail

Install-Module microsoft.graph.mail

Bei der Installation werden entsprechende Abhängigkeiten mit berücksichtigt. So wird sicherlich auch noch "Microsoft.Graph.Authentication" mit installiert.

Berechtigungen

Ehe ich mich aber mit Graph als Benutzer verbinde, muss ich die die Berechtigungen ermitteln. Ich möchte erst einmal nur meine Ordner und Mails darin auflisten.

PS C:\> Find-MgGraphCommand -command Get-MGUserMailFolder | Select -First 1 -ExpandProperty Permissions

Name           IsAdmin Description                         FullDescription
----           ------- -----------                         ---------------
Mail.Read      False   Read your mail                      Allows the app to read email in your mailbox.
Mail.ReadBasic False   Read user basic mail                Allows the app to read email in the signed-in user's mailbo…
Mail.ReadWrite False   Read and write access to your mail  Allows the app to read, update, create and delete email in …

PS C:\> Find-MgGraphCommand -command Get-MGUserMessage | Select -First 1 -ExpandProperty Permissions

Name           IsAdmin Description                         FullDescription
----           ------- -----------                         ---------------
Mail.Read      False   Read your mail                      Allows the app to read email in your mailbox.
Mail.ReadBasic False   Read user basic mail                Allows the app to read email in the signed-in user's mailbo…
Mail.ReadWrite False   Read and write access to your mail  Allows the app to read, update, create and delete email in …

PS C:\> Find-MgGraphCommand -command Move-MGUserMessage | Select -First 1 -ExpandProperty Permissions

Name           IsAdmin Description                         FullDescription
----           ------- -----------                         ---------------
Mail.ReadWrite False   Read and write access to your mail  Allows the app to read, update, create and delete email in …

Daraus extrahiere ich, das ich vermutlich "Mail.ReadWrite" brauche. Diese Informationen bekomme ich natürlich auch aus den Graph-API-Dokumentation

Über das Azure Portal kann ich als Administrator dann diese App einrichten und zulassen oder der Benutzer kann dies selbst. Im Azure Portal sehe ich aber, welche Rechte es in Graph zum Thema Exchange überhaupt noch gibt. Wenn Sie der MGGraph-API alle Rechte geben, dann haben Sie natürlich vollen Zugriff:

Wenn Sie ihren Benutzern selbst eine App bereitstellen wollen, mit der Sie entsprechende Aktionen auslösen, dann sollten Sie als Administrator die App anlegen und die "Delegated Permissions" eintragen. Wenn Sie zentral mit einer Applikation ohne Mithilfe des Benutzers diese Änderungen vornehmen wollen, dann sind eben die "Application Permissions" der richtige Weg.

Denken Sie daran, dass es hier erst einmal nur um "Mails" geht. In einem Postfach kann es natürlich auch noch Kontakte, Termine u.a. geben, die über Graph gesondert betrachtet werden.

Wenn Sie eine Application berechtigen, dann hat sie standardmäßig die Rechte auf alle Postfächer. Dies können Sie aber mit einer Graph ApplicationAccessPolicy in Exchange steuern.

Verbinden

Danach kann ich die Graph PowerShell unter Angabe der gewünschten Berechtigungen verbinden.

Connect-MgGraph `
   -Scopes "Mail.ReadWrite", "Mail.ReadBasic", "Mail.Read"

Wenn der Administrator die Berechtigungen vorab noch nicht bestätigt hat, startet eine Rückfrage:

Danach kann ich das Browser-Fenster wieder schließen und die Shell weiter verwenden.

Ordner auflisten

Die Liste der Ordner in meinem Postfach erhalten ich direkt durch Get-MgUserMailFolder und der Angabe der BenutzerID. Leider nutzt Graph nicht automatisch den angemeldeten Benutzer, wenn ich den Wert weg lasse. Allerdings kann ich jede beliebe Mailadresse angeben, die dem Postfach zugeordnet ist, d.h. auch sekundäre ProxyAddresses funktionieren.

PS C:\> Get-MgUserMailFolder -UserId user1@msxfaq.de | ft displayname,TotalItemCount,unreaditemcount

DisplayName                   TotalItemCount UnreadItemCount
-----------                   -------------- ---------------
Archiv                                    91               1
Aufgezeichnete Unterhaltungen              2               0
Entwürfe                                   1               1
Gelöschte Elemente                         3               2
Gesendete Elemente                         7               4
Junk-E-Mail                                0               0
Postausgang                                1               0
Posteingang                               92              10
RSS-Abonnements                            0               0
Synchronisierungsprobleme                  0               0

Intern arbeitet Exchange aber mit MessageIDs und "FolderIDs". Sind sind Base64 codierte Binärwerte, die ich hier nicht ausgegeben habe. Einen Ordner gezielt ansprechen geht über die Filter-Funktion

Get-MgUserMailFolder `
   -UserId user1@msxfaq.de `
   -Filter "Displayname eq 'Posteingang'" `
| fl *

ChildFolderCount              : 13
ChildFolders                  :
DisplayName                   : Posteingang
Id                            : xxx-JAIApY4q6AQDxqBdxPwfQEa_eAIApY4q6AAAAAXE-AAA=
IsHidden                      : False
MessageRules                  :
Messages                      :
MultiValueExtendedProperties  :
ParentFolderId                : xxx-JAIApY4q6AQDxqBdxPwfQEa_eAIApY4q6AAAAAXE8AAA=
SingleValueExtendedProperties :
TotalItemCount                : 92
UnreadItemCount               : 10
AdditionalProperties          : {[sizeInBytes, 431019]}

Ein Exchange Postfach kann natürlich eine verschachtelte Ordnerstruktur haben. Leider gibt es hier keinen "-Recurse"-Parameter, sondern nur das Commandlet Get-MgUserMailFolderChildFolder, welches eine FolderID und UserID braucht aber leider keine Input-Pipeline versteht. Die erste Ebene unter dem Posteingang bekommen wir daher mit.

$userid = "user1@msxfaq.de"

Get-MgUserMailFolder `
   -UserId $userid `
   -Filter "Displayname eq 'Posteingang'" `
| %{`
      Get-MgUserMailFolderChildFolder `
         -UserId $userid `
         -MailFolderId $_.id `
   } `
| fl displayname

DisplayName                TotalItemCount UnreadItemCount
-----------                -------------- ---------------
SUB1                                    0               0
Sub2                                    2               1

Wer also alle Ordner benötigt, muss rekursiv durch die Ordnerstruktur laufen, bis der Order gefunden wurde oder mehrere Aufruf mit dem "-Filter"-Parameter verschachteln, z.B.

# Get-MgUserMailFolderRecurse
# uses existing MGGraph-Session to get all Folders in a mailbox

[CMDLetBinding()]
param (
    $userid = "user1@msxfaq.de",
    $startfolderid  ="",
    $path = "\"
)

function Get-MgUserMailFolderRecurse( 
        [string]$userid, 
        [string]$folderid,
        [string]$path 
)
{
    Write-Verbose " Recurse: userid $($userid)"
    Write-Verbose " Recurse: Path $($path)"
    $currentfolder = Get-MgUserMailFolder -userid $userid -MailFolderId $folderid
    $path = "$($path)\$($currentfolder.Displayname)"
    Write-Verbose "FolderPath $($path)"
    [PSCustomObject]@{
        Displayname = $currentfolder.Displayname
        TotalItemCount = $currentfolder.TotalItemCount
        UnreadItemCount = $currentfolder.UnreadItemCount
        SizeinBytes = $currentfolder.AdditionalProperties.Item("sizeInBytes")
        Path = $path
    }
    if ($currentfolder.ChildFolderCount -gt 0) {
        foreach ($subfolder in (Get-MgUserMailFolderChildFolder -userid $userid -mailfolderid $folderid)) {
            Get-MgUserMailFolderRecurse -userid $userid -folderid $subfolder.id -path $path
        }
    }
}

Write-Verbose " Recurse: Main Start"

if ($startfolderid -eq "") {
    Write-Verbose " Recurse: get Root Folder"
    $startfolderid = (Get-MgUserMailFolder -UserId $userid)[0].ParentFolderId
}
Write-Verbose "  StartfolderID $($startfolderid)"
Get-MgUserMailFolderRecurse `
   -userid $userid `
   -folderid $startfolderid `
   -path $path

Write-Verbose " Recurse: Main End"

Im Vergleiche zu MAPI oder EWS ist der Zugriff per MGGraph-PowerShell schon sehr einfach.

Mails auflisten

Oder sind ja nur ein Teil der Aufgabe. Wenn ich nun eine Ordner habe, kann ich darin natürlich auch die Mail-Elemente anzeigen. Ich muss dazu nicht einmal einen Ordner angeben, denn Get-MgUserMessage nutzt direkt den Posteingang. Ich muss nur die UserID angeben.

PS C:\> get-mgusermessage -UserId user1@msxfaq.de | ft LastModifiedDateTime,createddatetime,subject

LastModifiedDateTime CreatedDateTime     Subject
-------------------- ---------------     -------
04.06.2022 12:28:16  04.06.2022 10:58:50 Umweltgipfel Stockholm+50 beendet – Klimaschützer enttäuscht
04.06.2022 10:58:39  04.06.2022 10:58:50 Umfrage: Zu abhängig von sozialen Medien – Mehrheit wünscht sich Offline-Zonen
04.06.2022 12:28:31  04.06.2022 10:58:50 Displayneuheiten für Mobilisten, Gamer und Videofans
04.06.2022 12:10:27  04.06.2022 07:25:11 Sie haben überfällige Aufgaben.
04.06.2022 12:28:31  04.06.2022 10:58:51 Bauwerke in Szene: Die Bilder der Woche (KW 22)
04.06.2022 12:28:31  04.06.2022 10:58:51 Hardware-Trends 2022 | c’t uplink 43.4
04.06.2022 11:36:55  04.06.2022 11:36:53 DS216J Local backup - Backup2USB erfolgreich auf DS216j
04.06.2022 11:36:56  04.06.2022 11:36:53 DS216J DSM auf DS216j ist veraltet
04.06.2022 11:36:54  04.06.2022 11:36:49 Sehen Sie sich Ihre Azure-Abrechnung für MPN 100US$ carius.de an.
04.06.2022 12:10:27  03.06.2022 22:04:44 Joey sent a message

Allerdings fallen hier mehrere Dinge direkt auf:

  • Sortierung
    Ich habe noch keine Sortierung erkannt. Zumindest ist es weder das CreatedDateTime noch das LastModifiedDateTime-Feld.
  • Flache Liste
    Sie können das hier nur schwer sehen, aber ich bekomme sowohl Mails aus dem Posteingang als auch aus anderen Ordnern, z.B. RSS-Feeds. Es hat den Eindruck, dass Graph meine Element einfach als riesengroße Liste ansieht
  • Paging
    Per Default kommen 10 Elemente zurück. Ich kann allerdings mit dem Parameter PageSize auch mehr Elemente anfordern und mit "-All" bekomme ich die komplette Liste, was aber nur in Verbindung mit Filtern sinnvoll ist. Ich habe auch kein "GetNext"-Element gefunden aber mit "-Skip" können Sie die Elemente überspringen

Die Rückgabe zu einer Nachricht enthält folgende Elemente (Beispiel einer Statusmail meiner Synology NAS, welche per SMTP zugestellt wurde)

Attachments                   :
BccRecipients                 : {}
Body                          : Microsoft.Graph.PowerShell.Models.MicrosoftGraphItemBody
BodyPreview                   : Ihre Datensicherungsaufgabe Backup2USB ist jetzt abgeschlossen.

                                Datensicherungsaufgabe: Backup2USB
                                Datensicherungsziel: usbshare1 / DS216j_2.hbk
                                Startzeit: Sa, 4 Jun 2022 03:00:24
                                Dauer: 16 Minute 57 Second

                                Quellgröße gesamt:
                                - Freigegebener Ord
Categories                    : {}
CcRecipients                  : {}
ChangeKey                     : xxxxxxxxxxxxxxx/bIS9ioeHCAAP/dbKk
ConversationId                : xxxxxxxxxxxxxxxxxxxxxxxxxxxxx=
ConversationIndex             : {1, 0, 217, 187…}
CreatedDateTime               : 04.06.2022 11:36:53
Extensions                    :
Flag                          : Microsoft.Graph.PowerShell.Models.MicrosoftGraphFollowupFlag
From                          : Microsoft.Graph.PowerShell.Models.MicrosoftGraphRecipient
HasAttachments                : False
Id                            : xxxxxxxxxxxxxxxxxxxxxxxxxxxx-JAIApY4q6BwB
                                QX0-jF08WQYQ0LFeJY-iRAADzMst1AACUB1odPgg0QI-bIS9ioeHCAANPM_D-AAA=
Importance                    : normal
InferenceClassification       : focused
InternetMessageHeaders        :
InternetMessageId             : <xxxxxxxxxxxx.692d183af58082246dbd3633c44d0664@carius.de>
IsDeliveryReceiptRequested    :
IsDraft                       : False
IsRead                        : False
IsReadReceiptRequested        : False
LastModifiedDateTime          : 04.06.2022 11:36:55
MultiValueExtendedProperties  :
ParentFolderId                : xxxxxxxxxxxxxxxxxxxxx-JAIApY4q6AQB
                                QX0-jF08WQYQ0LFeJY-iRAADzMst1AAA=
ReceivedDateTime              : 04.06.2022 01:17:25
ReplyTo                       : {}
Sender                        : Microsoft.Graph.PowerShell.Models.MicrosoftGraphRecipient
SentDateTime                  : 04.06.2022 01:17:25
SingleValueExtendedProperties :
Subject                       : DS216J Local backup - Backup2USB erfolgreich auf DS216j
ToRecipients                  : {Microsoft.Graph.PowerShell.Models.MicrosoftGraphRecipient}
UniqueBody                    : Microsoft.Graph.PowerShell.Models.MicrosoftGraphItemBody
WebLink                       : https://outlook.office365.com/owa/?ItemID=xxxxxxxxxxxxxxxxxxxxxxxxx
                                xxxxxxxxx%2FJAIApY4q6BwBQX0%2FjF08WQYQ0LFeJY%2FiRAADzMst1AACUB1odPg
                                g0QI%2FbIS9ioeHCAANPM%2BD%2FAAA%3D&exvsurl=1&viewmodel=ReadMessageItem
AdditionalProperties          : {[@odata.etag, W/"CQAAABYAAACUB1odPgg0QI/bIS9ioeHCAAP/dbKk"]}

Über "Body.Content" ist auch der komplette Body enthalten. Der "WebLink" verweist dann direkt auf das Element aber nicht nicht anonym erreichbar.

Schon daher sollten Sie überlegen, die Nachrichten beim Auflisten gezielt zu filtern oder die Properties zu beschränken. Bei mir sind dann nur folgende Felder gekommen. Der Body war zwar noch ein Objekte aber leer.

PS C:\> get-mgusermessage -UserId user1@msxfaq.de -Property subject,LastModifiedDateTime,createddatetime |fl *

Attachments                   :
BccRecipients                 :
Body                          : Microsoft.Graph.PowerShell.Models.MicrosoftGraphItemBody
BodyPreview                   :
Categories                    :
CcRecipients                  :
ChangeKey                     :
ConversationId                :
ConversationIndex             :
CreatedDateTime               : 04.06.2022 11:36:53
Extensions                    :
Flag                          : Microsoft.Graph.PowerShell.Models.MicrosoftGraphFollowupFlag
From                          : Microsoft.Graph.PowerShell.Models.MicrosoftGraphRecipient
HasAttachments                :
Id                            : xxxxxxxxxxxxxxxxxxxxx-JAIApY4q6BwB
                                QX0-jF08WQYQ0LFeJY-iRAADzMst1AACUB1odPgg0QI-bIS9ioeHCAANPM_D-AAA=
Importance                    :
InferenceClassification       :
InternetMessageHeaders        :
InternetMessageId             :
IsDeliveryReceiptRequested    :
IsDraft                       :
IsRead                        :
IsReadReceiptRequested        :
LastModifiedDateTime          : 04.06.2022 11:36:55
MultiValueExtendedProperties  :
ParentFolderId                :
ReceivedDateTime              :
ReplyTo                       :
Sender                        : Microsoft.Graph.PowerShell.Models.MicrosoftGraphRecipient
SentDateTime                  :
SingleValueExtendedProperties :
Subject                       : DS216J Local backup - Backup2USB erfolgreich auf DS216j
ToRecipients                  :
UniqueBody                    : Microsoft.Graph.PowerShell.Models.MicrosoftGraphItemBody
WebLink                       :
AdditionalProperties          : {[@odata.etag, W/"xxxxxxxxxx/bIS9ioeHCAAP/dbKk"]}

Dennoch möchte ich ja nicht alle Mails im gesamten Postfach ohne Sortierung. Exemplarisch möchte die nur Mails im Posteingang. Der Trick besteht nun dies über Filter und Suchen zu arbeiten oder über das Input-Object entsprechende Vorgaben zu machen  Startpunkt ist dafür natürlich ein Ordner, den ich über die FolderID anspreche. Um z.B. die neuesten Mails in meinem deutschen Postfach zu finden, reichen folgende Zeilen.

# UserID
$userid = "user@msxfaq.de"

# Posteingangsordner suchen
$inbox= Get-MgUserMailFolder `
           -UserId $userid `
           -Filter "Displayname eq 'Posteingang'"

$Inboxmessages = Get-MGUserMessage `
                    -UserId $userid `
                    -filter "ParentFolderId eq '$($inbox.id)'"

Das funktioniert natürlich auch mit jeder anderen FolderID und erweiterten Suchbegriffen. Allerdings versteckt die MGGraph-PowerShell die absoluten URLs, die im Hintergrund genutzt werden.

Leider habe ich noch keinen Weg gefunden, die "WellKownFolder"-ID als Suche zu verwenden oder den Pfad z.B. zu "GET /me/mailFolders/inbox" oder anderen bekannten Ordnern zu verwenden

Ändern

Natürlich kann ich per MGGraph nicht nur lesen sondern auch Aktionen auf die Element ausführen. Eine einfache Suche liefert die verschiedenen Commandlets für MGUserMessage:

PS C:\group\Technik\Skripte\mggraph> Get-Command *-MgUserMessage | ft -AutoSize

CommandType Name                 Version Source
----------- ----                 ------- ------
Function    Copy-MgUserMessage   1.9.5   Microsoft.Graph.Users.Actions
Function    Get-MgUserMessage    1.9.5   Microsoft.Graph.Mail
Function    Move-MgUserMessage   1.9.5   Microsoft.Graph.Users.Actions
Function    New-MgUserMessage    1.9.5   Microsoft.Graph.Mail
Function    Remove-MgUserMessage 1.9.5   Microsoft.Graph.Mail
Function    Send-MgUserMessage   1.9.5   Microsoft.Graph.Users.Actions
Function    Update-MgUserMessage 1.9.5   Microsoft.Graph.Mail

Achtung: Graph stellt keine Rückfrage, ob sie das Element wirklich löschen wollen.

Ein Element zu löschen geht allein durch die Angabe der UserID und der MessageID.

$mails= Get-MGUserMessage
Remove-MgUserMessage `
   -UserId $userid `
   -MessageId $mails[0].id

Achtung: Die Elemente werden direkt hart gelöscht und nicht in den Papierkorb des Postfachs gelegt.

Beim Verschieben benötigen Sie neben der UserID und der MessageID natürlich auch noch die ID des Ziels:

$mails= Get-MGUserMessage
Move-MgUserMessage `
   -MessageId $mails[0].id `
   -UserId $userid `
   -DestinationId $inbox.id

Hinweis: Durch den Move verändert sich die ID des Objekts und sie können das verschobene Objekte nicht mehr unter der originalen ID ansprechen.

InputObject

Alternativ können all die Parameter auch über ein Input-Objekt übergeben werden. Dazu brauche ich eine Hashtable mit den gewünschten Filtern.


Quelle: https://docs.microsoft.com/en-us/powershell/module/microsoft.graph.mail/get-mgusermessage?view=graph-powershell-beta#notes

Das kann dann wie folgt aussehen, um wieder den Posteingang zu lesen.

# UserID
$userid = "user@msxfaq.de"

# Posteingangsordner suchen
$inbox= Get-MgUserMailFolder `
           -UserId $userid `
           -Filter "Displayname eq 'Posteingang'"

$MgMessageParam = @{
   UserID = $userid
   MailFolderId = $inbox.id
}
Get-MgUserMessage -InputObject $MgMessageParam

# Leider liefert mit der Code folgenden Fehler
Get-MgUserMessage_GetViaIdentity: The pipeline has been stopped.
Get-MgUserMessage_GetViaIdentity: InputObject has null value for InputObject.MessageId

Ich muss hier also zumindest noch das Property "InputObject.MessageId" mitliefern.

Delta

Interessant könnten noch die Befehle sein, die auf ein "*Delta" enden. Ich habe damit noch nicht weiter experimentiert, aber damit kann man differenzielle Abfragen starten, d.h. eine Lösung müsste dann nicht mehr alle Mails lesen sondern könnte nur Änderungen (neu aber auch gelöscht, geändert, verschoben) ermitteln.

PS C:\> get-command get-mguser*delta

CommandType     Name
-----------     ----
Function        Get-MgUserCalendarEventDelta
Function        Get-MgUserContactDelta
Function        Get-MgUserContactFolderChildFolderDelta
Function        Get-MgUserContactFolderContactDelta
Function        Get-MgUserContactFolderDelta
Function        Get-MgUserDelta
Function        Get-MgUserEventDelta
Function        Get-MgUserEventInstanceDelta
Function        Get-MgUserMailFolderChildFolderDelta
Function        Get-MgUserMailFolderDelta
Function        Get-MgUserMailFolderMessageDelta
Function        Get-MgUserMessageDelta
Function        Get-MgUserTodoListDelta
Function        Get-MgUserTodoListTaskDelta

Ein Blick in die darunterliegende Graph-API liefert folgende Beschreibung:

Get a set of messages that have been added, deleted, or updated in a specified folder. A delta function call for messages in a folder is similar to a GET request, except that by appropriately applying state tokens in one or more of these calls, you can query for incremental changes in the messages in that folder. This allows you to maintain and synchronize a local store of a user's messages without having to fetch the entire set of messages from the server every time.
Quelle: https://docs.microsoft.com/en-us/graph/api/message-delta?view=graph-rest-1.0&tabs=http

Weitere Links