rcmdnk's blog

MenuBarGmail_200_200

rumps を使ってMacのメニューバーアプリが簡単に作れたので、 これと Gmail API を使って Gmailの通知を行うアプリを作ってみました。

Sponsored Links

コードはGitHubにあります。

インストールはHomebrewを使っているならCaskを使って

$ brew cask install rcmdnk/rcmdnkcask/menubargmail

でインストールできます1

もしくは Releases ページから最新のバージョンをzipかtar.gzで取ってきて、 展開して中に入ってるMenuBarGmail.app/Applicationsなり 好きな所に入れてください。

立ち上げると、最初にブラウザでGoogleの認証があるので、 許可してください。

20151117_menubargmailauthentication.jpg

そうすると、デフォルトではInboxに入ってる未読メールを取得しに行って、 未読数をメニューバーに表示します。

さらに、メニューバーのアイコンをクリックすると Unread messagesというメニューがあるので、 そこへマウスを持って行くと、 未読メッセージの到着時間、タイトル、メッセージの概略のリストが表示されます。

20151117_menubargmailinbox.jpg

各メニューについて。

以下の様なメニューがあります。

  • About: MenuBarGmailの情報を表示。
  • Account: 現在使っているアカウント名が表示されます。
    • ここをクリックするとGmailのトップページをブラウザで開きます。
  • Reconnect: 認証を再度行います。
    • アカウントを変更したい場合は、既定のブラウザで別のアカウントでログインしておいてReconnectしてください。
  • Unread messages: 上にも書いた通り未読メッセージ一覧。
    • ラベルを複数指定した時はラベル毎の未読数も表示されます。
  • Set checking interval: チェックする間隔の設定。初期値は60(秒)。
  • Set labels: チェックするラベルの設定。初期値は”“。
  • Set filter: チェックするメッセージを検索するフィルタの設定。初期値は”“。
    • labelsとfilterが共に空の場合はInboxの未読メールを検索します。
  • Mail notification: 新しいメールが来た時に通知センターに知らせるかどうかのトグル。
  • Start at login: ログイン時に自動でスタートするかどうかのトグル。
  • Uninstall: MenuBarGmailのアンインストール。
  • Quit: 終了。

こんな感じです。 ラベルを複数設定すると、

20151117_menubargmaillabels.jpg

こんな感じでUnread messagesのメニューの中に もう一段階ラベル毎のメニューが出来ます。

また、各メッセージの部分をクリックすると、Gmailのそのメッセージのラベルの ページがブラウザに表示されます。

メッセージ毎のURL、と言うのを取得するのが無理だったので、 ラベル単位でブラウザを開く様にしています。

取り敢えずこれでやりたいことはなんとなく出来たかな、と言った感じ。

ビルド方法

コードを取ってきてビルドして使うことも出来ます。

今行っているのはOS X 10.11.1 El Capitan, Python 2.7.10な環境です。

ビルドするには 以下のパッケージをpip install等を使ってインストールする必要があります。

また、下にも書いたように、 El Capitanにおいてpyobjc関連のエラーが出たり上手く行かない場合は pyobjc関連のパッケージをpip等で再インストールを試みてください。

後は、上のレポジトリを取ってきてsetup.pyのあるディレクトリで

$ python setup.py py2app

とすれば、distというディレクトリの中にMenuBarGmail.appが出来ます。

細かい所

作るにあたってちょっと問題があったところや細かい所について。

BaseHTTPServer

Googleの oauth2client を使っていますが、 この中で、

from six.moves import BaseHTTPServer

と言ったimportがあります。

pythonスクリプトをそのまま動かすと何も問題が無いのですが、 py2appを使ってアプリケーションにして動かそうとすると、 エラーのポップアップが出て、そこにあるコンソール表示を押して開いてみると

ImportError: No module named BaseHTTPServer

と言ったエラーが出ています。

このsixというライブラリーですが、 HomebrewでPythonをインストールしていると /usr/local/lib/python2.7/site-packages/six.py にあると思いますが、 この中では各モジュールの再定義を行っていて、 これはPython2と3の互換性を作るライブラリだそうです。

Six: Python 2 と 3 の互換性ライブラリ — six 1.9.0 ドキュメント

BaseHTTPServerは実際には

/usr/lib/python2.7/BaseHTTPServer.py

にあります。

これがpy2appでは上手く機能しないようで、 BaseHTTPServerを取り込めてない様です。

これを解決するに、sixを使わずにBaseHTTPServerを直接使う様に oauth2clientのモジュールを書き換えて使う方法もあるかと思いますが、 今回はMenuBarGmail.pyの中に

import BaseHTTPServer

という行を加える事で解決できました。 これでBaseHTTPServerは取り入れられるので、後は呼ぶ方はsixを使っても大丈夫なようです。

ただし、直接BaseHTTPServerを取り入れてるのでPython 3とは互換性がありません。 (Python 3ではこれはhttp.serverになっています。)

ssl.pyの問題

