README

Blackmail is a Go package to send emails. It has an easy to use API and supports email signing without too much effort.

Current status: work-in-progress. Most of it works, but the API isn't stable yet and some things are not yet implemented as documented (specifically: signing and "direct" sending doesn't work yet, and some Mailer options don't either).

Why a new package? I didn't care much for the API of many of the existing solutions. I also wanted an email package which supports easy PGP signing out-of-the-box (see this article for some background on that).

Import the library as zgo.at/blackmail; godoc. There is also a smtp client library at zgo.at/blackmail/smtp which can be used without the main blackmail client if you want. It's a modified version of net/smtp (via go-smtp, although I removed most added features from that).

There is a small commandline utility at cmd/blackmail; try it with go run ./cmd/blackmail.

The main use case where you just want to send off an email and be done with it. Non-goals include things like parsing email messages, support for encodings other than ASCII and UTF-8, or a one-stop-shop for your very specific complex requirements. It should be able to handle all common (and not-so-common) use cases though.

Example

// Send a new message using blackmail.DefaultMailer
err := blackmail.Send("Send me bitcoins or I will leak your browsing history!",
    blackmail.From("", "blackmail@example.com"),
    blackmail.To("Name", "victim@example.com"),
    blackmail.Bodyf("I can haz ur bitcoinz?"))

// A more complex message with a text and HTML part and inline image.
err = blackmail.Send("I saw what you did last night 😏",
    blackmail.From("😏", "blackmail@example.com"),
    append(blackmail.To("Name", "victim@example.com"), blackmail.Cc("Other", "other@example.com")...),
    blackmail.Text("Text part")
    blackmail.HTML("HTML part: <img src="cid:blackmail:1">",
        blackmail.InlineImage("image/png", "logo.png", imgbytes)))

// You can create your own (re-usable) mailer.
mailer := blackmail.NewMailer("smtp://user:pass@localhost:25")
err = mailer.Send([..])

// Add some options to your mailer.
mailer = blackmail.NewMailer("smtp://user:pass@localhost:25
    blackmail.MailerAuth(..),
    blackmail.MailerTLS(&tls.Config{}),
    blackmail.RequireSTARTLS(true))

// Get RF5322 message with a list of recipients to send it to (To + Cc + Bcc).
msg, to := blackmail.Message([.. same arguments as Send() ..])

See the test cases in blackmail_test.go for various other examples.

Supports signing out-of-the-box (this is not yet functional):

// Create a new signing key.
priv, pub, err := blackmail.SignCreateKeys()

// Convenience function to read keys from filesystem.
//priv, pub, err := blackmail.SignKeys("test.priv", "test.pub")

