segfault-larch 1.0.2.3
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/HISTORY +44 -0
 - data/LICENSE +280 -0
 - data/README.rdoc +273 -0
 - data/bin/larch +123 -0
 - data/lib/larch.rb +254 -0
 - data/lib/larch/config.rb +105 -0
 - data/lib/larch/db/account.rb +12 -0
 - data/lib/larch/db/mailbox.rb +12 -0
 - data/lib/larch/db/message.rb +6 -0
 - data/lib/larch/db/migrate/001_create_schema.rb +42 -0
 - data/lib/larch/errors.rb +14 -0
 - data/lib/larch/imap.rb +343 -0
 - data/lib/larch/imap/mailbox.rb +505 -0
 - data/lib/larch/logger.rb +50 -0
 - data/lib/larch/version.rb +9 -0
 - metadata +106 -0
 
| 
         @@ -0,0 +1,12 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Larch; module Database
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            class Mailbox < Sequel::Model
         
     | 
| 
      
 4 
     | 
    
         
            +
              plugin :hook_class_methods
         
     | 
| 
      
 5 
     | 
    
         
            +
              one_to_many :messages, :class => Larch::Database::Message
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
              before_destroy do
         
     | 
| 
      
 8 
     | 
    
         
            +
                Larch::Database::Message.filter(:mailbox_id => id).destroy
         
     | 
| 
      
 9 
     | 
    
         
            +
              end
         
     | 
| 
      
 10 
     | 
    
         
            +
            end
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            end; end
         
     | 
| 
         @@ -0,0 +1,42 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            class CreateSchema < Sequel::Migration
         
     | 
| 
      
 2 
     | 
    
         
            +
              def down
         
     | 
| 
      
 3 
     | 
    
         
            +
                drop_table :accounts, :mailboxes, :messages
         
     | 
| 
      
 4 
     | 
    
         
            +
              end
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
              def up
         
     | 
| 
      
 7 
     | 
    
         
            +
                create_table :accounts do
         
     | 
| 
      
 8 
     | 
    
         
            +
                  primary_key :id
         
     | 
| 
      
 9 
     | 
    
         
            +
                  text :hostname, :null => false
         
     | 
| 
      
 10 
     | 
    
         
            +
                  text :username, :null => false
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                  unique [:hostname, :username]
         
     | 
| 
      
 13 
     | 
    
         
            +
                end
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                create_table :mailboxes do
         
     | 
| 
      
 16 
     | 
    
         
            +
                  primary_key :id
         
     | 
| 
      
 17 
     | 
    
         
            +
                  foreign_key :account_id, :table => :accounts
         
     | 
| 
      
 18 
     | 
    
         
            +
                  text :name, :null => false
         
     | 
| 
      
 19 
     | 
    
         
            +
                  text :delim, :null => false
         
     | 
| 
      
 20 
     | 
    
         
            +
                  text :attr, :null => false, :default => ''
         
     | 
| 
      
 21 
     | 
    
         
            +
                  integer :subscribed, :null => false, :default => 0
         
     | 
| 
      
 22 
     | 
    
         
            +
                  integer :uidvalidity
         
     | 
| 
      
 23 
     | 
    
         
            +
                  integer :uidnext
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                  unique [:account_id, :name, :uidvalidity]
         
     | 
| 
      
 26 
     | 
    
         
            +
                end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                create_table :messages do
         
     | 
| 
      
 29 
     | 
    
         
            +
                  primary_key :id
         
     | 
| 
      
 30 
     | 
    
         
            +
                  foreign_key :mailbox_id, :table => :mailboxes
         
     | 
| 
      
 31 
     | 
    
         
            +
                  integer :uid, :null => false
         
     | 
| 
      
 32 
     | 
    
         
            +
                  text :guid, :null => false
         
     | 
| 
      
 33 
     | 
    
         
            +
                  text :message_id
         
     | 
| 
      
 34 
     | 
    
         
            +
                  integer :rfc822_size, :null => false
         
     | 
| 
      
 35 
     | 
    
         
            +
                  integer :internaldate, :null => false
         
     | 
| 
      
 36 
     | 
    
         
            +
                  text :flags, :null => false, :default => ''
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                  index :guid
         
     | 
| 
      
 39 
     | 
    
         
            +
                  unique [:mailbox_id, :uid]
         
     | 
| 
      
 40 
     | 
    
         
            +
                end
         
     | 
| 
      
 41 
     | 
    
         
            +
              end
         
     | 
| 
      
 42 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/larch/errors.rb
    ADDED
    
    | 
         @@ -0,0 +1,14 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Larch
         
     | 
| 
      
 2 
     | 
    
         
            +
              class Error < StandardError; end
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
              class Config
         
     | 
| 
      
 5 
     | 
    
         
            +
                class Error < Larch::Error; end
         
     | 
| 
      
 6 
     | 
    
         
            +
              end
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
              class IMAP
         
     | 
| 
      
 9 
     | 
    
         
            +
                class Error < Larch::Error; end
         
     | 
| 
      
 10 
     | 
    
         
            +
                class FatalError < Error; end
         
     | 
| 
      
 11 
     | 
    
         
            +
                class MailboxNotFoundError < Error; end
         
     | 
| 
      
 12 
     | 
    
         
            +
                class MessageNotFoundError < Error; end
         
     | 
| 
      
 13 
     | 
    
         
            +
              end
         
     | 
| 
      
 14 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/larch/imap.rb
    ADDED
    
    | 
         @@ -0,0 +1,343 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Larch
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            # Manages a connection to an IMAP server and all the glorious fun that entails.
         
     | 
| 
      
 4 
     | 
    
         
            +
            #
         
     | 
| 
      
 5 
     | 
    
         
            +
            # This class borrows heavily from Sup, the source code of which should be
         
     | 
| 
      
 6 
     | 
    
         
            +
            # required reading if you're doing anything with IMAP in Ruby:
         
     | 
| 
      
 7 
     | 
    
         
            +
            # http://sup.rubyforge.org
         
     | 
| 
      
 8 
     | 
    
         
            +
            class IMAP
         
     | 
| 
      
 9 
     | 
    
         
            +
              attr_reader :conn, :db_account, :mailboxes, :options
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
              # URI format validation regex.
         
     | 
| 
      
 12 
     | 
    
         
            +
              REGEX_URI = URI.regexp(['imap', 'imaps'])
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
              # Larch::IMAP::Message represents a transferable IMAP message which can be
         
     | 
| 
      
 15 
     | 
    
         
            +
              # passed between Larch::IMAP instances.
         
     | 
| 
      
 16 
     | 
    
         
            +
              Message = Struct.new(:guid, :envelope, :rfc822, :flags, :internaldate)
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
              # Initializes a new Larch::IMAP instance that will connect to the specified
         
     | 
| 
      
 19 
     | 
    
         
            +
              # IMAP URI.
         
     | 
| 
      
 20 
     | 
    
         
            +
              #
         
     | 
| 
      
 21 
     | 
    
         
            +
              # In addition to the URI, the following options may be specified:
         
     | 
| 
      
 22 
     | 
    
         
            +
              #
         
     | 
| 
      
 23 
     | 
    
         
            +
              # [:create_mailbox]
         
     | 
| 
      
 24 
     | 
    
         
            +
              #   If +true+, mailboxes that don't already exist will be created if
         
     | 
| 
      
 25 
     | 
    
         
            +
              #   necessary.
         
     | 
| 
      
 26 
     | 
    
         
            +
              #
         
     | 
| 
      
 27 
     | 
    
         
            +
              # [:dry_run]
         
     | 
| 
      
 28 
     | 
    
         
            +
              #   If +true+, read-only operations will be performed as usual and all change
         
     | 
| 
      
 29 
     | 
    
         
            +
              #   operations will be simulated, but no changes will actually be made. Note
         
     | 
| 
      
 30 
     | 
    
         
            +
              #   that it's not actually possible to simulate mailbox creation, so
         
     | 
| 
      
 31 
     | 
    
         
            +
              #   +:dry_run+ mode always behaves as if +:create_mailbox+ is +false+.
         
     | 
| 
      
 32 
     | 
    
         
            +
              #
         
     | 
| 
      
 33 
     | 
    
         
            +
              # [:max_retries]
         
     | 
| 
      
 34 
     | 
    
         
            +
              #   After a recoverable error occurs, retry the operation up to this many
         
     | 
| 
      
 35 
     | 
    
         
            +
              #   times. Default is 3.
         
     | 
| 
      
 36 
     | 
    
         
            +
              #
         
     | 
| 
      
 37 
     | 
    
         
            +
              # [:ssl_certs]
         
     | 
| 
      
 38 
     | 
    
         
            +
              #   Path to a trusted certificate bundle to use to verify server SSL
         
     | 
| 
      
 39 
     | 
    
         
            +
              #   certificates. You can download a bundle of certificate authority root
         
     | 
| 
      
 40 
     | 
    
         
            +
              #   certs at http://curl.haxx.se/ca/cacert.pem (it's up to you to verify that
         
     | 
| 
      
 41 
     | 
    
         
            +
              #   this bundle hasn't been tampered with, however; don't trust it blindly).
         
     | 
| 
      
 42 
     | 
    
         
            +
              #
         
     | 
| 
      
 43 
     | 
    
         
            +
              # [:ssl_verify]
         
     | 
| 
      
 44 
     | 
    
         
            +
              #   If +true+, server SSL certificates will be verified against the trusted
         
     | 
| 
      
 45 
     | 
    
         
            +
              #   certificate bundle specified in +ssl_certs+. By default, server SSL
         
     | 
| 
      
 46 
     | 
    
         
            +
              #   certificates are not verified.
         
     | 
| 
      
 47 
     | 
    
         
            +
              #
         
     | 
| 
      
 48 
     | 
    
         
            +
              def initialize(uri, options = {})
         
     | 
| 
      
 49 
     | 
    
         
            +
                raise ArgumentError, "not an IMAP URI: #{uri}" unless uri.is_a?(URI) || uri =~ REGEX_URI
         
     | 
| 
      
 50 
     | 
    
         
            +
                raise ArgumentError, "options must be a Hash" unless options.is_a?(Hash)
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                @options = {:max_retries => 3, :ssl_verify => false}.merge(options)
         
     | 
