📧

OutlookアドインからLLMを呼び出す方法 - Office.jsとFastAPIで実現するAI連携

に公開

はじめに

こんにちは!
株式会社エクスプラザのhyodoです!

Office製品に独自のAIを組み込みたいと思ったことはありませんか?

今回は、OutlookアドインからLLM(GPT-3.5-turbo)を呼び出す仕組みを利用して、メール本文の宛名と送信先メールアドレスのドメインを検証するアドインを実装しました。Office.js APIとFastAPIを使って、OutlookとOpenAI APIをシームレスに連携させる方法を解説します。
Outlookアドインの動作画面

なぜOutlookアドインからLLMを呼び出すのか

Microsoft 365には標準でAI機能(Copilot)が搭載されつつありますが、以下のようなケースでは独自実装が必要です:

  • 組織独自のプロンプトでLLMを使いたい
  • 特定の業務ロジック(宛名とドメインの照合など)に特化したAI処理を実装したい
  • カスタムモデルや異なるLLMプロバイダーを使いたい

今回作成したアドイン

このアドインでは、LLMを使って以下のようなご送信を防ぐためにメール本文の宛名と送信先アドレスのドメインを検証しています。

❌ 誤送信例
本文: 「エクスプラザ様」
送信先: user@yahoo.co.jp
→ LLMが不一致を検出して送信をブロック

✅ 正常例
本文: 「株式会社エクスプラザ様」
送信先: user@explaza.jp
→ カタカナとローマ字でもLLMが同じ会社と判定

アーキテクチャ

技術スタック

  • フロントエンド: TypeScript + Office.js API
  • バックエンド: Python + FastAPI + litellm
  • LLM: OpenAI GPT-3.5-turbo
  • 通信: HTTPS + CORS

サンプルコード

プロジェクト構造

outlook-llm-addon/
├── backend/
│   ├── main.py
│   ├── requirements.txt
│   └── .env
├── src/
│   ├── taskpane/
│   │   ├── taskpane.html
│   │   └── taskpane.js
│   └── runtime/
│       └── runtime.js
├── manifest.xml
├── package.json
└── webpack.config.js

1. バックエンド実装(backend/main.py)

backend/main.py
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import litellm
from typing import List, Dict, Optional
import os
from dotenv import load_dotenv
import json

# 環境変数を読み込む
load_dotenv()

app = FastAPI()

# CORS設定(Outlookアドインからのアクセスを許可)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://localhost:3000", "http://localhost:3000"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 環境変数からAPIキーを取得して設定
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
if not OPENAI_API_KEY:
    raise ValueError("OPENAI_API_KEY is not set in environment variables")

# 環境変数として設定(litellmはos.environから読み取る)
os.environ["OPENAI_API_KEY"] = OPENAI_API_KEY

class Recipient(BaseModel):
    email: str
    name: Optional[str] = ""
    type: Optional[str] = "To"

class AnalyzeRequest(BaseModel):
    recipients: List[Recipient]
    body: Optional[str] = ""
    subject: Optional[str] = ""

@app.get("/")
async def root():
    return {"message": "Outlook LLM Backend is running"}

@app.get("/health")
async def health_check():
    return {"status": "healthy"}

