discourse/lib/tasks/emails.rake
Régis Hanol db9d998de3
FIX: improve mailman email parsing (#21627)
https://meta.discourse.org/t/improving-mailman-email-parsing/253041

When mirroring a public mailling list which uses mailman, there were some cases where the incoming email was not associated to the proper user.

As it happens, for various (undertermined) reasons, the email from the sender is often not in the `From` header but can be in any of the following headers: `Reply-To`, `CC`, `X-Original-From`, `X-MailFrom`.

It might be in other headers as well, but those were the ones we found the most reliable.
2023-05-19 10:33:48 +02:00

263 lines
8.7 KiB
Ruby

# frozen_string_literal: true
def process_popmail(popmail)
begin
mail_string = popmail.pop
Email::Receiver.new(mail_string).process
rescue StandardError
putc "!"
else
putc "."
end
end
desc "use this task to import a mailbox into Discourse"
task "emails:import" => :environment do
begin
unless SiteSetting.email_in
puts "ERROR: you should enable the 'email_in' site setting before running this task"
exit(1)
end
address = ENV["ADDRESS"].presence || "pop.gmail.com"
port = (ENV["PORT"].presence || 995).to_i
ssl = (ENV["SSL"].presence || "1") == "1"
username = ENV["USERNAME"].presence
password = ENV["PASSWORD"].presence
if username.blank?
puts "ERROR: expecting USERNAME=<username> rake emails:import"
exit(2)
elsif password.blank?
puts "ERROR: expecting PASSWORD=<password> rake emails:import"
exit(3)
end
RateLimiter.disable
mails_left = 1
pop3 = Net::POP3.new(address, port)
pop3.enable_ssl(max_version: OpenSSL::SSL::TLS1_2_VERSION) if ssl
while mails_left > 0
pop3.start(username, password) do |pop|
pop.delete_all { |p| process_popmail(p) }
mails_left = pop.n_mails
end
end
puts "Done"
rescue Net::POPAuthenticationError
puts "AUTH EXCEPTION: please make sure your credentials are correct."
exit(10)
ensure
RateLimiter.enable
end
end
desc "Check if SMTP connection is successful and send test message"
task "emails:test", [:email] => [:environment] do |_, args|
email = args[:email]
message = "OK"
begin
smtp = Discourse::Application.config.action_mailer.smtp_settings
puts <<~TEXT if smtp[:address].match(/smtp\.gmail\.com/)
#{smtp}
============================== WARNING ==============================
Sending mail with Gmail is a violation of their terms of service.
Sending with G Suite might work, but it is not recommended. For information see:
https://meta.discourse.org/t/discourse-aws-ec2-g-suite-troubleshooting/62931?u=pfaffman
========================= CONTINUING TEST ============================
TEXT
puts "Testing sending to #{email} using #{smtp[:address]}:#{smtp[:port]}, username:#{smtp[:user_name]} with #{smtp[:authentication]} auth."
# We are not formatting the messages using EmailSettingsExceptionHandler here
# because we are doing custom messages in the rake task with more details.
EmailSettingsValidator.validate_smtp(
host: smtp[:address],
port: smtp[:port],
domain: smtp[:domain] || "localhost",
username: smtp[:user_name],
password: smtp[:password],
authentication: smtp[:authentication] || "plain",
)
rescue Exception => e
if e.to_s.match(/execution expired/)
message = <<~TEXT
======================================== ERROR ========================================
Connection to port #{smtp[:port]} failed.
====================================== SOLUTION =======================================
The most likely problem is that your server has outgoing SMTP traffic blocked.
If you are using a service like Mailgun or Sendgrid, try using port 2525.
=======================================================================================
TEXT
elsif e.to_s.match(/530.*STARTTLS/)
# We can't run a preliminary test with STARTTLS, we'll just try sending the test email.
message = "OK"
elsif e.to_s.match(/535/)
message = <<~TEXT
======================================== ERROR ========================================
AUTHENTICATION FAILED
#{e}
====================================== SOLUTION =======================================
The most likely problem is that your SMTP username and/or Password is incorrect.
Check them and try again.
=======================================================================================
TEXT
elsif e.to_s.match(/Connection refused/)
message = <<~TEXT
======================================== ERROR ========================================
CONNECTION REFUSED
#{e}
====================================== SOLUTION =======================================
The most likely problem is that you have chosen the wrong port or a network problem is
blocking access from the Docker container.
Check the port and your networking configuration.
=======================================================================================
TEXT
elsif e.to_s.match(/service not known/)
message = <<~TEXT
======================================== ERROR ========================================
SMTP SERVER NOT FOUND
#{e}
====================================== SOLUTION =======================================
The most likely problem is that the host name of your SMTP server is incorrect.
Check it and try again.
=======================================================================================
TEXT
else
message = <<~TEXT
======================================== ERROR ========================================
UNEXPECTED ERROR
#{e}
====================================== SOLUTION =======================================
This is not a common error. No recommended solution exists!
Please report the exact error message above to https://meta.discourse.org/
(And a solution, if you find one!)
=======================================================================================
TEXT
end
end
if message == "OK"
puts "SMTP server connection successful."
else
puts message
exit
end
begin
puts "Sending to #{email}. . . "
email_log = Email::Sender.new(TestMailer.send_test(email), :test_message).send
case email_log
when SkippedEmailLog
puts <<~TEXT
Mail was not sent.
Reason: #{email_log.reason}
TEXT
when EmailLog
puts <<~TEXT
Mail accepted by SMTP server.
Message-ID: #{email_log.message_id}
If you do not receive the message, check your SPAM folder
or test again using a service like http://www.mail-tester.com/.
If the message is not delivered it is not a problem with Discourse.
Check the SMTP server logs for the above Message ID to see why it
failed to deliver the message.
TEXT
when nil
puts <<~TEXT
Mail was not sent.
Verify the status of the `disable_emails` site setting.
TEXT
else
puts <<~TEXT
SCRIPT BUG: Got back a #{email_log.class}
#{email_log.inspect}
Mail may or may not have been sent. Check the destination mailbox.
TEXT
end
rescue => error
puts "Sending mail failed."
puts error.message
end
puts <<~TEXT if SiteSetting.disable_emails != "no"
### WARNING
The `disable_emails` site setting is currently set to #{SiteSetting.disable_emails}.
Consider changing it to 'no' before performing any further troubleshooting.
TEXT
end
desc "run this to fix users associated to emails mirrored from a mailman mailing list"
task "emails:fix_mailman_users" => :environment do
if !SiteSetting.enable_staged_users
puts "Please enable staged users first"
exit 1
end
def find_or_create_user(email, name)
user = nil
User.transaction do
unless user = User.find_by_email(email)
username = UserNameSuggester.sanitize_username(name) if name.present?
username = UserNameSuggester.suggest(username.presence || email)
name = name.presence || User.suggest_name(email)
begin
user = User.create!(email: email, username: username, name: name, staged: true)
rescue PG::UniqueViolation, ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
end
end
end
user
end
IncomingEmail
.includes(:user, :post)
.where("raw LIKE '%X-Mailman-Version: %'")
.find_each do |ie|
next unless ie.post.present?
mail = Mail.new(ie.raw)
email, name = Email::Receiver.extract_email_address_and_name_from_mailman(mail)
if email.blank? || email == ie.user.email
putc "."
elsif new_owner = find_or_create_user(email, name)
PostOwnerChanger.new(
post_ids: [ie.post_id],
topic_id: ie.post.topic_id,
new_owner: new_owner,
acting_user: Discourse.system_user,
skip_revision: true,
).change_owner!
putc "#"
else
putc "X"
end
end
nil
end