跳转到主要内容

插入一条消息和附件,并发送电子邮件/签名/加密内容。

项目描述

信封

Build Status Downloads

Quick layer over python-gnupg, M2Crypto, smtplib, magic and email handling packages. Their common use cases merged into a single function. Want to sign a text and tired of forgetting how to do it right? You do not need to know everything about GPG or S/MIME, you do not have to bother with importing keys. Do not hassle with reconnecting to an SMTP server. Do not study various headers meanings to let your users unsubscribe via a URL. You insert a message, attachments and inline images and receive signed and/or encrypted output to the file or to your recipients' e-mail. Just single line of code. With the great help of the examples below.

Envelope("my message")
    .subject("hello world")
    .to("example@example.com")
    .attach(file_contents, name="attached-file.txt")
    .smtp("localhost", 587, "user", "pass", "starttls")
    .signature()
    .send()
# Inline image
Envelope("My inline image: <img src='cid:image.jpg' />")
    .attach(path="image.jpg", inline=True)

# Load a message and read its attachments
Envelope.load(path="message.eml").attachments()
# in bash: envelope --load message.eml --attachments

安装

  • PyPi使用单条命令安装

    pip3 install envelope
    
    • 或者安装当前的GitHub master版本
    pip3 install git+https://github.com/CZ-NIC/envelope.git
    
    • 或者直接下载项目并运行python3 -m envelope
  • 如果您计划使用GPG进行签名/加密,请确保系统上已安装GPG,可以使用sudo apt install gpg,并可能查看配置您的GPG教程。

  • 如果您计划使用S/MIME,您应该确保一些先决条件:sudo apt install swig build-essential python3-dev libssl-dev && pip3 install M2Crypto

  • 如果您计划发送电子邮件,请准备SMTP凭证或访问配置您的SMTP教程。

  • 如果您要收到的电子邮件不在您的本地域中,请访问DMARC部分。

  • python-magic作为依赖项使用。由于与file-magic包的知名名称冲突,如果您需要使用后者,无需担心,在安装envelope后,运行pip uninstall python-magic && pip install file-magic即可。这两个项目都与底层libmagic兼容。这可能是已经安装的。然而,如果它尚未安装,请安装sudo apt install libmagic1

Bash completion

  1. 运行:apt install bash-completion jq
  2. 复制:extra/envelope-autocompletion.bash/etc/bash_completion.d/
  3. 重新启动终端

用法

例如,让我们以三种相同的方式生成一个包含GPG加密的"Hello world"内容的output_file

CLI

在终端中以CLI应用程序启动,查看envelope --help

envelope --message "Hello world" \
               --output "/tmp/output_file" \
               --from "me@example.com" \
               --to "remote_person@example.com" \
               --encrypt-path "/tmp/remote_key.asc"

模块:流畅接口

如果您的IDE支持自动完成,这是一个舒适的方式创建结构。

from envelope import Envelope
Envelope().message("Hello world")\
    .output("/tmp/output_file")\
    .from_("me@example.com")\
    .to("remote_person@example.com")\
    .encrypt(key_path="/tmp/remote_key.asc")

模块:单行函数

您可以通过将其作为模块导入来轻松编写一个单行函数,以加密您的代码或从应用程序中发送电子邮件。请参阅pydoc3 envelope或下面的文档。

from envelope import Envelope
Envelope(message="Hello world",
        output="/tmp/output_file",
        from_="me@example.com",
        to="remote_person@example.com",
        encrypt="/tmp/remote_key.asc")

文档

envelope --help(CLI参数帮助)和pydoc3 envelope(查看模块参数帮助)应包含与这里相同的信息。

命令列表

所有参数都是可选的。

  • --param在CLI中使用
  • .param(value)表示位置参数
  • .param(value=)表示关键字参数
  • Envelope(param=)是单行参数

任何可达到的内容

当提到任何可获取的内容时,我们指的是纯文本、字节或流(例如:从open())。在模块接口中,您可以使用指向文件的Path对象。在CLI接口中,提供了额外的标志。

如果对象不可访问,它将立即引发FileNotFoundError

Envelope().attach(path="file.jpg")
# Could not fetch file .../file.jpg
# FileNotFoundError: [Errno 2] No such file or directory: 'file.jpg'

输入/输出

  • message:消息/正文文本。如果没有设置字符串,则读取消息。此外,当"Content-Transfer-Encoding"设置为"base64"或"quoted-printable"时,它将被解码(当快速读取EML文件内容时很有用,例如:cat file.eml | envelope --message)。

    • --message:字符串。为空则读取。
    • --input(CLI仅限)消息文件的路径。(--message参数的替代方案。)
    • .message():在str中读取当前消息。
    • .message(text):将消息设置为任何可获取的内容
    • .message(path=None, alternative="auto", boundary=None)
      • path:文件的路径。
      • 替代: "auto", "html", "plain" 您可以指定电子邮件文本替代。一些电子邮件阅读器更喜欢显示纯文本版本而不是HTML。默认情况下,我们尝试自动确定内容类型(见 mime)。
        print(Envelope().message("He<b>llo</b>").message("Hello", alternative="plain"))
        
        # (output shortened)
        # Content-Type: multipart/alternative;
        #  boundary="===============0590677381100492396=="
        #
        # --===============0590677381100492396==
        # Content-Type: text/plain; charset="utf-8"
        # Hello
        #
        # --===============0590677381100492396==
        # Content-Type: text/html; charset="utf-8"
        # He<b>llo</b>
        
      • 边界: 当指定替代时,如果您不希望创建随机边界,您可以设置电子邮件边界。
    • .body(path=None): .message 的别名(不带 alternativeboundary 参数)
    • .text(path=None): .message 的别名(不带 alternativeboundary 参数)
    • 信封(message=): 任何可获取的内容

    设置字符串的等效方法(在 PythonBash 中)。

    Envelope(message="hello") == Envelope().message("hello")
    
    envelope --message "hello"
    

    设置文件内容的等效方法(在 PythonBash 中)。

    from pathlib import Path
    Envelope(message=Path("file.txt")) == Envelope(message=open("file.txt")) == Envelope.message(path="file.txt")
    
    envelope --input file.txt
    

    信封有时可以处理错误的编码或尝试打印出有意义的警告。

    # Issue a warning when trying to represent a mal-encoded message.
    b ="€".encode("cp1250")  # converted to bytes b'\x80'
    e = Envelope(b)
    repr(e)
    # WARNING: Cannot decode the message correctly, plain alternative bytes are not in Unicode.
    # Envelope(message="b'\x80'")
    
    # When trying to output a mal-encoded message, we end up with a ValueError exception.
    e.message()
    # ValueError: Cannot decode the message correctly, it is not in Unicode. b'\x80'
    
    # Setting up an encoding (even ex-post) solves the issue.
    e.header("Content-Type", "text/plain;charset=cp1250")
    e.message()  # '€'
    
  • 输出: 要写入的文件的路径(否则返回内容)。

    • --output
    • .output(output_file)
    • Envelope(output=)

收件人

  • from: 电子邮件 – 如果加密则需要。
    • --from 电子邮件。留空以读取值。
    • --no-from 声明我们想要加密且永不解密。
    • .from_(email): 电子邮件 | False | None。如果为 None,则返回当前 From 作为 地址 对象(即使是空的)。
    • Envelope(from_=): 发件人电子邮件或 False 以明确省略。在无发件人加密时,我们不使用他们的密钥,因此我们无法再次解密。
    # These statements are identical.
    Envelope(from_="identity@example.com")
    Envelope().from_("identity@example.com")
    
    # This statement produces both From header and Sender header.
    Envelope(from_="identity@example.com", headers=[("Sender", "identity2@example.com")])
    
    # reading an Address object
    a = Envelope(from_="identity@example.com").from_()
    a == "identity@example.com", a.host == "example.com"
    
  • to: 电子邮件或可迭代的更多电子邮件。在加密时,我们使用这些身份的密钥。可以在字符串中给出多个地址,由逗号(或分号)分隔。(同样适用于 toccbccreply-to。)
    • --to: 一个或多个电子邮件地址。留空以读取。
      $ envelope --to first@example.com second@example.com --message "hello"
      $ envelope --to
      first@example.com
      second@example.com
      
    • .to(email_or_more): 如果为 None,则返回当前 地址 列表。如果为 False 或 "",则清除当前列表。
        Envelope()
            .to("person1@example.com")
            .to("person1@example.com, John <person2@example.com>")
            .to(["person3@example.com"])
            .to()  # ["person1@example.com", "John <person2@example.com>", "person3@example.com"]
    
    • Envelope(to=): 电子邮件或可迭代的更多电子邮件。
  • cc: 电子邮件或可迭代的更多电子邮件。可以在字符串中给出多个地址,由逗号(或分号)分隔。(同样适用于 toccbccreply-to。)
    • --cc: 一个或多个电子邮件地址。留空以读取。
    • .cc(email_or_more): 如果为 None,则返回当前 地址 列表。如果为 False 或 "",则清除当前列表。
      Envelope()
          .cc("person1@example.com")
          .cc("person1@example.com, John <person2@example.com>")
          .cc(["person3@example.com"])
          .cc()  # ["person1@example.com", "John <person2@example.com>", "person3@example.com"]
      
    • Envelope(cc=)
  • bcc: 电子邮件或可迭代的更多电子邮件。可以在字符串中给出多个地址,由逗号(或分号)分隔。(同样适用于 toccbccreply-to。)头部不会发送。
    • --bcc: 一个或多个电子邮件地址。留空以读取。
    • .bcc(email_or_more): 如果为 None,则返回当前 地址 列表。如果为 False 或 "",则清除当前列表。
    • Envelope(bcc=)
  • reply-to: 电子邮件或可迭代的更多电子邮件。可以在字符串中给出多个地址,由逗号(或分号)分隔。(同样适用于 toccbccreply-to。)该字段不会被加密。
    • --reply-to: 电子邮件地址或留空以读取值。
    • .reply_to(email_or_more): 如果为 None,则返回当前 地址 列表。如果为 False 或 "",则清除当前列表。
    • Envelope(reply_to=)
  • from_addr: SMTP 信封 MAIL FROM 地址。
    • --from-addr: 电子邮件地址或留空以读取值。
    • from_addr(email):电子邮件或False。如果为None,则返回当前SMTP信封MAIL FROM作为一个地址对象(即使是空的)。
    • .Envelope(from_addr=)

