• Stars
    star
    171
  • Rank 222,266 (Top 5 %)
  • Language
    Python
  • License
    Other
  • Created about 5 years ago
  • Updated 3 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Insert a message and attachments and send e-mail / sign / encrypt contents by a single line.

Envelope

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("[email protected]")
    .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

Installation

  • Install with a single command from PyPi

    pip3 install envelope
    • Or install current GitHub master
    pip3 install git+https://github.com/CZ-NIC/envelope.git
    • Or just download the project and launch python3 -m envelope
  • If planning to sign/encrypt with GPG, assure you have it on the system with sudo apt install gpg and possibly see Configure your GPG tutorial.

  • If planning to use S/MIME, you should ensure some prerequisites: sudo apt install swig build-essential python3-dev libssl-dev && pip3 install M2Crypto

  • If planning to send e-mails, prepare SMTP credentials or visit Configure your SMTP tutorial.

  • If your e-mails are to be received outside your local domain, visit DMARC section.

  • Package python-magic is used as a dependency. Due to a well-known name clash with the file-magic package, in case you need to use the latter, don't worry to run pip uninstall python-magic && pip install file-magic after installing envelope which is fully compatible with both projects.

Bash completion

  1. Run: apt install bash-completion jq
  2. Copy: extra/envelope-autocompletion.bash to /etc/bash_completion.d/
  3. Restart terminal

Usage

As an example, let's produce in three equal ways an output_file with the GPG-encrypted "Hello world" content.

CLI

Launch as a CLI application in terminal, see envelope --help

envelope --message "Hello world" \
               --output "/tmp/output_file" \
               --from "[email protected]" \
               --to "[email protected]" \
               --encrypt-path "/tmp/remote_key.asc"

Module: fluent interface

Comfortable way to create the structure if your IDE supports autocompletion.

from envelope import Envelope
Envelope().message("Hello world")\
    .output("/tmp/output_file")\
    .from_("[email protected]")\
    .to("[email protected]")\
    .encrypt(key_path="/tmp/remote_key.asc")

Module: one-liner function

You can easily write a one-liner function that encrypts your code or sends an e-mail from within your application when imported as a module. See pydoc3 envelope or documentation below.

from envelope import Envelope
Envelope(message="Hello world",
        output="/tmp/output_file",
        from_="[email protected]",
        to="[email protected]",
        encrypt="/tmp/remote_key.asc")

Documentation

Both envelope --help for CLI arguments help and pydoc3 envelope to see module arguments help should contain same information as here.

Command list

All parameters are optional.

  • --param is used in CLI
  • .param(value) denotes a positional argument
  • .param(value=) denotes a keyword argument
  • Envelope(param=) is a one-liner argument

Any attainable contents

Whenever any attainable contents is mentioned, we mean plain text, bytes or stream (ex: from open()). In module interface, you may use a Path object to the file. In CLI interface, additional flags are provided instead.

If the object is not accesible, it will immediately raise FileNotFoundError.

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

