From 56718504ac927da6b8945e32de8f6d2619bafee5 Mon Sep 17 00:00:00 2001 From: Alessio Cosenza Date: Mon, 26 Jun 2023 07:16:03 +0200 Subject: [PATCH] FEATURE: Add hooks for email poller plugins (#21384) While we are unable to support OAUTH2 with pop3 (due to upstream dependency ruby/net-pop#16), we are adding the support for mail pollers plugin. Doing so, it would be possible to write a plugin which then uses other ways (microsoft graph sdk for example) to poll emails from a mailbox. The idea is that a plugin would define a class which inherits from Email::Poller and defines a poll_mailbox static method which returns an array of strings. Then the plugin could call register_mail_poller() to have it registered. All the configuration (oauth2 tokens, email, etc) could be managed by sitesettings defined in the plugin. --- app/jobs/scheduled/poll_mailbox.rb | 6 ++++ app/models/site_setting.rb | 3 +- config/locales/server.en.yml | 2 +- lib/discourse_plugin_registry.rb | 5 +++ lib/email/poller.rb | 18 ++++++++++ lib/plugin/instance.rb | 5 +++ spec/jobs/poll_mailbox_spec.rb | 42 ++++++++++++++++++++++ spec/lib/discourse_plugin_registry_spec.rb | 7 ++++ 8 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 lib/email/poller.rb diff --git a/app/jobs/scheduled/poll_mailbox.rb b/app/jobs/scheduled/poll_mailbox.rb index f4535bc0779..48922da83fb 100644 --- a/app/jobs/scheduled/poll_mailbox.rb +++ b/app/jobs/scheduled/poll_mailbox.rb @@ -12,6 +12,12 @@ module Jobs def execute(args) @args = args poll_pop3 if should_poll? + + DiscoursePluginRegistry.mail_pollers.each do |poller| + return if !poller.enabled? + + poller.poll_mailbox(method(:process_popmail)) + end end def should_poll? diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb index 411d157bf67..990c2ab9361 100644 --- a/app/models/site_setting.rb +++ b/app/models/site_setting.rb @@ -115,7 +115,8 @@ class SiteSetting < ActiveRecord::Base end def self.email_polling_enabled? - SiteSetting.manual_polling_enabled? || SiteSetting.pop3_polling_enabled? + SiteSetting.manual_polling_enabled? || SiteSetting.pop3_polling_enabled? || + DiscoursePluginRegistry.mail_pollers.any?(&:enabled?) end def self.blocked_attachment_content_types_regex diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 71bb90ae97b..06919ff3f34 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -2457,7 +2457,7 @@ en: pop3_polling_password_is_empty: "You must set a 'pop3 polling password' before enabling POP3 polling." pop3_polling_authentication_failed: "POP3 authentication failed. Please verify your pop3 credentials." reply_by_email_address_is_empty: "You must set a 'reply by email address' before enabling reply by email." - email_polling_disabled: "You must enable either manual or POP3 polling before enabling reply by email." + email_polling_disabled: "You must enable either manual, POP3 polling or have a custom mail poller enabled before enabling reply by email." user_locale_not_enabled: "You must first enable 'allow user locale' before enabling this setting." personal_message_enabled_groups_invalid: "You must specify at least one group for this setting. If you do not want anyone except staff to send PMs, choose the staff group." invalid_regex: "Regex is invalid or not allowed." diff --git a/lib/discourse_plugin_registry.rb b/lib/discourse_plugin_registry.rb index 59fa648bb34..697d93474f4 100644 --- a/lib/discourse_plugin_registry.rb +++ b/lib/discourse_plugin_registry.rb @@ -72,6 +72,7 @@ class DiscoursePluginRegistry define_register :seedfu_filter, Set define_register :demon_processes, Set define_register :groups_callback_for_users_search_controller_action, Hash + define_register :mail_pollers, Set define_filtered_register :staff_user_custom_fields define_filtered_register :public_user_custom_fields @@ -119,6 +120,10 @@ class DiscoursePluginRegistry self.auth_providers << auth_provider end + def self.register_mail_poller(mail_poller) + self.mail_pollers << mail_poller + end + def register_js(filename, options = {}) # If we have a server side option, add that too. self.class.javascripts << filename diff --git a/lib/email/poller.rb b/lib/email/poller.rb new file mode 100644 index 00000000000..a364d28c936 --- /dev/null +++ b/lib/email/poller.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Email + class Poller + # To be implemented by concrete classes. + # This function takes as input a function that processes the incoming email. + # The function passed as argument should take as an argument the MIME string of the email. + # An example of function to pass is `process_popmail` in `app/jobs/scheduled/poll_mailbox.rb` + def poll_mailbox(process_cb) + raise NotImplementedError + end + + # Child class can override this + def enabled? + true + end + end +end diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index c1278def0a0..21ddd51c167 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -647,6 +647,11 @@ class Plugin::Instance end end + def register_email_poller(poller) + plugin = self + DiscoursePluginRegistry.register_mail_poller(poller) if plugin.enabled? + end + def register_asset(file, opts = nil) raise <<~ERROR if file.end_with?(".hbs", ".handlebars") [#{name}] Handlebars templates can no longer be included via `register_asset`. diff --git a/spec/jobs/poll_mailbox_spec.rb b/spec/jobs/poll_mailbox_spec.rb index ab6162e60b1..a4ad5aa0537 100644 --- a/spec/jobs/poll_mailbox_spec.rb +++ b/spec/jobs/poll_mailbox_spec.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +require "email/poller" RSpec.describe Jobs::PollMailbox do let(:poller) { Jobs::PollMailbox.new } @@ -175,4 +176,45 @@ RSpec.describe Jobs::PollMailbox do ) end end + + describe "poller plugin" do + let(:poller_plugin) do + Class + .new(described_class) do + def set_enabled(e) + @enabled = e + end + + def enabled? + @enabled + end + + def poll_mailbox(process_cb) + process_cb.call(file_from_fixtures("original_message.eml", "emails")) + end + end + .new + end + + let(:plugin) { Plugin::Instance.new } + + before(:each) { plugin.register_email_poller(poller_plugin) } + + after(:each) do + Discourse.plugins.delete plugin + DiscoursePluginRegistry.reset! + end + + it "doesn't call process method when plugin is not active" do + poller_plugin.set_enabled(false) + poller.expects(:process_popmail).never + poller.execute({}) + end + + it "calls process method when plugin is active" do + poller_plugin.set_enabled(true) + poller.expects(:process_popmail).once + poller.execute({}) + end + end end diff --git a/spec/lib/discourse_plugin_registry_spec.rb b/spec/lib/discourse_plugin_registry_spec.rb index b37bbd2bf6b..6bd872f78f0 100644 --- a/spec/lib/discourse_plugin_registry_spec.rb +++ b/spec/lib/discourse_plugin_registry_spec.rb @@ -98,6 +98,13 @@ RSpec.describe DiscoursePluginRegistry do end end + describe "#mail_pollers" do + it "defaults to an empty Set" do + registry.reset! + expect(registry.mail_pollers).to eq(Set.new) + end + end + describe ".register_html_builder" do it "can register and build html" do DiscoursePluginRegistry.register_html_builder(:my_html) { "my html" }