Doc AWS SDK 예제 GitHub 리포지토리에서 더 많은 SDK 예제를 사용할 수 있습니다. AWS
기계 번역으로 제공되는 번역입니다. 제공된 번역과 원본 영어의 내용이 상충하는 경우에는 영어 버전이 우선합니다.
Amazon SES API v2를 사용하여 첨부 파일이 포함된 이메일 전송
다음 코드 예제에서는 Amazon SES API v2를 사용하여 첨부 파일이 포함된 이메일을 보내는 방법을 보여줍니다.
대량 전송을 위한 이메일 템플릿을 생성합니다.
파일 첨부 파일이 포함된 간단한 이메일을 보냅니다.
인라인 이미지가 포함된 간단한 이메일을 보냅니다.
첨부 파일이 포함된 대량 템플릿 이메일을 보냅니다.
리소스를 정리합니다.
- Python
-
- SDK for Python(Boto3)
-
참고
GitHub에 더 많은 내용이 있습니다. AWS 코드 예 리포지토리
에서 전체 예를 찾고 설정 및 실행하는 방법을 배워보세요. 이메일 첨부 파일을 보여주는 대화형 시나리오를 실행합니다.
class SESv2EmailAttachmentsScenario: """ Demonstrates sending emails with attachments using Amazon SESv2. This scenario demonstrates: 1. Setting up an email identity and template. 2. Sending a simple email with a file attachment. 3. Sending a simple email with an inline image. 4. Sending bulk templated emails with attachments. 5. Cleaning up created resources. """ TEMPLATE_NAME = "AttachmentDemoTemplate" def __init__(self, sesv2_wrapper: SESv2Wrapper) -> None: """ :param sesv2_wrapper: An instance of the SESv2Wrapper class. """ self.sesv2_wrapper = sesv2_wrapper self.sender_email = "" self.recipient_emails: list = [] self.identity_was_created = False def run_scenario(self) -> None: """Runs the SESv2 email attachments scenario.""" print("-" * 88) print("Welcome to the Amazon SESv2 Email Attachments Scenario.") print("-" * 88) print( "This scenario demonstrates how to send emails with attachments\n" "using SESv2 attachment support. SES handles MIME\n" "construction automatically, so you don't need to build raw\n" "MIME messages.\n" ) try: self._setup() self._step1_send_email_with_attachment() self._step2_send_email_with_inline_image() self._step3_send_bulk_email_with_attachments() except Exception as e: logger.error("Scenario failed: %s", e) print(f"\nThe scenario encountered an error: {e}") finally: self._cleanup() # ---------- Setup ---------- def _setup(self) -> None: """ Prompts for configuration, verifies the sender identity, prepares a sample attachment, and creates an email template. """ print("\n--- Setup ---\n") # Prompt for sender and recipient addresses. print( "Both sender and recipient addresses must be verified if your\n" "account is in the SES sandbox.\n" ) self.sender_email = q.ask( "Enter a verified sender email address: " ) recipient_input = q.ask( "Enter one or more recipient email addresses (comma-separated): " ) self.recipient_emails = [ addr.strip() for addr in recipient_input.split(",") if addr.strip() ] # Verify the sender identity. print(f"\nChecking identity for {self.sender_email}...") try: identity_info = self.sesv2_wrapper.get_email_identity( self.sender_email ) verified = identity_info.get("VerifiedForSendingStatus", False) if verified: print(f" {self.sender_email} is verified and ready to send.") else: print( f" {self.sender_email} exists but is not yet verified." ) except ClientError as err: if err.response["Error"]["Code"] == "NotFoundException": print( f" Identity {self.sender_email} not found. " "Creating it now..." ) result = self.sesv2_wrapper.create_email_identity( self.sender_email ) self.identity_was_created = True print( f" Identity created. Verification status: " f"{result.get('VerifiedForSendingStatus', False)}" ) print( " Check your inbox and click the verification link " "before continuing." ) q.ask("Press Enter when you have verified the address...") else: raise # Create the email template for the bulk-send step. print("\nCreating email template for the bulk email step...") try: self.sesv2_wrapper.create_email_template( template_name=self.TEMPLATE_NAME, subject="Bulk Email with Attachment for {{name}}", html_body=( "<h1>Hello {{name}}</h1>" "<p>Please find the attached document.</p>" ), text_body=( "Hello {{name}}, Please find the attached document." ), ) print(f" Template '{self.TEMPLATE_NAME}' created.\n") except ClientError as err: if err.response["Error"]["Code"] == "AlreadyExistsException": print( f" Template '{self.TEMPLATE_NAME}' already exists. " "Using it.\n" ) else: raise # ---------- Step 1: Simple email with file attachment ---------- def _step1_send_email_with_attachment(self) -> None: """Sends a simple email with a text file attachment.""" print("\n--- Step 1: Send a Simple Email with a File Attachment ---\n") print( "Creating a sample text file attachment and sending it with\n" "the Simple email content type. SES constructs the MIME message\n" "automatically.\n" ) # Prepare a sample text file as bytes. sample_content = b"This is a sample report attachment." attachment = { "RawContent": sample_content, "FileName": "sample-report.txt", "ContentType": "text/plain", "ContentDisposition": "ATTACHMENT", "ContentDescription": "Sample report text file", "ContentTransferEncoding": "BASE64", } print( "Note: When using an AWS SDK, the SDK handles base64 encoding\n" "automatically. Direct API callers must encode content themselves.\n" ) message_id = self.sesv2_wrapper.send_email( from_address=self.sender_email, to_addresses=self.recipient_emails, subject="SESv2 Attachment Demo - Simple Email with Attachment", html_body=( "<h1>Attachment Demo</h1>" "<p>Please see the attached <b>report document</b>.</p>" ), text_body="Please see the attached report document.", attachments=[attachment], ) print(f" Email sent. MessageId: {message_id}") print( " SES automatically constructed the MIME message with the " "attachment.\n" ) # ---------- Step 2: Simple email with inline image ---------- def _step2_send_email_with_inline_image(self) -> None: """Sends a simple email with an inline image that renders in HTML.""" print("\n--- Step 2: Send a Simple Email with an Inline Image ---\n") print( "This step demonstrates INLINE disposition. The image renders\n" "directly in the HTML body using a 'cid:' reference instead of\n" "appearing as a downloadable attachment.\n" ) # Create a minimal 1x1 red PNG (valid PNG file). sample_image = ( b"\x89PNG\r\n\x1a\n" # PNG signature b"\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01" b"\x08\x02\x00\x00\x00\x90wS\xde" # 1x1 RGB b"\x00\x00\x00\x0cIDATx\x9cc\xf8\x0f\x00\x00\x01\x01" b"\x00\x05\x18\xd8N" # compressed data b"\x00\x00\x00\x00IEND\xaeB`\x82" # IEND ) attachment = { "RawContent": sample_image, "FileName": "logo.png", "ContentType": "image/png", "ContentDisposition": "INLINE", "ContentId": "logo123", "ContentDescription": "Company logo", "ContentTransferEncoding": "BASE64", } html_body = ( "<html><body>" "<h1>Inline Image Demo</h1>" "<p>Here is our logo:</p>" '<img src="cid:logo123" alt="Company Logo">' "</body></html>" ) message_id = self.sesv2_wrapper.send_email( from_address=self.sender_email, to_addresses=self.recipient_emails, subject="SESv2 Attachment Demo - Inline Image", html_body=html_body, text_body=( "Inline Image Demo - Please view this email in an " "HTML-capable client to see the embedded image." ), attachments=[attachment], ) print(f" Email sent. MessageId: {message_id}") print( " The ContentId 'logo123' is referenced in the HTML body via\n" " 'cid:logo123', which lets the image render inline.\n" ) # ---------- Step 3: Bulk templated email with attachments ---------- def _step3_send_bulk_email_with_attachments(self) -> None: """Sends bulk templated emails with attachments to multiple recipients.""" print("\n--- Step 3: Send Bulk Templated Emails with Attachments ---\n") print( "Using SendBulkEmail to send a templated email with an attachment\n" "to multiple recipients in a single API call. Each recipient gets\n" "personalized content via template data.\n" ) sample_content = b"This is a sample report attachment." attachment = { "RawContent": sample_content, "FileName": "sample-report.txt", "ContentType": "text/plain", "ContentDisposition": "ATTACHMENT", "ContentDescription": "Sample report for bulk recipients", "ContentTransferEncoding": "BASE64", } # Build one entry per recipient with personalized names. names = ["Alice", "Bob", "Charlie", "Diana", "Eve"] bulk_entries = [] for i, email in enumerate(self.recipient_emails): name = names[i] if i < len(names) else f"Recipient{i + 1}" bulk_entries.append( { "Destination": {"ToAddresses": [email]}, "ReplacementEmailContent": { "ReplacementTemplate": { "ReplacementTemplateData": json.dumps( {"name": name} ) } }, } ) results = self.sesv2_wrapper.send_bulk_email( from_address=self.sender_email, template_name=self.TEMPLATE_NAME, default_template_data='{"name": "Valued Customer"}', bulk_entries=bulk_entries, attachments=[attachment], ) print(" Bulk email results:") for idx, result in enumerate(results): status = result.get("Status", "Unknown") msg_id = result.get("MessageId", "N/A") error = result.get("Error", "") recipient = ( self.recipient_emails[idx] if idx < len(self.recipient_emails) else "Unknown" ) print(f" {recipient}: Status={status}, MessageId={msg_id}") if error: print(f" Error: {error}") print( "\n All recipients receive the same attachment(s) defined in\n" " DefaultContent. Template data is personalized per recipient.\n" ) # ---------- Cleanup ---------- def _cleanup(self) -> None: """Deletes the email template and optionally the email identity.""" print("\n--- Cleanup ---\n") # Delete the email template. try: self.sesv2_wrapper.delete_email_template(self.TEMPLATE_NAME) print(f" Template '{self.TEMPLATE_NAME}' deleted.") except ClientError as err: if err.response["Error"]["Code"] == "NotFoundException": print( f" Template '{self.TEMPLATE_NAME}' was already deleted." ) else: logger.error("Failed to delete template: %s", err) # Optionally delete the email identity. if self.identity_was_created and self.sender_email: delete_identity = q.ask( f"Delete the email identity '{self.sender_email}'? (y/n) ", q.is_yesno, ) if delete_identity: try: self.sesv2_wrapper.delete_email_identity( self.sender_email ) print( f" Email identity '{self.sender_email}' deleted." ) except ClientError as err: if err.response["Error"]["Code"] == "NotFoundException": print( f" Identity '{self.sender_email}' was " "already deleted." ) else: logger.error( "Failed to delete identity: %s", err ) else: print( f" Skipping identity deletion for {self.sender_email}." ) else: print( " Sender identity was pre-existing. Skipping deletion." ) print("\nAll resources have been cleaned up.") print("-" * 88)SESv2 래퍼 클래스를 생성하여 작업을 관리합니다.
class SESv2Wrapper: """Encapsulates Amazon SESv2 email sending actions.""" def __init__(self, sesv2_client: Any) -> None: """ Initializes the SESv2Wrapper with an SESv2 client. :param sesv2_client: A Boto3 SESv2 client. """ self.sesv2_client = sesv2_client @classmethod def from_client(cls) -> "SESv2Wrapper": """ Creates an SESv2Wrapper instance with a default Boto3 SESv2 client. :return: A new SESv2Wrapper instance. """ sesv2_client = boto3.client("sesv2") return cls(sesv2_client) def get_email_identity(self, email_address: str) -> Dict[str, Any]: """ Gets information about an email identity, including its verification status. :param email_address: The email address or domain to look up. :return: A dictionary with identity information including verification status. :raises ClientError: If the identity is not found (NotFoundException). """ try: response = self.sesv2_client.get_email_identity( EmailIdentity=email_address ) logger.info("Got email identity for %s.", email_address) return response except ClientError as err: if err.response["Error"]["Code"] == "NotFoundException": logger.info( "Email identity %s not found.", email_address ) else: logger.error( "Couldn't get email identity %s. Here's why: %s: %s", email_address, err.response["Error"]["Code"], err.response["Error"]["Message"], ) raise def create_email_identity(self, email_address: str) -> Dict[str, Any]: """ Starts the process of verifying an email identity (email address or domain). :param email_address: The email address or domain to verify. :return: A dictionary with the identity type and verification status. :raises ClientError: If the limit is exceeded (LimitExceededException). """ try: response = self.sesv2_client.create_email_identity( EmailIdentity=email_address ) logger.info( "Started verification for email identity %s.", email_address ) return response except ClientError as err: if err.response["Error"]["Code"] == "LimitExceededException": logger.error( "Couldn't create email identity %s. You have exceeded " "the maximum number of email identities. " "Use an existing verified identity.", email_address, ) else: logger.error( "Couldn't create email identity %s. Here's why: %s: %s", email_address, err.response["Error"]["Code"], err.response["Error"]["Message"], ) raise def create_email_template( self, template_name: str, subject: str, html_body: str, text_body: str, ) -> None: """ Creates an email template for use with templated and bulk email sends. :param template_name: The name for the new template. :param subject: The subject line of the template. May include {{placeholders}}. :param html_body: The HTML body of the template. :param text_body: The plain text body of the template. :raises ClientError: If the template limit is exceeded (LimitExceededException). """ try: self.sesv2_client.create_email_template( TemplateName=template_name, TemplateContent={ "Subject": subject, "Html": html_body, "Text": text_body, }, ) logger.info("Created email template %s.", template_name) except ClientError as err: if err.response["Error"]["Code"] == "LimitExceededException": logger.error( "Couldn't create email template %s. You have exceeded " "the maximum number of email templates. " "Delete unused templates first.", template_name, ) else: logger.error( "Couldn't create email template %s. Here's why: %s: %s", template_name, err.response["Error"]["Code"], err.response["Error"]["Message"], ) raise def send_email( self, from_address: str, to_addresses: List[str], subject: str, html_body: str, text_body: str, attachments: Optional[List[Dict[str, Any]]] = None, ) -> str: """ Sends a simple email message with optional attachments. SES handles MIME construction automatically when using attachments with the Simple content type, so developers don't need to build raw MIME messages. :param from_address: The verified sender email address. :param to_addresses: A list of recipient email addresses. :param subject: The subject line of the email. :param html_body: The HTML body content. :param text_body: The plain text body content. :param attachments: An optional list of attachment dictionaries. Each attachment should contain 'RawContent' (bytes), 'FileName' (str), and optionally 'ContentType', 'ContentDisposition', 'ContentId', 'ContentDescription', and 'ContentTransferEncoding'. :return: The MessageId of the sent email. :raises ClientError: If the message is rejected (MessageRejected). """ try: simple_message: Dict[str, Any] = { "Subject": {"Data": subject}, "Body": { "Html": {"Data": html_body}, "Text": {"Data": text_body}, }, } if attachments: simple_message["Attachments"] = attachments response = self.sesv2_client.send_email( FromEmailAddress=from_address, Destination={"ToAddresses": to_addresses}, Content={"Simple": simple_message}, ) message_id = response["MessageId"] logger.info( "Sent email from %s to %s. MessageId: %s", from_address, to_addresses, message_id, ) return message_id except ClientError as err: if err.response["Error"]["Code"] == "MessageRejected": logger.error( "Message was rejected. Check that attachments use " "supported file types and total message size is " "under 40 MB. Details: %s", err.response["Error"]["Message"], ) else: logger.error( "Couldn't send email. Here's why: %s: %s", err.response["Error"]["Code"], err.response["Error"]["Message"], ) raise def send_bulk_email( self, from_address: str, template_name: str, default_template_data: str, bulk_entries: List[Dict[str, Any]], attachments: Optional[List[Dict[str, Any]]] = None, ) -> List[Dict[str, Any]]: """ Sends a templated email to multiple recipients in a single API call. All recipients receive the same attachment(s) defined in the default content, while template data can be personalized per recipient. :param from_address: The verified sender email address. :param template_name: The name of an existing email template. :param default_template_data: Default JSON template data string. :param bulk_entries: A list of BulkEmailEntry dicts, each containing 'Destination' and optionally 'ReplacementEmailContent'. :param attachments: An optional list of attachment dicts for all recipients. :return: A list of BulkEmailEntryResult dicts with status and MessageId. :raises ClientError: If the message is rejected (MessageRejected). """ try: template_content: Dict[str, Any] = { "TemplateName": template_name, "TemplateData": default_template_data, } if attachments: template_content["Attachments"] = attachments response = self.sesv2_client.send_bulk_email( FromEmailAddress=from_address, DefaultContent={"Template": template_content}, BulkEmailEntries=bulk_entries, ) results = response.get("BulkEmailEntryResults", []) logger.info( "Sent bulk email from %s to %d recipients.", from_address, len(bulk_entries), ) return results except ClientError as err: if err.response["Error"]["Code"] == "MessageRejected": logger.error( "Bulk message was rejected. Check that the template " "exists, attachment file types are supported, and " "total message size is within limits. Details: %s", err.response["Error"]["Message"], ) else: logger.error( "Couldn't send bulk email. Here's why: %s: %s", err.response["Error"]["Code"], err.response["Error"]["Message"], ) raise def delete_email_template(self, template_name: str) -> None: """ Deletes an email template. :param template_name: The name of the template to delete. :raises ClientError: If the template is not found (NotFoundException). """ try: self.sesv2_client.delete_email_template( TemplateName=template_name ) logger.info("Deleted email template %s.", template_name) except ClientError as err: if err.response["Error"]["Code"] == "NotFoundException": logger.info( "Email template %s not found or already deleted.", template_name, ) else: logger.error( "Couldn't delete email template %s. Here's why: %s: %s", template_name, err.response["Error"]["Code"], err.response["Error"]["Message"], ) raise def delete_email_identity(self, email_address: str) -> None: """ Deletes an email identity. :param email_address: The email address or domain to delete. :raises ClientError: If the identity is not found (NotFoundException). """ try: self.sesv2_client.delete_email_identity( EmailIdentity=email_address ) logger.info("Deleted email identity %s.", email_address) except ClientError as err: if err.response["Error"]["Code"] == "NotFoundException": logger.info( "Email identity %s not found or already deleted.", email_address, ) else: logger.error( "Couldn't delete email identity %s. Here's why: %s: %s", email_address, err.response["Error"]["Code"], err.response["Error"]["Message"], ) raise-
API 세부 정보는 AWS SDK for Python (Boto3) API 참조의 다음 주제를 참조하세요.
-
시나리오
뉴스레터 시나리오