class IMAPProcessor::Learn

IMAPLearn flags messages per-folder based on what you've flagged before.

aka part three of my Plan for Total Email Domination.

Constants

BLAND_KEYWORD

IMAP keyword for bland messages

LEARN_KEYWORD

IMAP keyword for learned messages

TASTY_KEYWORD

IMAP keyword for tasty messages

Public Class Methods

new(options) click to toggle source

Creates a new IMAPLearn from options.

Options include:

+:Threshold+:: Tastiness threshold for flagging

and all options from IMAPClient

Calls superclass method IMAPProcessor::Client::new
# File lib/imap_processor/learn.rb, line 63
def initialize(options)
  super

  @db_root = File.join '~', '.imap_learn',
                    "#{options[:User]}@#{options[:Host]}:#{options[:Port]}"
  @db_root = File.expand_path @db_root

  @threshold = options[:Threshold]

  @classifiers = Hash.new do |h,k|
    filter_db = File.join @db_root, "#{k}.db"
    FileUtils.mkdir_p File.dirname(filter_db)
    h[k] = RBayes.new filter_db
  end

  @unlearned_flagged = []
  @tasty_unflagged = []
  @bland_flagged = []
  @tasty_unlearned = []
  @bland_unlearned = []

  @noop = false
end
process_args(args) click to toggle source

Handles processing of args.

Calls superclass method IMAPProcessor::process_args
# File lib/imap_processor/learn.rb, line 42
def self.process_args(args)
  @@options[:Threshold] = [0.85, 'Tastiness threshold not set']

  super __FILE__, args, {} do |opts, options|
    opts.on("-t", "--threshold THRESHOLD",
            "Flag messages more tasty than THRESHOLD",
            "Default: #{options[:Threshold].inspect}",
            "Options file name: Threshold", Float) do |threshold|
      options[:Threshold] = threshold
    end
  end
end

Public Instance Methods

run() click to toggle source

Flags tasty messages from all selected mailboxes.

# File lib/imap_processor/learn.rb, line 90
def run
  log "Flagging tasty messages"

  message_count = 0
  mailboxes = find_mailboxes

  mailboxes.each do |mailbox|
    @mailbox = mailbox
    @imap.select @mailbox
    log "Selected #{@mailbox}"

    message_count += process_unlearned_flagged
    message_count += process_tasty_unflagged
    message_count += process_bland_flagged
    message_count += process_unlearned
  end

  log "Done. Found #{message_count} messages in #{mailboxes.length} mailboxes"
end

Private Instance Methods

bland_flagged_in_curr() click to toggle source

Returns an Array of tasty message sequence numbers that should be marked as tasty.

# File lib/imap_processor/learn.rb, line 149
def bland_flagged_in_curr
  log "Finding messages re-marked tasty"
  @bland_flagged = @imap.search [
    'FLAGGED',
    'KEYWORD', BLAND_KEYWORD
  ]

  update_db @bland_flagged, :remove_bland, :add_tasty

  @bland_flagged.length
end
chunk(messages, size = 20) { |chunk| ... } click to toggle source
# File lib/imap_processor/learn.rb, line 189
def chunk(messages, size = 20)
  messages = messages.dup

  until messages.empty? do
    chunk = messages.slice! 0, size
    yield chunk
  end
end
classify(text) click to toggle source

Returns true if text is “tasty”

# File lib/imap_processor/learn.rb, line 201
def classify(text)
  rating = @classifiers[@mailbox].rate text
  rating > @threshold
end
tasty_unflagged_in_curr() click to toggle source

Returns an Array of message sequence numbers that should be marked as bland.

# File lib/imap_processor/learn.rb, line 132
def tasty_unflagged_in_curr
  log "Finding messages re-marked bland"

  @bland_flagged = @imap.search [
   'NOT', 'FLAGGED',
   'KEYWORD', TASTY_KEYWORD
  ]

  update_db @tasty_unflagged, :remove_tasty, :add_bland
  
  @bland_flagged.length
end
unlearned_flagged_in_curr() click to toggle source

Returns an Array of tasty message sequence numbers.

# File lib/imap_processor/learn.rb, line 115
def unlearned_flagged_in_curr
  log "Finding unlearned, flagged messages"

  @unlearned_flagged = @imap.search [
    'FLAGGED',
    'NOT', 'KEYWORD', LEARN_KEYWORD
  ]

  update_db @unlearned_flagged, :add_tasty

  @unlearned_flagged.length
end
unlearned_in_curr() click to toggle source

Returns two Arrays, one of tasty message sequence numbers and one of bland message sequence numbers.

# File lib/imap_processor/learn.rb, line 165
def unlearned_in_curr
  log "Learning new, unmarked messages"
  unlearned = @imap.search [
    'NOT', 'KEYWORD', LEARN_KEYWORD
  ]

  tasty = []
  bland = []

  chunk unlearned do |messages|
    bodies = @imap.fetch messages, 'RFC822'
    bodies.each do |body|
      text = body.attr['RFC822']
      bucket = classify(text) ? tasty : bland
      bucket << body.seqno
    end
  end

  update_db tasty, :add_tasty
  update_db bland, :add_bland

  tasty.length + bland.length
end
update_db(messages, *actions) click to toggle source
# File lib/imap_processor/learn.rb, line 206
def update_db(messages, *actions)
  chunk messages do |chunk|
    bodies = @imap.fetch chunk, 'RFC822'
    bodies.each do |body|
      text = body.attr['RFC822']
      actions.each do |action|
        @classifiers[@mailbox].update_db_with text, action
        case action
        when :add_bland then
          @imap.store body.seqno, '+FLAG.SILENT',
                      [LEARN_KEYWORD, BLAND_KEYWORD]
        when :add_tasty then
          @imap.store body.seqno, '+FLAG.SILENT',
                      [:Flagged, LEARN_KEYWORD, TASTY_KEYWORD]
        when :remove_bland then
          @imap.store body.seqno, '-FLAG.SILENT', [BLAND_KEYWORD]
        when :remove_tasty then
          @imap.store body.seqno, '-FLAG.SILENT', [TASTY_KEYWORD]
        end
      end
    end
  end
end