An intelligent Outlook add-in that leverages Large Language Models (LLMs) to automatically summarize email content, helping users quickly understand key points, action items, and important information.
- ๐ Smart Email Analysis: Automatically extracts and analyzes email content
- ๐ค AI-Powered Summarization: Integrates with multiple LLM APIs (OpenAI, Claude, Azure OpenAI)
- ๐ Structured Summaries: Provides organized summaries with key points and action items
- ๐ Real-time Processing: Instant email analysis with loading indicators
- ๐ Cross-Platform: Works in Outlook desktop, web, and mobile
- ๐ก๏ธ Secure: API keys stored securely, no email content stored externally
The add-in is fully functional and working as designed! Here's what the demo mode shows:
Demo Features Demonstrated:
- โ Clean, modern UI with blue gradient header
- โ Prominent "๐ Analyze Email" button
- โ Mock email content processing (596 characters)
- โ AI summarization with structured output
- โ Demo mode instructions with proper bullet formatting
- โ Real-time feedback and loading states
- โ Professional styling with consistent branding
Key Testing Results:
- ๐ Button click functionality: Working
- ๐ง Email content extraction: Working (mock data)
- ๐ค AI summarization: Working (demo mode)
- ๐จ UI/UX design: Polished and professional
- ๐ฑ Responsive layout: Mobile-friendly
To see the AI Email Summarizer in action right now:
- Start the dev server:
npm run dev-server - Open your browser: Navigate to
https://localhost:3000/taskpane.html - Click "๐ Analyze Email": See instant AI summarization with mock email data
- View the demo: Experience the full workflow with realistic business email content
Note: The demo uses mock email data. In a real Outlook add-in, this would process actual email content.
- OpenAI GPT (GPT-3.5, GPT-4)
- Anthropic Claude (Claude-3 Sonnet)
- Azure OpenAI
- Google Gemini (can be added)
- Custom API endpoints
- Node.js (latest LTS version)
- npm or yarn
- Microsoft 365 account
- Outlook (web, desktop, or mobile)
- Clone the repository:
git clone <repository-url>
cd kevinxu-test-sample- Install dependencies:
npm install- Start development server:
npm run dev-server- Build for production:
npm run build-
OpenAI Setup:
- Get API key from OpenAI Platform
- Replace
API_KEYinsrc/taskpane/taskpane.ts
-
Claude Setup:
- Get API key from Anthropic Console
- Uncomment Claude function in code
-
Azure OpenAI Setup:
- Set up Azure OpenAI resource
- Configure endpoint and API key
Create a .env file (not committed to git):
OPENAI_API_KEY=your_openai_key_here
CLAUDE_API_KEY=your_claude_key_here
AZURE_OPENAI_ENDPOINT=your_azure_endpoint
AZURE_OPENAI_KEY=your_azure_keynpm run dev-server
open https://localhost:3000/taskpane.html- Outlook on the Web: Manually sideload via add-in settings
- Outlook Desktop: Use
npm start(if sideloading is supported) - Development Account: Use Microsoft 365 Developer subscription
โโโ src/
โ โโโ taskpane/
โ โ โโโ taskpane.ts # Main add-in logic with LLM integration
โ โ โโโ taskpane.html # Add-in UI
โ โ โโโ taskpane.css # Styling
โ โโโ commands/
โโโ assets/ # Icons and images
โโโ manifest.json # Add-in manifest
โโโ package.json # Dependencies and scripts
โโโ webpack.config.js # Build configuration
// Access email subject and body
const item = Office.context.mailbox.item;
const subject = item.subject;
item.body.getAsync("text", callback);// Call OpenAI API
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: "gpt-3.5-turbo",
messages: [...]
})
});- โ API keys are excluded from version control
- โ HTTPS required for all API calls
- โ Input validation for email content
- โ Error handling for API failures
โ ๏ธ Consider data privacy when sending emails to external APIs
npm start- Start with automatic sideloadingnpm run dev-server- Start development server onlynpm run build- Build for productionnpm run lint- Run ESLintnpm stop- Stop development server
- Sideloading fails: Use manual sideloading via Outlook settings
- Certificate issues: Accept localhost certificate when prompted
- API errors: Check API key configuration and rate limits
- Browser caching: Hard refresh or use incognito mode
Open browser dev tools to see console logs:
๐ง Running in standalone browser mode for testing- API call logs and error messages
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
- Microsoft Office Add-ins team for the excellent documentation
- OpenAI for the GPT API
- Anthropic for the Claude API
- The open-source community for webpack and other tools
This guide explains the build and deployment process for the Outlook Connector application, common issues, and troubleshooting steps.
npm run build # Executes: webpack --mode production-
Webpack Configuration Loading
- Loads
webpack.config.js - Sets
options.mode = "production" - Configures
publicPath = "/OutlookConnector/"
- Loads
-
Entry Points Processing
entry: { polyfill: ["core-js/stable", "regenerator-runtime/runtime"], taskpane: ["./src/taskpane/taskpane.ts", "./src/taskpane/taskpane.html"], commands: "./src/commands/commands.ts", }
-
Module Transformations
- TypeScript โ JavaScript:
.tsfiles compiled via Babel - HTML Processing: Templates processed with asset injection
- Asset Optimization: Images copied and optimized
- TypeScript โ JavaScript:
-
Plugin Execution
- HtmlWebpackPlugin: Generates
taskpane.htmlwith correct script tags - CopyWebpackPlugin: Copies assets and transforms manifest URLs
- HtmlWebpackPlugin: Generates
-
Production Optimizations
- Minification: JavaScript compressed
- Tree Shaking: Unused code removed
- Code Splitting: Separate bundles for different concerns
dist/
โโโ taskpane.html # Processed HTML with correct paths
โโโ taskpane.js # Compiled & minified JavaScript (322KB)
โโโ taskpane.js.map # Source map for debugging
โโโ polyfill.js # Browser compatibility bundle (203KB)
โโโ commands.html # Commands page
โโโ commands.js # Commands bundle
โโโ manifest.json # Production URLs
โโโ [hash].css # Compiled CSS
โโโ assets/ # Static assets
โโโ icon-80.png
โโโ icon-128.png
โโโ logo-filled.png
name: Deploy to GitHub Pages
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build project
run: npm run build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: './dist'
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to GitHub Pages
uses: actions/deploy-pages@v4- Trigger: Push to
mainbranch - Build Job:
- Install dependencies with
npm ci - Build project with
npm run build - Upload
dist/folder as artifact
- Install dependencies with
- Deploy Job:
- Wait for build completion
- Deploy artifact to GitHub Pages
- Result: Live at
https://jkevinxu.github.io/OutlookConnector/
Symptoms:
- Build successful but pages return 404
- Assets not loading correctly
Causes:
- Incorrect
publicPathconfiguration - Conflicting deployment workflows
Solution:
// webpack.config.js
const publicPath = dev ? "/" : "/OutlookConnector/";Problem: Multiple workflow files deploying different content
Files to check:
.github/workflows/deploy.ymlโ (Keep this).github/workflows/static.ymlโ (Remove this)
Key Differences:
deploy.yml โ
|
static.yml โ |
|---|---|
Builds project with npm run build |
No build step |
Deploys ./dist (built files) |
Deploys . (entire repo) |
| ~2MB deployment | ~500MB deployment |
| Compiled JavaScript | Raw TypeScript |
| Production URLs | Development URLs |
Problem: Office add-in manifest contains localhost URLs in production
Solution: Webpack transforms URLs during build:
transform(content) {
if (dev) {
return content;
} else {
return content.toString().replace(new RegExp(urlDev, "g"), urlProd);
}
}- Development:
https://localhost:3000/ - Production:
https://jkevinxu.github.io/OutlookConnector/
-
GitHub Actions
- Go to repository โ Actions tab
- Look for "Deploy to GitHub Pages" workflow
- โ Green = Success, โ Red = Failed
-
GitHub Pages Settings
- Repository โ Settings โ Pages
- Source should be "GitHub Actions"
- Check deployment status and URL
-
Test URLs
# Should return 200 OK curl -I https://jkevinxu.github.io/OutlookConnector/taskpane.html curl -I https://jkevinxu.github.io/OutlookConnector/taskpane.js
Method 1: Re-run from GitHub UI
- Go to Actions โ Select workflow run โ Re-run jobs
Method 2: Trigger new deployment
git commit --allow-empty -m "Trigger deployment"
git push origin mainCheck build output:
npm run build
# Look for errors in webpack output
# Check generated files in dist/Common build errors:
- TypeScript compilation errors
- Missing dependencies
- Asset path issues
-
Local Development
npm run dev-server # Local development with hot reload -
Test Build Locally
npm run build # Test production build -
Deploy
git add . git commit -m "Your changes" git push origin main # Triggers deployment
- Source files: Keep in
src/directory - Static assets: Keep in
assets/directory - Build output: Generated in
dist/(don't commit) - Workflows: One deployment workflow only
- Development:
publicPath = "/" - Production:
publicPath = "/OutlookConnector/" - URLs: Transform localhost โ production in manifest
- Build process is essential - Browsers can't execute TypeScript directly
- Only one deployment workflow - Multiple workflows cause conflicts
- Correct publicPath - Must match GitHub Pages URL structure
- URL transformation - Manifest must use production URLs
- Test locally first - Always verify build works before deploying
Build Commands:
npm run build- Production buildnpm run build:dev- Development buildnpm run dev-server- Local development server
Deployment URL:
https://jkevinxu.github.io/OutlookConnector/taskpane.html
Key Files:
webpack.config.js- Build configuration.github/workflows/deploy.yml- Deployment workflowdist/- Generated build output
This document outlines the capabilities and limitations of accessing email conversation threads in Outlook add-ins using Office.js APIs, and provides solutions using Microsoft Graph API.
Office.js for Outlook add-ins has very limited support for accessing conversation threads:
- โ Get all emails in a conversation thread
- โ Navigate to parent/child emails in thread
- โ Access previous messages in the same conversation
- โ Retrieve conversation history
- โ List related emails with same subject
Office.js primarily provides access to the current email item only:
// Access current email item
const item = Office.context.mailbox.item;
// Available properties
const subject = item.subject;
const body = await getEmailBody();
const sender = item.from;
const recipients = item.to;
// Basic conversation info (limited)
const conversationId = item.conversationId; // May not always be availableTo access conversation threads, you need to combine Office.js with Microsoft Graph API.
- Permissions in manifest.json:
{
"authorization": {
"permissions": {
"resourceSpecific": [
{
"name": "MailboxItem.Read.User",
"type": "Delegated"
},
{
"name": "Mail.Read",
"type": "Delegated"
}
]
}
}
}- Authentication Setup: Your add-in must support Microsoft Graph authentication.
/**
* Fetch all emails in the same conversation thread
*/
async function getConversationEmails(): Promise<any[]> {
try {
// Get access token for Graph API
const tokenResult = await OfficeRuntime.auth.getAccessToken({
allowSignInPrompt: true,
allowConsentPrompt: true
});
const currentItem = Office.context.mailbox.item;
const conversationId = currentItem.conversationId;
if (!conversationId) {
console.warn('No conversation ID available for current email');
return [];
}
// Call Graph API to get conversation emails
const response = await fetch(
`https://graph.microsoft.com/v1.0/me/messages?$filter=conversationId eq '${conversationId}'&$orderby=receivedDateTime desc`,
{
headers: {
'Authorization': `Bearer ${tokenResult}`,
'Content-Type': 'application/json'
}
}
);
if (!response.ok) {
throw new Error(`Graph API request failed: ${response.status}`);
}
const data = await response.json();
return data.value || [];
} catch (error) {
console.error('Error fetching conversation emails:', error);
return [];
}
}/**
* Find related emails by subject (fallback approach)
*/
async function getEmailsBySubject(subject: string): Promise<any[]> {
try {
const token = await OfficeRuntime.auth.getAccessToken();
// Clean and encode subject for search
const cleanSubject = subject.replace(/^(RE:|FW:|FWD:)\s*/i, '').trim();
const encodedSubject = encodeURIComponent(cleanSubject);
const response = await fetch(
`https://graph.microsoft.com/v1.0/me/messages?$filter=contains(subject, '${encodedSubject}')&$orderby=receivedDateTime desc&$top=10`,
{
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
);
const data = await response.json();
return data.value || [];
} catch (error) {
console.error('Error searching emails by subject:', error);
return [];
}
}/**
* Analyze current email and its conversation thread
*/
async function analyzeConversationThread() {
try {
// Get current email info via Office.js
const currentItem = Office.context.mailbox.item;
const currentEmailInfo = {
subject: currentItem.subject,
sender: currentItem.from?.emailAddress,
senderName: currentItem.from?.displayName,
conversationId: currentItem.conversationId
};
// Get email body
const bodyResult = await new Promise<string>((resolve, reject) => {
currentItem.body.getAsync("text", (result) => {
if (result.status === Office.AsyncResultStatus.Succeeded) {
resolve(result.value);
} else {
reject(new Error(result.error?.message || "Failed to get email body"));
}
});
});
// Extract first 20 words
const words = bodyResult.trim().split(/\s+/);
const first20Words = words.slice(0, 20).join(' ');
currentEmailInfo.first20Words = first20Words + (words.length > 20 ? '...' : '');
currentEmailInfo.wordCount = words.length;
currentEmailInfo.charCount = bodyResult.length;
// Get conversation thread emails via Graph API
let threadEmails: any[] = [];
if (currentEmailInfo.conversationId) {
threadEmails = await getConversationEmails();
} else {
// Fallback to subject-based search
threadEmails = await getEmailsBySubject(currentEmailInfo.subject);
}
return {
currentEmail: currentEmailInfo,
threadEmails: threadEmails.map(email => ({
id: email.id,
subject: email.subject,
sender: email.from?.emailAddress?.address,
senderName: email.from?.emailAddress?.name,
receivedDateTime: email.receivedDateTime,
bodyPreview: email.bodyPreview,
isRead: email.isRead,
hasAttachments: email.hasAttachments
})),
threadCount: threadEmails.length
};
} catch (error) {
console.error('Error analyzing conversation thread:', error);
throw error;
}
}/**
* Display current email and conversation thread in the task pane
*/
async function displayEmailWithThread() {
try {
const analysis = await analyzeConversationThread();
const container = document.getElementById("item-subject");
if (!container) return;
container.innerHTML = "";
// Display current email info
const currentEmailSection = document.createElement("div");
currentEmailSection.style.cssText = `
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
`;
currentEmailSection.innerHTML = `
<h3>๐ง Current Email</h3>
<p><strong>Subject:</strong> ${analysis.currentEmail.subject}</p>
<p><strong>From:</strong> ${analysis.currentEmail.senderName} <${analysis.currentEmail.sender}></p>
<p><strong>First 20 words:</strong> ${analysis.currentEmail.first20Words}</p>
<p><strong>Stats:</strong> ${analysis.currentEmail.wordCount} words, ${analysis.currentEmail.charCount} characters</p>
`;
container.appendChild(currentEmailSection);
// Display conversation thread
if (analysis.threadEmails.length > 1) {
const threadSection = document.createElement("div");
threadSection.style.cssText = `
background-color: #fff;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
`;
threadSection.innerHTML = `
<h3>๐ฌ Conversation Thread (${analysis.threadCount} emails)</h3>
<div id="thread-emails"></div>
`;
const threadContainer = threadSection.querySelector("#thread-emails");
analysis.threadEmails.forEach((email, index) => {
const emailItem = document.createElement("div");
emailItem.style.cssText = `
border-left: 3px solid ${index === 0 ? '#007bff' : '#dee2e6'};
padding: 10px 15px;
margin: 10px 0;
background: ${index === 0 ? '#f8f9fa' : '#fff'};
`;
emailItem.innerHTML = `
<div style="font-weight: bold; color: #495057;">
${email.senderName || email.sender}
${index === 0 ? '(Current)' : ''}
</div>
<div style="font-size: 12px; color: #6c757d; margin: 5px 0;">
${new Date(email.receivedDateTime).toLocaleDateString()} -
${email.isRead ? 'Read' : 'Unread'}${email.hasAttachments ? ' ๐' : ''}
</div>
<div style="font-size: 13px; color: #495057;">
${email.bodyPreview || 'No preview available'}
</div>
`;
threadContainer.appendChild(emailItem);
});
container.appendChild(threadSection);
}
} catch (error) {
console.error('Error displaying email with thread:', error);
showError('Failed to load conversation thread: ' + error.message);
}
}// Get messages by conversation ID
GET /me/messages?$filter=conversationId eq '{conversation-id}'
// Get messages with specific ordering and limiting
GET /me/messages?$filter=conversationId eq '{conversation-id}'&$orderby=receivedDateTime desc&$top=50
// Search messages by subject
GET /me/messages?$filter=contains(subject, '{subject-text}')&$orderby=receivedDateTime desc
// Get conversation threads (alternative approach)
GET /me/conversations/{conversation-id}/threads
// Get message details with expanded properties
GET /me/messages/{message-id}?$expand=attachmentsMail.Read- Read user's emailMail.ReadWrite- Read and write user's email (if modifications needed)
Here's how to integrate this into your existing run() function:
export async function run() {
// Check authentication first
if (!authService.isAuthenticated()) {
showError("Please sign in to analyze emails");
return;
}
try {
// Display current email and conversation thread
await displayEmailWithThread();
} catch (error) {
console.error("Error in run():", error);
showError("Failed to analyze email: " + error.message);
}
}- Always validate Graph API responses
- Handle authentication failures gracefully
- Implement proper error handling for network issues
- Cache conversation data when possible
- Use
$topparameter to limit result sets - Implement pagination for large conversations
- Use subject-based search if
conversationIdis unavailable - Gracefully degrade to current email only if Graph API fails
- Provide clear user feedback about limitations
Microsoft may expand Office.js conversation capabilities in future updates. Monitor the Office Add-ins roadmap for new features.
- Microsoft Graph API Documentation
- Office.js API Reference
- Outlook Add-ins Documentation
- Authentication in Office Add-ins
Current State: Office.js alone cannot access conversation threads
Solution: Combine Office.js + Microsoft Graph API
Limitation: Add-ins are designed around single-item interaction
Workaround: Use Graph API for comprehensive thread-level operations
