Documentation
¶
Overview ¶
Package intake is the inbound adapter for content that arrives from outside without a person behind it: the jumble's doors, the webhook intake and the mail intake. An intake differs from the REST API in who is asking - a bridge with a token, not an account with a role - and in nothing else: everything it stores goes through the same application services and the same bounds.
Index ¶
Constants ¶
const ( DefaultMaxMailParts = 100 DefaultMaxMailDepth = 4 DefaultMaxMailAttachments = 20 DefaultMaxMailAttachmentSize = 10 << 20 DefaultMaxMailTextBytes = 64 << 10 )
The documented defaults. Mail-shaped rather than generous: the numbers are what an ordinary message needs with room to spare, and the point of a bound is that the extraordinary one is refused rather than served.
const ( CodeMailUnparseable = "mail.unparseable" CodeMailTooManyParts = "mail.too_many_parts" CodeMailTooDeep = "mail.too_deeply_nested" CodeMailTooManyAttached = "mail.too_many_attachments" CodeMailAttachmentTooBig = "mail.attachment_too_large" )
The refusals. Codes rather than sentences (rule 8), and one per bound, because "too big" and "too many" send whoever is looking to two different places.
const HTMLAlternativeFileName = "message.html"
HTMLAlternativeFileName is what the kept HTML alternative is called. Stable, because a person looking at an entry with two attachments has to be able to tell which one is the mail.
Variables ¶
var ErrUnparseable = shared.ErrValidation.WithDetail(CodeMailUnparseable)
ErrUnparseable is a payload that is not a message this parser can walk. The intake answers it by storing the raw bytes rather than by refusing the delivery - see MailIntake.
Functions ¶
func Parsed ¶
func Parsed(raw []byte, recipients []string, limits MailLimits) (jumbleservice.ReceivedMail, error)
Parsed is a raw message as the intake takes it: the recipients the door knows first, then the message's own; an unparseable message is kept as bytes rather than refused.
Types ¶
type AddressedMailDeliverer ¶
type AddressedMailDeliverer interface {
Execute(
ctx context.Context, mail jumbleservice.ReceivedMail,
) (jumbleservice.MailDeliveryResult, error)
}
AddressedMailDeliverer is the use case this hands a parsed message to.
type AddressedMailIntake ¶
type AddressedMailIntake struct {
Deliveries AddressedMailDeliverer
Limits MailLimits
// Key is HUBTASK_MAIL_INBOUND_KEY. Empty leaves the door shut: an installation without a key
// takes nothing at it.
Key string
}
AddressedMailIntake is the operator's inbound service's door (ADR-0094 decision 1): the service posts each raw message, authenticated by the installation's inbound key, and the parsed message goes into the intake of the workspace its address names.
The parser is MailIntake's, unchanged; a connected mailbox hands its messages to the same use case through Parsed.
func (AddressedMailIntake) Authorized ¶
func (i AddressedMailIntake) Authorized(header string) bool
Authorized reports whether the presented Authorization header carries the key: as a bearer token, or as the password of basic credentials - the shape the inbound services that only take a URL can send. Compared in constant time.
func (AddressedMailIntake) Deliver ¶
func (i AddressedMailIntake) Deliver( ctx context.Context, recipient string, raw []byte, ) (jumbleservice.MailDeliveryResult, error)
Deliver parses the message and routes it. The recipient the inbound service named goes first; the message's own headers after it.
type Mail ¶
type Mail struct {
// Sender is the address the message claims to come from, decoded and normalised. Data, never
// an identity: a From header authenticates nothing (security.md §4), and every consumer of
// this field treats it as text somebody typed.
Sender string
// SenderName is the display name the From header carried, decoded. As untrusted as the address.
SenderName string
// Recipients are the addresses the message names as where it went: the envelope headers an
// inbound service writes first (X-Original-To, Delivered-To), then To and Cc. Lowercased.
Recipients []string
// Signals are what the headers say about the message rather than in it.
Signals MailSignals
// Subject is the decoded subject line, RFC 2047 words included.
Subject string
// Text is what a person reads: the plain part where there is one, and the HTML part's source
// as text where there is not. Never rendered markup - an HTML mail read here is somebody
// reading tags, which is the honest thing for an inbox that must not run them.
Text string
// Attachments are the files the mail carried, decoded and bounded. The HTML alternative of a
// mail that also had a plain part is one of them, carried as text: keeping it is what "stored
// alongside" means, and keeping it out of Text is what stops a second copy of the same message
// from being the message.
Attachments []MailAttachment
// MessageID is the Message-ID header without its angle brackets, which is what makes a message
// received twice one message (ADR-0094 decision 6). Empty where the sender wrote none.
MessageID string
}
Mail is one message, parsed into what a jumble entry is made of.
func ParseMail ¶
func ParseMail(raw []byte, limits MailLimits) (Mail, error)
ParseMail walks one message.
The bounds are refusals with their own codes, and they are not the same answer as ErrUnparseable: a mail with two hundred parts is a mail this installation declines to take, and one whose shape the parser cannot follow is a mail it takes as bytes. Telling an operator which of the two happened is the difference between "raise the bound" and "look at the entry".
type MailAttachment ¶
MailAttachment is one file out of a message.
type MailDeliverer ¶
type MailDeliverer interface {
Execute(ctx context.Context, delivery jumbleservice.MailDelivery) (domain.Entry, error)
}
MailDeliverer is the use case this hands a parsed message to.
type MailIntake ¶
type MailIntake struct {
Deliveries MailDeliverer
// Limits are the parser's bounds. Zero fields take the documented defaults, so an installation
// that configures nothing is bounded rather than unbounded.
Limits MailLimits
}
MailIntake turns one posted message into a jumble entry.
The seam between the two halves of the mail intake, and it is deliberately thin: the parser above it knows MIME and knows nothing about this system, the use case below it knows the inbox and knows nothing about MIME, and what happens here is that one becomes the other. That is what makes the transport replaceable - an IMAP poll, when AM-1 is decided, produces bytes and calls this same function (automation.md §5).
It authenticates nothing itself. The token travels through to the use case, which resolves the tenant from it before a byte is stored, and every reason not to serve answers the same not-found - a malformed token included, because "that is not the right shape" tells somebody guessing that the shape is what to fix (T-21).
func (MailIntake) Deliver ¶
func (i MailIntake) Deliver( ctx context.Context, presented string, raw []byte, ) (domain.Entry, error)
Deliver parses the payload and stores what it found.
A payload the parser cannot read is not a refusal: the delivery goes on with the raw bytes, and the entry carries them. A payload that breaks one of the parser's *bounds* is a refusal, with the code that says which one - the difference between "look at the entry" and "raise the bound" is the difference between a message this installation could not read and one it declines to take.
type MailLimits ¶
type MailLimits struct {
// MaxParts is how many MIME parts one message may have, counted across the whole tree.
MaxParts int
// MaxDepth is how deeply parts may nest. A multipart/mixed holding a multipart/alternative is
// depth two, which is what an ordinary mail with a text, an HTML alternative and a file looks
// like; four leaves room for the unordinary one.
MaxDepth int
// MaxAttachments is how many files one mail may carry into the jumble.
MaxAttachments int
// MaxAttachmentBytes bounds one attachment, decoded.
MaxAttachmentBytes int64
// MaxTextBytes bounds the text this becomes, decoded. The jumble's own bound on a body is the
// domain's (jumble.MaxBodyBytes); this one is the parser's, so an over-long part is cut here
// rather than refused three layers later.
MaxTextBytes int64
}
MailLimits bounds what one parse may cost. Zero means the default rather than "no limit": an unbounded parse is exactly the thing this file exists to prevent, so forgetting to set a bound gives the documented one.
type MailSignals ¶
type MailSignals struct {
// Automatic is a message a machine sent on its own: `Auto-Submitted` other than `no` (RFC 3834),
// or Hubtask's own notification header. Such a message is discarded - a loop ends at its first
// turn.
Automatic bool
// Spam is the inbound service's verdict where it wrote one: `X-Spam-Flag: YES`, an
// `X-Spam-Status` beginning with yes, or Mailgun's `X-Mailgun-Sflag: Yes`.
Spam bool
// AuthFailed is SPF or DMARC reported as failed in `Authentication-Results` or `Received-SPF`.
// The sender is then nobody the workspace knows.
AuthFailed bool
}
MailSignals are the headers that decide where a message may go (ADR-0092 decisions 5 and 7).
type WebhookIntake ¶
type WebhookIntake struct {
Deliveries jumbleservice.IntakeJumbleEntry
}
WebhookIntake turns one posted delivery into a jumble entry (automation.md §1.1's credential discipline applied to the inbox).
It authenticates the tenant, never a person: the token is the whole credential, and every reason not to serve answers the same not-found - a malformed token included, because "that is not the right shape" tells somebody guessing that the shape is what to fix (T-21).