Commit 4c91ee72 authored by Jan Provaznik's avatar Jan Provaznik Committed by Stan Hu

Allow to listen on multiple email addresses

This is the first step toward having a separate incoming email
address for service desk. It allows to configure mail_room to
listen on multiple email addresses (but we don't add
`service_desk_email` address into config file yet).
parent b9765bd3
:mailboxes: :mailboxes:
<% <%
require_relative "../lib/gitlab/mail_room" unless defined?(Gitlab::MailRoom) require_relative "../lib/gitlab/mail_room" unless defined?(Gitlab::MailRoom)
config = Gitlab::MailRoom.config Gitlab::MailRoom.enabled_configs.each do |config|
if Gitlab::MailRoom.enabled?
%> %>
- -
:host: <%= config[:host].to_json %> :host: <%= config[:host].to_json %>
...@@ -24,8 +22,8 @@ ...@@ -24,8 +22,8 @@
:delivery_options: :delivery_options:
:redis_url: <%= config[:redis_url].to_json %> :redis_url: <%= config[:redis_url].to_json %>
:namespace: <%= Gitlab::Redis::Queues::SIDEKIQ_NAMESPACE %> :namespace: <%= Gitlab::Redis::Queues::SIDEKIQ_NAMESPACE %>
:queue: email_receiver :queue: <%= config[:queue] %>
:worker: EmailReceiverWorker :worker: <%= config[:worker] %>
<% if config[:sentinels] %> <% if config[:sentinels] %>
:sentinels: :sentinels:
<% config[:sentinels].each do |sentinel| %> <% config[:sentinels].each do |sentinel| %>
......
...@@ -224,6 +224,8 @@ ...@@ -224,6 +224,8 @@
- 2 - 2
- - self_monitoring_project_delete - - self_monitoring_project_delete
- 2 - 2
- - service_desk_email_receiver
- 1
- - system_hook_push - - system_hook_push
- 1 - 1
- - todos_destroyer - - todos_destroyer
......
...@@ -465,3 +465,9 @@ ...@@ -465,3 +465,9 @@
:latency_sensitive: :latency_sensitive:
:resource_boundary: :unknown :resource_boundary: :unknown
:weight: 1 :weight: 1
- :name: service_desk_email_receiver
:feature_category: :issue_tracking
:has_external_dependencies:
:latency_sensitive:
:resource_boundary: :unknown
:weight: 1
# frozen_string_literal: true
class ServiceDeskEmailReceiverWorker < EmailReceiverWorker
include ApplicationWorker
def perform(raw)
return unless service_desk_enabled?
raise NotImplementedError
end
private
def service_desk_enabled?
!!config&.enabled && Feature.enabled?(:service_desk_email)
end
def config
@config ||= Gitlab.config.service_desk_email
end
end
# frozen_string_literal: true
require "spec_helper"
describe ServiceDeskEmailReceiverWorker, :mailer do
describe '#perform' do
let(:worker) { described_class.new }
let(:email) { 'foo' }
context 'when service_desk_email config is enabled' do
before do
allow(worker).to receive(:config)
.and_return(double(enabled: true, address: 'foo'))
end
context 'when service_desk_email feature is enabled' do
before do
stub_feature_flags(service_desk_email: true)
end
it 'does not ignore the email' do
expect { worker.perform(email) }.to raise_error(NotImplementedError)
end
end
context 'when service_desk_email feature is disabled' do
before do
stub_feature_flags(service_desk_email: false)
end
it 'ignores the email' do
expect { worker.perform(email) }.not_to raise_error(NotImplementedError)
end
end
end
context 'when service_desk_email config is disabled' do
before do
allow(worker).to receive(:config)
.and_return(double(enabled: false, address: 'foo'))
end
it 'ignores the email' do
expect { worker.perform(email) }.not_to raise_error(NotImplementedError)
end
end
end
end
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
require 'yaml' require 'yaml'
require 'json' require 'json'
require 'pathname'
require_relative 'redis/queues' unless defined?(Gitlab::Redis::Queues) require_relative 'redis/queues' unless defined?(Gitlab::Redis::Queues)
# This service is run independently of the main Rails process, # This service is run independently of the main Rails process,
...@@ -21,39 +22,60 @@ module Gitlab ...@@ -21,39 +22,60 @@ module Gitlab
log_path: RAILS_ROOT_DIR.join('log', 'mail_room_json.log') log_path: RAILS_ROOT_DIR.join('log', 'mail_room_json.log')
}.freeze }.freeze
# Email specific configuration which is merged with configuration
# fetched from YML config file.
ADDRESS_SPECIFIC_CONFIG = {
incoming_email: {
queue: 'email_receiver',
worker: 'EmailReceiverWorker'
},
service_desk_email: {
queue: 'service_desk_email_receiver',
worker: 'ServiceDeskEmailReceiverWorker'
}
}.freeze
class << self class << self
def enabled? def enabled_configs
config[:enabled] && config[:address] @enabled_configs ||= configs.select { |config| enabled?(config) }
end end
def config private
@config ||= fetch_config
end
def reset_config! def enabled?(config)
@config = nil config[:enabled] && !config[:address].to_s.empty?
end end
private def configs
ADDRESS_SPECIFIC_CONFIG.keys.map { |key| fetch_config(key) }
end
def fetch_config def fetch_config(config_key)
return {} unless File.exist?(config_file) return {} unless File.exist?(config_file)
config = load_from_yaml || {} config = merged_configs(config_key)
config = DEFAULT_CONFIG.merge(config) do |_key, oldval, newval| config.merge!(redis_config) if enabled?(config)
config[:log_path] = File.expand_path(config[:log_path], RAILS_ROOT_DIR)
config
end
def merged_configs(config_key)
yml_config = load_yaml.fetch(config_key, {})
specific_config = ADDRESS_SPECIFIC_CONFIG.fetch(config_key, {})
DEFAULT_CONFIG.merge(specific_config, yml_config) do |_key, oldval, newval|
newval.nil? ? oldval : newval newval.nil? ? oldval : newval
end end
end
if config[:enabled] && config[:address] def redis_config
gitlab_redis_queues = Gitlab::Redis::Queues.new(rails_env) gitlab_redis_queues = Gitlab::Redis::Queues.new(rails_env)
config[:redis_url] = gitlab_redis_queues.url config = { redis_url: gitlab_redis_queues.url }
if gitlab_redis_queues.sentinels? if gitlab_redis_queues.sentinels?
config[:sentinels] = gitlab_redis_queues.sentinels config[:sentinels] = gitlab_redis_queues.sentinels
end end
end
config[:log_path] = File.expand_path(config[:log_path], RAILS_ROOT_DIR)
config config
end end
...@@ -65,8 +87,8 @@ module Gitlab ...@@ -65,8 +87,8 @@ module Gitlab
ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] || File.expand_path('../../config/gitlab.yml', __dir__) ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] || File.expand_path('../../config/gitlab.yml', __dir__)
end end
def load_from_yaml def load_yaml
YAML.load_file(config_file)[rails_env].deep_symbolize_keys[:incoming_email] @yaml ||= YAML.load_file(config_file)[rails_env].deep_symbolize_keys
end end
end end
end end
......
...@@ -39,39 +39,31 @@ describe 'mail_room.yml' do ...@@ -39,39 +39,31 @@ describe 'mail_room.yml' do
end end
end end
context 'when incoming email is enabled' do context 'when both incoming email and service desk email are enabled' do
let(:gitlab_config_path) { 'spec/fixtures/config/mail_room_enabled.yml' } let(:gitlab_config_path) { 'spec/fixtures/config/mail_room_enabled.yml' }
let(:queues_config_path) { 'spec/fixtures/config/redis_queues_new_format_host.yml' } let(:queues_config_path) { 'spec/fixtures/config/redis_queues_new_format_host.yml' }
let(:gitlab_redis_queues) { Gitlab::Redis::Queues.new(Rails.env) } let(:gitlab_redis_queues) { Gitlab::Redis::Queues.new(Rails.env) }
it 'contains the intended configuration' do it 'contains the intended configuration' do
expect(configuration[:mailboxes].length).to eq(1) expected_mailbox = {
mailbox = configuration[:mailboxes].first host: 'imap.gmail.com',
port: 993,
expect(mailbox[:host]).to eq('imap.gmail.com') ssl: true,
expect(mailbox[:port]).to eq(993) start_tls: false,
expect(mailbox[:ssl]).to eq(true) email: 'gitlab-incoming@gmail.com',
expect(mailbox[:start_tls]).to eq(false) password: '[REDACTED]',
expect(mailbox[:email]).to eq('gitlab-incoming@gmail.com') name: 'inbox',
expect(mailbox[:password]).to eq('[REDACTED]') idle_timeout: 60
expect(mailbox[:name]).to eq('inbox') }
expect(mailbox[:idle_timeout]).to eq(60) expected_options = {
redis_url: gitlab_redis_queues.url,
redis_url = gitlab_redis_queues.url sentinels: gitlab_redis_queues.sentinels
sentinels = gitlab_redis_queues.sentinels }
expect(mailbox[:delivery_options][:redis_url]).to be_present
expect(mailbox[:delivery_options][:redis_url]).to eq(redis_url)
expect(mailbox[:delivery_options][:sentinels]).to be_present
expect(mailbox[:delivery_options][:sentinels]).to eq(sentinels)
expect(mailbox[:arbitration_options][:redis_url]).to be_present
expect(mailbox[:arbitration_options][:redis_url]).to eq(redis_url)
expect(mailbox[:arbitration_options][:sentinels]).to be_present expect(configuration[:mailboxes].length).to eq(2)
expect(mailbox[:arbitration_options][:sentinels]).to eq(sentinels) expect(configuration[:mailboxes]).to all(include(expected_mailbox))
expect(configuration[:mailboxes].map { |m| m[:delivery_options] }).to all(include(expected_options))
expect(configuration[:mailboxes].map { |m| m[:arbitration_options] }).to all(include(expected_options))
end end
end end
......
...@@ -9,3 +9,14 @@ test: ...@@ -9,3 +9,14 @@ test:
ssl: true ssl: true
start_tls: false start_tls: false
mailbox: "inbox" mailbox: "inbox"
service_desk_email:
enabled: false
address: "gitlab-incoming+%{key}@gmail.com"
user: "gitlab-incoming@gmail.com"
password: "[REDACTED]"
host: "imap.gmail.com"
port: 993
ssl: true
start_tls: false
mailbox: "inbox"
...@@ -9,3 +9,14 @@ test: ...@@ -9,3 +9,14 @@ test:
ssl: true ssl: true
start_tls: false start_tls: false
mailbox: "inbox" mailbox: "inbox"
service_desk_email:
enabled: true
address: "gitlab-incoming+%{key}@gmail.com"
user: "gitlab-incoming@gmail.com"
password: "[REDACTED]"
host: "imap.gmail.com"
port: 993
ssl: true
start_tls: false
mailbox: "inbox"
...@@ -4,9 +4,10 @@ require 'spec_helper' ...@@ -4,9 +4,10 @@ require 'spec_helper'
describe Gitlab::MailRoom do describe Gitlab::MailRoom do
let(:default_port) { 143 } let(:default_port) { 143 }
let(:default_config) do let(:yml_config) do
{ {
enabled: false, enabled: true,
address: 'address@example.com',
port: default_port, port: default_port,
ssl: false, ssl: false,
start_tls: false, start_tls: false,
...@@ -16,71 +17,73 @@ describe Gitlab::MailRoom do ...@@ -16,71 +17,73 @@ describe Gitlab::MailRoom do
} }
end end
shared_examples_for 'only truthy if both enabled and address are truthy' do |target_proc| let(:custom_config) { {} }
context 'with both enabled and address as truthy values' do let(:incoming_email_config) { yml_config.merge(custom_config) }
it 'is truthy' do let(:service_desk_email_config) { yml_config.merge(custom_config) }
stub_config(enabled: true, address: 'localhost')
expect(target_proc.call).to be_truthy let(:configs) do
end {
incoming_email: incoming_email_config,
service_desk_email: service_desk_email_config
}
end end
context 'with address only as truthy' do before do
it 'is falsey' do described_class.instance_variable_set(:@enabled_configs, nil)
stub_config(enabled: false, address: 'localhost')
expect(target_proc.call).to be_falsey
end
end end
context 'with enabled only as truthy' do describe '#enabled_configs' do
it 'is falsey' do before do
stub_config(enabled: true, address: nil) allow(described_class).to receive(:load_yaml).and_return(configs)
expect(target_proc.call).to be_falsey
end
end end
context 'with neither address nor enabled as truthy' do context 'when both email and address is set' do
it 'is falsey' do it 'returns email configs' do
stub_config(enabled: false, address: nil) expect(described_class.enabled_configs.size).to eq(2)
expect(target_proc.call).to be_falsey
end
end end
end end
context 'when the yml file cannot be found' do
before do before do
described_class.reset_config! allow(described_class).to receive(:config_file).and_return('not_existing_file')
allow(File).to receive(:exist?).and_return true
end end
describe '#config' do it 'returns an empty list' do
context 'if the yml file cannot be found' do expect(described_class.enabled_configs).to be_empty
before do
allow(File).to receive(:exist?).and_return false
end
it 'returns an empty hash' do
expect(described_class.config).to be_empty
end end
end end
before do context 'when email is disabled' do
allow(described_class).to receive(:load_from_yaml).and_return(default_config) let(:custom_config) { { enabled: false } }
it 'returns an empty list' do
expect(described_class.enabled_configs).to be_empty
end
end end
it 'sets up config properly' do context 'when email is enabled but address is not set' do
expected_result = default_config let(:custom_config) { { enabled: true, address: '' } }
expect(described_class.config).to match expected_result it 'returns an empty list' do
expect(described_class.enabled_configs).to be_empty
end
end end
context 'when a config value is missing from the yml file' do context 'when a config value is missing from the yml file' do
let(:yml_config) { {} }
let(:custom_config) { { enabled: true, address: 'address@example.com' } }
it 'overwrites missing values with the default' do it 'overwrites missing values with the default' do
stub_config(port: nil) expect(described_class.enabled_configs.first[:port]).to eq(Gitlab::MailRoom::DEFAULT_CONFIG[:port])
end
end
context 'when only incoming_email config is present' do
let(:configs) { { incoming_email: incoming_email_config } }
expect(described_class.config[:port]).to eq default_port it 'returns only encoming_email' do
expect(described_class.enabled_configs.size).to eq(1)
expect(described_class.enabled_configs.first[:worker]).to eq('EmailReceiverWorker')
end end
end end
...@@ -91,50 +94,31 @@ describe Gitlab::MailRoom do ...@@ -91,50 +94,31 @@ describe Gitlab::MailRoom do
allow(Gitlab::Redis::Queues).to receive(:new).and_return(fake_redis_queues) allow(Gitlab::Redis::Queues).to receive(:new).and_return(fake_redis_queues)
end end
target_proc = proc { described_class.config[:redis_url] } it 'sets redis config' do
config = described_class.enabled_configs.first
it_behaves_like 'only truthy if both enabled and address are truthy', target_proc expect(config[:redis_url]).to eq('localhost')
expect(config[:sentinels]).to eq('yes, them')
end
end end
describe 'setting up the log path' do describe 'setting up the log path' do
context 'if the log path is a relative path' do context 'if the log path is a relative path' do
it 'expands the log path to an absolute value' do let(:custom_config) { { log_path: 'tiny_log.log' } }
stub_config(log_path: 'tiny_log.log')
new_path = Pathname.new(described_class.config[:log_path]) it 'expands the log path to an absolute value' do
new_path = Pathname.new(described_class.enabled_configs.first[:log_path])
expect(new_path.absolute?).to be_truthy expect(new_path.absolute?).to be_truthy
end end
end end
context 'if the log path is absolute path' do context 'if the log path is absolute path' do
it 'leaves the path as-is' do let(:custom_config) { { log_path: '/dev/null' } }
new_path = '/dev/null'
stub_config(log_path: new_path)
expect(described_class.config[:log_path]).to eq new_path
end
end
end
end
describe '#enabled?' do it 'leaves the path as-is' do
target_proc = proc { described_class.enabled? } expect(described_class.enabled_configs.first[:log_path]).to eq '/dev/null'
it_behaves_like 'only truthy if both enabled and address are truthy', target_proc
end end
describe '#reset_config?' do
it 'resets config' do
described_class.instance_variable_set(:@config, { some_stuff: 'hooray' })
described_class.reset_config!
expect(described_class.instance_variable_get(:@config)).to be_nil
end end
end end
def stub_config(override_values)
modified_config = default_config.merge(override_values)
allow(described_class).to receive(:load_from_yaml).and_return(modified_config)
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment