mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-05-07 20:15:31 -05:00
switch to go vendoring
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
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
# 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
|
||||
+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.
|
||||
+270
@@ -0,0 +1,270 @@
|
||||
# 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)
|
||||
- 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)
|
||||
|
||||
## Documentation
|
||||
|
||||
https://pkg.go.dev/github.com/xhit/go-simple-mail/v2?tab=doc
|
||||
|
||||
Note: by default duplicated recipients throws an error, from `v2.13.0` you can use `email.AllowDuplicateAddress = true` to avoid the check.
|
||||
|
||||
## Download
|
||||
|
||||
This package uses go modules.
|
||||
|
||||
```console
|
||||
$ go get github.com/xhit/go-simple-mail/v2
|
||||
```
|
||||
|
||||
# Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"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
|
||||
|
||||
// Since v2.3.0 you can specified authentication type:
|
||||
// - PLAIN (default)
|
||||
// - LOGIN
|
||||
// - CRAM-MD5
|
||||
// - None
|
||||
// server.Authentication = mail.AuthPlain
|
||||
|
||||
// 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})
|
||||
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 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
|
||||
}
|
||||
+946
@@ -0,0 +1,946 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/mail"
|
||||
"net/textproto"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/toorop/go-dkim"
|
||||
)
|
||||
|
||||
// Email represents an email message.
|
||||
type Email struct {
|
||||
from string
|
||||
sender string
|
||||
replyTo string
|
||||
returnPath string
|
||||
recipients []string
|
||||
headers textproto.MIMEHeader
|
||||
parts []part
|
||||
attachments []*File
|
||||
inlines []*File
|
||||
Charset string
|
||||
Encoding encoding
|
||||
Error error
|
||||
SMTPServer *smtpClient
|
||||
DkimMsg string
|
||||
AllowDuplicateAddress bool
|
||||
}
|
||||
|
||||
/*
|
||||
SMTPServer represents a SMTP Server
|
||||
If authentication is CRAM-MD5 then the Password is the Secret
|
||||
*/
|
||||
type SMTPServer struct {
|
||||
Authentication AuthType
|
||||
Encryption Encryption
|
||||
Username string
|
||||
Password string
|
||||
Helo string
|
||||
ConnectTimeout time.Duration
|
||||
SendTimeout time.Duration
|
||||
Host string
|
||||
Port int
|
||||
KeepAlive bool
|
||||
TLSConfig *tls.Config
|
||||
}
|
||||
|
||||
// SMTPClient represents a SMTP Client for send email
|
||||
type SMTPClient struct {
|
||||
Client *smtpClient
|
||||
KeepAlive bool
|
||||
SendTimeout time.Duration
|
||||
}
|
||||
|
||||
// part represents the different content parts of an email body.
|
||||
type part struct {
|
||||
contentType string
|
||||
body *bytes.Buffer
|
||||
}
|
||||
|
||||
// Encryption type to enum encryption types (None, SSL/TLS, STARTTLS)
|
||||
type Encryption int
|
||||
|
||||
// TODO: Remove EncryptionSSL and EncryptionTLS before launch v3
|
||||
|
||||
const (
|
||||
// EncryptionNone uses no encryption when sending email
|
||||
EncryptionNone Encryption = iota
|
||||
// EncryptionSSL: DEPRECATED. Use EncryptionSSLTLS. Sets encryption type to SSL/TLS when sending email
|
||||
EncryptionSSL
|
||||
// EncryptionTLS: DEPRECATED. Use EncryptionSTARTTLS. sets encryption type to STARTTLS when sending email
|
||||
EncryptionTLS
|
||||
// EncryptionSSLTLS sets encryption type to SSL/TLS when sending email
|
||||
EncryptionSSLTLS
|
||||
// EncryptionSTARTTLS sets encryption type to STARTTLS when sending email
|
||||
EncryptionSTARTTLS
|
||||
)
|
||||
|
||||
// TODO: Remove last two indexes
|
||||
var encryptionTypes = [...]string{"None", "SSL/TLS", "STARTTLS", "SSL/TLS", "STARTTLS"}
|
||||
|
||||
func (encryption Encryption) String() string {
|
||||
return encryptionTypes[encryption]
|
||||
}
|
||||
|
||||
type encoding int
|
||||
|
||||
const (
|
||||
// EncodingNone turns off encoding on the message body
|
||||
EncodingNone encoding = iota
|
||||
// EncodingBase64 sets the message body encoding to base64
|
||||
EncodingBase64
|
||||
// EncodingQuotedPrintable sets the message body encoding to quoted-printable
|
||||
EncodingQuotedPrintable
|
||||
)
|
||||
|
||||
var encodingTypes = [...]string{"binary", "base64", "quoted-printable"}
|
||||
|
||||
func (encoding encoding) string() string {
|
||||
return encodingTypes[encoding]
|
||||
}
|
||||
|
||||
type ContentType int
|
||||
|
||||
const (
|
||||
// TextPlain sets body type to text/plain in message body
|
||||
TextPlain ContentType = iota
|
||||
// TextHTML sets body type to text/html in message body
|
||||
TextHTML
|
||||
// TextCalendar sets body type to text/calendar in message body
|
||||
TextCalendar
|
||||
)
|
||||
|
||||
var contentTypes = [...]string{"text/plain", "text/html", "text/calendar"}
|
||||
|
||||
func (contentType ContentType) string() string {
|
||||
return contentTypes[contentType]
|
||||
}
|
||||
|
||||
type AuthType int
|
||||
|
||||
const (
|
||||
// AuthPlain implements the PLAIN authentication
|
||||
AuthPlain AuthType = iota
|
||||
// AuthLogin implements the LOGIN authentication
|
||||
AuthLogin
|
||||
// AuthCRAMMD5 implements the CRAM-MD5 authentication
|
||||
AuthCRAMMD5
|
||||
// AuthNone for SMTP servers without authentication
|
||||
AuthNone
|
||||
)
|
||||
|
||||
// NewMSG creates a new email. It uses UTF-8 by default. All charsets: http://webcheatsheet.com/HTML/character_sets_list.php
|
||||
func NewMSG() *Email {
|
||||
email := &Email{
|
||||
headers: make(textproto.MIMEHeader),
|
||||
Charset: "UTF-8",
|
||||
Encoding: EncodingQuotedPrintable,
|
||||
}
|
||||
|
||||
email.AddHeader("MIME-Version", "1.0")
|
||||
|
||||
return email
|
||||
}
|
||||
|
||||
// NewSMTPClient returns the client for send email
|
||||
func NewSMTPClient() *SMTPServer {
|
||||
server := &SMTPServer{
|
||||
Authentication: AuthPlain,
|
||||
Encryption: EncryptionNone,
|
||||
ConnectTimeout: 10 * time.Second,
|
||||
SendTimeout: 10 * time.Second,
|
||||
Helo: "localhost",
|
||||
}
|
||||
return server
|
||||
}
|
||||
|
||||
// GetEncryptionType returns the encryption type used to connect to SMTP server
|
||||
func (server *SMTPServer) GetEncryptionType() Encryption {
|
||||
return server.Encryption
|
||||
}
|
||||
|
||||
// GetError returns the first email error encountered
|
||||
func (email *Email) GetError() error {
|
||||
return email.Error
|
||||
}
|
||||
|
||||
// SetFrom sets the From address.
|
||||
func (email *Email) SetFrom(address string) *Email {
|
||||
if email.Error != nil {
|
||||
return email
|
||||
}
|
||||
|
||||
email.AddAddresses("From", address)
|
||||
|
||||
return email
|
||||
}
|
||||
|
||||
// SetSender sets the Sender address.
|
||||
func (email *Email) SetSender(address string) *Email {
|
||||
if email.Error != nil {
|
||||
return email
|
||||
}
|
||||
|
||||
email.AddAddresses("Sender", address)
|
||||
|
||||
return email
|
||||
}
|
||||
|
||||
// SetReplyTo sets the Reply-To address.
|
||||
func (email *Email) SetReplyTo(address string) *Email {
|
||||
if email.Error != nil {
|
||||
return email
|
||||
}
|
||||
|
||||
email.AddAddresses("Reply-To", address)
|
||||
|
||||
return email
|
||||
}
|
||||
|
||||
// SetReturnPath sets the Return-Path address. This is most often used
|
||||
// to send bounced emails to a different email address.
|
||||
func (email *Email) SetReturnPath(address string) *Email {
|
||||
if email.Error != nil {
|
||||
return email
|
||||
}
|
||||
|
||||
email.AddAddresses("Return-Path", address)
|
||||
|
||||
return email
|
||||
}
|
||||
|
||||
// AddTo adds a To address. You can provide multiple
|
||||
// addresses at the same time.
|
||||
func (email *Email) AddTo(addresses ...string) *Email {
|
||||
if email.Error != nil {
|
||||
return email
|
||||
}
|
||||
|
||||
email.AddAddresses("To", addresses...)
|
||||
|
||||
return email
|
||||
}
|
||||
|
||||
// AddCc adds a Cc address. You can provide multiple
|
||||
// addresses at the same time.
|
||||
func (email *Email) AddCc(addresses ...string) *Email {
|
||||
if email.Error != nil {
|
||||
return email
|
||||
}
|
||||
|
||||
email.AddAddresses("Cc", addresses...)
|
||||
|
||||
return email
|
||||
}
|
||||
|
||||
// AddBcc adds a Bcc address. You can provide multiple
|
||||
// addresses at the same time.
|
||||
func (email *Email) AddBcc(addresses ...string) *Email {
|
||||
if email.Error != nil {
|
||||
return email
|
||||
}
|
||||
|
||||
email.AddAddresses("Bcc", addresses...)
|
||||
|
||||
return email
|
||||
}
|
||||
|
||||
// AddAddresses allows you to add addresses to the specified address header.
|
||||
func (email *Email) AddAddresses(header string, addresses ...string) *Email {
|
||||
if email.Error != nil {
|
||||
return email
|
||||
}
|
||||
|
||||
found := false
|
||||
|
||||
// check for a valid address header
|
||||
for _, h := range []string{"To", "Cc", "Bcc", "From", "Sender", "Reply-To", "Return-Path"} {
|
||||
if header == h {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
email.Error = errors.New("Mail Error: Invalid address header; Header: [" + header + "]")
|
||||
return email
|
||||
}
|
||||
|
||||
// check to see if the addresses are valid
|
||||
for i := range addresses {
|
||||
var address = new(mail.Address)
|
||||
var err error
|
||||
|
||||
// ignore parse the address if empty
|
||||
if len(addresses[i]) > 0 {
|
||||
address, err = mail.ParseAddress(addresses[i])
|
||||
if err != nil {
|
||||
email.Error = errors.New("Mail Error: " + err.Error() + "; Header: [" + header + "] Address: [" + addresses[i] + "]")
|
||||
return email
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
// check for more than one address
|
||||
switch {
|
||||
case header == "Sender" && len(email.sender) > 0:
|
||||
fallthrough
|
||||
case header == "Reply-To" && len(email.replyTo) > 0:
|
||||
fallthrough
|
||||
case header == "Return-Path" && len(email.returnPath) > 0:
|
||||
email.Error = errors.New("Mail Error: There can only be one \"" + header + "\" address; Header: [" + header + "] Address: [" + addresses[i] + "]")
|
||||
return email
|
||||
default:
|
||||
// other address types can have more than one address
|
||||
}
|
||||
|
||||
// save the address
|
||||
switch header {
|
||||
case "From":
|
||||
// delete the current "From" to set the new
|
||||
// when "From" need to be changed in the message
|
||||
if len(email.from) > 0 && header == "From" {
|
||||
email.headers.Del("From")
|
||||
}
|
||||
email.from = address.Address
|
||||
case "Sender":
|
||||
email.sender = address.Address
|
||||
case "Reply-To":
|
||||
email.replyTo = address.Address
|
||||
case "Return-Path":
|
||||
email.returnPath = address.Address
|
||||
default:
|
||||
// check that the address was added to the recipients list
|
||||
email.recipients, err = addAddress(email.recipients, address.Address, email.AllowDuplicateAddress)
|
||||
if err != nil {
|
||||
email.Error = errors.New("Mail Error: " + err.Error() + "; Header: [" + header + "] Address: [" + addresses[i] + "]")
|
||||
return email
|
||||
}
|
||||
}
|
||||
|
||||
// make sure the from and sender addresses are different
|
||||
if email.from != "" && email.sender != "" && email.from == email.sender {
|
||||
email.sender = ""
|
||||
email.headers.Del("Sender")
|
||||
email.Error = errors.New("Mail Error: From and Sender should not be set to the same address")
|
||||
return email
|
||||
}
|
||||
|
||||
// add all addresses to the headers except for Bcc and Return-Path
|
||||
if header != "Bcc" && header != "Return-Path" {
|
||||
// add the address to the headers
|
||||
email.headers.Add(header, address.String())
|
||||
}
|
||||
}
|
||||
|
||||
return email
|
||||
}
|
||||
|
||||
// addAddress adds an address to the address list if it hasn't already been added
|
||||
func addAddress(addressList []string, address string, allowDuplicateAddress bool) ([]string, error) {
|
||||
if !allowDuplicateAddress {
|
||||
// loop through the address list to check for dups
|
||||
for _, a := range addressList {
|
||||
if address == a {
|
||||
return addressList, errors.New("Mail Error: Address: [" + address + "] has already been added")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return append(addressList, address), nil
|
||||
}
|
||||
|
||||
type priority int
|
||||
|
||||
const (
|
||||
// PriorityLow sets the email priority to Low
|
||||
PriorityLow priority = iota
|
||||
// PriorityHigh sets the email priority to High
|
||||
PriorityHigh
|
||||
)
|
||||
|
||||
// SetPriority sets the email message priority. Use with
|
||||
// either "High" or "Low".
|
||||
func (email *Email) SetPriority(priority priority) *Email {
|
||||
if email.Error != nil {
|
||||
return email
|
||||
}
|
||||
|
||||
switch priority {
|
||||
case PriorityLow:
|
||||
email.AddHeaders(textproto.MIMEHeader{
|
||||
"X-Priority": {"5 (Lowest)"},
|
||||
"X-MSMail-Priority": {"Low"},
|
||||
"Importance": {"Low"},
|
||||
})
|
||||
case PriorityHigh:
|
||||
email.AddHeaders(textproto.MIMEHeader{
|
||||
"X-Priority": {"1 (Highest)"},
|
||||
"X-MSMail-Priority": {"High"},
|
||||
"Importance": {"High"},
|
||||
})
|
||||
default:
|
||||
}
|
||||
|
||||
return email
|
||||
}
|
||||
|
||||
// SetDate sets the date header to the provided date/time.
|
||||
// The format of the string should be YYYY-MM-DD HH:MM:SS Time Zone.
|
||||
//
|
||||
// Example: SetDate("2015-04-28 10:32:00 CDT")
|
||||
func (email *Email) SetDate(dateTime string) *Email {
|
||||
if email.Error != nil {
|
||||
return email
|
||||
}
|
||||
|
||||
const dateFormat = "2006-01-02 15:04:05 MST"
|
||||
|
||||
// Try to parse the provided date/time
|
||||
dt, err := time.Parse(dateFormat, dateTime)
|
||||
if err != nil {
|
||||
email.Error = errors.New("Mail Error: Setting date failed with: " + err.Error())
|
||||
return email
|
||||
}
|
||||
|
||||
email.headers.Set("Date", dt.Format(time.RFC1123Z))
|
||||
|
||||
return email
|
||||
}
|
||||
|
||||
// SetSubject sets the subject of the email message.
|
||||
func (email *Email) SetSubject(subject string) *Email {
|
||||
if email.Error != nil {
|
||||
return email
|
||||
}
|
||||
|
||||
email.AddHeader("Subject", subject)
|
||||
|
||||
return email
|
||||
}
|
||||
|
||||
// SetListUnsubscribe sets the Unsubscribe address.
|
||||
func (email *Email) SetListUnsubscribe(address string) *Email {
|
||||
if email.Error != nil {
|
||||
return email
|
||||
}
|
||||
|
||||
email.AddHeader("List-Unsubscribe", address)
|
||||
|
||||
return email
|
||||
}
|
||||
|
||||
// SetDkim adds DomainKey signature to the email message (header+body)
|
||||
func (email *Email) SetDkim(options dkim.SigOptions) *Email {
|
||||
if email.Error != nil {
|
||||
return email
|
||||
}
|
||||
|
||||
msg := []byte(email.GetMessage())
|
||||
err := dkim.Sign(&msg, options)
|
||||
|
||||
if err != nil {
|
||||
email.Error = errors.New("Mail Error: cannot dkim sign message due: %s" + err.Error())
|
||||
return email
|
||||
}
|
||||
|
||||
email.DkimMsg = string(msg)
|
||||
|
||||
return email
|
||||
}
|
||||
|
||||
// SetBody sets the body of the email message.
|
||||
func (email *Email) SetBody(contentType ContentType, body string) *Email {
|
||||
if email.Error != nil {
|
||||
return email
|
||||
}
|
||||
|
||||
email.parts = []part{
|
||||
{
|
||||
contentType: contentType.string(),
|
||||
body: bytes.NewBufferString(body),
|
||||
},
|
||||
}
|
||||
|
||||
return email
|
||||
}
|
||||
|
||||
// SetBodyData sets the body of the email message from []byte
|
||||
func (email *Email) SetBodyData(contentType ContentType, body []byte) *Email {
|
||||
if email.Error != nil {
|
||||
return email
|
||||
}
|
||||
|
||||
email.parts = []part{
|
||||
{
|
||||
contentType: contentType.string(),
|
||||
body: bytes.NewBuffer(body),
|
||||
},
|
||||
}
|
||||
|
||||
return email
|
||||
}
|
||||
|
||||
// AddHeader adds the given "header" with the passed "value".
|
||||
func (email *Email) AddHeader(header string, values ...string) *Email {
|
||||
if email.Error != nil {
|
||||
return email
|
||||
}
|
||||
|
||||
// check that there is actually a value
|
||||
if len(values) < 1 {
|
||||
email.Error = errors.New("Mail Error: no value provided; Header: [" + header + "]")
|
||||
return email
|
||||
}
|
||||
|
||||
if header != "MIME-Version" {
|
||||
// Set header to correct canonical Mime
|
||||
header = textproto.CanonicalMIMEHeaderKey(header)
|
||||
}
|
||||
|
||||
switch header {
|
||||
case "Sender":
|
||||
fallthrough
|
||||
case "From":
|
||||
fallthrough
|
||||
case "To":
|
||||
fallthrough
|
||||
case "Bcc":
|
||||
fallthrough
|
||||
case "Cc":
|
||||
fallthrough
|
||||
case "Reply-To":
|
||||
fallthrough
|
||||
case "Return-Path":
|
||||
email.AddAddresses(header, values...)
|
||||
case "Date":
|
||||
if len(values) > 1 {
|
||||
email.Error = errors.New("Mail Error: To many dates provided")
|
||||
return email
|
||||
}
|
||||
email.SetDate(values[0])
|
||||
case "List-Unsubscribe":
|
||||
fallthrough
|
||||
default:
|
||||
email.headers[header] = values
|
||||
}
|
||||
|
||||
return email
|
||||
}
|
||||
|
||||
// AddHeaders is used to add multiple headers at once
|
||||
func (email *Email) AddHeaders(headers textproto.MIMEHeader) *Email {
|
||||
if email.Error != nil {
|
||||
return email
|
||||
}
|
||||
|
||||
for header, values := range headers {
|
||||
email.AddHeader(header, values...)
|
||||
}
|
||||
|
||||
return email
|
||||
}
|
||||
|
||||
// AddAlternative allows you to add alternative parts to the body
|
||||
// of the email message. This is most commonly used to add an
|
||||
// html version in addition to a plain text version that was
|
||||
// already added with SetBody.
|
||||
func (email *Email) AddAlternative(contentType ContentType, body string) *Email {
|
||||
if email.Error != nil {
|
||||
return email
|
||||
}
|
||||
|
||||
email.parts = append(email.parts,
|
||||
part{
|
||||
contentType: contentType.string(),
|
||||
body: bytes.NewBufferString(body),
|
||||
},
|
||||
)
|
||||
|
||||
return email
|
||||
}
|
||||
|
||||
// AddAlternativeData allows you to add alternative parts to the body
|
||||
// of the email message. This is most commonly used to add an
|
||||
// html version in addition to a plain text version that was
|
||||
// already added with SetBody.
|
||||
func (email *Email) AddAlternativeData(contentType ContentType, body []byte) *Email {
|
||||
if email.Error != nil {
|
||||
return email
|
||||
}
|
||||
|
||||
email.parts = append(email.parts,
|
||||
part{
|
||||
contentType: contentType.string(),
|
||||
body: bytes.NewBuffer(body),
|
||||
},
|
||||
)
|
||||
|
||||
return email
|
||||
}
|
||||
|
||||
// GetFrom returns the sender of the email, if any
|
||||
func (email *Email) GetFrom() string {
|
||||
from := email.returnPath
|
||||
if from == "" {
|
||||
from = email.sender
|
||||
if from == "" {
|
||||
from = email.from
|
||||
if from == "" {
|
||||
from = email.replyTo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return from
|
||||
}
|
||||
|
||||
// GetRecipients returns a slice of recipients emails
|
||||
func (email *Email) GetRecipients() []string {
|
||||
return email.recipients
|
||||
}
|
||||
|
||||
func (email *Email) hasMixedPart() bool {
|
||||
return (len(email.parts) > 0 && len(email.attachments) > 0) || len(email.attachments) > 1
|
||||
}
|
||||
|
||||
func (email *Email) hasRelatedPart() bool {
|
||||
return (len(email.parts) > 0 && len(email.inlines) > 0) || len(email.inlines) > 1
|
||||
}
|
||||
|
||||
func (email *Email) hasAlternativePart() bool {
|
||||
return len(email.parts) > 1
|
||||
}
|
||||
|
||||
// GetMessage builds and returns the email message (RFC822 formatted message)
|
||||
func (email *Email) GetMessage() string {
|
||||
msg := newMessage(email)
|
||||
|
||||
if email.hasMixedPart() {
|
||||
msg.openMultipart("mixed")
|
||||
}
|
||||
|
||||
if email.hasRelatedPart() {
|
||||
msg.openMultipart("related")
|
||||
}
|
||||
|
||||
if email.hasAlternativePart() {
|
||||
msg.openMultipart("alternative")
|
||||
}
|
||||
|
||||
for _, part := range email.parts {
|
||||
msg.addBody(part.contentType, part.body.Bytes())
|
||||
}
|
||||
|
||||
if email.hasAlternativePart() {
|
||||
msg.closeMultipart()
|
||||
}
|
||||
|
||||
msg.addFiles(email.inlines, true)
|
||||
if email.hasRelatedPart() {
|
||||
msg.closeMultipart()
|
||||
}
|
||||
|
||||
msg.addFiles(email.attachments, false)
|
||||
if email.hasMixedPart() {
|
||||
msg.closeMultipart()
|
||||
}
|
||||
|
||||
return msg.getHeaders() + msg.body.String()
|
||||
}
|
||||
|
||||
// Send sends the composed email
|
||||
func (email *Email) Send(client *SMTPClient) error {
|
||||
return email.SendEnvelopeFrom(email.from, client)
|
||||
}
|
||||
|
||||
// SendEnvelopeFrom sends the composed email with envelope
|
||||
// sender. 'from' must be an email address.
|
||||
func (email *Email) SendEnvelopeFrom(from string, client *SMTPClient) error {
|
||||
if email.Error != nil {
|
||||
return email.Error
|
||||
}
|
||||
|
||||
if from == "" {
|
||||
from = email.from
|
||||
}
|
||||
|
||||
if len(email.recipients) < 1 {
|
||||
return errors.New("Mail Error: No recipient specified")
|
||||
}
|
||||
|
||||
var msg string
|
||||
if email.DkimMsg != "" {
|
||||
msg = email.DkimMsg
|
||||
} else {
|
||||
msg = email.GetMessage()
|
||||
}
|
||||
|
||||
return send(from, email.recipients, msg, client)
|
||||
}
|
||||
|
||||
// dial connects to the smtp server with the request encryption type
|
||||
func dial(host string, port string, encryption Encryption, config *tls.Config) (*smtpClient, error) {
|
||||
var conn net.Conn
|
||||
var err error
|
||||
|
||||
address := host + ":" + port
|
||||
|
||||
// do the actual dial
|
||||
switch encryption {
|
||||
// TODO: Remove EncryptionSSL check before launch v3
|
||||
case EncryptionSSL, EncryptionSSLTLS:
|
||||
conn, err = tls.Dial("tcp", address, config)
|
||||
default:
|
||||
conn, err = net.Dial("tcp", address)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.New("Mail Error on dialing with encryption type " + encryption.String() + ": " + err.Error())
|
||||
}
|
||||
|
||||
c, err := newClient(conn, host)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Mail Error on smtp dial: %w", err)
|
||||
}
|
||||
|
||||
return c, err
|
||||
}
|
||||
|
||||
// smtpConnect connects to the smtp server and starts TLS and passes auth
|
||||
// if necessary
|
||||
func smtpConnect(host, port, helo string, a auth, at AuthType, encryption Encryption, config *tls.Config) (*smtpClient, error) {
|
||||
// connect to the mail server
|
||||
c, err := dial(host, port, encryption, config)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if helo == "" {
|
||||
helo = "localhost"
|
||||
}
|
||||
|
||||
// send Helo
|
||||
if err = c.hi(helo); err != nil {
|
||||
c.close()
|
||||
return nil, fmt.Errorf("Mail Error on Hello: %w", err)
|
||||
}
|
||||
|
||||
// STARTTLS if necessary
|
||||
// TODO: Remove EncryptionTLS check before launch v3
|
||||
if encryption == EncryptionTLS || encryption == EncryptionSTARTTLS {
|
||||
if ok, _ := c.extension("STARTTLS"); ok {
|
||||
if err = c.startTLS(config); err != nil {
|
||||
c.close()
|
||||
return nil, fmt.Errorf("Mail Error on STARTTLS: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// only pass authentication if defined
|
||||
if at != AuthNone {
|
||||
// pass the authentication if necessary
|
||||
if a != nil {
|
||||
if ok, _ := c.extension("AUTH"); ok {
|
||||
if err = c.authenticate(a); err != nil {
|
||||
c.close()
|
||||
return nil, fmt.Errorf("Mail Error on Auth: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Connect returns the smtp client
|
||||
func (server *SMTPServer) Connect() (*SMTPClient, error) {
|
||||
|
||||
var a auth
|
||||
|
||||
switch server.Authentication {
|
||||
case AuthPlain:
|
||||
if server.Username != "" || server.Password != "" {
|
||||
a = plainAuthfn("", server.Username, server.Password, server.Host)
|
||||
}
|
||||
case AuthLogin:
|
||||
if server.Username != "" || server.Password != "" {
|
||||
a = loginAuthfn("", server.Username, server.Password, server.Host)
|
||||
}
|
||||
case AuthCRAMMD5:
|
||||
if server.Username != "" || server.Password != "" {
|
||||
a = cramMD5Authfn(server.Username, server.Password)
|
||||
}
|
||||
}
|
||||
|
||||
var smtpConnectChannel chan error
|
||||
var c *smtpClient
|
||||
var err error
|
||||
|
||||
tlsConfig := server.TLSConfig
|
||||
if tlsConfig == nil {
|
||||
tlsConfig = &tls.Config{ServerName: server.Host}
|
||||
}
|
||||
|
||||
// if there is a ConnectTimeout, setup the channel and do the connect under a goroutine
|
||||
if server.ConnectTimeout != 0 {
|
||||
smtpConnectChannel = make(chan error, 2)
|
||||
go func() {
|
||||
c, err = smtpConnect(server.Host, fmt.Sprintf("%d", server.Port), server.Helo, a, server.Authentication, server.Encryption, tlsConfig)
|
||||
// send the result
|
||||
smtpConnectChannel <- err
|
||||
}()
|
||||
// get the connect result or timeout result, which ever happens first
|
||||
select {
|
||||
case err = <-smtpConnectChannel:
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case <-time.After(server.ConnectTimeout):
|
||||
return nil, errors.New("Mail Error: SMTP Connection timed out")
|
||||
}
|
||||
} else {
|
||||
// no ConnectTimeout, just fire the connect
|
||||
c, err = smtpConnect(server.Host, fmt.Sprintf("%d", server.Port), server.Helo, a, server.Authentication, server.Encryption, tlsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &SMTPClient{
|
||||
Client: c,
|
||||
KeepAlive: server.KeepAlive,
|
||||
SendTimeout: server.SendTimeout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Reset send RSET command to smtp client
|
||||
func (smtpClient *SMTPClient) Reset() error {
|
||||
return smtpClient.Client.reset()
|
||||
}
|
||||
|
||||
// Noop send NOOP command to smtp client
|
||||
func (smtpClient *SMTPClient) Noop() error {
|
||||
return smtpClient.Client.noop()
|
||||
}
|
||||
|
||||
// Quit send QUIT command to smtp client
|
||||
func (smtpClient *SMTPClient) Quit() error {
|
||||
return smtpClient.Client.quit()
|
||||
}
|
||||
|
||||
// Close closes the connection
|
||||
func (smtpClient *SMTPClient) Close() error {
|
||||
return smtpClient.Client.close()
|
||||
}
|
||||
|
||||
// SendMessage sends a message (a RFC822 formatted message)
|
||||
// 'from' must be an email address, recipients must be a slice of email address
|
||||
func SendMessage(from string, recipients []string, msg string, client *SMTPClient) error {
|
||||
if from == "" {
|
||||
return errors.New("Mail Error: No From email specifier")
|
||||
}
|
||||
if len(recipients) < 1 {
|
||||
return errors.New("Mail Error: No recipient specified")
|
||||
}
|
||||
|
||||
return send(from, recipients, msg, client)
|
||||
}
|
||||
|
||||
// send does the low level sending of the email
|
||||
func send(from string, to []string, msg string, client *SMTPClient) error {
|
||||
//Check if client struct is not nil
|
||||
if client != nil {
|
||||
|
||||
//Check if client is not nil
|
||||
if client.Client != nil {
|
||||
var smtpSendChannel chan error
|
||||
|
||||
// if there is a SendTimeout, setup the channel and do the send under a goroutine
|
||||
if client.SendTimeout != 0 {
|
||||
smtpSendChannel = make(chan error, 1)
|
||||
|
||||
go func(from string, to []string, msg string, c *smtpClient) {
|
||||
smtpSendChannel <- sendMailProcess(from, to, msg, c)
|
||||
}(from, to, msg, client.Client)
|
||||
}
|
||||
|
||||
if client.SendTimeout == 0 {
|
||||
// no SendTimeout, just fire the sendMailProcess
|
||||
return sendMailProcess(from, to, msg, client.Client)
|
||||
}
|
||||
|
||||
// get the send result or timeout result, which ever happens first
|
||||
select {
|
||||
case sendError := <-smtpSendChannel:
|
||||
checkKeepAlive(client)
|
||||
return sendError
|
||||
case <-time.After(client.SendTimeout):
|
||||
checkKeepAlive(client)
|
||||
return errors.New("Mail Error: SMTP Send timed out")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("Mail Error: No SMTP Client Provided")
|
||||
}
|
||||
|
||||
func sendMailProcess(from string, to []string, msg string, c *smtpClient) error {
|
||||
|
||||
cmdArgs := make(map[string]string)
|
||||
|
||||
if _, ok := c.ext["SIZE"]; ok {
|
||||
cmdArgs["SIZE"] = strconv.Itoa(len(msg))
|
||||
}
|
||||
|
||||
// Set the sender
|
||||
if err := c.mail(from, cmdArgs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the recipients
|
||||
for _, address := range to {
|
||||
if err := c.rcpt(address); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Send the data command
|
||||
w, err := c.data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// write the message
|
||||
_, err = fmt.Fprint(w, msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// check if keepAlive for close or reset
|
||||
func checkKeepAlive(client *SMTPClient) {
|
||||
if client.KeepAlive {
|
||||
client.Client.reset()
|
||||
} else {
|
||||
client.Client.quit()
|
||||
client.Client.close()
|
||||
}
|
||||
}
|
||||
+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 string) error {
|
||||
if err := validateLine(to); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _, err := c.cmd(25, "RCPT TO:<%s>", to)
|
||||
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