err := blackmail.Send("Subject!",
    blackmail.From("My name", "myemail@example.com"),
    blackmail.To("Name", "addr"),
    blackmail.Bodyf("Well, hello there!"),
    blackmail.Sign(priv, pub)

Note there is no support for PGP encryption (and never will be).

You can use the blackmail_no_sign build tag to exclude signing support and avoid depending on golang.org/x/crypto if you want.

Questions you may have

Is this package stable?

Not quite; I might tweak the API a bit. For example I'm not 100% with how passing options to the Mailer works, and this may get a backwards incompatible change. I'll probably also change some of the smtp package.

Regular users will never understand all this OpenPGP signing stuff!

Setting up email clients to verify signatures is easy:

  1. Import public key from a trusted(ish) source, such as the applications website or "welcome to our service" email.

And that's it. Signing your own stuff, encryption, and key distribution from random strangers from the internet is hard, but this kind of trust-on-first-use model isn't too hard.

I suspect a lot of the opposition against any form of PGP comes from people traumatised from the gpg CLI or other really hard PGP interfaces like Enigmail. If you restrict yourself to just a subset then it's mostly okay.

I wrote a thing about this last year: Why isn’t Amazon.com signing their emails?.

But that gives a false sense of security!

There's always at least one person who says that, and I don't buy it. Does a cheap lock on your front door give you a "false sense of security" or does it make it harder for many people to break in to your house?

Even experts can struggle to determine if an email is genuine or a phising/scam attempt. Signing doesn't provide perfect protection against it, but it does improve on the current situation – a situation which has been unchanged for over 20 years. Perfect security guarantees don't exist, but this is better security.

There are perhaps better solutions – there is certainly space for a better signing protocol like minisign – but the infrastructure and support already exists for OpenPGP and it will be years before [something-else] will gain enough traction to be usable. For the time being, OpenPGP is what we're stuck with.

I get the error "tls: first record does not look like a TLS handshake"

You are attempting to establish a TLS connection to a server which doesn't support TLS or only supports it via the STARTTLS command.

I get the error "x509: certificate signed by unknown authority"

The certificate chain used for the TLS connection is not signed by a known authority. It's a self-signed certificate or you don't have the root certificates installed.

How can I use a @ in my username?

Encode as %40:

smtp://carpetsmoker%40fastmail.nl:PASS@smtp.fastmail.com:587'

Dedication

I first had the idea of blackmail well over 10 years ago after seeing some Joe Armstrong interview where he mentioned he or a co-worker (I forgot) maintained an email client in the 80s blackmail. I rewrote my PHP "mailview" webmail client to Python years ago and called it blackmail, but never finished or released it.

Finally, after all these years I have a change to ~steal~ use the blackmail name for an email-related thing!

This package is dedicated to Joe Armstrong. I never programmed Erlang, but found many of his writings and talks insightful, and – more importantly – he was a funny guy.

…and now for the tricky bit…

Expand ▾ Collapse ▴

Documentation

Overview

    Package blackmail sends emails.

    Index

    Constants

    View Source
    const (
    	ConnectWriter = "writer" // Write to an io.Writer.
    	ConnectDirect = "direct" // Connect directly to MX records.
    )
    View Source
    const (
    	AuthLogin   = "login"
    	AuthPlain   = "plain"
    	AuthCramMD5 = "cram-md5"
    )

      Authentication methods for MailerAuth().

      Variables

      View Source
      var DefaultMailer = NewMailer(ConnectDirect)

        DefaultMailer is used with blackmail.Send().

        Functions

        func Attachment

        func Attachment(contentType, filename string, body []byte) bodyPart

          Attachment returns a new attachment part with the given Content-Type.

          It will try to guess the Content-Type if empty.

          func Bcc

          func Bcc(addr ...string) []recipient

          func BccAddress

          func BccAddress(addr ...mail.Address) []recipient

          func BccNames

          func BccNames(nameAddr ...string) []recipient

          func Body

          func Body(contentType string, body []byte) bodyPart

            Body returns a new part with the given Content-Type.

            func BodyHTML

            func BodyHTML(body []byte, images ...bodyPart) bodyPart

              BodyHTML returns a new text/html part.

              func BodyMust

              func BodyMust(contentType string, fn func() ([]byte, error)) bodyPart

                BodyMust sets the body using a callback, propagating any errors back up.

                This is useful when using Go templates for the mail body;

                buf := new(bytes.Buffer)
                err := tpl.ExecuteTemplate(buf, "email", struct{
                    Name string
                }{"Martin"})
                if err != nil {
                    log.Fatal(err)
                }
                
                err := Send("Basic test", From("", "me@example.com"),
                    To("to@to.to"),
                    Body("text/plain", buf.Bytes()))
                

                With BodyMust(), it's simpler; you just need to define a little helper re-usable helper function and call that:

                func template(tplname string, args interface{}) func() ([]byte, error) {
                    return func() ([]byte, error) {
                        buf := new(bytes.Buffer)
                        err := tpl.ExecuteTemplate(buf, tplname, args)
                        return buf.Bytes(), err
                    }
                }
                
                err := Send("Basic test", From("", "me@example.com"),
                    To("to@to.to"),
                    BodyMust("text/html", template("email", struct {
                        Name string
                    }{"Martin"})))
                

                Other use cases include things like loading data from a file, reading from a stream, etc.

                func BodyMustHTML

                func BodyMustHTML(fn func() ([]byte, error)) bodyPart

                  BodyMustHTML is like BodyMust() with contentType text/html.

                  func BodyMustText

                  func BodyMustText(fn func() ([]byte, error)) bodyPart

                    BodyMustText is like BodyMust() with contentType text/plain.

                    func BodyText

                    func BodyText(body []byte) bodyPart

                      BodyText returns a new text/plain part.

                      func Bodyf

                      func Bodyf(s string, args ...interface{}) bodyPart

                        Bodyf returns a new text/plain part.

                        func Cc

                        func Cc(addr ...string) []recipient

                        func CcAddress

                        func CcAddress(addr ...mail.Address) []recipient

                        func CcNames

                        func CcNames(nameAddr ...string) []recipient

                        func From

                        func From(name, address string) mail.Address

                          From makes creating a mail.Address a bit more convenient.

                          mail.Address{Name: "foo, Address: "foo@example.com}
                          blackmail.From{"foo, "foo@example.com)
                          

                          func Headers

                          func Headers(keyValue ...string) bodyPart

                            Headers adds the headers to the message.

                            This will override any headers set automatically by the system, such as Date: or Message-Id:

                            Headers("My-Header", "value",
                                "Message-Id", "<my-message-id@example.com>")
                            

                            func HeadersAutoreply

                            func HeadersAutoreply() bodyPart

                              HeadersAutoreply sets headers to indicate this message is a an autoreply.

                              See e.g: https://www.arp242.net/autoreply.html#what-you-need-to-set-on-your-auto-response

                              func InlineImage

                              func InlineImage(contentType, filename string, body []byte) bodyPart

                                InlineImage returns a new inline image part.

                                It will try to guess the Content-Type if empty.

                                Then use "cid:blackmail:<n>" to reference it:

                                <img src="cid:blackmail:1">     First InlineImage()
                                <img src="cid:blackmail:2">     Second InlineImage()
                                

                                func MailerAuth

                                func MailerAuth(v string) senderOpt

                                  MailerAuth sets the AUTH method for the relay mailer. Currently LOGIN, PLAIN, and CRAM-MD5 are supported.

                                  In general, PLAIN is preferred and it's the default. Note that CRAM-MD5 only provides weak security over untrusted connections.

                                  func MailerOut

                                  func MailerOut(v io.Writer) senderOpt

                                    MailerOut sets the output for the writer mailer.

                                    func MailerRequireTLS

                                    func MailerRequireTLS(v bool) senderOpt

                                      MailerRequireTLS sets whether TLS is required.

                                      func MailerTLS

                                      func MailerTLS(v *tls.Config) senderOpt

                                        MailerTLS sets the tls config for the relay and direct mailer.

                                        func Message

                                        func Message(subject string, from mail.Address, rcpt []recipient, firstPart bodyPart, parts ...bodyPart) ([]byte, []string, error)

                                          Message formats a message.

                                          func NopCloser

                                          func NopCloser(r io.Writer) io.WriteCloser

                                          func Send

                                          func Send(subject string, from mail.Address, rcpt []recipient, firstPart bodyPart, parts ...bodyPart) error

                                            Send an email using the DefaultMailer.

                                            The arguments are identical to Message().

                                            func Sign

                                            func Sign(pubkey, privkey []byte, parts ...bodyPart) bodyPart

                                              Sign the message with the given PGP key.

                                              func SignCreateKeys

                                              func SignCreateKeys() ([]byte, []byte, error)

                                                SignCreateKeys creates a new signing key.

                                                $ gpg2 --no-default-keyring --keyring /tmp/test.gpg --batch --passphrase ” --quick-gen-key 'martin@arp242.net' gpg: keybox '/tmp/test.gpg' created gpg: key 6B4ED72ADCA0189C marked as ultimately trusted gpg: revocation certificate stored as '/home/martin/.config/gnupg/openpgp-revocs.d/B0D7F5E12D2E1FBB7F20CB256B4ED72ADCA0189C.rev'

                                                $ gpg2 -a --no-default-keyring --keyring /tmp/test.gpg --export 6B4ED72ADCA0189C > test.pub $ gpg2 -a --no-default-keyring --keyring /tmp/test.gpg --export-secret-keys B0D7F5E12D2E1FBB7F20CB256B4ED72ADCA0189C > test.priv

                                                $ gpg2 -a --no-keyring --detach-sign $ gpg2 -a --no-default-keyring --keyring /tmp/test.gpg --detach-sign < signed >! signature

                                                $ gpg2 --no-default-keyring --keyring /tmp/test.gpg --verify ./signature ./signed gpg: Signature made Tue 26 May 2020 19:06:03 WITA gpg: using RSA key B0D7F5E12D2E1FBB7F20CB256B4ED72ADCA0189C gpg: Good signature from "martin@arp242.net" [ultimate]

                                                gpg: Signature made Tue 26 May 2020 19:09:50 WITA gpg: using RSA key 6B4ED72ADCA0189C gpg: BAD signature from "martin@arp242.net" [ultimate]

                                                public is gpg export w/p --armor $ gpg2 --no-default-keyring --keyring ./public --verify ./signature ./signed

                                                https://gist.github.com/eliquious/9e96017f47d9bd43cdf9 https://github.com/jchavannes/go-pgp

                                                func SignKeys

                                                func SignKeys(pubFile, privFile string) (pub, priv []byte, err error)

                                                  SignKeys loads the public and private keys from the

                                                  func To

                                                  func To(addr ...string) []recipient

                                                    To sets the To: from a list of email addresses.

                                                    func ToAddress

                                                    func ToAddress(addr ...mail.Address) []recipient

                                                      ToAddress sets the To: from a list of mail.Addresses.

                                                      func ToNames

                                                      func ToNames(nameAddr ...string) []recipient

                                                        ToNames sets the To: from a list of "name", "addr" arguments.

                                                        Types

                                                        type Mailer

                                                        type Mailer struct {
                                                        	// contains filtered or unexported fields
                                                        }

                                                          Mailer to send messages; use NewMailer() to construct a new instance.

                                                          func NewMailer

                                                          func NewMailer(smtp string, opts ...senderOpt) Mailer

                                                            NewMailer returns a new re-usable mailer.

                                                            Setting the connection string to blackmail.Writer will print all messages to stdout without sending them:

                                                            NewMailer(blackmail.Writer)
                                                            

                                                            You can pass Mailer.Writer() as an option to send them somewhere else:

                                                            NewMailer(blackmail.Writer, blackmail.MailerOut(os.Stderr))
                                                            
                                                            buf := new(bytes.Buffer)
                                                            NewMailer(blackmail.Writer, blackmail.MailerOut(buf))
                                                            

                                                            If the connection string is set to blackmail.Direct, blackmail will look up the MX records and attempt to deliver to them.

                                                            NewMailer(blackmail.Direct)
                                                            

                                                            Any URL will be used as a SMTP relay:

                                                            NewMailer("smtps://foo:foo@mail.foo.com")
                                                            

                                                            The default authentication is PLAIN; add MailerAuth() to set something different.

                                                            func (Mailer) Send

                                                            func (m Mailer) Send(subject string, from mail.Address, rcpt []recipient, firstPart bodyPart, parts ...bodyPart) error

                                                              Send an email.

                                                              The arguments are identical to Message().

                                                              type SoftError

                                                              type SoftError struct {
                                                              	// contains filtered or unexported fields
                                                              }

                                                              func (SoftError) Error

                                                              func (f SoftError) Error() string

                                                              func (SoftError) Unwrap

                                                              func (f SoftError) Unwrap() error

                                                              Directories

                                                              Path Synopsis
                                                              cmd
                                                              Package smtp implements a SMTP client as defined in RFC 5321.
                                                              Package smtp implements a SMTP client as defined in RFC 5321.