@@ -18,6 +18,7 @@ class OutgoingMessagePrototype
1818 attr_accessor :tag
1919 attr_accessor :credential
2020 attr_accessor :bounce
21+ attr_accessor :message_ids
2122
2223 def initialize ( server , ip , source_type , attributes )
2324 @server = server
@@ -76,7 +77,19 @@ def create_messages
7677 if valid?
7778 all_addresses . each_with_object ( { } ) do |address , hash |
7879 if address = Postal ::Helpers . strip_name_from_address ( address )
79- hash [ address ] = create_message ( address )
80+ # Check for existing message if message_ids provided
81+ message_id = @message_ids . is_a? ( Hash ) ? @message_ids [ address ] : nil
82+ if message_id
83+ # Strip angle brackets if present
84+ message_id = message_id . gsub ( /^<|>$/ , '' )
85+ # Check for duplicate
86+ existing = @server . message_db . select ( :messages , fields : [ :id , :token , :message_id ] , where : { message_id : message_id } ) . first
87+ if existing
88+ hash [ address ] = { id : existing [ "id" ] , token : existing [ "token" ] , message_id : existing [ "message_id" ] , existing : true }
89+ next
90+ end
91+ end
92+ hash [ address ] = create_message ( address , message_id )
8093 end
8194 end
8295 else
@@ -105,6 +118,14 @@ def attachments
105118 end
106119 # rubocop:enable Lint/DuplicateMethods
107120
121+ def valid_message_id_format? ( message_id )
122+ return false if message_id . blank?
123+ # Strip angle brackets if present for validation
124+ id = message_id . gsub ( /^<|>$/ , '' )
125+ # RFC 5322: local-part@domain (must have @ and both parts non-empty, no spaces)
126+ id . match? ( /\A [^@\s ]+@[^@\s ]+\z / )
127+ end
128+
108129 def validate
109130 @errors = [ ]
110131
@@ -145,57 +166,76 @@ def validate
145166 end
146167 end
147168 end
169+
170+ if @message_ids . is_a? ( Hash )
171+ @message_ids . each do |recipient , message_id |
172+ unless valid_message_id_format? ( message_id )
173+ @errors << "InvalidMessageID" unless @errors . include? ( "InvalidMessageID" )
174+ break
175+ end
176+ end
177+ end
148178 @errors
149179 end
150180
151181 def raw_message
152- @raw_message ||= begin
153- mail = Mail . new
154- if @custom_headers . is_a? ( Hash )
155- @custom_headers . each { |key , value | mail [ key . to_s ] = value . to_s }
156- end
157- mail . to = to_addresses . join ( ", " ) if to_addresses . present?
158- mail . cc = cc_addresses . join ( ", " ) if cc_addresses . present?
159- mail . from = @from
160- mail . sender = @sender
161- mail . subject = @subject
162- mail . reply_to = @reply_to
163- mail . part content_type : "multipart/alternative" do |p |
164- if @plain_body . present?
165- p . text_part = Mail ::Part . new
166- p . text_part . body = @plain_body
167- end
168- if @html_body . present?
169- p . html_part = Mail ::Part . new
170- p . html_part . content_type = "text/html; charset=UTF-8"
171- p . html_part . body = @html_body
172- end
182+ @raw_message ||= raw_message_for_recipient ( nil , @message_id )
183+ end
184+
185+ def raw_message_for_recipient ( address , message_id )
186+ mail = Mail . new
187+ if @custom_headers . is_a? ( Hash )
188+ @custom_headers . each { |key , value | mail [ key . to_s ] = value . to_s }
189+ end
190+ mail . to = to_addresses . join ( ", " ) if to_addresses . present?
191+ mail . cc = cc_addresses . join ( ", " ) if cc_addresses . present?
192+ mail . from = @from
193+ mail . sender = @sender
194+ mail . subject = @subject
195+ mail . reply_to = @reply_to
196+ mail . part content_type : "multipart/alternative" do |p |
197+ if @plain_body . present?
198+ p . text_part = Mail ::Part . new
199+ p . text_part . body = @plain_body
173200 end
174- attachments . each do |attachment |
175- mail . attachments [ attachment [ :name ] ] = {
176- mime_type : attachment [ :content_type ] ,
177- content : attachment [ :data ]
178- }
201+ if @html_body . present?
202+ p . html_part = Mail ::Part . new
203+ p . html_part . content_type = "text/html; charset=UTF-8"
204+ p . html_part . body = @html_body
179205 end
180- mail . header [ "Received" ] = ReceivedHeader . generate ( @server , @source_type , @ip , :http )
181- mail . message_id = "<#{ @message_id } >"
182- mail . to_s
183206 end
207+ attachments . each do |attachment |
208+ mail . attachments [ attachment [ :name ] ] = {
209+ mime_type : attachment [ :content_type ] ,
210+ content : attachment [ :data ]
211+ }
212+ end
213+ mail . header [ "Received" ] = ReceivedHeader . generate ( @server , @source_type , @ip , :http )
214+ mail . message_id = "<#{ message_id } >"
215+ mail . to_s
184216 end
185217
186- def create_message ( address )
218+ def create_message ( address , message_id = nil )
219+ # Use provided message_id or generate one
220+ msg_id = message_id || "#{ SecureRandom . uuid } @#{ Postal ::Config . dns . return_path_domain } "
221+
187222 message = @server . message_db . new_message
188223 message . scope = "outgoing"
189224 message . rcpt_to = address
190225 message . mail_from = from_address
191226 message . domain_id = domain . id
192- message . raw_message = raw_message
227+ message . raw_message = raw_message_for_recipient ( address , msg_id )
193228 message . tag = tag
194229 message . credential_id = credential &.id
195230 message . received_with_ssl = true
196231 message . bounce = @bounce
197232 message . save
198- { id : message . id , token : message . token }
233+ # Include message_id in response only if message_ids parameter was provided
234+ if @message_ids . is_a? ( Hash )
235+ { id : message . id , token : message . token , message_id : message . message_id }
236+ else
237+ { id : message . id , token : message . token }
238+ end
199239 end
200240
201241end
0 commit comments