1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-19 05:09:42 +02:00

New SMTP library and refactoring of mailer code

This commit is contained in:
sauls8t 2018-03-07 18:52:19 +00:00
parent 99220641f3
commit bd43319bb2
39 changed files with 3163 additions and 1268 deletions

8
Gopkg.lock generated
View file

@ -149,9 +149,15 @@
revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a" revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
version = "v1.0.0" version = "v1.0.0"
[[projects]]
branch = "v3"
name = "gopkg.in/alexcesaro/quotedprintable.v3"
packages = ["."]
revision = "2caba252f4dc53eaf6b553000885530023f54623"
[solve-meta] [solve-meta]
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
inputs-digest = "98ebdf85207168f55b51629c6c21eb1459881fa58c280503054994887cbde045" inputs-digest = "56eea54cf0b9e18b3e456199ee01aedeeb07b55073ca3d10345e0c178689aee2"
solver-name = "gps-cdcl" solver-name = "gps-cdcl"
solver-version = 1 solver-version = 1

65
core/mail/CHANGELOG.md Normal file
View file

@ -0,0 +1,65 @@
# Change Log
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## [2.2.0] - 2018-03-01
### Added
- #20: Adds `Message.SetBoundary` to allow specifying a custom MIME boundary.
- #22: Adds `Message.SetBodyWriter` to make it easy to use text/template and
html/template for message bodies. Contributed by Quantcast.
- #25: Adds `Dialer.StartTLSPolicy` so that `MandatoryStartTLS` can be required,
or `NoStartTLS` can disable it. Contributed by Quantcast.
## [2.1.0] - 2017-12-14
### Added
- go-gomail#40: Adds `Dialer.LocalName` field to allow specifying the hostname
sent with SMTP's HELO command.
- go-gomail#47: `Message.SetBody`, `Message.AddAlternative`, and
`Message.AddAlternativeWriter` allow specifying the encoding of message parts.
- `Dialer.Dial`'s returned `SendCloser` automatically redials after a timeout.
- go-gomail#55, go-gomail#56: Adds `Rename` to allow specifying filename
of an attachment.
- go-gomail#100: Exports `NetDialTimeout` to allow setting a custom dialer.
- go-gomail#70: Adds `Dialer.Timeout` field to allow specifying a timeout for
dials, reads, and writes.
### Changed
- go-gomail#52: `Dialer.Dial` automatically uses CRAM-MD5 when available.
- `Dialer.Dial` specifies a default timeout of 10 seconds.
- Gomail is forked from <https://github.com/go-gomail/gomail/> to
<https://github.com/go-mail/mail/>.
### Deprecated
- go-gomail#52: `NewPlainDialer` is deprecated in favor of `NewDialer`.
### Fixed
- go-gomail#41, go-gomail#42: Fixes a panic when a `Message` contains a
nil header.
- go-gomail#44: Fixes `AddAlternativeWriter` replacing the message body instead
of adding a body part.
- go-gomail#53: Folds long header lines for RFC 2047 compliance.
- go-gomail#54: Fixes `Message.FormatAddress` when name is blank.
## [2.0.0] - 2015-09-02
- Mailer has been removed. It has been replaced by Dialer and Sender.
- `File` type and the `CreateFile` and `OpenFile` functions have been removed.
- `Message.Attach` and `Message.Embed` have a new signature.
- `Message.GetBodyWriter` has been removed. Use `Message.AddAlternativeWriter`
instead.
- `Message.Export` has been removed. `Message.WriteTo` can be used instead.
- `Message.DelHeader` has been removed.
- The `Bcc` header field is no longer sent. It is far more simpler and
efficient: the same message is sent to all recipients instead of sending a
different email to each Bcc address.
- LoginAuth has been removed. `NewPlainDialer` now implements the LOGIN
authentication mechanism when needed.
- Go 1.2 is now required instead of Go 1.3. No external dependency are used when
using Go 1.5.

20
core/mail/CONTRIBUTING.md Normal file
View file

@ -0,0 +1,20 @@
Thank you for contributing to Gomail! Here are a few guidelines:
## Bugs
If you think you found a bug, create an issue and supply the minimum amount
of code triggering the bug so it can be reproduced.
## Fixing a bug
If you want to fix a bug, you can send a pull request. It should contains a
new test or update an existing one to cover that bug.
## New feature proposal
If you think Gomail lacks a feature, you can open an issue or send a pull
request. I want to keep Gomail code and API as simple as possible so please
describe your needs so we can discuss whether this feature should be added to
Gomail or not.

20
core/mail/LICENSE Normal file
View file

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2014 Alexandre Cesaro
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.

129
core/mail/README.md Normal file
View file