@app.post("/analyze-with-llm")
def analyze_with_llm(request: AnalyzeRequest):
    try:
        # プロンプトの構築
        system_prompt = """
        あなたはメール分析の専門家です。
        与えられたメールの情報を分析し、以下の形式のJSONで結果を返してください:
        {
            "risk_level": "low/medium/high",
            "risk_reason": "リスクの理由",
            "suggestions": ["改善提案1", "改善提案2"],
            "summary": "全体的な分析結果"
        }
        """
        
        # 宛先を種類別に整理
        to_emails = [r.email for r in request.recipients if r.type == "To"]
        cc_emails = [r.email for r in request.recipients if r.type == "Cc"]
        
        user_prompt = f"""
        以下のメール情報を分析してください:
        
        宛先(To): {', '.join(to_emails)}
        宛先(Cc): {', '.join(cc_emails)}
        件名: {request.subject}
        本文(最初の500文字): {request.body[:500]}
        
        特に以下の点に注意して分析してください:
        1. 宛先の適切性(社内外の混在、誤送信の可能性)
        2. 本文と宛先の整合性
        3. 機密情報の漏洩リスク
        4. 一般的なビジネスマナー
        
        JSON形式で回答してください。
        """
        
        # LLM呼び出し
        response = litellm.completion(
            model="gpt-3.5-turbo",
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": user_prompt}
            ],
            temperature=0.1,
            max_tokens=500
        )
        
        # レスポンスの解析
        llm_result = response.choices[0].message.content
        
        # JSONとしてパース(エラーハンドリング付き)
        try:
            analysis = json.loads(llm_result)
        except json.JSONDecodeError:
            # パースできない場合のフォールバック
            analysis = {
                "risk_level": "medium",
                "risk_reason": "分析結果の解析に失敗しました",
                "suggestions": ["メールを再確認してください"],
                "summary": llm_result
            }
        
        # usageオブジェクトを取得(辞書形式の場合もある)
        usage = response.get('usage', {}) if isinstance(response, dict) else response.usage
        if hasattr(usage, 'prompt_tokens'):
            # 属性としてアクセス可能な場合
            prompt_tokens = usage.prompt_tokens
            completion_tokens = usage.completion_tokens
            total_tokens = usage.total_tokens
        else:
            # 辞書としてアクセスする場合
            prompt_tokens = usage.get('prompt_tokens', 0)
            completion_tokens = usage.get('completion_tokens', 0)
            total_tokens = usage.get('total_tokens', 0)
        
        return {
            "status": "success",
            "analysis": analysis,
            "model": response.model if hasattr(response, 'model') else response.get('model', 'gpt-3.5-turbo'),
            "usage": {
                "prompt_tokens": prompt_tokens,
                "completion_tokens": completion_tokens,
                "total_tokens": total_tokens,
                "estimated_cost": total_tokens * 0.000002  # GPT-3.5の料金
            }
        }
        
    except Exception as e:
        print(f"Error in analyze_with_llm: {str(e)}")
        raise HTTPException(status_code=500, detail=str(e))

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

2. フロントエンド実装(src/taskpane/taskpane.js)

src/taskpane/taskpane.js
Office.onReady((info) => {
    if (info.host === Office.HostType.Outlook) {
        document.getElementById("analyzeButton").onclick = analyzeEmail;
    }
});

async function analyzeEmail() {
    try {
        showLoading(true);
        updateStatus("メール情報を取得中...");
        
        // メールデータを取得
        const mailData = await getMailData();
        
        updateStatus("AIで分析中...");
        
        // LLMで分析
        const result = await callLLMAPI(mailData);
        
        // 結果を表示
        displayResults(result);
        
    } catch (error) {
        console.error("Analysis failed:", error);
        showError(`エラーが発生しました: ${error.message}`);
    } finally {
        showLoading(false);
    }
}

async function getMailData() {
    const item = Office.context.mailbox.item;
    
    // 宛先情報を取得
    const toRecipients = await getRecipientsAsync(item.to);
    const ccRecipients = await getRecipientsAsync(item.cc);
    const bccRecipients = await getRecipientsAsync(item.bcc);
    
    // 本文を取得
    const body = await new Promise((resolve) => {
        item.body.getAsync(Office.CoercionType.Text, (result) => {
            if (result.status === Office.AsyncResultStatus.Succeeded) {
                resolve(result.value || "");
            } else {
                resolve("");
            }
        });
    });
    
    // 件名を取得(非同期)
    const subject = await new Promise((resolve) => {
        if (item.subject && typeof item.subject === 'string') {
            resolve(item.subject);
        } else if (item.subject && item.subject.getAsync) {
            item.subject.getAsync((result) => {
                if (result.status === Office.AsyncResultStatus.Succeeded) {
                    resolve(result.value || "");
                } else {
                    resolve("");
                }
            });
        } else {
            resolve("");
        }
    });
    
    return {
        recipients: [
            ...toRecipients.map(r => ({ ...r, type: "To" })),
            ...ccRecipients.map(r => ({ ...r, type: "Cc" })),
            ...bccRecipients.map(r => ({ ...r, type: "Bcc" }))
        ],
        body: body || "",
        subject: subject || ""
    };
}

