From bb3a5910d7af38700cb84535fb674884e33f2dc2 Mon Sep 17 00:00:00 2001 From: Bianca Nenciu Date: Mon, 28 Aug 2017 17:07:30 +0100 Subject: [PATCH] Support for sending PMs to email addresses (#4988) * Added support for sending PMs to email addresses. * Made changes after review. * Added settings validator. * Fixed tests. --- .../discourse/components/user-selector.js.es6 | 5 ++- .../discourse/lib/user-search.js.es6 | 10 ++++- .../user-selector-autocomplete.raw.hbs | 12 ++++++ app/controllers/posts_controller.rb | 3 ++ app/serializers/current_user_serializer.rb | 5 +++ config/locales/server.en.yml | 6 +++ config/site_settings.yml | 7 +++ lib/guardian.rb | 11 +++++ lib/topic_creator.rb | 43 ++++++++++++++++++- ...enable_private_email_messages_validator.rb | 20 +++++++++ spec/components/topic_creator_spec.rb | 15 +++++++ 11 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 lib/validators/enable_private_email_messages_validator.rb diff --git a/app/assets/javascripts/discourse/components/user-selector.js.es6 b/app/assets/javascripts/discourse/components/user-selector.js.es6 index 99bfeba07c0..c583960abeb 100644 --- a/app/assets/javascripts/discourse/components/user-selector.js.es6 +++ b/app/assets/javascripts/discourse/components/user-selector.js.es6 @@ -42,8 +42,11 @@ export default TextField.extend({ updateData: (opts && opts.updateData) ? opts.updateData : false, dataSource: function(term) { + const termRegex = Discourse.User.currentProp('can_send_private_email_messages') ? + /[^a-zA-Z0-9_\-\.@\+]/ : /[^a-zA-Z0-9_\-\.]/; + var results = userSearch({ - term: term.replace(/[^a-zA-Z0-9_\-\.]/, ''), + term: term.replace(termRegex, ''), topicId: self.get('topicId'), exclude: excludedUsernames(), includeGroups, diff --git a/app/assets/javascripts/discourse/lib/user-search.js.es6 b/app/assets/javascripts/discourse/lib/user-search.js.es6 index 6361ea92138..5186779d3c1 100644 --- a/app/assets/javascripts/discourse/lib/user-search.js.es6 +++ b/app/assets/javascripts/discourse/lib/user-search.js.es6 @@ -46,6 +46,7 @@ function organizeResults(r, options) { var exclude = options.exclude || [], limit = options.limit || 5, users = [], + emails = [], groups = [], results = []; @@ -59,6 +60,12 @@ function organizeResults(r, options) { }); } + if (options.term.match(/@/)) { + let e = { username: options.term }; + emails = [ e ]; + results.push(e); + } + if (r.groups) { r.groups.every(function(g) { if (results.length > limit && options.term.toLowerCase() !== g.name.toLowerCase()) return false; @@ -71,6 +78,7 @@ function organizeResults(r, options) { } results.users = users; + results.emails = emails; results.groups = groups; return results; } @@ -94,7 +102,7 @@ export default function userSearch(options) { return new Ember.RSVP.Promise(function(resolve) { // TODO site setting for allowed regex in username - if (term.match(/[^\w\.\-]/)) { + if (term.match(/[^\w_\-\.@\+]/)) { resolve([]); return; } diff --git a/app/assets/javascripts/discourse/templates/user-selector-autocomplete.raw.hbs b/app/assets/javascripts/discourse/templates/user-selector-autocomplete.raw.hbs index 4c353fbd065..a72dd3e5186 100644 --- a/app/assets/javascripts/discourse/templates/user-selector-autocomplete.raw.hbs +++ b/app/assets/javascripts/discourse/templates/user-selector-autocomplete.raw.hbs @@ -9,6 +9,18 @@ {{/each}} + + {{#if options.emails}} + {{#each options.emails as |email|}} +
  • + + + {{email.username}} + +
  • + {{/each}} + {{/if}} + {{#if options.groups}} {{#each options.groups as |group|}}
  • diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index fa99d1f2d99..42c0202d767 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -617,7 +617,10 @@ class PostsController < ApplicationController usernames = usernames.split(",") groups = Group.mentionable(current_user).where('name in (?)', usernames).pluck('name') usernames -= groups + emails = usernames.select { |user| user.match(/@/) } + usernames -= emails result[:target_usernames] = usernames.join(",") + result[:target_emails] = emails.join(",") result[:target_group_names] = groups.join(",") end diff --git a/app/serializers/current_user_serializer.rb b/app/serializers/current_user_serializer.rb index ddf62198993..b7489167f4d 100644 --- a/app/serializers/current_user_serializer.rb +++ b/app/serializers/current_user_serializer.rb @@ -18,6 +18,7 @@ class CurrentUserSerializer < BasicUserSerializer :external_links_in_new_tab, :dynamic_favicon, :trust_level, + :can_send_private_email_messages, :can_edit, :can_invite_to_forum, :no_password, @@ -87,6 +88,10 @@ class CurrentUserSerializer < BasicUserSerializer PostAction.flagged_posts_count end + def can_send_private_email_messages + scope.cand_send_private_messages_to_email? + end + def can_edit true end diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 6e098af27a4..351832b2754 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -382,6 +382,7 @@ en: too_many_users: "You can only send warnings to one user at a time." cant_send_pm: "Sorry, you cannot send a private message to that user." no_user_selected: "You must select a valid user." + reply_by_email_disabled: "Reply by email has been disabled." featured_link: invalid: "is invalid. URL should include http:// or https://." invalid_category: "can't be edited in this category." @@ -1013,6 +1014,7 @@ en: summary_max_results: "Maximum posts returned by 'Summary This Topic'" enable_private_messages: "Allow trust level 1 (configurable via min trust level to send messages) users to create messages and reply to messages. Note that staff can always send messages no matter what." + enable_private_email_messages: "Allow trust level 4 (configurable via min trust level to send messages) users to send private email messages. Note that staff can always send messages no matter what." enable_long_polling: "Message bus used for notification can use long polling" long_polling_base_url: "Base URL used for long polling (when a CDN is serving dynamic content, be sure to set this to origin pull) eg: http://origin.site.com" @@ -1259,6 +1261,8 @@ en: min_trust_to_send_messages: "The minimum trust level required to create new private messages." + min_trust_to_send_email_messages: "The minimum trust level required to send new private messages via email (to staged users)." + newuser_max_links: "How many links a new user can add to a post." newuser_max_images: "How many images a new user can add to a post." newuser_max_attachments: "How many attachments a new user can add to a post." @@ -1586,6 +1590,8 @@ en: invalid_regex: "Regex is invalid or not allowed." email_editable_enabled: "You must disable 'email editable' before enabling this setting." enable_sso_disabled: "You must first enable 'enable sso' before enabling this setting." + staged_users_disabled: "You must first enable 'staged users' before enabling this setting." + reply_by_email_disabled: "You must first enable 'reply by email' before enabling this setting." search: within_post: "#%{post_number} by %{username}" diff --git a/config/site_settings.yml b/config/site_settings.yml index 697b667c707..29e574dc73a 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -508,6 +508,10 @@ posting: enable_private_messages: default: true client: true + enable_private_email_messages: + default: false + client: true + validator: "EnablePrivateEmailMessagesValidator" editing_grace_period: 300 post_edit_time_limit: default: 86400 @@ -857,6 +861,9 @@ trust: min_trust_to_send_messages: default: 1 enum: 'TrustLevelSetting' + min_trust_to_send_email_messages: + default: 4 + enum: 'TrustLevelSetting' tl1_requires_topics_entered: 5 tl1_requires_read_posts: default: 30 diff --git a/lib/guardian.rb b/lib/guardian.rb index dd75225f824..a09d11ae1d1 100644 --- a/lib/guardian.rb +++ b/lib/guardian.rb @@ -299,6 +299,17 @@ class Guardian (!is_blocked? || target.staff?) end + def cand_send_private_messages_to_email? + # Staged users must be enabled to create a temporary user. + SiteSetting.enable_staged_users && + # User is authenticated + authenticated? && + # User is trusted enough + @user.has_trust_level?(SiteSetting.min_trust_to_send_email_messages) && + # PMs to email addresses are enabled + (is_staff? || SiteSetting.enable_private_email_messages) + end + def can_see_emails? @can_see_emails end diff --git a/lib/topic_creator.rb b/lib/topic_creator.rb index 885f4f26053..19b656d5fb9 100644 --- a/lib/topic_creator.rb +++ b/lib/topic_creator.rb @@ -162,11 +162,16 @@ class TopicCreator return unless @opts[:archetype] == Archetype.private_message topic.subtype = TopicSubtype.user_to_user unless topic.subtype - unless @opts[:target_usernames].present? || @opts[:target_group_names].present? + unless @opts[:target_usernames].present? || @opts[:target_emails].present? || @opts[:target_group_names].present? rollback_with!(topic, :no_user_selected) end + if @opts[:target_emails].present? && !@guardian.cand_send_private_messages_to_email? then + rollback_with!(topic, :reply_by_email_disabled) + end + add_users(topic, @opts[:target_usernames]) + add_emails(topic, @opts[:target_emails]) add_groups(topic, @opts[:target_group_names]) topic.topic_allowed_users.build(user_id: @user.id) end @@ -195,6 +200,23 @@ class TopicCreator rollback_with!(topic, :target_user_not_found) unless len == names.length end + def add_emails(topic, emails) + return unless emails + + emails = emails.split(',').flatten + len = 0 + + emails.each do |email| + display_name = email.split("@").first + user = find_or_create_user(email, display_name) + @added_users << user + topic.topic_allowed_users.build(user_id: user.id) + len += 1 + end + + rollback_with!(topic, :target_user_not_found) unless len == emails.length + end + def add_groups(topic, groups) return unless groups names = groups.split(',').flatten @@ -213,4 +235,23 @@ class TopicCreator def check_can_send_permission!(topic, obj) rollback_with!(topic, :cant_send_pm) unless @opts[:skip_validations] || @guardian.can_send_private_message?(obj) end + + def find_or_create_user(email, display_name) + user = User.find_by_email(email) + + if user.nil? && SiteSetting.enable_staged_users + username = UserNameSuggester.sanitize_username(display_name) if display_name.present? + user = User.create!( + email: email, + username: UserNameSuggester.suggest(username.presence || email), + name: display_name.presence || User.suggest_name(email), + staged: true + ) + end + + user + rescue + rollback_with!(topic, :target_user_not_found) + end + end diff --git a/lib/validators/enable_private_email_messages_validator.rb b/lib/validators/enable_private_email_messages_validator.rb new file mode 100644 index 00000000000..42e8aa64e8f --- /dev/null +++ b/lib/validators/enable_private_email_messages_validator.rb @@ -0,0 +1,20 @@ +class EnablePrivateEmailMessagesValidator + + def initialize(opts = {}) + @opts = opts + end + + def valid_value?(val) + return true if val == "f" + SiteSetting.enable_staged_users && + SiteSetting.reply_by_email_enabled + end + + def error_message + if !SiteSetting.enable_staged_users + I18n.t("site_settings.errors.staged_users_disabled") + elsif !SiteSetting.reply_by_email_enabled + I18n.t("site_settings.errors.reply_by_email_disabled") + end + end +end diff --git a/spec/components/topic_creator_spec.rb b/spec/components/topic_creator_spec.rb index a8cf92f315b..ade3f596042 100644 --- a/spec/components/topic_creator_spec.rb +++ b/spec/components/topic_creator_spec.rb @@ -8,6 +8,7 @@ describe TopicCreator do let(:valid_attrs) { Fabricate.attributes_for(:topic) } let(:pm_valid_attrs) { { raw: 'this is a new post', title: 'this is a new title', archetype: Archetype.private_message, target_usernames: moderator.username } } + let(:pm_to_email_valid_attrs) { { raw: 'this is a new email', title: 'this is a new subject', archetype: Archetype.private_message, target_emails: 'moderator@example.com' } } describe '#create' do context 'topic success cases' do @@ -58,6 +59,7 @@ describe TopicCreator do TopicCreator.any_instance.expects(:save_topic).returns(true) TopicCreator.any_instance.expects(:watch_topic).returns(true) SiteSetting.allow_duplicate_topic_titles = true + SiteSetting.enable_staged_users = true end it "should be possible for a regular user to send private message" do @@ -68,6 +70,14 @@ describe TopicCreator do SiteSetting.min_trust_to_create_topic = TrustLevel[4] expect(TopicCreator.create(user, Guardian.new(user), pm_valid_attrs)).to be_valid end + + it "should be possible for a trusted user to send private messages via email" do + SiteSetting.expects(:enable_staged_users).returns(true) + SiteSetting.expects(:enable_staged_users).returns(true) + SiteSetting.expects(:enable_private_email_messages).returns(true) + SiteSetting.min_trust_to_send_email_messages = TrustLevel[1] + expect(TopicCreator.create(user, Guardian.new(user), pm_to_email_valid_attrs)).to be_valid + end end context 'failure cases' do @@ -75,6 +85,11 @@ describe TopicCreator do SiteSetting.min_trust_to_send_messages = TrustLevel[4] expect(-> { TopicCreator.create(user, Guardian.new(user), pm_valid_attrs) }).to raise_error(ActiveRecord::Rollback) end + + it "min_trust_to_send_email_messages should be checked when sending private messages via email" do + SiteSetting.min_trust_to_send_email_messages = TrustLevel[4] + expect(-> { TopicCreator.create(user, Guardian.new(user), pm_to_email_valid_attrs) }).to raise_error(ActiveRecord::Rollback) + end end end end