@ -0,0 +1,129 @@
# Gomail
[![Build Status](https://travis-ci.org/go-mail/mail.svg?branch=master)](https://travis-ci.org/go-mail/mail) [![Code Coverage](http://gocover.io/_badge/github.com/go-mail/mail)](http://gocover.io/github.com/go-mail/mail) [![Documentation](https://godoc.org/github.com/go-mail/mail?status.svg)](https://godoc.org/github.com/go-mail/mail)
This is an actively maintained fork of [Gomail][1] and includes fixes and
improvements for a number of outstanding issues. The current progress is
as follows:
- [x] Timeouts and retries can be specified outside of the 10 second default.
- [x] Proxying is supported through specifying a custom [NetDialTimeout][2].
- [ ] Filenames are properly encoded for non-ASCII characters.
- [ ] Email addresses are properly encoded for non-ASCII characters.
- [ ] Embedded files and attachments are tested for their existence.
- [ ] An `io.Reader` can be supplied when embedding and attaching files.
See [Transitioning Existing Codebases][3] for more information on switching.
[1]: https://github.com/go-gomail/gomail
[2]: https://godoc.org/gopkg.in/mail.v2#NetDialTimeout
[3]: #transitioning-existing-codebases
## Introduction
Gomail is a simple and efficient package to send emails. It is well tested and
documented.
Gomail 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.
It requires Go 1.2 or newer. With Go 1.5, no external dependencies are used.
## Features
Gomail supports:
- Attachments
- Embedded images
- HTML and text templates
- Automatic encoding of special characters
- SSL and TLS
- Sending multiple emails with the same SMTP connection
## Documentation
https://godoc.org/github.com/go-mail/mail
## Download
If you're already using a dependency manager, like [dep][dep], use the following
import path:
```
github.com/go-mail/mail
```
If you *aren't* using vendoring, `go get` the [Gopkg.in](http://gopkg.in)
import path:
```
gopkg.in/mail.v2
```
[dep]: https://github.com/golang/dep#readme
## Examples
See the [examples in the documentation](https://godoc.org/github.com/go-mail/mail#example-package).
## FAQ
### x509: certificate signed by unknown authority
If you get this error it means the certificate used by the SMTP server is not
considered valid by the client running Gomail. As a quick workaround you can
bypass the verification of the server's certificate chain and host name by using
`SetTLSConfig`:
```go
package main
import (
"crypto/tls"
"gopkg.in/mail.v2"
)
func main() {
d := mail.NewDialer("smtp.example.com", 587, "user", "123456")
d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
// Send emails using d.
}
```
Note, however, that this is insecure and should not be used in production.
### Transitioning Existing Codebases
If you're already using the original Gomail, switching is as easy as updating
the import line to:
```
import gomail "gopkg.in/mail.v2"
```
## Contribute
Contributions are more than welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for
more info.
## Change log
See [CHANGELOG.md](CHANGELOG.md).
## License
[MIT](LICENSE)
## Support & Contact
You can ask questions on the [Gomail
thread](https://groups.google.com/d/topic/golang-nuts/jMxZHzvvEVg/discussion)
in the Go mailing-list.

49
core/mail/auth.go Normal file
View file

@ -0,0 +1,49 @@
package mail
import (
"bytes"
"errors"
"fmt"
"net/smtp"
)
// loginAuth is an smtp.Auth that implements the LOGIN authentication mechanism.
type loginAuth struct {
username string
password string
host string
}
func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
if !server.TLS {
advertised := false
for _, mechanism := range server.Auth {
if mechanism == "LOGIN" {
advertised = true
break
}
}
if !advertised {
return "", nil, errors.New("gomail: unencrypted connection")
}
}
if server.Name != a.host {
return "", nil, errors.New("gomail: wrong host name")
}
return "LOGIN", nil, nil
}
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
if !more {
return nil, nil
}
switch {
case bytes.Equal(fromServer, []byte("Username:")):
return []byte(a.username), nil
case bytes.Equal(fromServer, []byte("Password:")):
return []byte(a.password), nil
default:
return nil, fmt.Errorf("gomail: unexpected server challenge: %s", fromServer)
}
}

6
core/mail/doc.go Normal file
View file

@ -0,0 +1,6 @@
// Package gomail provides a simple interface to compose emails and to mail them
// efficiently.
//
// More info on Github: https://github.com/go-mail/mail
//
package mail

334
core/mail/message.go Normal file
View file

@ -0,0 +1,334 @@
package mail
import (
"bytes"
"io"
"os"
"path/filepath"
"time"
)
// Message represents an email.
type Message struct {
header header
parts []*part
attachments []*file
embedded []*file
charset string
encoding Encoding
hEncoder mimeEncoder
buf bytes.Buffer
boundary string
}
type header map[string][]string
type part struct {
contentType string
copier func(io.Writer) error
encoding Encoding
}
// NewMessage creates a new message. It uses UTF-8 and quoted-printable encoding
// by default.
func NewMessage(settings ...MessageSetting) *Message {
m := &Message{
header: make(header),
charset: "UTF-8",
encoding: QuotedPrintable,
}
m.applySettings(settings)
if m.encoding == Base64 {
m.hEncoder = bEncoding
} else {
m.hEncoder = qEncoding
}
return m
}
// Reset resets the message so it can be reused. The message keeps its previous
// settings so it is in the same state that after a call to NewMessage.
func (m *Message) Reset() {
for k := range m.header {
delete(m.header, k)
}
m.parts = nil
m.attachments = nil
m.embedded = nil
}
func (m *Message) applySettings(settings []MessageSetting) {
for _, s := range settings {
s(m)
}
}
// A MessageSetting can be used as an argument in NewMessage to configure an
// email.
type MessageSetting func(m *Message)
// SetCharset is a message setting to set the charset of the email.
func SetCharset(charset string) MessageSetting {
return func(m *Message) {
m.charset = charset
}
}
// SetEncoding is a message setting to set the encoding of the email.
func SetEncoding(enc Encoding) MessageSetting {
return func(m *Message) {
m.encoding = enc
}
}
// Encoding represents a MIME encoding scheme like quoted-printable or base64.
type Encoding string
const (
// QuotedPrintable represents the quoted-printable encoding as defined in
// RFC 2045.
QuotedPrintable Encoding = "quoted-printable"
// Base64 represents the base64 encoding as defined in RFC 2045.
Base64 Encoding = "base64"
// Unencoded can be used to avoid encoding the body of an email. The headers
// will still be encoded using quoted-printable encoding.
Unencoded Encoding = "8bit"
)
// SetBoundary sets a custom multipart boundary.
func (m *Message) SetBoundary(boundary string) {
m.boundary = boundary
}
// SetHeader sets a value to the given header field.
func (m *Message) SetHeader(field string, value ...string) {
m.encodeHeader(value)
m.header[field] = value
}
func (m *Message) encodeHeader(values []string) {
for i := range values {
values[i] = m.encodeString(values[i])
}
}
func (m *Message) encodeString(value string) string {
return m.hEncoder.Encode(m.charset, value)
}
// SetHeaders sets the message headers.
func (m *Message) SetHeaders(h map[string][]string) {
for k, v := range h {
m.SetHeader(k, v...)
}
}
// SetAddressHeader sets an address to the given header field.
func (m *Message) SetAddressHeader(field, address, name string) {
m.header[field] = []string{m.FormatAddress(address, name)}
}
// FormatAddress formats an address and a name as a valid RFC 5322 address.
func (m *Message) FormatAddress(address, name string) string {
if name == "" {
return address
}
enc := m.encodeString(name)
if enc == name {
m.buf.WriteByte('"')
for i := 0; i < len(name); i++ {
b := name[i]
if b == '\\' || b == '"' {
m.buf.WriteByte('\\')
}
m.buf.WriteByte(b)
}
m.buf.WriteByte('"')
} else if hasSpecials(name) {
m.buf.WriteString(bEncoding.Encode(m.charset, name))
} else {
m.buf.WriteString(enc)
}
m.buf.WriteString(" <")
m.buf.WriteString(address)
m.buf.WriteByte('>')
addr := m.buf.String()
m.buf.Reset()
return addr
}
func hasSpecials(text string) bool {
for i := 0; i < len(text); i++ {
switch c := text[i]; c {
case '(', ')', '<', '>', '[', ']', ':', ';', '@', '\\', ',', '.', '"':
return true
}
}
return false
}
// SetDateHeader sets a date to the given header field.
func (m *Message) SetDateHeader(field string, date time.Time) {
m.header[field] = []string{m.FormatDate(date)}
}
// FormatDate formats a date as a valid RFC 5322 date.
func (m *Message) FormatDate(date time.Time) string {
return date.Format(time.RFC1123Z)
}
// GetHeader gets a header field.
func (m *Message) GetHeader(field string) []string {
return m.header[field]
}
// SetBody sets the body of the message. It replaces any content previously set
// by SetBody, SetBodyWriter, AddAlternative or AddAlternativeWriter.
func (m *Message) SetBody(contentType, body string, settings ...PartSetting) {
m.SetBodyWriter(contentType, newCopier(body), settings...)
}
// SetBodyWriter sets the body of the message. It can be useful with the
// text/template or html/template packages.
func (m *Message) SetBodyWriter(contentType string, f func(io.Writer) error, settings ...PartSetting) {
m.parts = []*part{m.newPart(contentType, f, settings)}
}
// AddAlternative adds an alternative part to the message.
//
// It is commonly used to send HTML emails that default to the plain text
// version for backward compatibility. AddAlternative appends the new part to
// the end of the message. So the plain text part should be added before the
// HTML part. See http://en.wikipedia.org/wiki/MIME#Alternative
func (m *Message) AddAlternative(contentType, body string, settings ...PartSetting) {
m.AddAlternativeWriter(contentType, newCopier(body), settings...)
}
func newCopier(s string) func(io.Writer) error {
return func(w io.Writer) error {
_, err := io.WriteString(w, s)
return err
}
}
// AddAlternativeWriter adds an alternative part to the message. It can be
// useful with the text/template or html/template packages.
func (m *Message) AddAlternativeWriter(contentType string, f func(io.Writer) error, settings ...PartSetting) {
m.parts = append(m.parts, m.newPart(contentType, f, settings))
}
func (m *Message) newPart(contentType string, f func(io.Writer) error, settings []PartSetting) *part {
p := &part{
contentType: contentType,
copier: f,
encoding: m.encoding,
}
for _, s := range settings {
s(p)
}
return p
}
// A PartSetting can be used as an argument in Message.SetBody,
// Message.SetBodyWriter, Message.AddAlternative or Message.AddAlternativeWriter
// to configure the part added to a message.
type PartSetting func(*part)
// SetPartEncoding sets the encoding of the part added to the message. By
// default, parts use the same encoding than the message.
func SetPartEncoding(e Encoding) PartSetting {
return PartSetting(func(p *part) {
p.encoding = e
})
}
type file struct {
Name string
Header map[string][]string
CopyFunc func(w io.Writer) error
}
func (f *file) setHeader(field, value string) {
f.Header[field] = []string{value}
}
// A FileSetting can be used as an argument in Message.Attach or Message.Embed.
type FileSetting func(*file)
// SetHeader is a file setting to set the MIME header of the message part that
// contains the file content.
//
// Mandatory headers are automatically added if they are not set when sending
// the email.
func SetHeader(h map[string][]string) FileSetting {
return func(f *file) {
for k, v := range h {
f.Header[k] = v
}
}
}
// Rename is a file setting to set the name of the attachment if the name is
// different than the filename on disk.
func Rename(name string) FileSetting {
return func(f *file) {
f.Name = name
}
}
// SetCopyFunc is a file setting to replace the function that runs when the
// message is sent. It should copy the content of the file to the io.Writer.
//
// The default copy function opens the file with the given filename, and copy
// its content to the io.Writer.
func SetCopyFunc(f func(io.Writer) error) FileSetting {
return func(fi *file) {
fi.CopyFunc = f
}
}
func (m *Message) appendFile(list []*file, name string, settings []FileSetting) []*file {
f := &file{
Name: filepath.Base(name),
Header: make(map[string][]string),
CopyFunc: func(w io.Writer) error {
h, err := os.Open(name)
if err != nil {
return err
}
if _, err := io.Copy(w, h); err != nil {
h.Close()
return err
}
return h.Close()
},
}
for _, s := range settings {
s(f)
}
if list == nil {
return []*file{f}
}
return append(list, f)
}
// Attach attaches the files to the email.
func (m *Message) Attach(filename string, settings ...FileSetting) {
m.attachments = m.appendFile(m.attachments, filename, settings)
}
// Embed embeds the images to the email.
func (m *Message) Embed(filename string, settings ...FileSetting) {
m.embedded = m.appendFile(m.embedded, filename, settings)
}

21
core/mail/mime.go Normal file
View file

@ -0,0 +1,21 @@
// +build go1.5
package mail
import (
"mime"
"mime/quotedprintable"
"strings"
)
var newQPWriter = quotedprintable.NewWriter
type mimeEncoder struct {
mime.WordEncoder
}
var (
bEncoding = mimeEncoder{mime.BEncoding}
qEncoding = mimeEncoder{mime.QEncoding}
lastIndexByte = strings.LastIndexByte
)

25
core/mail/mime_go14.go Normal file
View file

@ -0,0 +1,25 @@
// +build !go1.5
package mail
import "gopkg.in/alexcesaro/quotedprintable.v3"
var newQPWriter = quotedprintable.NewWriter
type mimeEncoder struct {
quotedprintable.WordEncoder
}
var (
bEncoding = mimeEncoder{quotedprintable.BEncoding}
qEncoding = mimeEncoder{quotedprintable.QEncoding}
lastIndexByte = func(s string, c byte) int {
for i := len(s) - 1; i >= 0; i-- {
if s[i] == c {
return i
}
}
return -1
}
)

116
core/mail/send.go Normal file
View file

@ -0,0 +1,116 @@
package mail
import (
"errors"
"fmt"
"io"
stdmail "net/mail"
)
// Sender is the interface that wraps the Send method.
//
// Send sends an email to the given addresses.
type Sender interface {
Send(from string, to []string, msg io.WriterTo) error
}
// SendCloser is the interface that groups the Send and Close methods.
type SendCloser interface {
Sender
Close() error
}
// A SendFunc is a function that sends emails to the given addresses.
//
// The SendFunc type is an adapter to allow the use of ordinary functions as
// email senders. If f is a function with the appropriate signature, SendFunc(f)
// is a Sender object that calls f.
type SendFunc func(from string, to []string, msg io.WriterTo) error
// Send calls f(from, to, msg).
func (f SendFunc) Send(from string, to []string, msg io.WriterTo) error {
return f(from, to, msg)
}
// Send sends emails using the given Sender.
func Send(s Sender, msg ...*Message) error {
for i, m := range msg {
if err := send(s, m); err != nil {
return fmt.Errorf("gomail: could not send email %d: %v", i+1, err)
}
}
return nil
}
func send(s Sender, m *Message) error {
from, err := m.getFrom()
if err != nil {
return err
}
to, err := m.getRecipients()
if err != nil {
return err
}
if err := s.Send(from, to, m); err != nil {
return err
}
return nil
}
func (m *Message) getFrom() (string, error) {
from := m.header["Sender"]
if len(from) == 0 {
from = m.header["From"]
if len(from) == 0 {
return "", errors.New(`gomail: invalid message, "From" field is absent`)
}
}
return parseAddress(from[0])
}
func (m *Message) getRecipients() ([]string, error) {
n := 0
for _, field := range []string{"To", "Cc", "Bcc"} {
if addresses, ok := m.header[field]; ok {
n += len(addresses)
}
}
list := make([]string, 0, n)
for _, field := range []string{"To", "Cc", "Bcc"} {
if addresses, ok := m.header[field]; ok {
for _, a := range addresses {
addr, err := parseAddress(a)
if err != nil {
return nil, err
}
list = addAddress(list, addr)
}
}
}
return list, nil
}
func addAddress(list []string, addr string) []string {
for _, a := range list {
if addr == a {
return list
}
}
return append(list, addr)
}
func parseAddress(field string) (string, error) {
addr, err := stdmail.ParseAddress(field)
if err != nil {
return "", fmt.Errorf("gomail: invalid address %q: %v", field, err)
}
return addr.Address, nil
}

292
core/mail/smtp.go Normal file
View file

@ -0,0 +1,292 @@
package mail
import (
"crypto/tls"
"fmt"
"io"
"net"
"net/smtp"
"strings"
"time"
)
// A Dialer is a dialer to an SMTP server.
type Dialer struct {
// Host represents the host of the SMTP server.
Host string
// Port represents the port of the SMTP server.
Port int
// Username is the username to use to authenticate to the SMTP server.
Username string
// Password is the password to use to authenticate to the SMTP server.
Password string
// Auth represents the authentication mechanism used to authenticate to the
// SMTP server.
Auth smtp.Auth
// SSL defines whether an SSL connection is used. It should be false in
// most cases since the authentication mechanism should use the STARTTLS
// extension instead.
SSL bool
// TLSConfig represents the TLS configuration used for the TLS (when the
// STARTTLS extension is used) or SSL connection.
TLSConfig *tls.Config
// StartTLSPolicy represents the TLS security level required to
// communicate with the SMTP server.
//
// This defaults to OpportunisticStartTLS for backwards compatibility,
// but we recommend MandatoryStartTLS for all modern SMTP servers.
//
// This option has no effect if SSL is set to true.
StartTLSPolicy StartTLSPolicy
// LocalName is the hostname sent to the SMTP server with the HELO command.
// By default, "localhost" is sent.
LocalName string
// Timeout to use for read/write operations. Defaults to 10 seconds, can
// be set to 0 to disable timeouts.
Timeout time.Duration
// Whether we should retry mailing if the connection returned an error,
// defaults to true.
RetryFailure bool
}
// NewDialer returns a new SMTP Dialer. The given parameters are used to connect
// to the SMTP server.
func NewDialer(host string, port int, username, password string) *Dialer {
return &Dialer{
Host: host,
Port: port,
Username: username,
Password: password,
SSL: port == 465,
Timeout: 10 * time.Second,
RetryFailure: true,
}
}
// NewPlainDialer returns a new SMTP Dialer. The given parameters are used to
// connect to the SMTP server.
//
// Deprecated: Use NewDialer instead.
func NewPlainDialer(host string, port int, username, password string) *Dialer {
return NewDialer(host, port, username, password)
}
// NetDialTimeout specifies the DialTimeout function to establish a connection
// to the SMTP server. This can be used to override dialing in the case that a
// proxy or other special behavior is needed.
var NetDialTimeout = net.DialTimeout
// Dial dials and authenticates to an SMTP server. The returned SendCloser
// should be closed when done using it.
func (d *Dialer) Dial() (SendCloser, error) {
conn, err := NetDialTimeout("tcp", addr(d.Host, d.Port), d.Timeout)
if err != nil {
return nil, err
}
if d.SSL {
conn = tlsClient(conn, d.tlsConfig())
}
c, err := smtpNewClient(conn, d.Host)
if err != nil {
return nil, err
}
if d.Timeout > 0 {
conn.SetDeadline(time.Now().Add(d.Timeout))
}
if d.LocalName != "" {
if err := c.Hello(d.LocalName); err != nil {
return nil, err
}
}
if !d.SSL && d.StartTLSPolicy != NoStartTLS {
ok, _ := c.Extension("STARTTLS")
if !ok && d.StartTLSPolicy == MandatoryStartTLS {
err := StartTLSUnsupportedError{
Policy: d.StartTLSPolicy}
return nil, err
}
if ok {
if err := c.StartTLS(d.tlsConfig()); err != nil {
c.Close()
return nil, err
}
}
}
if d.Auth == nil && d.Username != "" {
if ok, auths := c.Extension("AUTH"); ok {
if strings.Contains(auths, "CRAM-MD5") {
d.Auth = smtp.CRAMMD5Auth(d.Username, d.Password)
} else if strings.Contains(auths, "LOGIN") &&
!strings.Contains(auths, "PLAIN") {
d.Auth = &loginAuth{
username: d.Username,
password: d.Password,
host: d.Host,
}
} else {
d.Auth = smtp.PlainAuth("", d.Username, d.Password, d.Host)
}
}
}
if d.Auth != nil {
if err = c.Auth(d.Auth); err != nil {
c.Close()
return nil, err
}
}
return &smtpSender{c, conn, d}, nil
}
func (d *Dialer) tlsConfig() *tls.Config {
if d.TLSConfig == nil {
return &tls.Config{ServerName: d.Host}
}
return d.TLSConfig
}
// StartTLSPolicy constants are valid values for Dialer.StartTLSPolicy.
type StartTLSPolicy int
const (
// OpportunisticStartTLS means that SMTP transactions are encrypted if
// STARTTLS is supported by the SMTP server. Otherwise, messages are
// sent in the clear. This is the default setting.
OpportunisticStartTLS StartTLSPolicy = iota
// MandatoryStartTLS means that SMTP transactions must be encrypted.
// SMTP transactions are aborted unless STARTTLS is supported by the
// SMTP server.
MandatoryStartTLS
// NoStartTLS means encryption is disabled and messages are sent in the
// clear.
NoStartTLS = -1
)
func (policy *StartTLSPolicy) String() string {
switch *policy {
case OpportunisticStartTLS:
return "OpportunisticStartTLS"
case MandatoryStartTLS:
return "MandatoryStartTLS"
case NoStartTLS:
return "NoStartTLS"
default:
return fmt.Sprintf("StartTLSPolicy:%v", *policy)
}
}
// StartTLSUnsupportedError is returned by Dial when connecting to an SMTP
// server that does not support STARTTLS.
type StartTLSUnsupportedError struct {
Policy StartTLSPolicy
}
func (e StartTLSUnsupportedError) Error() string {
return "gomail: " + e.Policy.String() + " required, but " +
"SMTP server does not support STARTTLS"
}
func addr(host string, port int) string {
return fmt.Sprintf("%s:%d", host, port)
}
// DialAndSend opens a connection to the SMTP server, sends the given emails and
// closes the connection.
func (d *Dialer) DialAndSend(m ...*Message) error {
s, err := d.Dial()
if err != nil {
return err
}
defer s.Close()
return Send(s, m...)
}
type smtpSender struct {
smtpClient
conn net.Conn
d *Dialer
}
func (c *smtpSender) retryError(err error) bool {
if !c.d.RetryFailure {
return false
}
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
return true
}
return err == io.EOF
}
func (c *smtpSender) Send(from string, to []string, msg io.WriterTo) error {
if c.d.Timeout > 0 {
c.conn.SetDeadline(time.Now().Add(c.d.Timeout))
}
if err := c.Mail(from); err != nil {
if c.retryError(err) {
// This is probably due to a timeout, so reconnect and try again.
sc, derr := c.d.Dial()
if derr == nil {
if s, ok := sc.(*smtpSender); ok {
*c = *s
return c.Send(from, to, msg)
}
}
}
return err
}
for _, addr := range to {
if err := c.Rcpt(addr); err != nil {
return err
}
}
w, err := c.Data()
if err != nil {
return err
}
if _, err = msg.WriteTo(w); err != nil {
w.Close()
return err
}
return w.Close()
}
func (c *smtpSender) Close() error {
return c.Quit()
}
// Stubbed out for tests.
var (
tlsClient = tls.Client
smtpNewClient = func(conn net.Conn, host string) (smtpClient, error) {
return smtp.NewClient(conn, host)
}
)
type smtpClient interface {
Hello(string) error
Extension(string) (bool, string)
StartTLS(*tls.Config) error
Auth(smtp.Auth) error
Mail(string) error
Rcpt(string) error
Data() (io.WriteCloser, error)
Quit() error
Close() error
}

309
core/mail/writeto.go Normal file
View file

@ -0,0 +1,309 @@
package mail
import (
"encoding/base64"
"errors"
"io"
"mime"
"mime/multipart"
"path/filepath"
"strings"
"time"
)
// WriteTo implements io.WriterTo. It dumps the whole message into w.
func (m *Message) WriteTo(w io.Writer) (int64, error) {
mw := &messageWriter{w: w}
mw.writeMessage(m)
return mw.n, mw.err
}
func (w *messageWriter) writeMessage(m *Message) {
if _, ok := m.header["Mime-Version"]; !ok {
w.writeString("Mime-Version: 1.0\r\n")
}
if _, ok := m.header["Date"]; !ok {
w.writeHeader("Date", m.FormatDate(now()))
}
w.writeHeaders(m.header)
if m.hasMixedPart() {
w.openMultipart("mixed", m.boundary)
}
if m.hasRelatedPart() {
w.openMultipart("related", m.boundary)
}
if m.hasAlternativePart() {
w.openMultipart("alternative", m.boundary)
}
for _, part := range m.parts {
w.writePart(part, m.charset)
}
if m.hasAlternativePart() {
w.closeMultipart()
}
w.addFiles(m.embedded, false)
if m.hasRelatedPart() {
w.closeMultipart()
}
w.addFiles(m.attachments, true)
if m.hasMixedPart() {
w.closeMultipart()
}
}
func (m *Message) hasMixedPart() bool {
return (len(m.parts) > 0 && len(m.attachments) > 0) || len(m.attachments) > 1
}
func (m *Message) hasRelatedPart() bool {
return (len(m.parts) > 0 && len(m.embedded) > 0) || len(m.embedded) > 1
}
func (m *Message) hasAlternativePart() bool {
return len(m.parts) > 1
}
type messageWriter struct {
w io.Writer
n int64
writers [3]*multipart.Writer
partWriter io.Writer
depth uint8
err error
}
func (w *messageWriter) openMultipart(mimeType, boundary string) {
mw := multipart.NewWriter(w)
if boundary != "" {
mw.SetBoundary(boundary)
}
contentType := "multipart/" + mimeType + ";\r\n boundary=" + mw.Boundary()
w.writers[w.depth] = mw
if w.depth == 0 {
w.writeHeader("Content-Type", contentType)
w.writeString("\r\n")
} else {
w.createPart(map[string][]string{
"Content-Type": {contentType},
})
}
w.depth++
}
func (w *messageWriter) createPart(h map[string][]string) {
w.partWriter, w.err = w.writers[w.depth-1].CreatePart(h)
}
func (w *messageWriter) closeMultipart() {
if w.depth > 0 {
w.writers[w.depth-1].Close()
w.depth--
}
}
func (w *messageWriter) writePart(p *part, charset string) {
w.writeHeaders(map[string][]string{
"Content-Type": {p.contentType + "; charset=" + charset},
"Content-Transfer-Encoding": {string(p.encoding)},
})
w.writeBody(p.copier, p.encoding)
}
func (w *messageWriter) addFiles(files []*file, isAttachment bool) {
for _, f := range files {
if _, ok := f.Header["Content-Type"]; !ok {
mediaType := mime.TypeByExtension(filepath.Ext(f.Name))
if mediaType == "" {
mediaType = "application/octet-stream"
}
f.setHeader("Content-Type", mediaType+`; name="`+f.Name+`"`)
}
if _, ok := f.Header["Content-Transfer-Encoding"]; !ok {
f.setHeader("Content-Transfer-Encoding", string(Base64))
}
if _, ok := f.Header["Content-Disposition"]; !ok {
var disp string
if isAttachment {
disp = "attachment"
} else {
disp = "inline"
}
f.setHeader("Content-Disposition", disp+`; filename="`+f.Name+`"`)
}
if !isAttachment {
if _, ok := f.Header["Content-ID"]; !ok {
f.setHeader("Content-ID", "<"+f.Name+">")
}
}
w.writeHeaders(f.Header)
w.writeBody(f.CopyFunc, Base64)
}
}
func (w *messageWriter) Write(p []byte) (int, error) {
if w.err != nil {
return 0, errors.New("gomail: cannot write as writer is in error")
}
var n int
n, w.err = w.w.Write(p)
w.n += int64(n)
return n, w.err
}
func (w *messageWriter) writeString(s string) {
n, _ := io.WriteString(w.w, s)
w.n += int64(n)
}
func (w *messageWriter) writeHeader(k string, v ...string) {
w.writeString(k)
if len(v) == 0 {
w.writeString(":\r\n")
return
}
w.writeString(": ")
// Max header line length is 78 characters in RFC 5322 and 76 characters
// in RFC 2047. So for the sake of simplicity we use the 76 characters
// limit.
charsLeft := 76 - len(k) - len(": ")
for i, s := range v {
// If the line is already too long, insert a newline right away.
if charsLeft < 1 {
if i == 0 {
w.writeString("\r\n ")
} else {
w.writeString(",\r\n ")
}
charsLeft = 75
} else if i != 0 {
w.writeString(", ")
charsLeft -= 2
}
// While the header content is too long, fold it by inserting a newline.
for len(s) > charsLeft {
s = w.writeLine(s, charsLeft)
charsLeft = 75
}
w.writeString(s)
if i := lastIndexByte(s, '\n'); i != -1 {
charsLeft = 75 - (len(s) - i - 1)
} else {
charsLeft -= len(s)
}
}
w.writeString("\r\n")
}
func (w *messageWriter) writeLine(s string, charsLeft int) string {
// If there is already a newline before the limit. Write the line.
if i := strings.IndexByte(s, '\n'); i != -1 && i < charsLeft {
w.writeString(s[:i+1])
return s[i+1:]
}
for i := charsLeft - 1; i >= 0; i-- {
if s[i] == ' ' {
w.writeString(s[:i])
w.writeString("\r\n ")
return s[i+1:]
}
}
// We could not insert a newline cleanly so look for a space or a newline
// even if it is after the limit.
for i := 75; i < len(s); i++ {
if s[i] == ' ' {
w.writeString(s[:i])
w.writeString("\r\n ")
return s[i+1:]
}
if s[i] == '\n' {
w.writeString(s[:i+1])
return s[i+1:]
}
}
// Too bad, no space or newline in the whole string. Just write everything.
w.writeString(s)
return ""
}
func (w *messageWriter) writeHeaders(h map[string][]string) {
if w.depth == 0 {
for k, v := range h {
if k != "Bcc" {
w.writeHeader(k, v...)
}
}
} else {
w.createPart(h)
}
}
func (w *messageWriter) writeBody(f func(io.Writer) error, enc Encoding) {
var subWriter io.Writer
if w.depth == 0 {
w.writeString("\r\n")
subWriter = w.w
} else {
subWriter = w.partWriter
}
if enc == Base64 {
wc := base64.NewEncoder(base64.StdEncoding, newBase64LineWriter(subWriter))
w.err = f(wc)
wc.Close()
} else if enc == Unencoded {
w.err = f(subWriter)
} else {
wc := newQPWriter(subWriter)
w.err = f(wc)
wc.Close()
}
}
// As required by RFC 2045, 6.7. (page 21) for quoted-printable, and
// RFC 2045, 6.8. (page 25) for base64.
const maxLineLen = 76
// base64LineWriter limits text encoded in base64 to 76 characters per line
type base64LineWriter struct {
w io.Writer
lineLen int
}
func newBase64LineWriter(w io.Writer) *base64LineWriter {
return &base64LineWriter{w: w}
}
func (w *base64LineWriter) Write(p []byte) (int, error) {
n := 0
for len(p)+w.lineLen > maxLineLen {
w.w.Write(p[:maxLineLen-w.lineLen])
w.w.Write([]byte("\r\n"))
p = p[maxLineLen-w.lineLen:]
n += maxLineLen - w.lineLen
w.lineLen = 0
}
w.w.Write(p)
w.lineLen += len(p)
return n + len(p), nil
}
// Stubbed out for testing.
var now = time.Now

View file

@ -76,7 +76,7 @@ background-color: #f6f6f6;
</tr> </tr>
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;"> <tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">
<td class="content-block" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top"> <td class="content-block" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
<a href="{{.Url}}" class="btn-primary" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background: #4ccb6a; margin: 0; padding: 0; border-color: #4ccb6a; border-style: solid; border-width: 10px 20px;">View document</a> <a href="{{.URL}}" class="btn-primary" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background: #4ccb6a; margin: 0; padding: 0; border-color: #4ccb6a; border-style: solid; border-width: 10px 20px;">View document</a>
</td> </td>
</tr> </tr>
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;"> <tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">

View file

@ -14,57 +14,50 @@
package mail package mail
import ( import (
"bytes"
"fmt" "fmt"
"html/template"
"github.com/documize/community/server/web" "github.com/documize/community/domain/smtp"
) )
// DocumentApprover notifies user who has just been granted document approval rights. // DocumentApprover notifies user who has just been granted document approval rights.
func (m *Mailer) DocumentApprover(recipient, inviter, url, document string) { func (m *Mailer) DocumentApprover(recipient, inviter, url, document string) {
method := "DocumentApprover" method := "DocumentApprover"
m.LoadCredentials() m.Initialize()
file, err := web.ReadFile("mail/document-approver.html")
if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
return
}
emailTemplate := string(file)
// check inviter name // check inviter name
if inviter == "Hello You" || len(inviter) == 0 { if inviter == "Hello You" || len(inviter) == 0 {
inviter = "Your colleague" inviter = "Your colleague"
} }
subject := fmt.Sprintf("%s has granted you document approval", inviter) em := smtp.EmailMessage{}
em.Subject = fmt.Sprintf("%s has granted you document approval", inviter)
e := NewEmail() em.ToEmail = recipient
e.From = m.Credentials.SMTPsender em.ToName = recipient
e.To = []string{recipient}
e.Subject = subject
parameters := struct { parameters := struct {
Subject string Subject string
Inviter string Inviter string
Url string URL string
Document string Document string
}{ }{
subject, em.Subject,
inviter, inviter,
url, url,
document, document,
} }
buffer := new(bytes.Buffer) html, err := m.ParseTemplate("mail/document-approver.html", parameters)
t := template.Must(template.New("emailTemplate").Parse(emailTemplate)) if err != nil {
t.Execute(buffer, &parameters) m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
e.HTML = buffer.Bytes() return
}
em.BodyHTML = html
err = e.Send(m.GetHost(), m.GetAuth()) ok, err := smtp.SendMessage(m.Dialer, m.Config, em)
if err != nil { if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err) m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err)
} }
if !ok {
m.Runtime.Log.Info(fmt.Sprintf("%s unable to send email"))
}
} }

