Garoon 6.17のメッセージをコメント込みで取得する――REST APIとSOAP APIの併用
パッケージ版Garoonの回覧スレッドを、コメントまで含めてMarkdownへ出力する仕組みを作りました。
管理者権限は使っていません。情シスへの設定変更依頼も行わず、自分のGaroonアカウントだけで完結させています。
最初は「Garoon 6.17でメッセージのREST APIが追加されたなら、RESTだけで取れるだろう」と考えていました。
ところが実際に試してみると、REST APIで取得できるのはメッセージ本体まででした。コメントを取得するには、以前から提供されているSOAP APIも併用する必要があります。
この記事では、実装中につまずいたポイントを、実際に確認した順番で紹介します。
なお、今回Markdownへ出力する対象は次のとおりです。
- メッセージの標題
- メッセージの本文
- 宛先
- 現時点で閲覧できるコメントのプレーンテキスト
- コメントの投稿者と投稿日時
添付ファイルや、コメントの書式情報である html_text は対象外としています。
環境
- パッケージ版 Garoon 6.17.0
- Linux環境
- インストール識別子は既定の
cbgrn - Windows 10
- Windows PowerShell 5.1
- 実行アカウントは一般ユーザー
実行アカウントは、Garoonのアプリケーション管理者でも、cybozu.com共通管理者でもありません。
以降、ホスト名は garoon.example.local、メッセージIDは 123456 と表記します。
結論を先に
メッセージ本体とコメントを取得する場合、今回利用したAPIは次のとおりです。
| 欲しいもの | 手段 | 管理者権限 |
|---|---|---|
| 標題・本文・宛先・更新日時 | REST GET /api/v1/message/messages/{id}
|
不要 |
| コメント | SOAP MessageGetFollows
|
不要※ |
| 閲覧できるメッセージの検索 | SOAP MessageSearchThreads
|
不要※ |
| メッセージの更新情報 | SOAP MessageGetThreadVersions
|
不要※ |
| 管理者視点のメッセージ一覧 | REST GET /api/v1/message/admin/messages
|
必要 |
※実行ユーザーが差出人または宛先に含まれている範囲です。
今回の実装では、REST APIでメッセージ本体を取得し、SOAP APIの MessageGetFollows でコメントを取得しました。
一般ユーザー向けのSOAP APIには、メッセージを検索したり更新情報を取得したりするAPIもあります。
ただし、今回は監視したいスレッドがあらかじめ決まっていたため、メッセージIDを設定ファイルで管理する方式にしています。
1. メッセージのREST APIは6.17から
Garoon REST APIは、長らくスケジュールやワークフローが中心で、メッセージは取得対象に含まれていませんでした。
パッケージ版Garoon 6.17から、メッセージを取得するREST APIが利用できるようになっています。
Linux環境のURLは、次の形式です。
GET http://garoon.example.local/cgi-bin/cbgrn/grn.cgi/api/v1/message/messages/123456
Windows環境の場合は、次の形式になります。
GET http://garoon.example.local/scripts/cbgrn/grn.exe/api/v1/message/messages/123456
今回はパスワード認証を使用しました。
X-Cybozu-Authorization ヘッダーには、次の文字列をBase64エンコードして設定します。
ログイン名:パスワード
PowerShellでは、次のように生成できます。
$pair = '{0}:{1}' -f $loginName, $password
$b64 = [Convert]::ToBase64String(
[Text.Encoding]::UTF8.GetBytes($pair)
)
自分の環境が6.17以降かどうかは、Garoon画面のフッターに表示されるバージョンで確認できます。
6.16以前の場合はこのREST APIを利用できないため、メッセージ本体もSOAP APIで取得する必要があります。
2. X-Requested-With がないと401になりました
最初のリクエストは、HTTP 401で失敗しました。
パスワードを間違えたと思い、何度か入力し直したのですが、原因は認証情報ではありませんでした。
エラーレスポンスの本文には、次のように書かれていました。
{
"error": {
"errorCode": "GRN_REST_API_00002",
"message": "認証に失敗しました。",
"cause": "「X-Requested-With」ヘッダーが指定されていません。"
}
}
検証環境では、X-Requested-With ヘッダーの指定が必要でした。
$headers = @{
'X-Cybozu-Authorization' = $b64
'X-Requested-With' = 'XMLHttpRequest'
}
Invoke-RestMethod `
-Uri $uri `
-Method Get `
-Headers $headers
ブラウザのアドレスバーへURLを直接入力しても、このヘッダーは付きません。そのため、認証済みのブラウザであっても、URLの直打ちでは確認できませんでした。
ここで学んだのは、GaroonのAPIエラーでは、HTTPステータスだけでなくレスポンス本文も読む必要があるということです。
GRN_ で始まるエラーコードだけでなく、cause に具体的な原因が書かれている場合があります。
PowerShellでは、例外発生時にレスポンス本文を読み出す処理を最初から用意しておくと、調査しやすくなります。
try {
Invoke-RestMethod `
-Uri $uri `
-Method Get `
-Headers $headers
}
catch {
$response = $_.Exception.Response
if ($null -ne $response) {
$stream = $response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($stream)
try {
$errorBody = $reader.ReadToEnd()
Write-Error $errorBody
}
finally {
$reader.Dispose()
$stream.Dispose()
}
}
else {
throw
}
}
3. REST APIの body にコメントは含まれません
REST APIが通ったため、レスポンスの内容を確認しました。
標題 : (回覧のタイトル)
更新日時 : 2026-07-02T14:04:48Z
宛先件数 : 15
本文文字数: 922
本文は922文字でした。
しかし、Garoonの画面で同じスレッドを開くと、500件以上のコメントが付いています。
REST APIのレスポンスには、次のようなプロパティが含まれています。
titlebodyrecipientsfolderscreatedAtupdatedAt
一方で、コメントに相当するプロパティはありません。
つまり、body はあくまでメッセージ本体の本文であり、コメント欄の内容は含まれません。
今回対象にした回覧スレッドは、本文よりもコメント欄で議論が進んでいました。そのため、REST APIだけでは、必要な情報の大半を取得できない状態でした。
また、検証環境では、コメントを追加してもREST APIの updatedAt はコメントの投稿日時にはなりませんでした。
このため、updatedAt だけを見て「スレッドに新しいコメントがあるか」を判断する設計にはしていません。
4. コメントはSOAP APIの MessageGetFollows で取得します
コメントは、SOAP APIの MessageGetFollows で取得できます。
このAPIは、パッケージ版Garoon 3.0以降で利用できます。
重要なのは、APIを実行するユーザーが差出人または宛先に含まれているメッセージのコメントを取得する仕様になっていることです。
自分が閲覧できるメッセージであれば、コメントを取得するために管理者権限を用意する必要はありません。
主なパラメーターは次の3つです。
| パラメーター | 内容 |
|---|---|
thread_id |
メッセージID |
offset |
コメントの取得開始位置 |
limit |
コメントの取得上限数 |
SOAPリクエストは次のようになります。
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
<soap:Header>
<Action>MessageGetFollows</Action>
<Security>
<UsernameToken>
<Username>LOGIN_NAME</Username>
<Password>PASSWORD</Password>
</UsernameToken>
</Security>
<Timestamp>
<Created>2026-07-28T06:00:00Z</Created>
<Expires>2026-07-28T06:10:00Z</Expires>
</Timestamp>
<Locale>ja</Locale>
</soap:Header>
<soap:Body>
<MessageGetFollows>
<parameters
thread_id="123456"
offset="0"
limit="100">
</parameters>
</MessageGetFollows>
</soap:Body>
</soap:Envelope>
送信時のContent-Typeには、次を指定しました。
text/xml; charset=UTF-8
SOAPエンベロープの名前空間には、次を指定します。
http://www.w3.org/2003/05/soap-envelope
検証中に異なる名前空間を指定したところ、GRN_UTIL_API_65001 が返りました。
認証方法には、主に次の選択肢があります。
- WS-Security認証
- Cookie認証
今回は、SOAPヘッダーにログイン名とパスワードを入れるWS-Security認証を使用しました。
同じセッションで多数のAPIを実行する場合は、ログインAPIでCookieを取得し、再利用する方法も検討できます。
なお、クラウド版では、2要素認証を設定したcybozu.comユーザーはWS-Security認証を利用できないという制限があります。パッケージ版の仕様と混同しないよう、注意が必要です。
5. パッケージ版のSOAPエンドポイントは .csp ではありませんでした
公式ドキュメントのリクエスト例には、クラウド版のSOAPエンドポイントとして次のようなURLが記載されています。
/g/cbpapi/base/api.csp
これをそのままパッケージ版のURLへ置き換え、次のようにアクセスしました。
http://garoon.example.local/cgi-bin/cbgrn/grn.cgi/cbpapi/message/api.csp
結果は、HTTP 404でした。
No input file specified.
検証環境での正しいエンドポイントは、末尾が api? で、.csp は付きませんでした。
http://garoon.example.local/cgi-bin/cbgrn/grn.cgi/cbpapi/message/api?
URLを推測するより、WSDLを確認した方が確実でした。
http://garoon.example.local/cgi-bin/cbgrn/grn.cgi?WSDL
このWSDLには、各モジュールの soap:address が含まれています。
PowerShellで一覧を抜き出しました。
$wsdlUri = 'http://garoon.example.local/cgi-bin/cbgrn/grn.cgi?WSDL'
$response = Invoke-WebRequest `
-Uri $wsdlUri `
-UseBasicParsing
$raw = [Text.Encoding]::UTF8.GetString(
$response.RawContentStream.ToArray()
)
[regex]::Matches($raw, 'location="([^"]+)"') |
ForEach-Object {
$_.Groups[1].Value
} |
Sort-Object -Unique
検証環境では、次のようなエンドポイントを確認できました。
cbpapi/address/api?
cbpapi/base/api?
cbpapi/bulletin/api?
cbpapi/cabinet/api?
cbpapi/mail/api?
cbpapi/message/api?
cbpapi/notification/api?
cbpapi/report/api?
cbpapi/schedule/api?
cbpapi/star/api?
cbpapi/workflow/api?
util_api/util/api?
sysapi/admin/api?
他のSOAP APIを利用するときも、まずWSDLの soap:address を確認した方が早そうです。
6. コメント本文は子要素ではなく属性に入っていました
SOAP APIからレスポンスは返ったものの、最初に書いたコードではコメント本文が空になりました。
text という子要素を探していたことが原因です。
実際のレスポンスは、次のような構造でした。
<follow
id="614318"
number="510"
text="コメント本文がここに入る
改行もここ"
xmlns:flw="http://schemas.cybozu.co.jp/message/2008">
<flw:creator
user_id="5457"
name="投稿者名"
date="2026-07-28T03:10:08Z" />
</follow>
コメント本文は、follow 要素の text 属性に入っています。
書式編集が使われているコメントでは、HTML表現が html_text 属性に入る場合もあります。
今回のMarkdown出力では、書式情報を使わず、text 属性のみを取得しています。
改行は、次のような文字参照で格納されていました。