Input / Output

  • message: Message / body text. If no string is set, message gets read. Besides, when "Content-Transfer-Encoding" is set to "base64" or "quoted-printable", it gets decoded (useful when quickly reading an EML file content cat file.eml | envelope --message).

    • --message: String. Empty to read.
    • --input: (CLI only) Path to the message file. (Alternative to the --message parameter.)
    • .message(): Read current message in str.
    • .message(text): Set the message to any attainable contents.
    • .message(path=None, alternative="auto", boundary=None)
      • path: Path to the file.
      • alternative: "auto", "html", "plain" You may specify e-mail text alternative. Some e-mail readers prefer to display plain text version over HTML. By default, we try to determine content type automatically (see 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>
      • boundary: When specifying alternative, you may set e-mail boundary if you do not wish a random one to be created.
    • .body(path=None): Alias of .message (without alternative and boundary parameter)
    • .text(path=None): Alias of .message (without alternative and boundary parameter)
    • Envelope(message=): Any attainable contents

    Equivalents for setting a string (in Python and in Bash).

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

    Equivalents for setting contents of a file (in Python and in Bash).

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

    Envelope is sometimes able to handle wrong encoding or tries to print out a meaningful warning.

    # 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: Path to file to be written to (else the contents is returned).

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

Recipients

  • from: E-mail – needed to choose our key if encrypting.
    • --from E-mail. Empty to read value.
    • --no-from Declare we want to encrypt and never decrypt back.
    • .from_(email): E-mail | False | None. If None, current From returned as an Address object (even an empty one).
    • Envelope(from_=): Sender e-mail or False to explicitly omit. When encrypting without sender, we do not use their key so that we will not be able to decipher again.
    # These statements are identical.
    Envelope(from_="[email protected]")
    Envelope().from_("[email protected]")
    
    # This statement produces both From header and Sender header.
    Envelope(from_="[email protected]", headers=[("Sender", "[email protected]")])
    
    # reading an Address object
    a = Envelope(from_="[email protected]").from_()
    a == "[email protected]", a.host == "example.com"
  • to: E-mail or more in an iterable. When encrypting, we use keys of these identities. Multiple addresses may be given in a string, delimited by a comma (or semicolon). (The same is valid for to, cc, bcc and reply-to.)
    • Envelope(to=): E-mail or more in an iterable.
  • cc: E-mail or more in an iterable. Multiple addresses may be given in a string, delimited by a comma (or semicolon). (The same is valid for to, cc, bcc and reply-to.)
  • bcc: E-mail or more in an iterable. Multiple addresses may be given in a string, delimited by a comma (or semicolon). (The same is valid for to, cc, bcc and reply-to.) The header is not sent.
    • --bcc: One or more e-mail addresses. Empty to read.
    • .bcc(email_or_more): If None, current list of Addresses returned. If False or "", current list is cleared.
    • Envelope(bcc=)
  • reply-to: E-mail or more in an iterable. Multiple addresses may be given in a string, delimited by a comma (or semicolon). (The same is valid for to, cc, bcc and reply-to.) The field is not encrypted.
    • --reply-to: E-mail address or empty to read value.
    • .reply_to(email_or_more): If None, current list of Addresses returned. If False or "", current list is cleared.
    • Envelope(reply_to=)
  • from_addr: SMTP envelope MAIL FROM address.
    • --from-addr: E-mail address or empty to read value.
    • .from_addr(email): E-mail or False. If None, current SMTP envelope MAIL FROM returned as an Address object (even an empty one).
    • .Envelope(from_addr=)

Sending

  • send: Send the message to the recipients by e-mail. True (blank in CLI) to send now or False to print out debug information.

    • --send
    • .send(send=True, sign=None, encrypt=None)
      • send: True to send now. False (or 0/false/no in CLI) to print debug information.
      • Returns the object back which converted to bool returns True if the message has been sent successfully.
    • Envelope(send=)
    $ envelope --to "[email protected]" --message "Hello world" --send 0
    ****************************************************************************************************
    Have not been sent from - to [email protected]
    
    Content-Type: text/html; charset="utf-8"
    Content-Transfer-Encoding: 7bit
    MIME-Version: 1.0
    Subject:
    From:
    To: [email protected]
    Date: Mon, 07 Oct 2019 16:13:37 +0200
    Message-ID: <157045761791.29779.5279828659897745855@...>
    
    Hello world
  • subject: Mail subject. Gets encrypted with GPG, stays visible with S/MIME.

    • --subject
    • .subject(text=None, encrypt=None):
      • text Subject text.
      • encrypt Text used instead of the real protected subject while PGP encrypting. False to not encrypt.
      • If neither parameter specified, current subject returned.
    • Envelope(subject=)
    • Envelope(subject_encrypted=)
  • date:

    • .date(date) str|False Specify Date header (otherwise Date is added automatically). If False, the Date header will not be added automatically.
  • smtp: SMTP server

    • --smtp
    • .smtp(host="localhost", port=25, user=, password=, security=, timeout=3, attempts=3, delay=3)
    • Envelope(smtp=)
    • Parameters:
      • host May include hostname or any of the following input formats (ex: path to an INI file or a dict)
      • security If not set, automatically set to starttls for port 587 and to tls for port 465
      • timeout How many seconds should SMTP wait before timing out.
      • attempts How many times we try to send the message to an SMTP server.
      • delay How many seconds to sleep before re-trying a timed out connection.
    • Input format may be in the following form:
      • None default localhost server used
      • smtplib.SMTP object
      • list or tuple having host, [port, [username, password, [security, [timeout, [attempts, [delay]]]]]] parameters
        • ex: envelope --smtp localhost 125 [email protected] will set up host, port and username parameters
      • dict specifying {"host": ..., "port": ...}
        • ex: envelope --smtp '{"host": "localhost"}' will set up host parameter
      • str hostname or path to an INI file (existing file, ending at .ini, with the section [SMTP])
        [SMTP]
        host = example.com
        port = 587
    • Do not fear to pass the smtp in a loop, we make just a single connection to the server. If timed out, we attempt to reconnect once.
    smtp = localhost, 25
    for mail in mails:
        Envelope(...).smtp(smtp).send()
  • attachments

    • --attach: Path to the attachment, followed by optional file name to be used and/or mime type. This parameter may be used multiple times.
    envelope --attachment "/tmp/file.txt" "displayed-name.txt" "text/plain" --attachment "/tmp/another-file.txt"
    • .attach(attachment=, mimetype=, name=, path=, inline=):
      Envelope().attach(path="/tmp/file.txt").attach(path="/tmp/another-file.txt")
      • Three different usages when specifying contents:
        • .attach(attachment=, mimetype=, name=): You can put any attainable contents of a single attachment into attachment and optionally add mime type or displayed file name.
        • .attach(mimetype=, name=, path=): You can specify path and optionally mime type or displayed file name.
        • .attach(attachment=): You can put a list of attachments. The list may contain tuples: contents [,mime type] [,file name] [, True for inline].
      • .attach(inline=True|str): Specify content-id (CID) to reference the image from within HTML message body.
        • True: Filename or attachment or path file name is set as CID.
        • str: The attachment will get this 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' />")
    • Envelope(attachments=): Attachment or their list. Attachment is defined by any attainable contents, optionally in tuple with the file name to be used in the e-mail and/or mime type and/or True for being inline: contents [,mime type] [,file name] [, True for inline]
    Envelope(attachments=[(Path("/tmp/file.txt"), "displayed-name.txt", "text/plain"), Path("/tmp/another-file.txt")])
    • mime: Sets contents mime subtype: "auto" (default), "html" or "plain" for plain text. Maintype is always set to "text". Set maintype to "text". If a line is longer than 1000 characters, makes the message be transferred safely by bytes (otherwise these non-standard long lines might cause a transferring SMTP server to include line breaks and redundant spaces that might break up ex: DKIM signature). In case of Content-Type header put to the message, mime section functionality is skipped.

      • --mime SUBTYPE
      • .mime(subtype="auto", nl2br="auto")
        • nl2br: True will append <br> to every line break in the HTML message. "auto": line breaks are changed only if there is no <br or <p in the HTML message,
      • Envelope(mime=)
    • headers: Any custom headers (these will not be encrypted with GPG nor S/MIME)

      • --header name value (may be used multiple times)
      • .header(name, value=None, replace=False)
        • value If None, returns value of the header or its list if the header was used multiple times. (Note that To, Cc, Bcc and Reply-To headers always return list.)
        • replace If True, any header of the key name are removed first and if val is None, the header is deleted. Otherwise another header of the same name is appended.
        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)])

      Equivalent headers:

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

Specific headers

These helpers are available via fluent interface.

  • .list_unsubscribe(uri=None, one_click=False, web=None, email=None): You can specify either url, email or both.

    • .list_unsubscribe(uri): We try to determine whether this is e-mail and prepend brackets and 'https:'/'mailto:' if needed. Ex: [email protected]?subject=unsubscribe, example.com/unsubscribe, <https://example.com/unsubscribe>
    • .list_unsubscribe(email=): E-mail address. Ex: [email protected], mailto:[email protected]
    • .list_unsubscribe(web=, one_click=False): Specify URL. Ex: example.com/unsubscribe, http://example.com/unsubscribe. If one_click=True, rfc8058 List-Unsubscribe-Post header is added. This says user can unsubscribe with a single click that is realized by a POST request in order to prevent e-mail scanner to access the unsubscribe page by mistake. A 'https' url must be present.
    # 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:[email protected]?subject=unsubscribe>
    Envelope().list_unsubscribe("example.com/unsubscribe", mail="[email protected]?subject=unsubscribe")
  • .auto_submitted:

    • .auto_submitted(val="auto-replied"): Direct response to another message by an automatic process.
    • .auto_submitted.auto_generated(): automatic (often periodic) processes (such as UNIX "cron jobs") which are not direct responses to other messages
    • .auto_submitted.no(): message was originated by a human
Envelope().auto_submitted()  # mark message as automatic
Envelope().auto_submitted.no()  # mark message as human produced

Cipher standard method

Note that if neither gpg nor smime is specified, we try to determine the method automatically.

  • gpg: True to prefer GPG over S/MIME or home path to GNUPG rings (otherwise default ~/.gnupg is used)
    • --gpg [path]
    • .gpg(gnugp_home=True)
    • Envelope(gpg=True)
  • .smime: Prefer S/MIME over GPG
    • --smime
    • .smime()
    • Envelope(smime=True)

Signing

  • sign: Sign the message.
    • key parameter
      • GPG:
        • Blank (CLI) or True (module) for user default key
        • "auto" for turning on signing if there is a key matching to the "from" header
        • key ID/fingerprint
        • e-mail address of the identity whose key is to be signed with
        • Any attainable contents with the key to be signed with (will be imported into keyring)
      • S/MIME: Any attainable contents with key to be signed with. May contain signing certificate as well.
    • --sign key: (for key see above)
    • --sign-path: Filename with the From's private key. (Alternative to the sign parameter.)
    • --passphrase: Passphrase to the key if needed.
    • --attach-key: GPG: Blank for appending public key to the attachments when sending.
    • --cert: S/MIME: Certificate contents if not included in the key.
    • --cert-path: S/MIME: Filename with the From's private cert if cert not included in the key. (Alternative to the cert parameter.)
    • .sign(key=True, passphrase=, attach_key=False, cert=None, key_path=None): Sign now (and you may specify the parameters). (For key see above.)
    • .signature(key=True, passphrase=, attach_key=False, cert=None, key_path=None): Sign later (when launched with .sign(), .encrypt() or .send() functions
    • Envelope(sign=key): (for key see above)
    • Envelope(passphrase=): Passphrase to the signing key if needed.
    • Envelope(attach_key=): If true, append GPG public key as an attachment when sending.
    • Envelope(cert=): S/MIME: Any attainable contents

Encrypting

  • encrypt: Recipient GPG public key or S/MIME certificate to be encrypted with.

    • key parameter
      • GPG:
        • Blank (CLI) or True (module) to force encrypt with the user default keys (identities in the "from", "to", "cc" and "bcc" headers)
        • "auto" for turning on encrypting if there is a matching key for every recipient
        • key ID/fingerprint
        • e-mail address of the identity whose key is to be encrypted with
        • Any attainable contents with the key to be encrypted with (will be imported into keyring)
        • an iterable with the identities specified by key ID / fingerprint / e-mail address / raw key data
      • S/MIME any attainable contents with a certificate to be encrypted with or more in an iterable
    • --encrypt [key]: (for key see above) Put 0/false/no to disable encrypt-path.
    • --encrypt-path (CLI only): Filename(s) with the recipient's public key(s). (Alternative to the encrypt parameter.)
    • .encrypt(key=True, sign=, key_path=):
      • sign See signing, ex: you may specify boolean or default signing key ID/fingerprint or "auto" for GPG or any attainable contents with an S/MIME key + signing certificate.
      • key_path: Key/certificate contents (alternative to the key parameter)
    • .encryption(key=True, key_path=): Encrypt later (when launched with .sign(), .encrypt() or .send() functions. If needed, in the parameters specify any attainable contents with GPG encryption key or S/MIME encryption certificate.
    • Envelope(encrypt=key): (for key see above)
    # 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 [email protected] --to [email protected]
    
    # message not encrypted for the sender (from Bash)
    envelope  --message "Encrypted GPG message!" --subject "Secret subject will not be shown" --encrypt [email protected] [email protected] --from [email protected] --to [email protected] [email protected]
    # message not encrypted for the sender (from Python)
    Envelope()
        .message("Encrypted GPG message!")
        .subject("Secret subject will not be shown")
        .from_("[email protected]")
        .to(("[email protected]", "[email protected]"))
        .encrypt(("[email protected]", "[email protected]"))

GPG notes

  • If the GPG encryption fails, it tries to determine which recipient misses the key.
  • By default, GPG encrypts with the key of the from header recipient too.
  • Key ID/fingerprint is internally ignored right now, GPG decides itself which key is to be used.

Supportive

  • .recipients(): Return set of all recipients – To, Cc, Bcc
    • .recipients(clear=True): All To, Cc and Bcc recipients are removed and the Envelope object is returned.
  • attachments: Access the list of attachments.
    • --attachments [NAME] Get the list of attachments or a contents of the one specified by NAME
    • .attachments(name=None, inline=None)
      • name (str): The name of the only desired attachment to be returned.
      • inline (bool): Filter inline/enclosed attachments only.
      • Attachment object has the attributes .name file name, .mimetype, .data raw data
        • if casted to str/bytes, its raw .data are returned
  • .copy(): Return deep copy of the instance to be used independently.
  factory = Envelope().cc("[email protected]").copy
  e1 = factory().to("[email protected]")
  e2 = factory().to("[email protected]").cc("[email protected]")  #

  print(e1.recipients())  # {'[email protected]', '[email protected]'}
  print(e2.recipients())  # {'[email protected]', '[email protected]', '[email protected]'}
  • Read message and subject by .message() and .subject()

  • preview: Returns the string of the message or data as a human-readable text. Ex: whilst we have to use quoted-printable (as seen in str), here the output will be plain text.

    • --preview
    • .preview()
  • check: Check all e-mail addresses and SMTP connection and return True/False if succeeded. Tries to find SPF, DKIM and DMARC DNS records depending on the From's domain and print them out.

    • --check
    • .check(check_mx=True, check_smtp=True)
      • check_mx E-mail addresses can be checked for MX record, not only for their format.
      • check_smtp We try to connect to the SMTP host.
    $ envelope --smtp localhost 25 --from [email protected] --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(): Generates an email.message.Message object.

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

    Note: due to a bug in a standard Python library python/cpython#99533 and #19 you void GPG when you access the message this way wihle signing an attachment with a name longer than 34 chars.

  • load: Parse any attainable contents (including email.message.Message) like an EML file to build an Envelope object.

    • It can decrypt the message and parse its (inline or enclosed) attachments.
    • Note that if you will send this reconstructed message, you might not probably receive it due to the Message-ID duplication. Delete at least Message-ID header prior to re-sending.
    • (static) .load(message, *, path=None, key=None, cert=None, gnupg_home=None)
      • message: Any attainable contents
      • path: Path to the file, alternative to the message
      • key, cert: Specify when decrypting an S/MIME message (may be bundled together to the key)
      • gnupg_home: Path to the GNUPG_HOME or None if the environment default should be used.
      Envelope.load("Subject: testing message").subject()  # "testing message"
    • bash
      • allows use blank --subject or --message flags to display the
      • --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) piped in content, envelope executable used with no argument
        $ 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(): As Envelope tends to re-use all the SMTP instances, you may want to quit them explicitly. Either call this method to the Envelope class to close all the cached connections or to an Envelope object to close only the connection it currently uses.

    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

Any e-mail address encountered is internally converted to an Address(str) object that can be imported from the envelope package. You can safely access following str properties:

  • .name – the real name
  • .address – the e-mail address
  • .host – its domain
  • .user – the user name part of the e-mail
from envelope import Address
a = Address("John <[email protected]>")
a.name == "John", a.address == "[email protected]", a.host == "example.com", a.user == "person"

Empty object works too. For example, if the From header is not set, we get an empty Address object. Still it is safe to access its properties.

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

Method .casefold() returns casefolded Address object which is useful for comparing with strings whereas comparing with other Address object casefolds automatically

a = Address("John <[email protected]>")
c = a.casefold()
a is not c, a == c, a.name == "john", a.name != c.name

Method .is_valid(check_mx=False) returns boolean if the format is valid. When check_mx set to True, MX server is inquired too.

Since the Address is a subclass of str, you can safely join such objects.

", ".join([a, a]) # "John <[email protected]>, "John <[email protected]>"
a + " hello"  #  "John <[email protected]> hello"

Address objects are equal if their e-mail address are equal. (Their real names might differ.) Address object is equal to a string if the string contains its e-mail address or the whole representation.

"[email protected]" == Address("John <[email protected]>") == "John <[email protected]>"  # True

Concerning to, cc, bcc and reply-to, multiple addresses may always be given in a string, delimited by comma (or semicolon). The .get(address:bool, name:bool) method may be called on an Address object to filter the desired information.

e = (Envelope()
    .to("[email protected]")
    .to("[email protected], John <[email protected]>")
    .to(["[email protected]"]))

[str(x) for x in e.to()]                # ["[email protected]", "John <[email protected]>", "[email protected]"]
[x.get(address=False) for x in e.to()]  # ["", "John", ""]
[x.get(name=True) for x in e.to()]      # ["[email protected]", "John", "[email protected]"]
                                        # return an address if no name given
[x.get(address=True) for x in e.to()]   # ["[email protected]", "[email protected]", "[email protected]"]
                                        # addresses only

For some exotic cases, Address tends to do the parsing job better than the underlying standard library (see the bug report from 2004).

from email.utils import parseaddr
from envelope import Address
parseaddr("[email protected] <[email protected]>")
# ('', '[email protected]') -> empty name and wrong address
Address("[email protected] <[email protected]>").address
# '[email protected]' -> the right address

Experimental

Since we tend to keep the API simple and do the least amount of backward incompatible changes, it is hard to decide the right way. Your suggestions are welcome! Following methods have no stable API, hence their name begins with an underscore.

  • _report(): Accessing multipart/report.

Currently only XARF is supported in the moment. You may directly access the fields, without any additional json parsing.

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

Envelope object

Converting object to str or bool

When successfully signing, encrypting or sending, object is resolvable to True and signed text / produced e-mail could be obtained via str().

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

Object equality

Envelope object is equal to a str, bytes or another Envelope if their bytes are the same.

# 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": "[email protected]"}
Envelope(**encrypt) != Envelope(**encrypt)  # Envelope objects are not equal

Examples

Signing and encrypting

Sign the message.

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

Sign the message loaded from a file by standard pathlib library

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

Sign the message got from a file-stream

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

Sign and encrypt the message so that's decryptable by keys for [email protected] and [email protected] (that should already be loaded in the keyring).

Envelope(message="Hello world", sign=True,
        encrypt=True,
        from_="[email protected]",
        to="[email protected]")

Sign and encrypt the message so that's decryptable by keys for [email protected] and [email protected] (that get's imported to the keyring from the file).

