ラベル Office365 の投稿を表示しています。 すべての投稿を表示
ラベル Office365 の投稿を表示しています。 すべての投稿を表示

2025年10月25日土曜日

Microsoft Graph API を使って個人の OneDrive を操作する方法

Microsoft Graph API を使って個人の OneDrive を操作する方法

概要

前回 OneDrive にアクセスできるトークンの取得まで行いました
今回はそのトークンを使って実際に OneDrive にアクセスします

環境

  • macOS 15.7.1
  • Python 3.12.11

サンプルコード

import json
import os

import requests

TENANT_ID = "xxx"
CLIENT_ID = "xxx"
CLIENT_SECRET = "xxx"
REDIRECT_URI = "http://localhost:8080"
TOKEN_FILE = "onedrive_token.json"
GRAPH_API_BASE = "https://graph.microsoft.com/v1.0"


# =========================
# トークン管理
# =========================
def load_tokens():
    """保存済みトークンを読み込む"""
    if not os.path.exists(TOKEN_FILE):
        raise FileNotFoundError(f"{TOKEN_FILE} が存在しません。先にトークン取得を実行してください。")
    with open(TOKEN_FILE, "r", encoding="utf-8") as f:
        return json.load(f)


def save_tokens(tokens):
    """トークンを保存"""
    with open(TOKEN_FILE, "w", encoding="utf-8") as f:
        json.dump(tokens, f, indent=2)


def refresh_access_token(refresh_token, client_id, client_secret, redirect_uri):
    """リフレッシュトークンからアクセストークンを更新"""
    token_url = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"
    data = {
        "client_id": client_id,
        "client_secret": client_secret,
        "redirect_uri": redirect_uri,
        "grant_type": "refresh_token",
        "refresh_token": refresh_token
    }
    res = requests.post(token_url, data=data)
    res.raise_for_status()
    return res.json()


# =========================
# OneDrive 操作
# =========================
def get_headers(access_token):
    return {"Authorization": f"Bearer {access_token}"}


def upload_file(access_token, local_path, remote_name):
    """小さなファイルをアップロード"""
    url = f"{GRAPH_API_BASE}/me/drive/root:/{remote_name}:/content"
    with open(local_path, "rb") as f:
        res = requests.put(url, headers=get_headers(access_token), data=f)
    if res.status_code in (200, 201):
        print(f"✅ アップロード成功: {remote_name}")
    else:
        print(f"❌ アップロード失敗 ({res.status_code}): {res.text}")


def list_files(access_token):
    """ルートディレクトリのファイル一覧取得"""
    url = f"{GRAPH_API_BASE}/me/drive/root/children"
    res = requests.get(url, headers=get_headers(access_token))
    if res.status_code == 200:
        items = res.json().get("value", [])
        print("📄 OneDrive ファイル一覧:")
        for item in items:
            print(f" - {item['name']}")
    else:
        print(f"❌ 取得失敗 ({res.status_code}): {res.text}")


def delete_file(access_token, remote_name):
    """ファイル削除"""
    url = f"{GRAPH_API_BASE}/me/drive/root:/{remote_name}"
    res = requests.delete(url, headers=get_headers(access_token))
    if res.status_code in (204, 200):
        print(f"🗑️ 削除成功: {remote_name}")
    else:
        print(f"❌ 削除失敗 ({res.status_code}): {res.text}")


# =========================
# メイン
# =========================
if __name__ == "__main__":
    tokens = load_tokens()
    access_token = tokens["access_token"]

    # 必要に応じてリフレッシュ
    tokens = refresh_access_token(tokens["refresh_token"], CLIENT_ID, CLIENT_SECRET, REDIRECT_URI)
    access_token = tokens["access_token"]
    save_tokens(tokens)

    # サンプルファイル作成
    sample_file = "test.txt"
    with open(sample_file, "w", encoding="utf-8") as f:
        f.write("Hello OneDrive via Graph API!")

    # アップロード
    upload_file(access_token, sample_file, "test.txt")

    # ファイル一覧
    list_files(access_token)

    # 削除(必要であればコメント解除)
    delete_file(access_token, "test.txt")

ポイント

  • トークンが切れた場合にリフレッシュトークを使って再度トークンを取得します
  • 毎回行っても問題ないので実際は毎回リフレッシュトークを使って問い合わせたほうがいいです
  • トークンをファイルで保存していますが漏洩すると大変なので管理には注意しましょう

最後に

Python で OneDrive の操作をしてみました
認証が面倒ですが一度認証してしまえばあとはスクリプトを実行するだけです

アプリのシークレットに期限があるのでその期限がすぎたら再度ブラウザを開いてトークンファイルを更新する必要があります

2025年10月24日金曜日

Microsoft Graph API を使って個人の OneDrive にアクセスする方法

Microsoft Graph API を使って個人の OneDrive にアクセスする方法

概要

前回 Graph API に入門しました
今回は個人用の OneDrive アクセスしてみます
なそアプリーケーション権限は使えず「委任されたアクセス許可」に変更する必要があるので注意してください

簡単に言うと API をコールするためのアクセストークンを一度ブラウザを開いて取得しなければなりません

環境

  • macOS 15.7.1
  • Python 3.12.11

サポートされているアカウントの種類

  • 任意の組織ディレクトリ内のアカウント (任意の Microsoft Entra ID テナント - マルチテナント) と個人用の Microsoft アカウント (Skype、Xbox など)