ssl(/usr/lib/python2.7/ssl.py)というライブラリの中にある SSLSocketというクラスの__init__の中に、

/usr/lib/python2.7/ssl.py
1
2
if ca_certs:
    self._context.load_verify_locations(ca_certs)

と言ったコードがあるんですが、 ここで

IOError: [Errno 20] Not a directory

と言ったエラーが起きていました。

ここでsslはoauth2clientからhttplib2を通して呼ばれていて、 通常は

/usr/local/lib/python2.7/site-packages/httplib2/cacerts.txt

というファイルを読みに行っています。

これが、py2appでアプリを作ると、site-packages

MenuBarGmail.app/Contents/Resources/lib/python2.7/site-packages.zip

とzipされた状態で入っていて、さらに上のエラーが出ている部分では

...MenuBarGmail.app/Contents/Resources/lib/python2.7/site-packages.zip/httplib2/cacerts.txt

の様に、zipの下にある様な形で呼んでいました。 zipの中にはファイルは入っているのですが、これだと読めません。

これに関してはちょっと無理やりやっていますが、 まず、setup.pyの中で、OPTIONSresourcesの値に

setup.py
1
2
3
4
5
6
7
8
9
10
OPTIONS = {
    'argv_emulation': True,
    'plist': {
        'LSUIElement': True,
    },
    'iconfile': 'MenuBarGmail.icns',
    'resources': [
        'MenuBarGmailMenuBarIcon.png',
        '/usr/local/lib/python2.7/site-packages/httplib2/cacerts.txt']
}

と、cacerts.txtをインクルードするようにしています。 これで、

...MenuBarGmail.app/Contents/Resources/cacerts.txt

に取り入れられるので、後は ssl.py内のca_certsを読んでる部分を

/usr/lib/python2.7/ssl.py
1
2
3
4
if ca_certs:
    if ca_certs.find("Contents/Resources/lib/python2.7/site-packages.zip/httplib2/cacerts.txt") != -1:
        ca_certs = ca_certs.replace("lib/python2.7/site-packages.zip/httplib2/", "")
    self._context.load_verify_locations(ca_certs)

こんな感じでzipを使っていたら無理やりパスを変更する、 みたいなことをします。

MenuBarGmail.pyと同じディレクトリにssl.pyをコピーしてきて、 この変更を与えて置いておけば、 後はpy2app時にもこのssl.pyを使ってコンパイルしてくれます。

取り敢えずこれで動きますが、 これもPython 3に互換性が無いし、ちょっと考えもの。

ログイン項目に追加

通常、ログイン時に起動させるアプリを自分で登録するには、 システム環境設定 ユーザとグループログイン項目 へ行ってアプリを追加したりします。

最初はここにアプリを追加したり出来る様にしたのですが、 この辺りAPIがOS X 10.11から結構変更したらしく、 Webにあるような方法を使っても上手く行きません。

CoreServices Changes for Objective-C

LaunchServicesLSSharedFileList等をimportしたいのですが、 LaunchServicesSharedFileList*に変更、となっています。 ただ、これをPythonから素直に

from SharedFileList import LSSharedFileList

としてもSharedFileListというそのようなモジュールはない、と言われてしまいます。

もう少し調べてみると、

osx - LaunchAgents for GUI app - Stack Overflow

ここに、

Apparently kLSSharedFileListSessionLoginItems was deprecated (OS X 10.11) already, and Apple is suggesting that using launch agents is a better practice – gbdavid Oct 23 at 7:59

ともあります。 そもそもログインアイテムに追加するようなやりかたはdeprecatedである、と。

代わりにLaunch Agentsを使いなさい、と。

Launch Agentsを使うには ~/Library/LaunchAgents/ に起動用plistファイルを入れてあげれば良いわけですが、 ファイルを作らないといけないのでちょっと面倒。

ですが、今回はこの方法を使っています。

メニューのStart at loginをクリックすると、 ~/Library/LaunchAgents/menubargmail.plistというファイルが 以下の様な内容で作成されます。

~/Library/LaunchAgents/menubargmail.plist
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>Label</key>
        <string>menubargmail</string>
        <key>ProgramArguments</key>
        <array>
            <string>/Applications/MenuBarGmail.app/Contents/MacOS/MenuBarGmail</string>
        </array>
        <key>RunAtLoad</key>
        <true/>
</dict>
</plist>

簡単な雛形で、Labelには適当な名前を、 ProgramArgumentsにはアプリの実行ファイルの位置を指定し、 RunAtLoadをtrueにしておけば ログイン時にこのファイルがロードされ(~/Library/LaunchAgentsにあるplistが自動でロードされる) アプリが起動されます。

ここでちょっと詰まったのが実行ファイルの位置の指定です。

まず、実行ファイルはアプリそのものではなく、 アプリ内のContents/MacOS/に入ってる実行ファイルMenuBarGmailを 指定する必要があります(アプリをダブルクリック時にも実際にはこれが起動します)。

