PageRenderTime 52ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/email/sender.rb

https://gitlab.com/Ruwan-Ranganath/discourse
Ruby | 194 lines | 131 code | 41 blank | 22 comment | 32 complexity | 6fad3a29762ba55d332505e01f8f7941 MD5 | raw file
  1. #
  2. # A helper class to send an email. It will also handle a nil message, which it considers
  3. # to be "do nothing". This is because some Mailers will decide not to do work for some
  4. # reason. For example, emailing a user too frequently. A nil to address is also considered
  5. # "do nothing"
  6. #
  7. # It also adds an HTML part for the plain text body
  8. #
  9. require_dependency 'email/renderer'
  10. require 'uri'
  11. require 'net/smtp'
  12. SMTP_CLIENT_ERRORS = [Net::SMTPFatalError, Net::SMTPSyntaxError]
  13. module Email
  14. class Sender
  15. def initialize(message, email_type, user=nil)
  16. @message = message
  17. @email_type = email_type
  18. @user = user
  19. end
  20. def send
  21. return if SiteSetting.disable_emails && @email_type.to_s != "admin_login"
  22. return if ActionMailer::Base::NullMail === @message
  23. return if ActionMailer::Base::NullMail === (@message.message rescue nil)
  24. return skip(I18n.t('email_log.message_blank')) if @message.blank?
  25. return skip(I18n.t('email_log.message_to_blank')) if @message.to.blank?
  26. if @message.text_part
  27. return skip(I18n.t('email_log.text_part_body_blank')) if @message.text_part.body.to_s.blank?
  28. else
  29. return skip(I18n.t('email_log.body_blank')) if @message.body.to_s.blank?
  30. end
  31. @message.charset = 'UTF-8'
  32. opts = {}
  33. renderer = Email::Renderer.new(@message, opts)
  34. if @message.html_part
  35. @message.html_part.body = renderer.html
  36. else
  37. @message.html_part = Mail::Part.new do
  38. content_type 'text/html; charset=UTF-8'
  39. body renderer.html
  40. end
  41. end
  42. @message.parts[0].body = @message.parts[0].body.to_s.gsub(/\[\/?email-indent\]/, '')
  43. # Fix relative (ie upload) HTML links in markdown which do not work well in plain text emails.
  44. # These are the links we add when a user uploads a file or image.
  45. # Ideally we would parse general markdown into plain text, but that is almost an intractable problem.
  46. url_prefix = Discourse.base_url
  47. @message.parts[0].body = @message.parts[0].body.to_s.gsub(/<a class="attachment" href="(\/uploads\/default\/[^"]+)">([^<]*)<\/a>/, '[\2]('+url_prefix+'\1)')
  48. @message.parts[0].body = @message.parts[0].body.to_s.gsub(/<img src="(\/uploads\/default\/[^"]+)"([^>]*)>/, '![]('+url_prefix+'\1)')
  49. @message.text_part.content_type = 'text/plain; charset=UTF-8'
  50. # Set up the email log
  51. email_log = EmailLog.new(email_type: @email_type, to_address: to_address, user_id: @user.try(:id))
  52. host = Email::Sender.host_for(Discourse.base_url)
  53. topic_id = header_value('X-Discourse-Topic-Id')
  54. post_id = header_value('X-Discourse-Post-Id')
  55. reply_key = header_value('X-Discourse-Reply-Key')
  56. # always set a default Message ID from the host
  57. uuid = SecureRandom.uuid
  58. @message.header['Message-ID'] = "<#{uuid}@#{host}>"
  59. if topic_id.present?
  60. email_log.topic_id = topic_id
  61. incoming_email = IncomingEmail.find_by(post_id: post_id, topic_id: topic_id)
  62. incoming_message_id = nil
  63. incoming_message_id = "<#{incoming_email.message_id}>" if incoming_email.try(:message_id).present?
  64. topic_identifier = "<topic/#{topic_id}@#{host}>"
  65. post_identifier = "<topic/#{topic_id}/#{post_id}@#{host}>"
  66. @message.header['Message-ID'] = post_identifier
  67. @message.header['In-Reply-To'] = incoming_message_id || topic_identifier
  68. @message.header['References'] = topic_identifier
  69. topic = Topic.where(id: topic_id).first
  70. # http://www.ietf.org/rfc/rfc2919.txt
  71. if topic && topic.category && !topic.category.uncategorized?
  72. list_id = "<#{topic.category.name.downcase.gsub(' ', '-')}.#{host}>"
  73. # subcategory case
  74. if !topic.category.parent_category_id.nil?
  75. parent_category_name = Category.find_by(id: topic.category.parent_category_id).name
  76. list_id = "<#{topic.category.name.downcase.gsub(' ', '-')}.#{parent_category_name.downcase.gsub(' ', '-')}.#{host}>"
  77. end
  78. else
  79. list_id = "<#{host}>"
  80. end
  81. # http://www.ietf.org/rfc/rfc3834.txt
  82. @message.header['Precedence'] = 'list'
  83. @message.header['List-ID'] = list_id
  84. @message.header['List-Archive'] = topic.url if topic
  85. end
  86. if reply_key.present? && @message.header['Reply-To'] =~ /\<([^\>]+)\>/
  87. email = Regexp.last_match[1]
  88. @message.header['List-Post'] = "<mailto:#{email}>"
  89. end
  90. if SiteSetting.reply_by_email_address.present? && SiteSetting.reply_by_email_address["+"]
  91. email_log.bounce_key = SecureRandom.hex
  92. # WARNING: RFC claims you can not set the Return Path header, this is 100% correct
  93. # however Rails has special handling for this header and ends up using this value
  94. # as the Envelope From address so stuff works as expected
  95. @message.header[:return_path] = SiteSetting.reply_by_email_address.sub("%{reply_key}", "verp-#{email_log.bounce_key}")
  96. end
  97. email_log.post_id = post_id if post_id.present?
  98. email_log.reply_key = reply_key if reply_key.present?
  99. # Remove headers we don't need anymore
  100. @message.header['X-Discourse-Topic-Id'] = nil if topic_id.present?
  101. @message.header['X-Discourse-Post-Id'] = nil if post_id.present?
  102. @message.header['X-Discourse-Reply-Key'] = nil if reply_key.present?
  103. # Suppress images from short emails
  104. if SiteSetting.strip_images_from_short_emails &&
  105. @message.html_part.body.to_s.bytesize <= SiteSetting.short_email_length &&
  106. @message.html_part.body =~ /<img[^>]+>/
  107. style = Email::Styles.new(@message.html_part.body.to_s)
  108. @message.html_part.body = style.strip_avatars_and_emojis
  109. end
  110. begin
  111. @message.deliver_now
  112. rescue *SMTP_CLIENT_ERRORS => e
  113. return skip(e.message)
  114. end
  115. # Save and return the email log
  116. email_log.save!
  117. email_log
  118. end
  119. def to_address
  120. @to_address ||= begin
  121. to = @message.try(:to)
  122. to = to.first if Array === to
  123. to.presence || "no_email_found"
  124. end
  125. end
  126. def self.host_for(base_url)
  127. host = "localhost"
  128. if base_url.present?
  129. begin
  130. uri = URI.parse(base_url)
  131. host = uri.host.downcase if uri.host.present?
  132. rescue URI::InvalidURIError
  133. end
  134. end
  135. host
  136. end
  137. private
  138. def header_value(name)
  139. header = @message.header[name]
  140. return nil unless header
  141. header.value
  142. end
  143. def skip(reason)
  144. EmailLog.create!(
  145. email_type: @email_type,
  146. to_address: to_address,
  147. user_id: @user.try(:id),
  148. skipped: true,
  149. skipped_reason: "[Sender] #{reason}"
  150. )
  151. end
  152. end
  153. end