발송 API 연동 가이드
구매자 서비스(청구 시스템·쇼핑몰·예약 시스템 등)에서 rousme-mailer 로 메일을 보내는 개발자를 위한 문서입니다. HTTPS 로 JSON 을 보내면 되고, 별도 라이브러리는 필요 없습니다.
이 문서는 쓰는 쪽(구매자 서비스 개발자) 기준입니다. 관리 화면 사용법은 사용법.
1. 시작 전에 관리자에게 받을 것
| 항목 | 예 | 어디서 |
|---|---|---|
| API 주소 | https://mailer.example.com/api/v1 |
관리 화면 [발송 API] 탭 아래 "연동 안내" |
| API 키 | mk_ 로 시작하는 긴 문자열 |
관리 화면 [발송 API] 탭 → [키 만들기] (한 번만 보여 줌) |
| 보낼 수 있는 보낸사람 도메인 | example.com |
GET /api/v1/status 의 sender_domains |
| 템플릿 이름·변수 (템플릿을 쓸 때) | invoice / 고객명, 금액 … |
관리 화면 [템플릿] 탭, GET /api/v1/templates |
- 키는 비밀번호처럼 다루세요: 서버 환경변수·비밀 저장소에 두고, 소스 코드·브라우저·앱에 넣지 않습니다. 새면 관리자에게 끄고 새로 만들어 달라고 하세요.
- 키에 허용 IP 를 걸어 두면 그 주소에서 온 요청만 받습니다 (구매자 서비스 서버의 고정 IP 를 알려 주세요).
- 키 권한 (여러 개를 같이 줄 수 있음):
send(보내기·조회 — 자기가 보낸 메일만 조회),read(조회만 — 모든 API 메일 조회, 대시보드용),contacts(주소록 읽기·쓰기 — 10절).
2. 연동 시험은 이렇게
서버가 테스트 모드면 받는사람이 누구든 모든 메일이 서버 안의 Mailpit 으로만 가고 밖으로 나가지 않습니다.
응답의 "mode": "test" 로 알 수 있습니다. 연동을 만들 때는 관리자에게 테스트 모드로 두어 달라고 하거나,
받는사람을 아무거나@mailpit.test 로 쓰세요 (실서버 모드에서도 이 도메인은 Mailpit 으로 갑니다).
curl https://mailer.example.com/api/v1/status -H "Authorization: Bearer $MAILER_KEY"
# {"mode": "test", "james": "active", "key": "청구 시스템", "scopes": ["send"],
# "sender_domains": ["example.com"], "test_domain": "mailpit.test",
# "live_limit": {"daily_limit": 1000, "used_today": 12, "remaining": 988}, "queue": 0}
3. 공통
- 모든 요청:
Authorization: Bearer <키>, 본문이 있으면Content-Type: application/json(UTF-8) - 응답은 JSON, 시각은 ISO 8601 (
2026-10-01T09:30:12+09:00) - 오류 응답은 항상
{"error": "코드", "message": "한국어 설명"}(+ 경우에 따라missing) - 요청 수 한도: 키마다 초당 10회, 분당 300회 (넘으면
429+Retry-After)
4. 한 통 보내기 — POST /api/v1/messages
받는사람 1명에 한 통입니다. 청구서처럼 사람마다 내용이 다르므로 여러 명이면 한 명씩 보냅니다.
{
"from": "billing",
"from_name": "○○상사",
"to": "customer@example.org",
"subject": "[○○상사] 10월 청구서",
"text": "홍길동 님, 10월 청구 금액은 55,000원입니다.",
"html": "<p>홍길동 님, 10월 청구 금액은 <b>55,000원</b>입니다.</p>",
"attachments": [{"filename": "청구서-202610.pdf", "content": "<base64>", "type": "application/pdf"}],
"category": "transactional",
"tags": ["invoice", "2026-10"],
"metadata": {"invoice_id": "00123"}
}
| 항목 | 필수 | 설명 |
|---|---|---|
from |
✔ | 보낸사람. billing 처럼 앞부분만 주면 대표 발신 도메인(billing@example.com), 전체 주소는 발신 도메인 안이어야 함 |
from_name |
보낸사람 이름 (80자) | |
to |
✔ | 받는사람 주소 1개 |
subject |
✔ | 제목 (한 줄, 300자) |
text, html |
하나 이상 | 본문 (합계 1MB). html 만 주면 텍스트 본문은 서버가 HTML 에서 만들어 넣음 |
attachments |
[{filename, content(base64), type?}] 5개·합계 10MB까지. type 을 빼면 파일 이름으로 정함 |
|
category |
transactional(거래성, 기본) 또는 marketing(광고) — 7절 |
|
tags |
문자열 10개까지 (각 40자, 한글·영문·숫자·_.:-). 목록에서 거를 때 씀 |
|
metadata |
아무 JSON 객체 (2,000자). 조회할 때 그대로 돌려줌 — 주문번호·청구번호 등 | |
template, variables |
템플릿으로 보낼 때 (5절). 이때 subject·text·html·category 는 넣지 않음 |
모르는 항목을 넣으면 400 입니다 (오타를 조용히 무시하지 않으려고).
응답 202 Accepted:
{"id": "msg_1a0eacc71711a615dc", "status": "queued", "mode": "live", "to": "customer@example.org",
"from": "billing@example.com", "subject": "[○○상사] 10월 청구서", "category": "transactional",
"template": null, "tags": ["invoice", "2026-10"], "metadata": {"invoice_id": "00123"},
"reason": null, "host": null, "created": "2026-10-01T09:30:12+09:00", "accepted": null, "finished": null,
"events": [{"time": "2026-10-01T09:30:12+09:00", "status": "queued"}]}
202 는 "받아서 대기열에 넣었다" 는 뜻입니다. 실제 전달 결과는 6절처럼 조회합니다 (보통 몇 초).
중복 방지: Idempotency-Key (꼭 쓰세요)
네트워크 시간 초과로 응답을 못 받으면, 보냈는지 모르는 채로 다시 보내게 됩니다. 요청에 Idempotency-Key 머리글을 주면
같은 키의 요청은 24시간 동안 한 통만 보내고 처음 메일을 다시 돌려줍니다 (응답 머리글 Idempotent-Replayed: true).
- 값은 그 메일을 나타내는 고유한 문자열 (100자까지) — 청구번호,
주문번호-안내종류등 - 같은 키로 다른 내용을 보내면
409 idempotency_conflict(실수로 키를 재사용한 경우를 막음) - 키는 API 키마다 따로 셉니다
5. 템플릿으로 보내기
문구는 관리 화면 [템플릿] 탭에서 만들고, 구매자 서비스는 템플릿 이름과 변수 값만 보냅니다. 문구를 고칠 때 구매자 서비스를 고치지 않아도 됩니다.
{
"template": "invoice",
"to": "customer@example.org",
"variables": {"고객명": "홍길동", "청구월": "10월", "금액": "55,000", "납부기한": "10월 31일",
"청구번호": "INV-202610-00123", "항목표": "<table><tr><td>기본료</td><td>50,000</td></tr></table>"},
"attachments": [{"filename": "청구서-202610.pdf", "content": "<base64>"}]
}
- 제목·본문·종류(거래성/광고)는 템플릿에서, 보낸사람(
from·from_name)은 요청에 주면 요청 값, 없으면 템플릿 기본값 - 변수 값은 문자열이나 숫자. 템플릿의
{{변수}}자리에 들어가고, HTML 본문에서는<>&같은 글자가 그대로 보이게 바뀝니다 (고객 이름에<script>가 와도 안전) - 템플릿에
{{{변수}}}(중괄호 3개)로 된 자리는 값을 HTML 그대로 넣습니다 — 구매자 서비스가 직접 만든 표 등. 믿을 수 있는 값만 넣으세요 - 템플릿에 필요한 변수가 하나라도 빠지면 보내지 않고
422 missing_variables+"missing": ["금액"]. 템플릿에 없는 변수는 무시 - 없거나 꺼진 템플릿은
422 template_not_found
도움 API (권한 read 이상):
GET /api/v1/templates → {"items": [{"name", "description", "category", "subject", "variables", "from", "from_name", "active"}]}
POST /api/v1/templates/{이름}/render {"variables": {...}} → {"subject", "text", "html"} (보내지 않음)
6. 결과 조회
GET /api/v1/messages/{id}
status |
뜻 | 다음 |
|---|---|---|
queued |
받아서 대기 중 | 몇 초 안에 accepted |
accepted |
메일 서버(James)가 접수, 받는 쪽으로 보내는 중 | delivered / deferred / bounced |
delivered |
받는 쪽 메일 서버가 받음 (host 에 서버 이름, 테스트 모드는 Mailpit) |
끝 |
deferred |
일시 실패, 메일 서버가 다시 시도 중 (reason) |
delivered 또는 bounced |
bounced |
영구 실패 (reason 에 받는 쪽 응답, 예: 550 5.1.1 … does not exist) |
끝. 주소 문제면 발송 제외 목록에 자동 추가 |
suppressed |
발송 제외 목록에 있어 보내지 않음 (reason) — 한도도 쓰지 않음 |
끝 |
failed |
메일 서버에 넣지 못함 (30분 넘게 멈춤, 거부, 받은 뒤에 관리자가 모드를 바꾸거나 하루 한도를 줄여 한도에 걸림) | 끝. 필요하면 새로 보냄 |
delivered 는 받는 쪽 서버가 받았다는 뜻이고 받은편지함·스팸함 중 어디에 들어갔는지까지는 알 수 없습니다.
events 에 단계별 시각이 쌓이고, 광고 메일은 받는 사람이 수신거부를 누르면 unsubscribed 사건이 붙습니다.
조회 주기: 결과 알림(웹훅)은 아직 없습니다. 보낸 뒤 5초·30초·5분 정도 간격으로 조회하고,
delivered·bounced·suppressed·failed 가 되면 멈추세요. 청구서라면 bounced·failed 일 때 담당자에게 알리면 됩니다.
목록:
GET /api/v1/messages?status=bounced&tag=invoice&since=2026-10-01T00:00:00%2B09:00&limit=50&cursor=<next_cursor>
→ {"items": [...], "next_cursor": "msg_…" 또는 null}
최신순이고, next_cursor 를 다음 요청의 cursor 로 넣으면 다음 쪽입니다. 기록은 90일 보관됩니다.
7. 거래성 메일과 광고 메일
거래성 transactional (기본) |
광고 marketing |
|
|---|---|---|
| 예 | 청구서·영수증·예약 확인·비밀번호 재설정 | 뉴스레터·할인·신상품 안내 |
| 수신거부한 주소 | 보냄 (광고가 아니므로) | 보내지 않음 (suppressed) |
| 영구 반송·관리자가 막은 주소 | 보내지 않음 | 보내지 않음 |
| 제목 | 제한 없음 | (광고) 로 시작해야 함 — 없으면 400 (정보통신망법 50조, 자동으로 붙이지 않음) |
| 수신거부 링크 | 없음 | 메일 앱의 "구독 취소" 버튼(List-Unsubscribe)이 자동으로 붙음 |
| 주소록에서 "광고 동의 안 함" | 보냄 | 보내지 않음 (suppressed, 사유 "주소록에서 광고 수신 동의 안 함") |
| 보내는 시각 | 언제나 | 기본 8~21시만 — 밤(21시~다음 날 8시)에 요청하면 202 queued 로 받아 두었다가 다음 날 8시에 보냄 (reason 에 보낼 시각). 법으로는 전자우편이 야간 광고 별도 동의의 예외지만, 밤 광고는 신고로 이어지기 쉬워 기본으로 막아 둠. 관리자가 서버 설정 AD_HOURS 로 바꾸거나 끌 수 있음 |
광고를 거래성으로 보내면 법 위반이고 메일 평판도 떨어집니다. 종류를 정확히 넣어 주세요. 광고 수신 동의는 구매자 서비스가 받아 두고 주소록 API(10절)로 알려 주세요. 주소록에 없는 주소로 광고를 보내면 막지 않으므로(구매자 서비스 책임) 동의한 사람에게만 보내야 합니다.
8. 오류와 다시 보내기
| 상태 | error 예 |
다시 보내도 되나 |
|---|---|---|
400 |
invalid_request |
아니오 — 요청을 고침 (message 참고) |
401 |
unauthorized |
아니오 — 키 확인 (없음·틀림·꺼짐) |
403 |
forbidden |
아니오 — 권한(read 키로 보내기)·허용 IP |
409 |
idempotency_conflict |
아니오 — 같은 키로 다른 내용 |
413 |
too_large |
아니오 — 본문·첨부 줄이기 |
422 |
invalid_recipient, sender_not_allowed, template_not_found, missing_variables |
아니오 — 값 고침 |
429 |
rate_limited, daily_limit |
예 — Retry-After 초 뒤. daily_limit 은 자정 뒤 |
503 |
unavailable |
예 — 잠시 뒤 |
연결 실패·시간 초과·5xx |
예 — 같은 Idempotency-Key 로 |
다시 보낼 때는 반드시 같은 Idempotency-Key 를 쓰세요. 그래야 앞 요청이 사실은 성공했더라도 두 통이 가지 않습니다.
간격은 1초·5초·30초·2분처럼 늘리는 것을 권합니다.
9. 예시 코드
아래 예시는 모두 같은 일을 합니다: 템플릿 invoice 로 PDF 첨부 청구서 한 통 → 실패하면 같은 키로 다시 → 결과 조회.
주소와 키는 환경변수 MAILER_URL(예 https://mailer.example.com), MAILER_KEY 로 받습니다.
curl
PDF=$(base64 < 청구서-202610.pdf | tr -d '\n')
curl -sS "$MAILER_URL/api/v1/messages" \
-H "Authorization: Bearer $MAILER_KEY" \
-H "Idempotency-Key: INV-202610-00123" \
-H "Content-Type: application/json" \
--retry 3 --retry-all-errors --max-time 30 \
-d @- <<EOF
{"template": "invoice", "to": "customer@example.org",
"variables": {"고객명": "홍길동", "청구월": "10월", "금액": "55,000", "납부기한": "10월 31일",
"청구번호": "INV-202610-00123", "항목표": ""},
"attachments": [{"filename": "청구서-202610.pdf", "content": "$PDF"}],
"tags": ["invoice"], "metadata": {"invoice_id": "00123"}}
EOF
curl -sS "$MAILER_URL/api/v1/messages/msg_…" -H "Authorization: Bearer $MAILER_KEY"
Python (requests)
import base64, os, time
import requests
URL, KEY = os.environ["MAILER_URL"], os.environ["MAILER_KEY"]
H = {"Authorization": f"Bearer {KEY}"}
RETRY = {429, 500, 502, 503, 504}
def send_invoice(to, invoice_no, values, pdf_path):
with open(pdf_path, "rb") as f:
pdf = base64.b64encode(f.read()).decode()
body = {"template": "invoice", "to": to, "variables": values,
"attachments": [{"filename": os.path.basename(pdf_path), "content": pdf}],
"tags": ["invoice"], "metadata": {"invoice_no": invoice_no}}
for wait in (1, 5, 30, 120, None):
try:
r = requests.post(f"{URL}/api/v1/messages", json=body, timeout=30,
headers={**H, "Idempotency-Key": invoice_no}) # 다시 보낼 때도 같은 키
if r.status_code not in RETRY:
break
if wait:
wait = max(wait, int(r.headers.get("Retry-After", 0)))
except requests.RequestException:
pass
if wait is None:
raise RuntimeError("메일 서버에 보내지 못했습니다")
time.sleep(wait)
if r.status_code != 202:
err = r.json()
raise ValueError(f"{r.status_code} {err['error']}: {err['message']} {err.get('missing', '')}")
return r.json()["id"]
def wait_result(msg_id, timeout=300):
end = time.time() + timeout
while time.time() < end:
m = requests.get(f"{URL}/api/v1/messages/{msg_id}", headers=H, timeout=30).json()
if m["status"] in ("delivered", "bounced", "suppressed", "failed"):
return m
time.sleep(5)
return m # 아직 accepted/deferred — 나중에 다시 조회
msg_id = send_invoice("customer@example.org", "INV-202610-00123",
{"고객명": "홍길동", "청구월": "10월", "금액": "55,000", "납부기한": "10월 31일",
"청구번호": "INV-202610-00123", "항목표": ""},
"청구서-202610.pdf")
m = wait_result(msg_id)
print(m["status"], m["host"] or m["reason"])
PHP (curl 확장)
<?php
$url = getenv('MAILER_URL');
$key = getenv('MAILER_KEY');
function mailer($method, $path, $body = null, $idem = null) {
global $url, $key;
$h = ["Authorization: Bearer $key", "Content-Type: application/json"];
if ($idem) $h[] = "Idempotency-Key: $idem";
$ch = curl_init($url . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h,
CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body, JSON_UNESCAPED_UNICODE));
$res = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
return [$code, $res === false ? null : json_decode($res, true)];
}
$no = 'INV-202610-00123';
$body = [
'template' => 'invoice', 'to' => 'customer@example.org',
'variables' => ['고객명' => '홍길동', '청구월' => '10월', '금액' => '55,000', '납부기한' => '10월 31일',
'청구번호' => $no, '항목표' => ''],
'attachments' => [['filename' => '청구서-202610.pdf', 'content' => base64_encode(file_get_contents('청구서-202610.pdf'))]],
'tags' => ['invoice'], 'metadata' => ['invoice_no' => $no],
];
foreach ([1, 5, 30, 120, 0] as $wait) { // 실패하면 같은 키로 다시
[$code, $res] = mailer('POST', '/api/v1/messages', $body, $no);
if ($code && !in_array($code, [429, 500, 502, 503, 504])) break;
if (!$wait) throw new Exception('메일 서버에 보내지 못했습니다');
sleep($wait);
}
if ($code != 202) throw new Exception("$code {$res['error']}: {$res['message']}");
$id = $res['id'];
for ($i = 0; $i < 60; $i++) {
sleep(5);
[, $m] = mailer('GET', "/api/v1/messages/$id");
if (in_array($m['status'], ['delivered', 'bounced', 'suppressed', 'failed'])) break;
}
echo $m['status'], ' ', $m['host'] ?? $m['reason'], "\n";
Node.js (18 이상, 내장 fetch)
send-invoice.mjs 처럼 .mjs 로 저장하거나 package.json 에 "type": "module".
import { readFile } from "node:fs/promises";
const URL = process.env.MAILER_URL, KEY = process.env.MAILER_KEY;
const H = { Authorization: `Bearer ${KEY}` };
const sleep = (s) => new Promise((r) => setTimeout(r, s * 1000));
async function sendInvoice(to, invoiceNo, variables, pdfPath) {
const body = JSON.stringify({
template: "invoice", to, variables,
attachments: [{ filename: pdfPath.split("/").pop(), content: (await readFile(pdfPath)).toString("base64") }],
tags: ["invoice"], metadata: { invoice_no: invoiceNo },
});
for (const wait of [1, 5, 30, 120, 0]) {
let r;
try {
r = await fetch(`${URL}/api/v1/messages`, {
method: "POST", body, signal: AbortSignal.timeout(30000),
headers: { ...H, "Content-Type": "application/json", "Idempotency-Key": invoiceNo }, // 다시 보낼 때도 같은 키
});
} catch { r = null; }
if (r && ![429, 500, 502, 503, 504].includes(r.status)) {
const d = await r.json();
if (r.status !== 202) throw new Error(`${r.status} ${d.error}: ${d.message}`);
return d.id;
}
if (!wait) throw new Error("메일 서버에 보내지 못했습니다");
await sleep(Number(r?.headers.get("retry-after")) || wait);
}
}
async function waitResult(id) {
for (let i = 0; i < 60; i++) {
await sleep(5);
const m = await (await fetch(`${URL}/api/v1/messages/${id}`, { headers: H })).json();
if (["delivered", "bounced", "suppressed", "failed"].includes(m.status)) return m;
}
}
const id = await sendInvoice("customer@example.org", "INV-202610-00123",
{ 고객명: "홍길동", 청구월: "10월", 금액: "55,000", 납부기한: "10월 31일", 청구번호: "INV-202610-00123", 항목표: "" },
"청구서-202610.pdf");
const m = await waitResult(id);
console.log(m.status, m.host ?? m.reason);
10. 주소록 맞추기 — 회원 가입·동의 변경·탈퇴
관리 화면의 [주소록]은 광고·뉴스레터를 그룹으로 보낼 때 쓰고, 사람마다 광고 수신 동의(일시·방법) 를 기록합니다.
회원 가입·설정 변경·탈퇴 때 구매자 서비스가 아래 API 를 부르면 주소록이 늘 맞게 유지됩니다. 키에 contacts 권한이 필요합니다.
| 방법 | 경로 | 설명 |
|---|---|---|
| PUT | /api/v1/contacts/{이메일} |
넣거나 고침 — 새로 만들면 201, 고치면 200 (result: added·updated·unchanged) |
| GET | /api/v1/contacts/{이메일} |
한 명 (그룹, 동의, 발송 제외 사유, 동의 기록) |
| GET | /api/v1/contacts?email=&group=&consent=yes\|no\|unknown&limit=&cursor= |
목록·찾기 (email 은 일부만 넣어도 됨) |
| DELETE | /api/v1/contacts/{이메일} |
지움 (탈퇴 — 동의 기록도 지움. 발송 제외 목록의 주소는 남아 다시 보내지 않음) |
| GET | /api/v1/groups |
그룹 이름·인원 |
PUT 본문 (준 항목만 바뀜):
{
"name": "홍길동",
"fields": {"등급": "골드", "지역": "서울", "예전필드": null},
"groups": {"add": ["회원", "뉴스레터"], "remove": ["휴면"]},
"consent": {"marketing": true, "at": "2026-09-01T10:00:00+09:00", "method": "회원가입 체크박스"}
}
fields: 템플릿 변수로 쓰는 추가 정보 (이름은 한글·영문·숫자·_, 20개·값 500자).null이면 그 필드를 지움groups:{"add", "remove"}또는 목록(그 목록으로 맞춤). 없는 그룹 이름은add할 때 만들어짐consent: 광고 수신 동의.true로 바꿀 때는method필수(어떻게 동의받았는지),at은 동의 시각(없으면 지금).false는 철회. 모든 변경은 동의 기록에api:키 이름으로 남음. 수신거부로 발송 제외된 사람이 다시 동의하면 그 제외가 풀림- 받는 사람이 메일의 수신거부 링크를 누르면 주소록의 동의도 자동으로 "동의 안 함"이 됩니다 (구매자 서비스에도 반영하려면 주기적으로
GET /api/v1/contacts?consent=no를 보거나, 발송 결과의unsubscribed사건을 보세요)
# 회원 가입·설정 변경 때
def sync_member(m):
body = {"name": m.name, "fields": {"등급": m.grade}, "groups": {"add": ["회원"]}}
if m.marketing_changed:
body["consent"] = {"marketing": m.marketing, "at": m.marketing_changed_at.isoformat(), "method": "회원 설정"}
requests.put(f"{URL}/api/v1/contacts/{m.email}", json=body, headers=H, timeout=30).raise_for_status()
# 탈퇴 때
requests.delete(f"{URL}/api/v1/contacts/{email}", headers=H, timeout=30)
11. 자주 묻는 것
- 여러 명에게 같은 공지를 보내려면? 한 명씩 요청하세요 (초당 10회). 수천 명 단위의 뉴스레터는 관리 화면 [발송] 탭의 대량 발송(속도 제한 가능)이 맞습니다.
- SMTP(587)로 보내도 되나? 됩니다 (관리자에게 587 계정을 받음). 다만 587 로 넣은 메일에는 발송 제외 목록·하루 한도·
(광고)검사·수신거부 헤더·결과 조회가 적용되지 않습니다 — 메일 서버가 그대로 내보냅니다. 수신거부한 사람을 빼는 일과 광고 규칙은 보내는 프로그램이 지켜야 합니다. 되도록 API 를 쓰세요. - 하루 한도가 있나? 새 서버는 평판을 쌓는 동안(IP 예열) 관리자가 하루 실제 발송량을 제한합니다. 요청할 때 넘으면
429 daily_limit로 거절하고(받지 않음), 이미 받은 메일이 보낼 때 한도에 걸리면failed가 됩니다 (받은 뒤 관리자가 한도를 줄인 경우). 남은 양은GET /api/v1/status의live_limit.remaining. - 대량 발송 중이면 청구서가 늦나? API 메일은 별도 대기열이라 웹 대량 발송 작업에 막히지 않습니다. 다만 메일 서버 안의 전달 순서는 하나라, 아주 큰 대량 발송이 먼저 들어가 있으면 그만큼 늦게 나갈 수 있습니다.
- 본문·첨부는 서버에 남나? 메일 서버에 넣은 뒤 바로 지웁니다. 기록에는 받는사람·제목·상태·태그·메타데이터만 90일 남습니다.