Python SMTP email sending from entry to advanced

SMTP (Simple Mail Transfer Protocol) is the most widely used email sending standard on the Internet. Whether it is a personal mailbox, a corporate mailbox, or a server monitoring and alarm system, the bottom layer almost all relies on SMTP to deliver letters.

The Python standard library provides two core modules related to email, with clear responsibilities:

  • email: Responsible for assembling the content of the email, which can construct plain text, HTML, attachments, embedded images, and set email headers (sender, recipient, subject, etc.).
  • smtplib: Responsible for sending out the assembled emails, establishing SMTP connection, login, and transmission.

This article will start from scratch and take you step by step to master using Python to send various styles of emails, and provide pitfall avoidance guides and complete examples in a production environment.


1. Preparation: Make sure your email supports SMTP

Before writing code, first ensure that the SMTP service of the mailbox is turned on and obtain the corresponding authorization information:

  1. Enable SMTP service Log in to the email backend (such as QQ Mail, 163, Gmail, etc.), find the SMTP option in "Settings → Account → POP3/IMAP/SMTP Service" and turn it on.

  2. Get authorization code The vast majority of third-party clients (including our Python scripts) ** cannot use login passwords directly ** and require an "authorization code" or "application-specific password" generated by email. After turning on the SMTP service, the page will usually guide you to generate it.

  3. Record SMTP configuration information Different service providers have different SMTP addresses and ports. Common ports are:

  • 25: Traditional clear text transmission, unsafe, most have been blocked or disabled
  • 465: SSL direct connection encryption
  • 587: STARTTLS encryption (recommended)

SMTP server addresses for some commonly used email addresses:

EmailSMTP ServerRecommended Port
QQ Emailsmtp.qq.com587 (STARTTLS) or 465 (SSL)
163 Emailsmtp.163.com465 (SSL)
Gmailsmtp.gmail.com587 (STARTTLS) or 465 (SSL)

For specific information, please refer to the latest documentation from the service provider.


2. First email: plain text

2.1 Assembling email content

The simplest email is plain text and can be used directlyemail.mime.text.MIMETextto create:

from email.mime.text import MIMEText

# 参数:正文内容、MIME 子类型、编码
msg = MIMEText('你好!这是一封用 Python 写的测试邮件😎', 'plain', 'utf-8')

MIMETextThe first parameter is the message body and the second parameter is the subtype ('plain'Represents plain text,'html'represents HTML), the third parameter specifies the character encoding, which is uniformly usedutf-8It can avoid most garbled characters.

2.2 Establish connection and send

usesmtplib.SMTPEstablish a connection and useloginMethod to log in. During the debugging phase, you can turn on the debug mode to view the complete process of SMTP protocol interaction:

import smtplib

# 配置信息(实际使用时请换成自己的)
from_addr = input('你的邮箱: ')
auth_code = input('授权码/应用专用密码: ')
to_addr = input('收件人邮箱: ')
smtp_server = input('SMTP 服务器地址(如 smtp.qq.com): ')

with smtplib.SMTP(smtp_server, 25) as server:
    server.set_debuglevel(1)   # 开启调试,打印交互日志
    server.login(from_addr, auth_code)
    # sendmail 参数:发件人、收件人列表、完整邮件字符串
    server.sendmail(from_addr, [to_addr], msg.as_string())

Now, run this script and you can send the first email sent by a program in your life. However, the received email may look crude, without a subject or nickname. The next step is to improve it.


3. Complete the email header: Make the email look more like it was sent by a human

The above email lacks the subject, sender nickname, and recipient nickname, and looks very stiff. we can useemail.header.Headerandemail.utils.formataddrto set this information.

In order to make the code more concise, first encapsulate an "address formatting" function:

from email.header import Header
from email.utils import formataddr, parseaddr

def _format_addr(name, addr):
    # 将中文名称用 Header 编码,再与地址拼接成标准格式
    return formataddr((Header(name, 'utf-8').encode(), addr))

Then add to the mail objectFromToandSubjecthead:

msg['From'] = _format_addr('Python 小助手', from_addr)
msg['To'] = _format_addr('亲爱的测试员', to_addr)
msg['Subject'] = Header('Python 发来的第一封正经邮件', 'utf-8').encode()

NOTE:msg['To']Only the recipient information displayed in the email header is affected. The actual mailbox to which the email is sent is still determined bysendmailDetermined by the second parameter.


4. Advanced scenario: constructing various types of emails

4.1 HTML email

Just need to putMIMETextThe second parameter of'plain'Change to'html'That’s it:

