Class: Posthaste::MessageMapper
- Inherits:
-
Object
- Object
- Posthaste::MessageMapper
- Defined in:
- lib/posthaste/message_mapper.rb
Overview
Turning a Mail::Message into the body of POST /v1/emails.
ActionMailer hands the delivery method a fully-composed Mail::Message,
which is a rich RFC 5322 object; the send API takes FIELDS. Every part of
the message therefore lands in one of four buckets, and the governing rule
is that NOTHING IS DROPPED IN SILENCE — a team that migrates and does not
notice their Bcc stopped arriving is the outcome this design exists to
prevent. So:
mapped it becomes a field, unchanged
mapped, noted it becomes a field with something changed, and a
MappingWarning says what
refused a synchronous MappingError naming the field, before any
request is made
generated the platform composes it (Date, Message-ID, MIME structure,
DKIM-Signature) and the Mail object's copy is an artefact of
Mail, not authored intent, so it is discarded quietly
Constant Summary collapse
- RESERVED_HEADER_NAMES =
Header names the platform owns, ported from
packages/mail/src/mime.ts's RESERVED_HEADER_NAMES.Sending one of these is a 400 from the API's own
reservedHeaderrefinement, so catching it here only changes WHEN the caller finds out — synchronously, naming the first-class field to use instead, rather than after a round trip with a message about a "refinement".WHY EACH GROUP IS RESERVED (the reasons are the point): identity and routing headers are DKIM-signed by the platform, so a customer copy either duplicates a signed header — some receivers reject two
Froms — or drifts from the signed value and fails DMARC. Delivery-controlling headers (Bcc,Cc,Sender,Return-Path) would silently fan a message out or mislead bounce handling. Trust and trace headers (DKIM-Signature,Received,Authentication-Results,ARC-*) are the audit trail a receiver reads, and forging them is forging it.Feedback-IDis the key Google aggregates complaint rates by, so a customer who could set it could file their complaints under somebody else's identifier. %w[ from to cc bcc sender subject date message-id mime-version content-type content-transfer-encoding content-disposition return-path dkim-signature received authentication-results list-unsubscribe list-unsubscribe-post feedback-id ].freeze
- CONSUMED =
Reserved names this mapper reads into a first-class field. Their value is carried, not lost, so they never reach the
headersmap and never warn. %w[from to cc bcc subject reply-to list-unsubscribe].freeze
- GENERATED =
Reserved names that are an artefact of composing the message in Ruby.
DateandMessage-IDare in here on EVIDENCE, not on assumption: Mail puts both on every message before the delivery method sees it, so warning about them would fire on every single send and train people to ignore warnings — which is precisely how the ones that matter get missed. The MIME headers are structure the platform rebuilds, and the bytes the recipient's client decodes are identical either way.All of them are listed in the README under what the platform composes.
%w[ date message-id mime-version content-type content-transfer-encoding content-disposition list-unsubscribe-post ].freeze
- ALTERNATIVE =
The first-class field to reach for instead, named in the error, because "you may not set this" without "set that" is a dead end for somebody mid-migration.
{ 'sender' => 'the mailer\'s `from:`', 'return-path' => 'nothing — bounces are routed by the platform\'s own VERP return path', 'dkim-signature' => 'nothing — the platform signs every message with your verified key', 'received' => 'nothing — the trace is written by the receiving hops', 'authentication-results' => 'nothing — this is written by the receiver, not the sender', 'feedback-id' => '`X-Posthaste-Tags`, which is what the log and analytics filter on' }.freeze
- CONTROL_PREFIX =
Fields the send API has that ActionMailer has no word for.
An
X-Posthaste-…header is the one extension point ActionMailer offers without changing a singlemail()call's shape:mail()passes any unrecognised key straight through as a header, somail(to: …, 'X-Posthaste-Stream' => 'transactional')works today in every Rails version. They are CONSUMED here and never forwarded. 'x-posthaste-'- CONTROL_HEADERS =
{ 'x-posthaste-stream' => :stream, 'x-posthaste-tags' => :tags, 'x-posthaste-metadata' => :metadata, 'x-posthaste-idempotency-key' => :idempotency_key, 'x-posthaste-template' => :template, 'x-posthaste-template-version' => :template_version, 'x-posthaste-variables' => :variables, 'x-posthaste-scheduled-at' => :scheduled_at }.freeze
Instance Attribute Summary collapse
-
#warnings ⇒ Object
readonly
Returns the value of attribute warnings.
Instance Method Summary collapse
-
#call(mail) ⇒ Object
=> [payload_hash, warnings].
-
#initialize(defaults = {}) ⇒ MessageMapper
constructor
defaultsare the settings-level values applied to every message:stream,tags,metadata.
Constructor Details
#initialize(defaults = {}) ⇒ MessageMapper
defaults are the settings-level values applied to every message:
stream, tags, metadata. A control header on an individual message
overrides them.
116 117 118 119 |
# File 'lib/posthaste/message_mapper.rb', line 116 def initialize(defaults = {}) @defaults = defaults @warnings = [] end |
Instance Attribute Details
#warnings ⇒ Object (readonly)
Returns the value of attribute warnings.
111 112 113 |
# File 'lib/posthaste/message_mapper.rb', line 111 def warnings @warnings end |
Instance Method Details
#call(mail) ⇒ Object
=> [payload_hash, warnings]
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/posthaste/message_mapper.rb', line 122 def call(mail) @warnings = [] control = extract_control(mail) payload = {} payload[:from] = map_from(mail) payload[:to] = map_recipients(mail, :to) cc = map_recipients(mail, :cc) payload[:cc] = cc unless cc.empty? bcc = map_recipients(mail, :bcc) payload[:bcc] = bcc unless bcc.empty? subject = mail.subject payload[:subject] = subject if subject && !subject.empty? reply_to = map_reply_to(mail) payload[:replyTo] = reply_to if reply_to list_unsubscribe = single_header(mail, 'List-Unsubscribe') payload[:listUnsubscribe] = list_unsubscribe if list_unsubscribe apply_body(payload, mail, control) = (mail) payload[:attachments] = unless .empty? headers = map_headers(mail) payload[:headers] = headers unless headers.empty? apply_control(payload, control) if payload[:to].empty? raise MappingError.new( 'the message has no `to` recipient, so there is nobody to send it to', type: 'unmappable_message' ) end [payload, @warnings] end |