| 
      
 53 
     | 
    
         
            +
                @uri     = uri.is_a?(URI) ? uri : URI(uri)
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                raise ArgumentError, "must provide a username and password" unless @uri.user && @uri.password
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
                @conn       = nil
         
     | 
| 
      
 58 
     | 
    
         
            +
                @mailboxes  = {}
         
     | 
| 
      
 59 
     | 
    
         
            +
                @mutex      = Mutex.new
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
                @db_account = Database::Account.find_or_create(
         
     | 
| 
      
 62 
     | 
    
         
            +
                  :hostname => host,
         
     | 
| 
      
 63 
     | 
    
         
            +
                  :username => username
         
     | 
| 
      
 64 
     | 
    
         
            +
                )
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                # Create private convenience methods (debug, info, warn, etc.) to make
         
     | 
| 
      
 67 
     | 
    
         
            +
                # logging easier.
         
     | 
| 
      
 68 
     | 
    
         
            +
                Logger::LEVELS.each_key do |level|
         
     | 
| 
      
 69 
     | 
    
         
            +
                  IMAP.class_eval do
         
     | 
| 
      
 70 
     | 
    
         
            +
                    define_method(level) do |msg|
         
     | 
| 
      
 71 
     | 
    
         
            +
                      Larch.log.log(level, "#{username}@#{host}: #{msg}")
         
     | 
| 
      
 72 
     | 
    
         
            +
                    end
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                    private level
         
     | 
| 
      
 75 
     | 
    
         
            +
                  end
         
     | 
| 
      
 76 
     | 
    
         
            +
                end
         
     | 
| 
      
 77 
     | 
    
         
            +
              end
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
              # Connects to the IMAP server and logs in if a connection hasn't already been
         
     | 
| 
      
 80 
     | 
    
         
            +
              # established.
         
     | 
| 
      
 81 
     | 
    
         
            +
              def connect
         
     | 
| 
      
 82 
     | 
    
         
            +
                return if @conn
         
     | 
| 
      
 83 
     | 
    
         
            +
                safely {} # connect, but do nothing else
         
     | 
| 
      
 84 
     | 
    
         
            +
              end
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
              # Gets the server's mailbox hierarchy delimiter.
         
     | 
| 
      
 87 
     | 
    
         
            +
              def delim
         
     | 
| 
      
 88 
     | 
    
         
            +
                @delim ||= safely { @conn.list('', '')[0].delim }
         
     | 
| 
      
 89 
     | 
    
         
            +
              end
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
              # Closes the IMAP connection if one is currently open.
         
     | 
| 
      
 92 
     | 
    
         
            +
              def disconnect
         
     | 
| 
      
 93 
     | 
    
         
            +
                return unless @conn
         
     | 
| 
      
 94 
     | 
    
         
            +
             
     | 
| 
      
 95 
     | 
    
         
            +
                begin
         
     | 
| 
      
 96 
     | 
    
         
            +
                  @conn.disconnect
         
     | 
| 
      
 97 
     | 
    
         
            +
                rescue Errno::ENOTCONN => e
         
     | 
| 
      
 98 
     | 
    
         
            +
                  debug "#{e.class.name}: #{e.message}"
         
     | 
| 
      
 99 
     | 
    
         
            +
                end
         
     | 
| 
      
 100 
     | 
    
         
            +
             
     | 
| 
      
 101 
     | 
    
         
            +
                reset
         
     | 
| 
      
 102 
     | 
    
         
            +
             
     | 
| 
      
 103 
     | 
    
         
            +
                info "disconnected"
         
     | 
| 
      
 104 
     | 
    
         
            +
              end
         
     | 
| 
      
 105 
     | 
    
         
            +
             
     | 
| 
      
 106 
     | 
    
         
            +
              # Iterates through all mailboxes in the account, yielding each one as a
         
     | 
| 
      
 107 
     | 
    
         
            +
              # Larch::IMAP::Mailbox instance to the given block.
         
     | 
| 
      
 108 
     | 
    
         
            +
              def each_mailbox
         
     | 
| 
      
 109 
     | 
    
         
            +
                update_mailboxes
         
     | 
| 
      
 110 
     | 
    
         
            +
                @mailboxes.each_value {|mailbox| yield mailbox }
         
     | 
| 
      
 111 
     | 
    
         
            +
              end
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
              # Gets the IMAP hostname.
         
     | 
| 
      
 114 
     | 
    
         
            +
              def host
         
     | 
| 
      
 115 
     | 
    
         
            +
                @uri.host
         
     | 
| 
      
 116 
     | 
    
         
            +
              end
         
     | 
| 
      
 117 
     | 
    
         
            +
             
     | 
| 
      
 118 
     | 
    
         
            +
              # Gets a Larch::IMAP::Mailbox instance representing the specified mailbox. If
         
     | 
| 
      
 119 
     | 
    
         
            +
              # the mailbox doesn't exist and the <tt>:create_mailbox</tt> option is
         
     | 
| 
      
 120 
     | 
    
         
            +
              # +false+, or if <tt>:create_mailbox</tt> is +true+ and mailbox creation
         
     | 
| 
      
 121 
     | 
    
         
            +
              # fails, a Larch::IMAP::MailboxNotFoundError will be raised.
         
     | 
| 
      
 122 
     | 
    
         
            +
              def mailbox(name, delim = '/')
         
     | 
| 
      
 123 
     | 
    
         
            +
                retries = 0
         
     | 
| 
      
 124 
     | 
    
         
            +
             
     | 
| 
      
 125 
     | 
    
         
            +
                name = name.gsub(delim, self.delim)
         
     | 
| 
      
 126 
     | 
    
         
            +
             
     | 
| 
      
 127 
     | 
    
         
            +
                begin
         
     | 
| 
      
 128 
     | 
    
         
            +
                  @mailboxes.fetch(name) do
         
     | 
| 
      
 129 
     | 
    
         
            +
                    update_mailboxes
         
     | 
| 
      
 130 
     | 
    
         
            +
                    return @mailboxes[name] if @mailboxes.has_key?(name)
         
     | 
| 
      
 131 
     | 
    
         
            +
                    raise MailboxNotFoundError, "mailbox not found: #{name}"
         
     | 
| 
      
 132 
     | 
    
         
            +
                  end
         
     | 
| 
      
 133 
     | 
    
         
            +
             
     | 
| 
      
 134 
     | 
    
         
            +
                rescue MailboxNotFoundError => e
         
     | 
| 
      
 135 
     | 
    
         
            +
                  raise unless @options[:create_mailbox] && retries == 0
         
     | 
| 
      
 136 
     | 
    
         
            +
             
     | 
| 
      
 137 
     | 
    
         
            +
                  info "creating mailbox: #{name}"
         
     | 
| 
      
 138 
     | 
    
         
            +
                  safely { @conn.create(name) } unless @options[:dry_run]
         
     | 
| 
      
 139 
     | 
    
         
            +
             
     | 
| 
      
 140 
     | 
    
         
            +
                  retries += 1
         
     | 
| 
      
 141 
     | 
    
         
            +
                  retry
         
     | 
| 
      
 142 
     | 
    
         
            +
                end
         
     | 
| 
      
 143 
     | 
    
         
            +
              end
         
     | 
| 
      
 144 
     | 
    
         
            +
             
     | 
| 
      
 145 
     | 
    
         
            +
              # Sends an IMAP NOOP command.
         
     | 
| 
      
 146 
     | 
    
         
            +
              def noop
         
     | 
| 
      
 147 
     | 
    
         
            +
                safely { @conn.noop }
         
     | 
| 
      
 148 
     | 
    
         
            +
              end
         
     | 
| 
      
 149 
     | 
    
         
            +
             
     | 
| 
      
 150 
     | 
    
         
            +
              # Gets the IMAP password.
         
     | 
| 
      
 151 
     | 
    
         
            +
              def password
         
     | 
| 
      
 152 
     | 
    
         
            +
                CGI.unescape(@uri.password)
         
     | 
| 
      
 153 
     | 
    
         
            +
              end
         
     | 
| 
      
 154 
     | 
    
         
            +
             
     | 
| 
      
 155 
     | 
    
         
            +
              # Gets the IMAP port number.
         
     | 
| 
      
 156 
     | 
    
         
            +
              def port
         
     | 
| 
      
 157 
     | 
    
         
            +
                @uri.port || (ssl? ? 993 : 143)
         
     | 
| 
      
 158 
     | 
    
         
            +
              end
         
     | 
| 
      
 159 
     | 
    
         
            +
             
     | 
| 
      
 160 
     | 
    
         
            +
              # Connect if necessary, execute the given block, retry if a recoverable error
         
     | 
| 
      
 161 
     | 
    
         
            +
              # occurs, die if an unrecoverable error occurs.
         
     | 
| 
      
 162 
     | 
    
         
            +
              def safely
         
     | 
| 
      
 163 
     | 
    
         
            +
                safe_connect
         
     | 
| 
      
 164 
     | 
    
         
            +
             
     | 
| 
      
 165 
     | 
    
         
            +
                retries = 0
         
     | 
| 
      
 166 
     | 
    
         
            +
             
     | 
| 
      
 167 
     | 
    
         
            +
                begin
         
     | 
| 
      
 168 
     | 
    
         
            +
                  yield
         
     | 
| 
      
 169 
     | 
    
         
            +
             
     | 
| 
      
 170 
     | 
    
         
            +
                rescue Errno::ECONNABORTED,
         
     | 
| 
      
 171 
     | 
    
         
            +
                       Errno::ECONNRESET,
         
     | 
| 
      
 172 
     | 
    
         
            +
                       Errno::ENOTCONN,
         
     | 
| 
      
 173 
     | 
    
         
            +
                       Errno::EPIPE,
         
     | 
| 
      
 174 
     | 
    
         
            +
                       Errno::ETIMEDOUT,
         
     | 
| 
      
 175 
     | 
    
         
            +
                       EOFError,
         
     | 
| 
      
 176 
     | 
    
         
            +
                       Net::IMAP::ByeResponseError,
         
     | 
| 
      
 177 
     | 
    
         
            +
                       OpenSSL::SSL::SSLError => e
         
     | 
| 
      
 178 
     | 
    
         
            +
             
     | 
