LemonChannels.Adapters.Email.Attachments (lemon_channels v0.1.0)

View Source

Persists inbound email attachments to disk and describes them for the agent.

Shape of the work

prepare/1 runs on the request path and is deliberately cheap: it decides a target path, sanitizes the filename and applies the size cap, but writes nothing. It returns metadata plus a list of write thunks, and schedule/1 runs those in a background task. The provider's webhook therefore gets its response back without waiting on file I/O, while the path in the metadata is already valid — the run that reads it is queued behind scheduling and engine startup, which is the buffer this relies on.

What is refused

An attachment over the cap is dropped with a warning rather than truncated. The cap defaults to three quarters of LemonChannels.InboundHttp.max_body_bytes/0 rather than to a number of its own, because the two limits are not independent: an attachment arrives base64-encoded, taking about a third more space on the wire than decoded, so anything larger than that fraction of the body limit is unreachable — the request is rejected with a 413 before this code ever sees it. Deriving it means raising the body limit raises the attachment cap with it, and a cap that promises more than the listener will accept cannot be configured by accident. Override deliberately with:

config :lemon_channels, LemonChannels.Adapters.Email, attachment_max_bytes: n

Filenames are reduced to their basename with everything outside [A-Za-z0-9._-] replaced — an attachment name is attacker-controlled, and it is about to become a path. Written files are chmod 0600 on Unix, since they can contain anything someone chose to mail an agent.

Attachments that are only a URL are passed through undownloaded: fetching a remote URL named by an unauthenticated sender is a request forgery this adapter should not make on its own.

Summary

Functions

One human-readable line per attachment, for the prompt the agent sees.

Splits raw attachments into metadata and deferred writes.

Runs deferred writes off the request path. Always returns :ok.

Types

meta()

@type meta() :: %{
  filename: binary(),
  content_type: binary() | nil,
  path: binary() | nil,
  url: binary() | nil,
  bytes: non_neg_integer() | nil
}

Functions

describe(metas)

@spec describe([meta()]) :: [binary()]

One human-readable line per attachment, for the prompt the agent sees.

prepare(attachments)

@spec prepare(term()) :: {[meta()], [(-> any())]}

Splits raw attachments into metadata and deferred writes.

Anything unrecognized, oversized or empty is dropped, so the returned lists can be shorter than the input.

schedule(writes)

@spec schedule([(-> any())]) :: :ok

Runs deferred writes off the request path. Always returns :ok.