├── LICENSE ├── README.md ├── email.go ├── email_test.go └── go.mod /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Steve Manuel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Email 2 | 3 | I needed a way to send email from a [Ponzu](https://ponzu-cms.org) installation 4 | running on all kinds of systems without shelling out. `sendmail` or `postfix` et 5 | al are not standard on all systems, and I didn't want to force users to add API 6 | keys from a third-party just to send something like an account recovery email. 7 | 8 | ### Usage: 9 | `$ go get github.com/nilslice/email` 10 | 11 | ```go 12 | package main 13 | 14 | import ( 15 | "fmt" 16 | "github.com/nilslice/email" 17 | ) 18 | 19 | func main() { 20 | msg := email.Message{ 21 | To: "you@server.name", // do not add < > or name in quotes 22 | From: "me@server.name", // do not add < > or name in quotes 23 | Subject: "A simple email", 24 | Body: "Plain text email body. HTML not yet supported, but send a PR!", 25 | } 26 | 27 | err := msg.Send() 28 | if err != nil { 29 | fmt.Println(err) 30 | } 31 | } 32 | 33 | ``` 34 | 35 | ### Under the hood 36 | `email` looks at a `Message`'s `To` field, splits the string on the @ symbol and 37 | issues an MX lookup to find the mail exchange server(s). Then it iterates over 38 | all the possibilities in combination with commonly used SMTP ports for non-SSL 39 | clients: `25, 2525, & 587` 40 | 41 | It stops once it has an active client connected to a mail server and sends the 42 | initial information, the message, and then closes the connection. 43 | 44 | Currently, this doesn't support any additional headers or `To` field formatting 45 | (the recipient's email must be the only string `To` takes). Although these would 46 | be fairly strightforward to implement, I don't need them yet.. so feel free to 47 | contribute anything you find useful. 48 | 49 | #### Warning 50 | Be cautious of how often you run this locally or in testing, as it's quite 51 | likely your IP will be blocked/blacklisted if it is not already. 52 | -------------------------------------------------------------------------------- /email.go: -------------------------------------------------------------------------------- 1 | package email 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/smtp" 7 | "strings" 8 | ) 9 | 10 | // Message creates a email to be sent 11 | type Message struct { 12 | To string 13 | From string 14 | Subject string 15 | Body string 16 | } 17 | 18 | var ( 19 | ports = []int{25, 2525, 587} 20 | ) 21 | 22 | // Send sends a message to recipient(s) listed in the 'To' field of a Message 23 | func (m Message) Send() error { 24 | if !strings.Contains(m.To, "@") { 25 | return fmt.Errorf("Invalid recipient address: <%s>", m.To) 26 | } 27 | 28 | host := strings.Split(m.To, "@")[1] 29 | addrs, err := net.LookupMX(host) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | c, err := newClient(addrs, ports) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | err = send(m, c) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | return nil 45 | } 46 | 47 | func newClient(mx []*net.MX, ports []int) (*smtp.Client, error) { 48 | for i := range mx { 49 | for j := range ports { 50 | server := strings.TrimSuffix(mx[i].Host, ".") 51 | hostPort := fmt.Sprintf("%s:%d", server, ports[j]) 52 | client, err := smtp.Dial(hostPort) 53 | if err != nil { 54 | if j == len(ports)-1 { 55 | return nil, err 56 | } 57 | 58 | continue 59 | } 60 | 61 | return client, nil 62 | } 63 | } 64 | 65 | return nil, fmt.Errorf("Couldn't connect to servers %v on any common port.", mx) 66 | } 67 | 68 | func send(m Message, c *smtp.Client) error { 69 | if err := c.Mail(m.From); err != nil { 70 | return err 71 | } 72 | 73 | if err := c.Rcpt(m.To); err != nil { 74 | return err 75 | } 76 | 77 | msg, err := c.Data() 78 | if err != nil { 79 | return err 80 | } 81 | 82 | if m.Subject != "" { 83 | _, err = msg.Write([]byte("Subject: " + m.Subject + "\r\n")) 84 | if err != nil { 85 | return err 86 | } 87 | } 88 | 89 | if m.From != "" { 90 | _, err = msg.Write([]byte("From: <" + m.From + ">\r\n")) 91 | if err != nil { 92 | return err 93 | } 94 | } 95 | 96 | if m.To != "" { 97 | _, err = msg.Write([]byte("To: <" + m.To + ">\r\n")) 98 | if err != nil { 99 | return err 100 | } 101 | } 102 | 103 | _, err = fmt.Fprint(msg, m.Body) 104 | if err != nil { 105 | return err 106 | } 107 | 108 | err = msg.Close() 109 | if err != nil { 110 | return err 111 | } 112 | 113 | err = c.Quit() 114 | if err != nil { 115 | return err 116 | } 117 | 118 | return nil 119 | } 120 | -------------------------------------------------------------------------------- /email_test.go: -------------------------------------------------------------------------------- 1 | package email 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSend(t *testing.T) { 8 | m := Message{ 9 | To: "", 10 | From: "", 11 | Subject: "", 12 | Body: "", 13 | } 14 | 15 | err := m.Send() 16 | if err != nil { 17 | t.Fatal("Send returned error:", err) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nilslice/email 2 | --------------------------------------------------------------------------------