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
- Get message
https://docs.microsoft.com/en-us/graph/api/message-get?view=graph-rest-1.0&tabs=http - List mailFolders
https://docs.microsoft.com/en-us/graph/api/user-list-mailfolders?view=graph-rest-1.0&tabs=http - List messages
https://docs.microsoft.com/en-us/graph/api/mailfolder-list-messages?view=graph-rest-1.0&tabs=http
Ü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.
- Graph ApplicationAccessPolicy
- Get started with the Microsoft Graph PowerShell SDK Authentication
https://docs.microsoft.com/en-us/powershell/microsoftgraph/get-started?view=graph-powershell-beta#authentication
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.
- Get-MGUserMailFolder
https://docs.microsoft.com/en-us/powershell/module/microsoft.graph.mail/get-mgusermailfolder?view=graph-powershell-beta - Get-MgUserMailFolderChildFolder
https://docs.microsoft.com/en-us/powershell/module/microsoft.graph.mail/get-mgusermailfolderchildfolder?view=graph-powershell-beta - List mailFolders
https://docs.microsoft.com/en-us/graph/api/user-list-mailfolders?view=graph-rest-1.0&tabs=http
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
- Get-MgUserMessage
https://docs.microsoft.com/en-us/powershell/module/microsoft.graph.mail/get-mgusermessage - mailFolder resource type
https://docs.microsoft.com/en-us/graph/api/resources/mailfolder?view=graph-rest-1.0
Ä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.
- Remove-MgUserMessage
https://docs.microsoft.com/en-us/powershell/module/microsoft.graph.mail/remove-mgusermessage?view=graph-powershell-beta - Move-MgUserMessage
https://docs.microsoft.com/en-us/powershell/module/microsoft.graph.users.actions/move-mgusermessage?view=graph-powershell-beta
InputObject
Alternativ können all die Parameter auch über ein Input-Objekt übergeben werden. Dazu brauche ich eine Hashtable mit den gewünschten Filtern.
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
- Graph message: delta
https://docs.microsoft.com/en-us/graph/api/message-delta?view=graph-rest-1.0&tabs=http - Use delta query to track changes in
Microsoft Graph data
https://docs.microsoft.com/en-us/graph/delta-query-overview - Get incremental changes to messages in a
folder
https://docs.microsoft.com/en-us/graph/delta-query-messages
Weitere Links
- Graph PowerShell
- Graph und Kennworte
- Exchange Online PowerShell V2
- Graph Mail.Send
- Graph ApplicationAccessPolicy
- Microsoft.Graph.Mail
https://docs.microsoft.com/en-us/powershell/module/microsoft.graph.mail/?view=graph-powershell-beta - Get started with the Microsoft Graph PowerShell SDK
https://docs.microsoft.com/en-us/powershell/microsoftgraph/get-started?view=graph-powershell-beta - Get-MgUserMailFolder
https://docs.microsoft.com/en-us/powershell/module/microsoft.graph.mail/get-mgusermailfolder?view=graph-powershell-beta - Moving from the Exchange PowerShell v1 Module to the v2 Preview
https://techcommunity.microsoft.com/t5/exchange-team-blog/moving-from-the-exchange-powershell-v1-module-to-the-v2-preview/ba-p/3450679 - SENDING EMAIL WITH SEND-MGUSERMAIL (MICROSOFT GRAPH POWERSHELL)
https://mikecrowley.us/2021/10/27/sending-email-with-send-mgusermail-microsoft-graph-powershell/ - Connect to Microsoft Graph PowerShell
using an App Registration
https://helloitsliam.com/2022/04/20/connect-to-microsoft-graph-powershell-using-an-app-registration/