function getRecipientsAsync(recipients) {
    return new Promise((resolve) => {
        if (!recipients) {
            resolve([]);
            return;
        }
        
        recipients.getAsync((result) => {
            if (result.status === Office.AsyncResultStatus.Succeeded) {
                resolve(result.value.map(r => ({
                    email: r.emailAddress,
                    name: r.displayName
                })));
            } else {
                resolve([]);
            }
        });
    });
}

async function callLLMAPI(mailData) {
    const response = await fetch("http://localhost:8000/analyze-with-llm", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify(mailData)
    });
    
    if (!response.ok) {
        const error = await response.text();
        throw new Error(`API Error: ${error}`);
    }
    
    return await response.json();
}

function displayResults(result) {
    const resultDiv = document.getElementById("results");
    const analysis = result.analysis;
    
    // リスクレベルに応じた色を設定
    const riskColors = {
        low: "#4CAF50",
        medium: "#FF9800",
        high: "#F44336"
    };
    
    resultDiv.innerHTML = `
        <div style="padding: 20px; background-color: #f5f5f5; border-radius: 8px;">
            <h3>AI分析結果</h3>
            
            <div style="margin-bottom: 15px;">
                <strong>リスクレベル:</strong>
                <span style="background-color: ${riskColors[analysis.risk_level]}; 
                            color: white; padding: 5px 10px; border-radius: 4px;">
                    ${analysis.risk_level.toUpperCase()}
                </span>
            </div>
            
            <div style="margin-bottom: 15px;">
                <strong>理由:</strong>
                <p>${analysis.risk_reason}</p>
            </div>
            
            <div style="margin-bottom: 15px;">
                <strong>改善提案:</strong>
                <ul>
                    ${analysis.suggestions.map(s => `<li>${s}</li>`).join('')}
                </ul>
            </div>
            
            <div style="margin-bottom: 15px;">
                <strong>分析サマリー:</strong>
                <p>${analysis.summary}</p>
            </div>
            
            <div style="font-size: 12px; color: #666;">
                <strong>使用モデル:</strong> ${result.model}<br>
                <strong>トークン数:</strong> ${result.usage.total_tokens}<br>
                <strong>推定コスト:</strong> $${result.usage.estimated_cost.toFixed(4)}
            </div>
        </div>
    `;
}

function showLoading(show) {
    document.getElementById("loading").style.display = show ? "block" : "none";
    document.getElementById("analyzeButton").disabled = show;
}

function updateStatus(message) {
    document.getElementById("status").textContent = message;
}

function showError(message) {
    const resultDiv = document.getElementById("results");
    resultDiv.innerHTML = `
        <div style="padding: 20px; background-color: #ffebee; border-radius: 8px; color: #c62828;">
            <strong>エラー:</strong> ${message}
        </div>
    `;
}

3. タスクペインHTML(src/taskpane/taskpane.html)

src/taskpane/taskpane.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=Edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Outlook LLM Analyzer</title>
    <script type="text/javascript" src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            margin: 0;
            padding: 20px;
            background-color: #f8f9fa;
        }
        .container {
            max-width: 100%;
            background: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        h1 {
            color: #0078d4;
            font-size: 24px;
            margin-bottom: 20px;
        }
        button {
            background-color: #0078d4;
            color: white;
            border: none;
            padding: 10px 20px;
            font-size: 16px;
            border-radius: 4px;
            cursor: pointer;
            width: 100%;
            margin-bottom: 20px;
        }
        button:hover {
            background-color: #106ebe;
        }
        button:disabled {
            background-color: #ccc;
            cursor: not-allowed;
        }
        #loading {
            display: none;
            text-align: center;
            margin: 20px 0;
        }
        .spinner {
            border: 3px solid #f3f3f3;
            border-top: 3px solid #0078d4;
            border-radius: 50%;
            width: 40px;
            height: 40px;
            animation: spin 1s linear infinite;
            margin: 0 auto;
        }
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
        #status {
            text-align: center;
            color: #666;
            margin: 10px 0;
        }
        #results {
            margin-top: 20px;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>📧 メールAI分析</h1>
        <p>このメールをAIが分析し、潜在的なリスクや改善点を提案します。</p>
        
        <button id="analyzeButton">メールを分析する</button>
        
        <div id="loading">
            <div class="spinner"></div>
            <p id="status">処理中...</p>
        </div>
        
        <div id="results"></div>
    </div>
    
    <script src="taskpane.js"></script>