View file

@ -72,7 +72,7 @@ background-color: #f6f6f6;
</tr> </tr>
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;"> <tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">
<td class="content-block" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top"> <td class="content-block" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
<a href="{{.Url}}" class="btn-primary" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background: #4ccb6a; margin: 0; padding: 0; border-color: #4ccb6a; border-style: solid; border-width: 10px 20px;">Click here to access Documize</a> <a href="{{.URL}}" class="btn-primary" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background: #4ccb6a; margin: 0; padding: 0; border-color: #4ccb6a; border-style: solid; border-width: 10px 20px;">Click here to access Documize</a>
</td> </td>
</tr> </tr>
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;"> <tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">

View file

@ -82,7 +82,7 @@ background-color: #f6f6f6;
</tr> </tr>
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;"> <tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">
<td class="content-block" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top"> <td class="content-block" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
<a href="{{.Url}}" class="btn-primary" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background: #4ccb6a; margin: 0; padding: 0; border-color: #4ccb6a; border-style: solid; border-width: 10px 20px;">Click here to access Documize</a> <a href="{{.URL}}" class="btn-primary" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background: #4ccb6a; margin: 0; padding: 0; border-color: #4ccb6a; border-style: solid; border-width: 10px 20px;">Click here to access Documize</a>
</td> </td>
</tr> </tr>
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;"> <tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">

