Commit 9f218fc1 authored by Rémy Coutable's avatar Rémy Coutable

Improve and finish the fallback to the In-Reply-To and References header for...

Improve and finish the fallback to the In-Reply-To and References header for the reply-by-email feature

A few things to note:
- The IncomingEmail feature is now enabled even without a
  correctly-formatted sub-address
- Message-ID for new thread mail are kept the same so that subsequent
  notifications to this thread are grouped in the thread by the email
  service that receives the notification
  (i.e. In-Reply-To of the answer == Message-ID of the first thread message)
- To maximize our chance to be able to retrieve the reply key, we look
  for it in the In-Reply-To header and the References header
- The pattern for the fallback reply message id is "reply-[key]@[gitlab_host]"
- Improve docs thanks to Axil
parent 31e76baf
...@@ -46,6 +46,7 @@ v 8.6.0 ...@@ -46,6 +46,7 @@ v 8.6.0
- Fix wiki search results point to raw source (Hiroyuki Sato) - Fix wiki search results point to raw source (Hiroyuki Sato)
- Don't load all of GitLab in mail_room - Don't load all of GitLab in mail_room
- Add information about `image` and `services` field at `job` level in the `.gitlab-ci.yml` documentation (Pat Turner) - Add information about `image` and `services` field at `job` level in the `.gitlab-ci.yml` documentation (Pat Turner)
- Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla)
- HTTP error pages work independently from location and config (Artem Sidorenko) - HTTP error pages work independently from location and config (Artem Sidorenko)
- Update `omniauth-saml` to 1.5.0 to allow for custom response attributes to be set - Update `omniauth-saml` to 1.5.0 to allow for custom response attributes to be set
- Memoize @group in Admin::GroupsController (Yatish Mehta) - Memoize @group in Admin::GroupsController (Yatish Mehta)
......
...@@ -110,6 +110,10 @@ class Notify < BaseMailer ...@@ -110,6 +110,10 @@ class Notify < BaseMailer
headers['Reply-To'] = address headers['Reply-To'] = address
fallback_reply_message_id = "<reply-#{reply_key}@#{Gitlab.config.gitlab.host}>".freeze
headers['References'] ||= ''
headers['References'] << ' ' << fallback_reply_message_id
@reply_by_email = true @reply_by_email = true
end end
...@@ -121,17 +125,11 @@ class Notify < BaseMailer ...@@ -121,17 +125,11 @@ class Notify < BaseMailer
# #
# See: mail_answer_thread # See: mail_answer_thread
def mail_new_thread(model, headers = {}) def mail_new_thread(model, headers = {})
headers['Message-ID'] = message_reply_id headers['Message-ID'] = message_id(model)
headers['In-Reply-To'] = message_id(model)
headers['References'] = message_id(model)
mail_thread(model, headers) mail_thread(model, headers)
end end
def message_reply_id
Gitlab.config.incoming_email["address"].gsub("%{key}", reply_key)
end
# Send an email that responds to an existing conversation thread, # Send an email that responds to an existing conversation thread,
# with headers suitable for grouping by thread in email clients. # with headers suitable for grouping by thread in email clients.
# #
......
...@@ -106,7 +106,7 @@ production: &base ...@@ -106,7 +106,7 @@ production: &base
enabled: false enabled: false
# The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
# The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`. # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
address: "gitlab-incoming+%{key}@gmail.com" address: "gitlab-incoming+%{key}@gmail.com"
# Email account username # Email account username
......
...@@ -17,7 +17,7 @@ if File.exists?(config_file) ...@@ -17,7 +17,7 @@ if File.exists?(config_file)
config['start_tls'] = false if config['start_tls'].nil? config['start_tls'] = false if config['start_tls'].nil?
config['mailbox'] = "inbox" if config['mailbox'].nil? config['mailbox'] = "inbox" if config['mailbox'].nil?
if config['enabled'] && config['address'] && config['address'].include?('%{key}') if config['enabled'] && config['address']
redis_url = Gitlab::RedisConfig.new(rails_env).url redis_url = Gitlab::RedisConfig.new(rails_env).url
%> %>
- -
......
# Reply by email # Reply by email
GitLab can be set up to allow users to comment on issues and merge requests by replying to notification emails. GitLab can be set up to allow users to comment on issues and merge requests by
replying to notification emails.
## Get a mailbox ## Requirement
Reply by email requires an IMAP-enabled email account, with a provider or server that supports [email sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing). Sub-addressing is a feature where any email to `user+some_arbitrary_tag@example.com` will end up in the mailbox for `user@example.com`, and is supported by providers such as Gmail, Google Apps, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix mail server which you can run on-premises. Reply by email requires an IMAP-enabled email account. GitLab allows you to use
three strategies for this feature:
- using email sub-addressing
- using a dedicated email address
- using a catch-all mailbox
If you want to use Gmail / Google Apps with Reply by email, make sure you have [IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018) and [allow less secure apps to access the account](https://support.google.com/accounts/answer/6010255). ### Email sub-addressing
To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these instructions](./postfix.md). **If your provider or server supports email sub-addressing, we recommend using it.**
[Sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing) is
a feature where any email to `user+some_arbitrary_tag@example.com` will end up
in the mailbox for `user@example.com`, and is supported by providers such as
Gmail, Google Apps, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix
mail server which you can run on-premises.
### Dedicated email address
This solution is really simple to set up: you just have to create an email
address dedicated to receive your users' replies to GitLab notifications.
### Catch-all mailbox
A [catch-all mailbox](https://en.wikipedia.org/wiki/Catch-all) for a domain will
"catch all" the emails addressed to the domain that do not exist in the mail
server.
## How it works?
### 1. GitLab sends a notification email
When GitLab sends a notification and Reply by email is enabled, the `Reply-To`
header is set to the address defined in your GitLab configuration, with the
`%{key}` placeholder (if present) replaced by a specific "reply key". In
addition, this "reply key" is also added to the `References` header.
### 2. You reply to the notification email
When you reply to the notification email, your email client will:
- send the email to the `Reply-To` address it got from the notification email
- set the `In-Reply-To` header to the value of the `Message-ID` header from the
notification email
- set the `References` header to the value of the `Message-ID` plus the value of
the notification email's `References` header.
### 3. GitLab receives your reply to the notification email
When GitLab receives your reply, it will look for the "reply key" in the
following headers, in this order:
1. the `To` header
1. the `References` header
If it finds a reply key, it will be able to leave your reply as a comment on
the entity the notification was about (issue, merge request, commit...).
For more details about the `Message-ID`, `In-Reply-To`, and `References headers`,
please consult [RFC 5322](https://tools.ietf.org/html/rfc5322#section-3.6.4).
## Set it up ## Set it up
If you want to use Gmail / Google Apps with Reply by email, make sure you have
[IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018)
and [allowed less secure apps to access the account](https://support.google.com/accounts/answer/6010255).
To set up a basic Postfix mail server with IMAP access on Ubuntu, follow
[these instructions](./postfix.md).
### Omnibus package installations ### Omnibus package installations
1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the feature and fill in the details for your specific IMAP server and email account: 1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the
feature and fill in the details for your specific IMAP server and email account:
```ruby ```ruby
# Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com # Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com
gitlab_rails['incoming_email_enabled'] = true gitlab_rails['incoming_email_enabled'] = true
# The email address including a placeholder for the key that references the item being replied to. # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
# The `%{key}` placeholder is added after the user part, before the `@`. # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
gitlab_rails['incoming_email_address'] = "incoming+%{key}@gitlab.example.com" gitlab_rails['incoming_email_address'] = "incoming+%{key}@gitlab.example.com"
# Email account username # Email account username
...@@ -49,7 +112,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these ...@@ -49,7 +112,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
gitlab_rails['incoming_email_enabled'] = true gitlab_rails['incoming_email_enabled'] = true
# The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
# The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`. # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
gitlab_rails['incoming_email_address'] = "gitlab-incoming+%{key}@gmail.com" gitlab_rails['incoming_email_address'] = "gitlab-incoming+%{key}@gmail.com"
# Email account username # Email account username
...@@ -72,8 +135,6 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these ...@@ -72,8 +135,6 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
gitlab_rails['incoming_email_mailbox_name'] = "inbox" gitlab_rails['incoming_email_mailbox_name'] = "inbox"
``` ```
As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `incoming@gitlab.example.com`/`gitlab-incoming@gmail.com`.
1. Reconfigure GitLab and restart mailroom for the changes to take effect: 1. Reconfigure GitLab and restart mailroom for the changes to take effect:
```sh ```sh
...@@ -97,7 +158,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these ...@@ -97,7 +158,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
cd /home/git/gitlab cd /home/git/gitlab
``` ```
1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature and fill in the details for your specific IMAP server and email account: 1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature
and fill in the details for your specific IMAP server and email account:
```sh ```sh
sudo editor config/gitlab.yml sudo editor config/gitlab.yml
...@@ -109,7 +171,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these ...@@ -109,7 +171,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
enabled: true enabled: true
# The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
# The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`. # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
address: "incoming+%{key}@gitlab.example.com" address: "incoming+%{key}@gitlab.example.com"
# Email account username # Email account username
...@@ -138,7 +200,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these ...@@ -138,7 +200,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
enabled: true enabled: true
# The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
# The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`. # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
address: "gitlab-incoming+%{key}@gmail.com" address: "gitlab-incoming+%{key}@gmail.com"
# Email account username # Email account username
...@@ -161,8 +223,6 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these ...@@ -161,8 +223,6 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
mailbox: "inbox" mailbox: "inbox"
``` ```
As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `incoming@gitlab.example.com`/`gitlab-incoming@gmail.com`.
1. Enable `mail_room` in the init script at `/etc/default/gitlab`: 1. Enable `mail_room` in the init script at `/etc/default/gitlab`:
```sh ```sh
...@@ -195,8 +255,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these ...@@ -195,8 +255,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
incoming_email: incoming_email:
enabled: true enabled: true
# The email address including a placeholder for the key that references the item being replied to. # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
# The `%{key}` placeholder is added after the user part, before the `@`. # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
address: "gitlab-incoming+%{key}@gmail.com" address: "gitlab-incoming+%{key}@gmail.com"
# Email account username # Email account username
......
...@@ -63,10 +63,10 @@ module Gitlab ...@@ -63,10 +63,10 @@ module Gitlab
end end
def reply_key def reply_key
key_from_to_address || key_from_in_reply_to_header key_from_to_header || key_from_additional_headers
end end
def key_from_to_address def key_from_to_header
key = nil key = nil
message.to.each do |address| message.to.each do |address|
key = Gitlab::IncomingEmail.key_from_address(address) key = Gitlab::IncomingEmail.key_from_address(address)
...@@ -76,11 +76,11 @@ module Gitlab ...@@ -76,11 +76,11 @@ module Gitlab
key key
end end
def key_from_in_reply_to_header def key_from_additional_headers
reply_key = nil reply_key = nil
message[:in_reply_to].message_ids.each do |message_id| Array(message.references).each do |message_id|
reply_key = Gitlab::IncomingEmail.key_from_address(message_id) reply_key = Gitlab::IncomingEmail.key_from_fallback_reply_message_id(message_id)
break if reply_key break if reply_key
end end
......
module Gitlab module Gitlab
module IncomingEmail module IncomingEmail
class << self class << self
def enabled? FALLBACK_REPLY_MESSAGE_ID_REGEX = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\Z/.freeze
config.enabled && address_formatted_correctly?
end
def address_formatted_correctly? def enabled?
config.address && config.enabled && config.address
config.address.include?("%{key}")
end end
def reply_address(key) def reply_address(key)
...@@ -24,6 +21,13 @@ module Gitlab ...@@ -24,6 +21,13 @@ module Gitlab
match[1] match[1]
end end
def key_from_fallback_reply_message_id(message_id)
match = message_id.match(FALLBACK_REPLY_MESSAGE_ID_REGEX)
return unless match
match[1]
end
def config def config
Gitlab.config.incoming_email Gitlab.config.incoming_email
end end
......
...@@ -623,7 +623,6 @@ namespace :gitlab do ...@@ -623,7 +623,6 @@ namespace :gitlab do
start_checking "Reply by email" start_checking "Reply by email"
if Gitlab.config.incoming_email.enabled if Gitlab.config.incoming_email.enabled
check_address_formatted_correctly
check_imap_authentication check_imap_authentication
if Rails.env.production? if Rails.env.production?
...@@ -643,20 +642,6 @@ namespace :gitlab do ...@@ -643,20 +642,6 @@ namespace :gitlab do
# Checks # Checks
######################## ########################
def check_address_formatted_correctly
print "Address formatted correctly? ... "
if Gitlab::IncomingEmail.address_formatted_correctly?
puts "yes".green
else
puts "no".red
try_fixing_it(
"Make sure that the address in config/gitlab.yml includes the '%{key}' placeholder."
)
fix_and_rerun
end
end
def check_initd_configured_correctly def check_initd_configured_correctly
print "Init.d configured correctly? ... " print "Init.d configured correctly? ... "
......
...@@ -3,12 +3,12 @@ Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2. ...@@ -3,12 +3,12 @@ Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.
Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400 Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700 Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700 Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
In-Reply-To: <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>
References: <issue_1@localhost> <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>
Date: Thu, 13 Jun 2013 17:03:48 -0400 Date: Thu, 13 Jun 2013 17:03:48 -0400
From: Jake the Dog <jake@adventuretime.ooo> From: Jake the Dog <jake@adventuretime.ooo>
To: reply@appmail.adventuretime.ooo To: reply@appmail.adventuretime.ooo
Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com> Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
In-Reply-To: <issue_1@localhost>
References: <issue_1@localhost> <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>
Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux' Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'
Mime-Version: 1.0 Mime-Version: 1.0
Content-Type: text/plain; Content-Type: text/plain;
......
...@@ -3,12 +3,12 @@ Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2. ...@@ -3,12 +3,12 @@ Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.
Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400 Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700 Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700 Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
In-Reply-To: <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>
References: <issue_1@localhost> <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>
Date: Thu, 13 Jun 2013 17:03:48 -0400 Date: Thu, 13 Jun 2013 17:03:48 -0400
From: Jake the Dog <jake@adventuretime.ooo> From: Jake the Dog <jake@adventuretime.ooo>
To: reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo To: reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo
Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com> Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
In-Reply-To: <issue_1@localhost>
References: <issue_1@localhost> <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>
Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux' Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'
Mime-Version: 1.0 Mime-Version: 1.0
Content-Type: text/plain; Content-Type: text/plain;
......
...@@ -3,6 +3,7 @@ require "spec_helper" ...@@ -3,6 +3,7 @@ require "spec_helper"
describe Gitlab::Email::Receiver, lib: true do describe Gitlab::Email::Receiver, lib: true do
before do before do
stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo") stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo")
stub_config_setting(host: 'localhost')
end end
let(:reply_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" } let(:reply_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" }
...@@ -138,17 +139,26 @@ describe Gitlab::Email::Receiver, lib: true do ...@@ -138,17 +139,26 @@ describe Gitlab::Email::Receiver, lib: true do
expect(note.note).to include(markdown) expect(note.note).to include(markdown)
end end
context "when the reply key is in the In-Reply-To header" do context 'when sub-addressing is not supported' do
let(:email_raw) { fixture_file("emails/key_in_headers_reply.eml") } before do
stub_incoming_email_setting(enabled: true, address: nil)
end
it "creates a comment" do shared_examples 'an email that contains a reply key' do |header|
it "fetches the reply key from the #{header} header and creates a comment" do
expect { receiver.execute }.to change { noteable.notes.count }.by(1) expect { receiver.execute }.to change { noteable.notes.count }.by(1)
note = noteable.notes.last note = noteable.notes.last
expect(note.author).to eq(sent_notification.recipient) expect(note.author).to eq(sent_notification.recipient)
expect(note.note).to include("I could not disagree more.") expect(note.note).to include('I could not disagree more.')
end
end
context 'reply key is in the References header' do
let(:email_raw) { fixture_file('emails/reply_without_subaddressing_and_key_inside_references.eml') }
it_behaves_like 'an email that contains a reply key', 'References'
end end
end end
end end
end end
...@@ -7,27 +7,11 @@ describe Gitlab::IncomingEmail, lib: true do ...@@ -7,27 +7,11 @@ describe Gitlab::IncomingEmail, lib: true do
stub_incoming_email_setting(enabled: true) stub_incoming_email_setting(enabled: true)
end end
context "when the address is valid" do it 'returns true' do
before do
stub_incoming_email_setting(address: "replies+%{key}@example.com")
end
it "returns true" do
expect(described_class.enabled?).to be_truthy expect(described_class.enabled?).to be_truthy
end end
end end
context "when the address is invalid" do
before do
stub_incoming_email_setting(address: "replies@example.com")
end
it "returns false" do
expect(described_class.enabled?).to be_falsey
end
end
end
context "when reply by email is disabled" do context "when reply by email is disabled" do
before do before do
stub_incoming_email_setting(enabled: false) stub_incoming_email_setting(enabled: false)
...@@ -58,4 +42,10 @@ describe Gitlab::IncomingEmail, lib: true do ...@@ -58,4 +42,10 @@ describe Gitlab::IncomingEmail, lib: true do
expect(described_class.key_from_address("replies+key@example.com")).to eq("key") expect(described_class.key_from_address("replies+key@example.com")).to eq("key")
end end
end end
context 'self.key_from_fallback_reply_message_id' do
it 'returns reply key' do
expect(described_class.key_from_fallback_reply_message_id('reply-key@localhost')).to eq('key')
end
end
end end
...@@ -35,7 +35,9 @@ describe Notify do ...@@ -35,7 +35,9 @@ describe Notify do
subject { Notify.new_issue_email(issue.assignee_id, issue.id) } subject { Notify.new_issue_email(issue.assignee_id, issue.id) }
it_behaves_like 'an assignee email' it_behaves_like 'an assignee email'
it_behaves_like 'an email starting a new thread', 'issue' it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
let(:model) { issue }
end
it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread' it_behaves_like 'an unsubscribeable thread'
...@@ -73,9 +75,11 @@ describe Notify do ...@@ -73,9 +75,11 @@ describe Notify do
subject { Notify.reassigned_issue_email(recipient.id, issue.id, previous_assignee.id, current_user.id) } subject { Notify.reassigned_issue_email(recipient.id, issue.id, previous_assignee.id, current_user.id) }
it_behaves_like 'a multiple recipients email' it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'issue' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { issue }
end
it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like "an unsubscribeable thread" it_behaves_like 'an unsubscribeable thread'
it 'is sent as the author' do it 'is sent as the author' do
sender = subject.header[:from].addrs[0] sender = subject.header[:from].addrs[0]
...@@ -104,7 +108,9 @@ describe Notify do ...@@ -104,7 +108,9 @@ describe Notify do
subject { Notify.relabeled_issue_email(recipient.id, issue.id, %w[foo bar baz], current_user.id) } subject { Notify.relabeled_issue_email(recipient.id, issue.id, %w[foo bar baz], current_user.id) }
it_behaves_like 'a multiple recipients email' it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'issue' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { issue }
end
it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'a user cannot unsubscribe through footer link' it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with a labels subscriptions link in its footer' it_behaves_like 'an email with a labels subscriptions link in its footer'
...@@ -132,7 +138,9 @@ describe Notify do ...@@ -132,7 +138,9 @@ describe Notify do
let(:status) { 'closed' } let(:status) { 'closed' }
subject { Notify.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) } subject { Notify.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) }
it_behaves_like 'an answer to an existing thread', 'issue' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { issue }
end
it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread' it_behaves_like 'an unsubscribeable thread'
...@@ -163,7 +171,9 @@ describe Notify do ...@@ -163,7 +171,9 @@ describe Notify do
let(:new_issue) { create(:issue) } let(:new_issue) { create(:issue) }
subject { Notify.issue_moved_email(recipient, issue, new_issue, current_user) } subject { Notify.issue_moved_email(recipient, issue, new_issue, current_user) }
it_behaves_like 'an answer to an existing thread', 'issue' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { issue }
end
it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread' it_behaves_like 'an unsubscribeable thread'
...@@ -196,9 +206,11 @@ describe Notify do ...@@ -196,9 +206,11 @@ describe Notify do
subject { Notify.new_merge_request_email(merge_request.assignee_id, merge_request.id) } subject { Notify.new_merge_request_email(merge_request.assignee_id, merge_request.id) }
it_behaves_like 'an assignee email' it_behaves_like 'an assignee email'
it_behaves_like 'an email starting a new thread', 'merge_request' it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
let(:model) { merge_request }
end
it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like "an unsubscribeable thread" it_behaves_like 'an unsubscribeable thread'
it 'has the correct subject' do it 'has the correct subject' do
is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
...@@ -216,14 +228,6 @@ describe Notify do ...@@ -216,14 +228,6 @@ describe Notify do
is_expected.to have_body_text /#{merge_request.target_branch}/ is_expected.to have_body_text /#{merge_request.target_branch}/
end end
it 'has the correct message-id set' do
is_expected.to have_header 'Message-ID', /<reply\+(.*)@#{Gitlab.config.gitlab.host}>/
end
it 'has the correct references set' do
is_expected.to have_header 'References', "<merge_request_#{merge_request.id}@#{Gitlab.config.gitlab.host}>"
end
context 'when enabled email_author_in_body' do context 'when enabled email_author_in_body' do
before do before do
allow(current_application_settings).to receive(:email_author_in_body).and_return(true) allow(current_application_settings).to receive(:email_author_in_body).and_return(true)
...@@ -251,7 +255,9 @@ describe Notify do ...@@ -251,7 +255,9 @@ describe Notify do
subject { Notify.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id) } subject { Notify.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id) }
it_behaves_like 'a multiple recipients email' it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'merge_request' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { merge_request }
end
it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like "an unsubscribeable thread" it_behaves_like "an unsubscribeable thread"
...@@ -282,7 +288,9 @@ describe Notify do ...@@ -282,7 +288,9 @@ describe Notify do
subject { Notify.relabeled_merge_request_email(recipient.id, merge_request.id, %w[foo bar baz], current_user.id) } subject { Notify.relabeled_merge_request_email(recipient.id, merge_request.id, %w[foo bar baz], current_user.id) }
it_behaves_like 'a multiple recipients email' it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'merge_request' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { merge_request }
end
it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'a user cannot unsubscribe through footer link' it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with a labels subscriptions link in its footer' it_behaves_like 'an email with a labels subscriptions link in its footer'
...@@ -310,9 +318,11 @@ describe Notify do ...@@ -310,9 +318,11 @@ describe Notify do
let(:status) { 'reopened' } let(:status) { 'reopened' }
subject { Notify.merge_request_status_email(recipient.id, merge_request.id, status, current_user.id) } subject { Notify.merge_request_status_email(recipient.id, merge_request.id, status, current_user.id) }
it_behaves_like 'an answer to an existing thread', 'merge_request' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { merge_request }
end
it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like "an unsubscribeable thread" it_behaves_like 'an unsubscribeable thread'
it 'is sent as the author' do it 'is sent as the author' do
sender = subject.header[:from].addrs[0] sender = subject.header[:from].addrs[0]
...@@ -341,9 +351,11 @@ describe Notify do ...@@ -341,9 +351,11 @@ describe Notify do
subject { Notify.merged_merge_request_email(recipient.id, merge_request.id, merge_author.id) } subject { Notify.merged_merge_request_email(recipient.id, merge_request.id, merge_author.id) }
it_behaves_like 'a multiple recipients email' it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'merge_request' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { merge_request }
end
it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like "an unsubscribeable thread" it_behaves_like 'an unsubscribeable thread'
it 'is sent as the merge author' do it 'is sent as the merge author' do
sender = subject.header[:from].addrs[0] sender = subject.header[:from].addrs[0]
...@@ -460,9 +472,11 @@ describe Notify do ...@@ -460,9 +472,11 @@ describe Notify do
subject { Notify.note_commit_email(recipient.id, note.id) } subject { Notify.note_commit_email(recipient.id, note.id) }
it_behaves_like 'a note email' it_behaves_like 'a note email'
it_behaves_like 'an answer to an existing thread', 'commit' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { commit }
end
it_behaves_like 'it should show Gmail Actions View Commit link' it_behaves_like 'it should show Gmail Actions View Commit link'
it_behaves_like "a user cannot unsubscribe through footer link" it_behaves_like 'a user cannot unsubscribe through footer link'
it 'has the correct subject' do it 'has the correct subject' do
is_expected.to have_subject /#{commit.title} \(#{commit.short_id}\)/ is_expected.to have_subject /#{commit.title} \(#{commit.short_id}\)/
...@@ -481,7 +495,9 @@ describe Notify do ...@@ -481,7 +495,9 @@ describe Notify do
subject { Notify.note_merge_request_email(recipient.id, note.id) } subject { Notify.note_merge_request_email(recipient.id, note.id) }
it_behaves_like 'a note email' it_behaves_like 'a note email'
it_behaves_like 'an answer to an existing thread', 'merge_request' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { merge_request }
end
it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'an unsubscribeable thread' it_behaves_like 'an unsubscribeable thread'
...@@ -502,7 +518,9 @@ describe Notify do ...@@ -502,7 +518,9 @@ describe Notify do
subject { Notify.note_issue_email(recipient.id, note.id) } subject { Notify.note_issue_email(recipient.id, note.id) }
it_behaves_like 'a note email' it_behaves_like 'a note email'
it_behaves_like 'an answer to an existing thread', 'issue' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { issue }
end
it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread' it_behaves_like 'an unsubscribeable thread'
......
...@@ -10,6 +10,13 @@ shared_context 'gitlab email notification' do ...@@ -10,6 +10,13 @@ shared_context 'gitlab email notification' do
ActionMailer::Base.deliveries.clear ActionMailer::Base.deliveries.clear
email = recipient.emails.create(email: "notifications@example.com") email = recipient.emails.create(email: "notifications@example.com")
recipient.update_attribute(:notification_email, email.email) recipient.update_attribute(:notification_email, email.email)
stub_incoming_email_setting(enabled: true, address: "reply+%{key}@#{Gitlab.config.gitlab.host}")
end
end
shared_context 'reply-by-email is enabled with incoming address without %{key}' do
before do
stub_incoming_email_setting(enabled: true, address: "reply@#{Gitlab.config.gitlab.host}")
end end
end end
...@@ -46,26 +53,76 @@ shared_examples 'an email with X-GitLab headers containing project details' do ...@@ -46,26 +53,76 @@ shared_examples 'an email with X-GitLab headers containing project details' do
end end
end end
shared_examples 'an email starting a new thread' do |message_id_prefix| shared_examples 'a new thread email with reply-by-email enabled' do
include_examples 'an email with X-GitLab headers containing project details' let(:regex) { /\A<reply\-(.*)@#{Gitlab.config.gitlab.host}>\Z/ }
it 'has a Message-ID header' do
is_expected.to have_header 'Message-ID', "<#{model.class.model_name.singular_route_key}_#{model.id}@#{Gitlab.config.gitlab.host}>"
end
it 'has a discussion identifier' do it 'has a References header' do
is_expected.to have_header 'Message-ID', /<reply\+(.*)@#{Gitlab.config.gitlab.host}>/ is_expected.to have_header 'References', regex
is_expected.to have_header 'References', /<#{message_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/
end end
end end
shared_examples 'an answer to an existing thread' do |thread_id_prefix| shared_examples 'a thread answer email with reply-by-email enabled' do
include_examples 'an email with X-GitLab headers containing project details' include_examples 'an email with X-GitLab headers containing project details'
let(:regex) { /\A<#{model.class.model_name.singular_route_key}_#{model.id}@#{Gitlab.config.gitlab.host}> <reply\-(.*)@#{Gitlab.config.gitlab.host}>\Z/ }
it 'has a Message-ID header' do
is_expected.to have_header 'Message-ID', /\A<(.*)@#{Gitlab.config.gitlab.host}>\Z/
end
it 'has a In-Reply-To header' do
is_expected.to have_header 'In-Reply-To', "<#{model.class.model_name.singular_route_key}_#{model.id}@#{Gitlab.config.gitlab.host}>"
end
it 'has a References header' do
is_expected.to have_header 'References', regex
end
it 'has a subject that begins with Re: ' do it 'has a subject that begins with Re: ' do
is_expected.to have_subject /^Re: / is_expected.to have_subject /^Re: /
end end
end
shared_examples 'an email starting a new thread with reply-by-email enabled' do
include_examples 'an email with X-GitLab headers containing project details'
include_examples 'a new thread email with reply-by-email enabled'
it 'has headers that reference an existing thread' do context 'when reply-by-email is enabled with incoming address with %{key}' do
is_expected.to have_header 'Message-ID', /<(.*)@#{Gitlab.config.gitlab.host}>/ it 'has a Reply-To header' do
is_expected.to have_header 'References', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ is_expected.to have_header 'Reply-To', /<reply+(.*)@#{Gitlab.config.gitlab.host}>\Z/
is_expected.to have_header 'In-Reply-To', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ end
end
context 'when reply-by-email is enabled with incoming address without %{key}' do
include_context 'reply-by-email is enabled with incoming address without %{key}'
include_examples 'a new thread email with reply-by-email enabled'
it 'has a Reply-To header' do
is_expected.to have_header 'Reply-To', /<reply@#{Gitlab.config.gitlab.host}>\Z/
end
end
end
shared_examples 'an answer to an existing thread with reply-by-email enabled' do
include_examples 'an email with X-GitLab headers containing project details'
include_examples 'a thread answer email with reply-by-email enabled'
context 'when reply-by-email is enabled with incoming address with %{key}' do
it 'has a Reply-To header' do
is_expected.to have_header 'Reply-To', /<reply+(.*)@#{Gitlab.config.gitlab.host}>\Z/
end
end
context 'when reply-by-email is enabled with incoming address without %{key}' do
include_context 'reply-by-email is enabled with incoming address without %{key}'
include_examples 'a thread answer email with reply-by-email enabled'
it 'has a Reply-To header' do
is_expected.to have_header 'Reply-To', /<reply@#{Gitlab.config.gitlab.host}>\Z/
end
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