</body>
</html>

4. Event-based Activation(src/runtime/runtime.js)

src/runtime/runtime.js
Office.initialize = function () {
    console.log("Runtime initialized");
};

// 新規メール作成時のハンドラー
function onNewMessageComposeHandler(event) {
    console.log("新規メール作成を検出しました");
    
    try {
        // 通知バーに情報を表示
        Office.context.mailbox.item.notificationMessages.addAsync(
            "autoCheckNotification",
            {
                type: Office.MailboxEnums.ItemNotificationMessageType.InformationalMessage,
                message: "LLM宛先確認アドインが有効です。送信前に宛先を自動チェックします。",
                icon: "icon-16",
                persistent: true
            }
        );
    } catch (error) {
        console.error("onNewMessageComposeHandler error:", error);
    }
    
    event.completed();
}

// 送信時の自動チェック
async function onMessageSendHandler(event) {
    try {
        // メールデータを取得
        const item = Office.context.mailbox.item;
        const recipients = await getAllRecipients(item);
        const body = await getBodyText(item);
        
        // 件名を取得(文字列として)
        let subject = "";
        try {
            if (typeof item.subject === 'string') {
                subject = item.subject;
            }
        } catch (e) {
            console.error("Subject retrieval error:", e);
        }
        
        // LLMで分析
        const response = await fetch("http://localhost:8000/analyze-with-llm", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify({
                recipients,
                body,
                subject
            })
        });
        
        if (!response.ok) {
            throw new Error("API call failed");
        }
        
        const result = await response.json();
        const analysis = result.analysis;
        
        // リスクレベルに応じて送信可否を決定
        if (analysis.risk_level === "high") {
            event.completed({
                allowEvent: false,
                errorMessage: `⚠️ 高リスクが検出されました\n\n${analysis.risk_reason}\n\n改善提案:\n${analysis.suggestions.join('\n')}`
            });
        } else if (analysis.risk_level === "medium") {
            // 中リスクの場合は警告のみ(送信は許可)
            event.completed({
                allowEvent: true,
                errorMessage: `⚠️ 注意: ${analysis.risk_reason}`
            });
        } else {
            // 低リスクの場合はそのまま送信
            event.completed({ allowEvent: true });
        }
        
    } catch (error) {
        console.error("Send check error:", error);
        // エラー時は送信を許可(業務を止めない)
        event.completed({
            allowEvent: true,
            errorMessage: "AI分析に失敗しました。注意して送信してください。"
        });
    }
}

async function getAllRecipients(item) {
    const recipients = [];
    
    const toRecipients = await getRecipientsAsync(item.to);
    const ccRecipients = await getRecipientsAsync(item.cc);
    const bccRecipients = await getRecipientsAsync(item.bcc);
    
    recipients.push(...toRecipients.map(r => ({ ...r, type: "To" })));
    recipients.push(...ccRecipients.map(r => ({ ...r, type: "Cc" })));
    recipients.push(...bccRecipients.map(r => ({ ...r, type: "Bcc" })));
    
    return recipients;
}

function getRecipientsAsync(recipients) {
    return new Promise((resolve) => {
        if (!recipients) {
            resolve([]);
            return;
        }
        
        recipients.getAsync((result) => {
            if (result.status === Office.AsyncResultStatus.Succeeded) {
                resolve(result.value.map(r => ({
                    email: r.emailAddress,
                    name: r.displayName
                })));
            } else {
                resolve([]);
            }
        });
    });
}