| 
      
 179 
     | 
    
         
            +
                  raise unless (retries += 1) <= @options[:max_retries]
         
     | 
| 
      
 180 
     | 
    
         
            +
             
     | 
| 
      
 181 
     | 
    
         
            +
                  info "#{e.class.name}: #{e.message} (reconnecting)"
         
     | 
| 
      
 182 
     | 
    
         
            +
             
     | 
| 
      
 183 
     | 
    
         
            +
                  reset
         
     | 
| 
      
 184 
     | 
    
         
            +
                  sleep 1 * retries
         
     | 
| 
      
 185 
     | 
    
         
            +
                  safe_connect
         
     | 
| 
      
 186 
     | 
    
         
            +
                  retry
         
     | 
| 
      
 187 
     | 
    
         
            +
             
     | 
| 
      
 188 
     | 
    
         
            +
                rescue Net::IMAP::BadResponseError,
         
     | 
| 
      
 189 
     | 
    
         
            +
                       Net::IMAP::NoResponseError,
         
     | 
| 
      
 190 
     | 
    
         
            +
                       Net::IMAP::ResponseParseError => e
         
     | 
| 
      
 191 
     | 
    
         
            +
             
     | 
| 
      
 192 
     | 
    
         
            +
                  raise unless (retries += 1) <= @options[:max_retries]
         
     | 
| 
      
 193 
     | 
    
         
            +
             
     | 
| 
      
 194 
     | 
    
         
            +
                  info "#{e.class.name}: #{e.message} (will retry)"
         
     | 
| 
      
 195 
     | 
    
         
            +
             
     | 
| 
      
 196 
     | 
    
         
            +
                  sleep 1 * retries
         
     | 
| 
      
 197 
     | 
    
         
            +
                  retry
         
     | 
| 
      
 198 
     | 
    
         
            +
                end
         
     | 
| 
      
 199 
     | 
    
         
            +
             
     | 
| 
      
 200 
     | 
    
         
            +
              rescue Larch::Error => e
         
     | 
| 
      
 201 
     | 
    
         
            +
                raise
         
     | 
| 
      
 202 
     | 
    
         
            +
             
     | 
| 
      
 203 
     | 
    
         
            +
              rescue Net::IMAP::Error => e
         
     | 
| 
      
 204 
     | 
    
         
            +
                raise Error, "#{e.class.name}: #{e.message} (giving up)"
         
     | 
| 
      
 205 
     | 
    
         
            +
             
     | 
| 
      
 206 
     | 
    
         
            +
              rescue => e
         
     | 
| 
      
 207 
     | 
    
         
            +
                raise FatalError, "#{e.class.name}: #{e.message} (cannot recover)"
         
     | 
| 
      
 208 
     | 
    
         
            +
              end
         
     | 
| 
      
 209 
     | 
    
         
            +
             
     | 
| 
      
 210 
     | 
    
         
            +
              # Gets the SSL status.
         
     | 
| 
      
 211 
     | 
    
         
            +
              def ssl?
         
     | 
| 
      
 212 
     | 
    
         
            +
                @uri.scheme == 'imaps'
         
     | 
| 
      
 213 
     | 
    
         
            +
              end
         
     | 
| 
      
 214 
     | 
    
         
            +
             
     | 
| 
      
 215 
     | 
    
         
            +
              # Gets the IMAP URI.
         
     | 
| 
      
 216 
     | 
    
         
            +
              def uri
         
     | 
| 
      
 217 
     | 
    
         
            +
                @uri.to_s
         
     | 
| 
      
 218 
     | 
    
         
            +
              end
         
     | 
| 
      
 219 
     | 
    
         
            +
             
     | 
| 
      
 220 
     | 
    
         
            +
              # Gets the IMAP mailbox specified in the URI, or +nil+ if none.
         
     | 
| 
      
 221 
     | 
    
         
            +
              def uri_mailbox
         
     | 
| 
      
 222 
     | 
    
         
            +
                mb = @uri.path[1..-1]
         
     | 
| 
      
 223 
     | 
    
         
            +
                mb.nil? || mb.empty? ? nil : CGI.unescape(mb)
         
     | 
| 
      
 224 
     | 
    
         
            +
              end
         
     | 
| 
      
 225 
     | 
    
         
            +
             
     | 
| 
      
 226 
     | 
    
         
            +
              # Gets the IMAP username.
         
     | 
| 
      
 227 
     | 
    
         
            +
              def username
         
     | 
| 
      
 228 
     | 
    
         
            +
                CGI.unescape(@uri.user)
         
     | 
| 
      
 229 
     | 
    
         
            +
              end
         
     | 
| 
      
 230 
     | 
    
         
            +
             
     | 
| 
      
 231 
     | 
    
         
            +
              private
         
     | 
| 
      
 232 
     | 
    
         
            +
             
     | 
| 
      
 233 
     | 
    
         
            +
              # Resets the connection and mailbox state.
         
     | 
| 
      
 234 
     | 
    
         
            +
              def reset
         
     | 
| 
      
 235 
     | 
    
         
            +
                @mutex.synchronize do
         
     | 
| 
      
 236 
     | 
    
         
            +
                  @conn = nil
         
     | 
| 
      
 237 
     | 
    
         
            +
                  @mailboxes.each_value {|mb| mb.reset }
         
     | 
| 
      
 238 
     | 
    
         
            +
                end
         
     | 
| 
      
 239 
     | 
    
         
            +
              end
         
     | 
| 
      
 240 
     | 
    
         
            +
             
     | 
| 
      
 241 
     | 
    
         
            +
              def safe_connect
         
     | 
| 
      
 242 
     | 
    
         
            +
                return if @conn
         
     | 
| 
      
 243 
     | 
    
         
            +
             
     | 
| 
      
 244 
     | 
    
         
            +
                retries = 0
         
     | 
| 
      
 245 
     | 
    
         
            +
             
     | 
| 
      
 246 
     | 
    
         
            +
                begin
         
     | 
| 
      
 247 
     | 
    
         
            +
                  unsafe_connect
         
     | 
| 
      
 248 
     | 
    
         
            +
             
     | 
| 
      
 249 
     | 
    
         
            +
                rescue Errno::ECONNRESET,
         
     | 
| 
      
 250 
     | 
    
         
            +
                       Errno::EPIPE,
         
     | 
| 
      
 251 
     | 
    
         
            +
                       Errno::ETIMEDOUT,
         
     | 
| 
      
 252 
     | 
    
         
            +
                       OpenSSL::SSL::SSLError => e
         
     | 
| 
      
 253 
     | 
    
         
            +
             
     | 
| 
      
 254 
     | 
    
         
            +
                  raise unless (retries += 1) <= @options[:max_retries]
         
     | 
| 
      
 255 
     | 
    
         
            +
             
     | 
| 
      
 256 
     | 
    
         
            +
                  # Special check to ensure that we don't retry on OpenSSL certificate
         
     | 
| 
      
 257 
     | 
    
         
            +
                  # verification errors.
         
     | 
| 
      
 258 
     | 
    
         
            +
                  raise if e.is_a?(OpenSSL::SSL::SSLError) && e.message =~ /certificate verify failed/
         
     | 
| 
      
 259 
     | 
    
         
            +
             
     | 
| 
      
 260 
     | 
    
         
            +
                  info "#{e.class.name}: #{e.message} (will retry)"
         
     | 
| 
      
 261 
     | 
    
         
            +
             
     | 
| 
      
 262 
     | 
    
         
            +
                  reset
         
     | 
| 
      
 263 
     | 
    
         
            +
                  sleep 1 * retries
         
     | 
| 
      
 264 
     | 
    
         
            +
                  retry
         
     | 
| 
      
 265 
     | 
    
         
            +
                end
         
     | 
| 
      
 266 
     | 
    
         
            +
             
     | 
| 
      
 267 
     | 
    
         
            +
              rescue => e
         
     | 
| 
      
 268 
     | 
    
         
            +
                raise FatalError, "#{e.class.name}: #{e.message} (cannot recover)"
         
     | 
| 
      
 269 
     | 
    
         
            +
              end
         
     | 
| 
      
 270 
     | 
    
         
            +
             
     | 
| 
      
 271 
     | 
    
         
            +
              def unsafe_connect
         
     | 
| 
      
 272 
     | 
    
         
            +
                info "connecting..."
         
     | 
| 
      
 273 
     | 
    
         
            +
             
     | 
| 
      
 274 
     | 
    
         
            +
                exception = nil
         
     | 
| 
      
 275 
     | 
    
         
            +
             
     | 
| 
      
 276 
     | 
    
         
            +
                Thread.new do
         
     | 
| 
      
 277 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 278 
     | 
    
         
            +
                    @conn = Net::IMAP.new(host, port, ssl?,
         
     | 
| 
      
 279 
     | 
    
         
            +
                        ssl? && @options[:ssl_verify] ? @options[:ssl_certs] : nil,
         
     | 
| 
      
 280 
     | 
    
         
            +
                        @options[:ssl_verify])
         
     | 
| 
      
 281 
     | 
    
         
            +
             
     | 
| 
      
 282 
     | 
    
         
            +
                    info "connected on port #{port}" << (ssl? ? ' using SSL' : '')
         
     | 
| 
      
 283 
     | 
    
         
            +
             
     | 
| 
      
 284 
     | 
    
         
            +
                    auth_methods = ['PLAIN']
         
     | 
| 
      
 285 
     | 
    
         
            +
                    tried        = []
         
     | 
| 
      
 286 
     | 
    
         
            +
                    capability   = @conn.capability
         
     | 
| 
      
 287 
     | 
    
         
            +
             
     | 
| 
      
 288 
     | 
    
         
            +
                    ['LOGIN', 'CRAM-MD5'].each do |method|
         
     | 
| 
      
 289 
     | 
    
         
            +
                      auth_methods << method if capability.include?("AUTH=#{method}")
         
     | 
| 
      
 290 
     | 
    
         
            +
                    end
         
     | 
| 
      
 291 
     | 
    
         
            +
             
     | 
| 
      
 292 
     | 
    
         
            +
                    begin
         
     | 
| 
      
 293 
     | 
    
         
            +
                      tried << method = auth_methods.pop
         
     | 
