aerc

Working clone of aerc-mail.org
git clone git://git.danielmoch.com/aerc.git
Log | Files | Refs | README | LICENSE

send.go (4953B)


      1 package compose
      2 
      3 import (
      4 	"crypto/tls"
      5 	"fmt"
      6 	"io"
      7 	"net/mail"
      8 	"net/url"
      9 	"strings"
     10 	"time"
     11 
     12 	"github.com/emersion/go-sasl"
     13 	"github.com/emersion/go-smtp"
     14 	"github.com/gdamore/tcell"
     15 	"github.com/miolini/datacounter"
     16 	"github.com/pkg/errors"
     17 
     18 	"git.sr.ht/~sircmpwn/aerc/widgets"
     19 	"git.sr.ht/~sircmpwn/aerc/worker/types"
     20 )
     21 
     22 func init() {
     23 	register("send", SendMessage)
     24 }
     25 
     26 func SendMessage(aerc *widgets.Aerc, args []string) error {
     27 	if len(args) > 1 {
     28 		return errors.New("Usage: send-message")
     29 	}
     30 	composer, _ := aerc.SelectedTab().(*widgets.Composer)
     31 	config := composer.Config()
     32 
     33 	if config.Outgoing == "" {
     34 		return errors.New(
     35 			"No outgoing mail transport configured for this account")
     36 	}
     37 
     38 	uri, err := url.Parse(config.Outgoing)
     39 	if err != nil {
     40 		return errors.Wrap(err, "url.Parse(outgoing)")
     41 	}
     42 	var (
     43 		scheme string
     44 		auth   string = "plain"
     45 	)
     46 	parts := strings.Split(uri.Scheme, "+")
     47 	if len(parts) == 1 {
     48 		scheme = parts[0]
     49 	} else if len(parts) == 2 {
     50 		scheme = parts[0]
     51 		auth = parts[1]
     52 	} else {
     53 		return fmt.Errorf("Unknown transfer protocol %s", uri.Scheme)
     54 	}
     55 
     56 	header, rcpts, err := composer.PrepareHeader()
     57 	if err != nil {
     58 		return errors.Wrap(err, "PrepareHeader")
     59 	}
     60 
     61 	if config.From == "" {
     62 		return errors.New("No 'From' configured for this account")
     63 	}
     64 	from, err := mail.ParseAddress(config.From)
     65 	if err != nil {
     66 		return errors.Wrap(err, "ParseAddress(config.From)")
     67 	}
     68 
     69 	var (
     70 		saslClient sasl.Client
     71 		conn       *smtp.Client
     72 	)
     73 	switch auth {
     74 	case "":
     75 		fallthrough
     76 	case "none":
     77 		saslClient = nil
     78 	case "plain":
     79 		password, _ := uri.User.Password()
     80 		saslClient = sasl.NewPlainClient("", uri.User.Username(), password)
     81 	default:
     82 		return fmt.Errorf("Unsupported auth mechanism %s", auth)
     83 	}
     84 
     85 	aerc.RemoveTab(composer)
     86 
     87 	var starttls bool
     88 	if starttls_, ok := config.Params["smtp-starttls"]; ok {
     89 		starttls = starttls_ == "yes"
     90 	}
     91 
     92 	sendAsync := func() (int, error) {
     93 		switch scheme {
     94 		case "smtp":
     95 			host := uri.Host
     96 			serverName := uri.Host
     97 			if !strings.ContainsRune(host, ':') {
     98 				host = host + ":587" // Default to submission port
     99 			} else {
    100 				serverName = host[:strings.IndexRune(host, ':')]
    101 			}
    102 			conn, err = smtp.Dial(host)
    103 			if err != nil {
    104 				return 0, errors.Wrap(err, "smtp.Dial")
    105 			}
    106 			defer conn.Close()
    107 			if sup, _ := conn.Extension("STARTTLS"); sup {
    108 				if !starttls {
    109 					err := errors.New("STARTTLS is supported by this server, " +
    110 						"but not set in accounts.conf. " +
    111 						"Add smtp-starttls=yes")
    112 					return 0, err
    113 				}
    114 				if err = conn.StartTLS(&tls.Config{
    115 					ServerName: serverName,
    116 				}); err != nil {
    117 					return 0, errors.Wrap(err, "StartTLS")
    118 				}
    119 			} else {
    120 				if starttls {
    121 					err := errors.New("STARTTLS requested, but not supported " +
    122 						"by this SMTP server. Is someone tampering with your " +
    123 						"connection?")
    124 					return 0, err
    125 				}
    126 			}
    127 		case "smtps":
    128 			host := uri.Host
    129 			serverName := uri.Host
    130 			if !strings.ContainsRune(host, ':') {
    131 				host = host + ":465" // Default to smtps port
    132 			} else {
    133 				serverName = host[:strings.IndexRune(host, ':')]
    134 			}
    135 			conn, err = smtp.DialTLS(host, &tls.Config{
    136 				ServerName: serverName,
    137 			})
    138 			if err != nil {
    139 				return 0, errors.Wrap(err, "smtp.DialTLS")
    140 			}
    141 			defer conn.Close()
    142 		}
    143 
    144 		// TODO: sendmail
    145 		if saslClient != nil {
    146 			if err = conn.Auth(saslClient); err != nil {
    147 				return 0, errors.Wrap(err, "conn.Auth")
    148 			}
    149 		}
    150 		// TODO: the user could conceivably want to use a different From and sender
    151 		if err = conn.Mail(from.Address); err != nil {
    152 			return 0, errors.Wrap(err, "conn.Mail")
    153 		}
    154 		for _, rcpt := range rcpts {
    155 			if err = conn.Rcpt(rcpt); err != nil {
    156 				return 0, errors.Wrap(err, "conn.Rcpt")
    157 			}
    158 		}
    159 		wc, err := conn.Data()
    160 		if err != nil {
    161 			return 0, errors.Wrap(err, "conn.Data")
    162 		}
    163 		defer wc.Close()
    164 		ctr := datacounter.NewWriterCounter(wc)
    165 		composer.WriteMessage(header, ctr)
    166 		return int(ctr.Count()), nil
    167 	}
    168 
    169 	go func() {
    170 		aerc.SetStatus("Sending...")
    171 		nbytes, err := sendAsync()
    172 		if err != nil {
    173 			aerc.SetStatus(" "+err.Error()).
    174 				Color(tcell.ColorDefault, tcell.ColorRed)
    175 			return
    176 		}
    177 		if config.CopyTo != "" {
    178 			aerc.SetStatus("Copying to " + config.CopyTo)
    179 			worker := composer.Worker()
    180 			r, w := io.Pipe()
    181 			worker.PostAction(&types.AppendMessage{
    182 				Destination: config.CopyTo,
    183 				Flags:       []string{},
    184 				Date:        time.Now(),
    185 				Reader:      r,
    186 				Length:      nbytes,
    187 			}, func(msg types.WorkerMessage) {
    188 				switch msg := msg.(type) {
    189 				case *types.Done:
    190 					aerc.SetStatus("Message sent.")
    191 					r.Close()
    192 					composer.Close()
    193 				case *types.Error:
    194 					aerc.PushStatus(" "+msg.Error.Error(), 10*time.Second).
    195 						Color(tcell.ColorDefault, tcell.ColorRed)
    196 					r.Close()
    197 					composer.Close()
    198 				}
    199 			})
    200 			header, _, _ := composer.PrepareHeader()
    201 			composer.WriteMessage(header, w)
    202 			w.Close()
    203 		} else {
    204 			aerc.SetStatus("Message sent.")
    205 			composer.Close()
    206 		}
    207 	}()
    208 	return nil
    209 }