Microsoft365のメール数やファイルサイズをPowerShellで取得する

Microsoft365からGoogle Workspaceに移行するといっても、Exchange OnlineやSharepointのそれぞれのオブジェクト数を割り出さないと移行スケジュールが建てられません。

そこで、この「オブジェクト数」を調査する為に、PowerShellの追加モジュールである「Graph Powershell SDK」を使って、Exchange OnlineやSharepointのオブジェクト数を割り出してみようと思います。

Google Workspaceを新規に導入する時に押さえたいポイント

今回利用するツール等

PowerShellは現在のWindowsには標準でインストール済みではあるのですが、実はバージョンが低いものが入っており、起動する度に最新版を入れろとメッセージが出てくるので、まずはPowerShellの最新版からのインストールより説明します。

また、Graphと名前がついていますが、Graph APIでは全領域をカバーできておらず、PowerShellはAzureの細部にまで手が伸ばせるため、こういった作業の時には必須のツールとなっています。

このオブジェクト取得によってGoogle Workspaceへ移行したい場合にはどれくらいの日数で移行できるのか?といった算出の為の元データとなるため、事前準備の段階から取得しておき、GWM実施直前でも取得してどれくらい増量したのか?といった調査に使えます。

※ちなみにmacOSでもインストールすることが可能です。

Google Workspace MigrateでMicrosoft365からお引越し - 事前準備編

図:macOSでも動かしてみた

調査の為の事前準備

PowerShell最新版のインストール

Windowsの場合

まずはインストール済みのPowershellではなく最新版のPowerShellをインストールします。

  1. こちらのサイトを開く
  2. MSIパッケージとなるので、ここからx64版もしくはarm64版、環境に合わせてダウンロードする
  3. ダウンロードしたインストーラを起動する
  4. 色々オプションが出てきますが基本デフォルトのまま次へ進んでインストールを完了する
  5. スタートメニューをクリックし、検索でPowerShellと入れるとPowershell7というのが出てきます。古いバージョンも共存してるので注意。
  6. これをタスクバーにピン留めすると良いでしょう。

図:インストーラを起動する

図:PowerSehll7が本体です

macOSの場合

macOSの場合もGithubにpkgファイルがリリースされているので同様にインストールは可能です。但し、macOSの場合はターミナル上で動くコマンドであるため、独立したアプリという形ではありません。

また、Homebrew経由でインストールも可能です。その場合、以下のコマンドでインストールが可能です。

brew install powershell/tap/powershell

インストールができたらターミナル上で「pwsh」コマンドを打てばPowerShellになります。あとはps1ファイルをそのまま実行したり、PowerShellコマンドをそのまま実行することが可能です。

図:ターミナルがPowerShellになる

Graph PowerShell SDKのインストール

続けて、PowerShell7を管理者権限で起動して以下のコマンドを実行します。

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
Install-Module Microsoft.Graph -Scope CurrentUser -Repository PSGallery -Force

動き始めるまでちょっと時間が掛かるので辛抱して待ちます。すると色々とインストールが始まります。インストール後は、以下のコマンドでインストール済みかどうか確認出来ます。

Get-InstalledModule Microsoft.Graph

アップデートする場合には以下のコマンドを実行。アップデートが掛かるまで結構時間が掛かる。

Update-Module Microsoft.Graph

図:モジュールのインストール

アカウント接続

次の作業として同じくPowerShell7にて、Microsoft365と接続します。以下のコマンドを実行します。

Connect-MgGraph

するとブラウザが起動して、ログインを促してくるので特権管理者アカウントでログインします。「Authentication complete. You can return to the application. Feel free to close this browser tab.」と出たらブラウザは閉じてオッケー。

PowerShell側は「Welcome to Microsoft Graph!」と出たら接続完了。実際にはここにMicrosoft365の各種スコープを付けて許可をしてあげる必要があるので、後述のPowerShellコマンドではそれらを付与して実行しています。

図:実行時の初回認証

試しにライセンス剥奪をしてみた

インストールと剥奪用コマンド

PowerShellで試しに特定ユーザに対してライセンス剥奪をしてみようと思い、こちらのサイトを参考にチャレンジ。ここで必要になる情報は特定のユーザのID(メアドではない)と特定ライセンスのSKUID。ユーザのオブジェクトIDはGraph API Explorerで調べて、SKUIDはこちらのサイトの対象ライセンスのGUIDを調べた。

事前にモジュールのインストールが必要です

Install-Module Microsoft.Graph
Import-Module Microsoft.Graph.Users.Actions

どうも、-RemoveLicensesのオプションのみだとエラーが出るので以下のように-AddLicensesは空指定で追加してあげたら、特定ライセンスをユーザから剥がすことができました。

#接続
Connect-MgGraph -Scopes "User.ReadWrite.All", "LicenseAssignment.ReadWrite.All"

# 削除するライセンスの SKU ID
# Power Automate Freeのライセンスを指定
$licenseToRemove = 'f30db892-07e9-47e9-837c-80727f46fd3d'

# 対象ユーザーの ObjectId
$userId = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'

Set-MgUserLicense -UserId $userId -AddLicenses @() -RemoveLicenses @($licenseToRemove) -Verbose

冒頭でライセンス操作に必要なスコープをつけて、licenseToRemoveでSKUIDを指定(カンマ区切りで複数指定できる)。userIdも同様で、最後にSet-MgUserLicenseにて上記のようにAddLicensesとRemoveLicensesを必ず組み合わせて使ってみたところ、追加ライセンスは無し、Power Automate Freeのライセンスを剥がすという形で実行して通りました。