| 
      
 294 
     | 
    
         
            +
             
     | 
| 
      
 295 
     | 
    
         
            +
                      debug "authenticating using #{method}"
         
     | 
| 
      
 296 
     | 
    
         
            +
             
     | 
| 
      
 297 
     | 
    
         
            +
                      if method == 'PLAIN'
         
     | 
| 
      
 298 
     | 
    
         
            +
                        @conn.login(username, password)
         
     | 
| 
      
 299 
     | 
    
         
            +
                      else
         
     | 
| 
      
 300 
     | 
    
         
            +
                        @conn.authenticate(method, username, password)
         
     | 
| 
      
 301 
     | 
    
         
            +
                      end
         
     | 
| 
      
 302 
     | 
    
         
            +
             
     | 
| 
      
 303 
     | 
    
         
            +
                      info "authenticated using #{method}"
         
     | 
| 
      
 304 
     | 
    
         
            +
             
     | 
| 
      
 305 
     | 
    
         
            +
                    rescue Net::IMAP::BadResponseError, Net::IMAP::NoResponseError => e
         
     | 
| 
      
 306 
     | 
    
         
            +
                      debug "#{method} auth failed: #{e.message}"
         
     | 
| 
      
 307 
     | 
    
         
            +
                      retry unless auth_methods.empty?
         
     | 
| 
      
 308 
     | 
    
         
            +
             
     | 
| 
      
 309 
     | 
    
         
            +
                      raise e, "#{e.message} (tried #{tried.join(', ')})"
         
     | 
| 
      
 310 
     | 
    
         
            +
                    end
         
     | 
| 
      
 311 
     | 
    
         
            +
             
     | 
| 
      
 312 
     | 
    
         
            +
                  rescue => e
         
     | 
| 
      
 313 
     | 
    
         
            +
                    exception = e
         
     | 
| 
      
 314 
     | 
    
         
            +
                  end
         
     | 
| 
      
 315 
     | 
    
         
            +
                end.join
         
     | 
| 
      
 316 
     | 
    
         
            +
             
     | 
| 
      
 317 
     | 
    
         
            +
                raise exception if exception
         
     | 
| 
      
 318 
     | 
    
         
            +
              end
         
     | 
| 
      
 319 
     | 
    
         
            +
             
     | 
| 
      
 320 
     | 
    
         
            +
              def update_mailboxes
         
     | 
| 
      
 321 
     | 
    
         
            +
                all        = safely { @conn.list('', '*') } || []
         
     | 
| 
      
 322 
     | 
    
         
            +
                subscribed = safely { @conn.lsub('', '*') } || []
         
     | 
| 
      
 323 
     | 
    
         
            +
             
     | 
| 
      
 324 
     | 
    
         
            +
                @mutex.synchronize do
         
     | 
| 
      
 325 
     | 
    
         
            +
                  # Remove cached mailboxes that no longer exist.
         
     | 
| 
      
 326 
     | 
    
         
            +
                  @mailboxes.delete_if {|k, v| !all.any?{|mb| mb.name == k}}
         
     | 
| 
      
 327 
     | 
    
         
            +
             
     | 
| 
      
 328 
     | 
    
         
            +
                  # Update cached mailboxes.
         
     | 
| 
      
 329 
     | 
    
         
            +
                  all.each do |mb|
         
     | 
| 
      
 330 
     | 
    
         
            +
                    @mailboxes[mb.name] ||= Mailbox.new(self, mb.name, mb.delim,
         
     | 
| 
      
 331 
     | 
    
         
            +
                        subscribed.any?{|s| s.name == mb.name}, mb.attr)
         
     | 
| 
      
 332 
     | 
    
         
            +
                  end
         
     | 
| 
      
 333 
     | 
    
         
            +
                end
         
     | 
| 
      
 334 
     | 
    
         
            +
             
     | 
| 
      
 335 
     | 
    
         
            +
                # Remove mailboxes that no longer exist from the database.
         
     | 
| 
      
 336 
     | 
    
         
            +
                @db_account.mailboxes.each do |db_mailbox|
         
     | 
| 
      
 337 
     | 
    
         
            +
                  db_mailbox.destroy unless @mailboxes.has_key?(db_mailbox.name)
         
     | 
| 
      
 338 
     | 
    
         
            +
                end
         
     | 
| 
      
 339 
     | 
    
         
            +
              end
         
     | 
| 
      
 340 
     | 
    
         
            +
             
     | 
| 
      
 341 
     | 
    
         
            +
            end
         
     | 
| 
      
 342 
     | 
    
         
            +
             
     | 
| 
      
 343 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,505 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            nodule Larch; class IMAP
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            # Represents an IMAP mailbox.
         
     | 
| 
      
 4 
     | 
    
         
            +
            class Mailbox
         
     | 
| 
      
 5 
     | 
    
         
            +
              attr_reader :attr, :db_mailbox, :delim, :imap, :name, :state, :subscribed
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
              # Maximum number of message headers to fetch with a single IMAP command.
         
     | 
| 
      
 8 
     | 
    
         
            +
              FETCH_BLOCK_SIZE = 1024
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
              # Regex to capture a Message-Id header.
         
     | 
| 
      
 11 
     | 
    
         
            +
              REGEX_MESSAGE_ID = /message-id\s*:\s*(\S+)/i
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
              # Minimum time (in seconds) allowed between mailbox scans.
         
     | 
| 
      
 14 
     | 
    
         
            +
              SCAN_INTERVAL = 60
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
              def initialize(imap, name, delim, subscribed, *attr)
         
     | 
| 
      
 17 
     | 
    
         
            +
                raise ArgumentError, "must provide a Larch::IMAP instance" unless imap.is_a?(Larch::IMAP)
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                @imap       = imap
         
     | 
| 
      
 20 
     | 
    
         
            +
                @name       = name
         
     | 
| 
      
 21 
     | 
    
         
            +
                @name_utf7  = Net::IMAP.encode_utf7(@name)
         
     | 
| 
      
 22 
     | 
    
         
            +
                @delim      = delim
         
     | 
| 
      
 23 
     | 
    
         
            +
                @subscribed = subscribed
         
     | 
| 
      
 24 
     | 
    
         
            +
                @attr       = attr.flatten
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                @last_scan = nil
         
     | 
| 
      
 27 
     | 
    
         
            +
                @mutex     = Mutex.new
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                # Valid mailbox states are :closed (no mailbox open), :examined (mailbox
         
     | 
| 
      
 30 
     | 
    
         
            +
                # open and read-only), or :selected (mailbox open and read-write).
         
     | 
| 
      
 31 
     | 
    
         
            +
                @state = :closed
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                # Create/update this mailbox in the database.
         
     | 
| 
      
 34 
     | 
    
         
            +
                mb_data = {
         
     | 
| 
      
 35 
     | 
    
         
            +
                  :name       => @name,
         
     | 
| 
      
 36 
     | 
    
         
            +
                  :delim      => @delim,
         
     | 
| 
      
 37 
     | 
    
         
            +
                  :attr       => @attr.map{|a| a.to_s }.join(','),
         
     | 
| 
      
 38 
     | 
    
         
            +
                  :subscribed => @subscribed ? 1 : 0
         
     | 
| 
      
 39 
     | 
    
         
            +
                }
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                @db_mailbox = imap.db_account.mailboxes_dataset.filter(:name => @name).first
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                if @db_mailbox
         
     | 
| 
      
 44 
     | 
    
         
            +
                  @db_mailbox.update(mb_data)
         
     | 
| 
      
 45 
     | 
    
         
            +
                else
         
     | 
| 
      
 46 
     | 
    
         
            +
                  @db_mailbox = Database::Mailbox.create(mb_data)
         
     | 
| 
      
 47 
     | 
    
         
            +
                  imap.db_account.add_mailbox(@db_mailbox)
         
     | 
| 
      
 48 
     | 
    
         
            +
                end
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                # Create private convenience methods (debug, info, warn, etc.) to make
         
     | 
| 
      
 51 
     | 
    
         
            +
                # logging easier.
         
     | 
| 
      
 52 
     | 
    
         
            +
                Logger::LEVELS.each_key do |level|
         
     | 
| 
      
 53 
     | 
    
         
            +
                  Mailbox.class_eval do
         
     | 
| 
      
 54 
     | 
    
         
            +
                    define_method(level) do |msg|
         
     | 
| 
      
 55 
     | 
    
         
            +
                      Larch.log.log(level, "#{@imap.username}@#{@imap.host}: #{@name}: #{msg}")
         
     | 
| 
      
 56 
     | 
    
         
            +
                    end
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                    private level
         
     | 
| 
      
 59 
     | 
    
         
            +
                  end
         
     | 
| 
      
 60 
     | 
    
         
            +
                end
         
     | 
| 
      
 61 
     | 
    
         
            +
              end
         
     | 
| 
      
 62 
     | 
    
         
            +
             
     | 
| 
      
 63 
     | 
    
         
            +
              # Appends the specified Larch::IMAP::Message to this mailbox if it doesn't
         
     | 
| 
      
 64 
     | 
    
         
            +
              # already exist. Returns +true+ if the message was appended successfully,
         
     | 
| 
      
 65 
     | 
    
         
            +
              # +false+ if the message already exists in the mailbox.
         
     | 
| 
      
 66 
     | 
    
         
            +
              def append(message)
         
     | 
| 
      
 67 
     | 
    
         
            +
                raise ArgumentError, "must provide a Larch::IMAP::Message object" unless message.is_a?(Larch::IMAP::Message)
         
     | 
| 
      
 68 
     | 
    
         
            +
                return false if has_guid?(message.guid)
         
     | 
| 
      
 69 
     | 
    
         
            +
             
     | 
| 
      
 70 
     | 
    
         
            +
                @imap.safely do
         
     | 
| 
      
 71 
     | 
    
         
            +
                  unless imap_select(!!@imap.options[:create_mailbox])
         
     | 
| 
      
 72 
     | 
    
         
            +
                    raise Larch::IMAP::Error, "mailbox cannot contain messages: #{@name}"
         
     | 
| 
      
 73 
     | 
    
         
            +
                  end
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
                  debug "appending message: #{message.guid}"
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
                  # The \Recent flag is read-only, so we shouldn't try to set it at the
         
     | 
| 
      
 78 
     | 
    
         
            +
                  # destination.
         
     | 
| 
      
 79 
     | 
    
         
            +
                  flags = message.flags.dup
         
     | 
| 
      
 80 
     | 
    
         
            +
                  flags.delete(:Recent)
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
      
 82 
     | 
    
         
            +
                  @imap.conn.append(@name_utf7, message.rfc822, flags, message.internaldate) unless @imap.options[:dry_run]
         
     | 
| 
      
 83 
     | 
    
         
            +
                end
         
     | 
| 
      
 84 
     | 
    
         
            +
             
     | 
| 
      
 85 
     | 
    
         
            +
                true
         
     | 
| 
      
 86 
     | 
    
         
            +
              end
         
     | 
| 
      
 87 
     | 
    
         
            +
              alias << append
         
     | 
| 
      
 88 
     | 
    
         
            +
             
     | 
| 
      
 89 
     | 
    
         
            +
              # Iterates through messages in this mailbox, yielding the Larch message guid
         
     | 
| 
      
 90 
     | 
    
         
            +
              # of each to the provided block.
         
     | 
| 
      
 91 
     | 
    
         
            +
              def each_guid # :yields: guid
         
     | 
| 
      
 92 
     | 
    
         
            +
                scan
         
     | 
| 
      
 93 
     | 
    
         
            +
                @db_mailbox.messages.each {|db_message| yield db_message.guid }
         
     | 
| 
      
 94 
     | 
    
         
            +
              end
         
     | 
| 
      
 95 
     | 
    
         
            +
             
     | 
| 
      
 96 
     | 
    
         
            +
              # Iterates through mailboxes that are first-level children of this mailbox,
         
     | 
| 
      
 97 
     | 
    
         
            +
              # yielding a Larch::IMAP::Mailbox object for each to the provided block.
         
     | 
| 
      
 98 
     | 
    
         
            +
              def each_mailbox # :yields: mailbox
         
     | 
| 
      
 99 
     | 
    
         
            +
                mailboxes.each {|mb| yield mb }
         
     | 
| 
      
 100 
     | 
    
         
            +
              end
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
              # Returns a Larch::IMAP::Message struct representing the message with the
         
     | 
| 
      
 103 
     | 
    
         
            +
              # specified Larch _guid_, or +nil+ if the specified guid was not found in this
         
     | 
| 
      
 104 
     | 
    
         
            +
              # mailbox.
         
     | 
| 
      
 105 
     | 
    
         
            +
              def fetch(guid, peek = false)
         
     | 
| 
      
 106 
     | 
    
         
            +
                scan
         
     | 
| 
      
 107 
     | 
    
         
            +
             
     | 
| 
      
 108 
     | 
    
         
            +
                unless db_message = @db_mailbox.messages_dataset.filter(:guid => guid).first
         
     | 
| 
      
 109 
     | 
    
         
            +
                  warn "message not found in local db: #{guid}"
         
     | 
| 
      
 110 
     | 
    
         
            +
                  return nil
         
     | 
| 
      
 111 
     | 
    
         
            +
                end
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
                debug "#{peek ? 'peeking at' : 'fetching'} message: #{guid}"
         
     | 
| 
      
 114 
     | 
    
         
            +
             
     | 
| 
      
 115 
     | 
    
         
            +
                imap_uid_fetch([db_message.uid], [(peek ? 'BODY.PEEK[]' : 'BODY[]'), 'FLAGS', 'INTERNALDATE', 'ENVELOPE']) do |fetch_data|
         
     | 
| 
      
 116 
     | 
    
         
            +
                  data = fetch_data.first
         
     | 
| 
      
 117 
     | 
    
         
            +
                  check_response_fields(data, 'BODY[]', 'FLAGS', 'INTERNALDATE', 'ENVELOPE')
         
     | 
| 
      
 118 
     | 
    
         
            +
             
     | 
| 
      
 119 
     | 
    
         
            +
                  return Message.new(guid, data.attr['ENVELOPE'], data.attr['BODY[]'],
         
     | 
| 
      
 120 
     | 
    
         
            +
                      data.attr['FLAGS'], Time.parse(data.attr['INTERNALDATE']))
         
     | 
| 
      
 121 
     | 
    
         
            +
                end
         
     | 
| 
      
 122 
     | 
    
         
            +
             
     | 
| 
      
 123 
     | 
    
         
            +
                warn "message not found on server: #{guid}"
         
     | 
| 
      
 124 
     | 
    
         
            +
                return nil
         
     | 
| 
      
 125 
     | 
    
         
            +
              end
         
     | 
| 
      
 126 
     | 
    
         
            +
              alias [] fetch
         
     | 
| 
      
 127 
     | 
    
         
            +
             
     | 
| 
      
 128 
     | 
    
         
            +
              # Returns +true+ if a message with the specified Larch guid exists in this
         
     | 
| 
      
 129 
     | 
    
         
            +
              # mailbox, +false+ otherwise.
         
     | 
| 
      
 130 
     | 
    
         
            +
              def has_guid?(guid)
         
     | 
| 
      
 131 
     | 
    
         
            +
                scan
         
     | 
| 
      
 132 
     | 
    
         
            +
                @db_mailbox.messages_dataset.filter(:guid => guid).count > 0
         
     | 
| 
      
 133 
     | 
    
         
            +
              end
         
     | 
| 
      
 134 
     | 
    
         
            +
             
     | 
| 
      
 135 
     | 
    
         
            +
              # Gets the number of messages in this mailbox.
         
     | 
| 
      
 136 
     | 
    
         
            +
              def length
         
     | 
| 
      
 137 
     | 
    
         
            +
                scan
         
     | 
| 
      
 138 
     | 
    
         
            +
                @db_mailbox.messages_dataset.count
         
     | 
| 
      
 139 
     | 
    
         
            +
              end
         
     | 
| 
      
 140 
     | 
    
         
            +
              alias size length
         
     | 
| 
      
 141 
     | 
    
         
            +
             
     | 
| 
      
 142 
     | 
    
         
            +
              # Returns an Array of Larch::IMAP::Mailbox objects representing mailboxes that
         
     | 
| 
      
 143 
     | 
    
         
            +
              # are first-level children of this mailbox.
         
     | 
| 
      
 144 
     | 
    
         
            +
              def mailboxes
         
     | 
| 
      
 145 
     | 
    
         
            +
                return [] if @attr.include?(:Noinferiors)
         
     | 
| 
      
 146 
     | 
    
         
            +
             
     | 
| 
      
 147 
     | 
    
         
            +
                all        = @imap.safely{ @imap.conn.list('', "#{@name_utf7}#{@delim}%") } || []
         
     | 
| 
      
 148 
     | 
    
         
            +
                subscribed = @imap.safely{ @imap.conn.lsub('', "#{@name_utf7}#{@delim}%") } || []
         
     | 
| 
      
 149 
     | 
    
         
            +
             
     | 
| 
      
 150 
     | 
    
         
            +
                all.map{|mb| Mailbox.new(@imap, mb.name, mb.delim,
         
     | 
| 
      
 151 
     | 
    
         
            +
                    subscribed.any?{|s| s.name == mb.name}, mb.attr) }
         
     | 
| 
      
 152 
     | 
    
         
            +
              end
         
     | 
| 
      
 153 
     | 
    
         
            +
             
     | 
| 
      
 154 
     | 
    
         
            +
              # Same as fetch, but doesn't mark the message as seen.
         
     | 
| 
      
 155 
     | 
    
         
            +
              def peek(message_id)
         
     | 
| 
      
 156 
     | 
    
         
            +
                fetch(message_id, true)
         
     | 
| 
      
 157 
     | 
    
         
            +
              end
         
     | 
| 
      
 158 
     | 
    
         
            +
             
     | 
| 
      
 159 
     | 
    
         
            +
              # Resets the mailbox state.
         
     | 
| 
      
 160 
     | 
    
         
            +
              def reset
         
     | 
| 
      
 161 
     | 
    
         
            +
                @mutex.synchronize { @state = :closed }
         
     | 
| 
      
 162 
     | 
    
         
            +
              end
         
     | 
| 
      
 163 
     | 
    
         
            +
             
     | 
| 
      
 164 
     | 
    
         
            +
              # Fetches message headers from this mailbox.
         
     | 
| 
      
 165 
     | 
    
         
            +
              def scan
         
     | 
| 
      
 166 
     | 
    
         
            +
                return if @last_scan && (Time.now - @last_scan) < SCAN_INTERVAL
         
     | 
| 
      
 167 
     | 
    
         
            +
                first_scan = @last_scan.nil?
         
     | 
| 
      
 168 
     | 
    
         
            +
                @mutex.synchronize { @last_scan = Time.now }
         
     | 
| 
      
 169 
     | 
    
         
            +
             
     | 
| 
      
 170 
     | 
    
         
            +
                # Compare the mailbox's current status with its last known status.
         
     | 
| 
      
 171 
     | 
    
         
            +
                begin
         
     | 
| 
      
 172 
     | 
    
         
            +
                  return unless status = imap_status('MESSAGES', 'UIDNEXT', 'UIDVALIDITY')
         
     | 
| 
      
 173 
     | 
    
         
            +
                rescue Error => e
         
     | 
| 
      
 174 
     | 
    
         
            +
                  return if @imap.options[:create_mailbox]
         
     | 
| 
      
 175 
     | 
    
         
            +
                  raise
         
     | 
| 
      
 176 
     | 
    
         
            +
                end
         
     | 
| 
      
 177 
     | 
    
         
            +
             
     | 
| 
      
 178 
     | 
    
         
            +
                flag_range = nil
         
     | 
| 
      
 179 
     | 
    
         
            +
                full_range = nil
         
     | 
| 
      
 180 
     | 
    
         
            +
             
     | 
| 
      
 181 
     | 
    
         
            +
                if @db_mailbox.uidvalidity && @db_mailbox.uidnext &&
         
     | 
| 
      
 182 
     | 
    
         
            +
                    status['UIDVALIDITY'] == @db_mailbox.uidvalidity
         
     | 
| 
      
 183 
     | 
    
         
            +
             
     | 
| 
      
 184 
     | 
    
         
            +
                  # The UIDVALIDITY is the same as what we saw last time we scanned this
         
     | 