html_body = """
<html>
<body>
    <h1 style="color: #00BFFF;">Hello 进阶版!</h1>
    <p>这是一封带<b>加粗</b>、<i>斜体</i>,还有<a href="https://www.python.org">链接</a>的 HTML 邮件。</p>
</body>
</html>
"""

msg = MIMEText(html_body, 'html', 'utf-8')
# 别忘了设置邮件头!

Mail can now display rich HTML content.

4.2 Emails with attachments

To send attachments, you need to useMIMEMultipartThis "container" object.MIMEMultipartMultiple parts (text, attachments) can be combined together. The default subtype is'mixed'(mix).

from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders

# 创建一个容器
msg = MIMEMultipart()
# 设置邮件头(与之前相同,略)
msg['From'] = _format_addr('Python 小助手', from_addr)
msg['To'] = _format_addr('收件人', to_addr)
msg['Subject'] = Header('带附件的邮件', 'utf-8').encode()

# 添加正文(纯文本)
msg.attach(MIMEText('附件里是本周工作总结,请查收😊', 'plain', 'utf-8'))

# 添加附件(以 PDF 为例,图片等其他二进制文件同理)
with open('weekly_report.pdf', 'rb') as f:
    attachment = MIMEBase('application', 'octet-stream', filename='weekly_report.pdf')
    attachment.add_header('Content-Disposition', 'attachment', filename='weekly_report.pdf')
    attachment.set_payload(f.read())          # 读取文件内容
    encoders.encode_base64(attachment)        # 进行 Base64 编码
    msg.attach(attachment)

Key points:

  • MIMEBaseUsed to represent binary data, the first parameter is the main type (application), the second is a subtype (octet-streamrepresents a binary stream).
  • Content-Disposition: attachmentTell the client this is an attachment,filenameSpecify the file name to be displayed.
  • Mail transmission requires Base64 encoding,encoders.encode_base64It can be done in one step.

4.3 Embedding local images in HTML

Internet images directly referenced in HTML emails can usually be displayed normally, but if you want to embed local images (such as company logos, charts) in the email, you need to useContent-ID(Content ID).

msg = MIMEMultipart()
# 邮件头设置(略)

# 编写引用 cid 的 HTML
html_with_img = """
<html>
<body>
    <h1>这是一张本地图片</h1>
    <p><img src="cid:cat_pic" width="300"></p>
</body>
</html>
"""
msg.attach(MIMEText(html_with_img, 'html', 'utf-8'))

# 将图片作为附件挂载,并指定 Content-ID
with open('cat.jpg', 'rb') as f:
    from email.mime.image import MIMEImage
    img = MIMEImage(f.read())
    img.add_header('Content-ID', '<cat_pic>')   # 与 HTML 中的 cid 一致,注意要有尖括号
    msg.attach(img)

This way, the image is embedded in the email and no longer relies on external links.

4.4 Available in both plain text and HTML versions

Some email clients may have HTML display turned off or not load remote resources by default for security reasons. For the sake of compatibility, we should provide both plain text and HTML versions, allowing the client to choose the appropriate display method.

At this time it is necessary to useMIMEMultipart('alternative'). It will contain body text in two formats, with the email client showing the last recognized part first (usually HTML).

msg = MIMEMultipart('alternative')
# 邮件头设置(略)

# 纯文本版本(放在前面)
msg.attach(MIMEText('这是纯文本备用内容,请使用支持 HTML 的客户端查看。', 'plain', 'utf-8'))
# HTML 版本
msg.attach(MIMEText(html_body, 'html', 'utf-8'))

5. Security first: Encrypted SMTP connections

The default port 25 is clear text transmission, and account passwords and email content may be intercepted. Almost all email service providers now mandate encrypted connections, and there are two main ways.

5.1 STARTTLS encryption

First establish a normal SMTP connection and then callstarttls()Method to upgrade the connection to TLS encryption. A typical port is 587.

with smtplib.SMTP(smtp_server, 587) as server:
    server.starttls()            # 开启 TLS 加密
    server.login(from_addr, auth_code)
    server.sendmail(from_addr, [to_addr], msg.as_string())

5.2 SSL/TLS direct connection

Use directlySMTP_SSLclass to establish an encrypted connection from the beginning. Commonly used port is 465.

with smtplib.SMTP_SSL(smtp_server, 465) as server:
    server.login(from_addr, auth_code)
    server.sendmail(from_addr, [to_addr], msg.as_string())

