Ruby を使ってGmailの内容を別のアカウントのGmailへ移す方法

まくら

明日の東京は,予想最高気温4℃とか降雪とか,もう勘弁して下さいという感じのようなのですが. 数ヶ月もすれば暖かくもなりましょう. そしてやってくるのは,別れと出会いの春でございます.

いまどき,IT関連でなくてもメールくらいは日用品になっておるわけでして. 所属が変わるときには,自分も,自分の所属組織も,この扱いに困ったりするわけです. 私が最初にメアドなるものを貰ってから20年以上経ちますが,この辺りは何の進歩も見られないなと,思ったりするような,しないような.

Gmailの内容保全について考える.

今や社会インフラといえるよなサービスの代表として Gmail は筆頭でしょう. 個人,学生のみならず,Google Apps for Bysiness (GAB)などで会社でも使われていることも多いですし.

学生が学校のアカウントとして Gmail を持っている場合は,卒業と同時にそれを失うのが一般的なはずです. 卒業後を見越していればよいのですが,新入生の頃は右も左も判らないので,つい学校の Gmail に頼っちゃった,ということは十分に有り得るでしょう.

会社の場合は,退職と同時に,その持ち出しを禁止されるのが一般的だろうと思います. この場合,困るのは,会社に残った側です. メールのやり取りは,退職者が行っていた仕事のログでもあり,保全したいと思うはずです. が,GABのアカウントは有料でして,アカウントを消して経費節減に繋げたいという経営経理側の言い分もわかります. その辺を上手く調整しないと…

…こういう哀しいできごとがおこるわけです.(この例はメールではありませんが)

それ,Rubyでやってみよう.

いや別に Ruby でなくてもできますが…(汗.閑話休題.

一般的に言うと,Gmail の保全には,imapsync などの汎用ツールを使うだろうと思います. しかし,ここでは,独自にスクリプトを書くという提案をします.

Ruby には,強力なIMAPライブラリがあるので,あんまり難しくはありません. あと,Ruby 入門の日記ではありませんので,処理の詳細は説明しません.

注目は X-GM-RAW

注目していただきたいのは,「 #ここに注目 」の行です. Gmail は IMAP の SEARCH に X-GM-RAW なる指定ができます. これは,Gmail のweb画面にある検索窓に入力する書式を,そのまま指定できます. この例では,Googleグループのandroid-x86というグループから来ているメールを抽出してコピーしています.

Gmail はディスク容量が大きく,検索のスピードも早いため,一般的なメールフォルダよりも多くのメールを溜め込みがちです(個人差はありますが). これを,imapsync などの汎用ツールを使って全コピーしようとすると,涙目になります.

まとめ

スクリプトの中には,いろいろ細かいネタが仕込まれているのですが,それらもサックリと省略します. 汎用ツールで開発時間を短縮するという考え方も間違いではありませんが,汎用ツールでは対応できない機能を見つけたときには,小さなスクリプトを作るのもアリですよね,と. まとめになっていませんが,今日はこんな感じで.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#!/usr/bin/env ruby
# -*- coding: utf-8 -*-
# Copyright (C) 2014 Masaki "monaka" Muranaka.
# License: MIT.

require 'net/imap'
require 'time'

src_imap = Net::IMAP.new('imap.gmail.com', 993, true)
dst_imap = Net::IMAP.new('imap.gmail.com', 993, true)

def parseHeader(msg)
  msg =~/\A(.*?)\r\n\r\n(.*)\Z/m
  head,body=$1,$2
  head=head.split(/\r\n/).map{|i|i[0]+i}.join("\r\n")[1..-1].split(/\r\n[^\s]/).map{|i|i.split(/\r\n\s\s/).join("")}
  Hash[*head.map{|i|i=~/^(.*?):\s(.*)$/;[$1,$2]}.flatten]
end


###################################################################
# ここは設定
searchParams = [
                ['FROM', '[email protected]'],
                ['X-GM-RAW', 'list:(<android-x86.googlegroups.com>)'],      #ここに注目
                ['X-GM-RAW', 'list:(<receipt.movsign.info>)']
               ]

src_imap.login('[email protected]', 'password')            #コピー元
dst_imap.login('[email protected]', 'another-password') #コピー先

#この辺もお好みで.
src_imap.select('INBOX')
dst_imap.select('INBOX')

###################################################################


searchParams.each do |searchArray|
  puts "Searching #{searchArray[1]}"
  searchIds = src_imap.search(searchArray).each do |id|
    msg = src_imap.fetch(id, 'RFC822')[0].attr['RFC822']
    head = parseHeader(msg)

    searchIds = dst_imap.search(['X-GM-RAW', "rfc822msgid:#{head[%q{Message-ID}]}"])
    if (searchIds.length == 0) # このチェックを省くと遅くなる
      dst_imap.append('INBOX', msg, nil, Time.parse(head['Date']))
      putc 'w'
    else
      putc '.'
    end
  end
  puts
end

src_imap.logout()
dst_imap.logout()

src_imap.disconnect()
dst_imap.disconnect()