Envelope(message="Hello world", sign=True,
        encrypt=Path("/tmp/remote_key.asc"),
        from_="[email protected]",
        to="[email protected]")

Sign the message via different keyring.

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

Sign the message with a key that needs passphrase.

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

Sign a message with signing by default turned previously on and having a default keyring path. Every factory call will honour these defaults.

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

Sending

Send an e-mail via module call.

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

Send an e-mail via CLI and default SMTP server localhost on port 25.

envelope --to "[email protected]" --message "Hello world" --send

Send while having specified the SMTP server host, port, username, password.

envelope --to "[email protected]" message "Hello world" --send --smtp localhost 123 username password

Send while having specified the SMTP server through a dictionary.

envelope --to "[email protected]" --message "Hello world" --send --smtp '{"host": "localhost", "port": "123"}'

Send while having specified the SMTP server via module call.

Envelope(message="Hello world", to="[email protected]", send=True, smtp={"host":"localhost"})

Attachment

You can attach a file in many different ways. Pick the one that suits you the best.

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 images

The only thing you have to do is to set the inline=True parameter of the attachment. Then, you can reference the image from within your message, with the help of cid keyword. For more details, see attachments in the Sending section.

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

Complex example

Send an encrypted and signed message (GPG) via the default SMTP server, via all three interfaces.