もしくは

  • 個人用 Microsoft アカウントのみ

権限の設定

  • 管理 -> API のアクセス許可 -> アクセス許可の追加 -> Microsoft Graph -> 委任されたアクセス許可
    • Files.ReadWrite.All
    • Sites.ReadWrite.All
    • User.Read
    • offline_access
  • 既定のディレクトリに管理者の権限を与えますを選択

コールバックURLの設定

  • 管理 -> 認証 -> URLの追加 -> http://localhost:8080

トークンの取得

これは一度だけ実行します
表示された URL にブラウザでアクセスしログインします

  • vim ./fetch_token.py
import http.server
import json
import socketserver
import urllib.parse
import webbrowser

import requests

# ======== 設定 ========
TENANT_ID = "xxx"
CLIENT_ID = "xxx"
CLIENT_SECRET = "xxx"
REDIRECT_URI = "http://localhost:8080"
SCOPES = ["Files.ReadWrite.All", "Sites.ReadWrite.All", "User.Read", "offline_access"]
TOKEN_FILE = "onedrive_token.json"
# =======================


def start_local_server():
    """localhostで認可コードを受け取るための簡易HTTPサーバー"""
    class Handler(http.server.SimpleHTTPRequestHandler):
        def do_GET(self):
            query = urllib.parse.urlparse(self.path).query
            params = urllib.parse.parse_qs(query)
            if "code" in params:
                setattr(self.server, "auth_code", params["code"][0])
                self.send_response(200)
                self.end_headers()
                self.wfile.write(b"Authorization successful. You can close this window.")
            else:
                self.send_response(400)
                self.end_headers()
                self.wfile.write(b"Missing authorization code.")

    with socketserver.TCPServer(("localhost", 8080), Handler) as httpd:
        setattr(httpd, "auth_code", None)
        print("🌐 ローカルサーバー起動中 (http://localhost:8080)")
        httpd.handle_request()
        return getattr(httpd, "auth_code", None)


def get_tokens(auth_code):
    """認可コードからトークンを取得"""
    token_url = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"
    data = {
        "client_id": CLIENT_ID,
        "redirect_uri": REDIRECT_URI,
        "client_secret": CLIENT_SECRET,
        "code": auth_code,
        "grant_type": "authorization_code"
    }
    res = requests.post(token_url, data=data)
    res.raise_for_status()
    return res.json()


def save_tokens(tokens):
    """トークンをローカルに保存"""
    with open(TOKEN_FILE, "w", encoding="utf-8") as f:
        json.dump(tokens, f, indent=2)
    print(f"💾 トークンを保存しました → {TOKEN_FILE}")


def main():
    # 1️⃣ 認可URLを生成
    params = {
        "client_id": CLIENT_ID,
        "response_type": "code",
        "redirect_uri": REDIRECT_URI,
        "response_mode": "query",
        "scope": " ".join(SCOPES)
    }
    auth_url = f"https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?{urllib.parse.urlencode(params)}"

    print("\n🔗 以下のURLを開いてMicrosoftアカウントでサインインしてください:")
    print(auth_url)
    webbrowser.open(auth_url)

    # 2️⃣ 認可コード受け取り
    auth_code = start_local_server()
    if not auth_code:
        print("❌ 認可コードを取得できませんでした。")
        return

    print(f"✅ 認可コード取得成功: {auth_code[:20]}...")

    # 3️⃣ トークン取得
    tokens = get_tokens(auth_code)
    print("✅ アクセストークン・リフレッシュトークン取得成功")
    save_tokens(tokens)

    print("\n🧾 トークン内容:")
    print(json.dumps(tokens, indent=2))


if __name__ == "__main__":
    main()

動作確認

  • pipenv run python ./app.py

でブラウザが開いたらMicrosoft アカウントでログインしアプリに許可を与えます
成功するとトークン情報が onedrive_token.json に保存されるので今後はこれを使って OneDrive にアクセスします

最後に

Python から OneDrive にアクセスするためのトークンの取得まで行いました
次回は実際にファイルのアップロードや取得などを行います

Onedrive for bussiness だとアプリケーションの許可の権限で操作できるようです

2025年10月23日木曜日

Microsoft Graph API を Python で使ってみる

Microsoft Graph API を Python で使ってみる

概要

Microsoft365 の API である Microsoft Graph API を Python から利用してみました

環境

  • macOS 15.7.1
  • Python 3.12.11

アプリの作成

  • https://portal.azure.com にログイン
  • 左メニューから「Microsoft Entra ID」を選択
  • 「アプリの登録」->「新規登録」をクリック
    • 名前 -> 何でも OK
    • サポートされているアカウントの種類 -> この組織ディレクトリのみに含まれるアカウント (既定のディレクトリ のみ - シングル テナント)
    • リダイレクト URI -> とりあえず空でOK
  • テナントID、クライアントID をコピーしておく

シークレットの登録

  • 「証明書とシークレット」->「新しいクライアント シークレット」から発行
  • 「値」をコピーしておく

API アクセスの許可

  • アプリ登録ページ左メニューから「API のアクセス許可」を選択
  • 「+ アクセス許可の追加」->「Microsoft Graph」->「アプリケーションの許可」を選択
    • Sites.ReadWrite.All
    • Files.ReadWrite.All
  • アクセス許可の追加」->「管理者の同意を与える」をクリック(管理者権限が必要)