PowerShellではこのようなスタイルでスコープ指定で接続し、特定メソッドで操作みたいなスタイルですがクセがあるようなので、盛大に沼にハマりました。

図:無事にライセンス剥がせた

バージョンによりトラブルが起きることがある

自分の場合、Microsoft.Graphのモジュールは2.25.0を使っていました。しかし、2025年3月時点で2.26.1を使っている人の中には400エラーが出る人がいるようです。構文やskuId, userIdも間違いがなく以下のようなエラーが出るようです。

Set-MgUserLicense : One or more parameters of the operation 'assignLicense' are missing from the request payload. The missing parameters 
are: addLicenses.
Status: 400 (BadRequest)
ErrorCode: Request_BadRequest

そこで、以下の構文でModuleアップデートをしてみてテストをしてみましたが、Windows11 ARM版のPowerShell7で試していますが、自分の環境の場合、問題なく通ってしまった・・・そこで一部を2.26.1にしてみたところ問題発生。どうやらモジュールのバージョンの競合が原因のようだ。

ちなみにライセンス割り当ててないのに剥がそうとすると以下のようなエラーになるので、今回の事例とは違う。

User does not have a corresponding license.  Status: 400 (BadRequest) ErrorCode: Request_BadRequest

GithubのIssueによると、同様の事例が報告されており、このバージョンのバグとして報告されているようで、エラーが出る場合は2.25.0にロールバックして使ってくれということのよう。以下の手順で一旦アンインストールしてバージョン指定でインストールしてみます。バージョンが2.25.0で返ってきたら成功です。事前にモジュールが読み込まれていない状態にするためにPowerShellは再起動してから実行が必要です。

結論を言えば、Versionの競合が起きてるのでv2.25.0で全部合わせてインストールしなさいということのようです。一部のモジュールがなぜかv2.26.1になっていてGraphだけが2.25.0だとエラーになったりするようです。

#モジュールのアンインストール
Uninstall-Module Microsoft.Graph -AllVersions -force
Uninstall-Module Microsoft.Graph.Users.Actions -RequiredVersion 2.26.1
Uninstall-Module Microsoft.Graph.Users -RequiredVersion 2.26.1
Uninstall-Module Microsoft.Graph.Authentication -force

#モジュールの再インストール
install-module Microsoft.Graph -RequiredVersion 2.25.0 -force
Install-Module PowerShellGet -Force
Install-Module Microsoft.Graph.Users -RequiredVersion 2.25.0
Install-Module Microsoft.Graph.Users.Actions -RequiredVersion 2.25.0
Install-Module Microsoft.Graph.Authentication -RequiredVersion 2.25.0

#バージョン確認
Get-InstalledModule Microsoft.Graph.Users
Get-InstalledModule Microsoft.Graph.Users.Actions
Get-InstalledModule Microsoft.Graph.Authentication
Get-InstalledModule -Name Microsoft.Graph

通常時のアップデートの場合は以下の構文。ただしダウングレードはできないので、上記のアンインストールの構文を使う必要があります。

Update-Module -Name Microsoft.Graph -RequiredVersion 2.25.0

この問題に対しての回避策として、Invoke-MgGraphRequestを使っての同様の処理についてはこちらのサイトで提示されています。また、Reddit上でも別の回避策として、AZモジュールをアンインストールしてから、上記のコマンドでモジュールを再度追加すると良いといった情報もあります。

このようにPowerShellでは沼にハマるポイントの1つとしてモジュールのバージョンというものがあったりするので要注意です。

※なんでこんな事が起きるのか?Microsoft.Graphはバージョン指定しないでインストールすると2.25.0がデフォルトで入る。他はバージョン指定しないと2.26.1が入るのが原因の模様。

図:非常に厄介な問題でした。

図:モジュールのバージョンを統一する

PowerShellで調査する

VSCodeで開発する

今回のPowerShellスクリプトの開発は、MicrosoftのVisual Studio Code上で行っています。もはや開発者であれば知らない人はいないであろう、テキストエディタであり実行環境です。

そこに拡張機能としてこれもMicrosoft謹製のPowerShell for Visual Studio Codeをインストールして作成をすると良いでしょう。

図:拡張機能をインストールする

図:ps1ファイルを編集中の様子

Exchange Online

単一のアカウントの情報を取得する

単純に特定の単一アカウントにおけるOutlook上のフォルダ別のメールの数およびサイズを計測するコードを作成してみます。特定のフォルダのIDを取得しそのサブフォルダ内まで含めて探索して、カウントします。

以下はアーカイブフォルダおよびそのサブフォルダ内を探索します。これは以下のメソッドを利用しています。

$global:foldersize = 0

# スコープ指定して接続
Connect-MgGraph -Scopes Mail.ReadBasic

#接続用のアカウント
$userId = "ここに探索する対象のメアドを入れる"

#フォルダ別にアイテム数とサイズを表示する関数
function Get-MessageCountRecursively {
    param(
        [string]$userId,
        [string]$folderId
    )

    # 現在のフォルダ内のメッセージ数を取得
    $count = Get-MgUserMessageCount -UserId $userId -Filter "parentFolderId eq '$folderId'"

    # 現在のフォルダのサイズを取得
    $targetbox = Get-MgUserMailFolder -UserId $userId -MailFolderId $folderId
    $global:foldersize = $global:foldersize + $targetbox.AdditionalProperties.sizeInBytes

    #サブフォルダを取得する
    $subfolders = Get-MgUserMailFolderChildFolder -UserId $userid -MailFolderId $folderId

    #サブフォルダを再帰処理
    foreach ($subfolder in $subfolders) {
        # サブフォルダ内のメッセージ数を再帰的に取得
        $count += Get-MessageCountRecursively -UserId $userId -FolderId $subfolder.Id
        $singlecnt = Get-MessageCountRecursively -UserId $userId -FolderId $subfolder.Id

        #サブフォルダ内のサイズを取得
        $targetbox = Get-MgUserMailFolder -UserId $userId -MailFolderId $subfolder.Id
        $global:foldersize = $global:foldersize + $targetbox.AdditionalProperties.sizeInBytes
    }

    return $count
}

#アーカイブフォルダのIDを取得
$rootFolderId = (Get-MgUserMailFolder -UserId $userid -Filter "displayName eq 'アーカイブ'").Id

#アーカイブおよびそのサブフォルダ内のメールの数を取得
$totalMessageCount = Get-MessageCountRecursively -UserId $userId -FolderId $rootFolderId
Write-Host "アーカイブメール件数: $totalMessageCount"
Write-Host "アーカイブメールサイズ: $($global:foldersize) バイト"

もし、対象ユーザの全フォルダの全件数とサイズの合計を出したい場合には、$rootFolderIdの指定方法を以下のように変更しフォルダを大元のルートフォルダに指定します。受信トレイの親フォルダのIDつまり大元のルートフォルダを指定し、そこを基準に処理をします。

$rootFolderId = (Get-MgUserMailFolder -UserId $userid -Filter "displayName eq '受信トレイ'").parentFolderId
$totalMessageCount = Get-MessageCountRecursively -UserId $userId -FolderId $rootFolderId
Write-Host "全メールサイズ: $global:foldersize"

但しこれでは調査するだけでなおかつ単一ユーザの情報を知ることが出来るだけなので、実際には全ユーザの情報をまとめてCSV出力するなどしなければ実際の現場では活用しにくいです。

rootFolderIdでは-Filterというオプションを付けてdisplayNameが受信トレイとeq(イコール)のものという条件をつけていますが、ここを日付を指定し「特定の日以前 or 以降の受信日のメール」みたいな指定方法が可能です。Redditでこのあたりが詳しく紹介されています。

$DaysRange = (Get-Date).AddDays(-1) 

$time = Get-Date ($DaysRange).ToUniversalTime() -UFormat '+%Y-%m-%dT%H:%M:%S.000Z' 

Get-MgUserMailFolderMessage -All -UserId $user -MailFolderId $myfolder -filter "ReceivedDateTime ge $time"

上記の例では、ReceivedDateTime ge $timeとあり、受信日が$timeの日付より以降(ge)が指定されています。こうすることで指定期間内のメールの件数やサイズに限定することが可能です。geではなくleとすれば以下となるので日付的には指定日より前という表現になります。フィルタについてはこちらに公式ドキュメントがあります。

※但し、この手法は所謂オンライン・アーカイブと呼ばれる受信トレイ内とは別にあるアーカイブフォルダ内のデータは取得できません。

図:メールの件数とサイズを算出できました

まとめて取得しCSVでエクスポートする

mailという名前の列にメアドを縦に列挙しただけのinbox.csvというCSVファイルを用意します。ここに全ユーザを列挙しておきます。今回のコードは共有メールボックスについてはエラーとなり動作しません。

基本的なコードは前述と同じですが、reportというカスタムオブジェクトを用意し、値をどんどん格納していきます。メールアカウントは今回はもう簡単に「Get-MgUserMessageCount -UserId $userId」でざっくり取得します。ファイルサイズだけ関数を使ってサブフォルダ以下まで探索して取得します。

最終的にresult.csvとして出力して完了というコードになります。ちなみに、$PSScriptRootはps1スクリプトの実行されているカレントディレクトリのパスを取得するもので、inbox.csvはこのスクリプトと同じ場所に保存する必要があり、またresult.csvも同じ場所に出力されることになります。

※但し、この手法は所謂オンライン・アーカイブと呼ばれる受信トレイ内とは別にあるアーカイブフォルダ内のデータは取得できません。

図:出力された結果のCSV

$global:foldersize = 0
$report = @() 

#ユーザリストのCSV
$Users = Import-CSV "$PSScriptRoot\inbox.csv"

# スコープ指定して接続
Connect-MgGraph -Scopes Mail.ReadBasic

#フォルダ別にアイテム数とサイズを表示する関数
function Get-MessageCountRecursively {
    param(
        [string]$userId,
        [string]$folderId
    )

    # 現在のフォルダのサイズを取得
    $targetbox = Get-MgUserMailFolder -UserId $userId -MailFolderId $folderId
    $global:foldersize = $global:foldersize + $targetbox.AdditionalProperties.sizeInBytes

    #サブフォルダを取得する
    $subfolders = Get-MgUserMailFolderChildFolder -UserId $userid -MailFolderId $folderId

    #サブフォルダを再帰処理
    foreach ($subfolder in $subfolders) {
        #サブフォルダ内のサイズを取得
        $targetbox = Get-MgUserMailFolder -UserId $userId -MailFolderId $subfolder.Id
        $global:foldersize = $global:foldersize + $targetbox.AdditionalProperties.sizeInBytes
    }

    return $count
}

#ユーザ情報を表示する
foreach($User in $Users) {
    #ユーザアドレスを取得する
    $userId = $User.mail

    #カウンタを初期化
    $global:foldersize = 0

    #ルートフォルダのIDを取得
    $rootFolderId = (Get-MgUserMailFolder -UserId $userid -Filter "displayName eq '受信トレイ'").parentFolderId

    #カウントの実行開始
    $totalMessageCount = Get-MessageCountRecursively -UserId $userId -FolderId $rootFolderId

    #配列データを構築する
    $report += New-Object PSObject -Property $([ordered]@{  
        "UserName" = $userId
        "mailcount" = Get-MgUserMessageCount -UserId $userId
        "mailsize" = $global:foldersize
    }) 
}
  
