vmail 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/NOTES +541 -0
- data/README.markdown +29 -0
- data/Rakefile +21 -0
- data/bin/vmail +11 -0
- data/bin/vmail_client +13 -0
- data/config/environment.rb +18 -0
- data/config/gmail.orig.yml +8 -0
- data/gmail.vim +180 -0
- data/lib/contacts_extractor.rb +50 -0
- data/lib/gmail.rb +145 -0
- data/lib/vmail.rb +45 -0
- data/lib/vmail/imap_client.rb +495 -0
- data/lib/vmail/message_formatter.rb +113 -0
- data/lib/vmail/string_ext.rb +11 -0
- data/lib/vmail/version.rb +3 -0
- data/test/base64_test.rb +13 -0
- data/test/fixtures/euc-kr-header.eml +23 -0
- data/test/fixtures/euc-kr-html.eml +162 -0
- data/test/fixtures/google-affiliate.eml +1049 -0
- data/test/fixtures/htmlbody.eml +68 -0
- data/test/fixtures/moleskine-html.eml +82 -0
- data/test/fixtures/textbody-nocontenttype.eml +118 -0
- data/test/fixtures/with-attachments.eml +123 -0
- data/test/message_formatter_test.rb +84 -0
- data/test/test_helper.rb +10 -0
- data/test/time_format_test.rb +15 -0
- data/viewer.vim +623 -0
- data/vmail.gemspec +23 -0
- data/wrapper.rb +8 -0
- metadata +117 -0
data/README.markdown
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# vmail
|
2
|
+
|
3
|
+
This project (soon to be documented) provides a Vim interface to Gmail.
|
4
|
+
|
5
|
+
## Open Source License
|
6
|
+
|
7
|
+
The source code for vmail is governed by the MIT License, which reads as
|
8
|
+
follows:
|
9
|
+
|
10
|
+
Copyright (c) 2010 Daniel Choi
|
11
|
+
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
13
|
+
of this software and associated documentation files (the "Software"), to
|
14
|
+
deal in the Software without restriction, including without limitation the
|
15
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
16
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
17
|
+
furnished to do so, subject to the following conditions:
|
18
|
+
|
19
|
+
The above copyright notice and this permission notice shall be included in
|
20
|
+
all copies or substantial portions of the Software.
|
21
|
+
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
27
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
28
|
+
IN THE SOFTWARE.
|
29
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'bundler'
|
4
|
+
Bundler::GemHelper.install_tasks
|
5
|
+
|
6
|
+
task :environment do
|
7
|
+
require(File.join(File.dirname(__FILE__), 'config', 'environment'))
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "Run tests"
|
11
|
+
task :test => :environment do
|
12
|
+
$:.unshift File.expand_path("test")
|
13
|
+
require 'test_helper'
|
14
|
+
require 'time_format_test'
|
15
|
+
require 'message_formatter_test'
|
16
|
+
require 'base64_test'
|
17
|
+
MiniTest::Unit.autorun
|
18
|
+
end
|
19
|
+
|
20
|
+
task :default => :test
|
21
|
+
|
data/bin/vmail
ADDED
data/bin/vmail_client
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'drb'
|
3
|
+
|
4
|
+
server = DRbObject.new_with_uri ARGV.shift
|
5
|
+
method = ARGV.shift
|
6
|
+
|
7
|
+
if method == 'deliver' || method == 'save_draft'
|
8
|
+
text = STDIN.read
|
9
|
+
puts server.send(method, text)
|
10
|
+
else
|
11
|
+
puts server.send(method, *ARGV)
|
12
|
+
end
|
13
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# configure activerecord to use mysql
|
2
|
+
#require 'active_record'
|
3
|
+
require 'logger'
|
4
|
+
require 'yaml'
|
5
|
+
require 'net/imap'
|
6
|
+
require 'mail'
|
7
|
+
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
8
|
+
|
9
|
+
#config_file = File.join(File.dirname(__FILE__), 'database.yml')
|
10
|
+
#config = YAML::load(File.read(config_file))['development']
|
11
|
+
#ActiveRecord::Base.establish_connection config
|
12
|
+
|
13
|
+
gmail_config_file = File.join(File.dirname(__FILE__), 'gmail.yml')
|
14
|
+
gmail_config = YAML::load(File.read(gmail_config_file))
|
15
|
+
require 'gmail'
|
16
|
+
$gmail = Gmail.new gmail_config['login'], gmail_config['password']
|
17
|
+
|
18
|
+
require 'message_formatter'
|
data/gmail.vim
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
" read the mailboxes list
|
2
|
+
|
3
|
+
let s:messagebufnr = -1
|
4
|
+
|
5
|
+
function! s:CreateWindowA()
|
6
|
+
let l:res = system("ruby bin/mailboxes.rb")
|
7
|
+
put =res
|
8
|
+
1delete
|
9
|
+
setlocal bufhidden=delete
|
10
|
+
setlocal buftype=nofile
|
11
|
+
setlocal nomodifiable
|
12
|
+
setlocal noswapfile
|
13
|
+
setlocal nowrap
|
14
|
+
setlocal nonumber
|
15
|
+
setlocal foldcolumn=0
|
16
|
+
setlocal nospell
|
17
|
+
setlocal nobuflisted
|
18
|
+
setlocal textwidth=0
|
19
|
+
setlocal noreadonly
|
20
|
+
setlocal cursorline
|
21
|
+
let s:mailboxesbufnr = bufnr('%')
|
22
|
+
let s:dataDir = expand('%:h')
|
23
|
+
endfunction
|
24
|
+
|
25
|
+
call s:CreateWindowA()
|
26
|
+
let s:currentBox = getline('.')
|
27
|
+
|
28
|
+
function! s:CreateWindowB()
|
29
|
+
split List
|
30
|
+
setlocal bufhidden=delete
|
31
|
+
setlocal buftype=nofile
|
32
|
+
" setlocal nomodifiable
|
33
|
+
setlocal noswapfile
|
34
|
+
setlocal nomodifiable
|
35
|
+
setlocal nowrap
|
36
|
+
setlocal nonumber
|
37
|
+
setlocal foldcolumn=0
|
38
|
+
setlocal nospell
|
39
|
+
setlocal nobuflisted
|
40
|
+
setlocal textwidth=0
|
41
|
+
setlocal noreadonly
|
42
|
+
" hi CursorLine cterm=NONE ctermbg=darkred ctermfg=white guibg=darkred guifg=white
|
43
|
+
setlocal cursorline
|
44
|
+
let s:listbufnr = bufnr('%')
|
45
|
+
endfunction
|
46
|
+
|
47
|
+
" the message display buffer window
|
48
|
+
function! s:CreateWindowC()
|
49
|
+
bot vne Message
|
50
|
+
setlocal buftype=nofile
|
51
|
+
setlocal noswapfile
|
52
|
+
let s:messagebufnr = bufnr('%')
|
53
|
+
endfunction
|
54
|
+
|
55
|
+
call s:CreateWindowC()
|
56
|
+
call s:CreateWindowB()
|
57
|
+
|
58
|
+
" switch to WindowA
|
59
|
+
1 wincmd w
|
60
|
+
vertical resize 20
|
61
|
+
2 wincmd w
|
62
|
+
|
63
|
+
function! s:ListMessages()
|
64
|
+
" load mailbox TOC
|
65
|
+
1 wincmd w
|
66
|
+
let s:selected_mailbox = getline(".")
|
67
|
+
2 wincmd w " window 2 is the List
|
68
|
+
" fetch data
|
69
|
+
call s:refresh_message_list()
|
70
|
+
1 wincmd w
|
71
|
+
endfunction
|
72
|
+
|
73
|
+
|
74
|
+
function! s:ShowMessage()
|
75
|
+
" assume we're in window 2
|
76
|
+
let line = getline(line("."))
|
77
|
+
let l:uid = matchstr(line, '^\d\+')
|
78
|
+
|
79
|
+
let s:uid = l:uid
|
80
|
+
|
81
|
+
3 wincmd w
|
82
|
+
1,$delete
|
83
|
+
" fetch data
|
84
|
+
let l:res = system("ruby bin/message.rb " . shellescape(s:selected_mailbox) . " " . shellescape(s:uid))
|
85
|
+
put =res
|
86
|
+
|
87
|
+
1delete
|
88
|
+
normal 1
|
89
|
+
normal jk
|
90
|
+
wincmd p
|
91
|
+
endfunction
|
92
|
+
|
93
|
+
1 wincmd w
|
94
|
+
|
95
|
+
|
96
|
+
function! s:UpdateMailbox()
|
97
|
+
echo "Updating ". s:selected_mailbox
|
98
|
+
let res = system("ruby bin/update.rb " . shellescape(s:selected_mailbox) . " >> update.log 2>&1 &")
|
99
|
+
endfunction
|
100
|
+
|
101
|
+
function! s:reprint_message_list()
|
102
|
+
setlocal modifiable
|
103
|
+
1,$delete
|
104
|
+
put =s:res
|
105
|
+
1delete
|
106
|
+
setlocal nomodifiable
|
107
|
+
normal G
|
108
|
+
endfunction
|
109
|
+
|
110
|
+
function! s:refresh_message_list()
|
111
|
+
" assume we're in the message list window
|
112
|
+
2 wincmd w
|
113
|
+
let s:res = system("ruby bin/messages.rb " . shellescape(s:selected_mailbox))
|
114
|
+
call s:reprint_message_list()
|
115
|
+
endfunction
|
116
|
+
|
117
|
+
function! s:show_next_message()
|
118
|
+
2 wincmd w
|
119
|
+
normal j
|
120
|
+
endfunction
|
121
|
+
|
122
|
+
function! s:page_message_down()
|
123
|
+
3wincmd w
|
124
|
+
" check if we're already showing the bottom
|
125
|
+
if line('w$') == line('$')
|
126
|
+
" show next message
|
127
|
+
call s:show_next_message()
|
128
|
+
else
|
129
|
+
exe "normal \<c-f>"
|
130
|
+
endif
|
131
|
+
endfunction
|
132
|
+
|
133
|
+
function! s:search_mailbox()
|
134
|
+
let query = input("search:")
|
135
|
+
let s:res = system("ruby bin/search.rb " . shellescape(s:selected_mailbox) . " " . shellescape(query))
|
136
|
+
" fetch data
|
137
|
+
call s:reprint_message_list()
|
138
|
+
endfunction
|
139
|
+
|
140
|
+
" can map number keys to focus windows and also to alter layout
|
141
|
+
|
142
|
+
noremap <Leader>1 :execute "1wincmd w"<CR>
|
143
|
+
noremap <Leader>2 :execute "2wincmd w"<CR>
|
144
|
+
noremap <Leader>3 :execute "3wincmd w"<CR>
|
145
|
+
|
146
|
+
3 wincmd w
|
147
|
+
noremap <silent> <buffer> <space> :call <SID>page_message_down()<CR>
|
148
|
+
|
149
|
+
2 wincmd w
|
150
|
+
autocmd CursorMoved <buffer> call <SID>ShowMessage()
|
151
|
+
noremap <silent> <buffer> u :call <SID>UpdateMailbox()<CR>
|
152
|
+
noremap <silent> <buffer> r :call <SID>refresh_message_list()<CR>
|
153
|
+
noremap <silent> <buffer> <space> :call <SID>page_message_down()<CR>
|
154
|
+
noremap <silent> <buffer> s :call <SID>search_mailbox()<CR>
|
155
|
+
|
156
|
+
1 wincmd w
|
157
|
+
autocmd CursorMoved <buffer> call <SID>ListMessages()
|
158
|
+
noremap <silent> <buffer> u :call <SID>UpdateMailbox()<CR>
|
159
|
+
noremap <silent> <buffer> r :call <SID>refresh_message_list()<CR>
|
160
|
+
|
161
|
+
finish
|
162
|
+
|
163
|
+
|
164
|
+
function! s:GotoMessageWindow()
|
165
|
+
let currentbufnr = bufnr('%')
|
166
|
+
if bufwinnr(s:messagebufnr) != -1
|
167
|
+
wincmd w
|
168
|
+
while bufnr('%') != s:messagebufnr && bufnr('%') != currentbufnr
|
169
|
+
wincmd w
|
170
|
+
endwhile
|
171
|
+
endif
|
172
|
+
if bufnr('%') != s:messagebufnr
|
173
|
+
call s:CreateWindowC()
|
174
|
+
end
|
175
|
+
endfunction
|
176
|
+
noremap <silent> <buffer> <CR> :call <SID>GmailVimShowMessage()<CR>
|
177
|
+
|
178
|
+
|
179
|
+
|
180
|
+
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'net/imap'
|
2
|
+
|
3
|
+
class ContactsExtractor
|
4
|
+
def initialize(config)
|
5
|
+
@username, @password = config['login'], config['password']
|
6
|
+
end
|
7
|
+
|
8
|
+
def open
|
9
|
+
@imap = Net::IMAP.new('imap.gmail.com', 993, true, nil, false)
|
10
|
+
@imap.login(@username, @password)
|
11
|
+
yield @imap
|
12
|
+
@imap.close
|
13
|
+
@imap.disconnect
|
14
|
+
end
|
15
|
+
|
16
|
+
def extract!
|
17
|
+
@contacts = []
|
18
|
+
open do |imap|
|
19
|
+
mailbox = '[Gmail]/Sent Mail'
|
20
|
+
STDERR.puts "selecting #{mailbox}"
|
21
|
+
imap.select(mailbox)
|
22
|
+
STDERR.puts "fetching last 500 sent messages"
|
23
|
+
all_uids = imap.uid_search('ALL')
|
24
|
+
limit = [500, all_uids.size].max
|
25
|
+
uids = all_uids[-limit ,limit]
|
26
|
+
imap.uid_fetch(uids, ["FLAGS", "ENVELOPE"]).each do |fetch_data|
|
27
|
+
recipients = fetch_data.attr["ENVELOPE"].to
|
28
|
+
next unless recipients
|
29
|
+
recipients.each do |address_struct|
|
30
|
+
email = [address_struct.mailbox, address_struct.host].join('@')
|
31
|
+
name = address_struct.name
|
32
|
+
if name
|
33
|
+
puts "#{name} <#{email}>"
|
34
|
+
else
|
35
|
+
puts email
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
@contacts
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
if __FILE__ == $0
|
45
|
+
require 'yaml'
|
46
|
+
config = YAML::load(File.read(File.expand_path("../../config/gmail.yml", __FILE__)))
|
47
|
+
extractor = ContactsExtractor.new(config)
|
48
|
+
# pipe through uniq | sort
|
49
|
+
extractor.extract!
|
50
|
+
end
|
data/lib/gmail.rb
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'net/imap'
|
2
|
+
|
3
|
+
class Gmail
|
4
|
+
DEMARC = "------=gmail-tool="
|
5
|
+
|
6
|
+
def initialize(username, password)
|
7
|
+
@username, @password = username, password
|
8
|
+
end
|
9
|
+
|
10
|
+
def open
|
11
|
+
raise "block missing" unless block_given?
|
12
|
+
@imap = Net::IMAP.new('imap.gmail.com', 993, true, nil, false)
|
13
|
+
@imap.login(@username, @password)
|
14
|
+
yield @imap
|
15
|
+
rescue Exception => ex
|
16
|
+
raise
|
17
|
+
ensure
|
18
|
+
if @imap
|
19
|
+
@imap.close rescue Net::IMAP::BadResponseError
|
20
|
+
@imap.disconnect
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# lists mailboxes
|
25
|
+
def mailboxes
|
26
|
+
open do |imap|
|
27
|
+
imap.list("[Gmail]/", "%") + imap.list("", "%")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# selects the mailbox and returns self
|
32
|
+
def mailbox(x)
|
33
|
+
@mailbox = x
|
34
|
+
# allow chaining
|
35
|
+
return self
|
36
|
+
end
|
37
|
+
|
38
|
+
def fetch(opts = {})
|
39
|
+
num_messages = opts[:num_messages] || 10
|
40
|
+
mailbox_label = opts[:mailbox] || @mailbox || 'inbox'
|
41
|
+
query = opts[:query] || ["ALL"]
|
42
|
+
open do |imap|
|
43
|
+
imap.select(mailbox_label)
|
44
|
+
all_uids = imap.uid_search(query)
|
45
|
+
STDERR.puts "#{all_uids.size} UIDS TOTAL"
|
46
|
+
uids = all_uids[-([num_messages, all_uids.size].min)..-1] || []
|
47
|
+
STDERR.puts "imap process uids #{uids.inspect}"
|
48
|
+
yield imap, uids
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# generic mailbox operations
|
53
|
+
def imap
|
54
|
+
open do |imap|
|
55
|
+
imap.select((@mailbox || 'inbox'))
|
56
|
+
yield imap
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
class String
|
63
|
+
def col(width)
|
64
|
+
self[0,width].ljust(width)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def format_time(x)
|
69
|
+
Time.parse(x.to_s).localtime.strftime "%D %I:%M%P"
|
70
|
+
end
|
71
|
+
|
72
|
+
require 'time'
|
73
|
+
|
74
|
+
def search
|
75
|
+
mailbox = ARGV.shift
|
76
|
+
num_messages = ARGV.shift.to_i
|
77
|
+
#query = ["BODY", "politics"]
|
78
|
+
query = ARGV
|
79
|
+
puts mailbox
|
80
|
+
$gmail.mailbox(mailbox).fetch(:num_messages => num_messages, :query => query) do |imap,uids|
|
81
|
+
uids.each do |uid|
|
82
|
+
res = imap.uid_fetch(uid, ["FLAGS", "BODY", "ENVELOPE", "RFC822.HEADER"])[0]
|
83
|
+
#puts res.inspect
|
84
|
+
#puts res
|
85
|
+
header = res.attr["RFC822.HEADER"]
|
86
|
+
mail = Mail.new(header)
|
87
|
+
mail_id = uid
|
88
|
+
flags = res.attr["FLAGS"]
|
89
|
+
puts "#{mail_id} #{format_time(mail.date.to_s)} #{mail.from[0][0,30].ljust(30)} #{mail.subject.to_s[0,70].ljust(70)} #{flags.inspect.col(30)}"
|
90
|
+
|
91
|
+
next
|
92
|
+
|
93
|
+
mail = Mail.new(res)
|
94
|
+
foldline = [mail[:from], mail[:date], mail[:subject]].join(" ")
|
95
|
+
puts foldline + " {{{1"
|
96
|
+
if mail.parts.empty?
|
97
|
+
puts mail.body.decoded
|
98
|
+
else
|
99
|
+
puts mail.parts.inspect
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def lookup(raw=false)
|
106
|
+
mailbox, uid = *ARGV[0,2]
|
107
|
+
$gmail.mailbox(mailbox).imap do |imap|
|
108
|
+
res = imap.uid_fetch(uid.to_i, ["FLAGS", "RFC822"])[0].attr["RFC822"]
|
109
|
+
if raw
|
110
|
+
puts res
|
111
|
+
return
|
112
|
+
end
|
113
|
+
mail = Mail.new(res)
|
114
|
+
if mail.parts.empty?
|
115
|
+
puts mail.header["Content-Type"]
|
116
|
+
puts mail.body.charset
|
117
|
+
puts mail.body.decoded
|
118
|
+
else
|
119
|
+
puts mail.parts.inspect
|
120
|
+
part = mail.parts.detect {|part|
|
121
|
+
(part.header["Content-Type"].to_s =~ /text\/plain/)
|
122
|
+
}
|
123
|
+
if part
|
124
|
+
puts "PART"
|
125
|
+
puts part.header["Content-Type"]
|
126
|
+
puts part.charset
|
127
|
+
puts part.body.decoded
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
if __FILE__ == $0
|
134
|
+
require 'yaml'
|
135
|
+
require 'mail'
|
136
|
+
config = YAML::load(File.read(File.expand_path("../../config/gmail.yml", __FILE__)))
|
137
|
+
$gmail = Gmail.new(config['login'], config['password'])
|
138
|
+
if ARGV.length == 2
|
139
|
+
lookup
|
140
|
+
elsif ARGV[2] == 'raw'
|
141
|
+
lookup(raw=true)
|
142
|
+
else
|
143
|
+
search
|
144
|
+
end
|
145
|
+
end
|