发送

  • send:通过电子邮件将消息发送给收件人。在CLI中为空白时发送,或为False时打印调试信息。

    • --send
    • .send(send=True, sign=None, encrypt=None)
      • send:True立即发送。在CLI中为False(或0/false/no)时打印调试信息。
      • 返回转换成bool的对象,如果消息成功发送则返回True。
    • Envelope(send=)
    $ envelope --to "user@example.org" --message "Hello world" --send 0
    ****************************************************************************************************
    Have not been sent from - to user@example.org
    
    Content-Type: text/html; charset="utf-8"
    Content-Transfer-Encoding: 7bit
    MIME-Version: 1.0
    Subject:
    From:
    To: user@example.org
    Date: Mon, 07 Oct 2019 16:13:37 +0200
    Message-ID: <157045761791.29779.5279828659897745855@...>
    
    Hello world
    
  • subject:邮件主题。使用GPG加密,使用S/MIME时可见。

    • --subject
    • .subject(text=None, encrypt=None):
      • text 主题文本。
      • encrypt 当使用PGP加密时,使用该文本代替实际受保护的主体。False表示不加密。
      • 如果没有指定任何参数,则返回当前主题。
    • Envelope(subject=)
    • Envelope(subject_encrypted=)
  • date:

    • .date(date) str|False 指定日期头(否则自动添加日期)。如果为False,则不会自动添加日期头。
  • smtp:SMTP服务器

    • --smtp
    • .smtp(host="localhost", port=25, user=, password=, security=, timeout=3, attempts=3, delay=3, local_hostname=None)
    • Envelope(smtp=)
    • 参数
      • host 可以包含主机名或以下输入格式之一(例如:INI文件的路径或dict
      • security 如果未设置,则对于端口587自动设置为starttls,对于端口465设置为tls
      • timeout SMTP等待多长时间才超时。
      • attempts 我们尝试将消息发送到SMTP服务器的次数。
      • delay 在重新尝试超时连接之前要睡眠多少秒。
      • local_hostname HELO/EHLO命令中本地主机的FQDN。
    • 输入格式可能具有以下形式
      • None 默认使用localhost服务器
      • 标准smtplib.SMTP对象
      • listtuple 包含 host, [port, [username, password, [security, [timeout, [attempts, [delay, [local_hostname]]]]]]] 参数
        • 例如:envelope --smtp localhost 125 me@example.com 将设置主机、端口和用户名参数
      • dict 指定 {"host": ..., "port": ...}
        • 例如:envelope --smtp '{"host": "localhost"}' 将设置主机参数
      • str 主机名或INI文件的路径(现有文件,以.ini结尾,具有[SMTP]部分)
        [SMTP]
        host = example.com
        port = 587
        
    • 不要担心在循环中传递smtp,我们只与服务器建立一次连接。如果超时,我们尝试重新连接一次。
    smtp = "localhost", 25
    for mail in mails:
        Envelope(...).smtp(smtp).send()
    
  • 附件

    • --attach:附件路径,后面跟可选的文件名和/或MIME类型。此参数可以多次使用。
    envelope --attach "/tmp/file.txt" "displayed-name.txt" "text/plain" --attach "/tmp/another-file.txt"
    
    • .attach(attachment=, mimetype=, name=, path=, inline=):
      Envelope().attach(path="/tmp/file.txt").attach(path="/tmp/another-file.txt")
      
      • 指定内容时的三种不同用法
        • .attach(attachment=, mimetype=, name=):可以将单个附件的任何可获取内容放入attachment中,并可选地添加MIME类型或显示的文件名。
        • .attach(mimetype=, name=, path=):可以指定路径和可选的MIME类型或显示的文件名。
        • .attach(attachment=):可以将附件列表放入其中。该列表可以包含元组:contents [,mime type] [,file name] [, True for inline]
      • .attach(inline=True|str):指定内容ID(CID)以从HTML消息正文中引用图像。
        • True:文件名、附件或路径文件名设置为CID。
        • str:附件将获得此CID。
        from pathlib import Path
        Envelope().attach(Path("file.jpg"), inline=True) # <img src='cid:file.jpg' />
        Envelope().attach(b"GIF89a\x03\x00\x03...", name="file.gif", inline=True) # <img src='cid:file.gif' />
        Envelope().attach(Path("file.jpg"), inline="foo") # <img src='cid:foo' />
        
        # Reference it like: .message("Hey, this is an inline image: <img src='cid:foo' />")
        
    • 信封(附件=):附件或其列表。附件由 任何可获取的内容 定义,可选地与用于电子邮件的文件名、MIME 类型以及/或用于内联的 True 结合: 内容[, MIME 类型] [, 文件名] [, 内联 True]
    Envelope(attachments=[(Path("/tmp/file.txt"), "displayed-name.txt", "text/plain"), Path("/tmp/another-file.txt")])
    
    • MIME:设置内容的 MIME 子类型:“auto”(默认)、“html” 或 “plain” 用于纯文本。主类型始终设置为“text”。如果一行超过 1000 个字符,则通过字节安全地传输消息(否则这些非标准的长行可能会导致传输 SMTP 服务器包含行断和多余的空格,这可能会破坏 DKIM 签名)。如果消息中放置了 Content-Type 头部,则 MIME 部分功能将 跳过

      • --mime SUBTYPE
      • .mime(subtype="auto", nl2br="auto")
        • nl2br:如果为 True,将在 HTML 消息中的每一行断处追加 <br>。如果为 "auto",则只有在 HTML 消息中没有 <br<p 时才会更改行断。
      • Envelope(mime=)
    • headers:任何自定义头部(这些将不会与 GPG 或 S/MIME 加密)

      • --header name value(可多次使用)
      • .header(name, value=None, replace=False)
        • value 如果为 None,则返回头部或其列表(如果该头部被多次使用)。(注意,To、Cc、Bcc 和 Reply-To 头部始终返回列表。)
        • replace 如果为 True,则先删除任何具有 key 名称的头部,如果 val 为 None,则删除该头部。否则,将追加具有相同名称的另一个头部。
        Envelope().header("X-Mailer", "my-app").header("X-Mailer") # "my-app"
        Envelope().header("Generic-Header", "1") \
                  .header("Generic-Header", "2") \
                  .header("Generic-Header") # ["1", "2"]
        
      • Envelope(headers=[(name, value)])

      等效头部

      envelope --header X-Mailer my-app
      
      Envelope(headers=[("X-Mailer", "my-app")])
      Envelope().header("X-Mailer", "my-app")
      

特定头信息

这些辅助功能通过流畅接口提供。

  • .list_unsubscribe(uri=None, one_click=False, web=None, email=None):您可以指定 URL、电子邮件或两者都指定。

    • .list_unsubscribe(uri):我们尝试确定这是否是电子邮件,并在需要时添加方括号和 'https:'/'mailto:'。例如:me@example.com?subject=unsubscribeexample.com/unsubscribe<https://example.com/unsubscribe>
    • .list_unsubscribe(email=):电子邮件地址。例如:me@example.commailto:me@example.com
    • .list_unsubscribe(web=, one_click=False):指定 URL。例如:example.com/unsubscribehttp://example.com/unsubscribe。如果 one_click=True,则添加 rfc8058 List-Unsubscribe-Post 头部。这表示用户可以通过单个点击来取消订阅,该点击是通过 POST 请求实现的,以防止电子邮件扫描器错误地访问取消订阅页面。必须存在 'https' URL。
    # These will produce:
    # List-Unsubscribe: <https://example.com/unsubscribe>
    Envelope().list_unsubscribe("example.com/unsubscribe")
    Envelope().list_unsubscribe(web="example.com/unsubscribe")
    Envelope().list_unsubscribe("<https://example.com/unsubscribe>")
    
    # This will produce:
    # List-Unsubscribe: <https://example.com/unsubscribe>, <mailto:me@example.com?subject=unsubscribe>
    Envelope().list_unsubscribe("example.com/unsubscribe", mail="me@example.com?subject=unsubscribe")
    
  • .auto_submitted:

    • .auto_submitted(val="auto-replied"):由自动进程直接对另一条消息进行响应。
    • .auto_submitted.auto_generated():自动(通常是周期性)进程(如 UNIX “cron jobs”),这些进程不是对其他消息的直接响应
    • .auto_submitted.no():消息是由人类发起的
Envelope().auto_submitted()  # mark message as automatic
Envelope().auto_submitted.no()  # mark message as human produced

加密标准方法

注意,如果没有指定 gpgsmime,我们将尝试自动确定方法。

  • gpg:如果为 True,则优先选择 GPG 而不是 S/MIME 或 GNUPG 环的 home 路径(否则使用默认的 ~/.gnupg)
    • --gpg [path]
    • .gpg(gnugp_home=True)
    • Envelope(gpg=True)
  • .smime:优先选择 S/MIME 而不是 GPG
    • --smime
    • .smime()
    • Envelope(smime=True)

签名

  • sign:签名消息。
    • key 参数
      • GPG
        • 空白(CLI)或 True(模块)表示用户默认密钥
        • "auto" 用于在存在匹配 "from" 头部的密钥时启用签名
        • 密钥 ID/指纹
        • 要签名的身份的电子邮件地址
        • 任何可获取的内容与要签名的密钥(将被导入到密钥库中)
      • S/MIME: 任何可获得的内 容,需要用密钥进行签 名。可能包含签名证书。
    • –sign key:(对于 key 见上文)
    • –sign-path:包含发送者私钥的文件名。(sign 参数的替代方案。)
    • –passphrase:如果需要,为密钥提供密码。
    • –attach-key:GPG:在发送时将公钥附加到附件(留空)。
    • –cert:S/MIME:如果未包含在密钥中,证书内容。
    • –cert-path:S/MIME:如果密钥中未包含证书,包含发送者私钥的文件名。(cert 参数的替代方案。)
    • .sign(key=True, passphrase=, attach_key=False, cert=None, key_path=None):现在签名(可以指定参数)。(对于 key 见上文。)
    • .signature(key=True, passphrase=, attach_key=False, cert=None, key_path=None):稍后签名(当使用 .sign().encrypt().send() 函数时)
    • Envelope(sign=key):(对于 key 见上文)
    • Envelope(passphrase=):如果需要,为签名密钥提供密码。
    • Envelope(attach_key=):如果为真,在发送时将 GPG 公钥作为附件附加。
    • Envelope(cert=):S/MIME:任何可获得的内 容

加密

  • encrypt:要加密的接收者 GPG 公钥或 S/MIME 证书。

    • key 参数
      • GPG
        • 留空(CLI)或 True(module)以强制使用用户默认密钥(在 "from"、"to"、"cc" 和 "bcc" 标头中的身份)加密。
        • "auto" 用于开启加密,如果每个接收者都有一个匹配的密钥。
        • 密钥 ID/指纹
        • 要加密的密钥所属身份的电子邮件地址
        • 任何可获得的内 容,包含要加密的密钥(将导入到密钥库中)
        • 一个可迭代对象,包含通过密钥 ID/指纹/电子邮件地址/原始密钥数据指定的身份
      • S/MIME 任何可获得的内 容,包含要加密的证书或更多内容(在可迭代对象中)
    • –encrypt [key]:(对于 key 见上文) 将 0/false/no 输入以禁用 encrypt-path
    • –encrypt-path (CLI only):包含接收者公钥的文件名。(encrypt 参数的替代方案。)
    • .encrypt(key=True, sign=, key_path=):
      • sign 参见签名,例如:您可以指定布尔值或默认签名密钥 ID/指纹或 "auto" 用于 GPG 或包含 S/MIME 密钥 + 签名证书的 任何可获得的内 容
      • key_path:密钥/证书内容(key 参数的替代方案)
    • .encryption(key=True, key_path=):稍后加密(当使用 .sign().encrypt().send() 函数时。如果需要,在参数中指定包含 GPG 加密密钥或 S/MIME 加密证书的 任何可获得的内 容。)
    • Envelope(encrypt=key):(对于 key 见上文)
    # message gets encrypted for multiple S/MIME certificates
    envelope --smime --encrypt-path recipient1.pem recipient2.pem --message "Hello"
    
    # message gets encrypted with the default GPG key
    envelope  --message "Encrypted GPG message!" --subject "Secret subject will not be shown" --encrypt --from person@example.com --to person@example.com
    
    # message not encrypted for the sender (from Bash)
    envelope  --message "Encrypted GPG message!" --subject "Secret subject will not be shown" --encrypt receiver@example.com receiver2@example.com --from person@example.com --to receiver@example.com receiver2@example.com
    
    # message not encrypted for the sender (from Python)
    Envelope()
        .message("Encrypted GPG message!")
        .subject("Secret subject will not be shown")
        .from_("person@example.com")
        .to(("receiver@example.com", "receiver2@example.com"))
        .encrypt(("receiver@example.com", "receiver2@example.com"))
    

GPG 注意事项

  • 如果 GPG 加密失败,它将尝试确定哪个接收者缺少密钥。
  • 默认情况下,GPG 使用 from 标头接收者的密钥进行加密。
  • 当前内部忽略密钥 ID/指纹,GPG 自己决定使用哪个密钥。

支持

  • .recipients():返回所有接收者的集合 – ToCcBcc
    • .recipients(clear=True):删除所有 ToCcBcc 接收者,并返回 Envelope 对象。
  • attachments:访问附件列表。
    • –attachments [NAME] 获取附件列表或指定 NAME 的附件内容。
    • .attachments(name=None, inline=None)
      • name (str):要返回的唯一附件名称。
      • inline (bool):仅过滤内联/嵌入附件。
      • Attachment 对象具有 .name 文件名、.mimetype.data 原始数据属性
        • 如果转换为 str/bytes,则返回其原始 .data
  • .copy():返回实例的深度副本,可用于独立使用。
  factory = Envelope().cc("original@example.com").copy
  e1 = factory().to("to-1@example.com")
  e2 = factory().to("to-2@example.com").cc("additional@example.com")  #

  print(e1.recipients())  # {'to-1@example.com', 'original@example.com'}
  print(e2.recipients())  # {'to-2@example.com', 'original@example.com', 'additional@example.com'}
  • 通过 .message().subject() 读取消息和主题

  • 预览:将消息或数据作为可读文本返回字符串。例如:虽然我们必须使用 quoted-printable(如 str 中所示),但这里输出将是纯文本。

    • --preview
    • .preview()
  • 检查:检查所有电子邮件地址和SMTP连接,如果成功则返回 True/False。根据发件人的域名尝试查找 SPF、DKIM 和 DMARC DNS 记录并打印出来。

    • --check
    • .check(check_mx=True, check_smtp=True)
      • check_mx 可以检查电子邮件地址的 MX 记录,而不仅仅是它们的格式。
      • check_smtp 我们尝试连接到 SMTP 服务器。
    $ envelope --smtp localhost 25 --from me@example.com --check
    SPF found on the domain example.com: v=spf1 -all
    See: dig -t SPF example.com && dig -t TXT example.com
    DKIM found: ['v=DKIM1; g=*; k=rsa; p=...']
    Could not spot DMARC.
    Trying to connect to the SMTP...
    Check succeeded.
    
  • .as_message():生成 email.message.Message 对象。

    e = Envelope("hello").as_message()
    print(type(e), e.get_payload())  # <class 'email.message.EmailMessage'> hello\n
    

    注意:由于标准 Python 库中的一个错误(https://github.com/python/cpython/issues/99533 和 #19),当您使用这种方式访问消息并使用名称超过 34 个字符的附件进行签名时,您将丢失 GPG。

  • 加载:解析 任何可获取的内容(包括 email.message.Message),如 EML 文件,以构建 Envelope 对象。

    • 它可以解密消息并解析其(内联或附件)附件。
    • 注意:如果您将此重构的消息发送出去,您可能因为 Message-ID 重复而无法接收。在重新发送之前,至少删除 Message-ID 报头。
    • (静态) .load(message, *, path=None, key=None, cert=None, gnupg_home=None)
      • message任何可获取的内容
      • path:文件路径,是 message 的替代
      • keycert:在解密 S/MIME 消息时指定(可以捆绑在一起作为 key
      • gnupg_home:GNUPG_HOME 的路径或 None(如果应使用环境默认值)。
      Envelope.load("Subject: testing message").subject()  # "testing message"
      
    • bash
      • 允许使用空白 --subject--message 标志来显示
      • --load FILE
        $ envelope --load email.eml
        Content-Type: text/plain; charset="utf-8"
        Content-Transfer-Encoding: 7bit
        MIME-Version: 1.0
        Subject: testing message
        
        Message body
        
        $ envelope --load email.eml --subject
        testing message
        
      • (bash) 管道中的内容,envelope 可执行文件在没有参数的情况下使用
        $ echo "Subject: testing message" | envelope
        Content-Type: text/plain; charset="utf-8"
        Content-Transfer-Encoding: 7bit
        MIME-Version: 1.0
        Subject: testing message
        
        $ cat email.eml | envelope
        
        $ envelope < email.eml
        
  • smtp_quit():由于 Envelope 倾向于重用所有 SMTP 实例,您可能需要显式退出它们。要么调用 Envelope 类的此方法关闭所有缓存的连接,要么调用 Envelope 对象以仅关闭当前使用的连接。

    e = Envelope().smtp(server1).smtp(server2)
    e.smtp_quit()  # called on an instance → closes connection to `server2` only
    Envelope.smtp_quit()  # called on the class → closes both connections
    

地址

遇到的任何电子邮件地址都会内部转换为可以从中导入的 Address(str) 对象。您可以安全地访问以下 str 属性

  • .name – 真实名称
  • .address – 电子邮件地址
  • .host – 其域名
  • .user – 电子邮件的用户名部分
from envelope import Address
a = Address("John <person@example.com>")
a.name == "John", a.address == "person@example.com", a.host == "example.com", a.user == "person"

空对象也行。例如,如果 From 报头未设置,我们将得到一个空的 Address 对象。尽管如此,仍然可以安全地访问其属性。

a = Envelope.load("Empty message").from_()
bool(a) is False, a.host == ""
Address() == Address("") == "", Address().address == ""

方法 .casefold() 返回 casefolded Address 对象,这对于与字符串进行比较很有用,而与其他 Address 对象的比较会自动进行 casefold。

a = Address("John <person@example.com>")
c = a.casefold()
a is not c, a == c, a.name == "john", a.name != c.name

方法 .is_valid(check_mx=False) 如果格式有效则返回布尔值。当 check_mx 设置为 True 时,也会查询 MX 服务器。

由于 Addressstr 的子类,因此您可以安全地连接此类对象。

", ".join([a, a]) # "John <person@example.com>, "John <person@example.com>"
a + " hello"  #  "John <person@example.com> hello"

如果它们的电子邮件地址相同,则 Address 对象相等。(它们的真实名称可能不同。)如果字符串包含其电子邮件地址或整个表示,则 Address 对象等于字符串。

"person@example.com" == Address("John <person@example.com>") == "John <person@example.com>"  # True

关于 toccbccreply-to,可以在字符串中提供多个地址,用逗号(或分号)分隔。可以在 Address 对象上调用 .get(address:bool, name:bool) 方法来过滤所需信息。

e = (Envelope()
    .to("person1@example.com")
    .to("person1@example.com, John <person2@example.com>")
    .to(["person3@example.com"]))

[str(x) for x in e.to()]                # ["person1@example.com", "John <person2@example.com>", "person3@example.com"]
[x.get(address=False) for x in e.to()]  # ["", "John", ""]
[x.get(name=True) for x in e.to()]      # ["person1@example.com", "John", "person3@example.com"]
                                        # return an address if no name given
[x.get(address=True) for x in e.to()]   # ["person1@example.com", "person2@example.com", "person3@example.com"]
                                        # addresses only

对于一些异乎寻常的情况,Address 通常比底层的标准库更好地执行解析任务(参见 2004 年的 错误报告)。

from email.utils import parseaddr
from envelope import Address
parseaddr("alice@example.com <bob@example.malware>")
# ('', 'alice@example.com') -> empty name and wrong address
Address("alice@example.com <bob@example.malware>").address
# 'bob@example.malware' -> the right address

实验性

由于我们倾向于保持API简单并尽可能减少向后不兼容的更改,因此很难确定正确的方法。欢迎您提出建议!以下方法没有稳定的API,因此它们的名称以下划线开头。

  • _report():访问 multipart/report

目前仅支持 XARF。您可以直接访问字段,无需任何额外的 json 解析。

if xarf := Envelope.load(path="xarf.eml")._report():
  print(xarf['SourceIp'])  # '192.0.2.1'

信封对象

将对象转换为字符串或布尔值

在成功签名、加密或发送后,对象解析为 True,签名文本/生成的电子邮件可以通过 str() 获取。

o = Envelope("message", sign=True)
str(o)  # signed text
bool(o)  # True

对象相等性

信封对象等于一个 strbytes 或另一个 Envelope,如果它们的 bytes 相同。

# Envelope objects are equal
sign = {"message": "message", "sign": True}
Envelope(**sign) == Envelope(**sign)  # True
bytes(Envelope(**sign))  # because their bytes are the same
# b'-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA512\n\nmessage\n-----BEGIN PGP SIGNATURE-----\n\niQEzBAEBCgAdFiE...\n-----END PGP SIGNATURE-----\n'

# however, result of a PGP encrypting produces always a different output
encrypt = {"message": "message", "encrypt": True, "from_": False, "to": "person@example.com"}
Envelope(**encrypt) != Envelope(**encrypt)  # Envelope objects are not equal

示例

签名和加密

签名消息。

Envelope(message="Hello world", sign=True)

使用标准 pathlib 库从文件中加载消息并签名。

from pathlib import Path
Envelope(message=Path("/tmp/message.txt"), sign=True)

从文件流中获取消息并签名。

with open("/tmp/message.txt") as f:
    Envelope(message=f, sign=True)

签名并加密消息,以便可以使用 me@example.comremote_person@example.com 的密钥(应已加载到密钥环中)进行解密。

Envelope(message="Hello world", sign=True,
        encrypt=True,
        from_="me@example.com",
        to="remote_person@example.com")

签名并加密消息,以便可以使用 me@example.comremote_person@example.com 的密钥(从文件中导入到密钥环)进行解密。

Envelope(message="Hello world", sign=True,
        encrypt=Path("/tmp/remote_key.asc"),
        from_="me@example.com",
        to="remote_person@example.com")

通过不同的密钥环签名消息。

Envelope(message="Hello world", sign=True, gnupg="/tmp/my-keyring/")

使用需要密码的密钥签名消息。

Envelope(message="Hello world", sign=True, passphrase="my-password")

使用默认启用签名并具有默认密钥环路径的密钥签名消息。每次 factory 调用都将遵守这些默认值。

factory = Envelope().signature(True).gpg("/tmp/my-keyring").copy
factory().(message="Hello world")

发送

通过模块调用发送电子邮件。

Envelope(message="Hello world", send=True)

通过 CLI 和默认 SMTP 服务器 localhost 的端口 25 发送电子邮件。

envelope --to "user@example.org" --message "Hello world" --send

指定 SMTP 服务器的主机、端口、用户名和密码时发送。

envelope --to "user@example.org" message "Hello world" --send --smtp localhost 123 username password

通过字典指定 SMTP 服务器时发送。

envelope --to "user@example.org" --message "Hello world" --send --smtp '{"host": "localhost", "port": "123"}'

通过模块调用指定 SMTP 服务器时发送。

Envelope(message="Hello world", to="user@example.org", send=True, smtp={"host":"localhost"})

附件

您可以通过许多不同的方式附加文件。选择最适合您的方法。

Envelope(attachment=Path("/tmp/file.txt"))  # file name will be 'file.txt'

with open("/tmp/file.txt") as f:
    Envelope(attachment=f)  # file name will be 'file.txt'

with open("/tmp/file.txt") as f:
    Envelope(attachment=(f, "filename.txt"))

Envelope().attach(path="/tmp/file.txt", name="filename.txt")

内联图片

您需要做的唯一事情是将附件的 inline=True 参数设置。然后,您可以在消息内部使用 cid 关键字引用图像。有关更多详细信息,请参阅 发送 部分的 附件

(Envelope()
    .attach(path="/tmp/file.jpg", inline=True)
    .message("Hey, this is an inline image: <img src='cid:file.jpg' />"))

复杂示例

通过默认 SMTP 服务器,通过所有三个接口发送加密并签名的消息(GPG)。

# CLI interface
envelope --message "Hello world" --from "me@example.org" --to "user@example.org" --subject "Test" --sign --encrypt -a /tmp/file.txt -a /tmp/file2 application/gzip zipped-file.zip --send
from pathlib import Path
from envelope import Envelope

# fluent interface
Envelope().message("Hello world").from_("me@example.org").to("user@example.org").subject("Test").signature().encryption().attach(path="/tmp/file.txt").attach(Path("/tmp/file2"), "application/gzip", "zipped-file.zip").send()

# one-liner interface
Envelope("Hello world", "me@example.org", "user@example.org", "Test", sign=True, encrypt=True, attachments=[(Path("/tmp/file.txt"), (Path("/tmp/file2"), "application/gzip", "zipped-file.zip")], send=True)

me@example.com 签名私钥、user@example.com 加密公钥和本地主机 localhost:25 上的开放 SMTP 服务器都可用的情况下,将 --send 更改为 --send 0(或将 .send() 更改为 .send(False) 或将 send=True 更改为 send=False),以调查可能类似于以下输出的生成消息

****************************************************************************************************
Have not been sent from me@example.org to user@example.org
Encrypted subject: Test
Encrypted message: b'Hello world'

Subject: Encrypted message
MIME-Version: 1.0
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
 boundary="===============8462917939563016793=="
From: me@example.org
To: user@example.org
Date: Tue, 08 Oct 2019 16:16:18 +0200
Message-ID: <157054417817.4405.938581433237601455@promyka>

--===============8462917939563016793==
Content-Type: application/pgp-encrypted

Version: 1
--===============8462917939563016793==
Content-Type: application/octet-stream; name="encrypted.asc"
Content-Description: OpenPGP encrypted message
Content-Disposition: inline; filename="encrypted.asc"

-----BEGIN PGP MESSAGE-----

hQMOAyx1c9zl1h4wEAv+PmtwjQDt+4XCn8YQJ6d7kyrp2R7xzS3PQwOZ7e+HWJjY
(...)
RQ8QtLLEza+rs+1lgcPgdBZEHFpYpgDb0AUvYg9d
=YuqI
-----END PGP MESSAGE-----

--===============8462917939563016793==--

相关事项

发送电子邮件并不意味着它会被接收。通过您的本地域成功发送它并不意味着公共邮箱会接受它。如果您不够可靠,您的电子邮件甚至可能不会出现在收件人的垃圾邮件中,它可能未经通知就被丢弃。

配置您的SMTP

如果您在应用程序能够发送电子邮件的 SMTP 服务器上有账户,这总是更容易。如果不是这种情况,存在各种 SMTP 服务器,但作为一个快速且不安全的解决方案,我测试了 bytemark/smtp Docker 映像,它允许您通过单行启动 SMTP 服务器。

docker run --network=host --restart always -d bytemark/smtp   # starts open port 25 on localhost
envelope --message "SMTP test" --from [your e-mail] --to [your e-mail] --smtp localhost 25 --send

选择加密方法

配置您的GPG

为了签名消息,您需要一个私钥。让我们假设一个用例,当您的应用程序在 www-data 用户下运行时,并通过位于:/var/www/.gnupg 的密钥进行 GPG 签名。您有一个 SMTP 服务器,应用程序可以使用电子邮件账户。

ls -l $(tty)  # see current TTY owner
sudo chown www-data $(tty)  # if creating the key for a different user and generation fails, changing temporarily the ownership of the terminal might help (when handling passphrase, the agent opens the controlling terminal rather than using stdin/stdout for security purposes)
GNUPGHOME=/var/www/.gnupg sudo -H -u www-data gpg --full-generate-key  # put application e-mail you are able to send e-mails from
# sudo chown [USER] $(tty)  # you may set back the TTY owner
GNUPGHOME=/var/www/.gnupg sudo -H -u www-data gpg --list-secret-keys  # get key ID
GNUPGHOME=/var/www/.gnupg sudo -H -u www-data gpg --send-keys [key ID]  # now the world is able to pull the key from a global webserver when they receive an e-mail from you
GNUPGHOME=/var/www/.gnupg sudo -H -u www-data gpg --export [APPLICATION_EMAIL] | curl -T - https://keys.openpgp.org  # prints out the link you can verify your key with on `keys.openpgp.org` (ex: used by default by Thunderbird Enigmail; standard --send-keys method will not verify the identity information here, hence your e-mail would not be searchable)
GNUPGHOME=/var/www/.gnupg sudo -H -u www-data envelope --message "Hello world" --subject "GPG signing test" --sign [key ID] --from [application e-mail] --to [your e-mail] --send  # you now receive e-mail and may import the key and set the trust to the key

密钥传播需要数小时。如果密钥因服务器上找不到而无法导入您的电子邮件客户端,请早上再次尝试或检查在线搜索表单:http://hkps.pool.sks-keyservers.net。将您的指纹放在网页或名片上,以便每个人都可检查您的签名是否有效。

配置您的S/MIME

如果您应该使用S/MIME,您可能会被告知从哪里获取您的密钥和证书。如果您计划自己尝试,请生成您的certificate.pem

  • 要么:您有私钥吗?
openssl req -key YOUR-KEY.pem -nodes -x509 -days 365 -out certificate.pem  # will generate privkey.pem alongside
  • 要么:您没有私钥吗?
openssl req -newkey rsa:1024 -nodes -x509 -days 365 -out certificate.pem  # will generate privkey.pem alongside

现在,您可以使用您的密钥和证书签名一条消息。(然而,消息将不可信,因为没有权威机构签署了证书。)将证书给您的朋友,以便他们可以验证消息是否来自您。从朋友那里接收证书,以便使用它加密消息。

envelope --message "Hello world" --subject "S/MIME signing test" --sign-path [key file] --cert-path [certificate file] --from [application e-mail] --to [your e-mail] --send # you now receive e-mail

DNS验证工具

这只是一个关于这些反垃圾邮件机制的简要说明,以便您了解基本情况。

每次,接收者都应该通过DNS向发件人的域提出这些问题。

SPF

接收者询问发件人的域:您是否允许发送者的IP/域名代表您发送电子邮件?邮件起源的IP/域名是否在SMTP信封MAIL FROM地址域的DNS中列为有效?

检查您的域名是否在SPF上

dig -t TXT example.com

SPF技术与指定的.from_addr方法的SMTP信封MAIL FROM地址相关联,然后由接收服务器存储到Return-Path头中,它与像From .from_、Reply-To .reply_to或Sender .header("Sender")这样的头没有任何共同之处。

DKIM

接收者询问发件人的域:给我公钥,以便我可以检查电子邮件头中的哈希值,该哈希值声称消息是由您的私钥编写的。这样,电子邮件就会从您那里可信地发出,并且在途中没有人修改过它。

检查您的域名是否在DKIM上

dig -t TXT [selector]._domainkey.example.com

您可以从收到的电子邮件消息中获取selector。检查DKIM-Signature行及其s参数的值。

DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=example.com; s=default;

DMARC

您关于SPF和DKIM的政策是什么?您有哪些滥用地址?

检查您的域名是否在DMARC上

dig -t TXT _dmarc.example.com

项目详情


下载文件

下载您平台对应的文件。如果您不确定选择哪个,请了解更多关于安装包的信息。

源分发

envelope-2.0.5.tar.gz (79.6 kB 查看哈希值)

上传时间

由以下组织支持