class Vmail::ImapClient

Attributes

max_seqno[RW]

Public Class Methods

daemon(config) click to toggle source
# File lib/vmail/imap_client.rb, line 529
def self.daemon(config)
  puts "Starting Vmail::ImapClient in dir #{ Dir.pwd }"
  $gmail = self.start(config)
  use_uri = config['drb_uri'] || nil # redundant but explicit
  DRb.start_service(use_uri, $gmail)
  uri = DRb.uri
  puts "Starting gmail service at #{ uri }"
  uri
end
new(config) click to toggle source
# File lib/vmail/imap_client.rb, line 31
def initialize(config)
  @username, @password = config['username'], config['password']
  #load user-specified value for from field
  @from = config['from'] || config['username']
  @name = config['name']
  @signature = config['signature']
  @signature_script = config['signature_script']
  @always_cc = config['always_cc']
  @always_bcc = config['always_bcc']
  @mailbox = nil
  @logger = Logger.new(config['logfile'] || STDERR)
  @logger.level = Logger::DEBUG
  $logger = @logger
  @imap_server = config['server'] || 'imap.gmail.com'
  @imap_port = config['port'] || 993
  # generic smtp settings
  @smtp_server = config['smtp_server'] || 'smtp.gmail.com'
  @smtp_port = config['smtp_port'] || 587
  @smtp_domain = config['smtp_domain'] || 'gmail.com'
  @authentication = config['authentication'] || 'plain'
  @width = 100

  @date_formatter_this_year = config['date_format'] || '%b %d %I:%M%P'
  @date_formatter_prev_years = config['date_format_previous_years'] || '%b %d %Y'
  @date_width = DateTime.parse("12/12/2012 12:12:12").strftime(@date_formatter_this_year).length

  mailbox_aliases_config = config['mailbox_aliases'] || {}
  @default_mailbox_aliases = Vmail::Defaults::MAILBOX_ALIASES.merge(
    mailbox_aliases_config)

  current_message = nil
end
start(config) click to toggle source
# File lib/vmail/imap_client.rb, line 523
def self.start(config)
  imap_client  = self.new config
  imap_client.open
  imap_client
end

Public Instance Methods

append_to_file(message_ids, file) click to toggle source
# File lib/vmail/imap_client.rb, line 297
def append_to_file(message_ids, file)
  message_ids = message_ids.split(',')
  log "Append to file uid set #{ message_ids.inspect } to file: #{ file }"
  message_ids.each do |message_id|
    message = show_message(message_id)
    File.open(file, 'a') {|f| f.puts(divider('=') + "\n" + message + "\n\n")}
    subject = (message[/^subject:(.*)/,1] || '').strip
    log "Appended message '#{ subject }'"
  end
  "Printed #{ message_ids.size } message#{ message_ids.size == 1 ? '' : 's' } to #{ file.strip }"
end
check_for_new_messages() click to toggle source
# File lib/vmail/imap_client.rb, line 208
def check_for_new_messages
  log "Checking for new messages"
  if search_query?
    log "Update aborted because query is search query: #{ @query.inspect }"
    return ""
  end
  old_num_messages = @num_messages
  # we need to re-select the mailbox to get the new highest id
  reload_mailbox
  update_query = @query.dup
  # set a new range filter
  # this may generate a negative rane, e.g., "19893:19992" but that seems harmless
  update_query[0] = "#{ old_num_messages }:#@num_messages"
  ids = reconnect_if_necessary {
    log "Search #update_query"
    @imap.search(Vmail::Query.args2string(update_query))
  }
  log "- got seqnos: #{ ids.inspect }"
  log "- getting seqnos > #{ self.max_seqno }"
  new_ids = ids.select {|seqno| seqno.to_i > self.max_seqno}
  # reset the max_seqno
  self.max_seqno = ids.max
  log "- setting max_seqno to #{ self.max_seqno }"
  log "- new uids found: #{ new_ids.inspect }"
  update_message_list(new_ids) unless new_ids.empty?
  new_ids
end
clear_cached_message() click to toggle source

TODO no need for this if all shown messages are stored in SQLITE3 and keyed by UID.

# File lib/vmail/imap_client.rb, line 117
def clear_cached_message
  return unless STDIN.tty?
  log "Clearing cached message"
  current_message = nil
end
close() click to toggle source
# File lib/vmail/imap_client.rb, line 81
def close
  log "Closing connection"
  Timeout::timeout(5) do
    @imap.close rescue Net::IMAP::BadResponseError
    @imap.disconnect rescue IOError
  end
rescue Timeout::Error
end
create_if_necessary(mailbox) click to toggle source
# File lib/vmail/imap_client.rb, line 286
def create_if_necessary(mailbox)
  current_mailboxes = mailboxes.map {|m| mailbox_aliases[m] || m}
  if !current_mailboxes.include?(mailbox)
    log "Current mailboxes: #{ current_mailboxes.inspect }"
    log "Creating mailbox #{ mailbox }"
    log @imap.create(mailbox)
    @mailboxes = nil # force reload ...
    list_mailboxes
  end
end
decrement_max_seqno(num) click to toggle source
# File lib/vmail/imap_client.rb, line 202
def decrement_max_seqno(num)
  return unless STDIN.tty?
  log "Decremented max seqno from #{ self.max_seqno } to #{ self.max_seqno - num }"
  self.max_seqno -= num
end
deliver(text) click to toggle source
# File lib/vmail/imap_client.rb, line 370
def deliver(text)
  # parse the text. The headers are yaml. The rest is text body.
  require 'net/smtp'
  prime_connection
  mail = new_mail_from_input(text)
  mail.delivery_method(*smtp_settings)
  res = mail.deliver!
  log res.inspect
  log "\n"
  msg = if res.is_a?(Mail::Message)
    "Message '#{ mail.subject }' sent"
  else
    "Failed to deliver message '#{ mail.subject }'!"
  end
  log msg
  msg
end
format_headers(hash) click to toggle source
# File lib/vmail/imap_client.rb, line 320
def format_headers(hash)
  lines = []
  hash.each_pair do |key, value|
    if value.nil? && key != 'to' && key != 'subject'
      next
    end
    if value.is_a?(Array)
      value = value.join(", ")
    end
    lines << "#{ key.gsub("_", '-') }: #{ value }"
  end
  lines.join("\n")
end
format_sent_message(mail) click to toggle source
# File lib/vmail/imap_client.rb, line 359
    def format_sent_message(mail)
      formatter = Vmail::MessageFormatter.new(mail)
      message_text = <<-EOF
Sent Message #{ self.format_parts_info(formatter.list_parts) }

#{ format_headers(formatter.extract_headers) }

#{ formatter.plaintext_part }
EOF
    end
forward_template() click to toggle source
# File lib/vmail/imap_client.rb, line 345
def forward_template
  original_body = current_message.plaintext.split(/\n-{20,}\n/, 2)[1]
  formatter = Vmail::MessageFormatter.new(current_mail)
  headers = formatter.extract_headers
  subject = headers['subject']
  if subject !~ /Fwd: /
    subject = "Fwd: #{ subject }"
  end

  new_message_template(subject, false) +
    "\n---------- Forwarded message ----------\n" +
    original_body + signature
end
get_highest_message_id() click to toggle source
# File lib/vmail/imap_client.rb, line 123
def get_highest_message_id
  # get highest message ID
  res = @imap.search(['ALL'])
  if res && res[-1]
    @num_messages = res[-1]
    log "Highest seqno: #@num_messages"
  else
    @num_messages = 1
    log "NO HIGHEST ID: setting @num_messages to 1"
  end
end
get_mailbox_status() click to toggle source

not used for anything