View file

@ -9,16 +9,18 @@
// //
// https://documize.com // https://documize.com
// jshint ignore:start
package mail package mail
import ( import (
"net/smtp" "bytes"
"strings" "html/template"
"github.com/documize/community/core/env" "github.com/documize/community/core/env"
"github.com/documize/community/core/mail"
"github.com/documize/community/domain" "github.com/documize/community/domain"
"github.com/documize/community/domain/setting"
ds "github.com/documize/community/domain/smtp"
"github.com/documize/community/server/web"
) )
// Mailer provides emailing facilities // Mailer provides emailing facilities
@ -26,44 +28,38 @@ type Mailer struct {
Runtime *env.Runtime Runtime *env.Runtime
Store *domain.Store Store *domain.Store
Context domain.RequestContext Context domain.RequestContext
Credentials Credentials Config ds.Config
Dialer *mail.Dialer
} }
// Credentials holds SMTP endpoint and authentication methods // Initialize prepares mailer instance for action.
type Credentials struct { func (m *Mailer) Initialize() {
SMTPuserid string m.Config = setting.GetSMTPConfig(m.Store)
SMTPpassword string m.Dialer, _ = ds.Connect(m.Config)
SMTPhost string
SMTPport string
SMTPsender string
} }
// GetAuth to return SMTP authentication details // Send prepares and sends email.
func (m *Mailer) GetAuth() smtp.Auth { func (m *Mailer) Send(em ds.EmailMessage) (ok bool, err error) {
a := smtp.PlainAuth("", m.Credentials.SMTPuserid, m.Credentials.SMTPpassword, m.Credentials.SMTPhost) ok, err = ds.SendMessage(m.Dialer, m.Config, em)
return a return
} }
// GetHost to return SMTP host details // ParseTemplate produces email template.
func (m *Mailer) GetHost() string { func (m *Mailer) ParseTemplate(filename string, params interface{}) (html string, err error) {
h := m.Credentials.SMTPhost + ":" + m.Credentials.SMTPport html = ""
return h
file, err := web.ReadFile(filename)
if err != nil {
return
} }
// LoadCredentials loads up SMTP details from database emailTemplate := string(file)
func (m *Mailer) LoadCredentials() { buffer := new(bytes.Buffer)
userID, _ := m.Store.Setting.Get("SMTP", "userid")
m.Credentials.SMTPuserid = strings.TrimSpace(userID)
pwd, _ := m.Store.Setting.Get("SMTP", "password") t := template.Must(template.New("emailTemplate").Parse(emailTemplate))
m.Credentials.SMTPpassword = strings.TrimSpace(pwd) t.Execute(buffer, &params)
host, _ := m.Store.Setting.Get("SMTP", "host") html = buffer.String()
m.Credentials.SMTPhost = strings.TrimSpace(host)
port, _ := m.Store.Setting.Get("SMTP", "port") return
m.Credentials.SMTPport = strings.TrimSpace(port)
sender, _ := m.Store.Setting.Get("SMTP", "sender")
m.Credentials.SMTPsender = strings.TrimSpace(sender)
} }

View file

@ -79,7 +79,7 @@ background-color: #f6f6f6;
</tr> </tr>
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;"> <tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">
<td class="content-block" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top"> <td class="content-block" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
<a href="{{.Url}}" class="btn-primary" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background: #4ccb6a; margin: 0; padding: 0; border-color: #4ccb6a; border-style: solid; border-width: 10px 20px;">Click here to reset your password</a> <a href="{{.URL}}" class="btn-primary" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background: #4ccb6a; margin: 0; padding: 0; border-color: #4ccb6a; border-style: solid; border-width: 10px 20px;">Click here to reset your password</a>
</td> </td>
</tr> </tr>
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;"> <tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">

View file

@ -75,7 +75,7 @@ background-color: #f6f6f6;
</tr> </tr>
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;"> <tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">
<td class="content-block" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top"> <td class="content-block" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
<a href="{{.Url}}" class="btn-primary" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background: #4ccb6a; margin: 0; padding: 0; border-color: #4ccb6a; border-style: solid; border-width: 10px 20px;">Login to Documize</a> <a href="{{.URL}}" class="btn-primary" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background: #4ccb6a; margin: 0; padding: 0; border-color: #4ccb6a; border-style: solid; border-width: 10px 20px;">Login to Documize</a>
</td> </td>
</tr> </tr>
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;"> <tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">

View file

@ -81,7 +81,7 @@ background-color: #f6f6f6;
</tr> </tr>
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;"> <tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">
<td class="content-block" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top"> <td class="content-block" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
<a href="{{.Url}}" class="btn-primary" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background: #4ccb6a; margin: 0; padding: 0; border-color: #4ccb6a; border-style: solid; border-width: 10px 20px;">Go to Documize</a> <a href="{{.URL}}" class="btn-primary" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background: #4ccb6a; margin: 0; padding: 0; border-color: #4ccb6a; border-style: solid; border-width: 10px 20px;">Go to Documize</a>
</td> </td>
</tr> </tr>
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;"> <tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">

View file

