Class: Email::MessageIdService
- Inherits:
-
Object
- Object
- Email::MessageIdService
- Defined in:
- lib/email/message_id_service.rb
Overview
Email Message-IDs are used in both our outbound and inbound email flow. For the outbound flow via Email::Sender, we assign a unique Message-ID for any emails sent out from the application. If we are sending an email related to a post, such as through the PostAlerter class, then the Message-ID will contain references to the post ID. The host must also be included on the Message-IDs. The format looks like this:
discourse/post/POST_ID@HOST
We previously had the following formats, but support for these will be removed in 2023:
topic/TOPIC_ID/POST_ID@HOST topic/TOPIC_ID@HOST
For the inbound email flow via Email::Receiver, we use Message-IDs to discern which topic and post the inbound email reply should be in response to. In this case, the Message-ID is extracted from the References and/or In-Reply-To headers, and compared with either the IncomingEmail table, the Post table, or the IncomingEmail to determine where to send the reply.
See datatracker.ietf.org/doc/html/rfc2822#section-3.6.4 for more specific information around Message-IDs in email.
See tools.ietf.org/html/rfc850#section-2.1.7 for the Message-ID format specification.
Class Method Summary collapse
- .discourse_generated_message_id?(message_id) ⇒ Boolean
-
.find_post_from_message_ids(message_ids) ⇒ Object
Uses extracted Message-IDs from both the In-Reply-To and References headers from an incoming email.
- .generate_default ⇒ Object
-
.generate_or_use_existing(post_ids) ⇒ Object
The outbound_message_id may be present because either:.
- .host ⇒ Object
- .is_message_id_rfc?(message_id) ⇒ Boolean
- .message_id_clean(message_id) ⇒ Object
- .message_id_discourse_regexp ⇒ Object
- .message_id_rfc_format(message_id) ⇒ Object
Class Method Details
.discourse_generated_message_id?(message_id) ⇒ Boolean
93 94 95 |
# File 'lib/email/message_id_service.rb', line 93 def () .match?() end |
.find_post_from_message_ids(message_ids) ⇒ Object
Uses extracted Message-IDs from both the In-Reply-To and References headers from an incoming email.
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/email/message_id_service.rb', line 71 def () = .map { || () } post_ids = .map { || [, 1] } .compact .map(&:to_i) post_ids << Post.where(outbound_message_id: ).pluck(:id) post_ids << EmailLog.where(message_id: ).pluck(:post_id) post_ids << IncomingEmail.where(message_id: ).pluck(:post_id) post_ids.flatten! post_ids.compact! post_ids.uniq! return if post_ids.empty? Post.where(id: post_ids).order(:created_at).last end |
.generate_default ⇒ Object
35 36 37 |
# File 'lib/email/message_id_service.rb', line 35 def generate_default "<#{SecureRandom.uuid}@#{host}>" end |
.generate_or_use_existing(post_ids) ⇒ Object
The outbound_message_id may be present because either:
-
The post was created via incoming email and Email::Receiver, and references a Message-ID generated by an external email client or service.
-
At least one email has been sent because of the post being created to inform interested parties via email.
If it is blank then we should assume Discourse was the originator of the post, and generate a Message-ID to be used from now on using our discourse/post/POST_ID@HOST format.
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/email/message_id_service.rb', line 50 def generate_or_use_existing(post_ids) post_ids = Array.wrap(post_ids) return [] if post_ids.empty? DB.exec(<<~SQL, host: host) UPDATE posts SET outbound_message_id = 'discourse/post/' || posts.id || '@' || :host WHERE outbound_message_id IS NULL AND posts.id IN (#{post_ids.join(",")}); SQL DB.query_single(<<~SQL) SELECT '<' || posts.outbound_message_id || '>' FROM posts WHERE posts.id IN (#{post_ids.join(",")}) ORDER BY posts.created_at ASC; SQL end |
.host ⇒ Object
117 118 119 |
# File 'lib/email/message_id_service.rb', line 117 def host Email::Sender.host_for(Discourse.base_url) end |
.is_message_id_rfc?(message_id) ⇒ Boolean
113 114 115 |
# File 'lib/email/message_id_service.rb', line 113 def () .start_with?("<") && .include?("@") && .end_with?(">") end |
.message_id_clean(message_id) ⇒ Object
105 106 107 108 109 110 111 |
# File 'lib/email/message_id_service.rb', line 105 def () if .present? && () .gsub(/\A<|>\z/, "") else end end |
.message_id_discourse_regexp ⇒ Object
97 98 99 |
# File 'lib/email/message_id_service.rb', line 97 def Regexp.new "discourse/post/(\\d+)@#{Regexp.escape(host)}" end |
.message_id_rfc_format(message_id) ⇒ Object
101 102 103 |
# File 'lib/email/message_id_service.rb', line 101 def () .present? && !() ? "<#{}>" : end |