#CSVでエクスポートする
$report | export-csv $PSScriptRoot\result.csv

オンライン・アーカイブを取得する

前述までの状態は通常のOutlookの個人のデータを取得する場合のPowerShellスクリプトになります。しかし企業の場合はIn-Place Archiveと呼ばれるオンライン・アーカイブと呼ばれるものを用意してる場合があります。デフォルトでは無効なのですが、有効化してる場合、ここにアーカイブしたデータは前述のコードでは取得できません。そこでこのデータを取得してみたいと思います。

オンライン・アーカイブを有効化する

オンライン・アーカイブはExchange管理センターから個別にユーザに対して有効化する必要があります。以下の手順で有効化すると特別なアーカイブスペースが用意されて、Business Basicだと50GBまで用意されます。

  1. Exchange管理センターを開く
  2. 左サイドバーから受信者→メールボックスを開く
  3. 対象のユーザをクリックする
  4. 右サイドバーが出てくるので、その他タブを開く
  5. メールボックスアーカイブの管理をクリックする
  6. メールボックスアーカイブのステータスを有効にチェックする
  7. 名前はこのアーカイブスペースの名前を入れます(例:Archivemanなど)
  8. 保存をクリックする

これで暫くすると、Web版およびローカル版のOutlookに7.の名前でもってオンライン・アーカイブスペースが表示されます。

図:管理センターから有効化可能

In-Place Archiveが表示されない

有効化して暫くしたら表示されるハズなのに表示されないケースがあります。その場合、PowerShellで以下のコマンドを実行して対象ユーザの更新を行い、ユーザは一度ログアウトしてログインし直すと表示されるようになります。

但し、Get-Mailboxメソッドを使うので、事前にモジュールをインストールが必要です。

#モジュールの追加インストールと接続
Install-Module ExchangeOnlineManagement

これで特権管理者アカウントでログインをすると、メソッドが使えるようになります。

その後以下のようなPS1ファイルを作成してPowerShellで実行してみます。-ArchiveNameで指定する名前はコードではIn-Place Archive ユーザ名となっていますが、ここは前述の7.の項目のアーカイブの名前に該当する部分です。

#スコープを指定して接続
Connect-ExchangeOnline

#ユーザのメアドを指定
$user = "更新する対象ユーザのメールアドレス"

#ユーザのメールボックスに接続
Get-Mailbox $user | FL ArchiveName

#設定を変更する
$dn=Get-Mailbox $user | Select -ExpandProperty DisplayName
Set-Mailbox $user -ArchiveName "In-Place Archive -$dn"

図:アーカイブスペースが表示されました。

データを取得する

このIn-Place Archive内のデータを取得するのはちょっとだけ大変です。メールの件数とフォルダサイズを取得するわけなのですが、まずは以下のコマンドで調べてみます。こちらも前述のGet-Mailbox同様にExchangeOnlineManagementモジュールのインストールと、Connect-ExchangeOnlineでの接続が必要です。

Get-MailboxFolderStatistics -Identity "ここに対象のメアド" -Archive | Select-Object Name, FolderPath

すると、以下のスクショのような結果が得られました。onlineは自分が作ったArchive用メール格納フォルダ。削除済みアイテムやらDeletetionsやらは自動で生成されてるものですが、これらは今回の対象からは除外したいです。よって、それらを考慮してメールの件数とファイルサイズを取得する必要があります。

図:余計なフォルダも出てくる

実際にオンラインアーカイブ内のメールの件数およびファイルサイズの合計値を出すスクリプトは以下のようになります。但し、注意点がいくつかあって

  • アーカイブ内の余計なフォルダは除外したいのでexcludeFoldersでリストを作っておく
  • Get-MailboxFolderStatisticsにてArchiveオプションにて情報を取得できます。
  • 但し返ってくる値が 100kb( 100000 bytes)みたいな文字列で返ってくるので正規表現等で文字だけ取り出す
  • 同様にアーカイブ内のフォルダのリストを取得して、今度はメールの件数を合計していく
  • where-objectでは、-andを構文内で使うことで複数の条件を指定可能です。