ライブラリインストール

  • pipenv install msal requests

アクセストークンを取得するサンプルコード

とりあえず作成したアプリがアクセスできる認証情報だけ取得してみます

  • vim ./app.py
import pprint

from msal import ConfidentialClientApplication

TENANT_ID = "xxx"
CLIENT_ID = "xxx"
CLIENT_SECRET = "xxx"

app = ConfidentialClientApplication(
    CLIENT_ID,
    authority=f"https://login.microsoftonline.com/{TENANT_ID}",
    client_credential=CLIENT_SECRET,
)

result = app.acquire_token_for_client(scopes=["https://graph.microsoft.com/.default"])

pprint.pprint(result)
  • pipenv run python ./app.py

以下のようにトークンが取得できれば OK です

{'access_token': 'xxx',
 'expires_in': 3599,
 'ext_expires_in': 3599,
 'token_source': 'identity_provider',
 'token_type': 'Bearer'}

sharepoint のサイト一覧を取得する

  • vim ./app.py
import requests
from msal import ConfidentialClientApplication

TENANT_ID = "xxx"
CLIENT_ID = "xxx"
CLIENT_SECRET = "xxx"

app = ConfidentialClientApplication(
    CLIENT_ID,
    authority=f"https://login.microsoftonline.com/{TENANT_ID}",
    client_credential=CLIENT_SECRET,
)
token = app.acquire_token_for_client(scopes=["https://graph.microsoft.com/.default"])
if token is None:
    raise Exception("トークンの取得に失敗しました。")
headers = {"Authorization": f"Bearer {token['access_token']}"}

# サイト一覧を検索(すべて表示)
url = "https://graph.microsoft.com/v1.0/sites?search=*"
response = requests.get(url, headers=headers)
print(response.json())
sites = response.json()

for s in sites.get("value", []):
    print(f"名前: {s['name']}")
    print(f"URL:  {s['webUrl']}")
    print(f"ID:   {s['id']}")
    print("---")

sharepoint は当然ですがライセンスがないと使えないので「Tenant does not have a SPO license.」のエラーになります

最後に

Python から MicrosoftGraphAPIをコールしてみました
無料アカウントでもここまではいけます
次回は実際に Microsoft365 上にあるリソースにアクセスしてみます

参考サイト

2017年1月11日水曜日

Office365 のカレンダー API を使って予定の削除を行ってみた

概要

前回、カレンダー API を使って予定の登録を行いました
今回は削除もしてみます
Gem の追加や新しいクラスの追加はないので必要な箇所のみコードを紹介します

環境

  • Windows7 64bit
  • Ruby 2.2.4p230
  • gem 2.4.8
  • Rails 5.0.0
  • Office365 API v2.0 (2016/08/22 時点)

削除するロジックの追記

ファイル名は前回の記事のものをそのまま使っています

  • vim app/controllers/calendar_controller.rb
def delete
  token = get_access_token($token)
  email = session[:user_email]
  if token
    conn = Faraday.new(:url => 'https://outlook.office.com') do |faraday|
      faraday.response :logger
      faraday.adapter  Faraday.default_adapter
    end
    response = conn.delete do |req|
      req.headers['Authorization'] = "Bearer #{token}"
      req.headers['Content-Type'] = 'application/json'
      req.url "/api/v2.0/Me/Events/#{params['event_id']}"
    end
    puts response.body
    redirect_to calendar_url
  else
    redirect_to root_url
  end
end
  • vim views/calendar/index.html.erb
<td>
<%= form_tag("/dcal", method: "post") do %>
  <%= hidden_field_tag("event_id", event['Id']) %>
  <%= submit_tag("削除") %>
<% end %>
</td>
  • vim config/routes.rb
post 'dcal' => 'calendar#delete'

という感じです
View で削除ボタン用のフォームを作成しています
作成するときに hidden フィールドに予定ごとに一意に振られる ID を設定します
コントローラ側でその ID を受け取り、その ID を元に削除 API をコールします

最後に

カレンダーの削除 API の使い方を紹介しました
URI に削除したい予定の ID を指定するだけなので簡単でした
当たり前ですが、メソッドは DELETE メソッドでコールしてください

Office365 のカレンダー API を使って予定の登録を行ってみた

概要

Office365 のカレンダー API を実行して予定の登録を行ってみました
前回の予定を取得するアプリを拡張しています

環境

  • Windows7 64bit
  • Ruby 2.2.4p230
  • gem 2.4.8
  • Rails 5.0.0
  • Office365 API v2.0 (2016/08/18 時点)

MS アプリケーションポータルでアプリの作成

前回を参考にして作成してください
「アプリケーション ID」と「アプリケーションシークレット」の 2 つを取得できれば OK です

Rails アプリの作成

  • rails new o365-tutorial
  • cd o365-tutorial
  • vim Gemfile
gem 'json'
gem 'oauth2'
gem 'activerecord-session_store'
gem 'faraday'

を追記して

  • bundle install
  • rails generate active_record:session_migration
  • rails db:migrate

コーディングしていく

トップ画面の作成

  • cd o365-tutorial
  • vim app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include AuthHelper

  def home
    login_url = get_login_url
    render html: "<a href='#{login_url}'>Log in and view my calendar</a>".html_safe
  end
