📨

サイボウズ Garoon API を C# から使ってみた

2020/12/26に公開

ガルーンとは

サイボウズ社のGaroon(ガルーン) は、主に中堅~大企業をターゲットにしたグループウェアです。
電子メールの機能はあるもののSMTPサーバを持たないため、外部プログラムから社内メールを送信するにはGaroonのWeb APIを叩く必要がありました。

.NETアプリ(C#)でガルーンから社内メールを一括送信させてみたので、開発手順を紹介します。

Garoon API について

Garoon APIでは、従前からあるSOAPと、新しいRESTの両方をサポートします。
詳細は cybozu developer network 内の公式ドキュメントを参照して下さい。
https://developer.cybozu.io/hc/ja/articles/360039875971

SOAPとRESTの違いについて簡単に触れておきます。

SOAPは、Webサービスの初期に定義された古くからあるプロトコル(規格)で、HTTPのPOSTメソッドでXMLデータをやり取りします。WSDLと呼ばれるインターフェース仕様(XMLで記述されている)から、ネイティブAPIのコードを自動生成して使うことが多いです。ステートフルです。JavaやC#と相性が良く、WebサービスといえばSOAPといわれるくらい昔は主流でした。

RESTは、プロトコルというより概念とか設計思想に近いです。HTTPのGET/POST/PUT/DELETEメソッドで主にjsonデータをやり取りします。フォーマットが厳密に決まっているわけでは無いです。ステートレスです。実装がシンプルなので容易に始められます。

APIというと、筆者はRPCとかCORBAを叩きまくっていた世代ですが、今日、たんにAPIといえばRESTを指すことが一般的です。

開発手順

GaroonのREST APIは基本的にクラウド版しか提供されないので、本記事ではSOAP APIで作成していきます。

開発環境

  • Visual Studio 2019(C#)
  • サイボウズ Garoon(パッケージ版)

プロキシクラスの生成

WSDLの定義情報を元に、Webサービスにアクセスするためのプロキシクラスを自動生成します。
オブジェクトをXMLデータにシリアライズしたり、逆にデシリアライズするのは、このプロキシクラスがやってくれます。

サービス参照の追加

赤枠内を順にクリックします。


Web参照の追加

URLの欄にWSDLの場所を入力します。
WSDLの場所は、公式ドキュメントGaroon SOAP APIの共通仕様に記載があります。

プロキシクラスの修正

ソリューションエクスプローラ上部にある [すべてのファイルを表示] ボタンをクリックし、自動生成されたReference.csを表示します。

Reference.csにGaroonのSOAPヘッダを追加します。手順は【VisualStudio2017でガルーンAPIを使ってメッセージを送ってみる】が詳しいので、ここでは割愛します。

リクエストクラスの作成

プロキシクラスの下記APIを呼ぶリクエストクラスを作成します。

Garoon共通のSOAPヘッダをメンバ変数に定義し、コンストラクタで設定するようにしました。認証情報もコンストラクタで受け取ります。

GaroonRequest.cs
using System;
using System.IO;
using System.Linq;
using MindWood.GaroonClientApp.GaroonService;

namespace MindWood.GaroonClientApp
{
    /*
     * Garoonリクエストクラス
     */
    class GaroonRequest
    {
        // Garoon共通SOAPヘッダ
        private ActionElement actionElement;
        private UsernameTokenElement userNameTokenElement;
        private SecurityElement securityElement;
        private TimestampElement timeStampElement;

        // コンストラクタ
        public GaroonRequest(string username, string password)
        {
            actionElement = new ActionElement();
            userNameTokenElement = new UsernameTokenElement();
            securityElement = new SecurityElement();
            timeStampElement = new TimestampElement();

            userNameTokenElement.Username = username;
            userNameTokenElement.Password = password;
            securityElement.usernameToken = userNameTokenElement;

            timeStampElement.Created = DateTime.UtcNow;
            timeStampElement.Expires = timeStampElement.Created.AddDays(8);
        }

        // ログイン名からユーザIDを取得する
        public UserInfo BaseGetGetUsersByLoginName(string login_name)
        {
            actionElement.actionValue = "BaseGetUsersByLoginName";
            BaseBinding api = new BaseBinding {
                action = actionElement,
                security = securityElement,
                timeStamp = timeStampElement
            };
            string[] param = { login_name };

            UserInfo user = new UserInfo();
            try {
                UserType[] resp = api.BaseGetUsersByLoginName(param);
                user.id = resp[0].key;
                user.name = resp[0].name;
                user.email = resp[0].email;
            } catch (Exception ex) {
                Console.WriteLine(ex.Message);
            }
            return user;
        }

        // メッセージを送信する
        public string MessageCreateThreads(string user_id, string subject, string body, string fullpath_file)
        {
            actionElement.actionValue = "MessageCreateThreads";
            MessageBinding api = new MessageBinding {
                action = actionElement,
                security = securityElement,
                timeStamp = timeStampElement
            };

            string id_str = null;
            try {
                ThreadType threadType = new ThreadType();
                ThreadTypeAddressee threadTypeAddressee = new ThreadTypeAddressee();
                content content = new content();
                ThreadTypeFolder threadTypeFolder = new ThreadTypeFolder();
                ThreadTypeFollow threadTypeFollow = new ThreadTypeFollow();
                MessageCreateThreadType messageThreadType = new MessageCreateThreadType();
                MessageCreateThreadsRequestType messageThreadsRequestType = new MessageCreateThreadsRequestType();

                threadType.id = "dummy";  // ID
                threadType.version = "dummy";  // スレッドのバージョン
                threadTypeFolder.id = "dummy";  // フォルダID
                threadType.folder = new ThreadTypeFolder[1];
                threadType.folder[0] = threadTypeFolder;
                threadTypeFollow.id = "dummy";
                threadType.follow = new ThreadTypeFollow[1];
                threadType.follow[0] = threadTypeFollow;
                threadType.confirm = false;  // 閲覧状況の確認は不要

                // タイトル
                threadType.subject = subject;

                // 本文
                content.body = body;
                threadType.content = content;

                int i;

                // 宛先
                i = 0;
                string[] ids = user_id.Split(',');
                threadType.addressee = new ThreadTypeAddressee[ids.Length];
                foreach (var id in ids) {
                    threadTypeAddressee = new ThreadTypeAddressee();
                    threadTypeAddressee.user_id = id;
                    threadTypeAddressee.name = "dummy";
                    threadType.addressee[i] = threadTypeAddressee;
                    i++;
                }

                // 添付ファイル
                if (fullpath_file != null) {
                    i = 0;
                    string[] files = fullpath_file.Split(',');
                    MessageCreateThreadTypeFile[] typeFiles = new MessageCreateThreadTypeFile[0];
                    contentFile[] contFiles = new contentFile[0];

                    foreach (var file in files) {
                        // 添付ファイルをバイト配列に読み込む
                        FileStream fs = new FileStream(file, FileMode.Open, FileAccess.Read);
                        byte[] bs = new byte[fs.Length];
                        fs.Read(bs, 0, bs.Length);
                        fs.Close();

                        // ファイル実体
                        MessageCreateThreadTypeFile typeFile = new MessageCreateThreadTypeFile();
                        typeFile.content = bs;
                        typeFile.id = i.ToString();

                        Array.Resize(ref typeFiles, i + 1);
                        typeFiles[i] = typeFile;

                        // ファイル情報
                        contentFile contFile = new contentFile();
                        contFile.id = i.ToString();
                        contFile.size = (ulong)bs.Length;
                        contFile.name = Path.GetFileName(file);
                        contFile.mime_type = System.Web.MimeMapping.GetMimeMapping(contFile.name);

                        Array.Resize(ref contFiles, i + 1);
                        contFiles[i] = contFile;

                        i++;
                    }
                    messageThreadType.file = typeFiles;
                    threadType.content.file = contFiles;
                }

                // メッセージの送信
                messageThreadType.thread = threadType;
                messageThreadsRequestType.create_thread = new MessageCreateThreadType[1];
                messageThreadsRequestType.create_thread[0] = messageThreadType;

                ThreadType[] resp = api.MessageCreateThreads(messageThreadsRequestType);

                if (resp.Any()) {
                    id_str = resp[0].id.ToString();
                }

            } catch (Exception ex) {
                Console.WriteLine(ex.Message);
            }
            return id_str;
        }

    }
}

ユーザ情報を管理するクラスは次のようにしました。これだけなら構造体で良いかもしれません。

UserInfo.cs
namespace MindWood.GaroonClientApp
{
    class UserInfo
    {
        public string id;
        public string name;
        public string email;
    }
}

リクエストクラスの使い方

インスタンスの作成方法

GaroonRequest req = new GaroonRequest(自ログイン名, パスワード);

メールの送信方法

宛先ログイン名(連絡帳から確認できます)から、システム内のユーザIDを取得し、メールを送信します。
添付ファイルが要らなければ、nullを添付ファイル名に渡せば良いです。

UserInfo user = req.BaseGetGetUsersByLoginName(宛先ログイン名);
if (user == null) {
    // エラー時の処理
} else {
    string res = req.MessageCreateThreads(user.id, タイトル, 本文, 添付ファイル名);
}

おまけ(完成イメージ)

Discussion