💡 Selection suggestions:

  • Try 587 + STARTTLS first, which is the latest standard recommendation;
  • If the server does not support STARTTLS, then consider using port 465 SSL direct connection.

6. Pitfall avoidance guide and best practices

In actual projects, we need more robust code. The following are some key experience summaries:

  1. Sensitive information is never hardcoded It is very dangerous to write authorization codes, email addresses and other information directly in plain text in the code. It is recommended to use environment variables,.envfile or configuration center, e.g. viaos.getenv()Read.

  2. Add perfect exception-handling Network fluctuations, authentication failures, attachments that are too large, etc. may cause sending failures. Corresponding captures and prompts must be made.

  3. Uniformly use UTF-8 encoding As long as the email content, subject, and nickname involve Chinese, they must be specified'utf-8'Encoding, otherwise gibberish will be everywhere.

  4. The recipient must be an iterable object sendmailThe second parameter requirement is an iterable object such as a list or tuple. Even if there is only one recipient, it must be written as[to_addr]

  5. Control the size of attachments Most free mailboxes limit the size of attachments to 10 to 50MB, and will be rejected if it exceeds. It is recommended to upload large files to a cloud disk and then share the link in an email.

Complete example with exception-handling

import os
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.header import Header
from email.utils import formataddr

def _format_addr(name, addr):
    return formataddr((Header(name, 'utf-8').encode(), addr))

def send_secure_email():
    # 从环境变量读取(需要提前设置好 export)
    from_addr = os.getenv('SMTP_FROM')
    auth_code = os.getenv('SMTP_AUTH')
    to_addrs = os.getenv('SMTP_TO', '').split(',')   # 支持多个收件人,逗号分隔
    smtp_server = os.getenv('SMTP_SERVER')
    smtp_port = int(os.getenv('SMTP_PORT', 587))     # 默认 587

    # 创建多格式邮件(同时包含纯文本和 HTML)
    msg = MIMEMultipart('alternative')
    msg['From'] = _format_addr('自动化告警系统', from_addr)
    msg['To'] = ','.join([_format_addr('', addr) for addr in to_addrs])
    msg['Subject'] = Header('服务器 CPU 使用率告警', 'utf-8').encode()

    text = "服务器 CPU 使用率已超过 80%,请及时处理!"
    html = """
    <html>
        <body style="font-family: Arial, sans-serif;">
            <h2 style="color: #DC143C;">⚠️ 服务器告警</h2>
            <p>当前 CPU 使用率:<b>85%</b></p>
            <p>请登录服务器查看详情</p>
        </body>
    </html>
    """
    msg.attach(MIMEText(text, 'plain', 'utf-8'))
    msg.attach(MIMEText(html, 'html', 'utf-8'))

    # 发送邮件并捕获常见异常
    try:
        with smtplib.SMTP(smtp_server, smtp_port) as server:
            server.starttls()
            server.login(from_addr, auth_code)
            server.sendmail(from_addr, to_addrs, msg.as_string())
        print("✅ 邮件发送成功!")
    except smtplib.SMTPAuthenticationError:
        print("❌ 认证失败,请检查邮箱地址和授权码")
    except smtplib.SMTPConnectError:
        print("❌ 连接失败,请检查 SMTP 服务器地址和端口")
    except Exception as e:
        print(f"❌ 发送出错:{e}")

if __name__ == '__main__':
    send_secure_email()

After the environment variables in the above script are configured, they can be easily integrated into monitoring, business notification and other scenarios.


7. Simple summary: object relationship of email module

Understanding the relationship between these classes will help you make the right choices when building complex emails:

Message(基类,一般不直接使用)
└─ MIMEBase(所有 MIME 类型的基类)
   ├─ MIMEMultipart(混合容器,可包含多种内容)
   └─ MIMENonMultipart(非混合,单一内容)
      ├─ MIMEText(纯文本 / HTML)
      ├─ MIMEImage(图片)
      ├─ MIMEAudio(音频)
      └─ ...(其他格式)
  • Send a single content (plain text, HTML, image): use directlyMIMETextMIMEImagewait.
  • Send multiple combinations (attachments + text, embedded images + HTML): create firstMIMEMultipartcontainer, reuseattachmethod to add each section one by one.
  • Need to support both plain text and HTML display: useMIMEMultipart('alternative')

Through this tutorial, you should have mastered a full set of skills from simple plain text emails, to complex emails containing attachments, embedded images, and HTML format, to encrypted connections and exception-handling. Encapsulate what you learn into functions or classes and you can quickly apply it to actual projects. I wish you smooth sailing on your email sending journey!