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, inReplyTo string, references []string) 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
66 if inReplyTo != "" {
67 headers["In-Reply-To"] = inReplyTo
68 // When replying, the references should be the previous references plus the message-id of the email we're replying to.
69 if len(references) > 0 {
70 headers["References"] = strings.Join(references, " ") + " " + inReplyTo
71 } else {
72 headers["References"] = inReplyTo
73 }
74 }
75
76 for k, v := range headers {
77 fmt.Fprintf(&msg, "%s: %s\r\n", k, v)
78 }
79 fmt.Fprintf(&msg, "\r\n") // End of headers
80
81 // Create the multipart/alternative part as a nested part
82 altHeader := textproto.MIMEHeader{}
83 altBoundary := "alt-" + mainWriter.Boundary()
84 altHeader.Set("Content-Type", "multipart/alternative; boundary="+altBoundary)
85 altPartWriter, err := mainWriter.CreatePart(altHeader)
86 if err != nil {
87 return err
88 }
89
90 altWriter := multipart.NewWriter(altPartWriter)
91 altWriter.SetBoundary(altBoundary)
92
93 // Create plain text part inside multipart/alternative
94 textHeader := textproto.MIMEHeader{}
95 textHeader.Set("Content-Type", "text/plain; charset=UTF-8")
96 textPart, err := altWriter.CreatePart(textHeader)
97 if err != nil {
98 return err
99 }
100 fmt.Fprint(textPart, plainBody)
101
102 // Create HTML part inside multipart/alternative
103 htmlHeader := textproto.MIMEHeader{}
104 htmlHeader.Set("Content-Type", "text/html; charset=UTF-8")
105 htmlPart, err := altWriter.CreatePart(htmlHeader)
106 if err != nil {
107 return err
108 }
109 fmt.Fprint(htmlPart, htmlBody)
110
111 altWriter.Close()
112
113 // Attach images to the main multipart/related part
114 for cid, data := range images {
115 ext := filepath.Ext(strings.Split(cid, "@")[0]) // Extract extension from CID
116 mimeType := mime.TypeByExtension(ext)
117 if mimeType == "" {
118 mimeType = "application/octet-stream"
119 }
120
121 imgHeader := textproto.MIMEHeader{}
122 imgHeader.Set("Content-Type", mimeType)
123 imgHeader.Set("Content-Transfer-Encoding", "base64")
124 imgHeader.Set("Content-ID", "<"+cid+">")
125 imgHeader.Set("Content-Disposition", "inline; filename=\""+cid+"\"")
126
127 imgPart, err := mainWriter.CreatePart(imgHeader)
128 if err != nil {
129 return err
130 }
131 decodedData, err := base64.StdEncoding.DecodeString(string(data))
132 if err != nil {
133 return err
134 }
135 imgPart.Write(decodedData)
136 }
137
138 mainWriter.Close()
139
140 addr := fmt.Sprintf("%s:%d", smtpServer, smtpPort)
141 return smtp.SendMail(addr, auth, cfg.Email, to, msg.Bytes())
142}