Package: envelope
EnvelopeQuick layer overpython-gnupg,M2Crypto,smtplib,magicandemailhandling 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 imageEnvelope("My inline image: <img src='cid:image.jpg' />").attach(path="image.jpg",inline=True)# Load a message and read its attachmentsEnvelope.load(path="message.eml").attachments()# in bash: envelope --load message.eml --attachmentsInstallationBash completionUsageCLIModule: one-liner functionModule: fluent interfaceDocumentationCommand listAny attainable contentsInput / OutputRecipientsSendingSpecific headersCipher standard methodSigningEncryptingSupportiveAddressExperimentalEnvelope objectConverting object to str or boolObject equalityExamplesSigning and encryptingSendingAttachmentInline imagesComplex exampleRelated affairsConfigure your SMTPChoose ciphering methodConfigure your GPGConfigure your S/MIMEDNS validation toolsSPFDKIMDMARCInstallationInstall with a single command fromPyPipip3installenvelopeOr install current GitHub masterpip3installgit+https://github.com/CZ-NIC/envelope.gitOr just download the project and launchpython3 -m envelopeIf planning to sign/encrypt with GPG, assure you have it on the system withsudo apt install gpgand possibly seeConfigure your GPGtutorial.If planning to use S/MIME, you should ensure some prerequisites:sudo apt install swig build-essential python3-dev libssl-dev && pip3 install M2CryptoIf planning to send e-mails, prepare SMTP credentials or visitConfigure your SMTPtutorial.If your e-mails are to be received outside your local domain, visitDMARCsection.Packagepython-magicis used as a dependency. Due to awell-knownname clash with thefile-magicpackage, in case you need to use the latter, don't worry to runpip uninstall python-magic && pip install file-magicafter installing envelope which is fully compatible with both projects.Bash completionRun:apt install bash-completion jqCopy:extra/envelope-autocompletion.bashto/etc/bash_completion.d/Restart terminalUsageAs an example, let's produce in three equal ways anoutput_filewith the GPG-encrypted "Hello world" content.CLILaunch as a CLI application in terminal, seeenvelope --helpenvelope--message"Hello world"\--output"/tmp/output_file"\--from"me@example.com"\--to"remote_person@example.com"\--encrypt-path"/tmp/remote_key.asc"Module: fluent interfaceComfortable way to create the structure if your IDE supports autocompletion.fromenvelopeimportEnvelopeEnvelope().message("Hello world")\.output("/tmp/output_file")\.from_("me@example.com")\.to("remote_person@example.com")\.encrypt(key_path="/tmp/remote_key.asc")Module: one-liner functionYou 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. Seepydoc3 envelopeor documentation below.fromenvelopeimportEnvelopeEnvelope(message="Hello world",output="/tmp/output_file",from_="me@example.com",to="remote_person@example.com",encrypt="/tmp/remote_key.asc")DocumentationBothenvelope --helpfor CLI arguments help andpydoc3 envelopeto see module arguments help should contain same information as here.Command listAll parameters are optional.--paramis used in CLI.param(value)denotes a positional argument.param(value=)denotes a keyword argumentEnvelope(param=)is a one-liner argumentAny attainable contentsWhenever any attainable contents is mentioned, we mean plaintext,bytesorstream(ex: fromopen()). Inmodule interface, you may use aPathobject to the file. InCLI interface, additional flags are provided instead.If the object is not accesible, it will immediately raiseFileNotFoundError.Envelope().attach(path="file.jpg")# Could not fetch file .../file.jpg# FileNotFoundError: [Errno 2] No such file or directory: 'file.jpg'Input / Outputmessage: 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 contentcat file.eml | envelope --message).--message: String. Empty to read.--input:(CLI only)Path to the message file. (Alternative to the--messageparameter.).message(): Read current message instr..message(text): Set the message toany 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 (seemime).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(withoutalternativeandboundaryparameter).text(path=None): Alias of.message(withoutalternativeandboundaryparameter)Envelope(message=):Any attainable contentsEquivalents for setting a string (inPythonand inBash).Envelope(message="hello")==Envelope().message("hello")envelope--message"hello"Equivalents for setting contents of a file (inPythonand inBash).frompathlibimportPathEnvelope(message=Path("file.txt"))==Envelope(message=open("file.txt"))==Envelope.message(path="file.txt")envelope--inputfile.txtEnvelope 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=)Recipientsfrom: E-mail – needed to choose our key if encrypting.--fromE-mail. Empty to read value.--no-fromDeclare we want to encrypt and never decrypt back..from_(email): E-mail | False | None. If None, currentFromreturned as anAddressobject (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_="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 objecta=Envelope(from_="identity@example.com").from_()a=="identity@example.com",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 forto,cc,bccandreply-to.)--to: One or more e-mail addresses. Empty to read.$envelope--tofirst@example.comsecond@example.com--message"hello"$envelope--to
first@example.com
second@example.com.to(email_or_more): If None, current list ofAddressesreturned. If False or "", current list is cleared.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=): 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 forto,cc,bccandreply-to.)--cc: One or more e-mail addresses. Empty to read..cc(email_or_more): If None, current list ofAddressesreturned. If False or "", current list is cleared.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: 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 forto,cc,bccandreply-to.) The header is not sent.--bcc: One or more e-mail addresses. Empty to read..bcc(email_or_more): If None, current list ofAddressesreturned. 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 forto,cc,bccandreply-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 ofAddressesreturned. 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, currentSMTP envelope MAIL FROMreturned as anAddressobject (even an empty one)..Envelope(from_addr=)Sendingsend: Send the message to the recipients by e-mail. True (blank inCLI) 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 inCLI) 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"user@example.org"--message"Hello world"--send0****************************************************************************************************
Havenotbeensentfrom-touser@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,07Oct201916:13:37+0200
Message-ID:<157045761791.29779.5279828659897745855@...>

Helloworldsubject: Mail subject. Gets encrypted with GPG, stays visible with S/MIME.--subject.subject(text=None, encrypt=None):textSubject text.encryptText 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|FalseSpecify 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, local_hostname=None)Envelope(smtp=)Parameters:hostMay include hostname or any of the following input formats (ex: path to an INI file or adict)securityIf not set, automatically set tostarttlsfor port587and totlsfor port465timeoutHow many seconds should SMTP wait before timing out.attemptsHow many times we try to send the message to an SMTP server.delayHow many seconds to sleep before re-trying a timed out connection.local_hostnameFQDN of the local host in the HELO/EHLO command.Input format may be in the following form:Nonedefault localhost server usedstandardsmtplib.SMTPobjectlistortuplehavinghost, [port, [username, password, [security, [timeout, [attempts, [delay, [local_hostname]]]]]]]parametersex:envelope --smtp localhost 125 me@example.comwill set up host, port and username parametersdictspecifying {"host": ..., "port": ...}ex:envelope --smtp '{"host": "localhost"}'will set up host parameterstrhostname or path to an INI file (existing file, ending at.ini, with the section [SMTP])[SMTP]host=example.comport=587Do not fear to pass thesmtpin a loop, we make just a single connection to the server. If timed out, we attempt to reconnect once.smtp="localhost",25formailinmails: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 putany attainable contentsof a single attachment intoattachmentand 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.frompathlibimportPathEnvelope().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 byany 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 ofContent-Typeheader put to the message,mimesection functionalityis 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<bror<pin 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)valueIf 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.)replaceIf True, any header of thekeyname are removed first and ifvalis 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--headerX-Mailermy-appEnvelope(headers=[("X-Mailer","my-app")])Envelope().header("X-Mailer","my-app")Specific headersThese 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:me@example.com?subject=unsubscribe,example.com/unsubscribe,<https://example.com/unsubscribe>.list_unsubscribe(email=): E-mail address. Ex:me@example.com,mailto:me@example.com.list_unsubscribe(web=, one_click=False): Specify URL. Ex:example.com/unsubscribe,http://example.com/unsubscribe. Ifone_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:.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 humanEnvelope().auto_submitted()# mark message as automaticEnvelope().auto_submitted.no()# mark message as human producedCipher standard methodNote that if neithergpgnorsmimeis 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)Signingsign: Sign the message.keyparameterGPG:Blank (CLI) or True (module) for user default key"auto" for turning on signing if there is a key matching to the "from" headerkey ID/fingerprinte-mail address of the identity whose key is to be signed withAny attainable contentswith the key to be signed with (will be imported into keyring)S/MIME:Any attainable contentswith key to be signed with. May contain signing certificate as well.--sign key: (forkeysee above)--sign-path: Filename with the From's private key. (Alternative to thesignparameter.)--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 thecertparameter.).sign(key=True, passphrase=, attach_key=False, cert=None, key_path=None): Sign now (and you may specify the parameters). (Forkeysee above.).signature(key=True, passphrase=, attach_key=False, cert=None, key_path=None): Sign later (when launched with.sign(),.encrypt()or.send()functionsEnvelope(sign=key): (forkeysee 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 contentsEncryptingencrypt:  Recipient GPG public key or S/MIME certificate to be encrypted with.keyparameterGPG: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 recipientkey ID/fingerprinte-mail address of the identity whose key is to be encrypted withAny attainable contentswith 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 dataS/MIMEany attainable contentswith a certificate to be encrypted with or more in an iterable--encrypt [key]: (forkeysee above) Put 0/false/no to disableencrypt-path.--encrypt-path(CLI only): Filename(s) with the recipient's public key(s). (Alternative to theencryptparameter.).encrypt(key=True, sign=, key_path=):signSee signing, ex: you may specify boolean or default signing key ID/fingerprint or "auto" for GPG orany attainable contentswith an S/MIME key + signing certificate.key_path: Key/certificate contents (alternative to thekeyparameter).encryption(key=True, key_path=): Encrypt later (when launched with.sign(),.encrypt()or.send()functions. If needed, in the parameters specifyany attainable contentswith GPG encryption key or S/MIME encryption certificate.Envelope(encrypt=key): (forkeysee above)# message gets encrypted for multiple S/MIME certificatesenvelope--smime--encrypt-pathrecipient1.pemrecipient2.pem--message"Hello"# message gets encrypted with the default GPG keyenvelope--message"Encrypted GPG message!"--subject"Secret subject will not be shown"--encrypt--fromperson@example.com--toperson@example.com# message not encrypted for the sender (from Bash)envelope--message"Encrypted GPG message!"--subject"Secret subject will not be shown"--encryptreceiver@example.comreceiver2@example.com--fromperson@example.com--toreceiver@example.comreceiver2@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 notesIf the GPG encryption fails, it tries to determine which recipient misses the key.By default, GPG encrypts with the key of thefromheader 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): AllTo,CcandBccrecipients are removed and theEnvelopeobject is returned.attachments: Access the list of attachments.--attachments [NAME]Get the list of attachments or a contents of the one specified byNAME.attachments(name=None, inline=None)name(str): The name of the only desired attachment to be returned.inline(bool): Filter inline/enclosed attachments only.Attachmentobject has the attributes.namefile name,.mimetype,.dataraw dataif casted tostr/bytes, its raw.dataare returned.copy(): Return deep copy of the instance to be used independently.factory=Envelope().cc("original@example.com").copye1=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 instr), 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_mxE-mail addresses can be checked for MX record, not only for their format.check_smtpWe try to connect to the SMTP host.$envelope--smtplocalhost25--fromme@example.com--check
SPFfoundonthedomainexample.com:v=spf1-all
See:dig-tSPFexample.com&&dig-tTXTexample.com
DKIMfound:['v=DKIM1; g=*; k=rsa; p=...']CouldnotspotDMARC.
TryingtoconnecttotheSMTP...
Checksucceeded..as_message(): Generates an email.message.Message object.e=Envelope("hello").as_message()print(type(e),e.get_payload())# <class 'email.message.EmailMessage'> hello\nNote: due to a bug in a standard Python libraryhttps://github.com/python/cpython/issues/99533and #19 you void GPG when you access the message this way wihle signing an attachment with a name longer than 34 chars.load: Parseany 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 contentspath: Path to the file, alternative to themessagekey,cert: Specify when decrypting an S/MIME message (may be bundled together to thekey)gnupg_home: Path to the GNUPG_HOME or None if the environment default should be used.Envelope.load("Subject: testing message").subject()# "testing message"bashallows use blank--subjector--messageflags to display the--load FILE$envelope--loademail.eml
Content-Type:text/plain;charset="utf-8"Content-Transfer-Encoding:7bit
MIME-Version:1.0
Subject:testingmessage

