Documentation
¶
Index ¶
- func BuildForwardBody(comment string, ctx ForwardContext) string
- func BuildForwardHTMLBody(commentHTML string, ctx ForwardContext) string
- func BuildForwardSubject(orig string) string
- func BuildReferencesChain(rawMessage []byte, parentMsgID string) []string
- func ComposeMessage(from string, to []string, cc []string, ...) ([]byte, error)
- func ComposeMessageWithAttachments(from string, to []string, cc []string, ...) ([]byte, error)
- func ComposeMultipartMessage(from string, to []string, cc []string, ...) ([]byte, error)
- func DecodeAttachmentData(data string) ([]byte, error)
- func IsValidationError(err error) bool
- type Attachment
- type DKIMKeyLookup
- type ForwardContext
- type ReplyRecipients
- type SMTPRelay
- type SendRequest
- type SendResult
- type Sender
- type ValidationError
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func BuildForwardBody ¶ added in v0.3.0
func BuildForwardBody(comment string, ctx ForwardContext) string
BuildForwardBody composes the text/plain body of a forward: the caller's optional comment, a Gmail-style divider, the original headers as a quote block, then the original text body if extraction succeeded.
func BuildForwardHTMLBody ¶ added in v0.3.0
func BuildForwardHTMLBody(commentHTML string, ctx ForwardContext) string
BuildForwardHTMLBody composes the text/html body of a forward. The caller's HTML comment is emitted as-is (the API contract treats html_body as caller-controlled markup); the forwarded block is wrapped in a blockquote so mail clients render it visually as a quote.
func BuildForwardSubject ¶ added in v0.3.0
BuildForwardSubject prefixes "Fwd: " unless the subject already starts with Fwd:, Fw:, or Re:. The dedup avoids stacking on chains. Empty inputs produce "Fwd: (no subject)" so the recipient still sees the message is a forward.
func BuildReferencesChain ¶ added in v0.3.0
BuildReferencesChain returns the References chain to write on a reply, per RFC 5322 § 3.6.4:
- If the parent has a References header, return: parent.References ++ [parentMsgID]
- Else if the parent has an In-Reply-To header, return: parent.InReplyTo ++ [parentMsgID]
- Else return: [parentMsgID] (the reply still has at least the parent in its chain)
parentMsgID must be the canonicalized RFC 5322 Message-ID of the inbound being replied to (i.e. the same value the caller passes as SendRequest.ReplyToMessageID for the In-Reply-To header). Empty input yields an empty chain — callers fall back to legacy single-id behavior.
This chain matters for multi-party threads where one participant's reply is delivered only to a subset of the recipients (e.g. agent-mediated scheduler scenarios). Without the full chain, a downstream reply-to-all has In-Reply-To pointing at a Message-ID that recipients outside the subset have never seen, and Gmail/other clients fork the thread.
func ComposeMessage ¶
func ComposeMessage(from string, to []string, cc []string, subject, body, contentType, replyToMsgID string, references []string, fromDomain, replyTo, conversationID string) ([]byte, error)
ComposeMessage builds an RFC 2822 email message (single content type). Message-ID is omitted — SES assigns one on send. If to is empty, the To: header is omitted entirely (CC-only send). BCC is never written to headers — it is handled at the SMTP envelope level. When conversationID is non-empty, an X-E2A-Conversation-ID header is written so recipient agents on this platform can continue the same application thread without depending on In-Reply-To chains.
Threading headers (RFC 5322 § 3.6.4):
- replyToMsgID is the immediate parent's Message-ID — written to In-Reply-To.
- references is the FULL ancestor chain in conversation order (oldest → newest, including the immediate parent). When non-empty, written as the References header in space-separated form. When empty but replyToMsgID is set, References falls back to [replyToMsgID] for backwards compat.
Why the full chain matters: in multi-party email threads, some participants may not have seen every prior Message-ID (e.g. agent A replies only to agent B; agent B then replies-all back to user — user has no record of agent A's reply). Without the full References chain, the user's mail client (Gmail) can't anchor the reply to the existing thread and forks a new one. With the full chain, the client matches on ANY prior ID and threads correctly.
func ComposeMessageWithAttachments ¶
func ComposeMessageWithAttachments(from string, to []string, cc []string, subject, textBody, htmlBody, replyToMsgID string, references []string, fromDomain, replyTo, conversationID string, attachments []Attachment) ([]byte, error)
ComposeMessageWithAttachments builds an RFC 2822 multipart/mixed email with attachments. If no attachments are provided, falls back to ComposeMultipartMessage. See ComposeMessage for replyToMsgID / references semantics.
func ComposeMultipartMessage ¶
func ComposeMultipartMessage(from string, to []string, cc []string, subject, textBody, htmlBody, replyToMsgID string, references []string, fromDomain, replyTo, conversationID string) ([]byte, error)
ComposeMultipartMessage builds an RFC 2822 multipart/alternative email with text and HTML parts. If htmlBody is empty, falls back to a single text/plain message via ComposeMessage. See ComposeMessage for replyToMsgID / references semantics.
func DecodeAttachmentData ¶
DecodeAttachmentData decodes a base64-encoded attachment data string.
func IsValidationError ¶
IsValidationError returns true if err is a ValidationError.
Types ¶
type Attachment ¶
type Attachment struct {
Filename string `json:"filename" example:"report.pdf"`
ContentType string `json:"content_type" example:"application/pdf"`
Data string `json:"data" example:"base64-encoded-content"` // base64-encoded
} // @name Attachment
Attachment is a base64-encoded file attachment.
type DKIMKeyLookup ¶ added in v0.3.0
type DKIMKeyLookup interface {
GetDKIMKeyInternal(ctx context.Context, domain string) (selector string, privateKey []byte, err error)
}
DKIMKeyLookup returns the DKIM selector and PKCS#1 DER private key bytes for a domain. Empty selector OR empty key means "no key available — skip signing". Implementations should NOT return an error for the not-found case; that's a normal flow during the migration window when older domains haven't been keyed yet.
Method name carries the "Internal" suffix to flag the boundary: this is NOT user-input-safe. The caller must have already authenticated and authorized the from-domain (e.g. via the agent layer's ownership check on the sender). A handler that ever calls this with a user-supplied domain string becomes a "sign as anyone" primitive.
type ForwardContext ¶ added in v0.3.0
type ForwardContext struct {
From string
Date string
Subject string
To string
Cc string
Text string
HTML string
}
ForwardContext captures the header + body fields from an inbound message that a forward should quote. Headers are kept as raw strings (no re-parsing) so the quoted block renders the same lexical text the original sender chose, including display names. Text/HTML are best-effort decoded from the raw MIME — empty strings on parse failure so the forward still ships with the header block.
func ExtractForwardContext ¶ added in v0.3.0
func ExtractForwardContext(rawMessage []byte) ForwardContext
ExtractForwardContext parses an RFC 5322 raw message and pulls out the fields needed to compose a forward quote. Parse failures degrade gracefully — the returned context's body fields stay empty so the caller still gets a usable header block to prepend.
type ReplyRecipients ¶
ReplyRecipients holds the resolved To and CC lists for a reply.
func ParseReplyRecipients ¶
func ParseReplyRecipients(rawMessage []byte, replyAll bool, extraCC []string) (*ReplyRecipients, error)
ParseReplyRecipients resolves reply recipients from a raw inbound email.
Reply To is determined by Reply-To if present, otherwise From — both parsed as address lists. All parsed mailboxes become To recipients.
If replyAll is true, original To and CC recipients are added to CC. The agent's own address is excluded from all fields. Explicit extraCC addresses are merged into CC.
Normalization, deduplication, and self-removal are handled downstream by Sender.Send(). This function does best-effort parsing and returns raw lowercase addresses.
type SMTPRelay ¶
type SMTPRelay struct {
// contains filtered or unexported fields
}
func NewSMTPRelay ¶
func NewSMTPRelay(cfg *config.OutboundSMTPConfig) *SMTPRelay
func (*SMTPRelay) Configured ¶
func (*SMTPRelay) Send ¶
Send sends an email to one or more recipients and returns the Message-ID assigned by the remote server (e.g. SES).
func (*SMTPRelay) SendWithEnvelope ¶
func (r *SMTPRelay) SendWithEnvelope(envelopeFrom string, recipients []string, message []byte) (string, error)
SendWithEnvelope sends an email using envelopeFrom for SMTP MAIL FROM. Issues RCPT TO for each recipient. If any RCPT TO is rejected, the transaction is aborted. Returns the Message-ID assigned by the remote SMTP server from the DATA response. Retries transient SMTP errors (4xx) up to 3 times with backoff.
type SendRequest ¶
type SendRequest struct {
From string `json:"from,omitempty"`
To []string `json:"to"`
CC []string `json:"cc,omitempty"`
BCC []string `json:"bcc,omitempty"`
Subject string `json:"subject"`
Body string `json:"body"`
HTMLBody string `json:"html_body,omitempty"`
ReplyToMessageID string `json:"reply_to_message_id"`
References []string `json:"references,omitempty"`
ConversationID string `json:"conversation_id,omitempty"`
Attachments []Attachment `json:"attachments,omitempty"`
}
SendRequest is the outbound email contract.
References is the full ancestor Message-ID chain for a reply, oldest → newest. When non-empty, it is written verbatim into the References: header so receiving mail clients can anchor the reply to an existing thread by matching ANY id in the chain — required for multi-party threads where the immediate-parent Message-ID may not be in every participant's mailbox. When empty but ReplyToMessageID is set, the References header falls back to a single id (legacy behavior).
type SendResult ¶
type SendResult struct {
MessageID string `json:"message_id"`
Method string `json:"method"` // "smtp"
To []string `json:"-"` // canonicalized To recipients
CC []string `json:"-"` // canonicalized CC recipients
BCC []string `json:"-"` // canonicalized BCC recipients
}
SendResult contains the result of a successful send, including the canonicalized recipient lists for persistence.
type Sender ¶
type Sender struct {
// contains filtered or unexported fields
}
func NewSenderWithDKIM ¶ added in v0.3.0
func NewSenderWithDKIM(smtpRelay *SMTPRelay, fromDomain string, dkimLookup DKIMKeyLookup) *Sender
NewSenderWithDKIM is NewSender with per-domain DKIM signing enabled. The lookup is queried once per send; key misses silently skip signing rather than fail the send.
func (*Sender) Send ¶
func (s *Sender) Send(agent *identity.AgentIdentity, req SendRequest) (*SendResult, error)
Send normalizes recipients, composes, and sends an email via SMTP relay. Returns a ValidationError for caller errors (bad addresses, no visible recipients) and a plain error for transport failures.
type ValidationError ¶
type ValidationError struct {
Message string
}
ValidationError indicates a caller error (invalid addresses, no visible recipients). Handlers should map this to HTTP 400.
func (*ValidationError) Error ¶
func (e *ValidationError) Error() string