| 
      
 185 
     | 
    
         
            +
                  # mailbox, which means that all the existing messages in the database are
         
     | 
| 
      
 186 
     | 
    
         
            +
                  # still valid. We only need to request headers for new messages.
         
     | 
| 
      
 187 
     | 
    
         
            +
                  #
         
     | 
| 
      
 188 
     | 
    
         
            +
                  # If this is the first scan of this mailbox during this Larch session,
         
     | 
| 
      
 189 
     | 
    
         
            +
                  # then we'll also update the flags of all messages in the mailbox.
         
     | 
| 
      
 190 
     | 
    
         
            +
             
     | 
| 
      
 191 
     | 
    
         
            +
                  flag_range = 1...@db_mailbox.uidnext if first_scan
         
     | 
| 
      
 192 
     | 
    
         
            +
                  full_range = @db_mailbox.uidnext...status['UIDNEXT']
         
     | 
| 
      
 193 
     | 
    
         
            +
             
     | 
| 
      
 194 
     | 
    
         
            +
                else
         
     | 
| 
      
 195 
     | 
    
         
            +
             
     | 
| 
      
 196 
     | 
    
         
            +
                  # The UIDVALIDITY has changed or this is the first time we've scanned this
         
     | 
| 
      
 197 
     | 
    
         
            +
                  # mailbox (ever). Either way, all existing messages in the database are no
         
     | 
| 
      
 198 
     | 
    
         
            +
                  # longer valid, so we have to throw them out and re-request everything.
         
     | 
| 
      
 199 
     | 
    
         
            +
             
     | 
| 
      
 200 
     | 
    
         
            +
                  @db_mailbox.remove_all_messages
         
     | 
| 
      
 201 
     | 
    
         
            +
                  full_range = 1...status['UIDNEXT']
         
     | 
| 
      
 202 
     | 
    
         
            +
                end
         
     | 
| 
      
 203 
     | 
    
         
            +
             
     | 
| 
      
 204 
     | 
    
         
            +
                @db_mailbox.update(:uidvalidity => status['UIDVALIDITY'])
         
     | 
| 
      
 205 
     | 
    
         
            +
             
     | 
| 
      
 206 
     | 
    
         
            +
                return unless flag_range || full_range.last - full_range.first > 0
         
     | 
| 
      
 207 
     | 
    
         
            +
             
     | 
| 
      
 208 
     | 
    
         
            +
                # Open the mailbox for read-only access.
         
     | 
| 
      
 209 
     | 
    
         
            +
                return unless imap_examine
         
     | 
| 
      
 210 
     | 
    
         
            +
             
     | 
| 
      
 211 
     | 
    
         
            +
                if flag_range && flag_range.last - flag_range.first > 0
         
     | 
| 
      
 212 
     | 
    
         
            +
                  info "fetching latest message flags..."
         
     | 
| 
      
 213 
     | 
    
         
            +
             
     | 
| 
      
 214 
     | 
    
         
            +
                  expected_uids = {}
         
     | 
| 
      
 215 
     | 
    
         
            +
                  @db_mailbox.messages.each {|db_message| expected_uids[db_message.uid] = true }
         
     | 
| 
      
 216 
     | 
    
         
            +
             
     | 
| 
      
 217 
     | 
    
         
            +
                  imap_uid_fetch(flag_range, "(UID FLAGS)", 16384) do |fetch_data|
         
     | 
| 
      
 218 
     | 
    
         
            +
                    Larch.db.transaction do
         
     | 
| 
      
 219 
     | 
    
         
            +
                      fetch_data.each do |data|
         
     | 
| 
      
 220 
     | 
    
         
            +
                        check_response_fields(data, 'UID', 'FLAGS')
         
     | 
| 
      
 221 
     | 
    
         
            +
                        expected_uids.delete(data.attr['UID'])
         
     | 
| 
      
 222 
     | 
    
         
            +
             
     | 
| 
      
 223 
     | 
    
         
            +
                        @db_mailbox.messages_dataset.filter(:uid => data.attr['UID']).
         
     | 
| 
      
 224 
     | 
    
         
            +
                              update(:flags => data.attr['FLAGS'].map{|f| f.to_s }.join(','))
         
     | 
| 
      
 225 
     | 
    
         
            +
                      end
         
     | 
| 
      
 226 
     | 
    
         
            +
                    end
         
     | 
| 
      
 227 
     | 
    
         
            +
                  end
         
     | 
| 
      
 228 
     | 
    
         
            +
             
     | 
| 
      
 229 
     | 
    
         
            +
                  # Any UIDs that are in the database but weren't in the response have been
         
     | 
| 
      
 230 
     | 
    
         
            +
                  # deleted from the server, so we need to delete them from the database as
         
     | 
| 
      
 231 
     | 
    
         
            +
                  # well.
         
     | 
| 
      
 232 
     | 
    
         
            +
                  unless expected_uids.empty?
         
     | 
| 
      
 233 
     | 
    
         
            +
                    debug "removing #{expected_uids.length} deleted messages from the database..."
         
     | 
| 
      
 234 
     | 
    
         
            +
             
     | 
| 
      
 235 
     | 
    
         
            +
                    Larch.db.transaction do
         
     | 
| 
      
 236 
     | 
    
         
            +
                      expected_uids.each do |uid|
         
     | 
| 
      
 237 
     | 
    
         
            +
                        @db_mailbox.messages_dataset.filter(:uid => uid).destroy
         
     | 
| 
      
 238 
     | 
    
         
            +
                      end
         
     | 
| 
      
 239 
     | 
    
         
            +
                    end
         
     | 
| 
      
 240 
     | 
    
         
            +
                  end
         
     | 
| 
      
 241 
     | 
    
         
            +
             
     | 
| 
      
 242 
     | 
    
         
            +
                  expected_uids = nil
         
     | 
| 
      
 243 
     | 
    
         
            +
                  fetch_data    = nil
         
     | 
| 
      
 244 
     | 
    
         
            +
                end
         
     | 
| 
      
 245 
     | 
    
         
            +
             
     | 
| 
      
 246 
     | 
    
         
            +
                if full_range && full_range.last - full_range.first > 0
         
     | 
| 
      
 247 
     | 
    
         
            +
                  start    = @db_mailbox.messages_dataset.count + 1
         
     | 
| 
      
 248 
     | 
    
         
            +
                  total    = status['MESSAGES']
         
     | 
| 
      
 249 
     | 
    
         
            +
                  fetched  = 0
         
     | 
| 
      
 250 
     | 
    
         
            +
                  progress = 0
         
     | 
| 
      
 251 
     | 
    
         
            +
             
     | 
| 
      
 252 
     | 
    
         
            +
                  show_progress = total - start > FETCH_BLOCK_SIZE * 4
         
     | 
| 
      
 253 
     | 
    
         
            +
             
     | 
| 
      
 254 
     | 
    
         
            +
                  info "fetching message headers #{start} through #{total}..."
         
     | 
| 
      
 255 
     | 
    
         
            +
             
     | 
| 
      
 256 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 257 
     | 
    
         
            +
                    last_good_uid = nil
         
     | 
| 
      
 258 
     | 
    
         
            +
             
     | 
| 
      
 259 
     | 
    
         
            +
                    imap_uid_fetch(full_range, "(UID BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)] RFC822.SIZE INTERNALDATE FLAGS)") do |fetch_data|
         
     | 
| 
      
 260 
     | 
    
         
            +
                      check_response_fields(fetch_data, 'UID', 'RFC822.SIZE', 'INTERNALDATE', 'FLAGS')
         
     | 
| 
      
 261 
     | 
    
         
            +
             
     | 
| 
      
 262 
     | 
    
         
            +
                      Larch.db.transaction do
         
     | 
| 
      
 263 
     | 
    
         
            +
                        fetch_data.each do |data|
         
     | 
| 
      
 264 
     | 
    
         
            +
                          uid = data.attr['UID']
         
     | 
| 
      
 265 
     | 
    
         
            +
             
     | 
| 
      
 266 
     | 
    
         
            +
                          Database::Message.create(
         
     | 
| 
      
 267 
     | 
    
         
            +
                            :mailbox_id   => @db_mailbox.id,
         
     | 
| 
      
 268 
     | 
    
         
            +
                            :guid         => create_guid(data),
         
     | 
| 
      
 269 
     | 
    
         
            +
                            :uid          => uid,
         
     | 
| 
      
 270 
     | 
    
         
            +
                            :message_id   => parse_message_id(data.attr['BODY[HEADER.FIELDS (MESSAGE-ID)]']),
         
     | 
| 
      
 271 
     | 
    
         
            +
                            :rfc822_size  => data.attr['RFC822.SIZE'].to_i,
         
     | 
| 
      
 272 
     | 
    
         
            +
                            :internaldate => Time.parse(data.attr['INTERNALDATE']).to_i,
         
     | 
| 
      
 273 
     | 
    
         
            +
                            :flags        => data.attr['FLAGS'].map{|f| f.to_s }.join(',')
         
     | 
| 
      
 274 
     | 
    
         
            +
                          )
         
     | 
| 
      
 275 
     | 
    
         
            +
             
     | 
| 
      
 276 
     | 
    
         
            +
                          last_good_uid = uid
         
     | 
| 
      
 277 
     | 
    
         
            +
                        end
         
     | 
| 
      
 278 
     | 
    
         
            +
             
     | 
| 
      
 279 
     | 
    
         
            +
                        @db_mailbox.update(:uidnext => last_good_uid + 1)
         
     | 
| 
      
 280 
     | 
    
         
            +
                      end
         
     | 
| 
      
 281 
     | 
    
         
            +
             
     | 
| 
      
 282 
     | 
    
         
            +
                      if show_progress
         
     | 
| 
      
 283 
     | 
    
         
            +
                        fetched       += fetch_data.length
         
     | 
| 
      
 284 
     | 
    
         
            +
                        last_progress  = progress
         
     | 
| 
      
 285 
     | 
    
         
            +
                        progress       = ((100 / (total - start).to_f) * fetched).round
         
     | 
| 
      
 286 
     | 
    
         
            +
             
     | 
| 
      
 287 
     | 
    
         
            +
                        info "#{progress}% complete" if progress > last_progress
         
     | 
