2011年 07月 27日

gerry++

Maildir のメールを Gmail にそのままインポートしたい

手元に昔使っていた Maildir 形式で保存されているメールがたくさんあるのを、Gmail にインポートしたいと思った。

残念ながら、いくら手元に完全な MIME のメッセージがあっても、Gmail にはそのままアップロードするみたいな機能がないために、簡単にそのままインポートすることができない。とはいえ Gmail には Mail Fetcher 機能 (POP3 で外部サービスのメールをとりにいく) というのがあるので、それを利用することにした。(メールで送ってしまう、という手もあるけど、From/To が書きかわったり、時刻が狂ったりして嫌だった)

いろいろ試した (Perl で Net::Server::POP3 を使ったり) した結果、結局必要な実装だけ自力で書いた。なんとなく TCP のサーバ実装を書くみたいなときは Ruby 使うのが手に馴染むので Ruby で書いた。割と簡単だったのでさっさと自力で書けばよかったと思った……

  • 以下のスクリプトの冒頭をいい感じに設定
  • パブリックなサーバでポート1110をあけて、起動する
    • telnet [server] 1110 とかで接続できることを確認
  • [設定] → [アカウントとインポート] → [POP3 を使用したメッセージの確認] から [POP3 のメールアカウントを追加] を選ぶ
  • [メールアドレス] にインポートしたメールのメールアドレスを入れる (間違ってると正しくインポートされない)
  • [ユーザ名] [パスワード] にスクリプト冒頭に設定したものを入れる
  • [POP サーバー] に立ち上げたサーバのホスト名を入れる
  • [ポート] は 1110 にする
  • [アカウントを追加] ボタンをおすとたぶんうまくいく
#!/usr/bin/env ruby

USER = 'cho45'
PASS = 'xxxxxx'
MESSAGES = Dir.glob('./path/to/Maildir/cur/*')

require 'socket'
class POP3Session
	def self.messages=(messages)
		@@messages = messages
	end

	def initialize(s)
		@s = s
		authorization
	end

	def authorization
		p :authrization
		post "+OK POP3 server ready"
		session do |command, args|
			case command
			when 'USER'
				@user = args[0]
				if @user == USER
					post "+OK name is a valid mailbox"
				else
					post "-ERR never heard of mailbox name"
				end
			when 'PASS'
				pass = args[0]
				if pass == PASS
					post "+OK maildrop locked and ready"
					transaction
				else
					post "-ERR invalid password"
				end
			when 'QUIT'
				@s.close
			else
				post "-ERR unknown command"
			end
		end
	end

	def transaction
		p :transaction
		session do |command, args|
			p [command, args]
			case command
			when 'CAPA'
				post "+OK capability list follows."
				post "USER"
				post "UIDL"
				post "."
			when 'STAT'
				nom  = @@messages.size # number of messages
				size = @@messages.inject(0) {|r,i| r + i.size }  # size of the maildrop in octets
				post "+OK #{nom} #{size}"
			when 'LIST'
				if args.size.nonzero?
					msg = args[0].to_i - 1
					size = @@messages[msg].size
					post "+OK #{msg} #{size}"
				else
					post "+OK scan listing follows"
					@@messages.each_with_index do |m,i|
						post "#{i+1} #{m.size}" unless m.deleted
					end
					post "."
				end
			when 'UIDL'
				if args.size.nonzero?
					msg = args[0].to_i - 1
					post "+OK #{msg} #{@@messages[msg].hash}"
				else
					post "+OK message-id listing follows"
					@@messages.each_with_index do |m,i|
						post "#{i+1} #{m.hash}" unless m.deleted
					end
					post "."
				end
			when 'RETR'
				msg = args[0].to_i - 1

				m = @@messages[msg]
				if m
					post "+OK message follows"
					post m.content
					post "."
				else
					post "-ERR no such message"
				end
			when 'DELE'
				msg = args[0].to_i - 1
				m = @@messages[msg]
				if m
					m.deleted = true
					post "+OK message deleted"
				else
					post "-ERR no such message"
				end
			when 'NOOP'
				post "+OK"
			when 'RSET'
				@@messages.each do |m|
					m.deleted = false
				end
				post "+OK"
			when 'QUIT'
				update
			end
		end
	end

	def update
		p :update
		post "+OK"
		@@messages.reject! {|i| i.deleted }
		@s.close
	end

	private
	def post(msg="")
		puts msg
		@s.write("#{msg}\r\n")
	end

	def session(&block)
		while line = @s.gets
			line.chomp!
			puts line
			command, *args = *line.split(/\s+/)
			res = yield command, args
		end
	end

	class Message
		attr_accessor :deleted

		def initialize(file)
			@file = file
		end

		def content
			File.read(@file)
		end

		def size
			File.size(@file)
		end
	end
