Custom GPT Experiment: Building an M365 Email Assistant
Published 13 May 2025
Custom GPT Experiment: Building an M365 Email Assistant
TL;DR
- Created a custom GPT that connects directly to Microsoft 365 email via GPT Actions
- Allows managing email through natural language commands like “show me unread emails” or “draft a reply to Sarah”
- Only requires a ChatGPT Plus subscription and basic Microsoft Azure app registration
- Solves the “folder confusion” problem - when I say “check my inbox,” it only shows Inbox folder messages, not all mailbox items
- Eliminates constant re-authentication prompts by using application permissions with mailbox-specific access policies
- Makes email management significantly more efficient with minimal setup
- Biggest challenge was teaching it to respect folder boundaries and correctly interpret complex commands
The Challenge: Email Overload
As a technology executive, my inbox is constantly flooded with messages. Managing emails efficiently has become a critical productivity challenge - one that I suspect many professionals can relate to. Traditional email interfaces often require multiple clicks and navigation steps to perform simple tasks, and the mental overhead of switching between different folders and views can be draining.
I wanted a solution that would allow me to interact with my email in a more natural, conversational way. Something that would understand exactly what I mean when I say “show me my emails” (and not pull messages from every folder in my account).
Enter Custom GPTs
When OpenAI introduced the ability to create custom GPTs, I immediately saw the potential to build an assistant specifically designed for email management. Custom GPTs allow you to create specialized AI assistants with specific knowledge, capabilities, and behaviors tailored to particular use cases.
Requirements
The beauty of this implementation is its simplicity. Here’s all you need to build a similar solution:
**Basic Requirements **- Accounts and Subscriptions:
-
ChatGPT Plus subscription (to access Custom GPT capabilities)
-
Microsoft 365 account with mailbox access
-
Custom GPT Setup:
-
Knowledge of creating Custom GPTs in the ChatGPT interface
-
Understanding of effective prompt engineering
-
Ability to write clear instructions for email management tasks
-
Microsoft 365 Integration:
-
App Registration in Microsoft Azure AD
-
Basic understanding of OAuth permissions for Microsoft Graph API
That’s really it! No complex development environment, no hosting infrastructure, and no extensive coding required.
App Registration in Azure AD
The only “technical” part is setting up the Microsoft connection:
-
Register an application in the Azure Portal
-
Create a simple App Registration
-
Configure redirect URI (usually provided by the Custom GPT action)
-
Generate a client secret
-
Configure these API permissions:
-
Mail.Read
-
Mail.ReadWrite
-
Mail.Send
-
Set up authentication:
-
Configure the OAuth settings
-
Use the provided client ID and secret in your Custom GPT action
-
Authentication Types and Security Considerations:
-
Delegated Permissions (default): The app acts on behalf of the logged-in user, but requires re-authentication frequently
-
Application Permissions: Eliminates re-authentication prompts, but grants access to all mailboxes in your organization by default
-
Enhancing Security: Use Application Access Policies to limit application permissions to specific mailboxes:
- This approach provides the convenience of application permissions without over-privileging the integration
The Custom GPT handles the complex interaction patterns, natural language processing, and email command interpretation, while the direct connection to Microsoft Graph API allows it to securely access and manage your emails.
With these minimal requirements satisfied, let’s look at how I built the M365 Email Assistant GPT experiment.
The M365 Email Assistant Experiment
I set out to create a custom GPT that would:
- Understand natural language commands for email management
- Focus specifically on Microsoft 365 email integration
- Learn my preferences and communication style
- Respect proper folder organization (a big pain point with other solutions)
Key Features
**My M365 Email Assistant GPT can: **- Read and manage emails from my Inbox folder (and only that folder when requested)
- Compose and send professional messages that match my communication style
- Find specific messages quickly without having to remember complex search syntax
- Handle attachments and draft processing
- Organize emails across folders with simple commands
The Technical Setup
The assistant connects to the Microsoft Graph API with appropriate OAuth scopes to:
- Read emails (Mail.Read)
- Modify messages (Mail.ReadWrite)
- Send messages (Mail.Send)
I’ve implemented several optimizations to make it perform efficiently:
- Requesting minimal fields in list operations
- Using specific filters to narrow results
- Only retrieving full message bodies when necessary
Deep Dive: Technical Implementation
For those interested in the technical details of how this integration works, let’s explore the architecture and setup process.
OAuth Authentication Flow
The integration relies on Azure AD OAuth 2.0 to authenticate securely with Microsoft Graph. Here’s what the authentication flow looks like:
-
App Registration: I registered an application in the Azure Portal to get:
-
Application (client) ID
-
Directory (tenant) ID
-
Client secret
-
Permission Configuration: I configured the following permissions:
-
Redirect URI Setup: I configured the redirect URI to safely receive the authentication code:
-
Web redirect URI:
https://example.com/auth/callback -
Mobile/desktop URI format:
msauth://your-app-name/callback -
Authorization Code Flow: The actual authentication follows these steps:
-
User is directed to Microsoft login (
https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize) -
After successful login, the authorization code is sent to the redirect URI
-
The code is exchanged for access and refresh tokens
-
Tokens are securely stored and refreshed as needed
Custom GPT Connection
The custom GPT connects to my Microsoft 365 account through an API layer I built. This required:
-
Secure Secrets Management:
-
Client secret and tokens are stored in a secure vault
-
No credentials are ever exposed to the GPT itself
-
API Schema: Here’s a simplified version of the API schema that powers the integration:
json{ "paths": { "/emails": { "get": { "parameters": [ {"name": "folder", "default": "inbox"}, {"name": "filter", "optional": true}, {"name": "count", "default": 10} ], "responses": { "200": { "description": "List of emails", "schema": {"$ref": "#/definitions/EmailList"} } } } }, "/emails/{id}": { "get": { "parameters": [{"name": "id", "required": true}], "responses": { "200": { "description": "Email details", "schema": {"$ref": "#/definitions/Email"} } } } }, "/send": { "post": { "parameters": [ {"name": "to", "required": true}, {"name": "subject", "required": true}, {"name": "body", "required": true}, {"name": "attachments", "optional": true} ]-
Rate Limiting & Security:
-
Implemented token-based rate limiting (100 requests per minute)
-
All API requests are logged for security auditing
-
Automated alerts for suspicious activity patterns
Microsoft Graph Queries
The integration uses specific Microsoft Graph queries to efficiently retrieve only the necessary data:
Error Handling and Resilience
To ensure the system remains reliable:
- Exponential Backoff: Implemented for API call retries
- Token Refresh Logic: Automatic refresh when tokens expire
- Graceful Degradation: Fallback options when specific features are unavailable
- Verbose Logging: Detailed logs for troubleshooting integration issues
The integration is containerized using Docker and deployed on Azure, making it easily portable and scalable as needed.
Appendix: Full Implementation Resources
For those who want to implement something similar, here are the complete resources I used for my implementation, which you can adapt to your needs.
Complete Custom GPT Instructions (Anonymized)
Below are the full instructions I provided to my Custom GPT. You should customize these based on your personal preferences and email management style:
# M365 Email Assistant GPT
You are a specialized assistant that helps users interact with their Microsoft 365 email through natural language commands. Your primary purpose is to simplify email management by allowing users to perform common email actions through conversation.
## Core Capabilities- Read and manage emails efficiently- Compose and send professional messages- Organize emails across folders- Find specific messages quickly- Handle attachments- Process email drafts
## Email Management Style- Be concise and practical in your responses- Focus on completing email tasks efficiently- Confirm actions before executing irreversible operations- Provide clear summaries of actions taken- Suggest helpful email management tips when relevant
## User ProfileThe user is a technology executive. They value clear, concise, and professional email communication.
**Default Signature:**Best,[Name][Title] | [Company][Contact Info] | Schedule a meeting[Company Tagline]
**Important:** Always confirm recipient, subject, and message body before sending any email.
## User Interaction Approach- For checking emails: List recent unread messages in the Inbox folder by default- For sending emails: Guide through recipient, subject, and body conversationally and confirm details- For finding emails: Ask for key search details (sender, timeframe, subject keywords)- For organizing emails: Suggest appropriate folder structures
## Smart Folder Search LogicApply this search logic based on the query:- **CRITICAL: When a user mentions "inbox" or asks to "show emails," ONLY search within the specific Inbox folder, not the entire mailbox **- **NEVER show emails from other folders when asked to check the inbox**- Always prioritize the main Inbox folder unless the user explicitly specifies another folder- For sent emails → Search Sent Items folder first → Then Inbox folder/All Mail if needed- For deleted emails → Search Deleted Items folder first → Then Inbox folder/Archive if needed- For archived emails → Check Archive folder first, then expand- For unspecified location → Default to Inbox folder only, then escalate as needed only if explicitly requested
## Common Command Interpretation- "Check my email" → List recent unread Inbox folder messages only- "Show me emails" → Only display messages from the Inbox folder, not from any other folders- "Show me emails from [person]" → Filter messages by sender in the Inbox folder only- "Find emails about [topic]" → Search message content in the Inbox folder only- "Send an email to [person]" → Initiate new message flow- "Reply to the email from [person]" → Find most recent message from that sender (in Inbox folder by default) and generate a reply- "Forward that email to [person]" → Forward the most recently discussed email- "Move this to [folder]" → Move identified email to specified folder- "Mark as read/unread" → Update isRead status- "Delete that email" → Move to deleted items folder- "Create a new folder called [name]" → Create mail folder
## Email Composition Assistance- Match the user's tone: clear, efficient, professional, and courteous- Offer to draft messages based on stated intent- Include appropriate greetings and closings based on context- Include the user's signature unless otherwise noted
## Technical Implementation
### Microsoft Graph API Connection- Uses OAuth flow with scopes: Mail.Read, Mail.ReadWrite, Mail.Send
### Performance Optimization- Request minimal fields in list operations (id, subject, from, receivedDateTime)- Limit to 3 messages at a time in initial responses- Use specific filters to narrow results before retrieving- Only request full message body when viewing specific email
### Error Handling- Provide clear explanations when errors occur- Suggest specific troubleshooting steps- For large responses, automatically retry with fewer fields
### Folder Management- Support hierarchical folder navigation (parent/child relationships)- Verify folder existence before attempting moves- Provide clear success/failure feedback for all operations- **When searching or displaying emails from the Inbox, ONLY include messages from the specific Inbox folder, NOT from any other folders**Complete OpenAPI Schema (Anonymized)
Below is the full OpenAPI schema I used for the Microsoft Graph API integration. You can adapt this to include additional endpoints as needed for your implementation:
yamlopenapi: 3.1.0info: title: Microsoft 365 Email API (Minimal) description: Minimal API for interacting with Microsoft 365 Email using Microsoft Graph version: 1.5.0servers: - url: https://graph.microsoft.com/v1.0 description: Microsoft Graph API v1.0 endpointpaths: /me/mailFolders/{folderId}/childFolders: get: operationId: listChildFolders summary: List child folders description: Get child folders of a specific mail folder parameters: - name: folderId in: path required: true description: ID of the parent folder schema: type: string - name: $select in: query description: Properties to return required: false schema: type: string default: "id,displayName,childFolderCount,parentFolderId" responses: '200': description: List of child folders content: application/json: schema: $ref: '#/components/schemas/MailFolderCollection' /me/messages: get: operationId: listMessages summary: List messages in the user's mailbox description: Get messages from the user's mailbox parameters: - name: $select in: query description: Properties to return required: false schema: type: string default: "id,subject,from,receivedDateTime,isRead" - name: $top in: query description: Maximum number of messages to return required: false schema: type: integer default: 3 - name: $filter in: query description: Filter criteria required: false schema: type: string - name: $orderby in: query description: Sort criteria required: false schema: type: string default: "receivedDateTime desc" responses: '200': description: List of messages content: application/json: schema: $ref: '#/components/schemas/MessageCollection'
/me/mailFolders: get: operationId: listFolders summary: List mail folders description: Get all mail folders in the user's mailbox parameters: - name: $select in: query description: Properties to return required: false schema: type: string default: "id,displayName,childFolderCount,parentFolderId" responses: '200': description: List of mail folders content: application/json: schema: $ref: '#/components/schemas/MailFolderCollection' post: operationId: createFolder summary: Create a new mail folder description: Create a new mail folder in the user's mailbox requestBody: required: true content: application/json: schema: type: object required: - displayName properties: displayName: type: string description: The display name of the folder responses: '201': description: Folder created successfully content: application/json: schema: $ref: '#/components/schemas/MailFolder'
/me/mailFolders/{folderId}: get: operationId: getFolder summary: Get a mail folder description: Get details of a specific mail folder parameters: - name: folderId in: path required: true description: ID of the folder schema: type: string - name: $select in: query description: Properties to return required: false schema: type: string default: "id,displayName,childFolderCount,parentFolderId" responses: '200': description: Mail folder details content: application/json: schema: $ref: '#/components/schemas/MailFolder' delete: operationId: deleteFolder summary: Delete a mail folder description: Delete a mail folder parameters: - name: folderId in: path required: true description: ID of the folder schema: type: string responses: '204': description: Folder deleted successfully
/me/messages/{messageId}/move: post: operationId: moveMessage summary: Move a message to a different folder description: Move a message from one folder to another parameters: - name: messageId in: path required: true description: ID of the message to move schema: type: string requestBody: required: true content: application/json: schema: type: object required: - destinationId properties: destinationId: type: string description: ID of the destination folder responses: '204': description: Message moved successfully without returning content
/me/messages/{messageId}/reply: post: operationId: replyToMessage summary: Reply to a message description: Reply to a specific email message with original content included parameters: - name: messageId in: path required: true description: ID of the message to reply to schema: type: string requestBody: required: true content: application/json: schema: type: object required: - comment properties: comment: type: string description: The comment to include in the reply, which will be added above the original message content responses: '202': description: Reply accepted
/me/sendMail: post: operationId: sendMail summary: Send an email description: Send an email message requestBody: required: true content: application/json: schema: type: object required: - message properties: message: type: object required: - subject - body - toRecipients properties: subject: type: string description: The subject of the message body: type: object required: - contentType - content properties: contentType: type: string enum: [Text, HTML]Implementation Tips
When adapting these resources for your own use:
- Modify the User Profile: Update the signature and user details to match your own
- Custom Commands: Add specific command interpretations that match your email workflow
- Folder Structure: Adjust the folder logic to match your Microsoft 365 organization
- API Endpoints: Add or remove endpoints from the OpenAPI schema based on your needs
- Security Stance: Consider the tradeoffs between delegated and application permissions, using Application Access Policies if you choose the latter
By providing these full resources, I hope to make it easier for others to create their own email management GPTs that respect their folder organization and workflow preferences.
Troubleshooting: Lessons From the Trenches
Building this integration wasn’t without challenges. Here are some of the major issues I encountered and how I resolved them:
Authentication Failures
Issue: The GPT would frequently lose connection to Microsoft Graph after about an hour of usage, requiring re-authentication. Root Cause: The default token expiration was set to 1 hour, and the refresh token flow wasn’t properly implemented. Solution: Implemented a proper token refresh mechanism that automatically detects expired tokens and refreshes them before making API calls. Added a token validation check before each request:
Alternative Solution: Switched from delegated permissions to application permissions to eliminate re-authentication prompts entirely. To maintain security:
- Used Application Access Policies to restrict access to only my specific mailbox:
- Implemented strict auditing and monitoring for the application credentials
- Set shorter expiration times for client secrets with automated rotation
This approach provided a seamless experience without constant re-authentication prompts while maintaining proper security boundaries.
Folder Confusion
Issue: When asking to “check my inbox,” the GPT would search across all mail folders, returning confusing results. Root Cause: The Microsoft Graph API defaults to searching across all folders if not explicitly told otherwise. The GPT instructions weren’t specific enough about folder scope. Solution:
- Modified Graph API calls to explicitly specify the Inbox folder ID:
- Refined the GPT’s instructions to be extremely explicit about folder scope:
Permission Scopes
Issue: Certain commands like “move this email” would fail with 403 Forbidden errors. Root Cause: The initial OAuth scope configuration only included Mail.Read and Mail.Send, but not Mail.ReadWrite which is required for modifying messages. Solution: Re-registered the application with the complete set of required permissions and implemented a permission check function:
javascriptfunction checkRequiredPermissions(operation) { const permissionMap = { 'read': ['Mail.Read', 'Mail.ReadWrite'], 'write': ['Mail.ReadWrite'], 'send': ['Mail.Send'], 'move': ['Mail.ReadWrite'], 'delete': ['Mail.ReadWrite'] };
const grantedPermissions = getUserPermissions(); const requiredPermissions = permissionMap[operation]Rate Limiting
Issue: During heavy usage, the integration would hit Microsoft Graph API rate limits. Root Cause: Multiple rapid requests without proper throttling management. Solution: Implemented an adaptive throttling mechanism:
- Tracked the remaining request quota from response headers
- Added automatic request queuing when approaching limits
- Implemented a circuit breaker pattern for API stability:
Handling Large Attachments
Issue: The system would crash when trying to process emails with large attachments. Root Cause: Memory limitations when trying to load entire attachments into memory. Solution: Implemented streaming attachment processing:
- Created a streaming API endpoint for attachment retrieval
- Added chunked processing for large files:
Natural Language Processing Challenges
Issue: The GPT sometimes misunderstood complex commands like “forward the second email from John to Sarah with a note saying I’ll review this tomorrow.” Root Cause: The GPT struggled to parse complex commands with multiple actions and parameters. Solution: Implemented a command parser that breaks down complex requests:
- Added a pre-processing layer to identify command components
- Created a structured format for command interpretation
- Implemented a clarification system for ambiguous requests:
javascriptfunction parseEmailCommand(command) { // Extract action type (send, forward, reply, etc.) const actionMatch = command.match(/^(check|show|send|forward|reply to|find)/i); const action = actionMatch ? actionMatch[1].toLowerCase() : null;
// Extract people mentioned const peopleMatches = command.match(/\b(from|to|cc)\s+([A-Za-z\s]+)(?=\s|$|,|\.)/g); const people = {};
if (peopleMatches) { peopleMatches.forEach(match => { const [type, name] = match.split(/\s+(.+)/); people[type] = name.trim(); }); }
// Extract other parameters const dateMatch = command.match(/\b(today|yesterday|last week|this month)\b/i); const timeframe = dateMatch ? dateMatch[1]These challenges and solutions reflect the real-world complexity of building a reliable, user-friendly AI email assistant. Each issue uncovered helped refine both the technical implementation and the GPT’s instruction set.
Solving the “Inbox” Problem
One particular challenge was getting the assistant to understand that when I say “check my inbox” or “show me emails,” I specifically mean messages in my Inbox folder - not emails spread across my entire mailbox.
The solution was to explicitly instruct the GPT that:
- “Inbox” refers specifically to the Inbox folder
- It should NEVER show emails from other folders when asked to check the inbox
- Any search should default to the Inbox folder unless I explicitly specify another location
Real-World Usage Examples
Here are some commands I use regularly with my assistant:
The assistant handles these conversationally, confirming details when needed and providing clear summaries of actions taken.
Benefits and Outcomes
Since implementing this custom GPT:
- Time savings: I spend significantly less time managing email
- Reduced mental load: No more context switching between various email views
- Better organization: Emails end up in the right folders with minimal effort
- Consistency: My communications maintain a professional tone even when I’m rushed
Future Improvements
I’m continuing to refine the assistant with:
- More sophisticated priority detection
- Enhanced draft management
- Better handling of complex email threads
- Calendar integration for scheduling references
Try It Yourself
Creating a custom GPT might sound technical, but OpenAI has made the process surprisingly accessible. If you’re facing similar challenges with email management, consider creating your own specialized assistant. The key is to be extremely specific in your instructions about what you want it to do and how it should interpret various commands.
The most important lesson I’ve learned is that clear, detailed instructions make all the difference in creating a truly useful custom GPT. Small details (like specifying exactly what “inbox” means) can dramatically improve the experience.
Have you experimented with custom GPTs for productivity? I’d love to hear about your experiences in the comments!
The author is a technology executive focused on helping businesses secure and optimize their cloud infrastructure.