| 
      
 288 
     | 
    
         
            +
                      end
         
     | 
| 
      
 289 
     | 
    
         
            +
                    end
         
     | 
| 
      
 290 
     | 
    
         
            +
             
     | 
| 
      
 291 
     | 
    
         
            +
                  rescue => e
         
     | 
| 
      
 292 
     | 
    
         
            +
                    # Set this mailbox's uidnext value to the last known good UID that was
         
     | 
| 
      
 293 
     | 
    
         
            +
                    # stored in the database, plus 1. This will allow Larch to resume where
         
     | 
| 
      
 294 
     | 
    
         
            +
                    # the error occurred on the next attempt rather than having to start over.
         
     | 
| 
      
 295 
     | 
    
         
            +
                    @db_mailbox.update(:uidnext => last_good_uid + 1) if last_good_uid
         
     | 
| 
      
 296 
     | 
    
         
            +
                    raise
         
     | 
| 
      
 297 
     | 
    
         
            +
                  end
         
     | 
| 
      
 298 
     | 
    
         
            +
                end
         
     | 
| 
      
 299 
     | 
    
         
            +
             
     | 
| 
      
 300 
     | 
    
         
            +
                @db_mailbox.update(:uidnext => status['UIDNEXT'])
         
     | 
| 
      
 301 
     | 
    
         
            +
                return
         
     | 
| 
      
 302 
     | 
    
         
            +
              end
         
     | 
| 
      
 303 
     | 
    
         
            +
             
     | 
| 
      
 304 
     | 
    
         
            +
              # Subscribes to this mailbox.
         
     | 
| 
      
 305 
     | 
    
         
            +
              def subscribe(force = false)
         
     | 
| 
      
 306 
     | 
    
         
            +
                return false if subscribed? && !force
         
     | 
| 
      
 307 
     | 
    
         
            +
             
     | 
| 
      
 308 
     | 
    
         
            +
                @imap.safely { @imap.conn.subscribe(@name_utf7) } unless @imap.options[:dry_run]
         
     | 
| 
      
 309 
     | 
    
         
            +
                @mutex.synchronize { @subscribed = true }
         
     | 
| 
      
 310 
     | 
    
         
            +
                @db_mailbox.update(:subscribed => 1)
         
     | 
| 
      
 311 
     | 
    
         
            +
             
     | 
| 
      
 312 
     | 
    
         
            +
                true
         
     | 
| 
      
 313 
     | 
    
         
            +
              end
         
     | 
| 
      
 314 
     | 
    
         
            +
             
     | 
| 
      
 315 
     | 
    
         
            +
              # Returns +true+ if this mailbox is subscribed, +false+ otherwise.
         
     | 
| 
      
 316 
     | 
    
         
            +
              def subscribed?
         
     | 
| 
      
 317 
     | 
    
         
            +
                @subscribed
         
     | 
| 
      
 318 
     | 
    
         
            +
              end
         
     | 
| 
      
 319 
     | 
    
         
            +
             
     | 
| 
      
 320 
     | 
    
         
            +
              # Unsubscribes from this mailbox.
         
     | 
| 
      
 321 
     | 
    
         
            +
              def unsubscribe(force = false)
         
     | 
| 
      
 322 
     | 
    
         
            +
                return false unless subscribed? || force
         
     | 
| 
      
 323 
     | 
    
         
            +
             
     | 
| 
      
 324 
     | 
    
         
            +
                @imap.safely { @imap.conn.unsubscribe(@name_utf7) } unless @imap.options[:dry_run]
         
     | 
| 
      
 325 
     | 
    
         
            +
                @mutex.synchronize { @subscribed = false }
         
     | 
| 
      
 326 
     | 
    
         
            +
                @db_mailbox.update(:subscribed => 0)
         
     | 
| 
      
 327 
     | 
    
         
            +
             
     | 
| 
      
 328 
     | 
    
         
            +
                true
         
     | 
| 
      
 329 
     | 
    
         
            +
              end
         
     | 
| 
      
 330 
     | 
    
         
            +
             
     | 
| 
      
 331 
     | 
    
         
            +
              private
         
     | 
| 
      
 332 
     | 
    
         
            +
             
     | 
| 
      
 333 
     | 
    
         
            +
              # Checks the specified Net::IMAP::FetchData object and raises a
         
     | 
| 
      
 334 
     | 
    
         
            +
              # Larch::IMAP::Error unless it contains all the specified _fields_.
         
     | 
| 
      
 335 
     | 
    
         
            +
              #
         
     | 
| 
      
 336 
     | 
    
         
            +
              # _data_ can be a single object or an Array of objects; if it's an Array, then
         
     | 
| 
      
 337 
     | 
    
         
            +
              # only the first object in the Array will be checked.
         
     | 
| 
      
 338 
     | 
    
         
            +
              def check_response_fields(data, *fields)
         
     | 
| 
      
 339 
     | 
    
         
            +
                check_data = data.is_a?(Array) ? data.first : data
         
     | 
| 
      
 340 
     | 
    
         
            +
             
     | 
| 
      
 341 
     | 
    
         
            +
                fields.each do |f|
         
     | 
| 
      
 342 
     | 
    
         
            +
                  raise Error, "required data not in IMAP response: #{f}" unless check_data.attr.has_key?(f)
         
     | 
| 
      
 343 
     | 
    
         
            +
                end
         
     | 
| 
      
 344 
     | 
    
         
            +
             
     | 
| 
      
 345 
     | 
    
         
            +
                true
         
     | 
| 
      
 346 
     | 
    
         
            +
              end
         
     | 
| 
      
 347 
     | 
    
         
            +
             
     | 
| 
      
 348 
     | 
    
         
            +
              # Creates a globally unique id suitable for identifying a specific message
         
     | 
| 
      
 349 
     | 
    
         
            +
              # on any mail server (we hope) based on the given IMAP FETCH _data_.
         
     | 
| 
      
 350 
     | 
    
         
            +
              #
         
     | 
| 
      
 351 
     | 
    
         
            +
              # If the given message data includes a valid Message-Id header, then that will
         
     | 
| 
      
 352 
     | 
    
         
            +
              # be used to generate an MD5 hash. Otherwise, the hash will be generated based
         
     | 
| 
      
 353 
     | 
    
         
            +
              # on the message's RFC822.SIZE and INTERNALDATE.
         
     | 
| 
      
 354 
     | 
    
         
            +
              def create_guid(data)
         
     | 
| 
      
 355 
     | 
    
         
            +
                if message_id = parse_message_id(data.attr['BODY[HEADER.FIELDS (MESSAGE-ID)]'])
         
     | 
| 
      
 356 
     | 
    
         
            +
                  Digest::MD5.hexdigest(message_id)
         
     | 
| 
      
 357 
     | 
    
         
            +
                else
         
     | 
| 
      
 358 
     | 
    
         
            +
                  check_response_fields(data, 'RFC822.SIZE', 'INTERNALDATE')
         
     | 
| 
      
 359 
     | 
    
         
            +
             
     | 
| 
      
 360 
     | 
    
         
            +
                  Digest::MD5.hexdigest(sprintf('%d%d', data.attr['RFC822.SIZE'],
         
     | 
| 
      
 361 
     | 
    
         
            +
                      Time.parse(data.attr['INTERNALDATE']).to_i))
         
     | 
| 
      
 362 
     | 
    
         
            +
                end
         
     | 
| 
      
 363 
     | 
    
         
            +
              end
         
     | 
| 
      
 364 
     | 
    
         
            +
             
     | 
| 
      
 365 
     | 
    
         
            +
              # Examines this mailbox. If _force_ is true, the mailbox will be examined even
         
     | 
| 
      
 366 
     | 
    
         
            +
              # if it is already selected (which isn't necessary unless you want to ensure
         
     | 
| 
      
 367 
     | 
    
         
            +
              # that it's in a read-only state).
         
     | 
| 
      
 368 
     | 
    
         
            +
              #
         
     | 
| 
      
 369 
     | 
    
         
            +
              # Returns +false+ if this mailbox cannot be examined, which may be the case if
         
     | 
| 
      
 370 
     | 
    
         
            +
              # the \Noselect attribute is set.
         
     | 
| 
      
 371 
     | 
    
         
            +
              def imap_examine(force = false)
         
     | 
| 
      
 372 
     | 
    
         
            +
                return false if @attr.include?(:Noselect)
         
     | 
| 
      
 373 
     | 
    
         
            +
                return true if @state == :examined || (!force && @state == :selected)
         
     | 
| 
      
 374 
     | 
    
         
            +
             
     | 
| 
      
 375 
     | 
    
         
            +
                @imap.safely do
         
     | 
| 
      
 376 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 377 
     | 
    
         
            +
                    @mutex.synchronize { @state = :closed }
         
     | 
| 
      
 378 
     | 
    
         
            +
             
     | 
| 
      
 379 
     | 
    
         
            +
                    debug "examining mailbox"
         
     | 
| 
      
 380 
     | 
    
         
            +
                    @imap.conn.examine(@name_utf7)
         
     | 
| 
      
 381 
     | 
    
         
            +
             
     | 
| 
      
 382 
     | 
    
         
            +
                    @mutex.synchronize { @state = :examined }
         
     | 
| 
      
 383 
     | 
    
         
            +
             
     | 
| 
      
 384 
     | 
    
         
            +
                  rescue Net::IMAP::NoResponseError => e
         
     | 
| 
      
 385 
     | 
    
         
            +
                    raise Error, "unable to examine mailbox: #{e.message}"
         
     | 
| 
      
 386 
     | 
    
         
            +
                  end
         
     | 
| 
      
 387 
     | 
    
         
            +
                end
         
     | 
| 
      
 388 
     | 
    
         
            +
             
     | 
| 
      
 389 
     | 
    
         
            +
                return true
         
     | 
| 
      
 390 
     | 
    
         
            +
              end
         
     | 
| 
      
 391 
     | 
    
         
            +
             
     | 
| 
      
 392 
     | 
    
         
            +
              # Selects the mailbox if it is not already selected. If the mailbox does not
         
     | 
| 
      
 393 
     | 
    
         
            +
              # exist and _create_ is +true+, it will be created. Otherwise, a
         
     | 
