vmail 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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