# CLI interface
envelope --message "Hello world" --from "[email protected]" --to "[email protected]" --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_("[email protected]").to("[email protected]").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", "[email protected]", "[email protected]", "Test", sign=True, encrypt=True, attachments=[(Path("/tmp/file.txt"), (Path("/tmp/file2"), "application/gzip", "zipped-file.zip")], send=True)

In the condition [email protected] private key for signing, [email protected] public key for encrypting and open SMTP server on localhost:25 are available, change --send to --send 0 (or .send() to .send(False) or send=True to send=False) to investigate the generated message that may be similar to the following output:

****************************************************************************************************
Have not been sent from [email protected] to [email protected]
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: [email protected]
To: [email protected]
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==--

Related affairs

Sending an e-mail does not mean it will be received. Sending it successfully through your local domain does not mean a public mailbox will accept it as well. If you are not trustworthy enough, your e-mail may not even appear at the recipient's spam bin, it can just be discarded without notice.

Configure your SMTP

It is always easier if you have an account on an SMTP server the application is able to send e-mails with. If it is not the case, various SMTP server exist but as a quick and non-secure solution, I've tested bytemark/smtp docker image that allows you to start up a SMTP server by a single line.

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

Choose ciphering method

Configure your GPG

In order to sign messages, you need a private key. Let's pretend a usecase when your application will run under www-data user and GPG sign messages through the keys located at: /var/www/.gnupg. You have got a SMTP server with an e-mail account the application may use.

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