@ -1,374 +0,0 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
/*
Elements of the software in this file were modified from github.com/jordan-wright/email and
are subject to the licence below:
The MIT License (MIT)
Copyright (c) 2013 Jordan Wright
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.
*/
// Package mail sends transactional emails.
// The code in smtp.go is designed to provide an "email interface for humans".
// Designed to be robust and flexible, the email package aims to make sending email easy without getting in the way.
package mail
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
"mime"
"mime/multipart"
"net/mail"
"net/smtp"
"net/textproto"
"os"
"path/filepath"
"strings"
"time"
)
const (
// MaxLineLength is the maximum line length per RFC 2045
MaxLineLength = 76
)
// Email is the type used for email messages
type Email struct {
From string
To []string
Bcc []string
Cc []string
Subject string
Text []byte // Plaintext message (optional)
HTML []byte // Html message (optional)
Headers textproto.MIMEHeader
Attachments []*Attachment
ReadReceipt []string
}
// NewEmail creates an Email, and returns the pointer to it.
func NewEmail() *Email {
return &Email{Headers: textproto.MIMEHeader{}}
}
// Attach is used to attach content from an io.Reader to the email.
// Required parameters include an io.Reader, the desired filename for the attachment, and the Content-Type
// The function will return the created Attachment for reference, as well as nil for the error, if successful.
func (e *Email) Attach(r io.Reader, filename string, c string) (a *Attachment, err error) {
var buffer bytes.Buffer
if _, err = io.Copy(&buffer, r); err != nil {
return
}
at := &Attachment{
Filename: filename,
Header: textproto.MIMEHeader{},
Content: buffer.Bytes(),
}
// Get the Content-Type to be used in the MIMEHeader
if c != "" {
at.Header.Set("Content-Type", c)
} else {
// If the Content-Type is blank, set the Content-Type to "application/octet-stream"
at.Header.Set("Content-Type", "application/octet-stream")
}
at.Header.Set("Content-Disposition", fmt.Sprintf("attachment;\r\n filename=\"%s\"", filename))
at.Header.Set("Content-ID", fmt.Sprintf("<%s>", filename))
at.Header.Set("Content-Transfer-Encoding", "base64")
e.Attachments = append(e.Attachments, at)
return at, nil
}
// AttachFile is used to attach content to the email.
// It attempts to open the file referenced by filename and, if successful, creates an Attachment.
// This Attachment is then appended to the slice of Email.Attachments.
// The function will then return the Attachment for reference, as well as nil for the error, if successful.
func (e *Email) AttachFile(filename string) (a *Attachment, err error) {
f, err := os.Open(filename)
if err != nil {
return
}
ct := mime.TypeByExtension(filepath.Ext(filename))
basename := filepath.Base(filename)
return e.Attach(f, basename, ct)
}
// msgHeaders merges the Email's various fields and custom headers together in a
// standards compliant way to create a MIMEHeader to be used in the resulting
// message. It does not alter e.Headers.
//
// "e"'s fields To, Cc, From, Subject will be used unless they are present in
// e.Headers. Unless set in e.Headers, "Date" will filled with the current time.
func (e *Email) msgHeaders() textproto.MIMEHeader {
res := make(textproto.MIMEHeader, len(e.Headers)+4)
if e.Headers != nil {
for _, h := range []string{"To", "Cc", "From", "Subject", "Date"} {
if v, ok := e.Headers[h]; ok {
res[h] = v
}
}
}
// Set headers if there are values.
if _, ok := res["To"]; !ok && len(e.To) > 0 {
res.Set("To", strings.Join(e.To, ", "))
}
if _, ok := res["Cc"]; !ok && len(e.Cc) > 0 {
res.Set("Cc", strings.Join(e.Cc, ", "))
}
if _, ok := res["Subject"]; !ok && e.Subject != "" {
res.Set("Subject", e.Subject)
}
// Date and From are required headers.
if _, ok := res["From"]; !ok {
res.Set("From", e.From)
}
if _, ok := res["Date"]; !ok {
res.Set("Date", time.Now().Format(time.RFC1123Z))
}
if _, ok := res["Mime-Version"]; !ok {
res.Set("Mime-Version", "1.0")
}
for field, vals := range e.Headers {
if _, ok := res[field]; !ok {
res[field] = vals
}
}
return res
}
// Bytes converts the Email object to a []byte representation, including all needed MIMEHeaders, boundaries, etc.
func (e *Email) Bytes() ([]byte, error) {
// TODO: better guess buffer size
buff := bytes.NewBuffer(make([]byte, 0, 4096))
headers := e.msgHeaders()
w := multipart.NewWriter(buff)
// TODO: determine the content type based on message/attachment mix.
headers.Set("Content-Type", "multipart/mixed;\r\n boundary="+w.Boundary())
headerToBytes(buff, headers)
io.WriteString(buff, "\r\n")
// Start the multipart/mixed part
fmt.Fprintf(buff, "--%s\r\n", w.Boundary())
header := textproto.MIMEHeader{}
// Check to see if there is a Text or HTML field
if len(e.Text) > 0 || len(e.HTML) > 0 {
subWriter := multipart.NewWriter(buff)
// Create the multipart alternative part
header.Set("Content-Type", fmt.Sprintf("multipart/alternative;\r\n boundary=%s\r\n", subWriter.Boundary()))
// Write the header
headerToBytes(buff, header)
// Create the body sections
if len(e.Text) > 0 {
header.Set("Content-Type", fmt.Sprintf("text/plain; charset=UTF-8"))
header.Set("Content-Transfer-Encoding", "quoted-printable")
if _, err := subWriter.CreatePart(header); err != nil {
return nil, err
}
// Write the text
if err := quotePrintEncode(buff, e.Text); err != nil {
return nil, err
}
}
if len(e.HTML) > 0 {
header.Set("Content-Type", fmt.Sprintf("text/html; charset=UTF-8"))
header.Set("Content-Transfer-Encoding", "quoted-printable")
if _, err := subWriter.CreatePart(header); err != nil {
return nil, err
}
// Write the text
if err := quotePrintEncode(buff, e.HTML); err != nil {
return nil, err
}
}
if err := subWriter.Close(); err != nil {
return nil, err
}
}
// Create attachment part, if necessary
for _, a := range e.Attachments {
ap, err := w.CreatePart(a.Header)
if err != nil {
return nil, err
}
// Write the base64Wrapped content to the part
base64Wrap(ap, a.Content)
}
if err := w.Close(); err != nil {
return nil, err
}
return buff.Bytes(), nil
}
// Send an email using the given host and SMTP auth (optional), returns any error thrown by smtp.SendMail
// This function merges the To, Cc, and Bcc fields and calls the smtp.SendMail function using the Email.Bytes() output as the message
func (e *Email) Send(addr string, a smtp.Auth) error {
// Merge the To, Cc, and Bcc fields
to := make([]string, 0, len(e.To)+len(e.Cc)+len(e.Bcc))
to = append(append(append(to, e.To...), e.Cc...), e.Bcc...)
for i := 0; i < len(to); i++ {
addr, err := mail.ParseAddress(to[i])
if err != nil {
return err
}
to[i] = addr.Address
}
// Check to make sure there is at least one recipient and one "From" address
if e.From == "" || len(to) == 0 {
return errors.New("Must specify at least one From address and one To address")
}
from, err := mail.ParseAddress(e.From)
if err != nil {
return err
}
raw, err := e.Bytes()
if err != nil {
return err
}
return smtpSendMail(addr, a, from.Address, to, raw)
}
var smtpSendMail = smtp.SendMail // so that it can be overloaded for testing
// Attachment is a struct representing an email attachment.
// Based on the mime/multipart.FileHeader struct, Attachment contains the name, MIMEHeader, and content of the attachment in question
type Attachment struct {
Filename string
Header textproto.MIMEHeader
Content []byte
}
// quotePrintEncode writes the quoted-printable text to the IO Writer (according to RFC 2045)
func quotePrintEncode(w io.Writer, body []byte) error {
var buf [3]byte
mc := 0
for _, c := range body {
// We're assuming Unix style text formats as input (LF line break), and
// quoted-printable uses CRLF line breaks. (Literal CRs will become
// "=0D", but probably shouldn't be there to begin with!)
if c == '\n' {
_, err := io.WriteString(w, "\r\n")
if err != nil {
return err
}
mc = 0
continue
}
var nextOut []byte
if isPrintable[c] {
buf[0] = c
nextOut = buf[:1]
} else {
nextOut = buf[:]
qpEscape(nextOut, c)
}
// Add a soft line break if the next (encoded) byte would push this line
// to or past the limit.
if mc+len(nextOut) >= MaxLineLength {
if _, err := io.WriteString(w, "=\r\n"); err != nil {
return err
}
mc = 0
}
if _, err := w.Write(nextOut); err != nil {
return err
}
mc += len(nextOut)
}
// No trailing end-of-line?? Soft line break, then. TODO: is this sane?
if mc > 0 {
_, err := io.WriteString(w, "=\r\n")
if err != nil {
return err
}
}
return nil
}
// isPrintable holds true if the byte given is "printable" according to RFC 2045, false otherwise
var isPrintable [256]bool
func init() {
for c := '!'; c <= '<'; c++ {
isPrintable[c] = true
}
for c := '>'; c <= '~'; c++ {
isPrintable[c] = true
}
isPrintable[' '] = true
isPrintable['\n'] = true
isPrintable['\t'] = true
}
// qpEscape is a helper function for quotePrintEncode which escapes a
// non-printable byte. Expects len(dest) == 3.
func qpEscape(dest []byte, c byte) {
const nums = "0123456789ABCDEF"
dest[0] = '='
dest[1] = nums[(c&0xf0)>>4]
dest[2] = nums[(c & 0xf)]
}
// base64Wrap encodes the attachment content, and wraps it according to RFC 2045 standards (every 76 chars)
// The output is then written to the specified io.Writer
func base64Wrap(w io.Writer, b []byte) {
// 57 raw bytes per 76-byte base64 line.
const maxRaw = 57
// Buffer for each line, including trailing CRLF.
buffer := make([]byte, MaxLineLength+len("\r\n"))
copy(buffer[MaxLineLength:], "\r\n")
// Process raw chunks until there's no longer enough to fill a line.
for len(b) >= maxRaw {
base64.StdEncoding.Encode(buffer, b[:maxRaw])
w.Write(buffer)
b = b[maxRaw:]
}
// Handle the last chunk of bytes.
if len(b) > 0 {
out := buffer[:base64.StdEncoding.EncodedLen(len(b))]
base64.StdEncoding.Encode(out, b)
out = append(out, "\r\n"...)
w.Write(out)
}
}
// headerToBytes renders "header" to "buff". If there are multiple values for a
// field, multiple "Field: value\r\n" lines will be emitted.
func headerToBytes(buff *bytes.Buffer, header textproto.MIMEHeader) {
for field, vals := range header {
for _, subval := range vals {
io.WriteString(buff, field+": "+subval+"\r\n")
}
}
}

View file

@ -9,114 +9,100 @@
// //
// https://documize.com // https://documize.com
// jshint ignore:start
package mail package mail
import ( import (
"bytes"
"fmt" "fmt"
"html/template"
"github.com/documize/community/server/web" "github.com/documize/community/domain/smtp"
) )
// ShareSpaceExistingUser provides an existing user with a link to a newly shared space. // ShareSpaceExistingUser provides an existing user with a link to a newly shared space.
func (m *Mailer) ShareSpaceExistingUser(recipient, inviter, url, folder, intro string) { func (m *Mailer) ShareSpaceExistingUser(recipient, inviter, url, folder, intro string) {
method := "ShareSpaceExistingUser" method := "ShareSpaceExistingUser"
m.LoadCredentials() m.Initialize()
file, err := web.ReadFile("mail/share-space-existing-user.html")
if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
return
}
emailTemplate := string(file)
// check inviter name // check inviter name
if inviter == "Hello You" || len(inviter) == 0 { if inviter == "Hello You" || len(inviter) == 0 {
inviter = "Your colleague" inviter = "Your colleague"
} }
subject := fmt.Sprintf("%s has shared %s with you", inviter, folder) em := smtp.EmailMessage{}
em.Subject = fmt.Sprintf("%s has shared %s with you", inviter, folder)
e := NewEmail() em.ToEmail = recipient
e.From = m.Credentials.SMTPsender em.ToName = recipient
e.To = []string{recipient}
e.Subject = subject
parameters := struct { parameters := struct {
Subject string Subject string
Inviter string Inviter string
Url string URL string
Folder string Folder string
Intro string Intro string
}{ }{
subject, em.Subject,
inviter, inviter,
url, url,
folder, folder,
intro, intro,
} }
buffer := new(bytes.Buffer) html, err := m.ParseTemplate("mail/share-space-existing-user.html", parameters)
t := template.Must(template.New("emailTemplate").Parse(emailTemplate)) if err != nil {
t.Execute(buffer, &parameters) m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
e.HTML = buffer.Bytes() return
}
em.BodyHTML = html
err = e.Send(m.GetHost(), m.GetAuth()) ok, err := smtp.SendMessage(m.Dialer, m.Config, em)
if err != nil { if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err) m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err)
} }
if !ok {
m.Runtime.Log.Info(fmt.Sprintf("%s unable to send email"))
}
} }
// ShareSpaceNewUser invites new user providing Credentials, explaining the product and stating who is inviting them. // ShareSpaceNewUser invites new user providing Credentials, explaining the product and stating who is inviting them.
func (m *Mailer) ShareSpaceNewUser(recipient, inviter, url, space, invitationMessage string) { func (m *Mailer) ShareSpaceNewUser(recipient, inviter, url, space, invitationMessage string) {
method := "ShareSpaceNewUser" method := "ShareSpaceNewUser"
m.LoadCredentials() m.Initialize()
file, err := web.ReadFile("mail/share-space-new-user.html")
if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
return
}
emailTemplate := string(file)
// check inviter name // check inviter name
if inviter == "Hello You" || len(inviter) == 0 { if inviter == "Hello You" || len(inviter) == 0 {
inviter = "Your colleague" inviter = "Your colleague"
} }
subject := fmt.Sprintf("%s has shared %s with you on Documize", inviter, space) em := smtp.EmailMessage{}
em.Subject = fmt.Sprintf("%s has shared %s with you on Documize", inviter, space)
e := NewEmail() em.ToEmail = recipient
e.From = m.Credentials.SMTPsender em.ToName = recipient
e.To = []string{recipient}
e.Subject = subject
parameters := struct { parameters := struct {
Subject string Subject string
Inviter string Inviter string
Url string URL string
Invitation string Invitation string
Folder string Folder string
}{ }{
subject, em.Subject,
inviter, inviter,
url, url,
invitationMessage, invitationMessage,
space, space,
} }
buffer := new(bytes.Buffer) html, err := m.ParseTemplate("mail/share-space-new-user.html", parameters)
t := template.Must(template.New("emailTemplate").Parse(emailTemplate)) if err != nil {
t.Execute(buffer, &parameters) m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
e.HTML = buffer.Bytes() return
}
em.BodyHTML = html
err = e.Send(m.GetHost(), m.GetAuth()) ok, err := smtp.SendMessage(m.Dialer, m.Config, em)
if err != nil { if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err) m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err)
} }
if !ok {
m.Runtime.Log.Info(fmt.Sprintf("%s unable to send email"))
}
} }

View file

@ -9,149 +9,130 @@
// //
// https://documize.com // https://documize.com
// jshint ignore:start
package mail package mail
import ( import (
"bytes"
"fmt" "fmt"
"html/template"
"github.com/documize/community/server/web" "github.com/documize/community/domain/smtp"
) )
// InviteNewUser invites someone new providing credentials, explaining the product and stating who is inviting them. // InviteNewUser invites someone new providing credentials, explaining the product and stating who is inviting them.
func (m *Mailer) InviteNewUser(recipient, inviter, url, username, password string) { func (m *Mailer) InviteNewUser(recipient, inviter, url, username, password string) {
method := "InviteNewUser" method := "InviteNewUser"
m.LoadCredentials() m.Initialize()
file, err := web.ReadFile("mail/invite-new-user.html")
if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
return
}
emailTemplate := string(file)
// check inviter name // check inviter name
if inviter == "Hello You" || len(inviter) == 0 { if inviter == "Hello You" || len(inviter) == 0 {
inviter = "Your colleague" inviter = "Your colleague"
} }
subject := fmt.Sprintf("%s has invited you to Documize", inviter) em := smtp.EmailMessage{}
em.Subject = fmt.Sprintf("%s has invited you to Documize", inviter)
e := NewEmail() em.ToEmail = recipient
e.From = m.Credentials.SMTPsender em.ToName = recipient
e.To = []string{recipient}
e.Subject = subject
parameters := struct { parameters := struct {
Subject string Subject string
Inviter string Inviter string
Url string URL string
Username string Username string
Password string Password string
}{ }{
subject, em.Subject,
inviter, inviter,
url, url,
recipient, recipient,
password, password,
} }
buffer := new(bytes.Buffer) html, err := m.ParseTemplate("mail/invite-new-user.html", parameters)
t := template.Must(template.New("emailTemplate").Parse(emailTemplate)) if err != nil {
t.Execute(buffer, &parameters) m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
e.HTML = buffer.Bytes() return
}
em.BodyHTML = html
err = e.Send(m.GetHost(), m.GetAuth()) ok, err := smtp.SendMessage(m.Dialer, m.Config, em)
if err != nil { if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err) m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err)
} }
if !ok {
m.Runtime.Log.Info(fmt.Sprintf("%s unable to send email"))
}
} }
// InviteExistingUser invites a known user to an organization. // InviteExistingUser invites a known user to an organization.
func (m *Mailer) InviteExistingUser(recipient, inviter, url string) { func (m *Mailer) InviteExistingUser(recipient, inviter, url string) {
method := "InviteExistingUser" method := "InviteExistingUser"
m.LoadCredentials() m.Initialize()
file, err := web.ReadFile("mail/invite-existing-user.html")
if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
return
}
emailTemplate := string(file)
// check inviter name // check inviter name
if inviter == "Hello You" || len(inviter) == 0 { if inviter == "Hello You" || len(inviter) == 0 {
inviter = "Your colleague" inviter = "Your colleague"
} }
subject := fmt.Sprintf("%s has invited you to their Documize account", inviter) em := smtp.EmailMessage{}
em.Subject = fmt.Sprintf("%s has invited you to their Documize account", inviter)
e := NewEmail() em.ToEmail = recipient
e.From = m.Credentials.SMTPsender em.ToName = recipient
e.To = []string{recipient}
e.Subject = subject
parameters := struct { parameters := struct {
Subject string Subject string
Inviter string Inviter string
Url string URL string
}{ }{
subject, em.Subject,
inviter, inviter,
url, url,
} }
buffer := new(bytes.Buffer) html, err := m.ParseTemplate("mail/invite-existing-user.html", parameters)
t := template.Must(template.New("emailTemplate").Parse(emailTemplate)) if err != nil {
t.Execute(buffer, &parameters) m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
e.HTML = buffer.Bytes() return
}
em.BodyHTML = html
err = e.Send(m.GetHost(), m.GetAuth()) ok, err := smtp.SendMessage(m.Dialer, m.Config, em)
if err != nil { if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err) m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err)
} }
if !ok {
m.Runtime.Log.Info(fmt.Sprintf("%s unable to send email"))
}
} }
// PasswordReset sends a reset email with an embedded token. // PasswordReset sends a reset email with an embedded token.
func (m *Mailer) PasswordReset(recipient, url string) { func (m *Mailer) PasswordReset(recipient, url string) {
method := "PasswordReset" method := "PasswordReset"
m.LoadCredentials() m.Initialize()
file, err := web.ReadFile("mail/password-reset.html") em := smtp.EmailMessage{}
em.Subject = "Documize password reset request"
em.ToEmail = recipient
em.ToName = recipient
parameters := struct {
Subject string
URL string
}{
em.Subject,
url,
}
html, err := m.ParseTemplate("mail/password-reset.html", parameters)
if err != nil { if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err) m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
return return
} }
em.BodyHTML = html
emailTemplate := string(file) ok, err := smtp.SendMessage(m.Dialer, m.Config, em)
subject := "Documize password reset request"
e := NewEmail()
e.From = m.Credentials.SMTPsender //e.g. "Documize <hello@documize.com>"
e.To = []string{recipient}
e.Subject = subject
parameters := struct {
Subject string
Url string
}{
subject,
url,
}
buffer := new(bytes.Buffer)
t := template.Must(template.New("emailTemplate").Parse(emailTemplate))
t.Execute(buffer, &parameters)
e.HTML = buffer.Bytes()
err = e.Send(m.GetHost(), m.GetAuth())
if err != nil { if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err) m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err)
} }
if !ok {
m.Runtime.Log.Info(fmt.Sprintf("%s unable to send email"))
}
} }

View file

@ -15,6 +15,7 @@ package setting
import ( import (
"encoding/json" "encoding/json"
"encoding/xml" "encoding/xml"
"fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
@ -22,6 +23,7 @@ import (
"github.com/documize/community/core/event" "github.com/documize/community/core/event"
"github.com/documize/community/core/response" "github.com/documize/community/core/response"
"github.com/documize/community/domain" "github.com/documize/community/domain"
"github.com/documize/community/domain/smtp"
"github.com/documize/community/model/audit" "github.com/documize/community/model/audit"
) )
@ -91,7 +93,37 @@ func (h *Handler) SetSMTP(w http.ResponseWriter, r *http.Request) {
h.Store.Audit.Record(ctx, audit.EventTypeSystemSMTP) h.Store.Audit.Record(ctx, audit.EventTypeSystemSMTP)
response.WriteEmpty(w) // test connection
var result struct {
Success bool `json:"success"`
Message string `json:"message"`
}
result.Message = "Email sent successfully!"
u, err := h.Store.User.Get(ctx, ctx.UserID)
if err != nil {
result.Success = false
result.Message = err.Error()
h.Runtime.Log.Error(method, err)
response.WriteJSON(w, result)
return
}
cfg := GetSMTPConfig(h.Store)
dialer, err := smtp.Connect(cfg)
em := smtp.EmailMessage{}
em.Subject = "Documize SMTP Test"
em.BodyHTML = "<p>This is a test email from Documize using current SMTP settings.</p>"
em.ToEmail = u.Email
em.ToName = u.Fullname()
result.Success, err = smtp.SendMessage(dialer, cfg, em)
if !result.Success {
result.Message = fmt.Sprintf("Unable to send test email: %s", err.Error())
}
response.WriteJSON(w, result)
} }
// License returns product license // License returns product license

60
domain/setting/smtp.go Normal file
View file

@ -0,0 +1,60 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
// Package setting manages both global and user level settings
package setting
import (
"strconv"
"github.com/documize/community/domain"
"github.com/documize/community/domain/smtp"
)
// GetSMTPConfig returns SMTP configuration.
func GetSMTPConfig(s *domain.Store) (c smtp.Config) {
c = smtp.Config{}
// server
c.Host, _ = s.Setting.Get("SMTP", "host")
port, _ := s.Setting.Get("SMTP", "port")
c.Port, _ = strconv.Atoi(port)
// credentials
c.Username, _ = s.Setting.Get("SMTP", "userid")
c.Password, _ = s.Setting.Get("SMTP", "password")
// sender
c.SenderEmail, _ = s.Setting.Get("SMTP", "sender")
c.SenderName, _ = s.Setting.Get("SMTP", "senderName")
if c.SenderName == "" {
c.SenderName = "Documize"
}
// anon auth?
anon, _ := s.Setting.Get("SMTP", "anonymous")
c.AnonymousAuth, _ = strconv.ParseBool(anon)
// base64 encode creds?
b64, _ := s.Setting.Get("SMTP", "base64creds")
c.Base64EncodeCredentials, _ = strconv.ParseBool(b64)
// SSL?
ssl, _ := s.Setting.Get("SMTP", "usessl")
c.UseSSL, _ = strconv.ParseBool(ssl)
// verify SSL?
verifySSL, _ := s.Setting.Get("SMTP", "verifyssl")
c.SkipSSLVerify, _ = strconv.ParseBool(verifySSL)
c.SkipSSLVerify = true
return
}

114
domain/smtp/send.go Normal file
View file