end
  • vim config/routes.rb
Rails.application.routes.draw do
  root 'application#home'
  get 'authorize' => 'auth#gettoken'
  get 'calendar/index'
end

認証部分の作成

  • cd o365-tutorial
  • rails generate controller Auth
  • vim app/helpers/auth_helper.rb
module AuthHelper
  CLIENT_ID = 'be0c3ce7-1379-45b9-830a-9960c8656b30'
  CLIENT_SECRET = 'Tkp******************************'

  SCOPES = [ 'openid',
             'offline_access',
             'https://outlook.office.com/calendars.readwrite' ]

  def get_login_url
    client = OAuth2::Client.new(CLIENT_ID,
                                CLIENT_SECRET,
                                :site => 'https://login.microsoftonline.com',
                                :authorize_url => '/common/oauth2/v2.0/authorize',
                                :token_url => '/common/oauth2/v2.0/token')

    login_url = client.auth_code.authorize_url(:redirect_uri => authorize_url, :scope => SCOPES.join(' '))
  end

  def get_token_from_code(auth_code)
    client = OAuth2::Client.new(CLIENT_ID,
                              CLIENT_SECRET,
                              :site => 'https://login.microsoftonline.com',
                              :authorize_url => '/common/oauth2/v2.0/authorize',
                              :token_url => '/common/oauth2/v2.0/token')

    token = client.auth_code.get_token(auth_code,
                                     :redirect_uri => authorize_url,
                                     :scope => SCOPES.join(' '))
  end

  def get_user_email(access_token)
    conn = Faraday.new(:url => 'https://outlook.office.com') do |faraday|
      faraday.response :logger
      faraday.adapter  Faraday.default_adapter  
    end

    response = conn.get do |request|
      request.url 'api/v2.0/Me'
      request.headers['Authorization'] = "Bearer #{access_token}"
      request.headers['Accept'] = 'application/json'
    end

    email = JSON.parse(response.body)['EmailAddress']
  end

  def get_access_token(token_hash)
    client = OAuth2::Client.new(CLIENT_ID,
                                CLIENT_SECRET,
                                :site => 'https://login.microsoftonline.com',
                                :authorize_url => '/common/oauth2/v2.0/authorize',
                                :token_url => '/common/oauth2/v2.0/token')

    token = OAuth2::AccessToken.from_hash(client, token_hash)

    if token.expired?
      new_token = token.refresh!
      session[:azure_token] = new_token.to_hash
      access_token = new_token.token
    else
      access_token = token.token
    end
  end
end

CLIENT_ID, CLIENT_SECRET の部分はアプリポータルで取得したアプリケーション ID とアプリケーションシークレットを入力してください
権限としてカレンダーの取得と登録ができる「calendars.readwrite」を指定します
これを指定しないと API を実行しても 401 エラーが返却され続けてしまいます
しかも 401 エラーはメッセージボディを含んでいないので何が原因でエラーになっているかわからないので注意しましょう

  • vim app/controllers/auth_controller.rb
class AuthController < ApplicationController

  $token = {}

  def gettoken
    token = get_token_from_code params[:code]
    $token = token.to_hash
    session[:user_email] = get_user_email token.token
    redirect_to calendar_index_url
  end
end

カレンダーの一覧を表示する部分の作成

  • cd o365-tutorial
  • rails generate controller Calendar index
  • vim app/controllers/calendar_controller.rb
class CalendarController < ApplicationController
  include AuthHelper

  def index
    token = get_access_token($token)
    email = session[:user_email]
    if token
      conn = Faraday.new(:url => 'https://outlook.office.com') do |faraday|
        faraday.response :logger
        faraday.adapter  Faraday.default_adapter  
      end
      response = conn.get do |request|
        request.url '/api/v2.0/Me/Events?$orderby=Start/DateTime desc&$select=Subject,Start,End&$top=10'
        request.headers['Authorization'] = "Bearer #{token}"
        request.headers['Accept'] = "application/json"
        request.headers['X-AnchorMailbox'] = email
      end
      #puts JSON.pretty_generate(JSON.parse(response.body))
      @events = JSON.parse(response.body)['value']
    else
      redirect_to root_url
    end
  end

  def create
    token = get_access_token($token)
    email = session[:user_email]
    if token
      conn = Faraday.new(:url => 'https://outlook.office.com') do |faraday|
        faraday.response :logger
        faraday.adapter  Faraday.default_adapter  
      end
      response = conn.post do |req|
        req.headers['Authorization'] = "Bearer #{token}"
        req.headers['Content-Type'] = 'application/json'
        req.url '/api/v2.0/Me/Events'
        req.body = {
          :Subject => params['subject'],
          :Body => {
            :ContentType => "HTML",
            :Content => "I think it will meet our requirements!"
          },
          :Start => {
            :DateTime => params['start'],
            :TimeZone => "Tokyo Standard Time"
          },
          :End => {
            :DateTime => params['end'],
            :TimeZone => "Tokyo Standard Time"
          },
          :Attendees => [
              {
                :EmailAddress => {
                  :Address => "hoge@sample.com",
                  :Name => "Hoge"
                },
                :Type => "Required"
              },
              {
                :EmailAddress => {
                  :Address => "fuga@sample.com",
                  :Name => "Fuga"
                },
                :Type => "Required"
              }
          ]
        }.to_json
      end
      puts response.body
      redirect_to calendar_url
    else
      redirect_to root_url
    end
  end