It takes few hours to a key to propagate. If the key cannot be imported in your e-mail client because not found on the servers, try in the morning again or check the online search form at http://hkps.pool.sks-keyservers.net. Put your fingerprint on the web or on the business card then so that everybody can check your signature is valid.

Configure your S/MIME

If you are supposed to use S/MIME, you would probably be told where to take your key and certificate from. If planning to try it all by yourself, generate your certificate.pem.

  • Either: Do you have private key?
openssl req -key YOUR-KEY.pem -nodes -x509 -days 365 -out certificate.pem  # will generate privkey.pem alongside
  • Or: Do not you have private key?
openssl req -newkey rsa:1024 -nodes -x509 -days 365 -out certificate.pem  # will generate privkey.pem alongside

Now, you may sign a message with your key and certificate. (However, the messages will not be trustworthy because no authority signed the certificate.) Give your friend the certificate so that they might verify the message comes from you. Receive a certificate from a friend to encrypt them a message with.

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 validation tools

This is just a short explanation on these anti-spam mechanisms so that you can take basic notion what is going on.

Every time, the receiver should ask the From's domain these questions over DNS.

SPF

The receiver asks the sender's domain: Do you allow the senders IP/domain to send the e-mail on your behalf? Is the IP/domain the mail originates from enlisted as valid in the DNS of the SMTP envelope MAIL FROM address domain?

Check your domain on SPF:

dig -t TXT example.com

SPF technology is tied to the SMTP envelope MAIL FROM address which is specified with the .from_addr method and then stored into the Return-Path header by the receiving server, and it has nothing in common with the headers like From .from_, Reply-To .reply_to, or Sender .header("Sender").

DKIM

The receiver asks the sender's domain: Give me the public key so that I may check the hash in the e-mail header that assert the message was composed by your private key. So that the e-mail comes trustworthy from you and nobody modified it on the way.

Check your domain on DKIM:

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

You can obtain the selector from an e-mail message you received. Check the line DKIM-Signature and the value of the param s.

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

DMARC

What is your policy concerning SPF and DKIM? What abuse address do you have?

Check your domain on DMARC:

dig -t TXT _dmarc.example.com

More Repositories

1

pz

Easily handle day to day CLI operation via Python instead of regular Bash programs. 🇺🇦 #supporting
Python
719
star
2

pyoidc

A complete OpenID Connect implementation in Python
Python
714
star
3

knot-resolver

Knot Resolver - resolve DNS names like it's 2024
C
355
star
4

knot

A mirrored repository
C
196
star
5

turris-os

Mirror of https://gitlab.nic.cz/turris/openwrt
C
188
star
6

run-or-raise

GNOME3 extension
JavaScript
141
star
7

bird

BIRD Internet Routing Daemon
C
76
star
8

yangson

Python
51
star
9

python-rt

Python interface to Request Tracker API
Python
50
star
10

turris-os-packages

Mirror of https://gitlab.nic.cz/turris/turris-os-packages
Makefile
43
star
11

deckard

DNS test harness
Python
43
star
12

dns-fuzzing

Repository to store unique seeds for DNS server fuzzing
42
star
13

shotgun

DNS Shotgun: high-performance realistic DNS benchmarking tool. This is a mirrored repository.
C
39
star
14

django-fido

Django application for FIDO protocol U2F
Python
27
star
15

reforis

Mirror of https://gitlab.nic.cz/turris/reforis/reforis
JavaScript
19
star
16

convey

CSV processing and web related data types mutual conversion
Python
17
star
17

jetconf

Python
13
star
18

foris

Foris is a simplified web interface to router administration using netconf and nuci behind the scenes. Foris means gate in Latin.
Python
13
star
19

turris-misc

Mirror of https://gitlab.nic.cz/turris/misc
C
11
star
20

turris-updater

Mirror of https://gitlab.nic.cz/turris/updater/updater
C
11
star
21

kippo

now testing changes from cowrie
Python
10
star
22

ucollect

Network monitoring probe with plugins for anomaly detection
C
10
star
23

gadgets_demo

Demos related to the TURRIS:GADGETS project of Jablotron and CZ.NIC
Python
9
star
24

nuci

