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 you will see the current state and message for the out of office:
When you enable it will set a green dot on the website:
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 {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:
- ASP.NET Web Application - Clean, responsive interface
- Graph API Integration - Direct HTTPS calls for speed
- 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:
- Create app registration in Azure AD
- Add the above application permissions (not delegated)
- Grant admin consent - this is non-negotiable
- Generate client secret with appropriate expiry
- 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:"Segoe UI"..."">
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.