mail

package
v0.2.2 Latest Latest
Warning

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

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package mail holds mailbox business logic that is independent of HTTP: the send guardrail (external recipients fall back to a draft for human review) and construction of Graph message payloads.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func BuildInlineAttachment added in v0.2.2

func BuildInlineAttachment(img InlineImage) ([]byte, error)

BuildInlineAttachment renders the Graph fileAttachment payload that adds an inline image to an existing draft (POST /messages/{id}/attachments).

func BuildMessage

func BuildMessage(m Message) ([]byte, error)

BuildMessage renders a Graph message object (used to create a draft via POST /messages).

func BuildReplyBodyPatch added in v0.2.1

func BuildReplyBodyPatch(body Body, quoted string) ([]byte, error)

BuildReplyBodyPatch renders the PATCH payload that fills a reply draft created via createReply / createReplyAll. That draft is an HTML message (it carries the quoted original), so the body is always written as HTML.

quoted is the body the draft already has. The reply is spliced in above it instead of replacing it — that quote IS the thread, including its inline images. Pass "" only when there is demonstrably nothing to keep.

func BuildReplyComment

func BuildReplyComment(body Body) ([]byte, error)

BuildReplyComment renders the {comment} payload for reply / replyAll. Graph splices the comment into the HTML body of the reply, so a plain-text body is converted first — otherwise every line break is lost on the way out.

func BuildSendMail

func BuildSendMail(m Message) ([]byte, error)

BuildSendMail renders a Graph sendMail payload ({message, saveToSentItems}).

func DecodeAttachment

func DecodeAttachment(attachmentJSON []byte) (string, []byte, error)

DecodeAttachment extracts the file name and decoded bytes from a Graph fileAttachment. It errors for attachments without contentBytes (item or reference attachments cannot be downloaded this way).

func LooksLikeHTML added in v0.2.1

func LooksLikeHTML(body string) bool

LooksLikeHTML reports whether a body was written as HTML. Only the opening of the body decides: prose never *starts* with a tag, so this cannot misfire on a mail that merely mentions one ("der Platzhalter <name> …").

It is a safety net, not the interface — `--html` states the intent explicitly. But a body that opens with `<html><body>` has exactly one plausible reading, and sending it as text/plain shows the recipient the tags.

func ReplyRecipients

func ReplyRecipients(messageJSON []byte, replyAll bool) ([]string, error)

ReplyRecipients extracts the addresses a reply would go to: the sender for a plain reply, plus the original To/Cc (deduplicated) for reply-all. These feed the send guardrail so replies are subject to the same send_allow policy.

func SpliceIntoQuoted added in v0.2.2

func SpliceIntoQuoted(reply, quoted string) string

SpliceIntoQuoted puts a reply body at the TOP of the quoted original that a createReply / createReplyAll draft already carries — the shape an Outlook reply has: the new text first, the original thread below it, with its inline images, signatures and formatting untouched.

Why this exists: the draft Graph hands back is not empty. It already contains the quoted original. Writing the reply as the *whole* body — what this CLI did until 2026-08-05 — REPLACES that quote, so the thread the reader expects under the answer is gone, and every inline image with it. Reported by Gerald Katterbauer as "mal ohne Thread, mal ohne Bilder" (GC 975b01a9).

The quote is an HTML document, so the reply cannot simply be prepended: anything before <html> lands outside the rendered body. The insertion point is therefore the first <body …> tag. A bare fragment without <body> is concatenated, which renders the same.

func TextToHTML added in v0.2.1

func TextToHTML(text string) string

TextToHTML renders a plain-text mail body as HTML that keeps the author's layout: a blank line starts a new paragraph, a single newline becomes a line break. Everything else is escaped, so text that happens to contain angle brackets stays literal.

Why this exists: Graph's reply/replyAll `comment` is injected into the HTML body of the reply, and a reply draft handed back by createReply is an HTML message too. Plain text placed there renders as one run-on paragraph — the newlines are simply insignificant in HTML.

Types

type Body added in v0.2.1

type Body struct {
	Content string
	HTML    bool
}

Body is an outgoing mail body plus how its author wrote it. It travels through the backend seam unchanged, because whether plain text needs converting depends on the sink: Graph's reply path is HTML-only, EWS types its bodies explicitly and keeps plain text as plain text.

func (Body) AsHTML added in v0.2.1

func (b Body) AsHTML() string

AsHTML returns the content ready for an HTML sink: verbatim when it already is HTML, converted from plain text otherwise.

func (Body) IsHTML added in v0.2.1

func (b Body) IsHTML() bool

IsHTML reports whether the content must be treated as HTML — either the author said so (--html) or the body visibly opens with a tag.

type InlineImage added in v0.2.2

type InlineImage struct {
	Name        string // file name shown in the attachment list
	ContentID   string // the cid: the body references
	ContentType string // image/png, image/jpeg, …
	Data        []byte
}

InlineImage is a picture that travels inside the message body, referenced from the HTML as <img src="cid:CONTENT-ID">. Signature logos are the reason this exists: the quoted original brings its own images along, but the block the agent writes on top has none unless they are attached explicitly.

func NewInlineImage added in v0.2.2

func NewInlineImage(contentID, path string, data []byte) (InlineImage, error)

NewInlineImage builds an InlineImage from a content id and the file's bytes, deriving name and MIME type from the path.

type Message

type Message struct {
	Subject string
	Body    Body
	To      []string
	Cc      []string
}

Message is a message to compose.

type SendAction

type SendAction int

SendAction is the outcome of the send guardrail.

const (
	// SendDirect: every recipient is permitted, send immediately.
	SendDirect SendAction = iota
	// DraftOnly: at least one recipient is not in send_allow — create a draft
	// for human review instead of sending.
	DraftOnly
)

func (SendAction) String

func (a SendAction) String() string

type SendPlan

type SendPlan struct {
	Action  SendAction
	Blocked []string // recipients that forced a draft (empty for SendDirect)
}

SendPlan is the guardrail decision for a set of recipients.

func PlanSend

func PlanSend(cfg *config.Config, recipients []string) SendPlan

PlanSend decides whether a message may be sent directly or must become a draft. A single recipient outside send_allow downgrades the whole message to a draft (we never partially send). send_unrestricted permits everything via CanSendTo.

Jump to

Keyboard shortcuts

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