@ -0,0 +1,114 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
// Package smtp provides access to SMTP server for sending email.
package smtp
import (
"crypto/tls"
"encoding/base64"
"strings"
"github.com/documize/community/core/mail"
)
// Config is used to create SMTP server connection
type Config struct {
// IP/hostname of SMTP server
Host string
// Port number of SMTP server
Port int
// Username for authentication with SMTP server
Username string
// Password for authentication with SMTP server
Password string
// SenderEmail is FROM address
SenderEmail string
// SenderName is FROM display name
SenderName string
// AnonymousAuth does not send username/password to server
AnonymousAuth bool
// Base64EncodeCredentials encodes User and Password as base64 before sending to SMTP server
Base64EncodeCredentials bool
// UseSSL uses SMTP SSL connection with SMTP server
UseSSL bool
// SkipSSLVerify allows unverified certificates
SkipSSLVerify bool
}
// Connect returns open connection to server for sending email
func Connect(c Config) (d *mail.Dialer, err error) {
// prepare credentials
u := strings.TrimSpace(c.Username)
p := strings.TrimSpace(c.Password)
// anonymous, no credentials
if c.AnonymousAuth {
u = ""
p = ""
}
// base64 encode if required
if c.Base64EncodeCredentials {
u = base64.StdEncoding.EncodeToString([]byte(u))
p = base64.StdEncoding.EncodeToString([]byte(p))
}
// basic server
d = mail.NewDialer(c.Host, c.Port, u, p)
// use SSL
d.SSL = c.UseSSL
// verify SSL cert chain
d.TLSConfig = &tls.Config{InsecureSkipVerify: c.SkipSSLVerify}
// TLS mode
d.StartTLSPolicy = mail.OpportunisticStartTLS
return d, nil
}
// EmailMessage represents email to be sent.
type EmailMessage struct {
ToEmail string
ToName string
Subject string
BodyHTML string
}
// SendMessage sends email using specified SMTP connection
func SendMessage(d *mail.Dialer, c Config, em EmailMessage) (b bool, err error) {
m := mail.NewMessage()
// participants
m.SetHeader("From", m.FormatAddress(c.SenderEmail, c.SenderName))
m.SetHeader("To", m.FormatAddress(em.ToEmail, em.ToName))
// content
m.SetHeader("Subject", em.Subject)
m.SetBody("text/html", em.BodyHTML)
// send email
if err = d.DialAndSend(m); err != nil {
return false, err
}
return true, nil
}

File diff suppressed because one or more lines are too long

View file

@ -17,8 +17,10 @@ export default Component.extend({
SMTPHostEmptyError: empty('model.smtp.host'), SMTPHostEmptyError: empty('model.smtp.host'),
SMTPPortEmptyError: empty('model.smtp.port'), SMTPPortEmptyError: empty('model.smtp.port'),
SMTPSenderEmptyError: empty('model.smtp.sender'), SMTPSenderEmptyError: empty('model.smtp.sender'),
SMTPUserIdEmptyError: empty('model.smtp.userid'), senderNameError: empty('model.smtp.senderName'),
SMTPPasswordEmptyError: empty('model.smtp.password'),
buttonText: 'Save & Test',
testSMTP: null,
actions: { actions: {
saveSMTP() { saveSMTP() {
@ -34,16 +36,22 @@ export default Component.extend({
$("#smtp-sender").focus(); $("#smtp-sender").focus();
return; return;
} }
if (this.get('SMTPUserIdEmptyError')) { if (this.get('senderNameError')) {
$("#smtp-userid").focus(); $("#smtp-senderName").focus();
return;
}
if (this.get('SMTPPasswordEmptyError')) {
$("#smtp-password").focus();
return; return;
} }
this.get('saveSMTP')().then(() => { this.set('testSMTP', {
success: true,
message: ''
},
);
this.set('buttonText', 'Please wait...');
this.get('saveSMTP')().then((result) => {
this.set('buttonText', 'Save & Test');
this.set('testSMTP', result);
}); });
} }
} }

View file

@ -18,7 +18,8 @@ export default Controller.extend({
actions: { actions: {
saveSMTP() { saveSMTP() {
if(this.get('session.isGlobalAdmin')) { if(this.get('session.isGlobalAdmin')) {
return this.get('global').saveSMTPConfig(this.model.smtp).then(() => { return this.get('global').saveSMTPConfig(this.model.smtp).then((response) => {
return response;
}); });
} }
} }

View file

@ -124,4 +124,16 @@
} }
} }
} }
> .smtp-failure {
font-size: 1.2rem;
font-weight: bold;
color: $color-red;
}
> .smtp-success {
font-size: 1.2rem;
font-weight: bold;
color: $color-green;
}
} }

View file

