mirror of
https://github.com/discourse/discourse.git
synced 2025-06-02 04:08:41 +08:00
DEV: Apply syntax_tree formatting to lib/*
This commit is contained in:
@ -8,11 +8,11 @@
|
||||
#
|
||||
# It also adds an HTML part for the plain text body
|
||||
#
|
||||
require 'uri'
|
||||
require 'net/smtp'
|
||||
require "uri"
|
||||
require "net/smtp"
|
||||
|
||||
SMTP_CLIENT_ERRORS = [Net::SMTPFatalError, Net::SMTPSyntaxError]
|
||||
BYPASS_DISABLE_TYPES = %w(
|
||||
BYPASS_DISABLE_TYPES = %w[
|
||||
admin_login
|
||||
test_message
|
||||
new_version
|
||||
@ -20,11 +20,10 @@ BYPASS_DISABLE_TYPES = %w(
|
||||
invite_password_instructions
|
||||
download_backup_message
|
||||
admin_confirmation_message
|
||||
)
|
||||
]
|
||||
|
||||
module Email
|
||||
class Sender
|
||||
|
||||
def initialize(message, email_type, user = nil)
|
||||
@message = message
|
||||
@message_attachments_index = {}
|
||||
@ -35,33 +34,40 @@ module Email
|
||||
def send
|
||||
bypass_disable = BYPASS_DISABLE_TYPES.include?(@email_type.to_s)
|
||||
|
||||
if SiteSetting.disable_emails == "yes" && !bypass_disable
|
||||
return if SiteSetting.disable_emails == "yes" && !bypass_disable
|
||||
|
||||
return if ActionMailer::Base::NullMail === @message
|
||||
if ActionMailer::Base::NullMail ===
|
||||
(
|
||||
begin
|
||||
@message.message
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
return if ActionMailer::Base::NullMail === @message
|
||||
return if ActionMailer::Base::NullMail === (@message.message rescue nil)
|
||||
|
||||
return skip(SkippedEmailLog.reason_types[:sender_message_blank]) if @message.blank?
|
||||
return skip(SkippedEmailLog.reason_types[:sender_message_blank]) if @message.blank?
|
||||
return skip(SkippedEmailLog.reason_types[:sender_message_to_blank]) if @message.to.blank?
|
||||
|
||||
if SiteSetting.disable_emails == "non-staff" && !bypass_disable
|
||||
return unless find_user&.staff?
|
||||
end
|
||||
|
||||
return skip(SkippedEmailLog.reason_types[:sender_message_to_invalid]) if to_address.end_with?(".invalid")
|
||||
if to_address.end_with?(".invalid")
|
||||
return skip(SkippedEmailLog.reason_types[:sender_message_to_invalid])
|
||||
end
|
||||
|
||||
if @message.text_part
|
||||
if @message.text_part.body.to_s.blank?
|
||||
return skip(SkippedEmailLog.reason_types[:sender_text_part_body_blank])
|
||||
end
|
||||
else
|
||||
if @message.body.to_s.blank?
|
||||
return skip(SkippedEmailLog.reason_types[:sender_body_blank])
|
||||
end
|
||||
return skip(SkippedEmailLog.reason_types[:sender_body_blank]) if @message.body.to_s.blank?
|
||||
end
|
||||
|
||||
@message.charset = 'UTF-8'
|
||||
@message.charset = "UTF-8"
|
||||
|
||||
opts = {}
|
||||
|
||||
@ -70,50 +76,58 @@ module Email
|
||||
if @message.html_part
|
||||
@message.html_part.body = renderer.html
|
||||
else
|
||||
@message.html_part = Mail::Part.new do
|
||||
content_type 'text/html; charset=UTF-8'
|
||||
body renderer.html
|
||||
end
|
||||
@message.html_part =
|
||||
Mail::Part.new do
|
||||
content_type "text/html; charset=UTF-8"
|
||||
body renderer.html
|
||||
end
|
||||
end
|
||||
|
||||
# Fix relative (ie upload) HTML links in markdown which do not work well in plain text emails.
|
||||
# These are the links we add when a user uploads a file or image.
|
||||
# Ideally we would parse general markdown into plain text, but that is almost an intractable problem.
|
||||
url_prefix = Discourse.base_url
|
||||
@message.parts[0].body = @message.parts[0].body.to_s.gsub(/<a class="attachment" href="(\/uploads\/default\/[^"]+)">([^<]*)<\/a>/, '[\2|attachment](' + url_prefix + '\1)')
|
||||
@message.parts[0].body = @message.parts[0].body.to_s.gsub(/<img src="(\/uploads\/default\/[^"]+)"([^>]*)>/, '')
|
||||
@message.parts[0].body =
|
||||
@message.parts[0].body.to_s.gsub(
|
||||
%r{<a class="attachment" href="(/uploads/default/[^"]+)">([^<]*)</a>},
|
||||
'[\2|attachment](' + url_prefix + '\1)',
|
||||
)
|
||||
@message.parts[0].body =
|
||||
@message.parts[0].body.to_s.gsub(
|
||||
%r{<img src="(/uploads/default/[^"]+)"([^>]*)>},
|
||||
"',
|
||||
)
|
||||
|
||||
@message.text_part.content_type = 'text/plain; charset=UTF-8'
|
||||
@message.text_part.content_type = "text/plain; charset=UTF-8"
|
||||
user_id = @user&.id
|
||||
|
||||
# Set up the email log
|
||||
email_log = EmailLog.new(
|
||||
email_type: @email_type,
|
||||
to_address: to_address,
|
||||
user_id: user_id
|
||||
)
|
||||
email_log = EmailLog.new(email_type: @email_type, to_address: to_address, user_id: user_id)
|
||||
|
||||
if cc_addresses.any?
|
||||
email_log.cc_addresses = cc_addresses.join(";")
|
||||
email_log.cc_user_ids = User.with_email(cc_addresses).pluck(:id)
|
||||
end
|
||||
|
||||
if bcc_addresses.any?
|
||||
email_log.bcc_addresses = bcc_addresses.join(";")
|
||||
end
|
||||
email_log.bcc_addresses = bcc_addresses.join(";") if bcc_addresses.any?
|
||||
|
||||
host = Email::Sender.host_for(Discourse.base_url)
|
||||
|
||||
post_id = header_value('X-Discourse-Post-Id')
|
||||
topic_id = header_value('X-Discourse-Topic-Id')
|
||||
post_id = header_value("X-Discourse-Post-Id")
|
||||
topic_id = header_value("X-Discourse-Topic-Id")
|
||||
reply_key = get_reply_key(post_id, user_id)
|
||||
from_address = @message.from&.first
|
||||
smtp_group_id = from_address.blank? ? nil : Group.where(
|
||||
email_username: from_address, smtp_enabled: true
|
||||
).pluck_first(:id)
|
||||
smtp_group_id =
|
||||
(
|
||||
if from_address.blank?
|
||||
nil
|
||||
else
|
||||
Group.where(email_username: from_address, smtp_enabled: true).pluck_first(:id)
|
||||
end
|
||||
)
|
||||
|
||||
# always set a default Message ID from the host
|
||||
@message.header['Message-ID'] = Email::MessageIdService.generate_default
|
||||
@message.header["Message-ID"] = Email::MessageIdService.generate_default
|
||||
|
||||
if topic_id.present? && post_id.present?
|
||||
post = Post.find_by(id: post_id, topic_id: topic_id)
|
||||
@ -130,12 +144,14 @@ module Email
|
||||
# See https://www.ietf.org/rfc/rfc2919.txt for the List-ID
|
||||
# specification.
|
||||
if topic&.category && !topic.category.uncategorized?
|
||||
list_id = "#{SiteSetting.title} | #{topic.category.name} <#{topic.category.name.downcase.tr(' ', '-')}.#{host}>"
|
||||
list_id =
|
||||
"#{SiteSetting.title} | #{topic.category.name} <#{topic.category.name.downcase.tr(" ", "-")}.#{host}>"
|
||||
|
||||
# subcategory case
|
||||
if !topic.category.parent_category_id.nil?
|
||||
parent_category_name = Category.find_by(id: topic.category.parent_category_id).name
|
||||
list_id = "#{SiteSetting.title} | #{parent_category_name} #{topic.category.name} <#{topic.category.name.downcase.tr(' ', '-')}.#{parent_category_name.downcase.tr(' ', '-')}.#{host}>"
|
||||
list_id =
|
||||
"#{SiteSetting.title} | #{parent_category_name} #{topic.category.name} <#{topic.category.name.downcase.tr(" ", "-")}.#{parent_category_name.downcase.tr(" ", "-")}.#{host}>"
|
||||
end
|
||||
else
|
||||
list_id = "#{SiteSetting.title} <#{host}>"
|
||||
@ -148,16 +164,15 @@ module Email
|
||||
# conversation between the group and a small handful of people
|
||||
# directly contacting the group, often just one person.
|
||||
if !smtp_group_id
|
||||
|
||||
# https://www.ietf.org/rfc/rfc3834.txt
|
||||
@message.header['Precedence'] = 'list'
|
||||
@message.header['List-ID'] = list_id
|
||||
@message.header["Precedence"] = "list"
|
||||
@message.header["List-ID"] = list_id
|
||||
|
||||
if topic
|
||||
if SiteSetting.private_email?
|
||||
@message.header['List-Archive'] = "#{Discourse.base_url}#{topic.slugless_url}"
|
||||
@message.header["List-Archive"] = "#{Discourse.base_url}#{topic.slugless_url}"
|
||||
else
|
||||
@message.header['List-Archive'] = topic.url
|
||||
@message.header["List-Archive"] = topic.url
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -176,61 +191,59 @@ module Email
|
||||
email_log.topic_id = topic_id if topic_id.present?
|
||||
|
||||
if reply_key.present?
|
||||
@message.header['Reply-To'] = header_value('Reply-To').gsub!("%{reply_key}", reply_key)
|
||||
@message.header["Reply-To"] = header_value("Reply-To").gsub!("%{reply_key}", reply_key)
|
||||
@message.header[Email::MessageBuilder::ALLOW_REPLY_BY_EMAIL_HEADER] = nil
|
||||
end
|
||||
|
||||
MessageBuilder.custom_headers(SiteSetting.email_custom_headers).each do |key, _|
|
||||
# Any custom headers added via MessageBuilder that are doubled up here
|
||||
# with values that we determine should be set to the last value, which is
|
||||
# the one we determined. Our header values should always override the email_custom_headers.
|
||||
#
|
||||
# While it is valid via RFC5322 to have more than one value for certain headers,
|
||||
# we just want to keep it to one, especially in cases where the custom value
|
||||
# would conflict with our own.
|
||||
#
|
||||
# See https://datatracker.ietf.org/doc/html/rfc5322#section-3.6 and
|
||||
# https://github.com/mikel/mail/blob/8ef377d6a2ca78aa5bd7f739813f5a0648482087/lib/mail/header.rb#L109-L132
|
||||
custom_header = @message.header[key]
|
||||
if custom_header.is_a?(Array)
|
||||
our_value = custom_header.last.value
|
||||
MessageBuilder
|
||||
.custom_headers(SiteSetting.email_custom_headers)
|
||||
.each do |key, _|
|
||||
# Any custom headers added via MessageBuilder that are doubled up here
|
||||
# with values that we determine should be set to the last value, which is
|
||||
# the one we determined. Our header values should always override the email_custom_headers.
|
||||
#
|
||||
# While it is valid via RFC5322 to have more than one value for certain headers,
|
||||
# we just want to keep it to one, especially in cases where the custom value
|
||||
# would conflict with our own.
|
||||
#
|
||||
# See https://datatracker.ietf.org/doc/html/rfc5322#section-3.6 and
|
||||
# https://github.com/mikel/mail/blob/8ef377d6a2ca78aa5bd7f739813f5a0648482087/lib/mail/header.rb#L109-L132
|
||||
custom_header = @message.header[key]
|
||||
if custom_header.is_a?(Array)
|
||||
our_value = custom_header.last.value
|
||||
|
||||
# Must be set to nil first otherwise another value is just added
|
||||
# to the array of values for the header.
|
||||
@message.header[key] = nil
|
||||
@message.header[key] = our_value
|
||||
end
|
||||
# Must be set to nil first otherwise another value is just added
|
||||
# to the array of values for the header.
|
||||
@message.header[key] = nil
|
||||
@message.header[key] = our_value
|
||||
end
|
||||
|
||||
value = header_value(key)
|
||||
value = header_value(key)
|
||||
|
||||
# Remove Auto-Submitted header for group private message emails, it does
|
||||
# not make sense there and may hurt deliverability.
|
||||
#
|
||||
# From https://www.iana.org/assignments/auto-submitted-keywords/auto-submitted-keywords.xhtml:
|
||||
#
|
||||
# > Indicates that a message was generated by an automatic process, and is not a direct response to another message.
|
||||
if key.downcase == "auto-submitted" && smtp_group_id
|
||||
@message.header[key] = nil
|
||||
end
|
||||
# Remove Auto-Submitted header for group private message emails, it does
|
||||
# not make sense there and may hurt deliverability.
|
||||
#
|
||||
# From https://www.iana.org/assignments/auto-submitted-keywords/auto-submitted-keywords.xhtml:
|
||||
#
|
||||
# > Indicates that a message was generated by an automatic process, and is not a direct response to another message.
|
||||
@message.header[key] = nil if key.downcase == "auto-submitted" && smtp_group_id
|
||||
|
||||
# Replace reply_key in custom headers or remove
|
||||
if value&.include?('%{reply_key}')
|
||||
# Delete old header first or else the same header will be added twice
|
||||
@message.header[key] = nil
|
||||
if reply_key.present?
|
||||
@message.header[key] = value.gsub!('%{reply_key}', reply_key)
|
||||
# Replace reply_key in custom headers or remove
|
||||
if value&.include?("%{reply_key}")
|
||||
# Delete old header first or else the same header will be added twice
|
||||
@message.header[key] = nil
|
||||
@message.header[key] = value.gsub!("%{reply_key}", reply_key) if reply_key.present?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# pass the original message_id when using mailjet/mandrill/sparkpost
|
||||
case ActionMailer::Base.smtp_settings[:address]
|
||||
when /\.mailjet\.com/
|
||||
@message.header['X-MJ-CustomID'] = @message.message_id
|
||||
@message.header["X-MJ-CustomID"] = @message.message_id
|
||||
when "smtp.mandrillapp.com"
|
||||
merge_json_x_header('X-MC-Metadata', message_id: @message.message_id)
|
||||
merge_json_x_header("X-MC-Metadata", message_id: @message.message_id)
|
||||
when "smtp.sparkpostmail.com"
|
||||
merge_json_x_header('X-MSYS-API', metadata: { message_id: @message.message_id })
|
||||
merge_json_x_header("X-MSYS-API", metadata: { message_id: @message.message_id })
|
||||
end
|
||||
|
||||
# Parse the HTML again so we can make any final changes before
|
||||
@ -239,8 +252,8 @@ module Email
|
||||
|
||||
# Suppress images from short emails
|
||||
if SiteSetting.strip_images_from_short_emails &&
|
||||
@message.html_part.body.to_s.bytesize <= SiteSetting.short_email_length &&
|
||||
@message.html_part.body =~ /<img[^>]+>/
|
||||
@message.html_part.body.to_s.bytesize <= SiteSetting.short_email_length &&
|
||||
@message.html_part.body =~ /<img[^>]+>/
|
||||
style.strip_avatars_and_emojis
|
||||
end
|
||||
|
||||
@ -291,23 +304,26 @@ module Email
|
||||
end
|
||||
|
||||
def to_address
|
||||
@to_address ||= begin
|
||||
to = @message.try(:to)
|
||||
to = to.first if Array === to
|
||||
to.presence || "no_email_found"
|
||||
end
|
||||
@to_address ||=
|
||||
begin
|
||||
to = @message.try(:to)
|
||||
to = to.first if Array === to
|
||||
to.presence || "no_email_found"
|
||||
end
|
||||
end
|
||||
|
||||
def cc_addresses
|
||||
@cc_addresses ||= begin
|
||||
@message.try(:cc) || []
|
||||
end
|
||||
@cc_addresses ||=
|
||||
begin
|
||||
@message.try(:cc) || []
|
||||
end
|
||||
end
|
||||
|
||||
def bcc_addresses
|
||||
@bcc_addresses ||= begin
|
||||
@message.try(:bcc) || []
|
||||
end
|
||||
@bcc_addresses ||=
|
||||
begin
|
||||
@message.try(:bcc) || []
|
||||
end
|
||||
end
|
||||
|
||||
def self.host_for(base_url)
|
||||
@ -333,7 +349,7 @@ module Email
|
||||
optimized_1X = original_upload.optimized_images.first
|
||||
|
||||
if FileHelper.is_supported_image?(original_upload.original_filename) &&
|
||||
!should_attach_image?(original_upload, optimized_1X)
|
||||
!should_attach_image?(original_upload, optimized_1X)
|
||||
next
|
||||
end
|
||||
|
||||
@ -341,11 +357,12 @@ module Email
|
||||
next if email_size + attached_upload.filesize > max_email_size
|
||||
|
||||
begin
|
||||
path = if attached_upload.local?
|
||||
Discourse.store.path_for(attached_upload)
|
||||
else
|
||||
Discourse.store.download(attached_upload).path
|
||||
end
|
||||
path =
|
||||
if attached_upload.local?
|
||||
Discourse.store.path_for(attached_upload)
|
||||
else
|
||||
Discourse.store.download(attached_upload).path
|
||||
end
|
||||
|
||||
@message_attachments_index[original_upload.sha1] = @message.attachments.size
|
||||
@message.attachments[original_upload.original_filename] = File.read(path)
|
||||
@ -357,8 +374,8 @@ module Email
|
||||
env: {
|
||||
post_id: post.id,
|
||||
upload_id: original_upload.id,
|
||||
filename: original_upload.original_filename
|
||||
}
|
||||
filename: original_upload.original_filename,
|
||||
},
|
||||
)
|
||||
end
|
||||
end
|
||||
@ -368,7 +385,10 @@ module Email
|
||||
|
||||
def should_attach_image?(upload, optimized_1X = nil)
|
||||
return if !SiteSetting.secure_uploads_allow_embed_images_in_emails || !upload.secure?
|
||||
return if (optimized_1X&.filesize || upload.filesize) > SiteSetting.secure_uploads_max_email_embed_image_size_kb.kilobytes
|
||||
if (optimized_1X&.filesize || upload.filesize) >
|
||||
SiteSetting.secure_uploads_max_email_embed_image_size_kb.kilobytes
|
||||
return
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
@ -391,8 +411,7 @@ module Email
|
||||
#
|
||||
def fix_parts_after_attachments!
|
||||
has_attachments = @message.attachments.present?
|
||||
has_alternative_renderings =
|
||||
@message.html_part.present? && @message.text_part.present?
|
||||
has_alternative_renderings = @message.html_part.present? && @message.text_part.present?
|
||||
|
||||
if has_attachments && has_alternative_renderings
|
||||
@message.content_type = "multipart/mixed"
|
||||
@ -403,15 +422,16 @@ module Email
|
||||
text_part = @message.text_part
|
||||
@message.text_part = nil
|
||||
|
||||
content = Mail::Part.new do
|
||||
content_type "multipart/alternative"
|
||||
content =
|
||||
Mail::Part.new do
|
||||
content_type "multipart/alternative"
|
||||
|
||||
# we have to re-specify the charset and give the part the decoded body
|
||||
# here otherwise the parts will get encoded with US-ASCII which makes
|
||||
# a bunch of characters not render correctly in the email
|
||||
part content_type: "text/html; charset=utf-8", body: html_part.body.decoded
|
||||
part content_type: "text/plain; charset=utf-8", body: text_part.body.decoded
|
||||
end
|
||||
# we have to re-specify the charset and give the part the decoded body
|
||||
# here otherwise the parts will get encoded with US-ASCII which makes
|
||||
# a bunch of characters not render correctly in the email
|
||||
part content_type: "text/html; charset=utf-8", body: html_part.body.decoded
|
||||
part content_type: "text/plain; charset=utf-8", body: text_part.body.decoded
|
||||
end
|
||||
|
||||
@message.parts.unshift(content)
|
||||
end
|
||||
@ -437,7 +457,7 @@ module Email
|
||||
email_type: @email_type,
|
||||
to_address: to_address,
|
||||
user_id: @user&.id,
|
||||
reason_type: reason_type
|
||||
reason_type: reason_type,
|
||||
}
|
||||
|
||||
attributes[:custom_reason] = custom_reason if custom_reason
|
||||
@ -445,7 +465,12 @@ module Email
|
||||
end
|
||||
|
||||
def merge_json_x_header(name, value)
|
||||
data = JSON.parse(@message.header[name].to_s) rescue nil
|
||||
data =
|
||||
begin
|
||||
JSON.parse(@message.header[name].to_s)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
data ||= {}
|
||||
data.merge!(value)
|
||||
# /!\ @message.header is not a standard ruby hash.
|
||||
@ -460,12 +485,12 @@ module Email
|
||||
def get_reply_key(post_id, user_id)
|
||||
# ALLOW_REPLY_BY_EMAIL_HEADER is only added if we are _not_ sending
|
||||
# via group SMTP and if reply by email site settings are configured
|
||||
return if !user_id || !post_id || !header_value(Email::MessageBuilder::ALLOW_REPLY_BY_EMAIL_HEADER).present?
|
||||
if !user_id || !post_id ||
|
||||
!header_value(Email::MessageBuilder::ALLOW_REPLY_BY_EMAIL_HEADER).present?
|
||||
return
|
||||
end
|
||||
|
||||
PostReplyKey.create_or_find_by!(
|
||||
post_id: post_id,
|
||||
user_id: user_id
|
||||
).reply_key
|
||||
PostReplyKey.create_or_find_by!(post_id: post_id, user_id: user_id).reply_key
|
||||
end
|
||||
|
||||
def self.bounceable_reply_address?
|
||||
@ -514,7 +539,9 @@ module Email
|
||||
#
|
||||
# https://meta.discourse.org/t/discourse-email-messages-are-incorrectly-threaded/233499
|
||||
def add_identification_field_headers(topic, post)
|
||||
@message.header["Message-ID"] = Email::MessageIdService.generate_or_use_existing(post.id).first
|
||||
@message.header["Message-ID"] = Email::MessageIdService.generate_or_use_existing(
|
||||
post.id,
|
||||
).first
|
||||
|
||||
if post.post_number > 1
|
||||
op_message_id = Email::MessageIdService.generate_or_use_existing(topic.first_post.id).first
|
||||
@ -523,11 +550,12 @@ module Email
|
||||
# Whenever we reply to a post directly _or_ quote a post, a PostReply
|
||||
# record is made, with the reply_post_id referencing the newly created
|
||||
# post, and the post_id referencing the post that was quoted or replied to.
|
||||
referenced_posts = Post
|
||||
.joins("INNER JOIN post_replies ON post_replies.post_id = posts.id ")
|
||||
.where("post_replies.reply_post_id = ?", post.id)
|
||||
.order(id: :desc)
|
||||
.to_a
|
||||
referenced_posts =
|
||||
Post
|
||||
.joins("INNER JOIN post_replies ON post_replies.post_id = posts.id ")
|
||||
.where("post_replies.reply_post_id = ?", post.id)
|
||||
.order(id: :desc)
|
||||
.to_a
|
||||
|
||||
##
|
||||
# No referenced posts means that we are just creating a new post not
|
||||
@ -543,7 +571,8 @@ module Email
|
||||
# every directly replied to post can go into In-Reply-To.
|
||||
#
|
||||
# We want to make sure all of the outbound_message_ids are already filled here.
|
||||
in_reply_to_message_ids = MessageIdService.generate_or_use_existing(referenced_posts.map(&:id))
|
||||
in_reply_to_message_ids =
|
||||
MessageIdService.generate_or_use_existing(referenced_posts.map(&:id))
|
||||
@message.header["In-Reply-To"] = in_reply_to_message_ids
|
||||
most_recent_post_message_id = in_reply_to_message_ids.last
|
||||
|
||||
@ -559,7 +588,9 @@ module Email
|
||||
parent_message_ids = MessageIdService.generate_or_use_existing(reply_tree.values.flatten)
|
||||
|
||||
@message.header["References"] = [
|
||||
op_message_id, parent_message_ids, most_recent_post_message_id
|
||||
op_message_id,
|
||||
parent_message_ids,
|
||||
most_recent_post_message_id,
|
||||
].flatten.uniq
|
||||
end
|
||||
end
|
||||
|
Reference in New Issue
Block a user