最初、MenuBarGmail.pyの中で、

os.path.abspath(__file__)

みたいにして自分へのパスを取ってきて使おうと思ったのですが、 アプリにコンパイルして実行してみても、このパスは

MenuBarGmail.app/Contents/Resources/MenuBarGmail.py

になります。 実際にはコンパイルした実行ファイルの

MenuBarGmail.app/Contents/MacOS/MenuBarGmail

になってないといけません。

ここでもちょっと無理やりですが、

MenuBarGmail.py
1
2
3
4
5
6
7
exe = os.path.abspath(__file__)
if exe.find('Contents/Resources/') != -1:
    name, ext = os.path.splitext(exe)
    if ext == ".py":
        exe = name
    exe = exe.replace("Resources", "MacOS")
return exe

こんな風に、アプリ内で呼ばれている場合、MacOS内の実行ファイルへ パスを変更するようにしました。 (これだと…/Resources/MenuBarGmail.pyを実行しても MacOSの方パスになりますが、まあ、実害はないでしょう、と。)

ログイン時の起動を止めるには RunAtLoadの値をfalseにすれば実行は止められますが、 このファイルはログイン時起動に使うためだけのものであり、 下手にアプリを削除した時に残るの可能性も下げたいので、 Start at loginをオフにするとファイルごと消すようにしています。

oauth2client.tools.run()がdeprecatedになっていた

GoogleのApiを使って認証を取る時に、 最初

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import httplib2
from oauth2client.file import Storage
from oauth2client.tools import run

storage = Storage(os.path.expanduser(self.authentication_file())
credentials = storage.get()

if credentials is None or credentials.invalid:
    credentials = run(
        OAuth2WebServerFlow(
            client_id=self.google_client_id,
            client_secret=self.google_client_secret
            scope=[
                'https://www.googleapis.com/auth/gmail.readonly']),
        storage)

http = httplib2.Http()
http = credentials.authorize(http)

service = build('gmail', 'v1', http=http)

こんな感じで認証をしてましたが、 これだと

WARNING:root:This function, oauth2client.tools.run(), and the use of the gflags library are deprecated and will be removed in a future version of the library.

みたいな注意が出るようになっていました。

現在はrun_flowという関数を使うのが正しいようです。

OAuth 2.0 API Client Library for Python Google Developers

以下のように変更しました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import httplib2
from oauth2client.file import Storage
from oauth2client import tools

storage = Storage(os.path.expanduser(self.authentication_file())
credentials = storage.get()

if credentials is None or credentials.invalid:
    credentials = tools.run_flow(
        OAuth2WebServerFlow(
            client_id=self.google_client_id,
            client_secret=self.google_client_secret,
            scope=['https://www.googleapis.com/auth/gmail.readonly']),
        storage, tools.argparser.parse_args([]))

http = httplib2.Http()
http = credentials.authorize(http)

service = build('gmail', 'v1', http=http)

run_flowには最後に位置指定引数(positional arguments)が必要で ここにtools.argparse.parse_args([])を使って空の物を与えています。

上のAPIの説明では

1
2
3
4
5
6
7
import argparse
from oauth2client import tools

parser = argparse.ArgumentParser(parents=[tools.argparser])
flags = parser.parse_args()
...
credentials = tools.run_flow(flow, storage, flags)

と言った使い方になっていますが、今はコマンドラインから フラグを与える事はしないので(下手に渡されても困るので) ここでは空の物を渡すようにしています。

まとめ

ということで、MacでGmailの通知を行うアプリを作ってみました。

最近は Notify Pro というアプリを使っていましたが、 これと比べると複数のラベルや フィルターを付けて自由にメールを選べる所が優れています。

他のアプリでもその辺選べるのは余り無いと思います(使い方によっては意味ないんでしょうが)。

ちょっと見た目がチープで、特にメールの情報の部分が ただ一行に並べてるだけでちょっと見にくいのがあれですが、 取り敢えず欲しいものは出来たな、という感じ。

これ以上やろうと思うと、rumpsの中を直接変更するか、 同じような物を自分で作った方が早いかな、と言った感じもしますが、 せっかくなのでそのうちやるかも。

UI部分などはいっそのことSwiftとかで書いたほうが早いのかもしれませんが、 GoogleのApiとかがPythonだと慣れてて使いやすいのであれな感じ。

GmailのApiは今回はメールを読みだすだけに使ってますが、 メールを既読にしたりメールを送ったりするのも結構簡単に出来るので、 UIが一新出来たらその辺色々と入れる事はできるな、とは思います。

まあ、基本ブラウザで見るので、簡単な通知さえあれば良いわけですが。。。

Sponsored Links
  1. 既にrcmdnk/homebrew-rcmdnkcask をタップしてる場合は

    $ brew update && brew cask install menubargmail
    

Sponsored Links

« rumpsを使ってPythonで簡単にMacのメニューバーアプリを作る MenuBarGmailに既読機能、メール送信機能を付けた »