Messagebody

$envelope--loademail.eml--subject
testingmessage(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:testingmessage

$catemail.eml|envelope

$envelope<email.emlsmtp_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` onlyEnvelope.smtp_quit()# called on the class → closes both connectionsAddressAny e-mail address encountered is internally converted to anAddress(str)object that can be imported from theenvelopepackage. You can safely access followingstrproperties:.name– the real name.address– the e-mail address.host– its domain.user– the user name part of the e-mailfromenvelopeimportAddressa=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 theFromheader 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)isFalse,a.host==""Address()==Address("")=="",Address().address==""Method.casefold()returns casefoldedAddressobject which is useful for comparing with strings whereas comparing with otherAddressobject casefolds automaticallya=Address("John <person@example.com>")c=a.casefold()aisnotc,a==c,a.name=="john",a.name!=c.nameMethod.is_valid(check_mx=False)returns boolean if the format is valid. Whencheck_mxset toTrue, MX server is inquired too.Since theAddressis a subclass ofstr, 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>"# TrueConcerningto,cc,bccandreply-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 anAddressobject 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)forxine.to()]# ["person1@example.com", "John <person2@example.com>", "person3@example.com"][x.get(address=False)forxine.to()]# ["", "John", ""][x.get(name=True)forxine.to()]# ["person1@example.com", "John", "person3@example.com"]# return an address if no name given[x.get(address=True)forxine.to()]# ["person1@example.com", "person2@example.com", "person3@example.com"]# addresses onlyFor some exotic cases, Address tends to do the parsing job better than the underlying standard library (see thebug reportfrom 2004).fromemail.utilsimportparseaddrfromenvelopeimportAddressparseaddr("alice@example.com <bob@example.malware>")# ('', 'alice@example.com') -> empty name and wrong addressAddress("alice@example.com <bob@example.malware>").address# 'bob@example.malware' -> the right addressExperimentalSince 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(): Accessingmultipart/report.Currently onlyXARFis supported in the moment. You may directly access the fields, without any additionaljsonparsing.ifxarf:=Envelope.load(path="xarf.eml")._report():print(xarf['SourceIp'])# '192.0.2.1'Envelope objectConverting object to str or boolWhen 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 textbool(o)# TrueObject equalityEnvelope object is equal to astr,bytesor anotherEnvelopeif theirbytesare the same.# Envelope objects are equalsign={"message":"message","sign":True}Envelope(**sign)==Envelope(**sign)# Truebytes(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 outputencrypt={"message":"message","encrypt":True,"from_":False,"to":"person@example.com"}Envelope(**encrypt)!=Envelope(**encrypt)# Envelope objects are not equalExamplesSigning and encryptingSign the message.Envelope(message="Hello world",sign=True)Sign the message loaded from a file by standard pathlib libraryfrompathlibimportPathEnvelope(message=Path("/tmp/message.txt"),sign=True)Sign the message got from a file-streamwithopen("/tmp/message.txt")asf:Envelope(message=f,sign=True)Sign and encrypt the message so that's decryptable by keys forme@example.comandremote_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 forme@example.comandremote_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. Everyfactorycall will honour these defaults.factory=Envelope().signature(True).gpg("/tmp/my-keyring").copyfactory().(message="Hello world")SendingSend 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"--sendSend while having specified the SMTP server host, port, username, password.envelope--to"user@example.org"message"Hello world"--send--smtplocalhost123usernamepasswordSend 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"})AttachmentYou 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'withopen("/tmp/file.txt")asf:Envelope(attachment=f)# file name will be 'file.txt'withopen("/tmp/file.txt")asf:Envelope(attachment=(f,"filename.txt"))Envelope().attach(path="/tmp/file.txt",name="filename.txt")Inline imagesThe only thing you have to do is to set theinline=Trueparameter of the attachment. Then, you can reference the image from within your message, with the help ofcidkeyword. For more details, seeattachmentsin theSendingsection.(Envelope().attach(path="/tmp/file.jpg",inline=True).message("Hey, this is an inline image: <img src='cid:file.jpg' />"))Complex exampleSend an encrypted and signed message (GPG) via the default SMTP server, via all three interfaces.# CLI interfaceenvelope--message"Hello world"--from"me@example.org"--to"user@example.org"--subject"Test"--sign--encrypt-a/tmp/file.txt-a/tmp/file2application/gzipzipped-file.zip--sendfrompathlibimportPathfromenvelopeimportEnvelope# fluent interfaceEnvelope().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 interfaceEnvelope("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 conditionme@example.comprivate key for signing,user@example.compublic key for encrypting and open SMTP server onlocalhost:25are available, change--sendto--send 0(or.send()to.send(False)orsend=Truetosend=False) to investigate the generated message that may be similar to the following output:****************************************************************************************************
Havenotbeensentfromme@example.orgtouser@example.org
Encryptedsubject:Test
Encryptedmessage:b'Hello world'Subject:Encryptedmessage
MIME-Version:1.0
Content-Type:multipart/encrypted;protocol="application/pgp-encrypted";boundary="===============8462917939563016793=="From:me@example.org
To:user@example.org
Date:Tue,08Oct201916: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:OpenPGPencryptedmessage
Content-Disposition:inline;filename="encrypted.asc"-----BEGINPGPMESSAGE-----

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

--===============8462917939563016793==--Related affairsSending 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 SMTPIt 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 testedbytemark/smtpdocker image that allows you to start up a SMTP server by a single line.dockerrun--network=host--restartalways-dbytemark/smtp# starts open port 25 on localhostenvelope--message"SMTP test"--from[youre-mail]--to[youre-mail]--smtplocalhost25--sendChoose ciphering methodConfigure your GPGIn order to sign messages, you need a private key. Let's pretend a usecase when your application will run underwww-datauser 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 ownersudochownwww-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/.gnupgsudo-H-uwww-datagpg--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 ownerGNUPGHOME=/var/www/.gnupgsudo-H-uwww-datagpg--list-secret-keys# get key IDGNUPGHOME=/var/www/.gnupgsudo-H-uwww-datagpg--send-keys[keyID]# now the world is able to pull the key from a global webserver when they receive an e-mail from youGNUPGHOME=/var/www/.gnupgsudo-H-uwww-datagpg--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/.gnupgsudo-H-uwww-dataenvelope--message"Hello world"--subject"GPG signing test"--sign[keyID]--from[applicatione-mail]--to[youre-mail]--send# you now receive e-mail and may import the key and set the trust to the keyIt 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 athttp://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/MIMEIf 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 yourcertificate.pem.Either: Do you have private key?opensslreq-keyYOUR-KEY.pem-nodes-x509-days365-outcertificate.pem# will generate privkey.pem alongsideOr: Do not you have private key?opensslreq-newkeyrsa:1024-nodes-x509-days365-outcertificate.pem# will generate privkey.pem alongsideNow, you may sign a message with your key and certificate. (However, the messageswill not be trustworthybecause 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-mailDNS validation toolsThis 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.SPFThe 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-tTXTexample.comSPF technology is tied to the SMTP envelope MAIL FROM address which is specified with the.from_addrmethod 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").DKIMThe 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-tTXT[selector]._domainkey.example.comYou can obtain theselectorfrom an e-mail message you received. Check the lineDKIM-Signatureand the value of the params.DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=example.com; s=default;DMARCWhat is your policy concerning SPF and DKIM? What abuse address do you have?Check your domain on DMARC:dig-tTXT_dmarc.example.com