end

create というメソッドを作成してこれでカレンダーを登録する POST リクエストを受け取ります
ポイントは req.body に Json を設定するところです
APIリファレンス にサンプルが記載されているのでそれを元に設定するといいと思います

簡単に各要素を説明すると

  • Subject・・・予定のタイトルを設定
  • Body・・・予定の本文を設定
  • Start・・・予定の開始時刻を設定
  • End・・・予定の終了時刻を設定
  • Attendees・・・予定の参加者を設定

という感じです
簡単なフォーマットチェックやバリデーションチェックは API 側でやってくれているようで Start が End を超えていたり時刻のフォーマットがおかしいと 400 系のエラーが返ってきます
response の body にエラーのメッセージも返っているので、それを見てもいいと思います

  • vim app/views/calendar/index.html.erb
<h1>My events</h1>
<table>
  <tr>
    <th>ID</th>
    <th>Subject</th>
    <th>Start</th>
    <th>End</th>
  </tr>
  <% @events.each do |event| %>
    <tr>
      <td><%= event['Id'] %></td>
      <td><%= event['Subject'] %></td>
      <td><%= event['Start'] %></td>
      <td><%= event['End'] %></td>
    </tr>
  <% end %>
</table>
<br>
<%= form_tag("/calendar", method: "post") do %>
  <%= label_tag(:subject, "予定名") %>
  <%= text_field_tag(:subject, '打ち合わせ') %>
  <%= label_tag(:start, "開始時刻") %>
  <%= text_field_tag(:start, Time.now.strftime("%Y-%m-%dT%H:%M:00")) %>
  <%= label_tag(:end, "終了時刻") %>
  <%= text_field_tag(:end, (Time.now + 3600).strftime("%Y-%m-%dT%H:%M:00")) %>
  <%= submit_tag("登録") %>
<% end %>

動作確認

  • cd o365-tutorial
  • rails server –bind=0.0.0.0

http://localhost:3000/ にブラウザでアクセスします
ログインリンクがあるのでログインすると最新のカレンダーに登録された予定の一覧が表示されるはずです
そして、画面したに予定を登録する登録フォームがあるので「登録」をクリックすれば予定が登録されるはずです

最後に

Office365 の カレンダー API を使って予定の登録をしてみました
日付は UTC で登録できるので localization してください

次は削除の API でも実行してみようと思います

参考サイト

2017年1月10日火曜日

Office365 API のカレンダー API を Rails で実行してみた

概要

前回 Outlook の API を実行してメールの一覧を取得するところまでやってみました
前回も参考にしたこのページが更新されみたいでやり方が変わっていました
なので再度試してみました
ハマリポイントもあったので紹介します

環境

  • Windows7 64bit
  • Ruby 2.2.4p230
  • gem 2.4.8
  • Rails 5.0.0
  • Office365 API v2.0 (2016/08/18 時点)

MS アプリケーションポータルでアプリの作成

前回を参考にして作成してください
「アプリケーション ID」と「アプリケーションシークレット」の 2 つを取得できれば OK です

Rails アプリの作成

  • rails new o365-tutorial
  • cd o365-tutorial
  • vim Gemfile
gem 'json'
gem 'oauth2'
gem 'activerecord-session_store'
gem 'faraday'

を追記して

  • bundle install
  • rails generate active_record:session_migration
  • rails db:migrate

コーディングしていく

トップ画面の作成

  • cd o365-tutorial
  • vim app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include AuthHelper

  def home
    login_url = get_login_url
    render html: "<a href='#{login_url}'>Log in and view my calendar</a>".html_safe
  end
end
  • vim config/routes.rb
Rails.application.routes.draw do
  root 'application#home'
  get 'authorize' => 'auth#gettoken'
  get 'calendar/index'
end

認証部分の作成

  • cd o365-tutorial
  • rails generate controller Auth
  • vim app/helpers/auth_helper.rb
module AuthHelper
  CLIENT_ID = 'be0c3ce7-1379-45b9-830a-9960c8656b30'
  CLIENT_SECRET = 'Tkp******************************'

  SCOPES = [ 'openid',
             'offline_access',
             'https://outlook.office.com/calendars.read' ]

  def get_login_url
    client = OAuth2::Client.new(CLIENT_ID,
                                CLIENT_SECRET,
                                :site => 'https://login.microsoftonline.com',
                                :authorize_url => '/common/oauth2/v2.0/authorize',
                                :token_url => '/common/oauth2/v2.0/token')

    login_url = client.auth_code.authorize_url(:redirect_uri => authorize_url, :scope => SCOPES.join(' '))
  end

  def get_token_from_code(auth_code)
    client = OAuth2::Client.new(CLIENT_ID,
                              CLIENT_SECRET,
                              :site => 'https://login.microsoftonline.com',
                              :authorize_url => '/common/oauth2/v2.0/authorize',
                              :token_url => '/common/oauth2/v2.0/token')

    token = client.auth_code.get_token(auth_code,
                                     :redirect_uri => authorize_url,
                                     :scope => SCOPES.join(' '))
  end

  def get_user_email(access_token)
    conn = Faraday.new(:url => 'https://outlook.office.com') do |faraday|
      # Outputs to the console
      faraday.response :logger
      # Uses the default Net::HTTP adapter
      faraday.adapter  Faraday.default_adapter  
    end

    response = conn.get do |request|
      request.url 'api/v2.0/Me'
      request.headers['Authorization'] = "Bearer #{access_token}"
      request.headers['Accept'] = 'application/json'
    end

    email = JSON.parse(response.body)['EmailAddress']
  end

  def get_access_token(token_hash)
    # Get the current token hash from session
    client = OAuth2::Client.new(CLIENT_ID,
                                CLIENT_SECRET,
                                :site => 'https://login.microsoftonline.com',
                                :authorize_url => '/common/oauth2/v2.0/authorize',
                                :token_url => '/common/oauth2/v2.0/token')

    token = OAuth2::AccessToken.from_hash(client, token_hash)

    if token.expired?
      new_token = token.refresh!
      session[:azure_token] = new_token.to_hash
      access_token = new_token.token
    else
      access_token = token.token
    end
  end
