discourse/app/models/email_log.rb
Alan Guo Xiang Tan ab3a032b4b
SECURITY: BCC active user emails from group SMTP (#19725)
When sending emails out via group SMTP, if we
are sending them to non-staged users we want
to mask those emails with BCC, just so we don't
expose them to anyone we shouldn't. Staged users
are ones that have likely only interacted with
support via email, and will likely include other
people who were CC'd on the original email to the
group.

Co-authored-by: Martin Brennan <martin@discourse.org>
2023-01-05 06:07:50 +08:00

159 lines
4.3 KiB
Ruby

# frozen_string_literal: true
class EmailLog < ActiveRecord::Base
CRITICAL_EMAIL_TYPES ||= Set.new %w{
account_created
admin_login
confirm_new_email
confirm_old_email
confirm_old_email_add
forgot_password
notify_old_email
notify_old_email_add
signup
signup_after_approval
}
# cf. https://www.iana.org/assignments/smtp-enhanced-status-codes/smtp-enhanced-status-codes.xhtml
SMTP_ERROR_CODE_REGEXP = Regexp.new(/\d\.\d\.\d+|\d{3}/).freeze
belongs_to :user
belongs_to :post
belongs_to :smtp_group, class_name: 'Group'
validates :email_type, :to_address, presence: true
scope :bounced, -> { where(bounced: true) }
scope :addressed_to_user, ->(user) do
where(<<~SQL, user_id: user.id)
EXISTS(
SELECT 1
FROM user_emails
WHERE user_emails.user_id = :user_id AND
(email_logs.to_address = user_emails.email OR
email_logs.cc_addresses ILIKE '%' || user_emails.email || '%')
)
SQL
end
before_save do
if self.bounce_error_code.present?
match = SMTP_ERROR_CODE_REGEXP.match(self.bounce_error_code)
self.bounce_error_code = match.present? ? match[0] : nil
end
end
after_create do
# Update last_emailed_at if the user_id is present and email was sent
User.where(id: user_id).update_all("last_emailed_at = CURRENT_TIMESTAMP") if user_id.present?
end
def topic
@topic ||= self.topic_id.present? ? Topic.find_by(id: self.topic_id) : self.post&.topic
end
def self.unique_email_per_post(post, user)
return yield unless post && user
DistributedMutex.synchronize("email_log_#{post.id}_#{user.id}") do
if where(post_id: post.id, user_id: user.id).exists?
nil
else
yield
end
end
end
def self.reached_max_emails?(user, email_type = nil)
return false if SiteSetting.max_emails_per_day_per_user == 0 || CRITICAL_EMAIL_TYPES.include?(email_type)
count = where('created_at > ?', 1.day.ago)
.where(user_id: user.id)
.count
count >= SiteSetting.max_emails_per_day_per_user
end
def self.count_per_day(start_date, end_date)
where("created_at BETWEEN ? AND ?", start_date, end_date)
.group("DATE(created_at)")
.order("DATE(created_at)")
.count
end
def self.for(reply_key)
self.find_by(reply_key: reply_key)
end
def self.last_sent_email_address
self.where(email_type: "signup")
.order(created_at: :desc)
.limit(1)
.pluck(:to_address)
.first
end
def bounce_key
super&.delete('-')
end
def cc_users
return [] if !self.cc_user_ids
@cc_users ||= User.where(id: self.cc_user_ids)
end
def cc_addresses_split
@cc_addresses_split ||= self.cc_addresses&.split(";") || []
end
def as_mail_message
return if self.raw.blank?
@mail_message ||= Mail.new(self.raw)
end
def raw_headers
return if self.raw.blank?
as_mail_message.header.raw_source
end
def raw_body
return if self.raw.blank?
as_mail_message.body
end
end
# == Schema Information
#
# Table name: email_logs
#
# id :integer not null, primary key
# to_address :string not null
# email_type :string not null
# user_id :integer
# created_at :datetime not null
# updated_at :datetime not null
# post_id :integer
# bounce_key :uuid
# bounced :boolean default(FALSE), not null
# message_id :string
# smtp_group_id :integer
# cc_addresses :text
# cc_user_ids :integer is an Array
# raw :text
# topic_id :integer
# bounce_error_code :string
# smtp_transaction_response :string(500)
# bcc_addresses :text
#
# Indexes
#
# index_email_logs_on_bounce_key (bounce_key) UNIQUE WHERE (bounce_key IS NOT NULL)
# index_email_logs_on_bounced (bounced)
# index_email_logs_on_created_at (created_at)
# index_email_logs_on_message_id (message_id)
# index_email_logs_on_post_id (post_id)
# index_email_logs_on_topic_id (topic_id) WHERE (topic_id IS NOT NULL)
# index_email_logs_on_user_id (user_id)
#