├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── build.sh ├── command.go ├── go.mod ├── go.sum ├── main.go ├── sendmail.go └── smtp.go /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .git 3 | bin 4 | src 5 | pkg 6 | README.md 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | src 27 | bin 28 | pkg 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.11 2 | 3 | RUN mkdir -p /usr/src/email-me 4 | WORKDIR /usr/src/email-me 5 | 6 | ENV CROSSPLATFORMS \ 7 | linux/amd64 linux/386 linux/arm \ 8 | darwin/amd64 darwin/386 \ 9 | freebsd/amd64 freebsd/386 freebsd/arm \ 10 | windows/amd64 windows/386 11 | 12 | ENV GOARM 5 13 | 14 | CMD set -x \ 15 | && for platform in $CROSSPLATFORMS; do \ 16 | GOOS=${platform%/*} \ 17 | GOARCH=${platform##*/} \ 18 | go build -v -o bin/email-me-${platform%/*}-${platform##*/}; \ 19 | done 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Matt Robenolt 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of email-me nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # email-me 2 | 3 | sometimes you run processes and you want to be emailed when they're done running 4 | 5 | ``` 6 | $ email-me -to=m@robenolt.com ls -lah 7 | ``` 8 | 9 | ## Installation 10 | 11 | ### OS X (Homebrew) 12 | 13 | ```bash 14 | $ brew install mattrobenolt/stuff/email-me 15 | ``` 16 | 17 | ### From source 18 | 19 | Compiled binaries can be found under: https://github.com/mattrobenolt/email-me/releases, or 20 | if you're familiar with Go, you can install via `go get`: 21 | 22 | ```bash 23 | $ go get github.com/mattrobenolt/email-me 24 | ``` 25 | 26 | ## Usage 27 | 28 | ``` 29 | NAME: 30 | email-me - email me when a thing is done 31 | 32 | USAGE: 33 | email-me [global options] [arguments...] 34 | 35 | VERSION: 36 | 0.4.0 37 | 38 | GLOBAL OPTIONS: 39 | --to value email address to send output to [$EMAIL_ME_TO] 40 | --subject value, -s value subject of email (optional) 41 | --max value max bytes to capture for stdout/stderr (default: 10000) [$EMAIL_ME_MAX] 42 | --on-error only notify on a non-0 exit code 43 | --version, -v print the version 44 | ``` 45 | 46 | ## Example email 47 | 48 | ``` 49 | Cmd: [ls -lah] 50 | Start: 2015-06-28 08:10:46.880877935 -0700 PDT 51 | End: 2015-06-28 08:10:46.899524717 -0700 PDT 52 | Duration: 18.646782ms total 1.981ms user 7.601ms system 53 | ProcessState: exit status 0 54 | Error: 55 | Stderr: 56 | 57 | 58 | Stdout: 59 | total 11416 60 | drwxr-xr-x 16 matt staff 544B Jun 28 08:10 . 61 | drwxr-xr-x@ 219 matt staff 7.3K Jun 24 18:11 .. 62 | -rw-r--r-- 1 matt staff 34B Jun 24 18:38 .dockerignore 63 | drwxr-xr-x 13 matt staff 442B Jun 28 08:10 .git 64 | -rw-r--r-- 1 matt staff 275B Jun 24 18:38 .gitignore 65 | -rw-r--r-- 1 matt staff 507B Jun 24 17:32 Dockerfile 66 | -rw------- 1 matt staff 1.4K Jun 28 06:55 LICENSE 67 | -rw-r--r-- 1 matt staff 1.5K Jun 28 08:06 README.md 68 | drwxr-xr-x 12 matt staff 408B Jun 28 08:03 bin 69 | -rwxr-xr-x 1 matt staff 109B Jun 24 18:48 build.sh 70 | -rw-r--r-- 1 matt staff 1.6K Jun 25 10:42 command.go 71 | -rwx------ 1 matt staff 5.5M Jun 28 07:06 email-me 72 | -rw-r--r-- 1 matt staff 2.3K Jun 28 08:01 main.go 73 | -rw-r--r-- 1 matt staff 263B Jun 24 17:25 sendmail.go 74 | -rw-r--r-- 1 matt staff 193B Jun 24 17:26 smtp.go 75 | drwxr-xr-x 3 matt staff 102B Jun 24 18:18 src 76 | ``` 77 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xe 3 | 4 | rm -rf bin/ 5 | docker build --pull --rm -t email-me . 6 | docker run --rm -v $PWD:/usr/src/email-me email-me 7 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "os/exec" 7 | "os/user" 8 | "text/template" 9 | "time" 10 | ) 11 | 12 | const emailTemplate = `From: {{.From}} 13 | To: {{.To}} 14 | Subject: {{.Subject}} 15 | 16 | Cmd: {{.Result.Cmd.Args}} 17 | Start: {{.Result.Start}} 18 | End: {{.Result.End}} 19 | Duration: {{.Result.Duration}} total {{.Result.Cmd.ProcessState.UserTime}} user {{.Result.Cmd.ProcessState.SystemTime}} system 20 | ProcessState: {{.Result.Cmd.ProcessState}} 21 | Error: {{.Result.Error}} 22 | Stderr:{{if .Result.StderrExtra}} 23 | ... {{.Result.StderrExtra}} more bytes ...{{end}} 24 | {{.Result.Stderr}} 25 | 26 | Stdout:{{if .Result.StdoutExtra}} 27 | ... {{.Result.StdoutExtra}} more bytes ...{{end}} 28 | {{.Result.Stdout}} 29 | ` 30 | 31 | type Message struct { 32 | To string 33 | From string 34 | Subject string 35 | Result *Result 36 | } 37 | 38 | func (m *Message) Bytes() []byte { 39 | var buf bytes.Buffer 40 | t := template.New("mail") 41 | t, _ = t.Parse(emailTemplate) 42 | t.Execute(&buf, m) 43 | return buf.Bytes() 44 | } 45 | 46 | func (m *Message) String() string { 47 | return string(m.Bytes()) 48 | } 49 | 50 | type Result struct { 51 | Cmd *exec.Cmd 52 | Error error 53 | Start time.Time 54 | End time.Time 55 | Duration time.Duration 56 | Stdout string 57 | Stderr string 58 | StdoutExtra int 59 | StderrExtra int 60 | } 61 | 62 | func identity() string { 63 | var username string 64 | user, err := user.Current() 65 | if err != nil { 66 | username = "root" 67 | } else { 68 | username = user.Username 69 | } 70 | 71 | hostname, err := os.Hostname() 72 | if err != nil { 73 | hostname = "localhost" 74 | } 75 | return username + "@" + hostname 76 | } 77 | 78 | type Mailer interface { 79 | Send([]string, string, []byte) error 80 | } 81 | 82 | func findMailer() Mailer { 83 | if path, err := FindSendmail(); err == nil { 84 | return &SendMailMailer{path} 85 | } 86 | 87 | return &SMTPMailer{} 88 | } 89 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mattrobenolt/email-me 2 | 3 | require ( 4 | github.com/BurntSushi/toml v0.3.1 5 | github.com/cespare/window v0.0.0-20171225023059-41bd47abf76a 6 | github.com/codegangsta/cli v1.20.0 7 | github.com/jeanfric/goembed v0.0.0-20150102173004-6e25e9e10085 8 | github.com/urfave/cli v1.20.0 9 | gopkg.in/urfave/cli.v1 v1.20.0 10 | gopkg.in/yaml.v2 v2.2.1 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/cespare/window v0.0.0-20171225023059-41bd47abf76a h1:07gZffk1B6PDDBqKGEhLmPbg8Eoy8BPRqvnkFtLWW1U= 4 | github.com/cespare/window v0.0.0-20171225023059-41bd47abf76a/go.mod h1:qwz7ImUCNIeTsG45IirkwRhbeqiE3wNTNHo0yEedfZo= 5 | github.com/codegangsta/cli v1.20.0 h1:iX1FXEgwzd5+XN6wk5cVHOGQj6Q3Dcp20lUeS4lHNTw= 6 | github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA= 7 | github.com/jeanfric/goembed v0.0.0-20150102173004-6e25e9e10085 h1:LrtiEavQ1Z2Noia6FeIumN6Mk77HMhEk56IWb7sIB0Y= 8 | github.com/jeanfric/goembed v0.0.0-20150102173004-6e25e9e10085/go.mod h1:SwIQi40DEpdwnPYtdLn5U6hNLXIJF2BsW8ziB4H4E4A= 9 | github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= 10 | github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 12 | gopkg.in/urfave/cli.v1 v1.20.0 h1:NdAVW6RYxDif9DhDHaAortIu956m2c0v+09AZBPTbE0= 13 | gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= 14 | gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= 15 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 16 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "runtime" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/cespare/window" 14 | "github.com/codegangsta/cli" 15 | "github.com/jeanfric/goembed/countingwriter" 16 | ) 17 | 18 | const Version = "0.4.0" 19 | 20 | func usageAndExit(s string, c *cli.Context) { 21 | fmt.Printf("!! %s\n\n", s) 22 | cli.ShowAppHelp(c) 23 | os.Exit(1) 24 | } 25 | 26 | func init() { 27 | runtime.GOMAXPROCS(1) 28 | runtime.LockOSThread() 29 | } 30 | 31 | func main() { 32 | app := cli.NewApp() 33 | app.Name = "email-me" 34 | app.Version = Version 35 | app.Usage = "email me when a thing is done" 36 | app.Action = main2 37 | app.HideHelp = true 38 | app.Flags = []cli.Flag{ 39 | cli.StringFlag{ 40 | Name: "to", 41 | Value: "", 42 | Usage: "email address to send output to", 43 | EnvVar: "EMAIL_ME_TO", 44 | }, 45 | cli.StringFlag{ 46 | Name: "subject, s", 47 | Value: "", 48 | Usage: "subject of email (optional)", 49 | }, 50 | cli.IntFlag{ 51 | Name: "max", 52 | Value: 10000, 53 | Usage: "max bytes to capture for stdout/stderr", 54 | EnvVar: "EMAIL_ME_MAX", 55 | }, 56 | cli.BoolFlag{ 57 | Name: "on-error", 58 | Usage: "only notify on a non-0 exit code", 59 | }, 60 | } 61 | app.Run(os.Args) 62 | } 63 | 64 | func main2(c *cli.Context) { 65 | to := c.String("to") 66 | max := c.Int("max") 67 | subject := c.String("subject") 68 | onError := c.Bool("on-error") 69 | 70 | if to == "" { 71 | usageAndExit("missing --to=[address]", c) 72 | } 73 | 74 | args := c.Args() 75 | if len(args) == 0 { 76 | usageAndExit("missing [command]", c) 77 | } 78 | 79 | truncStdout := window.NewWriter(max) 80 | truncStderr := window.NewWriter(max) 81 | countedStdout := countingwriter.New(truncStdout) 82 | countedStderr := countingwriter.New(truncStderr) 83 | child := exec.Command(args[0], args[1:]...) 84 | child.Stdout = io.MultiWriter(os.Stdout, countedStdout) 85 | child.Stderr = io.MultiWriter(os.Stderr, countedStderr) 86 | child.Stdin = os.Stdin 87 | 88 | start := time.Now() 89 | err := child.Run() 90 | end := time.Now() 91 | 92 | stdout := truncStdout.Bytes() 93 | stderr := truncStderr.Bytes() 94 | 95 | r := &Result{ 96 | Cmd: child, 97 | Error: err, 98 | Start: start, 99 | End: end, 100 | Duration: end.Sub(start), 101 | Stdout: string(stdout), 102 | Stderr: string(stderr), 103 | StdoutExtra: countedStdout.BytesWritten() - len(stdout), 104 | StderrExtra: countedStderr.BytesWritten() - len(stderr), 105 | } 106 | 107 | me := identity() 108 | if subject == "" { 109 | subject = fmt.Sprintf("%s", child.Args) 110 | } 111 | 112 | m := &Message{ 113 | To: to, 114 | From: me, 115 | Subject: subject, 116 | Result: r, 117 | } 118 | 119 | exitStatus := 0 120 | if !child.ProcessState.Success() { 121 | exitStatus = 1 122 | } 123 | 124 | // Try to fetch the actual status code if we can 125 | if status, ok := child.ProcessState.Sys().(syscall.WaitStatus); ok { 126 | exitStatus = status.ExitStatus() 127 | } 128 | 129 | if onError && exitStatus == 0 { 130 | os.Exit(0) 131 | } 132 | 133 | if err := findMailer().Send([]string{to}, me, m.Bytes()); err != nil { 134 | log.Fatal(err) 135 | } 136 | 137 | os.Exit(exitStatus) 138 | } 139 | -------------------------------------------------------------------------------- /sendmail.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "os/exec" 6 | ) 7 | 8 | func FindSendmail() (path string, err error) { 9 | for _, option := range []string{"/usr/sbin/sendmail", "sendmail"} { 10 | path, err = exec.LookPath(option) 11 | if err == nil { 12 | break 13 | } 14 | } 15 | return 16 | } 17 | 18 | type SendMailMailer struct { 19 | path string 20 | } 21 | 22 | func (m *SendMailMailer) Send(to []string, from string, body []byte) error { 23 | sendmail := exec.Command(m.path, to...) 24 | sendmail.Stdin = bytes.NewReader(body) 25 | return sendmail.Run() 26 | } 27 | -------------------------------------------------------------------------------- /smtp.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "net/smtp" 4 | 5 | type SMTPMailer struct{} 6 | 7 | func (m *SMTPMailer) Send(to []string, from string, body []byte) error { 8 | return smtp.SendMail("localhost:25", nil, from, to, body) 9 | } 10 | --------------------------------------------------------------------------------