2017-07-02 13:40:24 +00:00
|
|
|
// Copyright 2017 syzkaller project authors. All rights reserved.
|
|
|
|
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
package email
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/base64"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"mime"
|
|
|
|
"mime/multipart"
|
|
|
|
"net/mail"
|
2017-08-17 17:09:07 +00:00
|
|
|
"regexp"
|
|
|
|
"sort"
|
2017-07-02 13:40:24 +00:00
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Email struct {
|
|
|
|
BugID string
|
|
|
|
MessageID string
|
2017-08-17 17:09:07 +00:00
|
|
|
Link string
|
2017-07-02 13:40:24 +00:00
|
|
|
Subject string
|
|
|
|
From string
|
|
|
|
Cc []string
|
2017-08-17 17:09:07 +00:00
|
|
|
Body string // text/plain part
|
|
|
|
Patch string // attached patch, if any
|
|
|
|
Command string // command to bot (#syz is stripped)
|
|
|
|
CommandArgs string // arguments for the command
|
2017-07-02 13:40:24 +00:00
|
|
|
}
|
|
|
|
|
2017-08-17 17:09:07 +00:00
|
|
|
const commandPrefix = "#syz "
|
|
|
|
|
|
|
|
var groupsLinkRe = regexp.MustCompile("\nTo view this discussion on the web visit (https://groups\\.google\\.com/.*?)\\.(?:\r)?\n")
|
2017-07-02 14:08:04 +00:00
|
|
|
|
2017-07-02 13:40:24 +00:00
|
|
|
func Parse(r io.Reader, ownEmail string) (*Email, error) {
|
|
|
|
msg, err := mail.ReadMessage(r)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to read email: %v", err)
|
|
|
|
}
|
|
|
|
from, err := msg.Header.AddressList("From")
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to parse email header 'From': %v", err)
|
|
|
|
}
|
|
|
|
if len(from) == 0 {
|
|
|
|
return nil, fmt.Errorf("failed to parse email header 'To': no senders")
|
|
|
|
}
|
2017-11-22 11:03:31 +00:00
|
|
|
// Ignore errors since To: header may not be present (we've seen such case).
|
|
|
|
to, _ := msg.Header.AddressList("To")
|
2017-07-02 13:40:24 +00:00
|
|
|
// AddressList fails if the header is not present.
|
|
|
|
cc, _ := msg.Header.AddressList("Cc")
|
|
|
|
bugID := ""
|
|
|
|
var ccList []string
|
2017-07-05 19:29:41 +00:00
|
|
|
if addr, err := mail.ParseAddress(ownEmail); err == nil {
|
|
|
|
ownEmail = addr.Address
|
|
|
|
}
|
2017-08-17 17:09:07 +00:00
|
|
|
fromMe := false
|
|
|
|
for _, addr := range from {
|
|
|
|
cleaned, _, _ := RemoveAddrContext(addr.Address)
|
|
|
|
if addr, err := mail.ParseAddress(cleaned); err == nil && addr.Address == ownEmail {
|
|
|
|
fromMe = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, addr := range append(append(cc, to...), from...) {
|
2017-07-05 17:45:56 +00:00
|
|
|
cleaned, context, _ := RemoveAddrContext(addr.Address)
|
2017-07-05 19:29:41 +00:00
|
|
|
if addr, err := mail.ParseAddress(cleaned); err == nil {
|
|
|
|
cleaned = addr.Address
|
|
|
|
}
|
2017-07-05 17:45:56 +00:00
|
|
|
if cleaned == ownEmail {
|
|
|
|
if bugID == "" {
|
|
|
|
bugID = context
|
|
|
|
}
|
|
|
|
} else {
|
2017-08-17 17:09:07 +00:00
|
|
|
ccList = append(ccList, cleaned)
|
2017-07-02 13:40:24 +00:00
|
|
|
}
|
|
|
|
}
|
2017-08-17 17:09:07 +00:00
|
|
|
ccList = MergeEmailLists(ccList)
|
2017-07-02 13:40:24 +00:00
|
|
|
body, attachments, err := parseBody(msg.Body, msg.Header)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2017-08-17 17:09:07 +00:00
|
|
|
bodyStr := string(body)
|
|
|
|
patch, cmd, cmdArgs := "", "", ""
|
|
|
|
if !fromMe {
|
|
|
|
for _, a := range attachments {
|
|
|
|
_, patch, _ = ParsePatch(string(a))
|
|
|
|
if patch != "" {
|
|
|
|
break
|
|
|
|
}
|
2017-07-02 13:40:24 +00:00
|
|
|
}
|
2017-08-17 17:09:07 +00:00
|
|
|
if patch == "" {
|
|
|
|
_, patch, _ = ParsePatch(bodyStr)
|
|
|
|
}
|
|
|
|
cmd, cmdArgs = extractCommand(body)
|
2017-07-02 13:40:24 +00:00
|
|
|
}
|
2017-08-17 17:09:07 +00:00
|
|
|
link := ""
|
|
|
|
if match := groupsLinkRe.FindStringSubmatchIndex(bodyStr); match != nil {
|
|
|
|
link = bodyStr[match[2]:match[3]]
|
2017-07-02 13:40:24 +00:00
|
|
|
}
|
|
|
|
email := &Email{
|
|
|
|
BugID: bugID,
|
|
|
|
MessageID: msg.Header.Get("Message-ID"),
|
2017-08-17 17:09:07 +00:00
|
|
|
Link: link,
|
2017-07-02 13:40:24 +00:00
|
|
|
Subject: msg.Header.Get("Subject"),
|
|
|
|
From: from[0].String(),
|
|
|
|
Cc: ccList,
|
|
|
|
Body: string(body),
|
|
|
|
Patch: patch,
|
|
|
|
Command: cmd,
|
|
|
|
CommandArgs: cmdArgs,
|
|
|
|
}
|
|
|
|
return email, nil
|
|
|
|
}
|
|
|
|
|
2017-07-05 17:45:56 +00:00
|
|
|
// AddAddrContext embeds context into local part of the provided email address using '+'.
|
|
|
|
// Returns the resulting email address.
|
|
|
|
func AddAddrContext(email, context string) (string, error) {
|
|
|
|
addr, err := mail.ParseAddress(email)
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("failed to parse %q as email: %v", email, err)
|
|
|
|
}
|
|
|
|
at := strings.IndexByte(addr.Address, '@')
|
|
|
|
if at == -1 {
|
|
|
|
return "", fmt.Errorf("failed to parse %q as email: no @", email)
|
2017-07-02 13:40:24 +00:00
|
|
|
}
|
2017-07-05 17:45:56 +00:00
|
|
|
addr.Address = addr.Address[:at] + "+" + context + addr.Address[at:]
|
|
|
|
return addr.String(), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// RemoveAddrContext extracts context after '+' from the local part of the provided email address.
|
|
|
|
// Returns address without the context and the context.
|
|
|
|
func RemoveAddrContext(email string) (string, string, error) {
|
|
|
|
addr, err := mail.ParseAddress(email)
|
|
|
|
if err != nil {
|
|
|
|
return "", "", fmt.Errorf("failed to parse %q as email: %v", email, err)
|
2017-07-02 13:40:24 +00:00
|
|
|
}
|
2017-07-05 17:45:56 +00:00
|
|
|
at := strings.IndexByte(addr.Address, '@')
|
|
|
|
if at == -1 {
|
|
|
|
return "", "", fmt.Errorf("failed to parse %q as email: no @", email)
|
2017-07-02 13:40:24 +00:00
|
|
|
}
|
2017-07-05 17:45:56 +00:00
|
|
|
plus := strings.LastIndexByte(addr.Address[:at], '+')
|
|
|
|
if plus == -1 {
|
|
|
|
return email, "", nil
|
2017-07-02 13:40:24 +00:00
|
|
|
}
|
2017-07-05 17:45:56 +00:00
|
|
|
context := addr.Address[plus+1 : at]
|
|
|
|
addr.Address = addr.Address[:plus] + addr.Address[at:]
|
|
|
|
return addr.String(), context, nil
|
2017-07-02 13:40:24 +00:00
|
|
|
}
|
|
|
|
|
2017-10-24 09:10:19 +00:00
|
|
|
func CanonicalEmail(email string) string {
|
|
|
|
addr, err := mail.ParseAddress(email)
|
|
|
|
if err != nil {
|
|
|
|
return email
|
|
|
|
}
|
|
|
|
at := strings.IndexByte(addr.Address, '@')
|
|
|
|
if at == -1 {
|
|
|
|
return email
|
|
|
|
}
|
|
|
|
if plus := strings.IndexByte(addr.Address[:at], '+'); plus != -1 {
|
|
|
|
addr.Address = addr.Address[:plus] + addr.Address[at:]
|
|
|
|
}
|
|
|
|
return strings.ToLower(addr.Address)
|
|
|
|
}
|
|
|
|
|
2017-07-02 13:40:24 +00:00
|
|
|
// extractCommand extracts command to syzbot from email body.
|
|
|
|
// Commands are of the following form:
|
2017-08-17 17:09:07 +00:00
|
|
|
// ^#syz cmd args...
|
|
|
|
func extractCommand(body []byte) (cmd, args string) {
|
2017-07-02 14:08:04 +00:00
|
|
|
cmdPos := bytes.Index(append([]byte{'\n'}, body...), []byte("\n"+commandPrefix))
|
2017-07-02 13:40:24 +00:00
|
|
|
if cmdPos == -1 {
|
|
|
|
return
|
|
|
|
}
|
2017-08-17 17:09:07 +00:00
|
|
|
cmdPos += len(commandPrefix)
|
2017-12-19 12:36:40 +00:00
|
|
|
for cmdPos < len(body) && body[cmdPos] == ' ' {
|
|
|
|
cmdPos++
|
|
|
|
}
|
2017-07-02 13:40:24 +00:00
|
|
|
cmdEnd := bytes.IndexByte(body[cmdPos:], '\n')
|
|
|
|
if cmdEnd == -1 {
|
|
|
|
cmdEnd = len(body) - cmdPos
|
|
|
|
}
|
2017-12-19 12:36:40 +00:00
|
|
|
if cmdEnd1 := bytes.IndexByte(body[cmdPos:], '\r'); cmdEnd1 != -1 && cmdEnd1 < cmdEnd {
|
|
|
|
cmdEnd = cmdEnd1
|
2017-07-02 13:40:24 +00:00
|
|
|
}
|
2017-12-19 12:36:40 +00:00
|
|
|
if cmdEnd1 := bytes.IndexByte(body[cmdPos:], ' '); cmdEnd1 != -1 && cmdEnd1 < cmdEnd {
|
|
|
|
cmdEnd = cmdEnd1
|
2017-11-17 19:43:47 +00:00
|
|
|
}
|
2017-12-19 12:36:40 +00:00
|
|
|
cmd = string(body[cmdPos : cmdPos+cmdEnd])
|
|
|
|
// Some email clients split text emails at 80 columns are the transformation is irrevesible.
|
|
|
|
// We try hard to restore what was there before.
|
|
|
|
// For "test:" command we know that there must be 2 tokens without spaces.
|
|
|
|
// For "fix:"/"dup:" we need a whole non-empty line of text.
|
2017-11-17 19:43:47 +00:00
|
|
|
switch cmd {
|
|
|
|
case "test:":
|
2017-12-19 12:36:40 +00:00
|
|
|
args = extractArgsTokens(body[cmdPos+cmdEnd:], 2)
|
2017-11-17 19:43:47 +00:00
|
|
|
case "test_5_arg_cmd":
|
2017-12-19 12:36:40 +00:00
|
|
|
args = extractArgsTokens(body[cmdPos+cmdEnd:], 5)
|
|
|
|
case "fix:", "dup:":
|
|
|
|
args = extractArgsLine(body[cmdPos+cmdEnd:])
|
2017-11-17 19:43:47 +00:00
|
|
|
}
|
2017-12-19 12:36:40 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func extractArgsTokens(body []byte, num int) string {
|
|
|
|
var args []string
|
|
|
|
for pos := 0; len(args) < num && pos < len(body); {
|
2017-11-17 19:43:47 +00:00
|
|
|
lineEnd := bytes.IndexByte(body[pos:], '\n')
|
|
|
|
if lineEnd == -1 {
|
|
|
|
lineEnd = len(body) - pos
|
|
|
|
}
|
|
|
|
line := strings.TrimSpace(string(body[pos : pos+lineEnd]))
|
2017-12-19 12:36:40 +00:00
|
|
|
for {
|
|
|
|
line1 := strings.Replace(line, " ", " ", -1)
|
|
|
|
if line == line1 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
line = line1
|
|
|
|
}
|
2017-11-17 19:43:47 +00:00
|
|
|
if line != "" {
|
2017-12-19 12:36:40 +00:00
|
|
|
args = append(args, strings.Split(line, " ")...)
|
2017-11-17 19:43:47 +00:00
|
|
|
}
|
|
|
|
pos += lineEnd + 1
|
2017-07-02 13:40:24 +00:00
|
|
|
}
|
2017-12-19 12:36:40 +00:00
|
|
|
return strings.TrimSpace(strings.Join(args, " "))
|
|
|
|
}
|
|
|
|
|
|
|
|
func extractArgsLine(body []byte) string {
|
|
|
|
pos := 0
|
|
|
|
for pos < len(body) && (body[pos] == ' ' || body[pos] == '\t' ||
|
|
|
|
body[pos] == '\n' || body[pos] == '\r') {
|
|
|
|
pos++
|
|
|
|
}
|
|
|
|
lineEnd := bytes.IndexByte(body[pos:], '\n')
|
|
|
|
if lineEnd == -1 {
|
|
|
|
lineEnd = len(body) - pos
|
|
|
|
}
|
|
|
|
return strings.TrimSpace(string(body[pos : pos+lineEnd]))
|
2017-07-02 13:40:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func parseBody(r io.Reader, headers mail.Header) (body []byte, attachments [][]byte, err error) {
|
|
|
|
mediaType, params, err := mime.ParseMediaType(headers.Get("Content-Type"))
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, fmt.Errorf("failed to parse email header 'Content-Type': %v", err)
|
|
|
|
}
|
2017-11-16 09:07:24 +00:00
|
|
|
// Note: mime package handles quoted-printable internally.
|
|
|
|
if strings.ToLower(headers.Get("Content-Transfer-Encoding")) == "base64" {
|
|
|
|
r = base64.NewDecoder(base64.StdEncoding, r)
|
|
|
|
}
|
2017-07-02 13:40:24 +00:00
|
|
|
disp, _, _ := mime.ParseMediaType(headers.Get("Content-Disposition"))
|
|
|
|
if disp == "attachment" {
|
|
|
|
attachment, err := ioutil.ReadAll(r)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, fmt.Errorf("failed to read email body: %v", err)
|
|
|
|
}
|
|
|
|
return nil, [][]byte{attachment}, nil
|
|
|
|
}
|
|
|
|
if mediaType == "text/plain" {
|
|
|
|
body, err := ioutil.ReadAll(r)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, fmt.Errorf("failed to read email body: %v", err)
|
|
|
|
}
|
|
|
|
return body, nil, nil
|
|
|
|
}
|
|
|
|
if !strings.HasPrefix(mediaType, "multipart/") {
|
|
|
|
return nil, nil, nil
|
|
|
|
}
|
|
|
|
mr := multipart.NewReader(r, params["boundary"])
|
|
|
|
for {
|
|
|
|
p, err := mr.NextPart()
|
|
|
|
if err == io.EOF {
|
|
|
|
return body, attachments, nil
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, fmt.Errorf("failed to parse MIME parts: %v", err)
|
|
|
|
}
|
|
|
|
body1, attachments1, err1 := parseBody(p, mail.Header(p.Header))
|
|
|
|
if err1 != nil {
|
|
|
|
return nil, nil, err1
|
|
|
|
}
|
|
|
|
if body == nil {
|
|
|
|
body = body1
|
|
|
|
}
|
|
|
|
attachments = append(attachments, attachments1...)
|
|
|
|
}
|
|
|
|
}
|
2017-08-17 17:09:07 +00:00
|
|
|
|
|
|
|
// MergeEmailLists merges several email lists removing duplicates and invalid entries.
|
|
|
|
func MergeEmailLists(lists ...[]string) []string {
|
|
|
|
const (
|
|
|
|
maxEmailLen = 1000
|
|
|
|
maxEmails = 50
|
|
|
|
)
|
|
|
|
merged := make(map[string]bool)
|
|
|
|
for _, list := range lists {
|
|
|
|
for _, email := range list {
|
|
|
|
addr, err := mail.ParseAddress(email)
|
|
|
|
if err != nil || len(addr.Address) > maxEmailLen {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
merged[addr.Address] = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var result []string
|
|
|
|
for e := range merged {
|
|
|
|
result = append(result, e)
|
|
|
|
}
|
|
|
|
sort.Strings(result)
|
|
|
|
if len(result) > maxEmails {
|
|
|
|
result = result[:maxEmails]
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|