# frozen_string_literal: true class UserNotifications < ActionMailer::Base include UserNotificationsHelper include ApplicationHelper helper :application, :email default charset: "UTF-8" layout "email_template" include Email::BuildEmailHelper def signup(user, opts = {}) build_user_email_token_by_template("user_notifications.signup", user, opts[:email_token]) end def activation_reminder(user, opts = {}) build_user_email_token_by_template( "user_notifications.activation_reminder", user, opts[:email_token], ) end def signup_after_approval(user, opts = {}) locale = user_locale(user) tips = I18n.t( "system_messages.usage_tips.text_body_template", base_url: Discourse.base_url, locale: locale, ) build_email( user.email, template: "user_notifications.signup_after_approval", locale: locale, new_user_tips: tips, ) end def post_approved(user, opts = {}) post_url = opts.dig(:notification_data_hash, :post_url) return if post_url.nil? locale = user_locale(user) build_email( user.email, template: "user_notifications.post_approved", locale: locale, base_url: Discourse.base_url, post_url: post_url, ) end def signup_after_reject(user, opts = {}) locale = user_locale(user) build_email( user.email, template: "user_notifications.signup_after_reject", locale: locale, reject_reason: opts[:reject_reason], ) end def suspicious_login(user, opts = {}) ipinfo = DiscourseIpInfo.get(opts[:client_ip]) location = ipinfo[:location] browser = BrowserDetection.browser(opts[:user_agent]) device = BrowserDetection.device(opts[:user_agent]) os = BrowserDetection.os(opts[:user_agent]) build_email( user.email, template: "user_notifications.suspicious_login", locale: user_locale(user), client_ip: opts[:client_ip], location: location.present? ? location : I18n.t("staff_action_logs.unknown"), browser: I18n.t("user_auth_tokens.browser.#{browser}"), device: I18n.t("user_auth_tokens.device.#{device}"), os: I18n.t("user_auth_tokens.os.#{os}"), ) end def notify_old_email(user, opts = {}) build_email( user.email, template: "user_notifications.notify_old_email", locale: user_locale(user), new_email: opts[:new_email], ) end def notify_old_email_add(user, opts = {}) build_email( user.email, template: "user_notifications.notify_old_email_add", locale: user_locale(user), new_email: opts[:new_email], ) end def confirm_old_email(user, opts = {}) build_user_email_token_by_template( "user_notifications.confirm_old_email", user, opts[:email_token], ) end def confirm_old_email_add(user, opts = {}) build_user_email_token_by_template( "user_notifications.confirm_old_email_add", user, opts[:email_token], ) end def confirm_new_email(user, opts = {}) build_user_email_token_by_template( ( if opts[:requested_by_admin] "user_notifications.confirm_new_email_via_admin" else "user_notifications.confirm_new_email" end ), user, opts[:email_token], ) end def forgot_password(user, opts = {}) build_user_email_token_by_template( user.has_password? ? "user_notifications.forgot_password" : "user_notifications.set_password", user, opts[:email_token], ) end def email_login(user, opts = {}) build_user_email_token_by_template("user_notifications.email_login", user, opts[:email_token]) end def admin_login(user, opts = {}) build_user_email_token_by_template("user_notifications.admin_login", user, opts[:email_token]) end def account_created(user, opts = {}) build_user_email_token_by_template( "user_notifications.account_created", user, opts[:email_token], ) end def account_silenced(user, opts = nil) opts ||= {} return unless user_history = opts[:user_history] if user.silenced_forever? build_email( user.email, template: "user_notifications.account_silenced_forever", locale: user_locale(user), reason: user_history.details, ) else silenced_till = user.silenced_till.in_time_zone(user.user_option.timezone) build_email( user.email, template: "user_notifications.account_silenced", locale: user_locale(user), reason: user_history.details, silenced_till: I18n.l(silenced_till, format: :long), ) end end def account_suspended(user, opts = nil) opts ||= {} return unless user_history = opts[:user_history] if user.suspended_forever? build_email( user.email, template: "user_notifications.account_suspended_forever", locale: user_locale(user), reason: user_history.details, ) else suspended_till = user.suspended_till.in_time_zone(user.user_option.timezone) build_email( user.email, template: "user_notifications.account_suspended", locale: user_locale(user), reason: user_history.details, suspended_till: I18n.l(suspended_till, format: :long), ) end end def account_exists(user, opts = {}) build_email( user.email, template: "user_notifications.account_exists", locale: user_locale(user), email: user.email, ) end def account_second_factor_disabled(user, opts = {}) build_email( user.email, template: "user_notifications.account_second_factor_disabled", locale: user_locale(user), email: user.email, ) end def digest(user, opts = {}) build_summary_for(user) @unsubscribe_key = UnsubscribeKey.create_key_for(@user, UnsubscribeKey::DIGEST_TYPE) min_date = opts[:since] || user.last_emailed_at || user.last_seen_at || 1.month.ago # Fetch some topics and posts to show digest_opts = { limit: SiteSetting.digest_topics + SiteSetting.digest_other_topics, top_order: true, } topics_for_digest = Topic.for_digest(user, min_date, digest_opts).to_a if topics_for_digest.empty? && !user.user_option.try(:include_tl0_in_digests) # Find some topics from new users that are at least 24 hours old topics_for_digest = Topic .for_digest(user, min_date, digest_opts.merge(include_tl0: true)) .where("topics.created_at < ?", 24.hours.ago) .to_a end @popular_topics = topics_for_digest[0, SiteSetting.digest_topics] if @popular_topics.present? @other_new_for_you = ( if topics_for_digest.size > SiteSetting.digest_topics topics_for_digest[SiteSetting.digest_topics..-1] else [] end ) @popular_posts = if SiteSetting.digest_posts > 0 Post .order("posts.score DESC") .for_mailing_list(user, min_date) .where("posts.post_type = ?", Post.types[:regular]) .where( "posts.deleted_at IS NULL AND posts.hidden = false AND posts.user_deleted = false", ) .where( "posts.post_number > ? AND posts.score > ?", 1, ScoreCalculator.default_score_weights[:like_score] * 5.0, ) .where("posts.created_at < ?", (SiteSetting.editing_grace_period || 0).seconds.ago) .limit(SiteSetting.digest_posts) else [] end @excerpts = {} @popular_topics.map do |t| @excerpts[t.first_post.id] = email_excerpt( t.first_post.cooked, t.first_post, ) if t.first_post.present? end # Try to find 3 interesting stats for the top of the digest new_topics_count = Topic.for_digest(user, min_date).count if new_topics_count == 0 # We used topics from new users instead, so count should match new_topics_count = topics_for_digest.size end @counts = [ { id: "new_topics", label_key: "user_notifications.digest.new_topics", value: new_topics_count, href: "#{Discourse.base_url}/new", }, ] # totalling unread notifications (which are low-priority only) and unread # PMs and bookmark reminder notifications, so the total is both unread low # and high priority PMs value = user.unread_notifications + user.unread_high_priority_notifications if value > 0 @counts << { id: "unread_notifications", label_key: "user_notifications.digest.unread_notifications", value: value, href: "#{Discourse.base_url}/my/notifications", } end if @counts.size < 3 value = user.unread_notifications_of_type(Notification.types[:liked], since: min_date) if value > 0 @counts << { id: "likes_received", label_key: "user_notifications.digest.liked_received", value: value, href: "#{Discourse.base_url}/my/notifications", } end end if @counts.size < 3 && user.user_option.digest_after_minutes.to_i >= 1440 value = summary_new_users_count(min_date) if value > 0 @counts << { id: "new_users", label_key: "user_notifications.digest.new_users", value: value, href: "#{Discourse.base_url}/about", } end end @last_seen_at = short_date(user.last_seen_at || user.created_at) @preheader_text = I18n.t("user_notifications.digest.preheader", last_seen_at: @last_seen_at) opts = { from_alias: I18n.t("user_notifications.digest.from", site_name: Email.site_title), subject: I18n.t( "user_notifications.digest.subject_template", email_prefix: @email_prefix, date: short_date(Time.now), ), add_unsubscribe_link: true, unsubscribe_url: "#{Discourse.base_url}/email/unsubscribe/#{@unsubscribe_key}", } build_email(user.email, opts) end end def user_replied(user, opts) opts[:allow_reply_by_email] = true opts[:use_site_subject] = true opts[:show_category_in_subject] = true opts[:show_tags_in_subject] = true notification_email(user, opts) end def user_quoted(user, opts) opts[:allow_reply_by_email] = true opts[:use_site_subject] = true opts[:show_category_in_subject] = true opts[:show_tags_in_subject] = true notification_email(user, opts) end def user_linked(user, opts) opts[:allow_reply_by_email] = true opts[:use_site_subject] = true opts[:show_category_in_subject] = true opts[:show_tags_in_subject] = true notification_email(user, opts) end def user_mentioned(user, opts) opts[:allow_reply_by_email] = true opts[:use_site_subject] = true opts[:show_category_in_subject] = true opts[:show_tags_in_subject] = true notification_email(user, opts) end def group_mentioned(user, opts) opts[:allow_reply_by_email] = true opts[:use_site_subject] = true opts[:show_category_in_subject] = true opts[:show_tags_in_subject] = true notification_email(user, opts) end def user_posted(user, opts) opts[:allow_reply_by_email] = true opts[:use_site_subject] = true opts[:add_re_to_subject] = true opts[:show_category_in_subject] = true opts[:show_tags_in_subject] = true notification_email(user, opts) end def user_private_message(user, opts) opts[:allow_reply_by_email] = true opts[:use_site_subject] = true opts[:add_re_to_subject] = true opts[:show_category_in_subject] = false opts[:show_tags_in_subject] = false opts[:show_group_in_subject] = true if SiteSetting.group_in_subject # We use the 'user_posted' event when you are emailed a post in a PM. opts[:notification_type] = "posted" notification_email(user, opts) end def user_invited_to_private_message(user, opts) opts[:allow_reply_by_email] = false opts[:use_invite_template] = true notification_email(user, opts) end def user_invited_to_topic(user, opts) opts[:allow_reply_by_email] = false opts[:use_invite_template] = true opts[:show_category_in_subject] = true opts[:show_tags_in_subject] = true notification_email(user, opts) end def user_watching_first_post(user, opts) user_posted(user, opts) end def mailing_list_notify(user, post) opts = { post: post, allow_reply_by_email: true, use_site_subject: true, add_re_to_subject: true, show_category_in_subject: true, show_tags_in_subject: true, notification_type: "posted", notification_data_hash: { original_username: post.user.username, topic_title: post.topic.title, }, } notification_email(user, opts) end protected def user_locale(user) user.effective_locale end def email_post_markdown(post, add_posted_by = false) result = +"#{post.raw}\n\n" if add_posted_by result << "#{I18n.t("user_notifications.posted_by", username: post.username, post_date: post.created_at.strftime("%m/%d/%Y"))}\n\n" end result end def self.get_context_posts(post, topic_user, user) if (user.user_option.email_previous_replies == UserOption.previous_replies_type[:never]) || SiteSetting.private_email? return [] end allowed_post_types = [Post.types[:regular]] allowed_post_types << Post.types[:whisper] if topic_user.try(:user).try(:staff?) context_posts = Post .where(topic_id: post.topic_id) .where("post_number < ?", post.post_number) .where(user_deleted: false) .where(hidden: false) .where(post_type: allowed_post_types) .order("created_at desc") .limit(SiteSetting.email_posts_context) if topic_user && topic_user.last_emailed_post_number && user.user_option.email_previous_replies == UserOption.previous_replies_type[:unless_emailed] context_posts = context_posts.where("post_number > ?", topic_user.last_emailed_post_number) end context_posts end def notification_email(user, opts) notification_type = opts[:notification_type] notification_data = opts[:notification_data_hash] post = opts[:post] unless String === notification_type notification_type = Notification.types[notification_type] if Numeric === notification_type notification_type = notification_type.to_s end user_name = notification_data[:original_username] if post && SiteSetting.enable_names && SiteSetting.display_name_on_email_from name = User.where(id: notification_data[:original_user_id] || post.user_id).pick(:name) user_name = name unless name.blank? end allow_reply_by_email = opts[:allow_reply_by_email] unless user.suspended? original_username = notification_data[:original_username] || notification_data[:display_username] if user.staged && post original_subject = IncomingEmail .joins(:post) .where("posts.topic_id = ? AND posts.post_number = 1", post.topic_id) .pluck(:subject) .first end if original_subject topic_title = original_subject opts[:use_site_subject] = false opts[:add_re_to_subject] = true use_topic_title_subject = true else topic_title = notification_data[:topic_title] use_topic_title_subject = false end email_options = { title: topic_title, post: post, username: original_username, from_alias: I18n.t("email_from", user_name: user_name, site_name: Email.site_title), allow_reply_by_email: allow_reply_by_email, use_site_subject: opts[:use_site_subject], add_re_to_subject: opts[:add_re_to_subject], show_category_in_subject: opts[:show_category_in_subject], show_tags_in_subject: opts[:show_tags_in_subject], show_group_in_subject: opts[:show_group_in_subject], notification_type: notification_type, use_invite_template: opts[:use_invite_template], use_topic_title_subject: use_topic_title_subject, user: user, } if group_id = notification_data[:group_id] email_options[:group_name] = Group.find_by(id: group_id)&.name end send_notification_email(email_options) end def send_notification_email(opts) post = opts[:post] title = opts[:title] allow_reply_by_email = opts[:allow_reply_by_email] use_site_subject = opts[:use_site_subject] add_re_to_subject = opts[:add_re_to_subject] && post.post_number > 1 use_topic_title_subject = opts[:use_topic_title_subject] username = opts[:username] from_alias = opts[:from_alias] notification_type = opts[:notification_type] user = opts[:user] group_name = opts[:group_name] locale = user_locale(user) template = +"user_notifications.user_#{notification_type}" if post.topic.private_message? template << "_pm" if group_name template << "_group" elsif user.staged template << "_staged" end end # category name category = Topic.find_by(id: post.topic_id)&.category if opts[:show_category_in_subject] && post.topic_id && category && !category.uncategorized? show_category_in_subject = category.name # subcategory case if !category.parent_category_id.nil? show_category_in_subject = "#{Category.where(id: category.parent_category_id).pick(:name)}/#{show_category_in_subject}" end else show_category_in_subject = nil end # tag names if opts[:show_tags_in_subject] && post.topic_id tags = DiscourseTagging .visible_tags(Guardian.new(user)) .joins(:topic_tags) .where("topic_tags.topic_id = ?", post.topic_id) .limit(SiteSetting.max_tags_per_topic) .pluck(:name) show_tags_in_subject = tags.any? ? tags.join(" ") : nil end group = post.topic.allowed_groups&.first if post.topic.private_message? subject_pm = if opts[:show_group_in_subject] && group.present? if group.full_name "[#{group.full_name}] " else "[#{group.name}] " end else I18n.t("subject_pm") end participants = self.class.participants(post, user) end if SiteSetting.private_email? title = I18n.t("system_messages.private_topic_title", id: post.topic_id) end context = +"" tu = TopicUser.get(post.topic_id, user) context_posts = self.class.get_context_posts(post, tu, user) # make .present? cheaper context_posts = context_posts.to_a if context_posts.present? context << +"-- \n*#{I18n.t("user_notifications.previous_discussion")}*\n" context_posts.each { |cp| context << email_post_markdown(cp, true) } end translation_override_exists = TranslationOverride.where( locale: SiteSetting.default_locale, translation_key: "#{template}.text_body_template", ).exists? if opts[:use_invite_template] invite_template = +"user_notifications.invited" invite_template << "_group" if group_name invite_template << if post.topic.private_message? "_to_private_message_body" else "_to_topic_body" end topic_excerpt = post.excerpt.tr("\n", " ") if post.is_first_post? && post.excerpt topic_url = post.topic&.url if SiteSetting.private_email? topic_excerpt = "" topic_url = "" end message = I18n.t( invite_template, username: username, group_name: group_name, topic_title: gsub_emoji_to_unicode(title), topic_excerpt: topic_excerpt, site_title: SiteSetting.title, site_description: SiteSetting.site_description, topic_url: topic_url, ) html = PrettyText.cook(message, sanitize: false).html_safe else reached_limit = SiteSetting.max_emails_per_day_per_user > 0 reached_limit &&= (EmailLog.where(user_id: user.id).where("created_at > ?", 1.day.ago).count) >= (SiteSetting.max_emails_per_day_per_user - 1) in_reply_to_post = post.reply_to_post if user.user_option.email_in_reply_to if SiteSetting.private_email? message = I18n.t("system_messages.contents_hidden") else message = email_post_markdown(post) + ( if reached_limit "\n\n#{I18n.t "user_notifications.reached_limit", count: SiteSetting.max_emails_per_day_per_user}" else "" end ) end first_footer_classes = "highlight" if (allow_reply_by_email && user.staged) || (user.suspended? || user.staged?) first_footer_classes = "" end unless translation_override_exists html = UserNotificationRenderer.render( template: "email/notification", format: :html, locals: { context_posts: context_posts, reached_limit: reached_limit, post: post, in_reply_to_post: in_reply_to_post, classes: Rtl.new(user).css_class, first_footer_classes: first_footer_classes, reply_above_line: false, }, ) end end email_opts = { topic_title: Emoji.gsub_emoji_to_unicode(title), topic_title_url_encoded: title ? UrlHelper.encode_component(title) : title, message: message, url: post.url(without_slug: SiteSetting.private_email?), post_id: post.id, topic_id: post.topic_id, context: context, username: username, group_name: group_name, add_unsubscribe_link: !user.staged, mailing_list_mode: user.user_option.mailing_list_mode, unsubscribe_url: post.unsubscribe_url(user), allow_reply_by_email: allow_reply_by_email, only_reply_by_email: allow_reply_by_email && user.staged, use_site_subject: use_site_subject, add_re_to_subject: add_re_to_subject, show_category_in_subject: show_category_in_subject, show_tags_in_subject: show_tags_in_subject, private_reply: post.topic.private_message?, subject_pm: subject_pm, participants: participants, include_respond_instructions: !(user.suspended? || user.staged?), template: template, use_topic_title_subject: use_topic_title_subject, site_description: SiteSetting.site_description, site_title: SiteSetting.title, site_title_url_encoded: UrlHelper.encode_component(SiteSetting.title), locale: locale, } email_opts[:html_override] = html unless translation_override_exists # If we have a display name, change the from address email_opts[:from_alias] = from_alias if from_alias.present? TopicUser.change(user.id, post.topic_id, last_emailed_post_number: post.post_number) build_email(user.email, email_opts) end def self.participants(post, recipient_user, reveal_staged_email: false) list = [] allowed_groups = post.topic.allowed_groups.order("user_count DESC") allowed_groups.each do |g| list.push("[#{g.name_full_preferred} (#{g.user_count})](#{g.full_url})") break if list.size >= SiteSetting.max_participant_names end recent_posts_query = post .topic .posts .select("user_id, MAX(post_number) AS post_number") .where(post_type: Post.types[:regular], post_number: ..post.post_number) .where.not(user_id: recipient_user.id) .group(:user_id) .order("post_number DESC") .limit(SiteSetting.max_participant_names) .to_sql allowed_users = post .topic .allowed_users .joins("LEFT JOIN (#{recent_posts_query}) pu ON topic_allowed_users.user_id = pu.user_id") .order("post_number DESC NULLS LAST", :id) .where.not(id: recipient_user.id) .human_users allowed_users.each do |u| break if list.size >= SiteSetting.max_participant_names if reveal_staged_email && u.staged? list.push("#{u.email}") else list.push("[#{u.display_name}](#{u.full_url})") end end participants = list.join(I18n.t("word_connector.comma")) others_count = allowed_groups.size + allowed_users.size - list.size if others_count > 0 I18n.t( "user_notifications.more_pm_participants", participants: participants, count: others_count, ) else participants end end private def build_user_email_token_by_template(template, user, email_token) build_email(user.email, template: template, locale: user_locale(user), email_token: email_token) end def build_summary_for(user) @site_name = SiteSetting.email_prefix.presence || SiteSetting.title # used by I18n @user = user @date = short_date(Time.now) @base_url = Discourse.base_url @email_prefix = SiteSetting.email_prefix.presence || SiteSetting.title @header_color = ColorScheme.hex_for_name("header_primary") @header_bgcolor = ColorScheme.hex_for_name("header_background") @anchor_color = ColorScheme.hex_for_name("tertiary") @markdown_linker = MarkdownLinker.new(@base_url) @disable_email_custom_styles = !SiteSetting.apply_custom_styles_to_digest end def self.summary_new_users_count_key(min_date_str) "summary-new-users:#{min_date_str}" end def summary_new_users_count(min_date) min_date_str = min_date.is_a?(String) ? min_date : min_date.strftime("%Y-%m-%d") key = self.class.summary_new_users_count_key(min_date_str) ((count = Discourse.redis.get(key)) && count.to_i) || begin count = User .real .where(active: true, staged: false) .not_suspended .where("created_at > ?", min_date_str) .count Discourse.redis.setex(key, 1.day, count) count end end end