# File lib/vmail/imap_client.rb, line 136
def get_mailbox_status
  return
  @status = @imap.status(@mailbox,  ["MESSAGES", "RECENT", "UNSEEN"])
  log "Mailbox status: #@status.inspect"
end
handle_error(error) click to toggle source
# File lib/vmail/imap_client.rb, line 499
def handle_error(error)
  log error
end
list_mailboxes() click to toggle source
# File lib/vmail/imap_client.rb, line 163
def list_mailboxes
  log 'loading mailboxes...'
  @mailboxes ||= (@imap.list("", "*") || []).
    select {|struct| struct.attr.none? {|a| a == :Noselect} }.
    map {|struct|
      Net::IMAP.decode_utf7(struct.name)
    }.uniq
  @mailboxes.delete("INBOX")
  @mailboxes.unshift("INBOX")
  log "Loaded mailboxes: #@mailboxes.inspect"
  @mailboxes = @mailboxes.map {|name| mailbox_aliases.invert[name] || name}
  @mailboxes.join("\n")
end
log(string) click to toggle source
# File lib/vmail/imap_client.rb, line 492
def log(string)
  if string.is_a?(::Net::IMAP::TaggedResponse)
    string = string.raw_data
  end
  @logger.debug string
end
mailbox_aliases() click to toggle source

do this just once

# File lib/vmail/imap_client.rb, line 178
def mailbox_aliases
  return @mailbox_aliases if @mailbox_aliases
  @mailbox_aliases = {}
  @default_mailbox_aliases.each do |shortname, fullname_list|
    fullname_list.each do |fullname|
      [ "[Gmail]", "[Google Mail]" ].each do |prefix|
        if self.mailboxes.include?( "#{ prefix }/#{ fullname }" )
          @mailbox_aliases[shortname] =  "#{ prefix }/#{ fullname }"
        end
      end
    end
  end
  log "Setting aliases to #{ @mailbox_aliases.inspect }"
  @mailbox_aliases
end
mailboxes() click to toggle source

called internally, not by vim client

# File lib/vmail/imap_client.rb, line 195
def mailboxes
  if @mailboxes.nil?
    list_mailboxes
  end
  @mailboxes
end
more_messages() click to toggle source

gets 100 messages prior to id

# File lib/vmail/imap_client.rb, line 263
def more_messages
  log "Getting more_messages"
  log "Old start_index: #@start_index"
  max = @start_index - 1
  @start_index = [(max + 1 - @limit), 1].max
  log "New start_index: #@start_index"
  fetch_ids = search_query? ? @ids[@start_index..max] : (@start_index..max).to_a
  log fetch_ids.inspect
  message_ids = fetch_and_cache_headers(fetch_ids)
  res = get_message_headers message_ids
  with_more_message_line(res)
end
new_mail_from_input(text) click to toggle source
# File lib/vmail/imap_client.rb, line 388
def new_mail_from_input(text)
  require 'mail'
  mail = Mail.new
  raw_headers, raw_body = *text.split(/\n\s*\n/, 2)
  headers = {}

  raw_headers.split("\n").each do |line|
    key, value = *line.split(/:\s*/, 2)
    if key == 'references'
      mail.references = value
    else
      next if (value.nil? || value.strip == '')
      log [key, value].join(':')
      if %w(from to cc bcc).include?(key)
        value = quote_addresses(value)
      end
      headers[key] = value
    end
  end
  log "Delivering message with headers: #{ headers.to_yaml }"
  mail.from = headers['from'] || @username
  mail.to = headers['to'] #.split(/,\s+/)
  mail.cc = headers['cc'] #&& headers['cc'].split(/,\s+/)
  mail.bcc = headers['bcc'] #&& headers['cc'].split(/,\s+/)
  mail.subject = headers['subject']
  mail.from ||= @username
  mail.charset = 'UTF-8'
  # attachments are added as a snippet of YAML after a blank line
  # after the headers, and followed by a blank line
  if (attachments_section = raw_body.split(/\n\s*\n/, 2)[0]) =~ /^attach(ment|ments)*:/
    files = attachments_section.split(/\n/).map {|line| line[/[-:]\s*(.*)\s*$/, 1]}.compact
    log "Attach: #{ files.inspect }"
    files.each do |file|
      if File.directory?(file)
        Dir.glob("#{ file }/*").each {|f| mail.add_file(f) if File.size?(f)}
      else
        mail.add_file(file) if File.size?(file)
      end
    end
    mail.text_part do
      body raw_body.split(/\n\s*\n/, 2)[1]
    end
  else
    mail.text_part do
      body raw_body
    end
  end
  mail.text_part.charset = 'UTF-8'
  mail
