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/.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/lib/vmail.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'vmail/imap_client'
|
2
|
+
|
3
|
+
module Vmail
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def start
|
7
|
+
config = YAML::load(File.read(File.expand_path("~/gmail.yml")))
|
8
|
+
config.merge! 'logfile' => "vmail.log"
|
9
|
+
|
10
|
+
puts "starting vmail imap client with config #{config}"
|
11
|
+
|
12
|
+
drb_uri = Vmail::ImapClient.daemon config
|
13
|
+
|
14
|
+
server = DRbObject.new_with_uri drb_uri
|
15
|
+
server.window_width = `stty size`.strip.split(' ')[1]
|
16
|
+
server.select_mailbox ARGV.shift || 'INBOX'
|
17
|
+
|
18
|
+
query = ARGV.empty? ? [100, 'ALL'] : nil
|
19
|
+
|
20
|
+
buffer_file = "vmail-buffer.txt"
|
21
|
+
File.open(buffer_file, "w") do |file|
|
22
|
+
file.puts server.search(*query)
|
23
|
+
end
|
24
|
+
|
25
|
+
# invoke vim
|
26
|
+
# TODO
|
27
|
+
# - mvim; move viewer.vim to new file
|
28
|
+
|
29
|
+
vimscript = "viewer.vim"
|
30
|
+
system("DRB_URI='#{drb_uri}' vim -S #{vimscript} #{buffer_file}")
|
31
|
+
|
32
|
+
File.delete(buffer_file)
|
33
|
+
|
34
|
+
puts "closing imap connection"
|
35
|
+
begin
|
36
|
+
Timeout::timeout(5) do
|
37
|
+
$gmail.close
|
38
|
+
end
|
39
|
+
rescue Timeout::Error
|
40
|
+
puts "close connection attempt timed out"
|
41
|
+
end
|
42
|
+
puts "bye"
|
43
|
+
exit
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,495 @@
|
|
1
|
+
require 'drb'
|
2
|
+
require 'vmail/message_formatter'
|
3
|
+
require 'vmail/string_ext'
|
4
|
+
require 'yaml'
|
5
|
+
require 'mail'
|
6
|
+
require 'net/imap'
|
7
|
+
require 'time'
|
8
|
+
require 'logger'
|
9
|
+
|
10
|
+
module Vmail
|
11
|
+
class ImapClient
|
12
|
+
|
13
|
+
MailboxAliases = { 'sent' => '[Gmail]/Sent Mail',
|
14
|
+
'all' => '[Gmail]/All Mail',
|
15
|
+
'starred' => '[Gmail]/Starred',
|
16
|
+
'important' => '[Gmail]/Important',
|
17
|
+
'drafts' => '[Gmail]/Drafts',
|
18
|
+
'spam' => '[Gmail]/Spam',
|
19
|
+
'trash' => '[Gmail]/Trash'
|
20
|
+
}
|
21
|
+
|
22
|
+
def initialize(config)
|
23
|
+
@username, @password = config['username'], config['password']
|
24
|
+
@name = config['name']
|
25
|
+
@signature = config['signature']
|
26
|
+
@mailbox = nil
|
27
|
+
@logger = Logger.new(config['logfile'] || STDERR)
|
28
|
+
@logger.level = Logger::DEBUG
|
29
|
+
@current_message = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def open
|
33
|
+
@imap = Net::IMAP.new('imap.gmail.com', 993, true, nil, false)
|
34
|
+
@imap.login(@username, @password)
|
35
|
+
end
|
36
|
+
|
37
|
+
def close
|
38
|
+
log "closing connection"
|
39
|
+
@imap.close rescue Net::IMAP::BadResponseError
|
40
|
+
@imap.disconnect
|
41
|
+
end
|
42
|
+
|
43
|
+
def select_mailbox(mailbox)
|
44
|
+
if MailboxAliases[mailbox]
|
45
|
+
mailbox = MailboxAliases[mailbox]
|
46
|
+
end
|
47
|
+
if mailbox == @mailbox
|
48
|
+
return
|
49
|
+
end
|
50
|
+
log "selecting mailbox #{mailbox.inspect}"
|
51
|
+
reconnect_if_necessary do
|
52
|
+
@imap.select(mailbox)
|
53
|
+
end
|
54
|
+
@mailbox = mailbox
|
55
|
+
@all_uids = []
|
56
|
+
@bad_uids = []
|
57
|
+
return "OK"
|
58
|
+
end
|
59
|
+
|
60
|
+
def revive_connection
|
61
|
+
log "reviving connection"
|
62
|
+
open
|
63
|
+
log "reselecting mailbox #@mailbox"
|
64
|
+
@imap.select(@mailbox)
|
65
|
+
end
|
66
|
+
|
67
|
+
def list_mailboxes
|
68
|
+
@mailboxes ||= (@imap.list("[Gmail]/", "%") + @imap.list("", "%")).
|
69
|
+
select {|struct| struct.attr.none? {|a| a == :Noselect} }.
|
70
|
+
map {|struct| struct.name}.
|
71
|
+
map {|name| MailboxAliases.invert[name] || name}
|
72
|
+
@mailboxes.delete("INBOX")
|
73
|
+
@mailboxes.unshift("INBOX")
|
74
|
+
@mailboxes.join("\n")
|
75
|
+
end
|
76
|
+
|
77
|
+
def fetch_headers(uid_set)
|
78
|
+
if uid_set.is_a?(String)
|
79
|
+
uid_set = uid_set.split(",").map(&:to_i)
|
80
|
+
elsif uid_set.is_a?(Integer)
|
81
|
+
uid_set = [uid_set]
|
82
|
+
end
|
83
|
+
max_uid = uid_set.max
|
84
|
+
log "fetch headers for #{uid_set.inspect}"
|
85
|
+
if uid_set.empty?
|
86
|
+
log "empty set"
|
87
|
+
return ""
|
88
|
+
end
|
89
|
+
results = reconnect_if_necessary do
|
90
|
+
@imap.uid_fetch(uid_set, ["FLAGS", "ENVELOPE", "RFC822.SIZE" ])
|
91
|
+
end
|
92
|
+
log "extracting headers"
|
93
|
+
lines = results.sort_by {|x| Time.parse(x.attr['ENVELOPE'].date)}.map {|x| format_header(x, max_uid)}
|
94
|
+
log "returning result"
|
95
|
+
return lines.join("\n")
|
96
|
+
end
|
97
|
+
|
98
|
+
def format_header(fetch_data, max_uid=nil)
|
99
|
+
uid = fetch_data.attr["UID"]
|
100
|
+
envelope = fetch_data.attr["ENVELOPE"]
|
101
|
+
size = fetch_data.attr["RFC822.SIZE"]
|
102
|
+
flags = fetch_data.attr["FLAGS"]
|
103
|
+
address_struct = if @mailbox == '[Gmail]/Sent Mail'
|
104
|
+
structs = envelope.to || envelope.cc
|
105
|
+
structs.nil? ? nil : structs.first
|
106
|
+
else
|
107
|
+
envelope.from.first
|
108
|
+
end
|
109
|
+
address = if address_struct.nil?
|
110
|
+
"unknown"
|
111
|
+
elsif address_struct.name
|
112
|
+
"#{Mail::Encodings.unquote_and_convert_to(address_struct.name, 'utf-8')} <#{[address_struct.mailbox, address_struct.host].join('@')}>"
|
113
|
+
else
|
114
|
+
[address_struct.mailbox, address_struct.host].join('@')
|
115
|
+
end
|
116
|
+
if @mailbox == '[Gmail]/Sent Mail' && envelope.to && envelope.cc
|
117
|
+
total_recips = (envelope.to + envelope.cc).size
|
118
|
+
address += " + #{total_recips - 1}"
|
119
|
+
end
|
120
|
+
date = Time.parse(envelope.date).localtime
|
121
|
+
date_formatted = if date.year != Time.now.year
|
122
|
+
date.strftime "%b %d %Y" rescue envelope.date.to_s
|
123
|
+
else
|
124
|
+
date.strftime "%b %d %I:%M%P" rescue envelope.date.to_s
|
125
|
+
end
|
126
|
+
subject = envelope.subject || ''
|
127
|
+
subject = Mail::Encodings.unquote_and_convert_to(subject, 'utf-8')
|
128
|
+
flags = format_flags(flags)
|
129
|
+
first_col_width = max_uid.to_s.length
|
130
|
+
mid_width = @width - (first_col_width + 14 + 2) - (10 + 2) - 5
|
131
|
+
address_col_width = (mid_width * 0.3).ceil
|
132
|
+
subject_col_width = (mid_width * 0.7).floor
|
133
|
+
[uid.to_s.col(first_col_width),
|
134
|
+
(date_formatted || '').col(14),
|
135
|
+
address.col(address_col_width),
|
136
|
+
subject.encode('utf-8').col(subject_col_width),
|
137
|
+
number_to_human_size(size).rcol(6),
|
138
|
+
flags.rcol(7)].join(' ')
|
139
|
+
end
|
140
|
+
|
141
|
+
UNITS = [:b, :kb, :mb, :gb].freeze
|
142
|
+
|
143
|
+
# borrowed from ActionView/Helpers
|
144
|
+
def number_to_human_size(number)
|
145
|
+
if number.to_i < 1024
|
146
|
+
"#{number} b"
|
147
|
+
else
|
148
|
+
max_exp = UNITS.size - 1
|
149
|
+
exponent = (Math.log(number) / Math.log(1024)).to_i # Convert to base 1024
|
150
|
+
exponent = max_exp if exponent > max_exp # we need this to avoid overflow for the highest unit
|
151
|
+
number /= 1024 ** exponent
|
152
|
+
unit = UNITS[exponent]
|
153
|
+
"#{number} #{unit}"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
|
158
|
+
FLAGMAP = {:Flagged => '[*]'}
|
159
|
+
# flags is an array like [:Flagged, :Seen]
|
160
|
+
def format_flags(flags)
|
161
|
+
flags = flags.map {|flag| FLAGMAP[flag] || flag}
|
162
|
+
if flags.delete(:Seen).nil?
|
163
|
+
flags << '[+]' # unread
|
164
|
+
end
|
165
|
+
flags.join('')
|
166
|
+
end
|
167
|
+
|
168
|
+
def search(limit, *query)
|
169
|
+
log "uid_search limit: #{limit} query: #{@query.inspect}"
|
170
|
+
limit = 25 if limit.to_s !~ /^\d+$/
|
171
|
+
query = ['ALL'] if query.empty?
|
172
|
+
@query = query.join(' ')
|
173
|
+
log "uid_search #@query #{limit}"
|
174
|
+
@all_uids = reconnect_if_necessary do
|
175
|
+
@imap.uid_search(@query)
|
176
|
+
end
|
177
|
+
uids = @all_uids[-([limit.to_i, @all_uids.size].min)..-1] || []
|
178
|
+
res = fetch_headers(uids)
|
179
|
+
add_more_message_line(res, uids)
|
180
|
+
end
|
181
|
+
|
182
|
+
def update
|
183
|
+
reconnect_if_necessary(4) do
|
184
|
+
# this is just to prime the IMAP connection
|
185
|
+
# It's necessary for some reason.
|
186
|
+
log "priming connection for update"
|
187
|
+
res = @imap.uid_fetch(@all_uids[-1], ["ENVELOPE"])
|
188
|
+
if res.nil?
|
189
|
+
raise IOError, "IMAP connection seems broken"
|
190
|
+
end
|
191
|
+
end
|
192
|
+
uids = reconnect_if_necessary {
|
193
|
+
log "uid_search #@query"
|
194
|
+
@imap.uid_search(@query)
|
195
|
+
}
|
196
|
+
new_uids = uids - @all_uids
|
197
|
+
log "UPDATE: NEW UIDS: #{new_uids.inspect}"
|
198
|
+
if !new_uids.empty?
|
199
|
+
res = fetch_headers(new_uids)
|
200
|
+
@all_uids = uids
|
201
|
+
res
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# gets 100 messages prior to uid
|
206
|
+
def more_messages(uid, limit=100)
|
207
|
+
uid = uid.to_i
|
208
|
+
x = [(@all_uids.index(uid) - limit), 0].max
|
209
|
+
y = [@all_uids.index(uid) - 1, 0].max
|
210
|
+
uids = @all_uids[x..y]
|
211
|
+
res = fetch_headers(uids)
|
212
|
+
add_more_message_line(res, uids)
|
213
|
+
end
|
214
|
+
|
215
|
+
def add_more_message_line(res, uids)
|
216
|
+
return res if uids.empty?
|
217
|
+
start_index = @all_uids.index(uids[0])
|
218
|
+
if start_index > 0
|
219
|
+
remaining = start_index
|
220
|
+
res = "> Load #{[100, remaining].min} more messages. #{remaining} remaining.\n" + res
|
221
|
+
end
|
222
|
+
res
|
223
|
+
end
|
224
|
+
|
225
|
+
def lookup(uid, raw=false, forwarded=false)
|
226
|
+
if raw
|
227
|
+
return @current_message.to_s
|
228
|
+
end
|
229
|
+
log "fetching #{uid.inspect}"
|
230
|
+
fetch_data = reconnect_if_necessary do
|
231
|
+
@imap.uid_fetch(uid.to_i, ["FLAGS", "RFC822", "RFC822.SIZE"])[0]
|
232
|
+
end
|
233
|
+
res = fetch_data.attr["RFC822"]
|
234
|
+
mail = Mail.new(res)
|
235
|
+
@current_message = mail # used later to show raw message or extract attachments if any
|
236
|
+
formatter = MessageFormatter.new(mail)
|
237
|
+
part = formatter.find_text_part
|
238
|
+
out = formatter.process_body
|
239
|
+
size = fetch_data.attr["RFC822.SIZE"]
|
240
|
+
message = <<-EOF
|
241
|
+
#{@mailbox} #{uid} #{number_to_human_size size} #{forwarded ? nil : format_parts_info(formatter.list_parts)}
|
242
|
+
----------------------------------------
|
243
|
+
#{format_headers(formatter.extract_headers)}
|
244
|
+
|
245
|
+
#{out}
|
246
|
+
EOF
|
247
|
+
end
|
248
|
+
|
249
|
+
def format_parts_info(parts)
|
250
|
+
lines = parts.select {|part| part !~ %r{text/plain}}
|
251
|
+
if lines.size > 0
|
252
|
+
"\n#{lines.join("\n")}"
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
# uid_set is a string comming from the vim client
|
257
|
+
# action is -FLAGS or +FLAGS
|
258
|
+
def flag(uid_set, action, flg)
|
259
|
+
if uid_set.is_a?(String)
|
260
|
+
uid_set = uid_set.split(",").map(&:to_i)
|
261
|
+
end
|
262
|
+
# #<struct Net::IMAP::FetchData seqno=17423, attr={"FLAGS"=>[:Seen, "Flagged"], "UID"=>83113}>
|
263
|
+
log "flag #{uid_set} #{flg} #{action}"
|
264
|
+
if flg == 'Deleted'
|
265
|
+
# for delete, do in a separate thread because deletions are slow
|
266
|
+
Thread.new do
|
267
|
+
@imap.uid_copy(uid_set, "[Gmail]/Trash")
|
268
|
+
res = @imap.uid_store(uid_set, action, [flg.to_sym])
|
269
|
+
end
|
270
|
+
uid_set.each { |uid| @all_uids.delete(uid) }
|
271
|
+
elsif flg == '[Gmail]/Spam'
|
272
|
+
@imap.uid_copy(uid_set, "[Gmail]/Spam")
|
273
|
+
res = @imap.uid_store(uid_set, action, [:Deleted])
|
274
|
+
"#{uid} deleted"
|
275
|
+
else
|
276
|
+
log "Flagging"
|
277
|
+
res = @imap.uid_store(uid_set, action, [flg.to_sym])
|
278
|
+
# log res.inspect
|
279
|
+
fetch_headers(uid_set)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
# uid_set is a string comming from the vim client
|
284
|
+
def move_to(uid_set, mailbox)
|
285
|
+
if MailboxAliases[mailbox]
|
286
|
+
mailbox = MailboxAliases[mailbox]
|
287
|
+
end
|
288
|
+
log "move_to #{uid_set.inspect} #{mailbox}"
|
289
|
+
if uid_set.is_a?(String)
|
290
|
+
uid_set = uid_set.split(",").map(&:to_i)
|
291
|
+
end
|
292
|
+
log @imap.uid_copy(uid_set, mailbox)
|
293
|
+
log @imap.uid_store(uid_set, '+FLAGS', [:Deleted])
|
294
|
+
end
|
295
|
+
|
296
|
+
# TODO mark spam
|
297
|
+
|
298
|
+
def new_message_template
|
299
|
+
headers = {'from' => "#{@name} <#{@username}>",
|
300
|
+
'to' => nil,
|
301
|
+
'subject' => nil
|
302
|
+
}
|
303
|
+
format_headers(headers) + "\n\n"
|
304
|
+
end
|
305
|
+
|
306
|
+
def format_headers(hash)
|
307
|
+
lines = []
|
308
|
+
hash.each_pair do |key, value|
|
309
|
+
if value.is_a?(Array)
|
310
|
+
value = value.join(", ")
|
311
|
+
end
|
312
|
+
lines << "#{key.gsub("_", '-')}: #{value}"
|
313
|
+
end
|
314
|
+
lines.join("\n")
|
315
|
+
end
|
316
|
+
|
317
|
+
def reply_template(uid, replyall=false)
|
318
|
+
log "sending reply template for #{uid}"
|
319
|
+
fetch_data = @imap.uid_fetch(uid.to_i, ["FLAGS", "ENVELOPE", "RFC822"])[0]
|
320
|
+
envelope = fetch_data.attr['ENVELOPE']
|
321
|
+
recipient = [envelope.reply_to, envelope.from].flatten.map {|x| address_to_string(x)}[0]
|
322
|
+
cc = [envelope.to, envelope.cc]
|
323
|
+
cc = cc.flatten.compact.
|
324
|
+
select {|x| @username !~ /#{x.mailbox}@#{x.host}/}.
|
325
|
+
map {|x| address_to_string(x)}.join(", ")
|
326
|
+
mail = Mail.new fetch_data.attr['RFC822']
|
327
|
+
formatter = MessageFormatter.new(mail)
|
328
|
+
headers = formatter.extract_headers
|
329
|
+
subject = headers['subject']
|
330
|
+
if subject !~ /Re: /
|
331
|
+
subject = "Re: #{subject}"
|
332
|
+
end
|
333
|
+
cc = replyall ? cc : nil
|
334
|
+
date = headers['date'].is_a?(String) ? Time.parse(headers['date']) : headers['date']
|
335
|
+
quote_header = "On #{date.strftime('%a, %b %d, %Y at %I:%M %p')}, #{recipient} wrote:\n\n"
|
336
|
+
body = quote_header + formatter.process_body.gsub(/^(?=>)/, ">").gsub(/^(?!>)/, "> ")
|
337
|
+
reply_headers = { 'from' => "#@name <#@username>", 'to' => recipient, 'cc' => cc, 'subject' => headers['subject']}
|
338
|
+
format_headers(reply_headers) + "\n\n\n" + body + signature
|
339
|
+
end
|
340
|
+
|
341
|
+
def address_to_string(x)
|
342
|
+
x.name ? "#{x.name} <#{x.mailbox}@#{x.host}>" : "#{x.mailbox}@#{x.host}"
|
343
|
+
end
|
344
|
+
|
345
|
+
def signature
|
346
|
+
return '' unless @signature
|
347
|
+
"\n\n#@signature"
|
348
|
+
end
|
349
|
+
|
350
|
+
# TODO, forward with attachments
|
351
|
+
def forward_template(uid)
|
352
|
+
original_body = lookup(uid, false, true)
|
353
|
+
new_message_template +
|
354
|
+
"\n---------- Forwarded message ----------\n" +
|
355
|
+
original_body + signature
|
356
|
+
end
|
357
|
+
|
358
|
+
def deliver(text)
|
359
|
+
# parse the text. The headers are yaml. The rest is text body.
|
360
|
+
require 'net/smtp'
|
361
|
+
mail = new_mail_from_input(text)
|
362
|
+
mail.delivery_method(*smtp_settings)
|
363
|
+
mail.deliver!
|
364
|
+
"message '#{mail.subject}' sent"
|
365
|
+
end
|
366
|
+
|
367
|
+
def save_draft(text)
|
368
|
+
mail = new_mail_from_input(text)
|
369
|
+
log mail.to_s
|
370
|
+
reconnect_if_necessary do
|
371
|
+
log "saving draft"
|
372
|
+
log @imap.append("[Gmail]/Drafts", text.gsub(/\n/, "\r\n"), [:Seen], Time.now)
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
def new_mail_from_input(text)
|
377
|
+
require 'mail'
|
378
|
+
mail = Mail.new
|
379
|
+
raw_headers, body = *text.split(/\n\s*\n/, 2)
|
380
|
+
# handle attachments
|
381
|
+
if (attachments = body.split(/\n\s*\n/, 2)[0]) =~ /^attach:/
|
382
|
+
# TODO
|
383
|
+
log "attachments: #{YAML::load(attachments).inspect}"
|
384
|
+
end
|
385
|
+
headers = {}
|
386
|
+
raw_headers.split("\n").each do |line|
|
387
|
+
key, value = *line.split(/:\s*/, 2)
|
388
|
+
headers[key] = value
|
389
|
+
end
|
390
|
+
log "headers: #{headers.inspect}"
|
391
|
+
log "delivering: #{headers.inspect}"
|
392
|
+
mail.from = headers['from'] || @username
|
393
|
+
mail.to = headers['to'] #.split(/,\s+/)
|
394
|
+
mail.cc = headers['cc'] #&& headers['cc'].split(/,\s+/)
|
395
|
+
mail.bcc = headers['bcc'] #&& headers['cc'].split(/,\s+/)
|
396
|
+
mail.subject = headers['subject']
|
397
|
+
mail.from ||= @username
|
398
|
+
mail.body = body
|
399
|
+
mail
|
400
|
+
end
|
401
|
+
|
402
|
+
def save_attachments(dir)
|
403
|
+
log "save_attachments #{dir}"
|
404
|
+
if !@current_message
|
405
|
+
log "missing a current message"
|
406
|
+
end
|
407
|
+
return unless dir && @current_message
|
408
|
+
attachments = @current_message.attachments
|
409
|
+
`mkdir -p #{dir}`
|
410
|
+
attachments.each do |x|
|
411
|
+
path = File.join(dir, x.filename)
|
412
|
+
log "saving #{path}"
|
413
|
+
File.open(path, 'wb') {|f| f.puts x.decoded}
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
def open_html_part(uid)
|
418
|
+
log "open_html_part #{uid}"
|
419
|
+
fetch_data = @imap.uid_fetch(uid.to_i, ["RFC822"])[0]
|
420
|
+
mail = Mail.new(fetch_data.attr['RFC822'])
|
421
|
+
multipart = mail.parts.detect {|part| part.multipart?}
|
422
|
+
html_part = (multipart || mail).parts.detect {|part| part.header["Content-Type"].to_s =~ /text\/html/}
|
423
|
+
outfile = 'htmlpart.html'
|
424
|
+
File.open(outfile, 'w') {|f| f.puts(html_part.decoded)}
|
425
|
+
# client should handle opening the html file
|
426
|
+
return outfile
|
427
|
+
end
|
428
|
+
|
429
|
+
def window_width=(width)
|
430
|
+
log "setting window width to #{width}"
|
431
|
+
@width = width.to_i
|
432
|
+
end
|
433
|
+
|
434
|
+
def smtp_settings
|
435
|
+
[:smtp, {:address => "smtp.gmail.com",
|
436
|
+
:port => 587,
|
437
|
+
:domain => 'gmail.com',
|
438
|
+
:user_name => @username,
|
439
|
+
:password => @password,
|
440
|
+
:authentication => 'plain',
|
441
|
+
:enable_starttls_auto => true}]
|
442
|
+
end
|
443
|
+
|
444
|
+
def log(string)
|
445
|
+
@logger.debug string
|
446
|
+
end
|
447
|
+
|
448
|
+
def handle_error(error)
|
449
|
+
log error
|
450
|
+
end
|
451
|
+
|
452
|
+
def reconnect_if_necessary(timeout = 60, &block)
|
453
|
+
# if this times out, we know the connection is stale while the user is
|
454
|
+
# trying to update
|
455
|
+
Timeout::timeout(timeout) do
|
456
|
+
block.call
|
457
|
+
end
|
458
|
+
rescue IOError, Errno::EADDRNOTAVAIL, Timeout::Error
|
459
|
+
log "error: #{$!}"
|
460
|
+
log "attempting to reconnect"
|
461
|
+
log(revive_connection)
|
462
|
+
# try just once
|
463
|
+
block.call
|
464
|
+
end
|
465
|
+
|
466
|
+
def self.start(config)
|
467
|
+
imap_client = Vmail::ImapClient.new config
|
468
|
+
imap_client.open
|
469
|
+
imap_client
|
470
|
+
end
|
471
|
+
|
472
|
+
def self.daemon(config)
|
473
|
+
$gmail = self.start(config)
|
474
|
+
puts DRb.start_service(nil, $gmail)
|
475
|
+
uri = DRb.uri
|
476
|
+
puts "starting gmail service at #{uri}"
|
477
|
+
uri
|
478
|
+
end
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
trap("INT") {
|
483
|
+
require 'timeout'
|
484
|
+
puts "closing imap connection"
|
485
|
+
begin
|
486
|
+
Timeout::timeout(5) do
|
487
|
+
$gmail.close
|
488
|
+
end
|
489
|
+
rescue Timeout::Error
|
490
|
+
puts "close connection attempt timed out"
|
491
|
+
end
|
492
|
+
exit
|
493
|
+
}
|
494
|
+
|
495
|
+
|