composeScanBody reconstructs the outbound message as a REAL MIME blob for piguard.Extract, so the same extractor that handles inbound (charset decode, attachment scanning regardless of declared type, unscannable→review) handles the egress side identically. Outbound content isn't composed into final
(req outbound.SendRequest)
| 84 | // egress side identically. Outbound content isn't composed into final MIME until |
| 85 | // the sender runs, so we rebuild it here from the SendRequest. |
| 86 | func composeScanBody(req outbound.SendRequest) []byte { |
| 87 | var b strings.Builder |
| 88 | b.WriteString("Subject: ") |
| 89 | b.WriteString(headerSafe(req.Subject)) |
| 90 | b.WriteString("\r\n") |
| 91 | |
| 92 | writeBody := func() { |
| 93 | if req.HTMLBody != "" { |
| 94 | b.WriteString("Content-Type: text/html\r\n\r\n") |
| 95 | b.WriteString(req.HTMLBody) |
| 96 | if req.Body != "" { |
| 97 | b.WriteString("\r\n") |
| 98 | b.WriteString(req.Body) |
| 99 | } |
| 100 | } else { |
| 101 | b.WriteString("Content-Type: text/plain\r\n\r\n") |
| 102 | b.WriteString(req.Body) |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | if len(req.Attachments) == 0 { |
| 107 | writeBody() |
| 108 | return []byte(b.String()) |
| 109 | } |
| 110 | |
| 111 | // Multipart so the attachments are real parts: Extract base64-decodes each, |
| 112 | // scans textual content (a payload mislabeled image/png, a secret in |
| 113 | // octet-stream — the declared type is attacker-controlled and not trusted), and |
| 114 | // flags genuinely binary parts unscannable → review. |
| 115 | const boundary = "e2ascanboundary" |
| 116 | b.WriteString("Content-Type: multipart/mixed; boundary=" + boundary + "\r\n\r\n") |
| 117 | b.WriteString("--" + boundary + "\r\n") |
| 118 | writeBody() |
| 119 | b.WriteString("\r\n") |
| 120 | for _, att := range req.Attachments { |
| 121 | ct := headerSafe(att.ContentType) |
| 122 | if ct == "" { |
| 123 | ct = "application/octet-stream" |
| 124 | } |
| 125 | b.WriteString("--" + boundary + "\r\n") |
| 126 | b.WriteString("Content-Type: " + ct + "\r\n") |
| 127 | b.WriteString("Content-Disposition: attachment; filename=\"" + headerSafe(att.Filename) + "\"\r\n") |
| 128 | b.WriteString("Content-Transfer-Encoding: base64\r\n\r\n") |
| 129 | b.WriteString(att.Data) // already base64; Extract decodes + caps it |
| 130 | b.WriteString("\r\n") |
| 131 | } |
| 132 | b.WriteString("--" + boundary + "--\r\n") |
| 133 | return []byte(b.String()) |
| 134 | } |
| 135 | |
| 136 | // headerSafe strips CR/LF so attacker-controlled subject/filename/content-type can't |
| 137 | // inject extra headers into the reconstructed scan MIME. |