2019-05-03 06:17:27 +08:00
# frozen_string_literal: true
2016-03-15 01:18:58 +08:00
require " digest "
2013-06-11 04:46:08 +08:00
module Email
class Receiver
2017-09-15 23:22:51 +08:00
# If you add a new error, you need to
# * add it to Email::Processor#handle_failure()
# * add text to server.en.yml (parent key: "emails.incoming.errors")
2016-04-19 04:58:30 +08:00
class ProcessingError < StandardError
end
2023-12-15 23:46:04 +08:00
2016-04-19 04:58:30 +08:00
class EmptyEmailError < ProcessingError
end
2023-12-15 23:46:04 +08:00
2016-04-19 04:58:30 +08:00
class ScreenedEmailError < ProcessingError
end
2023-12-15 23:46:04 +08:00
2016-04-19 04:58:30 +08:00
class UserNotFoundError < ProcessingError
end
2023-12-15 23:46:04 +08:00
2016-04-19 04:58:30 +08:00
class AutoGeneratedEmailError < ProcessingError
end
2023-12-15 23:46:04 +08:00
2016-04-19 04:58:30 +08:00
class BouncedEmailError < ProcessingError
end
2023-12-15 23:46:04 +08:00
2016-04-19 04:58:30 +08:00
class NoBodyDetectedError < ProcessingError
end
2023-12-15 23:46:04 +08:00
2017-09-13 04:35:24 +08:00
class NoSenderDetectedError < ProcessingError
end
2023-12-15 23:46:04 +08:00
2018-05-23 16:04:45 +08:00
class FromReplyByAddressError < ProcessingError
end
2023-12-15 23:46:04 +08:00
2016-04-19 04:58:30 +08:00
class InactiveUserError < ProcessingError
end
2023-12-15 23:46:04 +08:00
2017-11-11 01:18:08 +08:00
class SilencedUserError < ProcessingError
end
2023-12-15 23:46:04 +08:00
2016-04-19 04:58:30 +08:00
class BadDestinationAddress < ProcessingError
end
2023-12-15 23:46:04 +08:00
2016-04-19 04:58:30 +08:00
class StrangersNotAllowedError < ProcessingError
end
2023-12-15 23:46:04 +08:00
2019-06-03 05:49:05 +08:00
class ReplyNotAllowedError < ProcessingError
end
2023-12-15 23:46:04 +08:00
2016-04-19 04:58:30 +08:00
class InsufficientTrustLevelError < ProcessingError
end
2023-12-15 23:46:04 +08:00
2016-04-19 04:58:30 +08:00
class ReplyUserNotMatchingError < ProcessingError
end
2023-12-15 23:46:04 +08:00
2016-04-19 04:58:30 +08:00
class TopicNotFoundError < ProcessingError
end
2023-12-15 23:46:04 +08:00
2016-04-19 04:58:30 +08:00
class TopicClosedError < ProcessingError
end
2023-12-15 23:46:04 +08:00
2016-04-19 04:58:30 +08:00
class InvalidPost < ProcessingError
end
2023-12-15 23:46:04 +08:00
2018-08-03 03:43:53 +08:00
class TooShortPost < ProcessingError
end
2023-12-15 23:46:04 +08:00
2016-04-19 04:58:30 +08:00
class InvalidPostAction < ProcessingError
end
2023-12-15 23:46:04 +08:00
2017-10-03 16:13:19 +08:00
class UnsubscribeNotAllowed < ProcessingError
end
2023-12-15 23:46:04 +08:00
2017-10-03 17:23:18 +08:00
class EmailNotAllowed < ProcessingError
end
2023-12-15 23:46:04 +08:00
2018-05-10 00:51:01 +08:00
class OldDestinationError < ProcessingError
end
2023-12-15 23:46:04 +08:00
2020-05-14 22:04:58 +08:00
class ReplyToDigestError < ProcessingError
end
2016-01-19 07:57:55 +08:00
2022-08-18 23:18:58 +08:00
class TooManyRecipientsError < ProcessingError
attr_reader :recipients_count
def initialize ( recipients_count : )
@recipients_count = recipients_count
end
end
2016-03-07 23:56:17 +08:00
attr_reader :incoming_email
2017-05-27 04:26:18 +08:00
attr_reader :raw_email
attr_reader :mail
attr_reader :message_id
2016-03-07 23:56:17 +08:00
2024-10-16 10:09:07 +08:00
COMMON_ENCODINGS = [ - " utf-8 " , - " windows-1252 " , - " iso-8859-1 " ]
2018-01-31 06:45:04 +08:00
2017-11-15 23:39:29 +08:00
def self . formats
2018-05-10 00:51:01 +08:00
@formats || = Enum . new ( plaintext : 1 , markdown : 2 )
2017-11-15 23:39:29 +08:00
end
2017-12-06 08:47:31 +08:00
def initialize ( mail_string , opts = { } )
2016-01-19 07:57:55 +08:00
raise EmptyEmailError if mail_string . blank?
2017-10-03 16:13:19 +08:00
@staged_users = [ ]
2022-08-18 23:19:20 +08:00
@created_staged_users = [ ]
2018-01-31 06:45:04 +08:00
@raw_email = mail_string
2018-10-11 09:46:32 +08:00
2018-01-31 06:45:04 +08:00
COMMON_ENCODINGS . each do | encoding |
fixed = try_to_encode ( mail_string , encoding )
break @raw_email = fixed if fixed . present?
end
2018-10-11 09:46:32 +08:00
2016-01-19 07:57:55 +08:00
@mail = Mail . new ( @raw_email )
2016-03-15 01:18:58 +08:00
@message_id = @mail . message_id . presence || Digest :: MD5 . hexdigest ( mail_string )
2017-12-06 08:47:31 +08:00
@opts = opts
2020-07-10 17:05:55 +08:00
@destinations || = opts [ :destinations ]
2013-06-11 04:46:08 +08:00
end
2016-03-07 23:56:17 +08:00
def process!
2020-07-27 08:23:54 +08:00
return if is_blocked?
2021-04-28 23:08:48 +08:00
2019-07-24 20:45:02 +08:00
id_hash = Digest :: SHA1 . hexdigest ( @message_id )
2021-04-28 23:08:48 +08:00
2019-07-24 20:45:02 +08:00
DistributedMutex . synchronize ( " process_email_ #{ id_hash } " ) do
2017-05-18 07:09:51 +08:00
begin
2021-04-28 23:08:48 +08:00
# If we find an existing incoming email record with the exact same `message_id`
# do not create a new `IncomingEmail` record to avoid double ups.
return if @incoming_email = find_existing_and_update_imap
2020-08-03 11:10:17 +08:00
2021-10-06 20:07:29 +08:00
Email :: Validator . ensure_valid! ( @mail )
2021-04-28 23:08:48 +08:00
2023-05-19 16:33:48 +08:00
@from_email , @from_display_name = parse_from_field ( @mail )
2018-11-27 02:59:37 +08:00
@from_user = User . find_by_email ( @from_email )
2017-05-18 22:43:07 +08:00
@incoming_email = create_incoming_email
2021-04-28 23:08:48 +08:00
2020-07-10 17:05:55 +08:00
post = process_internal
2021-04-28 23:08:48 +08:00
2018-11-27 02:59:37 +08:00
raise BouncedEmailError if is_bounce?
2021-04-28 23:08:48 +08:00
post
2020-07-09 20:39:01 +08:00
rescue Exception = > e
2022-03-03 21:28:13 +08:00
@incoming_email . update_columns ( error : e . class . name ) if @incoming_email
2022-08-18 23:19:20 +08:00
delete_created_staged_users
2017-05-18 07:09:51 +08:00
raise
end
end
2016-01-19 07:57:55 +08:00
end
2014-08-14 02:06:17 +08:00
2020-08-03 11:10:17 +08:00
def find_existing_and_update_imap
2021-04-28 23:08:48 +08:00
return unless incoming_email = IncomingEmail . find_by ( message_id : @message_id )
2020-08-03 11:10:17 +08:00
2020-08-03 11:31:34 +08:00
# If we are not doing this for IMAP purposes just return the record.
2021-04-28 23:08:48 +08:00
return incoming_email if @opts [ :imap_uid ] . blank?
2020-08-03 11:10:17 +08:00
2020-08-03 11:31:34 +08:00
# If the message_id matches the post id regexp then we
2020-08-03 11:10:17 +08:00
# generated the message_id not the imap server, e.g. in GroupSmtpEmail,
2020-08-03 11:31:34 +08:00
# so we want to update the incoming email because it will
# be missing IMAP details.
#
# Otherwise the incoming email is a completely new one from the IMAP
# server (e.g. a message_id generated by Gmail) and does not need to
# be updated, because message_ids from the IMAP server are not guaranteed
# to be unique.
2021-12-06 08:34:39 +08:00
return unless Email :: MessageIdService . discourse_generated_message_id? ( @message_id )
2020-08-03 11:10:17 +08:00
incoming_email . update (
imap_uid_validity : @opts [ :imap_uid_validity ] ,
imap_uid : @opts [ :imap_uid ] ,
imap_group_id : @opts [ :imap_group_id ] ,
imap_sync : false ,
)
2021-04-28 23:08:48 +08:00
2020-08-03 11:10:17 +08:00
incoming_email
end
2020-07-27 08:23:54 +08:00
def is_blocked?
2016-05-19 05:07:01 +08:00
return false if SiteSetting . ignore_by_title . blank?
2017-11-12 08:43:18 +08:00
Regexp . new ( SiteSetting . ignore_by_title , Regexp :: IGNORECASE ) =~ @mail . subject
2016-05-19 05:07:01 +08:00
end
2017-05-18 22:43:07 +08:00
def create_incoming_email
2021-09-07 06:46:28 +08:00
cc_addresses = Array . wrap ( @mail . cc )
cc_addresses . concat ( embedded_email . cc ) if has_been_forwarded? && embedded_email & . cc
2017-05-18 22:43:07 +08:00
IncomingEmail . create (
message_id : @message_id ,
2019-10-30 13:54:35 +08:00
raw : Email :: Cleaner . new ( @raw_email ) . execute ,
2017-05-18 22:43:07 +08:00
subject : subject ,
from_address : @from_email ,
2021-01-05 13:32:04 +08:00
to_addresses : @mail . to ,
2021-09-07 06:46:28 +08:00
cc_addresses : cc_addresses ,
2020-08-03 11:10:17 +08:00
imap_uid_validity : @opts [ :imap_uid_validity ] ,
imap_uid : @opts [ :imap_uid ] ,
imap_group_id : @opts [ :imap_group_id ] ,
2021-01-20 11:22:41 +08:00
imap_sync : false ,
2021-01-21 10:59:50 +08:00
created_via : IncomingEmail . created_via_types [ @opts [ :source ] || :unknown ] ,
2017-05-18 22:43:07 +08:00
)
2016-01-19 07:57:55 +08:00
end
2014-02-27 23:36:33 +08:00
2016-01-19 07:57:55 +08:00
def process_internal
2018-11-27 02:59:37 +08:00
handle_bounce if is_bounce?
2017-09-13 04:35:24 +08:00
raise NoSenderDetectedError if @from_email . blank?
2018-05-23 16:04:45 +08:00
raise FromReplyByAddressError if is_from_reply_by_email_address?
2016-04-19 04:58:30 +08:00
raise ScreenedEmailError if ScreenedEmail . should_block? ( @from_email )
2018-11-27 02:59:37 +08:00
user = @from_user
2016-03-24 01:56:03 +08:00
2017-10-03 16:13:19 +08:00
if user . present?
2017-10-03 17:23:18 +08:00
log_and_validate_user ( user )
2017-10-03 16:13:19 +08:00
else
raise UserNotFoundError unless SiteSetting . enable_staged_users
end
2016-04-21 03:29:27 +08:00
2022-08-18 23:18:58 +08:00
recipients = get_all_recipients ( @mail )
if recipients . size > SiteSetting . maximum_recipients_per_new_group_email
raise TooManyRecipientsError . new ( recipients_count : recipients . size )
end
2016-11-17 02:42:11 +08:00
body , elided = select_body
2016-03-10 01:51:54 +08:00
body || = " "
2016-03-12 00:51:16 +08:00
2018-11-28 21:34:09 +08:00
raise NoBodyDetectedError if body . blank? && attachments . empty? && ! is_bounce?
2016-04-21 03:29:27 +08:00
2018-01-03 22:29:06 +08:00
if is_auto_generated? && ! sent_to_mailinglist_mirror?
2016-04-21 03:29:27 +08:00
@incoming_email . update_columns ( is_auto_generated : true )
2017-11-17 21:49:10 +08:00
2020-07-10 17:05:55 +08:00
if SiteSetting . block_auto_generated_emails? && ! is_bounce? && ! @opts [ :allow_auto_generated ]
2017-11-17 21:49:10 +08:00
raise AutoGeneratedEmailError
end
2016-04-21 03:29:27 +08:00
end
2015-12-08 00:01:08 +08:00
2016-02-01 19:16:15 +08:00
if action = subscription_action_for ( body , subject )
2017-10-03 16:13:19 +08:00
raise UnsubscribeNotAllowed if user . nil?
send_subscription_mail ( action , user )
return
end
if post = find_related_post
2019-04-08 17:36:39 +08:00
# Most of the time, it is impossible to **reply** without a reply key, so exit early
if user . blank?
if sent_to_mailinglist_mirror? || ! SiteSetting . find_related_post_with_key
user = stage_from_user
elsif user . blank?
raise BadDestinationAddress
end
end
2016-03-15 05:21:18 +08:00
create_reply (
user : user ,
raw : body ,
2016-11-17 02:42:11 +08:00
elided : elided ,
2016-03-15 05:21:18 +08:00
post : post ,
topic : post . topic ,
2018-11-27 02:59:37 +08:00
skip_validations : user . staged? ,
bounce : is_bounce? ,
)
2016-01-19 07:57:55 +08:00
else
2016-08-03 21:57:37 +08:00
first_exception = nil
destinations . each do | destination |
begin
2020-01-22 00:12:00 +08:00
return process_destination ( destination , user , body , elided )
2016-08-03 21:57:37 +08:00
rescue = > e
first_exception || = e
2016-06-17 10:01:08 +08:00
end
2016-01-19 07:57:55 +08:00
end
2016-08-03 21:57:37 +08:00
2018-05-10 00:51:01 +08:00
raise first_exception if first_exception
2019-04-08 17:36:39 +08:00
# We don't stage new users for emails to reply addresses, exit if user is nil
raise BadDestinationAddress if user . blank?
2021-06-21 09:45:00 +08:00
# We only get here if there are no destinations (the email is not going to
# a Category, Group, or PostReplyKey)
2018-08-21 16:17:08 +08:00
post = find_related_post ( force : true )
if post && Guardian . new ( user ) . can_see_post? ( post )
2021-06-21 09:45:00 +08:00
if destination_too_old? ( post )
2018-05-10 00:51:01 +08:00
raise OldDestinationError . new ( " #{ Discourse . base_url } /p/ #{ post . id } " )
end
end
2020-05-14 22:04:58 +08:00
if EmailLog . where ( email_type : " digest " , message_id : @mail . in_reply_to ) . exists?
raise ReplyToDigestError
2023-01-09 20:10:19 +08:00
end
2018-05-10 00:51:01 +08:00
raise BadDestinationAddress
2016-01-19 07:57:55 +08:00
end
end
2014-08-27 08:31:51 +08:00
2017-10-03 17:23:18 +08:00
def log_and_validate_user ( user )
2017-10-03 16:13:19 +08:00
@incoming_email . update_columns ( user_id : user . id )
raise InactiveUserError if ! user . active && ! user . staged
2017-11-11 01:18:08 +08:00
raise SilencedUserError if user . silenced?
2017-10-03 16:13:19 +08:00
end
2022-08-18 23:18:58 +08:00
def get_all_recipients ( mail )
recipients = Set . new
% i [ to cc bcc ] . each do | field |
next if mail [ field ] . blank?
mail [ field ] . each do | address_field |
begin
address_field . decoded
recipients << address_field . address . downcase
end
end
end
recipients
end
2016-05-03 05:15:32 +08:00
def is_bounce?
2018-11-28 10:54:23 +08:00
@mail . bounced? || bounce_key
2018-11-27 02:59:37 +08:00
end
2016-05-03 05:15:32 +08:00
2018-11-27 02:59:37 +08:00
def handle_bounce
2016-05-03 05:15:32 +08:00
@incoming_email . update_columns ( is_bounce : true )
2022-02-15 12:17:26 +08:00
mail_error_statuses = Array . wrap ( @mail . error_status )
2016-05-03 05:15:32 +08:00
2018-11-28 21:34:09 +08:00
if email_log . present?
2022-02-15 12:17:26 +08:00
email_log . update_columns ( bounced : true , bounce_error_code : mail_error_statuses . first )
2018-11-28 23:43:06 +08:00
post = email_log . post
2018-11-27 02:59:37 +08:00
topic = email_log . topic
2016-05-03 05:15:32 +08:00
end
2022-01-07 00:50:37 +08:00
DiscourseEvent . trigger ( :email_bounce , @mail , @incoming_email , @email_log )
2022-02-15 12:17:26 +08:00
if mail_error_statuses . any? { | s | s . start_with? ( Email :: SMTP_STATUS_TRANSIENT_FAILURE ) }
2018-11-28 21:34:09 +08:00
Email :: Receiver . update_bounce_score ( @from_email , SiteSetting . soft_bounce_score )
2016-07-16 00:00:40 +08:00
else
2018-11-28 21:34:09 +08:00
Email :: Receiver . update_bounce_score ( @from_email , SiteSetting . hard_bounce_score )
2016-06-28 22:42:05 +08:00
end
2022-12-17 00:42:51 +08:00
if SiteSetting . whispers_allowed_groups . present? && @from_user & . staged?
2018-11-28 23:43:06 +08:00
return if email_log . blank?
if post . present? && topic . present? && topic . archetype == Archetype . private_message
body , elided = select_body
body || = " "
create_reply (
user : @from_user ,
raw : body ,
elided : elided ,
post : post ,
topic : topic ,
skip_validations : true ,
bounce : true ,
)
end
end
2018-11-27 02:59:37 +08:00
raise BouncedEmailError
2016-05-03 05:15:32 +08:00
end
2018-05-23 16:04:45 +08:00
def is_from_reply_by_email_address?
Email :: Receiver . reply_by_email_address_regex . match ( @from_email )
end
2018-11-28 10:54:23 +08:00
def bounce_key
@bounce_key || =
begin
verp = all_destinations . select { | to | to [ / \ +verp- \ h{32}@ / ] } . first
verp && verp [ / \ +verp-( \ h{32})@ / , 1 ]
end
2016-05-03 05:15:32 +08:00
end
2018-11-28 21:34:09 +08:00
def email_log
2018-11-30 14:59:51 +08:00
return nil if bounce_key . blank?
2018-11-28 21:34:09 +08:00
@email_log || = EmailLog . find_by ( bounce_key : bounce_key )
end
2016-05-30 23:11:17 +08:00
def self . update_bounce_score ( email , score )
2018-05-09 22:40:52 +08:00
if user = User . find_by_email ( email )
old_bounce_score = user . user_stat . bounce_score
new_bounce_score = old_bounce_score + score
range = ( old_bounce_score + 1 .. new_bounce_score )
user . user_stat . bounce_score = new_bounce_score
user . user_stat . reset_bounce_score_after =
SiteSetting . reset_bounce_score_after_days . days . from_now
user . user_stat . save!
2020-01-30 18:47:31 +08:00
if range === SiteSetting . bounce_score_threshold
2018-08-03 22:39:22 +08:00
# NOTE: we check bounce_score before sending emails
# So log we revoked the email...
2018-05-09 22:40:52 +08:00
reason =
I18n . t (
" user.email.revoked " ,
email : user . email ,
date : user . user_stat . reset_bounce_score_after ,
)
StaffActionLogger . new ( Discourse . system_user ) . log_revoke_email ( user , reason )
2018-08-03 22:39:22 +08:00
# ... and PM the user
SystemMessage . create_from_system_user ( user , :email_revoked )
2016-05-03 05:15:32 +08:00
end
end
end
2016-01-19 07:57:55 +08:00
def is_auto_generated?
2020-07-27 08:23:54 +08:00
return false if SiteSetting . auto_generated_allowlist . split ( " | " ) . include? ( @from_email )
2016-03-31 00:41:09 +08:00
@mail [ :precedence ] . to_s [ / list|junk|bulk|auto_reply /i ] ||
2016-08-02 06:04:59 +08:00
@mail [ :from ] . to_s [ / (mailer[ \ -_]?daemon|post[ \ -_]?master|no[ \ -_]?reply)@ /i ] ||
@mail [ :subject ] . to_s [
2023-01-21 02:52:49 +08:00
/ \ A \ s*(Auto:|Automatic reply|Autosvar|Automatisk svar|Automatisch antwoord|Abwesenheitsnotiz|Risposta Non al computer|Automatisch antwoord|Auto Response|Respuesta automática|Fuori sede|Out of Office|Frånvaro|Réponse automatique) /i
2016-08-02 06:04:59 +08:00
] ||
2023-05-04 00:17:19 +08:00
@mail . header . reject { | h | h . name . downcase == " x-auto-response-suppress " } . to_s [
2016-03-31 00:41:09 +08:00
/ auto[ \ -_]?(response|submitted|replied|reply|generated|respond)|holidayreply|machinegenerated /i
]
2014-08-27 08:31:51 +08:00
end
2013-06-20 00:14:01 +08:00
2018-07-05 17:07:46 +08:00
def is_spam?
case SiteSetting . email_in_spam_header
when " X-Spam-Flag "
@mail [ :x_spam_flag ] . to_s [ / YES /i ]
when " X-Spam-Status "
2023-01-21 02:52:49 +08:00
@mail [ :x_spam_status ] . to_s [ / \ AYes, /i ]
2019-10-29 00:46:53 +08:00
when " X-SES-Spam-Verdict "
@mail [ :x_ses_spam_verdict ] . to_s [ / FAIL /i ]
2018-07-05 17:07:46 +08:00
else
false
end
end
2019-11-26 22:55:22 +08:00
def auth_res_action
@auth_res_action || = AuthenticationResults . new ( @mail . header [ :authentication_results ] ) . action
end
2016-01-19 07:57:55 +08:00
def select_body
text = nil
2014-08-27 08:31:51 +08:00
html = nil
2017-12-06 08:47:31 +08:00
text_content_type = nil
2015-12-01 01:33:24 +08:00
2016-01-19 07:57:55 +08:00
if @mail . multipart?
text = fix_charset ( @mail . text_part )
html = fix_charset ( @mail . html_part )
2017-12-06 08:47:31 +08:00
text_content_type = @mail . text_part & . content_type
2016-01-19 07:57:55 +08:00
elsif @mail . content_type . to_s [ " text/html " ]
html = fix_charset ( @mail )
2018-02-17 01:14:56 +08:00
elsif @mail . content_type . blank? || @mail . content_type [ " text/plain " ]
2016-01-19 07:57:55 +08:00
text = fix_charset ( @mail )
2017-12-06 08:47:31 +08:00
text_content_type = @mail . content_type
2014-01-17 10:24:32 +08:00
end
2014-03-28 21:57:12 +08:00
2024-05-27 18:27:13 +08:00
return if text . blank? && html . blank?
2018-02-17 01:14:56 +08:00
2017-12-06 08:47:31 +08:00
if text . present?
2016-06-06 16:30:04 +08:00
text = trim_discourse_markers ( text )
2018-01-17 19:03:57 +08:00
text , elided_text = trim_reply_and_extract_elided ( text )
2017-12-06 08:47:31 +08:00
if @opts [ :convert_plaintext ] || sent_to_mailinglist_mirror?
text_content_type || = " "
converter_opts = {
format_flowed : ! ! ( text_content_type =~ / format \ s*= \ s*["']?flowed["']? /i ) ,
delete_flowed_space : ! ! ( text_content_type =~ / DelSp \ s*= \ s*["']?yes["']? /i ) ,
}
text = PlainTextToMarkdown . new ( text , converter_opts ) . to_markdown
elided_text = PlainTextToMarkdown . new ( elided_text , converter_opts ) . to_markdown
end
2016-06-06 16:30:04 +08:00
end
2017-04-26 22:49:06 +08:00
2024-07-10 15:59:27 +08:00
# keep track of inlined images in html version
# so we can later check if they were elided
@cids = ( html . presence || " " ) . scan ( / src \ s*= \ s*['"](cid:.+?)["'] / ) . flatten
2017-04-27 20:31:11 +08:00
markdown , elided_markdown =
if html . present?
2018-03-02 08:51:15 +08:00
# use the first html extracter that matches
if html_extracter = HTML_EXTRACTERS . select { | _ , r | html [ r ] } . min_by { | _ , r | html =~ r }
2020-05-05 11:46:57 +08:00
doc = Nokogiri :: HTML5 . fragment ( html )
2019-05-07 09:27:05 +08:00
self . public_send ( :" extract_from_ #{ html_extracter [ 0 ] } " , doc )
2018-02-27 06:54:02 +08:00
else
markdown =
HtmlToMarkdown . new ( html , keep_img_tags : true , keep_cid_imgs : true ) . to_markdown
markdown = trim_discourse_markers ( markdown )
trim_reply_and_extract_elided ( markdown )
2023-01-09 20:10:19 +08:00
end
2018-02-27 06:54:02 +08:00
end
2017-04-27 20:31:11 +08:00
2019-04-15 14:26:00 +08:00
text_format = Receiver . formats [ :plaintext ]
2017-04-27 20:31:11 +08:00
if text . blank? || ( SiteSetting . incoming_email_prefer_html && markdown . present? )
2019-04-15 14:26:00 +08:00
text , elided_text , text_format = markdown , elided_markdown , Receiver . formats [ :markdown ]
end
2020-10-02 21:44:35 +08:00
if SiteSetting . strip_incoming_email_lines && text . present?
2019-04-15 14:26:00 +08:00
in_code = nil
text =
text
. lines
. map! do | line |
stripped = line . strip << " \n "
2023-01-09 20:10:19 +08:00
2019-04-16 16:39:16 +08:00
# Do not strip list items.
if ( stripped [ 0 ] == " * " || stripped [ 0 ] == " - " || stripped [ 0 ] == " + " ) &&
stripped [ 1 ] == " "
next line
2019-04-15 14:26:00 +08:00
end
2019-04-16 16:39:16 +08:00
# Match beginning and ending of code blocks.
2019-04-15 14:26:00 +08:00
if ! in_code && stripped [ 0 .. 2 ] == " ``` "
in_code = " ``` "
elsif in_code == " ``` " && stripped [ 0 .. 2 ] == " ``` "
in_code = nil
elsif ! in_code && stripped [ 0 .. 4 ] == " [code "
in_code = " [code] "
elsif in_code == " [code] " && stripped [ 0 .. 6 ] == " [/code] "
in_code = nil
2023-01-09 20:10:19 +08:00
end
2019-04-16 16:39:16 +08:00
# Strip only lines outside code blocks.
2019-04-15 14:26:00 +08:00
in_code ? line : stripped
end
. join
2017-04-26 22:49:06 +08:00
end
2019-04-15 14:26:00 +08:00
[ text , elided_text , text_format ]
2016-01-19 07:57:55 +08:00
end
2018-03-02 08:51:15 +08:00
def to_markdown ( html , elided_html )
markdown = HtmlToMarkdown . new ( html , keep_img_tags : true , keep_cid_imgs : true ) . to_markdown
2020-07-08 13:50:30 +08:00
elided_markdown =
HtmlToMarkdown . new ( elided_html , keep_img_tags : true , keep_cid_imgs : true ) . to_markdown
[ EmailReplyTrimmer . trim ( markdown ) , elided_markdown ]
2018-03-02 08:51:15 +08:00
end
2024-10-16 10:09:07 +08:00
HTML_EXTRACTERS = [
2018-07-05 02:04:46 +08:00
[ :gmail , / class="gmail_(signature|extra) / ] ,
2018-05-03 18:29:21 +08:00
[ :outlook , / id="(divRplyFwdMsg|Signature)" / ] ,
[ :word , / class="WordSection1" / ] ,
[ :exchange , / name="message(Body|Reply)Section" / ] ,
[ :apple_mail , / id="AppleMailSignature" / ] ,
[ :mozilla , / class="moz- / ] ,
[ :protonmail , / class="protonmail_ / ] ,
[ :zimbra , / data-marker="__ / ] ,
2018-04-19 18:39:55 +08:00
[ :newton , / (id|class)="cm_ / ] ,
2021-04-28 23:08:48 +08:00
[ :front , / class="front- / ] ,
2018-03-02 08:51:15 +08:00
]
2018-04-14 01:04:27 +08:00
def extract_from_gmail ( doc )
2018-07-05 02:04:46 +08:00
# GMail adds a bunch of 'gmail_' prefixed classes like: gmail_signature, gmail_extra, gmail_quote, gmail_default...
elided = doc . css ( " .gmail_signature, .gmail_extra " ) . remove
2018-03-02 08:51:15 +08:00
to_markdown ( doc . to_html , elided . to_html )
2018-02-27 22:00:50 +08:00
end
2018-04-14 01:04:27 +08:00
def extract_from_outlook ( doc )
2018-03-02 08:51:15 +08:00
# Outlook properly identifies the signature and any replied/forwarded email
# Use their id to remove them and anything that comes after
elided = doc . css ( " # Signature, # Signature ~ *, hr, # divRplyFwdMsg, # divRplyFwdMsg ~ * " ) . remove
to_markdown ( doc . to_html , elided . to_html )
end
2018-04-14 01:04:27 +08:00
def extract_from_word ( doc )
2018-03-02 08:51:15 +08:00
# Word (?) keeps the content in the 'WordSection1' class and uses <p> tags
# When there's something else (<table>, <div>, etc..) there's high chance it's a signature or forwarded email
elided =
doc . css (
" .WordSection1 > :not(p):not(ul):first-of-type, .WordSection1 > :not(p):not(ul):first-of-type ~ * " ,
) . remove
to_markdown ( doc . at ( " .WordSection1 " ) . to_html , elided . to_html )
end
2018-04-14 01:04:27 +08:00
def extract_from_exchange ( doc )
2018-03-02 08:51:15 +08:00
# Exchange is using the 'messageReplySection' class for forwarded emails
# And 'messageBodySection' for the actual email
elided = doc . css ( " div[name='messageReplySection'] " ) . remove
2018-03-15 05:02:43 +08:00
to_markdown ( doc . css ( " div[name='messageReplySection'] " ) . to_html , elided . to_html )
2018-03-02 08:51:15 +08:00
end
2018-04-14 01:04:27 +08:00
def extract_from_apple_mail ( doc )
2018-03-02 08:51:15 +08:00
# AppleMail is the worst. It adds 'AppleMailSignature' ids (!) to several div/p with no deterministic rules
# Our best guess is to elide whatever comes after that.
2018-03-06 18:34:47 +08:00
elided = doc . css ( " # AppleMailSignature:last-of-type ~ * " ) . remove
2018-03-02 08:51:15 +08:00
to_markdown ( doc . to_html , elided . to_html )
end
2018-04-14 01:04:27 +08:00
def extract_from_mozilla ( doc )
2018-03-02 08:51:15 +08:00
# Mozilla (Thunderbird ?) properly identifies signature and forwarded emails
# Remove them and anything that comes after
2022-04-26 00:57:56 +08:00
elided =
doc . css (
" *[class^='moz-cite'], *[class^='moz-cite'] ~ *, " \
" *[class^='moz-signature'], *[class^='moz-signature'] ~ *, " \
" *[class^='moz-forward'], *[class^='moz-forward'] ~ * " ,
) . remove
2018-03-02 08:51:15 +08:00
to_markdown ( doc . to_html , elided . to_html )
2018-02-27 06:54:02 +08:00
end
2018-04-14 01:04:27 +08:00
def extract_from_protonmail ( doc )
2018-03-30 16:41:32 +08:00
# Removes anything that has a class starting with "protonmail_" and everything after that
elided = doc . css ( " *[class^='protonmail_'], *[class^='protonmail_'] ~ * " ) . remove
to_markdown ( doc . to_html , elided . to_html )
end
2018-04-14 01:04:27 +08:00
def extract_from_zimbra ( doc )
# Removes anything that has a 'data-marker' attribute
elided = doc . css ( " *[data-marker] " ) . remove
to_markdown ( doc . to_html , elided . to_html )
end
2018-04-19 18:39:55 +08:00
def extract_from_newton ( doc )
# Removes anything that has an id or a class starting with 'cm_'
elided = doc . css ( " *[id^='cm_'], *[class^='cm_'] " ) . remove
to_markdown ( doc . to_html , elided . to_html )
end
2021-04-28 23:08:48 +08:00
def extract_from_front ( doc )
# Removes anything that has a class starting with 'front-'
elided = doc . css ( " *[class^='front-'] " ) . remove
to_markdown ( doc . to_html , elided . to_html )
end
2018-01-17 19:03:57 +08:00
def trim_reply_and_extract_elided ( text )
2021-04-28 23:08:48 +08:00
return text , " " if @opts [ :skip_trimming ] || ! SiteSetting . trim_incoming_emails
2018-01-17 19:03:57 +08:00
EmailReplyTrimmer . trim ( text , true )
end
2016-01-19 07:57:55 +08:00
def fix_charset ( mail_part )
return nil if mail_part . blank? || mail_part . body . blank?
2016-01-30 08:29:31 +08:00
string =
2023-01-09 20:10:19 +08:00
begin
2016-01-30 08:29:31 +08:00
mail_part . body . decoded
rescue StandardError
nil
2023-01-09 20:10:19 +08:00
end
2013-06-21 00:38:03 +08:00
2016-01-30 08:29:31 +08:00
return nil if string . blank?
2015-05-23 03:40:26 +08:00
2016-06-26 19:27:34 +08:00
# common encodings
2018-01-31 06:45:04 +08:00
encodings = COMMON_ENCODINGS . dup
2016-06-26 19:27:34 +08:00
encodings . unshift ( mail_part . charset ) if mail_part . charset . present?
2021-04-28 23:08:48 +08:00
# mail (>=2.5) decodes mails with 8bit transfer encoding to utf-8, so always try UTF-8 first
2017-05-01 05:30:40 +08:00
if mail_part . content_transfer_encoding == " 8bit "
encodings . delete ( " UTF-8 " )
encodings . unshift ( " UTF-8 " )
end
2016-06-26 19:27:34 +08:00
encodings . uniq . each do | encoding |
fixed = try_to_encode ( string , encoding )
2016-01-19 07:57:55 +08:00
return fixed if fixed . present?
2014-08-27 08:31:51 +08:00
end
2013-11-21 02:29:42 +08:00
2016-06-26 19:27:34 +08:00
nil
2016-01-19 07:57:55 +08:00
end
def try_to_encode ( string , encoding )
2016-03-31 01:54:38 +08:00
encoded = string . encode ( " UTF-8 " , encoding )
2017-05-27 04:26:18 +08:00
! encoded . nil? && encoded . valid_encoding? ? encoded : nil
2016-03-12 01:51:53 +08:00
rescue Encoding :: InvalidByteSequenceError ,
Encoding :: UndefinedConversionError ,
Encoding :: ConverterNotFoundError
2016-01-19 07:57:55 +08:00
nil
2013-06-21 00:38:03 +08:00
end
2016-01-30 08:29:31 +08:00
def previous_replies_regex
2021-08-19 00:42:04 +08:00
strings =
I18n
. available_locales
. map do | locale |
I18n . with_locale ( locale ) { I18n . t ( " user_notifications.previous_discussion " ) }
end
. uniq
@previous_replies_regex || =
2023-01-21 02:52:49 +08:00
/ \ A--[- ] \ n \ *(?: #{ strings . map { | x | Regexp . escape ( x ) } . join ( " | " ) } ) \ * \ n /im
2016-01-30 08:29:31 +08:00
end
2021-08-04 01:08:19 +08:00
def reply_above_line_regex
2021-08-19 00:42:04 +08:00
strings =
I18n
. available_locales
. map do | locale |
I18n . with_locale ( locale ) { I18n . t ( " user_notifications.reply_above_line " ) }
end
. uniq
@reply_above_line_regex || = / \ n(?: #{ strings . map { | x | Regexp . escape ( x ) } . join ( " | " ) } ) \ n /im
2021-08-04 01:08:19 +08:00
end
2016-01-30 08:29:31 +08:00
def trim_discourse_markers ( reply )
2021-08-10 21:49:32 +08:00
return " " if reply . blank?
2021-08-04 01:08:19 +08:00
reply = reply . split ( previous_replies_regex ) [ 0 ]
reply . split ( reply_above_line_regex ) [ 0 ]
2016-01-30 08:29:31 +08:00
end
2023-05-19 16:33:48 +08:00
def parse_from_field ( mail , process_forwarded_emails : true )
2019-05-03 19:12:44 +08:00
if email_log . present?
email = email_log . to_address || email_log . user & . email
return email , email_log . user & . username
elsif mail . bounced?
2023-05-19 16:33:48 +08:00
# "Final-Recipient" has a specific format (<name> ; <address>)
# cf. https://www.ietf.org/rfc/rfc2298.html#section-3.2.4
address_type , generic_address = mail . final_recipient . to_s . split ( " ; " ) . map { _1 . to_s . strip }
return generic_address , nil if generic_address . include? ( " @ " ) && address_type == " rfc822 "
2018-11-27 02:59:37 +08:00
end
2017-01-10 05:59:30 +08:00
return unless mail [ :from ]
2021-08-24 06:57:28 +08:00
# For forwarded emails, where the from address matches a group incoming
# email, we want to use the from address of the original email sender,
# which we can extract from embedded_email_raw.
2023-05-19 16:33:48 +08:00
if process_forwarded_emails && has_been_forwarded?
if mail [ :from ] . to_s =~ group_incoming_emails_regex
if embedded_email && embedded_email [ :from ] . errors . blank?
from_address , from_display_name =
Email :: Receiver . extract_email_address_and_name ( embedded_email [ :from ] )
return from_address , from_display_name if from_address
end
2021-08-24 06:57:28 +08:00
end
end
2023-05-19 16:33:48 +08:00
# extract proper sender when using mailman mailing list
if mail [ :x_mailman_version ] . present?
address , name = Email :: Receiver . extract_email_address_and_name_from_mailman ( mail )
return address , name if address
end
2021-08-03 06:01:17 +08:00
# For now we are only using the Reply-To header if the email has
# been forwarded via Google Groups, which is why we are checking the
# X-Original-From header too. In future we may want to use the Reply-To
# header in more cases.
2023-05-19 16:33:48 +08:00
if mail [ :x_original_from ] . present? && mail [ :reply_to ] . present?
original_from_address , _ =
Email :: Receiver . extract_email_address_and_name ( mail [ :x_original_from ] )
reply_to_address , reply_to_name =
Email :: Receiver . extract_email_address_and_name ( mail [ :reply_to ] )
return reply_to_address , reply_to_name if original_from_address == reply_to_address
2016-02-25 00:40:57 +08:00
end
2016-11-17 02:42:11 +08:00
2023-05-19 16:33:48 +08:00
Email :: Receiver . extract_email_address_and_name ( mail [ :from ] )
2017-09-15 22:47:19 +08:00
rescue StandardError
2017-09-13 04:35:24 +08:00
nil
end
2023-05-19 16:33:48 +08:00
def self . extract_email_address_and_name_from_mailman ( mail )
list_address , _ = Email :: Receiver . extract_email_address_and_name ( mail [ :list_post ] )
list_address , _ =
Email :: Receiver . extract_email_address_and_name ( mail [ :x_beenthere ] ) if list_address . blank?
2021-09-06 13:02:13 +08:00
2023-05-19 16:33:48 +08:00
return if list_address . blank?
2021-09-06 13:02:13 +08:00
2023-05-19 16:33:48 +08:00
# the CC header often includes the name of the sender
address_to_name = mail [ :cc ] & . element & . addresses & . to_h { [ _1 . address , _1 . name ] } || { }
2018-11-27 02:59:37 +08:00
2023-05-19 16:33:48 +08:00
% i [ from reply_to x_mailfrom x_original_from ] . each do | header |
next if mail [ header ] . blank?
email , name = Email :: Receiver . extract_email_address_and_name ( mail [ header ] )
if email . present? && email != list_address
return email , name . presence || address_to_name [ email ]
end
2016-12-02 01:34:47 +08:00
end
2023-05-19 16:33:48 +08:00
end
2016-12-02 01:34:47 +08:00
2023-05-19 16:33:48 +08:00
def self . extract_email_address_and_name ( value )
begin
# ensure the email header value is a string
value = value . to_s
# in embedded emails, converts [mailto:foo@bar.com] to <foo@bar.com>
value = value . gsub ( / \ [mailto:([^ \ [ \ ]]+?) \ ] / , " < \\ 1> " )
# 'mailto:' suffix isn't supported by Mail::Address parsing
value = value . gsub ( " mailto: " , " " )
# parse the email header value
parsed = Mail :: Address . new ( value )
# extract the email address and name
mail = parsed . address . to_s . downcase . strip
name = parsed . name . to_s . strip
# ensure the email address is "valid"
if mail . include? ( " @ " )
# remove surrounding quotes from the name
name = name [ 1 ... - 1 ] if name . size > 2 && name [ / \ A(['"]).+( \ 1) \ z / ]
# return the email address and name
[ mail , name ]
end
2024-07-10 15:59:27 +08:00
rescue Mail :: Field :: ParseError , Mail :: Field :: IncompleteParseError
2023-05-19 16:33:48 +08:00
# something went wrong parsing the email header value, return nil
2016-12-02 01:34:47 +08:00
end
2016-01-19 07:57:55 +08:00
end
2016-02-01 19:16:15 +08:00
def subject
2018-10-11 10:45:01 +08:00
@subject || =
if mail_subject = @mail . subject
2020-07-10 17:05:55 +08:00
mail_subject . delete ( " \ u0000 " ) [ 0 .. 254 ]
2018-10-11 10:45:01 +08:00
else
I18n . t ( " emails.incoming.default_subject " , email : @from_email )
end
2016-02-01 19:16:15 +08:00
end
2013-06-21 00:38:03 +08:00
2022-08-18 23:19:20 +08:00
def find_or_create_user ( email , display_name , raise_on_failed_create : false , user : nil )
2016-03-24 01:56:03 +08:00
User . transaction do
2022-08-18 23:19:20 +08:00
user || = User . find_by_email ( email )
2016-04-19 04:58:30 +08:00
2017-10-03 17:23:18 +08:00
if user . nil? && SiteSetting . enable_staged_users
raise EmailNotAllowed unless EmailValidator . allowed? ( email )
2018-10-08 15:45:23 +08:00
username = UserNameSuggester . sanitize_username ( display_name ) if display_name . present?
2017-10-03 17:23:18 +08:00
begin
2016-04-19 04:58:30 +08:00
user =
User . create! (
email : email ,
username : UserNameSuggester . suggest ( username . presence || email ) ,
name : display_name . presence || User . suggest_name ( email ) ,
staged : true ,
)
2022-08-18 23:19:20 +08:00
@created_staged_users << user
2018-10-08 15:45:23 +08:00
rescue PG :: UniqueViolation , ActiveRecord :: RecordNotUnique , ActiveRecord :: RecordInvalid
raise if raise_on_failed_create
2017-10-03 17:23:18 +08:00
user = nil
2016-04-19 04:58:30 +08:00
end
2016-03-24 01:56:03 +08:00
end
2022-08-18 23:19:20 +08:00
@staged_users << user if user & . staged?
2014-08-27 08:31:51 +08:00
end
2016-03-24 01:56:03 +08:00
user
2013-06-20 00:14:01 +08:00
end
2013-06-14 06:11:10 +08:00
2018-10-08 15:45:23 +08:00
def find_or_create_user! ( email , display_name )
find_or_create_user ( email , display_name , raise_on_failed_create : true )
end
2016-05-07 01:34:33 +08:00
def all_destinations
@all_destinations || = [
@mail . destinations ,
2016-01-19 07:57:55 +08:00
[ @mail [ :x_forwarded_to ] ] . flatten . compact . map ( & :decoded ) ,
[ @mail [ :delivered_to ] ] . flatten . compact . map ( & :decoded ) ,
2016-05-07 01:34:33 +08:00
] . flatten . select ( & :present? ) . uniq . lazy
end
def destinations
2017-11-17 21:49:10 +08:00
@destinations || =
2018-11-28 10:54:23 +08:00
all_destinations . map { | d | Email :: Receiver . check_address ( d , is_bounce? ) } . reject ( & :blank? )
2017-11-17 21:49:10 +08:00
end
def sent_to_mailinglist_mirror?
2018-08-21 15:44:47 +08:00
@sent_to_mailinglist_mirror || =
begin
destinations . each do | destination |
2020-07-10 17:05:55 +08:00
return true if destination . is_a? ( Category ) && destination . mailinglist_mirror?
2018-08-21 15:44:47 +08:00
end
2017-11-17 21:49:10 +08:00
2018-08-21 15:44:47 +08:00
false
end
2015-11-19 04:22:50 +08:00
end
2018-11-28 10:54:23 +08:00
def self . check_address ( address , include_verp = false )
2016-01-19 07:57:55 +08:00
# only check for a group/category when 'email_in' is enabled
if SiteSetting . email_in
group = Group . find_by_email ( address )
2020-07-10 17:05:55 +08:00
return group if group
2013-07-25 02:22:32 +08:00
2016-01-19 07:57:55 +08:00
category = Category . find_by_email ( address )
2020-07-10 17:05:55 +08:00
return category if category
2015-11-19 04:22:50 +08:00
end
2016-01-19 07:57:55 +08:00
# reply
2018-11-28 10:54:23 +08:00
match = Email :: Receiver . reply_by_email_address_regex ( true , include_verp ) . match ( address )
2016-06-10 22:14:42 +08:00
if match && match . captures
match . captures . each do | c |
next if c . blank?
2018-07-18 16:28:44 +08:00
post_reply_key = PostReplyKey . find_by ( reply_key : c )
2020-07-10 17:05:55 +08:00
return post_reply_key if post_reply_key
2016-06-10 22:14:42 +08:00
end
2013-07-25 02:22:32 +08:00
end
2017-06-09 02:28:48 +08:00
nil
2016-01-19 07:57:55 +08:00
end
2013-07-25 02:22:32 +08:00
2020-01-22 00:12:00 +08:00
def process_destination ( destination , user , body , elided )
2019-08-07 18:32:19 +08:00
if SiteSetting . forwarded_emails_behaviour != " hide " && has_been_forwarded? &&
2016-11-17 02:42:11 +08:00
process_forwarded_email ( destination , user )
2023-01-09 20:10:19 +08:00
return
end
2016-11-17 02:42:11 +08:00
2020-07-10 17:05:55 +08:00
return if is_bounce? && ! destination . is_a? ( PostReplyKey )
2018-11-27 02:59:37 +08:00
2020-07-10 17:05:55 +08:00
if destination . is_a? ( Group )
2019-04-08 17:36:39 +08:00
user || = stage_from_user
2020-07-10 17:05:55 +08:00
create_group_post ( destination , user , body , elided )
elsif destination . is_a? ( Category )
if ( user . nil? || user . staged? ) && ! destination . email_in_allow_strangers
raise StrangersNotAllowedError
2023-01-09 20:10:19 +08:00
end
2019-04-08 17:36:39 +08:00
user || = stage_from_user
2023-11-28 22:34:02 +08:00
if ! user . staged? && ! user . in_any_groups? ( SiteSetting . email_in_allowed_groups_map ) &&
2023-11-23 09:03:28 +08:00
! sent_to_mailinglist_mirror?
2018-01-04 20:38:06 +08:00
raise InsufficientTrustLevelError
2017-06-29 12:03:14 +08:00
end
2016-08-03 21:57:37 +08:00
2018-03-30 20:37:19 +08:00
create_topic (
2019-08-07 18:32:19 +08:00
user : user ,
2021-06-21 09:45:00 +08:00
raw : body ,
elided : elided ,
2018-03-30 20:37:19 +08:00
title : subject ,
2020-07-10 17:05:55 +08:00
category : destination . id ,
2018-11-27 02:59:37 +08:00
skip_validations : user . staged? ,
2023-01-09 20:10:19 +08:00
)
2020-07-10 17:05:55 +08:00
elsif destination . is_a? ( PostReplyKey )
2019-04-08 17:36:39 +08:00
# We don't stage new users for emails to reply addresses, exit if user is nil
raise BadDestinationAddress if user . blank?
2020-07-10 17:05:55 +08:00
post = Post . with_deleted . find ( destination . post_id )
2019-06-03 05:49:05 +08:00
raise ReplyNotAllowedError if ! Guardian . new ( user ) . can_create_post? ( post & . topic )
2016-08-03 21:57:37 +08:00
2020-07-10 17:05:55 +08:00
if destination . user_id != user . id && ! forwarded_reply_key? ( destination , user )
raise ReplyUserNotMatchingError ,
" post_reply_key.user_id => #{ destination . user_id . inspect } , user.id => #{ user . id . inspect } "
2016-08-03 21:57:37 +08:00
end
create_reply (
user : user ,
raw : body ,
2016-11-17 02:42:11 +08:00
elided : elided ,
2018-09-04 05:06:25 +08:00
post : post ,
topic : post & . topic ,
2018-11-27 02:59:37 +08:00
skip_validations : user . staged? ,
bounce : is_bounce? ,
)
2016-08-03 21:57:37 +08:00
end
end
2020-01-22 00:12:00 +08:00
def create_group_post ( group , user , body , elided )
2018-03-30 20:37:19 +08:00
message_ids = Email :: Receiver . extract_reply_message_ids ( @mail , max_message_id_count : 5 )
2024-11-26 09:12:40 +08:00
# Incoming emails with matching message ids, and then cross references
2021-06-10 13:28:50 +08:00
# these with any email addresses for the user vs to/from/cc of the
# incoming emails. in effect, any incoming email record for these
2024-11-26 09:12:40 +08:00
# message ids where the user is involved in any way will be returned.
2021-01-29 07:59:10 +08:00
incoming_emails = IncomingEmail . where ( message_id : message_ids )
if ! group . allow_unknown_sender_topic_replies
incoming_emails = incoming_emails . addressed_to_user ( user )
end
2021-06-10 13:28:50 +08:00
post_ids = incoming_emails . pluck ( :post_id ) || [ ]
2024-11-26 09:12:40 +08:00
# If the user is directly replying to an email send to them from discourse,
2021-06-10 13:28:50 +08:00
# there will be a corresponding EmailLog record, so we can use that as the
2024-11-26 09:12:40 +08:00
# reply post if it exists.
#
# Since In-Reply-To can technically have multiple message ids, we only
# consider the first one here to simplify things.
first_in_reply_to = Array . wrap ( mail . in_reply_to ) . first
if Email :: MessageIdService . discourse_generated_message_id? ( first_in_reply_to )
2021-06-10 13:28:50 +08:00
post_id_from_email_log =
EmailLog
2024-11-26 09:12:40 +08:00
. where ( message_id : first_in_reply_to )
2021-06-10 13:28:50 +08:00
. addressed_to_user ( user )
. order ( created_at : :desc )
. limit ( 1 )
. pluck ( :post_id )
. last
2021-06-21 09:45:00 +08:00
post_ids << post_id_from_email_log if post_id_from_email_log
2018-03-30 20:37:19 +08:00
end
2021-06-21 09:45:00 +08:00
target_post = post_ids . any? && Post . where ( id : post_ids ) . order ( :created_at ) . last
too_old_for_group_smtp = ( destination_too_old? ( target_post ) && group . smtp_enabled )
2018-10-16 07:51:57 +08:00
2021-06-21 09:45:00 +08:00
if target_post . blank? || too_old_for_group_smtp
2018-03-30 20:37:19 +08:00
create_topic (
user : user ,
2021-06-21 09:45:00 +08:00
raw : new_group_topic_body ( body , target_post , too_old_for_group_smtp ) ,
2018-03-30 20:37:19 +08:00
elided : elided ,
title : subject ,
archetype : Archetype . private_message ,
target_group_names : [ group . name ] ,
is_group_message : true ,
skip_validations : true ,
)
2021-06-21 09:45:00 +08:00
else
# This must be done for the unknown user (who is staged) to
# be allowed to post a reply in the topic.
if group . allow_unknown_sender_topic_replies
target_post . topic . topic_allowed_users . find_or_create_by! ( user_id : user . id )
end
create_reply (
user : user ,
raw : body ,
elided : elided ,
post : target_post ,
topic : target_post . topic ,
skip_validations : true ,
)
2018-03-30 20:37:19 +08:00
end
end
2021-06-21 09:45:00 +08:00
def new_group_topic_body ( body , target_post , too_old_for_group_smtp )
return body if ! too_old_for_group_smtp
body + " \n \n ---- \n \n " +
I18n . t (
" emails.incoming.continuing_old_discussion " ,
url : target_post . topic . url ,
title : target_post . topic . title ,
count : SiteSetting . disallow_reply_by_email_after_days ,
)
end
2018-07-18 16:28:44 +08:00
def forwarded_reply_key? ( post_reply_key , user )
2017-11-13 06:44:22 +08:00
incoming_emails =
IncomingEmail
. joins ( :post )
2018-07-18 16:28:44 +08:00
. where ( " posts.topic_id = ? " , post_reply_key . post . topic_id )
. addressed_to ( post_reply_key . reply_key )
2018-03-30 20:37:19 +08:00
. addressed_to_user ( user )
. pluck ( :to_addresses , :cc_addresses )
2017-11-13 06:44:22 +08:00
2018-03-30 20:37:19 +08:00
incoming_emails . each do | to_addresses , cc_addresses |
unless contains_email_address_of_user? ( to_addresses , user ) ||
contains_email_address_of_user? ( cc_addresses , user )
2023-01-09 20:10:19 +08:00
next
end
2017-11-13 06:44:22 +08:00
2018-07-18 16:28:44 +08:00
if contains_reply_by_email_address ( to_addresses , post_reply_key . reply_key ) ||
contains_reply_by_email_address ( cc_addresses , post_reply_key . reply_key )
2019-08-07 18:32:19 +08:00
return true
2023-01-09 20:10:19 +08:00
end
2017-11-13 06:44:22 +08:00
end
false
end
2018-03-30 20:37:19 +08:00
def contains_email_address_of_user? ( addresses , user )
2017-11-13 06:44:22 +08:00
return false if addresses . blank?
2018-03-30 20:37:19 +08:00
addresses = addresses . split ( " ; " )
user . user_emails . any? { | user_email | addresses . include? ( user_email . email ) }
2017-11-13 06:44:22 +08:00
end
def contains_reply_by_email_address ( addresses , reply_key )
return false if addresses . blank?
addresses
. split ( " ; " )
. each do | address |
match = Email :: Receiver . reply_by_email_address_regex . match ( address )
return true if match && match . captures & . include? ( reply_key )
end
false
end
2016-11-17 02:42:11 +08:00
def has_been_forwarded?
2023-01-21 02:52:49 +08:00
subject [ / \ A[[:blank:]]*(fwd?|tr)[[:blank:]]?: /i ] && embedded_email_raw . present?
2016-11-17 02:42:11 +08:00
end
def embedded_email_raw
return @embedded_email_raw if @embedded_email_raw
text = fix_charset ( @mail . multipart? ? @mail . text_part : @mail )
@embedded_email_raw , @before_embedded = EmailReplyTrimmer . extract_embedded_email ( text )
@embedded_email_raw
end
2021-08-24 06:57:28 +08:00
def embedded_email
2021-10-06 20:07:29 +08:00
@embedded_email || =
if embedded_email_raw . present?
mail = Mail . new ( embedded_email_raw )
Email :: Validator . ensure_valid_address_lists! ( mail )
mail
else
nil
end
2021-08-24 06:57:28 +08:00
end
2016-11-17 02:42:11 +08:00
def process_forwarded_email ( destination , user )
2019-04-08 17:36:39 +08:00
user || = stage_from_user
2019-08-07 18:32:19 +08:00
case SiteSetting . forwarded_emails_behaviour
when " create_replies "
forwarded_email_create_replies ( destination , user )
when " quote "
forwarded_email_quote_forwarded ( destination , user )
else
false
end
end
2016-11-17 02:42:11 +08:00
2019-08-07 18:32:19 +08:00
def forwarded_email_create_topic (
destination : ,
user : ,
raw : ,
title : ,
date : nil ,
embedded_user : nil
)
2020-07-10 17:05:55 +08:00
if destination . is_a? ( Group )
2019-08-07 18:32:19 +08:00
topic_user = embedded_user & . call || user
create_topic (
user : topic_user ,
raw : raw ,
title : title ,
archetype : Archetype . private_message ,
target_usernames : [ user . username ] ,
2020-07-10 17:05:55 +08:00
target_group_names : [ destination . name ] ,
2019-08-07 18:32:19 +08:00
is_group_message : true ,
skip_validations : true ,
created_at : date ,
)
2020-07-10 17:05:55 +08:00
elsif destination . is_a? ( Category )
return false if user . staged? && ! destination . email_in_allow_strangers
2023-11-23 09:03:28 +08:00
if user . groups . any? && ! user . in_any_groups? ( SiteSetting . email_in_allowed_groups_map )
return false
end
2016-11-17 02:42:11 +08:00
2019-08-07 18:32:19 +08:00
topic_user = embedded_user & . call || user
create_topic (
user : topic_user ,
raw : raw ,
title : title ,
2020-07-10 17:05:55 +08:00
category : destination . id ,
2019-08-07 18:32:19 +08:00
skip_validations : topic_user . staged? ,
created_at : date ,
)
2016-11-17 02:42:11 +08:00
else
2019-08-07 18:32:19 +08:00
false
2016-11-17 02:42:11 +08:00
end
2019-08-07 18:32:19 +08:00
end
def forwarded_email_create_replies ( destination , user )
2023-05-19 16:33:48 +08:00
forwarded_by_address , forwarded_by_name =
Email :: Receiver . extract_email_address_and_name ( @mail [ :from ] )
2019-08-07 18:32:19 +08:00
2021-09-06 13:02:13 +08:00
if forwarded_by_address && forwarded_by_name
2021-09-07 06:46:28 +08:00
@forwarded_by_user = stage_sender_user ( forwarded_by_address , forwarded_by_name )
2021-09-06 13:02:13 +08:00
end
2023-05-19 16:33:48 +08:00
email_address , display_name =
parse_from_field ( embedded_email , process_forwarded_emails : false )
return false if email_address . blank? || ! email_address . include? ( " @ " )
2019-08-07 18:32:19 +08:00
post =
forwarded_email_create_topic (
destination : destination ,
user : user ,
2023-05-19 16:33:48 +08:00
raw : try_to_encode ( embedded_email . decoded , " UTF-8 " ) . presence || embedded_email . to_s ,
title : embedded_email . subject . presence || subject ,
date : embedded_email . date ,
embedded_user : lambda { find_or_create_user ( email_address , display_name ) } ,
2019-08-07 18:32:19 +08:00
)
2023-05-19 16:33:48 +08:00
2019-08-07 18:32:19 +08:00
return false unless post
2016-11-17 02:42:11 +08:00
2020-02-11 22:48:58 +08:00
if post . topic
2017-01-06 22:32:25 +08:00
# mark post as seen for the forwarder
PostTiming . record_timing (
user_id : user . id ,
topic_id : post . topic_id ,
post_number : post . post_number ,
msecs : 5000 ,
)
2016-12-02 01:43:56 +08:00
2017-01-06 22:32:25 +08:00
# create reply when available
if @before_embedded . present?
post_type = Post . types [ :regular ]
2020-07-10 17:05:55 +08:00
post_type = Post . types [ :whisper ] if post . topic . private_message? &&
destination . usernames [ user . username ]
2023-01-09 20:10:19 +08:00
2017-01-06 22:32:25 +08:00
create_reply (
user : user ,
raw : @before_embedded ,
post : post ,
topic : post . topic ,
2017-02-09 04:38:52 +08:00
post_type : post_type ,
skip_validations : user . staged? ,
)
2020-02-11 22:48:58 +08:00
else
2021-09-07 06:46:28 +08:00
if @forwarded_by_user
post . topic . topic_allowed_users . find_or_create_by! ( user_id : @forwarded_by_user . id )
2021-09-06 13:02:13 +08:00
end
2021-09-07 06:46:28 +08:00
post . topic . add_small_action ( @forwarded_by_user || user , " forwarded " )
2017-01-06 22:32:25 +08:00
end
2016-11-17 02:42:11 +08:00
end
true
end
2019-08-07 18:32:19 +08:00
def forwarded_email_quote_forwarded ( destination , user )
DEV: Correctly tag heredocs (#16061)
This allows text editors to use correct syntax coloring for the heredoc sections.
Heredoc tag names we use:
languages: SQL, JS, RUBY, LUA, HTML, CSS, SCSS, SH, HBS, XML, YAML/YML, MF, ICS
other: MD, TEXT/TXT, RAW, EMAIL
2022-03-01 03:50:55 +08:00
raw = << ~ MD
2019-08-07 18:32:19 +08:00
#{@before_embedded}
[ quote ]
2023-05-19 16:33:48 +08:00
#{PlainTextToMarkdown.new(@embedded_email_raw).to_markdown}
2019-08-07 18:32:19 +08:00
[ / quote]
DEV: Correctly tag heredocs (#16061)
This allows text editors to use correct syntax coloring for the heredoc sections.
Heredoc tag names we use:
languages: SQL, JS, RUBY, LUA, HTML, CSS, SCSS, SH, HBS, XML, YAML/YML, MF, ICS
other: MD, TEXT/TXT, RAW, EMAIL
2022-03-01 03:50:55 +08:00
MD
2019-08-07 18:32:19 +08:00
if forwarded_email_create_topic (
destination : destination ,
user : user ,
raw : raw ,
title : subject ,
)
true
2023-01-09 20:10:19 +08:00
end
2019-08-07 18:32:19 +08:00
end
2018-11-28 10:54:23 +08:00
def self . reply_by_email_address_regex ( extract_reply_key = true , include_verp = false )
2017-05-23 05:35:41 +08:00
reply_addresses = [ SiteSetting . reply_by_email_address ]
reply_addresses << ( SiteSetting . alternative_reply_by_email_addresses . presence || " " ) . split (
" | " ,
)
2018-11-28 10:54:23 +08:00
if include_verp && SiteSetting . reply_by_email_address . present? &&
SiteSetting . reply_by_email_address [ " + " ]
reply_addresses << SiteSetting . reply_by_email_address . sub (
" %{reply_key} " ,
" verp-%{reply_key} " ,
)
end
2017-05-23 05:35:41 +08:00
reply_addresses . flatten!
reply_addresses . select! ( & :present? )
reply_addresses . map! { | a | Regexp . escape ( a ) }
2018-05-23 16:04:45 +08:00
reply_addresses . map! { | a | a . gsub ( " \ + " , " \ +? " ) }
reply_addresses . map! { | a | a . gsub ( Regexp . escape ( " %{reply_key} " ) , " ( \\ h{32})? " ) }
if reply_addresses . empty?
/ $a / # a regex that can never match
else
/ #{ reply_addresses . join ( " | " ) } /
end
2013-07-25 02:22:32 +08:00
end
2016-01-21 06:08:27 +08:00
def group_incoming_emails_regex
2021-06-03 12:47:32 +08:00
@group_incoming_emails_regex =
Regexp . union ( DB . query_single ( << ~ SQL ) . map { | e | e . split ( " | " ) } . flatten . compact_blank . uniq )
SELECT CONCAT ( incoming_email , '|' , email_username )
FROM groups
WHERE incoming_email IS NOT NULL OR email_username IS NOT NULL
SQL
2016-01-21 06:08:27 +08:00
end
def category_email_in_regex
2016-02-25 02:47:58 +08:00
@category_email_in_regex || =
Regexp . union Category
. pluck ( :email_in )
. select ( & :present? )
. map { | e | e . split ( " | " ) }
. flatten
. uniq
2016-01-21 06:08:27 +08:00
end
2018-05-10 00:51:01 +08:00
def find_related_post ( force : false )
return if ! force && SiteSetting . find_related_post_with_key && ! sent_to_mailinglist_mirror?
2017-06-19 19:12:55 +08:00
2018-03-30 20:37:19 +08:00
message_ids = Email :: Receiver . extract_reply_message_ids ( @mail , max_message_id_count : 5 )
2016-01-19 07:57:55 +08:00
return if message_ids . empty?
2021-12-06 08:34:39 +08:00
Email :: MessageIdService . find_post_from_message_ids ( message_ids )
2020-08-03 11:10:17 +08:00
end
2018-03-30 20:37:19 +08:00
def self . extract_reply_message_ids ( mail , max_message_id_count : )
message_ids = [ mail . in_reply_to , Email :: Receiver . extract_references ( mail . references ) ]
message_ids . flatten!
message_ids . select! ( & :present? )
message_ids . uniq!
message_ids . first ( max_message_id_count )
end
2016-02-11 05:00:27 +08:00
def self . extract_references ( references )
if Array === references
references
elsif references . present?
2021-12-06 08:34:39 +08:00
references . split ( / [ \ s,] / ) . map { | r | Email :: MessageIdService . message_id_clean ( r ) }
2016-01-19 07:57:55 +08:00
end
2014-04-15 04:55:57 +08:00
end
2016-01-19 07:57:55 +08:00
def likes
2016-07-05 21:59:23 +08:00
@likes || = Set . new [ " +1 " , " <3 " , " ❤ " , I18n . t ( " post_action_types.like.title " ) . downcase ]
2015-12-30 19:17:45 +08:00
end
2016-01-20 17:25:25 +08:00
def subscription_action_for ( body , subject )
return unless SiteSetting . unsubscribe_via_email
2018-10-11 22:08:57 +08:00
return if sent_to_mailinglist_mirror?
2016-01-20 17:25:25 +08:00
if ( [ subject , body ] . compact . map ( & :to_s ) . map ( & :downcase ) & [ " unsubscribe " ] ) . any?
:confirm_unsubscribe
end
end
2015-12-30 19:17:45 +08:00
def post_action_for ( body )
2017-05-23 05:35:41 +08:00
PostActionType . types [ :like ] if likes . include? ( body . strip . downcase )
2015-12-30 19:17:45 +08:00
end
2016-01-19 07:57:55 +08:00
def create_topic ( options = { } )
2021-06-21 09:45:00 +08:00
enable_email_pm_setting ( options [ :user ] ) if options [ :archetype ] == Archetype . private_message
2016-01-19 07:57:55 +08:00
create_post_with_attachments ( options )
2013-06-11 04:46:08 +08:00
end
2014-02-25 00:36:53 +08:00
2019-03-06 15:38:49 +08:00
def notification_level_for ( body )
# since we are stripping save all this work on long replies
return nil if body . length > 40
body = body . strip . downcase
case body
when " mute "
NotificationLevels . topic_levels [ :muted ]
when " track "
NotificationLevels . topic_levels [ :tracking ]
when " watch "
NotificationLevels . topic_levels [ :watching ]
else
nil
end
end
2016-01-19 07:57:55 +08:00
def create_reply ( options = { } )
raise TopicNotFoundError if options [ :topic ] . nil? || options [ :topic ] . trashed?
2018-11-27 02:59:37 +08:00
if options [ :bounce ] && options [ :topic ] . archetype != Archetype . private_message
raise BouncedEmailError
2023-01-09 20:10:19 +08:00
end
2018-11-27 02:59:37 +08:00
2018-09-04 05:06:25 +08:00
options [ :post ] = nil if options [ :post ] & . trashed?
2018-10-16 07:51:57 +08:00
if options [ :topic ] . archetype == Archetype . private_message
enable_email_pm_setting ( options [ :user ] )
2023-01-09 20:10:19 +08:00
end
2014-04-15 04:55:57 +08:00
2016-01-19 07:57:55 +08:00
if post_action_type = post_action_for ( options [ :raw ] )
create_post_action ( options [ :user ] , options [ :post ] , post_action_type )
2019-03-06 15:38:49 +08:00
elsif notification_level = notification_level_for ( options [ :raw ] )
TopicUser . change (
options [ :user ] . id ,
options [ :post ] . topic_id ,
notification_level : notification_level ,
)
2016-01-19 07:57:55 +08:00
else
2016-07-05 23:33:08 +08:00
raise TopicClosedError if options [ :topic ] . closed?
2018-09-04 05:06:25 +08:00
options [ :topic_id ] = options [ :topic ] . id
options [ :reply_to_post_number ] = options [ :post ] & . post_number
2016-03-01 05:39:24 +08:00
options [ :is_group_message ] = options [ :topic ] . private_message? &&
options [ :topic ] . allowed_groups . exists?
2016-01-19 07:57:55 +08:00
create_post_with_attachments ( options )
end
2014-04-15 04:55:57 +08:00
end
2016-01-19 07:57:55 +08:00
def create_post_action ( user , post , type )
2019-01-04 01:03:01 +08:00
result = PostActionCreator . new ( user , post , type ) . perform
raise InvalidPostAction . new if result . failed? && result . forbidden
2016-01-19 07:57:55 +08:00
end
2014-04-15 04:55:57 +08:00
2020-07-27 08:23:54 +08:00
def is_allowed? ( attachment )
attachment . content_type !~ SiteSetting . blocked_attachment_content_types_regex &&
attachment . filename !~ SiteSetting . blocked_attachment_filenames_regex
2018-02-17 01:14:56 +08:00
end
2016-08-08 18:30:37 +08:00
def attachments
2018-02-17 01:14:56 +08:00
@attachments || =
begin
2020-07-27 08:23:54 +08:00
attachments = @mail . attachments . select { | attachment | is_allowed? ( attachment ) }
attachments << @mail if @mail . attachment? && is_allowed? ( @mail )
2019-01-26 02:13:34 +08:00
2020-07-27 08:23:54 +08:00
@mail . parts . each { | part | attachments << part if part . attachment? && is_allowed? ( part ) }
2019-01-26 02:13:34 +08:00
2019-01-29 01:40:52 +08:00
attachments . uniq!
2018-02-17 01:14:56 +08:00
attachments
2016-08-08 18:30:37 +08:00
end
end
2016-08-03 23:55:54 +08:00
2016-01-19 07:57:55 +08:00
def create_post_with_attachments ( options = { } )
2020-07-08 13:50:30 +08:00
add_elided_to_raw! ( options )
2018-10-17 22:48:09 +08:00
options [ :raw ] = add_attachments ( options [ :raw ] , options [ :user ] , options )
2017-10-06 20:28:26 +08:00
create_post ( options )
end
2018-10-17 22:48:09 +08:00
def add_attachments ( raw , user , options = { } )
2019-05-03 06:17:27 +08:00
raw = raw . dup
2024-04-29 22:56:16 +08:00
upload_ids =
UploadReference . where (
target_id : Post . where ( topic_id : options [ :topic_id ] ) . select ( :id ) ,
) . pluck ( " DISTINCT upload_id " )
upload_shas = Upload . where ( id : upload_ids ) . pluck ( " DISTINCT COALESCE(original_sha1, sha1) " )
2024-03-02 01:38:49 +08:00
2024-07-10 15:59:27 +08:00
is_duplicate = - > ( upload_id , upload_sha , attachment ) do
return true if upload_id && upload_ids . include? ( upload_id )
return true if upload_sha && upload_shas . include? ( upload_sha )
if attachment . respond_to? ( :url ) && attachment . url & . start_with? ( " cid: " ) &&
attachment . content_type & . start_with? ( " image/ " )
return true if @cids & . include? ( attachment . url )
end
false
end
added_attachments = [ ]
2018-10-04 22:08:28 +08:00
rejected_attachments = [ ]
2024-07-10 15:59:27 +08:00
2016-08-08 18:30:37 +08:00
attachments . each do | attachment |
2017-04-24 12:06:28 +08:00
tmp = Tempfile . new ( [ " discourse-email-attachment " , File . extname ( attachment . filename ) ] )
2014-04-15 04:55:57 +08:00
begin
# read attachment
File . open ( tmp . path , " w+b " ) { | f | f . write attachment . body . decoded }
# create the upload for the user
2017-06-13 04:41:29 +08:00
opts = { for_group_message : options [ :is_group_message ] }
2018-10-17 22:48:09 +08:00
upload = UploadCreator . new ( tmp , attachment . filename , opts ) . create_for ( user . id )
2024-04-29 22:56:16 +08:00
upload_sha = upload . original_sha1 . presence || upload . sha1
2020-02-04 01:21:22 +08:00
if upload . errors . empty?
2015-12-01 01:33:24 +08:00
# try to inline images
2017-10-06 20:28:26 +08:00
if attachment . content_type & . start_with? ( " image/ " )
if raw [ attachment . url ]
raw . sub! ( attachment . url , upload . url )
2019-06-11 14:46:37 +08:00
2020-07-08 13:50:30 +08:00
InlineUploads . match_img (
raw ,
uploads : {
upload . url = > upload ,
} ,
) do | match , src , replacement , _ |
raw = raw . sub ( match , replacement ) if src == upload . url
2019-06-11 14:46:37 +08:00
end
2021-04-29 15:17:33 +08:00
elsif raw [ / \ [image:[^ \ ]]* \ ] /i ]
raw . sub! ( / \ [image:[^ \ ]]* \ ] /i , UploadMarkdown . new ( upload ) . to_markdown )
2024-07-10 15:59:27 +08:00
elsif ! is_duplicate [ upload . id , upload_sha , attachment ]
added_attachments << upload
2017-05-04 04:54:26 +08:00
end
2024-07-10 15:59:27 +08:00
elsif ! is_duplicate [ upload . id , upload_sha , attachment ]
added_attachments << upload
2015-12-01 01:33:24 +08:00
end
2018-10-04 22:08:28 +08:00
else
rejected_attachments << upload
raw << " \n \n #{ I18n . t ( " emails.incoming.missing_attachment " , filename : upload . original_filename ) } \n \n "
2014-04-15 04:55:57 +08:00
end
ensure
2018-03-28 16:20:08 +08:00
tmp & . close!
2014-04-15 04:55:57 +08:00
end
end
2024-07-10 15:59:27 +08:00
2018-10-04 22:08:28 +08:00
if rejected_attachments . present? && ! user . staged?
notify_about_rejected_attachment ( rejected_attachments )
2023-01-09 20:10:19 +08:00
end
2014-04-15 04:55:57 +08:00
2024-07-10 15:59:27 +08:00
if added_attachments . present?
markdown =
added_attachments . map { | upload | UploadMarkdown . new ( upload ) . to_markdown } . join ( " \n " )
if markdown . present?
raw << " \n \n "
raw << " [details= \" #{ I18n . t ( " emails.incoming.attachments " ) } \" ] "
raw << " \n \n "
raw << markdown
raw << " \n \n "
raw << " [/details] "
end
end
2017-10-06 20:28:26 +08:00
raw
2014-04-15 04:55:57 +08:00
end
2018-10-04 22:08:28 +08:00
def notify_about_rejected_attachment ( attachments )
errors = [ ]
attachments . each do | a |
error = a . errors . messages . values [ 0 ] [ 0 ]
errors << " #{ a . original_filename } : #{ error } "
end
message = Mail :: Message . new ( @mail )
template_args = {
former_title : message . subject ,
destination : message . to ,
site_name : SiteSetting . title ,
rejected_errors : errors . join ( " \n " ) ,
}
client_message =
RejectionMailer . send_rejection ( :email_reject_attachment , message . from , template_args )
Email :: Sender . new ( client_message , :email_reject_attachment ) . send
end
2020-07-08 13:50:30 +08:00
def add_elided_to_raw! ( options )
2016-06-06 16:30:04 +08:00
is_private_message =
options [ :archetype ] == Archetype . private_message || options [ :topic ] . try ( :private_message? )
2016-03-18 06:10:46 +08:00
# only add elided part in messages
2016-11-17 05:06:07 +08:00
if options [ :elided ] . present? &&
( SiteSetting . always_show_trimmed_content || is_private_message )
2017-05-27 04:26:18 +08:00
options [ :raw ] << Email :: Receiver . elided_html ( options [ :elided ] )
2020-07-08 13:50:30 +08:00
options [ :elided ] = " "
2016-03-18 06:10:46 +08:00
end
2020-07-08 13:50:30 +08:00
end
def create_post ( options = { } )
2020-07-10 17:05:55 +08:00
options [ :import_mode ] = @opts [ :import_mode ]
2020-07-08 13:50:30 +08:00
options [ :via_email ] = true
options [ :raw_email ] = @raw_email
options [ :created_at ] || = @mail . date
options [ :created_at ] = DateTime . now if options [ :created_at ] > DateTime . now
add_elided_to_raw! ( options )
2016-03-18 06:10:46 +08:00
2018-01-04 20:38:06 +08:00
if sent_to_mailinglist_mirror?
options [ :skip_validations ] = true
options [ :skip_guardian ] = true
2020-01-22 00:12:00 +08:00
else
options [ :email_spam ] = is_spam?
options [ :first_post_checks ] = true if is_spam?
options [ :email_auth_res_action ] = auth_res_action
2018-01-04 20:38:06 +08:00
end
2016-04-12 00:20:26 +08:00
user = options . delete ( :user )
2018-11-27 02:59:37 +08:00
if options [ :bounce ]
options [ :raw ] = I18n . t (
" system_messages.email_bounced " ,
email : user . email ,
raw : options [ :raw ] ,
)
user = Discourse . system_user
options [ :post_type ] = Post . types [ :whisper ]
end
2021-01-15 08:54:46 +08:00
# To avoid race conditions with the post alerter and Group SMTP
# emails, we skip the jobs here and enqueue them only _after_
# the incoming email has been updated with the post and topic.
options [ :skip_jobs ] = true
2021-12-10 04:45:07 +08:00
options [ :skip_events ] = true
2017-01-06 22:32:25 +08:00
result = NewPostManager . new ( user , options ) . perform
2014-08-27 08:30:12 +08:00
2018-08-03 03:43:53 +08:00
errors = result . errors . full_messages
if errors . any? { | message |
message . include? ( I18n . t ( " activerecord.attributes.post.raw " ) . strip ) &&
message . include? (
I18n . t ( " errors.messages.too_short " , count : SiteSetting . min_post_length ) . strip ,
)
2023-01-09 20:10:19 +08:00
}
2018-08-03 03:43:53 +08:00
raise TooShortPost
end
2018-11-10 01:24:28 +08:00
2019-04-30 14:58:18 +08:00
raise InvalidPost , errors . join ( " \n " ) if result . errors . present?
2016-01-19 07:57:55 +08:00
if result . post
2022-09-26 07:14:24 +08:00
IncomingEmail . transaction do
@incoming_email . update_columns ( topic_id : result . post . topic_id , post_id : result . post . id )
result . post . update ( outbound_message_id : @incoming_email . message_id )
end
2018-11-28 10:54:23 +08:00
if result . post . topic & . private_message? && ! is_bounce?
2021-09-07 06:46:28 +08:00
add_other_addresses ( result . post , user , @mail )
if has_been_forwarded?
add_other_addresses ( result . post , @forwarded_by_user || user , embedded_email )
end
2016-01-19 07:57:55 +08:00
end
2021-01-15 08:54:46 +08:00
# Alert the people involved in the topic now that the incoming email
# has been linked to the post.
PostJobsEnqueuer . new (
result . post ,
result . post . topic ,
options [ :topic_id ] . blank? ,
import_mode : options [ :import_mode ] ,
post_alert_options : options [ :post_alert_options ] ,
) . enqueue_jobs
2022-01-10 16:24:10 +08:00
if result . post . is_first_post?
DiscourseEvent . trigger ( :topic_created , result . post . topic , options , user )
2023-01-09 20:10:19 +08:00
end
2021-12-10 04:45:07 +08:00
DiscourseEvent . trigger ( :post_created , result . post , options , user )
2014-07-31 16:46:02 +08:00
end
2016-11-17 02:42:11 +08:00
result . post
2016-01-19 07:57:55 +08:00
end
2014-08-27 08:30:12 +08:00
2017-05-27 04:26:18 +08:00
def self . elided_html ( elided )
2019-05-03 06:17:27 +08:00
html = + " \n \n " << " <details class='elided'> " << " \n "
2023-02-28 00:20:00 +08:00
html << " <summary title=' #{ I18n . t ( " emails.incoming.show_trimmed_content " ) } '>& # 183;& # 183;& # 183;</summary> " <<
" \n \n "
2017-12-06 08:47:31 +08:00
html << elided << " \n \n "
2017-05-27 04:26:18 +08:00
html << " </details> " << " \n "
html
end
2021-09-07 06:46:28 +08:00
def add_other_addresses ( post , sender , mail_object )
2022-08-18 23:19:20 +08:00
max_staged_users_post = nil
2016-01-19 07:57:55 +08:00
% i [ to cc bcc ] . each do | d |
2021-09-07 06:46:28 +08:00
next if mail_object [ d ] . blank?
2021-02-19 02:15:02 +08:00
2021-09-07 06:46:28 +08:00
mail_object [ d ] . each do | address_field |
2021-02-19 02:15:02 +08:00
begin
address_field . decoded
email = address_field . address . downcase
display_name = address_field . display_name . try ( :to_s )
2023-05-19 16:33:48 +08:00
next if ! email . include? ( " @ " )
2022-08-18 23:19:20 +08:00
2021-02-19 02:15:02 +08:00
if should_invite? ( email )
2022-08-18 23:19:20 +08:00
user = User . find_by_email ( email )
# cap number of staged users created per email
if ( ! user || user . staged ) &&
@staged_users . count > = SiteSetting . maximum_staged_users_per_email
max_staged_users_post || =
post . topic . add_moderator_post (
sender ,
I18n . t ( " emails.incoming.maximum_staged_user_per_email_reached " ) ,
import_mode : @opts [ :import_mode ] ,
)
next
end
user = find_or_create_user ( email , display_name , user : user )
2021-02-19 02:15:02 +08:00
if user && can_invite? ( post . topic , user )
post . topic . topic_allowed_users . create! ( user_id : user . id )
TopicUser . auto_notification_for_staging (
user . id ,
post . topic_id ,
TopicUser . notification_reasons [ :auto_watch ] ,
)
post . topic . add_small_action (
sender ,
" invited_user " ,
user . username ,
import_mode : @opts [ :import_mode ] ,
)
end
2016-01-19 07:57:55 +08:00
end
2021-02-19 02:15:02 +08:00
rescue ActiveRecord :: RecordInvalid , EmailNotAllowed
# don't care if user already allowed or the user's email address is not allowed
2016-01-19 07:57:55 +08:00
end
end
end
2014-02-24 14:01:37 +08:00
end
2013-06-11 04:46:08 +08:00
2016-01-21 06:08:27 +08:00
def should_invite? ( email )
2017-04-06 00:45:58 +08:00
email !~ Email :: Receiver . reply_by_email_address_regex &&
2016-01-21 06:08:27 +08:00
email !~ group_incoming_emails_regex && email !~ category_email_in_regex
end
2016-01-19 22:24:34 +08:00
def can_invite? ( topic , user )
! topic . topic_allowed_users . where ( user_id : user . id ) . exists? &&
! topic
. topic_allowed_groups
. where ( " group_id IN (SELECT group_id FROM group_users WHERE user_id = ?) " , user . id )
. exists?
end
2017-10-03 16:13:19 +08:00
def send_subscription_mail ( action , user )
2019-05-07 09:27:05 +08:00
message = SubscriptionMailer . public_send ( action , user )
2017-10-03 16:13:19 +08:00
Email :: Sender . new ( message , :subscription ) . send
end
2017-10-03 23:28:41 +08:00
2019-04-08 17:36:39 +08:00
def stage_from_user
2021-09-06 13:02:13 +08:00
@from_user || = stage_sender_user ( @from_email , @from_display_name )
end
def stage_sender_user ( email , display_name )
find_or_create_user! ( email , display_name ) . tap { | u | log_and_validate_user ( u ) }
2019-04-08 17:36:39 +08:00
end
2022-08-18 23:19:20 +08:00
def delete_created_staged_users
@created_staged_users . each do | user |
2017-10-31 22:13:23 +08:00
@incoming_email . update_columns ( user_id : nil ) if @incoming_email . user & . id == user . id
UserDestroyer . new ( Discourse . system_user ) . destroy ( user , quiet : true ) if user . posts . count == 0
2017-10-03 23:28:41 +08:00
end
end
2018-10-16 07:51:57 +08:00
def enable_email_pm_setting ( user )
# ensure user PM emails are enabled (since user is posting via email)
2019-03-15 22:55:11 +08:00
if ! user . staged &&
user . user_option . email_messages_level == UserOption . email_level_types [ :never ]
user . user_option . update! ( email_messages_level : UserOption . email_level_types [ :always ] )
2018-10-16 07:51:57 +08:00
end
end
2021-06-21 09:45:00 +08:00
def destination_too_old? ( post )
return false if post . blank?
num_of_days = SiteSetting . disallow_reply_by_email_after_days
num_of_days > 0 && post . created_at < num_of_days . days . ago
end
2013-06-11 04:46:08 +08:00
end
end