rescue
  $logger.debug $!
  raise
end
new_message_template(subject = nil, append_signature = true) click to toggle source
# File lib/vmail/imap_client.rb, line 309
def new_message_template(subject = nil, append_signature = true)
  #set from field to user-specified value
  headers = {'from' => "#@name <#@from>",
    'to' => nil,
    'subject' => subject,
    'cc' => @always_cc,
    'bcc' => @always_bcc
  }
  format_headers(headers) + (append_signature ? ("\n\n" + signature) : "\n\n")
end
open() click to toggle source
# File lib/vmail/imap_client.rb, line 65
def open
  @imap = Net::IMAP.new(@imap_server, @imap_port, true, nil, false)
  log @imap.login(@username, @password)
  list_mailboxes # prefetch mailbox list
rescue
  puts "VMAIL_ERROR: #{[$!.message, $!.backtrace].join("\n")}"
end
open_html_part() click to toggle source
# File lib/vmail/imap_client.rb, line 459
def open_html_part
  log "Open_html_part"
  log current_mail.parts.inspect
  multipart = current_mail.parts.detect {|part| part.multipart?}
  html_part = if multipart
                multipart.parts.detect {|part| part.header["Content-Type"].to_s =~ /text\/html/}
              elsif ! current_mail.parts.empty?
                current_mail.parts.detect {|part| part.header["Content-Type"].to_s =~ /text\/html/}
              else
                current_mail.body
              end
  return if html_part.nil?
  outfile = 'part.html'
  File.open(outfile, 'w') {|f| f.puts(html_part.decoded)}
  # client should handle opening the html file
  return outfile
end
prime_connection() click to toggle source
# File lib/vmail/imap_client.rb, line 149
def prime_connection
  return if @ids.nil? || @ids.empty?
  reconnect_if_necessary(4) do
    # this is just to prime the IMAP connection
    # It's necessary for some reason before update and deliver.
    log "Priming connection"
    res = @imap.fetch(@ids[-1], ["ENVELOPE"])
    if res.nil?
      # just go ahead, just log
      log "Priming connection didn't work, connection seems broken, but still going ahead..."
    end
  end
end
reconnect_if_necessary(timeout = 60, &block) click to toggle source
# File lib/vmail/imap_client.rb, line 503
def reconnect_if_necessary(timeout = 60, &block)
  # if this times out, we know the connection is stale while the user is
  # trying to update
  Timeout::timeout(timeout) do
    block.call
  end
rescue IOError, Errno::EADDRNOTAVAIL, Errno::ECONNRESET, Timeout::Error, Errno::ETIMEDOUT
  log "Error: #{$!}"
  log "Attempting to reconnect"
  close
  log(revive_connection)
  # hope this isn't an endless loop
  reconnect_if_necessary do
    block.call
  end
rescue
  log "Error: #{$!}"
  raise
end
reload_mailbox() click to toggle source
# File lib/vmail/imap_client.rb, line 110
def reload_mailbox
  return unless STDIN.tty?
  select_mailbox(@mailbox, true)
end
revive_connection() click to toggle source
# File lib/vmail/imap_client.rb, line 142
def revive_connection
  log "Reviving connection"
  open
  log "Reselecting mailbox #@mailbox"
  @imap.select(@mailbox)
