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
2014-04-15 04:55:57 +08:00
2013-06-11 04:46:08 +08:00
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
class EmptyEmailError < ProcessingError ; end
class ScreenedEmailError < ProcessingError ; end
class UserNotFoundError < ProcessingError ; end
class AutoGeneratedEmailError < ProcessingError ; end
class BouncedEmailError < ProcessingError ; end
class NoBodyDetectedError < ProcessingError ; end
2017-09-13 04:35:24 +08:00
class NoSenderDetectedError < ProcessingError ; end
2018-05-23 16:04:45 +08:00
class FromReplyByAddressError < ProcessingError ; end
2016-04-19 04:58:30 +08:00
class InactiveUserError < ProcessingError ; end
2017-11-11 01:18:08 +08:00
class SilencedUserError < ProcessingError ; end
2016-04-19 04:58:30 +08:00
class BadDestinationAddress < ProcessingError ; end
class StrangersNotAllowedError < ProcessingError ; end
2019-06-03 05:49:05 +08:00
class ReplyNotAllowedError < ProcessingError ; end
2016-04-19 04:58:30 +08:00
class InsufficientTrustLevelError < ProcessingError ; end
class ReplyUserNotMatchingError < ProcessingError ; end
class TopicNotFoundError < ProcessingError ; end
class TopicClosedError < ProcessingError ; end
class InvalidPost < ProcessingError ; end
2018-08-03 03:43:53 +08:00
class TooShortPost < ProcessingError ; end
2016-04-19 04:58:30 +08:00
class InvalidPostAction < ProcessingError ; end
2017-10-03 16:13:19 +08:00
class UnsubscribeNotAllowed < ProcessingError ; end
2017-10-03 17:23:18 +08:00
class EmailNotAllowed < ProcessingError ; end
2018-05-10 00:51:01 +08:00
class OldDestinationError < ProcessingError ; end
2020-05-14 22:04:58 +08:00
class ReplyToDigestError < ProcessingError ; end
2016-01-19 07:57:55 +08:00
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
2018-01-31 06:45:04 +08:00
COMMON_ENCODINGS || = [ - " utf-8 " , - " windows-1252 " , - " iso-8859-1 " ]
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 = [ ]
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?
2019-07-24 20:45:02 +08:00
id_hash = Digest :: SHA1 . hexdigest ( @message_id )
DistributedMutex . synchronize ( " process_email_ #{ id_hash } " ) do
2017-05-18 07:09:51 +08:00
begin
2020-08-03 11:10:17 +08:00
2020-08-03 11:31:34 +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.
2020-08-03 11:10:17 +08:00
@incoming_email = find_existing_and_update_imap
return if @incoming_email
2018-03-28 00:28:37 +08:00
ensure_valid_address_lists
2019-04-08 17:36:39 +08:00
ensure_valid_date
2018-11-30 14:59:51 +08:00
@from_email , @from_display_name = parse_from_field
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
2020-07-10 17:05:55 +08:00
post = process_internal
2018-11-27 02:59:37 +08:00
raise BouncedEmailError if is_bounce?
2020-07-10 17:05:55 +08:00
return post
2020-07-09 20:39:01 +08:00
rescue Exception = > e
2017-08-04 22:20:44 +08:00
error = e . to_s
error = e . class . name if error . blank?
@incoming_email . update_columns ( error : error ) if @incoming_email
2017-10-03 23:28:41 +08:00
delete_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
incoming_email = IncomingEmail . find_by ( message_id : @message_id )
2020-08-03 11:31:34 +08:00
return if ! incoming_email
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.
2020-08-03 11:10:17 +08:00
if @opts [ :imap_uid ] . blank?
return incoming_email
end
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.
return unless discourse_generated_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
)
incoming_email
end
2018-03-28 00:28:37 +08:00
def ensure_valid_address_lists
[ :to , :cc , :bcc ] . each do | field |
addresses = @mail [ field ]
if addresses & . errors . present?
@mail [ field ] = addresses . to_s . scan ( / \ b[A-Z0-9._%+-]+@[A-Z0-9.-]+ \ .[A-Z]{2,} \ b /i )
end
end
end
2019-04-08 17:36:39 +08:00
def ensure_valid_date
if @mail . date . nil?
raise InvalidPost , I18n . t ( " system_messages.email_reject_invalid_post_specified.date_invalid " )
end
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
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 ,
to_addresses : @mail . to & . map ( & :downcase ) & . join ( " ; " ) ,
cc_addresses : @mail . cc & . map ( & :downcase ) & . join ( " ; " ) ,
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 ] ,
2020-07-10 17:05:55 +08:00
imap_sync : false
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
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?
2018-08-21 16:17:08 +08:00
post = find_related_post ( force : true )
if post && Guardian . new ( user ) . can_see_post? ( post )
num_of_days = SiteSetting . disallow_reply_by_email_after_days
if num_of_days > 0 && post . created_at < num_of_days . days . ago
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
raise ReplyToDigestError if EmailLog . where ( email_type : " digest " , message_id : @mail . in_reply_to ) . exists?
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
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 )
2018-11-28 21:34:09 +08:00
if email_log . present?
2016-07-16 00:00:40 +08:00
email_log . update_columns ( bounced : true )
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
2018-01-04 00:59:20 +08:00
if @mail . error_status . present? && Array . wrap ( @mail . error_status ) . any? { | s | s . start_with? ( " 4. " ) }
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
2018-11-28 23:43:06 +08:00
if SiteSetting . enable_whispers? && @from_user & . staged?
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 [ / ^ \ 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-03-31 00:41:09 +08:00
@mail . header . to_s [ / 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'
@mail [ :x_spam_status ] . to_s [ / ^Yes, /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
2018-02-17 01:14:56 +08:00
return unless text . present? || html . present?
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
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 )
end
2017-04-27 20:31:11 +08:00
end
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
if SiteSetting . strip_incoming_email_lines
in_code = nil
text = text . lines . map! do | line |
stripped = line . strip << " \n "
2019-04-16 16:39:16 +08:00
# Do not strip list items.
next line if ( stripped [ 0 ] == '*' || stripped [ 0 ] == '-' || stripped [ 0 ] == '+' ) && stripped [ 1 ] == ' '
# 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
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
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_ / ] ,
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
elided = doc . css ( " *[class^='moz-'], *[class^='moz-'] ~ * " ) . remove
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
2018-01-17 19:03:57 +08:00
def trim_reply_and_extract_elided ( text )
return [ text , " " ] if @opts [ :skip_trimming ]
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 = mail_part . body . decoded rescue nil
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?
2017-05-01 05:30:40 +08:00
# mail (>=2.5) decodes mails with 8bit transfer encoding to utf-8, so
# always try UTF-8 first
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
2016-02-12 01:48:09 +08:00
@previous_replies_regex || = / ^--[- ] \ n \ * #{ I18n . t ( " user_notifications.previous_discussion " ) } \ * \ n /im
2016-01-30 08:29:31 +08:00
end
def trim_discourse_markers ( reply )
reply . split ( previous_replies_regex ) [ 0 ]
end
2018-11-30 14:59:51 +08:00
def parse_from_field ( mail = nil )
mail || = @mail
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?
2018-11-27 02:59:37 +08:00
Array . wrap ( mail . final_recipient ) . each do | from |
return extract_from_address_and_name ( from )
end
end
2017-01-10 05:59:30 +08:00
return unless mail [ :from ]
2016-11-17 02:42:11 +08:00
if mail [ :from ] . errors . blank?
mail [ :from ] . address_list . addresses . each do | address_field |
address_field . decoded
from_address = address_field . address
from_display_name = address_field . display_name . try ( :to_s )
2016-12-02 01:34:47 +08:00
return [ from_address & . downcase , from_display_name & . strip ] if from_address [ " @ " ]
2016-11-17 02:42:11 +08:00
end
2016-02-25 00:40:57 +08:00
end
2016-11-17 02:42:11 +08:00
2017-09-13 04:35:24 +08:00
return extract_from_address_and_name ( mail . from ) if mail . from . is_a? String
if mail . from . is_a? Mail :: AddressContainer
mail . from . each do | from |
from_address , from_display_name = extract_from_address_and_name ( from )
return [ from_address , from_display_name ] if from_address
end
end
2017-09-15 22:47:19 +08:00
nil
rescue StandardError
2017-09-13 04:35:24 +08:00
nil
end
def extract_from_address_and_name ( value )
2018-11-27 02:59:37 +08:00
if value [ " ; " ]
from_display_name , from_address = value . split ( " ; " )
return [ from_address & . strip & . downcase , from_display_name & . strip ]
end
2017-09-13 04:35:24 +08:00
if value [ / <[^>]+> / ]
from_address = value [ / <([^>]+)> / , 1 ]
from_display_name = value [ / ^([^<]+) / , 1 ]
2016-12-02 01:34:47 +08:00
end
2017-09-13 04:35:24 +08:00
if ( from_address . blank? || ! from_address [ " @ " ] ) && value [ / \ [mailto:[^ \ ]]+ \ ] / ]
from_address = value [ / \ [mailto:([^ \ ]]+) \ ] / , 1 ]
from_display_name = value [ / ^([^ \ []+) / , 1 ]
2016-12-02 01:34:47 +08:00
end
2016-11-17 02:42:11 +08:00
2016-12-02 01:34:47 +08:00
[ from_address & . downcase , from_display_name & . strip ]
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
2018-10-08 15:45:23 +08:00
def find_or_create_user ( email , display_name , raise_on_failed_create : false )
2016-03-24 01:56:03 +08:00
user = nil
User . transaction do
2017-10-03 17:23:18 +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
)
2017-10-03 16:13:19 +08:00
@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
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 || = all_destinations
2018-11-28 10:54:23 +08:00
. map { | d | Email :: Receiver . check_address ( d , is_bounce? ) }
2017-11-17 21:49:10 +08:00
. reject ( & :blank? )
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
return if SiteSetting . forwarded_emails_behaviour != " hide " &&
2016-11-17 02:42:11 +08:00
has_been_forwarded? &&
process_forwarded_email ( destination , user )
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 )
raise StrangersNotAllowedError if ( user . nil? || user . staged? ) && ! destination . email_in_allow_strangers
2019-04-08 17:36:39 +08:00
user || = stage_from_user
2018-01-04 20:38:06 +08:00
raise InsufficientTrustLevelError if ! user . has_trust_level? ( SiteSetting . email_in_min_trust ) && ! sent_to_mailinglist_mirror?
2016-08-03 21:57:37 +08:00
create_topic ( user : user ,
raw : body ,
2017-06-29 12:03:14 +08:00
elided : elided ,
2016-08-03 21:57:37 +08:00
title : subject ,
2020-07-10 17:05:55 +08:00
category : destination . id ,
2016-08-03 21:57:37 +08:00
skip_validations : user . staged? )
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 )
post_ids = [ ]
incoming_emails = IncomingEmail
. where ( message_id : message_ids )
. addressed_to_user ( user )
2020-07-10 17:05:55 +08:00
. pluck ( :post_id , :from_address , :to_addresses , :cc_addresses )
2018-03-30 20:37:19 +08:00
2020-07-10 17:05:55 +08:00
incoming_emails . each do | post_id , from_address , to_addresses , cc_addresses |
post_ids << post_id if contains_email_address_of_user? ( from_address , user ) ||
contains_email_address_of_user? ( to_addresses , user ) ||
contains_email_address_of_user? ( cc_addresses , user )
2018-03-30 20:37:19 +08:00
end
if post_ids . any? && post = Post . where ( id : post_ids ) . order ( :created_at ) . last
create_reply ( user : user ,
raw : body ,
elided : elided ,
post : post ,
topic : post . topic ,
skip_validations : true )
else
2018-10-16 07:51:57 +08:00
enable_email_pm_setting ( user )
2018-03-30 20:37:19 +08:00
create_topic ( user : user ,
raw : body ,
elided : elided ,
title : subject ,
archetype : Archetype . private_message ,
target_group_names : [ group . name ] ,
is_group_message : true ,
skip_validations : true )
end
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 |
next unless contains_email_address_of_user? ( to_addresses , user ) ||
contains_email_address_of_user? ( cc_addresses , user )
2017-11-13 06:44:22 +08:00
2018-07-18 16:28:44 +08:00
return true 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 )
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?
2017-02-09 04:38:52 +08:00
subject [ / ^[[: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
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 )
2016-11-17 02:42:11 +08:00
2020-07-10 17:05:55 +08:00
elsif destination . is_a? ( Category )
return false if user . staged? && ! destination . email_in_allow_strangers
2016-11-17 02:42:11 +08:00
return false if ! user . has_trust_level? ( SiteSetting . email_in_min_trust )
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 )
embedded = Mail . new ( embedded_email_raw )
email , display_name = parse_from_field ( embedded )
return false if email . blank? || ! email [ " @ " ]
post = forwarded_email_create_topic ( destination : destination ,
user : user ,
raw : try_to_encode ( embedded . decoded , " UTF-8 " ) . presence || embedded . to_s ,
title : embedded . subject . presence || subject ,
date : embedded . date ,
embedded_user : lambda { find_or_create_user ( email , display_name ) } )
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 ]
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
post . topic . add_small_action ( 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 )
embedded = embedded_email_raw
raw = << ~ EOF
#{@before_embedded}
[ quote ]
#{PlainTextToMarkdown.new(embedded).to_markdown}
[ / quote]
EOF
return true if forwarded_email_create_topic ( destination : destination , user : user , raw : raw , title : subject )
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
2016-02-25 02:47:58 +08:00
@group_incoming_emails_regex || = Regexp . union Group . pluck ( :incoming_email ) . select ( & :present? ) . map { | e | e . split ( " | " ) } . flatten . uniq
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?
2020-08-03 11:10:17 +08:00
post_ids = message_ids . map { | message_id | message_id [ message_id_post_id_regexp , 1 ] } . compact . map ( & :to_i )
post_ids << Post . where ( topic_id : message_ids . map { | message_id | message_id [ message_id_topic_id_regexp , 1 ] } . compact , post_number : 1 ) . pluck ( :id )
2017-02-09 04:38:52 +08:00
post_ids << EmailLog . where ( message_id : message_ids ) . pluck ( :post_id )
post_ids << IncomingEmail . where ( message_id : message_ids ) . pluck ( :post_id )
post_ids . flatten!
post_ids . compact!
post_ids . uniq!
return if post_ids . empty?
Post . where ( id : post_ids ) . order ( :created_at ) . last
2016-01-19 07:57:55 +08:00
end
2015-11-24 23:58:26 +08:00
2020-08-03 11:10:17 +08:00
def host
@host || = Email :: Sender . host_for ( Discourse . base_url )
end
2020-08-03 11:31:34 +08:00
def discourse_generated_message_id?
! ( @message_id =~ message_id_post_id_regexp ) . nil?
end
2020-08-03 11:10:17 +08:00
def message_id_post_id_regexp
@message_id_post_id_regexp || = Regexp . new " topic/ \\ d+/( \\ d+)@ #{ Regexp . escape ( host ) } "
end
def message_id_topic_id_regexp
@message_id_topic_id_regexp || = Regexp . new " topic/( \\ d+)@ #{ Regexp . escape ( host ) } "
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?
2017-02-09 04:38:52 +08:00
references . split ( / [ \ s,] / ) . map { | r | r . tr ( " <> " , " " ) }
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 = { } )
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
raise BouncedEmailError if options [ :bounce ] && options [ :topic ] . archetype != Archetype . private_message
2018-09-04 05:06:25 +08:00
options [ :post ] = nil if options [ :post ] & . trashed?
2018-10-16 07:51:57 +08:00
enable_email_pm_setting ( options [ :user ] ) if options [ :topic ] . archetype == Archetype . private_message
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
@mail . parts . each do | part |
2020-07-27 08:23:54 +08:00
attachments << part if part . attachment? && is_allowed? ( part )
2019-01-26 02:13:34 +08:00
end
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
2018-10-04 22:08:28 +08:00
rejected_attachments = [ ]
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 )
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 , _ |
2019-06-11 14:46:37 +08:00
if src == upload . url
2020-07-08 13:50:30 +08:00
raw = raw . sub ( match , replacement )
2019-06-11 14:46:37 +08:00
end
end
2017-10-06 20:28:26 +08:00
elsif raw [ / \ [image:.*? \ d+[^ \ ]]* \ ] /i ]
2019-07-25 22:34:46 +08:00
raw . sub! ( / \ [image:.*? \ d+[^ \ ]]* \ ] /i , UploadMarkdown . new ( upload ) . to_markdown )
2017-05-04 04:54:26 +08:00
else
2019-07-25 22:34:46 +08:00
raw << " \n \n #{ UploadMarkdown . new ( upload ) . to_markdown } \n \n "
2017-05-04 04:54:26 +08:00
end
2016-01-19 07:57:55 +08:00
else
2019-07-25 22:34:46 +08:00
raw << " \n \n #{ UploadMarkdown . new ( upload ) . to_markdown } \n \n "
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
2018-10-04 22:08:28 +08:00
notify_about_rejected_attachment ( rejected_attachments ) if rejected_attachments . present? && ! user . staged?
2014-04-15 04:55:57 +08:00
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
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? do | message |
message . include? ( I18n . t ( " activerecord.attributes.post.raw " ) . strip ) &&
message . include? ( I18n . t ( " errors.messages.too_short " , count : SiteSetting . min_post_length ) . strip )
end
raise TooShortPost
end
2018-11-10 01:24:28 +08:00
2019-04-30 14:58:18 +08:00
if result . errors . present?
raise InvalidPost , errors . join ( " \n " )
end
2016-01-19 07:57:55 +08:00
if result . post
@incoming_email . update_columns ( topic_id : result . post . topic_id , post_id : result . post . id )
2018-11-28 10:54:23 +08:00
if result . post . topic & . private_message? && ! is_bounce?
2017-10-06 22:37:28 +08:00
add_other_addresses ( result . post , user )
2016-01-19 07:57:55 +08:00
end
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 "
2017-12-06 08:47:31 +08:00
html << " <summary title=' #{ I18n . t ( 'emails.incoming.show_trimmed_content' ) } '>& # 183;& # 183;& # 183;</summary> " << " \n \n "
html << elided << " \n \n "
2017-05-27 04:26:18 +08:00
html << " </details> " << " \n "
html
end
2017-10-06 22:37:28 +08:00
def add_other_addresses ( post , sender )
2016-01-19 07:57:55 +08:00
% i ( to cc bcc ) . each do | d |
if @mail [ d ] && @mail [ d ] . address_list && @mail [ d ] . address_list . addresses
2016-01-19 22:24:34 +08:00
@mail [ d ] . address_list . addresses . each do | address_field |
2016-01-19 07:57:55 +08:00
begin
2016-02-25 00:40:57 +08:00
address_field . decoded
2016-01-19 22:24:34 +08:00
email = address_field . address . downcase
2016-02-25 00:40:57 +08:00
display_name = address_field . display_name . try ( :to_s )
2016-11-17 02:42:11 +08:00
next unless email [ " @ " ]
2016-01-21 06:08:27 +08:00
if should_invite? ( email )
2016-02-25 00:40:57 +08:00
user = find_or_create_user ( email , display_name )
2017-10-06 22:37:28 +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 ] )
2020-07-10 17:05:55 +08:00
post . topic . add_small_action ( sender , " invited_user " , user . username , import_mode : @opts [ :import_mode ] )
2016-01-19 07:57:55 +08:00
end
2016-05-17 03:45:34 +08:00
# cap number of staged users created per email
2017-10-03 16:13:19 +08:00
if @staged_users . count > SiteSetting . maximum_staged_users_per_email
2020-07-10 17:05:55 +08:00
post . topic . add_moderator_post ( sender , I18n . t ( " emails.incoming.maximum_staged_user_per_email_reached " ) , import_mode : @opts [ :import_mode ] )
2016-05-17 03:45:34 +08:00
return
end
2016-01-19 07:57:55 +08:00
end
2017-10-03 17:23:18 +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
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
@from_user || = find_or_create_user! ( @from_email , @from_display_name ) . tap do | u |
log_and_validate_user ( u )
end
end
2017-10-03 23:28:41 +08:00
def delete_staged_users
@staged_users . each do | user |
2019-04-08 17:36:39 +08:00
if @incoming_email . user & . id == user . id
2017-10-31 22:13:23 +08:00
@incoming_email . update_columns ( user_id : nil )
end
if user . posts . count == 0
UserDestroyer . new ( Discourse . system_user ) . destroy ( user , quiet : true )
end
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
2013-06-11 04:46:08 +08:00
end
end