Skip to main content
emailChannel(options?): EmailChannelAdapter
An email channel adapter: inbound(payload) normalises a received mail, send(request) replies in its thread through the inbox’s reply action. Parameters Returns EmailChannelAdapter

Over the Email API

emailChannel() is exported from @telnyx/agent-harness/channels, from 0.1.0-beta.3. The adapter it returns is a ChannelAdapter with channel: "email", for createChannelRouter(). It handles the email.received webhook of an inbox. The host verifies the webhook’s signature and passes its data.payload to inbound().
  • Inbox. The inbox is the payload’s inbox_id. Without one, the adapter looks the recipient up in the account’s inbox list (GET /email_inboxes), on domainId when it is set, and keeps the list per transport for 60 seconds. A mail for another inbox than inboxId, or to another address than address, is left alone.
  • Matching. The reply target is the inbox’s record of the mail. The adapter lists the inbox’s newest messages from the sender (filter[from], page[size] 10) and takes the inbound one the one whose id or message_id equals the envelope’s id or its RFC Message-ID (the live payload’s message_id_header). A Message-ID is compared without its angle brackets. The adapter never matches by subject or recency.
  • Text. The text given to the model is the record’s reply_text, else the plain-text body at text_body_url with its quoted history dropped, else the message read back from its thread. A body is fetched only over https from a host in bodyHosts, through the transport first and then with plain fetch. Redirects are never followed.
  • Conversation. conversationId is email-<thread id>, else email-<sender>. externalMessageId is the envelope’s id, which a redelivery repeats, so the router’s ledger treats a redelivery as a duplicate.
  • Reply. send() posts { text } to POST /email_inboxes/{inbox}/messages/{message}/actions/reply with the request’s idempotency key as the Idempotency-Key header. The platform addresses and threads the reply.

Unavailable mail

inbound() throws EmailInboundUnavailableError when the inbox has no record that matches the mail, and when a read it needs fails (the inbox list, the inbox, the body or the thread) and no other source has the text. The host answers the webhook with a non-2xx status, such as 503, so the platform delivers it again. A mail that is not this adapter’s, cannot be placed in an inbox or has no text returns undefined; a redelivery would end the same way, so the host acknowledges it.
The platform redelivers a failed email.received webhook once, about one second later.

EmailChannelOptions

Properties address?
readonly optional address?: EmailAddressScope
The address this adapter answers at, for a mail whose inbox is not yet known by id: the inbox is looked up for that recipient. bodyHosts?
readonly optional bodyHosts?: readonly string[]
Hosts an offloaded body may be fetched from (the host itself or a subdomain, over https). Default EMAIL_BODY_HOSTS. domainId?
readonly optional domainId?: string
The inbound domain, so the recipient lookup never takes an inbox of the same address on another domain. fetch?
readonly optional fetch?: (url, init?) => Promise<Response>
Plain fetch for a public body link, and for an adapter without a transport. Default: the global fetch. Redirects are never followed. Parameters Returns Promise<Response> inboxId?
readonly optional inboxId?: string
The inbox this adapter answers for and replies from. A mail to another inbox is left alone. now?
readonly optional now?: () => number
The clock for receivedAt. Default Date.now. Returns number onEvent?
readonly optional onEvent?: (event) => void
Told what happened to each mail. Parameters Returns void transport?
readonly optional transport?: EmailTransport
The API. Without one, an offloaded body is fetched with plain fetch, nothing is read back from the inbox, and send refuses.

EmailAddressScope

The address an adapter answers at: the local part, and the domain when one is named (both compared case-insensitively). Properties domain?
readonly optional domain?: string
username
readonly username: string

EmailTransport

The Telnyx API, as the adapter calls it: paths are relative to /v2; an absolute URL goes as given. A Telnyx client is one. Methods get()
get<T>(path, options?): Promise<T>
Type Parameters Parameters Returns Promise<T> post()
post<T>(path, options?): Promise<T>
Type Parameters Parameters Returns Promise<T>

EmailChannelAdapter

Properties channel
readonly channel: "email"
Methods inbound()
inbound(payload): Promise<EmailChannelInbound | undefined>
Normalise one email.received payload (the webhook’s data.payload). The envelope’s id is the message id a redelivery repeats; the thread is the conversation (email-<thread id>) when the envelope names it or the message is found in the inbox by its id, else the sender’s (email-<sender>), so a redelivery lands in the same conversation whatever the inbox holds by then. The text is the platform’s reply_text, else the plain-text body at text_body_url with its quoted tail dropped, else the message read back from its thread. Nothing when the mail is not this adapter’s, cannot be placed in an inbox, or has no text: an event says why. Throws EmailInboundUnavailableError when a read it needed failed and may pass. Parameters Returns Promise<EmailChannelInbound | undefined> send()
send(request): Promise<{ providerRequestId: string; }>
Reply in thread. content is an EmailReplyContent; the idempotency key goes with the request. providerRequestId is the API’s id of the reply, else reply:<idempotency key>. Parameters Returns Promise<{ providerRequestId: string; }>

EmailChannelInbound

Properties channel
readonly channel: "email"
content
readonly content: EmailInboundContent
conversationId
readonly conversationId: string
externalMessageId
readonly externalMessageId: string
receivedAt
readonly receivedAt: number

EmailInboundContent

EmailInboundContent = object
What the model is given of a mail: the new text, and where it came from and goes back to. Properties from
readonly from: string
inboxId
readonly inboxId: string
The inbox the mail arrived in; the reply goes out from it. messageId
readonly messageId: string
The inbox message the reply answers; the platform threads and addresses it. subject?
readonly optional subject?: string
text
readonly text: string
The new text of the mail, its quoted history and signature dropped. threadId?
readonly optional threadId?: string
to
readonly to: readonly string[]

EmailReplyContent

EmailReplyContent = object
A reply: the text, and the message it answers. The inbox is the adapter’s unless named. Properties inboxId?
readonly optional inboxId?: string
messageId
readonly messageId: string
text
readonly text: string

EmailChannelEvent

EmailChannelEvent = Readonly<{ type: EmailChannelEventType; } & Record<string, unknown>>
Something the adapter noticed while handling a mail: why one was left alone, what it read, what failed. type says which; the other fields are its details. Hosts log them.

EmailChannelEventType

EmailChannelEventType = "unrecognised payload" | "not this agent's inbox" | "not this agent's address" | "no inbox for the recipient" | "inbox list failed" | "payload" | "message not in inbox yet" | "inbox read failed" | "body url skipped" | "body redirect skipped" | "body fetch failed" | "thread read failed" | "no text"

EmailInboundUnavailableError

The mail could not be normalised now, for a reason that may pass: the API refused or did not answer a read it needed (the inbox list, the inbox, the body, the thread). The host should answer the webhook with an error so the platform delivers it again; a mail left alone for good is not this. Constructors Constructor
new EmailInboundUnavailableError(message, options?): EmailInboundUnavailableError
Parameters Returns EmailInboundUnavailableError Properties retryable
readonly retryable: true = true

emailConversation()

emailConversation(mail): string
The conversation an email thread is: one per thread, so a reply in the thread continues it; else one per sender. Parameters Returns string

stripQuotedEmail()

stripQuotedEmail(text): string | undefined
The new text of a reply, without the quoted history under it: everything from the first “On … wrote:” line or the first quoted (>) line on is dropped, and so is a signature after a -- line. Conservative: a mail that does not look quoted is returned whole. Parameters Returns string | undefined

isEmailBodyHost()

isEmailBodyHost(url, hosts?): boolean
Whether a body URL is on one of the allowed hosts (the host itself or a subdomain), over https. Parameters Returns boolean

EMAIL_BODY_HOSTS

const EMAIL_BODY_HOSTS: readonly string[]
Where an offloaded mail body may be fetched from by default: Telnyx’s own hosts, never an address the payload could point anywhere. The value is ["telnyx.com", "telnyxstorage.com"].

redactUrl()

redactUrl(url): string
A URL as it may be logged: origin and path only. A body link is often signed, and its query (or userinfo, or fragment) can grant access to the mail it points at. Parameters Returns string

forgetEmailInboxCache()

forgetEmailInboxCache(): void
Forget the cached inbox lists (tests, or after an inbox was made). Returns void