ComposeMessageWithAttachments builds an RFC 2822 multipart/mixed email with attachments. If no attachments are provided, falls back to ComposeMultipartMessage. See ComposeMessage for replyToMsgID / references semantics.
(from string, to []string, cc []string, subject, textBody, htmlBody, replyToMsgID string, references []string, fromDomain, replyTo, conversationID string, attachments []Attachment)
| 127 | // If no attachments are provided, falls back to ComposeMultipartMessage. |
| 128 | // See ComposeMessage for replyToMsgID / references semantics. |
| 129 | func ComposeMessageWithAttachments(from string, to []string, cc []string, subject, textBody, htmlBody, replyToMsgID string, references []string, fromDomain, replyTo, conversationID string, attachments []Attachment) ([]byte, error) { |
| 130 | // Defense-in-depth header-injection guard: reject any attachment |
| 131 | // whose user-supplied Filename or ContentType contains CR or LF. |
| 132 | // fmt.Sprintf("%q", ...) escapes Filename safely, but ContentType |
| 133 | // is written via "%s" and would inject extra MIME headers if it |
| 134 | // contained "\r\n" — so reject before composing. |
| 135 | for _, att := range attachments { |
| 136 | if strings.ContainsAny(att.Filename, "\r\n") { |
| 137 | return nil, fmt.Errorf("attachment filename contains CR/LF: header injection refused") |
| 138 | } |
| 139 | if strings.ContainsAny(att.ContentType, "\r\n") { |
| 140 | return nil, fmt.Errorf("attachment content_type contains CR/LF: header injection refused") |
| 141 | } |
| 142 | } |
| 143 | if len(attachments) == 0 { |
| 144 | return ComposeMultipartMessage(from, to, cc, subject, textBody, htmlBody, replyToMsgID, references, fromDomain, replyTo, conversationID) |
| 145 | } |
| 146 | |
| 147 | mixedBoundary := generateBoundary() |
| 148 | |
| 149 | var buf strings.Builder |
| 150 | writeHeader := headerWriter(&buf) |
| 151 | |
| 152 | writeHeader("From", from) |
| 153 | if len(to) > 0 { |
| 154 | writeHeader("To", strings.Join(to, ", ")) |
| 155 | } |
| 156 | if len(cc) > 0 { |
| 157 | writeHeader("Cc", strings.Join(cc, ", ")) |
| 158 | } |
| 159 | if replyTo != "" { |
| 160 | writeHeader("Reply-To", replyTo) |
| 161 | } |
| 162 | writeHeader("Subject", mime.QEncoding.Encode("utf-8", subject)) |
| 163 | writeHeader("Date", time.Now().UTC().Format(time.RFC1123Z)) |
| 164 | writeHeader("MIME-Version", "1.0") |
| 165 | writeHeader("Content-Type", fmt.Sprintf("multipart/mixed; boundary=%q", mixedBoundary)) |
| 166 | |
| 167 | writeThreadingHeaders(writeHeader, replyToMsgID, references) |
| 168 | if conversationID != "" { |
| 169 | writeHeader("X-E2A-Conversation-ID", conversationID) |
| 170 | } |
| 171 | |
| 172 | buf.WriteString("\r\n") |
| 173 | |
| 174 | // Body part |
| 175 | if htmlBody != "" { |
| 176 | altBoundary := generateBoundary() |
| 177 | buf.WriteString("--" + mixedBoundary + "\r\n") |
| 178 | buf.WriteString(fmt.Sprintf("Content-Type: multipart/alternative; boundary=%q\r\n\r\n", altBoundary)) |
| 179 | |
| 180 | buf.WriteString("--" + altBoundary + "\r\n") |
| 181 | buf.WriteString("Content-Type: text/plain; charset=utf-8\r\n\r\n") |
| 182 | buf.WriteString(textBody) |
| 183 | buf.WriteString("\r\n") |
| 184 | |
| 185 | buf.WriteString("--" + altBoundary + "\r\n") |
| 186 | buf.WriteString("Content-Type: text/html; charset=utf-8\r\n\r\n") |
no test coverage detected