Skip to content

Commit ad84cf0

Browse files
authored
[25.06.05 / TASK-184] Feature - 주간 뉴스레터 발송을 위한 mail module 추가 (#30)
* feature: boto3, botocore 라이브러리 추가 * feature: AWS 관련 환경변수 추가 * feature: mail module 및 SES Client 추가 * modify: base_client와의 시그니처 통일 * modify: 불필요한 클래스 변수 선언 삭제 * refactor: 중복되는 Common Error 핸들링을 메서드로 분리 * refactor: 불필요한 elif, else문 삭제 * modify: typing 관련 빌트인으로 수정 및 import 경로 수정 * refactor: JSON 직렬화 예외 처리 추가 * refactor: AWS Credentials를 dataclass로 추가 및 메시지 객체 분리 * refactor: except depth 정리 * delete: 템플릿 생성 & 삭제 메서드 삭제 * delete: send_templated_email 및 템플릿 관련 삭제 메일 템플릿은 내부 상수로 관리하는 것으로 정리하여 SES의 Template 리소스를 사용하던 기존의 메서드 삭제 * modify: 사용하지 않는 import 삭제 * refactor: UnexpectedClientError 추가
1 parent 92eeb82 commit ad84cf0

File tree

12 files changed

+631
-22
lines changed

12 files changed

+631
-22
lines changed

.env.sample

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,8 @@ SENTRY_TRACES_SAMPLE_RATE=0.2
3131

3232
# LLM
3333
OPENAI_API_KEY=sk-proj-...
34+
35+
# AWS SES
36+
AWS_ACCESS_KEY_ID=ID
37+
AWS_SECRET_ACCESS_KEY=AccEssKeY
38+
AWS_REGION=ap-northeast-2

.github/workflows/test-ci.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: Test CI
33
on:
44
workflow_dispatch:
55
push:
6-
branches: ["main"]
6+
branches: ['main']
77
pull_request:
88
branches:
99
- main
@@ -71,6 +71,9 @@ jobs:
7171
echo "SENTRY_ENVIRONMENT=gitaction" >> .env
7272
echo "SENTRY_TRACES_SAMPLE_RATE=0.2" >> .env
7373
echo "OPENAI_API_KEY= sk-proj" >> .env
74+
echo "AWS_ACCESS_KEY_ID=ID" >> .env
75+
echo "AWS_SECRET_ACCESS_KEY=AccEssKeY" >> .env
76+
echo "AWS_REGION=ap-northeast-2" >> .env
7477
echo "AES_KEY_0=${{ secrets.AES_KEY_0 }}" >> .env
7578
echo "AES_KEY_1=${{ secrets.AES_KEY_1 }}" >> .env
7679
echo "AES_KEY_2=${{ secrets.AES_KEY_2 }}" >> .env

modules/mail/__init__.py

Whitespace-only changes.

modules/mail/base_client.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from abc import ABC, abstractmethod
2+
from typing import Any, Generic, TypeVar
3+
4+
from modules.mail.schemas import EmailMessage
5+
6+
# 클라이언트 타입을 위한 제네릭 타입 변수
7+
T = TypeVar("T")
8+
9+
class MailClient(ABC, Generic[T]):
10+
"""
11+
모든 메일 클라이언트를 위한 추상 기본 클래스로 Lazy Initialization 패턴을 따릅니다.
12+
모든 메일 서비스 구현을 위한 템플릿을 제공합니다.
13+
"""
14+
15+
@classmethod
16+
@abstractmethod
17+
def get_client(cls, credentials: dict[str, Any]) -> "MailClient[T]":
18+
"""
19+
메일 클라이언트를 가져오거나 초기화합니다.
20+
21+
Args:
22+
credentials: 서비스 인증 정보 (필수)
23+
24+
Returns:
25+
초기화된 클라이언트 인스턴스
26+
27+
Raises:
28+
AuthenticationError: 인증 정보가 유효하지 않은 경우
29+
ConnectionError: 서비스 연결에 실패한 경우
30+
"""
31+
pass
32+
33+
@classmethod
34+
@abstractmethod
35+
def _initialize_client(cls, credentials: dict[str, Any]) -> T:
36+
"""
37+
특정 메일 클라이언트를 초기화하는 추상 메서드.
38+
각 구체적인 하위 클래스에서 구현되어야 합니다.
39+
40+
Args:
41+
credentials: 서비스 인증 정보 (필수)
42+
43+
Returns:
44+
초기화된 클라이언트 인스턴스
45+
46+
Raises:
47+
AuthenticationError: 인증 정보가 유효하지 않은 경우
48+
ConnectionError: 서비스 연결에 실패한 경우
49+
"""
50+
pass
51+
52+
@abstractmethod
53+
def send_email(self, message: EmailMessage) -> str:
54+
"""
55+
이메일을 발송합니다.
56+
57+
Args:
58+
message: 발송할 이메일 메시지 객체
59+
60+
Returns:
61+
발송한 메시지 ID
62+
63+
Raises:
64+
ClientNotInitializedError: 클라이언트가 초기화되지 않은 경우
65+
AuthenticationError: 인증 정보가 유효하지 않은 경우
66+
ValidationError: 입력이 유효하지 않은 경우
67+
LimitExceededException: 발송 한도를 초과한 경우
68+
SendError: 이메일 발송 과정에서 오류 발생
69+
ConnectionError: API 연결 실패
70+
"""
71+
pass
72+
73+
@classmethod
74+
@abstractmethod
75+
def reset_client(cls) -> None:
76+
"""
77+
클라이언트 인스턴스를 재설정합니다(테스트나 설정 변경 시 사용하기 위함)
78+
"""
79+
pass

modules/mail/constants.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""
2+
AWS SES API 에러 코드를 그룹화하여 관리합니다.
3+
Common API 에러 코드 참고:
4+
https://docs.aws.amazon.com/ses/latest/APIReference/CommonErrors.html
5+
"""
6+
7+
AWS_AUTH_ERROR_CODES = {
8+
# Common
9+
"InvalidClientTokenId",
10+
"AccessDeniedException",
11+
"MissingAuthenticationToken",
12+
"IncompleteSignature",
13+
"NotAuthorized",
14+
"AccessDenied",
15+
# SES
16+
"SignatureDoesNotMatch",
17+
}
18+
19+
AWS_VALUE_ERROR_CODES = {
20+
# Common
21+
"InvalidParameterCombination",
22+
"InvalidParameterValue",
23+
"InvalidQueryParameter",
24+
"MalformedQueryString",
25+
"MissingParameter",
26+
"ValidationError",
27+
}
28+
29+
AWS_LIMIT_ERROR_CODES = {
30+
# Common
31+
"ThrottlingException",
32+
"TooManyRequestsException",
33+
# SES
34+
"LimitExceededException",
35+
}
36+
37+
AWS_SERVICE_ERROR_CODES = {
38+
# Common
39+
"ServiceUnavailable",
40+
"InternalFailure",
41+
"InternalServerError",
42+
}

modules/mail/exceptions.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
class MailError(Exception):
2+
"""메일 관련 모든 예외의 기본 클래스"""
3+
4+
pass
5+
6+
7+
class AuthenticationError(MailError):
8+
"""인증 관련 오류"""
9+
10+
pass
11+
12+
13+
class ConnectionError(MailError):
14+
"""서비스 연결 오류"""
15+
16+
pass
17+
18+
19+
class ClientNotInitializedError(MailError):
20+
"""클라이언트가 초기화되지 않은 경우"""
21+
22+
pass
23+
24+
25+
class SendError(MailError):
26+
"""이메일 발송 중 발생한 오류"""
27+
28+
pass
29+
30+
class LimitExceededException(MailError):
31+
"""메일 서비스 할당량 초과 오류"""
32+
33+
pass
34+
35+
36+
class ValidationError(MailError):
37+
"""API 입력이 유효하지 않은 오류"""
38+
39+
pass
40+
41+
class UnexpectedClientError(MailError):
42+
"""예상하지 못한 ClientError"""
43+
44+
pass

modules/mail/schemas.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from dataclasses import dataclass
2+
3+
@dataclass
4+
class EmailAttachment:
5+
filename: str
6+
content: bytes
7+
content_type: str
8+
9+
10+
@dataclass
11+
class EmailMessage:
12+
to: list[str]
13+
from_email: str
14+
subject: str
15+
text_body: str
16+
html_body: str | None = None
17+
cc: list[str] | None = None
18+
bcc: list[str] | None = None
19+
attachments: list[EmailAttachment] | None = None
20+
21+
@dataclass
22+
class AWSSESCredentials:
23+
aws_access_key_id: str
24+
aws_secret_access_key: str
25+
aws_region_name: str

modules/mail/ses/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)