
Security News
libxml2 Maintainer Ends Embargoed Vulnerability Reports, Citing Unsustainable Burden
Libxml2’s solo maintainer drops embargoed security fixes, highlighting the burden on unpaid volunteers who keep critical open source software secure.
Insert a message and attachments and send e-mail / sign / encrypt contents by a single line.
Quick layer over python-gnupg, cryptography, 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
Install with a single command from PyPi
pip3 install envelope
pip3 install git+https://github.com/CZ-NIC/envelope.git
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 might be required to ensure some prerequisites, ex: sudo apt install build-essential libssl-dev libffi-dev python3-dev cargo pkg-config
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. Both use libmagic
under the hood which is probably already installed. However, if it is not, install sudo apt install libmagic1
.
apt install bash-completion jq
/etc/bash_completion.d/
As an example, let's produce in three equal ways an output_file
with the GPG-encrypted "Hello world" content.
Launch as a CLI application in terminal, see 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"
Comfortable way to create the structure if your IDE supports autocompletion.
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")
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_="me@example.com",
to="remote_person@example.com",
encrypt="/tmp/remote_key.asc")
Both envelope --help
for CLI arguments help and pydoc3 envelope
to see module arguments help should contain same information as here.
All parameters are optional.
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'
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
parameter.)str
.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>
.message
(without alternative
and boundary
parameter).message
(without alternative
and boundary
parameter)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).
From
returned as an Address object (even an empty one).# 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
, cc
, bcc
and reply-to
.)
$ envelope --to first@example.com second@example.com --message "hello"
$ envelope --to
first@example.com
second@example.com
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"]
to
, cc
, bcc
and reply-to
.)
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"]
to
, cc
, bcc
and reply-to
.) The header is not sent.
to
, cc
, bcc
and reply-to
.) The field is not encrypted.
SMTP envelope MAIL FROM
returned as an Address object (even an empty one).send: Send the message to the recipients by e-mail. True (blank in CLI) to send now or False to print out debug information.
$ 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: Mail subject. Gets encrypted with GPG, stays visible with S/MIME.
text
Subject text.encrypt
Text used instead of the real protected subject while PGP encrypting. False to not encrypt.date:
str|False
Specify Date header (otherwise Date is added automatically). If False, the Date header will not be added automatically.smtp: SMTP server
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 465timeout
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.local_hostname
FQDN of the local host in the HELO/EHLO command.None
default localhost server usedsmtplib.SMTP
objectlist
or tuple
having host, [port, [username, password, [security, [timeout, [attempts, [delay, [local_hostname]]]]]]]
parameters
envelope --smtp localhost 125 me@example.com
will set up host, port and username parametersdict
specifying {"host": ..., "port": ...}
envelope --smtp '{"host": "localhost"}'
will set up host parameterstr
hostname or path to an INI file (existing file, ending at .ini
, with the section [SMTP])
[SMTP]
host = example.com
port = 587
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
envelope --attach "/tmp/file.txt" "displayed-name.txt" "text/plain" --attach "/tmp/another-file.txt"
Envelope().attach(path="/tmp/file.txt").attach(path="/tmp/another-file.txt")
contents [,mime type] [,file name] [, True for inline]
.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' />")
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".
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.
<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,headers: Any custom headers (these will not be encrypted with GPG nor S/MIME)
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"]
Equivalent headers:
envelope --header X-Mailer my-app
Envelope(headers=[("X-Mailer", "my-app")])
Envelope().header("X-Mailer", "my-app")
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.
me@example.com?subject=unsubscribe
, example.com/unsubscribe
, <https://example.com/unsubscribe>
me@example.com
, mailto:me@example.com
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:me@example.com?subject=unsubscribe>
Envelope().list_unsubscribe("example.com/unsubscribe", mail="me@example.com?subject=unsubscribe")
.auto_submitted:
Envelope().auto_submitted() # mark message as automatic
Envelope().auto_submitted.no() # mark message as human produced
Note that if neither gpg nor smime is specified, we try to determine the method automatically.
key
parameter
key
see above)sign
parameter.)cert
parameter.)key
see above.)key
see above)encrypt: Recipient GPG public key or S/MIME certificate to be encrypted with.
key
parameter
key
see above) Put 0/false/no to disable encrypt-path
.encrypt
parameter.)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)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 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"))
To
, Cc
, Bcc
To
, Cc
and Bcc
recipients are removed and the Envelope
object is returned.NAME
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'}
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.
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_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 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(): 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 https://github.com/python/cpython/issues/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.
message
key
)Envelope.load("Subject: testing message").subject() # "testing message"
--subject
or --message
flags to display the$ 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
$ 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
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-mailfrom envelope import Address
a = Address("John <person@example.com>")
a.name == "John", a.address == "person@example.com", 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 <person@example.com>")
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 <person@example.com>, "John <person@example.com>"
a + " hello" # "John <person@example.com> 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.
"person@example.com" == Address("John <person@example.com>") == "John <person@example.com>" # 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("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
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("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
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'
_check_auth()
: To determine whether DMACR, SPF and DKIM are alright in a loaded message.from envelope import Envelope
auth = Envelope.load(path="test.eml")._check_auth()
auth # <AuthResult spf='pass', dkim='pass', dmarc='pass', spf_received='pass', verdict='pass', failure_reason=None>
if auth: # Use the output object as bool. It is True only if all present checks are 'pass'.
...
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
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": "person@example.com"}
Envelope(**encrypt) != Envelope(**encrypt) # Envelope objects are not equal
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 me@example.com and remote_person@example.com (that should already be loaded in the keyring).
Envelope(message="Hello world", sign=True,
encrypt=True,
from_="me@example.com",
to="remote_person@example.com")
Sign and encrypt the message so that's decryptable by keys for me@example.com and remote_person@example.com (that get's imported to the keyring from the file).
Envelope(message="Hello world", sign=True,
encrypt=Path("/tmp/remote_key.asc"),
from_="me@example.com",
to="remote_person@example.com")
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")
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 "user@example.org" --message "Hello world" --send
Send while having specified the SMTP server host, port, username, password.
envelope --to "user@example.org" message "Hello world" --send --smtp localhost 123 username password
Send while having specified the SMTP server through a dictionary.
envelope --to "user@example.org" --message "Hello world" --send --smtp '{"host": "localhost", "port": "123"}'
Send while having specified the SMTP server via module call.
Envelope(message="Hello world", to="user@example.org", send=True, smtp={"host":"localhost"})
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")
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' />"))
Send an encrypted and signed message (GPG) via the default SMTP server, via all three interfaces.
# 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)
In the condition me@example.com private key for signing, user@example.com 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 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==--
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.
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
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.
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
.
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
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
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.
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")
.
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;
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
FAQs
Insert a message and attachments and send e-mail / sign / encrypt contents by a single line.
We found that envelope demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Libxml2’s solo maintainer drops embargoed security fixes, highlighting the burden on unpaid volunteers who keep critical open source software secure.
Research
Security News
Socket researchers uncover how browser extensions in trusted stores are used to hijack sessions, redirect traffic, and manipulate user behavior.
Research
Security News
An in-depth analysis of credential stealers, crypto drainers, cryptojackers, and clipboard hijackers abusing open source package registries to compromise Web3 development environments.