Netconf based interface to OpenWrt UCI (unified configuration interface).
Lua
8
star
25

fred-client

FRED subproject: A Python EPP client – a CLI application and an API library
Python
7
star
26

fred-server

FRED subproject: C++ CORBA servers (back end) for the core registry functionality and a CLI administration tool
C++
7
star
27

django-pain

Django Payments and Invoices
Python
6
star
28

dns-collector

DNC collector colletcs data from DNS server into CSV file.
C
6
star
29

u-boot-turris

Mirror of https://gitlab.nic.cz/turris/u-boot-turris
C
6
star
30

domoticz-turris-gadgets

Fork of Domoticz (https://domoticz.com/) with support for Turris Gadgets.
C
6
star
31

fred-docs

FRED subproject: Documentation
Makefile
6
star
32

libatsha204

Mirror of https://gitlab.nic.cz/turris/libatsha204
C
5
star
33

fred-rdap

FRED subproject: An RDAP server (front end) prototype implemented with Django
Python
5
star
34

setuptools-grpc

Setuptools module for building protobuf and grpc service python modules.
Python
5
star
35

socket_wrapper

Modified version of https://cwrap.org/socket_wrapper.html which allows the program to bind to arbitrary IP addresses.
C
4
star
36

turris-build

Mirror of https://gitlab.nic.cz/turris/turris-build/
Shell
4
star
37

python-ubus

Python 2/3 bindings for ubus (mirror of https://gitlab.labs.nic.cz/turris/python-ubus , please report bugs/MR there)
C
4
star
38

nikola

Mirror of https://gitlab.nic.cz/turris/sentinel/nikola
Python
3
star
39

mininterface

A minimal interface to Python application
Python
3
star
40

schedule-shift

Simple work scheduling for a small team
JavaScript
2
star
41

silicon-clustering

SIngle-LInkage CONnnectivity clustering
Python
2
star
42

django-eidas-specific-node

Python
2
star
43

fred-webwhois

FRED subproject: The web WHOIS server (front end) implemented with Django
Python
2
star
44

smrt

Small Modem for Router Turris
C
2
star
45

yang-dns

YANG data models for DNS data and services
XSLT
2
star
46

django-oidc-sub

Django application for login through OpenID Connect
Python
2
star
47

djangocms-git-md-page

DjangoCMS plugin for displaying a page with markdown in git repository.
Python
2
star
48

fred-cdnskey-scanner

FRED subproject: A CLI tool to scan a set of domains for CDNSKEY records
C++
2
star
49

buildsystem

Lightweight replacement for autotools used in several Turris related applications
Perl
2
star
50

mojeid-connect-js

JavaScript
2
star
51

setuptools_webpack

Plugin for setuptools to build and compile JS files using webpack.
Python
2
star
52

foris-tls-plugin

Foris plugin providing frontend for NUCI TLS plugin.
Python
1
star
53

turris-schnapps

Mirror of https://gitlab.nic.cz/turris/schnapps
Shell
1
star
54

dns-server-yang

YANG data model for DNS server (mirror from Gitlab)
XSLT
1
star
55

jukebox-jetconf

Python
1
star
56

firewall3

Local fork of git://nbd.name/firewall3.git by Jo-Philipp Wich <[email protected]> used in router Turris
C
1
star
57

fred-mod-eppd

FRED subproject: An Apache module for parsing EPP commands and transforming them into CORBA calls to a back-end server (and vice versa)
C
1
star
58

slidershow

media player, organizer & presenter
JavaScript
1
star
59

zone-data-yang

XSLT
1
star
60

zone-monitor

Check for suspicious newly registered domains
Python
1
star
61

fw-rules

Firewall rules for Turris
Shell
1
star
62

fred-libfred

FRED basic entities operations library
C++
1
star
63

c-dns

Mirror repository of https://gitlab.nic.cz/knot/c-dns.
C++
1
star
64

fred-db

FRED subproject: A collection of SQL scripts to set up the database in PostgreSQL
PLpgSQL
1
star
65

boot-format

Development copy of a tool for preparation of bootable SD images. Cloned from http://git.freescale.com/git/cgit.cgi/ppc/sdk/boot-format.git/
C
1
star
66

respdiff

DNS responses gathering and differences analysis toolchain. This is a mirrored repository.
Python
1
star
67

fred-webadmin

FRED subproject: The Daphne web administration server (front end) for registry customer support
JavaScript
1
star