Blog

Custom GPT Experiment: Building an M365 Email Assistant

Custom GPT Experiment: Building an M365 Email Assistant

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

  1. Accounts and Subscriptions:

    • ChatGPT Plus subscription (to access Custom GPT capabilities)

    • Microsoft 365 account with mailbox access

  2. 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

  3. 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:

  1. 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

  2. Configure these API permissions:

    • Mail.Read

    • Mail.ReadWrite

    • Mail.Send

  3. Set up authentication:

    • Configure the OAuth settings

    • Use the provided client ID and secret in your Custom GPT action

  4. 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:

  1. Understand natural language commands for email management

  2. Focus specifically on Microsoft 365 email integration

  3. Learn my preferences and communication style

  4. 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:

  1. App Registration: I registered an application in the Azure Portal to get:

    • Application (client) ID

    • Directory (tenant) ID

    • Client secret

  2. Permission Configuration: I configured the following permissions:

    
    
  3. 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

  4. 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:

  1. Secure Secrets Management:

    • Client secret and tokens are stored in a secure vault

    • No credentials are ever exposed to the GPT itself

  2. 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}
            ]
    
    
  3. 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:

  1. Exponential Backoff: Implemented for API call retries

  2. Token Refresh Logic: Automatic refresh when tokens expire

  3. Graceful Degradation: Fallback options when specific features are unavailable

  4. 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 Profile
The 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 Logic
Apply 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.0
info:
  title: Microsoft 365 Email API (Minimal)
  description: Minimal API for interacting with Microsoft 365 Email using Microsoft Graph
  version: 1.5.0
servers:
  - url: https://graph.microsoft.com/v1.0
    description: Microsoft Graph API v1.0 endpoint
paths:
  /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:

  1. Modify the User Profile: Update the signature and user details to match your own

  2. Custom Commands: Add specific command interpretations that match your email workflow

  3. Folder Structure: Adjust the folder logic to match your Microsoft 365 organization

  4. API Endpoints: Add or remove endpoints from the OpenAPI schema based on your needs

  5. 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:

  1. Used Application Access Policies to restrict access to only my specific mailbox:

  2. Implemented strict auditing and monitoring for the application credentials

  3. 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:

  1. Modified Graph API calls to explicitly specify the Inbox folder ID:

  1. 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:

  1. Tracked the remaining request quota from response headers

  2. Added automatic request queuing when approaching limits

  3. 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:

  1. Created a streaming API endpoint for attachment retrieval

  2. 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:

  1. Added a pre-processing layer to identify command components

  2. Created a structured format for command interpretation

  3. 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:

  1. "Inbox" refers specifically to the Inbox folder

  2. It should NEVER show emails from other folders when asked to check the inbox

  3. 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:

  1. Time savings: I spend significantly less time managing email

  2. Reduced mental load: No more context switching between various email views

  3. Better organization: Emails end up in the right folders with minimal effort

  4. 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.