end

CLIENT_ID, CLIENT_SECRET の部分はアプリポータルで取得したアプリケーション ID とアプリケーションシークレットを入力してください

  • vim app/controllers/auth_controller.rb
class AuthController < ApplicationController

  $token = {}

  def gettoken
    token = get_token_from_code params[:code]
    $token = token.to_hash
    session[:user_email] = get_user_email token.token
    redirect_to calendar_index_url
  end
end

Office365 の OAuth 認証を実行して取得できたトークン情報をグローバル変数に格納しています
サンプルでは session オブジェクトに格納していたのですが、session に 4092bytes 以上のデータを保存しようとするとエラーになるので「ActionDispatch::Cookies::MAX_COOKIE_SIZE = 8192」という感じで無理矢理保存してみたのですが、これもダメだったのでグローバル変数を定義しました
おそらくあまり良いやり方ではないですが、とりあえず動かすのが目的だったのでグローバル変数を使いました

カレンダーの一覧を表示する部分の作成

  • cd o365-tutorial
  • rails generate controller Calendar index
  • vim app/controllers/calendar_controller.rb
class CalendarController < ApplicationController
  include AuthHelper

  def index
    token = get_access_token($token)
    email = session[:user_email]
    if token
      conn = Faraday.new(:url => 'https://outlook.office.com') do |faraday|
        faraday.response :logger
        faraday.adapter  Faraday.default_adapter  
      end

      response = conn.get do |request|
        # request.url '/api/v2.0/Me/Events?$orderby=Start/DateTime asc&$select=Subject,Start,End&$top=10'
        request.url '/api/v2.0/Me/Events?$orderby=Start/DateTime desc&$select=Subject,Start,End&$top=10'
        request.headers['Authorization'] = "Bearer #{token}"
        request.headers['Accept'] = "application/json"
        request.headers['X-AnchorMailbox'] = email
      end

      @events = JSON.parse(response.body)['value']
    else
      redirect_to root_url
    end
  end
end
  • vim app/views/calendar/index.html.erb
<h1>My events</h1>
<table>
  <tr>
    <th>Subject</th>
    <th>Start</th>
    <th>End</th>
  </tr>
  <% @events.each do |event| %>
    <tr>
      <td><%= event['Subject'] %></td>
      <td><%= event['Start'] %></td>
      <td><%= event['End'] %></td>
    </tr>
  <% end %>
</table>

動作確認

  • cd o365-tutorial
  • rails server –bind=0.0.0.0

http://localhost:3000/ にブラウザでアクセスします
ログインリンクがあるのでログインすると最新のカレンダーに登録された予定の一覧が表示されるはずです

最後に

Office365 の API を使ってカレンダーの予定を取得してみました
日付は UTC で取得できるので localization してください

あとは自分以外の予定の取得とか予定の登録とかもできるようになるとアプリを作れる技術は揃いそうです

参考サイト

Office365 API を Ruby で実行してみた

概要

Rails5 を使って Office365 の API をコールしてみました
コールしたのは Outlook の API になります

環境

  • Windows7 64bit
  • Ruby 2.2.4p230
  • gem 2.4.8
  • Rails 5.0.0
  • Office365 API v2.0 (2016/08/04 時点)

MS アプリケーションポータルでアプリの作成

Office365 API をコールするためにアプリを作成します
このアプリは Facebook アプリや Twitter アプリの概念と同じものになります

まず https://apps.dev.microsoft.com/ にアクセスします
自分の MS アカウントでログインしましょう
するとアプリケーションポータルという画面に移動するのでアプリを追加します
主にやることは以下の通り

  • アプリの追加 -> test_app -> アプリケーション ID が表示されていることを確認
  • アプリケーションシークレット -> 新しいパスワードを生成 -> ランダムな文字列のパスワードを取得
  • プラットフォーム -> プラットフォームの追加 -> リダイレクト URI -> http://localhost:3000/authorize を設定
  • 最後に「保存」

でアプリを作成することができます
アプリ名は自身の好きな名前を指定すれば OK です

最終的には以下のように「アプリケーション ID」と「アプリケーションシークレット」の 2 つが取得できれば OK です
try_office365_api_creating_app.png

Rails アプリの作成

  • rails new o365-tutorial
  • cd o365-tutorial
  • vim Gemfile
gem 'json'
gem 'oauth2'
gem 'faraday'