function getBodyText(item) {
    return new Promise((resolve) => {
        item.body.getAsync(Office.CoercionType.Text, (result) => {
            if (result.status === Office.AsyncResultStatus.Succeeded) {
                resolve(result.value || "");
            } else {
                resolve("");
            }
        });
    });
}

// 関数を登録
Office.actions.associate("onNewMessageComposeHandler", onNewMessageComposeHandler);
Office.actions.associate("onMessageSendHandler", onMessageSendHandler);

5. マニフェストファイル(manifest.xml)の主要部分

manifest.xml
<?xml version="1.0" encoding="UTF-8"?>
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0"
           xmlns:mailappor="http://schemas.microsoft.com/office/mailappversionoverrides/1.0"
           xsi:type="MailApp">
  
  <Id>12345678-1234-1234-1234-123456789012</Id>
  <Version>1.0.0.0</Version>
  <ProviderName>Your Company</ProviderName>
  <DefaultLocale>ja-JP</DefaultLocale>
  <DisplayName DefaultValue="Outlook LLM Analyzer"/>
  <Description DefaultValue="OutlookメールをAIで分析します"/>
  <IconUrl DefaultValue="https://localhost:3000/assets/icon-64.png"/>
  <HighResolutionIconUrl DefaultValue="https://localhost:3000/assets/icon-128.png"/>
  <SupportUrl DefaultValue="https://www.example.com/help"/>
  <AppDomains>
    <AppDomain>https://localhost:3000</AppDomain>
    <AppDomain>http://localhost:8000</AppDomain>
  </AppDomains>
  <Hosts>
    <Host Name="Mailbox"/>
  </Hosts>
  <Requirements>
    <Sets>
      <Set Name="Mailbox" MinVersion="1.10"/>
    </Sets>
  </Requirements>
  
  <FormSettings>
    <Form xsi:type="ItemRead">
      <DesktopSettings>
        <SourceLocation DefaultValue="https://localhost:3000/taskpane.html"/>
        <RequestedHeight>450</RequestedHeight>
      </DesktopSettings>
    </Form>
    <Form xsi:type="ItemEdit">
      <DesktopSettings>
        <SourceLocation DefaultValue="https://localhost:3000/taskpane.html"/>
      </DesktopSettings>
    </Form>
  </FormSettings>
  
  <Permissions>ReadWriteItem</Permissions>
  <Rule xsi:type="RuleCollection" Mode="Or">
    <Rule xsi:type="ItemIs" ItemType="Message" FormType="Read"/>
    <Rule xsi:type="ItemIs" ItemType="Message" FormType="Edit"/>
  </Rule>
  <DisableEntityHighlighting>false</DisableEntityHighlighting>
  <VersionOverrides xmlns="http://schemas.microsoft.com/office/mailappversionoverrides" xsi:type="VersionOverridesV1_0">
    <VersionOverrides xmlns="http://schemas.microsoft.com/office/mailappversionoverrides/1.1" xsi:type="VersionOverridesV1_1">
    <Requirements>
      <bt:Sets DefaultMinVersion="1.10">
        <bt:Set Name="Mailbox"/>
      </bt:Sets>
    </Requirements>
    
    <Hosts>
      <Host xsi:type="MailHost">
        <Runtimes>
          <Runtime resid="Runtime.Url">
            <Override type="javascript" resid="Runtime.Url"/>
          </Runtime>
        </Runtimes>
        
        <DesktopFormFactor>
          <FunctionFile resid="Commands.Url"/>
          <ExtensionPoint xsi:type="LaunchEvent">
            <LaunchEvents>
              <LaunchEvent Type="OnNewMessageCompose" FunctionName="onNewMessageComposeHandler"/>
              <LaunchEvent Type="OnMessageSend" FunctionName="onMessageSendHandler"/>
            </LaunchEvents>
            <SourceLocation resid="Runtime.Url"/>
          </ExtensionPoint>
          
          <ExtensionPoint xsi:type="MessageComposeCommandSurface">
            <OfficeTab id="TabDefault">
              <Group id="msgComposeGroup">
                <Label resid="GroupLabel"/>
                <Control xsi:type="Button" id="msgComposeOpenPaneButton">
                  <Label resid="TaskpaneButton.Label"/>
                  <Supertip>
                    <Title resid="TaskpaneButton.Label"/>
                    <Description resid="TaskpaneButton.Tooltip"/>
                  </Supertip>
                  <Icon>
                    <bt:Image size="16" resid="Icon.16x16"/>
                    <bt:Image size="32" resid="Icon.32x32"/>
                    <bt:Image size="80" resid="Icon.80x80"/>
                  </Icon>
                  <Action xsi:type="ShowTaskpane">
                    <SourceLocation resid="Taskpane.Url"/>
                    <SupportsPinning>true</SupportsPinning>
                  </Action>
                </Control>
              </Group>
            </OfficeTab>
          </ExtensionPoint>
        </DesktopFormFactor>
      </Host>
    </Hosts>
    
      <Resources>
        <bt:Images>
          <bt:Image id="Icon.16x16" DefaultValue="https://localhost:3000/assets/icon-16.png"/>
          <bt:Image id="Icon.32x32" DefaultValue="https://localhost:3000/assets/icon-32.png"/>
          <bt:Image id="Icon.80x80" DefaultValue="https://localhost:3000/assets/icon-80.png"/>
        </bt:Images>
        <bt:Urls>
          <bt:Url id="Commands.Url" DefaultValue="https://localhost:3000/commands.html"/>
          <bt:Url id="Taskpane.Url" DefaultValue="https://localhost:3000/taskpane.html"/>
          <bt:Url id="Runtime.Url" DefaultValue="https://localhost:3000/runtime.js"/>
        </bt:Urls>
        <bt:ShortStrings>
          <bt:String id="GroupLabel" DefaultValue="AI分析"/>
          <bt:String id="ButtonLabel" DefaultValue="メールを分析"/>
          <bt:String id="ButtonTitle" DefaultValue="AI分析"/>
          <bt:String id="TaskpaneButton.Label" DefaultValue="メールを分析"/>
        </bt:ShortStrings>
        <bt:LongStrings>
          <bt:String id="ButtonDesc" DefaultValue="メールをAIで分析してリスクをチェックします"/>
          <bt:String id="TaskpaneButton.Tooltip" DefaultValue="LLMを使用してメールの宛先を確認します"/>
        </bt:LongStrings>
      </Resources>
    </VersionOverrides>
  </VersionOverrides>
