Email categorization is an essential task for efficient email management, especially in professional settings. Automating this process using AI can significantly streamline workflow and enhance productivity. This guide explores how to use Azure, Microsoft Graph, and ChatGPT to intelligently categorize emails.
Why use these services?
ChatGPT:
Advanced Natural Language Processing: ChatGPT 3.5 Turbo’s sophisticated language models excel at understanding and categorizing email content, surpassing traditional keyword search algorithms.
Speed and Efficiency: GPT 3.5 offers rapid and cost-effective responses, making it pivotal for email categorization where prompt categorization matters more than intricate response formulation.
Microsoft Graph:
Seamless Integration with Email Services: Microsoft Graph provides a unified API endpoint for accessing Microsoft 365 services. This ensures direct and efficient interaction with email data. Emails are updated consistently, regardless of the end user’s viewing device.
Graph vs outlook: While Outlook can perform similar actions, using Graph eliminates the need to keep the application running. Graph connects directly to Microsoft servers, reducing dependency on additional applications.
Azure:
Integration with Azure Function Apps: Azure Function Apps facilitate scheduled code execution, acting like a timer for email categorization tasks. Azure’s serverless architecture reduces overhead and simplifies deployment.
Local vs cloud: Although this project can run on your local machine, using the cloud offers benefits such as not requiring a constantly connected device. The cloud-based solution ensures uninterrupted operation even when you’re out of the office.
Requirements:
- Microsoft 365 Setup
- A Microsoft 365 account with email access is necessary. This guide focuses on business accounts, but adaptations can be made for single-user accounts.
- Azure Account and Subscription
- Access to Azure services requires an active Azure account and an appropriate subscription plan.
- Azure admin access
- Configuration of a function app in Azure and an enterprise application in Entra (Azure Active Directory) is required.
- The Entra application should have the “Mail.ReadWrite” permission if you intend to authenticate using a daemon. Applications authenticated in this manner will have full access to emails within the tenant.
- OpenAI API Access
- Access to ChatGPT 3.5 Turbo via an OpenAI account is essential for the AI-driven categorization process.
Additional Considerations
- Data Privacy and Compliance:
- Ensure strict compliance with data protection laws and company policies when handling email data to maintain data privacy and security.
- Cost Management:
- Be aware of the costs associated with Azure services and OpenAI usage, and plan accordingly.
- Monitoring and Maintenance:
- Regular monitoring and maintenance of the system are essential for ensuring ongoing accuracy and efficiency in email categorization.
Creating the Azure Function
Setting Up the Function
Create a new function:
In visual studio, create a new project with the Azure Functions template. If you do not have the template, you may need to install the Azure development package.
![Visual studio modify page with Azure development selected](/wp-content/uploads/2024/01/VisualStudio-Packages-1024x518.png)
Name the project and set the “Function” to “Timer trigger.”
Configure the schedule using a cron expression. For example, “0 */5 * * * *” will trigger the function every 5 minutes. You can change this schedule in the code later if needed.
Function Timer Trigger:
The function is triggered on a defined schedule, specified by the cron expression “0 */5 6-20 * * *”, which runs the function every 5 minutes between 6 AM and 8 PM every day.
public class CategoriseEmail
{
[FunctionName("Function1")]
public void Run([TimerTrigger("0 */5 6-20 * * *")] TimerInfo myTimer, ILogger log)
{
log.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");
// add code here
}
}
If you wish to change any of the names of the class or function, then I suggest doing it now as once you have published the application you will not be able to change these.
Install required packages:
To use Microsoft graph, you will need to install the following packages with NuGet:
- Azure Identity
- Microsoft Graph
- Microsoft Graph core
Authentication and Graph Client Initialization
Create an application:
Within Entra (Azure active directory) portal, create an enterprise application. This application will serve as the access point for your program to connect with your tenant. If you plan to authenticate using a user login, no further action is required.
If you intend to authenticate using a daemon (service principal), grant the application the “Mail.ReadWrite” access permission, and create a client secret.
Establishing Connection with Microsoft Graph:
The function sets up authentication using tenant ID, client ID, and client secret, which are required to create a GraphServiceClient for interacting with Microsoft Graph. Make sure to replace the placeholders in the code with your actual values, the secret must be the value not the ID:
// Values from app registration
var tenantId = "";
var clientId = "";
var clientSecret = "";
// using Azure.Identity;
var options = new TokenCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
};
// Connect with Graph
var clientSecretCredential = new ClientSecretCredential(tenantId, clientId, clientSecret, options);
GraphServiceClient graphClient = new(clientSecretCredential);
var task = CatogorizeMail(graphClient);
task.Wait();
log.LogInformation(task.Result);
Email Categorization Logic
Fetching Uncategorized Emails:
The function retrieves uncategorized emails from a specified mailbox using Microsoft Graph.
To optimize performance, it filters only the necessary email details and sets the email body to plain text.
async static Task<string> CatogorizeMail(GraphServiceClient graphClient)
{
// Set the target mail box
string s = "Enquiries@transientgeometry.co.uk";
// Get mail from graph
var messages = await graphClient.Users[s].Messages.GetAsync((requestConfiguration) =>
{
requestConfiguration.QueryParameters.Select = new string[] { "Subject", "Body", "id", "Categories" };
requestConfiguration.Headers.Add("Prefer", "outlook.body-content-type=\"text\"");
requestConfiguration.QueryParameters.Filter = "Not categories/any()"; // Filter for uncategorized emails
});
AI-Driven Categorization:
As a variable number of emails (between 0 and 10) is returned, the function iterates through the results with a for loop.
For each email, it sends the subject and body to ChatGPT for analysis.
string log = "";
foreach (var message in messages.Value)
{
var categorys = GetCategoryFromAI(message.Subject, message.Body.Content); // Implement this
categorys.Wait();
// add code here
}
}
Updating Email Categories:
The categories returned by the AI are assigned to each email, and the changes are updated in Microsoft Graph.
// Update the message with the new category
message.Categories = categorys.Result;
await graphClient.Users[s].Messages[message.Id]
.PatchAsync(message);
Generating Categories with ChatGPT
Setting Up ChatGPT Call:
The function sends the email content to ChatGPT 3.5 Turbo, along with instructions on how to categorize the emails. We need to prompt GPT correctly in order to get a useful result, to do this we define how it should format the response and each category we want it to output.
Make sure to add your OpenAI API key in here for this to work.
static async Task<List<string>> GetCategoryFromAI(string subject, string emailContent)
{
// Setup authentication for ChatGPT
var client = new HttpClient();
string apiKey = ""; // Replace with your actual API key
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", apiKey);
// Prompt GPT to assign categories
var Role = new
{
role = "system",
content = "You are assigning categories to emails."
};
var setup = new
{
role = "user",
content = "only respond witht he following categories, if multiple categories apply then split them with ',':\r\n" +
"Urgent/High Priority: This category is for emails that require immediate attention or have a tight deadline. These might include time-sensitive requests, urgent issues, or high-priority tasks that need to be addressed as soon as possible.\r\n" +
"General Inquiries: Emails that consist of general questions or requests for information. These are typically standard queries that don’t require immediate action but are important for maintaining good customer relations or providing necessary information.\r\n" +
"Feedback and Suggestions: This category is for emails that contain feedback, suggestions, or comments about your services, products, or overall customer experience.\r\n" +
"Technical Support: Emails that involve technical questions or issues with products or services.\r\n" +
"Follow-up/Resolved: This category can be used for emails that require follow-up actions or have been resolved.\r\n" +
"Spam: Emails trying to sell a product or service.\r\n" +
"Unknown: Only use this if the Email does not fit any other category."
};
var mesage = new
{
role = "system",
content = $"I have received an email with the subject '{subject}' and the following content: \n\n{emailContent}\n\nBased on the content, what categories would this email fall into?"
};
var messages = new[]
{
Role,
setup,
mesage,
};
var data = new
{
model = "gpt-3.5-turbo", // Or another suitable model
messages,
max_tokens = 100
};
// get response form GPT
var content = new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json");
var response = await client.PostAsync("https://api.openai.com/v1/chat/completions", content);
string responseString = await response.Content.ReadAsStringAsync();
string extractedContent = ExtractContentFromResponse(responseString);
List<string> categories = extractedContent.Split(", ").ToList();
return categories;
}
Parsing AI Response:
The response from ChatGPT contains additional information not needed for categorization, so the function parses the response to extract the assigned categories.
public static string ExtractContentFromResponse(string jsonResponse)
{
var jObject = JObject.Parse(jsonResponse);
// Navigate to the 'choices' array and then to the 'content' of the first 'message'
var content = jObject["choices"]?[0]?["message"]?["content"]?.ToString();
return content ?? "No content found.";
}
Test and publish
After completing the project, run it locally using Visual Studio’s integration with Azure Function Core Tools to test its functionality.
Once satisfied with the project’s performance, right-click on it and select “Publish.” Follow the publishing wizard, and your project will automatically start running in the cloud.
The complete code:
public class CategoriseEmail
{
[FunctionName("Function1")]
public void Run([TimerTrigger("0 */5 6-20 * * *")] TimerInfo myTimer, ILogger log)
{
//"0 */5 6-20 * * *"
log.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");
// Values from app registration
var tenantId = "";
var clientId = "";
var clientSecret = "";
// using Azure.Identity;
var options = new TokenCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
};
// Connect with Graph
var clientSecretCredential = new ClientSecretCredential(tenantId, clientId, clientSecret, options);
GraphServiceClient graphClient = new(clientSecretCredential);
var task = CatogorizeMail(graphClient);
task.Wait();
log.LogInformation(task.Result);
}
async static Task<string> CatogorizeMail(GraphServiceClient graphClient)
{
// Set the target mail box
string s = "Enquiries@transientgeometry.co.uk";
// Get mail from graph
var messages = await graphClient.Users[s].Messages.GetAsync((requestConfiguration) =>
{
requestConfiguration.QueryParameters.Select = new string[] { "Subject", "Body", "id", "Categories" };
requestConfiguration.Headers.Add("Prefer", "outlook.body-content-type=\"text\"");
requestConfiguration.QueryParameters.Filter = "Not categories/any()"; // Filter for uncategorized emails
});
string log = "";
foreach (var message in messages.Value)
{
var categorys = GetCategoryFromAI(message.Subject, message.Body.Content); // Implement this
categorys.Wait();
// Update the message with the new category
message.Categories = categorys.Result;
await graphClient.Users[s].Messages[message.Id]
.PatchAsync(message);
// Write to log any changes made
log += message.Subject + ", ";
foreach (string ss in categorys.Result)
{ log += ss + ", "; }
log += "\r\n";
}
return $"Updated { messages.Value.Count } emails\r\n" + log;
}
static async Task<List<string>> GetCategoryFromAI(string subject, string emailContent)
{
// Setup authentication for ChatGPT
var client = new HttpClient();
string apiKey = ""; // Replace with your actual API key
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", apiKey);
// Prompt GPT to assign categories
var Role = new
{
role = "system",
content = "You are assigning categories to emails."
};
var setup = new
{
role = "user",
content = "only respond witht he following categories, if multiple categories apply then split them with ',':\r\n" +
"Urgent/High Priority: This category is for emails that require immediate attention or have a tight deadline. These might include time-sensitive requests, urgent issues, or high-priority tasks that need to be addressed as soon as possible.\r\n" +
"General Inquiries: Emails that consist of general questions or requests for information. These are typically standard queries that don’t require immediate action but are important for maintaining good customer relations or providing necessary information.\r\n" +
"Feedback and Suggestions: This category is for emails that contain feedback, suggestions, or comments about your services, products, or overall customer experience.\r\n" +
"Technical Support: Emails that involve technical questions or issues with products or services.\r\n" +
"Follow-up/Resolved: This category can be used for emails that require follow-up actions or have been resolved.\r\n" +
"Spam: Emails trying to sell a product or service.\r\n" +
"Unknown: Only use this if the Email does not fit any other category."
};
var mesage = new
{
role = "system",
content = $"I have received an email with the subject '{subject}' and the following content: \n\n{emailContent}\n\nBased on the content, what categories would this email fall into?"
};
var messages = new[]
{
Role,
setup,
mesage,
};
var data = new
{
model = "gpt-3.5-turbo", // Or another suitable model
messages,
max_tokens = 100
};
// get response form GPT
var content = new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json");
var response = await client.PostAsync("https://api.openai.com/v1/chat/completions", content);
string responseString = await response.Content.ReadAsStringAsync();
string extractedContent = ExtractContentFromResponse(responseString);
List<string> categories = extractedContent.Split(", ").ToList();
return categories;
}
// Extract the choices from the response
public static string ExtractContentFromResponse(string jsonResponse)
{
var jObject = JObject.Parse(jsonResponse);
// Navigate to the 'choices' array and then to the 'content' of the first 'message'
var content = jObject["choices"]?[0]?["message"]?["content"]?.ToString();
return content ?? "No content found.";
}
}
Leave a Reply