summary refs log blame commit diff stats
path: root/commands/compose/send.go
blob: 2c2e2944c75f9544e7df465e2f720218d26dfdd2 (plain) (tree)
1
2
3
4
5
6
7
8
9
10


               
                    
                
             
            


                  
              


                                     
                                  
                                        
 

                                               


             
                                     






                                                             







                                                                                 
                       















                                                                             
                                                      



























                                                                                   
                                
 




                                                                
                                          


                                        
                                              

                                                                                  

                                                                                


                                                   
                                             


                                                                      





                                                                                                    


                                                                   







                                                                                                        



                                        
                                              

                                                                             

                                                                                
                         


                                                                   
                                       
                                             

                                          
                 



                                                                    
                                             

                         

                                                                                           
                                     


                                                              
                                             
                         
                 
                                      
                               
                                     
                 
                                


                                                       

         
                   


                                            
                                                        








                                                                         



                                                        


                                                          
                                                                       








                                                                                               
                                                                


                                                        
                                                       

                                        
           

                  
package compose

import (
	"crypto/tls"
	"errors"
	"fmt"
	"io"
	"net/mail"
	"net/url"
	"strings"
	"time"

	"github.com/emersion/go-sasl"
	"github.com/emersion/go-smtp"
	"github.com/gdamore/tcell"
	"github.com/miolini/datacounter"

	"git.sr.ht/~sircmpwn/aerc/widgets"
	"git.sr.ht/~sircmpwn/aerc/worker/types"
)

func init() {
	register("send", SendMessage)
}