</OfficeApp>

セットアップ手順

1. バックエンドのセットアップ

# バックエンドディレクトリに移動
cd backend

# 仮想環境を作成
python -m venv venv

# 仮想環境を有効化
.\venv\Scripts\activate

# 必要なパッケージをインストール
pip install fastapi uvicorn litellm python-dotenv

# .envファイルを作成(PowerShellで)
# 重要: UTF-8ではなくASCIIエンコーディングで作成
"OPENAI_API_KEY=sk-your-api-key-here" | Set-Content -Path .env -Encoding ASCII

# APIキーが正しく設定されているか確認
python -c "from dotenv import load_dotenv; import os; load_dotenv(); print('APIキー設定:', 'OK' if os.getenv('OPENAI_API_KEY') else 'NG')"

# サーバーを起動
python main.py

2. フロントエンドのセットアップ

npm init -y

# 必要なパッケージをインストール
npm install --save-dev webpack webpack-cli webpack-dev-server
npm install --save-dev html-webpack-plugin copy-webpack-plugin html-loader
npm install --save-dev office-addin-cli office-addin-dev-certs
npm install --save-dev @types/office-js

# 開発サーバーを起動
npm run start

3. Outlookへのインストール

  1. Outlookを開く
  2. リボンにあるアプリ(アドイン)→ アドインを取得
  3. 個人用アドイン → カスタムアドインの追加 → ファイルから追加
  4. manifest.xmlを選択 → インストール

Outlookアドイン管理画面

まとめ

最後までお読みいだだきありがとうございました!
このコードをベースに、組織のニーズに合わせたカスタマイズを行っていただければと思います!
ご質問やフィードバックがあれば、コメント欄でお待ちしています!

参考


https://zenn.dev/p/explaza

株式会社エクスプラザ

Discussion