end

server = TCPServer.open(nil, 1110)
puts "User: #{USER}"
puts "Pass: #{PASS}"
puts "POP3: %3$s Port: %2$s" % server.addr

POP3Session.messages = MESSAGES.map {|f|
	POP3Session::Message.new(f)
}

loop do
	begin
		POP3Session.new(server.accept) # コネクションを1つずつしか受けつけないように
	rescue => e
		p e
	end
end

Mail Fetcher の挙動メモ

  • アカウント追加時に CAPA コマンドで UIDL コマンドが使えるかを見ている
  • TCPセッションごとのメッセージ番号が 1 から順番の数値になっていないダメ
  • 200件ごとにわけてインポートしようとする。200件ごとに STAT / LIST / RETR / DELE が走る
  • (Gmail 的に) 同じ Message-ID のメールは重複しないので、何度実行しても大丈夫
  • TRANSACTION フェーズで QUIT を発行したあと、こちらの応答を待たずに接続を切るっぽい
    • うまくメッセージの削除が行われないうちに次のセッションがはじまるので困る → 接続を1ずつにすることで解決
2011年 07月 26日

環境変数で設定を変える的なモジュール Config::ENV

なんかもっと簡単なのがあればいいなー と思ったのでつくりました。

package MyConfig;

use Config::ENV 'FOO_ENV';

common +{
	name => 'foobar',
};

config development => +{
	dsn_user => 'dbi:mysql:dbname=user;host=localhost',
};

config test => +{
	dsn_user => 'dbi:mysql:dbname=user;host=localhost',
};

config production => +{
	dsn_user => 'dbi:mysql:dbname=user;host=127.0.0.254',
};

config production_bot => +{
	parent('production'),
	bot => 1,
};

#

use MyConfig;

MyConfig->param('dsn_user');

みたいに書くと、$ENV{FOO_ENV} の値によって別の値をつかうようにするってだけです。

設定ファイルを別にわけたいみたいなのは適当に do したらいいだけだし、切替える機能だけほしいのです。

gerry++

2011年 07月 25日

gerry++

AnyEvent::setTimeout

AE::timer のインターフェイスが覚えられないなーと思うので、JS の setTimeout に似たものを書きました。

use AnyEvent::setTimeout;

setTimeout(sub {
  warn "1sec!";
}, 1000);

とか書けます。ちょっと細工をしていて、ファイルの最後に AnyEvent->condvar->recv とかおまじないを書かなくても、終了時に全てのタイマーの終了を待ちます (node.js 風にしたいけどできないのか的なことを言ってたら id:motemen:detail 先生ができそうなことを言ってくれたのでやってみた感じです)

ただ、このモジュールの管理下にあるタイマーしか終了時に待てないのがいまいち感あります……

とりあえず setTimeout さえあれば JSDeferred をそのまんま移植できるなーと思った感じなんですが、それやって意味があるのか謎なのでやめました。

まぁこういうの書いてるうちに AE::timer のインターフェイスとか覚えちゃうんですけど……

2011年 07月 21日

HTML メール

どうやって作るのが簡単なんでしょうか… 以下のようにしがちですがよくわかりません

use Encode;
use Email::MIME;
use Email::MIME::CreateHTML;

my $html = <<EOS;
<!DOCTYPE html>
<head>
<title>タイトル</title>
</head>
<body>
	<h1>ああああ</h1>
</body>
</html>
EOS

my $text = <<EOS;
テスト
EOS

my $email = Email::MIME->create_html(
	header => [
		From    => 'from@example.com',
		To      => 'cho45@lowreal.net',
		Subject => encode("MIME-Header-ISO_2022_JP", 'タイトル'),
	],

	body => encode('iso-2022-jp', $html),
	body_attributes => {
		content_type => 'text/html',
		charset      => 'iso-2022-jp',
		encoding     => 'base64',
	},

	text_body => encode('iso-2022-jp', $text),
	text_body_attributes => {
		content_type => 'text/plain',
		charset      => 'iso-2022-jp',
	},
);

print $email->as_string;

#use Email::Send;
#my $sender = Email::Send->new({mailer => 'SMTP'});
#$sender->send($email);
2011年 07月 20日

gerry++

2011年 07月 12日

gerry++

2011年 07月 10日

gerry++