といったちょっとトリッキーな流れになっています。この流れを関数化しておき、前述でも使ったreportのオブジェクトに結果として含めてCSV出力すると良いでしょう。ただ、ReceivedDateTimeを指定して期間指定してフィルタできるかどうかは不明です(調べた限りではできない。

#スコープ指定して接続
Connect-ExchangeOnline

#対象となるユーザのアドレス
$Mailbox = "ここに対象のユーザのメアドを入れる"

#除外するフォルダを指定
$excludeFolders = @(
    "インフォメーション ストアの先頭",
    "削除済みアイテム",
    "Recoverable Items",
    "Deletions"
)

#対象のフォルダのサイズを取得実行
$text = (Get-MailboxFolderStatistics -Identity $Mailbox -Archive | Where-Object { $_.Name -notin $excludeFolders }).Foldersize

# 正規表現で()内の数字を抽出
$matches = [regex]::Matches($text, "\((.*?)\)")

# 合計値を初期化
$sum = 0

# 抽出された数字を処理
foreach ($match in $matches) {
  # bytesという文字列とカンマを取り除く
  $numberString = $match.Groups[1].Value -replace " bytes|,", ""
  # 数値に変換
  $number = [int]$numberString
  # 合計に加算
  $sum += $number
}

# ファイルサイズ合計値を出力
Write-Host "合計: $sum bytes"

#アーカイブフォルダのリストを取得
$folderStats = Get-MailboxFolderStatistics -Identity $Mailbox -Archive | Where-Object { $_.Name -notin $excludeFolders }

#対象フォルダ内のメールの件数を合計していく
$totalItems = 0
foreach ($folder in $folderStats) {
    $totalItems += $folder.ItemsInFolder
}

#アーカイブ内のメールの件数合計を出力
Write-Host ("合計: {0} 件" -f $totalItems)

図:サイズと件数合計を取得できた

共有メールボックスを取得する

Microsoft365のExchange OnlineはGoogle Workspaceと違い、共有メールボックスという指定のメンバー内で共同利用する特殊なメールボックスがあります。GWS的には共同トレイに該当するもので、特定個人に紐つかない独立したメールボックスです。Exchange管理センターから作成することができます。

この共有メールボックスに来てるメールのアイテム数とファイルサイズを調査したいと思い調べてみると、また違うメソッドを使う必要があるようです。それを元に構築したコードが以下のとおりです。結果は共有メールボックスのアドレス毎にアイテム数とファイルサイズを列挙し、CSVで出力するものになっています。

  • sharedMailboxesの変数に調査する共有メールボックスのアドレスを列挙する
  • 共有メールボックスの調査はGet-EXOMailboxStatisticsを利用して調べることが可能です。
  • 調べた結果は割と素直に、ItemCountとTotalItemSizeの値を整えて取得が可能です。
  • 最後にレポート用オブジェクトに格納し、Export-CsvにてPS1ファイルと同じ場所にCSV出力しています。
# Exchange Online に接続 (未接続の場合)
Connect-ExchangeOnline

# 🔹 ここで調べたい共有メールボックスのアドレスを指定
$sharedMailboxes = @(
    "hogehoge@domain.onmicrosoft.com"
)

# メールボックスサイズとアイテム数を取得
$mailboxData = $sharedMailboxes | ForEach-Object {
  $mailboxStats = Get-EXOMailboxStatistics -Identity $_

  # メールボックスのサイズ(バイト数)とアイテム数
  $sizeBytes = $mailboxStats.TotalItemSize.Value.ToBytes()
  $itemCount = $mailboxStats.ItemCount

  # 結果をカスタムオブジェクトとして出力
  [PSCustomObject]@{
      Mailbox   = $_
      ItemCount = $itemCount
      SizeBytes = $sizeBytes
  }
}

# 🔹 結果をCSVファイルとして出力
$mailboxData | Export-Csv -Path "$PSScriptRoot\mailbox_sizes.csv" -NoTypeInformation -Force

Write-Host "📢 結果を 'mailbox_sizes.csv' に出力しました。"

図:共有メールボックスも出力できました

カレンダーを取得する

単一ユーザのイベント数を取得する

Microsoft365のオブジェクト調査対象はメールだけじゃありません。カレンダーもそういったものの1つです。しかし、カレンダーはユーザに1個だけ用意されてるわけじゃなく、予定表を追加にて自分の個人用の予定表に何個も追加して使い分けしてるのが定石です。これらのカレンダーのイベント数(オブジェクト数)も調査したい所です。

主に利用するメソッドは以下のとおりです。

カレンダー一覧を取得して回し、各カレンダーのイベント数を取得します。一度に100件取得して、ページネーションで分かれるのでその部分で再帰処理を掛けて全オブジェクト数を取得します。

# Microsoft Graph API に接続
Connect-MgGraph -Scopes "Calendars.Read"

# ユーザーIDを設定
$userId = "ここに取得対象のユーザのメールアドレスを入力する"

# すべてのカレンダーを取得
$calendars = Get-MgUserCalendar -UserId $userId

# 全カレンダーのイベントを格納する配列
$allEvents = @()

# 各カレンダーのイベントを取得
foreach ($calendar in $calendars) {
    $calendarId = $calendar.Id
    Write-Host "Processing Calendar: $($calendar.Name)"

    # 最初のページのイベントを取得
    $events = Get-MgUserCalendarEvent -UserId $userId -CalendarId $calendarId -Top 100
    
    # 取得したイベントを格納
    $calendarEvents = @()
    $calendarEvents += $events

    # ページネーション処理(次のページがある場合)
    while ($events.AdditionalData.'@odata.nextLink') {
        $nextLink = $events.AdditionalData.'@odata.nextLink'
        $events = Invoke-MgGraphRequest -Uri $nextLink
        $calendarEvents += $events
    }

    # 取得したイベントを全体のリストに追加
    $allEvents += $calendarEvents
}

# 全イベント数を表示
Write-Host "Total Events Count: $($allEvents.Count)"

図:個人の予定表のデータ全部を対象にする

図:カレンダーのオブジェクト数を取得してみた

全ユーザのイベント数とファイルサイズを取得

前述は単一ユーザのイベント数のみを取得していました。実際の現場では前述にもありましたがユーザリスト(inbox.csv)があって、それら全ユーザ分を取得しつつ、またイベントに付属の添付ファイルのファイルサイズも計測する必要があります(無い場合もある)。

よってこれらを踏まえて、ユーザリストを回しつつイベント数と添付ファイルがあればそのファイルサイズを取得、一覧表にまとめてCSVで出力というものを作成しました。

# Microsoft Graph API に接続
Connect-MgGraph -Scopes "Calendars.Read"

# ユーザーリストのCSVファイルを指定("mail" 列にユーザーのメールアドレスが含まれる)
$userListFile = "$PSScriptRoot\inbox.csv"

# 出力するCSVファイル名
$outputCsv = "$PSScriptRoot\calendar_events.csv"

# CSVファイルからユーザーリストを読み込み
$users = Import-Csv -Path $userListFile | Select-Object -ExpandProperty mail

# 出力用のリスト
$results = @()

# 各ユーザーのカレンダーとイベントを取得
foreach ($userId in $users) {
    Write-Host "Processing User: $userId"

    # すべてのカレンダーを取得
    try {
        $calendars = Get-MgUserCalendar -UserId $userId
    } catch {
        Write-Host "  -> Failed to get calendars for $userId. Skipping..."
        continue
    }

    # ユーザーごとのイベントカウントとファイルサイズ
    $totalEventCount = 0
    $totalFileSize = 0

    # 各カレンダーのイベントを取得
    foreach ($calendar in $calendars) {
        $calendarId = $calendar.Id
        Write-Host "  -> Processing Calendar: $($calendar.Name)"

        # 最初のページのイベントを取得
        try {
            $events = Get-MgUserCalendarEvent -UserId $userId -CalendarId $calendarId -Top 100
        } catch {
            Write-Host "    -> Failed to get events for calendar: $($calendar.Name). Skipping..."
            continue
        }

        # イベントを格納するリスト
        $calendarEvents = @()
        $calendarEvents += $events

        # ページネーション処理(次のページがある場合)
        while ($events.AdditionalData.'@odata.nextLink') {
            $nextLink = $events.AdditionalData.'@odata.nextLink'
            try {
                $events = Invoke-MgGraphRequest -Uri $nextLink
                $calendarEvents += $events
            } catch {
                Write-Host "    -> Failed to retrieve next page of events. Skipping pagination..."
                break
            }
        }

        # イベントカウントを更新
        $totalEventCount += $calendarEvents.Count

        # 各イベントの添付ファイルサイズを取得
        foreach ($event in $calendarEvents) {
            $eventId = $event.Id

            try {
                # 添付ファイルを取得
                $attachments = Get-MgUserEventAttachment -UserId $userId -EventId $eventId

                # 添付ファイルサイズを合計
                foreach ($attachment in $attachments) {
                    if ($attachment.PSObject.Properties["Size"]) {
                        $totalFileSize += $attachment.Size
                    }
                }
            } catch {
                Write-Host "    -> Failed to retrieve attachments for event: $eventId. Skipping..."
            }
        }
    }

    # 結果をリストに追加
    $results += [PSCustomObject]@{
        UserAddress = $userId
        EventCount  = $totalEventCount
        FileSize    = $totalFileSize
    }
}

# CSVファイルに出力
$results | Export-Csv -Path $outputCsv -NoTypeInformation -Encoding UTF8

Write-Host "Processing completed. Results saved to $outputCsv"

図:添付ファイルのサイズ等も含めて計測

図:出力されたCSVファイル

パブリックフォルダを取得する

パブリックフォルダ内のコンテンツはメールやカレンダー、ファイル類と広くいろいろなものがごっちゃになってるかと思います。これらのオブジェクト数を割り出すGet-PublicFolderStatisticsというメソッドがあります。現在研究中。

パブリックフォルダが表示されない

Outlookには特殊な機能として「パブリックフォルダ」というものがあります。これはExchange管理センターから作成できますが、これがユーザに表示されないことがあります。その覚書です。この原因は

  • Exchange管理センターでパブリックフォルダが作成されてるが、アクセス許可を対象ユーザに与えていない
  • ユーザにアクセス許可が与えられてるが、ユーザがOutlook上で自身で追加していない(勝手に表示されるわけじゃないため)

の2点になります。障害としてアクセス許可を削除して再度追加したら出てきたみたいなケースもあります。このパブリックフォルダ内のカレンダーやメール、カレンダー類は特殊な個別のメアドが与えられてるものになるので、共有メールボックスや自身のメアドとはまた違うものになるので要注意です。

図:アクセス許可を追加する

図:Outlook上で追加する

図:表示されるようになりました

スクリプトで取得する

Exchange管理センターで用意してるパブリックフォルダはルートとその下にぶら下がるサブフォルダで構成されています。各フォルダ名、アイテム数合計、ファイルサイズ合計を出すPowerShellのコマンドを作ってみました。ただ、サブフォルダ以下にさらにサブフォルダがある場合を考慮していないので、もしそうした構成の場合は再帰的に処理して孫フォルダ以下もすべて浚うように作り変える必要があります。

#CSVファイルの名前
$ReportOutput = "$PSScriptRoot\publicfolder.csv"

#スコープを指定して接続
Connect-ExchangeOnline

# すべてのパブリックフォルダを取得
$PublicFolders = Get-PublicFolder -Recurse

# 結果を格納するリスト
$FolderStats = @()

# 各パブリックフォルダの内容を取得
foreach ($Folder in $PublicFolders) {
    # パブリックフォルダのパス
    $FolderPath = $Folder.Identity

    # コンテンツ数とサイズを取得
    $Stats = Get-PublicFolderStatistics -Identity $FolderPath

    # 結果を格納
    $FolderStats += [PSCustomObject]@{
        FolderName   = $FolderPath
        ItemCount    = $Stats.ItemCount
        TotalSize    = $Stats.TotalItemSize.ToString()
    }
}

# 結果をCSVとして出力
$FolderStats | Export-Csv -Path $ReportOutput -NoTypeInformation -Encoding UTF8

Sharepoint Online

管理センターから取得する

Exchange Onlineは前述の通り様々な取得対象が存在しており、しかも簡単に取得ができませんでした。しかし、Sharepointの場合にはサイト毎のファイル数やファイルサイズは割と簡単に取得することが可能です。この手法はPowerShellを使わずに手に入れる方法です。

  1. こちらのリンクから自身のテナントのSharepoint管理センターを開く(テナント毎にURLが違うので要注意)
  2. するとアクティブなサイトというページが出てくる
  3. エクスポートをクリックする
  4. CSVでダウンロードされる

CSVの中身を開いてみると、サイト名の他にファイルがファイル数(つまりオブジェクト数)、使用済みストレージ (GB)がファイルサイズ合計となります。Teamsで使われてるかどうか?のフラグなどもあり、これで必要十分と言えば十分です。

図:CSVで簡単に手に入る

スクリプトで取得する

概要

Sharepoint管理センターを使わずにPowerShellにて取得する場合には、Exchange Onlineのように追加のモジュールをインストールしてからスクリプトを実行する必要がありますが、ここハマりポイントが2つありました。

  • SharePoint Online 管理シェルが必要でmacOSではインストールまではできますが、Connect-SPOServiceが動作しませんので接続ができません。
  • Windows11のPowerShell7で上記をインストールしたものの、やはり接続ができない。どうもPowerShell5系でないと動作しないようだ。
  • 公式ではないPnP PowerShellというモジュールもあるようで、こちらはmacOSからも使えるようだ。但しAzureにアプリ登録のコマンドはWindowsのPowerShellからじゃないとできないという罠。
  • そして対応を進めてわかったのが、「SharePoint Online 管理シェル」では標準でファイル数やファイルサイズを取得できないことが判明。故にPnP PowerShellを使うのが正解のようだ。

ということなので、公式ドキュメントにあるまま接続しようとするとエラーに遭遇して先に進めません。

追加モジュールのインストールと接続

ということでSharePoint Online Management Shellではなく、PnP PowerShellを使って取得をしてみることにします。その為には追加でモジュールのインストールが必要です。管理者権限のPowerShellで実行します。

Install-Module -Name PnP.PowerShell -Force -AllowClobber

インストールが完了したら、Azure Portalにアプリの登録をするのですが専用のコマンドが用意されています。

Register-PnPAzureADApp -ApplicationName PnPApp001 -Tenant yourtenant.onmicrosoft.com -Store CurrentUser -Interactive

yourtenantの部分はメアドの@より後ろの部分がそれに該当。これを実行するとログインと承認が求められるので許可するとアプリがAzure Portalに自動的に登録されます。上記コマンドだとすべてのアプリケーション欄にPnPApp001という名前で登録されます。開いてみて、アプリケーション(クライアント)IDの値が必要なので控えておく。

アプリのアクセス許可も自動的にすべて追加されています。

図:Azureにアプリ登録中

図:クライアントID等が必要

全サイトの情報を取得する

Azure Portalまで使ってClient IDを手に入れてようやくと思いきや、このコードが結構複雑。Sharepoint管理センターのような一覧がパッと取れるわけじゃないという。

  • サイトのドキュメント内のものだけを対象に検索します。
  • 直下→サブフォルダ内まで再帰的に処理してファイル件数とサイズを取得していかなければならない。
  • 量が多いと429エラーが起きる可能性がある(回避策はこちらのサイト
  • 再帰処理をさせる為に関数に切り出しておく箇所がある
  • サイトのURL毎にConnect-PnPOnlineで接続しなおさないときちんとデータが取れません。
  • /sites/以外のURLも含まれてくるのでこれらは処理対象外として除外する

かなり苦労して、結局はSharepoint管理センターのCSV一発で取れるものと殆ど同じなので、ちょっと使い勝手が悪いなぁと思う。別の使い方(フォルダの階層構造をしらべる等)では応用して使えるかもといったところです。

# CSVファイルの出力先パスを指定
$csvFilePath = "$PSScriptRoot\sharepoint_site_info.csv"

#認証情報
$clientId = "ここにクライアントIDを入れる"

# すべてのサイトコレクションを取得
$sites = Get-PnPTenantSite

# 結果を格納する配列を初期化
$results = @()

# 再帰的にフォルダ内のファイルを取得する関数
function Get-FilesInFolder {
    param (
        [Parameter(Mandatory=$true)]
        [string]$FolderServerRelativeUrl,

        [Parameter(Mandatory=$true)]
        [object]$List
    )

    # フォルダ内のアイテムを取得(ファイルとサブフォルダ)
    $items = Get-PnPListItem -List $List -FolderServerRelativeUrl $FolderServerRelativeUrl

    foreach ($item in $items) {
        if ($item.FileSystemObjectType -eq "File") {
            # ファイル数をカウント
            $global:totalFileCount++
            # ファイルサイズを加算
            $global:totalFileSize += $item["File_x0020_Size"]
        } elseif ($item.FileSystemObjectType -eq "Folder") {
            # サブフォルダがある場合、その中も再帰的に処理
            Get-FilesInFolder -FolderServerRelativeUrl $item["FileRef"] -List $List
        }
    }
}

# 各サイトコレクションを処理
foreach ($site in $sites) {
    Write-Host "サイトコレクション: $($site.Url) を処理中..."

    # "/sites/" を含むURLのみ処理
    if ($site.Url -match "/sites/") {
        #サイトごとに接続しなおす必要がある(でないとドキュメントが出てこない)
        Connect-PnPOnline -Url $site.Url -ClientId $clientId -Interactive

        # すべてのサイトを取得
        $webs = Get-PnPWeb -Includes Title, Url, ServerRelativeUrl, Lists

        foreach ($web in $webs) {
            Write-Host "  サイト: $($web.Title) を処理中..."

            # ファイル数とファイルサイズを初期化
            $global:totalFileCount = 0
            $global:totalFileSize = 0

            # 各リストを処理
            foreach ($list in $web.Lists) {
                # "ドキュメント"ライブラリのみを処理
                if ($list.BaseType -eq "DocumentLibrary" -and $list.Title -eq "ドキュメント") {
                    Write-Host "    リスト: $($list.Title) を処理中..."

                    # サイトのServerRelativeUrlを使ってShared Documentsフォルダの相対URLを組み立て
                    $rootFolderServerRelativeUrl = $web.ServerRelativeUrl + "/Shared Documents"

                    # ルートフォルダ内のファイルとサブフォルダを再帰的に取得
                    Get-FilesInFolder -FolderServerRelativeUrl $rootFolderServerRelativeUrl -List $list
                }
            }

            # 結果を配列に追加
            $results += [PSCustomObject]@{
                SiteUrl = $site.Url
                FileCount = $global:totalFileCount
                TotalFileSizeMB = $global:totalFileSize  # ファイルサイズをMBに変換
            }
        }

    } else {
        Write-Host "  サイトURL ($($site.Url)) は処理対象外です。"
    }
}

# 結果をCSVファイルに出力
$results | Export-Csv -Path $csvFilePath -NoTypeInformation

OneDrive Business

Microsoft365のOneDriveはURLを見るとわかるのですが、SharepointとしてのURLが表示されています。つまり仕組みはSharepointと同じで個人向けのサイトとして用意してるものと言えます。

Sharepointと似たようなものであるため、前述のSharepointのデータ取得法で出来ないか?現在研究中です。

管理センターから取得する

Sharepointの場合同様にOneDriveについても管理センターからユーザ別のファイル数とファイルサイズの合計がCSVで簡単に入手することが可能です。但しSharepoint管理センターではなくMicrosoft365管理センターからの入手になります。

  1. ここのリンクからMicrosoft365管理センターに入る
  2. 左サイドバー一番下の「すべてを表示」をクリックする
  3. 左サイドバーのレポート→利用状況をクリックする
  4. 新たな画面で左サイドにOneDriveがいるのでクリックする
  5. 利用状況タブが開かれていますが、右パネルの下に一覧が表示されています。但しユーザはメアドや名前じゃなく数値文字のIDのようなものとして表記されています。
  6. 一覧表を右スクロールしていくと、列の選択というのが出てくるのでクリックする
  7. 未チェックのものがあるので全部チェックを入れて保存をクリック
  8. ファイルやサイズなども表示されていたら、上部にある「エクスポート」をクリックする
  9. CSVがダウンロードされる

CSVの中の「File Count」がファイル数、Storage Used (Byte)が「ファイルサイズ(bytes)」となります。

図:利用状況レポートから出力する

図:CSVを覗いてみる

スクリプトで取得する

基本的にはSharepoint Onlineの時と同様ですが多少異なる点もあります。

  • クライアントIDが必要な点はSharepointと同様です
  • PnP PowerShellを使って取得する点も同様です。
  • yourtenantの部分についても同様です。書き換えが必要です。
  • 初回の接続時およびここのユーザの情報を取得時の2回、認証が必要になるため注意が必要です。
  • ファイル数のカウントの為にGet-FileCount関数を作成し再帰的に処理をしています。
  • 最後にCSV出力をして完成です
#接続するテナントの
$TenantAdminURL = "https://yourtenant-admin.sharepoint.com"

#出力するCSVのパスと名前
$ReportOutput = "$PSScriptRoot\OneDriveUsage.csv"

#認証情報
$clientId = "ここにクライアントIDを入れる"

#PnPOnlineでOneDriveに接続する
Connect-PnPOnline -Url $TenantAdminURL -ClientId $clientId -Interactive

#すべてのユーザのOneDriveリストを取得する
$OneDriveSites = Get-PnPTenantSite -IncludeOneDriveSites -Filter "Url -like '-my.sharepoint.com/personal/'" -Detailed

#レポート用のオブジェクト
$UsageData = @()

#ユーザのOneDriveのファイル数をサブフォルダまで含めて再帰的に取得する
function Get-FileCount($folderPath) {
    $count = 0
    
    #すべてのフォルダのリストを取得する
    $files = Get-PnPListItem -List "Documents" -PageSize 1000 | Where-Object { $_.FieldValues.FileRef -like "$folderPath/*" }
    $count += $files.Count
    
    #サブフォルダを取得する
    $folders = Get-PnPFolderItem -FolderSiteRelativeUrl $folderPath -ItemType Folder
    
    foreach ($folder in $folders) {
        $count += Get-FileCount $folder.ServerRelativeUrl
    }
    
    return $count
}

#ユーザのOneDriveリストを回す
ForEach($Site in $OneDriveSites)
{
    Try {
        Write-host "探索サイト:"$Site.URL -f Yellow

        # OneDrive サイトに接続
        Connect-PnPOnline -Url $Site.URL -ClientId $clientId -Interactive

        #対象ユーザのOneDriveのファイル数合計を取得する
        $TotalFiles = Get-FileCount "Documents"

        #出力用のレポートを作成する
        $UsageData += [PSCustomObject][ordered]@{
            SiteName         = $Site.Title
            Owner            = $Site.owner
            UsedSpaceMB      = $Site.StorageUsageCurrent
            TotalFiles       = $TotalFiles
        }
    }
    Catch {
        write-host "エラー: $($_.Exception.Message)" -foregroundcolor Red
    }
}

#CSVで出力する
$UsageData | Format-table
$UsageData | Export-Csv -Path $ReportOutput -NoTypeInformation

図:無事に全ユーザのOneDriveの情報を取得できた

連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)