vmail 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'vmail'
5
+ rescue LoadError
6
+ require 'rubygems'
7
+ require 'gmail'
8
+ end
9
+
10
+ Vmail.start
11
+
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'
@@ -0,0 +1,8 @@
1
+ login: dhchoi@gmail.com
2
+ name: Daniel Choi
3
+ password: somepassword
4
+ signature: |
5
+ --
6
+ Sent via vmail. http://danielchoi.com
7
+ drb_uri: druby://127.0.0.1:61676
8
+
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