Self-hosted auth email service for Python/FastAPI teams using their own SMTP
Project description
email-service
self-hosted auth email service for Python/FastAPI teams that need magic links, OTP codes, and password reset emails through their own SMTP.
email-service는 인증 플로우에서 반복되는 이메일 발송을 작은 내부 서비스로 분리한다. SMTP 자격증명은 서비스 안에만 두고, Python/FastAPI 백엔드나 다른 내부 서비스는 Bearer 인증이 걸린 HTTP API로 매직링크, OTP, 비밀번호 재설정 이메일을 요청한다.
소개
email-service는 SMTP로 auth-email HTML 메시지를 보내기 위한 재사용 가능한 파이썬 패키지이다. 다음 두 가지 사용 방식을 모두 지원한다.
- 라이브러리 모드 — 같은 Python 프로세스에서
SmtpSender,MagicLinkNotifier등을 직접 import해 호출한다. 외부 의존성 없음 (표준 라이브러리의smtplib,email만 사용). - HTTP 서비스 모드 —
python -m email_service로 FastAPI 서버를 기동하고, 다른 백엔드(언어 무관)가 REST로 메일 발송을 요청한다. SMTP 자격증명을 서버 측 환경변수에만 보관하고 호출자는 공유API_KEY로 인증한다.
비밀번호 설정 매직링크 / 일회용 인증코드(OTP) 같은 자주 쓰이는 템플릿은 기본 제공되며, 커스텀 템플릿도 쉽게 추가할 수 있다.
Who this is for
- Python/FastAPI 팀이 자체 SMTP 계정(Gmail, SES SMTP, 사내 relay 등)으로 인증 이메일을 보내고 싶을 때.
- SMTP 비밀번호를 여러 앱에 흩뿌리지 않고, 작은 내부 HTTP 서비스 하나에 모으고 싶을 때.
- 매직링크, OTP, 비밀번호 재설정처럼 트랜잭션 auth email만 안정적으로 처리하면 되는 초기 제품.
Who this is not for
- 마케팅 캠페인, 대량 뉴스레터, list unsubscribe, bounce/complaint 자동 처리, deliverability 컨설팅이 필요한 팀.
- 호출자별 API key, 조직별 quota, admin dashboard 같은 멀티테넌트 email platform이 필요한 경우.
- SMTP 계정을 직접 운영하고 싶지 않고, 완전 관리형 발송 API와 SLA를 사고 싶은 경우.
Why this exists
인증 이메일은 제품마다 필요하지만, 직접 smtplib 호출을 여러 서비스에 복붙하면 SMTP 자격증명 관리, HTML escaping, retry, metrics, webhook 결과 통지가 흩어진다. 이 프로젝트는 그 부분만 작게 묶어 내부 서비스로 실행하거나 라이브러리로 가져다 쓰는 것을 목표로 한다.
When to use this
- 이미 쓸 SMTP provider가 있고, 인증 이메일 발송 경로를 코드 몇 줄 또는 REST API로 표준화하고 싶다.
- self-hosted 배포와 운영 책임을 감수할 수 있다.
- 제품 초기 단계라 Postmark/SendGrid 같은 완전 관리형 플랫폼의 모든 기능까지는 필요 없다.
When not to use this
- 발송 평판, bounce processing, suppression list, analytics dashboard가 핵심 요구사항이다.
- 인터넷에 직접 공개되는 public email API gateway가 필요하다. 이 서비스는 reverse proxy/API gateway 뒤의 내부 서비스로 운영하는 전제가 안전하다.
- 팀이 SMTP credential rotation, proxy limits, metrics auth 같은 운영 작업을 맡을 수 없다.
Honest comparison
| 선택지 | 장점 | 트레이드오프 |
|---|---|---|
| Resend | 개발자 경험 좋은 관리형 이메일 API | SMTP 자격증명을 직접 쓰는 self-hosted 내부 서비스가 아님 |
| SendGrid | 대량 발송, 분석, deliverability 기능 풍부 | 인증 이메일만 필요한 작은 Python 서비스에는 과할 수 있음 |
| Postmark | 트랜잭션 이메일에 강하고 운영 부담 낮음 | 외부 SaaS 의존과 비용을 받아들여야 함 |
| AWS SES | 비용 효율 좋고 AWS 안에서 강력함 | 설정, IAM, sandbox, bounce 처리 등 운영 이해가 필요 |
raw smtplib |
의존성 적고 완전 직접 제어 | escaping, retry, metrics, API 인증, 템플릿 패턴을 직접 유지해야 함 |
| this project | 자체 SMTP를 쓰는 Python/FastAPI auth-email microservice로 작고 검토 가능 | 멀티테넌트 플랫폼이나 deliverability SaaS가 아니며 운영 guardrail은 배포자가 책임져야 함 |
보안 모델 / Security Model
- 매직링크 토큰 엔트로피는 호출자 책임이다. 본 패키지는
MagicLinkNotifier로 전달된token문자열을 그대로 URL 쿼리에 인코딩만 할 뿐, 생성·검증·저장하지 않는다. 호출자는 최소secrets.token_urlsafe(32)수준의 엔트로피로 토큰을 생성하고, 만료·1회용 사용 등 라이프사이클을 별도로 관리해야 한다. API_KEY는 공유 비밀 이며Authorization: Bearer헤더로 전달된다.openssl rand -hex 32등으로 충분히 길고 무작위인 값을 사용하고, 절대 저장소에 커밋하지 않는다.- CRLF 헤더 인젝션 은 sender 단계와 Pydantic 단계 모두에서 차단된다.
SMTP_FROM도 부팅 시 검증된다. - STARTTLS 가 서버에서 광고되지 않는 경우
use_tls=True발송은 명시적으로 실패한다 (다운그레이드 / STRIPTLS 방어).
주요 기능
- HTML + plain-text multipart 발송 —
text_body를 넘기면 HTML 미지원 클라이언트를 위한 대체본이 함께 첨부된다. - cc / bcc 지원 — 헤더/수신자 목록에 올바르게 반영. bcc 는 헤더에 노출되지 않는다.
- CRLF 헤더 인젝션 차단 —
to,subject,from,cc,bcc에\r/\n이 포함되면 발송을 거부한다. HTTP API 에서는 sender 까지 가기 전 Pydantic 단계에서422로 차단. - HTML 자동 이스케이프 —
MagicLinkNotifier/OTPNotifier/TemplateNotifier의 사용자 입력 값 (user_name, token, code, context) 은 기본적으로html.escape처리된다. - 플러그인 방식 Notifier —
Notifier추상 클래스 상속으로 새로운 이메일 템플릿을 손쉽게 추가. - STARTTLS + SMTP AUTH —
SmtpConfig.use_tls/user/password로 제어. 자격증명이 비면 AUTH 생략. - Fail-fast 기동 — HTTP 모드에서 필수 환경변수가 비어 있으면
RuntimeError로 즉시 실패. - OpenAPI 문서 자동 제공 — 기본 활성화된
/docs(Swagger UI),/openapi.json.
Deployment
운영 환경 배포 전 반드시 확인할 사항들. 이 섹션을 건너뛰면 본 서비스가 의도한 보안/안정성 보장이 무너질 수 있다.
Public deployment guardrails
공개 GitHub 저장소나 public deployed URL을 공유하기 전 최소 기준:
API_KEY는openssl rand -hex 32또는 동등한 방식으로 생성한 긴 랜덤 값만 사용한다. 예제용 짧은 문자열을 운영에 쓰지 않는다.- 서비스는 인터넷에 직접 노출하지 말고 reverse proxy/API gateway 뒤에 둔다.
- TLS termination은 앞단 proxy/gateway에서 처리한다.
- 앞단 proxy/gateway에서 body-size limit을 설정한다. 예: nginx
client_max_body_size 12m. - 실패한 인증 시도는 앞단 proxy/gateway/WAF에서 rate limit 한다. 앱 내부 rate limit은 인증된
/send*요청 보호용이며, 잘못된 Bearer 토큰 대입 공격을 대신 막지 않는다. METRICS_ENABLED=true로 운영할 때는METRICS_REQUIRE_AUTH=true를 함께 설정하고, 가능하면/metrics는 내부망에서만 접근시킨다.- SMTP 자격증명과
API_KEY는 secret store, 배포 플랫폼 secret, 또는.env처럼 git 밖의 저장소에 보관한다. /docs와/openapi.json은 편리하지만 운영 외부 공개가 불필요하면 앞단에서 차단한다.
워커 수 (single vs multi)
본 서비스의 다음 상태는 in-memory, per-process이다:
- Rate limit (
API_RATE_LIMIT_PER_MINUTE): 워커당 cap. uvicorn workers=N 이면 실제 처리량 = N × cap. - Idempotency cache (
API_IDEMPOTENCY_TTL_SECONDS): 워커당 dedup. 같은Idempotency-Key가 다른 워커에 분산되면 dedup 깨짐. - Per-key concurrency lock: 워커당 직렬화. 워커 N개면 같은 키가 최대 N회 동시 처리 가능.
권장:
- 단일 워커 + sticky LB: 가장 단순.
uvicorn email_service --workers 1또는 같은 워커로 라우팅하는 LB. 본 서비스가 의도한 정확한 동작. - 멀티 워커: rate-limit / idempotency 정확성이 SLA 일부면 외부 store (Redis 등) 로 교체 필요 — 현재 미지원, P1 향후 항목.
본문 크기 제한
Pydantic 의 max_length 가 422 거부를 보장하지만, FastAPI 가 요청 본문을 메모리에 전체 buffer 한 후 Pydantic 을 실행한다. 따라서 100 MB 요청이 들어오면 거부는 되어도 메모리는 일시 점유.
필수: 리버스 프록시에서 body cap 설정.
# /etc/nginx/sites-available/email-service
location / {
client_max_body_size 12m; # 10 MB body cap + 2 MB headers/overhead
proxy_pass http://email-service:8000;
}
uvicorn 자체에는 명시적 body cap 옵션이 없음 — proxy 단에서 차단.
환경변수 reference
| 변수 | 필수 | 기본 | 설명 |
|---|---|---|---|
SMTP_HOST |
✅ | — | SMTP 호스트 |
SMTP_PORT |
587 |
SMTP 포트 | |
SMTP_USER |
"" |
SMTP 사용자 (옵션) | |
SMTP_PASSWORD |
"" |
SMTP 비밀번호 (옵션) | |
SMTP_FROM |
SMTP_USER |
From 헤더 주소 | |
SMTP_USE_TLS |
true |
STARTTLS 사용 | |
API_KEY |
✅ | — | Bearer 인증 토큰. openssl rand -hex 32 권장 |
API_RATE_LIMIT_PER_MINUTE |
60 |
/send* 의 per-bearer 분당 호출 cap. 0 이면 비활성. |
|
API_IDEMPOTENCY_TTL_SECONDS |
86400 |
Idempotency-Key 캐시 TTL (초). 0 이면 비활성. |
|
WEBHOOK_ALLOW_HOSTS |
"" |
webhook_url SSRF 검증의 hostname allowlist (콤마 구분). 내부 콜백용. |
|
WEBHOOK_ALLOW_LOOPBACK |
false |
1 이면 loopback/private IP 허용. 테스트 전용 — production 금지 |
|
EMAIL_SERVICE_DEBUG |
false |
1 이면 smtplib 디버그 출력 (SMTP 비밀번호가 stderr 에 base64 로 출력됨 — production 절대 금지) |
|
MAGIC_LINK_BASE_URL |
unset | 설정 시 /send/magic-link 활성화 |
|
METRICS_ENABLED |
false |
/metrics 엔드포인트 활성화 |
|
METRICS_REQUIRE_AUTH |
false |
/metrics 에 Bearer 인증 강제. public 배포에서는 true 필수 |
|
EMAIL_SERVICE_LOG_FORMAT |
text |
json 시 구조화 로그 |
|
EMAIL_TEST_CAPTURE_DIR |
unset | 설정 시 SMTP 미접속, .eml 파일 저장 (테스트용) |
Webhook signature: V1 → V2 migration
본 서비스는 webhook payload 에 두 가지 서명 헤더를 동시 전송한다:
X-Email-Service-Signature(V1): HMAC-SHA256(secret, body) — replay 공격에 취약.X-Email-Service-Signature-V2: HMAC-SHA256(secret,"<timestamp>.<body>")X-Email-Service-Timestamp: Unix epoch seconds
V2 채택 권장 (수신자 측 마이그레이션 절차):
X-Email-Service-Timestamp읽기.abs(now - timestamp) > 300(5분) 이면 거부 — replay window 차단.hmac_sha256(secret, f"{timestamp}.{body}")를 V2 헤더와 constant-time 비교.
V1 헤더는 향후 major version 에서 제거 예정. CHANGELOG 참조.
Release 자동화 (PyPI)
release.yml 은 2-step manual gate 모델이다. tag push 만으로는 PyPI 에 publish 되지 않는다.
- tag push →
build-and-smokejob 만 실행. wheel 빌드 + smoke import + tag/version 일치 검증까지만 수행. PyPI 는 건드리지 않는다. - Actions →
release→ "Run workflow" (workflow_dispatch) 에서 publish 할 tag (예:v0.4.0) 를 입력하고 수동 실행. 이 dispatch 자체가 사람 승인 게이트다.build-and-smoke가 다시 검증된 뒤publishjob 이 PyPI 로 업로드한다.
왜 분리: private repo 에서는 GitHub Environment "Required reviewers" UI 가 플랜에 따라 노출되지 않을 수 있어 environment: pypi 게이트만으로는 manual approval 을 보장하기 어렵다. workflow_dispatch 트리거 자체를 사람 행동으로 만들어 이 빈틈을 막는다. Environment Required reviewers 가 활성화돼 있다면 그 위에 추가로 얹히는 defense-in-depth.
- 모든 GitHub Actions 는 commit SHA 로 핀 (mutable tag 금지).
- 잘못된 publish 는 yank 만 가능, 버전명 영구 소진.
docs/runbooks/pypi-yank-hotfix.md참조.
운영 runbook
장애 / 회전 / 핫픽스 절차:
docs/runbooks/public-deploy-readiness.mddocs/runbooks/smtp-outage.mddocs/runbooks/webhook-outage.mddocs/runbooks/api-key-rotation.mddocs/runbooks/pypi-yank-hotfix.mddocs/runbooks/smtp-disconnect-uncertain.md
Operations
운영 환경에서 발송 성공률·실패 사유·지연을 관측하기 위한 옵트인 기능들이다. 모두 환경변수로 켤 수 있으며, 기본값은 모두 off — 기존 동작과 100% 호환된다.
Prometheus 메트릭 (/metrics)
| 환경변수 | 기본값 | 설명 |
|---|---|---|
METRICS_ENABLED |
false |
true 일 때 GET /metrics 활성화. prometheus-client 설치 필요. |
METRICS_REQUIRE_AUTH |
false |
true 일 때 /metrics 호출에도 Authorization: Bearer $API_KEY 강제. public 배포에서는 필수. |
활성화:
pip install "hwan-email-service[http]" # prometheus-client 포함
export API_KEY=$(openssl rand -hex 32)
METRICS_ENABLED=true METRICS_REQUIRE_AUTH=true python -m email_service
curl -H "Authorization: Bearer $API_KEY" http://127.0.0.1:8000/metrics
METRICS_ENABLED=false 이거나 prometheus-client 가 설치되지 않은 환경에서는 /metrics route가 OpenAPI에 보여도 404 metrics disabled 를 반환한다.
노출되는 시리즈:
email_send_total{result, error_code}— Counter.result는success/failure,error_code는crlf_in_header/smtp_auth_failed/smtp_connection/smtp_timeout/smtp_transient/recipient_refused/starttls_unsupported/unknown(success시 빈 문자열).email_send_duration_seconds— Histogram. SMTP 호출 한 건의 종단 지연 (초).email_send_active— Gauge. 현재 처리 중인 발송 건수.email_retry_attempts_total{reason}— Counter. 재시도 시도 횟수 (Phase 4max_retries > 0일 때).email_webhook_failed_total— Counter. webhook 콜백 전달이 최종 실패한 건수.
샘플 출력:
# HELP email_send_total Total email send attempts
# TYPE email_send_total counter
email_send_total{result="success",error_code=""} 42.0
email_send_total{result="failure",error_code="smtp_connection"} 3.0
email_send_duration_seconds_bucket{le="0.5"} 41.0
권장 알람 (Prometheus):
- alert: EmailFailureRateHigh
expr: rate(email_send_total{result="failure"}[5m]) > 0.05
for: 10m
annotations:
summary: "email-service failure rate above 5% for 10 minutes"
구조화 로그 (JSON)
| 환경변수 | 기본값 | 설명 |
|---|---|---|
EMAIL_SERVICE_LOG_FORMAT |
text |
json 일 때 python-json-logger 로 JSON 한 줄 로그 출력. |
EMAIL_SERVICE_DEBUG |
0 |
1 일 때 smtplib.set_debuglevel(1) 활성화 (개발 전용). |
PII 안전성: 수신자 이메일 주소는 절대 평문으로 로그에 남지 않는다. 모든 발송 로그는 SHA-256 해시 앞 8자(to_hash) 로 표기되며, error_code·duration_ms·message_id·request_id 가 함께 기록된다.
⚠️ 보안 주의:
EMAIL_SERVICE_DEBUG=1은smtplib의 디버그 출력을 stderr 로 보내며, 여기에는AUTH PLAIN <base64>라인이 포함된다 (즉, 비밀번호가 base64 로 노출). 절대 운영 환경에서는 켜지 말 것. 표준 라이브러리 한계상 이 라인을 안전하게 마스킹할 수 없다.
분산 트레이싱 (X-Request-ID)
모든 요청은 X-Request-ID 헤더를 echo 하며, 헤더가 없으면 UUID 가 자동 발급된다. 이 ID 는 SMTP 발송 로그까지 그대로 전파되어, 게이트웨이 → email-service → SMTP 의 풀 트레이스를 단일 ID 로 grep 할 수 있다.
curl -H "Authorization: Bearer $API_KEY" \
-H "X-Request-ID: trace-abc-123" \
-X POST http://127.0.0.1:8000/send \
-d '{"to":"u@t.com","subject":"hi","html_body":"<p>x</p>"}'
# Response includes: X-Request-ID: trace-abc-123
SMTP 재시도 (max_retries)
라이브러리 모드에서만 사용. 기본값 0 으로 기존 동작과 호환된다.
from email_service import SmtpSender, SmtpConfig
sender = SmtpSender(
SmtpConfig(host="smtp.gmail.com", port=587, user="u", password="p"),
max_retries=2, # 1 회 시도 + 2 회 재시도 = 최대 3 회
backoff_seconds=(1, 5, 25), # 지수 백오프; 마지막 값으로 클램프
)
재시도 대상: SMTPServerDisconnected, SMTPConnectError, socket.timeout, SMTP 4xx 응답. 5xx 영구 실패와 부분 거부(partial refusal) 는 즉시 반환되어 같은 수신자에게 중복 발송되지 않는다. Message-ID 는 재시도 전체에서 동일하게 유지된다 (MTA dedup).
각 재시도는 email_retry_attempts_total{reason} 카운터를 증가시킨다.
Test mode — .eml 캡처 (EMAIL_TEST_CAPTURE_DIR)
환경변수 EMAIL_TEST_CAPTURE_DIR 가 set 되면 SMTP 호출을 건너뛰고 메시지를 <message_id>.eml 파일로 디렉토리에 저장한다. 통합 테스트에서 SMTP 없이 메일 내용을 검증할 때 사용.
EMAIL_TEST_CAPTURE_DIR=/tmp/outbox pytest tests/
전체 예시는 examples/integration_test_with_capture.py 참고.
dry_run (HTTP X-Dry-Run: true) 과 다름: dry-run 은 페이로드 검증 only (메시지 빌드 안 함), capture mode 는 실제 MIME 메시지를 생성하여 디스크에 저장 (헤더·바디 검증 가능).
Webhook 콜백 (비동기 발송 통지)
webhook_url 을 POST /send (또는 /send/magic-link, /send/otp) 의 body 에 포함하면 발송이 백그라운드로 처리되고 응답은 즉시 {"sent": false, "status": "accepted"} 로 반환된다. 최종 결과는 다음 페이로드로 webhook URL 에 POST 된다:
{
"message_id": "<...@host>",
"status": "delivered",
"error_code": null,
"refused": [],
"sent_at": "2026-05-15T10:00:00+00:00",
"attempts": 1
}
webhook_secret 도 함께 보내면 V2 timestamp 서명과 legacy V1 body-only 서명이 함께 포함된다. 새 receiver는 V2를 먼저 검증해야 한다.
webhook_secret 도 API_KEY 와 동일하게 긴 랜덤 값으로 생성하고 secret store 또는 git 밖의 환경변수로 관리한다.
수신자 측 검증 (Python, V2 권장):
import hashlib
import hmac
import time
body = await request.body()
timestamp = request.headers["X-Email-Service-Timestamp"]
signature = request.headers["X-Email-Service-Signature-V2"]
now = int(time.time())
if abs(now - int(timestamp)) > 300:
raise ValueError("stale webhook timestamp")
signed = timestamp.encode("ascii") + b"." + body
expected = "sha256=" + hmac.new(SECRET.encode(), signed, hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature, expected):
raise ValueError("bad webhook signature")
Legacy X-Email-Service-Signature 는 HMAC(secret, body) 형식이라 캡처된 요청을 시간 제한 없이 replay 할 수 있다. 기존 receiver 호환용으로만 사용하고, public receiver는 timestamp window를 검증하는 V2로 마이그레이션한다.
Webhook 전달 자체는 (1s, 2s, 5s) 백오프로 최대 3 회 재시도된다. 최종 실패 시 email_webhook_failed_total 메트릭이 증가하며 발송 자체에는 영향을 주지 않는다 (이미 발송 완료).
로컬 webhook 테스트
docker-compose.dev.yml 에 포함된 webhook-sink (httpbin) 서비스로 페이로드를 즉시 확인할 수 있다:
docker compose -f docker-compose.dev.yml up -d
export WEBHOOK_SECRET="$(openssl rand -hex 32)"
curl -H "Authorization: Bearer $API_KEY" \
-X POST http://127.0.0.1:8000/send \
-H "Content-Type: application/json" \
-d "{\"to\":\"u@t.com\",\"subject\":\"hi\",\"html_body\":\"<p>x</p>\",
\"webhook_url\":\"http://webhook-sink/post\",\"webhook_secret\":\"$WEBHOOK_SECRET\"}"
# httpbin echoes the received POST at http://127.0.0.1:8080/post
docker compose -f docker-compose.dev.yml logs webhook-sink
PowerShell에서 실행할 때는 먼저 $env:WEBHOOK_SECRET = -join ((1..32) | ForEach-Object { '{0:x2}' -f (Get-Random -Minimum 0 -Maximum 256) }) 로 생성한 뒤 payload의 webhook_secret 값에 $env:WEBHOOK_SECRET 를 넣는다.
사용 방식
| 모드 | 설치 | 실행 | 용도 |
|---|---|---|---|
| 라이브러리 | pip install hwan-email-service |
Python 코드에서 import email_service |
같은 프로세스 안에서 메일 발송 |
| HTTP 서비스 | pip install "hwan-email-service[http]" |
python -m email_service |
다른 서비스가 REST 로 호출 |
설치 명령 전체 예시:
# 라이브러리로만 사용 (PyPI)
pip install hwan-email-service
# HTTP 서비스로 띄워서 사용 (PyPI)
pip install "hwan-email-service[http]"
# 아직 PyPI 에 게시 안 된 버전을 미리 받고 싶을 때 (git 직접 설치)
# (git 설치 시에는 PyPI distribution 이름과 무관하게 동작한다)
pip install git+https://github.com/hwan96-ai/email-service.git
pip install "hwan-email-service[http] @ git+https://github.com/hwan96-ai/email-service.git"
요구 사항: Python 3.10+.
30초 안에 첫 메일 보내기
python -m email_service test 서브커맨드가 환경변수만으로 SMTP 설정을 검증하고 테스트 메일 한 통을 즉시 발송한다. 발송 결과가 SendResult 형태로 stdout 에 떨어진다.
# 1) 설치
pip install hwan-email-service
# 2) 환경변수 (Gmail 예시 — 앱 비밀번호 권장)
export SMTP_HOST=smtp.gmail.com
export SMTP_USER=sender@gmail.com
export SMTP_PASSWORD=app-password
# API_KEY 는 test 서브커맨드에서는 필요 없음 (HTTP 서버 모드 전용)
# 3) 발송
python -m email_service test --to me@example.com
# → SendResult(sent=True, error_code=None, ..., message_id='<...@host>')
# exit 0 on success, exit 1 on failure (with error_code printed)
자세한 옵션: python -m email_service test --help.
빠른 시작
HTTP 서비스로 띄워 curl 로 테스트
# 1) 설치
pip install "hwan-email-service[http]"
# 2) 환경변수 설정 (최소)
export SMTP_HOST=smtp.gmail.com
export SMTP_USER=sender@gmail.com
export SMTP_PASSWORD=app-password
export API_KEY=$(openssl rand -hex 32) # 임의의 긴 비밀문자열
# 3) 기동
python -m email_service
# → INFO: Uvicorn running on http://127.0.0.1:8000
# 4) 호출 (다른 터미널에서)
curl -X POST http://127.0.0.1:8000/send \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"to":"user@example.com","subject":"Hi","html_body":"<p>Hello</p>"}'
# → {"sent":true}
Python 라이브러리로 한 줄 발송
from email_service import SmtpSender
from email_service.sender import SmtpConfig
sender = SmtpSender(SmtpConfig(
host="smtp.gmail.com", user="sender@gmail.com", password="app-password",
))
sender.send("user@example.com", "Hi", "<p>Hello</p>")
Docker 로 실행
다른 서비스가 REST 로 호출하는 운영 시나리오라면 Dockerfile + docker-compose.yml + .env.example 이 함께 제공된다. Docker 이미지는 Python 3.12 slim 기반이며, 로컬 개발(Python 3.10+) 과 별개이다.
1) 환경변수 파일 준비
cp .env.example .env
# 에디터로 .env 열어 SMTP_HOST / SMTP_USER / SMTP_PASSWORD / API_KEY 채움
# API_KEY 생성: openssl rand -hex 32
.env 는 .gitignore 되어 있다. 절대 커밋하지 말 것.
2) 빌드 & 기동
docker compose up -d --build
- 이미지:
python:3.12-slim베이스, uid10001의 non-rootapp유저로 실행. - 컨테이너 내부
HOST=0.0.0.0,PORT=8000(Dockerfile/compose 에 기본 설정). - 호스트
8000↔ 컨테이너8000포트 매핑 (docker-compose.yml의ports:). docker-compose.yml에/health헬스체크 포함 —docker compose ps에healthy상태가 뜨며, 기동 후 약 10 초 이내에 초록색으로 전환된다.
3) 호출
curl -X POST http://127.0.0.1:8000/send \
-H "Authorization: Bearer $(grep ^API_KEY .env | cut -d= -f2-)" \
-H "Content-Type: application/json" \
-d '{"to":"user@example.com","subject":"Hi","html_body":"<p>Hello</p>"}'
4) 로그 / 중지
docker compose logs -f email-service # 로그 추적
docker compose down # 정지 및 컨테이너 제거
운영 배포 참고
docker-compose.yml은 편의를 위해ports: "8000:8000"으로 호스트에 직접 공개한다. 공용 인터넷에는 노출 금지. 내부망 / VPC / 방화벽 안에 두고 앞단에 Reverse Proxy (nginx, Traefik 등) + TLS 종단을 구성한다.- 같은 Docker 네트워크 안의 다른 컨테이너만 호출하면 되는 경우
ports:를 제거하고expose: ["8000"]로 바꾸면 호스트 포트가 열리지 않는다.
로컬 메일 테스트 (Mailpit)
실제 메일을 발송하지 않고 로컬에서 발송 결과를 눈으로 확인하려면 docker-compose.dev.yml 을 쓴다. Mailpit 이 SMTP 서버 + 웹 UI 를 같이 제공한다.
# 1) .env 준비: docker-compose.dev.yml 은 API_KEY 를 .env 에서 읽는다.
API_KEY=$(openssl rand -hex 32)
printf "API_KEY=%s\n" "$API_KEY" > .env
# 2) 빌드 & 기동 (email-service + mailpit)
docker compose -f docker-compose.dev.yml up -d --build
# 3) 헬스체크
curl http://127.0.0.1:8000/health
# → {"status":"ok"}
# 4) 메일 발송
curl -X POST http://127.0.0.1:8000/send/otp \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"to":"user@example.com","user_name":"홍길동","code":"482901"}'
PowerShell:
# 1) .env 준비
$Bytes = [byte[]]::new(32)
[System.Security.Cryptography.RandomNumberGenerator]::Fill($Bytes)
$ApiKey = [Convert]::ToHexString($Bytes).ToLower()
Set-Content .env "API_KEY=$ApiKey"
# 2) 빌드 & 기동
docker compose -f docker-compose.dev.yml up -d --build
# 3) 헬스체크
curl.exe http://127.0.0.1:8000/health
# 4) 메일 발송
curl.exe -X POST http://127.0.0.1:8000/send/otp `
-H "Authorization: Bearer $ApiKey" `
-H "Content-Type: application/json" `
-d '{\"to\":\"user@example.com\",\"user_name\":\"홍길동\",\"code\":\"482901\"}'
발송된 메일은 Mailpit 웹 UI 에서 확인한다:
- Mailpit UI: http://127.0.0.1:8025
- Mailpit SMTP:
mailpit:1025(컨테이너 내부),127.0.0.1:1025(호스트)
개발용 compose 는 SMTP 설정을 Mailpit/no-auth 값으로 제공하지만, API_KEY 는 .env 에서 반드시 읽는다. .env 는 git에 커밋하지 않는다.
| 변수 | 값 | 비고 |
|---|---|---|
SMTP_HOST |
mailpit |
|
SMTP_PORT |
1025 |
|
SMTP_USER / SMTP_PASSWORD |
빈 값 | Mailpit 은 SMTP AUTH 가 필요 없다 |
SMTP_USE_TLS |
false |
|
API_KEY |
.env 에서 필수 |
openssl rand -hex 32 등으로 생성 |
MAGIC_LINK_BASE_URL |
http://localhost:3000 |
정지:
docker compose -f docker-compose.dev.yml down
HTTP API 사용법
엔드포인트
POST 요청은 모두 Authorization: Bearer $API_KEY 헤더가 필요하다. 성공 시 200 {"sent": true}. GET /health 는 인증이 필요 없다.
| 메서드 | 경로 | 요청 body | 인증 | 설명 |
|---|---|---|---|---|
GET |
/health |
— | 불필요 | 헬스체크. 200 {"status": "ok"} 반환. 로드밸런서/Docker healthcheck 용 |
POST |
/send |
to, subject, html_body, text_body?, cc?, bcc? |
필요 | 일반 메일 |
POST |
/send/magic-link |
to, user_name, token |
필요 | 매직링크 메일 (MAGIC_LINK_BASE_URL 필요) |
POST |
/send/otp |
to, user_name, code |
필요 | OTP 메일 |
에러 코드
| 코드 | 의미 |
|---|---|
401 |
API 키 누락/오류 |
422 |
필수 필드 누락, 또는 헤더 (to/subject/cc/bcc) 에 CRLF 포함 (헤더 인젝션 차단) |
502 |
SMTP 연결 또는 발송 실패 |
503 |
/send/magic-link 호출 시 MAGIC_LINK_BASE_URL 미설정 |
curl 호출 예시
일반 메일 (cc/bcc 포함):
curl -X POST http://127.0.0.1:8000/send \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to":"user@example.com",
"subject":"Hi",
"html_body":"<p>Hello</p>",
"text_body":"Hello",
"cc":["cc@example.com"],
"bcc":["bcc@example.com"]
}'
매직링크 메일:
curl -X POST http://127.0.0.1:8000/send/magic-link \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"to":"user@example.com","user_name":"홍길동","token":"abc123"}'
OTP 메일:
curl -X POST http://127.0.0.1:8000/send/otp \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"to":"user@example.com","user_name":"홍길동","code":"482901"}'
Python 클라이언트 SDK
email-service[http] 로 설치하면 EmailServiceClient 를 import 해서 바로 쓸 수 있다. Bearer 헤더 자동 부착, dry-run 헤더 자동 전환, 4xx/5xx 시 예외 발생까지 담당한다.
import os
from email_service.client import EmailServiceClient
with EmailServiceClient(
"http://email-service:8000",
os.environ["EMAIL_SERVICE_API_KEY"],
) as client:
client.health()
# 일반 메일
client.send(
to="user@example.com",
subject="Hi",
html_body="<p>Hello</p>",
text_body="Hello",
cc=["cc@example.com"],
bcc=["bcc@example.com"],
)
# 매직링크 / OTP
client.send_magic_link("user@example.com", "홍길동", "abc123")
client.send_otp("user@example.com", "홍길동", "482901")
생성자: EmailServiceClient(base_url, api_key, timeout=10.0). context manager 를 지원하며, 직접 close() 를 호출해도 된다. 내부적으로 httpx.Client 를 사용하므로 http extras 가 필요하다.
Dry-run
메일을 실제로 발송하지 않고 payload 가 유효한지만 확인하고 싶을 때 X-Dry-Run 헤더를 쓴다.
- 헤더 값:
true/1/yes(대소문자 무시) 는 dry-run 으로 처리된다. - 적용 대상:
/send,/send/magic-link,/send/otp - 동작: API Key 인증과 Pydantic validation 은 그대로 수행되지만, SMTP 는 호출되지 않는다.
- 응답:
200 {"sent": false, "dry_run": true, "message": "Email payload is valid"}
curl -X POST http://127.0.0.1:8000/send/otp \
-H "Authorization: Bearer $API_KEY" \
-H "X-Dry-Run: true" \
-H "Content-Type: application/json" \
-d '{"to":"user@example.com","user_name":"홍길동","code":"482901"}'
# → {"sent":false,"dry_run":true,"message":"Email payload is valid"}
SDK 에서는 dry_run=True 만 넘기면 된다.
client.send_otp("user@example.com", "홍길동", "482901", dry_run=True)
직접 httpx 로 호출
SDK 를 쓰지 않고 raw 로 호출하는 예시.
import os, httpx
client = httpx.Client(
base_url=os.environ["EMAIL_SERVICE_URL"], # 예: http://email-service:8000
headers={"Authorization": f"Bearer {os.environ['EMAIL_API_KEY']}"},
timeout=10,
)
resp = client.post("/send/otp", json={
"to": "user@example.com",
"user_name": "홍길동",
"code": "482901",
})
resp.raise_for_status() # 401/422/502/503 → 예외
Node.js (fetch)
언어 무관하게 REST 로 호출 가능. Node 18+ 기본 내장 fetch 예시.
const resp = await fetch("http://email-service:8000/send/otp", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.EMAIL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
to: "user@example.com",
user_name: "홍길동",
code: "482901",
}),
});
if (!resp.ok) {
throw new Error(`email-service failed: ${resp.status}`);
}
console.log(await resp.json()); // { sent: true }
Python 라이브러리로 사용하기
패키지의 공개 API:
from email_service import SmtpSender, MagicLinkNotifier, OTPNotifier, TemplateNotifier
from email_service.sender import SmtpConfig
from email_service.notifiers import Notifier # 커스텀 Notifier 만들 때
SmtpConfig
SMTP 연결 설정. 단순 dataclass.
from email_service.sender import SmtpConfig
config = SmtpConfig(
host="smtp.gmail.com", # 기본: smtp.gmail.com
port=587, # 기본: 587
user="sender@gmail.com", # 로그인 계정
password="app-password", # 앱 비밀번호
from_addr="", # 발신자 주소 (비우면 user 와 동일)
use_tls=True, # STARTTLS 사용 여부 (기본: True)
timeout=10, # 연결 타임아웃 초 (기본: 10)
)
SmtpSender
HTML 이메일을 발송하는 저수준 sender.
from email_service import SmtpSender
from email_service.sender import SmtpConfig
sender = SmtpSender(SmtpConfig(
host="smtp.gmail.com",
user="sender@gmail.com",
password="app-password",
))
success = sender.send(
to="recipient@example.com",
subject="제목",
html_body="<h1>본문</h1>",
text_body="본문", # 선택: plain-text 대체본 (multipart/alternative 의 fallback)
cc=["cc@example.com"], # 선택
bcc=["bcc@example.com"], # 선택
)
# 반환: True (성공) / False (실패, 로그에 에러 기록)
# 헤더 값(to/subject/from/cc/bcc)에 CR/LF가 포함되면 발송 거부 (CRLF 인젝션 차단)
MagicLinkNotifier
비밀번호 설정 매직링크 이메일.
from email_service import SmtpSender, MagicLinkNotifier
from email_service.sender import SmtpConfig
sender = SmtpSender(SmtpConfig(
host="smtp.gmail.com", user="noreply@mycompany.com", password="app-password",
))
notifier = MagicLinkNotifier(
sender,
base_url="https://myapp.com", # 필수: 프론트엔드 URL
path="/set-password", # 선택: 링크 경로 (기본)
subject_prefix="[MyApp] ", # 선택: 메일 제목 접두어
expire_minutes=15, # 선택: 본문에 표시할 유효시간 (기본: 15)
)
# payload = 토큰 문자열. 토큰은 URL 인코딩되어 링크에 포함된다.
notifier.send("user@example.com", "홍길동", "abc123token")
# → 본문에 https://myapp.com/set-password?token=abc123token 링크 삽입
OTPNotifier
일회용 인증코드 이메일.
from email_service import SmtpSender, OTPNotifier
from email_service.sender import SmtpConfig
sender = SmtpSender(SmtpConfig(
host="smtp.gmail.com", user="noreply@mycompany.com", password="app-password",
))
notifier = OTPNotifier(sender, subject_prefix="[MyApp] ", expire_minutes=5)
# payload = OTP 코드 문자열
notifier.send("user@example.com", "홍길동", "482901")
# → 본문에 482901 코드를 큰 글씨로 표시
TemplateNotifier
임의의 제목/HTML 템플릿으로 메일을 렌더링해 발송. (user_name, payload) 고정 시그니처가 맞지 않는 케이스용.
from email_service import SmtpSender, TemplateNotifier
from email_service.sender import SmtpConfig
sender = SmtpSender(SmtpConfig(host="smtp.gmail.com", user="noreply@x.com", password="..."))
notifier = TemplateNotifier(
sender,
subject="[MyApp] {order_id} 주문이 접수되었습니다",
html_template="<p>{user_name}님, 주문 {order_id}번이 접수되었습니다. 금액: {amount}원</p>",
text_template="{user_name}님, 주문 {order_id}번 접수. 금액: {amount}원", # 선택
autoescape=True, # 기본 True — HTML 본문의 context 값만 html.escape 처리
)
notifier.send(
"user@example.com",
user_name="홍길동", order_id="A-1024", amount="45,000",
)
- 템플릿은
str.format문법. 플레이스홀더 ({key}) 는send(**context)의 키워드와 매칭. autoescape=True에서 HTML 템플릿의 context 값만 이스케이프된다. subject/text_template 은 HTML 컨텍스트가 아니므로 이스케이프하지 않음.
커스텀 Notifier
Notifier 를 상속하면 새 템플릿을 쉽게 추가할 수 있다.
from html import escape
from email_service.notifiers import Notifier
from email_service.sender import SmtpSender
class WelcomeNotifier(Notifier):
def __init__(self, sender: SmtpSender, *, company_name: str = ""):
super().__init__(sender)
self._company = company_name
def send(self, to_email: str, user_name: str, payload: str) -> bool:
safe_company = escape(self._company)
safe_name = escape(user_name)
safe_payload = escape(payload)
subject = f"{self._company} 가입을 환영합니다"
html = f"<h1>{safe_name}님, 환영합니다!</h1><p>{safe_company}: {safe_payload}</p>"
return self._sender.send(to_email, subject, html)
커스텀 HTML 템플릿에서 user_name, payload, 주문명, 조직명처럼 호출자나 사용자에게서 온 값은 HTML에 넣기 전에 반드시 escape 한다. TemplateNotifier(autoescape=True) 는 HTML context 값을 자동 escape 하지만, 직접 만든 Notifier 는 작성자가 책임져야 한다.
환경변수
HTTP 서비스 모드 (python -m email_service) 에서 사용한다. 라이브러리 모드에서는 무관하다.
필수
| 이름 | 설명 |
|---|---|
SMTP_HOST |
SMTP 서버 호스트 (예: smtp.gmail.com) |
API_KEY |
클라이언트가 Authorization: Bearer 로 보내는 공유 비밀 키 |
필수 환경변수가 비어 있으면 기동 즉시 RuntimeError 로 실패한다 (fail-fast). SMTP_USER / SMTP_PASSWORD 는 Mailpit, MailHog, 사내 no-auth relay처럼 SMTP AUTH가 없는 서버를 지원하기 위해 선택값이다.
선택
| 이름 | 기본값 | 설명 |
|---|---|---|
SMTP_PORT |
587 |
SMTP 포트 |
SMTP_USER |
"" |
SMTP 로그인 계정. Mailpit 같은 no-auth SMTP에서는 비워 둔다 |
SMTP_PASSWORD |
"" |
SMTP 비밀번호 / 앱 비밀번호. Mailpit 같은 no-auth SMTP에서는 비워 둔다 |
SMTP_FROM |
SMTP_USER 와 동일 |
발신자 주소 |
SMTP_USE_TLS |
true |
STARTTLS 사용 여부 (false 로 설정 시 비활성) |
MAGIC_LINK_BASE_URL |
— | /send/magic-link 엔드포인트 활성화용 프론트엔드 URL. 미설정 시 해당 엔드포인트는 503 반환 |
HOST |
127.0.0.1 |
uvicorn 바인딩 호스트. 로컬 python -m email_service 기본값은 127.0.0.1 (루프백). Docker 실행 시에는 컨테이너 밖에서 접근 가능해야 하므로 0.0.0.0 을 사용한다 (제공된 Dockerfile / docker-compose.yml 이 이미 0.0.0.0 으로 설정) |
PORT |
8000 |
uvicorn 바인딩 포트 |
보안 및 운영 주의사항
- 내부망 전제 — 로컬
python -m email_service기본HOST=127.0.0.1. 제공되는docker-compose.yml은 편의를 위해ports: "8000:8000"으로 호스트에 공개하지만, 운영에서 이 포트를 공용 인터넷에 직접 노출하지 말 것. 내부망·VPC·방화벽 뒤에 두고 앞단에 Reverse Proxy / TLS 종단 / WAF 를 구성한다. 외부 완전 차단이 필요하면docker-compose.yml의ports:를expose:로 바꾸면 같은 compose 네트워크의 다른 컨테이너만 접근하게 된다. - 단일 API 키 — 모든 호출자가 같은 키를 공유한다. 호출자별 구분이 필요하면 키를 분리하거나 리버스 프록시 레벨에서 인증을 추가한다.
- CRLF 헤더 인젝션 —
SmtpSender와 HTTP API Pydantic 모델 양쪽에서to/subject/from/cc/bcc의 CR/LF 를 차단한다. 사용자 입력을 그대로 넘겨도 안전하다. - HTML 이스케이프 — 내장 Notifier 들은 user_name, token, code, context 를 기본적으로
html.escape처리한다. HTML 구조 자체를 사용자 입력으로 만들지는 말 것. - 자격증명 관리 —
SMTP_PASSWORD,API_KEY는 .env / secret store 등 외부에 보관하고 저장소에 커밋하지 않는다. - 운영 API key — 짧거나 추측 가능한 키를 쓰지 말고, 길고 랜덤한 값을 배포 플랫폼 secret으로 주입한다. 앱은 non-empty 여부만 fail-fast로 확인하므로 강도와 회전은 운영자가 책임진다.
- 인증 실패 rate limit — 앱 내부 rate limit은 인증된 요청 기준이다. 잘못된 Bearer 토큰 반복 시도는 reverse proxy/API gateway/WAF에서 제한한다.
- Metrics 공개 금지 —
METRICS_ENABLED=true로 운영할 때는METRICS_REQUIRE_AUTH=true를 함께 설정하고, 가능하면 내부망에서만 scrape 한다. - 의존성 고정 — 제공 Dockerfile은 간단한 예시라
pip install ".[http]"로 설치한다. 운영 이미지는 CI에서 검증한 constraints/lock 파일로 transitive dependency를 고정해 빌드하는 것을 권장한다. - OpenAPI 스펙 — 기본 활성화된
/docs(Swagger UI),/openapi.json에서 조회 가능. 운영에서 불필요하다면 외부 노출 전에 앞단에서 차단한다.
Demo / screenshots
이 저장소에는 아직 실제 스크린샷 asset을 커밋하지 않았다. 공개 README에 이미지를 추가할 때는 실제 실행 화면만 사용한다.
권장 캡처:
- Swagger/OpenAPI docs:
http://127.0.0.1:8000/docs /send/otp성공 요청과{"sent": true, ...}응답docker compose -f docker-compose.dev.yml ps에서email-service-dev와mailpit이 healthy/running인 화면- Mailpit UI(
http://127.0.0.1:8025)에서 실제로 수신된 OTP 또는 magic-link 이메일 preview
개발 및 테스트
git clone https://github.com/hwan96-ai/email-service.git
cd email-service
# 개발 의존성 설치 (pytest, httpx)
pip install -e ".[dev]"
# HTTP 모드 테스트까지 같이 돌리려면 http extras 도
pip install -e ".[dev,http]"
# 전체 테스트
python -m pytest tests/ -v
# 일부만
python -m pytest tests/test_email_service.py -v # 코어 유닛 테스트
python -m pytest tests/test_api.py -v # HTTP API 통합 테스트
테스트는 실제 SMTP 서버에 연결하지 않는다 (smtplib.SMTP 를 mock 처리).
프로젝트 구조
email-service/
├── email_service/
│ ├── __init__.py # 공개 API re-export (SmtpSender, *Notifier)
│ ├── __main__.py # `python -m email_service` 진입점 (uvicorn 기동)
│ ├── api.py # FastAPI 앱 (create_app) + Pydantic 모델 + 인증 + dry-run
│ ├── client.py # EmailServiceClient — HTTP SDK (httpx 기반)
│ ├── sender.py # SmtpConfig, SmtpSender — SMTP 발송 코어
│ └── notifiers.py # Notifier(ABC), MagicLinkNotifier, OTPNotifier, TemplateNotifier
├── tests/
│ ├── test_email_service.py # sender + notifier 유닛 테스트
│ ├── test_api.py # HTTP API 통합 테스트
│ └── test_client.py # EmailServiceClient SDK 테스트
├── docker-compose.yml # 운영용 compose
├── docker-compose.dev.yml # 개발용 compose (Mailpit 포함)
├── pyproject.toml # 패키지 메타 + optional extras (dev, http)
└── README.md
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file hwan_email_service-0.4.1.tar.gz.
File metadata
- Download URL: hwan_email_service-0.4.1.tar.gz
- Upload date:
- Size: 97.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a3bae5ceb8302a59c5bcbd7a75c876267252dbb8c69113197a99523b03fe8dcb
|
|
| MD5 |
7eff5ed8a40583152f877d595d8a154c
|
|
| BLAKE2b-256 |
a65796e2de8f02289c9011218f58b98a2d6e4729b612f31ec0ba9d779bbfa344
|
Provenance
The following attestation bundles were made for hwan_email_service-0.4.1.tar.gz:
Publisher:
release.yml on hwan96-ai/email-service
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hwan_email_service-0.4.1.tar.gz -
Subject digest:
a3bae5ceb8302a59c5bcbd7a75c876267252dbb8c69113197a99523b03fe8dcb - Sigstore transparency entry: 1592358454
- Sigstore integration time:
-
Permalink:
hwan96-ai/email-service@294c23865457715959981a3e5de4f4b70620b24b -
Branch / Tag:
refs/heads/master - Owner: https://github.com/hwan96-ai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@294c23865457715959981a3e5de4f4b70620b24b -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file hwan_email_service-0.4.1-py3-none-any.whl.
File metadata
- Download URL: hwan_email_service-0.4.1-py3-none-any.whl
- Upload date:
- Size: 47.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
053be4c7095f860bd427d663f7a2d320ac0889d425ecd376922fe2e2c894bd04
|
|
| MD5 |
add6d92e95f7385b62b4c3eace7fd54b
|
|
| BLAKE2b-256 |
f4b922a8ca2f77b7ab6d9120ac4d650dd13f99d29b2ec418d6c3e37965a85a75
|
Provenance
The following attestation bundles were made for hwan_email_service-0.4.1-py3-none-any.whl:
Publisher:
release.yml on hwan96-ai/email-service
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hwan_email_service-0.4.1-py3-none-any.whl -
Subject digest:
053be4c7095f860bd427d663f7a2d320ac0889d425ecd376922fe2e2c894bd04 - Sigstore transparency entry: 1592358463
- Sigstore integration time:
-
Permalink:
hwan96-ai/email-service@294c23865457715959981a3e5de4f4b70620b24b -
Branch / Tag:
refs/heads/master - Owner: https://github.com/hwan96-ai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@294c23865457715959981a3e5de4f4b70620b24b -
Trigger Event:
workflow_dispatch
-
Statement type: