1package sender
2
3import (
4 "bytes"
5 "crypto/rand"
6 "crypto/tls"
7 "crypto/x509"
8 "encoding/base64"
9 "encoding/pem"
10 "errors"
11 "fmt"
12 "mime"
13 "mime/multipart"
14 "mime/quotedprintable"
15 "net/smtp"
16 "net/textproto"
17 "os"
18 "path/filepath"
19 "strings"
20 "time"
21
22 "github.com/floatpane/matcha/clib"
23 "github.com/floatpane/matcha/config"
24 "github.com/yuin/goldmark"
25 "github.com/yuin/goldmark/ast"
26 "github.com/yuin/goldmark/text"
27 "go.mozilla.org/pkcs7"
28)
29
30// xoauth2Auth implements the SMTP XOAUTH2 authentication mechanism for OAuth2.
31// See https://developers.google.com/gmail/imap/xoauth2-protocol
32type xoauth2Auth struct {
33 username, token string
34}
35
36func (a *xoauth2Auth) Start(server *smtp.ServerInfo) (string, []byte, error) {
37 resp := fmt.Sprintf("user=%s\x01auth=Bearer %s\x01\x01", a.username, a.token)
38 return "XOAUTH2", []byte(resp), nil
39}
40
41func (a *xoauth2Auth) Next(fromServer []byte, more bool) ([]byte, error) {
42 if more {
43 // Server sent an error challenge; respond with empty to finish.
44 return []byte{}, nil
45 }
46 return nil, nil
47}
48
49// loginAuth implements the SMTP LOGIN authentication mechanism.
50// Some SMTP servers (e.g. Mailo) only support LOGIN and not PLAIN.
51type loginAuth struct {
52 username, password string
53}
54
55func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
56 return "LOGIN", nil, nil
57}
58
59func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
60 if !more {
61 return nil, nil
62 }
63 prompt := strings.TrimSpace(string(fromServer))
64 switch strings.ToLower(prompt) {
65 case "username:":
66 return []byte(a.username), nil
67 case "password:":
68 return []byte(a.password), nil
69 default:
70 return nil, fmt.Errorf("unexpected LOGIN prompt: %s", prompt)
71 }
72}
73
74// generateMessageID creates a unique Message-ID header.
75func generateMessageID(from string) string {
76 buf := make([]byte, 16)
77 _, err := rand.Read(buf)
78 if err != nil {
79 return fmt.Sprintf("<%d.%s>", time.Now().UnixNano(), from)
80 }
81 return fmt.Sprintf("<%x@%s>", buf, from)
82}
83
84// containsMarkup returns true if the string contains Markdown or HTML elements.
85func containsMarkup(body string) bool {
86 // Parse the Markdown into an AST. We will consider most AST node kinds as
87 // markup, but treat bare/autolinks (raw URLs) as plaintext for this
88 // detection: if a link node's visible text equals its destination (or is
89 // the destination wrapped in <>), we allow it.
90 source := []byte(body)
91 md := goldmark.New()
92 reader := text.NewReader(source)
93 doc := md.Parser().Parse(reader)
94
95 var hasMarkup bool
96 ast.Walk(doc, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
97 if !entering {
98 return ast.WalkContinue, nil
99 }
100
101 switch node.Kind() {
102 case ast.KindDocument, ast.KindParagraph, ast.KindText:
103 // not considered formatting
104 return ast.WalkContinue, nil
105 case ast.KindLink:
106 // Check if this is an autolink/raw URL: the link's text equals the
107 // destination. If so, don't treat it as markup for our purposes.
108 linkNode, ok := node.(*ast.Link)
109 if !ok {
110 hasMarkup = true
111 return ast.WalkStop, nil
112 }
113
114 // Collect the visible text of the link
115 var b strings.Builder
116 for c := node.FirstChild(); c != nil; c = c.NextSibling() {
117 if txt, ok := c.(*ast.Text); ok {
118 b.Write(txt.Segment.Value(source))
119 } else {
120 // non-text content inside link -> treat as markup
121 hasMarkup = true
122 return ast.WalkStop, nil
123 }
124 }
125 linkText := b.String()
126 dest := string(linkNode.Destination)
127
128 // Normalize common autolink representations and allow them.
129 if linkText == dest || linkText == "<"+dest+">" {
130 return ast.WalkContinue, nil
131 }
132
133 // Otherwise treat as markup
134 hasMarkup = true
135 return ast.WalkStop, nil
136 default:
137 hasMarkup = true
138 return ast.WalkStop, nil
139 }
140 })
141 return hasMarkup
142}
143
144// detectPlaintextOnly returns true when the body contains only plain text
145// (no images, no attachments, no markdown/HTML formatting that requires multipart).
146func detectPlaintextOnly(body string, images, attachments map[string][]byte) bool {
147 if len(images) > 0 || len(attachments) > 0 {
148 return false
149 }
150 return !containsMarkup(body)
151}
152
153// SendEmail constructs a multipart message with plain text, HTML, embedded images, and attachments.
154func SendEmail(account *config.Account, to, cc, bcc []string, subject, plainBody, htmlBody string, images map[string][]byte, attachments map[string][]byte, inReplyTo string, references []string, signSMIME bool, encryptSMIME bool) error {
155 smtpServer := account.GetSMTPServer()
156 smtpPort := account.GetSMTPPort()
157
158 if smtpServer == "" {
159 return fmt.Errorf("unsupported or missing service_provider: %s", account.ServiceProvider)
160 }
161
162 plainAuth := smtp.PlainAuth("", account.Email, account.Password, smtpServer)
163 loginAuthFallback := &loginAuth{username: account.Email, password: account.Password}
164
165 fromHeader := account.FetchEmail
166 if account.Name != "" {
167 fromHeader = fmt.Sprintf("%s <%s>", account.Name, account.FetchEmail)
168 }
169
170 // Set top-level headers (From/To/Subject/Date/etc)
171 headers := map[string]string{
172 "From": fromHeader,
173 "To": strings.Join(to, ", "),
174 "Subject": subject,
175 "Date": time.Now().Format(time.RFC1123Z),
176 "Message-ID": generateMessageID(account.FetchEmail),
177 "MIME-Version": "1.0",
178 }
179
180 if len(cc) > 0 {
181 headers["Cc"] = strings.Join(cc, ", ")
182 }
183
184 if inReplyTo != "" {
185 headers["In-Reply-To"] = inReplyTo
186 if len(references) > 0 {
187 headers["References"] = strings.Join(references, " ") + " " + inReplyTo
188 } else {
189 headers["References"] = inReplyTo
190 }
191 }
192
193 // prepare final message buffer and S/MIME payload placeholder
194 var msg bytes.Buffer
195 headerOrder := []string{"From", "To", "Cc", "Subject", "Date", "Message-ID", "MIME-Version", "In-Reply-To", "References"}
196 for _, k := range headerOrder {
197 if v, ok := headers[k]; ok {
198 fmt.Fprintf(&msg, "%s: %s\r\n", k, v)
199 }
200 }
201
202 var payloadToEncrypt []byte
203 var innerBodyBytes []byte
204 var err error
205
206 // Detect plaintext-only mode
207 plaintextOnly := detectPlaintextOnly(plainBody, images, attachments)
208
209 // If plaintext-only mode is requested, build a single text/plain part (or a multipart/signed wrapper when signing)
210 if plaintextOnly {
211 if len(images) > 0 || len(attachments) > 0 {
212 return errors.New("plaintext-only messages cannot contain attachments or inline images")
213 }
214
215 // Build quoted-printable encoded body
216 var encBody bytes.Buffer
217 qp := quotedprintable.NewWriter(&encBody)
218 fmt.Fprint(qp, plainBody)
219 qp.Close()
220 encodedBody := encBody.Bytes()
221
222 // Build the canonical MIME part (headers + body) used for signing/encryption
223 var partBuf bytes.Buffer
224 fmt.Fprintf(&partBuf, "Content-Type: text/plain; charset=UTF-8; format=flowed\r\n")
225 fmt.Fprintf(&partBuf, "Content-Transfer-Encoding: quoted-printable\r\n\r\n")
226 partBuf.Write(encodedBody)
227 canonicalPart := partBuf.Bytes()
228
229 if signSMIME {
230 if account.SMIMECert == "" || account.SMIMEKey == "" {
231 return errors.New("S/MIME certificate or key path is missing")
232 }
233
234 certData, err := os.ReadFile(account.SMIMECert)
235 if err != nil {
236 return err
237 }
238 keyData, err := os.ReadFile(account.SMIMEKey)
239 if err != nil {
240 return err
241 }
242
243 certBlock, _ := pem.Decode(certData)
244 if certBlock == nil {
245 return errors.New("failed to parse certificate PEM")
246 }
247 cert, err := x509.ParseCertificate(certBlock.Bytes)
248 if err != nil {
249 return err
250 }
251
252 keyBlock, _ := pem.Decode(keyData)
253 if keyBlock == nil {
254 return errors.New("failed to parse private key PEM")
255 }
256 privKey, err := x509.ParsePKCS8PrivateKey(keyBlock.Bytes)
257 if err != nil {
258 privKey, err = x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
259 if err != nil {
260 return err
261 }
262 }
263
264 // canonicalize the part (normalize newlines)
265 canonicalBody := bytes.ReplaceAll(canonicalPart, []byte("\r\n"), []byte("\n"))
266 canonicalBody = bytes.ReplaceAll(canonicalBody, []byte("\n"), []byte("\r\n"))
267
268 signedData, err := pkcs7.NewSignedData(canonicalBody)
269 if err != nil {
270 return err
271 }
272 if err := signedData.AddSigner(cert, privKey, pkcs7.SignerInfoConfig{}); err != nil {
273 return err
274 }
275 detachedSig, err := signedData.Finish()
276 if err != nil {
277 return err
278 }
279
280 var rb [12]byte
281 var outerBoundary string
282 if _, rerr := rand.Read(rb[:]); rerr == nil {
283 outerBoundary = "signed-" + fmt.Sprintf("%x", rb[:])
284 } else {
285 // fallback to time-based boundary if crypto/rand fails
286 outerBoundary = "signed-" + fmt.Sprintf("%d", time.Now().UnixNano())
287 }
288 var signedMsg bytes.Buffer
289 fmt.Fprintf(&signedMsg, "Content-Type: multipart/signed; protocol=\"application/pkcs7-signature\"; micalg=\"sha-256\"; boundary=\"%s\"\r\n\r\n", outerBoundary)
290 fmt.Fprintf(&signedMsg, "This is a cryptographically signed message in MIME format.\r\n\r\n")
291 fmt.Fprintf(&signedMsg, "--%s\r\n", outerBoundary)
292 signedMsg.Write(canonicalBody)
293 fmt.Fprintf(&signedMsg, "\r\n--%s\r\n", outerBoundary)
294 fmt.Fprintf(&signedMsg, "Content-Type: application/pkcs7-signature; name=\"smime.p7s\"\r\n")
295 fmt.Fprintf(&signedMsg, "Content-Transfer-Encoding: base64\r\n")
296 fmt.Fprintf(&signedMsg, "Content-Disposition: attachment; filename=\"smime.p7s\"\r\n\r\n")
297 signedMsg.WriteString(clib.WrapBase64(base64.StdEncoding.EncodeToString(detachedSig)))
298 fmt.Fprintf(&signedMsg, "\r\n--%s--\r\n", outerBoundary)
299
300 if encryptSMIME {
301 payloadToEncrypt = bytes.ReplaceAll(signedMsg.Bytes(), []byte("\r\n"), []byte("\n"))
302 payloadToEncrypt = bytes.ReplaceAll(payloadToEncrypt, []byte("\n"), []byte("\r\n"))
303 } else {
304 msg.Write(signedMsg.Bytes())
305 }
306 } else {
307 // Not signing: either encrypt the canonical part or send as plain single-part
308 canonicalBody := bytes.ReplaceAll(canonicalPart, []byte("\r\n"), []byte("\n"))
309 canonicalBody = bytes.ReplaceAll(canonicalBody, []byte("\n"), []byte("\r\n"))
310 if encryptSMIME {
311 payloadToEncrypt = canonicalBody
312 } else {
313 // Write Content-Type and body as top-level single part
314 fmt.Fprintf(&msg, "Content-Type: text/plain; charset=UTF-8; format=flowed\r\n")
315 fmt.Fprintf(&msg, "Content-Transfer-Encoding: quoted-printable\r\n\r\n")
316 msg.Write(encodedBody)
317 }
318 }
319
320 } else {
321 // --- Non-plaintext path: build multipart/mixed with related/alternative, images and attachments ---
322 var innerMsg bytes.Buffer
323 innerWriter := multipart.NewWriter(&innerMsg)
324 innerHeaders := fmt.Sprintf("Content-Type: multipart/mixed; boundary=\"%s\"\r\n\r\n", innerWriter.Boundary())
325
326 // --- Body Part (multipart/related) ---
327 relatedHeader := textproto.MIMEHeader{}
328 relatedBoundary := "related-" + innerWriter.Boundary()
329 relatedHeader.Set("Content-Type", "multipart/related; boundary=\""+relatedBoundary+"\"")
330 relatedPartWriter, err := innerWriter.CreatePart(relatedHeader)
331 if err != nil {
332 return err
333 }
334 relatedWriter := multipart.NewWriter(relatedPartWriter)
335 relatedWriter.SetBoundary(relatedBoundary)
336
337 // --- Alternative Part (text and html) ---
338 altHeader := textproto.MIMEHeader{}
339 altBoundary := "alt-" + innerWriter.Boundary()
340 altHeader.Set("Content-Type", "multipart/alternative; boundary=\""+altBoundary+"\"")
341 altPartWriter, err := relatedWriter.CreatePart(altHeader)
342 if err != nil {
343 return err
344 }
345 altWriter := multipart.NewWriter(altPartWriter)
346 altWriter.SetBoundary(altBoundary)
347
348 // Plain text part
349 textHeader := textproto.MIMEHeader{
350 "Content-Type": {"text/plain; charset=UTF-8"},
351 "Content-Transfer-Encoding": {"quoted-printable"},
352 }
353 textPart, err := altWriter.CreatePart(textHeader)
354 if err != nil {
355 return err
356 }
357 qpText := quotedprintable.NewWriter(textPart)
358 fmt.Fprint(qpText, plainBody)
359 qpText.Close()
360
361 // HTML part
362 htmlHeader := textproto.MIMEHeader{
363 "Content-Type": {"text/html; charset=UTF-8"},
364 "Content-Transfer-Encoding": {"quoted-printable"},
365 }
366 htmlPart, err := altWriter.CreatePart(htmlHeader)
367 if err != nil {
368 return err
369 }
370 qpHTML := quotedprintable.NewWriter(htmlPart)
371 fmt.Fprint(qpHTML, htmlBody)
372 qpHTML.Close()
373
374 altWriter.Close() // Finish the alternative part
375
376 // --- Inline Images ---
377 for cid, data := range images {
378 ext := filepath.Ext(strings.Split(cid, "@")[0])
379 mimeType := mime.TypeByExtension(ext)
380 if mimeType == "" {
381 mimeType = "application/octet-stream"
382 }
383
384 imgHeader := textproto.MIMEHeader{}
385 imgHeader.Set("Content-Type", mimeType)
386 imgHeader.Set("Content-Transfer-Encoding", "base64")
387 imgHeader.Set("Content-ID", "<"+cid+">")
388 imgHeader.Set("Content-Disposition", "inline; filename=\""+cid+"\"")
389
390 imgPart, err := relatedWriter.CreatePart(imgHeader)
391 if err != nil {
392 return err
393 }
394 // Encode raw image bytes to base64, then wrap at 76 chars per MIME rules
395 encodedImg := base64.StdEncoding.EncodeToString(data)
396 imgPart.Write([]byte(clib.WrapBase64(encodedImg)))
397 }
398
399 relatedWriter.Close() // Finish the related part
400
401 // --- Attachments ---
402 for filename, data := range attachments {
403 mimeType := mime.TypeByExtension(filepath.Ext(filename))
404 if mimeType == "" {
405 mimeType = "application/octet-stream"
406 }
407
408 partHeader := textproto.MIMEHeader{}
409 partHeader.Set("Content-Type", mimeType)
410 partHeader.Set("Content-Transfer-Encoding", "base64")
411 partHeader.Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
412
413 attachmentPart, err := innerWriter.CreatePart(partHeader)
414 if err != nil {
415 return err
416 }
417 encodedData := base64.StdEncoding.EncodeToString(data)
418 // MIME requires base64 to be line-wrapped at 76 characters
419 attachmentPart.Write([]byte(clib.WrapBase64(encodedData)))
420 }
421
422 innerWriter.Close() // Finish the inner message
423
424 innerBodyBytes = append([]byte(innerHeaders), innerMsg.Bytes()...)
425
426 // If not signing, and not encrypting, write the multipart body now
427 if !signSMIME && !encryptSMIME {
428 fmt.Fprintf(&msg, "Content-Type: multipart/mixed; boundary=\"%s\"\r\n\r\n", innerWriter.Boundary())
429 msg.Write(innerMsg.Bytes())
430 }
431 }
432
433 // Handle S/MIME Detached Signing for non-plaintext messages
434 if signSMIME && len(innerBodyBytes) > 0 {
435 if account.SMIMECert == "" || account.SMIMEKey == "" {
436 return errors.New("S/MIME certificate or key path is missing")
437 }
438
439 certData, err := os.ReadFile(account.SMIMECert)
440 if err != nil {
441 return err
442 }
443 keyData, err := os.ReadFile(account.SMIMEKey)
444 if err != nil {
445 return err
446 }
447
448 certBlock, _ := pem.Decode(certData)
449 if certBlock == nil {
450 return errors.New("failed to parse certificate PEM")
451 }
452 cert, err := x509.ParseCertificate(certBlock.Bytes)
453 if err != nil {
454 return err
455 }
456
457 keyBlock, _ := pem.Decode(keyData)
458 if keyBlock == nil {
459 return errors.New("failed to parse private key PEM")
460 }
461 privKey, err := x509.ParsePKCS8PrivateKey(keyBlock.Bytes)
462 if err != nil {
463 privKey, err = x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
464 if err != nil {
465 return err
466 }
467 }
468
469 canonicalBody := bytes.ReplaceAll(innerBodyBytes, []byte("\r\n"), []byte("\n"))
470 canonicalBody = bytes.ReplaceAll(canonicalBody, []byte("\n"), []byte("\r\n"))
471
472 signedData, err := pkcs7.NewSignedData(canonicalBody)
473 if err != nil {
474 return err
475 }
476 if err := signedData.AddSigner(cert, privKey, pkcs7.SignerInfoConfig{}); err != nil {
477 return err
478 }
479 detachedSig, err := signedData.Finish()
480 if err != nil {
481 return err
482 }
483
484 var rb [12]byte
485 var outerBoundary string
486 if _, rerr := rand.Read(rb[:]); rerr == nil {
487 outerBoundary = "signed-" + fmt.Sprintf("%x", rb[:])
488 } else {
489 // fallback to time-based boundary if crypto/rand fails
490 outerBoundary = "signed-" + fmt.Sprintf("%d", time.Now().UnixNano())
491 }
492 var signedMsg bytes.Buffer
493 fmt.Fprintf(&signedMsg, "Content-Type: multipart/signed; protocol=\"application/pkcs7-signature\"; micalg=\"sha-256\"; boundary=\"%s\"\r\n\r\n", outerBoundary)
494 fmt.Fprintf(&signedMsg, "This is a cryptographically signed message in MIME format.\r\n\r\n")
495 fmt.Fprintf(&signedMsg, "--%s\r\n", outerBoundary)
496 signedMsg.Write(canonicalBody)
497 fmt.Fprintf(&signedMsg, "\r\n--%s\r\n", outerBoundary)
498 fmt.Fprintf(&signedMsg, "Content-Type: application/pkcs7-signature; name=\"smime.p7s\"\r\n")
499 fmt.Fprintf(&signedMsg, "Content-Transfer-Encoding: base64\r\n")
500 fmt.Fprintf(&signedMsg, "Content-Disposition: attachment; filename=\"smime.p7s\"\r\n\r\n")
501 signedMsg.WriteString(clib.WrapBase64(base64.StdEncoding.EncodeToString(detachedSig)))
502 fmt.Fprintf(&signedMsg, "\r\n--%s--\r\n", outerBoundary)
503
504 if encryptSMIME {
505 payloadToEncrypt = bytes.ReplaceAll(signedMsg.Bytes(), []byte("\r\n"), []byte("\n"))
506 payloadToEncrypt = bytes.ReplaceAll(payloadToEncrypt, []byte("\n"), []byte("\r\n"))
507 } else {
508 msg.Write(signedMsg.Bytes())
509 }
510 }
511
512 // Handle S/MIME Encryption
513 if encryptSMIME {
514 // Include the sender's own email so it can be decrypted in the Sent folder
515 allRecipients := append([]string{account.Email}, to...)
516 allRecipients = append(allRecipients, cc...)
517 allRecipients = append(allRecipients, bcc...)
518
519 cfgDir, _ := config.GetConfigDir()
520 certsDir := filepath.Join(cfgDir, "certs")
521 var certs []*x509.Certificate
522
523 for _, em := range allRecipients {
524 em = strings.TrimSpace(em)
525 if strings.Contains(em, "<") {
526 parts := strings.Split(em, "<")
527 if len(parts) == 2 {
528 em = strings.TrimSuffix(parts[1], ">")
529 }
530 }
531
532 var certPath string
533 // If this is our own account, use the path from settings rather than requiring it in the certs folder
534 if strings.EqualFold(em, account.Email) && account.SMIMECert != "" {
535 certPath = account.SMIMECert
536 } else {
537 certPath = filepath.Join(certsDir, em+".pem")
538 }
539
540 if certData, err := os.ReadFile(certPath); err == nil {
541 if block, _ := pem.Decode(certData); block != nil {
542 if cert, err := x509.ParseCertificate(block.Bytes); err == nil {
543 certs = append(certs, cert)
544 }
545 }
546 }
547 }
548
549 if len(certs) == 0 {
550 return errors.New("cannot encrypt: no valid public certificates found for recipients")
551 }
552
553 encryptedDer, err := pkcs7.Encrypt(payloadToEncrypt, certs)
554 if err != nil {
555 return err
556 }
557
558 msg.WriteString("Content-Type: application/pkcs7-mime; smime-type=enveloped-data; name=\"smime.p7m\"\r\n")
559 msg.WriteString("Content-Transfer-Encoding: base64\r\n")
560 msg.WriteString("Content-Disposition: attachment; filename=\"smime.p7m\"\r\n\r\n")
561 msg.WriteString(clib.WrapBase64(base64.StdEncoding.EncodeToString(encryptedDer)))
562 }
563
564 // Combine all recipients for the envelope
565 allRecipients := append([]string{}, to...)
566 allRecipients = append(allRecipients, cc...)
567 allRecipients = append(allRecipients, bcc...)
568
569 addr := fmt.Sprintf("%s:%d", smtpServer, smtpPort)
570
571 tlsConfig := &tls.Config{
572 ServerName: smtpServer,
573 InsecureSkipVerify: account.Insecure,
574 }
575
576 var c *smtp.Client
577
578 // Port 465 uses implicit TLS (the connection starts with TLS).
579 // All other ports use plain TCP with optional STARTTLS upgrade.
580 if smtpPort == 465 {
581 conn, err := tls.Dial("tcp", addr, tlsConfig)
582 if err != nil {
583 return err
584 }
585 c, err = smtp.NewClient(conn, smtpServer)
586 if err != nil {
587 conn.Close()
588 return err
589 }
590 } else {
591 var err error
592 c, err = smtp.Dial(addr)
593 if err != nil {
594 return err
595 }
596 }
597 defer c.Close()
598
599 if err = c.Hello("localhost"); err != nil {
600 return err
601 }
602
603 // Trigger STARTTLS if supported (not needed for implicit TLS on port 465)
604 if smtpPort != 465 {
605 if ok, _ := c.Extension("STARTTLS"); ok {
606 if err = c.StartTLS(tlsConfig); err != nil {
607 return err
608 }
609 }
610 }
611
612 // Authenticate using the best available mechanism.
613 // c.Extension("AUTH") returns the list of supported mechanisms.
614 if ok, mechs := c.Extension("AUTH"); ok {
615 mechList := strings.ToUpper(mechs)
616
617 if account.IsOAuth2() {
618 // Use XOAUTH2 for OAuth2-enabled accounts
619 token, tokenErr := config.GetOAuth2Token(account.Email)
620 if tokenErr != nil {
621 return fmt.Errorf("oauth2: %w", tokenErr)
622 }
623 err = c.Auth(&xoauth2Auth{username: account.Email, token: token})
624 } else if strings.Contains(mechList, "PLAIN") {
625 err = c.Auth(plainAuth)
626 } else if strings.Contains(mechList, "LOGIN") {
627 err = c.Auth(loginAuthFallback)
628 } else {
629 // Fall back to PLAIN and let the server decide
630 err = c.Auth(plainAuth)
631 }
632 if err != nil {
633 return err
634 }
635 }
636
637 // Send Envelope
638 if err = c.Mail(account.Email); err != nil {
639 return err
640 }
641 for _, r := range allRecipients {
642 if err = c.Rcpt(r); err != nil {
643 return err
644 }
645 }
646
647 // Write Data
648 w, err := c.Data()
649 if err != nil {
650 return err
651 }
652 _, err = w.Write(msg.Bytes())
653 if err != nil {
654 return err
655 }
656 err = w.Close()
657 if err != nil {
658 return err
659 }
660
661 return c.Quit()
662}