1package sender
2
3import (
4 "bytes"
5 "crypto/rand"
6 "encoding/base64"
7 "fmt"
8 "mime"
9 "mime/multipart"
10 "net/smtp"
11 "net/textproto"
12 "path/filepath"
13 "strings"
14 "time"
15
16 "github.com/andrinoff/email-cli/config"
17)
18
19// generateMessageID creates a unique Message-ID header.
20func generateMessageID(from string) string {
21 buf := make([]byte, 16)
22 _, err := rand.Read(buf)
23 if err != nil {
24 return fmt.Sprintf("<%d.%s>", time.Now().UnixNano(), from)
25 }
26 return fmt.Sprintf("<%x@%s>", buf, from)
27}
28
29// SendEmail constructs a multipart message with plain text, HTML, and embedded images.
30func SendEmail(cfg *config.Config, to []string, subject, plainBody, htmlBody string, images map[string][]byte) error {
31 var smtpServer string
32 var smtpPort int
33
34 switch cfg.ServiceProvider {
35 case "gmail":
36 smtpServer = "smtp.gmail.com"
37 smtpPort = 587
38 case "icloud":
39 smtpServer = "smtp.mail.me.com"
40 smtpPort = 587
41 default:
42 return fmt.Errorf("unsupported or missing service_provider in config.json: %s", cfg.ServiceProvider)
43 }
44
45 auth := smtp.PlainAuth("", cfg.Email, cfg.Password, smtpServer)
46
47 fromHeader := cfg.Email
48 if cfg.Name != "" {
49 fromHeader = fmt.Sprintf("%s <%s>", cfg.Name, cfg.Email)
50 }
51
52 // Main message buffer
53 var msg bytes.Buffer
54 mainWriter := multipart.NewWriter(&msg)
55
56 // Set top-level headers for multipart/related
57 headers := map[string]string{
58 "From": fromHeader,
59 "To": to[0],
60 "Subject": subject,
61 "Date": time.Now().Format(time.RFC1123Z),
62 "Message-ID": generateMessageID(cfg.Email),
63 "Content-Type": "multipart/related; boundary=" + mainWriter.Boundary(),
64 }
65 for k, v := range headers {
66 fmt.Fprintf(&msg, "%s: %s\r\n", k, v)
67 }
68 fmt.Fprintf(&msg, "\r\n") // End of headers
69
70 // Create the multipart/alternative part as a nested part
71 altHeader := textproto.MIMEHeader{}
72 altBoundary := "alt-" + mainWriter.Boundary()
73 altHeader.Set("Content-Type", "multipart/alternative; boundary="+altBoundary)
74 altPartWriter, err := mainWriter.CreatePart(altHeader)
75 if err != nil {
76 return err
77 }
78
79 altWriter := multipart.NewWriter(altPartWriter)
80 altWriter.SetBoundary(altBoundary)
81
82 // Create plain text part inside multipart/alternative
83 textHeader := textproto.MIMEHeader{}
84 textHeader.Set("Content-Type", "text/plain; charset=UTF-8")
85 textPart, err := altWriter.CreatePart(textHeader)
86 if err != nil {
87 return err
88 }
89 fmt.Fprint(textPart, plainBody)
90
91 // Create HTML part inside multipart/alternative
92 htmlHeader := textproto.MIMEHeader{}
93 htmlHeader.Set("Content-Type", "text/html; charset=UTF-8")
94 htmlPart, err := altWriter.CreatePart(htmlHeader)
95 if err != nil {
96 return err
97 }
98 fmt.Fprint(htmlPart, htmlBody)
99
100 altWriter.Close()
101
102 // Attach images to the main multipart/related part
103 for cid, data := range images {
104 ext := filepath.Ext(strings.Split(cid, "@")[0]) // Extract extension from CID
105 mimeType := mime.TypeByExtension(ext)
106 if mimeType == "" {
107 mimeType = "application/octet-stream"
108 }
109
110 imgHeader := textproto.MIMEHeader{}
111 imgHeader.Set("Content-Type", mimeType)
112 imgHeader.Set("Content-Transfer-Encoding", "base64")
113 imgHeader.Set("Content-ID", "<"+cid+">")
114 imgHeader.Set("Content-Disposition", "inline; filename=\""+cid+"\"")
115
116 imgPart, err := mainWriter.CreatePart(imgHeader)
117 if err != nil {
118 return err
119 }
120 decodedData, err := base64.StdEncoding.DecodeString(string(data))
121 if err != nil {
122 return err
123 }
124 imgPart.Write(decodedData)
125 }
126
127 mainWriter.Close()
128
129 addr := fmt.Sprintf("%s:%d", smtpServer, smtpPort)
130 return smtp.SendMail(addr, auth, cfg.Email, to, msg.Bytes())
131}