package network import ( "bytes" "errors" "fmt" "io" "net/smtp" "runtime" "time" netmail "net/mail" "pkg.jfrech.com/brief/config" "pkg.jfrech.com/brief/config/vault" "pkg.jfrech.com/brief/docket" "pkg.jfrech.com/brief/fieldnames" "pkg.jfrech.com/brief/mailx" "pkg.jfrech.com/brief/messageid" "pkg.jfrech.com/brief/sacks" "pkg.jfrech.com/brief/sem" "pkg.jfrech.com/brief/text" ) type Network interface { SendSMTP(crypter vault.Crypter, sender *config.Address, recipients []*netmail.Address, rawmessage sem.Undocketed) error } var OSNetwork osNetwork type osNetwork struct{} // recipients is not checked against rawmessage as e.g. Bcc recipients are at this stage no longer listed func (_ osNetwork) SendSMTP(crypter vault.Crypter, sender *config.Address, recipients []*netmail.Address, rawmessage sem.Undocketed) error { if !sender.SMTPEnabled { return fmt.Errorf("SMTP not enabled") } // ErrPartialSend should not need to refer to a mutable input parameter rawrecipients := make([]string, 0, len(recipients)) for _, recipient := range recipients { rawrecipients = append(rawrecipients, recipient.Address) } recipients = nil if true { // paranoically ensure that the verified message does not racily change rawmessage = bytes.Clone(rawmessage) splitter := mailx.NewSplitter(bytes.NewReader(rawmessage)) for { field, err := splitter.NextField() if errors.Is(err, mailx.EOH) { break } else if err != nil { // Brief must be able to parse messages it itself creates return fmt.Errorf("will not send non-parsable message: %v", err) } if fieldnames.ContainsCanonically(fieldnames.Category.Bcc(), field.Name) { return fmt.Errorf("will not send message containing Bcc information: %q", field) } } _, err := io.Copy(io.Discard, splitter) if err != nil { return fmt.Errorf("will not send non-readable message: %v", err) } } if crypter == nil { return fmt.Errorf("no decrypt: %v", vault.ErrNoCrypter) } // [2023-04-29, jfrech] TODO: Is keeping password in memory a big deal? If // the crypter.Decrypt call is moved inside the closure, it takes longer to // know that the password cannot be decrypted, reducing transparancy. password, err := crypter.Decrypt(sender.SMTPPassword) if err != nil { return fmt.Errorf("no decrypt: %v", err) } // [2023-10-16, jfrech] TODO This might be bogus. // try to expunge password from memory defer runtime.GC() a := smtp.PlainAuth("", sender.EffectiveSMTPUsername(), string(password), sender.EffectiveSMTPHost()) // for j := range rawrecipients { err := smtp.SendMail(sender.EffectiveSMTPHostPort(), a, sender.EMailAddress, []string{rawrecipients[j]}, rawmessage) if err != nil { return &ErrPartialSend{ succeeded: rawrecipients[:j], failed: rawrecipients[j], scheduled: rawrecipients[j+1:], reason: err, } } } return nil } // TODO export fields type ErrPartialSend struct { succeeded []string failed string scheduled []string reason error // TODO Possibly rename to "Err"? } func (err *ErrPartialSend) Error() string { return fmt.Sprintf("partial send (%q succeeded, %q failed, %q scheduled): %v", err.succeeded, err.failed, err.scheduled, err.reason) } func (err *ErrPartialSend) Unwrap() error { return err.reason } // // strips out Bcc func SendSMTP(network Network, cnf *config.Config, crypter vault.Crypter, sack sacks.Sack, mid messageid.MessageId) error { message, err := sack.Lookup(mid) if err != nil { return err } if ts, _ := message.Docket.TransferStatus(); ts != sem.TransferStatusOutgoing { return fmt.Errorf("cannot send %q: has transfer status %q", mid, ts) } // rawmessage := new(bytes.Buffer) var ( rawfroms []*netmail.Address rawsenders []*netmail.Address rawtos []*netmail.Address rawccs []*netmail.Address rawbccs []*netmail.Address ) splitter := mailx.NewSplitter(bytes.NewReader(message.Message)) for n := 1; ; n++ { wrap := func(err error) error { return fmt.Errorf("field #%d: %w", n, err) } rawfield, err := splitter.NextRawField() if errors.Is(err, mailx.EOH) { break } else if err != nil { return wrap(err) } field, err := rawfield.Decode() if err != nil { return wrap(err) } if fieldnames.ContainsCanonically(fieldnames.Category.Bcc(), field.CanonicalName()) { continue } // if field.Is(fieldnames.From) || field.Is(fieldnames.Sender) || field.Is(fieldnames.To) || field.Is(fieldnames.Cc) || field.Is(fieldnames.Bcc) { addrs, err := netmail.ParseAddressList(field.Body) if err != nil { return wrap(err) } for j := range addrs { addrs[j].Name = "" } // TODO mail address canonicalisation and error if the same address is used more than once switch { case field.Is(fieldnames.From): rawfroms = append(rawfroms, addrs...) case field.Is(fieldnames.Sender): rawsenders = append(rawsenders, addrs...) case field.Is(fieldnames.To): rawtos = append(rawtos, addrs...) case field.Is(fieldnames.Cc): rawccs = append(rawccs, addrs...) case field.Is(fieldnames.Bcc): rawbccs = append(rawbccs, addrs...) default: panic("unreachable") } } rawmessage.Write(rawfield) } if len(rawsenders) != 0 && len(rawsenders) != 1 { return fmt.Errorf("more than one Sender") } else if len(rawsenders) != 1 && len(rawfroms) < 1 { return fmt.Errorf("no From") } // TODO deliberate: //len(rawfroms)==0 allowed //len(rawtos)==0 allowed var rawsender *netmail.Address if len(rawsenders) > 0 { rawsender = rawsenders[0] } else { rawsender = rawfroms[0] } rawmessage.WriteString(text.CRLF) _, err = io.Copy(rawmessage, splitter) if err != nil { return err } // sender, err := cnf.LookupNetmailAddress(rawsender) if err != nil { return fmt.Errorf("cannot send with %q: %w", rawsender.Address, err) } else if !sender.SMTPEnabled { return fmt.Errorf("cannot send with %q: SMTP not enabled", sender.EMailAddress) } // recipients := make([]*netmail.Address, 0, len(rawtos)+len(rawccs)+len(rawbccs)) for _, rawto := range rawtos { recipients = append(recipients, rawto) } for _, rawcc := range rawccs { recipients = append(recipients, rawcc) } for _, rawbcc := range rawbccs { recipients = append(recipients, rawbcc) } // tSending := time.Now() err = sack.AppendDocket(mid, docket.TKV{tSending, sem.DocketKeyTransferStatus, []byte(sem.TransferStatusSending + " " + sender.URLSMTP().String())}) if err != nil { return err } err = network.SendSMTP(crypter, sender, recipients, rawmessage.Bytes()) if err != nil { return err } tSent := time.Now() err = sack.AppendDocket(mid, docket.TKV{tSent, sem.DocketKeyTransferStatus, []byte(sem.TransferStatusSent + " " + sender.URLSMTP().String())}) if err != nil { return err } return nil }