outbound

package
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 4, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

Documentation

Index

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

func BuildForwardSubject(orig string) string

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

func BuildReferencesChain(rawMessage []byte, parentMsgID string) []string

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

func DecodeAttachmentData(data string) ([]byte, error)

DecodeAttachmentData decodes a base64-encoded attachment data string.

func IsValidationError

func IsValidationError(err error) bool

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

type ReplyRecipients struct {
	To []string
	CC []string
}

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 (r *SMTPRelay) Configured() bool

func (*SMTPRelay) Send

func (r *SMTPRelay) Send(from string, recipients []string, message []byte) (string, error)

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 NewSender

func NewSender(smtpRelay *SMTPRelay, fromDomain string) *Sender

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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL