문서로 건너뛰기
SDK

이메일 발송

`emails.send`: 메시지 하나를 지금 또는 나중에 보냅니다.

emails.send

send-email.ts
const email = await openemail.emails.send({  from: { email: '[email protected]', name: 'Acme Billing' },  to: ['[email protected]', 'Grace <[email protected]>'],  cc: '[email protected]',  bcc: [{ email: '[email protected]' }],  replyTo: '[email protected]',  subject: 'Your September invoice',  html: '<p>Invoice attached.</p>',  text: 'Invoice attached.',  headers: { 'X-Campaign': 'invoices' },  attachments: [{ filename: 'invoice.pdf', content: pdfBytes }],  threadId: 'thread_…',  scheduledAt: 'PT1H',  tags: { order: '4021' },  tracking: { opens: true, clicks: true },})

to, cc, bcc는 수신자 하나 또는 여럿을 받으며, 하나만 주면 대신 배열로 감싸 줍니다. 각각은 주소만 쓰거나 Name <addr@host> 또는 { email, name }일 수 있습니다.

매개변수

fromRecipientInput필수
발신자입니다. 주소만 쓰거나 `Name <addr@host>` 또는 객체로 지정합니다. 이 키가 발신자로 쓸 수 있는 주소여야 합니다. 기본 발신자는 없는데, 그 기본값은 워크스페이스 기본 주소가 될 것이고 이 주소는 주소가 늘고 줄면서 바뀌기 때문입니다.
toRecipientInput | RecipientInput[]필수
수신자 하나 또는 여럿이며, 하나만 주면 대신 배열로 감싸 줍니다. to, cc, bcc를 합쳐 최대 50개입니다.
ccRecipientInput | RecipientInput[]
수신자 50명 제한에 포함됩니다.
bccRecipientInput | RecipientInput[]
수신자마다 봉투가 하나씩 전송되므로, 다른 사람이 받는 바이트에는 결코 이름이 나타나지 않습니다.
replyToRecipientInput
주소 하나이며, Reply-To 헤더로 전송됩니다.
subjectstring
최대 998자로 RFC 5322의 줄 길이 제한입니다. 기본값은 비어 있습니다.
htmlstring
html, text, draftId, template 중 하나는 필수입니다. 두 본문이 모두 주어지면 수신자가 보는 것은 HTML입니다.
textstring
일반 텍스트 파트입니다.
template{ id, version?, props?, slots? }
저장된 템플릿을 서버에서 렌더링합니다. `version`은 버전을 고정하며, 생략하면 요청이 수락되는 시점에 게시되어 있는 것을 사용합니다. 선언되지 않았거나 누락된 prop은 메시지의 빈칸이 아니라 422입니다.
draftIdstring
저장된 초안을 이 봉투로 보냅니다.
headersRecord<string, string>
`X-*`, `List-*`, Reply-To, Precedence, Auto-Submitted, Importance, Priority, Feedback-Id입니다. 전송 계층이 스스로 설정하는 것은 조용히 버려지지 않고 거부됩니다.
attachmentsAttachmentInput[]
`{ filename, content, contentType? }`이거나, 워크스페이스에 이미 있는 파일을 가리키는 `{ fileId }`입니다. content로 바이트를 넘기면 base64로 인코딩해 줍니다. 파일 20개까지이며 인라인 파일은 디코딩 후 합계 5 MB로 제한됩니다. 저장된 파일은 더 클 수 있고 다운로드 링크로 전달됩니다.
attachmentDeliveryAttachmentDeliveryMode
`mime`, `link`, `auto` 중 하나입니다. `auto`는 파일 도메인이 활성화된 도메인에서 2 MB를 넘기면 다운로드 링크로, 그렇지 않으면 메시지 안에 담아 보냅니다. 생략하면 메일함 설정이 적용되며, 그 기본값은 `auto`입니다.
threadIdstring
기존 스레드에 답장합니다. 전송 계층이 In-Reply-To와 References를 씁니다.
scheduledAtDate | string
Date, ISO-8601 시각, 또는 `PT1H` 같은 기간입니다. 최대 1년 뒤까지 가능하며 과거는 안 됩니다. cancellableForSeconds와 함께 쓸 수 없습니다.
cancellableForSecondsnumber
0에서 900까지입니다. 즉시 발송에 대한 실행 취소 시간으로, 작성기의 실행 취소 메커니즘을 하드코딩하는 대신 노출한 것입니다.
tagsRecord<string, string>
최대 10개의 레이블이며, 그대로 되돌려 주고 필터링할 수 있습니다. 해석하지는 않습니다.
signatureboolean
이 메시지가 발신 주소의 서명을 담을지 여부이며, 그 서명은 해당 주소 자체의 서명이거나 없으면 All addresses에 설정된 서명입니다. 기본값은 true인데, 서명은 메시지를 보낸 클라이언트가 아니라 주소에 속하기 때문입니다. 영수증, 비밀번호 재설정, 요약 메일처럼 프로그램이 누군가를 대신해 보내는 메일에는 `false`로 설정하세요. 그런 메일 아래에는 사람의 서명이 들어갈 이유가 없습니다.
tracking{ opens?, clicks? }
이 메시지에 열람 픽셀을 넣고 링크를 재작성할지 여부입니다. 워크스페이스 소유자가 발신 주소나 All addresses에 대해 추적을 끄지 않았다면 켜져 있으며, 여기서 어느 한쪽 필드를 명시하면 주소 설정이 어떻든 그 메시지 하나에 대해 결정됩니다.
translate{ to, from?, subject?, includeOriginal? }
수신자의 언어로 보냅니다. `to`는 코드, 영어 이름, 또는 해당 언어 자체의 이름을 받으며, `subject`와 `includeOriginal`은 둘 다 기본값이 true입니다. 요청이 수락될 때 해석되므로 예약된 메시지도 승인된 문구를 담고 나갑니다. `draftId`와 함께 쓰면 거부됩니다.

응답

idstring
발송 id인 `msg_…`입니다. `get`, `cancel`, `reschedule`, `getTracking`에 사용하세요.
statusEmailStatus
queued, scheduled, sending, sent, partial, cancelled, failed 중 하나입니다. 프로미스가 resolve되었다는 사실이 아니라 이 값을 읽으세요. `partial`은 그 자체로 하나의 상태입니다. 일부 수신자는 이미 메시지를 받았고 되돌릴 수 없으므로, 재시도는 잘못된 대응이고 실패라고 보고하는 것은 거짓입니다.
mode'live' | 'test'
어떤 종류의 키가 보냈는지입니다. 테스트 발송은 기록되며 결코 전송되지 않습니다.
fromstring
실제로 인가되어 전송에 사용된 주소이며, 요청한 주소와 항상 같지는 않습니다.
subjectstring | null
보낸 그대로입니다.
messageIdstring | null
RFC 5322의 Message-ID입니다. MIME이 만들어지기 전까지는 null입니다. 발송 서비스가 나가는 길에 헤더를 다시 쓰므로, 어떤 반송이나 배달 보고서도 이 값을 담지 않습니다. 이벤트가 담고 오는 것은 `id`입니다.
threadIdstring | null
메시지가 들어간 스레드입니다.
transportstring | null
메시지가 어떤 경로로 나갔는지입니다. 발송 전에는 null입니다.
attemptsnumber
발송을 몇 번 시도했는지입니다.
lastErrorstring | null
마지막 시도가 실패한 이유를 그대로 담습니다.
scheduledAtstring | null
나갈 예정인 ISO 시각입니다.
cancellableUntilstring | null
현재 시각이 이보다 앞이면 취소가 여전히 동작합니다.
sentAtstring | null
나간 ISO 시각입니다.
tagsRecord<string, string>
보낸 값을 그대로 되돌려 준 것입니다.
sourceEmailSource
composer, api, mcp, ai, queue 중 어떤 표면이 요청했는지입니다. `api`가 이 클라이언트입니다.
createdAtstring
기록이 쓰인 ISO 시각입니다.
replayedboolean
Idempotency-Key가 이미 존재하는 발송과 일치할 때 true입니다. 새로 보내진 것은 없으며 이것이 원래의 메시지입니다.
translationEmailTranslationResource | undefined
번역된 메시지에만, 그리고 저장된 요청 전체가 실리는 곳, 즉 이 응답과 `get`에만 있습니다. `{ language, languageName, detectedSourceLanguage, subject, includeOriginal }`이며 모두 언어 행이 아니라 코드입니다. 목록 행에는 결코 없으므로, 거기에 없다는 사실은 어느 쪽으로도 아무것도 말해 주지 않습니다.

수신자의 언어로

translate는 메시지가 나가기 전에 다른 사람의 언어로 다시 씁니다. 본문과, 끄지 않았다면 제목까지 API가 요청을 수락할 때 번역되고, 번역된 결과가 그대로 나갑니다. 번역을 만들어 내지 못하면 원래 쓴 언어로 보내는 대신 발송을 거부합니다.

