prod@blog:~$

Building a Reliable Out of Office Manager with Microsoft Graph API and ASP.NET

Leta talk about managing Out of Office (OOF) settings for users across our Exchange Online environment - and why building a management website for people that forget to set out of office notifications is a helpful tool to set these notifications on other people’s behalf. 

While PowerShell cmdlets exist for this purpose, they're painfully slow – often taking 30-60 seconds per operation due to module initialization overhead. After researching alternatives, I decided to build a custom web-based solution using Microsoft Graph API, and the performance improvements were game-changing.

Visuals of the Website

This is the main search screen:


Then when you enter a name it lookup that name on Exchange as below, then you can select the user:


Then you will see the current state and message for the out of office:


You can then also change the status of the out of office assistant:


When you enable it will set a green dot on the website:

Then when you have customised/checked the internal and external message is correct you can click save:


If you have no access to the application you will see the access denied page:


This will also save a log file on all operations performed as you can see below:
2026-01-21 07:17:43 | User: BEAR\lee.croucher | Action: Search | Details: Query: lee croucher | Outcome: Success
2026-01-21 07:18:01 | User: BEAR\lee.croucher | Action: GetOOF | Details: Target: lee@croucher.cloud | Outcome: Success
2026-01-21 07:25:11 | User: BEAR\lee.croucher | Action: UpdateOOF | Details: Target: lee@croucher.cloud | Status: alwaysEnabled | Outcome: Success
function Get-UserAuthMethods {

The Problem: Why PowerShell Wasn't Cutting It

Traditional PowerShell approaches to managing Exchange Online OOF settings suffer from several issues:

# This approach is slow and cumbersome
Connect-ExchangeOnline -UserPrincipalName admin@company.com
Set-MailboxAutoReplyConfiguration -Identity user@company.com -AutoReplyState Enabled

Performance Issues:

  • Module loading: 15-30 seconds
  • Authentication overhead: 10-15 seconds
  • Actual operation: 5-10 seconds
  • Total time per operation: 30-60 seconds

For an IT help desk managing multiple requests daily, this was simply to slow.

Enter the Solution: Microsoft Graph API

Microsoft Graph API offers a modern, RESTful approach to Exchange Online management with sub-second response times. Here's what I built:

Architecture Overview

The solution consists of three main components:

  1. ASP.NET Web Application - Clean, responsive interface
  2. Graph API Integration - Direct HTTPS calls for speed
  3. Windows Authentication - Integrated security using existing AD credentials

Key Design Decisions

Zero External Dependencies:

Rather than using MSAL libraries or Newtonsoft.Json, I opted for built-in .NET Framework classes to avoid deployment complexity on our Windows Server 2019 environment.

// Using built-in JavaScriptSerializer instead of Newtonsoft.Json
var js = new JavaScriptSerializer();
var data = js.Deserialize<Dictionary<string, object>>(json);

Token Caching Strategy

Implementing intelligent token management to avoid unnecessary authentication calls:

private static string _cachedToken = null;
private static DateTime _tokenExpiry = DateTime.MinValue;
private static readonly object _tokenLock = new object();

private static async Task<string> GetAccessTokenAsync()
{
    lock (_tokenLock)
    {
        // Return cached token if still valid
        if (!string.IsNullOrEmpty(_cachedToken) && _tokenExpiry > DateTime.UtcNow.AddMinutes(5))
            return _cachedToken;
    }
    // Request new token...
}

Azure AD App Registration: Getting the Permissions Right

Setting up the Azure AD app registration correctly is crucial. Here are the exact permissions needed:

Required Application Permissions

{
  "permissions": [
    {
      "name": "MailboxSettings.ReadWrite.All",
      "type": "Application",
      "description": "Read and modify mailbox settings including OOF"
    },
    {
      "name": "User.Read.All", 
      "type": "Application",
      "description": "Search and read user profiles from Azure AD"
    }
  ]
}

Critical Setup Steps:

  1. Create app registration in Azure AD
  2. Add the above application permissions (not delegated)
  3. Grant admin consent - this is non-negotiable
  4. Generate client secret with appropriate expiry
  5. Note down Tenant ID, Client ID, and Client Secret

Authentication Implementation

The authentication flow uses the OAuth2 client credentials grant type:

private static string GetAccessToken()
{
    string url = "https://login.microsoftonline.com/" + TenantId + "/oauth2/v2.0/token";
    string body = string.Format("client_id={0}&scope=https://graph.microsoft.com/.default&client_secret={1}&grant_type=client_credentials",
        ClientId, HttpUtility.UrlEncode(ClientSecret));
    
    using (var client = new WebClient())
    {
        client.Headers[HttpRequestHeader.ContentType] = "application/x-www-form-urlencoded";
        string json = client.UploadString(url, body);
        var js = new JavaScriptSerializer();
        var data = js.Deserialize<Dictionary<string, object>>(json);
        return data["access_token"].ToString();
    }
}

User Interface: Clean and Professional

One challenge I encountered was dealing with HTML-formatted OOF messages from the Graph API. Users were seeing raw HTML markup in text areas, which looked unprofessional:

<!-- What users were seeing -->
<html><body><div style="direction:ltr; font-family:&quot;Segoe UI&quot;..."">
I am temporarily away from Castle Grayskull until 7th April 2025.
</div></body></html>

HTML Stripping Solution

I implemented a client-side function to clean up the HTML and present readable text:

function stripHtml(html) {
    if (!html) return '';
    
    // Create temporary element to decode HTML entities
    var temp = document.createElement('div');
    temp.innerHTML = html;
    
    // Get clean text content
    var text = temp.textContent || temp.innerText || '';
    
    // Clean up whitespace
    return text.trim().replace(/\s+/g, ' ');
}

Real-time User Search

The user search functionality provides instant feedback as administrators type:

// Debounced search with 300ms delay
$('#userSearch').on('input', function() {
    clearTimeout(searchTimeout);
    var searchTerm = $(this).val();
    
    if (searchTerm.length >= 2) {
        searchTimeout = setTimeout(function() {
            searchUsers(searchTerm);
        }, 300);
    }
});

Graph API Integration: Responsive Responses

Getting OOF Status

[WebMethod]
public static string GetOOFStatus(string email)
{
    string token = GetAccessToken();
    string url = "https://graph.microsoft.com/v1.0/users/" + email + "/mailboxSettings/automaticRepliesSetting";
    
    using (var client = new WebClient())
    {
        client.Headers["Authorization"] = "Bearer " + token;
        string json = client.DownloadString(url);
        LogRequest("GetOOF", "Target: " + email, "Success");
        return json;
    }
}

Updating OOF Settings

The key insight here was understanding Graph API's specific requirements for status values:

[WebMethod]
public static void UpdateOOF(string email, string status, string internalMessage, string externalMessage)
{
    var payload = new {
        automaticRepliesSetting = new {
            status = status, // Must be "disabled", "alwaysEnabled", or "scheduled"
            internalReplyMessage = internalMessage,
            externalReplyMessage = externalMessage
        }
    };
    
    // Use PATCH method for partial updates
    var request = (HttpWebRequest)WebRequest.Create(url);
    request.Method = "PATCH";
    request.Headers["Authorization"] = "Bearer " + token;
    request.ContentType = "application/json";
}

Security and Auditing

Internal Access Control

I implemented a two-tier security model:

protected void Page_Load(object sender, EventArgs e)
{
    if (!IsUserAuthorized())
    {
        Response.StatusCode = 403;
        Response.Write(GenerateAccessDeniedPage());
        Response.End();
    }
}

private bool IsUserAuthorized()
{
    string currentUser = HttpContext.Current.User.Identity.Name;
    string authList = ConfigurationManager.AppSettings["AuthorizedUsers"] ?? "";
    return authList.Split(';').Any(u => currentUser.EndsWith(u.Trim(), StringComparison.OrdinalIgnoreCase));
}

Comprehensive Audit Logging

Every action is logged for compliance and troubleshooting:

private static void LogRequest(string action, string details, string outcome)
{
    string logEntry = string.Format("{0:yyyy-MM-dd HH:mm:ss} | User: {1} | Action: {2} | Details: {3} | Outcome: {4}",
        DateTime.Now, HttpContext.Current.User.Identity.Name, action, details, outcome);
    
    File.AppendAllText(logFile, logEntry + Environment.NewLine);
}

Configuration Management

I stored all sensitive configuration in web.config with the option to encrypt in production:

<appSettings>
  <add key="TenantId" value="your-tenant-id" />
  <add key="ClientId" value="your-client-id" />
  <add key="ClientSecret" value="your-client-secret" />
  <add key="AuthorizedUsers" value="DOMAIN\admin1;DOMAIN\helpdesk1;admin@company.com" />
</appSettings>

Lessons Learned and Best Practices

1. TLS 1.2 is Mandatory

Microsoft Graph requires TLS 1.2, which isn't the default in older .NET Framework versions:

ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072; // TLS 1.2

2. Error Handling is Critical

Graph API errors can be cryptic. Implement comprehensive error handling with meaningful user feedback:

if (errorMsg.includes("400") || errorMsg.includes("Bad Request")) {
    errorMsg += "\n\nPossible causes:\n";
    errorMsg += "• Azure AD app permissions not granted yet\n";
    errorMsg += "• User doesn't have an Exchange Online mailbox\n";
}

3. Status Values Matter

Graph API is strict about status values. Use "alwaysEnabled", not "enabled":

<option value="disabled">Disabled</option>
<option value="alwaysEnabled">Enabled</option>

Conclusion

Building this Out of Office Manager using Microsoft Graph API transformed our IT operations. What once took minutes now happens in seconds, dramatically improving our help desk efficiency and user satisfaction.

For IT administrators still struggling with slow PowerShell-based Exchange management, I strongly recommend exploring Graph API alternatives. The performance gains alone justify the development effort, and the user experience improvements are substantial.