を追記して

  • bundle install

コーディングしていく

一気にコーディングしていきます

トップ画面の作成

  • cd o365-tutorial
  • vim app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include AuthHelper

  def home
    login_url = get_login_url
    render html: "<a href=\"#{login_url}\">Log in and view my email</a>".html_safe
  end
end
  • vim config/routes.rb
Rails.application.routes.draw do
  root 'application#home'
  get 'authorize' => 'auth#gettoken'
  get 'mail/index'
end

認証部分の作成

  • cd o365-tutorial
  • rails generate controller Auth
  • vim app/helpers/auth_helper.rb
module AuthHelper
  # App's client ID. Register the app in Application Registration Portal to get this value.
  CLIENT_ID = 'be0c3ce7-1379-45b9-830a-9960c8656b30'
  # App's client secret. Register the app in Application Registration Portal to get this value.
  CLIENT_SECRET = 'Tkp******************************'

  REDIRECT_URI = 'http://localhost:3000/authorize' # Temporary!

  # Scopes required by the app
  SCOPES = [ 'openid',
             'https://outlook.office.com/mail.read' ]

  # Generates the login URL for the app.
  def get_login_url
    client = OAuth2::Client.new(CLIENT_ID,
                                CLIENT_SECRET,
                                :site => 'https://login.microsoftonline.com',
                                :authorize_url => '/common/oauth2/v2.0/authorize',
                                :token_url => '/common/oauth2/v2.0/token')

    login_url = client.auth_code.authorize_url(:redirect_uri => authorize_url, :scope => SCOPES.join(' '))
  end

  # Exchanges an authorization code for a token
  def get_token_from_code(auth_code)
    client = OAuth2::Client.new(CLIENT_ID,
                                CLIENT_SECRET,
                                :site => 'https://login.microsoftonline.com',
                                :authorize_url => '/common/oauth2/v2.0/authorize',
                                :token_url => '/common/oauth2/v2.0/token')

    token = client.auth_code.get_token(auth_code,
                                       :redirect_uri => authorize_url,
                                       :scope => SCOPES.join(' '))
  end

  # Parses an ID token and returns the user's email
  def get_email_from_id_token(id_token)

    # JWT is in three parts, separated by a '.'
    token_parts = id_token.split('.')
    # Token content is in the second part
    encoded_token = token_parts[1]

    # It's base64, but may not be padded
    # Fix padding so Base64 module can decode
    leftovers = token_parts[1].length.modulo(4)
    if leftovers == 2
      encoded_token += '=='
    elsif leftovers == 3
      encoded_token += '='
    end

    # Base64 decode (urlsafe version)
    decoded_token = Base64.urlsafe_decode64(encoded_token)

    # Load into a JSON object
    jwt = JSON.parse(decoded_token)

    # Email is in the 'preferred_username' field
    email = jwt['preferred_username']
  end
end

CLIENT_ID, CLIENT_SECRET の部分はアプリポータルで取得したアプリケーション ID とアプリケーションシークレットを入力してください

  • vim app/controllers/auth_controller.rb
class AuthController < ApplicationController
  def gettoken
    token = get_token_from_code params[:code]
    session[:azure_access_token] = token.token
    session[:user_email] = get_email_from_id_token token.params['id_token']
    redirect_to mail_index_url
  end
end

メールの一覧を表示する部分の作成

  • cd o365-tutorial
  • rails generate controller Mail index
  • vim app/controllers/mail_controller.rb
class MailController < ApplicationController
  def index
    # session.each {|k, v|
    #   puts "key -> #{k}, v -> #{v}"
    # }
    token = session[:azure_access_token]
    email = session[:session_id]
    if token
      # If a token is present in the session, get messages from the inbox
      conn = Faraday.new(:url => 'https://outlook.office.com') do |faraday|
        # Outputs to the console
        faraday.response :logger
        # Uses the default Net::HTTP adapter
        faraday.adapter  Faraday.default_adapter  
      end
      response = conn.get do |request|
        # Get messages from the inbox
        # Sort by ReceivedDateTime in descending orderby
        # Get the first 20 results
        request.url '/api/v2.0/Me/Messages?$orderby=ReceivedDateTime desc&$select=ReceivedDateTime,Subject,From&$top=20'
        request.headers['Authorization'] = "Bearer #{token}"
        request.headers['Accept'] = "application/json"
        # request.headers['X-AnchorMailbox'] = email
      end
      # Assign the resulting value to the @messages
      # variable to make it available to the view template.
      @messages = JSON.parse(response.body)['value']
    else
      # If no token, redirect to the root url so user
      # can sign in.
      redirect_to root_url
    end
  end
end

ポイントがあり session[:session_id] を取得するように変更しています
サンプルだと session[:user_email] というようになっていたのですが、session オブジェクトをデバッグしてみたらそんなシンボルはなく変更しました
が、結局 request.headers['X-AnchorMailbox'] = email の部分をコメントアウトしています
これがあるとエラーとなりうまく取得できませんでした

  • vim app/views/mail/index.html.erb
<h1>My messages</h1>
<table>
  <tr>
    <th>From</th>
    <th>Subject</th>
    <th>Received</th>
  </tr>
  <% @messages.each do |message| %>
    <tr>
      <% if !message['From'].nil? %>
      <td><%= message['From']['EmailAddress']['Name'] %></td>
      <% else %>
      <td>x</td>
      <% end %>
      <td><%= message['Subject'] %></td>
      <td><%= message['ReceivedDateTime'] %></td>
    </tr>
  <% end %>
