Initial QSfera import
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* -text
|
||||
|
||||
# Custom for Visual Studio
|
||||
*.cs diff=csharp
|
||||
|
||||
# Standard to msysgit
|
||||
*.doc diff=astextplain
|
||||
*.DOC diff=astextplain
|
||||
*.docx diff=astextplain
|
||||
*.DOCX diff=astextplain
|
||||
*.dot diff=astextplain
|
||||
*.DOT diff=astextplain
|
||||
*.pdf diff=astextplain
|
||||
*.PDF diff=astextplain
|
||||
*.rtf diff=astextplain
|
||||
*.RTF diff=astextplain
|
||||
@@ -0,0 +1,31 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
# Eclipse Project files
|
||||
.project
|
||||
.settings
|
||||
|
||||
# Go coverage file
|
||||
coverage.txt
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
# This file contains all available configuration options
|
||||
# with their default values.
|
||||
|
||||
# options for analysis running
|
||||
run:
|
||||
concurrency: 4
|
||||
timeout: 10m
|
||||
issues-exit-code: 1
|
||||
tests: true
|
||||
|
||||
# output configuration options
|
||||
output:
|
||||
format: line-number
|
||||
|
||||
# all available settings of specific linters
|
||||
linters-settings:
|
||||
govet:
|
||||
# report about shadowed variables
|
||||
check-shadowing: true
|
||||
misspell:
|
||||
locale: US
|
||||
unused:
|
||||
# treat code as a program (not a library) and report unused exported identifiers; default is false.
|
||||
# XXX: if you enable this setting, unused will report a lot of false-positives in text editors:
|
||||
# if it's called for subdir of a project it can't find funcs usages. All text editor integrations
|
||||
# with golangci-lint call it on a directory with the changed file.
|
||||
check-exported: false
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- asciicheck
|
||||
- deadcode
|
||||
- dogsled
|
||||
- exportloopref
|
||||
- golint
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- megacheck
|
||||
- misspell
|
||||
- nakedret
|
||||
- nolintlint
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unused
|
||||
- varcheck
|
||||
disable:
|
||||
- errcheck
|
||||
disable-all: false
|
||||
fast: false
|
||||
|
||||
issues:
|
||||
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
|
||||
max-issues-per-linter: 0
|
||||
|
||||
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
|
||||
max-same-issues: 0
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019 Santiago De la Cruz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+351
@@ -0,0 +1,351 @@
|
||||
# Go Simple Mail
|
||||
|
||||
The best way to send emails in Go with SMTP Keep Alive and Timeout for Connect and Send.
|
||||
|
||||
<a href="https://goreportcard.com/report/github.com/xhit/go-simple-mail/v2"><img src="https://goreportcard.com/badge/github.com/xhit/go-simple-mail" alt="Go Report Card"></a>
|
||||
<a href="https://pkg.go.dev/github.com/xhit/go-simple-mail/v2?tab=doc"><img src="https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white" alt="go.dev"></a>
|
||||
|
||||
|
||||
# IMPORTANT
|
||||
|
||||
Examples in this README are for v2.2.0 and above. Examples for older versions
|
||||
can be found [here](https://gist.github.com/xhit/54516917473420a8db1b6fff68a21c99).
|
||||
|
||||
Go 1.13+ is required.
|
||||
|
||||
Breaking change in 2.2.0: The signature of `SetBody` and `AddAlternative` used
|
||||
to accept strings ("text/html" and "text/plain") and not require on of the
|
||||
`contentType` constants (`TextHTML` or `TextPlain`). Upgrading, while not
|
||||
quite following semantic versioning, is quite simple:
|
||||
|
||||
```diff
|
||||
email := mail.NewMSG()
|
||||
- email.SetBody("text/html", htmlBody)
|
||||
- email.AddAlternative("text/plain", plainBody)
|
||||
+ email.SetBody(mail.TextHTML, htmlBody)
|
||||
+ email.AddAlternative(mail.TextPlain, plainBody)
|
||||
```
|
||||
|
||||
# Introduction
|
||||
|
||||
Go Simple Mail is a simple and efficient package to send emails. It is well tested and
|
||||
documented.
|
||||
|
||||
Go Simple Mail can only send emails using an SMTP server. But the API is flexible and it
|
||||
is easy to implement other methods for sending emails using a local Postfix, an API, etc.
|
||||
|
||||
This package contains (and is based on) two packages by **Joe Grasse**:
|
||||
|
||||
- https://github.com/joegrasse/mail (unmaintained since Jun 29, 2018), and
|
||||
- https://github.com/joegrasse/mime (unmaintained since Oct 1, 2015).
|
||||
|
||||
A lot of changes in Go Simple Mail were sent with not response.
|
||||
|
||||
## Features
|
||||
|
||||
Go Simple Mail supports:
|
||||
|
||||
- Multiple Attachments with path
|
||||
- Multiple Attachments in base64
|
||||
- Multiple Attachments from bytes (since v2.6.0)
|
||||
- Inline attachments from file, base64 and bytes (bytes since v2.6.0)
|
||||
- Multiple Recipients
|
||||
- Priority
|
||||
- Reply to
|
||||
- Set sender
|
||||
- Set from
|
||||
- Allow sending mail with different envelope from (since v2.7.0)
|
||||
- Embedded images
|
||||
- HTML and text templates
|
||||
- Automatic encoding of special characters
|
||||
- SSL/TLS and STARTTLS
|
||||
- Unencrypted connection (not recommended)
|
||||
- Sending multiple emails with the same SMTP connection (Keep Alive or Persistent Connection)
|
||||
- Timeout for connect to a SMTP Server
|
||||
- Timeout for send an email
|
||||
- Return Path
|
||||
- Alternative Email Body
|
||||
- CC and BCC
|
||||
- Add Custom Headers in Message
|
||||
- Send NOOP, RESET, QUIT and CLOSE to SMTP client
|
||||
- PLAIN, LOGIN and CRAM-MD5 Authentication (since v2.3.0)
|
||||
- AUTO Authentication (since v2.14.0)
|
||||
- Allow connect to SMTP without authentication (since v2.10.0)
|
||||
- Custom TLS Configuration (since v2.5.0)
|
||||
- Send a RFC822 formatted message (since v2.8.0)
|
||||
- Send from localhost (yes, Go standard SMTP package cannot do that because... WTF Google!)
|
||||
- Support text/calendar content type body (since v2.11.0)
|
||||
- Support add a List-Unsubscribe header (since v2.11.0)
|
||||
- Support to add a DKIM signarure (since v2.11.0)
|
||||
- Support to send using custom connection, ideal for proxy (since v2.15.0)
|
||||
- Support Delivery Status Notification (DSN) (since v2.16.0)
|
||||
- Support text/x-amp-html content type body (since v2.16.0)
|
||||
|
||||
## Documentation
|
||||
|
||||
https://pkg.go.dev/github.com/xhit/go-simple-mail/v2?tab=doc
|
||||
|
||||
Note 1: by default duplicated recipients throws an error, from `v2.13.0` you can use `email.AllowDuplicateAddress = true` to avoid the check.
|
||||
|
||||
Note 2: by default Bcc header is not set in email. From `v2.14.0` you can use `email.AddBccToHeader = true` to add this.
|
||||
|
||||
## Download
|
||||
|
||||
This package uses go modules.
|
||||
|
||||
```console
|
||||
$ go get github.com/xhit/go-simple-mail/v2
|
||||
```
|
||||
|
||||
# Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
mail "github.com/xhit/go-simple-mail/v2"
|
||||
"github.com/toorop/go-dkim"
|
||||
)
|
||||
|
||||
const htmlBody = `<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<title>Hello Gophers!</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>This is the <b>Go gopher</b>.</p>
|
||||
<p><img src="cid:Gopher.png" alt="Go gopher" /></p>
|
||||
<p>Image created by Renee French</p>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
const privateKey = `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAwGrscUxi9zEa9oMOJbS0kLVHZXNIW+EBjY7KFWIZSxuGAils
|
||||
wBVl+s5mMRrR5VlkyLQNulAdNemg6OSeB0R+2+8/lkHiMrimqQckZ5ig8slBoZhZ
|
||||
wUoL/ZkeQa1bacbdww5TuWkiVPD9kooT/+TZW1P/ugd6oYjpOI56ZjsXzJw5pz7r
|
||||
DiwcIJJaaDIqvvc5C4iW94GZjwtmP5pxhvBZ5D6Uzmh7Okvi6z4QCKzdJQLdVmC0
|
||||
CMiFeh2FwqMkVpjZhNt3vtCo7Z51kwHVscel6vl51iQFq/laEzgzAWOUQ+ZEoQpL
|
||||
uTaUiYzzNyEdGEzZ2CjMMoO8RgtXnUo2qX2FDQIDAQABAoIBAHWKW3kycloSMyhX
|
||||
EnNSGeMz+bMtYwxNPMeebC/3xv+shoYXjAkiiTNWlfJ1MbbqjrhT1Pb1LYLbfqIF
|
||||
1csWum/bjHpbMLRPO++RH1nxUJA/BMqT6HA8rWpy+JqiLW9GPf2DaP2gDYrZ0+yK
|
||||
UIFG6MfzXgnju7OlkOItlvOQMY+Y501u/h6xnN2yTeRqXXJ1YlWFPRIeFdS6UOtL
|
||||
J2wSxRVdymHbGwf+D7zet7ngMPwFBsbEN/83KGLRjkt8+dMQeUeob+nslsQofCZx
|
||||
iokIAvByTugmqrB4JqhNkAlZhC0mqkRQh7zUFrxSj5UppMWlxLH+gPFZHKAsUJE5
|
||||
mqmylcECgYEA8I/f90cpF10uH4NPBCR4+eXq1PzYoD+NdXykN65bJTEDZVEy8rBO
|
||||
phXRNfw030sc3R0waQaZVhFuSgshhRuryfG9c1FP6tQhqi/jiEj9IfCW7zN9V/P2
|
||||
r16pGjLuCK4SyxUC8H58Q9I0X2CQqFamtkLXC6Ogy86rZfIc8GcvZ9UCgYEAzMQZ
|
||||
WAiLhRF2MEmMhKL+G3jm20r+dOzPYkfGxhIryluOXhuUhnxZWL8UZfiEqP5zH7Li
|
||||
NeJvLz4pOL45rLw44qiNu6sHN0JNaKYvwNch1wPT/3/eDNZKKePqbAG4iamhjLy5
|
||||
gjO1KgA5FBbcNN3R6fuJAg1e4QJCOuo55eW6vFkCgYEA7UBIV72D5joM8iFzvZcn
|
||||
BPdfqh2QnELxhaye3ReFZuG3AqaZg8akWqLryb1qe8q9tclC5GIQulTInBfsQDXx
|
||||
MGLNQL0x/1ylsw417kRl+qIoidMTTLocUgse5erS3haoDEg1tPBaKB1Zb7NyF8QV
|
||||
+W1kX2NKg5bZbdrh9asekt0CgYA6tUam7NxDrLv8IDo/lRPSAJn/6cKG95aGERo2
|
||||
k+MmQ5XP+Yxd+q0LOs24ZsZyRXHwdrNQy7khDGt5L2EN23Fb2wO3+NM6zrGu/WbX
|
||||
nVbAdQKFUL3zZEUjOYtuqBemsJH27e0qHXUls6ap0dwU9DxJH6sqgXbggGtIxPsQ
|
||||
pQsjEQKBgQC9gAqAj+ZtMXNG9exVPT8I15reox9kwxGuvJrRu/5eSi6jLR9z3x9P
|
||||
2FrgxQ+GCB2ypoOUcliXrKesdSbolUilA8XQn/M113Lg8oA3gJXbAKqbTR/EgfUU
|
||||
kvYaR/rTFnivF4SL/P4k/gABQoJuFUtSKdouELqefXB+e94g/G++Bg==
|
||||
-----END RSA PRIVATE KEY-----`
|
||||
|
||||
func main() {
|
||||
server := mail.NewSMTPClient()
|
||||
|
||||
// SMTP Server
|
||||
server.Host = "smtp.example.com"
|
||||
server.Port = 587
|
||||
server.Username = "test@example.com"
|
||||
server.Password = "examplepass"
|
||||
server.Encryption = mail.EncryptionSTARTTLS
|
||||
|
||||
// You can specified authentication type:
|
||||
// - AUTO (default)
|
||||
// - PLAIN
|
||||
// - LOGIN
|
||||
// - CRAM-MD5
|
||||
// - None
|
||||
// server.Authentication = mail.AuthAuto
|
||||
|
||||
// Variable to keep alive connection
|
||||
server.KeepAlive = false
|
||||
|
||||
// Timeout for connect to SMTP Server
|
||||
server.ConnectTimeout = 10 * time.Second
|
||||
|
||||
// Timeout for send the data and wait respond
|
||||
server.SendTimeout = 10 * time.Second
|
||||
|
||||
// Set TLSConfig to provide custom TLS configuration. For example,
|
||||
// to skip TLS verification (useful for testing):
|
||||
server.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
|
||||
// SMTP client
|
||||
smtpClient,err := server.Connect()
|
||||
|
||||
if err != nil{
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// New email simple html with inline and CC
|
||||
email := mail.NewMSG()
|
||||
email.SetFrom("From Example <nube@example.com>").
|
||||
AddTo("xhit@example.com").
|
||||
AddCc("otherto@example.com").
|
||||
SetSubject("New Go Email").
|
||||
SetListUnsubscribe("<mailto:unsubscribe@example.com?subject=https://example.com/unsubscribe>")
|
||||
|
||||
email.SetBody(mail.TextHTML, htmlBody)
|
||||
|
||||
// also you can add body from []byte with SetBodyData, example:
|
||||
// email.SetBodyData(mail.TextHTML, []byte(htmlBody))
|
||||
// or alternative part
|
||||
// email.AddAlternativeData(mail.TextHTML, []byte(htmlBody))
|
||||
|
||||
// add inline
|
||||
email.Attach(&mail.File{FilePath: "/path/to/image.png", Name:"Gopher.png", Inline: true})
|
||||
|
||||
// also you can set Delivery Status Notification (DSN) (only is set when server supports DSN)
|
||||
email.SetDSN([]mail.DSN{mail.SUCCESS, mail.FAILURE}, false)
|
||||
|
||||
// you can add dkim signature to the email.
|
||||
// to add dkim, you need a private key already created one.
|
||||
if privateKey != "" {
|
||||
options := dkim.NewSigOptions()
|
||||
options.PrivateKey = []byte(privateKey)
|
||||
options.Domain = "example.com"
|
||||
options.Selector = "default"
|
||||
options.SignatureExpireIn = 3600
|
||||
options.Headers = []string{"from", "date", "mime-version", "received", "received"}
|
||||
options.AddSignatureTimestamp = true
|
||||
options.Canonicalization = "relaxed/relaxed"
|
||||
|
||||
email.SetDkim(options)
|
||||
}
|
||||
|
||||
// always check error after send
|
||||
if email.Error != nil{
|
||||
log.Fatal(email.Error)
|
||||
}
|
||||
|
||||
// Call Send and pass the client
|
||||
err = email.Send(smtpClient)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
} else {
|
||||
log.Println("Email Sent")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Send multiple emails in same connection
|
||||
|
||||
```go
|
||||
//Set your smtpClient struct to keep alive connection
|
||||
server.KeepAlive = true
|
||||
|
||||
for _, to := range []string{
|
||||
"to1@example1.com",
|
||||
"to3@example2.com",
|
||||
"to4@example3.com",
|
||||
} {
|
||||
// New email simple html with inline and CC
|
||||
email := mail.NewMSG()
|
||||
email.SetFrom("From Example <nube@example.com>").
|
||||
AddTo(to).
|
||||
SetSubject("New Go Email")
|
||||
|
||||
email.SetBody(mail.TextHTML, htmlBody)
|
||||
|
||||
// add inline
|
||||
email.Attach(&mail.File{FilePath: "/path/to/image.png", Name:"Gopher.png", Inline: true})
|
||||
|
||||
// always check error after send
|
||||
if email.Error != nil{
|
||||
log.Fatal(email.Error)
|
||||
}
|
||||
|
||||
// Call Send and pass the client
|
||||
err = email.Send(smtpClient)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
} else {
|
||||
log.Println("Email Sent")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Send with custom connection
|
||||
|
||||
It's possible to use a custom connection with custom dieler, like a dialer that uses a proxy server, etc...
|
||||
|
||||
With this, these servers params are ignored: `Host`, `Port`. You have total control of the connection.
|
||||
|
||||
Example using a conn for proxy:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
|
||||
mail "github.com/xhit/go-simple-mail/v2"
|
||||
"golang.org/x/net/proxy"
|
||||
)
|
||||
|
||||
func main() {
|
||||
server := mail.NewSMTPClient()
|
||||
|
||||
host := "smtp.example.com"
|
||||
port := 587
|
||||
proxyAddr := "proxy.server"
|
||||
|
||||
conn, err := getCustomConnWithProxy(proxyAddr, host, port)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
server.Username = "test@example.com"
|
||||
server.Password = "examplepass"
|
||||
server.Encryption = mail.EncryptionSTARTTLS
|
||||
server.CustomConn = conn
|
||||
|
||||
smtpClient, err := server.Connect()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
email := mail.NewMSG()
|
||||
|
||||
err = email.SetFrom("From Example <nube@example.com>").AddTo("xhit@example.com").Send(smtpClient)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func getCustomConnWithProxy(proxyAddr, host string, port int) (net.Conn, error) {
|
||||
dial, err := proxy.SOCKS5("tcp", proxyAddr, nil, proxy.Direct)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dialer, err := dial.Dial("tcp", fmt.Sprintf("%s:%d", host, port))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conf := &tls.Config{ServerName: host}
|
||||
conn := tls.Client(dialer, conf)
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## More examples
|
||||
|
||||
See [example/example_test.go](example/example_test.go).
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// File represents the file that can be added to the email message.
|
||||
// You can add attachment from file in path, from base64 string or from []byte.
|
||||
// You can define if attachment is inline or not.
|
||||
// Only one, Data, B64Data or FilePath is supported. If multiple are set, then
|
||||
// the first in that order is used.
|
||||
type File struct {
|
||||
// FilePath is the path of the file to attach.
|
||||
FilePath string
|
||||
// Name is the name of file in attachment. Required for Data and B64Data. Optional for FilePath.
|
||||
Name string
|
||||
// MimeType of attachment. If empty then is obtained from Name (if not empty) or FilePath. If cannot obtained, application/octet-stream is set.
|
||||
MimeType string
|
||||
// B64Data is the base64 string to attach.
|
||||
B64Data string
|
||||
// Data is the []byte of file to attach.
|
||||
Data []byte
|
||||
// Inline defines if attachment is inline or not.
|
||||
Inline bool
|
||||
}
|
||||
|
||||
type attachType int
|
||||
|
||||
const (
|
||||
attachData attachType = iota
|
||||
attachB64
|
||||
attachFile
|
||||
)
|
||||
|
||||
// Attach allows you to add an attachment to the email message.
|
||||
// The attachment can be inlined
|
||||
func (email *Email) Attach(file *File) *Email {
|
||||
if email.Error != nil {
|
||||
return email
|
||||
}
|
||||
|
||||
var name = file.Name
|
||||
var mimeType = file.MimeType
|
||||
|
||||
// if no alternative name was provided, get the filename
|
||||
if len(name) == 0 && len(file.FilePath) > 0 {
|
||||
_, name = filepath.Split(file.FilePath)
|
||||
}
|
||||
|
||||
// get the mimetype
|
||||
if mimeType == "" {
|
||||
mimeType = mime.TypeByExtension(filepath.Ext(name))
|
||||
if mimeType == "" {
|
||||
mimeType = "application/octet-stream"
|
||||
}
|
||||
}
|
||||
|
||||
attachTy, err := getAttachmentType(file)
|
||||
if err != nil {
|
||||
email.Error = errors.New("Mail Error: Failed to add attachment with following error: " + err.Error())
|
||||
return email
|
||||
}
|
||||
|
||||
file.Name = name
|
||||
file.MimeType = mimeType
|
||||
|
||||
switch attachTy {
|
||||
case attachData:
|
||||
email.attachData(file)
|
||||
case attachB64:
|
||||
email.Error = email.attachB64(file)
|
||||
case attachFile:
|
||||
email.Error = email.attachFile(file)
|
||||
}
|
||||
|
||||
return email
|
||||
}
|
||||
|
||||
func getAttachmentType(file *File) (attachType, error) {
|
||||
// 1- data
|
||||
// 2- base64
|
||||
// 3- file
|
||||
|
||||
// first check if Data
|
||||
if len(file.Data) > 0 {
|
||||
// data requires a name
|
||||
if len(file.Name) == 0 {
|
||||
return 0, errors.New("attach from bytes requires a name")
|
||||
}
|
||||
return attachData, nil
|
||||
}
|
||||
|
||||
// check if base64
|
||||
if len(file.B64Data) > 0 {
|
||||
// B64Data requires a name
|
||||
if len(file.Name) == 0 {
|
||||
return 0, errors.New("attach from base64 string requires a name")
|
||||
}
|
||||
return attachB64, nil
|
||||
}
|
||||
|
||||
// check if file
|
||||
if len(file.FilePath) > 0 {
|
||||
return attachFile, nil
|
||||
}
|
||||
|
||||
return 0, errors.New("empty attachment")
|
||||
}
|
||||
|
||||
// attachB64 does the low level attaching of the files but decoding base64
|
||||
func (email *Email) attachB64(file *File) error {
|
||||
|
||||
// decode the string
|
||||
dec, err := base64.StdEncoding.DecodeString(file.B64Data)
|
||||
if err != nil {
|
||||
return errors.New("Mail Error: Failed to decode base64 attachment with following error: " + err.Error())
|
||||
}
|
||||
|
||||
email.attachData(&File{
|
||||
Name: file.Name,
|
||||
MimeType: file.MimeType,
|
||||
Data: dec,
|
||||
Inline: file.Inline,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (email *Email) attachFile(file *File) error {
|
||||
data, err := ioutil.ReadFile(file.FilePath)
|
||||
if err != nil {
|
||||
return errors.New("Mail Error: Failed to add file with following error: " + err.Error())
|
||||
}
|
||||
|
||||
email.attachData(&File{
|
||||
Name: file.Name,
|
||||
MimeType: file.MimeType,
|
||||
Data: data,
|
||||
Inline: file.Inline,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// attachData does the low level attaching of the in-memory data
|
||||
func (email *Email) attachData(file *File) {
|
||||
// use inlines and attachments because is necessary to know if message has related parts and mixed parts
|
||||
if file.Inline {
|
||||
email.inlines = append(email.inlines, file)
|
||||
} else {
|
||||
email.attachments = append(email.attachments, file)
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// TODO: Remove this file before launch v3
|
||||
|
||||
// AddAttachment. DEPRECATED. Use Attach method. Allows you to add an attachment to the email message.
|
||||
// You can optionally provide a different name for the file.
|
||||
func (email *Email) AddAttachment(file string, name ...string) *Email {
|
||||
if email.Error != nil {
|
||||
return email
|
||||
}
|
||||
|
||||
if len(name) > 1 {
|
||||
email.Error = errors.New("Mail Error: Attach can only have a file and an optional name")
|
||||
return email
|
||||
}
|
||||
|
||||
var nm string
|
||||
if len(name) == 1 {
|
||||
nm = name[0]
|
||||
}
|
||||
return email.Attach(&File{Name: nm, FilePath: file})
|
||||
}
|
||||
|
||||
// AddAttachmentData. DEPRECATED. Use Attach method. Allows you to add an in-memory attachment to the email message.
|
||||
func (email *Email) AddAttachmentData(data []byte, filename, mimeType string) *Email {
|
||||
return email.Attach(&File{Data: data, Name: filename, MimeType: mimeType})
|
||||
}
|
||||
|
||||
// AddAttachmentBase64. DEPRECATED. Use Attach method. Allows you to add an attachment in base64 to the email message.
|
||||
// You need provide a name for the file.
|
||||
func (email *Email) AddAttachmentBase64(b64File, name string) *Email {
|
||||
return email.Attach(&File{B64Data: b64File, Name: name})
|
||||
}
|
||||
|
||||
// AddInline. DEPRECATED. Use Attach method. Allows you to add an inline attachment to the email message.
|
||||
// You can optionally provide a different name for the file.
|
||||
func (email *Email) AddInline(file string, name ...string) *Email {
|
||||
if email.Error != nil {
|
||||
return email
|
||||
}
|
||||
|
||||
if len(name) > 1 {
|
||||
email.Error = errors.New("Mail Error: Inline can only have a file and an optional name")
|
||||
return email
|
||||
}
|
||||
|
||||
var nm string
|
||||
if len(name) == 1 {
|
||||
nm = name[0]
|
||||
}
|
||||
|
||||
return email.Attach(&File{Name: nm, FilePath: file, Inline: true})
|
||||
}
|
||||
|
||||
// AddInlineData. DEPRECATED. Use Attach method. Allows you to add an inline in-memory attachment to the email message.
|
||||
func (email *Email) AddInlineData(data []byte, filename, mimeType string) *Email {
|
||||
return email.Attach(&File{Data: data, Name: filename, MimeType: mimeType, Inline: true})
|
||||
}
|
||||
|
||||
// AddInlineBase64. DEPRECATED. Use Attach method. Allows you to add an inline in-memory base64 encoded attachment to the email message.
|
||||
// You need provide a name for the file. If mimeType is an empty string, attachment mime type will be deduced
|
||||
// from the file name extension and defaults to application/octet-stream.
|
||||
func (email *Email) AddInlineBase64(b64File, name, mimeType string) *Email {
|
||||
return email.Attach(&File{B64Data: b64File, Name: name, MimeType: mimeType, Inline: true})
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in https://raw.githubusercontent.com/golang/go/master/LICENSE
|
||||
// auth.go file is a modification of smtp golang package what is frozen and is not accepting new features.
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// auth is implemented by an SMTP authentication mechanism.
|
||||
type auth interface {
|
||||
// start begins an authentication with a server.
|
||||
// It returns the name of the authentication protocol
|
||||
// and optionally data to include in the initial AUTH message
|
||||
// sent to the server. It can return proto == "" to indicate
|
||||
// that the authentication should be skipped.
|
||||
// If it returns a non-nil error, the SMTP client aborts
|
||||
// the authentication attempt and closes the connection.
|
||||
start(server *serverInfo) (proto string, toServer []byte, err error)
|
||||
|
||||
// next continues the authentication. The server has just sent
|
||||
// the fromServer data. If more is true, the server expects a
|
||||
// response, which next should return as toServer; otherwise
|
||||
// next should return toServer == nil.
|
||||
// If next returns a non-nil error, the SMTP client aborts
|
||||
// the authentication attempt and closes the connection.
|
||||
next(fromServer []byte, more bool) (toServer []byte, err error)
|
||||
}
|
||||
|
||||
// serverInfo records information about an SMTP server.
|
||||
type serverInfo struct {
|
||||
name string // SMTP server name
|
||||
tls bool // using TLS, with valid certificate for Name
|
||||
auth []string // advertised authentication mechanisms
|
||||
}
|
||||
|
||||
type plainAuth struct {
|
||||
identity, username, password string
|
||||
host string
|
||||
}
|
||||
|
||||
// plainAuthfn returns an auth that implements the PLAIN authentication
|
||||
// mechanism as defined in RFC 4616. The returned Auth uses the given
|
||||
// username and password to authenticate to host and act as identity.
|
||||
// Usually identity should be the empty string, to act as username.
|
||||
//
|
||||
// plainAuthfn will only send the credentials if the connection is using TLS
|
||||
// or is connected to localhost. Otherwise authentication will fail with an
|
||||
// error, without sending the credentials.
|
||||
func plainAuthfn(identity, username, password, host string) auth {
|
||||
return &plainAuth{identity, username, password, host}
|
||||
}
|
||||
|
||||
func (a *plainAuth) start(server *serverInfo) (string, []byte, error) {
|
||||
// Must have TLS, or else localhost server. Unencrypted connection is permitted here too but is not recommended
|
||||
// Note: If TLS is not true, then we can't trust ANYTHING in serverInfo.
|
||||
// In particular, it doesn't matter if the server advertises PLAIN auth.
|
||||
// That might just be the attacker saying
|
||||
// "it's ok, you can trust me with your password."
|
||||
if server.name != a.host {
|
||||
return "", nil, errors.New("wrong host name")
|
||||
}
|
||||
resp := []byte(a.identity + "\x00" + a.username + "\x00" + a.password)
|
||||
return "PLAIN", resp, nil
|
||||
}
|
||||
|
||||
func (a *plainAuth) next(fromServer []byte, more bool) ([]byte, error) {
|
||||
if more {
|
||||
// We've already sent everything.
|
||||
return nil, errors.New("unexpected server challenge")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
/*
|
||||
loginAuthfn authentication implements LOGIN Authentication, is the same PLAIN
|
||||
but username and password are sent in different commands
|
||||
*/
|
||||
|
||||
type loginAuth struct {
|
||||
identity, username, password string
|
||||
host string
|
||||
}
|
||||
|
||||
func loginAuthfn(identity, username, password, host string) auth {
|
||||
return &loginAuth{identity, username, password, host}
|
||||
}
|
||||
|
||||
func (a *loginAuth) start(server *serverInfo) (string, []byte, error) {
|
||||
if server.name != a.host {
|
||||
return "", nil, errors.New("wrong host name")
|
||||
}
|
||||
resp := []byte(a.username)
|
||||
return "LOGIN", resp, nil
|
||||
}
|
||||
|
||||
func (a *loginAuth) next(fromServer []byte, more bool) ([]byte, error) {
|
||||
if more {
|
||||
if strings.Contains(string(fromServer), "Username") {
|
||||
resp := []byte(a.username)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
if strings.Contains(string(fromServer), "Password") {
|
||||
resp := []byte(a.password)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// We've already sent everything.
|
||||
return nil, errors.New("unexpected server challenge")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type cramMD5Auth struct {
|
||||
username, secret string
|
||||
}
|
||||
|
||||
// cramMD5Authfn returns an Auth that implements the CRAM-MD5 authentication
|
||||
// mechanism as defined in RFC 2195.
|
||||
// The returned Auth uses the given username and secret to authenticate
|
||||
// to the server using the challenge-response mechanism.
|
||||
func cramMD5Authfn(username, secret string) auth {
|
||||
return &cramMD5Auth{username, secret}
|
||||
}
|
||||
|
||||
func (a *cramMD5Auth) start(server *serverInfo) (string, []byte, error) {
|
||||
return "CRAM-MD5", nil, nil
|
||||
}
|
||||
|
||||
func (a *cramMD5Auth) next(fromServer []byte, more bool) ([]byte, error) {
|
||||
if more {
|
||||
d := hmac.New(md5.New, []byte(a.secret))
|
||||
d.Write(fromServer)
|
||||
s := make([]byte, 0, d.Size())
|
||||
return []byte(fmt.Sprintf("%s %x", a.username, d.Sum(s))), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
+1097
File diff suppressed because it is too large
Load Diff
+197
@@ -0,0 +1,197 @@
|
||||
// headers.go implements "Q" encoding as specified by RFC 2047.
|
||||
//Modified from https://github.com/joegrasse/mime to use with Go Simple Mail
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type encoder struct {
|
||||
w *bufio.Writer
|
||||
charset string
|
||||
usedChars int
|
||||
}
|
||||
|
||||
// newEncoder returns a new mime header encoder that writes to w. The c
|
||||
// parameter specifies the name of the character set of the text that will be
|
||||
// encoded. The u parameter indicates how many characters have been used
|
||||
// already.
|
||||
func newEncoder(w io.Writer, c string, u int) *encoder {
|
||||
return &encoder{bufio.NewWriter(w), strings.ToUpper(c), u}
|
||||
}
|
||||
|
||||
// encode encodes p using the "Q" encoding and writes it to the underlying
|
||||
// io.Writer. It limits line length to 75 characters.
|
||||
func (e *encoder) encode(p []byte) (n int, err error) {
|
||||
var output bytes.Buffer
|
||||
allPrintable := true
|
||||
|
||||
// some lines we encode end in "
|
||||
//maxLineLength := 75 - 1
|
||||
maxLineLength := 76
|
||||
|
||||
// prevent header injection
|
||||
p = secureHeader(p)
|
||||
|
||||
// check to see if we have all printable characters
|
||||
for _, c := range p {
|
||||
if !isVchar(c) && !isWSP(c) {
|
||||
allPrintable = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// all characters are printable. just do line folding
|
||||
if allPrintable {
|
||||
text := string(p)
|
||||
words := strings.Split(text, " ")
|
||||
|
||||
lineBuffer := ""
|
||||
firstWord := true
|
||||
|
||||
// split the line where necessary
|
||||
for _, word := range words {
|
||||
/*fmt.Println("Current Line:",lineBuffer)
|
||||
fmt.Println("Here: Max:", maxLineLength ,"Buffer Length:", len(lineBuffer), "Used Chars:", e.usedChars, "Length Encoded Char:",len(word))
|
||||
fmt.Println("----------")*/
|
||||
|
||||
newWord := ""
|
||||
if !firstWord {
|
||||
newWord += " "
|
||||
}
|
||||
newWord += word
|
||||
|
||||
// check line length
|
||||
if (e.usedChars+len(lineBuffer)+len(newWord) /*+len(" ")+len(word)*/) > maxLineLength && (lineBuffer != "" || e.usedChars != 0) {
|
||||
output.WriteString(lineBuffer + "\r\n")
|
||||
|
||||
// first word on newline needs a space in front
|
||||
if !firstWord {
|
||||
lineBuffer = ""
|
||||
} else {
|
||||
lineBuffer = " "
|
||||
}
|
||||
|
||||
//firstLine = false
|
||||
//firstWord = true
|
||||
// reset since not on the first line anymore
|
||||
e.usedChars = 0
|
||||
}
|
||||
|
||||
/*if !firstWord {
|
||||
lineBuffer += " "
|
||||
}*/
|
||||
|
||||
lineBuffer += newWord /*word*/
|
||||
|
||||
firstWord = false
|
||||
|
||||
// reset since not on the first line anymore
|
||||
/*if !firstLine {
|
||||
e.usedChars = 0
|
||||
}*/
|
||||
}
|
||||
|
||||
output.WriteString(lineBuffer)
|
||||
|
||||
} else {
|
||||
firstLine := true
|
||||
|
||||
// A single encoded word can not be longer than 75 characters
|
||||
if e.usedChars == 0 {
|
||||
maxLineLength = 75
|
||||
}
|
||||
|
||||
wordBegin := "=?" + e.charset + "?Q?"
|
||||
wordEnd := "?="
|
||||
|
||||
lineBuffer := wordBegin
|
||||
|
||||
for i := 0; i < len(p); {
|
||||
// encode the character
|
||||
encodedChar, runeLength := encode(p, i)
|
||||
|
||||
/*fmt.Println("Current Line:",lineBuffer)
|
||||
fmt.Println("Here: Max:", maxLineLength ,"Buffer Length:", len(lineBuffer), "Used Chars:", e.usedChars, "Length Encoded Char:",len(encodedChar))
|
||||
fmt.Println("----------")*/
|
||||
|
||||
// Check line length
|
||||
if len(lineBuffer)+e.usedChars+len(encodedChar) > (maxLineLength - len(wordEnd)) {
|
||||
output.WriteString(lineBuffer + wordEnd + "\r\n")
|
||||
lineBuffer = " " + wordBegin
|
||||
firstLine = false
|
||||
}
|
||||
|
||||
lineBuffer += encodedChar
|
||||
|
||||
i = i + runeLength
|
||||
|
||||
// reset since not on the first line anymore
|
||||
if !firstLine {
|
||||
e.usedChars = 0
|
||||
maxLineLength = 76
|
||||
}
|
||||
}
|
||||
|
||||
output.WriteString(lineBuffer + wordEnd)
|
||||
}
|
||||
|
||||
e.w.Write(output.Bytes())
|
||||
e.w.Flush()
|
||||
n = output.Len()
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// encode takes a string and position in that string and encodes one utf-8
|
||||
// character. It then returns the encoded string and number of runes in the
|
||||
// character.
|
||||
func encode(text []byte, i int) (encodedString string, runeLength int) {
|
||||
started := false
|
||||
|
||||
for ; i < len(text) && (!utf8.RuneStart(text[i]) || !started); i++ {
|
||||
switch c := text[i]; {
|
||||
case c == ' ':
|
||||
encodedString += "_"
|
||||
case isVchar(c) && c != '=' && c != '?' && c != '_':
|
||||
encodedString += string(c)
|
||||
default:
|
||||
encodedString += fmt.Sprintf("=%02X", c)
|
||||
}
|
||||
|
||||
runeLength++
|
||||
|
||||
started = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// secureHeader removes all unnecessary values to prevent
|
||||
// header injection
|
||||
func secureHeader(text []byte) []byte {
|
||||
secureValue := strings.TrimSpace(string(text))
|
||||
secureValue = strings.Replace(secureValue, "\r", "", -1)
|
||||
secureValue = strings.Replace(secureValue, "\n", "", -1)
|
||||
secureValue = strings.Replace(secureValue, "\t", "", -1)
|
||||
|
||||
return []byte(secureValue)
|
||||
}
|
||||
|
||||
// isVchar returns true if c is an RFC 5322 VCHAR character.
|
||||
func isVchar(c byte) bool {
|
||||
// Visible (printing) characters.
|
||||
return '!' <= c && c <= '~'
|
||||
}
|
||||
|
||||
// isWSP returns true if c is a WSP (white space).
|
||||
// WSP is a space or horizontal tab (RFC5234 Appendix B).
|
||||
func isWSP(c byte) bool {
|
||||
return c == ' ' || c == '\t'
|
||||
}
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"mime/quotedprintable"
|
||||
"net/textproto"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type message struct {
|
||||
headers textproto.MIMEHeader
|
||||
body *bytes.Buffer
|
||||
writers []*multipart.Writer
|
||||
parts uint8
|
||||
cids map[string]string
|
||||
charset string
|
||||
encoding encoding
|
||||
}
|
||||
|
||||
func newMessage(email *Email) *message {
|
||||
return &message{
|
||||
headers: email.headers,
|
||||
body: new(bytes.Buffer),
|
||||
cids: make(map[string]string),
|
||||
charset: email.Charset,
|
||||
encoding: email.Encoding}
|
||||
}
|
||||
|
||||
func encodeHeader(text string, charset string, usedChars int) string {
|
||||
// create buffer
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
// encode
|
||||
encoder := newEncoder(buf, charset, usedChars)
|
||||
encoder.encode([]byte(text))
|
||||
|
||||
return buf.String()
|
||||
|
||||
/*
|
||||
switch encoding {
|
||||
case EncodingBase64:
|
||||
return mime.BEncoding.Encode(charset, text)
|
||||
default:
|
||||
return mime.QEncoding.Encode(charset, text)
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
// getHeaders returns the message headers
|
||||
func (msg *message) getHeaders() (headers string) {
|
||||
// if the date header isn't set, set it
|
||||
if date := msg.headers.Get("Date"); date == "" {
|
||||
msg.headers.Set("Date", time.Now().Format(time.RFC1123Z))
|
||||
}
|
||||
|
||||
// encode and combine the headers
|
||||
for header, values := range msg.headers {
|
||||
headers += header + ": " + encodeHeader(strings.Join(values, ", "), msg.charset, len(header)+2) + "\r\n"
|
||||
}
|
||||
|
||||
headers = headers + "\r\n"
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// getCID gets the generated CID for the provided text
|
||||
func (msg *message) getCID(text string) (cid string) {
|
||||
// set the date format to use
|
||||
const dateFormat = "20060102.150405"
|
||||
|
||||
// get the cid if we have one
|
||||
cid, exists := msg.cids[text]
|
||||
if !exists {
|
||||
// generate a new cid
|
||||
cid = time.Now().Format(dateFormat) + "." + strconv.Itoa(len(msg.cids)+1) + "@mail.0"
|
||||
// save it
|
||||
msg.cids[text] = cid
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// replaceCIDs replaces the CIDs found in a text string
|
||||
// with generated ones
|
||||
func (msg *message) replaceCIDs(text string) string {
|
||||
// regular expression to find cids
|
||||
re := regexp.MustCompile(`(src|href)="cid:(.*?)"`)
|
||||
// replace all of the found cids with generated ones
|
||||
for _, matches := range re.FindAllStringSubmatch(text, -1) {
|
||||
cid := msg.getCID(matches[2])
|
||||
text = strings.Replace(text, "cid:"+matches[2], "cid:"+cid, -1)
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
// openMultipart creates a new part of a multipart message
|
||||
func (msg *message) openMultipart(multipartType string) {
|
||||
// create a new multipart writer
|
||||
msg.writers = append(msg.writers, multipart.NewWriter(msg.body))
|
||||
// create the boundary
|
||||
contentType := "multipart/" + multipartType + ";\n \tboundary=" + msg.writers[msg.parts].Boundary()
|
||||
|
||||
// if no existing parts, add header to main header group
|
||||
if msg.parts == 0 {
|
||||
msg.headers.Set("Content-Type", contentType)
|
||||
} else { // add header to multipart section
|
||||
header := make(textproto.MIMEHeader)
|
||||
header.Set("Content-Type", contentType)
|
||||
msg.writers[msg.parts-1].CreatePart(header)
|
||||
}
|
||||
|
||||
msg.parts++
|
||||
}
|
||||
|
||||
// closeMultipart closes a part of a multipart message
|
||||
func (msg *message) closeMultipart() {
|
||||
if msg.parts > 0 {
|
||||
msg.writers[msg.parts-1].Close()
|
||||
msg.parts--
|
||||
}
|
||||
}
|
||||
|
||||
// base64Encode base64 encodes the provided text with line wrapping
|
||||
func base64Encode(text []byte) []byte {
|
||||
// create buffer
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
// create base64 encoder that linewraps
|
||||
encoder := base64.NewEncoder(base64.StdEncoding, &base64LineWrap{writer: buf})
|
||||
|
||||
// write the encoded text to buf
|
||||
encoder.Write(text)
|
||||
encoder.Close()
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// qpEncode uses the quoted-printable encoding to encode the provided text
|
||||
func qpEncode(text []byte) []byte {
|
||||
// create buffer
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
encoder := quotedprintable.NewWriter(buf)
|
||||
|
||||
encoder.Write(text)
|
||||
encoder.Close()
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
const maxLineChars = 76
|
||||
|
||||
type base64LineWrap struct {
|
||||
writer io.Writer
|
||||
numLineChars int
|
||||
}
|
||||
|
||||
func (e *base64LineWrap) Write(p []byte) (n int, err error) {
|
||||
n = 0
|
||||
// while we have more chars than are allowed
|
||||
for len(p)+e.numLineChars > maxLineChars {
|
||||
numCharsToWrite := maxLineChars - e.numLineChars
|
||||
// write the chars we can
|
||||
e.writer.Write(p[:numCharsToWrite])
|
||||
// write a line break
|
||||
e.writer.Write([]byte("\r\n"))
|
||||
// reset the line count
|
||||
e.numLineChars = 0
|
||||
// remove the chars that have been written
|
||||
p = p[numCharsToWrite:]
|
||||
// set the num of chars written
|
||||
n += numCharsToWrite
|
||||
}
|
||||
|
||||
// write what is left
|
||||
e.writer.Write(p)
|
||||
e.numLineChars += len(p)
|
||||
n += len(p)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (msg *message) write(header textproto.MIMEHeader, body []byte, encoding encoding) {
|
||||
msg.writeHeader(header)
|
||||
msg.writeBody(body, encoding)
|
||||
}
|
||||
|
||||
func (msg *message) writeHeader(headers textproto.MIMEHeader) {
|
||||
// if there are no parts add header to main headers
|
||||
if msg.parts == 0 {
|
||||
for header, value := range headers {
|
||||
msg.headers[header] = value
|
||||
}
|
||||
} else { // add header to multipart section
|
||||
msg.writers[msg.parts-1].CreatePart(headers)
|
||||
}
|
||||
}
|
||||
|
||||
func (msg *message) writeBody(body []byte, encoding encoding) {
|
||||
// encode and write the body
|
||||
switch encoding {
|
||||
case EncodingQuotedPrintable:
|
||||
msg.body.Write(qpEncode(body))
|
||||
case EncodingBase64:
|
||||
msg.body.Write(base64Encode(body))
|
||||
default:
|
||||
msg.body.Write(body)
|
||||
}
|
||||
}
|
||||
|
||||
func (msg *message) addBody(contentType string, body []byte) {
|
||||
body = []byte(msg.replaceCIDs(string(body)))
|
||||
|
||||
header := make(textproto.MIMEHeader)
|
||||
header.Set("Content-Type", contentType+"; charset="+msg.charset)
|
||||
header.Set("Content-Transfer-Encoding", msg.encoding.string())
|
||||
msg.write(header, body, msg.encoding)
|
||||
}
|
||||
|
||||
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
|
||||
|
||||
func escapeQuotes(s string) string {
|
||||
return quoteEscaper.Replace(s)
|
||||
}
|
||||
|
||||
func (msg *message) addFiles(files []*File, inline bool) {
|
||||
encoding := EncodingBase64
|
||||
for _, file := range files {
|
||||
header := make(textproto.MIMEHeader)
|
||||
header.Set("Content-Type", file.MimeType+";\n \tname=\""+encodeHeader(escapeQuotes(file.Name), msg.charset, 6)+`"`)
|
||||
header.Set("Content-Transfer-Encoding", encoding.string())
|
||||
if inline {
|
||||
header.Set("Content-Disposition", "inline;\n \tfilename=\""+encodeHeader(escapeQuotes(file.Name), msg.charset, 10)+`"`)
|
||||
header.Set("Content-ID", "<"+msg.getCID(file.Name)+">")
|
||||
} else {
|
||||
header.Set("Content-Disposition", "attachment;\n \tfilename=\""+encodeHeader(escapeQuotes(file.Name), msg.charset, 10)+`"`)
|
||||
}
|
||||
|
||||
msg.write(header, file.Data, encoding)
|
||||
}
|
||||
}
|
||||
+332
@@ -0,0 +1,332 @@
|
||||
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in https://raw.githubusercontent.com/golang/go/master/LICENSE
|
||||
|
||||
// Package mail implements the Simple Mail Transfer Protocol as defined in RFC 5321.
|
||||
// It also implements the following extensions:
|
||||
// 8BITMIME RFC 1652
|
||||
// SMTPUTF8 RFC 6531
|
||||
// AUTH RFC 2554
|
||||
// STARTTLS RFC 3207
|
||||
// SIZE RFC 1870
|
||||
// Additional extensions may be handled by clients using smtp.go in golang source code or pull request Go Simple Mail
|
||||
|
||||
// smtp.go file is a modification of smtp golang package what is frozen and is not accepting new features.
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/textproto"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A Client represents a client connection to an SMTP server.
|
||||
type smtpClient struct {
|
||||
// Text is the textproto.Conn used by the Client.
|
||||
text *textproto.Conn
|
||||
// keep a reference to the connection so it can be used to create a TLS
|
||||
// connection later
|
||||
conn net.Conn
|
||||
// whether the Client is using TLS
|
||||
tls bool
|
||||
serverName string
|
||||
// map of supported extensions
|
||||
ext map[string]string
|
||||
// supported auth mechanisms
|
||||
a []string
|
||||
localName string // the name to use in HELO/EHLO
|
||||
didHello bool // whether we've said HELO/EHLO
|
||||
helloError error // the error from the hello
|
||||
}
|
||||
|
||||
// newClient returns a new smtpClient using an existing connection and host as a
|
||||
// server name to be used when authenticating.
|
||||
func newClient(conn net.Conn, host string) (*smtpClient, error) {
|
||||
text := textproto.NewConn(conn)
|
||||
_, _, err := text.ReadResponse(220)
|
||||
if err != nil {
|
||||
text.Close()
|
||||
return nil, err
|
||||
}
|
||||
c := &smtpClient{text: text, conn: conn, serverName: host, localName: "localhost"}
|
||||
_, c.tls = conn.(*tls.Conn)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Close closes the connection.
|
||||
func (c *smtpClient) close() error {
|
||||
return c.text.Close()
|
||||
}
|
||||
|
||||
// hello runs a hello exchange if needed.
|
||||
func (c *smtpClient) hello() error {
|
||||
if !c.didHello {
|
||||
c.didHello = true
|
||||
err := c.ehlo()
|
||||
if err != nil {
|
||||
c.helloError = c.helo()
|
||||
}
|
||||
}
|
||||
return c.helloError
|
||||
}
|
||||
|
||||
// hi sends a HELO or EHLO to the server as the given host name.
|
||||
// Calling this method is only necessary if the client needs control
|
||||
// over the host name used. The client will introduce itself as "localhost"
|
||||
// automatically otherwise. If Hello is called, it must be called before
|
||||
// any of the other methods.
|
||||
func (c *smtpClient) hi(localName string) error {
|
||||
if err := validateLine(localName); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.didHello {
|
||||
return errors.New("smtp: Hello called after other methods")
|
||||
}
|
||||
c.localName = localName
|
||||
return c.hello()
|
||||
}
|
||||
|
||||
// cmd is a convenience function that sends a command and returns the response
|
||||
func (c *smtpClient) cmd(expectCode int, format string, args ...interface{}) (int, string, error) {
|
||||
id, err := c.text.Cmd(format, args...)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
c.text.StartResponse(id)
|
||||
defer c.text.EndResponse(id)
|
||||
code, msg, err := c.text.ReadResponse(expectCode)
|
||||
return code, msg, err
|
||||
}
|
||||
|
||||
// helo sends the HELO greeting to the server. It should be used only when the
|
||||
// server does not support ehlo.
|
||||
func (c *smtpClient) helo() error {
|
||||
c.ext = nil
|
||||
_, _, err := c.cmd(250, "HELO %s", c.localName)
|
||||
return err
|
||||
}
|
||||
|
||||
// ehlo sends the EHLO (extended hello) greeting to the server. It
|
||||
// should be the preferred greeting for servers that support it.
|
||||
func (c *smtpClient) ehlo() error {
|
||||
_, msg, err := c.cmd(250, "EHLO %s", c.localName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ext := make(map[string]string)
|
||||
extList := strings.Split(msg, "\n")
|
||||
if len(extList) > 1 {
|
||||
extList = extList[1:]
|
||||
for _, line := range extList {
|
||||
args := strings.SplitN(line, " ", 2)
|
||||
if len(args) > 1 {
|
||||
ext[args[0]] = args[1]
|
||||
} else {
|
||||
ext[args[0]] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
if mechs, ok := ext["AUTH"]; ok {
|
||||
c.a = strings.Split(mechs, " ")
|
||||
}
|
||||
c.ext = ext
|
||||
return err
|
||||
}
|
||||
|
||||
// startTLS sends the STARTTLS command and encrypts all further communication.
|
||||
// Only servers that advertise the STARTTLS extension support this function.
|
||||
func (c *smtpClient) startTLS(config *tls.Config) error {
|
||||
if err := c.hello(); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _, err := c.cmd(220, "STARTTLS")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.conn = tls.Client(c.conn, config)
|
||||
c.text = textproto.NewConn(c.conn)
|
||||
c.tls = true
|
||||
return c.ehlo()
|
||||
}
|
||||
|
||||
// authenticate authenticates a client using the provided authentication mechanism.
|
||||
// A failed authentication closes the connection.
|
||||
// Only servers that advertise the AUTH extension support this function.
|
||||
func (c *smtpClient) authenticate(a auth) error {
|
||||
if err := c.hello(); err != nil {
|
||||
return err
|
||||
}
|
||||
encoding := base64.StdEncoding
|
||||
mech, resp, err := a.start(&serverInfo{c.serverName, c.tls, c.a})
|
||||
if err != nil {
|
||||
c.quit()
|
||||
return err
|
||||
}
|
||||
resp64 := make([]byte, encoding.EncodedLen(len(resp)))
|
||||
encoding.Encode(resp64, resp)
|
||||
code, msg64, err := c.cmd(0, strings.TrimSpace(fmt.Sprintf("AUTH %s %s", mech, resp64)))
|
||||
for err == nil {
|
||||
var msg []byte
|
||||
switch code {
|
||||
case 334:
|
||||
msg, err = encoding.DecodeString(msg64)
|
||||
case 235:
|
||||
// the last message isn't base64 because it isn't a challenge
|
||||
msg = []byte(msg64)
|
||||
default:
|
||||
err = &textproto.Error{Code: code, Msg: msg64}
|
||||
}
|
||||
if err == nil {
|
||||
resp, err = a.next(msg, code == 334)
|
||||
}
|
||||
if err != nil {
|
||||
// abort the AUTH
|
||||
c.cmd(501, "*")
|
||||
c.quit()
|
||||
break
|
||||
}
|
||||
if resp == nil {
|
||||
break
|
||||
}
|
||||
resp64 = make([]byte, encoding.EncodedLen(len(resp)))
|
||||
encoding.Encode(resp64, resp)
|
||||
code, msg64, err = c.cmd(0, string(resp64))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// mail issues a MAIL command to the server using the provided email address.
|
||||
// If the server supports the 8BITMIME extension, Mail adds the BODY=8BITMIME
|
||||
// parameter.
|
||||
// If the server supports the SMTPUTF8 extension, Mail adds the
|
||||
// SMTPUTF8 parameter.
|
||||
// This initiates a mail transaction and is followed by one or more Rcpt calls.
|
||||
func (c *smtpClient) mail(from string, extArgs ...map[string]string) error {
|
||||
var args []interface{}
|
||||
var extMap map[string]string
|
||||
|
||||
if len(extArgs) > 0 {
|
||||
extMap = extArgs[0]
|
||||
}
|
||||
|
||||
if err := validateLine(from); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.hello(); err != nil {
|
||||
return err
|
||||
}
|
||||
cmdStr := "MAIL FROM:<%s>"
|
||||
if c.ext != nil {
|
||||
if _, ok := c.ext["8BITMIME"]; ok {
|
||||
cmdStr += " BODY=8BITMIME"
|
||||
}
|
||||
if _, ok := c.ext["SMTPUTF8"]; ok {
|
||||
cmdStr += " SMTPUTF8"
|
||||
}
|
||||
if _, ok := c.ext["SIZE"]; ok {
|
||||
if extMap["SIZE"] != "" {
|
||||
cmdStr += " SIZE=%s"
|
||||
args = append(args, extMap["SIZE"])
|
||||
}
|
||||
}
|
||||
}
|
||||
args = append([]interface{}{from}, args...)
|
||||
_, _, err := c.cmd(250, cmdStr, args...)
|
||||
return err
|
||||
}
|
||||
|
||||
// rcpt issues a RCPT command to the server using the provided email address.
|
||||
// A call to Rcpt must be preceded by a call to Mail and may be followed by
|
||||
// a Data call or another Rcpt call.
|
||||
func (c *smtpClient) rcpt(to, dsn string) error {
|
||||
if err := validateLine(to); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _, err := c.cmd(25, "RCPT TO:<%s>%s", to, dsn)
|
||||
return err
|
||||
}
|
||||
|
||||
type dataCloser struct {
|
||||
c *smtpClient
|
||||
io.WriteCloser
|
||||
}
|
||||
|
||||
func (d *dataCloser) Close() error {
|
||||
d.WriteCloser.Close()
|
||||
_, _, err := d.c.text.ReadResponse(250)
|
||||
return err
|
||||
}
|
||||
|
||||
// data issues a DATA command to the server and returns a writer that
|
||||
// can be used to write the mail headers and body. The caller should
|
||||
// close the writer before calling any more methods on c. A call to
|
||||
// Data must be preceded by one or more calls to Rcpt.
|
||||
func (c *smtpClient) data() (io.WriteCloser, error) {
|
||||
_, _, err := c.cmd(354, "DATA")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dataCloser{c, c.text.DotWriter()}, nil
|
||||
}
|
||||
|
||||
// extension reports whether an extension is support by the server.
|
||||
// The extension name is case-insensitive. If the extension is supported,
|
||||
// extension also returns a string that contains any parameters the
|
||||
// server specifies for the extension.
|
||||
func (c *smtpClient) extension(ext string) (bool, string) {
|
||||
if err := c.hello(); err != nil {
|
||||
return false, ""
|
||||
}
|
||||
if c.ext == nil {
|
||||
return false, ""
|
||||
}
|
||||
ext = strings.ToUpper(ext)
|
||||
param, ok := c.ext[ext]
|
||||
return ok, param
|
||||
}
|
||||
|
||||
// reset sends the RSET command to the server, aborting the current mail
|
||||
// transaction.
|
||||
func (c *smtpClient) reset() error {
|
||||
if err := c.hello(); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _, err := c.cmd(250, "RSET")
|
||||
return err
|
||||
}
|
||||
|
||||
// noop sends the NOOP command to the server. It does nothing but check
|
||||
// that the connection to the server is okay.
|
||||
func (c *smtpClient) noop() error {
|
||||
if err := c.hello(); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _, err := c.cmd(250, "NOOP")
|
||||
return err
|
||||
}
|
||||
|
||||
// quit sends the QUIT command and closes the connection to the server.
|
||||
func (c *smtpClient) quit() error {
|
||||
if err := c.hello(); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _, err := c.cmd(221, "QUIT")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.text.Close()
|
||||
}
|
||||
|
||||
// validateLine checks to see if a line has CR or LF as per RFC 5321
|
||||
func validateLine(line string) error {
|
||||
if strings.ContainsAny(line, "\n\r") {
|
||||
return errors.New("smtp: A line must not contain CR or LF")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user