2014-08-26 17:31:51 -07:00
require 'email/html_cleaner'
2013-06-10 16:46:08 -04:00
#
# Handles an incoming message
#
module Email
2014-04-14 22:55:57 +02:00
2013-06-10 16:46:08 -04:00
class Receiver
2014-04-14 22:55:57 +02:00
include ActionView :: Helpers :: NumberHelper
2014-02-28 13:05:09 +01:00
class ProcessingError < StandardError ; end
class EmailUnparsableError < ProcessingError ; end
class EmptyEmailError < ProcessingError ; end
class UserNotFoundError < ProcessingError ; end
class UserNotSufficientTrustLevelError < ProcessingError ; end
2014-08-13 11:06:17 -07:00
class BadDestinationAddress < ProcessingError ; end
2014-02-28 13:05:09 +01:00
class EmailLogNotFound < ProcessingError ; end
2014-07-31 18:46:02 +10:00
class InvalidPost < ProcessingError ; end
2013-06-10 16:46:08 -04:00
2014-08-13 11:06:17 -07:00
attr_reader :body , :email_log
2013-06-13 18:11:10 -04:00
def initialize ( raw )
@raw = raw
2013-06-10 16:46:08 -04:00
end
def process
2014-02-28 13:05:09 +01:00
raise EmptyEmailError if @raw . blank?
2013-06-13 18:11:10 -04:00
2014-08-26 17:31:51 -07:00
message = Mail . new ( @raw )
2013-06-13 18:11:10 -04:00
2014-08-26 17:31:51 -07:00
body = parse_body message
2014-02-24 17:36:53 +01:00
2014-08-13 11:06:17 -07:00
dest_info = { type : :invalid , obj : nil }
2014-08-26 17:31:51 -07:00
message . to . each do | to_address |
2014-08-13 11:06:17 -07:00
if dest_info [ :type ] == :invalid
dest_info = check_address to_address
end
end
raise BadDestinationAddress if dest_info [ :type ] == :invalid
2014-08-26 17:31:51 -07:00
# TODO get to a state where we can remove this
@message = message
@body = body
2014-08-13 11:06:17 -07:00
if dest_info [ :type ] == :category
raise BadDestinationAddress unless SiteSetting . email_in
category = dest_info [ :obj ]
@category_id = category . id
@allow_strangers = category . email_in_allow_strangers
user_email = @message . from . first
@user = User . find_by_email ( user_email )
2014-04-14 22:55:57 +02:00
if @user . blank? && @allow_strangers
2014-08-13 11:06:17 -07:00
wrap_body_in_quote user_email
2014-07-31 18:46:02 +10:00
# TODO This is WRONG it should register an account
# and email the user details on how to log in / activate
2014-02-27 16:36:33 +01:00
@user = Discourse . system_user
end
2014-02-28 13:05:09 +01:00
raise UserNotFoundError if @user . blank?
2014-09-05 15:20:39 +10:00
raise UserNotSufficientTrustLevelError . new @user unless @allow_strangers || @user . has_trust_level? ( TrustLevel [ SiteSetting . email_in_min_trust . to_i ] )
2014-02-24 17:36:53 +01:00
create_new_topic
2014-02-28 13:05:09 +01:00
else
2014-08-13 11:06:17 -07:00
@email_log = dest_info [ :obj ]
2014-02-27 13:44:21 +01:00
2014-02-28 13:05:09 +01:00
raise EmailLogNotFound if @email_log . blank?
2014-02-24 17:36:53 +01:00
2014-02-28 13:05:09 +01:00
create_reply
end
2014-08-26 17:31:51 -07:00
rescue Encoding :: UndefinedConversionError , Encoding :: InvalidByteSequenceError = > e
raise EmailUnparsableError . new ( e )
2013-06-13 18:11:10 -04:00
end
2014-08-13 11:06:17 -07:00
def check_address ( address )
category = Category . find_by_email ( address )
return { type : :category , obj : category } if category
regex = Regexp . escape SiteSetting . reply_by_email_address
regex = regex . gsub ( Regexp . escape ( '%{reply_key}' ) , " (.*) " )
regex = Regexp . new regex
match = regex . match address
if match && match [ 1 ] . present?
reply_key = match [ 1 ]
email_log = EmailLog . for ( reply_key )
return { type : :reply , obj : email_log }
end
{ type : :invalid , obj : nil }
end
2014-08-26 17:31:51 -07:00
def parse_body ( message )
body = select_body message
2014-08-28 12:09:42 -07:00
encoding = body . encoding
2014-08-26 17:31:51 -07:00
raise EmptyEmailError if body . strip . blank?
2013-06-13 18:11:10 -04:00
2014-08-26 17:31:51 -07:00
body = discourse_email_trimmer body
raise EmptyEmailError if body . strip . blank?
2013-06-20 12:38:03 -04:00
2014-08-26 17:31:51 -07:00
body = EmailReplyParser . parse_reply body
raise EmptyEmailError if body . strip . blank?
2014-08-28 12:09:42 -07:00
body . force_encoding ( encoding ) . encode ( " UTF-8 " )
2014-08-26 17:31:51 -07:00
end
2013-06-19 12:14:01 -04:00
2014-08-26 17:31:51 -07:00
def select_body ( message )
html = nil
# If the message is multipart, return that part (favor html)
if message . multipart?
html = fix_charset message . html_part
text = fix_charset message . text_part
# TODO picking text if available may be better
if text && ! html
return text
2014-01-17 10:24:32 +08:00
end
2014-08-26 17:31:51 -07:00
elsif message . content_type =~ / text \/ html /
html = fix_charset message
2014-01-17 10:24:32 +08:00
end
2014-03-28 09:57:12 -04:00
2014-08-26 17:31:51 -07:00
if html
body = HtmlCleaner . new ( html ) . output_html
else
body = fix_charset message
2013-06-20 12:38:03 -04:00
end
2013-11-20 13:29:42 -05:00
# Certain trigger phrases that means we didn't parse correctly
2014-08-26 17:31:51 -07:00
if body =~ / Content \ -Type \ : / || body =~ / multipart \/ alternative / || body =~ / text \/ plain /
raise EmptyEmailError
end
2013-11-20 13:29:42 -05:00
2014-08-26 17:31:51 -07:00
body
2013-06-20 12:38:03 -04:00
end
2014-08-26 17:31:51 -07:00
# Force encoding to UTF-8 on a Mail::Message or Mail::Part
def fix_charset ( object )
return nil if object . nil?
2013-06-20 12:38:03 -04:00
2014-08-26 17:31:51 -07:00
if object . charset
object . body . decoded . force_encoding ( object . charset ) . encode ( " UTF-8 " ) . to_s
else
object . body . to_s
end
2013-06-19 12:14:01 -04:00
end
2013-06-13 18:11:10 -04:00
2014-08-26 17:31:51 -07:00
REPLYING_HEADER_LABELS = [ 'From' , 'Sent' , 'To' , 'Subject' , 'Reply To' ]
REPLYING_HEADER_REGEX = Regexp . union ( REPLYING_HEADER_LABELS . map { | lbl | " #{ lbl } : " } )
def discourse_email_trimmer ( body )
lines = body . scrub . lines . to_a
2013-07-24 14:22:32 -04:00
range_end = 0
2013-12-30 14:05:25 +11:00
lines . each_with_index do | l , idx |
2013-07-24 14:22:32 -04:00
break if l =~ / \ A \ s* \ -{3,80} \ s* \ z / ||
l =~ Regexp . new ( " \\ A \\ s* " + I18n . t ( 'user_notifications.previous_discussion' ) + " \\ s* \\ Z " ) ||
2013-08-21 16:54:01 -04:00
( l =~ / via #{ SiteSetting . title } (.*) \ :$ / ) ||
2013-07-24 14:22:32 -04:00
# This one might be controversial but so many reply lines have years, times and end with a colon.
# Let's try it and see how well it works.
2014-09-05 18:26:45 -07:00
( l =~ / \ d{4} / && l =~ / \ d: \ d \ d / && l =~ / \ :$ / ) ||
( l =~ / On \ w+ \ d+,? \ d+,?.*wrote: / )
2013-07-24 14:22:32 -04:00
2014-08-26 17:31:51 -07:00
# Headers on subsequent lines
break if ( 0 .. 2 ) . all? { | off | lines [ idx + off ] =~ REPLYING_HEADER_REGEX }
# Headers on the same line
break if REPLYING_HEADER_LABELS . count { | lbl | l . include? lbl } > = 3
2013-07-24 14:22:32 -04:00
range_end = idx
end
2014-08-26 17:31:51 -07:00
lines [ 0 .. range_end ] . join . strip
2013-07-24 14:22:32 -04:00
end
2014-08-13 11:06:17 -07:00
def wrap_body_in_quote ( user_email )
@body = " [quote= \" #{ user_email } \" ]
2014-04-14 22:55:57 +02:00
#{@body}
[ / quote]"
end
2014-08-26 17:30:12 -07:00
private
2013-07-24 14:22:32 -04:00
def create_reply
2014-08-26 17:30:12 -07:00
create_post_with_attachments ( @email_log . user ,
raw : @body ,
topic_id : @email_log . topic_id ,
reply_to_post_number : @email_log . post . post_number )
2013-06-10 16:46:08 -04:00
end
2014-02-24 17:36:53 +01:00
def create_new_topic
2014-08-26 17:30:12 -07:00
post = create_post_with_attachments ( @user ,
raw : @body ,
title : @message . subject ,
category : @category_id )
2014-04-14 22:55:57 +02:00
EmailLog . create (
email_type : " topic_via_incoming_email " ,
2014-08-26 17:30:12 -07:00
to_address : @message . from . first , # pick from address because we want the user's email
topic_id : post . topic . id ,
2014-04-14 22:55:57 +02:00
user_id : @user . id ,
)
post
end
2014-08-26 17:30:12 -07:00
def create_post_with_attachments ( user , post_opts = { } )
2014-04-14 22:55:57 +02:00
options = {
cooking_options : { traditional_markdown_linebreaks : true } ,
2014-08-26 17:30:12 -07:00
} . merge ( post_opts )
raw = options [ :raw ]
2014-04-14 22:55:57 +02:00
# deal with attachments
@message . attachments . each do | attachment |
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
upload = Upload . create_for ( user . id , tmp , attachment . filename , File . size ( tmp ) )
if upload && upload . errors . empty?
# TODO: should use the same code as the client to insert attachments
raw << " \n #{ attachment_markdown ( upload ) } \n "
end
ensure
tmp . close!
end
end
2014-08-26 17:30:12 -07:00
options [ :raw ] = raw
2014-04-14 22:55:57 +02:00
create_post ( user , options )
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
def create_post ( user , options )
2014-09-04 13:04:22 -04:00
# Mark the reply as incoming via email
options [ :via_email ] = true
2014-07-31 18:46:02 +10:00
creator = PostCreator . new ( user , options )
post = creator . create
2014-08-26 17:30:12 -07:00
2014-07-31 18:46:02 +10:00
if creator . errors . present?
raise InvalidPost , creator . errors . full_messages . join ( " \n " )
end
2014-08-26 17:30:12 -07:00
2014-07-31 18:46:02 +10:00
post
2014-02-24 00:01:37 -06:00
end
2013-06-10 16:46:08 -04:00
end
end