translate.ts
const email = await openemail.emails.send({  from: '[email protected]',  to: '[email protected]',  subject: 'Your September invoice',  html: '<p>Invoice attached. Payment is due on the 14th.</p>',  translate: { to: 'de' },}) console.log(email.translation)// { language: 'de', languageName: 'German', detectedSourceLanguage: 'en', subject: true, includeOriginal: true }

그렇게 나간 메일은 아무도 검토하지 않은 것입니다. emails.translate는 같은 왕복을 한 단계 앞에서 멈춘 것입니다. 사람에게 보여 주고 고치게 한 다음, 승인된 내용을 호출에 translate 없이 그대로 보내세요. 다시 전달하면 두 번 번역되어 그 수정이 버려집니다.

preview-translation.ts
const preview = await openemail.emails.translate({  subject: 'Your September invoice',  html: '<p>Invoice attached. Payment is due on the 14th.</p>',  to: 'de',}) console.log(preview.language.native, preview.detectedSourceLanguage) const approved = await showToSomebody(preview) await openemail.emails.send({  from: '[email protected]',  to: '[email protected]',  subject: approved.subject,  html: approved.html,})
render-picker.ts
import { LANGUAGES, isRtlLanguage, languageByCode, openemail, resolveLanguage } from '@openemail/sdk' LANGUAGES.length // 200 const current = await openemail.languages.list() resolveLanguage('Deutsch')?.code // 'de'resolveLanguage('zh-TW')?.code // 'zh-Hant'languageByCode('DE')?.native // 'Deutsch'isRtlLanguage('ar') // true

이 표는 선택기 순서 그대로 번들에 포함되어 있으므로, 첫 요청 전에 선택기를 채울 수 있습니다. languages.list()는 이 버전에 포함된 것이 아니라 현재 목록을 원하는 호출자를 위해, 같은 행을 통신을 통해 받아 평범한 배열로 resolve합니다. resolveLanguage는 코드, 영어 이름, 자칭 이름, 별칭(zh-TW는 더 이상 목록에 없는 코드의 별칭입니다)을 받고, languageByCode는 대소문자를 무시하고 코드를 정확히 일치시키며, 행 중 16개는 오른쪽에서 왼쪽으로 씁니다. native, label, code를 함께 검색하고, native를 먼저 보여 주고, 코드를 저장하세요.

emails.translate는 자동으로 재시도되지 않습니다. 모델 호출을 소모하고 아무것도 쓰지 않으므로 멱등하게 만들 대상이 없고, 응답이 없던 요청을 재시도해 봐야 같은 답을 두 번 사는 것뿐입니다.

  • 어떤 것으로도 해석되지 않는 언어는 무엇이 보내지기 전에 translate.to에 대한 validation_error가 됩니다.
  • 30,000자를 넘으면 translation_too_long, 설치본에 AI가 구성되어 있지 않으면 translation_not_configured, 제공자가 응답하지 않으면 translation_failed입니다. 어느 것도 번역되지 않은 메시지를 대신 보내지 않습니다.
  • template과 함께 동작합니다. 번역되는 것은 렌더링된 결과이므로, 저장된 본문 하나가 고객이 읽는 모든 언어를 감당합니다. 문서 전체를 렌더링하는 템플릿은 doctype과 <style> 블록, @font-face 규칙을 그대로 유지합니다. 모델에 가는 것은 본문뿐이고 나머지는 다시 그 주위에 붙습니다. <title>은 그대로 두는데, 어차피 표시하는 곳이 없습니다.
  • 재시도에 추가 비용은 없습니다. 번역은 멱등성 지문의 일부가 아니므로(지문에 들어가는 것은 translate를 포함한 요청입니다), 응답이 없던 발송을 같은 Idempotency-Key로 재시도하면 두 번째로 번역해 보내는 대신 이미 존재하는 메시지를 재생합니다.
  • 큐에 있거나 예약된 번역 메시지는 문구 변경에 대해 고정됩니다. emails.reschedule로 시각은 옮길 수 있지만, 내용을 바꾸려면 취소하고 다시 보내야 합니다.

첨부 파일

content는 전송 시 base64입니다. 바이트를 넘기면 대신 인코딩해 줍니다.

attachment.ts
attachments: [  { filename: 'invoice.pdf', content: pdfBytes, contentType: 'application/pdf' },]

toBase64는 다른 곳에서 필요할 때를 위해 export되어 있습니다. 이 함수는 청크 단위로 처리하지만 btoa(String.fromCharCode(...bytes))는 그렇지 않습니다. 그 방식은 약 100 kB를 넘기면 실패하고, 테스트한 파일이 아니라 실제 파일에서 실패합니다.