</table>

動作確認

  • cd o365-tutorial
  • rails server --bind=0.0.0.0

http://localhost:3000/ にブラウザでアクセスします
ログインリンクがあるのでログインすると最新の受信メールの一覧が表示されるはずです

最後に

Rails を使って Office365 API をコールする方法を紹介しました
ログインしてコールバック URL にセッション情報が戻ってくるので GUI が必要なのがちょっと残念な点でした
認証は OAuth2.0 なので、GUI はなくてもたぶんできるとは思います

また、検証中に自分が遭遇したエラーだと

  • コールバック URL がちゃんと設定されていなく Office365 API から認証情報が取得できなかった
  • 認証後 Outlook API を叩く際に X-AnchorMailbox の値がおかしく Json のパースの部分でエラーになっていた
  • session[:session_id] がなく nil を X-AnchorMailbox に設定しているせいで null 参照が発生しエラーになったいた
  • メールを表示する erb で From がなぜか存在しない場合があり nil チェックを入れた

あたりが遭遇したエラーです
特に X-AnchorMailbox はよくわからなく、結局ヘッダに設定しないようにしたら成功しました

参考サイト

2017年1月5日木曜日

Outlook 2013 でメールを全部開封済みにするマクロを作成した

概要

メールの振り分け設定をしている環境で各ディレクトリごとに開封済みにするのが面倒でした
ということで、存在する全ディレクトリ (受信トレイを含む) 配下にあるメールを配信済みにするマクロを作成しました
今回のマクロでは 2 階層分まで対応しています

階層的には以下のようなディレクトリ構成を想定しています
これの受信トレイ配下のメールと ssubN 配下のメールを全部 UnRead にします

受信トレイ
├── sub1
│   ├── ssub1
│   ├── ssub2
│   └── ssub3
└── sub2
    ├── ssub1
    └── ssub2

環境

  • Windows7 64bit
  • Outlook 2013

マクロ

Sub allUnRead()
  Set myNamespace = Application.GetNamespace("MAPI")
  Set inbox = myNamespace.GetDefaultFolder(olFolderInbox)
  ' 受信トレイ配下のメールを UnRead する
  For Each mail In inbox.Items
    If (mail.Class = olMail) And (mail.UnRead) Then
      mail.UnRead = False
    End If
  Next
  Dim sum As Integer
  ' 受信トレイ直下のサブディレクトリ
  For i = 1 To inbox.Folders.Count
    Set subFolder = inbox.Folders.Item(i)
    ' サブディレクトリ直下のサブサブディレクトリ
    For j = 1 To subFolder.Folders.Count
      sum = sum + 1
      Set ssubFolder = subFolder.Folders.Item(j)
      ' それを UnRead する
      For Each mail In ssubFolder.Items
        If (mail.Class = olMail) And (mail.UnRead) Then
          mail.UnRead = False
        End If
      Next
    Next
  Next
  MsgBox sum
End Sub

簡単な説明

まずメールにアクセスするための準備をします

Set myNamespace = Application.GetNamespace("MAPI")

そして受信トレイのオブジェクトを取得します
olFolderInbox を指定することで受信トレイのオブジェクトが取得できます
ここで指定する値を変更すると連絡先や予定の情報も取得することができます
参考: https://msdn.microsoft.com/ja-jp/library/office/ff861868.aspx

Set inbox = myNamespace.GetDefaultFolder(olFolderInbox)

まずは受信トレイ直下にあるメールを全部 UnRead にします

For Each mail In inbox.Items
  If (mail.Class = olMail) And (mail.UnRead) Then
    mail.UnRead = False
  End If
Next

If 文の条件がないとすでに UnRead になっているメールに対しても再度 UnRead のフラグを立てようとしてしまいます
問題はないのですが、大量のメールがあるときに非常に重くなるので入れたほうが良いです

次に受信トレイ直下にあるサブディレクトリを取得します

For i = 1 To inbox.Folders.Count
  Set subFolder = inbox.Folders.Item(i)

そしてその後すぐにサブディレクトリ直下にあるサブサブディレクトリを取得します

For j = 1 To subFolder.Folders.Count
  Set ssubFolder = subFolder.Folders.Item(j)

あとはサブサブディレクトリ直下にあるメールをすべて UnRead にします

For Each mail In ssubFolder.Items
  If (mail.Class = olMail) And (mail.UnRead) Then
    mail.UnRead = False
  End If
Next

一応最後に終了したことを把握するためメッセージを表示します

MsgBox sum

これは取得したサブサブディレクトリの数を表示しています

マクロの登録

この記事の手順通りに実施しました

  1. 開発 -> マクロ -> マクロ
  2. 名前入力 (allUnRead) -> 作成
  3. 今回のスクリプトをコピペ
  4. 実行

でマクロを実行できます
再度実行したい場合は

開発 -> マクロ -> allUnRead

を選択すれば OK です

最後に

Outlook 2013 で階層化されているメールを全部 UnRead するマクロを作成しました
今回は Outlook のイベントを使っていないので普通のマクロとして作成しています
ThisOutlookSession 側のマクロではないのでそっちにコピペしないようにしてください