Outlook und Mailintegration mit Microsoft Graph
Connected Apps mit .NET, Teil 2
E-Mails gehören zu den wichtigsten Arbeitsdaten in Microsoft 365. Microsoft Graph macht Postfächer, Nachrichten und Anhänge für eigene .NET-Anwendungen zugänglich. Wir zeigen den Weg von der Anmeldung über eine Inbox-Ansicht bis zum Versand und zu automatisierten Mail-Workflows. Der Beitrag ergänzt damit den zugehörigen Leitartikel „Connected Apps mit .NET“ unter [1].
Wer Outlook-Nachrichten aus einer eigenen Anwendung lesen möchte, arbeitet mit den Mail-Ressourcen von Microsoft Graph. Nachrichten sind dabei nicht nur einzelne Datensätze. Sie liegen in Ordnern, besitzen Absender und Empfänger, enthalten Text oder HTML, können Anhänge tragen und verändern ihren Zustand, wenn sie gelesen, verschoben oder beantwortet werden. Für viele Anwendungen reicht bereits ein kleiner Ausschnitt dieses Modells, beispielsweise die neuesten Nachrichten aus dem Posteingang anzeigen, Detailinformationen bei Bedarf nachladen und ausgewählte Aktionen aus der eigenen Oberfläche anstoßen. Voraussetzung ist erneut eine Registrierung der App in Microsoft Entra Admin Center (siehe den vorangegangenen Teil 1 dieser Serie zu „Connected Apps mit .NET“).
Für eine interaktive Desktop- oder Client-Anwendung bietet sich ein delegierter Zugriff an. Der Benutzer meldet sich mit seinem Microsoft-365-Konto an, und die Anwendung arbeitet anschließend in seinem Kontext. Für das Lesen vollständiger Nachrichten und ihrer Anhänge benötigt man die Berechtigung Mail.Read. Für den Versand kommt Mail.Send hinzu. Mail.ReadWrite ist nur dann notwendig, wenn die Anwendung Nachrichten beispielsweise als gelesen markieren, verschieben oder löschen soll.
Auch bei Mail-Funktionen gilt damit das Prinzip der minimalen Rechte: Eine reine Anzeige sollte keine Schreibberechtigung erhalten.
Der technische Einstieg unterscheidet sich kaum von anderen Graph-Szenarien. Eine Instanz der Microsoft-Authentifizierungsbibliothek (MSAL) übernimmt die Anmeldung, der GraphServiceClient erhält die Zugriffstokens und kapselt die Requests. Es ist sinnvoll, die benötigten Scopes zentral zu definieren. Client-ID, Tenant und Redirect-URI stammen aus der App-Registrierung in Microsoft Entra ID.
Initialisiert wird der Graph-Client für den Outlook-Zugriff in folgender Weise:
private static readonly string[] Scopes = ["Mail.Read", "Mail.Send"];
_pca = PublicClientApplicationBuilder.Create(clientId)
.WithAuthority(AzureCloudInstance.AzurePublic, tenant)
.WithRedirectUri("http://localhost")
.Build();
var provider = new BaseBearerTokenAuthenticationProvider(
new MsalAccessTokenProvider(this));
_graphClient = new GraphServiceClient(provider);
Bei der Anmeldung prüft die Anwendung zunächst, ob bereits ein gültiges Zugriffstoken für den Benutzer vorhanden ist. Ist das der Fall, kann Microsoft Graph ohne erneute Anmeldung verwendet werden. Nur wenn kein gültiges Token verfügbar ist, wird der Benutzer interaktiv zur Anmeldung aufgefordert. Anschließend übernimmt der GraphServiceClient die Kommunikation mit Microsoft Graph. Die eigentliche Mail-Logik muss sich daher weder um OAuth-Abläufe noch um den Aufbau einzelner HTTP-Requests kümmern, sondern kann direkt mit den Klassen und Methoden des .NET SDK arbeiten, vergleiche Bild 1.
Mit dem Graph-API E-Mails lesen, schreiben und Anhänge verarbeiten (Bild 1)
AutorNachrichten aus dem Posteingang laden
Für eine typische Inbox-Ansicht ist es selten sinnvoll, das komplette Postfach abzurufen. Die Anwendung benötigt meist nur einen konkreten Ordner und wenige Eigenschaften der neuesten Nachrichten. Microsoft Graph stellt dafür unterhalb der Benutzer-Ebene Mail-Ordner und deren Messages bereit. Der bekannte Ordnername inbox kann direkt verwendet werden; alternativ lassen sich Ordner zunächst auflisten und anschließend über ihre ID adressieren.
Im folgenden Beispiel werden die 25 neuesten Nachrichten des Posteingangs geladen. Mit $select beschränkt die Abfrage die Antwort auf diejenigen Eigenschaften, die für eine Listenansicht benötigt werden: ID, Betreff, Absender, Empfangszeit, Lesestatus, Anhangskennzeichen und eine kurze Textvorschau. $orderby sortiert nach receivedDateTime absteigend. Dadurch kommen die neuesten Nachrichten zuerst, und die Übertragung bleibt kompakt:
public async Task<IReadOnlyList<Message>> LoadInboxAsync(CancellationToken ct = default)
{
var page = await _graphClient.Me
.MailFolders["inbox"].Messages.GetAsync(cfg =>
{
cfg.QueryParameters.Select =
["id", "subject", "from", "receivedDateTime",
"isRead", "hasAttachments", "bodyPreview"];
cfg.QueryParameters.Orderby =
["receivedDateTime desc"];
cfg.QueryParameters.Top = 25;
}, ct);
return page?.Value ?? [];
}
Für die Übersicht ist bodyPreview meist geeigneter als der vollständige Nachrichtentext. Die Vorschau genügt für ein oder zwei Zeilen in einer Inbox und vermeidet unnötige Daten. Erst wenn der Benutzer eine Nachricht öffnet, kann die Anwendung die vollständige Message anhand ihrer ID laden und zusätzlich body, Empfänger, Kategorien oder weitere Eigenschaften anfordern. Bei Bedarf lässt sich mit dem Prefer-Header outlook.body-content-type festlegen, ob der Nachrichtentext als HTML oder Text zurückgegeben werden soll.
Filter lassen sich über $filter ergänzen. Für eine Ansicht ungelesener Nachrichten kann beispielsweise isRead = false verwendet werden. Für Suchszenarien steht zusätzlich $search zur Verfügung. Wichtig ist, Abfrage und Darstellung nicht unnötig zu koppeln: Die Liste sollte nur die Daten abrufen, die sie benötigt, und Detailinformationen erst bei Bedarf nachladen.
Auch bei Nachrichten muss Pagination berücksichtigt werden. Ein Wert für die Top-Eigenschaft legt die gewünschte Seitengröße fest, garantiert aber nicht, dass damit der gesamte Datenbestand geliefert wird. Enthält die Antwort einen @odata.nextLink, existiert eine Folgeseite. Das Graph-SDK stellt mit PageIterator eine komfortable Möglichkeit bereit, solche Seiten nacheinander zu verarbeiten. Für eine sichtbare Inbox genügt dagegen häufig bewusst nur die erste Seite; beim Scrollen oder bei einer Synchronisation werden weitere Seiten nachgeladen.
Von der Message zur Inbox-Anzeige
Die von Microsoft Graph gelieferte Message ist ein umfangreiches Datenmodell. Eine Oberfläche benötigt davon meist nur einen kleinen Teil. Deshalb ist es sinnvoll, die Graph-Objekte in ein eigenes, kompaktes Anzeigemodell zu überführen. Dieses Modell enthält nur die Felder, die für die Oberfläche tatsächlich gebraucht werden, und kann Werte bereits in einer benutzerfreundlichen Form bereitstellen.
public sealed record MailDisplayItem(
string Id,
string Subject,
string Sender,
DateTime ReceivedLocal,
string Preview,
bool IsUnread,
bool HasAttachments);
private static MailDisplayItem ToDisplayItem(Message message)
{
var sender = message.From?.EmailAddress;
return new MailDisplayItem(
message.Id ?? "",
string.IsNullOrWhiteSpace(message.Subject)
? "(ohne Betreff)" : message.Subject,
sender?.Name ?? sender?.Address ?? "",
message.ReceivedDateTime?.LocalDateTime ?? DateTime.MinValue,
message.BodyPreview ?? "",
message.IsRead == false,
message.HasAttachments == true);
}
Aus den Graph-Nachrichten entstehen damit Elemente mit Betreff, Absender, lokaler Empfangszeit, Textvorschau sowie Kennzeichen für ungelesene Nachrichten und vorhandene Anhänge. Die Empfangszeit liegt bei Graph als DateTimeOffset vor und kann direkt in die lokale Zeitzone überführt werden. Fehlt ein Betreff, erhält die Anzeige einen neutralen Ersatztext; fehlt ein Anzeigename des Absenders, kann auf dessen E-Mail-Adresse zurückgegriffen werden.
Die Sortierung kann bereits beim Graph-Request erfolgen. Zusätzliche Darstellungslogik bleibt in der Anwendung: Ungelesene Nachrichten werden beispielsweise hervorgehoben, Nachrichten mit Anhang erhalten ein Symbol und lange Vorschautexte werden gekürzt. Auf diese Weise bleibt das UI unabhängig vom konkreten Graph-Modell und lässt sich mit Testdaten genauso betreiben wie mit einem echten Microsoft-365-Postfach.
Hier die beispielhafte Darstellung einer Inbox in XAML:
<CollectionView ItemsSource="{Binding Messages}">
<CollectionView.ItemTemplate>
<DataTemplate>
<VerticalStackLayout>
<Label Text="{Binding Subject}" />
<Label Text="{Binding Sender}" />
<Label Text="{Binding Preview}" />
</VerticalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
Das gleiche Prinzip funktioniert mit .NET MAUI, WinUI, WPF oder Blazor. Entscheidend ist die Trennung der Aufgaben: Microsoft Graph liefert die Nachrichten, die Anwendung normalisiert und reduziert die Daten auf ein kleines Darstellungsmodell, und die Oberfläche kümmert sich ausschließlich um die Anzeige.
E-Mails senden und Anhänge verarbeiten
Sobald eine Anwendung nicht E-Mails nur lesen, sondern auch senden soll, kommt die Berechtigung Mail.Send hinzu. Die zu versendende Nachricht wird als Graph-Message in folgender Weise aufgebaut:
using Microsoft.Graph.Me.SendMail;
using Microsoft.Graph.Models;
var request = new SendMailPostRequestBody
{
Message = new Message
{
Subject = "Status zum Projekt Alpha",
Body = new ItemBody
{
ContentType = BodyType.Text,
Content = "Der aktuelle Projektstatus ist verfügbar."
},
ToRecipients =
[
new Recipient
{
EmailAddress = new EmailAddress
{
Address = "max@example.com"
}
}
]
}
};
await _graphClient.Me.SendMail.PostAsync(request);
Betreff, Body und Empfänger werden gesetzt und anschließend an die sendMail-Aktion übergeben. Standardmäßig wird die gesendete Nachricht in Gesendete Elemente gespeichert. Ein erfolgreicher Aufruf liefert HTTP 202 Accepted. Damit wurde der Versandauftrag angenommen; eine tatsächliche Zustellung beim Empfänger ist zu diesem Zeitpunkt noch nicht bestätigt.
Anhänge sind ein weiterer häufiger Bestandteil von Mail-Workflows. Über die attachments-Navigation einer Message lassen sich vorhandene Anlagen auflisten. Graph unterscheidet unter anderem Datei-, Element- und Referenzanhänge. Bei einem FileAttachment stehen neben Name, MIME-Typ und Größe auch die ContentBytes zur Verfügung. Damit kann eine Anwendung etwa PDF- oder Office-Anhänge einer ausgewählten Nachricht übernehmen und kontrolliert weiterverarbeiten.
Anhänge einer Nachricht lassen sich in folgender Weise abrufen:
public async Task<IReadOnlyList<Attachment>> LoadAttachmentsAsync(
string messageId, CancellationToken ct = default)
{
var page = await _graphClient.Me.Messages[messageId]
.Attachments.GetAsync(cancellationToken: ct);
return page?.Value ?? [];
}
foreach (var file in attachments.OfType<FileAttachment>())
{
var name = file.Name;
var bytes = file.ContentBytes;
// Datei prüfen und kontrolliert weiterverarbeiten.
}
Dateiname, Typ und Größe sollten dabei nicht ungeprüft übernommen werden. In produktiven Workflows gehören Größenlimits, sichere Dateinamen und gegebenenfalls eine Malware-Prüfung zum Verarbeitungspfad.
Änderungen erkennen statt ständig abzufragen
Eine Inbox regelmäßig vollständig neu abzufragen funktioniert für kleine Anwendungen, skaliert aber schlecht. Für automatisierte Prozesse sind ereignisorientierte Workflows interessanter. Microsoft Graph unterstützt Change Notifications für Outlook-Nachrichten. Eine Anwendung kann ein Abonnement auf den Posteingang anlegen und sich informieren lassen, wenn dort neue oder geänderte Nachrichten auftreten:
var subscription = new Subscription {
ChangeType = "created", ClientState = clientState,
NotificationUrl = "https://example.com/api/graph/notifications",
Resource = "me/mailFolders('Inbox')/messages",
ExpirationDateTime = DateTimeOffset.UtcNow.AddHours(24)
};
await _graphClient.Subscriptions.PostAsync(subscription);
Das Abonnement enthält die überwachte Ressource, die gewünschten Änderungstypen, eine öffentliche HTTPS-Adresse für Benachrichtigungen und ein Ablaufdatum. Microsoft Graph validiert den angegebenen Webhook-URL beim Anlegen des Abonnements. Der Endpunkt muss dabei das übermittelte validationToken als Klartext zurückgeben. Während der Gültigkeitsdauer sendet Graph anschließend Benachrichtigungen an diese Adresse.
Eine Notification sollte nicht als vollständiger Ersatz für den eigentlichen Datenabruf betrachtet werden. Sie signalisiert in erster Linie, dass sich etwas geändert hat. Die Anwendung kann anschließend die betroffene Nachricht oder den aktuellen Zustand über Graph nachladen. Für dauerhafte Synchronisationen bietet sich zusätzlich Delta Query an. Nach einer initialen Synchronisation lassen sich damit nur noch hinzugekommene, geänderte oder gelöschte Nachrichten eines Ordners ermitteln.
Fazit
Microsoft Graph macht Outlook nicht nur als Mailprogramm, sondern als Daten- und Kommunikationsdienst für eigene .NET-Anwendungen nutzbar.
[1] Veikko Krypczyk, Connected Apps mit .NET, dotnetpro 4/2026, Seite 36 ff.