gitlab-mail_room 0.0.8 → 0.0.9
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.
- checksums.yaml +4 -4
- data/.gitlab-ci.yml +9 -1
- data/README.md +15 -0
- data/lib/mail_room.rb +2 -0
- data/lib/mail_room/cli.rb +1 -1
- data/lib/mail_room/configuration.rb +11 -1
- data/lib/mail_room/connection.rb +7 -181
- data/lib/mail_room/coordinator.rb +8 -4
- data/lib/mail_room/crash_handler.rb +0 -1
- data/lib/mail_room/health_check.rb +60 -0
- data/lib/mail_room/imap.rb +8 -0
- data/lib/mail_room/imap/connection.rb +200 -0
- data/lib/mail_room/imap/message.rb +19 -0
- data/lib/mail_room/mailbox.rb +5 -4
- data/lib/mail_room/mailbox_watcher.rb +3 -1
- data/lib/mail_room/message.rb +16 -0
- data/lib/mail_room/version.rb +1 -1
- data/mail_room.gemspec +1 -0
- data/spec/fixtures/test_config.yml +3 -0
- data/spec/lib/cli_spec.rb +3 -3
- data/spec/lib/configuration_spec.rb +9 -1
- data/spec/lib/coordinator_spec.rb +16 -2
- data/spec/lib/health_check_spec.rb +57 -0
- data/spec/lib/{connection_spec.rb → imap/connection_spec.rb} +12 -8
- data/spec/lib/imap/message_spec.rb +36 -0
- data/spec/lib/mailbox_spec.rb +10 -10
- data/spec/lib/mailbox_watcher_spec.rb +4 -4
- data/spec/lib/message_spec.rb +35 -0
- metadata +29 -4
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 046d032389d041dfea1d880c47ff567ad6ccd97439ba05db04f49fe8f6fd8596
         | 
| 4 | 
            +
              data.tar.gz: c1eda45ae5d2cbab863eabb843adfb89694ea563f73101bbf268fba18c25d175
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 58986de036f80e17d25440c25d4d6bbdb54a0340f865b455847f7a7bf4d2206ae4fefccb91edbb3b56990cf468505501ad1fa30f5223f589483a1e291a59c49a
         | 
| 7 | 
            +
              data.tar.gz: a085cd90036662bf26516bd5574a64d921cfff6b0728ec3350cac8eba80f75faef08dc0a69bf4909adb6e31a1f62ff9e4f2d62560eff5226e493af777eaa5810
         | 
    
        data/.gitlab-ci.yml
    CHANGED
    
    | @@ -10,7 +10,7 @@ services: | |
| 10 10 | 
             
              variables:
         | 
| 11 11 | 
             
                REDIS_URL: redis://redis:6379
         | 
| 12 12 | 
             
              script:
         | 
| 13 | 
            -
             | 
| 13 | 
            +
                - bundle exec rspec spec
         | 
| 14 14 | 
             
              before_script:
         | 
| 15 15 | 
             
                - apt update && apt install -y libicu-dev
         | 
| 16 16 | 
             
                - ruby -v                                   # Print out ruby version for debugging
         | 
| @@ -30,3 +30,11 @@ rspec-2.5: | |
| 30 30 | 
             
            rspec-2.6:
         | 
| 31 31 | 
             
              image: "ruby:2.6"
         | 
| 32 32 | 
             
              <<: *test
         | 
| 33 | 
            +
             | 
| 34 | 
            +
            rspec-2.7:
         | 
| 35 | 
            +
              image: "ruby:2.7"
         | 
| 36 | 
            +
              <<: *test
         | 
| 37 | 
            +
             | 
| 38 | 
            +
            rspec-3.0:
         | 
| 39 | 
            +
              image: "ruby:3.0"
         | 
| 40 | 
            +
              <<: *test
         | 
    
        data/README.md
    CHANGED
    
    | @@ -55,6 +55,9 @@ You will also need to install `faraday` or `letter_opener` if you use the `postb | |
| 55 55 |  | 
| 56 56 | 
             
            ```yaml
         | 
| 57 57 | 
             
            ---
         | 
| 58 | 
            +
            :health_check:
         | 
| 59 | 
            +
              - :address: "127.0.0.1"
         | 
| 60 | 
            +
                :port: 8080
         | 
| 58 61 | 
             
            :mailboxes:
         | 
| 59 62 | 
             
              -
         | 
| 60 63 | 
             
                :email: "user1@gmail.com"
         | 