XMLパーサーを通して属性を取得すれば、PowerShell側では改行を含む文字列として扱えます。
投稿者と投稿日時は、creator 要素に入っています。
XML名前空間をコードへ固定したくなかったため、XPathでは local-name() を使いました。
$nodes = $xml.SelectNodes(
"//*[local-name()='follow']"
)
$comments = foreach ($node in $nodes) {
$creator = $node.SelectSingleNode(
".//*[local-name()='creator']"
)
[pscustomobject]@{
Number = [int]$node.GetAttribute('number')
Text = $node.GetAttribute('text')
Who = $creator.GetAttribute('name')
DateZ = $creator.GetAttribute('date')
}
}
投稿日時は、ISO 8601形式のUTCで返ってきました。
JSTへ変換するときは、文字列へ単純に9時間を足すのではなく、DateTimeOffset として解釈しています。
$dateUtc = [DateTimeOffset]::Parse($comment.DateZ)
$dateJst = $dateUtc.ToOffset(
[TimeSpan]::FromHours(9)
)
これなら、UTCとJSTの区別を保ったまま扱えます。
7. コメントの差分管理には number を使いました
各 follow には、コメントの連番である number が付いています。
例えば、最後に出力したコメントが510番であれば、次回は511番以降だけをMarkdownへ追加すればよいことになります。
そのため、コメント本文のハッシュ比較や、投稿日時の文字列比較は行いませんでした。
状態ファイルには、メッセージIDごとの最終 number を保存しています。
{
"123456": {
"lastNumber": 510,
"lastExportedAt": "2026-07-28 15:42"
}
}
検証環境では、MessageGetFollows のレスポンスは number の降順で返ってきました。公式ドキュメントのレスポンス例も、同じ並びになっています。
ただし、コード側では返却順に依存しないよう、出力前に number で並べ替えています。
$newComments = $comments |
Where-Object {
$_.Number -gt $lastNumber
} |
Sort-Object Number
コメントは、offset と limit を使って複数回に分けて取得しました。
$offset = 0
$limit = 100
$all = @()
while ($true) {
$page = Get-GaroonComments `
-ThreadId $threadId `
-Offset $offset `
-Limit $limit
$all += $page
if ($page.Count -lt $limit) {
break
}
$offset += $page.Count
}
検証では、約500件のコメントを100件ずつ取得しました。
5ページ分の取得に加えて、終端を確認するリクエストが1回発生したため、合計6リクエスト、約18秒でした。
取得中に新しいコメントが追加される可能性もあるため、最終的な重複排除と差分判定は number を基準にしています。
8. RESTの管理者向け一覧APIは一般ユーザーでは使えません
どのメッセージが更新されたかをREST APIで取得する場合、次の管理者向けAPIが候補になります。
GET /api/v1/message/admin/messages
このAPIでは、更新日時の範囲を指定してメッセージを取得できます。
ただし、利用するには次のいずれかの権限が必要です。
- cybozu.com共通管理者
- メッセージのアプリケーション管理者
一般ユーザーで実行すると、検証環境では次のエラーになりました。
GRN_CMMN_00005
実行権限がないことを示すエラーです。
一方で、SOAP APIには、一般ユーザーが利用できる検索・更新情報取得用のAPIもあります。
MessageSearchThreads
検索文字列と検索対象を指定し、実行ユーザーが差出人または宛先に含まれているメッセージを検索できます。
検索対象には、次を指定できます。
- 標題
- 本文
- 差出人
- 宛先
- コメント
ただし、検索文字列を条件にするAPIなので、「コメントが追加されたすべてのスレッドを定期的に列挙する」という用途とは少し異なります。
MessageGetThreadVersions
メッセージIDとバージョン、取得期間を指定し、メッセージの更新情報を取得できます。
ただし、今回の実装時点では、コメントだけが追加された場合にメッセージのバージョンがどのように変化するかまでは、十分に検証できていません。
コメントの取りこぼしを避けるため、今回の仕組みでは、監視対象のメッセージIDを設定ファイルへ明示的に記載しました。
[
{
"code": "A001",
"site": "拠点A",
"mid": "123456"
},
{
"code": "A002",
"site": "拠点B",
"mid": "123457"
}
]
対象は13件です。
数十件程度で、監視したいスレッドも決まっているのであれば、この方式でも運用上の負担はそれほど大きくありません。
反対に、全社のメッセージを自動的に列挙して横断取得したい場合は、管理者向けREST APIの利用や、SOAP APIを使った検索・更新検知について、追加の検証が必要になります。
なお、パッケージ版では、common.ini の [API roles] セクションによって、APIを実行できるロールが制限されている場合があります。
一般ユーザー向けのAPIであっても GRN_CMMN_00005 が返る場合は、Garoonサーバー側でAPI実行ロールが制限されている可能性があります。
この設定は一般ユーザーから変更できないため、必要に応じて管理者へ確認する必要があります。
付録1:Windows PowerShell 5.1とcmd.exeの文字コード
GaroonのAPIとは直接関係ありませんが、日本語を含むPowerShellスクリプトをWindows PowerShell 5.1で動かす際、文字コードでも何度かつまずきました。
.ps1 はUTF-8 BOM付きで保存しました
Windows PowerShell 5.1では、BOMなしUTF-8の .ps1 が期待どおりに解釈されない場合があります。
日本語コメントや文字列が文字化けし、その結果として引用符の対応まで崩れ、無関係に見える場所で構文エラーが発生しました。
式が '(' の後に必要です。
発生場所 ...\script.ps1:121 文字:56
このようなエラーが数十行表示されましたが、実際の原因は、より前に書かれていた日本語コメントの文字コードでした。
そのため、Windows PowerShell 5.1で実行する .ps1 は、UTF-8 BOM付きで保存することにしました。
.cmd はShift-JISで保存しました
一方、バッチファイルへUTF-8 BOMを付けると、cmd.exe が1行目のBOMをコマンドの一部として扱うことがあります。
@echo off
が、実質的に次のように解釈されます。
@echo off
PowerShellスクリプトとバッチファイルでは、安定して扱える文字コードが異なりました。
今回の環境では、次のように分けています。
| ファイル | 文字コード |
|---|---|
.ps1 |
UTF-8 BOM付き |
.cmd |
Shift-JIS |
スクリプトの配置場所を基準にパスを組み立てます
$PSScriptRoot は、PowerShell 3.0以降で、実行中のスクリプトが置かれているディレクトリを示します。
そのため、PowerShell側では基本的に $PSScriptRoot を基準に、設定ファイルなどのパスを組み立てました。
$configPath = Join-Path `
$PSScriptRoot `
'config.json'
一方、バッチファイルを実行したときのカレントディレクトリは、バッチファイルの配置場所と一致するとは限りません。
そのため、呼び出し側では %~dp0 を使い、バッチファイルの配置場所を基準にPowerShellスクリプトを呼び出しました。
@echo off
powershell.exe ^
-NoProfile ^
-ExecutionPolicy Bypass ^
-File "%~dp0worker.ps1"
PowerShell側には、対話実行などの呼び出し方も考慮して、フォールバックを残しています。
$selfDir = if ($PSScriptRoot) {
$PSScriptRoot
}
elseif ($MyInvocation.MyCommand.Path) {
Split-Path `
-Parent `
$MyInvocation.MyCommand.Path
}
else {
(Get-Location).Path
}
通常の .ps1 実行では $PSScriptRoot を利用し、それ以外の呼び出し方でも、可能な範囲で動くようにしています。
付録2:資格情報の保存
X-Cybozu-Authorization に設定する値は、ログイン名とパスワードをBase64エンコードしたものに過ぎません。
Base64は暗号化ではないため、値を取得されれば簡単に元へ戻せます。
SOAPのWS-Security認証でも、リクエスト内にログイン名とパスワードが含まれます。
そのため、資格情報をPowerShellスクリプトへ直接記述することは避けました。
WindowsのDPAPIを利用し、実行ユーザーとマシンに紐付いた暗号化ファイルとして保存しています。
# 初回のみ実行します
Get-Credential |
Export-Clixml `
"$env:USERPROFILE\.garoon_cred.xml"
以降は、次のように読み込みます。
$cred = Import-Clixml `
"$env:USERPROFILE\.garoon_cred.xml"
$loginName = $cred.UserName
$password = $cred.GetNetworkCredential().Password
Windows上で出力された資格情報は、原則として同じユーザー、同じマシンのコンテキストで復号されます。
そのため、ファイルだけが別の端末へ持ち出された場合のリスクを抑えられます。
ただし、これはローカルへ保存する資格情報を保護する仕組みであり、通信経路を暗号化するものではありません。
今回のGaroon環境は平文HTTPだったため、次の情報は通信経路上で暗号化されません。
- ログイン情報
- メッセージ本文
- コメント本文
- 宛先や投稿者情報
DPAPIを使用しても、この通信上のリスクは解消されません。
本来は、Garoonへのアクセス経路をHTTPS化することが望ましいです。
タスクスケジューラで実行する場合は、資格情報ファイルを作成したユーザーと、タスクの実行ユーザーを一致させる必要があります。
また、実際のタスク実行条件で Import-Clixml が成功することを、事前に確認しておいた方が安全です。
今回の環境では、ユーザープロファイルとDPAPIの利用条件を確実にするため、「ユーザーがログオンしているときのみ実行する」を選択しました。
静かに失敗する自動化は、元の問題を先送りにするだけでした
この仕組みを作った理由は、回覧スレッドに書かれた課題を見落とさないためでした。
ところが定期実行へ移すと、別の問題が発生します。
ジョブが失敗しても、何も出力されません。
例えば、次のような場合です。
- 社内ネットワークへ接続していない
- Garoonのパスワードを変更した
- PCがスリープしていた
- 資格情報の復号に失敗した
- スクリプトや設定ファイルが移動された
- Garoon側のAPI設定が変更された
何も起きないため、失敗したこと自体に気づけません。
そして2週間後、古いMarkdownを最新データだと思って読むことになります。
これでは、見落としが見えない場所へ移動しただけです。
そのため、出力先の直下へ、実行状態を示すファイルを1つ置きました。
---
lastRun: 2026-07-28 15:42
lastRunResult: success
lastSuccess: 2026-07-28 15:42
durationSeconds: 214
---
それぞれの意味は次のとおりです。
| 項目 | 内容 |
|---|---|
lastRun |
最後に処理を開始した日時 |
lastRunResult |
最後の実行結果 |
lastSuccess |
最後に正常終了した日時 |
durationSeconds |
処理にかかった秒数 |
lastSuccess が2日以上前であれば、Markdownへ警告文を出すようにしています。
出力したMarkdownは、LLMに読ませることも想定しています。
そのため、状態ファイルには次の趣旨の文章も含めました。
lastSuccessが古い場合、この資料は最新ではない可能性があります。
回答するときは、情報が古い可能性を明示してください。
これにより、取得処理が止まっている状態で、LLMが古い情報を最新情報として断定する可能性を下げています。
また、検証中には、本体のPowerShellスクリプトが存在しない状態でバッチファイルを実行してしまったことがありました。
存在チェックがなければ、後続処理によっては、何も実行していないにもかかわらず、正常終了したように見える可能性があります。
そのため、呼び出し側にも明示的な存在チェックを入れました。
@echo off
setlocal
set "SCRIPT=%~dp0worker.ps1"
if not exist "%SCRIPT%" (
echo [ERROR] PowerShellスクリプトが見つかりません。
echo %SCRIPT%
exit /b 1
)
powershell.exe ^
-NoProfile ^
-ExecutionPolicy Bypass ^
-File "%SCRIPT%"
if errorlevel 1 (
echo [ERROR] 処理に失敗しました。
exit /b 1
)
echo 処理が完了しました。
exit /b 0
無人実行で注意したいのは、明確なエラーだけではありません。
何も実行していないのに、成功したように見える状態の方が発見しにくいです。
取得処理そのものだけでなく、次の点も含めて設計する必要があります。
- 最後に実行されたのはいつか
- 最後に成功したのはいつか
- 失敗したことを利用者が確認できるか
- 出力されたデータが古いと判断できるか
まとめ
パッケージ版Garoon 6.17では、REST APIでメッセージ本体を取得できます。
ただし、コメントはREST APIのレスポンスに含まれません。
コメントまで取得するには、SOAP APIの MessageGetFollows を併用する必要がありました。
今回の構成は次のようになっています。
設定ファイル
└─ 監視対象のメッセージID
REST API
└─ 標題・本文・宛先などを取得
SOAP API
└─ コメントをページングして取得
状態ファイル
└─ メッセージごとの最終コメント番号を保存
Markdown
└─ 本文と新規コメントを出力
死活記録
└─ 最終実行・最終成功日時を記録
今回、特に重要だったのは次の点です。
- REST APIの
bodyにコメントは含まれない - コメントはSOAP APIの
MessageGetFollowsで取得する - パッケージ版のSOAPエンドポイントはWSDLで確認する
- コメント本文は
followのtext属性に入っている - 差分管理にはコメントの
numberを利用できる - 一般ユーザー向けSOAP APIと管理者向けREST APIを区別する
- 資格情報の保管と通信経路の保護は分けて考える
- 自動化では、データが古いことを検知できる仕組みも用意する
最初は、コメントを取得してMarkdownへ書き出すだけの、小さなスクリプトを作るつもりでした。
実際に運用できる形まで持っていくには、APIの呼び出しよりも、権限、文字コード、資格情報、差分管理、失敗検知の方に多くの時間がかかりました。
それでも、その部分まで用意しておかないと、「動くスクリプト」にはなっても、「安心して任せられる仕組み」にはなりません。
APIを呼び出せた時点では、まだ半分くらいだったのだと思います。