func SendMessage(aerc *widgets.Aerc, args []string) error {
	if len(args) > 1 {
		return errors.New("Usage: send-message")
	}
	composer, _ := aerc.SelectedTab().(*widgets.Composer)
	config := composer.Config()

	if config.Outgoing == "" {
		return errors.New(
			"No outgoing mail transport configured for this account")
	}

	uri, err := url.Parse(config.Outgoing)
	if err != nil {
		return err
	}
	var (
		scheme string
		auth   string = "plain"
	)
	parts := strings.Split(uri.Scheme, "+")
	if len(parts) == 1 {
		scheme = parts[0]
	} else if len(parts) == 2 {
		scheme = parts[0]
		auth = parts[1]
	} else {
		return fmt.Errorf("Unknown transfer protocol %s", uri.Scheme)
	}

	header, rcpts, err := composer.PrepareHeader()
	if err != nil {
		return err
	}

	if config.From == "" {
		return errors.New("No 'From' configured for this account")
	}
	from, err := mail.ParseAddress(config.From)
	if err != nil {
		return err
	}

	var (
		saslClient sasl.Client
		conn       *smtp.Client
	)
	switch auth {
	case "":
		fallthrough
	case "none":
		saslClient = nil
	case "plain":
		password, _ := uri.User.Password()
		saslClient = sasl.NewPlainClient("", uri.User.Username(), password)
	default:
		return fmt.Errorf("Unsupported auth mechanism %s", auth)
	}

	aerc.RemoveTab(composer)

	var starttls bool
	if starttls_, ok := config.Params["smtp-starttls"]; ok {
		starttls = starttls_ == "yes"
	}

	sendAsync := func() (int, error) {
		switch scheme {
		case "smtp":
			host := uri.Host
			serverName := uri.Host
			if !strings.ContainsRune(host, ':') {
				host = host + ":587" // Default to submission port
			} else {
				serverName = host[:strings.IndexRune(host, ':')]
			}
			conn, err = smtp.Dial(host)
			if err != nil {
				return 0, err
			}
			defer conn.Close()
			if sup, _ := conn.Extension("STARTTLS"); sup {
				if !starttls {
					err := errors.New("STARTTLS is supported by this server, " +
						"but not set in accounts.conf. " +
						"Add smtp-starttls=yes")
					return 0, err
				}
				if err = conn.StartTLS(&tls.Config{
					ServerName: serverName,
				}); err != nil {
					return 0, err
				}
			} else {
				if starttls {
					err := errors.New("STARTTLS requested, but not supported " +
						"by this SMTP server. Is someone tampering with your " +
						"connection?")
					return 0, err
				}
			}
		case "smtps":
			host := uri.Host
			serverName := uri.Host
			if !strings.ContainsRune(host, ':') {
				host = host + ":465" // Default to smtps port
			} else {
				serverName = host[:strings.IndexRune(host, ':')]
			}
			conn, err = smtp.DialTLS(host, &tls.Config{
				ServerName: serverName,
			})
			if err != nil {
				return 0, err
			}
			defer conn.Close()
		}

		// TODO: sendmail
		if saslClient != nil {
			if err = conn.Auth(saslClient); err != nil {
				return 0, err
			}
		}
		// TODO: the user could conceivably want to use a different From and sender
		if err = conn.Mail(from.Address); err != nil {
			return 0, err
		}
		for _, rcpt := range rcpts {
			if err = conn.Rcpt(rcpt); err != nil {
				return 0, err
			}
		}
		wc, err := conn.Data()
		if err != nil {
			return 0, err
		}
		defer wc.Close()
		ctr := datacounter.NewWriterCounter(wc)
		composer.WriteMessage(header, ctr)
		return int(ctr.Count()), nil
	}

	go func() {
		aerc.SetStatus("Sending...")
		nbytes, err := sendAsync()
		if err != nil {
			aerc.SetStatus(" "+err.Error()).
				Color(tcell.ColorDefault, tcell.ColorRed)
			return
		}
		if config.CopyTo != "" {
			aerc.SetStatus("Copying to " + config.CopyTo)
			worker := composer.Worker()
			r, w := io.Pipe()
			worker.PostAction(&types.AppendMessage{
				Destination: config.CopyTo,
				Flags:       []string{},
				Date:        time.Now(),
				Reader:      r,
				Length:      nbytes,
			}, func(msg types.WorkerMessage) {
				switch msg := msg.(type) {
				case *types.Done:
					aerc.SetStatus("Message sent.")
					r.Close()
					composer.Close()
				case *types.Error:
					aerc.PushStatus(" "+msg.Error.Error(), 10*time.Second).
						Color(tcell.ColorDefault, tcell.ColorRed)
					r.Close()
					composer.Close()
				}
			})
			header, _, _ := composer.PrepareHeader()
			composer.WriteMessage(header, w)
			w.Close()
		} else {
			aerc.SetStatus("Message sent.")
			composer.Close()
		}
	}()
	return nil
}
span class="w"> ] ] scenario copy-a-sandbox-to-editor-2 [ local-scope trace-until 50/app # trace too long assume-screen 50/width, 10/height env:&:environment <- new-programming-environment screen, [add 1, 1] assume-console [ press F4 ] event-loop screen, console, env screen-should-contain [ . run (F4) . . . .──────────────────────────────────────────────────. .0 edit copy delete . .add 1, 1 . .2 . .──────────────────────────────────────────────────. . . . . . . ] # click at right edge of 'copy' button assume-console [ left-click 3, 33 ] run [ event-loop screen, console, env ] # it copies into editor screen-should-contain [ . run (F4) . .add 1, 1 . .──────────────────────────────────────────────────. .0 edit copy delete . .add 1, 1 . .2 . .──────────────────────────────────────────────────. . . . . . . ] # cursor should be in the right place assume-console [ type [0] ] run [ event-loop screen, console, env ] screen-should-contain [ . run (F4) . .0add 1, 1 . .──────────────────────────────────────────────────. .0 edit copy delete . .add 1, 1 . .2 . .──────────────────────────────────────────────────. . . . . . . ] ] after <global-touch> [ # support 'copy' button { copy?:bool <- should-attempt-copy? click-row, click-column, env break-unless copy? copy?, env <- try-copy-sandbox click-row, env break-unless copy? hide-screen screen screen <- render-sandbox-side screen, env, render screen <- update-cursor screen, current-sandbox, env show-screen screen loop +next-event:label } ] # some preconditions for attempting to copy a sandbox def should-attempt-copy? click-row:num, click-column:num, env:&:environment -> result:bool [ local-scope load-ingredients # are we below the sandbox editor? click-sandbox-area?:bool <- click-on-sandbox-area? click-row, env reply-unless click-sandbox-area?, 0/false # narrower, is the click in the columns spanning the 'copy' button? first-sandbox:&:editor <- get *env, current-sandbox:offset assert first-sandbox, [!!] sandbox-left-margin:num <- get *first-sandbox, left:offset sandbox-right-margin:num <- get *first-sandbox, right:offset _, _, copy-button-left:num, copy-button-right:num, _ <- sandbox-menu-columns sandbox-left-margin, sandbox-right-margin copy-button-vertical-area?:bool <- within-range? click-column, copy-button-left, copy-button-right reply-unless copy-button-vertical-area?, 0/false # finally, is sandbox editor empty? current-sandbox:&:editor <- get *env, current-sandbox:offset result <- empty-editor? current-sandbox ] def try-copy-sandbox click-row:num, env:&:environment -> clicked-on-copy-button?:bool, env:&:environment [ local-scope load-ingredients # identify the sandbox to copy, if the click was actually on the 'copy' button sandbox:&:sandbox <- find-sandbox env, click-row return-unless sandbox, 0/false clicked-on-copy-button? <- copy 1/true text:text <- get *sandbox, data:offset current-sandbox:&:editor <- get *env, current-sandbox:offset current-sandbox <- insert-text current-sandbox, text # reset scroll *env <- put *env, render-from:offset, -1 ] def find-sandbox env:&:environment, click-row:num -> result:&:sandbox [ local-scope load-ingredients curr-sandbox:&:sandbox <- get *env, sandbox:offset { break-unless curr-sandbox start:num <- get *curr-sandbox, starting-row-on-screen:offset found?:bool <- equal click-row, start return-if found?, curr-sandbox curr-sandbox <- get *curr-sandbox, next-sandbox:offset loop } return 0/not-found ] def click-on-sandbox-area? click-row:num, env:&:environment -> result:bool [ local-scope load-ingredients first-sandbox:&:sandbox <- get *env, sandbox:offset return-unless first-sandbox, 0/false first-sandbox-begins:num <- get *first-sandbox, starting-row-on-screen:offset result <- greater-or-equal click-row, first-sandbox-begins ] def empty-editor? editor:&:editor -> result:bool [ local-scope load-ingredients head:&:duplex-list:char <- get *editor, data:offset first:&:duplex-list:char <- next head result <- not first ] def within-range? x:num, low:num, high:num -> result:bool [ local-scope load-ingredients not-too-far-left?:bool <- greater-or-equal x, low not-too-far-right?:bool <- lesser-or-equal x, high result <- and not-too-far-left? not-too-far-right? ] scenario copy-fails-if-sandbox-editor-not-empty [ local-scope trace-until 50/app # trace too long assume-screen 50/width, 10/height env:&:environment <- new-programming-environment screen, [add 1, 1] assume-console [ press F4 ] event-loop screen, console, env screen-should-contain [ . run (F4) . . . .──────────────────────────────────────────────────. .0 edit copy delete . .add 1, 1 . .2 . .──────────────────────────────────────────────────. . . ] # type something into the sandbox editor, then click on the 'copy' button assume-console [ left-click 2, 20 # put cursor in sandbox editor type [0] # type something left-click 3, 20 # click 'copy' button ] run [ event-loop screen, console, env ] # copy doesn't happen screen-should-contain [ . run (F4) . .0 . .──────────────────────────────────────────────────. .0 edit copy delete . .add 1, 1 . .2 . .──────────────────────────────────────────────────. . . ] # cursor should be in the right place assume-console [ type [1] ] run [ event-loop screen, console, env ] screen-should-contain [ . run (F4) . .01 . .──────────────────────────────────────────────────. .0 edit copy delete . .add 1, 1 . .2 . .──────────────────────────────────────────────────. . . ] ]