zoukankan      html  css  js  c++  java
  • Day-18: 电子邮件

      假设要从**@163.com发送邮件到**@sina.com,会经过下面几个过程:

    • 首先,你得使用邮件代理软件(也就是MUA:Mail User Agent),例如Outlook,Foxmail。填写你的Email地址和密码,发送邮件。
    • Email从MUA发出后会到达163的服务器,也就是MTA:Mail Transfer Agent。之后再由网易的服务商传到新浪的服务商MTA,中间可能还会经过其他几个MTA。
    • Email到达新浪的MTA后,会被投递到最终目的地MDA:Mail Delivery Agent——邮件投递代理,并长期保存在这个电子邮箱中。
    • 最后,得收件人通过MUA将邮件获取得到。

      在发邮件时,MUA和MTA之间的协议是SMTP:Simple Mail Transfer Protocol。

      收邮件时,MUA和MDA使用的协议有POP:Post Office Protocol,版本号为3,俗称POP3;另一种是,IMAP: Internet Message Protocol,它的优点是不断可以取邮件,还可以直接操作MDA上存储的邮件,比如从收件箱一道垃圾箱。

      在Python中,收发邮件,只要做到以下两点:

    1. 编写MUA把邮件发到MTA;
    2. 编写MUA从MDA上收邮件。
    • STMP发送邮件

      SMTP是发送邮件的协议,Python中stmplib和email两个模块是对STMP支持的,其中,email负责构造邮件,stmplib负责发送邮件。

      首先,得了解构造的邮件对象就是Message对象,而MIMEText对象,就是一个文本邮件对象,如果构造一个MIMEImage对象,就是表示一个作为附件的图片,如果要把多个对象组合起来,就用MIMEMultipart对象,而MIMEBase可以表示任何对象,它们的继承关系如下:

    Message
    +- MIMEBase
       +- MIMEMultipart
       +- MIMENonMultipart
          +- MIMEMessage
          +- MIMEText
          +- MIMEImage

      如果我们要构造纯文本邮件,如下:

    from email.mime.text import MIMEText
    msg = MIMEText('hello, send by Python...', 'plain', 'utf-8')

      构造MIMEText对象时,第一个参数时邮件正文,第二个参数时MIME的subtype,传入“plain”表示纯文本,传入“html”就表示HTML格式,最终的MIME就是‘text/plain’,最后一定要用utf-8编码来保证多国语言兼容。

      邮件构建完成后,通过SMTP发送

    # 输入Email地址和口令:
    from_addr = raw_input('From: ')
    password = raw_input('Password: ')
    # 输入SMTP服务器地址:
    smtp_server = raw_input('SMTP server: ')
    # 输入收件人地址:
    to_addr = raw_input('To: ')
    
    import smtplib
    server = smtplib.SMTP(smtp_server, 25) # SMTP协议默认端口是25
    server.set_debuglevel(1) # 打印出和STMP服务器交互的所有信息
    server.login(from_addr, password) # 登录STMP服务器
    server.sendmail(from_addr, [to_addr], msg.as_string()) # 发送邮件
    server.quit() # 结束

      但是,效果不是很理想:

    1. 邮件没主题;
    2. 没有收件人的名字。

      这是因为显示主题、收件人、发件人等信息是记录在所发的message中的,STMP发送所传入的参数只是保证登录服务器和确定目标地址。所以,必须把from、To、subject等信息添加到MIMEText中,才能完整显示。

    # -*- coding: utf-8 -*-
    
    from email import encoders
    from email.header import Header
    from email.mime.text import MIMEText
    from email.utils import parseaddr, formataddr
    import smtplib
    
    def _format_addr(s):
        name, addr = parseaddr(s)
        return formataddr(( 
            Header(name, 'utf-8').encode(), 
            addr.encode('utf-8') if isinstance(addr, unicode) else addr))
    
    from_addr = raw_input('From: ')
    password = raw_input('Password: ')
    to_addr = raw_input('To: ')
    smtp_server = raw_input('SMTP server: ')
    
    msg = MIMEText('hello, send by Python...', 'plain', 'utf-8')
    msg['From'] = _format_addr(u'Python爱好者 <%s>' % from_addr)
    msg['To'] = _format_addr(u'管理员 <%s>' % to_addr)
    msg['Subject'] = Header(u'来自SMTP的问候……', 'utf-8').encode()
    
    server = smtplib.SMTP(smtp_server, 25)
    server.set_debuglevel(1)
    server.login(from_addr, password)
    server.sendmail(from_addr, [to_addr], msg.as_string())
    server.quit()

      首先创建_format_addr()函数来格式化一个邮件的地址信息。地址信息包括前面显示的name和后面备注的邮箱地址。前者由于可能涉及到中文,所以用Header对象进行编码(编码后用于传输的文本是包含了utf-8和Base64的文本),注意最后传出的name和addr都得是utf-8.另外msg['To']接受的不是list而是字符串,所以如果有多个邮件地址传入,就得用,分隔。

      改进后,效果更人性化:

      如果要发送HTML邮件,只用将前面构造MIMEText对象时,把HTML字符串传进去,再把第二个参数有plain换成html就ok了。

    msg = MIMEText('<html><body><h1>Hello</h1>' +
        '<p>send by <a href="http://www.python.org">Python</a>...</p>' +
        '</body></html>', 'html', 'utf-8')

      显示如下:

      前面都是直接发送纯文本或者HTML文档,如果要发送附件,就不能只创建MIMEText对象了,还有还有附件的对象,但是继承树种没有专门的附件对象,用可以代替任何邮件对象的MIMEBase对象代替。

      

    # 邮件对象:
    msg = MIMEMultipart()
    msg['From'] = _format_addr(u'Python爱好者 <%s>' % from_addr)
    msg['To'] = _format_addr(u'管理员 <%s>' % to_addr)
    msg['Subject'] = Header(u'来自SMTP的问候……', 'utf-8').encode()
    
    # 邮件正文是MIMEText:
    msg.attach(MIMEText('send with file...', 'plain', 'utf-8'))
    
    # 添加附件就是加上一个MIMEBase,从本地读取一个图片:
    with open('/Users/michael/Downloads/test.png', 'rb') as f:
        # 设置附件的MIME和文件名,这里是png类型:
        mime = MIMEBase('image', 'png', filename='test.png')
        # 加上必要的头信息:
        mime.add_header('Content-Disposition', 'attachment', filename='test.png')
        mime.add_header('Content-ID', '<0>')
        mime.add_header('X-Attachment-Id', '0')
        # 把附件的内容读进来:
        mime.set_payload(f.read())
        # 用Base64编码:
        encoders.encode_base64(mime)
        # 添加到MIMEMultipart:
        msg.attach(mime)

      先创建MIMEMultipart作为要发送的邮件对象msg,再将要显示主题,收件人,发件人信息加入到msg中,至于正文和附件就另外创建MIMEText对象和MIMEBase对象分别储存好内容后再添加到msg中。

      如果要将图片嵌入到正文中,就只能发送html文件,再上述发送附件的基础上将图片的链接地址传入到html文件中的相应位置。

    msg.attach(MIMEText('<html><body><h1>Hello</h1>' +
        '<p><img src="cid:0"></p>' +
        '</body></html>', 'html', 'utf-8'))

      效果如下:

      如果我们发送HTML邮件,而收件人使用的是比较老的MUA,无法显示HTML文件,只能显示纯文本文件,那就出问题了。所以,为了确保无论老旧设备都能显示,会发送纯文本和HTML两种邮件格式,同时将subtype换成alternative,如果无法查看HTML,会自动降级成纯文本。

    msg = MIMEMultipart('alternative')
    msg['From'] = ...
    msg['To'] = ...
    msg['Subject'] = ...
    
    msg.attach(MIMEText('hello', 'plain', 'utf-8'))
    msg.attach(MIMEText('<html><body><h1>Hello</h1></body></html>', 'html', 'utf-8'))
    # 正常发送msg对象...

      另外,使用标准的25端口连接SMTP服务器时,使用的是明文传输,发送邮件的整个过程有被窃听的风险。要更安全的发送邮件,都是先创建SSL安全连接,然后再使用SMTP协议发送邮件。

      例如gmail的SMTP端口是587

    smtp_server = 'smtp.gmail.com'
    smtp_port = 587
    server = smtplib.SMTP(smtp_server, smtp_port)
    server.starttls()
    # 剩下的代码和前面的一模一样:
    server.set_debuglevel(1)
    ...

      和之前不同的地方,在于创建了SMTP对象后,立刻调用了starttls()方法,就创建了安全连接,其他的都是一样的。

      最后,关于如何构造复杂的邮件内容可以参考email官方包

    • POP3收取文件

      使用POP3收取邮件分为两步:

      第一步:用poplib把邮件的原始文本下载到本地;

      第二步:用email解析原始文本,还原为邮件对象。

      将邮件下载到本地:

    import poplib
    
    # 输入邮件地址, 口令和POP3服务器地址:
    email = raw_input('Email: ')
    password = raw_input('Password: ')
    pop3_server = raw_input('POP3 server: ')
    
    # 连接到POP3服务器:
    server = poplib.POP3(pop3_server)
    # 可以打开或关闭调试信息:
    # server.set_debuglevel(1)
    # 可选:打印POP3服务器的欢迎文字:
    print(server.getwelcome())
    # 身份认证:
    server.user(email)
    server.pass_(password)
    # stat()返回邮件数量和占用空间:
    print('Messages: %s. Size: %s' % server.stat())
    # list()返回所有邮件的编号:
    resp, mails, octets = server.list()
    # 可以查看返回的列表类似['1 82923', '2 2184', ...]
    print(mails)
    # 获取最新一封邮件, 注意索引号从1开始:
    index = len(mails)
    resp, lines, octets = server.retr(index)
    # lines存储了邮件的原始文本的每一行,
    # 可以获得整个邮件的原始文本:
    msg_content = '
    '.join(lines)
    # 稍后解析出邮件:
    msg = Parser().parsestr(msg_content)
    # 可以根据邮件索引号直接从服务器删除邮件:
    # server.dele(index)
    # 关闭连接:
    server.quit()

      解析邮件:

      先得导入必要的模块:

    import email
    from email.parser import Parser
    from email.header import decode_header
    from email.utils import parseaddr

      然后,将邮件内容解析为Message对象:

    msg = Parser().parsestr(msg_content)

      如果解析的对象本身是个MIMEMultipart对象,就要递归地打印出Message对象的层次结构:

    # indent用于缩进显示:
    def print_info(msg, indent=0):
        if indent == 0:
            # 邮件的From, To, Subject存在于根对象上:
            for header in ['From', 'To', 'Subject']:
                value = msg.get(header, '')
                if value:
                    if header=='Subject':
                        # 需要解码Subject字符串:
                        value = decode_str(value)
                    else:
                        # 需要解码Email地址:
                        hdr, addr = parseaddr(value)
                        name = decode_str(hdr)
                        value = u'%s <%s>' % (name, addr)
                print('%s%s: %s' % ('  ' * indent, header, value))
        if (msg.is_multipart()):
            # 如果邮件对象是一个MIMEMultipart,
            # get_payload()返回list,包含所有的子对象:
            parts = msg.get_payload()
            for n, part in enumerate(parts):
                print('%spart %s' % ('  ' * indent, n))
                print('%s--------------------' % ('  ' * indent))
                # 递归打印每一个子对象:
                print_info(part, indent + 1)
        else:
            # 邮件对象不是一个MIMEMultipart,
            # 就根据content_type判断:
            content_type = msg.get_content_type()
            if content_type=='text/plain' or content_type=='text/html':
                # 纯文本或HTML内容:
                content = msg.get_payload(decode=True)
                # 要检测文本编码:
                charset = guess_charset(msg)
                if charset:
                    content = content.decode(charset)
                print('%sText: %s' % ('  ' * indent, content + '...'))
            else:
                # 不是文本,作为附件处理:
                print('%sAttachment: %s' % ('  ' * indent, content_type))

      邮件中的Subject和Email中包含的name都是经过编码后的str,要正常显示,就必须decode:

    def decode_str(s):
        value, charset = decode_header(s)[0]
        if charset:
            value = value.decode(charset)
        return value

      decode_header()返回一个list,可能包含多个邮件地址,但是这里只取了第一个邮件地址。

      另外,邮件的正文内容也是str,还需要检测编码,否则,非UTF-8编码的邮件无法正常显示:

    def guess_charset(msg):
        # 先从msg对象获取编码:
        charset = msg.get_charset()
        if charset is None:
            # 如果获取不到,再从Content-Type字段获取:
            content_type = msg.get('Content-Type', '').lower()
            pos = content_type.find('charset=')
            if pos >= 0:
                charset = content_type[pos + 8:].strip()
        return charset

      最后,在浏览器上和Python编写的POP3程序地结果:

    +OK Welcome to coremail Mail Pop3 Server (163coms[...])
    Messages: 126. Size: 27228317
    
    From: Test <xxxxxx@qq.com>
    To: Python爱好者 <xxxxxx@163.com>
    Subject: 用POP3收取邮件
    part 0
    --------------------
      part 0
      --------------------
        Text: Python可以使用POP3收取邮件……...
      part 1
      --------------------
        Text: Python可以<a href="...">使用POP3</a>收取邮件……...
    part 1
    --------------------
      Attachment: application/octet-stream

      注:本文为学习廖雪峰Python入门整理后的笔记

  • 相关阅读:
    MySql 用户 及权限操作
    MAC 重置MySQL root 密码
    在mac系统安装Apache Tomcat的详细步骤[转]
    Maven:mirror和repository 区别
    ES6 入门系列
    转场动画CALayer (Transition)
    OC 异常处理
    Foundation 框架
    Enum枚举
    Invalid App Store Icon. The App Store Icon in the asset catalog in 'xxx.app' can’t be transparent nor contain an alpha channel.
  • 原文地址:https://www.cnblogs.com/likely-kan/p/7553614.html
Copyright © 2011-2022 走看看