package smtp import ( "bytes" "encoding/base64" "fmt" "io" "mime" "mime/multipart" "net/textproto" "strings" "time" ) func buildMessage(req *SendRequest) string { if len(req.Attachments) == 0 { return buildMessageWithoutAttachments(req) } return buildMessageWithAttachments(req) } func buildMessageWithoutAttachments(req *SendRequest) string { var b strings.Builder writeCommonHeaders(&b, req) if req.BodyHTML != "" { boundary := fmt.Sprintf("----=_Part_%d", time.Now().UnixNano()) b.WriteString(fmt.Sprintf("Content-Type: multipart/alternative; boundary=\"%s\"\r\n", boundary)) b.WriteString("\r\n") b.WriteString(fmt.Sprintf("--%s\r\n", boundary)) b.WriteString("Content-Type: text/plain; charset=UTF-8\r\n\r\n") b.WriteString(req.BodyText) b.WriteString(fmt.Sprintf("\r\n--%s\r\n", boundary)) b.WriteString("Content-Type: text/html; charset=UTF-8\r\n\r\n") b.WriteString(req.BodyHTML) b.WriteString(fmt.Sprintf("\r\n--%s--\r\n", boundary)) } else { b.WriteString("Content-Type: text/plain; charset=UTF-8\r\n\r\n") b.WriteString(req.BodyText) } return b.String() } func buildMessageWithAttachments(req *SendRequest) string { body := &bytes.Buffer{} mixed := multipart.NewWriter(body) _ = writeMessageBodyPart(mixed, req.BodyText, req.BodyHTML) for _, att := range req.Attachments { _ = writeAttachmentPart(mixed, att) } _ = mixed.Close() var head strings.Builder writeCommonHeaders(&head, req) head.WriteString(fmt.Sprintf("Content-Type: multipart/mixed; boundary=\"%s\"\r\n\r\n", mixed.Boundary())) head.Write(body.Bytes()) return head.String() } func writeCommonHeaders(b *strings.Builder, req *SendRequest) { b.WriteString(fmt.Sprintf("From: %s\r\n", req.From)) b.WriteString(fmt.Sprintf("To: %s\r\n", strings.Join(req.To, ", "))) if len(req.Cc) > 0 { b.WriteString(fmt.Sprintf("Cc: %s\r\n", strings.Join(req.Cc, ", "))) } b.WriteString(fmt.Sprintf("Subject: %s\r\n", req.Subject)) b.WriteString(fmt.Sprintf("Date: %s\r\n", time.Now().Format(time.RFC1123Z))) b.WriteString(fmt.Sprintf("Message-ID: %s\r\n", generateMessageID(req.From))) b.WriteString("MIME-Version: 1.0\r\n") if req.InReplyTo != "" { b.WriteString(fmt.Sprintf("In-Reply-To: %s\r\n", req.InReplyTo)) } if len(req.References) > 0 { b.WriteString(fmt.Sprintf("References: %s\r\n", strings.Join(req.References, " "))) } } func writeMessageBodyPart(w *multipart.Writer, text, html string) error { if html != "" { altBuf := &bytes.Buffer{} alt := multipart.NewWriter(altBuf) if err := writeTextPart(alt, "text/plain; charset=UTF-8", text); err != nil { return err } if err := writeTextPart(alt, "text/html; charset=UTF-8", html); err != nil { return err } if err := alt.Close(); err != nil { return err } h := textproto.MIMEHeader{} h.Set("Content-Type", fmt.Sprintf("multipart/alternative; boundary=\"%s\"", alt.Boundary())) part, err := w.CreatePart(h) if err != nil { return err } _, err = part.Write(altBuf.Bytes()) return err } h := textproto.MIMEHeader{} h.Set("Content-Type", "text/plain; charset=UTF-8") part, err := w.CreatePart(h) if err != nil { return err } _, err = part.Write([]byte(text)) return err } func writeTextPart(w *multipart.Writer, contentType, body string) error { h := textproto.MIMEHeader{} h.Set("Content-Type", contentType) part, err := w.CreatePart(h) if err != nil { return err } _, err = part.Write([]byte(body)) return err } func writeAttachmentPart(w *multipart.Writer, att SendAttachment) error { contentType := att.ContentType if contentType == "" { contentType = "application/octet-stream" } h := textproto.MIMEHeader{} if att.Filename != "" { h.Set("Content-Type", mime.FormatMediaType(contentType, map[string]string{ "name": att.Filename, })) } else { h.Set("Content-Type", contentType) } h.Set("Content-Transfer-Encoding", "base64") disposition := "attachment" if att.IsInline { disposition = "inline" } if att.Filename != "" { h.Set("Content-Disposition", mime.FormatMediaType(disposition, map[string]string{ "filename": att.Filename, })) } else { h.Set("Content-Disposition", disposition) } if att.IsInline && att.ContentID != "" { cid := att.ContentID if !strings.HasPrefix(cid, "<") { cid = "<" + cid + ">" } h.Set("Content-ID", cid) } part, err := w.CreatePart(h) if err != nil { return err } return writeBase64(part, att.Data) } func writeBase64(w io.Writer, data []byte) error { encoded := base64.StdEncoding.EncodeToString(data) for i := 0; i < len(encoded); i += 76 { end := i + 76 if end > len(encoded) { end = len(encoded) } if _, err := io.WriteString(w, encoded[i:end]); err != nil { return err } if end < len(encoded) { if _, err := io.WriteString(w, "\r\n"); err != nil { return err } } } return nil }