@ -10,40 +10,82 @@
<div class="view-customize"> <div class="view-customize">
<form class="mt-5"> <form class="mt-5">
<div class="form-group row"> <div class="form-group row">
<label for="smtp-host" class="col-sm-2 col-form-label">Host</label> <label for="smtp-host" class="col-sm-4 col-form-label">Host</label>
<div class="col-sm-10"> <div class="col-sm-8">
{{focus-input id="smtp-host" type="text" value=model.smtp.host class=(if SMTPHostEmptyError 'form-control is-invalid' 'form-control')}} {{focus-input id="smtp-host" type="text" value=model.smtp.host class=(if SMTPHostEmptyError 'form-control is-invalid' 'form-control')}}
<small class="form-text text-muted">e.g. my.host.com</small> <small class="form-text text-muted">e.g. my.host.com</small>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="smtp-port" class="col-sm-2 col-form-label">Port</label> <label for="smtp-port" class="col-sm-4 col-form-label">Port</label>
<div class="col-sm-10"> <div class="col-sm-8">
{{input id="smtp-port" type="text" value=model.smtp.port class=(if SMTPPortEmptyError 'form-control is-invalid' 'form-control')}} {{input id="smtp-port" type="text" value=model.smtp.port class=(if SMTPPortEmptyError 'form-control is-invalid' 'form-control')}}
<small class="form-text text-muted">e.g. 587</small> <small class="form-text text-muted">e.g. 587</small>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="smtp-sender" class="col-sm-2 col-form-label">Sender</label> <label for="smtp-userid" class="col-sm-4 col-form-label">Username</label>
<div class="col-sm-10"> <div class="col-sm-8">
{{input id="smtp-sender" type="text" value=model.smtp.sender class=(if SMTPSenderEmptyError 'form-control is-invalid' 'form-control')}} {{input id="smtp-userid" type="text" value=model.smtp.userid class='form-control'}}
<small class="form-text text-muted">e.g. user@some-domain.com</small>
</div>
</div>
<div class="form-group row">
<label for="smtp-userid" class="col-sm-2 col-form-label">Username</label>
<div class="col-sm-10">
{{input id="smtp-userid" type="text" value=model.smtp.userid class=(if SMTPUserIdEmptyError 'form-control is-invalid' 'form-control')}}
<small class="form-text text-muted">e.g. Login username for SMTP server</small> <small class="form-text text-muted">e.g. Login username for SMTP server</small>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="smtp-password" class="col-sm-2 col-form-label">Password</label> <label for="smtp-password" class="col-sm-4 col-form-label">Password</label>
<div class="col-sm-10"> <div class="col-sm-8">
{{input id="smtp-password" type="password" value=model.smtp.password class=(if SMTPPasswordEmptyError 'form-control is-invalid' 'form-control')}} {{input id="smtp-password" type="password" value=model.smtp.password class='form-control'}}
<small class="form-text text-muted">e.g. Login password for SMTP server</small> <small class="form-text text-muted">e.g. Login password for SMTP server</small>
</div> </div>
</div> </div>
<div class="btn btn-success mt-4" {{action 'saveSMTP'}}>Save</div>
</form> <div class="form-group row">
<label for="smtp-sender" class="col-sm-4 col-form-label">Sender Email</label>
<div class="col-sm-8">
{{input id="smtp-sender" type="email" value=model.smtp.sender class=(if SMTPSenderEmptyError 'form-control is-invalid' 'form-control')}}
<small class="form-text text-muted">e.g. user@some-domain.com</small>
</div>
</div>
<div class="form-group row">
<label for="smtp-senderName" class="col-sm-4 col-form-label">Sender Name</label>
<div class="col-sm-8">
{{input id="smtp-senderName" type="text" value=model.smtp.senderName class=(if senderNameError 'form-control is-invalid' 'form-control')}}
<small class="form-text text-muted">e.g. Documize</small>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Anonymous</label>
<div class="col-sm-8">
<div class="form-check">
{{input id="smtp-anonymous" type="checkbox" checked=model.smtp.anonymous class='form-check-input'}}
<label class="form-check-label" for="smtp-anonymous">Anonymous authentication, ignore Username and Password fields</label>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Base64</label>
<div class="col-sm-8">
<div class="form-check">
{{input id="smtp-base64creds" type="checkbox" checked=model.smtp.base64creds class='form-check-input'}}
<label class="form-check-label" for="smtp-base64creds">Base64 encode Username and Password fields</label>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">SSL</label>
<div class="col-sm-8">
<div class="form-check">
{{input id="smtp-usessl" type="checkbox" checked=model.smtp.usessl class='form-check-input'}}
<label class="form-check-label" for="smtp-usessl">Use SSL</label>
</div>
</div>
</div>
<div class="btn btn-success mt-4" {{action 'saveSMTP'}}>{{buttonText}}</div>
</form>
{{#if testSMTP.success}}
<p class="smtp-success my-3">{{testSMTP.message}}</p>
{{else}}
<p class="smtp-failure my-3">{{testSMTP.message}}</p>
{{/if}}
</div> </div>

20
vendor/gopkg.in/alexcesaro/quotedprintable.v3/LICENSE generated vendored Normal file
View file

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2014 Alexandre Cesaro
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.

View file

@ -0,0 +1,16 @@
# quotedprintable
## Introduction
Package quotedprintable implements quoted-printable and message header encoding
as specified by RFC 2045 and RFC 2047.
It is a copy of the Go 1.5 package `mime/quotedprintable`. It also includes
the new functions of package `mime` concerning RFC 2047.
This code has minor changes with the standard library code in order to work
with Go 1.0 and newer.
## Documentation
https://godoc.org/gopkg.in/alexcesaro/quotedprintable.v3

View file

@ -0,0 +1,279 @@
package quotedprintable
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
"strings"
"unicode"
"unicode/utf8"
)
// A WordEncoder is a RFC 2047 encoded-word encoder.
type WordEncoder byte
const (
// BEncoding represents Base64 encoding scheme as defined by RFC 2045.
BEncoding = WordEncoder('b')
// QEncoding represents the Q-encoding scheme as defined by RFC 2047.
QEncoding = WordEncoder('q')
)
var (
errInvalidWord = errors.New("mime: invalid RFC 2047 encoded-word")
)
// Encode returns the encoded-word form of s. If s is ASCII without special
// characters, it is returned unchanged. The provided charset is the IANA
// charset name of s. It is case insensitive.
func (e WordEncoder) Encode(charset, s string) string {
if !needsEncoding(s) {
return s
}
return e.encodeWord(charset, s)
}
func needsEncoding(s string) bool {
for _, b := range s {
if (b < ' ' || b > '~') && b != '\t' {
return true
}
}
return false
}
// encodeWord encodes a string into an encoded-word.
func (e WordEncoder) encodeWord(charset, s string) string {
buf := getBuffer()
defer putBuffer(buf)
buf.WriteString("=?")
buf.WriteString(charset)
buf.WriteByte('?')
buf.WriteByte(byte(e))
buf.WriteByte('?')
if e == BEncoding {
w := base64.NewEncoder(base64.StdEncoding, buf)
io.WriteString(w, s)
w.Close()
} else {
enc := make([]byte, 3)
for i := 0; i < len(s); i++ {
b := s[i]
switch {
case b == ' ':
buf.WriteByte('_')
case b <= '~' && b >= '!' && b != '=' && b != '?' && b != '_':
buf.WriteByte(b)
default:
enc[0] = '='
enc[1] = upperhex[b>>4]
enc[2] = upperhex[b&0x0f]
buf.Write(enc)
}
}
}
buf.WriteString("?=")
return buf.String()
}
const upperhex = "0123456789ABCDEF"
// A WordDecoder decodes MIME headers containing RFC 2047 encoded-words.
type WordDecoder struct {
// CharsetReader, if non-nil, defines a function to generate
// charset-conversion readers, converting from the provided
// charset into UTF-8.
// Charsets are always lower-case. utf-8, iso-8859-1 and us-ascii charsets
// are handled by default.
// One of the the CharsetReader's result values must be non-nil.
CharsetReader func(charset string, input io.Reader) (io.Reader, error)
}
// Decode decodes an encoded-word. If word is not a valid RFC 2047 encoded-word,
// word is returned unchanged.
func (d *WordDecoder) Decode(word string) (string, error) {
fields := strings.Split(word, "?") // TODO: remove allocation?
if len(fields) != 5 || fields[0] != "=" || fields[4] != "=" || len(fields[2]) != 1 {
return "", errInvalidWord
}
content, err := decode(fields[2][0], fields[3])
if err != nil {
return "", err
}
buf := getBuffer()
defer putBuffer(buf)
if err := d.convert(buf, fields[1], content); err != nil {
return "", err
}
return buf.String(), nil
}
// DecodeHeader decodes all encoded-words of the given string. It returns an
// error if and only if CharsetReader of d returns an error.
func (d *WordDecoder) DecodeHeader(header string) (string, error) {
// If there is no encoded-word, returns before creating a buffer.
i := strings.Index(header, "=?")
if i == -1 {
return header, nil
}
buf := getBuffer()
defer putBuffer(buf)
buf.WriteString(header[:i])
header = header[i:]
betweenWords := false
for {
start := strings.Index(header, "=?")
if start == -1 {
break
}
cur := start + len("=?")
i := strings.Index(header[cur:], "?")
if i == -1 {
break
}
charset := header[cur : cur+i]
cur += i + len("?")
if len(header) < cur+len("Q??=") {
break
}
encoding := header[cur]
cur++
if header[cur] != '?' {
break
}
cur++
j := strings.Index(header[cur:], "?=")
if j == -1 {
break
}
text := header[cur : cur+j]
end := cur + j + len("?=")
content, err := decode(encoding, text)
if err != nil {
betweenWords = false
buf.WriteString(header[:start+2])
header = header[start+2:]
continue
}
// Write characters before the encoded-word. White-space and newline
// characters separating two encoded-words must be deleted.
if start > 0 && (!betweenWords || hasNonWhitespace(header[:start])) {
buf.WriteString(header[:start])
}
if err := d.convert(buf, charset, content); err != nil {
return "", err
}
header = header[end:]
betweenWords = true
}
if len(header) > 0 {
buf.WriteString(header)
}
return buf.String(), nil
}
func decode(encoding byte, text string) ([]byte, error) {
switch encoding {
case 'B', 'b':
return base64.StdEncoding.DecodeString(text)
case 'Q', 'q':
return qDecode(text)
}
return nil, errInvalidWord
}
func (d *WordDecoder) convert(buf *bytes.Buffer, charset string, content []byte) error {
switch {
case strings.EqualFold("utf-8", charset):
buf.Write(content)
case strings.EqualFold("iso-8859-1", charset):
for _, c := range content {
buf.WriteRune(rune(c))
}
case strings.EqualFold("us-ascii", charset):
for _, c := range content {
if c >= utf8.RuneSelf {
buf.WriteRune(unicode.ReplacementChar)
} else {
buf.WriteByte(c)
}
}
default:
if d.CharsetReader == nil {
return fmt.Errorf("mime: unhandled charset %q", charset)
}
r, err := d.CharsetReader(strings.ToLower(charset), bytes.NewReader(content))
if err != nil {
return err
}
if _, err = buf.ReadFrom(r); err != nil {
return err
}
}
return nil
}
// hasNonWhitespace reports whether s (assumed to be ASCII) contains at least
// one byte of non-whitespace.
func hasNonWhitespace(s string) bool {
for _, b := range s {
switch b {
// Encoded-words can only be separated by linear white spaces which does
// not include vertical tabs (\v).
case ' ', '\t', '\n', '\r':
default:
return true
}
}
return false
}
// qDecode decodes a Q encoded string.
func qDecode(s string) ([]byte, error) {
dec := make([]byte, len(s))
n := 0
for i := 0; i < len(s); i++ {
switch c := s[i]; {
case c == '_':
dec[n] = ' '
case c == '=':
if i+2 >= len(s) {
return nil, errInvalidWord
}
b, err := readHexByte(s[i+1], s[i+2])
if err != nil {
return nil, err
}
dec[n] = b
i += 2
case (c <= '~' && c >= ' ') || c == '\n' || c == '\r' || c == '\t':
dec[n] = c
default:
return nil, errInvalidWord
}
n++
}
return dec[:n], nil
}

26
vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool.go generated vendored Normal file
View file

@ -0,0 +1,26 @@
// +build go1.3
package quotedprintable
import (
"bytes"
"sync"
)
var bufPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func getBuffer() *bytes.Buffer {
return bufPool.Get().(*bytes.Buffer)
}
func putBuffer(buf *bytes.Buffer) {
if buf.Len() > 1024 {
return
}
buf.Reset()
bufPool.Put(buf)
}

View file

@ -0,0 +1,24 @@
// +build !go1.3
package quotedprintable
import "bytes"
var ch = make(chan *bytes.Buffer, 32)
func getBuffer() *bytes.Buffer {
select {
case buf := <-ch:
return buf
default:
}
return new(bytes.Buffer)
}
func putBuffer(buf *bytes.Buffer) {
buf.Reset()
select {
case ch <- buf:
default:
}
}

121
vendor/gopkg.in/alexcesaro/quotedprintable.v3/reader.go generated vendored Normal file
View file

@ -0,0 +1,121 @@
// Package quotedprintable implements quoted-printable encoding as specified by
// RFC 2045.
package quotedprintable
import (
"bufio"
"bytes"
"fmt"
"io"
)
// Reader is a quoted-printable decoder.
type Reader struct {
br *bufio.Reader
rerr error // last read error
line []byte // to be consumed before more of br
}
// NewReader returns a quoted-printable reader, decoding from r.
func NewReader(r io.Reader) *Reader {
return &Reader{
br: bufio.NewReader(r),
}
}
func fromHex(b byte) (byte, error) {
switch {
case b >= '0' && b <= '9':
return b - '0', nil
case b >= 'A' && b <= 'F':
return b - 'A' + 10, nil
// Accept badly encoded bytes.
case b >= 'a' && b <= 'f':
return b - 'a' + 10, nil
}
return 0, fmt.Errorf("quotedprintable: invalid hex byte 0x%02x", b)
}
func readHexByte(a, b byte) (byte, error) {
var hb, lb byte
var err error
if hb, err = fromHex(a); err != nil {
return 0, err
}
if lb, err = fromHex(b); err != nil {
return 0, err
}
return hb<<4 | lb, nil
}
func isQPDiscardWhitespace(r rune) bool {
switch r {
case '\n', '\r', ' ', '\t':
return true
}
return false
}
var (
crlf = []byte("\r\n")
lf = []byte("\n")
softSuffix = []byte("=")
)
// Read reads and decodes quoted-printable data from the underlying reader.
func (r *Reader) Read(p []byte) (n int, err error) {
// Deviations from RFC 2045:
// 1. in addition to "=\r\n", "=\n" is also treated as soft line break.
// 2. it will pass through a '\r' or '\n' not preceded by '=', consistent
// with other broken QP encoders & decoders.
for len(p) > 0 {
if len(r.line) == 0 {
if r.rerr != nil {
return n, r.rerr
}
r.line, r.rerr = r.br.ReadSlice('\n')
// Does the line end in CRLF instead of just LF?
hasLF := bytes.HasSuffix(r.line, lf)
hasCR := bytes.HasSuffix(r.line, crlf)
wholeLine := r.line
r.line = bytes.TrimRightFunc(wholeLine, isQPDiscardWhitespace)
if bytes.HasSuffix(r.line, softSuffix) {
rightStripped := wholeLine[len(r.line):]
r.line = r.line[:len(r.line)-1]
if !bytes.HasPrefix(rightStripped, lf) && !bytes.HasPrefix(rightStripped, crlf) {
r.rerr = fmt.Errorf("quotedprintable: invalid bytes after =: %q", rightStripped)
}
} else if hasLF {
if hasCR {
r.line = append(r.line, '\r', '\n')
} else {
r.line = append(r.line, '\n')
}
}
continue
}
b := r.line[0]
switch {
case b == '=':
if len(r.line[1:]) < 2 {
return n, io.ErrUnexpectedEOF
}
b, err = readHexByte(r.line[1], r.line[2])
if err != nil {
return n, err
}
r.line = r.line[2:] // 2 of the 3; other 1 is done below
case b == '\t' || b == '\r' || b == '\n':
break
case b < ' ' || b > '~':
return n, fmt.Errorf("quotedprintable: invalid unescaped byte 0x%02x in body", b)
}
p[0] = b
p = p[1:]
r.line = r.line[1:]
n++
}
return n, nil
}

166
vendor/gopkg.in/alexcesaro/quotedprintable.v3/writer.go generated vendored Normal file
View file

@ -0,0 +1,166 @@
package quotedprintable
import "io"
const lineMaxLen = 76
// A Writer is a quoted-printable writer that implements io.WriteCloser.
type Writer struct {
// Binary mode treats the writer's input as pure binary and processes end of
// line bytes as binary data.
Binary bool
w io.Writer
i int
line [78]byte
cr bool
}
// NewWriter returns a new Writer that writes to w.
func NewWriter(w io.Writer) *Writer {
return &Writer{w: w}
}
// Write encodes p using quoted-printable encoding and writes it to the
// underlying io.Writer. It limits line length to 76 characters. The encoded
// bytes are not necessarily flushed until the Writer is closed.
func (w *Writer) Write(p []byte) (n int, err error) {
for i, b := range p {
switch {
// Simple writes are done in batch.
case b >= '!' && b <= '~' && b != '=':
continue
case isWhitespace(b) || !w.Binary && (b == '\n' || b == '\r'):
continue
}
if i > n {
if err := w.write(p[n:i]); err != nil {
return n, err
}
n = i
}
if err := w.encode(b); err != nil {
return n, err
}
n++
}
if n == len(p) {
return n, nil
}
if err := w.write(p[n:]); err != nil {
return n, err
}
return len(p), nil
}
// Close closes the Writer, flushing any unwritten data to the underlying
// io.Writer, but does not close the underlying io.Writer.
func (w *Writer) Close() error {
if err := w.checkLastByte(); err != nil {
return err
}
return w.flush()
}
// write limits text encoded in quoted-printable to 76 characters per line.
func (w *Writer) write(p []byte) error {
for _, b := range p {
if b == '\n' || b == '\r' {
// If the previous byte was \r, the CRLF has already been inserted.
if w.cr && b == '\n' {
w.cr = false
continue
}
if b == '\r' {
w.cr = true
}
if err := w.checkLastByte(); err != nil {
return err
}
if err := w.insertCRLF(); err != nil {
return err
}
continue
}
if w.i == lineMaxLen-1 {
if err := w.insertSoftLineBreak(); err != nil {
return err
}
}
w.line[w.i] = b
w.i++
w.cr = false
}
return nil
}
func (w *Writer) encode(b byte) error {
if lineMaxLen-1-w.i < 3 {
if err := w.insertSoftLineBreak(); err != nil {
return err
}
}
w.line[w.i] = '='
w.line[w.i+1] = upperhex[b>>4]
w.line[w.i+2] = upperhex[b&0x0f]
w.i += 3
return nil
}
// checkLastByte encodes the last buffered byte if it is a space or a tab.
func (w *Writer) checkLastByte() error {
if w.i == 0 {
return nil
}
b := w.line[w.i-1]
if isWhitespace(b) {
w.i--
if err := w.encode(b); err != nil {
return err
}
}
return nil
}
func (w *Writer) insertSoftLineBreak() error {
w.line[w.i] = '='
w.i++
return w.insertCRLF()
}
func (w *Writer) insertCRLF() error {
w.line[w.i] = '\r'
w.line[w.i+1] = '\n'
w.i += 2
return w.flush()
}
func (w *Writer) flush() error {
if _, err := w.w.Write(w.line[:w.i]); err != nil {
return err
}
w.i = 0
return nil
}
func isWhitespace(b byte) bool {
return b == ' ' || b == '\t'
}