end
save_attachments(dir) click to toggle source
# File lib/vmail/imap_client.rb, line 442
def save_attachments(dir)
  log "Save_attachments #{ dir }"
  if !current_mail
    log "Missing a current message"
  end
  return unless dir && current_mail
  attachments = current_mail.attachments
  %x`mkdir -p #{ dir }`
  saved = attachments.map do |x|
    path = File.join(dir, x.filename)
    log "Saving #{ path }"
    File.open(path, 'wb') {|f| f.puts x.decoded}
    path
  end
  "Saved:\n" + saved.map {|x| "- #{ x }"}.join("\n")
end
select_mailbox(mailbox, force=false) click to toggle source
# File lib/vmail/imap_client.rb, line 90
def select_mailbox(mailbox, force=false)
  if mailbox_aliases[mailbox]
    mailbox = mailbox_aliases[mailbox]
  end
  log "Selecting mailbox #{ mailbox.inspect }"
  reconnect_if_necessary(30) do
    log @imap.select(Net::IMAP.encode_utf7(mailbox))
  end
  log "Done"

  @mailbox = mailbox
  @label = Label[name: @mailbox] || Label.create(name: @mailbox)

  log "Getting mailbox status"
  get_mailbox_status
  log "Getting highest message id"
  get_highest_message_id
  return "OK"
end
signature() click to toggle source
# File lib/vmail/imap_client.rb, line 335
def signature
  return signature_script if @signature_script
  "\n\n#@signature"
end
signature_script() click to toggle source
# File lib/vmail/imap_client.rb, line 340
def signature_script
  return unless @signature_script
  %x{ #{ @signature_script.strip } }
end
smtp_settings() click to toggle source
# File lib/vmail/imap_client.rb, line 482
def smtp_settings
  [:smtp, {:address => @smtp_server,
  :port => @smtp_port,
  :domain => @smtp_domain,
  :user_name => @username,
  :password => @password,
  :authentication => @authentication,
  :enable_starttls_auto => true}]
end
spawn_thread_if_tty(&block) click to toggle source
# File lib/vmail/imap_client.rb, line 276
def spawn_thread_if_tty(&block)
  if STDIN.tty?
    Thread.new do
      reconnect_if_necessary(10, &block)
    end
  else
    block.call
  end
end
update() click to toggle source
# File lib/vmail/imap_client.rb, line 247
def update
  prime_connection
  new_ids = check_for_new_messages
  if !new_ids.empty?
    @ids = @ids + new_ids
    message_ids = fetch_and_cache_headers(new_ids)
    res = get_message_headers(message_ids)
    res
  else
    ''
  end
rescue
  puts "VMAIL_ERROR: #{$!.class}\n#{[$!.message, $!.backtrace].join("\n")}"
end
update_message_list(new_ids) click to toggle source
# File lib/vmail/imap_client.rb, line 236
def update_message_list(new_ids)
  new_emails = DRbObject.new_with_uri($drb_uri).update
  return if new_emails.empty?

  tempfile_path = Tempfile.new('vmail-').path
  File.open(tempfile_path, 'w') { |file| file.write(new_emails) }
  server_name = "VMAIL:#{ @username }"

  system(%Q[vim --servername #{ server_name } --remote-expr 'UPDATE_MESSAGE_LIST("#{ tempfile_path }")'])
end
window_width=(width) click to toggle source
# File lib/vmail/imap_client.rb, line 477
def window_width=(width)
  @width = width.to_i
  log "Setting window width to #{ width }"
end
with_open() { |self| ... } click to toggle source

expects a block, closes on finish

# File lib/vmail/imap_client.rb, line 74
def with_open
  @imap = Net::IMAP.new(@imap_server, @imap_port, true, nil, false)
  log @imap.login(@username, @password)
  yield self
  close
end