gitlab-mail_room-0.0.24/0000755000004100000410000000000014563474377015067 5ustar www-datawww-datagitlab-mail_room-0.0.24/bin/0000755000004100000410000000000014563474377015637 5ustar www-datawww-datagitlab-mail_room-0.0.24/bin/mail_room0000755000004100000410000000011014563474377017533 0ustar www-datawww-data#!/usr/bin/env ruby require 'mail_room' MailRoom::CLI.new(ARGV).start gitlab-mail_room-0.0.24/.gitlab-ci.yml0000644000004100000410000000134114563474377017522 0ustar www-datawww-datadefault: image: "ruby:${RUBY_VERSION}" services: - redis:latest .test-template: &test cache: key: ruby-$RUBY_VERSION paths: - vendor/ruby variables: REDIS_URL: redis://redis:6379/0 before_script: - apt update && apt install -y libicu-dev - ruby -v # Print out ruby version for debugging - gem install bundler --no-document # Bundler is not installed with the image - bundle config set --local path 'vendor' - bundle install -j $(nproc) script: - bundle exec rspec spec rspec: parallel: matrix: - RUBY_VERSION: [ "2.6", "2.7", "3.0", "3.1", "3.2" ] <<: *test include: - template: Security/Dependency-Scanning.gitlab-ci.ymlgitlab-mail_room-0.0.24/.gitignore0000644000004100000410000000023514563474377017057 0ustar www-datawww-data*.gem *.rbc .bundle .config .yardoc Gemfile.lock InstalledFiles _yardoc coverage doc/ lib/bundler/man pkg rdoc spec/reports test/tmp test/version_tmp tmp loggitlab-mail_room-0.0.24/CONTRIBUTING.md0000644000004100000410000000424114563474377017321 0ustar www-datawww-data## Developer Certificate of Origin and License By contributing to GitLab B.V., you accept and agree to the following terms and conditions for your present and future contributions submitted to GitLab B.V. Except for the license granted herein to GitLab B.V. and recipients of software distributed by GitLab B.V., you reserve all right, title, and interest in and to your Contributions. All contributions are subject to the Developer Certificate of Origin and license set out at [docs.gitlab.com/ce/legal/developer_certificate_of_origin](https://docs.gitlab.com/ce/legal/developer_certificate_of_origin). _This notice should stay as the first item in the CONTRIBUTING.md file._ ## Code of conduct As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Instances of abusive, harassing, or otherwise unacceptable behavior can be reported by emailing contact@gitlab.com. This Code of Conduct is adapted from the [Contributor Covenant](https://contributor-covenant.org), version 1.1.0, available at [https://contributor-covenant.org/version/1/1/0/](https://contributor-covenant.org/version/1/1/0/). gitlab-mail_room-0.0.24/.github/0000755000004100000410000000000014563474377016427 5ustar www-datawww-datagitlab-mail_room-0.0.24/.github/dependabot.yml0000644000004100000410000000032014563474377021252 0ustar www-datawww-dataversion: 2 updates: - package-ecosystem: "bundler" directory: "/" schedule: interval: "daily" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" gitlab-mail_room-0.0.24/.github/workflows/0000755000004100000410000000000014563474377020464 5ustar www-datawww-datagitlab-mail_room-0.0.24/.github/workflows/ci.yml0000644000004100000410000000243314563474377021604 0ustar www-datawww-dataname: CI on: [push, pull_request] jobs: test: runs-on: ubuntu-latest services: # Label used to access the service container redis: # Docker Hub image image: redis # Set health checks to wait until redis has started options: >- --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 ports: # Maps port 6379 on service container to the host - 6379:6379 strategy: fail-fast: false matrix: ruby-version: - head - '3.2' - '3.1' - '3.0' - '2.7' steps: - uses: actions/checkout@v3 - name: Set up Ruby ${{ matrix.ruby-version }} uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} bundler-cache: true # 'bundle install' and cache - name: Run tests run: bundle exec rspec rubocop: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Ruby ${{ matrix.ruby-version }} uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} bundler-cache: true # 'bundle install' and cache - name: Run Rubocop run: bundle exec rubocop gitlab-mail_room-0.0.24/lib/0000755000004100000410000000000014563474377015635 5ustar www-datawww-datagitlab-mail_room-0.0.24/lib/mail_room.rb0000644000004100000410000000064514563474377020145 0ustar www-datawww-datarequire 'net/imap' require 'optparse' require 'yaml' module MailRoom end require "mail_room/version" require "mail_room/configuration" require "mail_room/health_check" require "mail_room/mailbox" require "mail_room/mailbox_watcher" require "mail_room/message" require "mail_room/connection" require "mail_room/coordinator" require "mail_room/cli" require 'mail_room/logger/structured' require 'mail_room/crash_handler' gitlab-mail_room-0.0.24/lib/mail_room/0000755000004100000410000000000014563474377017613 5ustar www-datawww-datagitlab-mail_room-0.0.24/lib/mail_room/mailbox.rb0000644000004100000410000001354014563474377021576 0ustar www-datawww-datarequire "mail_room/delivery" require "mail_room/arbitration" require "mail_room/imap" require "mail_room/microsoft_graph" module MailRoom # Mailbox Configuration fields MAILBOX_FIELDS = [ :email, :inbox_method, :inbox_options, :password, :host, :port, :ssl, :start_tls, :limit_max_unread, #to avoid 'Error in IMAP command UID FETCH: Too long argument' :idle_timeout, :search_command, :name, :delete_after_delivery, :expunge_deleted, :delivery_klass, :delivery_method, # :noop, :logger, :postback, :letter_opener :log_path, # for logger :delivery_url, # for postback :delivery_token, # for postback :content_type, # for postback :location, # for letter_opener :delivery_options, :arbitration_method, :arbitration_options, :logger ] ConfigurationError = Class.new(RuntimeError) IdleTimeoutTooLarge = Class.new(RuntimeError) # Holds configuration for each of the email accounts we wish to monitor # and deliver email to when new emails arrive over imap Mailbox = Struct.new(*MAILBOX_FIELDS) do # Keep it to 29 minutes or less # The IMAP serve will close the connection after 30 minutes of inactivity # (which sending IDLE and then nothing technically is), so we re-idle every # 29 minutes, as suggested by the spec: https://tools.ietf.org/html/rfc2177 IMAP_IDLE_TIMEOUT = 29 * 60 # 29 minutes in in seconds IMAP_CONFIGURATION = [:name, :email, :password, :host, :port].freeze MICROSOFT_GRAPH_CONFIGURATION = [:name, :email].freeze MICROSOFT_GRAPH_INBOX_OPTIONS = [:tenant_id, :client_id, :client_secret].freeze # Default attributes for the mailbox configuration DEFAULTS = { search_command: 'UNSEEN', delivery_method: 'postback', host: 'imap.gmail.com', port: 993, ssl: true, start_tls: false, limit_max_unread: 0, idle_timeout: IMAP_IDLE_TIMEOUT, delete_after_delivery: false, expunge_deleted: false, delivery_options: {}, arbitration_method: 'noop', arbitration_options: {}, logger: {} } # Store the configuration and require the appropriate delivery method # @param attributes [Hash] configuration options def initialize(attributes={}) super(*DEFAULTS.merge(attributes).values_at(*members)) validate! end def logger @logger ||= case self[:logger] when Logger self[:logger] else self[:logger] ||= {} MailRoom::Logger::Structured.new(normalize_log_path(self[:logger][:log_path])) end end def delivery_klass self[:delivery_klass] ||= Delivery[delivery_method] end def arbitration_klass Arbitration[arbitration_method] end def delivery @delivery ||= delivery_klass.new(parsed_delivery_options) end def arbitrator @arbitrator ||= arbitration_klass.new(parsed_arbitration_options) end def deliver?(uid) logger.info({context: context, uid: uid, action: "asking arbiter to deliver", arbitrator: arbitrator.class.name}) arbitrator.deliver?(uid) end # deliver the email message # @param message [MailRoom::Message] def deliver(message) body = message.body return true unless body logger.info({context: context, uid: message.uid, action: "sending to deliverer", deliverer: delivery.class.name, byte_size: body.bytesize}) delivery.deliver(body) end # true, false, or ssl options hash def ssl_options replace_verify_mode(ssl) end def context { email: self.email, name: self.name } end def imap? !microsoft_graph? end def microsoft_graph? self[:inbox_method].to_s == 'microsoft_graph' end def validate! if microsoft_graph? validate_microsoft_graph! else validate_imap! end end private def validate_imap! if self[:idle_timeout] > IMAP_IDLE_TIMEOUT raise IdleTimeoutTooLarge, "Please use an idle timeout smaller than #{29*60} to prevent " \ "IMAP server disconnects" end IMAP_CONFIGURATION.each do |k| if self[k].nil? raise ConfigurationError, "Field :#{k} is required in Mailbox: #{inspect}" end end end def validate_microsoft_graph! raise ConfigurationError, "Missing inbox_options in Mailbox: #{inspect}" unless self.inbox_options.is_a?(Hash) MICROSOFT_GRAPH_CONFIGURATION.each do |k| if self[k].nil? raise ConfigurationError, "Field :#{k} is required in Mailbox: #{inspect}" end end MICROSOFT_GRAPH_INBOX_OPTIONS.each do |k| if self[:inbox_options][k].nil? raise ConfigurationError, "inbox_options field :#{k} is required in Mailbox: #{inspect}" end end end def parsed_arbitration_options arbitration_klass::Options.new(self) end def parsed_delivery_options delivery_klass::Options.new(self) end def replace_verify_mode(options) return options unless options.is_a?(Hash) return options unless options.has_key?(:verify_mode) options[:verify_mode] = lookup_verify_mode(options[:verify_mode]) options end def lookup_verify_mode(verify_mode) case verify_mode.to_sym when :none OpenSSL::SSL::VERIFY_NONE when :peer OpenSSL::SSL::VERIFY_PEER when :client_once OpenSSL::SSL::VERIFY_CLIENT_ONCE when :fail_if_no_peer_cert OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT end end def normalize_log_path(log_path) case log_path when nil, "" nil when :stdout, "STDOUT" STDOUT when :stderr, "STDERR" STDERR else log_path end end end end gitlab-mail_room-0.0.24/lib/mail_room/microsoft_graph.rb0000644000004100000410000000022214563474377023322 0ustar www-datawww-data# frozen_string_literal: true module MailRoom module MicrosoftGraph autoload :Connection, 'mail_room/microsoft_graph/connection' end end gitlab-mail_room-0.0.24/lib/mail_room/configuration.rb0000644000004100000410000000245014563474377023010 0ustar www-datawww-datarequire "erb" module MailRoom # Wraps configuration for a set of individual mailboxes with global config # @author Tony Pitale class Configuration attr_accessor :mailboxes, :log_path, :quiet, :health_check # Initialize a new configuration of mailboxes def initialize(options={}) self.mailboxes = [] self.quiet = options.fetch(:quiet, false) if options.has_key?(:config_path) begin erb = ERB.new(File.read(options[:config_path])) erb.filename = options[:config_path] config_file = YAML.load(erb.result) set_mailboxes(config_file[:mailboxes]) set_health_check(config_file[:health_check]) rescue => e raise e unless quiet end end end # Builds individual mailboxes from YAML configuration # # @param mailboxes_config def set_mailboxes(mailboxes_config) mailboxes_config.each do |attributes| self.mailboxes << Mailbox.new(attributes) end end # Builds the health checker from YAML configuration # # @param health_check_config nil or a Hash containing :address and :port def set_health_check(health_check_config) return unless health_check_config self.health_check = HealthCheck.new(health_check_config) end end end gitlab-mail_room-0.0.24/lib/mail_room/delivery.rb0000644000004100000410000000066614563474377021773 0ustar www-datawww-datamodule MailRoom module Delivery def [](name) require_relative("./delivery/#{name}") case name when "postback" Delivery::Postback when "logger" Delivery::Logger when "letter_opener" Delivery::LetterOpener when "sidekiq" Delivery::Sidekiq when "que" Delivery::Que else Delivery::Noop end end module_function :[] end end gitlab-mail_room-0.0.24/lib/mail_room/mailbox_watcher.rb0000644000004100000410000000343014563474377023310 0ustar www-datawww-datarequire "mail_room/connection" module MailRoom # TODO: split up between processing and idling? # Watch a Mailbox # @author Tony Pitale class MailboxWatcher attr_accessor :watching_thread # Watch a new mailbox # @param mailbox [MailRoom::Mailbox] the mailbox to watch def initialize(mailbox) @mailbox = mailbox @running = false @connection = nil end # are we running? # @return [Boolean] def running? @running end # run the mailbox watcher def run @mailbox.logger.info({ context: @mailbox.context, action: "Setting up watcher" }) @running = true connection.on_new_message do |message| @mailbox.deliver(message) end self.watching_thread = Thread.start do while(running?) do connection.wait end end watching_thread.abort_on_exception = true end # stop running, cleanup connection def quit @mailbox.logger.info({ context: @mailbox.context, action: "Quitting connection..." }) @running = false if @connection @connection.quit @connection = nil end @mailbox.logger.info({ context: @mailbox.context, action: "Terminating watching thread..." }) if self.watching_thread thr = self.watching_thread.join(60) @mailbox.logger.info({ context: @mailbox.context, action: "Timeout waiting for watching thread" }) unless thr end @mailbox.logger.info({ context: @mailbox.context, action: "Done with thread cleanup" }) end private def connection @connection ||= if @mailbox.microsoft_graph? ::MailRoom::MicrosoftGraph::Connection.new(@mailbox) else ::MailRoom::IMAP::Connection.new(@mailbox) end end end end gitlab-mail_room-0.0.24/lib/mail_room/imap/0000755000004100000410000000000014563474377020541 5ustar www-datawww-datagitlab-mail_room-0.0.24/lib/mail_room/imap/message.rb0000644000004100000410000000050314563474377022510 0ustar www-datawww-data# frozen_string_literal:true module MailRoom module IMAP class Message < MailRoom::Message attr_reader :seqno def initialize(uid:, body:, seqno:) super(uid: uid, body: body) @seqno = seqno end def ==(other) super && seqno == other.seqno end end end end gitlab-mail_room-0.0.24/lib/mail_room/imap/connection.rb0000644000004100000410000001303314563474377023225 0ustar www-datawww-data# frozen_string_literal: true module MailRoom module IMAP class Connection < MailRoom::Connection def initialize(mailbox) super # log in and set the mailbox reset setup end # is the connection logged in? # @return [Boolean] def logged_in? @logged_in end # is the connection blocked idling? # @return [Boolean] def idling? @idling end # is the imap connection closed? # @return [Boolean] def disconnected? imap.disconnected? end # is the connection ready to idle? # @return [Boolean] def ready_to_idle? logged_in? && !idling? end def quit stop_idling reset end def wait # in case we missed any between idles process_mailbox idle process_mailbox rescue Net::IMAP::Error, IOError => e @mailbox.logger.warn({ context: @mailbox.context, action: 'Disconnected. Resetting...', error: e.message }) reset setup end private def reset @imap = nil @logged_in = false @idling = false end def setup @mailbox.logger.info({ context: @mailbox.context, action: 'Starting TLS session' }) start_tls @mailbox.logger.info({ context: @mailbox.context, action: 'Logging into mailbox' }) log_in @mailbox.logger.info({ context: @mailbox.context, action: 'Setting mailbox' }) set_mailbox end # build a net/imap connection to google imap def imap @imap ||= Net::IMAP.new(@mailbox.host, port: @mailbox.port, ssl: @mailbox.ssl_options) end # start a TLS session def start_tls imap.starttls if @mailbox.start_tls end # send the imap login command to google def log_in imap.login(@mailbox.email, @mailbox.password) @logged_in = true end # select the mailbox name we want to use def set_mailbox imap.select(@mailbox.name) if logged_in? end # is the response for a new message? # @param response [Net::IMAP::TaggedResponse] the imap response from idle # @return [Boolean] def message_exists?(response) response.respond_to?(:name) && response.name == 'EXISTS' end # @private def idle_handler ->(response) { imap.idle_done if message_exists?(response) } end # maintain an imap idle connection def idle return unless ready_to_idle? @mailbox.logger.info({ context: @mailbox.context, action: 'Idling' }) @idling = true imap.idle(@mailbox.idle_timeout, &idle_handler) ensure @idling = false end # trigger the idle to finish and wait for the thread to finish def stop_idling return unless idling? imap.idle_done # idling_thread.join # self.idling_thread = nil end def process_mailbox return unless @new_message_handler @mailbox.logger.info({ context: @mailbox.context, action: 'Processing started' }) msgs = new_messages any_deletions = msgs. # deliver each new message, collect success map(&@new_message_handler). # include messages with success zip(msgs). # filter failed deliveries, collect message select(&:first).map(&:last). # scrub delivered messages map { |message| scrub(message) } .any? imap.expunge if @mailbox.expunge_deleted && any_deletions end def scrub(message) if @mailbox.delete_after_delivery imap.store(message.seqno, '+FLAGS', [Net::IMAP::DELETED]) true end end # @private # fetch all messages for the new message ids def new_messages # Both of these calls may results in # imap raising an EOFError, we handle # this exception in the watcher messages_for_ids(new_message_ids) end # TODO: label messages? # @imap.store(id, "+X-GM-LABELS", [label]) # @private # search for all new (unseen) message ids # @return [Array] message ids def new_message_ids # uid_search still leaves messages UNSEEN all_unread = imap.uid_search(@mailbox.search_command) all_unread = all_unread.slice(0, @mailbox.limit_max_unread) if @mailbox.limit_max_unread.to_i > 0 to_deliver = all_unread.select { |uid| @mailbox.deliver?(uid) } @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 } }) to_deliver end # @private # fetch the email for all given ids in RFC822 format # @param ids [Array] list of message ids # @return [Array] the net/imap messages for the given ids def messages_for_ids(uids) return [] if uids.empty? # uid_fetch marks as SEEN, will not be re-fetched for UNSEEN imap_messages = imap.uid_fetch(uids, 'RFC822') imap_messages.each_with_object([]) do |msg, messages| messages << ::MailRoom::IMAP::Message.new(uid: msg.attr['UID'], body: msg.attr['RFC822'], seqno: msg.seqno) end end end end end gitlab-mail_room-0.0.24/lib/mail_room/logger/0000755000004100000410000000000014563474377021072 5ustar www-datawww-datagitlab-mail_room-0.0.24/lib/mail_room/logger/structured.rb0000644000004100000410000000135514563474377023627 0ustar www-datawww-datarequire 'date' require 'logger' require 'json' module MailRoom module Logger class Structured < ::Logger def format_message(severity, timestamp, progname, message) raise ArgumentError.new("Message must be a Hash") unless message.is_a? Hash data = {} data[:severity] = severity data[:time] = format_timestamp(timestamp || Time.now) # only accept a Hash data.merge!(message) data.to_json + "\n" end private def format_timestamp(timestamp) case timestamp when Time timestamp.to_datetime.iso8601(3).to_s when DateTime timestamp.iso8601(3).to_s else timestamp end end end end end gitlab-mail_room-0.0.24/lib/mail_room/cli.rb0000644000004100000410000000345314563474377020714 0ustar www-datawww-datamodule MailRoom # The CLI parses ARGV into configuration to start the coordinator with. # @author Tony Pitale class CLI attr_accessor :configuration, :coordinator, :options # Initialize a new CLI instance to handle option parsing from arguments # into configuration to start the coordinator running on all mailboxes # # @param args [Array] `ARGV` passed from `bin/mail_room` def initialize(args) @options = {} OptionParser.new do |parser| parser.banner = [ "Usage: #{@name} [-c config_file]\n", " #{@name} --help\n" ].compact.join parser.on('-c', '--config FILE') do |path| options[:config_path] = path end parser.on('-q', '--quiet') do options[:quiet] = true end parser.on('--log-exit-as') do |format| # accepts 'json' and 'plain' # 'plain' is equivalent to no format given options[:exit_error_format] = format unless format.nil? end # parser.on("-l", "--log FILE") do |path| # options[:log_path] = path # end parser.on_tail("-?", "--help", "Display this usage information.") do puts "#{parser}\n" exit end end.parse!(args) self.configuration = Configuration.new(options) self.coordinator = Coordinator.new(configuration.mailboxes, configuration.health_check) end # Start the coordinator running, sets up signal traps def start Signal.trap(:INT) do coordinator.running = false end Signal.trap(:TERM) do exit end coordinator.run rescue Exception => e # not just Errors, but includes lower-level Exceptions CrashHandler.new.handle(e, @options[:exit_error_format]) exit end end end gitlab-mail_room-0.0.24/lib/mail_room/jwt.rb0000644000004100000410000000170314563474377020745 0ustar www-datawww-data# frozen_string_literal: true require 'faraday' require 'securerandom' require 'jwt' require 'base64' module MailRoom # Responsible for validating and generating JWT token class JWT DEFAULT_ISSUER = 'mailroom' DEFAULT_ALGORITHM = 'HS256' attr_reader :header, :secret_path, :issuer, :algorithm def initialize(header:, secret_path:, issuer:, algorithm:) @header = header @secret_path = secret_path @issuer = issuer || DEFAULT_ISSUER @algorithm = algorithm || DEFAULT_ALGORITHM end def valid? [@header, @secret_path, @issuer, @algorithm].none?(&:nil?) end def token return nil unless valid? secret = Base64.strict_decode64(File.read(@secret_path).chomp) payload = { nonce: SecureRandom.hex(12), iat: Time.now.to_i, # https://github.com/jwt/ruby-jwt#issued-at-claim iss: @issuer } ::JWT.encode payload, secret, @algorithm end end end gitlab-mail_room-0.0.24/lib/mail_room/crash_handler.rb0000644000004100000410000000072514563474377022741 0ustar www-datawww-datarequire 'date' module MailRoom class CrashHandler SUPPORTED_FORMATS = %w[json none] def initialize(stream=STDOUT) @stream = stream end def handle(error, format) if format == 'json' @stream.puts json(error) return end raise error end private def json(error) { time: DateTime.now.iso8601(3), severity: :fatal, message: error.message, backtrace: error.backtrace }.to_json end end end gitlab-mail_room-0.0.24/lib/mail_room/imap.rb0000644000004100000410000000025614563474377021071 0ustar www-datawww-data# frozen_string_literal: true module MailRoom module IMAP autoload :Connection, 'mail_room/imap/connection' autoload :Message, 'mail_room/imap/message' end end gitlab-mail_room-0.0.24/lib/mail_room/health_check.rb0000644000004100000410000000217314563474377022545 0ustar www-datawww-data# frozen_string_literal: true module MailRoom class HealthCheck attr_reader :address, :port, :running def initialize(attributes = {}) @address = attributes[:address] @port = attributes[:port] validate! end def run @server = create_server @thread = Thread.new do @server.start end @thread.abort_on_exception = true @running = true end def quit @running = false @server&.shutdown @thread&.join(60) end private def validate! raise 'No health check address specified' unless address raise "Health check port #{@port.to_i} is invalid" unless port.to_i.positive? end def create_server require 'webrick' server = ::WEBrick::HTTPServer.new(Port: port, BindAddress: address, AccessLog: []) server.mount_proc '/liveness' do |_req, res| handle_liveness(res) end server end def handle_liveness(res) if @running res.status = 200 res.body = "OK\n" else res.status = 500 res.body = "Not running\n" end end end end gitlab-mail_room-0.0.24/lib/mail_room/version.rb0000644000004100000410000000012514563474377021623 0ustar www-datawww-datamodule MailRoom # Current version of gitlab-mail_room gem VERSION = "0.0.24" end gitlab-mail_room-0.0.24/lib/mail_room/coordinator.rb0000644000004100000410000000211114563474377022456 0ustar www-datawww-datamodule MailRoom # Coordinate the mailbox watchers # @author Tony Pitale class Coordinator attr_accessor :watchers, :running, :health_check # build watchers for a set of mailboxes # @params mailboxes [Array] mailboxes to be watched # @params health_check health checker to run def initialize(mailboxes, health_check = nil) self.watchers = [] @health_check = health_check mailboxes.each {|box| self.watchers << MailboxWatcher.new(box)} end alias :running? :running # start each of the watchers to running def run health_check&.run watchers.each(&:run) self.running = true sleep_while_running ensure quit end # quit each of the watchers when we're done running def quit health_check&.quit watchers.each(&:quit) end private # @private def sleep_while_running # do we need to sweep for dead watchers? # or do we let the mailbox rebuild connections while(running?) do; sleep 1; end end end end gitlab-mail_room-0.0.24/lib/mail_room/arbitration/0000755000004100000410000000000014563474377022131 5ustar www-datawww-datagitlab-mail_room-0.0.24/lib/mail_room/arbitration/redis.rb0000644000004100000410000000370214563474377023566 0ustar www-datawww-datarequire "redis" module MailRoom module Arbitration class Redis Options = Struct.new(:redis_url, :namespace, :sentinels) do def initialize(mailbox) redis_url = mailbox.arbitration_options[:redis_url] || "redis://localhost:6379" namespace = mailbox.arbitration_options[:namespace] sentinels = mailbox.arbitration_options[:sentinels] if namespace warn <<~MSG Redis namespaces are deprecated. This option will be ignored in future versions. See https://github.com/sidekiq/sidekiq/issues/2586 for more details." MSG end super(redis_url, namespace, sentinels) end end # Expire after 10 minutes so Redis doesn't get filled up with outdated data. EXPIRATION = 600 attr_accessor :options def initialize(options) @options = options end def deliver?(uid, expiration = EXPIRATION) key = "delivered:#{uid}" # Set the key, but only if it doesn't already exist; # the return value is true if successful, false if the key was already set, # which is conveniently the correct return value for this method # Any subsequent failure in the instance which gets the lock will be dealt # with by the expiration, at which time another instance can pick up the # message and try again. client.set(key, 1, nx: true, ex: expiration) end private def client @client ||= begin sentinels = options.sentinels redis_options = { url: options.redis_url } redis_options[:sentinels] = sentinels if sentinels redis = ::Redis.new(redis_options) namespace = options.namespace if namespace require 'redis/namespace' ::Redis::Namespace.new(namespace, redis: redis) else redis end end end end end end gitlab-mail_room-0.0.24/lib/mail_room/arbitration/noop.rb0000644000004100000410000000036614563474377023436 0ustar www-datawww-datamodule MailRoom module Arbitration class Noop Options = Class.new do def initialize(*) super() end end def initialize(*) end def deliver?(*) true end end end end gitlab-mail_room-0.0.24/lib/mail_room/delivery/0000755000004100000410000000000014563474377021436 5ustar www-datawww-datagitlab-mail_room-0.0.24/lib/mail_room/delivery/logger.rb0000644000004100000410000000165414563474377023250 0ustar www-datawww-datarequire 'logger' module MailRoom module Delivery # File/STDOUT Logger Delivery method # @author Tony Pitale class Logger Options = Struct.new(:log_path) do def initialize(mailbox) log_path = mailbox.log_path || mailbox.delivery_options[:log_path] super(log_path) end end # Build a new delivery, hold the delivery options # open a file or stdout for IO depending on the options # @param [MailRoom::Delivery::Logger::Options] def initialize(delivery_options) io = File.open(delivery_options.log_path, 'a') if delivery_options.log_path io ||= STDOUT io.sync = true @logger = ::Logger.new(io) end # Write the message to our logger # @param message [String] the email message as a string, RFC822 format def deliver(message) @logger.info message true end end end end gitlab-mail_room-0.0.24/lib/mail_room/delivery/que.rb0000644000004100000410000000471014563474377022557 0ustar www-datawww-datarequire 'pg' require 'json' require 'charlock_holmes' module MailRoom module Delivery # Que Delivery method # @author Tony Pitale class Que Options = Struct.new(:host, :port, :database, :username, :password, :queue, :priority, :job_class, :logger) do def initialize(mailbox) host = mailbox.delivery_options[:host] || "localhost" port = mailbox.delivery_options[:port] || 5432 database = mailbox.delivery_options[:database] username = mailbox.delivery_options[:username] password = mailbox.delivery_options[:password] queue = mailbox.delivery_options[:queue] || '' priority = mailbox.delivery_options[:priority] || 100 # lowest priority for Que job_class = mailbox.delivery_options[:job_class] logger = mailbox.logger super(host, port, database, username, password, queue, priority, job_class, logger) end end attr_reader :options # Build a new delivery, hold the mailbox configuration # @param [MailRoom::Delivery::Que::Options] def initialize(options) @options = options end # deliver the message by pushing it onto the configured Sidekiq queue # @param message [String] the email message as a string, RFC822 format def deliver(message) queue_job(utf8_encode_message(message)) @options.logger.info({ delivery_method: 'Que', action: 'message pushed' }) end private def connection PG.connect(connection_options) end def connection_options { host: options.host, port: options.port, dbname: options.database, user: options.username, password: options.password } end def queue_job(*args) sql = "INSERT INTO que_jobs (priority, job_class, queue, args) VALUES ($1, $2, $3, $4)" connection.exec(sql, [options.priority, options.job_class, options.queue, JSON.dump(args)]) end def utf8_encode_message(message) message = message.dup message.force_encoding("UTF-8") return message if message.valid_encoding? detection = CharlockHolmes::EncodingDetector.detect(message) return message unless detection && detection[:encoding] # Convert non-UTF-8 body UTF-8 so it can be dumped as JSON. CharlockHolmes::Converter.convert(message, detection[:encoding], 'UTF-8') end end end end gitlab-mail_room-0.0.24/lib/mail_room/delivery/letter_opener.rb0000644000004100000410000000164014563474377024633 0ustar www-datawww-datarequire 'erb' require 'mail' require 'letter_opener' module MailRoom module Delivery # LetterOpener Delivery method # @author Tony Pitale class LetterOpener Options = Struct.new(:location) do def initialize(mailbox) location = mailbox.location || mailbox.delivery_options[:location] super(location) end end # Build a new delivery, hold the delivery options # @param [MailRoom::Delivery::LetterOpener::Options] def initialize(delivery_options) @delivery_options = delivery_options end # Trigger `LetterOpener` to deliver our message # @param message [String] the email message as a string, RFC822 format def deliver(message) method = ::LetterOpener::DeliveryMethod.new(location: @delivery_options.location) method.deliver!(Mail.read_from_string(message)) true end end end end gitlab-mail_room-0.0.24/lib/mail_room/delivery/noop.rb0000644000004100000410000000057014563474377022740 0ustar www-datawww-datamodule MailRoom module Delivery # Noop Delivery method # @author Tony Pitale class Noop Options = Class.new do def initialize(*) super() end end # build a new delivery, do nothing def initialize(*) end # accept the delivery, do nothing def deliver(*) true end end end end gitlab-mail_room-0.0.24/lib/mail_room/delivery/sidekiq.rb0000644000004100000410000000572514563474377023425 0ustar www-datawww-datarequire "redis" require "securerandom" require "json" require "charlock_holmes" module MailRoom module Delivery # Sidekiq Delivery method # @author Douwe Maan class Sidekiq Options = Struct.new(:redis_url, :namespace, :sentinels, :queue, :worker, :logger, :redis_db) do def initialize(mailbox) redis_url = mailbox.delivery_options[:redis_url] || "redis://localhost:6379" redis_db = mailbox.delivery_options[:redis_db] || 0 namespace = mailbox.delivery_options[:namespace] sentinels = mailbox.delivery_options[:sentinels] queue = mailbox.delivery_options[:queue] || "default" worker = mailbox.delivery_options[:worker] logger = mailbox.logger if namespace warn <<~MSG Redis namespaces are deprecated. This option will be ignored in future versions. See https://github.com/sidekiq/sidekiq/issues/2586 for more details." MSG end super(redis_url, namespace, sentinels, queue, worker, logger, redis_db) end end attr_accessor :options # Build a new delivery, hold the mailbox configuration # @param [MailRoom::Delivery::Sidekiq::Options] def initialize(options) @options = options end # deliver the message by pushing it onto the configured Sidekiq queue # @param message [String] the email message as a string, RFC822 format def deliver(message) item = item_for(message) client.lpush("queue:#{options.queue}", JSON.generate(item)) @options.logger.info({ delivery_method: 'Sidekiq', action: 'message pushed' }) true end private def client @client ||= begin sentinels = options.sentinels redis_options = { url: options.redis_url, db: options.redis_db } redis_options[:sentinels] = sentinels if sentinels redis = ::Redis.new(redis_options) namespace = options.namespace if namespace require 'redis/namespace' Redis::Namespace.new(namespace, redis: redis) else redis end end end def item_for(message) { 'class' => options.worker, 'args' => [utf8_encode_message(message)], 'queue' => options.queue, 'jid' => SecureRandom.hex(12), 'retry' => false, 'enqueued_at' => Time.now.to_f } end def utf8_encode_message(message) message = message.dup message.force_encoding("UTF-8") return message if message.valid_encoding? detection = CharlockHolmes::EncodingDetector.detect(message) return message unless detection && detection[:encoding] # Convert non-UTF-8 body UTF-8 so it can be dumped as JSON. CharlockHolmes::Converter.convert(message, detection[:encoding], 'UTF-8') end end end end gitlab-mail_room-0.0.24/lib/mail_room/delivery/postback.rb0000644000004100000410000000664614563474377023605 0ustar www-datawww-datarequire 'faraday' require "mail_room/jwt" module MailRoom module Delivery # Postback Delivery method # @author Tony Pitale class Postback Options = Struct.new(:url, :token, :username, :password, :logger, :content_type, :jwt) do def initialize(mailbox) url = mailbox.delivery_url || mailbox.delivery_options[:delivery_url] || mailbox.delivery_options[:url] token = mailbox.delivery_token || mailbox.delivery_options[:delivery_token] || mailbox.delivery_options[:token] jwt = initialize_jwt(mailbox.delivery_options) username = mailbox.delivery_options[:username] || mailbox.delivery_options[:delivery_username] password = mailbox.delivery_options[:password] || mailbox.delivery_options[:delivery_password] logger = mailbox.logger content_type = mailbox.delivery_options[:content_type] super(url, token, username, password, logger, content_type, jwt) end def token_auth? !self[:token].nil? end def jwt_auth? self[:jwt].valid? end def basic_auth? !self[:username].nil? && !self[:password].nil? end private def initialize_jwt(delivery_options) ::MailRoom::JWT.new( header: delivery_options[:jwt_auth_header], secret_path: delivery_options[:jwt_secret_path], algorithm: delivery_options[:jwt_algorithm], issuer: delivery_options[:jwt_issuer] ) end end # Build a new delivery, hold the delivery options # @param [MailRoom::Delivery::Postback::Options] def initialize(delivery_options) puts delivery_options @delivery_options = delivery_options end # deliver the message using Faraday to the configured delivery_options url # @param message [String] the email message as a string, RFC822 format def deliver(message) connection = Faraday.new if @delivery_options.token_auth? connection.token_auth @delivery_options.token elsif @delivery_options.basic_auth? config_basic_auth(connection) end connection.post do |request| request.url @delivery_options.url request.body = message config_request_content_type(request) config_request_jwt_auth(request) end @delivery_options.logger.info({ delivery_method: 'Postback', action: 'message pushed', url: @delivery_options.url }) true end private def config_request_content_type(request) return if @delivery_options.content_type.nil? request.headers['Content-Type'] = @delivery_options.content_type end def config_request_jwt_auth(request) return unless @delivery_options.jwt_auth? request.headers[@delivery_options.jwt.header] = @delivery_options.jwt.token end def config_basic_auth(connection) if defined?(connection.basic_auth) connection.basic_auth( @delivery_options.username, @delivery_options.password ) else connection.request( :authorization, :basic, @delivery_options.username, @delivery_options.password ) end end end end end gitlab-mail_room-0.0.24/lib/mail_room/arbitration.rb0000644000004100000410000000037714563474377022465 0ustar www-datawww-datamodule MailRoom module Arbitration def [](name) require_relative("./arbitration/#{name}") case name when "redis" Arbitration::Redis else Arbitration::Noop end end module_function :[] end end gitlab-mail_room-0.0.24/lib/mail_room/message.rb0000644000004100000410000000042714563474377021567 0ustar www-datawww-data# frozen_string_literal: true module MailRoom class Message attr_reader :uid, :body def initialize(uid:, body:) @uid = uid @body = body end def ==(other) self.class == other.class && uid == other.uid && body == other.body end end end gitlab-mail_room-0.0.24/lib/mail_room/microsoft_graph/0000755000004100000410000000000014563474377023001 5ustar www-datawww-datagitlab-mail_room-0.0.24/lib/mail_room/microsoft_graph/connection.rb0000644000004100000410000001450614563474377025473 0ustar www-datawww-data# frozen_string_literal: true require 'json' require 'oauth2' module MailRoom module MicrosoftGraph class Connection < MailRoom::Connection NEXT_PAGE_KEY = '@odata.nextLink' DEFAULT_POLL_INTERVAL_S = 60 TooManyRequestsError = Class.new(RuntimeError) attr_accessor :token, :throttled_count def initialize(mailbox) super reset setup end def wait return if stopped? process_mailbox @throttled_count = 0 wait_for_new_messages rescue TooManyRequestsError => e @throttled_count += 1 @mailbox.logger.warn({ context: @mailbox.context, action: 'Too many requests, backing off...', backoff_s: backoff_secs, error: e.message, error_backtrace: e.backtrace }) backoff rescue IOError => e @mailbox.logger.warn({ context: @mailbox.context, action: 'Disconnected. Resetting...', error: e.message, error_backtrace: e.backtrace }) reset setup end private def wait_for_new_messages sleep_while_running(poll_interval) end def backoff sleep_while_running(backoff_secs) end def backoff_secs [60 * 10, 2**throttled_count].min end # Unless wake up periodically, we won't notice that the thread was stopped # if we sleep the entire interval. def sleep_while_running(sleep_interval) sleep_interval.times do do_sleep(1) return if stopped? end end def do_sleep(interval) sleep(interval) end def reset @token = nil @throttled_count = 0 end def setup @mailbox.logger.info({ context: @mailbox.context, action: 'Retrieving OAuth2 token...' }) @token = client.client_credentials.get_token({ scope: scope }) end def client @client ||= OAuth2::Client.new(client_id, client_secret, site: azure_ad_endpoint, authorize_url: "/#{tenant_id}/oauth2/v2.0/authorize", token_url: "/#{tenant_id}/oauth2/v2.0/token", auth_scheme: :basic_auth) end def inbox_options mailbox.inbox_options end def tenant_id inbox_options[:tenant_id] end def client_id inbox_options[:client_id] end def client_secret inbox_options[:client_secret] end def poll_interval @poll_interval ||= begin interval = inbox_options[:poll_interval].to_i if interval.positive? interval else DEFAULT_POLL_INTERVAL_S end end end def process_mailbox return unless @new_message_handler @mailbox.logger.info({ context: @mailbox.context, action: 'Processing started' }) new_messages.each do |msg| success = @new_message_handler.call(msg) handle_delivered(msg) if success end end def handle_delivered(msg) mark_as_read(msg) delete_message(msg) if @mailbox.delete_after_delivery end def delete_message(msg) token.delete(msg_url(msg.uid)) end def mark_as_read(msg) token.patch(msg_url(msg.uid), headers: { 'Content-Type' => 'application/json' }, body: { isRead: true }.to_json) end def new_messages messages_for_ids(new_message_ids) end # Yields a page of message IDs at a time def new_message_ids url = unread_messages_url Enumerator.new do |block| loop do messages, next_page_url = unread_messages(url: url) messages.each { |msg| block.yield msg } break unless next_page_url url = next_page_url end end end def unread_messages(url:) body = get(url) return [[], nil] unless body all_unread = body['value'].map { |msg| msg['id'] } to_deliver = all_unread.select { |uid| @mailbox.deliver?(uid) } @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 } }) [to_deliver, body[NEXT_PAGE_KEY]] rescue TypeError, JSON::ParserError => e log_exception('Error parsing JSON response', e) [[], nil] end # Returns the JSON response def get(url) response = token.get(url, { raise_errors: false }) # https://docs.microsoft.com/en-us/graph/errors case response.status when 509, 429 raise TooManyRequestsError when 400..599 raise OAuth2::Error, response end return unless response.body body = JSON.parse(response.body) raise TypeError, 'Response did not contain value hash' unless body.is_a?(Hash) && body.key?('value') body end def messages_for_ids(message_ids) message_ids.each_with_object([]) do |id, arr| response = token.get(rfc822_msg_url(id)) arr << ::MailRoom::Message.new(uid: id, body: response.body) end end def base_url "#{graph_endpoint}/v1.0/users/#{mailbox.email}/mailFolders/#{mailbox.name}/messages" end def unread_messages_url "#{base_url}?$filter=isRead eq false" end def msg_url(id) # Attempting to use the base_url fails with "The OData request is not supported" "#{graph_endpoint}/v1.0/users/#{mailbox.email}/messages/#{id}" end def rfc822_msg_url(id) # Attempting to use the base_url fails with "The OData request is not supported" "#{msg_url(id)}/$value" end def log_exception(message, exception) @mailbox.logger.warn({ context: @mailbox.context, message: message, exception: exception.to_s }) end def scope "#{graph_endpoint}/.default" end def graph_endpoint inbox_options[:graph_endpoint] || 'https://graph.microsoft.com' end def azure_ad_endpoint inbox_options[:azure_ad_endpoint] || 'https://login.microsoftonline.com' end end end end gitlab-mail_room-0.0.24/lib/mail_room/connection.rb0000644000004100000410000000064614563474377022305 0ustar www-datawww-data# frozen_string_literal: true module MailRoom class Connection attr_reader :mailbox, :new_message_handler def initialize(mailbox) @mailbox = mailbox @stopped = false end def on_new_message(&block) @new_message_handler = block end def stopped? @stopped end def wait raise NotImplementedError end def quit @stopped = true end end end gitlab-mail_room-0.0.24/logfile.log0000644000004100000410000000010214563474377017204 0ustar www-datawww-data# Logfile created on 2019-09-26 08:59:35 -0500 by logger.rb/66358 gitlab-mail_room-0.0.24/LICENSE.txt0000644000004100000410000000205314563474377016712 0ustar www-datawww-dataCopyright (c) 2013 Tony Pitale MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.gitlab-mail_room-0.0.24/spec/0000755000004100000410000000000014563474377016021 5ustar www-datawww-datagitlab-mail_room-0.0.24/spec/spec_helper.rb0000644000004100000410000000171414563474377020642 0ustar www-datawww-datarequire 'simplecov' SimpleCov.start require 'bundler/setup' require 'date' require 'rspec' require 'mocha/api' require File.expand_path('../../lib/mail_room', __FILE__) RSpec.configure do |config| config.mock_with :mocha config.run_all_when_everything_filtered = true config.filter_run :focus # Run specs in random order to surface order dependencies. If you find an # order dependency and want to debug it, you can fix the order by providing # the seed, which is printed after each run. # --seed 1234 config.order = 'random' end REQUIRED_MAILBOX_DEFAULTS = { name: "inbox", email: "user@example.com", password: "password123" } REQUIRED_MICROSOFT_GRAPH_DEFAULTS = { password: nil, inbox_method: :microsoft_graph, inbox_options: { tenant_id: '98776', client_id: '12345', client_secret: 'MY-SECRET', }.freeze }.freeze def build_mailbox(options = {}) MailRoom::Mailbox.new(REQUIRED_MAILBOX_DEFAULTS.merge(options)) end gitlab-mail_room-0.0.24/spec/lib/0000755000004100000410000000000014563474377016567 5ustar www-datawww-datagitlab-mail_room-0.0.24/spec/lib/health_check_spec.rb0000644000004100000410000000236314563474377022534 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe MailRoom::HealthCheck do let(:address) { '127.0.0.1' } let(:port) { 8000 } let(:params) { { address: address, port: port } } subject { described_class.new(params) } describe '#initialize' do context 'with valid parameters' do it 'validates successfully' do expect(subject).to be_a(described_class) end end context 'with invalid address' do let(:address) { nil } it 'raises an error' do expect { subject }.to raise_error('No health check address specified') end end context 'with invalid port' do let(:port) { nil } it 'raises an error' do expect { subject }.to raise_error('Health check port 0 is invalid') end end end describe '#run' do it 'sets running to true' do server = stub(start: true) subject.stubs(:create_server).returns(server) subject.run expect(subject.running).to be true end end describe '#quit' do it 'sets running to false' do server = stub(start: true, shutdown: true) subject.stubs(:create_server).returns(server) subject.run subject.quit expect(subject.running).to be false end end end gitlab-mail_room-0.0.24/spec/lib/mailbox_spec.rb0000644000004100000410000001236514563474377021570 0ustar www-datawww-datarequire 'spec_helper' describe MailRoom::Mailbox do let(:sample_message) { MailRoom::Message.new(uid: 123, body: 'a message') } context 'with IMAP configuration' do subject { build_mailbox } describe '#imap?' do it 'configured as an IMAP inbox' do expect(subject.imap?).to be true expect(subject.microsoft_graph?).to be false end end end describe "#deliver" do context "with arbitration_method of noop" do it 'arbitrates with a Noop instance' do mailbox = build_mailbox({arbitration_method: 'noop'}) noop = stub(:deliver?) MailRoom::Arbitration['noop'].stubs(new: noop) uid = 123 noop.expects(:deliver?).with(uid) mailbox.deliver?(uid) end end context "with arbitration_method of redis" do it 'arbitrates with a Redis instance' do mailbox = build_mailbox({arbitration_method: 'redis'}) redis = stub(:deliver?) MailRoom::Arbitration['redis'].stubs(new: redis) uid = 123 redis.expects(:deliver?).with(uid) mailbox.deliver?(uid) end end context "with delivery_method of noop" do it 'delivers with a Noop instance' do mailbox = build_mailbox({delivery_method: 'noop'}) noop = stub(:deliver) MailRoom::Delivery['noop'].stubs(new: noop) noop.expects(:deliver).with(sample_message.body) mailbox.deliver(sample_message) end end context "with delivery_method of logger" do it 'delivers with a Logger instance' do mailbox = build_mailbox({delivery_method: 'logger'}) logger = stub(:deliver) MailRoom::Delivery['logger'].stubs(new: logger) logger.expects(:deliver).with(sample_message.body) mailbox.deliver(sample_message) end end context "with delivery_method of postback" do it 'delivers with a Postback instance' do mailbox = build_mailbox({delivery_method: 'postback'}) postback = stub(:deliver) MailRoom::Delivery['postback'].stubs(new: postback) postback.expects(:deliver).with(sample_message.body) mailbox.deliver(sample_message) end end context "with delivery_method of letter_opener" do it 'delivers with a LetterOpener instance' do mailbox = build_mailbox({delivery_method: 'letter_opener'}) letter_opener = stub(:deliver) MailRoom::Delivery['letter_opener'].stubs(new: letter_opener) letter_opener.expects(:deliver).with(sample_message.body) mailbox.deliver(sample_message) end end context "without an RFC822 attribute" do it "doesn't deliver the message" do mailbox = build_mailbox({ name: "magic mailbox", delivery_method: 'noop' }) noop = stub(:deliver) MailRoom::Delivery['noop'].stubs(new: noop) noop.expects(:deliver).never mailbox.deliver(MailRoom::Message.new(uid: 1234, body: nil)) end end context "with ssl options hash" do it 'replaces verify mode with constant' do mailbox = build_mailbox({ssl: {verify_mode: :none}}) expect(mailbox.ssl_options).to eq({verify_mode: OpenSSL::SSL::VERIFY_NONE}) end end context 'structured logger setup' do it 'sets up the logger correctly and does not error' do mailbox = build_mailbox({ name: "magic mailbox", logger: { log_path: '/dev/null' } }) expect{ mailbox.logger.info(message: "asdf") }.not_to raise_error end it 'accepts stdout symbol to mean STDOUT' do mailbox = build_mailbox({ name: "magic mailbox", logger: { log_path: :stdout } }) expect{ mailbox.logger.info(message: "asdf") }.not_to raise_error end it 'sets up the noop logger correctly and does not error' do mailbox = build_mailbox({ name: "magic mailbox" }) expect{ mailbox.logger.info(message: "asdf") }.not_to raise_error end end end describe "#validate!" do context "with missing configuration" do it 'raises an error' do expect { build_mailbox({name: nil}) }.to raise_error(MailRoom::ConfigurationError) expect { build_mailbox({host: nil}) }.to raise_error(MailRoom::ConfigurationError) end end context "with Microsoft Graph configuration" do let(:options) do { arbitration_method: 'redis', }.merge(REQUIRED_MICROSOFT_GRAPH_DEFAULTS) end subject { build_mailbox(options) } def delete_inbox_option(key) options[:inbox_options] = options[:inbox_options].dup.delete(key) end it 'allows password omission' do expect { subject }.not_to raise_error end it 'configured as a Microsoft Graph inbox' do expect(subject.imap?).to be false expect(subject.microsoft_graph?).to be true end it 'raises an error when the inbox options are not present' do options.delete(:inbox_options) expect { subject }.to raise_error(MailRoom::ConfigurationError) end %i[tenant_id client_id client_secret].each do |item| it "raises an error when the #{item} is not present" do delete_inbox_option(item) expect { subject }.to raise_error(MailRoom::ConfigurationError) end end end end end gitlab-mail_room-0.0.24/spec/lib/cli_spec.rb0000644000004100000410000000407514563474377020703 0ustar www-datawww-datarequire 'spec_helper' describe MailRoom::CLI do let(:config_path) {File.expand_path('../fixtures/test_config.yml', File.dirname(__FILE__))} let!(:configuration) {MailRoom::Configuration.new({config_path: config_path})} let(:coordinator) {stub(run: true, quit: true)} let(:configuration_args) { anything } let(:coordinator_args) { [anything, anything] } describe '.new' do let(:args) {["-c", "a path"]} before :each do MailRoom::Configuration.expects(:new).with(configuration_args).returns(configuration) MailRoom::Coordinator.stubs(:new).with(*coordinator_args).returns(coordinator) end context 'with configuration args' do let(:configuration_args) do {config_path: 'a path'} end it 'parses arguments into configuration' do expect(MailRoom::CLI.new(args).configuration).to eq configuration end end context 'with coordinator args' do let(:coordinator_args) do [configuration.mailboxes, anything] end it 'creates a new coordinator with configuration' do expect(MailRoom::CLI.new(args).coordinator).to eq(coordinator) end end end describe '#start' do let(:cli) {MailRoom::CLI.new([])} before :each do cli.configuration = configuration cli.coordinator = coordinator cli.stubs(:exit) end it 'starts running the coordinator' do coordinator.expects(:run) cli.start end context 'on error' do let(:error) { RuntimeError.new("oh noes!") } let(:coordinator) { stub(run: true, quit: true) } let(:crash_handler) { stub(handle: nil) } before do cli.instance_variable_set(:@options, {exit_error_format: error_format}) coordinator.stubs(:run).raises(error) MailRoom::CrashHandler.stubs(:new).returns(crash_handler) end context 'json format provided' do let(:error_format) { 'json' } it 'passes onto CrashHandler' do crash_handler.expects(:handle).with(error, error_format) cli.start end end end end end gitlab-mail_room-0.0.24/spec/lib/imap/0000755000004100000410000000000014563474377017515 5ustar www-datawww-datagitlab-mail_room-0.0.24/spec/lib/imap/message_spec.rb0000644000004100000410000000160014563474377022475 0ustar www-datawww-data# frozen_string_literal:true require 'spec_helper' require 'securerandom' describe MailRoom::IMAP::Message do let(:uid) { SecureRandom.hex } let(:body) { 'hello world' } let(:seqno) { 5 } subject { described_class.new(uid: uid, body: body, seqno: seqno) } describe '#initalize' do it 'initializes with required parameters' do subject expect(subject.uid).to eq(uid) expect(subject.body).to eq(body) expect(subject.seqno).to eq(seqno) end end describe '#==' do let(:dup) { described_class.new(uid: uid, body: body, seqno: seqno) } let(:base_msg) { MailRoom::Message.new(uid: uid, body: body) } it 'matches an equivalent message' do expect(dup == subject).to be true end it 'does not match a base message' do expect(subject == base_msg).to be false expect(base_msg == subject).to be false end end end gitlab-mail_room-0.0.24/spec/lib/imap/connection_spec.rb0000644000004100000410000000312414563474377023213 0ustar www-datawww-datarequire 'spec_helper' describe MailRoom::IMAP::Connection do let(:imap) {stub} let(:mailbox) {build_mailbox(delete_after_delivery: true, expunge_deleted: true)} before :each do Net::IMAP.stubs(:new).returns(imap) end context "with imap set up" do let(:connection) {MailRoom::IMAP::Connection.new(mailbox)} let(:uid) { 1 } let(:seqno) { 8 } before :each do imap.stubs(:starttls) imap.stubs(:login) imap.stubs(:select) end it "is logged in" do expect(connection.logged_in?).to eq(true) end it "is not idling" do expect(connection.idling?).to eq(false) end it "is not disconnected" do imap.stubs(:disconnected?).returns(false) expect(connection.disconnected?).to eq(false) end it "is ready to idle" do expect(connection.ready_to_idle?).to eq(true) end it "waits for a message to process" do new_message = MailRoom::IMAP::Message.new(uid: uid, body: 'a message', seqno: seqno) connection.on_new_message do |message| expect(message).to eq(new_message) true end attr = { 'UID' => uid, 'RFC822' => new_message.body } fetch_data = Net::IMAP::FetchData.new(seqno, attr) imap.expects(:idle) imap.stubs(:uid_search).with(mailbox.search_command).returns([], [uid]) imap.expects(:uid_fetch).with([uid], "RFC822").returns([fetch_data]) mailbox.expects(:deliver?).with(uid).returns(true) imap.expects(:store).with(seqno, "+FLAGS", [Net::IMAP::DELETED]) imap.expects(:expunge).once connection.wait end end end gitlab-mail_room-0.0.24/spec/lib/jwt_spec.rb0000644000004100000410000000422514563474377020735 0ustar www-datawww-datarequire 'spec_helper' require 'mail_room/jwt' describe MailRoom::JWT do let(:secret_path) { File.expand_path('../fixtures/jwt_secret', File.dirname(__FILE__)) } let(:secret) { Base64.strict_decode64(File.read(secret_path).chomp) } let(:standard_config) do { secret_path: secret_path, issuer: 'mailroom', header: 'Mailroom-Api-Request', algorithm: 'HS256' } end describe '#token' do let(:jwt) { described_class.new(**standard_config) } it 'generates a valid jwt token' do token = jwt.token expect(token).not_to be_empty payload = nil expect do payload = JWT.decode(token, secret, true, iss: 'mailroom', verify_iat: true, verify_iss: true, algorithm: 'HS256') end.not_to raise_error expect(payload).to be_an(Array) expect(payload).to match( [ a_hash_including( 'iss' => 'mailroom', 'nonce' => be_a(String), 'iat' => be_a(Integer) ), { 'alg' => 'HS256' } ] ) end it 'generates a different token for each invocation' do expect(jwt.token).not_to eql(jwt.token) end end describe '#valid?' do it 'returns true if all essential components are present' do jwt = described_class.new(**standard_config) expect(jwt.valid?).to eql(true) end it 'returns true if header and secret path are present' do jwt = described_class.new( secret_path: secret_path, header: 'Mailroom-Api-Request', issuer: nil, algorithm: nil ) expect(jwt.valid?).to eql(true) expect(jwt.issuer).to eql(described_class::DEFAULT_ISSUER) expect(jwt.algorithm).to eql(described_class::DEFAULT_ALGORITHM) end it 'returns false if either header or secret_path are missing' do expect(described_class.new( secret_path: nil, header: 'Mailroom-Api-Request', issuer: nil, algorithm: nil ).valid?).to eql(false) expect(described_class.new( secret_path: secret_path, header: nil, issuer: nil, algorithm: nil ).valid?).to eql(false) end end end gitlab-mail_room-0.0.24/spec/lib/logger/0000755000004100000410000000000014563474377020046 5ustar www-datawww-datagitlab-mail_room-0.0.24/spec/lib/logger/structured_spec.rb0000644000004100000410000000431114563474377023610 0ustar www-datawww-datarequire 'spec_helper' describe MailRoom::Logger::Structured do subject { described_class.new $stdout } let!(:now) { Time.now } let(:timestamp) { now.to_datetime.iso8601(3) } let(:message) { { action: 'exciting development', message: 'testing 123' } } before do Time.stubs(:now).returns(now) end [:debug, :info, :warn, :error, :fatal].each do |level| it "logs #{level}" do expect { subject.send(level, message) }.to output(json_matching(level.to_s.upcase, message)).to_stdout_from_any_process end end it 'logs unknown' do expect { subject.unknown(message) }.to output(json_matching("ANY", message)).to_stdout_from_any_process end it 'only accepts hashes' do expect { subject.unknown("just a string!") }.to raise_error(ArgumentError, /must be a Hash/) end context 'logging a hash as a message' do it 'merges the contents' do input = { additional_field: "some value" } expected = { severity: 'DEBUG', time: timestamp, additional_field: "some value" } expect { subject.debug(input) }.to output(as_regex(expected)).to_stdout_from_any_process end end describe '#format_message' do shared_examples 'timestamp formatting' do it 'outputs ISO8601 timestamps' do data = JSON.parse(subject.format_message('debug', input_timestamp, 'test', { message: 'hello' } )) expect(data['time']).to eq(expected_timestamp) end end context 'with no timestamp' do let(:input_timestamp) { nil } let(:expected_timestamp) { timestamp } it_behaves_like 'timestamp formatting' end context 'with DateTime' do let(:input_timestamp) { now.to_datetime } let(:expected_timestamp) { timestamp } it_behaves_like 'timestamp formatting' end context 'with string' do let(:input_timestamp) { now.to_s } let(:expected_timestamp) { input_timestamp } it_behaves_like 'timestamp formatting' end end def json_matching(level, message) contents = { severity: level, time: timestamp }.merge(message) as_regex(contents) end def as_regex(contents) /#{Regexp.quote(contents.to_json)}/ end end gitlab-mail_room-0.0.24/spec/lib/coordinator_spec.rb0000644000004100000410000000425314563474377022455 0ustar www-datawww-datarequire 'spec_helper' describe MailRoom::Coordinator do describe '#initialize' do it 'builds a watcher for each mailbox' do MailRoom::MailboxWatcher.expects(:new).with('mailbox1').returns('watcher1') MailRoom::MailboxWatcher.expects(:new).with('mailbox2').returns('watcher2') coordinator = MailRoom::Coordinator.new(['mailbox1', 'mailbox2']) expect(coordinator.watchers).to eq(['watcher1', 'watcher2']) end it 'makes no watchers when mailboxes is empty' do coordinator = MailRoom::Coordinator.new([]) expect(coordinator.watchers).to eq([]) end it 'sets the health check' do health_check = MailRoom::HealthCheck.new({ address: '127.0.0.1', port: 8080}) coordinator = MailRoom::Coordinator.new([], health_check) expect(coordinator.health_check).to eq(health_check) end end describe '#run' do it 'runs each watcher' do watcher = stub watcher.stubs(:run) watcher.stubs(:quit) health_check = stub health_check.stubs(:run) health_check.stubs(:quit) MailRoom::MailboxWatcher.stubs(:new).returns(watcher) coordinator = MailRoom::Coordinator.new(['mailbox1'], health_check) coordinator.stubs(:sleep_while_running) watcher.expects(:run) watcher.expects(:quit) health_check.expects(:run) health_check.expects(:quit) coordinator.run end it 'should go to sleep after running watchers' do coordinator = MailRoom::Coordinator.new([]) coordinator.stubs(:running=) coordinator.stubs(:running?).returns(false) coordinator.expects(:running=).with(true) coordinator.expects(:running?) coordinator.run end it 'should set attribute running to true' do coordinator = MailRoom::Coordinator.new([]) coordinator.stubs(:sleep_while_running) coordinator.run expect(coordinator.running).to eq(true) end end describe '#quit' do it 'quits each watcher' do watcher = stub(:quit) MailRoom::MailboxWatcher.stubs(:new).returns(watcher) coordinator = MailRoom::Coordinator.new(['mailbox1']) watcher.expects(:quit) coordinator.quit end end end gitlab-mail_room-0.0.24/spec/lib/arbitration/0000755000004100000410000000000014563474377021105 5ustar www-datawww-datagitlab-mail_room-0.0.24/spec/lib/arbitration/redis_spec.rb0000644000004100000410000001011214563474377023545 0ustar www-datawww-datarequire 'spec_helper' require 'mail_room/arbitration/redis' describe MailRoom::Arbitration::Redis do let(:mailbox) { build_mailbox( arbitration_options: { namespace: "mail_room", redis_url: ENV['REDIS_URL'] } ) } let(:options) { described_class::Options.new(mailbox) } let(:redis5) { Gem::Version.new(Redis::VERSION) >= Gem::Version.new('5.0') } subject { described_class.new(options) } # Private, but we don't care. let(:redis) { subject.send(:client) } let(:raw_client) { redis._client } let(:server_url) do if redis5 raw_client.config.server_url else raw_client.options[:url] end end describe '#deliver?' do context "when called the first time" do after do redis.del("delivered:123") end it "returns true" do expect(subject.deliver?(123)).to be_truthy end it "increments the delivered flag" do subject.deliver?(123) expect(redis.get("delivered:123")).to eq("1") end it "sets an expiration on the delivered flag" do subject.deliver?(123) expect(redis.ttl("delivered:123")).to be > 0 end end context "when called the second time" do before do #Short expiration, 1 second, for testing subject.deliver?(123, 1) end after do redis.del("delivered:123") end it "returns false" do expect(subject.deliver?(123, 1)).to be_falsey end it "after expiration returns true" do # Fails locally because fakeredis returns 0, not false expect(subject.deliver?(123, 1)).to be_falsey sleep(redis.ttl("delivered:123")+1) expect(subject.deliver?(123, 1)).to be_truthy end end context "when called for another uid" do before do subject.deliver?(123) end after do redis.del("delivered:123") redis.del("delivered:124") end it "returns true" do expect(subject.deliver?(124)).to be_truthy end end end context 'redis client connection params' do context 'when only url is present' do let(:redis_url) { ENV.fetch('REDIS_URL', 'redis://localhost:6379/0') } let(:mailbox) { build_mailbox( arbitration_options: { redis_url: redis_url } ) } after do redis.del("delivered:123") end it 'client has same specified url' do subject.deliver?(123) expect(server_url).to eq redis_url end it 'client is a instance of Redis class' do expect(redis).to be_a Redis end end context 'when namespace is present' do let(:namespace) { 'mail_room' } let(:mailbox) { build_mailbox( arbitration_options: { namespace: namespace } ) } it 'client has same specified namespace' do expect(redis.namespace).to eq(namespace) end it 'client is a instance of RedisNamespace class' do expect(redis).to be_a ::Redis::Namespace end end context 'when sentinel is present' do let(:redis_url) { 'redis://:mypassword@sentinel-master:6379' } let(:sentinels) { [{ host: '10.0.0.1', port: '26379' }] } let(:mailbox) { build_mailbox( arbitration_options: { redis_url: redis_url, sentinels: sentinels } ) } it 'client has same specified sentinel params' do if redis5 expect(raw_client.config.password).to eq('mypassword') client_sentinels = raw_client.config.sentinels expect(client_sentinels.length).to eq(sentinels.length) expect(client_sentinels[0].host).to eq('10.0.0.1') expect(client_sentinels[0].port).to eq(26379) # rubocop:disable Style/NumericLiterals else expect(raw_client.options[:host]).to eq('sentinel-master') expect(raw_client.options[:password]).to eq('mypassword') expect(raw_client.options[:sentinels]).to eq(sentinels) end end end end end gitlab-mail_room-0.0.24/spec/lib/delivery/0000755000004100000410000000000014563474377020412 5ustar www-datawww-datagitlab-mail_room-0.0.24/spec/lib/delivery/que_spec.rb0000644000004100000410000000200114563474377022534 0ustar www-datawww-datarequire 'spec_helper' require 'mail_room/delivery/que' describe MailRoom::Delivery::Que do describe '#deliver' do let(:mailbox) {build_mailbox({ delivery_options: { database: 'delivery_test', username: 'postgres', password: '', queue: 'default', priority: 5, job_class: 'ParseMailJob' } })} let(:connection) {stub} let(:options) {MailRoom::Delivery::Que::Options.new(mailbox)} it 'stores the message in que_jobs table' do PG.expects(:connect).with({ host: 'localhost', port: 5432, dbname: 'delivery_test', user: 'postgres', password: '' }).returns(connection) connection.expects(:exec).with( "INSERT INTO que_jobs (priority, job_class, queue, args) VALUES ($1, $2, $3, $4)", [ 5, 'ParseMailJob', 'default', JSON.dump(['email']) ] ) MailRoom::Delivery::Que.new(options).deliver('email') end end end gitlab-mail_room-0.0.24/spec/lib/delivery/logger_spec.rb0000644000004100000410000000206314563474377023231 0ustar www-datawww-datarequire 'spec_helper' require 'mail_room/delivery/logger' describe MailRoom::Delivery::Logger do describe '#initialize' do context "without a log path" do let(:mailbox) {build_mailbox} it 'creates a new ruby logger' do ::Logger.stubs(:new) ::Logger.expects(:new).with(STDOUT) MailRoom::Delivery::Logger.new(mailbox) end end context "with a log path" do let(:mailbox) {build_mailbox(log_path: '/var/log/mail-room.log')} it 'creates a new file to append to' do file = stub file.stubs(:sync=) File.expects(:open).with('/var/log/mail-room.log', 'a').returns(file) ::Logger.stubs(:new).with(file) MailRoom::Delivery::Logger.new(mailbox) end end end describe '#deliver' do let(:mailbox) {build_mailbox} it 'writes the message to info' do logger = stub(:info) ::Logger.stubs(:new).returns(logger) logger.expects(:info).with('a message') MailRoom::Delivery::Logger.new(mailbox).deliver('a message') end end end gitlab-mail_room-0.0.24/spec/lib/delivery/letter_opener_spec.rb0000644000004100000410000000204714563474377024623 0ustar www-datawww-datarequire 'spec_helper' require 'mail_room/delivery/letter_opener' describe MailRoom::Delivery::LetterOpener do describe '#deliver' do let(:mailbox) {build_mailbox(location: '/tmp/somewhere')} let(:delivery_method) {stub} let(:mail) {stub} before :each do Mail.stubs(:read_from_string).returns(mail) ::LetterOpener::DeliveryMethod.stubs(:new).returns(delivery_method) delivery_method.stubs(:deliver!) end it 'creates a new LetterOpener::DeliveryMethod' do ::LetterOpener::DeliveryMethod.expects(:new).with(location: '/tmp/somewhere').returns(delivery_method) MailRoom::Delivery::LetterOpener.new(mailbox).deliver('a message') end it 'parses the message string with Mail' do ::Mail.expects(:read_from_string).with('a message') MailRoom::Delivery::LetterOpener.new(mailbox).deliver('a message') end it 'delivers the mail message' do delivery_method.expects(:deliver!).with(mail) MailRoom::Delivery::LetterOpener.new(mailbox).deliver('a message') end end endgitlab-mail_room-0.0.24/spec/lib/delivery/sidekiq_spec.rb0000644000004100000410000000656714563474377023420 0ustar www-datawww-datarequire 'spec_helper' require 'mail_room/delivery/sidekiq' describe MailRoom::Delivery::Sidekiq do subject { described_class.new(options) } let(:redis) { subject.send(:client) } let(:raw_client) { redis._client } let(:options) { MailRoom::Delivery::Sidekiq::Options.new(mailbox) } let(:redis5) { Gem::Version.new(Redis::VERSION) >= Gem::Version.new('5.0') } let(:server_url) do if redis5 raw_client.config.server_url else raw_client.options[:url] end end describe '#options' do let(:redis_url) { 'redis://localhost' } let(:redis_options) { { redis_url: redis_url } } context 'when only redis_url is specified' do let(:mailbox) { build_mailbox( delivery_method: :sidekiq, delivery_options: redis_options ) } context 'with simple redis url' do let(:expected_url) { redis5 ? "#{redis_url}:6379/0" : redis_url } it 'client has same specified redis_url' do expect(raw_client.server_url).to eq(expected_url) end it 'client is a instance of RedisNamespace class' do expect(redis).to be_a ::Redis end it 'connection has correct values' do expect(redis.connection[:host]).to eq('localhost') expect(redis.connection[:db]).to eq(0) end end context 'with redis_db specified in options' do let(:expected_url) { redis5 ? "#{redis_url}:6379/4" : redis_url } before do redis_options[:redis_db] = 4 end it 'client has correct redis_url' do expect(raw_client.server_url).to eq(expected_url) end it 'connection has correct values' do expect(redis.connection[:host]).to eq('localhost') expect(redis.connection[:db]).to eq(4) end end end context 'when namespace is specified' do let(:namespace) { 'sidekiq_mailman' } let(:mailbox) { build_mailbox( delivery_method: :sidekiq, delivery_options: { redis_url: redis_url, namespace: namespace } ) } it 'client has same specified namespace' do expect(redis.namespace).to eq(namespace) end it 'client is a instance of RedisNamespace class' do expect(redis).to be_a ::Redis::Namespace end end context 'when sentinel is specified' do let(:redis_url) { 'redis://:mypassword@sentinel-master:6379' } let(:sentinels) { [{ host: '10.0.0.1', port: '26379' }] } let(:mailbox) { build_mailbox( delivery_method: :sidekiq, delivery_options: { redis_url: redis_url, sentinels: sentinels } ) } it 'client has same specified sentinel params' do if redis5 expect(raw_client.config.password).to eq('mypassword') client_sentinels = raw_client.config.sentinels expect(client_sentinels.length).to eq(sentinels.length) expect(client_sentinels[0].host).to eq('10.0.0.1') expect(client_sentinels[0].port).to eq(26379) # rubocop:disable Style/NumericLiterals else expect(raw_client.options[:host]).to eq('sentinel-master') expect(raw_client.options[:password]).to eq('mypassword') expect(raw_client.options[:sentinels]).to eq(sentinels) end end end end end gitlab-mail_room-0.0.24/spec/lib/delivery/postback_spec.rb0000644000004100000410000001031614563474377023560 0ustar www-datawww-datarequire 'spec_helper' require 'mail_room/delivery/postback' describe MailRoom::Delivery::Postback do describe '#deliver' do context 'with token auth delivery' do let(:mailbox) {build_mailbox({ delivery_url: 'http://localhost/inbox', delivery_token: 'abcdefg' })} let(:delivery_options) { MailRoom::Delivery::Postback::Options.new(mailbox) } it 'posts the message with faraday' do connection = stub request = stub Faraday.stubs(:new).returns(connection) connection.expects(:token_auth).with('abcdefg') connection.expects(:post).yields(request) request.expects(:url).with('http://localhost/inbox') request.expects(:body=).with('a message') MailRoom::Delivery::Postback.new(delivery_options).deliver('a message') end end context 'with basic auth delivery options' do let(:mailbox) {build_mailbox({ delivery_options: { url: 'http://localhost/inbox', username: 'user1', password: 'password123abc' } })} let(:delivery_options) { MailRoom::Delivery::Postback::Options.new(mailbox) } it 'posts the message with faraday' do connection = stub request = stub Faraday.stubs(:new).returns(connection) connection.expects(:basic_auth).with('user1', 'password123abc') connection.expects(:post).yields(request) request.expects(:url).with('http://localhost/inbox') request.expects(:body=).with('a message') MailRoom::Delivery::Postback.new(delivery_options).deliver('a message') end context 'with content type in the delivery options' do let(:mailbox) {build_mailbox({ delivery_options: { url: 'http://localhost/inbox', username: 'user1', password: 'password123abc', content_type: 'text/plain' } })} let(:delivery_options) { MailRoom::Delivery::Postback::Options.new(mailbox) } it 'posts the message with faraday' do connection = stub request = stub Faraday.stubs(:new).returns(connection) connection.expects(:post).yields(request) request.stubs(:url) request.stubs(:body=) request.stubs(:headers).returns({}) connection.expects(:basic_auth).with('user1', 'password123abc') MailRoom::Delivery::Postback.new(delivery_options).deliver('a message') expect(request.headers['Content-Type']).to eq('text/plain') end end context 'with jwt token in the delivery options' do let(:mailbox) {build_mailbox({ delivery_options: { url: 'http://localhost/inbox', jwt_auth_header: "Mailroom-Api-Request", jwt_issuer: "mailroom", jwt_algorithm: "HS256", jwt_secret_path: "secret_path" } })} let(:delivery_options) { MailRoom::Delivery::Postback::Options.new(mailbox) } it 'posts the message with faraday' do connection = stub request = stub Faraday.stubs(:new).returns(connection) connection.expects(:post).yields(request).twice request.stubs(:url) request.stubs(:body=) request.stubs(:headers).returns({}) jwt = stub MailRoom::JWT.expects(:new).with( header: 'Mailroom-Api-Request', issuer: 'mailroom', algorithm: 'HS256', secret_path: 'secret_path' ).returns(jwt) jwt.stubs(:valid?).returns(true) jwt.stubs(:header).returns('Mailroom-Api-Request') jwt.stubs(:token).returns('a_jwt_token') delivery = MailRoom::Delivery::Postback.new(delivery_options) delivery.deliver('a message') expect(request.headers['Mailroom-Api-Request']).to eql('a_jwt_token') # A different jwt token for the second time jwt.stubs(:token).returns('another_jwt_token') delivery.deliver('another message') expect(request.headers['Mailroom-Api-Request']).to eql('another_jwt_token') end end end end end gitlab-mail_room-0.0.24/spec/lib/message_spec.rb0000644000004100000410000000143714563474377021557 0ustar www-datawww-data# frozen_string_literal:true require 'spec_helper' require 'securerandom' describe MailRoom::Message do let(:uid) { SecureRandom.hex } let(:body) { 'hello world' } subject { described_class.new(uid: uid, body: body) } describe '#initalize' do it 'initializes with required parameters' do subject expect(subject.uid).to eq(uid) expect(subject.body).to eq(body) end end describe '#==' do let(:dup) { described_class.new(uid: uid, body: body) } it 'matches an equivalent message' do expect(dup == subject).to be true end it 'does not match a message with a different UID' do msg = described_class.new(uid: '12345', body: body) expect(subject == msg).to be false expect(msg == subject).to be false end end end gitlab-mail_room-0.0.24/spec/lib/microsoft_graph/0000755000004100000410000000000014563474377021755 5ustar www-datawww-datagitlab-mail_room-0.0.24/spec/lib/microsoft_graph/connection_spec.rb0000644000004100000410000001573614563474377025467 0ustar www-datawww-data# frozen_string_literal: true require 'securerandom' require 'spec_helper' require 'json' require 'webmock/rspec' describe MailRoom::MicrosoftGraph::Connection do let(:tenant_id) { options[:inbox_options][:tenant_id] } let(:options) do { delete_after_delivery: true, expunge_deleted: true }.merge(REQUIRED_MICROSOFT_GRAPH_DEFAULTS) end let(:mailbox) { build_mailbox(options) } let(:graph_endpoint) { 'https://graph.microsoft.com' } let(:azure_ad_endpoint) { 'https://login.microsoftonline.com' } let(:base_url) { "#{graph_endpoint}/v1.0/users/user@example.com/mailFolders/inbox/messages" } let(:message_base_url) { "#{graph_endpoint}/v1.0/users/user@example.com/messages" } let(:connection) { described_class.new(mailbox) } let(:uid) { 1 } let(:access_token) { SecureRandom.hex } let(:refresh_token) { SecureRandom.hex } let(:expires_in) { Time.now + 3600 } let(:unread_messages_body) { '' } let(:status) { 200 } let!(:stub_token) do stub_request(:post, "#{azure_ad_endpoint}/#{tenant_id}/oauth2/v2.0/token").to_return( body: { 'access_token' => access_token, 'refresh_token' => refresh_token, 'expires_in' => expires_in }.to_json, headers: { 'Content-Type' => 'application/json' } ) end let!(:stub_unread_messages_request) do stub_request(:get, "#{base_url}?$filter=isRead%20eq%20false").to_return( status: status, body: unread_messages_body.to_json, headers: { 'Content-Type' => 'application/json' } ) end before do WebMock.enable! end context '#quit' do it 'returns false' do expect(connection.stopped?).to be_falsey end it 'returns true' do connection.quit expect(connection.stopped?).to be_truthy end it 'does not attempt to process the mailbox' do connection.quit connection.expects(:process_mailbox).times(0) connection.wait end end context '#wait' do before do connection.stubs(:do_sleep) end describe 'poll interval' do it 'defaults to 60 seconds' do expect(connection.send(:poll_interval)).to eq(60) end it 'calls do_sleep 60 times' do connection.expects(:do_sleep).with(1).times(60) connection.wait end context 'interval set to 10' do let(:options) do { inbox_method: :microsoft_graph, inbox_options: { tenant_id: '98776', client_id: '12345', client_secret: 'MY-SECRET', poll_interval: '10' } } end it 'sets the poll interval to 10' do expect(connection.send(:poll_interval)).to eq(10) end it 'calls do_sleep 10 times' do connection.expects(:do_sleep).with(1).times(10) connection.wait end end end shared_examples 'with a single message' do let(:message_id) { SecureRandom.hex } let(:unread_messages_body) { { value: ['id' => message_id] } } let(:message_url) { "#{message_base_url}/#{message_id}" } let(:message_body) { 'hello world' } it 'requests message ID' do stub_get = stub_request(:get, "#{message_url}/$value").to_return( status: 200, body: message_body ) stub_patch = stub_request(:patch, message_url).with(body: { "isRead": true }.to_json) stub_delete = stub_request(:delete, message_url) message_count = 0 connection.on_new_message do |message| message_count += 1 expect(message.uid).to eq(message_id) expect(message.body).to eq(message_body) end connection.wait assert_requested(stub_token) assert_requested(stub_unread_messages_request) assert_requested(stub_get) assert_requested(stub_patch) assert_requested(stub_delete) expect(message_count).to eq(1) end end context 'with default Azure settings' do before do puts options end it_behaves_like 'with a single message' end # https://docs.microsoft.com/en-us/graph/deployments context 'with an alternative Azure deployment' do let(:graph_endpoint) { 'https://graph.microsoft.us' } let(:azure_ad_endpoint) { 'https://login.microsoftonline.us' } let(:options) do { inbox_method: :microsoft_graph, delete_after_delivery: true, expunge_deleted: true, inbox_options: { tenant_id: '98776', client_id: '12345', client_secret: 'MY-SECRET', graph_endpoint: 'https://graph.microsoft.us', azure_ad_endpoint: 'https://login.microsoftonline.us' } } end it_behaves_like 'with a single message' end context 'with multiple pages of messages' do let(:message_ids) { [SecureRandom.hex, SecureRandom.hex] } let(:next_page_url) { "#{graph_endpoint}/v1.0/nextPage" } let(:unread_messages_body) { { value: ['id' => message_ids.first], '@odata.nextLink' => next_page_url } } let(:message_body) { 'hello world' } it 'requests message ID' do stub_request(:get, next_page_url).to_return( status: 200, body: { value: ['id' => message_ids[1]] }.to_json ) stubs = [] message_ids.each do |message_id| rfc822_msg_url = "#{message_base_url}/#{message_id}/$value" stubs << stub_request(:get, rfc822_msg_url).to_return( status: 200, body: message_body ) msg_url = "#{message_base_url}/#{message_id}" stubs << stub_request(:patch, msg_url).with(body: { "isRead": true }.to_json) stubs << stub_request(:delete, msg_url) end message_count = 0 connection.on_new_message do |message| expect(message.uid).to eq(message_ids[message_count]) expect(message.body).to eq(message_body) message_count += 1 end connection.wait stubs.each { |stub| assert_requested(stub) } expect(message_count).to eq(2) end end shared_examples 'request backoff' do it 'backs off' do connection.expects(:backoff) connection.on_new_message {} connection.wait expect(connection.throttled_count).to eq(1) end end context 'too many requests' do let(:status) { 429 } it_behaves_like 'request backoff' end context 'too much bandwidth' do let(:status) { 509 } it_behaves_like 'request backoff' end context 'invalid JSON response' do let(:body) { 'this is something' } it 'ignores the message and logs a warning' do mailbox.logger.expects(:warn) connection.on_new_message {} connection.wait end end context '500 error' do let(:status) { 500 } it 'terminates due to error' do connection.on_new_message {} expect { connection.wait }.to raise_error(OAuth2::Error) end end end end gitlab-mail_room-0.0.24/spec/lib/mailbox_watcher_spec.rb0000644000004100000410000000401614563474377023277 0ustar www-datawww-datarequire 'spec_helper' describe MailRoom::MailboxWatcher do context 'with IMAP configured' do let(:mailbox) {build_mailbox} describe '#running?' do it 'is false by default' do watcher = MailRoom::MailboxWatcher.new(mailbox) expect(watcher.running?).to eq(false) end end describe '#run' do let(:imap) {stub(login: true, select: true)} let(:watcher) {MailRoom::MailboxWatcher.new(mailbox)} before :each do Net::IMAP.stubs(:new).returns(imap) # prevent connection end it 'loops over wait while running' do connection = MailRoom::IMAP::Connection.new(mailbox) MailRoom::IMAP::Connection.stubs(:new).returns(connection) watcher.expects(:running?).twice.returns(true, false) connection.expects(:wait).once connection.expects(:on_new_message).once watcher.run watcher.watching_thread.join # wait for finishing run end end describe '#quit' do let(:imap) {stub(login: true, select: true)} let(:watcher) {MailRoom::MailboxWatcher.new(mailbox)} before :each do Net::IMAP.stubs(:new).returns(imap) # prevent connection end it 'closes and waits for the connection' do connection = MailRoom::IMAP::Connection.new(mailbox) connection.stubs(:wait) connection.stubs(:quit) MailRoom::IMAP::Connection.stubs(:new).returns(connection) watcher.run expect(watcher.running?).to eq(true) connection.expects(:quit) watcher.quit expect(watcher.running?).to eq(false) end end end context 'with Microsoft Graph configured' do let(:mailbox) { build_mailbox(REQUIRED_MICROSOFT_GRAPH_DEFAULTS) } subject { described_class.new(mailbox) } it 'initializes a Microsoft Graph connection' do connection = stub(on_new_message: nil) MailRoom::MicrosoftGraph::Connection.stubs(:new).returns(connection) expect(subject.send(:connection)).to eq(connection) end end end gitlab-mail_room-0.0.24/spec/lib/configuration_spec.rb0000644000004100000410000000201714563474377022775 0ustar www-datawww-datarequire 'spec_helper' describe MailRoom::Configuration do let(:config_path) {File.expand_path('../fixtures/test_config.yml', File.dirname(__FILE__))} describe '#initalize' do context 'with config_path' do let(:configuration) { MailRoom::Configuration.new(config_path: config_path) } it 'parses yaml into mailbox objects' do MailRoom::Mailbox.stubs(:new).returns('mailbox1', 'mailbox2') expect(configuration.mailboxes).to eq(['mailbox1', 'mailbox2']) end it 'parses health check' do expect(configuration.health_check).to be_a(MailRoom::HealthCheck) end end context 'without config_path' do let(:configuration) { MailRoom::Configuration.new } it 'sets mailboxes to an empty set' do MailRoom::Mailbox.stubs(:new) MailRoom::Mailbox.expects(:new).never expect(configuration.mailboxes).to eq([]) end it 'sets the health check to nil' do expect(configuration.health_check).to be_nil end end end end gitlab-mail_room-0.0.24/spec/lib/crash_handler_spec.rb0000644000004100000410000000175714563474377022735 0ustar www-datawww-datarequire 'spec_helper' describe MailRoom::CrashHandler do let(:error_message) { "oh noes!" } let(:error) { RuntimeError.new(error_message) } let(:stdout) { StringIO.new } describe '#handle' do subject{ described_class.new(stdout).handle(error, format) } context 'when given a json format' do let(:format) { 'json' } it 'writes a json message to stdout' do subject stdout.rewind output = stdout.read expect(output).to end_with("\n") expect(JSON.parse(output)['message']).to eq(error_message) end end context 'when given a blank format' do let(:format) { "" } it 'raises an error as designed' do expect{ subject }.to raise_error(error.class, error_message) end end context 'when given a nonexistent format' do let(:format) { "nonsense" } it 'raises an error as designed' do expect{ subject }.to raise_error(error.class, error_message) end end end end gitlab-mail_room-0.0.24/spec/fixtures/0000755000004100000410000000000014563474377017672 5ustar www-datawww-datagitlab-mail_room-0.0.24/spec/fixtures/test_config.yml0000644000004100000410000000065714563474377022731 0ustar www-datawww-data--- :health_check: :address: "127.0.0.1" :port: 8080 :mailboxes: - :email: "user1@gmail.com" :password: "password" :name: "inbox" :delivery_url: "http://localhost:3000/inbox" :delivery_token: "abcdefg" :logger: :log_path: "logfile.log" - :email: "user2@gmail.com" :password: "password" :name: "inbox" :delivery_url: "http://localhost:3000/inbox" :delivery_token: "abcdefg" gitlab-mail_room-0.0.24/spec/fixtures/jwt_secret0000644000004100000410000000002114563474377021757 0ustar www-datawww-dataaGVsbG93b3JsZA== gitlab-mail_room-0.0.24/mail_room.gemspec0000644000004100000410000000327314563474377020417 0ustar www-datawww-data# -*- encoding: utf-8 -*- lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'mail_room/version' Gem::Specification.new do |gem| gem.name = "gitlab-mail_room" gem.version = MailRoom::VERSION gem.authors = ["Tony Pitale"] gem.email = ["tpitale@gmail.com"] gem.description = %q{mail_room will proxy email (gmail) from IMAP to a delivery method} gem.summary = %q{mail_room will proxy email (gmail) from IMAP to a callback URL, logger, or letter_opener} gem.homepage = "http://github.com/tpitale/mail_room" gem.files = `git ls-files`.split($/) gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) gem.require_paths = ["lib"] gem.add_dependency "net-imap", ">= 0.2.1" gem.add_dependency "oauth2", [">= 1.4.4", "< 3"] gem.add_dependency "jwt", ">= 2.0" gem.add_dependency "redis", [">= 4", "< 6"] gem.add_dependency "redis-namespace", '>= 1.8.2' gem.add_development_dependency "rake" gem.add_development_dependency "rspec", "~> 3.9" gem.add_development_dependency "rubocop", "~> 1.11" gem.add_development_dependency "mocha", "~> 1.11" gem.add_development_dependency "simplecov" gem.add_development_dependency "webrick", "~> 1.6" # for testing delivery methods gem.add_development_dependency "faraday" gem.add_development_dependency "mail" gem.add_development_dependency "letter_opener" gem.add_development_dependency "pg" gem.add_development_dependency "charlock_holmes" gem.add_development_dependency "webmock" end gitlab-mail_room-0.0.24/Rakefile0000644000004100000410000000016214563474377016533 0ustar www-datawww-datarequire "bundler/gem_tasks" require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) task default: :spec gitlab-mail_room-0.0.24/CODE_OF_CONDUCT.md0000644000004100000410000000445414563474377017675 0ustar www-datawww-data# Contributor Code of Conduct As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery * Personal attacks * Trolling or insulting/derogatory comments * Public or private harassment * Publishing other's private information, such as physical or electronic addresses, without explicit permission * Other unethical or unprofessional conduct Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a project maintainer at [INSERT EMAIL ADDRESS]. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter of an incident. This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.3.0, available at [http://contributor-covenant.org/version/1/3/0/](http://contributor-covenant.org/version/1/3/0/) gitlab-mail_room-0.0.24/.rubocop_todo.yml0000644000004100000410000003715214563474377020376 0ustar www-datawww-data# This configuration was generated by # `rubocop --auto-gen-config` # on 2023-03-15 01:41:21 UTC using RuboCop version 1.48.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. # Offense count: 5 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include. # Include: **/*.gemspec Gemspec/OrderedDependencies: Exclude: - 'mail_room.gemspec' # Offense count: 1 # Configuration parameters: Severity, Include. # Include: **/*.gemspec Gemspec/RequiredRubyVersion: Exclude: - 'mail_room.gemspec' # Offense count: 5 # This cop supports safe autocorrection (--autocorrect). Layout/BlockEndNewline: Exclude: - 'spec/lib/delivery/postback_spec.rb' - 'spec/lib/delivery/que_spec.rb' # Offense count: 5 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, IndentOneStep, IndentationWidth. # SupportedStyles: case, end Layout/CaseIndentation: Exclude: - 'lib/mail_room/mailbox.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). Layout/EmptyLineAfterMagicComment: Exclude: - 'mail_room.gemspec' # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: around, only_before Layout/EmptyLinesAroundAccessModifier: Exclude: - 'lib/mail_room/coordinator.rb' - 'lib/mail_room/delivery/que.rb' # Offense count: 4 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: empty_lines, no_empty_lines Layout/EmptyLinesAroundBlockBody: Exclude: - 'spec/lib/crash_handler_spec.rb' - 'spec/lib/delivery/sidekiq_spec.rb' - 'spec/lib/logger/structured_spec.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines, beginning_only, ending_only Layout/EmptyLinesAroundClassBody: Exclude: - 'lib/mail_room/logger/structured.rb' # Offense count: 15 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, IndentationWidth. # SupportedStyles: special_inside_parentheses, consistent, align_braces Layout/FirstHashElementIndentation: Exclude: - 'spec/lib/delivery/postback_spec.rb' - 'spec/lib/delivery/que_spec.rb' - 'spec/lib/logger/structured_spec.rb' # Offense count: 5 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. # SupportedHashRocketStyles: key, separator, table # SupportedColonStyles: key, separator, table # SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit Layout/HashAlignment: Exclude: - 'lib/mail_room/delivery/sidekiq.rb' # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowDoxygenCommentStyle, AllowGemfileRubyComment. Layout/LeadingCommentSpace: Exclude: - 'lib/mail_room/mailbox.rb' - 'spec/lib/arbitration/redis_spec.rb' # Offense count: 5 # This cop supports safe autocorrection (--autocorrect). Layout/MultilineBlockLayout: Exclude: - 'spec/lib/delivery/postback_spec.rb' - 'spec/lib/delivery/que_spec.rb' # Offense count: 3 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: space, no_space Layout/SpaceAroundEqualsInParameterDefault: Exclude: - 'lib/mail_room/configuration.rb' - 'lib/mail_room/crash_handler.rb' - 'lib/mail_room/mailbox.rb' # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). Layout/SpaceAroundKeyword: Exclude: - 'lib/mail_room/coordinator.rb' - 'lib/mail_room/mailbox_watcher.rb' # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowForAlignment, EnforcedStyleForExponentOperator. # SupportedStylesForExponentOperator: space, no_space Layout/SpaceAroundOperators: Exclude: - 'lib/mail_room/mailbox.rb' - 'spec/lib/arbitration/redis_spec.rb' # Offense count: 7 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces. # SupportedStyles: space, no_space # SupportedStylesForEmptyBraces: space, no_space Layout/SpaceBeforeBlockBraces: Exclude: - 'mail_room.gemspec' - 'spec/lib/crash_handler_spec.rb' - 'spec/lib/mailbox_spec.rb' # Offense count: 51 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters. # SupportedStyles: space, no_space # SupportedStylesForEmptyBraces: space, no_space Layout/SpaceInsideBlockBraces: Exclude: - 'lib/mail_room/coordinator.rb' - 'spec/lib/cli_spec.rb' - 'spec/lib/configuration_spec.rb' - 'spec/lib/delivery/letter_opener_spec.rb' - 'spec/lib/delivery/logger_spec.rb' - 'spec/lib/delivery/postback_spec.rb' - 'spec/lib/delivery/que_spec.rb' - 'spec/lib/imap/connection_spec.rb' - 'spec/lib/mailbox_watcher_spec.rb' # Offense count: 32 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces. # SupportedStyles: space, no_space, compact # SupportedStylesForEmptyBraces: space, no_space Layout/SpaceInsideHashLiteralBraces: Exclude: - 'lib/mail_room/mailbox.rb' - 'spec/lib/cli_spec.rb' - 'spec/lib/mailbox_spec.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: space, compact, no_space Layout/SpaceInsideParens: Exclude: - 'spec/lib/logger/structured_spec.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: final_newline, final_blank_line Layout/TrailingEmptyLines: Exclude: - 'spec/lib/delivery/letter_opener_spec.rb' # Offense count: 4 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowInHeredoc. Layout/TrailingWhitespace: Exclude: - 'lib/mail_room/coordinator.rb' - 'lib/mail_room/imap.rb' - 'spec/lib/coordinator_spec.rb' # Offense count: 5 # Configuration parameters: AllowedMethods. # AllowedMethods: enums Lint/ConstantDefinitionInBlock: Exclude: - 'lib/mail_room/mailbox.rb' # Offense count: 1 Lint/RescueException: Exclude: - 'lib/mail_room/cli.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods. Lint/UnusedMethodArgument: Exclude: - 'lib/mail_room/logger/structured.rb' # Offense count: 5 # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: Max: 27 # Offense count: 32 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. # AllowedMethods: refine Metrics/BlockLength: Max: 198 # Offense count: 3 # Configuration parameters: CountComments, CountAsOne. Metrics/ClassLength: Max: 169 # Offense count: 1 # Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/CyclomaticComplexity: Max: 8 # Offense count: 13 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: Max: 22 # Offense count: 1 # Configuration parameters: CountComments, CountAsOne. Metrics/ModuleLength: Max: 172 # Offense count: 1 Naming/AccessorMethodName: Exclude: - 'lib/mail_room/configuration.rb' # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). Security/YAMLLoad: Exclude: - 'lib/mail_room/configuration.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: prefer_alias, prefer_alias_method Style/Alias: Exclude: - 'lib/mail_room/coordinator.rb' # Offense count: 16 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, ProceduralMethods, FunctionalMethods, AllowedMethods, AllowedPatterns, AllowBracesOnProceduralOneLiners, BracesRequiredMethods. # SupportedStyles: line_count_based, semantic, braces_for_chaining, always_braces # ProceduralMethods: benchmark, bm, bmbm, create, each_with_object, measure, new, realtime, tap, with_object # FunctionalMethods: let, let!, subject, watch # AllowedMethods: lambda, proc, it Style/BlockDelimiters: Exclude: - 'spec/lib/arbitration/redis_spec.rb' - 'spec/lib/delivery/postback_spec.rb' - 'spec/lib/delivery/que_spec.rb' - 'spec/lib/delivery/sidekiq_spec.rb' # Offense count: 14 # Configuration parameters: AllowedConstants. Style/Documentation: Exclude: - 'spec/**/*' - 'test/**/*' - 'lib/mail_room.rb' - 'lib/mail_room/arbitration.rb' - 'lib/mail_room/arbitration/noop.rb' - 'lib/mail_room/arbitration/redis.rb' - 'lib/mail_room/connection.rb' - 'lib/mail_room/crash_handler.rb' - 'lib/mail_room/delivery.rb' - 'lib/mail_room/imap.rb' - 'lib/mail_room/imap/connection.rb' - 'lib/mail_room/imap/message.rb' - 'lib/mail_room/logger/structured.rb' - 'lib/mail_room/message.rb' - 'lib/mail_room/microsoft_graph.rb' - 'lib/mail_room/microsoft_graph/connection.rb' # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: compact, expanded Style/EmptyMethod: Exclude: - 'lib/mail_room/arbitration/noop.rb' - 'lib/mail_room/delivery/noop.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). Style/Encoding: Exclude: - 'mail_room.gemspec' # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). Style/ExpandPathArguments: Exclude: - 'mail_room.gemspec' - 'spec/spec_helper.rb' # Offense count: 39 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: always, always_true, never Style/FrozenStringLiteralComment: Enabled: false # Offense count: 5 # This cop supports unsafe autocorrection (--autocorrect-all). Style/GlobalStdStream: Exclude: - 'lib/mail_room/crash_handler.rb' - 'lib/mail_room/delivery/logger.rb' - 'lib/mail_room/mailbox.rb' - 'spec/lib/delivery/logger_spec.rb' # Offense count: 3 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. Style/GuardClause: Exclude: - 'lib/mail_room/configuration.rb' - 'lib/mail_room/imap/connection.rb' - 'lib/mail_room/mailbox_watcher.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. # SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys # SupportedShorthandSyntax: always, never, either, consistent Style/HashSyntax: Exclude: - 'spec/lib/microsoft_graph/connection_spec.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). Style/IfUnlessModifier: Exclude: - 'lib/mail_room/mailbox_watcher.rb' # Offense count: 5 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: literals, strict Style/MutableConstant: Exclude: - 'lib/mail_room/crash_handler.rb' - 'lib/mail_room/mailbox.rb' - 'lib/mail_room/version.rb' - 'spec/spec_helper.rb' # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle, AllowedMethods, AllowedPatterns. # SupportedStyles: predicate, comparison Style/NumericPredicate: Exclude: - 'spec/**/*' - 'lib/mail_room/imap/connection.rb' # Offense count: 2 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: short, verbose Style/PreferredHashMethods: Exclude: - 'lib/mail_room/configuration.rb' - 'lib/mail_room/mailbox.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, AllowedCompactTypes. # SupportedStyles: compact, exploded Style/RaiseArgs: Exclude: - 'lib/mail_room/logger/structured.rb' # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). Style/RedundantPercentQ: Exclude: - 'mail_room.gemspec' # Offense count: 7 # This cop supports safe autocorrection (--autocorrect). Style/RedundantSelf: Exclude: - 'lib/mail_room/configuration.rb' - 'lib/mail_room/coordinator.rb' - 'lib/mail_room/mailbox.rb' - 'lib/mail_room/mailbox_watcher.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: implicit, explicit Style/RescueStandardError: Exclude: - 'lib/mail_room/configuration.rb' # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. # AllowedMethods: present?, blank?, presence, try, try! Style/SafeNavigation: Exclude: - 'lib/mail_room/mailbox_watcher.rb' # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: RequireEnglish, EnforcedStyle. # SupportedStyles: use_perl_names, use_english_names, use_builtin_english_names Style/SpecialGlobalVars: Exclude: - 'mail_room.gemspec' # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: Mode. Style/StringConcatenation: Exclude: - 'lib/mail_room/logger/structured.rb' # Offense count: 142 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. # SupportedStyles: single_quotes, double_quotes Style/StringLiterals: Enabled: false # Offense count: 4 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, MinSize. # SupportedStyles: percent, brackets Style/SymbolArray: Exclude: - 'lib/mail_room/mailbox.rb' - 'spec/lib/logger/structured_spec.rb' # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyleForMultiline. # SupportedStylesForMultiline: comma, consistent_comma, no_comma Style/TrailingCommaInHashLiteral: Exclude: - 'spec/lib/mailbox_spec.rb' - 'spec/spec_helper.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). Style/WhileUntilDo: Exclude: - 'lib/mail_room/mailbox_watcher.rb' # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). Style/WhileUntilModifier: Exclude: - 'lib/mail_room/coordinator.rb' - 'lib/mail_room/mailbox_watcher.rb' # Offense count: 3 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: WordRegex. # SupportedStyles: percent, brackets Style/WordArray: EnforcedStyle: percent MinSize: 3 # Offense count: 7 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns. # URISchemes: http, https Layout/LineLength: Max: 177 gitlab-mail_room-0.0.24/Gemfile0000644000004100000410000000013614563474377016362 0ustar www-datawww-datasource 'https://rubygems.org' # Specify your gem's dependencies in mail_room.gemspec gemspec gitlab-mail_room-0.0.24/README.md0000644000004100000410000004550614563474377016360 0ustar www-datawww-data# mail_room # ## Fork notice `mail_room` contains some merged functionality that GitLab requires, so this mirror fork is to help us release custom functionality. It needs to be more or less kept up to date with the original, so please feel free to incorporate changes to the upstream repo if you see them. ### Rationale This fork is required to reduce dependency on the upstream releases. The [original JSON structured logging PR](https://github.com/tpitale/mail_room/pull/88) was [released](https://github.com/tpitale/mail_room/commit/deb8fe63bab21c5c3003346961a815d137ff6d2d) and we [bumped the version](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/3719) to incorporate it into [omnibus](https://gitlab.com/gitlab-org/omnibus-gitlab). It turned out that when Mailroom crashed out (which it's designed to do), the crash log [wasn't being pulled into elastic in a very useful way](https://github.com/tpitale/mail_room/commits/master) (that is, every line of the stack trace was a new event) so [another PR](https://github.com/tpitale/mail_room/pull/103) was raised. Rather than wait for the author (or bugging him more than once), we [opted for bias for action](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/19186#note_290758986) and made a fork of the gem. Here it is [in omnibus](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/3960). The fork is useful as we can post quick fixes to our own fork and release fixes quickly, and still contribute those fixes upstream to help others. ## README mail_room is a configuration based process that will listen for incoming e-mail and execute a delivery method when a new message is received. mail_room supports the following methods for receiving e-mail: * IMAP * [Microsoft Graph API](https://docs.microsoft.com/en-us/graph/api/resources/mail-api-overview?view=graph-rest-1.0) Examples of delivery methods include: * POST to a delivery URL (Postback) * Queue a job to Sidekiq or Que for later processing (Sidekiq or Que) * Log the message or open with LetterOpener (Logger or LetterOpener) [![Build Status](https://travis-ci.org/tpitale/mail_room.png?branch=master)](https://travis-ci.org/tpitale/mail_room) [![Code Climate](https://codeclimate.com/github/tpitale/mail_room/badges/gpa.svg)](https://codeclimate.com/github/tpitale/mail_room) ## Installation ## Add this line to your application's Gemfile: gem 'mail_room' And then execute: $ bundle Or install it yourself as: $ gem install mail_room You will also need to install `faraday` or `letter_opener` if you use the `postback` or `letter_opener` delivery methods, respectively. ## Usage ## mail_room -c /path/to/config.yml **Note:** To ignore missing config file or missing `mailboxes` key, use `-q` or `--quiet` ## Configuration ## ```yaml --- :health_check: :address: "127.0.0.1" :port: 8080 :mailboxes: - :email: "user1@gmail.com" :password: "password" :name: "inbox" :search_command: 'NEW' :logger: :log_path: /path/to/logfile/for/mailroom :delivery_options: :delivery_url: "http://localhost:3000/inbox" :delivery_token: "abcdefg" :content_type: "text/plain" - :email: "user2@gmail.com" :password: "password" :name: "inbox" :delivery_method: postback :delivery_options: :delivery_url: "http://localhost:3000/inbox" :delivery_token: "abcdefg" - :email: "user3@gmail.com" :password: "password" :name: "inbox" :delivery_method: logger :delivery_options: :log_path: "/var/log/user3-email.log" - :email: "user4@gmail.com" :password: "password" :name: "inbox" :delivery_method: letter_opener :delete_after_delivery: true :expunge_deleted: true :delivery_options: :location: "/tmp/user4-email" - :email: "user5@gmail.com" :password: "password" :name: "inbox" :delivery_method: sidekiq :delivery_options: :redis_url: redis://localhost:6379 :worker: EmailReceiverWorker - :email: "user6@gmail.com" :password: "password" :name: "inbox" :delivery_method: sidekiq :delivery_options: # When pointing to sentinel, follow this sintax for redis URLs: # redis://:@/ :redis_url: redis://:password@my-redis-sentinel/ :sentinels: - :host: 127.0.0.1 :port: 26379 :worker: EmailReceiverWorker - :email: "user7@outlook365.com" :password: "password" :name: "inbox" :inbox_method: microsoft_graph :inbox_options: :tenant_id: 12345 :client_id: ABCDE :client_secret: YOUR-SECRET-HERE :poll_interval: 60 :azure_ad_endpoint: https://login.microsoftonline.com :graph_endpoint: https://graph.microsoft.com :delivery_method: sidekiq :delivery_options: :redis_url: redis://localhost:6379 :worker: EmailReceiverWorker - :email: "user8@gmail.com" :password: "password" :name: "inbox" :delivery_method: postback :delivery_options: :delivery_url: "http://localhost:3000/inbox" :jwt_auth_header: "Mailroom-Api-Request" :jwt_issuer: "mailroom" :jwt_algorithm: "HS256" :jwt_secret_path: "/etc/secrets/mailroom/.mailroom_secret" ``` **Note:** If using `delete_after_delivery`, you also probably want to use `expunge_deleted` unless you really know what you're doing. ## health_check ## Requires `webrick` gem to be installed. This option enables an HTTP server that listens to a bind address defined by `address` and `port`. The following endpoints are supported: * `/liveness`: This returns a 200 status code with `OK` as the body if the server is running. Otherwise, it returns a 500 status code. This feature is not included in upstream `mail_room` and is specific to GitLab. ## inbox_method By default, IMAP mode is assumed for reading a mailbox. ### IMAP Server Configuration ## You can set per-mailbox configuration for the IMAP server's `host` (default: 'imap.gmail.com'), `port` (default: 993), `ssl` (default: true), and `start_tls` (default: false). If you want to set additional options for IMAP SSL you can pass a YAML hash to match [SSLContext#set_params](http://docs.ruby-lang.org/en/2.2.0/OpenSSL/SSL/SSLContext.html#method-i-set_params). If you set `verify_mode` to `:none` it'll replace with the appropriate constant. If you're seeing the error `Please log in via your web browser: https://support.google.com/mail/accounts/answer/78754 (Failure)`, you need to configure your Gmail account to allow less secure apps to access it: https://support.google.com/accounts/answer/6010255. ### Microsoft Graph configuration To use the Microsoft Graph API instead of IMAP to read e-mail, you will need to create an application in Microsoft Entra ID (formerly known as Azure Active Directory). See the [Microsoft instructions](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app) for more details: 1. Sign in to the [Azure portal](https://portal.azure.com). 1. Search for and select `Microsoft Entra ID`. 1. Under `Manage`, select `App registrations` > `New registration`. 1. Enter a `Name` for your application, such as `MailRoom`. Users of your app might see this name, and you can change it later. 1. If `Supported account types` is listed, select the appropriate option. 1. Leave `Redirect URI` blank. This is not needed. 1. Select `Register`. 1. Under `Manage`, select `Certificates & secrets`. 1. Under `Client secrets`, select `New client secret`, and enter a name. 1. Under `Expires`, select `Never`, unless you plan on updating the credentials every time it expires. 1. Select `Add`. Record the secret value in a safe location for use in a later step. 1. Under `Manage`, select `API Permissions` > `Add a permission`. Select `Microsoft Graph`. 1. Select `Application permissions`. 1. Under the `Mail` node, select `Mail.ReadWrite`, and then select Add permissions. 1. If `User.Read` is listed in the permission list, you can delete this. 1. Click `Grant admin consent` for these permissions. #### Restrict mailbox access Note that for MailRoom to work as a service account, this application must have the `Mail.ReadWrite` to read/write mail in *all* mailboxes. However, while this appears to be security risk, we can configure an application access policy to limit the mailbox access for this account. [Follow these instructions](https://docs.microsoft.com/en-us/graph/auth-limit-mailbox-access) to setup PowerShell and configure this policy. #### MailRoom config for Microsoft Graph In the MailRoom configuration, set `inbox_method` to `microsoft_graph`. You will also need: * The client and tenant ID from the `Overview` section in the Azure app page * The client secret created earlier Fill in `inbox_options` with these values: ```yaml :inbox_method: microsoft_graph :inbox_options: :tenant_id: 12345 :client_id: ABCDE :client_secret: YOUR-SECRET-HERE :poll_interval: 60 ``` By default, MailRoom will poll for new messages every 60 seconds. `poll_interval` configures the number of seconds to poll. Setting the value to 0 or under will default to 60 seconds. ### Alternative Azure cloud deployments MailRoom will default to using the standard Azure HTTPS endpoints. To configure MailRoom with Microsoft Cloud for US Government or other [national cloud deployments](https://docs.microsoft.com/en-us/graph/deployments), set the `azure_ad_endpoint` and `graph_endpoint` accordingly. For example, for Microsoft Cloud for US Government: ```yaml :inbox_method: microsoft_graph :inbox_options: :tenant_id: 12345 :client_id: ABCDE :client_secret: YOUR-SECRET-HERE :poll_interval: 60 :azure_ad_endpoint: https://login.microsoftonline.us :graph_endpoint: https://graph.microsoft.us ``` ## delivery_method ## ### postback ### Requires `faraday` gem be installed. *NOTE:* If you're using Ruby `>= 2.0`, you'll need to use Faraday from `>= 0.8.9`. Versions before this seem to have some weird behavior with `mail_room`. The default delivery method, requires `delivery_url` and `delivery_token` in configuration. You can pass `content_type:` option to overwrite `faraday's` default content-type(`application/x-www-form-urlencoded`) for post requests, we recommend passing `text/plain` as content-type. As the postback is essentially using your app as if it were an API endpoint, you may need to disable forgery protection as you would with a JSON API. ### sidekiq ### Deliver the message by pushing it onto the configured Sidekiq queue to be handled by a custom worker. Requires `redis` gem to be installed. Configured with `:delivery_method: sidekiq`. Delivery options: - **redis_url**: The Redis server to connect with. Use the same Redis URL that's used to configure Sidekiq. Required, defaults to `redis://localhost:6379`. - **sentinels**: A list of sentinels servers used to provide HA to Redis. (see [Sentinel Support](#sentinel-support)) Optional. - **namespace**: [DEPRECATED] The Redis namespace Sidekiq works under. Use the same Redis namespace that's used to configure Sidekiq. Optional. - **queue**: The Sidekiq queue the job is pushed onto. Make sure Sidekiq actually reads off this queue. Required, defaults to `default`. - **worker**: The worker class that will handle the message. Required. An example worker implementation looks like this: ```ruby class EmailReceiverWorker include Sidekiq::Worker def perform(message) mail = Mail::Message.new(message) puts "New mail from #{mail.from.first}: #{mail.subject}" end end ``` ### que ### Deliver the message by pushing it onto the configured Que queue to be handled by a custom worker. Requires `pg` gem to be installed. Configured with `:delivery_method: que`. Delivery options: - **host**: The postgresql server host to connect with. Use the database you use with Que. Required, defaults to `localhost`. - **port**: The postgresql server port to connect with. Use the database you use with Que. Required, defaults to `5432`. - **database**: The postgresql database to use. Use the database you use with Que. Required. - **queue**: The Que queue the job is pushed onto. Make sure Que actually reads off this queue. Required, defaults to `default`. - **job_class**: The worker class that will handle the message. Required. - **priority**: The priority you want this job to run at. Required, defaults to `100`, lowest Que default priority. An example worker implementation looks like this: ```ruby class EmailReceiverJob < Que::Job def run(message) mail = Mail::Message.new(message) puts "New mail from #{mail.from.first}: #{mail.subject}" end end ``` ### logger ### Configured with `:delivery_method: logger`. If the `:log_path:` delivery option is not provided, defaults to `STDOUT` ### noop ### Configured with `:delivery_method: noop`. Does nothing, like it says. ### letter_opener ### Requires `letter_opener` gem be installed. Configured with `:delivery_method: letter_opener`. Uses Ryan Bates' excellent [letter_opener](https://github.com/ryanb/letter_opener) gem. ## ActionMailbox in Rails ## MailRoom can deliver mail to Rails using the ActionMailbox [configuration options for an SMTP relay](https://edgeguides.rubyonrails.org/action_mailbox_basics.html#configuration). In summary (from the ActionMailbox docs) 1. Configure Rails to use the `:relay` ingress option: ```rb # config/environments/production.rb config.action_mailbox.ingress = :relay ``` 2. Generate a strong password (e.g., using SecureRandom or something) and add it to Rails config: using `rails credentials:edit` under `action_mailbox.ingress_password`. And finally, configure MailRoom to use the postback configuration with the options: ```yaml :delivery_method: postback :delivery_options: :delivery_url: https://example.com/rails/action_mailbox/relay/inbound_emails :username: actionmailbox :password: ``` ## Receiving `postback` in Rails ## If you have a controller that you're sending to, with forgery protection disabled, you can get the raw string of the email using `request.body.read`. I would recommend having the `mail` gem bundled and parse the email using `Mail.read_from_string(request.body.read)`. *Note:* If you get the exception (`Rack::QueryParser::InvalidParameterError (invalid %-encoding...`) it's probably because the content-type is set to Faraday's default, which is `HEADERS['content-type'] = 'application/x-www-form-urlencoded'`. It can cause `Rack` to crash due to `InvalidParameterError` exception. When you send a post with `application/x-www-form-urlencoded`, `Rack` will attempt to parse the input and can end up raising an exception, for example if the email that you are forwarding contain `%%` in its content or headers it will cause Rack to crash with the message above. ## idle_timeout ## By default, the IDLE command will wait for 29 minutes (in order to keep the server connection happy). If you'd prefer not to wait that long, you can pass `idle_timeout` in seconds for your mailbox configuration. ## Search Command ## This setting allows configuration of the IMAP search command sent to the server. This still defaults 'UNSEEN'. You may find that 'NEW' works better for you. ## Running in Production ## I suggest running with either upstart or init.d. Check out this wiki page for some example scripts for both: https://github.com/tpitale/mail_room/wiki/Init-Scripts-for-Running-mail_room ## Arbitration ## When running multiple instances of MailRoom against a single mailbox, to try to prevent delivery of the same message multiple times, we can configure Arbitration using Redis. ```yaml :mailboxes: - :email: "user1@gmail.com" :password: "password" :name: "inbox" :delivery_method: postback :delivery_options: :delivery_url: "http://localhost:3000/inbox" :delivery_token: "abcdefg" :arbitration_method: redis :arbitration_options: # The Redis server to connect with. Defaults to redis://localhost:6379. :redis_url: redis://redis.example.com:6379 # [DEPRECATED] The Redis namespace to house the Redis keys under. Optional. :namespace: mail_room - :email: "user2@gmail.com" :password: "password" :name: "inbox" :delivery_method: postback :delivery_options: :delivery_url: "http://localhost:3000/inbox" :delivery_token: "abcdefg" :arbitration_method: redis :arbitration_options: # When pointing to sentinel, follow this sintax for redis URLs: # redis://:@/ :redis_url: redis://:password@my-redis-sentinel/ :sentinels: - :host: 127.0.0.1 :port: 26379 # [DEPRECATED] The Redis namespace to house the Redis keys under. Optional. :namespace: mail_room ``` **Note:** This will likely never be a _perfect_ system for preventing multiple deliveries of the same message, so I would advise checking the unique `message_id` if you are running in this situation. **Note:** There are other scenarios for preventing duplication of messages at scale that _may_ be more appropriate in your particular setup. One such example is using multiple inboxes in reply-by-email situations. Another is to use labels and configure a different `SEARCH` command for each instance of MailRoom. ## Sentinel Support Redis Sentinel provides high availability for Redis. Please read their [documentation](http://redis.io/topics/sentinel) first, before enabling it with mail_room. To connect to a Sentinel, you need to setup authentication to both sentinels and redis daemons first, and make sure both are binding to a reachable IP address. In mail_room, when you are connecting to a Sentinel, you have to inform the `master-name` and the `password` through `redis_url` param, following this syntax: ``` redis://:@/ ``` You also have to inform at least one pair of `host` and `port` for a sentinel in your cluster. To have a minimum reliable setup, you need at least `3` sentinel nodes and `3` redis servers (1 master, 2 slaves). ## Logging ## MailRoom will output JSON-formatted logs to give some observability into its operations. Simply configure a `log_path` for the `logger` on any of your mailboxes. By default, nothing will be logged. If you wish to log to `STDOUT` or `STDERR` instead of a file, you can pass `:stdout` or `:stderr`, respectively and MailRoom will log there. ## Contributing ## 1. Fork it 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create new Pull Request 6. If accepted, ask for commit rights ## TODO ## 1. specs, this is just a (working) proof of concept √ 2. finish code for POSTing to callback with auth √ 3. accept mailbox configuration for one account directly on the commandline; or ask for it 4. add example rails endpoint, with auth examples 5. add example configs for upstart/init.d √ 6. log to stdout √ 7. add a development mode that opens in letter_opener by ryanb √ gitlab-mail_room-0.0.24/.gitlab/0000755000004100000410000000000014563474377016407 5ustar www-datawww-datagitlab-mail_room-0.0.24/.gitlab/issue_templates/0000755000004100000410000000000014563474377021615 5ustar www-datawww-datagitlab-mail_room-0.0.24/.gitlab/issue_templates/Default.md0000644000004100000410000000106514563474377023525 0ustar www-datawww-dataPlease read me! This repository supports our packaged release of `mail_room` that ships with GitLab. We strive to diverge as little as possible from the canonical upstream at https://www.github.com/tpitale/mail_room. If we do diverge, our patches are sent upstream for review and we carry them here to support the GitLab application. You can raise an issue here if you're having an issue *specifically in the context of a GitLab installation*. Otherwise, please direct any issues upstream to https://www.github.com/tpitale/mail_room. Thank you for contributing!gitlab-mail_room-0.0.24/.gitlab/issue_templates/Release.md0000644000004100000410000000155414563474377023524 0ustar www-datawww-data# GitLab mail_room release checklist - [ ] create tag in https://gitlab.com/gitlab-org/gitlab-mail_room/ - [ ] publish gem from this tag to rubygems.org - [ ] update https://gitlab.com/gitlab-org/gitlab/-/blob/master/Gemfile to use the new gem version - [ ] update https://gitlab.com/gitlab-org/omnibus-gitlab/-/blob/master/config/software/mail_room.rb to use the new gem version - [ ] update gitlab-org/build/CNG to build container images from the new gem (example: https://gitlab.com/gitlab-org/build/CNG/-/merge_requests/451/diffs) - [ ] to deploy the new version to gitlab.com, update gitlab-com/gl-infra/k8s-workloads/gitlab-com to pin the new mailroom container image version and assign it the [release managers](https://about.gitlab.com/community/release-managers/) (example: https://gitlab.com/gitlab-com/gl-infra/k8s-workloads/gitlab-com/-/merge_requests/236/diffs) gitlab-mail_room-0.0.24/.ruby-version0000644000004100000410000000000614563474377017530 0ustar www-datawww-data2.7.5 gitlab-mail_room-0.0.24/.rubocop.yml0000644000004100000410000000021314563474377017335 0ustar www-datawww-datainherit_from: .rubocop_todo.yml Style/HashSyntax: Enabled: true EnforcedStyle: ruby19_no_mixed_keys EnforcedShorthandSyntax: either gitlab-mail_room-0.0.24/CHANGELOG.md0000644000004100000410000000535414563474377016707 0ustar www-datawww-data## mail_room 0.10.1 ## * Fix db attribute on redis URL PR#130 - @jarkaK ## mail_room 0.10.0 ## * Remove imap backports * Increase minimum ruby version to 2.3 * Postback basic_auth support - PR#92 * Docs for ActionMailbox - PR#92 * Configuration option for delivery_klass - PR#93 * Expunge deleted - PR#90 * Raise error on a few fields of missing configuration - PR#89 * Remove fakeredis gem - PR#87 *Tony Pitale <@tpitale>* * Fix redis arbitration to use NX+EX - PR#86 *Craig Miskell <@craigmiskell-gitlab>* * Structured (JSON) logger - PR#88 *charlie <@cablett>* ## mail_room 0.9.1 ## * __FILE__ support in yml ERb config - PR#80 *Gabriel Mazetto <@brodock>* ## mail_room 0.9.0 ## * Redis Sentinel configuration support - PR#79 *Gabriel Mazetto <@brodock>* ## mail_room 0.8.1 ## * Check watching thread exists before joining - PR#78 *Michal Galet <@galet>* ## mail_room 0.8.0 ## * Rework the mailbox watcher and handler into a new Connection class to abstract away IMAP handling details *Tony Pitale <@tpitale>* ## mail_room 0.7.0 ## * Backports idle timeout from ruby 2.3.0 * Sets default to 29 minutes to prevent IMAP disconnects * Validates that the timeout does not exceed 29 minutes *Tony Pitale <@tpitale>* ## mail_room 0.6.1 ## * ERB parsing of configuration yml file to enable using ENV variables *Douwe Maan <@DouweM>* ## mail_room 0.6.0 ## * Add redis Arbitration to reduce multiple deliveries of the same message when running multiple MailRoom instances on the same inbox *Douwe Maan <@DouweM>* ## mail_room 0.5.2 ## * Fix Sidekiq delivery method for non-UTF8 email *Douwe Maan <@DouweM>* * Add StartTLS session support *Tony Pitale <@tpitale>* ## mail_room 0.5.1 ## * Re-idle after 29 minutes to maintain IDLE connection *Douwe Maan <@DouweM>* ## mail_room 0.5.0 ## * Que delivery method *Tony Pitale <@tpitale>* ## mail_room 0.4.2 ## * rescue from all IOErrors, not just EOFError *Douwe Maan <@DouweM>* ## mail_room 0.4.1 ## * Fix redis default host/port configuration * Mailbox does not attempt delivery without a message *Douwe Maan <@DouweM>* ## mail_room 0.4.0 ## * Sidekiq delivery method * Option to delete messages after delivered *Douwe Maan <@DouweM>* * -q/--quiet do not raise errors on missing configuration * prefetch mail messages before idling * delivery-method-specific delivery options configuration *Tony Pitale <@tpitale>* ## mail_room 0.3.1 ## * Rescue from EOFError and re-setup mailroom *Tony Pitale <@tpitale>* ## mail_room 0.3.0 ## * Reconnect and idle if disconnected during an existing idle. * Set idling thread to abort on exception so any unhandled exceptions will stop mail_room running. *Tony Pitale <@tpitale>*