| @@ -119,6 +122,18 @@ You will also need to install `faraday` or `letter_opener` if you use the `postb | |
| 119 122 | 
             
            **Note:** If using `delete_after_delivery`, you also probably want to use
         | 
| 120 123 | 
             
            `expunge_deleted` unless you really know what you're doing.
         | 
| 121 124 |  | 
| 125 | 
            +
            ## health_check ##
         | 
| 126 | 
            +
             | 
| 127 | 
            +
            Requires `webrick` gem to be installed.
         | 
| 128 | 
            +
             | 
| 129 | 
            +
            This option enables an HTTP server that listens to a bind address
         | 
| 130 | 
            +
            defined by `address` and `port`. The following endpoints are supported:
         | 
| 131 | 
            +
             | 
| 132 | 
            +
            * `/liveness`: This returns a 200 status code with `OK` as the body if
         | 
| 133 | 
            +
            the server is running. Otherwise, it returns a 500 status code.
         | 
| 134 | 
            +
             | 
| 135 | 
            +
            This feature is not included in upstream `mail_room` and is specific to GitLab.
         | 
| 136 | 
            +
             | 
| 122 137 | 
             
            ## delivery_method ##
         | 
| 123 138 |  | 
| 124 139 | 
             
            ### postback ###
         | 
    
        data/lib/mail_room.rb
    CHANGED
    
    | @@ -7,8 +7,10 @@ end | |
| 7 7 |  | 
| 8 8 | 
             
            require "mail_room/version"
         | 
| 9 9 | 
             
            require "mail_room/configuration"
         | 
| 10 | 
            +
            require "mail_room/health_check"
         | 
| 10 11 | 
             
            require "mail_room/mailbox"
         | 
| 11 12 | 
             
            require "mail_room/mailbox_watcher"
         | 
| 13 | 
            +
            require "mail_room/message"
         | 
| 12 14 | 
             
            require "mail_room/connection"
         | 
| 13 15 | 
             
            require "mail_room/coordinator"
         | 
| 14 16 | 
             
            require "mail_room/cli"
         | 
    
        data/lib/mail_room/cli.rb
    CHANGED
    
    | @@ -42,7 +42,7 @@ module MailRoom | |
| 42 42 | 
             
                  end.parse!(args)
         | 
| 43 43 |  | 
| 44 44 | 
             
                  self.configuration = Configuration.new(options)
         | 
| 45 | 
            -
                  self.coordinator = Coordinator.new(configuration.mailboxes)
         | 
| 45 | 
            +
                  self.coordinator = Coordinator.new(configuration.mailboxes, configuration.health_check)
         | 
| 46 46 | 
             
                end
         | 
| 47 47 |  | 
| 48 48 | 
             
                # Start the coordinator running, sets up signal traps
         | 
| @@ -4,7 +4,7 @@ module MailRoom | |
| 4 4 | 
             
              # Wraps configuration for a set of individual mailboxes with global config
         | 
| 5 5 | 
             
              # @author Tony Pitale
         | 
| 6 6 | 
             
              class Configuration
         | 
| 7 | 
            -
                attr_accessor :mailboxes, :log_path, :quiet
         | 
| 7 | 
            +
                attr_accessor :mailboxes, :log_path, :quiet, :health_check
         | 
| 8 8 |  | 
| 9 9 | 
             
                # Initialize a new configuration of mailboxes
         | 
| 10 10 | 
             
                def initialize(options={})
         | 
| @@ -18,6 +18,7 @@ module MailRoom | |
| 18 18 | 
             
                      config_file = YAML.load(erb.result)
         | 
| 19 19 |  | 
| 20 20 | 
             
                      set_mailboxes(config_file[:mailboxes])
         | 
| 21 | 
            +
                      set_health_check(config_file[:health_check])
         | 
| 21 22 | 
             
                    rescue => e
         | 
| 22 23 | 
             
                      raise e unless quiet
         | 
| 23 24 | 
             
                    end
         | 
| @@ -32,5 +33,14 @@ module MailRoom | |
| 32 33 | 
             
                    self.mailboxes << Mailbox.new(attributes)
         | 
| 33 34 | 
             
                  end
         | 
| 34 35 | 
             
                end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                # Builds the health checker from YAML configuration
         | 
| 38 | 
            +
                #
         | 
| 39 | 
            +
                # @param health_check_config nil or a Hash containing :address and :port
         | 
| 40 | 
            +
                def set_health_check(health_check_config)
         | 
| 41 | 
            +
                  return unless health_check_config
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  self.health_check = HealthCheck.new(health_check_config)
         | 
| 44 | 
            +
                end
         | 
| 35 45 | 
             
              end
         | 
| 36 46 | 
             
            end
         | 
    
        data/lib/mail_room/connection.rb
    CHANGED
    
    | @@ -1,197 +1,23 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            module MailRoom
         | 
| 2 4 | 
             
              class Connection
         | 
| 5 | 
            +
                attr_reader :mailbox, :new_message_handler
         | 
| 6 | 
            +
             | 
| 3 7 | 
             
                def initialize(mailbox)
         | 
| 4 8 | 
             
                  @mailbox = mailbox
         | 
| 5 | 
            -
             | 
| 6 | 
            -
                  # log in and set the mailbox
         | 
| 7 | 
            -
                  reset
         | 
| 8 | 
            -
                  setup
         | 
| 9 9 | 
             
                end
         | 
| 10 10 |  | 
| 11 11 | 
             
                def on_new_message(&block)
         | 
| 12 12 | 
             
                  @new_message_handler = block
         | 
| 13 13 | 
             
                end
         | 
| 14 14 |  | 
| 15 | 
            -
                # is the connection logged in?
         | 
| 16 | 
            -
                # @return [Boolean]
         | 
| 17 | 
            -
                def logged_in?
         | 
| 18 | 
            -
                  @logged_in
         | 
| 19 | 
            -
                end
         | 
| 20 | 
            -
             | 
| 21 | 
            -
                # is the connection blocked idling?
         | 
| 22 | 
            -
                # @return [Boolean]
         | 
| 23 | 
            -
                def idling?
         | 
| 24 | 
            -
                  @idling
         | 
| 25 | 
            -
                end
         | 
| 26 | 
            -
             | 
| 27 | 
            -
                # is the imap connection closed?
         | 
| 28 | 
            -
                # @return [Boolean]
         | 
| 29 | 
            -
                def disconnected?
         | 
| 30 | 
            -
                  imap.disconnected?
         | 
| 31 | 
            -
                end
         | 
| 32 | 
            -
             | 
| 33 | 
            -
                # is the connection ready to idle?
         | 
| 34 | 
            -
                # @return [Boolean]
         | 
| 35 | 
            -
                def ready_to_idle?
         | 
| 36 | 
            -
                  logged_in? && !idling?
         | 
| 37 | 
            -
                end
         | 
| 38 | 
            -
             | 
| 39 | 
            -
                def quit
         | 
| 40 | 
            -
                  stop_idling
         | 
| 41 | 
            -
                  reset
         | 
| 42 | 
            -
                end
         | 
| 43 | 
            -
             | 
| 44 15 | 
             
                def wait
         | 
| 45 | 
            -
                   | 
| 46 | 
            -
                    # in case we missed any between idles
         | 
| 47 | 
            -
                    process_mailbox
         | 
| 48 | 
            -
             | 
| 49 | 
            -
                    idle
         | 
| 50 | 
            -
             | 
| 51 | 
            -
                    process_mailbox
         | 
| 52 | 
            -
                  rescue Net::IMAP::Error, IOError => e
         | 
| 53 | 
            -
                    @mailbox.logger.warn({ context: @mailbox.context, action: "Disconnected. Resetting...", error: e.message })
         | 
| 54 | 
            -
                    reset
         | 
| 55 | 
            -
                    setup
         | 
| 56 | 
            -
                  end
         | 
| 57 | 
            -
                end
         | 
| 58 | 
            -
             | 
| 59 | 
            -
                private
         | 
| 60 | 
            -
             | 
| 61 | 
            -
                def reset
         | 
| 62 | 
            -
                  @imap = nil
         | 
| 63 | 
            -
                  @logged_in = false
         | 
| 64 | 
            -
                  @idling = false
         | 
| 65 | 
            -
                end
         | 
| 66 | 
            -
             | 
| 67 | 
            -
                def setup
         | 
| 68 | 
            -
                  @mailbox.logger.info({ context: @mailbox.context, action: "Starting TLS session" })
         | 
| 69 | 
            -
                  start_tls
         | 
| 70 | 
            -
             | 
| 71 | 
            -
                  @mailbox.logger.info({ context: @mailbox.context, action: "Logging into mailbox" })
         | 
| 72 | 
            -
                  log_in
         | 
| 73 | 
            -
             | 
| 74 | 
            -
                  @mailbox.logger.info({ context: @mailbox.context, action: "Setting mailbox" })
         | 
| 75 | 
            -
                  set_mailbox
         | 
| 16 | 
            +
                  raise NotImplementedError
         | 
| 76 17 | 
             
                end
         | 
| 77 18 |  | 
| 78 | 
            -
                 | 
| 79 | 
            -
             | 
| 80 | 
            -
                  @imap ||= Net::IMAP.new(@mailbox.host, :port => @mailbox.port, :ssl => @mailbox.ssl_options)
         | 
| 81 | 
            -
                end
         | 
| 82 | 
            -
             | 
| 83 | 
            -
                # start a TLS session
         | 
| 84 | 
            -
                def start_tls
         | 
| 85 | 
            -
                  imap.starttls if @mailbox.start_tls
         | 
| 86 | 
            -
                end
         | 
| 87 | 
            -
             | 
| 88 | 
            -
                # send the imap login command to google
         | 
| 89 | 
            -
                def log_in
         | 
| 90 | 
            -
                  imap.login(@mailbox.email, @mailbox.password)
         | 
| 91 | 
            -
                  @logged_in = true
         | 
| 92 | 
            -
                end
         | 
| 93 | 
            -
             | 
| 94 | 
            -
                # select the mailbox name we want to use
         | 
| 95 | 
            -
                def set_mailbox
         | 
| 96 | 
            -
                  imap.select(@mailbox.name) if logged_in?
         | 
| 97 | 
            -
                end
         | 
| 98 | 
            -
             | 
| 99 | 
            -
                # is the response for a new message?
         | 
| 100 | 
            -
                # @param response [Net::IMAP::TaggedResponse] the imap response from idle
         | 
| 101 | 
            -
                # @return [Boolean]
         | 
| 102 | 
            -
                def message_exists?(response)
         | 
| 103 | 
            -
                  response.respond_to?(:name) && response.name == 'EXISTS'
         | 
| 104 | 
            -
                end
         | 
| 105 | 
            -
             | 
| 106 | 
            -
                # @private
         | 
| 107 | 
            -
                def idle_handler
         | 
| 108 | 
            -
                  lambda {|response| imap.idle_done if message_exists?(response)}
         | 
| 109 | 
            -
                end
         | 
| 110 | 
            -
             | 
| 111 | 
            -
                # maintain an imap idle connection
         | 
| 112 | 
            -
                def idle
         | 
| 113 | 
            -
                  return unless ready_to_idle?
         | 
| 114 | 
            -
             | 
| 115 | 
            -
                  @mailbox.logger.info({ context: @mailbox.context, action: "Idling" })
         | 
| 116 | 
            -
                  @idling = true
         | 
| 117 | 
            -
             | 
| 118 | 
            -
                  imap.idle(@mailbox.idle_timeout, &idle_handler)
         | 
| 119 | 
            -
                ensure
         | 
| 120 | 
            -
                  @idling = false
         | 
| 121 | 
            -
                end
         | 
| 122 | 
            -
             | 
| 123 | 
            -
                # trigger the idle to finish and wait for the thread to finish
         | 
| 124 | 
            -
                def stop_idling
         | 
| 125 | 
            -
                  return unless idling?
         | 
| 126 | 
            -
             | 
| 127 | 
            -
                  imap.idle_done
         | 
| 128 | 
            -
             | 
| 129 | 
            -
                  # idling_thread.join
         | 
| 130 | 
            -
                  # self.idling_thread = nil
         | 
| 131 | 
            -
                end
         | 
| 132 | 
            -
             | 
| 133 | 
            -
                def process_mailbox
         | 
| 134 | 
            -
                  return unless @new_message_handler
         | 
| 135 | 
            -
                  @mailbox.logger.info({ context: @mailbox.context, action: "Processing started" })
         | 
| 136 | 
            -
             | 
| 137 | 
            -
                  msgs = new_messages
         | 
| 138 | 
            -
             | 
| 139 | 
            -
                  any_deletions = msgs.
         | 
| 140 | 
            -
                                  # deliver each new message, collect success
         | 
| 141 | 
            -
                                  map(&@new_message_handler).
         | 
| 142 | 
            -
                                  # include messages with success
         | 
| 143 | 
            -
                                  zip(msgs).
         | 
| 144 | 
            -
                                  # filter failed deliveries, collect message
         | 
| 145 | 
            -
                                  select(&:first).map(&:last).
         | 
| 146 | 
            -
                                  # scrub delivered messages
         | 
| 147 | 
            -
                                  map { |message| scrub(message) }.
         | 
| 148 | 
            -
                                  any?
         | 
| 149 | 
            -
             | 
| 150 | 
            -
                  imap.expunge if @mailbox.expunge_deleted && any_deletions
         | 
| 151 | 
            -
                end
         | 
| 152 | 
            -
             | 
| 153 | 
            -
                def scrub(message)
         | 
| 154 | 
            -
                  if @mailbox.delete_after_delivery
         | 
| 155 | 
            -
                    imap.store(message.seqno, "+FLAGS", [Net::IMAP::DELETED])
         | 
| 156 | 
            -
                    true
         | 
| 157 | 
            -
                  end
         | 
| 158 | 
            -
                end
         | 
| 159 | 
            -
             | 
| 160 | 
            -
                # @private
         | 
| 161 | 
            -
                # fetch all messages for the new message ids
         | 
| 162 | 
            -
                def new_messages
         | 
| 163 | 
            -
                  # Both of these calls may results in
         | 
| 164 | 
            -
                  #   imap raising an EOFError, we handle
         | 
| 165 | 
            -
                  #   this exception in the watcher
         | 
| 166 | 
            -
                  messages_for_ids(new_message_ids)
         | 
| 167 | 
            -
                end
         | 
| 168 | 
            -
             | 
| 169 | 
            -
                # TODO: label messages?
         | 
| 170 | 
            -
                #   @imap.store(id, "+X-GM-LABELS", [label])
         | 
| 171 | 
            -
             | 
| 172 | 
            -
                # @private
         | 
| 173 | 
            -
                # search for all new (unseen) message ids
         | 
| 174 | 
            -
                # @return [Array<Integer>] message ids
         | 
| 175 | 
            -
                def new_message_ids
         | 
| 176 | 
            -
                  # uid_search still leaves messages UNSEEN
         | 
| 177 | 
            -
                  all_unread = imap.uid_search(@mailbox.search_command)
         | 
| 178 | 
            -
             | 
| 179 | 
            -
                  all_unread = all_unread.slice(0, @mailbox.limit_max_unread) unless @mailbox.limit_max_unread.nil? || @mailbox.limit_max_unread == 0
         | 
| 180 | 
            -
             | 
| 181 | 
            -
                  to_deliver = all_unread.select { |uid| @mailbox.deliver?(uid) }
         | 
| 182 | 
            -
                  @mailbox.logger.info({ context: @mailbox.context, action: "Getting new messages", unread: {count: all_unread.count, ids: all_unread}, to_be_delivered: { count: to_deliver.count, ids: to_deliver } })
         | 
| 183 | 
            -
                  to_deliver
         | 
| 184 | 
            -
                end
         | 
| 185 | 
            -
             | 
| 186 | 
            -
                # @private
         | 
| 187 | 
            -
                # fetch the email for all given ids in RFC822 format
         | 
| 188 | 
            -
                # @param ids [Array<Integer>] list of message ids
         | 
| 189 | 
            -
                # @return [Array<Net::IMAP::FetchData>] the net/imap messages for the given ids
         | 
| 190 | 
            -
                def messages_for_ids(uids)
         | 
| 191 | 
            -
                  return [] if uids.empty?
         | 
| 192 | 
            -
             | 
| 193 | 
            -
                  # uid_fetch marks as SEEN, will not be re-fetched for UNSEEN
         | 
| 194 | 
            -
                  imap.uid_fetch(uids, "RFC822")
         | 
| 19 | 
            +
                def quit
         | 
| 20 | 
            +
                  raise NotImplementedError
         | 
| 195 21 | 
             
                end
         | 
| 196 22 | 
             
              end
         | 
| 197 23 | 
             
            end
         | 
| @@ -2,13 +2,15 @@ module MailRoom | |
| 2 2 | 
             
              # Coordinate the mailbox watchers
         | 
| 3 3 | 
             
              # @author Tony Pitale
         | 
| 4 4 | 
             
              class Coordinator
         | 
| 5 | 
            -
                attr_accessor :watchers, :running
         | 
| 5 | 
            +
                attr_accessor :watchers, :running, :health_check
         | 
| 6 6 |  | 
| 7 7 | 
             
                # build watchers for a set of mailboxes
         | 
| 8 8 | 
             
                # @params mailboxes [Array<MailRoom::Mailbox>] mailboxes to be watched
         | 
| 9 | 
            -
                 | 
| 9 | 
            +
                # @params health_check <MailRoom::HealthCheck> health checker to run
         | 
| 10 | 
            +
                def initialize(mailboxes, health_check = nil)
         | 
| 10 11 | 
             
                  self.watchers = []
         | 
| 11 12 |  | 
| 13 | 
            +
                  @health_check = health_check
         | 
| 12 14 | 
             
                  mailboxes.each {|box| self.watchers << MailboxWatcher.new(box)}
         | 
| 13 15 | 
             
                end
         | 
| 14 16 |  | 
| @@ -16,10 +18,11 @@ module MailRoom | |
| 16 18 |  | 
| 17 19 | 
             
                # start each of the watchers to running
         | 
| 18 20 | 
             
                def run
         | 
| 21 | 
            +
                  health_check&.run
         | 
| 19 22 | 
             
                  watchers.each(&:run)
         | 
| 20 | 
            -
             | 
| 23 | 
            +
             | 
| 21 24 | 
             
                  self.running = true
         | 
| 22 | 
            -
             | 
| 25 | 
            +
             | 
| 23 26 | 
             
                  sleep_while_running
         | 
| 24 27 | 
             
                ensure
         | 
| 25 28 | 
             
                  quit
         | 
| @@ -27,6 +30,7 @@ module MailRoom | |
| 27 30 |  | 
| 28 31 | 
             
                # quit each of the watchers when we're done running
         | 
| 29 32 | 
             
                def quit
         | 
| 33 | 
            +
                  health_check&.quit
         | 
| 30 34 | 
             
                  watchers.each(&:quit)
         | 
| 31 35 | 
             
                end
         | 
| 32 36 |  | 
| @@ -0,0 +1,60 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module MailRoom
         | 
| 4 | 
            +
              class HealthCheck
         | 
| 5 | 
            +
                attr_reader :address, :port, :running
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def initialize(attributes = {})
         | 
| 8 | 
            +
                  @address = attributes[:address]
         | 
| 9 | 
            +
                  @port = attributes[:port]
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  validate!
         | 
| 12 | 
            +
                end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                def run
         | 
| 15 | 
            +
                  @server = create_server
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  @thread = Thread.new do
         | 
| 18 | 
            +
                    @server.start
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  @thread.abort_on_exception = true
         | 
| 22 | 
            +
                  @running = true
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                def quit
         | 
| 26 | 
            +
                  @running = false
         | 
| 27 | 
            +
                  @server&.shutdown
         | 
| 28 | 
            +
                  @thread&.join(60)
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                private
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                def validate!
         | 
| 34 | 
            +
                  raise 'No health check address specified' unless address
         | 
| 35 | 
            +
                  raise "Health check port #{@port.to_i} is invalid" unless port.to_i.positive?
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                def create_server
         | 
| 39 | 
            +
                  require 'webrick'
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  server = ::WEBrick::HTTPServer.new(Port: port, BindAddress: address, AccessLog: [])
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  server.mount_proc '/liveness' do |_req, res|
         | 
| 44 | 
            +
                    handle_liveness(res)
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                  server
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                def handle_liveness(res)
         | 
| 51 | 
            +
                  if @running
         | 
| 52 | 
            +
                    res.status = 200
         | 
| 53 | 
            +
                    res.body = "OK\n"
         | 
| 54 | 
            +
                  else
         | 
| 55 | 
            +
                    res.status = 500
         | 
| 56 | 
            +
                    res.body = "Not running\n"
         | 
| 57 | 
            +
                  end
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
              end
         | 
| 60 | 
            +
            end
         | 
| @@ -0,0 +1,200 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module MailRoom
         | 
| 4 | 
            +
              module IMAP
         | 
| 5 | 
            +
                class Connection < MailRoom::Connection
         | 
| 6 | 
            +
                  def initialize(mailbox)
         | 
| 7 | 
            +
                    super
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                    # log in and set the mailbox
         | 
| 10 | 
            +
                    reset
         | 
| 11 | 
            +
                    setup
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  # is the connection logged in?
         | 
| 15 | 
            +
                  # @return [Boolean]
         | 
| 16 | 
            +
                  def logged_in?
         | 
| 17 | 
            +
                    @logged_in
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  # is the connection blocked idling?
         | 
| 21 | 
            +
                  # @return [Boolean]
         | 
| 22 | 
            +
                  def idling?
         | 
| 23 | 
            +
                    @idling
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  # is the imap connection closed?
         | 
| 27 | 
            +
                  # @return [Boolean]
         | 
| 28 | 
            +
                  def disconnected?
         | 
| 29 | 
            +
                    imap.disconnected?
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  # is the connection ready to idle?
         | 
| 33 | 
            +
                  # @return [Boolean]
         | 
| 34 | 
            +
                  def ready_to_idle?
         | 
| 35 | 
            +
                    logged_in? && !idling?
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  def quit
         | 
| 39 | 
            +
                    stop_idling
         | 
| 40 | 
            +
                    reset
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  def wait
         | 
| 44 | 
            +
                    # in case we missed any between idles
         | 
| 45 | 
            +
                    process_mailbox
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    idle
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                    process_mailbox
         | 
| 50 | 
            +
                  rescue Net::IMAP::Error, IOError => e
         | 
| 51 | 
            +
                    @mailbox.logger.warn({ context: @mailbox.context, action: 'Disconnected. Resetting...', error: e.message })
         | 
| 52 | 
            +
                    reset
         | 
| 53 | 
            +
                    setup
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  private
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  def reset
         | 
| 59 | 
            +
                    @imap = nil
         | 
| 60 | 
            +
                    @logged_in = false
         | 
| 61 | 
            +
                    @idling = false
         | 
| 62 | 
            +
                  end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                  def setup
         | 
| 65 | 
            +
                    @mailbox.logger.info({ context: @mailbox.context, action: 'Starting TLS session' })
         | 
| 66 | 
            +
                    start_tls
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                    @mailbox.logger.info({ context: @mailbox.context, action: 'Logging into mailbox' })
         | 
| 69 | 
            +
                    log_in
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                    @mailbox.logger.info({ context: @mailbox.context, action: 'Setting mailbox' })
         | 
| 72 | 
            +
                    set_mailbox
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                  # build a net/imap connection to google imap
         | 
| 76 | 
            +
                  def imap
         | 
| 77 | 
            +
                    @imap ||= Net::IMAP.new(@mailbox.host, port: @mailbox.port, ssl: @mailbox.ssl_options)
         | 
| 78 | 
            +
                  end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                  # start a TLS session
         | 
| 81 | 
            +
                  def start_tls
         | 
| 82 | 
            +
                    imap.starttls if @mailbox.start_tls
         | 
| 83 | 
            +
                  end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                  # send the imap login command to google
         | 
| 86 | 
            +
                  def log_in
         | 
| 87 | 
            +
                    imap.login(@mailbox.email, @mailbox.password)
         | 
| 88 | 
            +
                    @logged_in = true
         | 
| 89 | 
            +
                  end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                  # select the mailbox name we want to use
         | 
| 92 | 
            +
                  def set_mailbox
         | 
| 93 | 
            +
                    imap.select(@mailbox.name) if logged_in?
         | 
| 94 | 
            +
                  end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                  # is the response for a new message?
         | 
| 97 | 
            +
                  # @param response [Net::IMAP::TaggedResponse] the imap response from idle
         | 
| 98 | 
            +
                  # @return [Boolean]
         | 
| 99 | 
            +
                  def message_exists?(response)
         | 
| 100 | 
            +
                    response.respond_to?(:name) && response.name == 'EXISTS'
         | 
| 101 | 
            +
                  end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                  # @private
         | 
| 104 | 
            +
                  def idle_handler
         | 
| 105 | 
            +
                    ->(response) { imap.idle_done if message_exists?(response) }
         | 
| 106 | 
            +
                  end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                  # maintain an imap idle connection
         | 
| 109 | 
            +
                  def idle
         | 
| 110 | 
            +
                    return unless ready_to_idle?
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                    @mailbox.logger.info({ context: @mailbox.context, action: 'Idling' })
         | 
| 113 | 
            +
                    @idling = true
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                    imap.idle(@mailbox.idle_timeout, &idle_handler)
         | 
| 116 | 
            +
                  ensure
         | 
| 117 | 
            +
                    @idling = false
         | 
| 118 | 
            +
                  end
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                  # trigger the idle to finish and wait for the thread to finish
         | 
| 121 | 
            +
                  def stop_idling
         | 
| 122 | 
            +
                    return unless idling?
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                    imap.idle_done
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                    # idling_thread.join
         | 
| 127 | 
            +
                    # self.idling_thread = nil
         | 
| 128 | 
            +
                  end
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                  def process_mailbox
         | 
| 131 | 
            +
                    return unless @new_message_handler
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                    @mailbox.logger.info({ context: @mailbox.context, action: 'Processing started' })
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                    msgs = new_messages
         | 
| 136 | 
            +
                    any_deletions = msgs.
         | 
| 137 | 
            +
                                    # deliver each new message, collect success
         | 
| 138 | 
            +
                                    map(&@new_message_handler).
         | 
| 139 | 
            +
                                    # include messages with success
         | 
| 140 | 
            +
                                    zip(msgs).
         | 
| 141 | 
            +
                                    # filter failed deliveries, collect message
         | 
| 142 | 
            +
                                    select(&:first).map(&:last).
         | 
| 143 | 
            +
                                    # scrub delivered messages
         | 
| 144 | 
            +
                                    map { |message| scrub(message) }
         | 
| 145 | 
            +
                                        .any?
         | 
| 146 | 
            +
             | 
| 147 | 
            +
                    imap.expunge if @mailbox.expunge_deleted && any_deletions
         | 
| 148 | 
            +
                  end
         | 
| 149 | 
            +
             | 
| 150 | 
            +
                  def scrub(message)
         | 
| 151 | 
            +
                    if @mailbox.delete_after_delivery
         | 
| 152 | 
            +
                      imap.store(message.seqno, '+FLAGS', [Net::IMAP::DELETED])
         | 
| 153 | 
            +
                      true
         | 
| 154 | 
            +
                    end
         | 
| 155 | 
            +
                  end
         | 
| 156 | 
            +
             | 
| 157 | 
            +
                  # @private
         | 
| 158 | 
            +
                  # fetch all messages for the new message ids
         | 
| 159 | 
            +
                  def new_messages
         | 
| 160 | 
            +
                    # Both of these calls may results in
         | 
| 161 | 
            +
                    #   imap raising an EOFError, we handle
         | 
| 162 | 
            +
                    #   this exception in the watcher
         | 
| 163 | 
            +
                    messages_for_ids(new_message_ids)
         | 
| 164 | 
            +
                  end
         | 
| 165 | 
            +
             | 
| 166 | 
            +
                  # TODO: label messages?
         | 
| 167 | 
            +
                  #   @imap.store(id, "+X-GM-LABELS", [label])
         | 
| 168 | 
            +
             | 
| 169 | 
            +
                  # @private
         | 
| 170 | 
            +
                  # search for all new (unseen) message ids
         | 
| 171 | 
            +
                  # @return [Array<Integer>] message ids
         | 
| 172 | 
            +
                  def new_message_ids
         | 
| 173 | 
            +
                    # uid_search still leaves messages UNSEEN
         | 
| 174 | 
            +
                    all_unread = imap.uid_search(@mailbox.search_command)
         | 
| 175 | 
            +
             | 
| 176 | 
            +
                    all_unread = all_unread.slice(0, @mailbox.limit_max_unread) if @mailbox.limit_max_unread.to_i > 0
         | 
| 177 | 
            +
             | 
| 178 | 
            +
                    to_deliver = all_unread.select { |uid| @mailbox.deliver?(uid) }
         | 
| 179 | 
            +
                    @mailbox.logger.info({ context: @mailbox.context, action: 'Getting new messages',
         | 
| 180 | 
            +
                                           unread: { count: all_unread.count, ids: all_unread }, to_be_delivered: { count: to_deliver.count, ids: to_deliver } })
         | 
| 181 | 
            +
                    to_deliver
         | 
| 182 | 
            +
                  end
         | 
| 183 | 
            +
             | 
| 184 | 
            +
                  # @private
         | 
| 185 | 
            +
                  # fetch the email for all given ids in RFC822 format
         | 
| 186 | 
            +
                  # @param ids [Array<Integer>] list of message ids
         | 
| 187 | 
            +
                  # @return [Array<MailRoom::IMAP::Message>] the net/imap messages for the given ids
         | 
| 188 | 
            +
                  def messages_for_ids(uids)
         | 
| 189 | 
            +
                    return [] if uids.empty?
         | 
| 190 | 
            +
             | 
| 191 | 
            +
                    # uid_fetch marks as SEEN, will not be re-fetched for UNSEEN
         | 
| 192 | 
            +
                    imap_messages = imap.uid_fetch(uids, 'RFC822')
         | 
| 193 | 
            +
             | 
| 194 | 
            +
                    imap_messages.each_with_object([]) do |msg, messages|
         | 
| 195 | 
            +
                      messages << ::MailRoom::IMAP::Message.new(uid: msg.attr['UID'], body: msg.attr['RFC822'], seqno: msg.seqno)
         | 
| 196 | 
            +
                    end
         | 
| 197 | 
            +
                  end
         | 
| 198 | 
            +
                end
         | 
| 199 | 
            +
              end
         | 
| 200 | 
            +
            end
         | 
| @@ -0,0 +1,19 @@ | |
| 1 | 
            +
            # frozen_string_literal:true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module MailRoom
         | 
| 4 | 
            +
              module IMAP
         | 
| 5 | 
            +
                class Message < MailRoom::Message
         | 
| 6 | 
            +
                  attr_reader :seqno
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def initialize(uid:, body:, seqno:)
         | 
| 9 | 
            +
                    super(uid: uid, body: body)
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                    @seqno = seqno
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  def ==(other)
         | 
| 15 | 
            +
                    super && seqno == other.seqno
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
            end
         | 
    
        data/lib/mail_room/mailbox.rb
    CHANGED
    
    | @@ -1,5 +1,6 @@ | |
| 1 1 | 
             
            require "mail_room/delivery"
         | 
| 2 2 | 
             
            require "mail_room/arbitration"
         | 
| 3 | 
            +
            require "mail_room/imap"
         | 
| 3 4 |  | 
| 4 5 | 
             
            module MailRoom
         | 
| 5 6 | 
             
              # Mailbox Configuration fields
         | 
| @@ -102,13 +103,13 @@ module MailRoom | |
| 102 103 | 
             
                  arbitrator.deliver?(uid)
         | 
| 103 104 | 
             
                end
         | 
| 104 105 |  | 
| 105 | 
            -
                # deliver the  | 
| 106 | 
            -
                # @param message [ | 
| 106 | 
            +
                # deliver the email message
         | 
| 107 | 
            +
                # @param message [MailRoom::Message]
         | 
| 107 108 | 
             
                def deliver(message)
         | 
| 108 | 
            -
                  body = message. | 
| 109 | 
            +
                  body = message.body
         | 
| 109 110 | 
             
                  return true unless body
         | 
| 110 111 |  | 
| 111 | 
            -
                  logger.info({context: context, uid: message. | 
| 112 | 
            +
                  logger.info({context: context, uid: message.uid, action: "sending to deliverer", deliverer: delivery.class.name, byte_size: body.bytesize})
         | 
| 112 113 | 
             
                  delivery.deliver(body)
         | 
| 113 114 | 
             
                end
         | 
| 114 115 |  | 
| @@ -1,3 +1,5 @@ | |
| 1 | 
            +
            require "mail_room/connection"
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            module MailRoom
         | 
| 2 4 | 
             
              # TODO: split up between processing and idling?
         | 
| 3 5 |  | 
| @@ -61,7 +63,7 @@ module MailRoom | |
| 61 63 |  | 
| 62 64 | 
             
                private
         | 
| 63 65 | 
             
                def connection
         | 
| 64 | 
            -
                  @connection ||= Connection.new(@mailbox)
         | 
| 66 | 
            +
                  @connection ||= ::MailRoom::IMAP::Connection.new(@mailbox)
         | 
| 65 67 | 
             
                end
         | 
| 66 68 | 
             
              end
         | 
| 67 69 | 
             
            end
         | 
| @@ -0,0 +1,16 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module MailRoom
         | 
| 4 | 
            +
              class Message
         | 
| 5 | 
            +
                attr_reader :uid, :body
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def initialize(uid:, body:)
         | 
| 8 | 
            +
                  @uid = uid
         | 
| 9 | 
            +
                  @body = body
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def ==(other)
         | 
| 13 | 
            +
                  self.class == other.class && uid == other.uid && body == other.body
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
              end
         | 
| 16 | 
            +
            end
         | 
    
        data/lib/mail_room/version.rb
    CHANGED
    
    
    
        data/mail_room.gemspec
    CHANGED
    
    | @@ -21,6 +21,7 @@ Gem::Specification.new do |gem| | |
| 21 21 | 
             
              gem.add_development_dependency "rspec", "~> 3.9"
         | 
| 22 22 | 
             
              gem.add_development_dependency "mocha", "~> 1.11"
         | 
| 23 23 | 
             
              gem.add_development_dependency "simplecov"
         | 
| 24 | 
            +
              gem.add_development_dependency "webrick", "~> 1.6"
         | 
| 24 25 |  | 
| 25 26 | 
             
              # for testing delivery methods
         | 
| 26 27 | 
             
              gem.add_development_dependency "faraday"
         | 
    
        data/spec/lib/cli_spec.rb
    CHANGED
    
    | @@ -5,14 +5,14 @@ describe MailRoom::CLI do | |
| 5 5 | 
             
              let!(:configuration) {MailRoom::Configuration.new({:config_path => config_path})}
         | 
| 6 6 | 
             
              let(:coordinator) {stub(:run => true, :quit => true)}
         | 
| 7 7 | 
             
              let(:configuration_args) { anything }
         | 
| 8 | 
            -
              let(:coordinator_args) { anything }
         | 
| 8 | 
            +
              let(:coordinator_args) { [anything, anything] }
         | 
| 9 9 |  | 
| 10 10 | 
             
              describe '.new' do
         | 
| 11 11 | 
             
                let(:args) {["-c", "a path"]}
         | 
| 12 12 |  | 
| 13 13 | 
             
                before :each do
         | 
| 14 14 | 
             
                  MailRoom::Configuration.expects(:new).with(configuration_args).returns(configuration)
         | 
| 15 | 
            -
                  MailRoom::Coordinator.stubs(:new).with(coordinator_args).returns(coordinator)
         | 
| 15 | 
            +
                  MailRoom::Coordinator.stubs(:new).with(*coordinator_args).returns(coordinator)
         | 
| 16 16 | 
             
                end
         | 
| 17 17 |  | 
| 18 18 | 
             
                context 'with configuration args' do
         | 
| @@ -27,7 +27,7 @@ describe MailRoom::CLI do | |
| 27 27 |  | 
| 28 28 | 
             
                context 'with coordinator args' do
         | 
| 29 29 | 
             
                  let(:coordinator_args) do
         | 
| 30 | 
            -
                    configuration.mailboxes
         | 
| 30 | 
            +
                    [configuration.mailboxes, anything]
         | 
| 31 31 | 
             
                  end
         | 
| 32 32 |  | 
| 33 33 | 
             
                  it 'creates a new coordinator with configuration' do
         | 
| @@ -3,7 +3,7 @@ require 'spec_helper' | |
| 3 3 | 
             
            describe MailRoom::Configuration do
         | 
| 4 4 | 
             
              let(:config_path) {File.expand_path('../fixtures/test_config.yml', File.dirname(__FILE__))}
         | 
| 5 5 |  | 
| 6 | 
            -
              describe ' | 
| 6 | 
            +
              describe '#initalize' do
         | 
| 7 7 | 
             
                context 'with config_path' do
         | 
| 8 8 | 
             
                  let(:configuration) { MailRoom::Configuration.new(:config_path => config_path) }
         | 
| 9 9 |  | 
| @@ -12,6 +12,10 @@ describe MailRoom::Configuration do | |
| 12 12 |  | 
| 13 13 | 
             
                    expect(configuration.mailboxes).to eq(['mailbox1', 'mailbox2'])
         | 
| 14 14 | 
             
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  it 'parses health check' do
         | 
| 17 | 
            +
                    expect(configuration.health_check).to be_a(MailRoom::HealthCheck)
         | 
| 18 | 
            +
                  end
         | 
| 15 19 | 
             
                end
         | 
| 16 20 |  | 
| 17 21 | 
             
                context 'without config_path' do
         | 
| @@ -23,6 +27,10 @@ describe MailRoom::Configuration do | |
| 23 27 |  | 
| 24 28 | 
             
                    expect(configuration.mailboxes).to eq([])
         | 
| 25 29 | 
             
                  end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  it 'sets the health check to nil' do
         | 
| 32 | 
            +
                    expect(configuration.health_check).to be_nil
         | 
| 33 | 
            +
                  end
         | 
| 26 34 | 
             
                end
         | 
| 27 35 | 
             
              end
         | 
| 28 36 | 
             
            end
         | 
| @@ -15,6 +15,13 @@ describe MailRoom::Coordinator do | |
| 15 15 | 
             
                  coordinator = MailRoom::Coordinator.new([])
         | 
| 16 16 | 
             
                  expect(coordinator.watchers).to eq([])
         | 
| 17 17 | 
             
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                it 'sets the health check' do
         | 
| 20 | 
            +
                  health_check = MailRoom::HealthCheck.new({ address: '127.0.0.1', port: 8080})
         | 
| 21 | 
            +
                  coordinator = MailRoom::Coordinator.new([], health_check)
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  expect(coordinator.health_check).to eq(health_check)
         | 
| 24 | 
            +
                end
         | 
| 18 25 | 
             
              end
         | 
| 19 26 |  | 
| 20 27 | 
             
              describe '#run' do
         | 
| @@ -22,15 +29,22 @@ describe MailRoom::Coordinator do | |
| 22 29 | 
             
                  watcher = stub
         | 
| 23 30 | 
             
                  watcher.stubs(:run)
         | 
| 24 31 | 
             
                  watcher.stubs(:quit)
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  health_check = stub
         | 
| 34 | 
            +
                  health_check.stubs(:run)
         | 
| 35 | 
            +
                  health_check.stubs(:quit)
         | 
| 36 | 
            +
             | 
| 25 37 | 
             
                  MailRoom::MailboxWatcher.stubs(:new).returns(watcher)
         | 
| 26 | 
            -
                  coordinator = MailRoom::Coordinator.new(['mailbox1'])
         | 
| 38 | 
            +
                  coordinator = MailRoom::Coordinator.new(['mailbox1'], health_check)
         | 
| 27 39 | 
             
                  coordinator.stubs(:sleep_while_running)
         | 
| 28 40 | 
             
                  watcher.expects(:run)
         | 
| 29 41 | 
             
                  watcher.expects(:quit)
         | 
| 42 | 
            +
                  health_check.expects(:run)
         | 
| 43 | 
            +
                  health_check.expects(:quit)
         | 
| 30 44 |  | 
| 31 45 | 
             
                  coordinator.run
         | 
| 32 46 | 
             
                end
         | 
| 33 | 
            -
             | 
| 47 | 
            +
             | 
| 34 48 | 
             
                it 'should go to sleep after running watchers' do
         | 
| 35 49 | 
             
                  coordinator = MailRoom::Coordinator.new([])
         | 
| 36 50 | 
             
                  coordinator.stubs(:running=)
         | 
| @@ -0,0 +1,57 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'spec_helper'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            describe MailRoom::HealthCheck do
         | 
| 6 | 
            +
              let(:address) { '127.0.0.1' }
         | 
| 7 | 
            +
              let(:port) { 8000 }
         | 
| 8 | 
            +
              let(:params) { { address: address, port: port } }
         | 
| 9 | 
            +
              subject { described_class.new(params) }
         | 
| 10 | 
            +
             | 
| 11 | 
            +
              describe '#initialize' do
         | 
| 12 | 
            +
                context 'with valid parameters' do
         | 
| 13 | 
            +
                  it 'validates successfully' do
         | 
| 14 | 
            +
                    expect(subject).to be_a(described_class)
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                context 'with invalid address' do
         | 
| 19 | 
            +
                  let(:address) { nil }
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  it 'raises an error' do
         | 
| 22 | 
            +
                    expect { subject }.to raise_error('No health check address specified')
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                context 'with invalid port' do
         | 
| 27 | 
            +
                  let(:port) { nil }
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  it 'raises an error' do
         | 
| 30 | 
            +
                    expect { subject }.to raise_error('Health check port 0 is invalid')
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
              end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
              describe '#run' do
         | 
| 36 | 
            +
                it 'sets running to true' do
         | 
| 37 | 
            +
                  server = stub(start: true)
         | 
| 38 | 
            +
                  subject.stubs(:create_server).returns(server)
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  subject.run
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  expect(subject.running).to be true
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
              end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
              describe '#quit' do
         | 
| 47 | 
            +
                it 'sets running to false' do
         | 
| 48 | 
            +
                  server = stub(start: true, shutdown: true)
         | 
| 49 | 
            +
                  subject.stubs(:create_server).returns(server)
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  subject.run
         | 
| 52 | 
            +
                  subject.quit
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  expect(subject.running).to be false
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
              end
         | 
| 57 | 
            +
            end
         | 
| @@ -1,6 +1,6 @@ | |
| 1 1 | 
             
            require 'spec_helper'
         | 
| 2 2 |  | 
| 3 | 
            -
            describe MailRoom::Connection do
         | 
| 3 | 
            +
            describe MailRoom::IMAP::Connection do
         | 
| 4 4 | 
             
              let(:imap) {stub}
         | 
| 5 5 | 
             
              let(:mailbox) {build_mailbox(delete_after_delivery: true, expunge_deleted: true)}
         | 
| 6 6 |  | 
| @@ -9,7 +9,9 @@ describe MailRoom::Connection do | |
| 9 9 | 
             
              end
         | 
| 10 10 |  | 
| 11 11 | 
             
              context "with imap set up" do
         | 
| 12 | 
            -
                let(:connection) {MailRoom::Connection.new(mailbox)}
         | 
| 12 | 
            +
                let(:connection) {MailRoom::IMAP::Connection.new(mailbox)}
         | 
| 13 | 
            +
                let(:uid) { 1 }
         | 
| 14 | 
            +
                let(:seqno) { 8 }
         | 
| 13 15 |  | 
| 14 16 | 
             
                before :each do
         | 
| 15 17 | 
             
                  imap.stubs(:starttls)
         | 
| @@ -36,19 +38,21 @@ describe MailRoom::Connection do | |
| 36 38 | 
             
                end
         | 
| 37 39 |  | 
| 38 40 | 
             
                it "waits for a message to process" do
         | 
| 39 | 
            -
                  new_message = 'a message'
         | 
| 40 | 
            -
                  new_message.stubs(:seqno).returns(8)
         | 
| 41 | 
            +
                  new_message = MailRoom::IMAP::Message.new(uid: uid, body: 'a message', seqno: seqno)
         | 
| 41 42 |  | 
| 42 43 | 
             
                  connection.on_new_message do |message|
         | 
| 43 44 | 
             
                    expect(message).to eq(new_message)
         | 
| 44 45 | 
             
                    true
         | 
| 45 46 | 
             
                  end
         | 
| 46 47 |  | 
| 48 | 
            +
                  attr = { 'UID' => uid, 'RFC822' => new_message.body }
         | 
| 49 | 
            +
                  fetch_data = Net::IMAP::FetchData.new(seqno, attr)
         | 
| 50 | 
            +
             | 
| 47 51 | 
             
                  imap.expects(:idle)
         | 
| 48 | 
            -
                  imap.stubs(:uid_search).with(mailbox.search_command).returns([], [ | 
| 49 | 
            -
                  imap.expects(:uid_fetch).with([ | 
| 50 | 
            -
                  mailbox.expects(:deliver?).with( | 
| 51 | 
            -
                  imap.expects(:store).with( | 
| 52 | 
            +
                  imap.stubs(:uid_search).with(mailbox.search_command).returns([], [uid])
         | 
| 53 | 
            +
                  imap.expects(:uid_fetch).with([uid], "RFC822").returns([fetch_data])
         | 
| 54 | 
            +
                  mailbox.expects(:deliver?).with(uid).returns(true)
         | 
| 55 | 
            +
                  imap.expects(:store).with(seqno, "+FLAGS", [Net::IMAP::DELETED])
         | 
| 52 56 | 
             
                  imap.expects(:expunge).once
         | 
| 53 57 |  | 
| 54 58 | 
             
                  connection.wait
         | 
| @@ -0,0 +1,36 @@ | |
| 1 | 
            +
            # frozen_string_literal:true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'spec_helper'
         | 
| 4 | 
            +
            require 'securerandom'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            describe MailRoom::IMAP::Message do
         | 
| 7 | 
            +
              let(:uid) { SecureRandom.hex }
         | 
| 8 | 
            +
              let(:body) { 'hello world' }
         | 
| 9 | 
            +
              let(:seqno) { 5 }
         | 
| 10 | 
            +
             | 
| 11 | 
            +
              subject { described_class.new(uid: uid, body: body, seqno: seqno) }
         | 
| 12 | 
            +
             | 
| 13 | 
            +
              describe '#initalize' do
         | 
| 14 | 
            +
                it 'initializes with required parameters' do
         | 
| 15 | 
            +
                  subject
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  expect(subject.uid).to eq(uid)
         | 
| 18 | 
            +
                  expect(subject.body).to eq(body)
         | 
| 19 | 
            +
                  expect(subject.seqno).to eq(seqno)
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
              end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
              describe '#==' do
         | 
| 24 | 
            +
                let(:dup) { described_class.new(uid: uid, body: body, seqno: seqno) }
         | 
| 25 | 
            +
                let(:base_msg) { MailRoom::Message.new(uid: uid, body: body) }
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                it 'matches an equivalent message' do
         | 
| 28 | 
            +
                  expect(dup == subject).to be true
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                it 'does not match a base message' do
         | 
| 32 | 
            +
                  expect(subject == base_msg).to be false
         | 
| 33 | 
            +
                  expect(base_msg == subject).to be false
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
              end
         | 
| 36 | 
            +
            end
         | 
    
        data/spec/lib/mailbox_spec.rb
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            require 'spec_helper'
         | 
| 2 2 |  | 
| 3 3 | 
             
            describe MailRoom::Mailbox do
         | 
| 4 | 
            -
              let(:sample_message) {  | 
| 4 | 
            +
              let(:sample_message) { MailRoom::Message.new(uid: 123, body: 'a message') }
         | 
| 5 5 |  | 
| 6 6 | 
             
              describe "#deliver" do
         | 
| 7 7 | 
             
                context "with arbitration_method of noop" do
         | 
| @@ -36,9 +36,9 @@ describe MailRoom::Mailbox do | |
| 36 36 | 
             
                    noop = stub(:deliver)
         | 
| 37 37 | 
             
                    MailRoom::Delivery['noop'].stubs(:new => noop)
         | 
| 38 38 |  | 
| 39 | 
            -
                    noop.expects(:deliver).with( | 
| 39 | 
            +
                    noop.expects(:deliver).with(sample_message.body)
         | 
| 40 40 |  | 
| 41 | 
            -
                    mailbox.deliver( | 
| 41 | 
            +
                    mailbox.deliver(sample_message)
         | 
| 42 42 | 
             
                  end
         | 
| 43 43 | 
             
                end
         | 
| 44 44 |  | 
| @@ -48,9 +48,9 @@ describe MailRoom::Mailbox do | |
| 48 48 | 
             
                    logger = stub(:deliver)
         | 
| 49 49 | 
             
                    MailRoom::Delivery['logger'].stubs(:new => logger)
         | 
| 50 50 |  | 
| 51 | 
            -
                    logger.expects(:deliver).with( | 
| 51 | 
            +
                    logger.expects(:deliver).with(sample_message.body)
         | 
| 52 52 |  | 
| 53 | 
            -
                    mailbox.deliver( | 
| 53 | 
            +
                    mailbox.deliver(sample_message)
         | 
| 54 54 | 
             
                  end
         | 
| 55 55 | 
             
                end
         | 
| 56 56 |  | 
| @@ -60,9 +60,9 @@ describe MailRoom::Mailbox do | |
| 60 60 | 
             
                    postback = stub(:deliver)
         | 
| 61 61 | 
             
                    MailRoom::Delivery['postback'].stubs(:new => postback)
         | 
| 62 62 |  | 
| 63 | 
            -
                    postback.expects(:deliver).with( | 
| 63 | 
            +
                    postback.expects(:deliver).with(sample_message.body)
         | 
| 64 64 |  | 
| 65 | 
            -
                    mailbox.deliver( | 
| 65 | 
            +
                    mailbox.deliver(sample_message)
         | 
| 66 66 | 
             
                  end
         | 
| 67 67 | 
             
                end
         | 
| 68 68 |  | 
| @@ -72,9 +72,9 @@ describe MailRoom::Mailbox do | |
| 72 72 | 
             
                    letter_opener = stub(:deliver)
         | 
| 73 73 | 
             
                    MailRoom::Delivery['letter_opener'].stubs(:new => letter_opener)
         | 
| 74 74 |  | 
| 75 | 
            -
                    letter_opener.expects(:deliver).with( | 
| 75 | 
            +
                    letter_opener.expects(:deliver).with(sample_message.body)
         | 
| 76 76 |  | 
| 77 | 
            -
                    mailbox.deliver( | 
| 77 | 
            +
                    mailbox.deliver(sample_message)
         | 
| 78 78 | 
             
                  end
         | 
| 79 79 | 
             
                end
         | 
| 80 80 |  | 
| @@ -85,7 +85,7 @@ describe MailRoom::Mailbox do | |
| 85 85 | 
             
                    MailRoom::Delivery['noop'].stubs(:new => noop)
         | 
| 86 86 | 
             
                    noop.expects(:deliver).never
         | 
| 87 87 |  | 
| 88 | 
            -
                    mailbox.deliver( | 
| 88 | 
            +
                    mailbox.deliver(MailRoom::Message.new(uid: 1234, body: nil))
         | 
| 89 89 | 
             
                  end
         | 
| 90 90 | 
             
                end
         | 
| 91 91 |  | 
| @@ -19,9 +19,9 @@ describe MailRoom::MailboxWatcher do | |
| 19 19 | 
             
                end
         | 
| 20 20 |  | 
| 21 21 | 
             
                it 'loops over wait while running' do
         | 
| 22 | 
            -
                  connection = MailRoom::Connection.new(mailbox)
         | 
| 22 | 
            +
                  connection = MailRoom::IMAP::Connection.new(mailbox)
         | 
| 23 23 |  | 
| 24 | 
            -
                  MailRoom::Connection.stubs(:new).returns(connection)
         | 
| 24 | 
            +
                  MailRoom::IMAP::Connection.stubs(:new).returns(connection)
         | 
| 25 25 |  | 
| 26 26 | 
             
                  watcher.expects(:running?).twice.returns(true, false)
         | 
| 27 27 | 
             
                  connection.expects(:wait).once
         | 
| @@ -41,11 +41,11 @@ describe MailRoom::MailboxWatcher do | |
| 41 41 | 
             
                end
         | 
| 42 42 |  | 
| 43 43 | 
             
                it 'closes and waits for the connection' do
         | 
| 44 | 
            -
                  connection = MailRoom::Connection.new(mailbox)
         | 
| 44 | 
            +
                  connection = MailRoom::IMAP::Connection.new(mailbox)
         | 
| 45 45 | 
             
                  connection.stubs(:wait)
         | 
| 46 46 | 
             
                  connection.stubs(:quit)
         | 
| 47 47 |  | 
| 48 | 
            -
                  MailRoom::Connection.stubs(:new).returns(connection)
         | 
| 48 | 
            +
                  MailRoom::IMAP::Connection.stubs(:new).returns(connection)
         | 
| 49 49 |  | 
| 50 50 | 
             
                  watcher.run
         | 
| 51 51 |  | 
| @@ -0,0 +1,35 @@ | |
| 1 | 
            +
            # frozen_string_literal:true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'spec_helper'
         | 
| 4 | 
            +
            require 'securerandom'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            describe MailRoom::Message do
         | 
| 7 | 
            +
              let(:uid) { SecureRandom.hex }
         | 
| 8 | 
            +
              let(:body) { 'hello world' }
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              subject { described_class.new(uid: uid, body: body) }
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              describe '#initalize' do
         | 
| 13 | 
            +
                it 'initializes with required parameters' do
         | 
| 14 | 
            +
                  subject
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  expect(subject.uid).to eq(uid)
         | 
| 17 | 
            +
                  expect(subject.body).to eq(body)
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
              end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
              describe '#==' do
         | 
| 22 | 
            +
                let(:dup) { described_class.new(uid: uid, body: body) }
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                it 'matches an equivalent message' do
         | 
| 25 | 
            +
                  expect(dup == subject).to be true
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                it 'does not match a message with a different UID' do
         | 
| 29 | 
            +
                  msg = described_class.new(uid: '12345', body: body)
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  expect(subject == msg).to be false
         | 
| 32 | 
            +
                  expect(msg == subject).to be false
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
            end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: gitlab-mail_room
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0.0. | 
| 4 | 
            +
              version: 0.0.9
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Tony Pitale
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date:  | 
| 11 | 
            +
            date: 2021-03-12 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: rake
         | 
| @@ -66,6 +66,20 @@ dependencies: | |
| 66 66 | 
             
                - - ">="
         | 
| 67 67 | 
             
                  - !ruby/object:Gem::Version
         | 
| 68 68 | 
             
                    version: '0'
         | 
| 69 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 70 | 
            +
              name: webrick
         | 
| 71 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 72 | 
            +
                requirements:
         | 
| 73 | 
            +
                - - "~>"
         | 
| 74 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 75 | 
            +
                    version: '1.6'
         | 
| 76 | 
            +
              type: :development
         | 
| 77 | 
            +
              prerelease: false
         | 
| 78 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 79 | 
            +
                requirements:
         | 
| 80 | 
            +
                - - "~>"
         | 
| 81 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 82 | 
            +
                    version: '1.6'
         | 
| 69 83 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 70 84 | 
             
              name: faraday
         | 
| 71 85 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| @@ -200,9 +214,14 @@ files: | |
| 200 214 | 
             
            - lib/mail_room/delivery/postback.rb
         | 
| 201 215 | 
             
            - lib/mail_room/delivery/que.rb
         | 
| 202 216 | 
             
            - lib/mail_room/delivery/sidekiq.rb
         | 
| 217 | 
            +
            - lib/mail_room/health_check.rb
         | 
| 218 | 
            +
            - lib/mail_room/imap.rb
         | 
| 219 | 
            +
            - lib/mail_room/imap/connection.rb
         | 
| 220 | 
            +
            - lib/mail_room/imap/message.rb
         | 
| 203 221 | 
             
            - lib/mail_room/logger/structured.rb
         | 
| 204 222 | 
             
            - lib/mail_room/mailbox.rb
         | 
| 205 223 | 
             
            - lib/mail_room/mailbox_watcher.rb
         | 
| 224 | 
            +
            - lib/mail_room/message.rb
         | 
| 206 225 | 
             
            - lib/mail_room/version.rb
         | 
| 207 226 | 
             
            - logfile.log
         | 
| 208 227 | 
             
            - mail_room.gemspec
         | 
| @@ -210,7 +229,6 @@ files: | |
| 210 229 | 
             
            - spec/lib/arbitration/redis_spec.rb
         | 
| 211 230 | 
             
            - spec/lib/cli_spec.rb
         | 
| 212 231 | 
             
            - spec/lib/configuration_spec.rb
         | 
| 213 | 
            -
            - spec/lib/connection_spec.rb
         | 
| 214 232 | 
             
            - spec/lib/coordinator_spec.rb
         | 
| 215 233 | 
             
            - spec/lib/crash_handler_spec.rb
         | 
| 216 234 | 
             
            - spec/lib/delivery/letter_opener_spec.rb
         | 
| @@ -218,9 +236,13 @@ files: | |
| 218 236 | 
             
            - spec/lib/delivery/postback_spec.rb
         | 
| 219 237 | 
             
            - spec/lib/delivery/que_spec.rb
         | 
| 220 238 | 
             
            - spec/lib/delivery/sidekiq_spec.rb
         | 
| 239 | 
            +
            - spec/lib/health_check_spec.rb
         | 
| 240 | 
            +
            - spec/lib/imap/connection_spec.rb
         | 
| 241 | 
            +
            - spec/lib/imap/message_spec.rb
         | 
| 221 242 | 
             
            - spec/lib/logger/structured_spec.rb
         | 
| 222 243 | 
             
            - spec/lib/mailbox_spec.rb
         | 
| 223 244 | 
             
            - spec/lib/mailbox_watcher_spec.rb
         | 
| 245 | 
            +
            - spec/lib/message_spec.rb
         | 
| 224 246 | 
             
            - spec/spec_helper.rb
         | 
| 225 247 | 
             
            homepage: http://github.com/tpitale/mail_room
         | 
| 226 248 | 
             
            licenses: []
         | 
| @@ -250,7 +272,6 @@ test_files: | |
| 250 272 | 
             
            - spec/lib/arbitration/redis_spec.rb
         | 
| 251 273 | 
             
            - spec/lib/cli_spec.rb
         | 
| 252 274 | 
             
            - spec/lib/configuration_spec.rb
         | 
| 253 | 
            -
            - spec/lib/connection_spec.rb
         | 
| 254 275 | 
             
            - spec/lib/coordinator_spec.rb
         | 
| 255 276 | 
             
            - spec/lib/crash_handler_spec.rb
         | 
| 256 277 | 
             
            - spec/lib/delivery/letter_opener_spec.rb
         | 
| @@ -258,7 +279,11 @@ test_files: | |
| 258 279 | 
             
            - spec/lib/delivery/postback_spec.rb
         | 
| 259 280 | 
             
            - spec/lib/delivery/que_spec.rb
         | 
| 260 281 | 
             
            - spec/lib/delivery/sidekiq_spec.rb
         | 
| 282 | 
            +
            - spec/lib/health_check_spec.rb
         | 
| 283 | 
            +
            - spec/lib/imap/connection_spec.rb
         | 
| 284 | 
            +
            - spec/lib/imap/message_spec.rb
         | 
| 261 285 | 
             
            - spec/lib/logger/structured_spec.rb
         | 
| 262 286 | 
             
            - spec/lib/mailbox_spec.rb
         | 
| 263 287 | 
             
            - spec/lib/mailbox_watcher_spec.rb
         | 
| 288 | 
            +
            - spec/lib/message_spec.rb
         | 
| 264 289 | 
             
            - spec/spec_helper.rb
         |