| 
      
 394 
     | 
    
         
            +
              # Larch::IMAP::Error will be raised.
         
     | 
| 
      
 395 
     | 
    
         
            +
              #
         
     | 
| 
      
 396 
     | 
    
         
            +
              # Returns +false+ if this mailbox cannot be selected, which may be the case if
         
     | 
| 
      
 397 
     | 
    
         
            +
              # the \Noselect attribute is set.
         
     | 
| 
      
 398 
     | 
    
         
            +
              def imap_select(create = false)
         
     | 
| 
      
 399 
     | 
    
         
            +
                return false if @attr.include?(:Noselect)
         
     | 
| 
      
 400 
     | 
    
         
            +
                return true if @state == :selected
         
     | 
| 
      
 401 
     | 
    
         
            +
             
     | 
| 
      
 402 
     | 
    
         
            +
                @imap.safely do
         
     | 
| 
      
 403 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 404 
     | 
    
         
            +
                    @mutex.synchronize { @state = :closed }
         
     | 
| 
      
 405 
     | 
    
         
            +
             
     | 
| 
      
 406 
     | 
    
         
            +
                    debug "selecting mailbox"
         
     | 
| 
      
 407 
     | 
    
         
            +
                    @imap.conn.select(@name_utf7)
         
     | 
| 
      
 408 
     | 
    
         
            +
             
     | 
| 
      
 409 
     | 
    
         
            +
                    @mutex.synchronize { @state = :selected }
         
     | 
| 
      
 410 
     | 
    
         
            +
             
     | 
| 
      
 411 
     | 
    
         
            +
                  rescue Net::IMAP::NoResponseError => e
         
     | 
| 
      
 412 
     | 
    
         
            +
                    raise Error, "unable to select mailbox: #{e.message}" unless create
         
     | 
| 
      
 413 
     | 
    
         
            +
             
     | 
| 
      
 414 
     | 
    
         
            +
                    info "creating mailbox: #{@name}"
         
     | 
| 
      
 415 
     | 
    
         
            +
             
     | 
| 
      
 416 
     | 
    
         
            +
                    begin
         
     | 
| 
      
 417 
     | 
    
         
            +
                      @imap.conn.create(@name_utf7) unless @imap.options[:dry_run]
         
     | 
| 
      
 418 
     | 
    
         
            +
                      retry
         
     | 
| 
      
 419 
     | 
    
         
            +
                    rescue => e
         
     | 
| 
      
 420 
     | 
    
         
            +
                      raise Error, "unable to create mailbox: #{e.message}"
         
     | 
| 
      
 421 
     | 
    
         
            +
                    end
         
     | 
| 
      
 422 
     | 
    
         
            +
                  end
         
     | 
| 
      
 423 
     | 
    
         
            +
                end
         
     | 
| 
      
 424 
     | 
    
         
            +
             
     | 
| 
      
 425 
     | 
    
         
            +
                return true
         
     | 
| 
      
 426 
     | 
    
         
            +
              end
         
     | 
| 
      
 427 
     | 
    
         
            +
             
     | 
| 
      
 428 
     | 
    
         
            +
              # Sends an IMAP STATUS command and returns the status of the requested
         
     | 
| 
      
 429 
     | 
    
         
            +
              # attributes. Supported attributes include:
         
     | 
| 
      
 430 
     | 
    
         
            +
              #
         
     | 
| 
      
 431 
     | 
    
         
            +
              #   - MESSAGES
         
     | 
| 
      
 432 
     | 
    
         
            +
              #   - RECENT
         
     | 
| 
      
 433 
     | 
    
         
            +
              #   - UIDNEXT
         
     | 
| 
      
 434 
     | 
    
         
            +
              #   - UIDVALIDITY
         
     | 
| 
      
 435 
     | 
    
         
            +
              #   - UNSEEN
         
     | 
| 
      
 436 
     | 
    
         
            +
              def imap_status(*attr)
         
     | 
| 
      
 437 
     | 
    
         
            +
                @imap.safely do
         
     | 
| 
      
 438 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 439 
     | 
    
         
            +
                    debug "getting mailbox status"
         
     | 
| 
      
 440 
     | 
    
         
            +
                    @imap.conn.status(@name_utf7, attr)
         
     | 
| 
      
 441 
     | 
    
         
            +
                  rescue Net::IMAP::NoResponseError => e
         
     | 
| 
      
 442 
     | 
    
         
            +
                    raise Error, "unable to get status of mailbox: #{e.message}"
         
     | 
| 
      
 443 
     | 
    
         
            +
                  end
         
     | 
| 
      
 444 
     | 
    
         
            +
                end
         
     | 
| 
      
 445 
     | 
    
         
            +
              end
         
     | 
| 
      
 446 
     | 
    
         
            +
             
     | 
| 
      
 447 
     | 
    
         
            +
              # Fetches the specified _fields_ for the specified _set_ of UIDs, which can be
         
     | 
| 
      
 448 
     | 
    
         
            +
              # a number, Range, or Array of UIDs.
         
     | 
| 
      
 449 
     | 
    
         
            +
              #
         
     | 
| 
      
 450 
     | 
    
         
            +
              # If _set_ is a number, an Array containing a single Net::IMAP::FetchData
         
     | 
| 
      
 451 
     | 
    
         
            +
              # object will be yielded to the given block.
         
     | 
| 
      
 452 
     | 
    
         
            +
              #
         
     | 
| 
      
 453 
     | 
    
         
            +
              # If _set_ is a Range or Array of UIDs, Arrays of up to <i>block_size</i>
         
     | 
| 
      
 454 
     | 
    
         
            +
              # Net::IMAP::FetchData objects will be yielded until all requested messages
         
     | 
| 
      
 455 
     | 
    
         
            +
              # have been fetched.
         
     | 
| 
      
 456 
     | 
    
         
            +
              #
         
     | 
| 
      
 457 
     | 
    
         
            +
              # However, if _set_ is a Range with an end value of -1, a single Array
         
     | 
| 
      
 458 
     | 
    
         
            +
              # containing all requested messages will be yielded, since it's impossible to
         
     | 
| 
      
 459 
     | 
    
         
            +
              # divide an infinite range into finite blocks.
         
     | 
| 
      
 460 
     | 
    
         
            +
              def imap_uid_fetch(set, fields, block_size = FETCH_BLOCK_SIZE, &block) # :yields: fetch_data
         
     | 
| 
      
 461 
     | 
    
         
            +
                if set.is_a?(Numeric) || (set.is_a?(Range) && set.last < 0)
         
     | 
| 
      
 462 
     | 
    
         
            +
                  data = @imap.safely do
         
     | 
| 
      
 463 
     | 
    
         
            +
                    imap_examine
         
     | 
| 
      
 464 
     | 
    
         
            +
                    @imap.conn.uid_fetch(set, fields)
         
     | 
| 
      
 465 
     | 
    
         
            +
                  end
         
     | 
| 
      
 466 
     | 
    
         
            +
             
     | 
| 
      
 467 
     | 
    
         
            +
                  yield data unless data.nil?
         
     | 
| 
      
 468 
     | 
    
         
            +
                end
         
     | 
| 
      
 469 
     | 
    
         
            +
             
     | 
| 
      
 470 
     | 
    
         
            +
                blocks = []
         
     | 
| 
      
 471 
     | 
    
         
            +
                pos    = 0
         
     | 
| 
      
 472 
     | 
    
         
            +
             
     | 
| 
      
 473 
     | 
    
         
            +
                if set.is_a?(Array)
         
     | 
| 
      
 474 
     | 
    
         
            +
                  while pos < set.length
         
     | 
| 
      
 475 
     | 
    
         
            +
                    blocks += set[pos, block_size]
         
     | 
| 
      
 476 
     | 
    
         
            +
                    pos    += block_size
         
     | 
| 
      
 477 
     | 
    
         
            +
                  end
         
     | 
| 
      
 478 
     | 
    
         
            +
             
     | 
| 
      
 479 
     | 
    
         
            +
                elsif set.is_a?(Range)
         
     | 
| 
      
 480 
     | 
    
         
            +
                  pos = set.first - 1
         
     | 
| 
      
 481 
     | 
    
         
            +
             
     | 
| 
      
 482 
     | 
    
         
            +
                  while pos < set.last
         
     | 
| 
      
 483 
     | 
    
         
            +
                    blocks << ((pos + 1)..[set.last, pos += block_size].min)
         
     | 
| 
      
 484 
     | 
    
         
            +
                  end
         
     | 
| 
      
 485 
     | 
    
         
            +
                end
         
     | 
| 
      
 486 
     | 
    
         
            +
             
     | 
| 
      
 487 
     | 
    
         
            +
                blocks.each do |block|
         
     | 
| 
      
 488 
     | 
    
         
            +
                  data = @imap.safely do
         
     | 
| 
      
 489 
     | 
    
         
            +
                    imap_examine
         
     | 
| 
      
 490 
     | 
    
         
            +
                    @imap.conn.uid_fetch(block, fields)
         
     | 
| 
      
 491 
     | 
    
         
            +
                  end
         
     | 
| 
      
 492 
     | 
    
         
            +
             
     | 
| 
      
 493 
     | 
    
         
            +
                  yield data unless data.nil?
         
     | 
| 
      
 494 
     | 
    
         
            +
                end
         
     | 
| 
      
 495 
     | 
    
         
            +
              end
         
     | 
| 
      
 496 
     | 
    
         
            +
             
     | 
| 
      
 497 
     | 
    
         
            +
              # Parses a Message-Id header out of _str_ and returns it, or +nil+ if _str_
         
     | 
| 
      
 498 
     | 
    
         
            +
              # doesn't contain a valid Message-Id header.
         
     | 
| 
      
 499 
     | 
    
         
            +
              def parse_message_id(str)
         
     | 
| 
      
 500 
     | 
    
         
            +
                return str =~ REGEX_MESSAGE_ID ? $1 : nil
         
     | 
| 
      
 501 
     | 
    
         
            +
              end
         
     | 
| 
      
 502 
     | 
    
         
            +
             
     | 
| 
      
 503 
     | 
    
         
            +
            end
         
     | 
| 
      
 504 
     | 
    
         
            +
             
     | 
| 
      
 505 
     | 
    
         
            +
            end; end
         
     |