Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/takuya/ruby-gmail-imap-watch
https://github.com/takuya/ruby-gmail-imap-watch
Last synced: 9 days ago
JSON representation
- Host: GitHub
- URL: https://github.com/takuya/ruby-gmail-imap-watch
- Owner: takuya
- License: gpl-3.0
- Created: 2024-09-22T19:47:01.000Z (about 2 months ago)
- Default Branch: master
- Last Pushed: 2024-11-08T03:30:08.000Z (11 days ago)
- Last Synced: 2024-11-08T04:24:54.829Z (11 days ago)
- Language: Ruby
- Size: 40 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
## Gmail IMAP サーバーに繋いで、Push通知をもらう。
IMAPにはIDLEコマンドが有り、IDLEコマンドで接続待ちをしてると、メール受信の通知を受け取ることができる。
このことから、GMailのIMAPにIDELコマンドで接続し、PUSHで通知をもらおうということである。
## Installing
## Example
example run
`bundle exec ruby bin/imap-subscribe.rb`sample usage
```ruby
# Load XOAUTH config
Dotenv.load('.env', '.env.sample')
ENV['client_secret_path'] = File.realpath ENV['client_secret_path']
ENV['token_path'] = File.realpath ENV['token_path']
ENV['user_id'] = ENV['user_id'].strip
##
raise "Empty file (#{ENV['token_path']})." unless YAML.load_file(ENV['token_path'])
ENV['user_id'] = YAML.load_file(ENV['token_path']).keys[0] if ENV['user_id'].empty?## enabling imap log
# ENV['DEBUG']='1'watcher = Takuya::GmailIMAPWatcher.new
## add event handler
watcher.on_message_received do |mail|
# @type mail [Mail]
puts " new message delivered."
puts mail.message_id
end## start listening
watcher.start```
## IMAP#idle
`IMAP#idle(timeout,&block)` は、排他的に動く。サーバーからのTCP受信を待ち受ける(PUSH通知)
`idle_done` は idle block 内部で行う必要がある。
idle_done すると idle は停止するが、TCP受信処理は行われる。(⇐ここがややこしい)
例えば、3つのFETCH が来たとき(3つ同時に既読フラグを付けた場合)
```text
+ idling
* 2 FETCH (UID 216 FLAGS (\Seen))
* 3 FETCH (UID 217 FLAGS (\Seen))
* 5 FETCH (UID 218 FLAGS (\Seen))
```idle_done は 最初の uid 216 で呼び出される。しかし、連続でTCP受信している。
idle_done は実行待ちになる。 3つのFETCHがそれぞれidle_doneを呼び出し、最初 idle_done が優先されるような動作になる。そのため、次のようなコードは、3つのFETCHのうち、どのresponseを見ているのだろうか。見失うことになる。
```ruby
imap.idle(300) do | res |
if res.kind_of?(Net::IMAP::UntaggedResponse) && res.name == 'FETCH'
# uid=216 で done されるので、uid=217,uid=218はココまで来ない。ただしTCP受信と保存はしてる。
uid = res.attr["UID"]
imap.idle_done
end
end
## でも受信はしてる
imap.responses["FETCH"].size # => 3
imap.responses["FETCH"].map{|e|e.attr["UID"]} #=> [216,217,218]
```なので、次のように、idle_doneを終えてからデータを取り出す必要がある。
```ruby
loop do
last_response = nil
imap.idle(300) do |res|
if res.kind_of?(Net::IMAP::UntaggedResponse) && res.name=='FETCH'
last_response = res
imap.idle_done
end
end## after idle_done
if last_response.kind_of?(Net::IMAP::UntaggedResponse)
case last_response.name
when 'FETCH'
imap.responses["FETCH"]
end
end
end```
IMAPをスレッドで扱えるようなことがマニュアルに書いてあるが、idle中は動作不良なので注意
IMAPのidle中はTCP(OpenSSL)のコネクションを占拠する。
IMAP#idle_doneの実装をつかうと、コード複雑になりがち。 idleとidle_doneの実装は問題が多い。idleを扱うなら、**IMAPコネクションを複数使ったほうがマシ**。
IDLE専用のコネクションと検索コネクションで別個にIMAPをインスタンス化するほうがスッキリかける。ただし、このレポジトリではコネクション1つで頑張った。コネクション複数つかうような書き方はしていない。
1接続なのでidle時に通知を受け取り後に、idleを中断して通知を処理する。
そのためにミュータブルオブジェクトを使って、通知を保存してメソッドの実装箇所を分けた。
```ruby
def callback_generator(mutable_object)
lambda do |res|
if res.kind_of?(Net::IMAP::UntaggedResponse) && res.name=='FETCH'
mutable_object.body = res
imap.idle_done
end
end
endloop do
mutable_object = Struct.new(:body).new
imap.idle(300, &( callback_generator mutable_object) )
## after idle_done
if mutable_object.body.kind_of?(Net::IMAP::UntaggedResponse)
case mutable_object.body.name
when 'FETCH'
imap.responses["FETCH"]
end
end
end```
`lambda`と`Proc.new` には例外キャッチや変数束縛に違いがあるので注意すること。
特に、IMAP.idleはスレッド動作だからProc.newでは例外キャッチができないので注意。
最初`Net::IMAP.responses`の存在に気づいて無かったので、コードが煩雑になった。気づいてたらもっと簡単に書けた気がする。
そのうちすべて書き直したい。
## IMAP EXPUNGE, EXISTS
GMail IMAP で EXPUNGE でメールが「ゴミ箱」「アーカイブ」された場合はuidが変わるので追跡できない。
IMAPはメールボックスごとにUIDが作られるので、ボックスを移動したらUIDは変化する。
また、idle時にメールを削除(移動)したとき、EXPUNGEとEXISTSがペアで通知されてくる。
## IMAP#select IMAP#examine
GmailをIMAPでメールを読み出すとき、自動的に既読になる。
```ruby
imap.select("INBOX")
envelope = imap.uid_fetch(uid, 'RFC822')[0].attr['RFC822'] # ここで既読になる。
```メールボックスの開き方には2種類ある。
```ruby
imap.select("INBOX")
imap.examine("INBOX")
```examineはREADONLYである。リードオンリーだと既読フラグがつかない。
SELECTは便利だけど、同期的に操作される。なので「メール本文」を読み込むと既読になる。
メール本文の開き方(既読をつけずに取得)
```ruby
# 既読がつく
envelope = imap.uid_fetch(uid, 'RFC822')[0].attr['RFC822']
# 影響なし
envelope = imap.uid_fetch(uid, "BODY.PEEK[]")[0].attr['BODY[]']
```RFC822で開くとき、`select`だと容赦なく既読 `read(:seen)` にされる。 `examine` の場合は変更されない。 不安定なので、`BODY.PEEK[]`を使うべき
まとめると、次のようになる。
|fetch/mbox| select | examine |
|:---:|:---:|:---:|
|RFC822|**既読**|無変|
|BODY.PEEK|無変|無変|select で開かない限り、既読がつくことはない。
- 自動化プログラムから扱うときは基本的に examineで行う。
- メールアプリなどユーザー操作と連動するときは selectをつかう。