2016-03-15 01:18:58 +08:00
require " digest "
2016-01-19 07:57:55 +08:00
require_dependency " new_post_manager "
require_dependency " post_action_creator "
2017-04-26 22:49:06 +08:00
require_dependency " html_to_markdown "
2017-12-06 08:47:31 +08:00
require_dependency " plain_text_to_markdown "
2017-05-11 06:16:57 +08:00
require_dependency " upload_creator "
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
2016-01-30 08:29:31 +08:00
include ActionView :: Helpers :: NumberHelper
2013-06-11 04:46:08 +08:00
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
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
class InsufficientTrustLevelError < ProcessingError ; end
class ReplyUserNotMatchingError < ProcessingError ; end
class TopicNotFoundError < ProcessingError ; end
class TopicClosedError < ProcessingError ; end
class InvalidPost < ProcessingError ; end
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
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
2017-11-15 23:39:29 +08:00
def self . formats
@formats || = Enum . new ( plaintext : 1 ,
markdown : 2 )
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 = [ ]
2016-06-26 19:27:34 +08:00
@raw_email = try_to_encode ( mail_string , " UTF-8 " ) || try_to_encode ( mail_string , " ISO-8859-1 " ) || mail_string
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
2013-06-11 04:46:08 +08:00
end
2016-03-07 23:56:17 +08:00
def process!
2016-05-19 05:07:01 +08:00
return if is_blacklisted?
2017-05-18 07:09:51 +08:00
DistributedMutex . synchronize ( @message_id ) do
begin
2017-05-18 22:43:07 +08:00
return if IncomingEmail . exists? ( message_id : @message_id )
2017-05-18 07:09:51 +08:00
@from_email , @from_display_name = parse_from_field ( @mail )
2017-05-18 22:43:07 +08:00
@incoming_email = create_incoming_email
2017-05-18 07:09:51 +08:00
process_internal
rescue = > 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
2016-05-19 05:07:01 +08:00
def is_blacklisted?
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 ,
raw : @raw_email ,
subject : subject ,
from_address : @from_email ,
to_addresses : @mail . to & . map ( & :downcase ) & . join ( " ; " ) ,
cc_addresses : @mail . cc & . map ( & :downcase ) & . join ( " ; " ) ,
)
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
2016-08-02 05:37:59 +08:00
raise BouncedEmailError if is_bounce?
2017-09-13 04:35:24 +08:00
raise NoSenderDetectedError if @from_email . blank?
2016-04-19 04:58:30 +08:00
raise ScreenedEmailError if ScreenedEmail . should_block? ( @from_email )
2017-10-03 16:13:19 +08:00
user = find_user ( @from_email )
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
2016-08-08 18:30:37 +08:00
raise NoBodyDetectedError if body . blank? && attachments . empty?
2016-04-21 03:29:27 +08:00
if is_auto_generated?
@incoming_email . update_columns ( is_auto_generated : true )
2017-11-17 21:49:10 +08:00
if SiteSetting . block_auto_generated_emails? && ! sent_to_mailinglist_mirror?
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
# Lets create a staged user if there isn't one yet. We will try to
# delete staged users in process!() if something bad happens.
2017-10-03 17:23:18 +08:00
if user . nil?
user = find_or_create_user ( @from_email , @from_display_name )
log_and_validate_user ( user )
end
2017-10-03 16:13:19 +08:00
if post = find_related_post
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 ,
skip_validations : user . staged? )
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
2016-11-17 02:42:11 +08:00
process_destination ( destination , user , body , elided )
2016-08-03 21:57:37 +08:00
rescue = > e
first_exception || = e
else
return
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
raise first_exception || 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?
return false unless @mail . bounced? || verp
@incoming_email . update_columns ( is_bounce : true )
2016-07-16 00:00:40 +08:00
if verp && ( bounce_key = verp [ / \ +verp-( \ h{32})@ / , 1 ] ) && ( email_log = EmailLog . find_by ( bounce_key : bounce_key ) )
email_log . update_columns ( bounced : true )
email = email_log . user . try ( :email ) . presence
2016-05-03 05:15:32 +08:00
end
2016-07-16 00:00:40 +08:00
email || = @from_email
2018-01-04 00:59:20 +08:00
if @mail . error_status . present? && Array . wrap ( @mail . error_status ) . any? { | s | s . start_with? ( " 4. " ) }
2016-07-25 23:27:28 +08:00
Email :: Receiver . update_bounce_score ( email , SiteSetting . soft_bounce_score )
2016-07-16 00:00:40 +08:00
else
2016-07-25 23:27:28 +08:00
Email :: Receiver . update_bounce_score ( email , SiteSetting . hard_bounce_score )
2016-06-28 22:42:05 +08:00
end
2016-05-03 05:15:32 +08:00
true
end
def verp
2016-05-07 01:34:33 +08:00
@verp || = all_destinations . select { | to | to [ / \ +verp- \ h{32}@ / ] } . first
2016-05-03 05:15:32 +08:00
end
2016-05-30 23:11:17 +08:00
def self . update_bounce_score ( email , score )
2016-05-03 05:15:32 +08:00
# only update bounce score once per day
key = " bounce_score: #{ email } : #{ Date . today } "
if $redis . setnx ( key , " 1 " )
$redis . expire ( key , 25 . hours )
2017-04-27 02:47:36 +08:00
if user = User . find_by_email ( email )
2016-05-03 05:15:32 +08:00
user . user_stat . bounce_score += score
2016-07-25 23:29:54 +08:00
user . user_stat . reset_bounce_score_after = SiteSetting . reset_bounce_score_after_days . days . from_now
2016-05-03 05:15:32 +08:00
user . user_stat . save
2016-07-26 00:57:06 +08:00
bounce_score = user . user_stat . bounce_score
if user . active && bounce_score > = SiteSetting . bounce_score_threshold_deactivate
user . update_columns ( active : false )
reason = I18n . t ( " user.deactivated " , email : user . email )
StaffActionLogger . new ( Discourse . system_user ) . log_user_deactivate ( user , reason )
elsif bounce_score > = SiteSetting . bounce_score_threshold
# NOTE: we check bounce_score before sending emails, nothing to do
# here other than log it happened.
2017-07-28 09:20:09 +08:00
reason = I18n . t ( " user.email.revoked " , email : user . email , date : user . user_stat . reset_bounce_score_after )
2016-07-26 00:57:06 +08:00
StaffActionLogger . new ( Discourse . system_user ) . log_revoke_email ( user , reason )
2016-05-03 05:15:32 +08:00
end
end
true
else
false
end
end
2016-01-19 07:57:55 +08:00
def is_auto_generated?
2016-04-12 04:47:34 +08:00
return false if SiteSetting . auto_generated_whitelist . 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
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 )
else
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
2017-12-06 08:47:31 +08:00
if text . present?
2016-06-06 16:30:04 +08:00
text = trim_discourse_markers ( text )
2017-12-06 08:47:31 +08:00
text , elided_text = EmailReplyTrimmer . trim ( text , true )
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?
2017-05-04 04:54:26 +08:00
markdown = HtmlToMarkdown . new ( html , keep_img_tags : true , keep_cid_imgs : true ) . to_markdown
2017-04-26 22:49:06 +08:00
markdown = trim_discourse_markers ( markdown )
2017-04-27 20:31:11 +08:00
EmailReplyTrimmer . trim ( markdown , true )
end
if text . blank? || ( SiteSetting . incoming_email_prefer_html && markdown . present? )
2017-11-15 23:39:29 +08:00
return [ markdown , elided_markdown , Receiver :: formats [ :markdown ] ]
2017-04-27 20:31:11 +08:00
else
2017-11-15 23:39:29 +08:00
return [ text , elided_text , Receiver :: formats [ :plaintext ] ]
2017-04-26 22:49:06 +08:00
end
2016-01-19 07:57:55 +08:00
end
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
encodings = [ " UTF-8 " , " ISO-8859-1 " ]
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
2016-11-17 02:42:11 +08:00
def parse_from_field ( mail )
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 )
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
2016-02-25 00:40:57 +08:00
@suject || = @mail . subject . presence || I18n . t ( " emails.incoming.default_subject " , email : @from_email )
2016-02-01 19:16:15 +08:00
end
2013-06-21 00:38:03 +08:00
2017-10-03 16:13:19 +08:00
def find_user ( email )
User . find_by_email ( email )
end
2016-02-25 00:40:57 +08:00
def find_or_create_user ( email , display_name )
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 )
begin
2016-04-19 04:58:30 +08:00
username = UserNameSuggester . sanitize_username ( display_name ) if display_name . present?
user = User . create! (
email : email ,
username : UserNameSuggester . suggest ( username . presence || email ) ,
name : display_name . presence || User . suggest_name ( email ) ,
staged : true
)
2017-10-03 16:13:19 +08:00
@staged_users << user
2017-10-03 17:23:18 +08:00
rescue
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
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
2017-04-06 00:45:58 +08:00
. map { | d | Email :: Receiver . check_address ( d ) }
2017-11-17 21:49:10 +08:00
. reject ( & :blank? )
end
def sent_to_mailinglist_mirror?
destinations . each do | destination |
next unless destination [ :type ] == :category
category = destination [ :obj ]
return true if category . mailinglist_mirror?
end
false
2015-11-19 04:22:50 +08:00
end
2017-04-06 00:45:58 +08:00
def self . check_address ( address )
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 )
return { type : :group , obj : 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 )
return { type : :category , obj : category } if category
2015-11-19 04:22:50 +08:00
end
2016-01-19 07:57:55 +08:00
# reply
2017-04-06 00:45:58 +08:00
match = Email :: Receiver . reply_by_email_address_regex . match ( address )
2016-06-10 22:14:42 +08:00
if match && match . captures
match . captures . each do | c |
next if c . blank?
email_log = EmailLog . for ( c )
return { type : :reply , obj : email_log } if email_log
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
2016-11-17 02:42:11 +08:00
def process_destination ( destination , user , body , elided )
return if SiteSetting . enable_forwarded_emails &&
has_been_forwarded? &&
process_forwarded_email ( destination , user )
2016-08-03 21:57:37 +08:00
case destination [ :type ]
when :group
group = destination [ :obj ]
create_topic ( user : user ,
raw : body ,
2016-11-17 02:42:11 +08:00
elided : elided ,
2016-08-03 21:57:37 +08:00
title : subject ,
archetype : Archetype . private_message ,
target_group_names : [ group . name ] ,
is_group_message : true ,
skip_validations : true )
when :category
category = destination [ :obj ]
raise StrangersNotAllowedError if user . staged? && ! category . email_in_allow_strangers
raise InsufficientTrustLevelError if ! user . has_trust_level? ( SiteSetting . email_in_min_trust )
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 ,
category : category . id ,
skip_validations : user . staged? )
when :reply
email_log = destination [ :obj ]
2017-11-13 06:44:22 +08:00
if email_log . user_id != user . id && ! forwareded_reply_key? ( email_log , user )
2016-08-03 21:57:37 +08:00
raise ReplyUserNotMatchingError , " email_log.user_id => #{ email_log . user_id . inspect } , user.id => #{ user . id . inspect } "
end
create_reply ( user : user ,
raw : body ,
2016-11-17 02:42:11 +08:00
elided : elided ,
2016-08-03 21:57:37 +08:00
post : email_log . post ,
2017-02-09 04:38:52 +08:00
topic : email_log . post . topic ,
skip_validations : user . staged? )
2016-08-03 21:57:37 +08:00
end
end
2017-11-13 06:44:22 +08:00
def forwareded_reply_key? ( email_log , user )
incoming_emails = IncomingEmail
. joins ( :post )
. where ( 'posts.topic_id = ?' , email_log . topic_id )
2017-11-13 22:20:36 +08:00
. addressed_to ( email_log . reply_key )
. addressed_to ( user . email )
2017-11-13 06:44:22 +08:00
incoming_emails . each do | email |
next unless contains_email_address? ( email . to_addresses , user . email ) ||
contains_email_address? ( email . cc_addresses , user . email )
return true if contains_reply_by_email_address ( email . to_addresses , email_log . reply_key ) ||
contains_reply_by_email_address ( email . cc_addresses , email_log . reply_key )
end
false
end
def contains_email_address? ( addresses , email )
return false if addresses . blank?
addresses . split ( " ; " ) . include? ( email )
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 )
2017-01-06 22:32:25 +08:00
embedded = Mail . new ( embedded_email_raw )
2016-11-17 02:42:11 +08:00
email , display_name = parse_from_field ( embedded )
2016-12-02 01:34:47 +08:00
return false if email . blank? || ! email [ " @ " ]
2016-11-17 02:42:11 +08:00
embedded_user = find_or_create_user ( email , display_name )
2016-11-17 19:44:39 +08:00
raw = try_to_encode ( embedded . decoded , " UTF-8 " ) . presence || embedded . to_s
2016-11-17 02:42:11 +08:00
title = embedded . subject . presence || subject
case destination [ :type ]
when :group
group = destination [ :obj ]
post = create_topic ( user : embedded_user ,
raw : raw ,
title : title ,
archetype : Archetype . private_message ,
2016-12-02 01:34:47 +08:00
target_usernames : [ user . username ] ,
2016-11-17 02:42:11 +08:00
target_group_names : [ group . name ] ,
is_group_message : true ,
skip_validations : true ,
created_at : embedded . date )
when :category
category = destination [ :obj ]
return false if user . staged? && ! category . email_in_allow_strangers
return false if ! user . has_trust_level? ( SiteSetting . email_in_min_trust )
post = create_topic ( user : embedded_user ,
raw : raw ,
title : title ,
category : category . id ,
skip_validations : embedded_user . staged? ,
created_at : embedded . date )
else
return false
end
2017-01-06 22:32:25 +08:00
if post & . topic
# 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 ]
post_type = Post . types [ :whisper ] if post . topic . private_message? && group . usernames [ user . username ]
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? )
2017-01-06 22:32:25 +08:00
end
2016-11-17 02:42:11 +08:00
end
true
end
2017-07-28 09:20:09 +08:00
def self . reply_by_email_address_regex ( extract_reply_key = true )
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 ( " | " )
reply_addresses . flatten!
reply_addresses . select! ( & :present? )
reply_addresses . map! { | a | Regexp . escape ( a ) }
reply_addresses . map! { | a | a . gsub ( Regexp . escape ( " %{reply_key} " ) , " ( \\ h{32}) " ) }
/ #{ reply_addresses . join ( " | " ) } /
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
2016-01-19 07:57:55 +08:00
def find_related_post
2017-11-17 21:49:10 +08:00
return if SiteSetting . find_related_post_with_key && ! sent_to_mailinglist_mirror?
2017-06-19 19:12:55 +08:00
2016-02-11 05:00:27 +08:00
message_ids = [ @mail . in_reply_to , Email :: Receiver . extract_references ( @mail . references ) ]
2016-01-19 07:57:55 +08:00
message_ids . flatten!
message_ids . select! ( & :present? )
message_ids . uniq!
return if message_ids . empty?
2017-02-09 04:38:52 +08:00
message_ids = message_ids . first ( 5 )
host = Email :: Sender . host_for ( Discourse . base_url )
post_id_regexp = Regexp . new " topic/ \\ d+/( \\ d+)@ #{ Regexp . escape ( host ) } "
topic_id_regexp = Regexp . new " topic/( \\ d+)@ #{ Regexp . escape ( host ) } "
2017-02-09 06:46:11 +08:00
post_ids = message_ids . map { | message_id | message_id [ post_id_regexp , 1 ] } . compact . map ( & :to_i )
2017-02-09 04:38:52 +08:00
post_ids << Post . where ( topic_id : message_ids . map { | message_id | message_id [ topic_id_regexp , 1 ] } . compact , post_number : 1 ) . pluck ( :id )
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
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
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
2017-07-28 09:20:09 +08:00
def create_topic ( options = { } )
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
2017-07-28 09:20:09 +08:00
def create_reply ( options = { } )
2016-01-19 07:57:55 +08:00
raise TopicNotFoundError if options [ :topic ] . nil? || options [ :topic ] . trashed?
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 )
else
2016-07-05 23:33:08 +08:00
raise TopicClosedError if options [ :topic ] . closed?
2016-01-19 07:57:55 +08:00
options [ :topic_id ] = options [ :post ] . try ( :topic_id )
options [ :reply_to_post_number ] = options [ :post ] . try ( :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 )
PostActionCreator . new ( user , post ) . perform ( type )
rescue PostAction :: AlreadyActed
# it's cool, don't care
rescue Discourse :: InvalidAccess = > e
raise InvalidPostAction . new ( e )
end
2014-04-15 04:55:57 +08:00
2016-08-08 18:30:37 +08:00
def attachments
# strip blacklisted attachments (mostly signatures)
@attachments || = @mail . attachments . select do | attachment |
attachment . content_type !~ SiteSetting . attachment_content_type_blacklist_regex &&
attachment . filename !~ SiteSetting . attachment_filename_blacklist_regex
end
end
2016-08-03 23:55:54 +08:00
2017-07-28 09:20:09 +08:00
def create_post_with_attachments ( options = { } )
2014-04-15 04:55:57 +08:00
# deal with attachments
2017-10-06 20:28:26 +08:00
options [ :raw ] = add_attachments ( options [ :raw ] , options [ :user ] . id , options )
create_post ( options )
end
def add_attachments ( raw , user_id , options = { } )
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 ] }
2017-10-06 20:28:26 +08:00
upload = UploadCreator . new ( tmp , attachment . filename , opts ) . create_for ( user_id )
2017-11-08 02:17:33 +08:00
if upload & . valid?
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 )
elsif raw [ / \ [image:.*? \ d+[^ \ ]]* \ ] /i ]
raw . sub! ( / \ [image:.*? \ d+[^ \ ]]* \ ] /i , attachment_markdown ( upload ) )
2017-05-04 04:54:26 +08:00
else
2017-10-06 20:28:26 +08:00
raw << " \n \n #{ attachment_markdown ( upload ) } \n \n "
2017-05-04 04:54:26 +08:00
end
2016-01-19 07:57:55 +08:00
else
2017-10-06 20:28:26 +08:00
raw << " \n \n #{ attachment_markdown ( upload ) } \n \n "
2015-12-01 01:33:24 +08:00
end
2014-04-15 04:55:57 +08:00
end
ensure
2016-01-19 07:57:55 +08:00
tmp . try ( :close! ) rescue nil
2014-04-15 04:55:57 +08:00
end
end
2017-10-06 20:28:26 +08:00
raw
2014-04-15 04:55:57 +08:00
end
2014-04-15 06:04:13 +08:00
def attachment_markdown ( upload )
if FileHelper . is_image? ( upload . original_filename )
2014-04-15 04:55:57 +08:00
" <img src=' #{ upload . url } ' width=' #{ upload . width } ' height=' #{ upload . height } '> "
else
" <a class='attachment' href=' #{ upload . url } '> #{ upload . original_filename } </a> ( #{ number_to_human_size ( upload . filesize ) } ) "
end
end
2017-07-28 09:20:09 +08:00
def create_post ( options = { } )
2014-09-05 01:04:22 +08:00
options [ :via_email ] = true
2016-01-19 07:57:55 +08:00
options [ :raw_email ] = @raw_email
2014-09-05 01:04:22 +08:00
2016-01-19 07:57:55 +08:00
# ensure posts aren't created in the future
2016-11-17 02:42:11 +08:00
options [ :created_at ] || = @mail . date
2017-01-13 08:05:00 +08:00
if options [ :created_at ] . nil?
raise InvalidPost , " No post creation date found. Is the e-mail missing a Date: header? "
end
2017-07-28 09:20:09 +08:00
options [ :created_at ] = DateTime . now if options [ :created_at ] > DateTime . now
2016-01-19 07:57:55 +08:00
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 ] )
2016-03-18 06:10:46 +08:00
end
2016-04-12 00:20:26 +08:00
user = options . delete ( :user )
2017-01-06 22:32:25 +08:00
result = NewPostManager . new ( user , options ) . perform
2014-08-27 08:30:12 +08:00
2016-01-19 07:57:55 +08:00
raise InvalidPost , result . errors . full_messages . join ( " \n " ) if result . errors . any?
if result . post
@incoming_email . update_columns ( topic_id : result . post . topic_id , post_id : result . post . id )
if result . post . topic && result . post . topic . private_message?
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 )
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 ] )
post . topic . add_small_action ( sender , " invited_user " , user . username )
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
2017-10-06 22:37:28 +08:00
post . topic . add_moderator_post ( sender , I18n . t ( " emails.incoming.maximum_staged_user_per_email_reached " ) )
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 )
message = SubscriptionMailer . send ( action , user )
Email :: Sender . new ( message , :subscription ) . send
end
2017-10-03 23:28:41 +08:00
def delete_staged_users
@staged_users . each do | user |
2017-10-31 22:13:23 +08:00
if @incoming_email . user . id == user . id
@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
2013-06-11 04:46:08 +08:00
end
2016-01-19 07:57:55 +08:00
2013-06-11 04:46:08 +08:00
end