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
TODO (martin) 2023-04-01 We should remove these backwards-compatible formats for the Message-ID and solely use the discourse/post/999@host format.
-
.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_post_id_regexp ⇒ Object
TODO (martin) 2023-04-01 We should remove these backwards-compatible formats for the Message-ID and solely use the discourse/post/999@host format.
- .message_id_rfc_format(message_id) ⇒ Object
- .message_id_topic_id_regexp ⇒ Object
Class Method Details
.discourse_generated_message_id?(message_id) ⇒ Boolean
TODO (martin) 2023-04-01 We should remove these backwards-compatible formats for the Message-ID and solely use the discourse/post/999@host format.
112 113 114 115 116 |
# File 'lib/email/message_id_service.rb', line 112 def () !!( =~ ) || !!( =~ ) || !!( =~ ) 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 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/email/message_id_service.rb', line 71 def () = .map { || () } # TODO (martin) 2023-04-01 We should remove these backwards-compatible # formats for the Message-ID and solely use the discourse/post/999@host # format. topic_ids = .map { || [, 1] } .compact .map(&:to_i) post_ids = .map { || [, 1] } .compact .map(&:to_i) post_ids << .map { || [, 1] } .compact .map(&:to_i) post_ids << Post .where(outbound_message_id: ) .or(Post.where(topic_id: topic_ids, post_number: 1)) .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
149 150 151 |
# File 'lib/email/message_id_service.rb', line 149 def host Email::Sender.host_for(Discourse.base_url) end |
.is_message_id_rfc?(message_id) ⇒ Boolean
145 146 147 |
# File 'lib/email/message_id_service.rb', line 145 def () .start_with?("<") && .include?("@") && .end_with?(">") end |
.message_id_clean(message_id) ⇒ Object
137 138 139 140 141 142 143 |
# File 'lib/email/message_id_service.rb', line 137 def () if .present? && () .gsub(/\A<|>\z/, "") else end end |
.message_id_discourse_regexp ⇒ Object
129 130 131 |
# File 'lib/email/message_id_service.rb', line 129 def Regexp.new "discourse/post/(\\d+)@#{Regexp.escape(host)}" end |
.message_id_post_id_regexp ⇒ Object
TODO (martin) 2023-04-01 We should remove these backwards-compatible formats for the Message-ID and solely use the discourse/post/999@host format.
121 122 123 |
# File 'lib/email/message_id_service.rb', line 121 def Regexp.new "topic/\\d+/(\\d+|\\d+\.\\w+)@#{Regexp.escape(host)}" end |
.message_id_rfc_format(message_id) ⇒ Object
133 134 135 |
# File 'lib/email/message_id_service.rb', line 133 def () .present? && !() ? "<#{}>" : end |
.message_id_topic_id_regexp ⇒ Object
125 126 127 |
# File 'lib/email/message_id_service.rb', line 125 def Regexp.new "topic/(\\d+|\\d+\.\\w+)@#{Regexp.escape(host)}" end |