2016-01-19 00:57:55 +01:00
require_dependency " new_post_manager "
require_dependency " post_action_creator "
require_dependency " email/html_cleaner "
2013-06-10 16:46:08 -04:00
module Email
2014-04-14 22:55:57 +02:00
2013-06-10 16:46:08 -04:00
class Receiver
2016-01-19 00:57:55 +01:00
class ProcessingError < StandardError ; end
class EmptyEmailError < ProcessingError ; end
class NoMessageIdError < ProcessingError ; end
class AutoGeneratedEmailError < ProcessingError ; end
class NoBodyDetectedError < ProcessingError ; end
class InactiveUserError < ProcessingError ; end
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
def initialize ( mail_string )
raise EmptyEmailError if mail_string . blank?
@raw_email = mail_string
@mail = Mail . new ( @raw_email )
raise NoMessageIdError if @mail . message_id . blank?
2013-06-10 16:46:08 -04:00
end
def process
2016-01-19 00:57:55 +01:00
@incoming_email = find_or_create_incoming_email
process_internal
rescue = > e
@incoming_email . update_columns ( error : e . to_s )
raise
end
2014-08-13 11:06:17 -07:00
2016-01-19 00:57:55 +01:00
def find_or_create_incoming_email
IncomingEmail . find_or_create_by ( message_id : @mail . message_id ) do | incoming_email |
incoming_email . raw = @raw_email
incoming_email . subject = @mail . subject
incoming_email . from_address = @mail . from . first . downcase
incoming_email . to_addresses = @mail . to . map ( & :downcase ) . join ( " ; " ) if @mail . to . present?
incoming_email . cc_addresses = @mail . cc . map ( & :downcase ) . join ( " ; " ) if @mail . cc . present?
end
end
2014-02-27 16:36:33 +01:00
2016-01-19 00:57:55 +01:00
def process_internal
raise AutoGeneratedEmailError if is_auto_generated?
2014-02-24 17:36:53 +01:00
2016-01-19 00:57:55 +01:00
body = select_body || " "
2014-02-27 13:44:21 +01:00
2016-01-19 00:57:55 +01:00
raise NoBodyDetectedError if body . blank? && ! @mail . has_attachments?
2014-02-24 17:36:53 +01:00
2016-01-19 00:57:55 +01:00
user = find_or_create_user ( from )
2015-12-07 17:01:08 +01:00
2016-01-19 00:57:55 +01:00
@incoming_email . update_columns ( user_id : user . id )
2013-06-13 18:11:10 -04:00
2016-01-19 00:57:55 +01:00
raise InactiveUserError if ! user . active && ! user . staged
2015-12-07 17:01:08 +01:00
2016-01-20 22:25:25 +13:00
if action = subscription_action_for ( body , @mail . subject )
message = SubscriptionMailer . send ( action , user )
Email :: Sender . new ( message , :subscription ) . send
elsif post = find_related_post
2016-01-19 00:57:55 +01:00
create_reply ( user : user , raw : body , post : post , topic : post . topic )
else
destination = destinations . first
2015-12-07 17:01:08 +01:00
2016-01-19 00:57:55 +01:00
raise BadDestinationAddress if destination . blank?
2014-08-13 11:06:17 -07:00
2016-01-19 00:57:55 +01:00
case destination [ :type ]
when :group
group = destination [ :obj ]
create_topic ( user : user , raw : body , title : @mail . subject , archetype : Archetype . private_message , target_group_names : [ group . name ] , skip_validations : true )
when :category
category = destination [ :obj ]
2014-08-13 11:06:17 -07:00
2016-01-19 00:57:55 +01:00
raise StrangersNotAllowedError if user . staged? && ! category . email_in_allow_strangers
raise InsufficientTrustLevelError if ! user . has_trust_level? ( SiteSetting . email_in_min_trust )
2014-08-13 11:06:17 -07:00
2016-01-19 00:57:55 +01:00
create_topic ( user : user , raw : body , title : @mail . subject , category : category . id )
when :reply
email_log = destination [ :obj ]
2013-06-13 18:11:10 -04:00
2016-01-19 00:57:55 +01:00
raise ReplyUserNotMatchingError if email_log . user_id != user . id
2013-06-20 12:38:03 -04:00
2016-01-19 00:57:55 +01:00
create_reply ( user : user , raw : body , post : email_log . post , topic : email_log . post . topic )
end
end
end
2014-08-26 17:31:51 -07:00
2016-01-19 00:57:55 +01:00
def is_auto_generated?
@mail . return_path . blank? ||
@mail [ :precedence ] . to_s [ / list|junk|bulk|auto_reply / ] ||
@mail . header . to_s [ / auto-(submitted|replied|generated) / ]
2014-08-26 17:31:51 -07:00
end
2013-06-19 12:14:01 -04:00
2016-01-19 00:57:55 +01:00
def select_body
text = nil
2014-08-26 17:31:51 -07:00
html = nil
2015-11-30 18:33:24 +01:00
2016-01-19 00:57:55 +01:00
if @mail . multipart?
text = fix_charset ( @mail . text_part )
html = fix_charset ( @mail . html_part )
elsif @mail . content_type . to_s [ " text/html " ]
html = fix_charset ( @mail )
else
text = fix_charset ( @mail )
2014-01-17 10:24:32 +08:00
end
2014-03-28 09:57:12 -04:00
2016-01-19 00:57:55 +01:00
# prefer text over html
if text . present?
text_encoding = text . encoding
text = DiscourseEmailParser . parse_reply ( text )
text = try_to_encode ( text , text_encoding )
return text if text . present?
end
# clean the html if that's all we've got
if html . present?
html_encoding = html . encoding
html = Email :: HtmlCleaner . new ( html ) . output_html
html = DiscourseEmailParser . parse_reply ( html )
html = try_to_encode ( html , html_encoding )
return html if html . present?
2013-06-20 12:38:03 -04:00
end
2016-01-19 00:57:55 +01:00
end
def fix_charset ( mail_part )
return nil if mail_part . blank? || mail_part . body . blank?
string = mail_part . body . to_s
2013-06-20 12:38:03 -04:00
2016-01-19 00:57:55 +01:00
# TODO (use charlock_holmes to properly detect encoding)
2015-05-22 15:40:26 -04:00
2016-01-19 00:57:55 +01:00
# 1) use the charset provided
if mail_part . charset . present?
fixed = try_to_encode ( string , mail_part . charset )
return fixed if fixed . present?
2014-08-26 17:31:51 -07:00
end
2013-11-20 13:29:42 -05:00
2016-01-19 00:57:55 +01:00
# 2) default to UTF-8
try_to_encode ( string , " UTF-8 " )
end
def try_to_encode ( string , encoding )
string . encode ( " UTF-8 " , encoding )
rescue Encoding :: InvalidByteSequenceError , Encoding :: UndefinedConversionError
nil
2013-06-20 12:38:03 -04:00
end
2016-01-19 00:57:55 +01:00
def from
@from || = @mail [ :from ] . address_list . addresses . first
end
2013-06-20 12:38:03 -04:00
2016-01-19 00:57:55 +01:00
def find_or_create_user ( address_field )
# decode the address field
address_field . decoded
# extract email and name
email = address_field . address . downcase
name = address_field . display_name . try ( :to_s )
username = UserNameSuggester . sanitize_username ( name ) if name . present?
User . find_or_create_by ( email : email ) do | user |
user . username = UserNameSuggester . suggest ( username . presence || email )
user . name = name . presence || User . suggest_name ( email )
user . staged = true
2014-08-26 17:31:51 -07:00
end
2013-06-19 12:14:01 -04:00
end
2013-06-13 18:11:10 -04:00
2016-01-19 00:57:55 +01:00
def destinations
[ @mail . destinations ,
[ @mail [ :x_forwarded_to ] ] . flatten . compact . map ( & :decoded ) ,
[ @mail [ :delivered_to ] ] . flatten . compact . map ( & :decoded ) ,
] . flatten
. select ( & :present? )
. uniq
. lazy
. map { | d | check_address ( d ) }
. drop_while ( & :blank? )
2015-11-18 15:22:50 -05:00
end
2016-01-19 00:57:55 +01:00
def check_address ( address )
# 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-24 14:22:32 -04:00
2016-01-19 00:57:55 +01:00
category = Category . find_by_email ( address )
return { type : :category , obj : category } if category
2015-11-18 15:22:50 -05:00
end
2016-01-19 00:57:55 +01:00
# reply
match = reply_by_email_address_regex . match ( address )
if match && match [ 1 ] . present?
email_log = EmailLog . for ( match [ 1 ] )
return { type : :reply , obj : email_log } if email_log
2013-07-24 14:22:32 -04:00
end
2016-01-19 00:57:55 +01:00
end
2013-07-24 14:22:32 -04:00
2016-01-19 00:57:55 +01:00
def reply_by_email_address_regex
@reply_by_email_address_regex || = Regexp . new Regexp . escape ( SiteSetting . reply_by_email_address )
. gsub ( Regexp . escape ( " %{reply_key} " ) , " ([[:xdigit:]]{32}) " )
2013-07-24 14:22:32 -04:00
end
2016-01-19 00:57:55 +01:00
def find_related_post
message_ids = [ @mail . in_reply_to , extract_references ]
message_ids . flatten!
message_ids . select! ( & :present? )
message_ids . uniq!
return if message_ids . empty?
IncomingEmail . where . not ( post_id : nil )
. where ( message_id : message_ids )
. first
. try ( :post )
end
2015-11-24 16:58:26 +01:00
2016-01-19 00:57:55 +01:00
def extract_references
if Array === @mail . references
@mail . references
elsif @mail . references . present?
@mail . references . split ( / [ \ s,] / ) . map { | r | r . sub ( / ^< / , " " ) . sub ( / >$ / , " " ) }
end
2014-04-14 22:55:57 +02:00
end
2016-01-19 00:57:55 +01:00
def likes
@likes || = Set . new [ " +1 " , I18n . t ( 'post_action_types.like.title' ) . downcase ]
2015-12-30 12:17:45 +01:00
end
2016-01-20 22:25:25 +13: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 12:17:45 +01:00
def post_action_for ( body )
2016-01-19 00:57:55 +01:00
if likes . include? ( body . strip . downcase )
2015-12-30 12:17:45 +01:00
PostActionType . types [ :like ]
end
end
2016-01-19 00:57:55 +01:00
def create_topic ( options = { } )
create_post_with_attachments ( options )
2013-06-10 16:46:08 -04:00
end
2014-02-24 17:36:53 +01:00
2016-01-19 00:57:55 +01:00
def create_reply ( options = { } )
raise TopicNotFoundError if options [ :topic ] . nil? || options [ :topic ] . trashed?
raise TopicClosedError if options [ :topic ] . closed?
2014-04-14 22:55:57 +02:00
2016-01-19 00:57:55 +01:00
if post_action_type = post_action_for ( options [ :raw ] )
create_post_action ( options [ :user ] , options [ :post ] , post_action_type )
else
options [ :topic_id ] = options [ :post ] . try ( :topic_id )
options [ :reply_to_post_number ] = options [ :post ] . try ( :post_number )
create_post_with_attachments ( options )
end
2014-04-14 22:55:57 +02:00
end
2016-01-19 00:57:55 +01: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-14 22:55:57 +02:00
2016-01-19 00:57:55 +01:00
def create_post_with_attachments ( options = { } )
2014-04-14 22:55:57 +02:00
# deal with attachments
2016-01-19 00:57:55 +01:00
@mail . attachments . each do | attachment |
2014-04-14 22:55:57 +02:00
tmp = Tempfile . new ( " discourse-email-attachment " )
begin
# read attachment
File . open ( tmp . path , " w+b " ) { | f | f . write attachment . body . decoded }
# create the upload for the user
2016-01-19 00:57:55 +01:00
upload = Upload . create_for ( options [ :user ] . id , tmp , attachment . filename , tmp . size )
2014-04-14 22:55:57 +02:00
if upload && upload . errors . empty?
2015-11-30 18:33:24 +01:00
# try to inline images
2016-01-19 00:57:55 +01:00
if attachment . content_type . start_with? ( " image/ " ) && options [ :raw ] [ / \ [image: .+ \ d+ \ ] / ]
options [ :raw ] . sub! ( / \ [image: .+ \ d+ \ ] / , attachment_markdown ( upload ) )
else
options [ :raw ] << " \n #{ attachment_markdown ( upload ) } \n "
2015-11-30 18:33:24 +01:00
end
2014-04-14 22:55:57 +02:00
end
ensure
2016-01-19 00:57:55 +01:00
tmp . try ( :close! ) rescue nil
2014-04-14 22:55:57 +02:00
end
end
2016-01-19 00:57:55 +01:00
post_options = {
cooking_options : { traditional_markdown_linebreaks : true } ,
} . merge ( options )
2014-08-26 17:30:12 -07:00
2016-01-19 00:57:55 +01:00
create_post ( post_options )
2014-04-14 22:55:57 +02:00
end
2014-04-15 00:04:13 +02:00
def attachment_markdown ( upload )
if FileHelper . is_image? ( upload . original_filename )
2014-04-14 22:55:57 +02: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
2016-01-19 00:57:55 +01:00
def create_post ( options = { } )
2014-09-04 13:04:22 -04:00
options [ :via_email ] = true
2016-01-19 00:57:55 +01:00
options [ :raw_email ] = @raw_email
2014-09-04 13:04:22 -04:00
2016-01-19 00:57:55 +01:00
# ensure posts aren't created in the future
options [ :created_at ] = [ @mail . date , DateTime . now ] . min
manager = NewPostManager . new ( options [ :user ] , options )
2015-04-28 13:04:34 -04:00
result = manager . perform
2014-08-26 17:30:12 -07:00
2016-01-19 00:57:55 +01: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?
add_other_addresses ( result . post . topic , options [ :user ] )
end
2014-07-31 18:46:02 +10:00
end
2016-01-19 00:57:55 +01:00
end
2014-08-26 17:30:12 -07:00
2016-01-19 00:57:55 +01:00
def add_other_addresses ( topic , sender )
% i ( to cc bcc ) . each do | d |
if @mail [ d ] && @mail [ d ] . address_list && @mail [ d ] . address_list . addresses
2016-01-19 15:24:34 +01:00
@mail [ d ] . address_list . addresses . each do | address_field |
2016-01-19 00:57:55 +01:00
begin
2016-01-19 15:24:34 +01:00
email = address_field . address . downcase
if email !~ reply_by_email_address_regex
user = find_or_create_user ( address_field )
if can_invite? ( topic , user )
2016-01-19 00:57:55 +01:00
topic . topic_allowed_users . create! ( user_id : user . id )
topic . add_small_action ( sender , " invited_user " , user . username )
end
end
rescue ActiveRecord :: RecordInvalid
# don't care if user already allowed
end
end
end
end
2014-02-24 00:01:37 -06:00
end
2013-06-10 16:46:08 -04:00
2016-01-19 15:24:34 +01: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
2013-06-10 16:46:08 -04:00
end
2016-01-19 00:57:55 +01:00
2013-06-10 16:46:08 -04:00
end